-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdnserver.py
executable file
·178 lines (145 loc) · 5.67 KB
/
dnserver.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
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
#!/usr/bin/env python3.6
## Original author: https://github.com/samuelcolvin/dnserver
## Forward DNS requests for the doorbell IP to this relay
## iptables -t nat -A PREROUTING -j DNAT -p udp --src E52294209.lan --dpt 53 --to-destination 192.168.10.2:5353
##
import json
import logging
import os
import signal
from datetime import datetime
from pathlib import Path
from textwrap import wrap
from time import sleep
from dnslib import DNSLabel, QTYPE, RR, dns
from dnslib.proxy import ProxyResolver
from dnslib.server import DNSServer
SERIAL_NO = int((datetime.utcnow() - datetime(1970, 1, 1)).total_seconds())
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
handler.setFormatter(logging.Formatter('%(asctime)s: %(message)s', datefmt='%H:%M:%S'))
logger = logging.getLogger(__name__)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
TYPE_LOOKUP = {
'A': (dns.A, QTYPE.A),
'AAAA': (dns.AAAA, QTYPE.AAAA),
'CAA': (dns.CAA, QTYPE.CAA),
'CNAME': (dns.CNAME, QTYPE.CNAME),
'DNSKEY': (dns.DNSKEY, QTYPE.DNSKEY),
'MX': (dns.MX, QTYPE.MX),
'NAPTR': (dns.NAPTR, QTYPE.NAPTR),
'NS': (dns.NS, QTYPE.NS),
'PTR': (dns.PTR, QTYPE.PTR),
'RRSIG': (dns.RRSIG, QTYPE.RRSIG),
'SOA': (dns.SOA, QTYPE.SOA),
'SRV': (dns.SRV, QTYPE.SRV),
'TXT': (dns.TXT, QTYPE.TXT),
'SPF': (dns.TXT, QTYPE.TXT),
}
import paho.mqtt.client as mqtt
mqttc=mqtt.Client()
mqttc.connect("127.0.0.1")
mqttc.loop_start()
class Record:
def __init__(self, rname, rtype, args):
self._rname = DNSLabel(rname)
rd_cls, self._rtype = TYPE_LOOKUP[rtype]
if self._rtype == QTYPE.SOA and len(args) == 2:
# add sensible times to SOA
args += (SERIAL_NO, 3600, 3600 * 3, 3600 * 24, 3600),
if self._rtype == QTYPE.TXT and len(args) == 1 and isinstance(args[0], str) and len(args[0]) > 255:
# wrap long TXT records as per dnslib's docs.
args = wrap(args[0], 255),
if self._rtype in (QTYPE.NS, QTYPE.SOA):
ttl = 3600 * 24
else:
ttl = 300
self.rr = RR(
rname=self._rname,
rtype=self._rtype,
rdata=rd_cls(*args),
ttl=ttl,
)
def match(self, q):
return q.qname == self._rname and (q.qtype == QTYPE.ANY or q.qtype == self._rtype)
def sub_match(self, q):
return self._rtype == QTYPE.SOA and q.qname.matchSuffix(self._rname)
def __str__(self):
return str(self.rr)
class Resolver(ProxyResolver):
def __init__(self, upstream, zone_file):
super().__init__(upstream, 53, 5)
self.records = self.load_zones(zone_file)
def zone_lines(self):
current_line = ''
for line in zone_file.open():
if line.startswith('#'):
continue
line = line.rstrip('\r\n\t ')
if not line.startswith(' ') and current_line:
yield current_line
current_line = ''
current_line += line.lstrip('\r\n\t ')
if current_line:
yield current_line
def load_zones(self, zone_file):
if not zone_file.exists():
return []
logger.info('loading zone file "%s":', zone_file)
zones = []
for line in self.zone_lines():
try:
rname, rtype, args_ = line.split(maxsplit=2)
if args_.startswith('['):
args = tuple(json.loads(args_))
else:
args = (args_,)
record = Record(rname, rtype, args)
zones.append(record)
logger.info(' %2d: %s', len(zones), record)
except Exception as e:
raise e
#raise RuntimeError(f'Error processing line ({e.__class__.__name__}: {e}) "{line.strip()}"')# from e
logger.info('%d zone resource records generated from zone file', len(zones))
return zones
def resolve(self, request, handler):
type_name = QTYPE[request.q.qtype]
reply = request.reply()
if request.q.qname == "alarm.eu.s3.amazonaws.com.":
logger.info("Detected doorbell ring!")
mqttc.publish("onvif2mqtt/doorbell/bell", "RING")
for record in self.records:
if record.match(request.q):
reply.add_answer(record.rr)
if reply.rr:
logger.info('found zone for %s[%s], %d replies', request.q.qname, type_name, len(reply.rr))
return reply
# no direct zone so look for an SOA record for a higher level zone
for record in self.records:
if record.sub_match(request.q):
reply.add_answer(record.rr)
if reply.rr:
logger.info('found higher level SOA resource for %s[%s]', request.q.qname, type_name)
return reply
logger.info('no local zone found, proxying %s[%s]', request.q.qname, type_name)
return super().resolve(request, handler)
def handle_sig(signum, frame):
logger.info('pid=%d, got signal: %s, stopping...', os.getpid(), signal.Signals(signum).name)
exit(0)
if __name__ == '__main__':
signal.signal(signal.SIGTERM, handle_sig)
port = int(os.getenv('PORT', 53))
upstream = os.getenv('UPSTREAM', '8.8.8.8')
zone_file = Path(os.getenv('ZONE_FILE', '/zones/zones.txt'))
resolver = Resolver(upstream, zone_file)
udp_server = DNSServer(resolver, port=port)
tcp_server = DNSServer(resolver, port=port, tcp=True)
logger.info('starting DNS server on port %d, upstream DNS server "%s"', port, upstream)
udp_server.start_thread()
tcp_server.start_thread()
try:
while udp_server.isAlive():
sleep(1)
except KeyboardInterrupt:
pass