Skip to content

Commit 37c69e4

Browse files
Add DDNS server example
Adjust the default values of the DDNS client example to connect to the one from the server example.
1 parent b7d966d commit 37c69e4

File tree

2 files changed

+123
-4
lines changed

2 files changed

+123
-4
lines changed
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
#!/usr/bin/env python3
2-
32
#
43
# Use a TSIG-signed DDNS update to update our hostname-to-address
54
# mapping.
65
#
7-
# usage: ddns.py <ip-address>
6+
# usage: ddns_client.py <ip-address>
87
#
98
# On linux systems, you can automatically update your DNS any time an
109
# interface comes up by adding an ifup-local script that invokes this
@@ -44,6 +43,6 @@
4443
update.replace("host", 300, "A", sys.argv[1])
4544

4645
#
47-
# Replace "10.0.0.1" with the IP address of your master server.
46+
# Replace "127.0.0.1" and 8053 with the hostname and port of your master server.
4847
#
49-
response = dns.query.tcp(update, "10.0.0.1", timeout=10)
48+
response = dns.query.tcp(update, where="127.0.0.1", timeout=10, port=8053)

examples/ddns_server.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Create a DNS server to receive DDNS update queries
4+
#
5+
# usage: ddns_server.py
6+
#
7+
8+
import asyncio
9+
import logging
10+
import struct
11+
import typing
12+
13+
import dns.exception
14+
import dns.message
15+
import dns.name
16+
import dns.opcode
17+
import dns.rcode
18+
import dns.rdataclass
19+
import dns.rdatatype
20+
import dns.tsigkeyring
21+
import dns.update
22+
23+
24+
KEYRING = dns.tsigkeyring.from_text({"keyname.": "NjHwPsMKjdN++dOfE5iAiQ=="})
25+
26+
TEST_ZONES = {
27+
dns.name.from_text("example."): [
28+
dns.name.from_text("keyname."),
29+
],
30+
}
31+
32+
33+
def response(msg, code=dns.rcode.SERVFAIL):
34+
response = dns.message.make_response(msg)
35+
response.set_rcode(code)
36+
return response.to_wire()
37+
38+
39+
async def handle_nsupdate(data, addr):
40+
cli = addr[0]
41+
msg = dns.message.from_wire(data, keyring=KEYRING)
42+
try:
43+
if msg.opcode() != dns.opcode.UPDATE:
44+
raise NotImplementedError("Opcode %s not implemented" % dns.opcode.to_text(msg.opcode()))
45+
update_msg = typing.cast(dns.update.UpdateMessage, msg)
46+
zone = update_msg.zone[0].name
47+
if not msg.had_tsig or msg.keyname not in TEST_ZONES[zone]:
48+
raise dns.exception.ValidationFailure(f"Key {msg.keyname} not allowed for zone {zone}")
49+
for r in update_msg.update:
50+
if r.deleting:
51+
if r.deleting == dns.rdataclass.ANY and r.rdtype == dns.rdatatype.ANY:
52+
logging.info("%s: delete_all_rrsets %s" % (cli, r))
53+
elif r.deleting == dns.rdataclass.ANY:
54+
logging.info("%s: delete_rrset %s" % (cli, r))
55+
elif r.deleting == dns.rdataclass.NONE:
56+
logging.info("%s: delete_from_rrset %s" % (cli, r))
57+
else:
58+
logging.info("%s: add_to_rrset %s" % (cli, r))
59+
except dns.exception.FormError:
60+
logging.exception("Rejected %s: Error parsing message" % cli)
61+
return response(msg, code=dns.rcode.FORMERR)
62+
except dns.exception.ValidationFailure:
63+
logging.exception("Rejected %s: Validation error" % cli)
64+
return response(msg, code=dns.rcode.REFUSED)
65+
except NotImplementedError:
66+
logging.exception("Rejected %s: Not implemented error" % cli)
67+
return response(msg, code=dns.rcode.NOTIMP)
68+
except:
69+
logging.exception("Rejected %s: Internal error" % cli)
70+
return response(msg, code=dns.rcode.SERVFAIL)
71+
return response(msg, code=dns.rcode.NOERROR)
72+
73+
74+
async def main():
75+
hostname = "0.0.0.0"
76+
port = 8053
77+
78+
logging.basicConfig(level=logging.INFO)
79+
logging.info("Starting servers at {hostname}:{port}")
80+
loop = asyncio.get_event_loop()
81+
82+
# Start UDP server
83+
class DatagramProtocol(asyncio.DatagramProtocol):
84+
def connection_made(self, transport):
85+
self.transport = transport
86+
87+
def datagram_received(self, data, addr):
88+
asyncio.ensure_future(self.handle(data, addr))
89+
90+
async def handle(self, data, addr):
91+
result = await handle_nsupdate(data, addr)
92+
self.transport.sendto(result, addr)
93+
94+
transport, _protocol = await loop.create_datagram_endpoint(lambda: DatagramProtocol(), local_addr=(hostname, port))
95+
96+
# Start TCP server
97+
class StreamReaderProtocol(asyncio.StreamReaderProtocol):
98+
def __init__(self):
99+
reader = asyncio.StreamReader()
100+
super().__init__(reader, self.handle_tcp)
101+
102+
async def handle_tcp(self, reader, writer):
103+
addr = writer.transport.get_extra_info("peername")
104+
while True:
105+
try:
106+
(size,) = struct.unpack("!H", await reader.readexactly(2))
107+
except asyncio.IncompleteReadError:
108+
break
109+
data = await reader.readexactly(size)
110+
111+
result = await handle_nsupdate(data, addr)
112+
bsize = struct.pack("!H", len(result))
113+
writer.write(bsize)
114+
writer.write(result)
115+
116+
server = await loop.create_server(lambda: StreamReaderProtocol(), hostname, port)
117+
await server.serve_forever()
118+
119+
120+
asyncio.run(main())

0 commit comments

Comments
 (0)