├── .github └── workflows │ └── test.yml ├── .gitignore ├── .hgignore ├── .hgtags ├── LICENSE ├── MANIFEST.in ├── README ├── README.md ├── _config.yml ├── dnslib ├── __init__.py ├── bimap.py ├── bit.py ├── buffer.py ├── client.py ├── digparser.py ├── dns.py ├── fixedresolver.py ├── intercept.py ├── label.py ├── lex.py ├── proxy.py ├── ranges.py ├── server.py ├── shellresolver.py ├── test │ ├── 1.1.1.1.in-addr.arpa.-PTR │ ├── 8.8.8.8.in-addr.arpa.-PTR │ ├── 9.9.9.9.in-addr.arpa.-PTR │ ├── ECC94C1D-7026-41AA-B47E-FDFE40EB9957.com-A │ ├── _sip._udp.sipgate.co.uk-SRV │ ├── cloudflare.com-A │ ├── cloudflare.com-A-dnssec │ ├── cloudflare.com-AAAA │ ├── cloudflare.com-AAAA-dnssec │ ├── cloudflare.com-CNAME │ ├── cloudflare.com-CNAME-dnssec │ ├── cloudflare.com-MX │ ├── cloudflare.com-MX-dnssec │ ├── cloudflare.com-NS │ ├── cloudflare.com-NS-dnssec │ ├── cloudflare.com-SOA │ ├── cloudflare.com-SOA-dnssec │ ├── cloudflare.com-TXT │ ├── cloudflare.com-TXT-dnssec │ ├── dig │ │ ├── google.com-A.dig │ │ └── google.com-ANY.dig │ ├── example.org-DNSKEY │ ├── example.org-DS │ ├── example.org-MX-dnssec │ ├── example.org-PTR-dnssec │ ├── example.org-SOA-dnssec │ ├── fedoraproject.org-TLSA │ ├── google.com-MX │ ├── google.com-NS │ ├── google.com-SOA │ ├── google.com-TXT │ ├── he.net-MX │ ├── he.net-NS │ ├── he.net-SOA │ ├── he.net-TXT │ ├── iana.org-A │ ├── iana.org-A-dnssec │ ├── iana.org-AAAA │ ├── iana.org-AAAA-dnssec │ ├── iana.org-DNSKEY │ ├── iana.org-MX │ ├── iana.org-MX-dnssec │ ├── iana.org-NS │ ├── iana.org-NS-dnssec │ ├── iana.org-PTR-dnssec │ ├── iana.org-SOA │ ├── iana.org-SOA-dnssec │ ├── iana.org-TXT │ ├── iana.org-TXT-dnssec │ ├── oilpro.ch-TYPE65534 │ ├── openssl.org-TLSA │ ├── salsa.debian.org-SSHFP │ ├── sip2sip.info-NAPTR │ ├── www.example.org-A-dnssec │ ├── www.example.org-AAAA-dnssec │ ├── www.example.org-ANY-dnssec │ ├── www.example.org-CNAME-dnssec │ ├── www.example.org-TXT-dnssec │ ├── www.google.com-A │ ├── www.google.com-AAAA │ ├── www.google.com-CNAME │ ├── www.he.net-A │ ├── www.he.net-AAAA │ ├── www.he.net-CNAME │ ├── www.iana.org-A-dnssec │ ├── www.iana.org-AAAA-dnssec │ ├── www.iana.org-ANY-dnssec │ └── www.iana.org-CNAME-dnssec ├── test_decode.py └── zoneresolver.py ├── fuzz.py ├── run_tests.sh └── setup.py /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | max-parallel: 4 11 | matrix: 12 | python-version: ['3.x'] 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Run tests 21 | run: | 22 | pwd 23 | export PYTHONPATH=$(pwd) 24 | python -m dnslib.__init__ 25 | python dnslib/__init__.py 26 | python dnslib/bimap.py 27 | python dnslib/bit.py 28 | python dnslib/buffer.py 29 | python dnslib/label.py 30 | python dnslib/dns.py 31 | python dnslib/lex.py 32 | python dnslib/server.py 33 | python dnslib/digparser.py 34 | python dnslib/ranges.py - 35 | python dnslib/test_decode.py 36 | python fuzz.py 37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build 3 | dist 4 | *.egg-info 5 | __pycache__ 6 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | *.swp 4 | *.pyc 5 | *~ 6 | *egg-info 7 | .DS_Store 8 | build 9 | dist 10 | MANIFEST 11 | PKG-INFO 12 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | 5d1c34b512568beea78225a0ce0b14d31e63695a py3-release 2 | 6622f8dfda7196c992c548c6cb3204dac0a099c8 0.8.3 3 | 5a67733005e02cf59cdec44f5f8a0289295178ad py2 4 | 78b64356f780269694d12b86e86d206c918c49fe 0.9.3 5 | 78b64356f780269694d12b86e86d206c918c49fe 0.9.3 6 | b8d4bd109949da943c03363606cd98d971bebcc5 0.9.3 7 | a248c72d340fd80ec8164a9f9d0ff1ab887f327a 0.9.4 8 | ef31508b35d5cf7b83b76743d2f7ef94efce818f 0.9.5 9 | ef31508b35d5cf7b83b76743d2f7ef94efce818f 0.9.5 10 | 0000000000000000000000000000000000000000 0.9.5 11 | 0000000000000000000000000000000000000000 0.9.5 12 | 12c05da42f9cf151579642e28496f8bd9e7194a3 0.9.5 13 | 13d7ed4d6c62bcb23cbd8792b86b873c64d774c0 0.9.6 14 | 13d7ed4d6c62bcb23cbd8792b86b873c64d774c0 0.9.6 15 | 0000000000000000000000000000000000000000 0.9.6 16 | 0000000000000000000000000000000000000000 0.9.6 17 | c6871d81b4f85c2028a2a2665633698e37321c40 0.9.6 18 | f303b7091b8bcc4f07ed46c5364992a20b5fc405 0.9.7 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 - 2017 Paul Chakravarti. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.github 2 | include run_tests.sh 3 | include fuzz.py 4 | include LICENSE 5 | recursive-include dnslib/test * 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /dnslib/bimap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Bimap - bidirectional mapping between code/value 5 | """ 6 | 7 | import sys,types 8 | 9 | class BimapError(Exception): 10 | pass 11 | 12 | class Bimap(object): 13 | 14 | """ 15 | Bi-directional mapping between code/text. 16 | 17 | Initialised using: 18 | 19 | name: Used for exceptions 20 | dict: Dict mapping from code (numeric) to text 21 | error: Error type to raise if key not found 22 | _or_ callable which either generates mapping 23 | return error 24 | 25 | The class provides: 26 | 27 | * A 'forward' map (code->text) which is accessed through 28 | __getitem__ (bimap[code]) 29 | * A 'reverse' map (code>value) which is accessed through 30 | __getattr__ (bimap.text) 31 | * A 'get' method which does a forward lookup (code->text) 32 | and returns a textual version of code if there is no 33 | explicit mapping (or default provided) 34 | 35 | >>> class TestError(Exception): 36 | ... pass 37 | 38 | >>> TEST = Bimap('TEST',{1:'A', 2:'B', 3:'C'},TestError) 39 | >>> TEST[1] 40 | 'A' 41 | >>> TEST.A 42 | 1 43 | >>> TEST.X 44 | Traceback (most recent call last): 45 | ... 46 | TestError: TEST: Invalid reverse lookup: [X] 47 | >>> TEST[99] 48 | Traceback (most recent call last): 49 | ... 50 | TestError: TEST: Invalid forward lookup: [99] 51 | >>> TEST.get(99) 52 | '99' 53 | 54 | # Test with callable error 55 | >>> def _error(name,key,forward): 56 | ... if forward: 57 | ... try: 58 | ... return "TEST%d" % (key,) 59 | ... except: 60 | ... raise TestError("%s: Invalid forward lookup: [%s]" % (name,key)) 61 | ... else: 62 | ... if key.startswith("TEST"): 63 | ... try: 64 | ... return int(key[4:]) 65 | ... except: 66 | ... pass 67 | ... raise TestError("%s: Invalid reverse lookup: [%s]" % (name,key)) 68 | >>> TEST2 = Bimap('TEST2',{1:'A', 2:'B', 3:'C'},_error) 69 | >>> TEST2[1] 70 | 'A' 71 | >>> TEST2[9999] 72 | 'TEST9999' 73 | >>> TEST2['abcd'] 74 | Traceback (most recent call last): 75 | ... 76 | TestError: TEST2: Invalid forward lookup: [abcd] 77 | >>> TEST2.A 78 | 1 79 | >>> TEST2.TEST9999 80 | 9999 81 | >>> TEST2.X 82 | Traceback (most recent call last): 83 | ... 84 | TestError: TEST2: Invalid reverse lookup: [X] 85 | 86 | """ 87 | 88 | def __init__(self,name,forward,error=AttributeError): 89 | self.name = name 90 | self.error = error 91 | self.forward = forward.copy() 92 | self.reverse = dict([(v,k) for (k,v) in list(forward.items())]) 93 | 94 | def get(self,k,default=None): 95 | try: 96 | return self.forward[k] 97 | except KeyError as e: 98 | return default or str(k) 99 | 100 | def __getitem__(self,k): 101 | try: 102 | return self.forward[k] 103 | except KeyError as e: 104 | if isinstance(self.error,types.FunctionType): 105 | return self.error(self.name,k,True) 106 | else: 107 | raise self.error("%s: Invalid forward lookup: [%s]" % (self.name,k)) 108 | 109 | def __getattr__(self,k): 110 | try: 111 | # Python 3.7 inspect module (called by doctest) checks for __wrapped__ attribute 112 | if k == "__wrapped__": 113 | raise AttributeError() 114 | return self.reverse[k] 115 | except KeyError as e: 116 | if isinstance(self.error,types.FunctionType): 117 | return self.error(self.name,k,False) 118 | else: 119 | raise self.error("%s: Invalid reverse lookup: [%s]" % (self.name,k)) 120 | 121 | if __name__ == '__main__': 122 | import doctest,sys 123 | sys.exit(0 if doctest.testmod().failed == 0 else 1) 124 | -------------------------------------------------------------------------------- /dnslib/bit.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Some basic bit mainpulation utilities 5 | """ 6 | from __future__ import print_function 7 | 8 | FILTER = bytearray([ (i < 32 or i > 127) and 46 or i for i in range(256) ]) 9 | 10 | def hexdump(src, length=16, prefix=''): 11 | """ 12 | Print hexdump of string 13 | 14 | >>> print(hexdump(b"abcd" * 4)) 15 | 0000 61 62 63 64 61 62 63 64 61 62 63 64 61 62 63 64 abcdabcd abcdabcd 16 | 17 | >>> print(hexdump(bytearray(range(48)))) 18 | 0000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ........ ........ 19 | 0010 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f ........ ........ 20 | 0020 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f !"#$%&' ()*+,-./ 21 | 22 | """ 23 | n = 0 24 | left = length // 2 25 | right = length - left 26 | result= [] 27 | src = bytearray(src) 28 | while src: 29 | s,src = src[:length],src[length:] 30 | l,r = s[:left],s[left:] 31 | hexa = "%-*s" % (left*3,' '.join(["%02x"%x for x in l])) 32 | hexb = "%-*s" % (right*3,' '.join(["%02x"%x for x in r])) 33 | lf = l.translate(FILTER) 34 | rf = r.translate(FILTER) 35 | result.append("%s%04x %s %s %s %s" % (prefix, n, hexa, hexb, 36 | lf.decode(), rf.decode())) 37 | n += length 38 | return "\n".join(result) 39 | 40 | def get_bits(data,offset,bits=1): 41 | """ 42 | Get specified bits from integer 43 | 44 | >>> bin(get_bits(0b0011100,2)) 45 | '0b1' 46 | >>> bin(get_bits(0b0011100,0,4)) 47 | '0b1100' 48 | 49 | """ 50 | mask = ((1 << bits) - 1) << offset 51 | return (data & mask) >> offset 52 | 53 | def set_bits(data,value,offset,bits=1): 54 | """ 55 | Set specified bits in integer 56 | 57 | >>> bin(set_bits(0,0b1010,0,4)) 58 | '0b1010' 59 | >>> bin(set_bits(0,0b1010,3,4)) 60 | '0b1010000' 61 | """ 62 | mask = ((1 << bits) - 1) << offset 63 | clear = 0xffff ^ mask 64 | data = (data & clear) | ((value << offset) & mask) 65 | return data 66 | 67 | def binary(n,count=16,reverse=False): 68 | """ 69 | Display n in binary (only difference from built-in `bin` is 70 | that this function returns a fixed width string and can 71 | optionally be reversed 72 | 73 | >>> binary(6789) 74 | '0001101010000101' 75 | >>> binary(6789,8) 76 | '10000101' 77 | >>> binary(6789,reverse=True) 78 | '1010000101011000' 79 | 80 | """ 81 | bits = [str((n >> y) & 1) for y in range(count-1, -1, -1)] 82 | if reverse: 83 | bits.reverse() 84 | return "".join(bits) 85 | 86 | if __name__ == '__main__': 87 | import doctest,sys 88 | sys.exit(0 if doctest.testmod().failed == 0 else 1) 89 | -------------------------------------------------------------------------------- /dnslib/buffer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Buffer - simple data buffer 5 | """ 6 | 7 | import binascii,struct 8 | 9 | class BufferError(Exception): 10 | pass 11 | 12 | class Buffer(object): 13 | 14 | """ 15 | A simple data buffer - supports packing/unpacking in struct format 16 | 17 | # Needed for Python 2/3 doctest compatibility 18 | >>> def p(s): 19 | ... if not isinstance(s,str): 20 | ... return s.decode() 21 | ... return s 22 | 23 | >>> b = Buffer() 24 | >>> b.pack("!BHI",1,2,3) 25 | >>> b.offset 26 | 7 27 | >>> b.append(b"0123456789") 28 | >>> b.offset 29 | 17 30 | >>> p(b.hex()) 31 | '0100020000000330313233343536373839' 32 | >>> b.offset = 0 33 | >>> b.unpack("!BHI") 34 | (1, 2, 3) 35 | >>> bytearray(b.get(5)) 36 | bytearray(b'01234') 37 | >>> bytearray(b.get(5)) 38 | bytearray(b'56789') 39 | >>> b.update(7,"2s",b"xx") 40 | >>> b.offset = 7 41 | >>> bytearray(b.get(5)) 42 | bytearray(b'xx234') 43 | """ 44 | 45 | def __init__(self,data=b''): 46 | """ 47 | Initialise Buffer from data 48 | """ 49 | self.data = bytearray(data) 50 | self.offset = 0 51 | 52 | def remaining(self): 53 | """ 54 | Return bytes remaining 55 | """ 56 | return len(self.data) - self.offset 57 | 58 | def get(self,length): 59 | """ 60 | Gen len bytes at current offset (& increment offset) 61 | """ 62 | if length > self.remaining(): 63 | raise BufferError("Not enough bytes [offset=%d,remaining=%d,requested=%d]" % 64 | (self.offset,self.remaining(),length)) 65 | start = self.offset 66 | end = self.offset + length 67 | self.offset += length 68 | return bytes(self.data[start:end]) 69 | 70 | def hex(self): 71 | """ 72 | Return data as hex string 73 | """ 74 | return binascii.hexlify(self.data) 75 | 76 | def pack(self,fmt,*args): 77 | """ 78 | Pack data at end of data according to fmt (from struct) & increment 79 | offset 80 | """ 81 | self.offset += struct.calcsize(fmt) 82 | self.data += struct.pack(fmt,*args) 83 | 84 | def append(self,s): 85 | """ 86 | Append s to end of data & increment offset 87 | """ 88 | self.offset += len(s) 89 | self.data += s 90 | 91 | def update(self,ptr,fmt,*args): 92 | """ 93 | Modify data at offset `ptr` 94 | """ 95 | s = struct.pack(fmt,*args) 96 | self.data[ptr:ptr+len(s)] = s 97 | 98 | def unpack(self,fmt): 99 | """ 100 | Unpack data at current offset according to fmt (from struct) 101 | """ 102 | try: 103 | data = self.get(struct.calcsize(fmt)) 104 | return struct.unpack(fmt,data) 105 | except struct.error as e: 106 | raise BufferError("Error unpacking struct '%s' <%s>" % 107 | (fmt,binascii.hexlify(data).decode())) 108 | 109 | def __len__(self): 110 | return len(self.data) 111 | 112 | if __name__ == '__main__': 113 | import doctest,sys 114 | sys.exit(0 if doctest.testmod().failed == 0 else 1) 115 | -------------------------------------------------------------------------------- /dnslib/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | DNS Client - DiG-like CLI utility. 5 | 6 | Mostly useful for testing. Can optionally compare results from two 7 | nameservers (--diff) or compare results against DiG (--dig). 8 | 9 | Usage: python -m dnslib.client [options|--help] 10 | 11 | See --help for usage. 12 | """ 13 | 14 | from __future__ import print_function 15 | 16 | try: 17 | from subprocess import getoutput,getstatusoutput 18 | except ImportError: 19 | from commands import getoutput,getstatusoutput 20 | 21 | import binascii,code,pprint,sys 22 | 23 | from dnslib.dns import DNSRecord,DNSHeader,DNSQuestion,DNSError,QTYPE,EDNS0 24 | from dnslib.digparser import DigParser 25 | 26 | if __name__ == '__main__': 27 | 28 | import argparse,sys,time 29 | 30 | p = argparse.ArgumentParser(description="DNS Client") 31 | p.add_argument("--server","-s",default="8.8.8.8", 32 | metavar="", 33 | help="Server address:port (default:8.8.8.8:53) (port is optional)") 34 | p.add_argument("--query",action='store_true',default=False, 35 | help="Show query (default: False)") 36 | p.add_argument("--hex",action='store_true',default=False, 37 | help="Dump packet in hex (default: False)") 38 | p.add_argument("--tcp",action='store_true',default=False, 39 | help="Use TCP (default: UDP)") 40 | p.add_argument("--noretry",action='store_true',default=False, 41 | help="Don't retry query using TCP if truncated (default: false)") 42 | p.add_argument("--diff",default="", 43 | help="Compare response from alternate nameserver (format: address:port / default: false)") 44 | p.add_argument("--dig",action='store_true',default=False, 45 | help="Compare result with DiG - if ---diff also specified use alternative nameserver for DiG request (default: false)") 46 | p.add_argument("--short",action='store_true',default=False, 47 | help="Short output - rdata only (default: false)") 48 | p.add_argument("--dnssec",action='store_true',default=False, 49 | help="Set DNSSEC (DO/AD) flags in query (default: false)") 50 | p.add_argument("--debug",action='store_true',default=False, 51 | help="Drop into CLI after request (default: false)") 52 | p.add_argument("domain",metavar="", 53 | help="Query domain") 54 | p.add_argument("qtype",metavar="",default="A",nargs="?", 55 | help="Query type (default: A)") 56 | args = p.parse_args() 57 | 58 | # Construct request 59 | try: 60 | q = DNSRecord(q=DNSQuestion(args.domain,getattr(QTYPE,args.qtype))) 61 | 62 | if args.dnssec: 63 | q.add_ar(EDNS0(flags="do",udp_len=4096)) 64 | q.header.ad = 1 65 | 66 | address,_,port = args.server.partition(':') 67 | port = int(port or 53) 68 | 69 | if args.query: 70 | print(";; Sending%s:" % (" (TCP)" if args.tcp else "")) 71 | if args.hex: 72 | print(";; QUERY:",binascii.hexlify(q.pack()).decode()) 73 | print(q) 74 | print() 75 | 76 | a_pkt = q.send(address,port,tcp=args.tcp) 77 | a = DNSRecord.parse(a_pkt) 78 | 79 | if q.header.id != a.header.id: 80 | raise DNSError('Response transaction id does not match query transaction id') 81 | 82 | if a.header.tc and args.noretry == False: 83 | # Truncated - retry in TCP mode 84 | a_pkt = q.send(address,port,tcp=True) 85 | a = DNSRecord.parse(a_pkt) 86 | 87 | if args.dig or args.diff: 88 | if args.diff: 89 | address,_,port = args.diff.partition(':') 90 | port = int(port or 53) 91 | 92 | if args.dig: 93 | if getstatusoutput("dig -v")[0] != 0: 94 | p.error("DiG not found") 95 | if args.dnssec: 96 | dig = getoutput("dig +qr +dnssec -p %d %s %s @%s" % ( 97 | port, args.domain, args.qtype, address)) 98 | else: 99 | dig = getoutput("dig +qr +noedns +noadflag -p %d %s %s @%s" % ( 100 | port, args.domain, args.qtype, address)) 101 | dig_reply = list(iter(DigParser(dig))) 102 | # DiG might have retried in TCP mode so get last q/a 103 | q_diff = dig_reply[-2] 104 | a_diff = dig_reply[-1] 105 | else: 106 | q_diff = DNSRecord(header=DNSHeader(id=q.header.id), 107 | q=DNSQuestion(args.domain, 108 | getattr(QTYPE,args.qtype))) 109 | q_diff = q 110 | diff = q_diff.send(address,port,tcp=args.tcp) 111 | a_diff = DNSRecord.parse(diff) 112 | if a_diff.header.tc and args.noretry == False: 113 | diff = q_diff.send(address,port,tcp=True) 114 | a_diff = DNSRecord.parse(diff) 115 | 116 | if args.short: 117 | print(a.short()) 118 | else: 119 | print(";; Got answer:") 120 | if args.hex: 121 | print(";; RESPONSE:",binascii.hexlify(a_pkt).decode()) 122 | if args.diff and not args.dig: 123 | print(";; DIFF :",binascii.hexlify(diff).decode()) 124 | print(a) 125 | print() 126 | 127 | if args.dig or args.diff: 128 | if q != q_diff: 129 | print(";;; ERROR: Diff Question differs") 130 | for (d1,d2) in q.diff(q_diff): 131 | if d1: 132 | print(";; - %s" % d1) 133 | if d2: 134 | print(";; + %s" % d2) 135 | if a != a_diff: 136 | print(";;; ERROR: Diff Response differs") 137 | for (d1,d2) in a.diff(a_diff): 138 | if d1: 139 | print(";; - %s" % d1) 140 | if d2: 141 | print(";; + %s" % d2) 142 | 143 | if args.debug: 144 | code.interact(local=locals()) 145 | 146 | except DNSError as e: 147 | p.error(e) 148 | -------------------------------------------------------------------------------- /dnslib/digparser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | digparser 5 | --------- 6 | 7 | Encode/decode DNS packets from DiG textual representation. Parses 8 | question (if present: +qr flag) & answer sections and returns list 9 | of DNSRecord objects. 10 | 11 | Unsupported RR types are skipped (this is different from the packet 12 | parser which will store and encode the RDATA as a binary blob) 13 | 14 | >>> dig = os.path.join(os.path.dirname(__file__),"test","dig","google.com-A.dig") 15 | >>> with open(dig) as f: 16 | ... l = DigParser(f) 17 | ... for record in l: 18 | ... print('---') 19 | ... print(repr(record)) 20 | --- 21 | 22 | 23 | --- 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | >>> dig = os.path.join(os.path.dirname(__file__),"test","dig","google.com-ANY.dig") 44 | >>> with open(dig) as f: 45 | ... l = DigParser(f) 46 | ... for record in l: 47 | ... print('---') 48 | ... print(repr(record)) 49 | --- 50 | 51 | 52 | --- 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | """ 71 | 72 | from __future__ import print_function 73 | 74 | import glob,os.path,string,re 75 | 76 | from dnslib.lex import WordLexer 77 | from dnslib.dns import (DNSRecord,DNSHeader,DNSQuestion,DNSError, 78 | RR,RD,RDMAP,QR,RCODE,CLASS,QTYPE,EDNS0) 79 | 80 | class DigParser: 81 | 82 | """ 83 | Parse Dig output 84 | """ 85 | 86 | def __init__(self,dig,debug=False): 87 | self.debug = debug 88 | self.l = WordLexer(dig) 89 | self.l.commentchars = ';' 90 | self.l.nltok = ('NL',None) 91 | self.i = iter(self.l) 92 | 93 | def parseHeader(self,l1,l2): 94 | _,_,_,opcode,_,status,_,_id = l1.split() 95 | _,flags,_ = l2.split(';') 96 | header = DNSHeader(id=int(_id),bitmap=0) 97 | header.opcode = getattr(QR,opcode.rstrip(',')) 98 | header.rcode = getattr(RCODE,status.rstrip(',')) 99 | for f in ('qr','aa','tc','rd','ra','ad','cd'): 100 | if f in flags: 101 | setattr(header,f,1) 102 | return header 103 | 104 | def expect(self,expect): 105 | t,val = next(self.i) 106 | if t != expect: 107 | raise ValueError("Invalid Token: %s (expecting: %s)" % (t,expect)) 108 | return val 109 | 110 | def parseQuestions(self,q,dns): 111 | for qname,qclass,qtype in q: 112 | dns.add_question(DNSQuestion(qname, 113 | getattr(QTYPE,qtype), 114 | getattr(CLASS,qclass))) 115 | 116 | def parseAnswers(self,a,auth,ar,dns): 117 | sect_map = {'a':'add_answer','auth':'add_auth','ar':'add_ar'} 118 | for sect in 'a','auth','ar': 119 | f = getattr(dns,sect_map[sect]) 120 | for rr in locals()[sect]: 121 | rname,ttl,rclass,rtype = rr[:4] 122 | rdata = rr[4:] 123 | rd = RDMAP.get(rtype,RD) 124 | try: 125 | if rd == RD and \ 126 | any([ x not in string.hexdigits for x in rdata[-1]]): 127 | # Only support hex encoded data for fallback RD 128 | pass 129 | else: 130 | f(RR(rname=rname, 131 | ttl=int(ttl), 132 | rtype=getattr(QTYPE,rtype), 133 | rclass=getattr(CLASS,rclass), 134 | rdata=rd.fromZone(rdata))) 135 | except DNSError as e: 136 | if self.debug: 137 | print("DNSError:",e,rr) 138 | else: 139 | # Skip records we dont understand 140 | pass 141 | 142 | def parseEDNS(self,edns,dns): 143 | args = {} 144 | m = re.search(r'version: (\d+),',edns) 145 | if m: 146 | args['version'] = int(m.group(1)) 147 | m = re.search(r'flags:\s*(.*?);',edns) 148 | if m: 149 | args['flags'] = m.group(1) 150 | m = re.search(r'udp: (\d+)',edns) 151 | if m: 152 | args['udp_len'] = int(m.group(1)) 153 | dns.add_ar(EDNS0(**args)) 154 | 155 | def __iter__(self): 156 | return self.parse() 157 | 158 | def parse(self): 159 | dns = None 160 | section = None 161 | paren = False 162 | rr = [] 163 | try: 164 | while True: 165 | tok,val = next(self.i) 166 | if tok == 'COMMENT': 167 | if val.startswith('; ->>HEADER<<-'): 168 | # Start new record 169 | if dns: 170 | # If we have a current record complete this 171 | self.parseQuestions(q,dns) 172 | self.parseAnswers(a,auth,ar,dns) 173 | yield(dns) 174 | dns = DNSRecord() 175 | q,a,auth,ar = [],[],[],[] 176 | self.expect('NL') 177 | val2 = self.expect('COMMENT') 178 | dns.header = self.parseHeader(val,val2) 179 | elif val.startswith('; QUESTION'): 180 | section = q 181 | elif val.startswith('; ANSWER'): 182 | section = a 183 | elif val.startswith('; AUTHORITY'): 184 | section = auth 185 | elif val.startswith('; ADDITIONAL'): 186 | section = ar 187 | elif val.startswith('; OPT'): 188 | # Only partial support for parsing EDNS records 189 | self.expect('NL') 190 | val2 = self.expect('COMMENT') 191 | self.parseEDNS(val2,dns) 192 | elif val.startswith(';') or tok[1].startswith('<<>>'): 193 | pass 194 | elif dns and section == q: 195 | q.append(val.split()) 196 | elif tok == 'ATOM': 197 | if val == '(': 198 | paren = True 199 | elif val == ')': 200 | paren = False 201 | else: 202 | rr.append(val) 203 | elif tok == 'NL' and not paren and rr: 204 | if self.debug: 205 | print(">>",rr) 206 | section.append(rr) 207 | rr = [] 208 | except StopIteration: 209 | if rr: 210 | self.section.append(rr) 211 | if dns: 212 | self.parseQuestions(q,dns) 213 | self.parseAnswers(a,auth,ar,dns) 214 | yield(dns) 215 | 216 | if __name__ == '__main__': 217 | 218 | import argparse,doctest,sys 219 | 220 | p = argparse.ArgumentParser(description="DigParser Test") 221 | p.add_argument("--dig",action='store_true',default=False, 222 | help="Parse DiG output (stdin)") 223 | p.add_argument("--debug",action='store_true',default=False, 224 | help="Debug output") 225 | 226 | args = p.parse_args() 227 | 228 | if args.dig: 229 | l = DigParser(sys.stdin,args.debug) 230 | for record in l: 231 | print(repr(record)) 232 | else: 233 | sys.exit(0 if doctest.testmod().failed == 0 else 1) 234 | -------------------------------------------------------------------------------- /dnslib/fixedresolver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | FixedResolver - example resolver which responds with fixed response 5 | to all requests 6 | """ 7 | 8 | from __future__ import print_function 9 | 10 | import copy 11 | 12 | from dnslib import RR 13 | from dnslib.server import DNSServer,DNSHandler,BaseResolver,DNSLogger 14 | 15 | class FixedResolver(BaseResolver): 16 | """ 17 | Respond with fixed response to all requests 18 | """ 19 | def __init__(self,zone): 20 | # Parse RRs 21 | self.rrs = RR.fromZone(zone) 22 | 23 | def resolve(self,request,handler): 24 | reply = request.reply() 25 | qname = request.q.qname 26 | # Replace labels with request label 27 | for rr in self.rrs: 28 | a = copy.copy(rr) 29 | a.rname = qname 30 | reply.add_answer(a) 31 | return reply 32 | 33 | if __name__ == '__main__': 34 | 35 | import argparse,sys,time 36 | 37 | p = argparse.ArgumentParser(description="Fixed DNS Resolver") 38 | p.add_argument("--response","-r",default=". 60 IN A 127.0.0.1", 39 | metavar="", 40 | help="DNS response (zone format) (default: 127.0.0.1)") 41 | p.add_argument("--zonefile","-f", 42 | metavar="", 43 | help="DNS response (zone file, '-' for stdin)") 44 | p.add_argument("--port","-p",type=int,default=53, 45 | metavar="", 46 | help="Server port (default:53)") 47 | p.add_argument("--address","-a",default="", 48 | metavar="
", 49 | help="Listen address (default:all)") 50 | p.add_argument("--udplen","-u",type=int,default=0, 51 | metavar="", 52 | help="Max UDP packet length (default:0)") 53 | p.add_argument("--tcp",action='store_true',default=False, 54 | help="TCP server (default: UDP only)") 55 | p.add_argument("--log",default="request,reply,truncated,error", 56 | help="Log hooks to enable (default: +request,+reply,+truncated,+error,-recv,-send,-data)") 57 | p.add_argument("--log-prefix",action='store_true',default=False, 58 | help="Log prefix (timestamp/handler/resolver) (default: False)") 59 | args = p.parse_args() 60 | 61 | if args.zonefile: 62 | if args.zonefile == '-': 63 | args.response = sys.stdin 64 | else: 65 | args.response = open(args.zonefile) 66 | 67 | resolver = FixedResolver(args.response) 68 | logger = DNSLogger(args.log,prefix=args.log_prefix) 69 | 70 | print("Starting Fixed Resolver (%s:%d) [%s]" % ( 71 | args.address or "*", 72 | args.port, 73 | "UDP/TCP" if args.tcp else "UDP")) 74 | 75 | for rr in resolver.rrs: 76 | print(" | ",rr.toZone().strip(),sep="") 77 | print() 78 | 79 | if args.udplen: 80 | DNSHandler.udplen = args.udplen 81 | 82 | udp_server = DNSServer(resolver, 83 | port=args.port, 84 | address=args.address, 85 | logger=logger) 86 | udp_server.start_thread() 87 | 88 | if args.tcp: 89 | tcp_server = DNSServer(resolver, 90 | port=args.port, 91 | address=args.address, 92 | tcp=True, 93 | logger=logger) 94 | tcp_server.start_thread() 95 | 96 | while udp_server.isAlive(): 97 | time.sleep(1) 98 | 99 | -------------------------------------------------------------------------------- /dnslib/intercept.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | InterceptResolver - proxy requests to upstream server 5 | (optionally intercepting) 6 | 7 | """ 8 | from __future__ import print_function 9 | 10 | import binascii,copy,socket,struct,sys 11 | 12 | from dnslib import DNSRecord,RR,QTYPE,RCODE,parse_time 13 | from dnslib.server import DNSServer,DNSHandler,BaseResolver,DNSLogger 14 | from dnslib.label import DNSLabel 15 | 16 | class InterceptResolver(BaseResolver): 17 | 18 | """ 19 | Intercepting resolver 20 | 21 | Proxy requests to upstream server optionally intercepting requests 22 | matching local records 23 | """ 24 | 25 | def __init__(self,address,port,ttl,intercept,skip,nxdomain,forward,all_qtypes,timeout=0): 26 | """ 27 | address/port - upstream server 28 | ttl - default ttl for intercept records 29 | intercept - list of wildcard RRs to respond to (zone format) 30 | skip - list of wildcard labels to skip 31 | nxdomain - list of wildcard labels to return NXDOMAIN 32 | forward - list of wildcard labels to forward 33 | all_qtypes - intercept all qtypes if qname matches. 34 | timeout - timeout for upstream server(s) 35 | """ 36 | self.address = address 37 | self.port = port 38 | self.ttl = parse_time(ttl) 39 | self.skip = skip 40 | self.nxdomain = nxdomain 41 | self.forward = [] 42 | for i in forward: 43 | qname, _, upstream = i.partition(':') 44 | upstream_ip, _, upstream_port = upstream.partition(':') 45 | self.forward.append((qname, upstream_ip, int(upstream_port or '53'))) 46 | self.all_qtypes = all_qtypes 47 | self.timeout = timeout 48 | self.zone = [] 49 | for i in intercept: 50 | if i == '-': 51 | i = sys.stdin.read() 52 | for rr in RR.fromZone(i,ttl=self.ttl): 53 | self.zone.append((rr.rname,QTYPE[rr.rtype],rr)) 54 | 55 | def resolve(self,request,handler): 56 | matched = False 57 | reply = request.reply() 58 | qname = request.q.qname 59 | qtype = QTYPE[request.q.qtype] 60 | # Try to resolve locally unless on skip list 61 | if not any([qname.matchGlob(s) for s in self.skip]): 62 | for name,rtype,rr in self.zone: 63 | if qname.matchGlob(name): 64 | if qtype in (rtype,'ANY','CNAME'): 65 | a = copy.copy(rr) 66 | a.rname = qname 67 | reply.add_answer(a) 68 | matched = True 69 | # Check for NXDOMAIN 70 | if any([qname.matchGlob(s) for s in self.nxdomain]): 71 | reply.header.rcode = getattr(RCODE,'NXDOMAIN') 72 | return reply 73 | if matched and self.all_qtypes: 74 | return reply 75 | # Otherwise proxy, first checking forwards, then to upstream. 76 | upstream, upstream_port = self.address,self.port 77 | if not any([qname.matchGlob(s) for s in self.skip]): 78 | for name, ip, port in self.forward: 79 | if qname.matchGlob(name): 80 | upstream, upstream_port = ip, port 81 | if not reply.rr: 82 | try: 83 | if handler.protocol == 'udp': 84 | proxy_r = request.send(upstream,upstream_port, 85 | timeout=self.timeout) 86 | else: 87 | proxy_r = request.send(upstream,upstream_port, 88 | tcp=True,timeout=self.timeout) 89 | reply = DNSRecord.parse(proxy_r) 90 | except socket.timeout: 91 | reply.header.rcode = getattr(RCODE,'SERVFAIL') 92 | 93 | return reply 94 | 95 | if __name__ == '__main__': 96 | 97 | import argparse,sys,time 98 | 99 | p = argparse.ArgumentParser(description="DNS Intercept Proxy") 100 | p.add_argument("--port","-p",type=int,default=53, 101 | metavar="", 102 | help="Local proxy port (default:53)") 103 | p.add_argument("--address","-a",default="", 104 | metavar="
", 105 | help="Local proxy listen address (default:all)") 106 | p.add_argument("--upstream","-u",default="8.8.8.8:53", 107 | metavar="", 108 | help="Upstream DNS server:port (default:8.8.8.8:53)") 109 | p.add_argument("--tcp",action='store_true',default=False, 110 | help="TCP proxy (default: UDP only)") 111 | p.add_argument("--intercept","-i",action="append", 112 | metavar="", 113 | help="Intercept requests matching zone record (glob) ('-' for stdin)") 114 | p.add_argument("--skip","-s",action="append", 115 | metavar="