dyndns

Check-in [b715809753]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Initial bringup completed
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | master | trunk
Files: files | file ages | folders
SHA3-256:b7158097534266ef52bd40b647c6a7faa8cb574b9656a22af3e0effe389fbddd
User & Date: ajv-899-334-8894@vsta.org 2017-05-12 16:19:43
Context
2017-05-12
16:20
Ignore some stuff check-in: 0d696b557e user: ajv-899-334-8894@vsta.org tags: master, trunk
16:19
Initial bringup completed check-in: b715809753 user: ajv-899-334-8894@vsta.org tags: master, trunk
16:19
Initial README for the project check-in: 8e43c0e365 user: ajv-899-334-8894@vsta.org tags: master, trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Added client.py.





























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#
# client.py
#	Dynamic DNS client
#
# Sends a UDP packet; it gets our global source address, and we
#  sign the request so it'll register our current address against
#  a hostname under the dynamic IP subzone.
#
# Invoked as:
#	client.py <server> <hostname> <password>
#
import socket, hashlib, sys, json, os
from utils import tohex, fold

def usage():
    sys.stderr.write("Usage is: %s <server> <port> <hostname> <password>\n" %
	(sys.argv[0],))
    sys.exit(1)

if __name__ == "__main__":
    if len(sys.argv) != 5:
	usage()

    # Arguments
    server = sys.argv[1]
    port = int(sys.argv[2])
    host = sys.argv[3]
    password = sys.argv[4]

    # Build request
    nonce = tohex(os.urandom(8))
    req = json.dumps({"host": host, "nonce": nonce})

    # Signature
    s = hashlib.sha256()
    s.update(req + password)
    sig = tohex(fold(fold(s.digest())))

    # Wrapped request
    signed = json.dumps({"req": req, "sig": sig})

    # Send it
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.sendto(signed, (server, port))

    sys.exit(0)

Changes to server.py.

1
2
3
4
5
6
7


8
















9
10
11
12
13




14
15




16
17
18
19
20
21


22
23

24
25
26
27

28



29



30
31
32



33
34



35
36
37
38
39





40

41





42


43
























44
45
46
47
48
49
50
51
52
53
54
55



56
57
58


59

60
61
62


63


64
65
66

67
68
69
70
71

72






73
74






































































































#
# dyndns_server.py
#	Field UDP DNS updates, serve a dynamic DNS sub-domain
#
import sys, time, socket
from dnslib import \
    SOA, NS, A, DNSRecord, DNSHeader, QTYPE, RR, CNAME



















# Dotted domain name helper
class DomainName(str):
    def __getattr__(self, item):
	return DomainName(item + '.' + self)





D = DomainName('dyn.vsta.org')
IP = '208.82.102.26'




TTL = 60 * 5
PORT = 5053

soa_record = SOA(
    mname=D.ns1,    # primary name server
    rname=D.vandys, # email of the domain administrator


    times=(
	2017051001,   # serial number

	60 * 60 * 1,  # refresh
	60 * 60 * 3,  # retry
	60 * 60 * 24, # expire
	60 * 60 * 1,  # minimum

    )



)




ns_records = [NS(D.ns1)]




records = {
    D: [A(IP), soa_record] + ns_records,




    # MX and NS records must never point to a CNAME alias
    D.ns1: [A(IP)],
    D.vandys: [CNAME(D)],
}







def dns_response(data):





    request = DNSRecord.parse(data)


    print request
























    reply = DNSRecord(DNSHeader(id=request.header.id,
	qr=1, aa=1, ra=1), q=request.q)
    qname = request.q.qname
    qn = str(qname)
    qtype = request.q.qtype
    qt = QTYPE[qtype]
    if (qn == D) or qn.endswith('.' + D):
	for name,rrs in records.iteritems():
	    if name == qn:
		for rdata in rrs:
		    rqt = rdata.__class__.__name__
		    if qt in ['*', rqt]:



			reply.add_answer(RR(rname=qname,
			    rtype=QTYPE[rqt], rclass=1, ttl=TTL, rdata=rdata))
    for rdata in ns_records:


	reply.add_answer(RR(rname=D, rtype=QTYPE.NS, rclass=1, ttl=TTL, rdata=rdata))

    reply.add_answer(RR(rname=D,
	rtype=QTYPE.SOA, rclass=1, ttl=TTL, rdata=soa_record))
    print "---- Reply:\n", reply


    return reply.pack()



if __name__ == '__main__':
    print "Starting nameserver..."

    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.bind(('', PORT))
    while True:
	buf,who = s.recvfrom(8192)
	sys.stderr.write("Request %s %s: %s\n" % (who, time.asctime(), buf))

	res = dns_response(buf)






	sys.stderr.write(" response %s\n" % (res,))
	s.sendto(res, who)










































































































|


>
>

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>





>
>
>
>
|
<
>
>
>
>
|
<

<
<
<
>
>
|
<
>
|
|
|
|
>
|
>
>
>
|
>
>
>

<
<
>
>
>
|
<
>
>
>
|
<
<
<
|
>
>
>
>
>

>
|
>
>
>
>
>
|
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
<
<
<
<
<
<
<
<
<
<
>
>
>
|
|
<
>
>
|
>
|
|
<
>
>
|
>
>

<
<
>
|
|
|
|
|
>
|
>
>
>
>
>
>
|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

37
38
39
40
41

42



43
44
45

46
47
48
49
50
51
52
53
54
55
56
57
58
59
60


61
62
63
64

65
66
67
68



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112










113
114
115
116
117

118
119
120
121
122
123

124
125
126
127
128
129


130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
#
# dyndns_server.py
#	Field UDP DNS updates, serve a dynamic DNS sub-domain
#
import sys, time, socket, threading, json, hashlib, os
from dnslib import \
    SOA, NS, A, DNSRecord, DNSHeader, QTYPE, RR, CNAME
from utils import tohex, fold
import pdb

# Use once and discard (nonces)
used = set()

# Dict of known host names (key), their PSK (value)
touched = None
accounts = {}

# Dict of host name to current IP
hosts = {}

# Port for UDP dynamic DNS requests/updates
PORT = 5053

# Our dynamic DNS server state
dyn_server = None

# Dotted domain name helper
class DomainName(str):
    def __getattr__(self, item):
	return DomainName(item + '.' + self)

class DynDNS(object):
    def __init__(self, dom, ipaddr):

	# This is the domain and address we operate under
	D = self.D = DomainName(dom)

	self.Dsuff = D+'.'
	self.IP = ipaddr

	# Time To Live, pretty short as they are, after all, dynamic
	self.TTL = 60 * 1





	# We keep this so we can tickle the serial number
	# It starts at the current second and walks upward
	self.times = [

	    int(time.time()),
	    60 * 1,  # refresh
	    60 * 3,  # retry
	    60 * 7,  # expire
	    60 * 1,  # minimum
	]

	# We'll rebuild SOA each time self.times has its serial
	#  number updated
	self.set_soa()

	# We're probably going to be asked a *lot* more times
	#  than updated.  Cache responses per host.
	self.cache = {}



    # Build a SOA record, using the current serial number
    def set_soa(self):
	D = self.D
	self.soa_record = SOA(

	    mname=D.ns1,    # primary name server
	    rname=D.admin,  # email of the domain administrator
	    times=self.times
	)




    # New DNS table version, rev our serial in SOA
    def rev(self):
	self.cache.clear()
	self.times[0] += 1
	self.set_soa()

    # Construct an A query response for a dynamic address host
    def dns_response(self, data):
	global hosts

	# Parse request
	import pdb
	pdb.set_trace()
	request = DNSRecord.parse(data)

	# We only answer 'A' records
	q = request.q
	qt = QTYPE[q.qtype]
	if qt != 'A':
	    return None

	# About our own domain
	qname = str(q.qname)
	if not qname.endswith(self.Dsuff):
	    return None
	lab = q.qname.label

	# Already calculated?
	host = lab[0]
	resp = self.cache.get(host)
	if resp is not None:
	    return resp

	# Only our registered hosts
	ip = hosts.get(host)
	if ip is None:
	    return None

	#
	# Assemble reply
	#
	reply = DNSRecord(DNSHeader(id=request.header.id,
	    qr=1, aa=1, ra=1), q=request.q)










	# IP address for this host
	TTL = self.TTL
	D = self.D
	reply.add_answer(RR(rname=qname,
	    rtype=QTYPE.A, rclass=1, ttl=TTL, rdata=A(self.IP)))

	# Nameserver (by convention, "ns1.dyn-dom.com")
	reply.add_answer(RR(rname=D,
	    rtype=QTYPE.NS, rclass=1, ttl=TTL, rdata=NS(D.ns1)))
	# Authority
	reply.add_answer(RR(rname=D,
	    rtype=QTYPE.SOA, rclass=1, ttl=TTL, rdata=self.soa_record))


	# Formatted answer, cached
	resp = reply.pack()
	self.cache[host] = resp
	return resp



    def serve_dns(self):
	sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
	sock.bind(('', 53))
	while True:
	    buf,who = sock.recvfrom(8192)
	    sys.stderr.write("Query %s %s\n" % (who, time.asctime()))
	    try:
		res = self.dns_response(buf)
	    except:
		sys.stderr.write(" malformed request\n")
		continue
	    if res is None:
		sys.stderr.write(" no response for this query\n")
		continue
	    sys.stderr.write(" response\n")
	    sock.sendto(res, who)

def load_accounts():
    global accounts, touched

    f = open("etc/accounts", "r")
    st = os.fstat(f.fileno())
    if (touched is not None) and (touched >= st.st_mtime):
	f.close()
	return
    touched = st.st_mtime
    sys.stderr.write("Reloading accounts\n")
    newa = {}
    for l in f:
	if l.startswith('#'):
	    continue
	tup = l.split()
	if len(tup) != 2:
	    continue
	newa[tup[0]] = tup[1]
    accounts = newa
    f.close()

def serve_dyn():
    global used, hosts, accounts

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind(('', PORT))
    while True:

	# Next request
	buf,who = sock.recvfrom(8192)
	sys.stderr.write("Update %s %s: %s\n" % (who, time.asctime(), buf))

	# Decode
	try:
	    # Decode outer wrapper
	    outer = json.loads(buf)
	    req = outer["req"]
	    sig = str(outer["sig"])

	    # Decode inner message
	    inner = json.loads(req)
	    host = inner["host"]
	    nonce = str(inner["nonce"])

	    # Sanity
	    if (not sig) or (not nonce) or (not host):
		sys.stderr.write(" null fields\n")
		continue

	    # Replay
	    if nonce in used:
		sys.stderr.write(" nonce reused\n")
		continue

	except:
	    sys.stderr.write(" invalid request\n");
	    continue

	# Maybe load the hosts file
	load_accounts()

	# Known host?
	pw = accounts.get(host)
	if pw is None:
	    sys.stderr.write(" unknown host\n")
	    continue

	# No change?
	if hosts.get(host) == who[0]:
	    sys.stderr.write(" no change\n")
	    continue

	# See if they signed correctly
	s = hashlib.sha256()
	s.update(req + pw)
	expected = tohex(fold(fold(s.digest())))
	if sig != expected:
	    sys.stderr.write(" invalid sig\n")
	    continue

	# Register
	sys.stderr.write("Update %s: %s -> %s\n" %
	    (host, hosts.get(host, "(none)"), who[0]))
	hosts[host] = who[0]
	dyn_server.rev()

def usage():
    sys.stderr.write("Usage is: %s <domain> <ip-addr>\n" % (sys.argv[0],))
    sys.exit(1)

if __name__ == '__main__':
    if len(sys.argv) != 3:
	usage()

    print "Starting dynamic DNS server..."
    t = threading.Thread(target=serve_dyn)
    t.start()

    print "Starting nameserver..."
    dyn_server = DynDNS(sys.argv[1], sys.argv[2])
    dyn_server.serve_dns()

Added utils.py.































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#
# utils.py
#	Common functions across server/client
#

# XOR a byte range into one half its size
def fold(buf):
    l = len(buf)/2
    return ''.join(chr(ord(c1) ^ ord(c2))
	for c1,c2 in zip(buf[:l], buf[l:]))

# Display bytes in hex
def tohex(buf):
    return ''.join(("%02x" % (ord(c),)) for c in buf)