%PDF- %PDF-
Direktori : /lib/python3/dist-packages/dnslib/ |
Current File : //lib/python3/dist-packages/dnslib/dns.py |
""" DNS - main dnslib module Contains core DNS packet handling code """ from __future__ import print_function import base64,binascii,calendar,collections,copy,os.path,random,socket,\ string,struct,textwrap,time from itertools import chain try: from itertools import zip_longest except ImportError: from itertools import izip_longest as zip_longest from dnslib.bit import get_bits,set_bits from dnslib.bimap import Bimap,BimapError from dnslib.buffer import Buffer,BufferError from dnslib.label import DNSLabel,DNSLabelError,DNSBuffer from dnslib.lex import WordLexer from dnslib.ranges import BYTES,B,H,I,IP4,IP6,ntuple_range,check_range,\ check_bytes class DNSError(Exception): pass # DNS codes QTYPE = Bimap('QTYPE', {1:'A', 2:'NS', 5:'CNAME', 6:'SOA', 10:'NULL', 12:'PTR', 13:'HINFO', 15:'MX', 16:'TXT', 17:'RP', 18:'AFSDB', 24:'SIG', 25:'KEY', 28:'AAAA', 29:'LOC', 33:'SRV', 35:'NAPTR', 36:'KX', 37:'CERT', 38:'A6', 39:'DNAME', 41:'OPT', 42:'APL', 43:'DS', 44:'SSHFP', 45:'IPSECKEY', 46:'RRSIG', 47:'NSEC', 48:'DNSKEY', 49:'DHCID', 50:'NSEC3', 51:'NSEC3PARAM', 52:'TLSA', 53:'HIP', 55:'HIP', 59:'CDS', 60:'CDNSKEY', 61:'OPENPGPKEY', 62:'CSYNC', 63:'ZONEMD', 64:'SVCB', 65:'HTTPS', 99:'SPF', 108:'EUI48', 109:'EUI64', 249:'TKEY', 250:'TSIG', 251:'IXFR', 252:'AXFR', 255:'ANY', 256:'URI', 257:'CAA', 32768:'TA', 32769:'DLV'}, DNSError) CLASS = Bimap('CLASS', {1:'IN', 2:'CS', 3:'CH', 4:'Hesiod', 254:'None', 255:'*'}, DNSError) QR = Bimap('QR', {0:'QUERY', 1:'RESPONSE'}, DNSError) RCODE = Bimap('RCODE', {0:'NOERROR', 1:'FORMERR', 2:'SERVFAIL', 3:'NXDOMAIN', 4:'NOTIMP', 5:'REFUSED', 6:'YXDOMAIN', 7:'YXRRSET', 8:'NXRRSET', 9:'NOTAUTH', 10:'NOTZONE'}, DNSError) OPCODE = Bimap('OPCODE',{0:'QUERY', 1:'IQUERY', 2:'STATUS', 4:'NOTIFY', 5:'UPDATE'}, DNSError) def label(label,origin=None): if label.endswith("."): return DNSLabel(label) else: return (origin if isinstance(origin,DNSLabel) else DNSLabel(origin)).add(label) class DNSRecord(object): """ Main DNS class - corresponds to DNS packet & comprises DNSHeader, DNSQuestion and RR sections (answer,ns,ar) >>> d = DNSRecord() >>> d.add_question(DNSQuestion("abc.com")) # Or DNSRecord.question("abc.com") >>> d.add_answer(RR("abc.com",QTYPE.CNAME,ttl=60,rdata=CNAME("ns.abc.com"))) >>> d.add_auth(RR("abc.com",QTYPE.SOA,ttl=60,rdata=SOA("ns.abc.com","admin.abc.com",(20140101,3600,3600,3600,3600)))) >>> d.add_ar(RR("ns.abc.com",ttl=60,rdata=A("1.2.3.4"))) >>> print(d) ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ... ;; flags: rd; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1 ;; QUESTION SECTION: ;abc.com. IN A ;; ANSWER SECTION: abc.com. 60 IN CNAME ns.abc.com. ;; AUTHORITY SECTION: abc.com. 60 IN SOA ns.abc.com. admin.abc.com. 20140101 3600 3600 3600 3600 ;; ADDITIONAL SECTION: ns.abc.com. 60 IN A 1.2.3.4 >>> str(d) == str(DNSRecord.parse(d.pack())) True """ @classmethod def parse(cls,packet): """ Parse DNS packet data and return DNSRecord instance Recursively parses sections (calling appropriate parse method) """ buffer = DNSBuffer(packet) try: header = DNSHeader.parse(buffer) questions = [] rr = [] auth = [] ar = [] for i in range(header.q): questions.append(DNSQuestion.parse(buffer)) for i in range(header.a): rr.append(RR.parse(buffer)) for i in range(header.auth): auth.append(RR.parse(buffer)) for i in range(header.ar): ar.append(RR.parse(buffer)) return cls(header,questions,rr,auth=auth,ar=ar) except DNSError: raise except (BufferError,BimapError) as e: raise DNSError("Error unpacking DNSRecord [offset=%d]: %s" % ( buffer.offset,e)) @classmethod def question(cls,qname,qtype="A",qclass="IN"): """ Shortcut to create question >>> q = DNSRecord.question("www.google.com") >>> print(q) ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ... ;; flags: rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;www.google.com. IN A >>> q = DNSRecord.question("www.google.com","NS") >>> print(q) ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ... ;; flags: rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;www.google.com. IN NS """ return DNSRecord(q=DNSQuestion(qname,getattr(QTYPE,qtype), getattr(CLASS,qclass))) def __init__(self,header=None,questions=None, rr=None,q=None,a=None,auth=None,ar=None): """ Create new DNSRecord """ self.header = header or DNSHeader() self.questions = questions or [] self.rr = rr or [] self.auth = auth or [] self.ar = ar or [] # Shortcuts to add a single Question/Answer if q: self.questions.append(q) if a: self.rr.append(a) self.set_header_qa() def reply(self,ra=1,aa=1): """ Create skeleton reply packet >>> q = DNSRecord.question("abc.com") >>> a = q.reply() >>> a.add_answer(RR("abc.com",QTYPE.A,rdata=A("1.2.3.4"),ttl=60)) >>> print(a) ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ... ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;abc.com. IN A ;; ANSWER SECTION: abc.com. 60 IN A 1.2.3.4 """ return DNSRecord(DNSHeader(id=self.header.id, bitmap=self.header.bitmap, qr=1,ra=ra,aa=aa), q=self.q) def replyZone(self,zone,ra=1,aa=1): """ Create reply with response data in zone-file format >>> q = DNSRecord.question("abc.com") >>> a = q.replyZone("abc.com 60 A 1.2.3.4") >>> print(a) ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ... ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;abc.com. IN A ;; ANSWER SECTION: abc.com. 60 IN A 1.2.3.4 """ return DNSRecord(DNSHeader(id=self.header.id, bitmap=self.header.bitmap, qr=1,ra=ra,aa=aa), q=self.q, rr=RR.fromZone(zone)) def add_question(self,*q): """ Add question(s) >>> q = DNSRecord() >>> q.add_question(DNSQuestion("abc.com"), ... DNSQuestion("abc.com",QTYPE.MX)) >>> print(q) ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ... ;; flags: rd; QUERY: 2, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;abc.com. IN A ;abc.com. IN MX """ self.questions.extend(q) self.set_header_qa() def add_answer(self,*rr): """ Add answer(s) >>> q = DNSRecord.question("abc.com") >>> a = q.reply() >>> a.add_answer(*RR.fromZone("abc.com A 1.2.3.4")) >>> print(a) ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ... ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;abc.com. IN A ;; ANSWER SECTION: abc.com. 0 IN A 1.2.3.4 """ self.rr.extend(rr) self.set_header_qa() def add_auth(self,*auth): """ Add authority records >>> q = DNSRecord.question("abc.com") >>> a = q.reply() >>> a.add_answer(*RR.fromZone("abc.com 60 A 1.2.3.4")) >>> a.add_auth(*RR.fromZone("abc.com 3600 NS nsa.abc.com")) >>> print(a) ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ... ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 0 ;; QUESTION SECTION: ;abc.com. IN A ;; ANSWER SECTION: abc.com. 60 IN A 1.2.3.4 ;; AUTHORITY SECTION: abc.com. 3600 IN NS nsa.abc.com. """ self.auth.extend(auth) self.set_header_qa() def add_ar(self,*ar): """ Add additional records >>> q = DNSRecord.question("abc.com") >>> a = q.reply() >>> a.add_answer(*RR.fromZone("abc.com 60 CNAME x.abc.com")) >>> a.add_ar(*RR.fromZone("x.abc.com 3600 A 1.2.3.4")) >>> print(a) ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ... ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; QUESTION SECTION: ;abc.com. IN A ;; ANSWER SECTION: abc.com. 60 IN CNAME x.abc.com. ;; ADDITIONAL SECTION: x.abc.com. 3600 IN A 1.2.3.4 """ self.ar.extend(ar) self.set_header_qa() def set_header_qa(self): """ Reset header q/a/auth/ar counts to match numver of records (normally done transparently) """ self.header.q = len(self.questions) self.header.a = len(self.rr) self.header.auth = len(self.auth) self.header.ar = len(self.ar) # Shortcut to get first question def get_q(self): return self.questions[0] if self.questions else DNSQuestion() q = property(get_q) # Shortcut to get first answer def get_a(self): return self.rr[0] if self.rr else RR() a = property(get_a) def pack(self): """ Pack record into binary packet (recursively packs each section into buffer) >>> q = DNSRecord.question("abc.com") >>> q.header.id = 1234 >>> a = q.replyZone("abc.com A 1.2.3.4") >>> a.header.aa = 0 >>> pkt = a.pack() >>> print(DNSRecord.parse(pkt)) ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 1234 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;abc.com. IN A ;; ANSWER SECTION: abc.com. 0 IN A 1.2.3.4 """ self.set_header_qa() buffer = DNSBuffer() self.header.pack(buffer) for q in self.questions: q.pack(buffer) for rr in self.rr: rr.pack(buffer) for auth in self.auth: auth.pack(buffer) for ar in self.ar: ar.pack(buffer) return buffer.data def truncate(self): """ Return truncated copy of DNSRecord (with TC flag set) (removes all Questions & RRs and just returns header) >>> q = DNSRecord.question("abc.com") >>> a = q.reply() >>> a.add_answer(*RR.fromZone('abc.com IN TXT %s' % ('x' * 255))) >>> a.add_answer(*RR.fromZone('abc.com IN TXT %s' % ('x' * 255))) >>> a.add_answer(*RR.fromZone('abc.com IN TXT %s' % ('x' * 255))) >>> len(a.pack()) 829 >>> t = a.truncate() >>> print(t) ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ... ;; flags: qr aa tc rd ra; QUERY: 0, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0 """ return DNSRecord(DNSHeader(id=self.header.id, bitmap=self.header.bitmap, tc=1)) def send(self,dest,port=53,tcp=False,timeout=None,ipv6=False): """ Send packet to nameserver and return response """ data = self.pack() if ipv6: inet = socket.AF_INET6 else: inet = socket.AF_INET try: sock = None if tcp: if len(data) > 65535: raise ValueError("Packet length too long: %d" % len(data)) data = struct.pack("!H",len(data)) + data sock = socket.socket(inet,socket.SOCK_STREAM) if timeout is not None: sock.settimeout(timeout) sock.connect((dest,port)) sock.sendall(data) response = sock.recv(8192) length = struct.unpack("!H",bytes(response[:2]))[0] while len(response) - 2 < length: response += sock.recv(8192) response = response[2:] else: sock = socket.socket(inet,socket.SOCK_DGRAM) if timeout is not None: sock.settimeout(timeout) sock.sendto(self.pack(),(dest,port)) response,server = sock.recvfrom(8192) finally: if (sock is not None): sock.close() return response def format(self,prefix="",sort=False): """ Formatted 'repr'-style representation of record (optionally with prefix and/or sorted RRs) """ s = sorted if sort else lambda x:x sections = [ repr(self.header) ] sections.extend(s([repr(q) for q in self.questions])) sections.extend(s([repr(rr) for rr in self.rr])) sections.extend(s([repr(rr) for rr in self.auth])) sections.extend(s([repr(rr) for rr in self.ar])) return prefix + ("\n" + prefix).join(sections) def toZone(self,prefix=""): """ Formatted 'DiG' (zone) style output (with optional prefix) """ z = self.header.toZone().split("\n") if self.questions: z.append(";; QUESTION SECTION:") [ z.extend(q.toZone().split("\n")) for q in self.questions ] if self.rr: z.append(";; ANSWER SECTION:") [ z.extend(rr.toZone().split("\n")) for rr in self.rr ] if self.auth: z.append(";; AUTHORITY SECTION:") [ z.extend(rr.toZone().split("\n")) for rr in self.auth ] if self.ar: z.append(";; ADDITIONAL SECTION:") [ z.extend(rr.toZone().split("\n")) for rr in self.ar ] return prefix + ("\n" + prefix).join(z) def short(self): """ Just return RDATA """ return "\n".join([rr.rdata.toZone() for rr in self.rr]) def __eq__(self,other): """ Test for equality by diffing records """ if type(other) != type(self): return False else: return self.diff(other) == [] def __ne__(self,other): return not(self.__eq__(other)) def diff(self,other): """ Diff records - recursively diff sections (sorting RRs) """ err = [] if self.header != other.header: err.append((self.header,other.header)) for section in ('questions','rr','auth','ar'): if section == 'questions': k = lambda x:tuple(map(str,(x.qname,x.qtype))) else: k = lambda x:tuple(map(str,(x.rname,x.rtype,x.rdata))) a = dict([(k(rr),rr) for rr in getattr(self,section)]) b = dict([(k(rr),rr) for rr in getattr(other,section)]) sa = set(a) sb = set(b) for e in sorted(sa.intersection(sb)): if a[e] != b[e]: err.append((a[e],b[e])) for e in sorted(sa.difference(sb)): err.append((a[e],None)) for e in sorted(sb.difference(sa)): err.append((None,b[e])) return err def __repr__(self): return self.format() def __str__(self): return self.toZone() class DNSHeader(object): """ DNSHeader section """ # Ensure attribute values match packet id = H('id') bitmap = H('bitmap') q = H('q') a = H('a') auth = H('auth') ar = H('ar') @classmethod def parse(cls,buffer): """ Implements parse interface """ try: (id,bitmap,q,a,auth,ar) = buffer.unpack("!HHHHHH") return cls(id,bitmap,q,a,auth,ar) except (BufferError,BimapError) as e: raise DNSError("Error unpacking DNSHeader [offset=%d]: %s" % ( buffer.offset,e)) def __init__(self,id=None,bitmap=None,q=0,a=0,auth=0,ar=0,**args): if id is None: self.id = random.randint(0,65535) else: self.id = id if bitmap is None: self.bitmap = 0 self.rd = 1 else: self.bitmap = bitmap self.q = q self.a = a self.auth = auth self.ar = ar for k,v in args.items(): if k.lower() == "qr": self.qr = v elif k.lower() == "opcode": self.opcode = v elif k.lower() == "aa": self.aa = v elif k.lower() == "tc": self.tc = v elif k.lower() == "rd": self.rd = v elif k.lower() == "ra": self.ra = v elif k.lower() == "z": self.z = v elif k.lower() == "ad": self.ad = v elif k.lower() == "cd": self.cd = v elif k.lower() == "rcode": self.rcode = v # Accessors for header properties (automatically pack/unpack # into bitmap) def get_qr(self): return get_bits(self.bitmap,15) def set_qr(self,val): self.bitmap = set_bits(self.bitmap,val,15) qr = property(get_qr,set_qr) def get_opcode(self): return get_bits(self.bitmap,11,4) def set_opcode(self,val): self.bitmap = set_bits(self.bitmap,val,11,4) opcode = property(get_opcode,set_opcode) def get_aa(self): return get_bits(self.bitmap,10) def set_aa(self,val): self.bitmap = set_bits(self.bitmap,val,10) aa = property(get_aa,set_aa) def get_tc(self): return get_bits(self.bitmap,9) def set_tc(self,val): self.bitmap = set_bits(self.bitmap,val,9) tc = property(get_tc,set_tc) def get_rd(self): return get_bits(self.bitmap,8) def set_rd(self,val): self.bitmap = set_bits(self.bitmap,val,8) rd = property(get_rd,set_rd) def get_ra(self): return get_bits(self.bitmap,7) def set_ra(self,val): self.bitmap = set_bits(self.bitmap,val,7) ra = property(get_ra,set_ra) def get_z(self): return get_bits(self.bitmap,6) def set_z(self,val): self.bitmap = set_bits(self.bitmap,val,6) z = property(get_z,set_z) def get_ad(self): return get_bits(self.bitmap,5) def set_ad(self,val): self.bitmap = set_bits(self.bitmap,val,5) ad = property(get_ad,set_ad) def get_cd(self): return get_bits(self.bitmap,4) def set_cd(self,val): self.bitmap = set_bits(self.bitmap,val,4) cd = property(get_cd,set_cd) def get_rcode(self): return get_bits(self.bitmap,0,4) def set_rcode(self,val): self.bitmap = set_bits(self.bitmap,val,0,4) rcode = property(get_rcode,set_rcode) def pack(self,buffer): buffer.pack("!HHHHHH",self.id,self.bitmap, self.q,self.a,self.auth,self.ar) def __repr__(self): f = [ self.aa and 'AA', self.tc and 'TC', self.rd and 'RD', self.ra and 'RA', self.z and 'Z', self.ad and 'AD', self.cd and 'CD'] if OPCODE.get(self.opcode) == 'UPDATE': f1='zo' f2='pr' f3='up' f4='ad' else: f1='q' f2='a' f3='ns' f4='ar' return "<DNS Header: id=0x%x type=%s opcode=%s flags=%s " \ "rcode='%s' %s=%d %s=%d %s=%d %s=%d>" % ( self.id, QR.get(self.qr), OPCODE.get(self.opcode), ",".join(filter(None,f)), RCODE.get(self.rcode), f1, self.q, f2, self.a, f3, self.auth, f4, self.ar ) def toZone(self): f = [ self.qr and 'qr', self.aa and 'aa', self.tc and 'tc', self.rd and 'rd', self.ra and 'ra', self.z and 'z', self.ad and 'ad', self.cd and 'cd' ] z1 = ';; ->>HEADER<<- opcode: %s, status: %s, id: %d' % ( OPCODE.get(self.opcode),RCODE.get(self.rcode),self.id) z2 = ';; flags: %s; QUERY: %d, ANSWER: %d, AUTHORITY: %d, ADDITIONAL: %d' % ( " ".join(filter(None,f)), self.q,self.a,self.auth,self.ar) return z1 + "\n" + z2 def __str__(self): return self.toZone() def __ne__(self,other): return not(self.__eq__(other)) def __eq__(self,other): if type(other) != type(self): return False else: # Ignore id attrs = ('qr','aa','tc','rd','ra','z','ad','cd','opcode','rcode') return all([getattr(self,x) == getattr(other,x) for x in attrs]) class DNSQuestion(object): """ DNSQuestion section """ @classmethod def parse(cls,buffer): try: qname = buffer.decode_name() qtype,qclass = buffer.unpack("!HH") return cls(qname,qtype,qclass) except (BufferError,BimapError) as e: raise DNSError("Error unpacking DNSQuestion [offset=%d]: %s" % ( buffer.offset,e)) def __init__(self,qname=None,qtype=1,qclass=1): self.qname = qname self.qtype = qtype self.qclass = qclass def set_qname(self,qname): if isinstance(qname,DNSLabel): self._qname = qname else: self._qname = DNSLabel(qname) def get_qname(self): return self._qname qname = property(get_qname,set_qname) def pack(self,buffer): buffer.encode_name(self.qname) buffer.pack("!HH",self.qtype,self.qclass) def toZone(self): return ';%-30s %-7s %s' % (self.qname,CLASS.get(self.qclass), QTYPE.get(self.qtype)) def __repr__(self): return "<DNS Question: '%s' qtype=%s qclass=%s>" % ( self.qname, QTYPE.get(self.qtype), CLASS.get(self.qclass)) def __str__(self): return self.toZone() def __ne__(self,other): return not(self.__eq__(other)) def __eq__(self,other): if type(other) != type(self): return False else: # List of attributes to compare when diffing attrs = ('qname','qtype','qclass') return all([getattr(self,x) == getattr(other,x) for x in attrs]) class EDNSOption(object): """ EDNSOption pseudo-section Very rudimentary support for EDNS0 options however this has not been tested due to a lack of data (anyone wanting to improve support or provide test data please raise an issue) >>> EDNSOption(1,b"1234") <EDNS Option: Code=1 Data='31323334'> >>> EDNSOption(99999,b"1234") Traceback (most recent call last): ... ValueError: Attribute 'code' must be between 0-65535 [99999] >>> EDNSOption(1,None) Traceback (most recent call last): ... ValueError: Attribute 'data' must be instance of ... """ code = H('code') data = BYTES('data') def __init__(self,code,data): self.code = code self.data = data def pack(self,buffer): buffer.pack("!HH",self.code,len(self.data)) buffer.append(self.data) def __repr__(self): return "<EDNS Option: Code=%d Data='%s'>" % ( self.code,binascii.hexlify(self.data).decode()) def toZone(self): return "; EDNS: code: %s; data: %s" % ( self.code,binascii.hexlify(self.data).decode()) def __str__(self): return self.toZone() def __ne__(self,other): return not(self.__eq__(other)) def __eq__(self,other): if type(other) != type(self): return False else: # List of attributes to compare when diffing attrs = ('code','data') return all([getattr(self,x) == getattr(other,x) for x in attrs]) class RR(object): """ DNS Resource Record Contains RR header and RD (resource data) instance """ rtype = H('rtype') rclass = H('rclass') ttl = I('ttl') rdlength = H('rdlength') @classmethod def parse(cls,buffer): try: rname = buffer.decode_name() rtype,rclass,ttl,rdlength = buffer.unpack("!HHIH") if rtype == QTYPE.OPT: options = [] option_buffer = Buffer(buffer.get(rdlength)) while option_buffer.remaining() > 4: code,length = option_buffer.unpack("!HH") data = option_buffer.get(length) options.append(EDNSOption(code,data)) rdata = options else: if rdlength: rdata = RDMAP.get(QTYPE.get(rtype),RD).parse( buffer,rdlength) else: rdata = '' return cls(rname,rtype,rclass,ttl,rdata) except (BufferError,BimapError) as e: raise DNSError("Error unpacking RR [offset=%d]: %s" % ( buffer.offset,e)) @classmethod def fromZone(cls,zone,origin="",ttl=0): """ Parse RR data from zone file and return list of RRs """ return list(ZoneParser(zone,origin=origin,ttl=ttl)) def __init__(self,rname=None,rtype=1,rclass=1,ttl=0,rdata=None): self.rname = rname self.rtype = rtype self.rclass = rclass self.ttl = ttl self.rdata = rdata # TODO Add property getters/setters (done for DO flag) if self.rtype == QTYPE.OPT: self.edns_len = self.rclass self.edns_ver = get_bits(self.ttl,16,8) self.edns_rcode = get_bits(self.ttl,24,8) def set_rname(self,rname): if isinstance(rname,DNSLabel): self._rname = rname else: self._rname = DNSLabel(rname) def get_rname(self): return self._rname rname = property(get_rname,set_rname) def get_do(self): if self.rtype == QTYPE.OPT: return get_bits(self.ttl,15) return 0 def set_do(self,val): if self.rtype == QTYPE.OPT: self.ttl = set_bits(self.ttl,val,15) edns_do = property(get_do,set_do) def pack(self,buffer): buffer.encode_name(self.rname) buffer.pack("!HHI",self.rtype,self.rclass,self.ttl) rdlength_ptr = buffer.offset buffer.pack("!H",0) start = buffer.offset if self.rtype == QTYPE.OPT: for opt in self.rdata: opt.pack(buffer) else: self.rdata.pack(buffer) end = buffer.offset buffer.update(rdlength_ptr,"!H",end-start) def __repr__(self): if self.rtype == QTYPE.OPT: s = ["<DNS OPT: edns_ver=%d do=%d ext_rcode=%d udp_len=%d>" % ( self.edns_ver,self.edns_do,self.edns_rcode,self.edns_len)] s.extend([repr(opt) for opt in self.rdata]) return "\n".join(s) else: return "<DNS RR: '%s' rtype=%s rclass=%s ttl=%d rdata='%s'>" % ( self.rname, QTYPE.get(self.rtype), CLASS.get(self.rclass), self.ttl, self.rdata) def toZone(self): if self.rtype == QTYPE.OPT: edns = [ ";; OPT PSEUDOSECTION", "; EDNS: version: %d, flags: %s; udp: %d" % ( self.edns_ver, "do" if self.edns_do else "", self.edns_len) ] edns.extend([str(opt) for opt in self.rdata]) return "\n".join(edns) else: return '%-23s %-7s %-7s %-7s %s' % (self.rname,self.ttl, CLASS.get(self.rclass), QTYPE.get(self.rtype), self.rdata.toZone()) def __str__(self): return self.toZone() def __ne__(self,other): return not(self.__eq__(other)) def __eq__(self,other): # Handle OPT specially as may be different types (RR/EDNS0) if self.rtype == QTYPE.OPT and getattr(other,"rtype",False) == QTYPE.OPT: attrs = ('rname','rclass','rtype','ttl','rdata') return all([getattr(self,x) == getattr(other,x) for x in attrs]) else: if type(other) != type(self): return False else: # List of attributes to compare when diffing (ignore ttl) attrs = ('rname','rclass','rtype','rdata') return all([getattr(self,x) == getattr(other,x) for x in attrs]) class EDNS0(RR): """ ENDS0 pseudo-record Wrapper around the ENDS0 support in RR to make it more convenient to create EDNS0 pseudo-record - this just makes it easier to specify the EDNS0 parameters directly EDNS flags should be passed as a space separated string of options (currently only 'do' is supported) >>> EDNS0("abc.com",flags="do",udp_len=2048,version=1) <DNS OPT: edns_ver=1 do=1 ext_rcode=0 udp_len=2048> >>> print(_) ;; OPT PSEUDOSECTION ; EDNS: version: 1, flags: do; udp: 2048 >>> opt = EDNS0("abc.com",flags="do",ext_rcode=1,udp_len=2048,version=1,opts=[EDNSOption(1,b'abcd')]) >>> opt <DNS OPT: edns_ver=1 do=1 ext_rcode=1 udp_len=2048> <EDNS Option: Code=1 Data='61626364'> >>> print(opt) ;; OPT PSEUDOSECTION ; EDNS: version: 1, flags: do; udp: 2048 ; EDNS: code: 1; data: 61626364 >>> r = DNSRecord.question("abc.com").replyZone("abc.com A 1.2.3.4") >>> r.add_ar(opt) >>> print(r) ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ... ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; QUESTION SECTION: ;abc.com. IN A ;; ANSWER SECTION: abc.com. 0 IN A 1.2.3.4 ;; ADDITIONAL SECTION: ;; OPT PSEUDOSECTION ; EDNS: version: 1, flags: do; udp: 2048 ; EDNS: code: 1; data: 61626364 >>> DNSRecord.parse(r.pack()) == r True """ def __init__(self,rname=None,rtype=QTYPE.OPT, ext_rcode=0,version=0,flags="",udp_len=0,opts=None): check_range('ext_rcode',ext_rcode,0,255) check_range('version',version,0,255) edns_flags = { 'do' : 1 << 15 } flag_bitmap = sum([edns_flags[x] for x in flags.split()]) ttl = (ext_rcode << 24) + (version << 16) + flag_bitmap if opts and not all([isinstance(o,EDNSOption) for o in opts]): raise ValueError("Option must be instance of EDNSOption") super(EDNS0,self).__init__(rname,rtype,udp_len,ttl,opts or []) class RD(object): """ Base RD object - also used as placeholder for unknown RD types To create a new RD type subclass this and add to RDMAP (below) Subclass should implement (as a mininum): parse (parse from packet data) __init__ (create class) __repr__ (return in zone format) fromZone (create from zone format) (toZone uses __repr__ by default) Unknown rdata types default to RD and store rdata as a binary blob (this allows round-trip encoding/decoding) """ @classmethod def parse(cls,buffer,length): """ Unpack from buffer """ try: data = buffer.get(length) return cls(data) except (BufferError,BimapError) as e: raise DNSError("Error unpacking RD [offset=%d]: %s" % (buffer.offset,e)) @classmethod def fromZone(cls,rd,origin=None): """ Create new record from zone format data RD is a list of strings parsed from DiG output """ # Unknown rata - assume hexdump in zone format # (DiG prepends "\\# <len>" to the hexdump so get last item) return cls(binascii.unhexlify(rd[-1].encode('ascii'))) def __init__(self,data=b""): # Assume raw bytes check_bytes('data',data) self.data = bytes(data) def pack(self,buffer): """ Pack record into buffer """ buffer.append(self.data) def __repr__(self): """ Default 'repr' format should be equivalent to RD zone format """ # For unknown rdata just default to hex return binascii.hexlify(self.data).decode() def toZone(self): return repr(self) # Comparison operations - in most cases only need to override 'attrs' # in subclass (__eq__ will automatically compare defined atttrs) # Attributes for comparison attrs = ('data',) def __eq__(self,other): if type(other) != type(self): return False else: return all([getattr(self,x) == getattr(other,x) for x in self.attrs]) def __ne__(self,other): return not(self.__eq__(other)) def _force_bytes(x): if isinstance(x,bytes): return x else: return x.encode() class TXT(RD): """ DNS TXT record. Pass in either a single string, or a tuple/list of strings. >>> TXT('txtvers=1') "txtvers=1" >>> TXT(('txtvers=1',)) "txtvers=1" >>> TXT(['txtvers=1',]) "txtvers=1" >>> TXT(['txtvers=1','swver=2.5']) "txtvers=1","swver=2.5" >>> a = DNSRecord() >>> a.add_answer(*RR.fromZone('example.com 60 IN TXT "txtvers=1"')) >>> a.add_answer(*RR.fromZone('example.com 120 IN TXT "txtvers=1" "swver=2.3"')) >>> print(a) ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ... ;; flags: rd; QUERY: 0, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0 ;; ANSWER SECTION: example.com. 60 IN TXT "txtvers=1" example.com. 120 IN TXT "txtvers=1" "swver=2.3" """ @classmethod def parse(cls,buffer,length): try: data = list() start_bo = buffer.offset now_length = 0 while buffer.offset < start_bo + length: (txtlength,) = buffer.unpack("!B") # First byte is TXT length (not in RFC?) if now_length + txtlength < length: now_length += txtlength data.append(buffer.get(txtlength)) else: raise DNSError("Invalid TXT record: len(%d) > RD len(%d)" % (txtlength,length)) return cls(data) except (BufferError,BimapError) as e: raise DNSError("Error unpacking TXT [offset=%d]: %s" % (buffer.offset,e)) @classmethod def fromZone(cls,rd,origin=None): return cls(list(map(lambda x: x.encode(), rd))) def __init__(self,data): if type(data) in (tuple,list): self.data = [ _force_bytes(x) for x in data ] else: self.data = [ _force_bytes(data) ] if any([len(x)>255 for x in self.data]): raise DNSError("TXT record too long: %s" % self.data) def pack(self,buffer): for ditem in self.data: if len(ditem) > 255: raise DNSError("TXT record too long: %s" % ditem) buffer.pack("!B",len(ditem)) buffer.append(ditem) def toZone(self): return " ".join([ '"%s"' % x.decode(errors='replace') for x in self.data ]) def __repr__(self): return ",".join([ '"%s"' % x.decode(errors='replace') for x in self.data ]) class A(RD): data = IP4('data') @classmethod def parse(cls,buffer,length): try: data = buffer.unpack("!BBBB") return cls(data) except (BufferError,BimapError) as e: raise DNSError("Error unpacking A [offset=%d]: %s" % (buffer.offset,e)) @classmethod def fromZone(cls,rd,origin=None): return cls(rd[0]) def __init__(self,data): if type(data) in (tuple,list): self.data = tuple(data) else: self.data = tuple(map(int,data.rstrip(".").split("."))) def pack(self,buffer): buffer.pack("!BBBB",*self.data) def __repr__(self): return "%d.%d.%d.%d" % self.data def _parse_ipv6(a): """ Parse IPv6 address. Ideally we would use the ipaddress module in Python3.3 but can't rely on having this. Does not handle dotted-quad addresses or subnet prefix >>> _parse_ipv6("::") == (0,) * 16 True >>> _parse_ipv6("1234:5678::abcd:0:ff00") (18, 52, 86, 120, 0, 0, 0, 0, 0, 0, 171, 205, 0, 0, 255, 0) """ l,_,r = a.partition("::") l_groups = list(chain(*[divmod(int(x,16),256) for x in l.split(":") if x])) r_groups = list(chain(*[divmod(int(x,16),256) for x in r.split(":") if x])) zeros = [0] * (16 - len(l_groups) - len(r_groups)) return tuple(l_groups + zeros + r_groups) def _format_ipv6(a): """ Format IPv6 address (from tuple of 16 bytes) compressing sequence of zero bytes to '::'. Ideally we would use the ipaddress module in Python3.3 but can't rely on having this. >>> _format_ipv6([0]*16) '::' >>> _format_ipv6(_parse_ipv6("::0012:5678")) '::12:5678' >>> _format_ipv6(_parse_ipv6("1234:0:5678::ff:0:1")) '1234:0:5678::ff:0:1' """ left = [] right = [] current = 'left' for i in range(0,16,2): group = (a[i] << 8) + a[i+1] if current == 'left': if group == 0 and i < 14: if (a[i+2] << 8) + a[i+3] == 0: current = 'right' else: left.append("0") else: left.append("%x" % group) else: if group == 0 and len(right) == 0: pass else: right.append("%x" % group) if len(left) < 8: return ":".join(left) + "::" + ":".join(right) else: return ":".join(left) class AAAA(RD): """ Basic support for AAAA record - accepts IPv6 address data as either a tuple of 16 bytes or in text format """ data = IP6('data') @classmethod def parse(cls,buffer,length): try: data = buffer.unpack("!16B") return cls(data) except (BufferError,BimapError) as e: raise DNSError("Error unpacking AAAA [offset=%d]: %s" % (buffer.offset,e)) @classmethod def fromZone(cls,rd,origin=None): return cls(rd[0]) def __init__(self,data): if type(data) in (tuple,list): self.data = tuple(data) else: self.data = _parse_ipv6(data) def pack(self,buffer): buffer.pack("!16B",*self.data) def __repr__(self): return _format_ipv6(self.data) class MX(RD): preference = H('preference') @classmethod def parse(cls,buffer,length): try: (preference,) = buffer.unpack("!H") mx = buffer.decode_name() return cls(mx,preference) except (BufferError,BimapError) as e: raise DNSError("Error unpacking MX [offset=%d]: %s" % (buffer.offset,e)) @classmethod def fromZone(cls,rd,origin=None): return cls(label(rd[1],origin),int(rd[0])) def __init__(self,label=None,preference=10): self.label = label self.preference = preference def set_label(self,label): if isinstance(label,DNSLabel): self._label = label else: self._label = DNSLabel(label) def get_label(self): return self._label label = property(get_label,set_label) def pack(self,buffer): buffer.pack("!H",self.preference) buffer.encode_name(self.label) def __repr__(self): return "%d %s" % (self.preference,self.label) attrs = ('preference','label') class CNAME(RD): @classmethod def parse(cls,buffer,length): try: label = buffer.decode_name() return cls(label) except (BufferError,BimapError) as e: raise DNSError("Error unpacking CNAME [offset=%d]: %s" % (buffer.offset,e)) @classmethod def fromZone(cls,rd,origin=None): return cls(label(rd[0],origin)) def __init__(self,label=None): self.label = label def set_label(self,label): if isinstance(label,DNSLabel): self._label = label else: self._label = DNSLabel(label) def get_label(self): return self._label label = property(get_label,set_label) def pack(self,buffer): buffer.encode_name(self.label) def __repr__(self): return "%s" % (self.label) attrs = ('label',) class PTR(CNAME): pass class NS(CNAME): pass class DNAME(CNAME): pass class SOA(RD): times = ntuple_range('times',5,0,4294967295) @classmethod def parse(cls,buffer,length): try: mname = buffer.decode_name() rname = buffer.decode_name() times = buffer.unpack("!IIIII") return cls(mname,rname,times) except (BufferError,BimapError) as e: raise DNSError("Error unpacking SOA [offset=%d]: %s" % (buffer.offset,e)) @classmethod def fromZone(cls,rd,origin=None): return cls(label(rd[0],origin),label(rd[1],origin),[parse_time(t) for t in rd[2:]]) def __init__(self,mname=None,rname=None,times=None): self.mname = mname self.rname = rname self.times = tuple(times) if times else (0,0,0,0,0) def set_mname(self,mname): if isinstance(mname,DNSLabel): self._mname = mname else: self._mname = DNSLabel(mname) def get_mname(self): return self._mname mname = property(get_mname,set_mname) def set_rname(self,rname): if isinstance(rname,DNSLabel): self._rname = rname else: self._rname = DNSLabel(rname) def get_rname(self): return self._rname rname = property(get_rname,set_rname) def pack(self,buffer): buffer.encode_name(self.mname) buffer.encode_name(self.rname) buffer.pack("!IIIII", *self.times) def __repr__(self): return "%s %s %s" % (self.mname,self.rname, " ".join(map(str,self.times))) attrs = ('mname','rname','times') class SRV(RD): priority = H('priority') weight = H('weight') port = H('port') @classmethod def parse(cls,buffer,length): try: priority,weight,port = buffer.unpack("!HHH") target = buffer.decode_name() return cls(priority,weight,port,target) except (BufferError,BimapError) as e: raise DNSError("Error unpacking SRV [offset=%d]: %s" % (buffer.offset,e)) @classmethod def fromZone(cls,rd,origin=None): return cls(int(rd[0]),int(rd[1]),int(rd[2]),rd[3]) def __init__(self,priority=0,weight=0,port=0,target=None): self.priority = priority self.weight = weight self.port = port self.target = target def set_target(self,target): if isinstance(target,DNSLabel): self._target = target else: self._target = DNSLabel(target) def get_target(self): return self._target target = property(get_target,set_target) def pack(self,buffer): buffer.pack("!HHH",self.priority,self.weight,self.port) buffer.encode_name(self.target) def __repr__(self): return "%d %d %d %s" % (self.priority,self.weight,self.port,self.target) attrs = ('priority','weight','port','target') class NAPTR(RD): order = H('order') preference = H('preference') @classmethod def parse(cls, buffer, length): try: order, preference = buffer.unpack('!HH') (length,) = buffer.unpack('!B') flags = buffer.get(length) (length,) = buffer.unpack('!B') service = buffer.get(length) (length,) = buffer.unpack('!B') regexp = buffer.get(length) replacement = buffer.decode_name() return cls(order, preference, flags, service, regexp, replacement) except (BufferError,BimapError) as e: raise DNSError("Error unpacking NAPTR [offset=%d]: %s" % (buffer.offset,e)) @classmethod def fromZone(cls,rd,origin=None): encode = lambda s : s.encode() _label = lambda s : label(s,origin) m = (int,int,encode,encode,encode,_label) return cls(*[ f(v) for f,v in zip(m,rd)]) def __init__(self,order,preference,flags,service,regexp,replacement=None): self.order = order self.preference = preference self.flags = flags self.service = service self.regexp = regexp self.replacement = replacement def set_replacement(self,replacement): if isinstance(replacement,DNSLabel): self._replacement = replacement else: self._replacement = DNSLabel(replacement) def get_replacement(self): return self._replacement replacement = property(get_replacement,set_replacement) def pack(self, buffer): buffer.pack('!HH', self.order, self.preference) buffer.pack('!B', len(self.flags)) buffer.append(self.flags) buffer.pack('!B', len(self.service)) buffer.append(self.service) buffer.pack('!B', len(self.regexp)) buffer.append(self.regexp) buffer.encode_name(self.replacement) def __repr__(self): return '%d %d "%s" "%s" "%s" %s' %( self.order,self.preference,self.flags.decode(), self.service.decode(), self.regexp.decode().replace('\\','\\\\'), self.replacement or '.' ) attrs = ('order','preference','flags','service','regexp','replacement') class DNSKEY(RD): flags = H('flags') protocol = B('protocol') algorithm = B('algorithm') @classmethod def parse(cls,buffer,length): try: (flags,protocol,algorithm) = buffer.unpack("!HBB") key = buffer.get(length - 4) return cls(flags,protocol,algorithm,key) except (BufferError,BimapError) as e: raise DNSError("Error unpacking DNSKEY [offset=%d]: %s" % (buffer.offset,e)) @classmethod def fromZone(cls,rd,origin=None): return cls(int(rd[0]),int(rd[1]),int(rd[2]), base64.b64decode(("".join(rd[3:])).encode('ascii'))) def __init__(self,flags,protocol,algorithm,key): self.flags = flags self.protocol = protocol self.algorithm = algorithm self.key = _force_bytes(key) def pack(self,buffer): buffer.pack("!HBB",self.flags,self.protocol,self.algorithm) buffer.append(self.key) def __repr__(self): return "%d %d %d %s" % (self.flags,self.protocol,self.algorithm, base64.b64encode(self.key).decode()) attrs = ('flags','protocol','algorithm','key') class RRSIG(RD): covered = H('covered') algorithm = B('algorithm') labels = B('labels') orig_ttl = I('orig_ttl') sig_exp = I('sig_exp') sig_inc = I('sig_inc') key_tag = H('key_tag') @classmethod def parse(cls,buffer,length): try: start = buffer.offset (covered,algorithm,labels, orig_ttl,sig_exp,sig_inc,key_tag) = buffer.unpack("!HBBIIIH") name = buffer.decode_name() sig = buffer.get(length - (buffer.offset - start)) return cls(covered,algorithm,labels,orig_ttl,sig_exp,sig_inc,key_tag, name,sig) except (BufferError,BimapError) as e: raise DNSError("Error unpacking DNSKEY [offset=%d]: %s" % (buffer.offset,e)) @classmethod def fromZone(cls,rd,origin=None): return cls(getattr(QTYPE,rd[0]),int(rd[1]),int(rd[2]),int(rd[3]), int(calendar.timegm(time.strptime(rd[4]+'UTC',"%Y%m%d%H%M%S%Z"))), int(calendar.timegm(time.strptime(rd[5]+'UTC',"%Y%m%d%H%M%S%Z"))), int(rd[6]),rd[7], base64.b64decode(("".join(rd[8:])).encode('ascii'))) def __init__(self,covered,algorithm,labels,orig_ttl, sig_exp,sig_inc,key_tag,name,sig): self.covered = covered self.algorithm = algorithm self.labels = labels self.orig_ttl = orig_ttl self.sig_exp = sig_exp self.sig_inc = sig_inc self.key_tag = key_tag self.name = DNSLabel(name) self.sig = sig def pack(self,buffer): buffer.pack("!HBBIIIH",self.covered,self.algorithm,self.labels, self.orig_ttl,self.sig_exp,self.sig_inc, self.key_tag) buffer.encode_name_nocompress(self.name) buffer.append(self.sig) def __repr__(self): timestamp_fmt = "{0.tm_year}{0.tm_mon:02}{0.tm_mday:02}{0.tm_hour:02}{0.tm_min:02}{0.tm_sec:02}" return "%s %d %d %d %s %s %d %s %s" % ( QTYPE.get(self.covered), self.algorithm, self.labels, self.orig_ttl, timestamp_fmt.format(time.gmtime(self.sig_exp)), timestamp_fmt.format(time.gmtime(self.sig_inc)), self.key_tag, self.name, base64.b64encode(self.sig).decode()) attrs = ('covered','algorithm','labels','orig_ttl','sig_exp','sig_inc', 'key_tag','name','sig') def decode_type_bitmap(type_bitmap): """ Parse RR type bitmap in NSEC record >>> decode_type_bitmap(binascii.unhexlify(b'0006400080080003')) ['A', 'TXT', 'AAAA', 'RRSIG', 'NSEC'] >>> decode_type_bitmap(binascii.unhexlify(b'000762008008000380')) ['A', 'NS', 'SOA', 'TXT', 'AAAA', 'RRSIG', 'NSEC', 'DNSKEY'] """ rrlist = [] buf = DNSBuffer(type_bitmap) while buf.remaining(): winnum,winlen = buf.unpack('BB') bitmap = bytearray(buf.get(winlen)) for (pos,value) in enumerate(bitmap): for i in range(8): if (value << i) & 0x80: bitpos = (256*winnum) + (8*pos) + i rrlist.append(QTYPE[bitpos]) return rrlist def encode_type_bitmap(rrlist): """ Encode RR type bitmap in NSEC record >>> p = lambda x: print(binascii.hexlify(x).decode()) >>> p(encode_type_bitmap(['A','TXT','AAAA','RRSIG','NSEC'])) 0006400080080003 >>> p(encode_type_bitmap(['A','NS','SOA','TXT','AAAA','RRSIG','NSEC','DNSKEY'])) 000762008008000380 >>> p(encode_type_bitmap(['A','ANY','URI','CAA','TA','DLV'])) 002040000000000000000000000000000000000000000000000000000000000000010101c08001c0 """ rrlist = sorted([getattr(QTYPE,rr) for rr in rrlist]) buf = DNSBuffer() curWindow = rrlist[0]//256 bitmap = bytearray(32) n = len(rrlist)-1 for i, rr in enumerate(rrlist): v = rr - curWindow*256 bitmap[v//8] |= 1 << (7 - v%8) if i == n or rrlist[i+1] >= (curWindow+1)*256: while bitmap[-1] == 0: bitmap = bitmap[:-1] buf.pack("BB", curWindow, len(bitmap)) buf.append(bitmap) if i != n: curWindow = rrlist[i+1]//256 bitmap = bytearray(32) return buf.data class NSEC(RD): @classmethod def parse(cls,buffer,length): try: end = buffer.offset + length name = buffer.decode_name() rrlist = decode_type_bitmap(buffer.get(end - buffer.offset)) return cls(name,rrlist) except (BufferError,BimapError) as e: raise DNSError("Error unpacking NSEC [offset=%d]: %s" % (buffer.offset,e)) @classmethod def fromZone(cls,rd,origin=None): return cls(rd.pop(0),rd) def __init__(self,label,rrlist): self.label = label self.rrlist = rrlist def set_label(self,label): if isinstance(label,DNSLabel): self._label = label else: self._label = DNSLabel(label) def get_label(self): return self._label label = property(get_label,set_label) def pack(self,buffer): buffer.encode_name_nocompress(self.label) buffer.append(encode_type_bitmap(self.rrlist)) def __repr__(self): return "%s %s" % (self.label," ".join(self.rrlist)) attrs = ('label','rrlist') class CAA(RD): """ CAA record. >>> CAA(0, 'issue', 'letsencrypt.org') 0 issue \"letsencrypt.org\" >>> a = DNSRecord() >>> a.add_answer(*RR.fromZone('example.com 60 IN CAA 0 issue "letsencrypt.org"')) >>> print(a) ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ... ;; flags: rd; QUERY: 0, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; ANSWER SECTION: example.com. 60 IN CAA 0 issue "letsencrypt.org" """ @classmethod def parse(cls,buffer,length): try: (flags, tag_length) = buffer.unpack("!BB") tag = buffer.get(tag_length).decode() value = buffer.get(length - tag_length - 2).decode() return cls(flags, tag, value) except (BufferError,BimapError) as e: raise DNSError("Error unpacking CAA [offset=%d]: %s" % (buffer.offset,e)) @classmethod def fromZone(cls,rd,origin=None): if len(rd) == 1: try: hex_parsed = bytes.fromhex(rd[0]) flags = hex_parsed[0] tag_length = hex_parsed[1] except: hex_parsed = rd[0].decode('hex').encode() flags = ord(hex_parsed[0]) tag_length = ord(hex_parsed[1]) tag = hex_parsed[2:2+tag_length].decode() value = hex_parsed[tag_length+2:].decode() else: (flags, tag, value) = rd return cls(int(flags), tag, value.replace('"', '')) def __init__(self, flags, tag, value): self.flags = flags self.tag = tag self.value = value self.data = None def pack(self,buffer): buffer.pack("!BB", self.flags, len(self.tag)) buffer.append(self.tag.encode()) buffer.append(self.value.encode()) def toZone(self): return "%d %s \"%s\"" % (self.flags, self.tag, self.value) def __repr__(self): return "%d %s \"%s\"" % (self.flags, self.tag, self.value) # Map from RD type to class (used to pack/unpack records) # If you add a new RD class you must add to RDMAP RDMAP = { 'CNAME':CNAME, 'A':A, 'AAAA':AAAA, 'TXT':TXT, 'MX':MX, 'PTR':PTR, 'SOA':SOA, 'NS':NS, 'NAPTR': NAPTR, 'SRV':SRV, 'DNSKEY':DNSKEY, 'RRSIG':RRSIG, 'NSEC':NSEC, 'CAA':CAA } ## ## Zone parser ## TODO - ideally this would be in a separate file but have to deal ## with circular dependencies ## secs = {'s':1,'m':60,'h':3600,'d':86400,'w':604800} def parse_time(s): """ Parse time spec with optional s/m/h/d/w suffix """ if s[-1].lower() in secs: return int(s[:-1]) * secs[s[-1].lower()] else: return int(s) class ZoneParser: """ Zone file parser >>> z = ZoneParser("www.example.com. 60 IN A 1.2.3.4") >>> list(z.parse()) [<DNS RR: 'www.example.com.' rtype=A rclass=IN ttl=60 rdata='1.2.3.4'>] """ def __init__(self,zone,origin="",ttl=0): self.l = WordLexer(zone) self.l.commentchars = ';' self.l.nltok = ('NL',None) self.l.spacetok = ('SPACE',None) self.i = iter(self.l) if type(origin) is DNSLabel: self.origin = origin else: self.origin= DNSLabel(origin) self.ttl = ttl self.label = DNSLabel("") self.prev = None def expect(self,expect): t,val = next(self.i) if t != expect: raise ValueError("Invalid Token: %s (expecting: %s)" % (t,expect)) return val def parse_label(self,label): if label.endswith("."): self.label = DNSLabel(label) elif label == "@": self.label = self.origin elif label == '': pass else: self.label = self.origin.add(label) return self.label def parse_rr(self,rr): label = self.parse_label(rr.pop(0)) ttl = int(rr.pop(0)) if rr[0].isdigit() else self.ttl rclass = rr.pop(0) if rr[0] in ('IN','CH','HS') else 'IN' rtype = rr.pop(0) rdata = rr rd = RDMAP.get(rtype,RD) return RR(rname=label, ttl=ttl, rclass=getattr(CLASS,rclass), rtype=getattr(QTYPE,rtype), rdata=rd.fromZone(rdata,self.origin)) def __iter__(self): return self.parse() def parse(self): rr = [] paren = False try: while True: tok,val = next(self.i) if tok == 'NL': if not paren and rr: self.prev = tok yield self.parse_rr(rr) rr = [] elif tok == 'SPACE' and self.prev == 'NL' and not paren: rr.append('') elif tok == 'ATOM': if val == '(': paren = True elif val == ')': paren = False elif val == '$ORIGIN': self.expect('SPACE') origin = self.expect('ATOM') self.origin = self.label = DNSLabel(origin) elif val == '$TTL': self.expect('SPACE') ttl = self.expect('ATOM') self.ttl = parse_time(ttl) else: rr.append(val) self.prev = tok except StopIteration: if rr: yield self.parse_rr(rr) if __name__ == '__main__': import doctest,sys sys.exit(0 if doctest.testmod(optionflags=doctest.ELLIPSIS).failed == 0 else 1)