cft_code.dnslib package

Submodules

cft_code.dnslib.bimap module

Bimap - bidirectional mapping between code/value

class cft_code.dnslib.bimap.Bimap(name, forward, error=<type 'exceptions.KeyError'>)

Bases: object

Bi-directional mapping between code/text.

Initialised using:

name: Used for exceptions dict: Dict mapping from code (numeric) to text error: Error type to raise if key not found

The class provides:

  • A ‘forward’ map (code->text) which is accessed through __getitem__ (bimap[code])
  • A ‘reverse’ map (code>value) which is accessed through __getattr__ (bimap.text)
  • A ‘get’ method which does a forward lookup (code->text) and returns a textual version of code if there is no explicit mapping (or default provided)
>>> class TestError(Exception):
...     pass
>>> TEST = Bimap('TEST',{1:'A', 2:'B', 3:'C'},TestError)
>>> TEST[1]
'A'
>>> TEST.A
1
>>> TEST.X
Traceback (most recent call last):
...
TestError: TEST: Invalid reverse lookup: [X]
>>> TEST[99]
Traceback (most recent call last):
...
TestError: TEST: Invalid forward lookup: [99]
>>> TEST.get(99)
'99'
get(k, default=None)
exception cft_code.dnslib.bimap.BimapError

Bases: exceptions.Exception

cft_code.dnslib.bit module

Some basic bit mainpulation utilities

cft_code.dnslib.bit.binary(n, count=16, reverse=False)

Display n in binary (only difference from built-in bin is that this function returns a fixed width string and can optionally be reversed

>>> binary(6789)
'0001101010000101'
>>> binary(6789,8)
'10000101'
>>> binary(6789,reverse=True)
'1010000101011000'
cft_code.dnslib.bit.get_bits(data, offset, bits=1)

Get specified bits from integer

>>> bin(get_bits(0b0011100,2))
'0b1'
>>> bin(get_bits(0b0011100,0,4))
'0b1100'
cft_code.dnslib.bit.hexdump(src, length=16, prefix='')

Print hexdump of string

>>> print(hexdump(b"abcd" * 4))
0000  61 62 63 64 61 62 63 64  61 62 63 64 61 62 63 64  abcdabcd abcdabcd
>>> print(hexdump(bytearray(range(48))))
0000  00 01 02 03 04 05 06 07  08 09 0a 0b 0c 0d 0e 0f  ........ ........
0010  10 11 12 13 14 15 16 17  18 19 1a 1b 1c 1d 1e 1f  ........ ........
0020  20 21 22 23 24 25 26 27  28 29 2a 2b 2c 2d 2e 2f   !"#$%&' ()*+,-./
cft_code.dnslib.bit.set_bits(data, value, offset, bits=1)

Set specified bits in integer

>>> bin(set_bits(0,0b1010,0,4))
'0b1010'
>>> bin(set_bits(0,0b1010,3,4))
'0b1010000'

cft_code.dnslib.buffer module

Buffer - simple data buffer

class cft_code.dnslib.buffer.Buffer(data='')

Bases: object

A simple data buffer - supports packing/unpacking in struct format

# Needed for Python 2/3 doctest compatibility >>> def p(s): … if not isinstance(s,str): … return s.decode() … return s

>>> b = Buffer()
>>> b.pack("!BHI",1,2,3)
>>> b.offset
7
>>> b.append(b"0123456789")
>>> b.offset
17
>>> p(b.hex())
'0100020000000330313233343536373839'
>>> b.offset = 0
>>> b.unpack("!BHI")
(1, 2, 3)
>>> bytearray(b.get(5))
bytearray(b'01234')
>>> bytearray(b.get(5))
bytearray(b'56789')
>>> b.update(7,"2s",b"xx")
>>> b.offset = 7
>>> bytearray(b.get(5))
bytearray(b'xx234')
append(s)

Append s to end of data & increment offset

get(length)

Gen len bytes at current offset (& increment offset)

hex()

Return data as hex string

pack(fmt, *args)

Pack data at end of data according to fmt (from struct) & increment offset

remaining()

Return bytes remaining

unpack(fmt)

Unpack data at current offset according to fmt (from struct)

update(ptr, fmt, *args)

Modify data at offset ptr

exception cft_code.dnslib.buffer.BufferError

Bases: exceptions.Exception

cft_code.dnslib.client module

DNS Client - DiG-like CLI utility.

Mostly useful for testing. Can optionally compare results from two nameservers (–diff) or compare results against DiG (–dig).

Usage: python -m dnslib.client [options|–help]

See –help for usage.

cft_code.dnslib.digparser module

digparser

Encode/decode DNS packets from DiG textual representation. Parses question (if present: +qr flag) & answer sections and returns list of DNSRecord objects.

Unsupported RR types are skipped (this is different from the packet parser which will store and encode the RDATA as a binary blob)

>>> dig = os.path.join(os.path.dirname(__file__),"test","dig","google.com-A.dig")
>>> with open(dig) as f:
...     l = DigParser(f)
...     for record in l:
...         print('---')
...         print(repr(record))
---
<DNS Header: id=0x5c9a type=QUERY opcode=QUERY flags=RD rcode='NOERROR' q=1 a=0 ns=0 ar=0>
<DNS Question: 'google.com.' qtype=A qclass=IN>
---
<DNS Header: id=0x5c9a type=RESPONSE opcode=QUERY flags=RD,RA rcode='NOERROR' q=1 a=16 ns=0 ar=0>
<DNS Question: 'google.com.' qtype=A qclass=IN>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.183'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.152'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.172'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.177'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.157'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.153'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.182'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.168'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.178'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.162'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.187'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.167'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.148'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.173'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.158'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.163'>
>>> dig = os.path.join(os.path.dirname(__file__),"test","dig","google.com-ANY.dig")
>>> with open(dig) as f:
...     l = DigParser(f)
...     for record in l:
...         print('---')
...         print(repr(record))
---
<DNS Header: id=0xfc6b type=QUERY opcode=QUERY flags=RD rcode='NOERROR' q=1 a=0 ns=0 ar=0>
<DNS Question: 'google.com.' qtype=ANY qclass=IN>
---
<DNS Header: id=0xa6fc type=QUERY opcode=QUERY flags=RD rcode='NOERROR' q=1 a=0 ns=0 ar=0>
<DNS Question: 'google.com.' qtype=ANY qclass=IN>
---
<DNS Header: id=0xa6fc type=RESPONSE opcode=QUERY flags=RD,RA rcode='NOERROR' q=1 a=28 ns=0 ar=0>
<DNS Question: 'google.com.' qtype=ANY qclass=IN>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.183'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.152'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.172'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.177'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.157'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.153'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.182'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.168'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.178'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.162'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.187'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.167'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.148'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.173'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.158'>
<DNS RR: 'google.com.' rtype=A rclass=IN ttl=299 rdata='62.252.169.163'>
<DNS RR: 'google.com.' rtype=NS rclass=IN ttl=21599 rdata='ns4.google.com.'>
<DNS RR: 'google.com.' rtype=MX rclass=IN ttl=599 rdata='50 alt4.aspmx.l.google.com.'>
<DNS RR: 'google.com.' rtype=NS rclass=IN ttl=21599 rdata='ns2.google.com.'>
<DNS RR: 'google.com.' rtype=MX rclass=IN ttl=599 rdata='10 aspmx.l.google.com.'>
<DNS RR: 'google.com.' rtype=NS rclass=IN ttl=21599 rdata='ns3.google.com.'>
<DNS RR: 'google.com.' rtype=SOA rclass=IN ttl=21599 rdata='ns1.google.com. dns-admin.google.com. 2014021800 7200 1800 1209600 300'>
<DNS RR: 'google.com.' rtype=MX rclass=IN ttl=599 rdata='40 alt3.aspmx.l.google.com.'>
<DNS RR: 'google.com.' rtype=MX rclass=IN ttl=599 rdata='20 alt1.aspmx.l.google.com.'>
<DNS RR: 'google.com.' rtype=TYPE257 rclass=IN ttl=21599 rdata='0005697373756573796d616e7465632e636f6d'>
<DNS RR: 'google.com.' rtype=TXT rclass=IN ttl=3599 rdata='"v=spf1 include:_spf.google.com ip4:216.73.93.70/31 ip4:216.73.93.72/31 ~all"'>
<DNS RR: 'google.com.' rtype=MX rclass=IN ttl=599 rdata='30 alt2.aspmx.l.google.com.'>
<DNS RR: 'google.com.' rtype=NS rclass=IN ttl=21599 rdata='ns1.google.com.'>
class cft_code.dnslib.digparser.DigParser(dig, debug=False)

Parse Dig output

expect(expect)
parse()
parseAnswers(a, auth, ar, dns)
parseHeader(l1, l2)
parseQuestions(q, dns)

cft_code.dnslib.dns module

DNS - main dnslib module

Contains core DNS packet handling code

class cft_code.dnslib.dns.A(data)

Bases: cft_code.dnslib.dns.RD

data
classmethod fromZone(rd, origin=None)

Create new record from zone format data RD is a list of strings parsed from DiG output

pack(buffer)

Pack record into buffer

classmethod parse(buffer, length)

Unpack from buffer

class cft_code.dnslib.dns.AAAA(data)

Bases: cft_code.dnslib.dns.RD

Basic support for AAAA record - accepts IPv6 address data as either a tuple of 16 bytes or in text format

data
classmethod fromZone(rd, origin=None)

Create new record from zone format data RD is a list of strings parsed from DiG output

pack(buffer)

Pack record into buffer

classmethod parse(buffer, length)

Unpack from buffer

class cft_code.dnslib.dns.CNAME(label=None)

Bases: cft_code.dnslib.dns.RD

attrs = ('label',)
classmethod fromZone(rd, origin=None)

Create new record from zone format data RD is a list of strings parsed from DiG output

get_label()
label
pack(buffer)

Pack record into buffer

classmethod parse(buffer, length)

Unpack from buffer

set_label(label)
exception cft_code.dnslib.dns.DNSError

Bases: exceptions.Exception

class cft_code.dnslib.dns.DNSHeader(id=None, bitmap=None, q=0, a=0, auth=0, ar=0, **args)

Bases: object

DNSHeader section

a
aa
ar
auth
bitmap
get_aa()
get_opcode()
get_qr()
get_ra()
get_rcode()
get_rd()
get_tc()
id
opcode
pack(buffer)
classmethod parse(buffer)

Implements parse interface

q
qr
ra
rcode
rd
set_aa(val)
set_opcode(val)
set_qr(val)
set_ra(val)
set_rcode(val)
set_rd(val)
set_tc(val)
tc
toZone()
class cft_code.dnslib.dns.DNSKEY(flags, protocol, algorithm, key)

Bases: cft_code.dnslib.dns.RD

algorithm
attrs = ('flags', 'protocol', 'algorithm', 'key')
flags
classmethod fromZone(rd, origin=None)

Create new record from zone format data RD is a list of strings parsed from DiG output

pack(buffer)

Pack record into buffer

classmethod parse(buffer, length)

Unpack from buffer

protocol
class cft_code.dnslib.dns.DNSQuestion(qname=None, qtype=1, qclass=1)

Bases: object

DNSQuestion section

get_qname()
pack(buffer)
classmethod parse(buffer)
qname
set_qname(qname)
toZone()
class cft_code.dnslib.dns.DNSRecord(header=None, questions=None, rr=None, q=None, a=None, auth=None, ar=None)

Bases: 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
a
add_answer(*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
add_ar(*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
add_auth(*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.
add_question(*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
diff(other)

Diff records - recursively diff sections (sorting RRs)

format(prefix='', sort=False)

Formatted ‘repr’-style representation of record (optionally with prefix and/or sorted RRs)

get_a()
get_q()
pack()

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
classmethod parse(packet)

Parse DNS packet data and return DNSRecord instance Recursively parses sections (calling appropriate parse method)

q
classmethod question(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
reply(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
replyZone(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

send(dest, port=53, tcp=False, timeout=None, ipv6=False)

Send packet to nameserver and return response

set_header_qa()

Reset header q/a/auth/ar counts to match numver of records (normally done transparently)

short()

Just return RDATA

toZone(prefix='')

Formatted ‘DiG’ (zone) style output (with optional prefix)

truncate()

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
class cft_code.dnslib.dns.EDNS0(rname=None, rtype=41, ext_rcode=0, version=0, flags='', udp_len=0, opts=None)

Bases: cft_code.dnslib.dns.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
class cft_code.dnslib.dns.EDNSOption(code, data)

Bases: 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
data
pack(buffer)
toZone()
class cft_code.dnslib.dns.MX(label=None, preference=10)

Bases: cft_code.dnslib.dns.RD

attrs = ('preference', 'label')
classmethod fromZone(rd, origin=None)

Create new record from zone format data RD is a list of strings parsed from DiG output

get_label()
label
pack(buffer)

Pack record into buffer

classmethod parse(buffer, length)

Unpack from buffer

preference
set_label(label)
class cft_code.dnslib.dns.NAPTR(order, preference, flags, service, regexp, replacement=None)

Bases: cft_code.dnslib.dns.RD

attrs = ('order', 'preference', 'flags', 'service', 'regexp', 'replacement')
classmethod fromZone(rd, origin=None)

Create new record from zone format data RD is a list of strings parsed from DiG output

get_replacement()
order
pack(buffer)

Pack record into buffer

classmethod parse(buffer, length)

Unpack from buffer

preference
replacement
set_replacement(replacement)
class cft_code.dnslib.dns.NS(label=None)

Bases: cft_code.dnslib.dns.CNAME

class cft_code.dnslib.dns.PTR(label=None)

Bases: cft_code.dnslib.dns.CNAME

class cft_code.dnslib.dns.RD(data='')

Bases: 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)

attrs = ('data',)
classmethod fromZone(rd, origin=None)

Create new record from zone format data RD is a list of strings parsed from DiG output

pack(buffer)

Pack record into buffer

classmethod parse(buffer, length)

Unpack from buffer

toZone()
class cft_code.dnslib.dns.RR(rname=None, rtype=1, rclass=1, ttl=0, rdata=None)

Bases: object

DNS Resource Record Contains RR header and RD (resource data) instance

classmethod fromZone(zone, origin='', ttl=0)

Parse RR data from zone file and return list of RRs

get_rname()
pack(buffer)
classmethod parse(buffer)
rclass
rdlength
rname
rtype
set_rname(rname)
toZone()
ttl
class cft_code.dnslib.dns.RRSIG(covered, algorithm, labels, orig_ttl, sig_exp, sig_inc, key_tag, name, sig)

Bases: cft_code.dnslib.dns.RD

algorithm
attrs = ('covered', 'algorithm', 'labels', 'orig_ttl', 'sig_exp', 'sig_inc', 'key_tag', 'name', 'sig')
covered
classmethod fromZone(rd, origin=None)

Create new record from zone format data RD is a list of strings parsed from DiG output

key_tag
labels
orig_ttl
pack(buffer)

Pack record into buffer

classmethod parse(buffer, length)

Unpack from buffer

sig_exp
sig_inc
class cft_code.dnslib.dns.SOA(mname=None, rname=None, times=None)

Bases: cft_code.dnslib.dns.RD

attrs = ('mname', 'rname', 'times')
classmethod fromZone(rd, origin=None)

Create new record from zone format data RD is a list of strings parsed from DiG output

get_mname()
get_rname()
mname
pack(buffer)

Pack record into buffer

classmethod parse(buffer, length)

Unpack from buffer

rname
set_mname(mname)
set_rname(rname)
times
class cft_code.dnslib.dns.SRV(priority=0, weight=0, port=0, target=None)

Bases: cft_code.dnslib.dns.RD

attrs = ('priority', 'weight', 'port', 'target')
classmethod fromZone(rd, origin=None)

Create new record from zone format data RD is a list of strings parsed from DiG output

get_target()
pack(buffer)

Pack record into buffer

classmethod parse(buffer, length)

Unpack from buffer

port
priority
set_target(target)
target
weight
class cft_code.dnslib.dns.TXT(data)

Bases: cft_code.dnslib.dns.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 fromZone(rd, origin=None)

Create new record from zone format data RD is a list of strings parsed from DiG output

pack(buffer)

Pack record into buffer

classmethod parse(buffer, length)

Unpack from buffer

toZone()
class cft_code.dnslib.dns.ZoneParser(zone, origin='', ttl=0)

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'>]
expect(expect)
parse()
parse_label(label)
parse_rr(rr)
cft_code.dnslib.dns.label(label, origin=None)
cft_code.dnslib.dns.parse_time(s)

Parse time spec with optional s/m/h/d/w suffix

cft_code.dnslib.fixedresolver module

FixedResolver - example resolver which responds with fixed response
to all requests
class cft_code.dnslib.fixedresolver.FixedResolver(zone)

Bases: dnslib.server.BaseResolver

Respond with fixed response to all requests

resolve(request, handler)

Example resolver - respond to all requests with NXDOMAIN

cft_code.dnslib.intercept module

InterceptResolver - proxy requests to upstream server
(optionally intercepting)
class cft_code.dnslib.intercept.InterceptResolver(address, port, ttl, intercept, skip, nxdomain, timeout=0)

Bases: dnslib.server.BaseResolver

Intercepting resolver

Proxy requests to upstream server optionally intercepting requests matching local records

resolve(request, handler)

Example resolver - respond to all requests with NXDOMAIN

cft_code.dnslib.label module

DNSLabel/DNSBuffer - DNS label handling & encoding/decoding

class cft_code.dnslib.label.DNSBuffer(data='')

Bases: dnslib.buffer.Buffer

Extends Buffer to provide DNS name encoding/decoding (with caching)

# Needed for Python 2/3 doctest compatibility >>> def p(s): … if not isinstance(s,str): … return s.decode() … return s

>>> b = DNSBuffer()
>>> b.encode_name(b'aaa.bbb.ccc.')
>>> len(b)
13
>>> b.encode_name(b'aaa.bbb.ccc.')
>>> len(b)
15
>>> b.encode_name(b'xxx.yyy.zzz')
>>> len(b)
28
>>> b.encode_name(b'zzz.xxx.bbb.ccc.')
>>> len(b)
38
>>> b.encode_name(b'aaa.xxx.bbb.ccc')
>>> len(b)
44
>>> b.offset = 0
>>> print(b.decode_name())
aaa.bbb.ccc.
>>> print(b.decode_name())
aaa.bbb.ccc.
>>> print(b.decode_name())
xxx.yyy.zzz.
>>> print(b.decode_name())
zzz.xxx.bbb.ccc.
>>> print(b.decode_name())
aaa.xxx.bbb.ccc.
>>> b = DNSBuffer()
>>> b.encode_name([b'a.aa',b'b.bb',b'c.cc'])
>>> b.offset = 0
>>> len(b.decode_name().label)
3
>>> b = DNSBuffer()
>>> b.encode_name_nocompress(b'aaa.bbb.ccc.')
>>> len(b)
13
>>> b.encode_name_nocompress(b'aaa.bbb.ccc.')
>>> len(b)
26
>>> b.offset = 0
>>> print(b.decode_name())
aaa.bbb.ccc.
>>> print(b.decode_name())
aaa.bbb.ccc.
decode_name(last=-1)

Decode label at current offset in buffer (following pointers to cached elements where necessary)

encode_name(name)

Encode label and store at end of buffer (compressing cached elements where needed) and store elements in ‘names’ dict

encode_name_nocompress(name)

Encode and store label with no compression (needed for RRSIG)

class cft_code.dnslib.label.DNSLabel(label)

Bases: object

Container for DNS label

Supports IDNA encoding for unicode domain names

>>> l1 = DNSLabel("aaa.bbb.ccc.")
>>> l2 = DNSLabel([b"aaa",b"bbb",b"ccc"])
>>> l1 == l2
True
>>> l3 = DNSLabel("AAA.BBB.CCC")
>>> l1 == l3
True
>>> l1 == 'AAA.BBB.CCC'
True
>>> x = { l1 : 1 }
>>> x[l1]
1
>>> l1
<DNSLabel: 'aaa.bbb.ccc.'>
>>> str(l1)
'aaa.bbb.ccc.'
>>> l3 = l1.add("xxx.yyy")
>>> l3
<DNSLabel: 'xxx.yyy.aaa.bbb.ccc.'>
>>> l3.matchSuffix(l1)
True
>>> l3.matchSuffix("xxx.yyy.")
False
>>> l3.stripSuffix("bbb.ccc.")
<DNSLabel: 'xxx.yyy.aaa.'>
>>> l3.matchGlob("*.[abc]aa.BBB.ccc")
True
>>> l3.matchGlob("*.[abc]xx.bbb.ccc")
False

# Too hard to get unicode doctests to work on Python 3.2 # (works on 3.3) # >>> u1 = DNSLabel(u’u2295.com’) # >>> u1.__str__() == u’u2295.com.’ # True # >>> u1.label == ( b”xn–keh”, b”com” ) # True

add(name)

Prepend name to label

idna()
matchGlob(pattern)
matchSuffix(suffix)

Return True if label suffix matches

stripSuffix(suffix)

Strip suffix from label

exception cft_code.dnslib.label.DNSLabelError

Bases: exceptions.Exception

cft_code.dnslib.lex module

class cft_code.dnslib.lex.Lexer(f, debug=False)

Bases: object

Simple Lexer base class. Provides basic lexer framework and helper functionality (read/peek/pushback etc)

Each state is implemented using a method (lexXXXX) which should match a single token and return a (token,lexYYYY) tuple, with lexYYYY representing the next state. If token is None this is not emitted and if lexYYYY is None or the lexer reaches the end of the input stream the lexer exits.

The ‘parse’ method returns a generator that will return tokens (the class also acts as an iterator)

The default start state is ‘lexStart’

Input can either be a string/bytes or file object.

The approach is based loosely on Rob Pike’s Go lexer presentation (using generators rather than channels).

>>> p = Lexer("a bcd efgh")
>>> p.read()
'a'
>>> p.read()
' '
>>> p.peek(3)
'bcd'
>>> p.read(5)
'bcd e'
>>> p.pushback('e')
>>> p.read(4)
'efgh'
escape = {'n': '\n', 'r': '\r', 't': '\t'}
escape_chars = '\\'
lexStart()
next_token()
parse()
peek(n=1)
pushback(s)
read(n=1)
readescaped()
class cft_code.dnslib.lex.RandomLexer(f, debug=False)

Bases: cft_code.dnslib.lex.Lexer

Test lexing from infinite stream.

Extract strings of letters/numbers from /dev/urandom

>>> import itertools,sys
>>> if sys.version[0] == '2':
...     f = open("/dev/urandom")
... else:
...     f = open("/dev/urandom",encoding="ascii",errors="replace")
>>> r = RandomLexer(f)
>>> i = iter(r)
>>> len(list(itertools.islice(i,10)))
10
lexAlpha()
lexDigits()
lexRandom()
lexStart()
minalpha = 4
mindigits = 3
class cft_code.dnslib.lex.WordLexer(f, debug=False)

Bases: cft_code.dnslib.lex.Lexer

Example lexer which will split input stream into words (respecting quotes)

To emit SPACE tokens: self.spacetok = (‘SPACE’,None) To emit NL tokens: self.nltok = (‘NL’,None)

>>> l = WordLexer(r'abc "def@=\. ghi" jkl')
>>> list(l)
[('ATOM', 'abc'), ('ATOM', 'def@=. ghi'), ('ATOM', 'jkl')]
>>> l = WordLexer(r"1 '2 3 4' 5")
>>> list(l)
[('ATOM', '1'), ('ATOM', '2 3 4'), ('ATOM', '5')]
>>> l = WordLexer("abc# a comment")
>>> list(l)
[('ATOM', 'abc'), ('COMMENT', 'a comment')]
commentchars = set(['#'])
lexComment()
lexNL()
lexQuote()
lexSpace()
lexStart()
lexWord()
nlchars = set(['\n', '\r'])
nltok = None
quotechars = set(['"', "'"])
spacechars = set(['\t', '\x0b', '\x0c', ' '])
spacetok = None
wordchars = set(['!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~'])

cft_code.dnslib.pan_client module

DNS Client - DiG-like CLI utility.

Mostly useful for testing. Can optionally compare results from two nameservers (–diff) or compare results against DiG (–dig).

Usage: python -m dnslib.client [options|–help]

See –help for usage.

cft_code.dnslib.pan_client.pan_dig(domain)

cft_code.dnslib.proxy module

class cft_code.dnslib.proxy.PassthroughDNSHandler(request, client_address, server)

Bases: dnslib.server.DNSHandler

Modify DNSHandler logic (get_reply method) to send directly to upstream DNS server rather then decoding/encoding packet and passing to Resolver (The request/response packets are still parsed and logged but this is not inline)

get_reply(data)
class cft_code.dnslib.proxy.ProxyResolver(address, port, timeout=0)

Bases: dnslib.server.BaseResolver

Proxy resolver - passes all requests to upstream DNS server and returns response

Note that the request/response will be each be decoded/re-encoded twice:

  1. Request packet received by DNSHandler and parsed into DNSRecord
  2. DNSRecord passed to ProxyResolver, serialised back into packet and sent to upstream DNS server
  3. Upstream DNS server returns response packet which is parsed into DNSRecord
  4. ProxyResolver returns DNSRecord to DNSHandler which re-serialises this into packet and returns to client

In practice this is actually fairly useful for testing but for a ‘real’ transparent proxy option the DNSHandler logic needs to be modified (see PassthroughDNSHandler)

resolve(request, handler)

Example resolver - respond to all requests with NXDOMAIN

cft_code.dnslib.proxy.send_tcp(data, host, port)

Helper function to send/receive DNS TCP request (in/out packets will have prepended TCP length header)

cft_code.dnslib.proxy.send_udp(data, host, port)

Helper function to send/receive DNS UDP request

cft_code.dnslib.ranges module

Wrapper around property builtin to restrict attribute to defined integer value range (throws ValueError).

Intended to ensure that values packed with struct are in the correct range

>>> class T(object):
...     a = range_property('a',-100,100)
...     b = B('b')
...     c = H('c')
...     d = I('d')
...     e = instance_property('e',(int,bool))
>>> t = T()
>>> for i in [0,100,-100]:
...     t.a = i
...     assert t.a == i
>>> t.a = 101
Traceback (most recent call last):
...
ValueError: Attribute 'a' must be between -100-100 [101]
>>> t.a = -101
Traceback (most recent call last):
...
ValueError: Attribute 'a' must be between -100-100 [-101]
>>> t.a = 'blah'
Traceback (most recent call last):
...
ValueError: Attribute 'a' must be between -100-100 [blah]
>>> t.e = 999
>>> t.e = False
>>> t.e = None
Traceback (most recent call last):
...
ValueError: Attribute 'e' must be instance of ...
>>> check_range("test",123,0,255)
>>> check_range("test",999,0,255)
Traceback (most recent call last):
...
ValueError: Attribute 'test' must be between 0-255 [999]
>>> check_instance("test",123,int)
>>> check_instance("test","xxx",int)
Traceback (most recent call last):
...
ValueError: Attribute 'test' must be instance of ...
cft_code.dnslib.ranges.B(attr)

Unsigned Byte

cft_code.dnslib.ranges.BYTES(attr)
cft_code.dnslib.ranges.H(attr)

Unsigned Short

cft_code.dnslib.ranges.I(attr)

Unsigned Long

cft_code.dnslib.ranges.IP4(attr)
cft_code.dnslib.ranges.IP6(attr)
cft_code.dnslib.ranges.check_bytes(name, val)
cft_code.dnslib.ranges.check_instance(name, val, types)
cft_code.dnslib.ranges.check_range(name, val, min, max)
cft_code.dnslib.ranges.instance_property(attr, types)
cft_code.dnslib.ranges.ntuple_range(attr, n, min, max)
cft_code.dnslib.ranges.range_property(attr, min, max)

cft_code.dnslib.server module

DNS server framework - intended to simplify creation of custom resolvers.

Comprises the following components:

DNSServer - socketserver wrapper (in most cases you should just
need to pass this an appropriate resolver instance and start in either foreground/background)
DNSHandler - handler instantiated by DNSServer to handle requests

The ‘handle’ method deals with the sending/receiving packets (handling TCP length prefix) and delegates the protocol handling to ‘get_reply’. This decodes packet, hands off a DNSRecord to the Resolver instance, and encodes the returned DNSRecord.

In most cases you dont need to change DNSHandler unless you need to get hold of the raw protocol data in the Resolver

DNSLogger - The class provides a default set of logging functions for
the various stages of the request handled by a DNSServer instance which are enabled/disabled by flags in the ‘log’ class variable.
Resolver - Instance implementing a ‘resolve’ method that receives

the decodes request packet and returns a response.

To implement a custom resolver in most cases all you need is to implement this interface.

Note that there is only a single instance of the Resolver so need to be careful about thread-safety and blocking

The following examples use the server framework:

fixedresolver.py - Simple resolver which will respond to all
requests with a fixed response
zoneresolver.py - Resolver which will take a standard zone
file input

shellresolver.py - Example of a dynamic resolver proxy.py - DNS proxy intercept.py - Intercepting DNS proxy

>>> resolver = BaseResolver()
>>> logger = DNSLogger(prefix=False)
>>> server = DNSServer(resolver,port=8053,address="localhost",logger=logger)
>>> server.start_thread()
>>> q = DNSRecord.question("abc.def")
>>> a = q.send("localhost",8053)
Request: [...] (udp) / 'abc.def.' (A)
Reply: [...] (udp) / 'abc.def.' (A) / NXDOMAIN
>>> print(DNSRecord.parse(a))
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: ...
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;abc.def.                       IN      A
>>> server.stop()
>>> class TestResolver:
...     def resolve(self,request,handler):
...         reply = request.reply()
...         reply.add_answer(*RR.fromZone("abc.def. 60 A 1.2.3.4"))
...         return reply
>>> resolver = TestResolver()
>>> server = DNSServer(resolver,port=8053,address="localhost",logger=logger,tcp=True)
>>> server.start_thread()
>>> a = q.send("localhost",8053,tcp=True)
Request: [...] (tcp) / 'abc.def.' (A)
Reply: [...] (tcp) / 'abc.def.' (A) / RRs: A
>>> print(DNSRecord.parse(a))
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ...
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;abc.def.                       IN      A
;; ANSWER SECTION:
abc.def.                60      IN      A       1.2.3.4
>>> server.stop()
class cft_code.dnslib.server.BaseResolver

Bases: object

Base resolver implementation. Provides ‘resolve’ method which is called by DNSHandler with the decode request (DNSRecord instance) and returns a DNSRecord instance as reply.

In most cases you should be able to create a custom resolver by just replacing the resolve method with appropriate resolver code for application (see fixedresolver/zoneresolver/shellresolver for examples)

Note that a single instance is used by all DNSHandler instances so need to consider blocking & thread safety.

resolve(request, handler)

Example resolver - respond to all requests with NXDOMAIN

class cft_code.dnslib.server.DNSHandler(request, client_address, server)

Bases: SocketServer.BaseRequestHandler

Handler for socketserver. Transparently handles both TCP/UDP requests (TCP requests have length prepended) and hands off lookup to resolver instance specified in <SocketServer>.resolver

get_reply(data)
handle()
udplen = 0
class cft_code.dnslib.server.DNSLogger(log='', prefix=True)

The class provides a default set of logging functions for the various stages of the request handled by a DNSServer instance which are enabled/disabled by flags in the ‘log’ class variable.

To customise logging create an object which implements the DNSLogger interface and pass instance to DNSServer.

The methods which the logger instance must implement are:

log_recv - Raw packet received log_send - Raw packet sent log_request - DNS Request log_reply - DNS Response log_truncated - Truncated log_error - Decoding error log_data - Dump full request/response
log_data(dnsobj)
log_error(handler, e)
log_pass(*args)
log_prefix(handler)
log_recv(handler, data)
log_reply(handler, reply)
log_request(handler, request)
log_send(handler, data)
log_truncated(handler, reply)
class cft_code.dnslib.server.DNSServer(resolver, address='', port=53, tcp=False, logger=None, handler=<class cft_code.dnslib.server.DNSHandler>, server=None)

Bases: object

Convenience wrapper for socketserver instance allowing either UDP/TCP server to be started in blocking more or as a background thread.

Processing is delegated to custom resolver (instance) and optionally custom logger (instance), handler (class), and server (class)

In most cases only a custom resolver instance is required (and possibly logger)

isAlive()
start()
start_thread()
stop()
class cft_code.dnslib.server.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)

Bases: SocketServer.ThreadingMixIn, SocketServer.TCPServer

allow_reuse_address = True
class cft_code.dnslib.server.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)

Bases: SocketServer.ThreadingMixIn, SocketServer.UDPServer

allow_reuse_address = True

cft_code.dnslib.shellresolver module

class cft_code.dnslib.shellresolver.ShellResolver(routes, origin, ttl)

Bases: dnslib.server.BaseResolver

Example dynamic resolver. Maps DNS labels to shell commands and returns result as TXT record (Note: No context is passed to the shell command)

Shell commands are passed in a a list in <label>:<cmd> format - eg:

[ ‘uptime.abc.com.:uptime’, ‘ls:ls’ ]

Would respond to requests to ‘uptime.abc.com.’ with the output of the ‘uptime’ command.

For non-absolute labels the ‘origin’ parameter is prepended

resolve(request, handler)

Example resolver - respond to all requests with NXDOMAIN

cft_code.dnslib.test_decode module

Test dnslib packet encoding/decoding

Reads test files from dnslib/test (by default) containing dump of DNS exchange (packet dump & parse output) and test round-trip parsing - specifically:

  • Parse packet data and zone format data and compare
  • Repack parsed packet data and compare with original

This should test the ‘parse’, ‘fromZone’ and ‘pack’ methods of the associated record types.

The original parsed output is created using dnslib by default so systematic encode/decode errors will not be found. By default the test data is checked against ‘dig’ to ensure that it is correct when generated using the –new option.

By default the module runs in ‘unittest’ mode (and supports unittest –verbose/–failfast options)

The module can also be run in interactive mode (–interactive) and inspect failed tests (–debug)

New test data files can be automatically created using the:

–new <domain> <type>

option. The data is checked against dig output and an error raised if this does not match. This is effectively the same as running:

python -m dnslib.client –query –hex –dig <domain> <type>

It is possible to manually generate test data files using dnslib.client even if the dig data doesn’t match (this is usually due to an unsupported RDATA type which dnslib will output in hex rather then parsing contents). The roundtrip tests will still work in this case (the unknown RDATA is handled as an opaque blob).

In some cases the tests will fail as a result of the zone file parser being more fragile than the packet parser (especially with broken data)

Note - unittests are dynamically generated from the test directory contents (matched against the –glob parameter)

class cft_code.dnslib.test_decode.TestContainer(methodName='runTest')

Bases: unittest.case.TestCase

cft_code.dnslib.test_decode.check_decode(f, debug=False)
cft_code.dnslib.test_decode.new_test(domain, qtype, address='8.8.8.8', port=53, nodig=False)
cft_code.dnslib.test_decode.print_errors(errors)
cft_code.dnslib.test_decode.test_generator(f)

cft_code.dnslib.zoneresolver module

class cft_code.dnslib.zoneresolver.ZoneResolver(zone, glob=False)

Bases: dnslib.server.BaseResolver

Simple fixed zone file resolver.

resolve(request, handler)

Respond to DNS request - parameters are request packet & handler. Method is expected to return DNS response

Module contents

dnslib

A library to encode/decode DNS wire-format packets supporting both Python 2.7 and Python 3.2+.

The library provides:

  • Support for encoding/decoding DNS packets between wire format, python objects, and Zone/DiG textual representation (dnslib.dns)
  • A server framework allowing the simple creation of custom DNS resolvers (dnslib.server) and a number of example servers created using this framework
  • A number of utilities for testing (dnslib.client, dnslib.proxy, dnslib.intercept)

Python 3 support was added in Version 0.9.0 which represented a fairly major update to the library - the key changes include:

  • Python 2.7/3.2+ support (the last version supporting Python 2.6 or earlier was version 0.8.3)
  • The ‘Bimap’ interface was changed significantly to explicitly split forward (value->text) lookups via __getitem__ and reverse (text->value) lookups via __getattr__. Applications using the old interface will need to be updated.
  • Hostnames are now returned with a trailing dot by default (in line with RFC)
  • Most object attributes are now typed in line with the record definitions to make it harder to generate invalid packets
  • Support for encoding/decoding resource records in ‘Zone’ (BIND) file format
  • Support for encoding/decoding packets in ‘DiG’ format
  • Server framework allowing (in most cases) custom resolvers to be created by just subclassing the DNSResolver class and overriding the ‘resolve’ method
  • A lot of fixes to error detection/handling which should make the library much more robust to invalid/unsupported data. The library should now either return a valid DNSRecord instance when parsing a packet or raise DNSError (tested via fuzzing)
  • Improved utilities (dnslib.client, dnslib.proxy, dnslib.intercept)
  • Improvements to encoding/decoding tests including the ability to generate test data automatically in test_decode.py (comparing outputs against DiG)
  • Ability to compare and diff DNSRecords

Classes

The key DNS packet handling classes are in dnslib.dns and map to the standard DNS packet sections:

  • DNSRecord - container for DNS packet. Contains:
    • DNSHeader
    • Question section containing zero or more DNSQuestion objects
    • Answer section containing zero or more RR objects
    • Authority section containing zero or more RR objects
    • Additional section containing zero or more RR objects
  • DNS RRs (resource records) contain an RR header and an RD object)
  • Specific RD types are implemented as subclasses of RD
  • DNS labels are represented by a DNSLabel class - in most cases this handles conversion to/from textual representation however does support arbitatry labels via a tuple of bytes objects

Usage

To decode a DNS packet:

>>> packet = binascii.unhexlify(b'd5ad818000010005000000000377777706676f6f676c6503636f6d0000010001c00c0005000100000005000803777777016cc010c02c0001000100000005000442f95b68c02c0001000100000005000442f95b63c02c0001000100000005000442f95b67c02c0001000100000005000442f95b93')
>>> d = DNSRecord.parse(packet)
>>> d
<DNS Header: id=0xd5ad type=RESPONSE opcode=QUERY flags=RD,RA rcode='NOERROR' q=1 a=5 ns=0 ar=0>
<DNS Question: 'www.google.com.' qtype=A qclass=IN>
<DNS RR: 'www.google.com.' rtype=CNAME rclass=IN ttl=5 rdata='www.l.google.com.'>
<DNS RR: 'www.l.google.com.' rtype=A rclass=IN ttl=5 rdata='66.249.91.104'>
<DNS RR: 'www.l.google.com.' rtype=A rclass=IN ttl=5 rdata='66.249.91.99'>
<DNS RR: 'www.l.google.com.' rtype=A rclass=IN ttl=5 rdata='66.249.91.103'>
<DNS RR: 'www.l.google.com.' rtype=A rclass=IN ttl=5 rdata='66.249.91.147'>

The default text representation of the DNSRecord is in zone file format:

>>> print(d)
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 54701
;; flags: qr rd ra; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;www.google.com.                IN      A
;; ANSWER SECTION:
www.google.com.         5       IN      CNAME   www.l.google.com.
www.l.google.com.       5       IN      A       66.249.91.104
www.l.google.com.       5       IN      A       66.249.91.99
www.l.google.com.       5       IN      A       66.249.91.103
www.l.google.com.       5       IN      A       66.249.91.147

To create a DNS Request Packet:

>>> d = DNSRecord.question("google.com")

(This is equivalent to: d = DNSRecord(q=DNSQuestion(“google.com”) )

>>> d
<DNS Header: id=... type=QUERY opcode=QUERY flags=RD rcode='NOERROR' q=1 a=0 ns=0 ar=0>
<DNS Question: 'google.com.' qtype=A qclass=IN>
>>> str(DNSRecord.parse(d.pack())) == str(d)
True
>>> print(d)
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ...
;; flags: rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;google.com.                    IN      A
>>> d = DNSRecord.question("google.com","MX")

(This is equivalent to: d = DNSRecord(q=DNSQuestion(“google.com”,QTYPE.MX) )

>>> str(DNSRecord.parse(d.pack())) == str(d)
True
>>> print(d)
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ...
;; flags: rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;google.com.                    IN      MX

To create a DNS Response Packet:

>>> d = DNSRecord(DNSHeader(qr=1,aa=1,ra=1),
...               q=DNSQuestion("abc.com"),
...               a=RR("abc.com",rdata=A("1.2.3.4")))
>>> d
<DNS Header: id=... type=RESPONSE opcode=QUERY flags=AA,RD,RA rcode='NOERROR' q=1 a=1 ns=0 ar=0>
<DNS Question: 'abc.com.' qtype=A qclass=IN>
<DNS RR: 'abc.com.' rtype=A rclass=IN ttl=0 rdata='1.2.3.4'>
>>> str(DNSRecord.parse(d.pack())) == str(d)
True
>>> print(d)
;; ->>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

It is also possible to create RRs from a string in zone file format

>>> RR.fromZone("abc.com IN A 1.2.3.4")
[<DNS RR: 'abc.com.' rtype=A rclass=IN ttl=0 rdata='1.2.3.4'>]
(Note: this produces a list of RRs which should be unpacked if being

passed to add_answer/add_auth/add_ar etc)

>>> q = DNSRecord.question("abc.com")
>>> a = q.reply()
>>> a.add_answer(*RR.fromZone("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

The zone file can contain multiple entries and supports most of the normal format defined in RFC1035 (specifically not $INCLUDE)

>>> z = '''
...         $TTL 300
...         $ORIGIN abc.com
...
...         @       IN      MX      10  mail.abc.com.
...         www     IN      A       1.2.3.4
...                 IN      TXT     "Some Text"
...         mail    IN      CNAME   www.abc.com.
... '''
>>> for rr in RR.fromZone(textwrap.dedent(z)):
...     print(rr)
abc.com.                300     IN      MX      10 mail.abc.com.
www.abc.com.            300     IN      A       1.2.3.4
www.abc.com.            300     IN      TXT     "Some Text"
mail.abc.com.           300     IN      CNAME   www.abc.com.

To create a skeleton reply to a DNS query:

>>> q = DNSRecord(q=DNSQuestion("abc.com",QTYPE.ANY)) 
>>> a = q.reply()
>>> a.add_answer(RR("abc.com",QTYPE.A,rdata=A("1.2.3.4"),ttl=60))
>>> str(DNSRecord.parse(a.pack())) == str(a)
True
>>> 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      ANY
;; ANSWER SECTION:
abc.com.                60      IN      A       1.2.3.4

Add additional RRs:

>>> a.add_answer(RR("xxx.abc.com",QTYPE.A,rdata=A("1.2.3.4")))
>>> a.add_answer(RR("xxx.abc.com",QTYPE.AAAA,rdata=AAAA("1234:5678::1")))
>>> str(DNSRecord.parse(a.pack())) == str(a)
True
>>> print(a)
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ...
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;abc.com.                       IN      ANY
;; ANSWER SECTION:
abc.com.                60      IN      A       1.2.3.4
xxx.abc.com.            0       IN      A       1.2.3.4
xxx.abc.com.            0       IN      AAAA    1234:5678::1

It is also possible to create a reply from a string in zone file format:

>>> q = DNSRecord(q=DNSQuestion("abc.com",QTYPE.ANY)) 
>>> a = q.replyZone("abc.com 60 IN CNAME xxx.abc.com")
>>> 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      ANY
;; ANSWER SECTION:
abc.com.                60      IN      CNAME   xxx.abc.com.
>>> str(DNSRecord.parse(a.pack())) == str(a)
True
>>> q = DNSRecord(q=DNSQuestion("abc.com",QTYPE.ANY)) 
>>> a = q.replyZone(textwrap.dedent(z))
>>> print(a)
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ...
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;abc.com.                       IN      ANY
;; ANSWER SECTION:
abc.com.                300     IN      MX      10 mail.abc.com.
www.abc.com.            300     IN      A       1.2.3.4
www.abc.com.            300     IN      TXT     "Some Text"
mail.abc.com.           300     IN      CNAME   www.abc.com.

The library also includes a simple framework for generating custom DNS resolvers in dnslib.server (see module docs). In post cases this just requires implementing a custom ‘resolve’ method which receives a question object and returns a response.

A number of sample resolvers are provided as examples (see CLI –help):

  • dnslib.fixedresolver - Respond to all requests with fixed response
  • dnslib.zoneresolver - Respond from Zone file
  • dnslib.shellresolver - Call shell script to generate response

The library includes a number of client utilities:

  • DiG like client library

    # python -m dnslib.client –help

  • DNS Proxy Server

    # python -m dnslib.proxy –help

  • Intercepting DNS Proxy Server (replace proxy responses for specified domains)

    # python -m dnslib.intercept –help

Changelog:

  • 0.1 2010-09-19 Initial Release

  • 0.2 2010-09-22 Minor fixes

  • 0.3 2010-10-02 Add DNSLabel class to support arbitrary labels (embedded ‘.’)

  • 0.4 2012-02-26 Merge with dbslib-circuits

  • 0.5 2012-09-13 Add support for RFC2136 DDNS updates

    Patch provided by Wesley Shields <wxs@FreeBSD.org> - thanks

  • 0.6 2012-10-20 Basic AAAA support

  • 0.7 2012-10-20 Add initial EDNS0 support (untested)

  • 0.8 2012-11-04 Add support for NAPTR, Authority RR and additional RR

    Patch provided by Stefan Andersson (https://bitbucket.org/norox) - thanks

  • 0.8.1 2012-11-05 Added NAPTR test case and fixed logic error

    Patch provided by Stefan Andersson (https://bitbucket.org/norox) - thanks

  • 0.8.2 2012-11-11 Patch to fix IPv6 formatting

    Patch provided by Torbjörn Lönnemark (https://bitbucket.org/tobbezz) - thanks

  • 0.8.3 2013-04-27 Don’t parse rdata if rdlength is 0

    Patch provided by Wesley Shields <wxs@FreeBSD.org> - thanks

  • 0.9.0 2014-05-05 Major update including Py3 support (see docs)

  • 0.9.1 2014-05-05 Minor fixes

  • 0.9.2 2014-08-26 Fix Bimap handling of unknown mappings to avoid exception in printing

    Add typed attributes to classes Misc fixes from James Mills - thanks

  • 0.9.3 2014-08-26 Workaround for argparse bug which raises AssertionError if [] is

    present in option text (really?)

  • 0.9.4 2015-04-10 Fix to support multiple strings in TXT record

    Patch provided by James Cherry (https://bitbucket.org/james_cherry) - thanks NOTE: For consistency this patch changes the ‘repr’ output for

    TXT records to always be quoted

  • 0.9.5 2015-10-27 Add threading & timeout handling to DNSServer

  • 0.9.6 2015-10-28 Replace strftime in RRSIG formatting to avoid possible locale issues

    Identified by Bryan Everly - thanks

License:

BSD

Author: