diff --git a/LINETIME b/LINETIME index 516e39f..a3b8624 100644 --- a/LINETIME +++ b/LINETIME @@ -1,3 +1,10 @@ +* 21/09/16 + Added the GUI!!! + Install : Install man files + Doc : Added new paper (Spanish) + Fixed Module : fbt/pr.pop + Fixed Import modules installed + * 23/08/16 Added New Function : Invoke, to call module within another module. Added New Dependence : xterm, For open news consoles in other windows diff --git a/core/Information.py b/core/Information.py index f1981d7..3461ce0 100644 --- a/core/Information.py +++ b/core/Information.py @@ -6,6 +6,6 @@ #######################HEADER# version="0.0.1.0" -date="28/07/16:18/08/16" -build="0063" +date="18/08/16:21/09/16" +build="0064" Type="FREE" diff --git a/core/modules.xml b/core/modules.xml index fbd9e5d..dd4f022 100644 --- a/core/modules.xml +++ b/core/modules.xml @@ -151,28 +151,28 @@ 1.1 LeSZO ZerO btf - bruteforcetoftprotol + brute_force_to_ftp_protocol Brute Force to FTprotocol. 1.1 RedToor btf - bruteforcetosqlprotocol + brute_force_to_sql_protocol Brute Force to SQL protocol. 1.1 RedToor btf - bruteforcetosshprotocol + brute_force_to_ssh_protocol Brute Force to SSH protocol. 1.1 RedToor btf - bruteforcetopop3rotocol + brute_force_to_pop3_protocol Brute Force to POP3 protocol. diff --git a/core/shorts/ktf.gui b/core/shorts/ktf.gui new file mode 100644 index 0000000..ff21444 --- /dev/null +++ b/core/shorts/ktf.gui @@ -0,0 +1,3 @@ +#!/bin/bash +cd /usr/share/KatanaFramework +java -jar ktf.gui.jar \ No newline at end of file diff --git a/core/version.json b/core/version.json index 56a78cc..1b1782c 100644 --- a/core/version.json +++ b/core/version.json @@ -1 +1 @@ -{"Katana":{"Description":"Katana Updates information","Author": "Redtoor","Version":"0.3","Update":{"Core":"0.0.1.0","Build":"0063","Date":"18/08/16"}}} +{"Katana":{"Description":"Katana Updates information","Author": "Redtoor","Version":"0.3","Update":{"Core":"0.0.1.0","Build":"0064","Date":"21/09/16"}}} diff --git a/doc/Paper-ES.pdf b/doc/Paper-ES.pdf new file mode 100644 index 0000000..feb0713 Binary files /dev/null and b/doc/Paper-ES.pdf differ diff --git a/install b/install index c010f46..15d603c 100644 --- a/install +++ b/install @@ -2,7 +2,7 @@ #HEAD######################################################### # # Katana Framework | install -# Last Modified: 18/08/2016 +# Last Modified: 21/09/2016 # #########################################################HEAD# @@ -50,9 +50,11 @@ ln -f -r -s /usr/share/KatanaFramework/core/shorts/ktf.linker /usr/bin/ktf.link ln -f -r -s /usr/share/KatanaFramework/core/shorts/ktf.update /usr/bin/ktf.update ; ln -f -r -s /usr/share/KatanaFramework/core/shorts/ktf.ktf /usr/bin/ktf.ktf ; ln -f -r -s /usr/share/KatanaFramework/core/shorts/ktf.tool /usr/bin/ktf.tool ; +ln -f -r -s /usr/share/KatanaFramework/core/shorts/ktf.gui /usr/bin/ktf.gui ; ln -f -r -s /usr/share/KatanaFramework/core/shorts/ktf.run /usr/bin/ktf.run """ Files=""" +cp -r * /usr/share/KatanaFramework/doc/man/ /usr/local/share/man/man1/ ; cd /usr/share/KatanaFramework/files/ ; tar xvf /usr/share/KatanaFramework/files/exiftool.tar >/dev/null 2>&1; tar xvf /usr/share/KatanaFramework/files/facebrok1.8.tar >/dev/null 2>&1; diff --git a/ktf.gui.jar b/ktf.gui.jar new file mode 100644 index 0000000..5112067 Binary files /dev/null and b/ktf.gui.jar differ diff --git a/ktf.linker b/ktf.linker index bc3beee..44b5949 100644 --- a/ktf.linker +++ b/ktf.linker @@ -1,71 +1,7 @@ - -from scripts.web import whois -from core import Errors - -from core.Function import KatanaCheckActionShowModules,KatanaCheckActionUseModule -from core.Function import KatanaCheckActionShowOptions -from core.Function import KatanaCheckActionGetInfo,ShowInformationModule -from core.Function import ShowOptions,KatanaCheckActionSetValue,UpdateValue,runModule -from core.Function import KatanaCheckActionisBack,KatanaCheckActionGlobalCommands - -import xml.etree.ElementTree as ET -import importlib -from core.design import * -from core import info -import argparse - -CLASS_LINKER=DESIGN() -tree = ET.parse('core/modules.xml') -root = tree.getroot() -parser = argparse.ArgumentParser() -parser = argparse.ArgumentParser(description='ktf.linker:'+info.version+info.build) -parser.add_argument("-m", "--module", help="Module") -parser.add_argument("-t", "--target", help="Target") -parser.add_argument("-p", "--port", help="Port") -parser.add_argument("-f", "--file", help="File") -parser.add_argument("-u", "--user", help="Username") -parser.add_argument("-ps","--pass", help="Password") -parser.add_argument("-i","--interface", help="Password") - -args = parser.parse_args() -args = parser.parse_args() -m=args.module - -if __name__=="__main__": - # For GUI not show this. - CLASS_LINKER.linker(info.version, info.build) - for modules in root.findall('module'): - if m == modules.get('name'): - Space() - category = modules.find('category').text - filename = modules.find('filename').text - ModuleToStart = importlib.import_module("modules."+category+"."+filename) - init=ModuleToStart.init() - - for option in init.options: - if init.options[option] == "target":init.options[option]=[args.target ,init.options[option][1],init.options[option][2]] - if init.options[option] == "port" :init.options[option]=[args.port ,init.options[option][1],init.options[option][2]] - if init.options[option] == "file" :init.options[option]=[args.file ,init.options[option][1],init.options[option][2]] - if init.options[option] == "user" :init.options[option]=[args.user ,init.options[option][1],init.options[option][2]] - if init.options[option] == "pass" :init.options[option]=[args.interface ,init.options[option][1],init.options[option][2]] - - actions = "run" - - while True: - try: - if actions != "run":actions=raw_input(Prompt(init.CodeName)) - if KatanaCheckActionShowOptions(actions) :ShowOptions(init) - elif KatanaCheckActionSetValue(actions) :init.options=UpdateValue(actions,init.options) - elif KatanaCheckActionGetInfo(actions) :ShowInformationModule(init) - elif runModule(actions): - for Namevalue in init.options: - init.var.update({Namevalue:init.options[Namevalue][0]}) - ModuleToStart.main(True) - elif KatanaCheckActionisBack(actions) :printAlert(6,"This Commands just work in console mode.") - else : - if a - ctions != "":KatanaCheckActionGlobalCommands(actions) - actions = "" - except:Errors.Errors() - Space() - ModuleNotFound(m) \ No newline at end of file +#!/usr/bin/env python +#HEAD######################################################### +# +# Katana Framework | ktf.linker +# Last Modified: 21/09/2016 +# +#########################################################HEAD# diff --git a/lib/IPy/build/lib.linux-i686-2.7/IPy.py b/lib/IPy/build/lib.linux-i686-2.7/IPy.py new file mode 100644 index 0000000..d1ea845 --- /dev/null +++ b/lib/IPy/build/lib.linux-i686-2.7/IPy.py @@ -0,0 +1,1652 @@ +""" +IPy - class and tools for handling of IPv4 and IPv6 addresses and networks. +See README file for learn how to use IPy. + +Further Information might be available at: +https://github.com/haypo/python-ipy +""" + +__version__ = '0.83' + +import bisect +import collections +import sys +import types + +# Definition of the Ranges for IPv4 IPs +# this should include www.iana.org/assignments/ipv4-address-space +# and www.iana.org/assignments/multicast-addresses +IPv4ranges = { + '0': 'PUBLIC', # fall back + '00000000': 'PRIVATE', # 0/8 + '00001010': 'PRIVATE', # 10/8 + '0110010001': 'CARRIER_GRADE_NAT', #100.64/10 + '01111111': 'PRIVATE', # 127.0/8 + '1': 'PUBLIC', # fall back + '1010100111111110': 'PRIVATE', # 169.254/16 + '101011000001': 'PRIVATE', # 172.16/12 + '1100000010101000': 'PRIVATE', # 192.168/16 + '111': 'RESERVED', # 224/3 + } + +# Definition of the Ranges for IPv6 IPs +# http://www.iana.org/assignments/ipv6-address-space/ +# http://www.iana.org/assignments/ipv6-unicast-address-assignments/ +# http://www.iana.org/assignments/ipv6-multicast-addresses/ +IPv6ranges = { + '00000000' : 'RESERVED', # ::/8 + '0' * 96 : 'RESERVED', # ::/96 Formerly IPV4COMP [RFC4291] + '0' * 128 : 'UNSPECIFIED', # ::/128 + '0' * 127 + '1' : 'LOOPBACK', # ::1/128 + '0' * 80 + '1' * 16 : 'IPV4MAP', # ::ffff:0:0/96 + '00000000011001001111111110011011' + '0' * 64 : 'WKP46TRANS', # 0064:ff9b::/96 Well-Known-Prefix [RFC6052] + '00000001' : 'UNASSIGNED', # 0100::/8 + '0000001' : 'RESERVED', # 0200::/7 Formerly NSAP [RFC4048] + '0000010' : 'RESERVED', # 0400::/7 Formerly IPX [RFC3513] + '0000011' : 'RESERVED', # 0600::/7 + '00001' : 'RESERVED', # 0800::/5 + '0001' : 'RESERVED', # 1000::/4 + '001' : 'GLOBAL-UNICAST', # 2000::/3 [RFC4291] + '00100000000000010000000' : 'SPECIALPURPOSE', # 2001::/23 [RFC4773] + '00100000000000010000000000000000' : 'TEREDO', # 2001::/32 [RFC4380] + '00100000000000010000000000000010' + '0' * 16 : 'BMWG', # 2001:0002::/48 Benchmarking [RFC5180] + '0010000000000001000000000001' : 'ORCHID', # 2001:0010::/28 (Temp until 2014-03-21) [RFC4843] + '00100000000000010000001' : 'ALLOCATED APNIC', # 2001:0200::/23 + '00100000000000010000010' : 'ALLOCATED ARIN', # 2001:0400::/23 + '00100000000000010000011' : 'ALLOCATED RIPE NCC', # 2001:0600::/23 + '00100000000000010000100' : 'ALLOCATED RIPE NCC', # 2001:0800::/23 + '00100000000000010000101' : 'ALLOCATED RIPE NCC', # 2001:0a00::/23 + '00100000000000010000110' : 'ALLOCATED APNIC', # 2001:0c00::/23 + '00100000000000010000110110111000' : 'DOCUMENTATION', # 2001:0db8::/32 [RFC3849] + '00100000000000010000111' : 'ALLOCATED APNIC', # 2001:0e00::/23 + '00100000000000010001001' : 'ALLOCATED LACNIC', # 2001:1200::/23 + '00100000000000010001010' : 'ALLOCATED RIPE NCC', # 2001:1400::/23 + '00100000000000010001011' : 'ALLOCATED RIPE NCC', # 2001:1600::/23 + '00100000000000010001100' : 'ALLOCATED ARIN', # 2001:1800::/23 + '00100000000000010001101' : 'ALLOCATED RIPE NCC', # 2001:1a00::/23 + '0010000000000001000111' : 'ALLOCATED RIPE NCC', # 2001:1c00::/22 + '00100000000000010010' : 'ALLOCATED RIPE NCC', # 2001:2000::/20 + '001000000000000100110' : 'ALLOCATED RIPE NCC', # 2001:3000::/21 + '0010000000000001001110' : 'ALLOCATED RIPE NCC', # 2001:3800::/22 + '0010000000000001001111' : 'RESERVED', # 2001:3c00::/22 Possible future allocation to RIPE NCC + '00100000000000010100000' : 'ALLOCATED RIPE NCC', # 2001:4000::/23 + '00100000000000010100001' : 'ALLOCATED AFRINIC', # 2001:4200::/23 + '00100000000000010100010' : 'ALLOCATED APNIC', # 2001:4400::/23 + '00100000000000010100011' : 'ALLOCATED RIPE NCC', # 2001:4600::/23 + '00100000000000010100100' : 'ALLOCATED ARIN', # 2001:4800::/23 + '00100000000000010100101' : 'ALLOCATED RIPE NCC', # 2001:4a00::/23 + '00100000000000010100110' : 'ALLOCATED RIPE NCC', # 2001:4c00::/23 + '00100000000000010101' : 'ALLOCATED RIPE NCC', # 2001:5000::/20 + '0010000000000001100' : 'ALLOCATED APNIC', # 2001:8000::/19 + '00100000000000011010' : 'ALLOCATED APNIC', # 2001:a000::/20 + '00100000000000011011' : 'ALLOCATED APNIC', # 2001:b000::/20 + '0010000000000010' : '6TO4', # 2002::/16 "6to4" [RFC3056] + '001000000000001100' : 'ALLOCATED RIPE NCC', # 2003::/18 + '001001000000' : 'ALLOCATED APNIC', # 2400::/12 + '001001100000' : 'ALLOCATED ARIN', # 2600::/12 + '00100110000100000000000' : 'ALLOCATED ARIN', # 2610::/23 + '00100110001000000000000' : 'ALLOCATED ARIN', # 2620::/23 + '001010000000' : 'ALLOCATED LACNIC', # 2800::/12 + '001010100000' : 'ALLOCATED RIPE NCC', # 2a00::/12 + '001011000000' : 'ALLOCATED AFRINIC', # 2c00::/12 + '00101101' : 'RESERVED', # 2d00::/8 + '0010111' : 'RESERVED', # 2e00::/7 + '0011' : 'RESERVED', # 3000::/4 + '010' : 'RESERVED', # 4000::/3 + '011' : 'RESERVED', # 6000::/3 + '100' : 'RESERVED', # 8000::/3 + '101' : 'RESERVED', # a000::/3 + '110' : 'RESERVED', # c000::/3 + '1110' : 'RESERVED', # e000::/4 + '11110' : 'RESERVED', # f000::/5 + '111110' : 'RESERVED', # f800::/6 + '1111110' : 'ULA', # fc00::/7 [RFC4193] + '111111100' : 'RESERVED', # fe00::/9 + '1111111010' : 'LINKLOCAL', # fe80::/10 + '1111111011' : 'RESERVED', # fec0::/10 Formerly SITELOCAL [RFC4291] + '11111111' : 'MULTICAST', # ff00::/8 + '1111111100000001' : 'NODE-LOCAL MULTICAST', # ff01::/16 + '1111111100000010' : 'LINK-LOCAL MULTICAST', # ff02::/16 + '1111111100000100' : 'ADMIN-LOCAL MULTICAST', # ff04::/16 + '1111111100000101' : 'SITE-LOCAL MULTICAST', # ff05::/16 + '1111111100001000' : 'ORG-LOCAL MULTICAST', # ff08::/16 + '1111111100001110' : 'GLOBAL MULTICAST', # ff0e::/16 + '1111111100001111' : 'RESERVED MULTICAST', # ff0f::/16 + '111111110011' : 'PREFIX-BASED MULTICAST', # ff30::/12 [RFC3306] + '111111110111' : 'RP-EMBEDDED MULTICAST', # ff70::/12 [RFC3956] + } + +MAX_IPV4_ADDRESS = 0xffffffff +MAX_IPV6_ADDRESS = 0xffffffffffffffffffffffffffffffff +IPV6_TEST_MAP = 0xffffffffffffffffffffffff00000000 +IPV6_MAP_MASK = 0x00000000000000000000ffff00000000 + +if sys.version_info >= (3,): + INT_TYPES = (int,) + STR_TYPES = (str,) + xrange = range +else: + INT_TYPES = (int, long) + STR_TYPES = (str, unicode) + + +class IPint(object): + """Handling of IP addresses returning integers. + + Use class IP instead because some features are not implemented for + IPint.""" + + def __init__(self, data, ipversion=0, make_net=0): + """Create an instance of an IP object. + + Data can be a network specification or a single IP. IP + addresses can be specified in all forms understood by + parseAddress(). The size of a network can be specified as + + /prefixlen a.b.c.0/24 2001:658:22a:cafe::/64 + -lastIP a.b.c.0-a.b.c.255 2001:658:22a:cafe::-2001:658:22a:cafe:ffff:ffff:ffff:ffff + /decimal netmask a.b.c.d/255.255.255.0 not supported for IPv6 + + If no size specification is given a size of 1 address (/32 for + IPv4 and /128 for IPv6) is assumed. + + If make_net is True, an IP address will be transformed into the network + address by applying the specified netmask. + + >>> print(IP('127.0.0.0/8')) + 127.0.0.0/8 + >>> print(IP('127.0.0.0/255.0.0.0')) + 127.0.0.0/8 + >>> print(IP('127.0.0.0-127.255.255.255')) + 127.0.0.0/8 + >>> print(IP('127.0.0.1/255.0.0.0', make_net=True)) + 127.0.0.0/8 + + See module documentation for more examples. + """ + + # Print no Prefixlen for /32 and /128 + self.NoPrefixForSingleIp = 1 + + # Do we want prefix printed by default? see _printPrefix() + self.WantPrefixLen = None + + netbits = 0 + prefixlen = -1 + + # handling of non string values in constructor + if isinstance(data, INT_TYPES): + self.ip = int(data) + if ipversion == 0: + if self.ip <= MAX_IPV4_ADDRESS: + ipversion = 4 + else: + ipversion = 6 + if ipversion == 4: + if self.ip > MAX_IPV4_ADDRESS: + raise ValueError("IPv4 Address can't be larger than %x: %x" % (MAX_IPV4_ADDRESS, self.ip)) + prefixlen = 32 + elif ipversion == 6: + if self.ip > MAX_IPV6_ADDRESS: + raise ValueError("IPv6 Address can't be larger than %x: %x" % (MAX_IPV6_ADDRESS, self.ip)) + prefixlen = 128 + else: + raise ValueError("only IPv4 and IPv6 supported") + self._ipversion = ipversion + self._prefixlen = prefixlen + # handle IP instance as an parameter + elif isinstance(data, IPint): + self._ipversion = data._ipversion + self._prefixlen = data._prefixlen + self.ip = data.ip + elif isinstance(data, STR_TYPES): + # TODO: refactor me! + # splitting of a string into IP and prefixlen et. al. + x = data.split('-') + if len(x) == 2: + # a.b.c.0-a.b.c.255 specification ? + (ip, last) = x + (self.ip, parsedVersion) = parseAddress(ip) + if parsedVersion != 4: + raise ValueError("first-last notation only allowed for IPv4") + (last, lastversion) = parseAddress(last) + if lastversion != 4: + raise ValueError("last address should be IPv4, too") + if last < self.ip: + raise ValueError("last address should be larger than first") + size = last - self.ip + netbits = _count1Bits(size) + # make sure the broadcast is the same as the last ip + # otherwise it will return /16 for something like: + # 192.168.0.0-192.168.191.255 + if IP('%s/%s' % (ip, 32-netbits)).broadcast().int() != last: + raise ValueError("the range %s is not on a network boundary." % data) + elif len(x) == 1: + x = data.split('/') + # if no prefix is given use defaults + if len(x) == 1: + ip = x[0] + prefixlen = -1 + elif len(x) > 2: + raise ValueError("only one '/' allowed in IP Address") + else: + (ip, prefixlen) = x + if prefixlen.find('.') != -1: + # check if the user might have used a netmask like + # a.b.c.d/255.255.255.0 + (netmask, vers) = parseAddress(prefixlen) + if vers != 4: + raise ValueError("netmask must be IPv4") + prefixlen = _netmaskToPrefixlen(netmask) + elif len(x) > 2: + raise ValueError("only one '-' allowed in IP Address") + else: + raise ValueError("can't parse") + + (self.ip, parsedVersion) = parseAddress(ip) + if ipversion == 0: + ipversion = parsedVersion + if prefixlen == -1: + bits = _ipVersionToLen(ipversion) + prefixlen = bits - netbits + self._ipversion = ipversion + self._prefixlen = int(prefixlen) + + if make_net: + self.ip = self.ip & _prefixlenToNetmask(self._prefixlen, self._ipversion) + + if not _checkNetaddrWorksWithPrefixlen(self.ip, + self._prefixlen, self._ipversion): + raise ValueError("%s has invalid prefix length (%s)" % (repr(self), self._prefixlen)) + else: + raise TypeError("Unsupported data type: %s" % type(data)) + + def int(self): + """Return the first / base / network addess as an (long) integer. + + The same as IP[0]. + + >>> "%X" % IP('10.0.0.0/8').int() + 'A000000' + """ + return self.ip + + def version(self): + """Return the IP version of this Object. + + >>> IP('10.0.0.0/8').version() + 4 + >>> IP('::1').version() + 6 + """ + return self._ipversion + + def prefixlen(self): + """Returns Network Prefixlen. + + >>> IP('10.0.0.0/8').prefixlen() + 8 + """ + return self._prefixlen + + def net(self): + """ + Return the base (first) address of a network as an (long) integer. + """ + return self.int() + + def broadcast(self): + """ + Return the broadcast (last) address of a network as an (long) integer. + + The same as IP[-1].""" + return self.int() + self.len() - 1 + + def _printPrefix(self, want): + """Prints Prefixlen/Netmask. + + Not really. In fact it is our universal Netmask/Prefixlen printer. + This is considered an internal function. + + want == 0 / None don't return anything 1.2.3.0 + want == 1 /prefix 1.2.3.0/24 + want == 2 /netmask 1.2.3.0/255.255.255.0 + want == 3 -lastip 1.2.3.0-1.2.3.255 + """ + + if (self._ipversion == 4 and self._prefixlen == 32) or \ + (self._ipversion == 6 and self._prefixlen == 128): + if self.NoPrefixForSingleIp: + want = 0 + if want == None: + want = self.WantPrefixLen + if want == None: + want = 1 + if want: + if want == 2: + # this should work with IP and IPint + netmask = self.netmask() + if not isinstance(netmask, INT_TYPES): + netmask = netmask.int() + return "/%s" % (intToIp(netmask, self._ipversion)) + elif want == 3: + return "-%s" % (intToIp(self.ip + self.len() - 1, self._ipversion)) + else: + # default + return "/%d" % (self._prefixlen) + else: + return '' + + # We have different flavours to convert to: + # strFullsize 127.0.0.1 2001:0658:022a:cafe:0200:c0ff:fe8d:08fa + # strNormal 127.0.0.1 2001:658:22a:cafe:200:c0ff:fe8d:08fa + # strCompressed 127.0.0.1 2001:658:22a:cafe::1 + # strHex 0x7F000001 0x20010658022ACAFE0200C0FFFE8D08FA + # strDec 2130706433 42540616829182469433547974687817795834 + + def strBin(self, wantprefixlen = None): + """Return a string representation as a binary value. + + >>> print(IP('127.0.0.1').strBin()) + 01111111000000000000000000000001 + >>> print(IP('2001:0658:022a:cafe:0200::1').strBin()) + 00100000000000010000011001011000000000100010101011001010111111100000001000000000000000000000000000000000000000000000000000000001 + """ + + bits = _ipVersionToLen(self._ipversion) + if self.WantPrefixLen == None and wantprefixlen == None: + wantprefixlen = 0 + ret = _intToBin(self.ip) + return '0' * (bits - len(ret)) + ret + self._printPrefix(wantprefixlen) + + def strCompressed(self, wantprefixlen = None): + """Return a string representation in compressed format using '::' Notation. + + >>> IP('127.0.0.1').strCompressed() + '127.0.0.1' + >>> IP('2001:0658:022a:cafe:0200::1').strCompressed() + '2001:658:22a:cafe:200::1' + >>> IP('ffff:ffff:ffff:ffff:ffff:f:f:fffc/127').strCompressed() + 'ffff:ffff:ffff:ffff:ffff:f:f:fffc/127' + """ + + if self.WantPrefixLen == None and wantprefixlen == None: + wantprefixlen = 1 + + if self._ipversion == 4: + return self.strFullsize(wantprefixlen) + else: + if self.ip >> 32 == 0xffff: + ipv4 = intToIp(self.ip & MAX_IPV4_ADDRESS, 4) + text = "::ffff:" + ipv4 + self._printPrefix(wantprefixlen) + return text + # find the longest sequence of '0' + hextets = [int(x, 16) for x in self.strFullsize(0).split(':')] + # every element of followingzeros will contain the number of zeros + # following the corresponding element of hextets + followingzeros = [0] * 8 + for i in xrange(len(hextets)): + followingzeros[i] = _countFollowingZeros(hextets[i:]) + # compressionpos is the position where we can start removing zeros + compressionpos = followingzeros.index(max(followingzeros)) + if max(followingzeros) > 1: + # genererate string with the longest number of zeros cut out + # now we need hextets as strings + hextets = [x for x in self.strNormal(0).split(':')] + while compressionpos < len(hextets) and hextets[compressionpos] == '0': + del(hextets[compressionpos]) + hextets.insert(compressionpos, '') + if compressionpos + 1 >= len(hextets): + hextets.append('') + if compressionpos == 0: + hextets = [''] + hextets + return ':'.join(hextets) + self._printPrefix(wantprefixlen) + else: + return self.strNormal(0) + self._printPrefix(wantprefixlen) + + def strNormal(self, wantprefixlen = None): + """Return a string representation in the usual format. + + >>> print(IP('127.0.0.1').strNormal()) + 127.0.0.1 + >>> print(IP('2001:0658:022a:cafe:0200::1').strNormal()) + 2001:658:22a:cafe:200:0:0:1 + """ + + if self.WantPrefixLen == None and wantprefixlen == None: + wantprefixlen = 1 + + if self._ipversion == 4: + ret = self.strFullsize(0) + elif self._ipversion == 6: + ret = ':'.join(["%x" % x for x in [int(x, 16) for x in self.strFullsize(0).split(':')]]) + else: + raise ValueError("only IPv4 and IPv6 supported") + + + + return ret + self._printPrefix(wantprefixlen) + + def strFullsize(self, wantprefixlen = None): + """Return a string representation in the non-mangled format. + + >>> print(IP('127.0.0.1').strFullsize()) + 127.0.0.1 + >>> print(IP('2001:0658:022a:cafe:0200::1').strFullsize()) + 2001:0658:022a:cafe:0200:0000:0000:0001 + """ + + if self.WantPrefixLen == None and wantprefixlen == None: + wantprefixlen = 1 + + return intToIp(self.ip, self._ipversion) + self._printPrefix(wantprefixlen) + + def strHex(self, wantprefixlen = None): + """Return a string representation in hex format in lower case. + + >>> print(IP('127.0.0.1').strHex()) + 0x7f000001 + >>> print(IP('2001:0658:022a:cafe:0200::1').strHex()) + 0x20010658022acafe0200000000000001 + """ + + if self.WantPrefixLen == None and wantprefixlen == None: + wantprefixlen = 0 + + x = '0x%x' % self.ip + return x + self._printPrefix(wantprefixlen) + + def strDec(self, wantprefixlen = None): + """Return a string representation in decimal format. + + >>> print(IP('127.0.0.1').strDec()) + 2130706433 + >>> print(IP('2001:0658:022a:cafe:0200::1').strDec()) + 42540616829182469433547762482097946625 + """ + + if self.WantPrefixLen == None and wantprefixlen == None: + wantprefixlen = 0 + + x = '%d' % self.ip + return x + self._printPrefix(wantprefixlen) + + def iptype(self): + """Return a description of the IP type ('PRIVATE', 'RESERVED', etc). + + >>> print(IP('127.0.0.1').iptype()) + PRIVATE + >>> print(IP('192.168.1.1').iptype()) + PRIVATE + >>> print(IP('195.185.1.2').iptype()) + PUBLIC + >>> print(IP('::1').iptype()) + LOOPBACK + >>> print(IP('2001:0658:022a:cafe:0200::1').iptype()) + ALLOCATED RIPE NCC + + The type information for IPv6 is out of sync with reality. + """ + + # this could be greatly improved + + if self._ipversion == 4: + iprange = IPv4ranges + elif self._ipversion == 6: + iprange = IPv6ranges + else: + raise ValueError("only IPv4 and IPv6 supported") + + bits = self.strBin() + for i in xrange(len(bits), 0, -1): + if bits[:i] in iprange: + return iprange[bits[:i]] + return "unknown" + + + def netmask(self): + """Return netmask as an integer. + + >>> "%X" % IP('195.185.0.0/16').netmask().int() + 'FFFF0000' + """ + + # TODO: unify with prefixlenToNetmask? + bits = _ipVersionToLen(self._ipversion) + locallen = bits - self._prefixlen + + return ((2 ** self._prefixlen) - 1) << locallen + + + def strNetmask(self): + """Return netmask as an string. Mostly useful for IPv6. + + >>> print(IP('195.185.0.0/16').strNetmask()) + 255.255.0.0 + >>> print(IP('2001:0658:022a:cafe::0/64').strNetmask()) + /64 + """ + + # TODO: unify with prefixlenToNetmask? + # Note: call to _ipVersionToLen() also validates version is 4 or 6 + bits = _ipVersionToLen(self._ipversion) + if self._ipversion == 4: + locallen = bits - self._prefixlen + return intToIp(((2 ** self._prefixlen) - 1) << locallen, 4) + elif self._ipversion == 6: + return "/%d" % self._prefixlen + + def len(self): + """Return the length of a subnet. + + >>> print(IP('195.185.1.0/28').len()) + 16 + >>> print(IP('195.185.1.0/24').len()) + 256 + """ + + bits = _ipVersionToLen(self._ipversion) + locallen = bits - self._prefixlen + return 2 ** locallen + + + def __nonzero__(self): + """All IPy objects should evaluate to true in boolean context. + Ordinarily they do, but if handling a default route expressed as + 0.0.0.0/0, the __len__() of the object becomes 0, which is used + as the boolean value of the object. + """ + return True + + + def __len__(self): + """ + Return the length of a subnet. + + Called to implement the built-in function len(). + It will break with large IPv6 Networks. + Use the object's len() instead. + """ + return self.len() + + def __add__(self, other): + """Emulate numeric objects through network aggregation""" + if self._ipversion != other._ipversion: + raise ValueError("Only networks with the same IP version can be added.") + if self._prefixlen != other._prefixlen: + raise ValueError("Only networks with the same prefixlen can be added.") + if self._prefixlen < 1: + raise ValueError("Networks with a prefixlen longer than /1 can't be added.") + if self > other: + # fixed by Skinny Puppy + return other.__add__(self) + if other.int() - self[-1].int() != 1: + raise ValueError("Only adjacent networks can be added together.") + ret = IP(self.int(), ipversion=self._ipversion) + ret._prefixlen = self.prefixlen() - 1 + if not _checkNetaddrWorksWithPrefixlen(ret.ip, ret._prefixlen, + ret._ipversion): + raise ValueError("The resulting %s has invalid prefix length (%s)" + % (repr(ret), ret._prefixlen)) + return ret + + def __sub__(self, other): + """Return the prefixes that are in this IP but not in the other""" + return _remove_subprefix(self, other) + + def __getitem__(self, key): + """Called to implement evaluation of self[key]. + + >>> ip=IP('127.0.0.0/30') + >>> for x in ip: + ... print(repr(x)) + ... + IP('127.0.0.0') + IP('127.0.0.1') + IP('127.0.0.2') + IP('127.0.0.3') + >>> ip[2] + IP('127.0.0.2') + >>> ip[-1] + IP('127.0.0.3') + """ + + if isinstance(key, slice): + return [self.ip + int(x) for x in xrange(*key.indices(len(self)))] + if not isinstance(key, INT_TYPES): + raise TypeError + if key < 0: + if abs(key) <= self.len(): + key = self.len() - abs(key) + else: + raise IndexError + else: + if key >= self.len(): + raise IndexError + + return self.ip + int(key) + + + + def __contains__(self, item): + """Called to implement membership test operators. + + Should return true if item is in self, false otherwise. Item + can be other IP-objects, strings or ints. + + >>> IP('195.185.1.1').strHex() + '0xc3b90101' + >>> 0xC3B90101 in IP('195.185.1.0/24') + True + >>> '127.0.0.1' in IP('127.0.0.0/24') + True + >>> IP('127.0.0.0/24') in IP('127.0.0.0/25') + False + """ + + if isinstance(item, IP): + if item._ipversion != self._ipversion: + return False + else: + item = IP(item) + if item.ip >= self.ip and item.ip < self.ip + self.len() - item.len() + 1: + return True + else: + return False + + + def overlaps(self, item): + """Check if two IP address ranges overlap. + + Returns 0 if the two ranges don't overlap, 1 if the given + range overlaps at the end and -1 if it does at the beginning. + + >>> IP('192.168.0.0/23').overlaps('192.168.1.0/24') + 1 + >>> IP('192.168.0.0/23').overlaps('192.168.1.255') + 1 + >>> IP('192.168.0.0/23').overlaps('192.168.2.0') + 0 + >>> IP('192.168.1.0/24').overlaps('192.168.0.0/23') + -1 + """ + + if not isinstance(item, IP): + item = IP(item) + if item.ip >= self.ip and item.ip < self.ip + self.len(): + return 1 + elif self.ip >= item.ip and self.ip < item.ip + item.len(): + return -1 + else: + return 0 + + + def __str__(self): + """Dispatch to the prefered String Representation. + + Used to implement str(IP).""" + + return self.strCompressed() + + + def __repr__(self): + """Print a representation of the Object. + + Used to implement repr(IP). Returns a string which evaluates + to an identical Object (without the wantprefixlen stuff - see + module docstring. + + >>> print(repr(IP('10.0.0.0/24'))) + IP('10.0.0.0/24') + """ + + return("IPint('%s')" % (self.strCompressed(1))) + + + def __cmp__(self, other): + """Called by comparison operations. + + Should return a negative integer if self < other, zero if self + == other, a positive integer if self > other. + + Order is first determined by the address family. IPv4 addresses + are always smaller than IPv6 addresses: + + >>> IP('10.0.0.0') < IP('2001:db8::') + 1 + + Then the first address is compared. Lower addresses are + always smaller: + + >>> IP('10.0.0.0') > IP('10.0.0.1') + 0 + >>> IP('10.0.0.0/24') > IP('10.0.0.1') + 0 + >>> IP('10.0.1.0') > IP('10.0.0.0/24') + 1 + >>> IP('10.0.1.0/24') > IP('10.0.0.0/24') + 1 + >>> IP('10.0.1.0/24') > IP('10.0.0.0') + 1 + + Then the prefix length is compared. Shorter prefixes are + considered smaller than longer prefixes: + + >>> IP('10.0.0.0/24') > IP('10.0.0.0') + 0 + >>> IP('10.0.0.0/24') > IP('10.0.0.0/25') + 0 + >>> IP('10.0.0.0/24') > IP('10.0.0.0/23') + 1 + + """ + if not isinstance(other, IPint): + raise TypeError + + # Lower version -> lower result + if self._ipversion != other._ipversion: + return self._ipversion < other._ipversion and -1 or 1 + + # Lower start address -> lower result + if self.ip != other.ip: + return self.ip < other.ip and -1 or 1 + + # Shorter prefix length -> lower result + if self._prefixlen != other._prefixlen: + return self._prefixlen < other._prefixlen and -1 or 1 + + # No differences found + return 0 + + def __eq__(self, other): + if not isinstance(other, IPint): + return False + return self.__cmp__(other) == 0 + + def __ne__(self, other): + return not self.__eq__(other) + + def __lt__(self, other): + return self.__cmp__(other) < 0 + + def __hash__(self): + """Called for the key object for dictionary operations, and by + the built-in function hash(). Should return a 32-bit integer + usable as a hash value for dictionary operations. The only + required property is that objects which compare equal have the + same hash value + + >>> IP('10.0.0.0/24').__hash__() + -167772185 + """ + + thehash = int(-1) + ip = self.ip + while ip > 0: + thehash = thehash ^ (ip & 0x7fffffff) + ip = ip >> 32 + thehash = thehash ^ self._prefixlen + return int(thehash) + + +class IP(IPint): + """Class for handling IP addresses and networks.""" + + def net(self): + """Return the base (first) address of a network as an IP object. + + The same as IP[0]. + + >>> IP('10.0.0.0/8').net() + IP('10.0.0.0') + """ + return IP(IPint.net(self), ipversion=self._ipversion) + + def broadcast(self): + """Return the broadcast (last) address of a network as an IP object. + + The same as IP[-1]. + + >>> IP('10.0.0.0/8').broadcast() + IP('10.255.255.255') + """ + return IP(IPint.broadcast(self)) + + def netmask(self): + """Return netmask as an IP object. + + >>> IP('10.0.0.0/8').netmask() + IP('255.0.0.0') + """ + return IP(IPint.netmask(self), ipversion=self._ipversion) + + def _getIPv4Map(self): + if self._ipversion != 6: + return None + if (self.ip >> 32) != 0xffff: + return None + ipv4 = self.ip & MAX_IPV4_ADDRESS + if self._prefixlen != 128: + ipv4 = '%s/%s' % (ipv4, 32-(128-self._prefixlen)) + return IP(ipv4, ipversion=4) + + def reverseNames(self): + """Return a list with values forming the reverse lookup. + + >>> IP('213.221.113.87/32').reverseNames() + ['87.113.221.213.in-addr.arpa.'] + >>> IP('213.221.112.224/30').reverseNames() + ['224.112.221.213.in-addr.arpa.', '225.112.221.213.in-addr.arpa.', '226.112.221.213.in-addr.arpa.', '227.112.221.213.in-addr.arpa.'] + >>> IP('127.0.0.0/24').reverseNames() + ['0.0.127.in-addr.arpa.'] + >>> IP('127.0.0.0/23').reverseNames() + ['0.0.127.in-addr.arpa.', '1.0.127.in-addr.arpa.'] + >>> IP('127.0.0.0/16').reverseNames() + ['0.127.in-addr.arpa.'] + >>> IP('127.0.0.0/15').reverseNames() + ['0.127.in-addr.arpa.', '1.127.in-addr.arpa.'] + >>> IP('128.0.0.0/8').reverseNames() + ['128.in-addr.arpa.'] + >>> IP('128.0.0.0/7').reverseNames() + ['128.in-addr.arpa.', '129.in-addr.arpa.'] + >>> IP('::1:2').reverseNames() + ['2.0.0.0.1.ip6.arpa.'] + """ + + if self._ipversion == 4: + ret = [] + # TODO: Refactor. Add support for IPint objects + if self.len() < 2**8: + for x in self: + ret.append(x.reverseName()) + elif self.len() < 2**16: + for i in xrange(0, self.len(), 2**8): + ret.append(self[i].reverseName()[2:]) + elif self.len() < 2**24: + for i in xrange(0, self.len(), 2**16): + ret.append(self[i].reverseName()[4:]) + else: + for i in xrange(0, self.len(), 2**24): + ret.append(self[i].reverseName()[6:]) + return ret + elif self._ipversion == 6: + ipv4 = self._getIPv4Map() + if ipv4 is not None: + return ipv4.reverseNames() + s = "%x" % self.ip + if self._prefixlen % 4 != 0: + raise NotImplementedError("can't create IPv6 reverse names at sub nibble level") + s = list(s) + s.reverse() + s = '.'.join(s) + first_nibble_index = int(32 - (self._prefixlen // 4)) * 2 + return ["%s.ip6.arpa." % s[first_nibble_index:]] + else: + raise ValueError("only IPv4 and IPv6 supported") + + def reverseName(self): + """Return the value for reverse lookup/PTR records as RFC 2317 look alike. + + RFC 2317 is an ugly hack which only works for sub-/24 e.g. not + for /23. Do not use it. Better set up a zone for every + address. See reverseName for a way to achieve that. + + >>> print(IP('195.185.1.1').reverseName()) + 1.1.185.195.in-addr.arpa. + >>> print(IP('195.185.1.0/28').reverseName()) + 0-15.1.185.195.in-addr.arpa. + >>> IP('::1:2').reverseName() + '2.0.0.0.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.' + >>> IP('ff02::/64').reverseName() + '0.0.0.0.0.0.0.0.0.0.0.0.2.0.f.f.ip6.arpa.' + """ + + if self._ipversion == 4: + s = self.strFullsize(0) + s = s.split('.') + s.reverse() + first_byte_index = int(4 - (self._prefixlen // 8)) + if self._prefixlen % 8 != 0: + nibblepart = "%s-%s" % (s[3-(self._prefixlen // 8)], intToIp(self.ip + self.len() - 1, 4).split('.')[-1]) + nibblepart += '.' + else: + nibblepart = "" + + s = '.'.join(s[first_byte_index:]) + return "%s%s.in-addr.arpa." % (nibblepart, s) + + elif self._ipversion == 6: + ipv4 = self._getIPv4Map() + if ipv4 is not None: + return ipv4.reverseName() + s = '%032x' % self.ip + if self._prefixlen % 4 != 0: + nibblepart = "%s-%x" % (s[self._prefixlen:], self.ip + self.len() - 1) + nibblepart += '.' + else: + nibblepart = "" + s = list(s) + s.reverse() + s = '.'.join(s) + first_nibble_index = int(32 - (self._prefixlen // 4)) * 2 + return "%s%s.ip6.arpa." % (nibblepart, s[first_nibble_index:]) + else: + raise ValueError("only IPv4 and IPv6 supported") + + def make_net(self, netmask): + """Transform a single IP address into a network specification by + applying the given netmask. + + Returns a new IP instance. + + >>> print(IP('127.0.0.1').make_net('255.0.0.0')) + 127.0.0.0/8 + """ + if '/' in str(netmask): + raise ValueError("invalid netmask (%s)" % netmask) + return IP('%s/%s' % (self, netmask), make_net=True) + + def __getitem__(self, key): + """Called to implement evaluation of self[key]. + + >>> ip=IP('127.0.0.0/30') + >>> for x in ip: + ... print(str(x)) + ... + 127.0.0.0 + 127.0.0.1 + 127.0.0.2 + 127.0.0.3 + >>> print(str(ip[2])) + 127.0.0.2 + >>> print(str(ip[-1])) + 127.0.0.3 + """ + if isinstance(key, slice): + return [IP(IPint.__getitem__(self, x), ipversion=self._ipversion) for x in xrange(*key.indices(len(self)))] + return IP(IPint.__getitem__(self, key), ipversion=self._ipversion) + + def __repr__(self): + """Print a representation of the Object. + + >>> IP('10.0.0.0/8') + IP('10.0.0.0/8') + """ + + return("IP('%s')" % (self.strCompressed(1))) + + def get_mac(self): + """ + Get the 802.3 MAC address from IPv6 RFC 2464 address, in lower case. + Return None if the address is an IPv4 or not a IPv6 RFC 2464 address. + + >>> IP('fe80::f66d:04ff:fe47:2fae').get_mac() + 'f4:6d:04:47:2f:ae' + """ + if self._ipversion != 6: + return None + if (self.ip & 0x20000ffff000000) != 0x20000fffe000000: + return None + return '%02x:%02x:%02x:%02x:%02x:%02x' % ( + (((self.ip >> 56) & 0xff) & 0xfd), + (self.ip >> 48) & 0xff, + (self.ip >> 40) & 0xff, + (self.ip >> 16) & 0xff, + (self.ip >> 8) & 0xff, + self.ip & 0xff, + ) + + def v46map(self): + """ + Returns the IPv6 mapped address of an IPv4 address, or the corresponding + IPv4 address if the IPv6 address is in the appropriate range. + Raises a ValueError if the IPv6 address is not translatable. See RFC 4291. + + >>> IP('192.168.1.1').v46map() + IP('::ffff:192.168.1.1') + >>> IP('::ffff:192.168.1.1').v46map() + IP('192.168.1.1') + """ + if self._ipversion == 4: + return IP(str(IPV6_MAP_MASK + self.ip) + + "/%s" % (self._prefixlen + 96)) + else: + if self.ip & IPV6_TEST_MAP == IPV6_MAP_MASK: + return IP(str(self.ip - IPV6_MAP_MASK) + + "/%s" % (self._prefixlen - 96)) + raise ValueError("%s cannot be converted to an IPv4 address." + % repr(self)) + +class IPSet(collections.MutableSet): + def __init__(self, iterable=[]): + # Make sure it's iterable, otherwise wrap + if not isinstance(iterable, collections.Iterable): + raise TypeError("'%s' object is not iterable" % type(iterable).__name__) + + # Make sure we only accept IP objects + for prefix in iterable: + if not isinstance(prefix, IP): + raise ValueError('Only IP objects can be added to an IPSet') + + # Store and optimize + self.prefixes = iterable[:] + self.optimize() + + def __contains__(self, ip): + valid_masks = self.prefixtable.keys() + if isinstance(ip, IP): + #Don't dig through more-specific ranges + ip_mask = ip._prefixlen + valid_masks = [x for x in valid_masks if x <= ip_mask] + for mask in sorted(valid_masks): + i = bisect.bisect(self.prefixtable[mask], ip) + # Because of sorting order, a match can only occur in the prefix + # that comes before the result of the search. + if i and ip in self.prefixtable[mask][i - 1]: + return True + + def __iter__(self): + for prefix in self.prefixes: + yield prefix + + def __len__(self): + return self.len() + + def __add__(self, other): + return IPSet(self.prefixes + other.prefixes) + + def __sub__(self, other): + new = IPSet(self.prefixes) + for prefix in other: + new.discard(prefix) + return new + + def __and__(self, other): + left = iter(self.prefixes) + right = iter(other.prefixes) + result = [] + try: + l = next(left) + r = next(right) + while True: + # iterate over prefixes in order, keeping the smaller of the + # two if they overlap + if l in r: + result.append(l) + l = next(left) + continue + elif r in l: + result.append(r) + r = next(right) + continue + if l < r: + l = next(left) + else: + r = next(right) + except StopIteration: + return IPSet(result) + + def __repr__(self): + return '%s([' % self.__class__.__name__ + ', '.join(map(repr, self.prefixes)) + '])' + + def len(self): + return sum(prefix.len() for prefix in self.prefixes) + + def add(self, value): + # Make sure it's iterable, otherwise wrap + if not isinstance(value, collections.Iterable): + value = [value] + + # Check type + for prefix in value: + if not isinstance(prefix, IP): + raise ValueError('Only IP objects can be added to an IPSet') + + # Append and optimize + self.prefixes.extend(value) + self.optimize() + + def discard(self, value): + # Make sure it's iterable, otherwise wrap + if not isinstance(value, collections.Iterable): + value = [value] + + # This is much faster than iterating over the addresses + if isinstance(value, IPSet): + value = value.prefixes + + # Remove + for del_prefix in value: + if not isinstance(del_prefix, IP): + raise ValueError('Only IP objects can be removed from an IPSet') + + # First check if this prefix contains anything in our list + found = False + d = 0 + for i in range(len(self.prefixes)): + if self.prefixes[i - d] in del_prefix: + self.prefixes.pop(i - d) + d = d + 1 + found = True + + if found: + # If the prefix was bigger than an existing prefix, then it's + # certainly not a subset of one, so skip the rest + continue + + # Maybe one of our prefixes contains this prefix + found = False + for i in range(len(self.prefixes)): + if del_prefix in self.prefixes[i]: + self.prefixes[i:i+1] = self.prefixes[i] - del_prefix + break + + self.optimize() + + def isdisjoint(self, other): + left = iter(self.prefixes) + right = iter(other.prefixes) + try: + l = next(left) + r = next(right) + while True: + if l in r or r in l: + return False + if l < r: + l = next(left) + else: + r = next(right) + except StopIteration: + return True + + def optimize(self): + # The algorithm below *depends* on the sort order + self.prefixes.sort() + + # First eliminate all values that are a subset of other values + addrlen = len(self.prefixes) + i = 0 + while i < addrlen: + # Everything that might be inside this prefix follows + # directly behind it + j = i+1 + while j < addrlen and self.prefixes[j] in self.prefixes[i]: + # Mark for deletion by overwriting with None + self.prefixes[j] = None + j += 1 + + # Continue where we left off + i = j + + # Try to merge as many prefixes as possible + run_again = True + while run_again: + # Filter None values. This happens when a subset is eliminated + # above, or when two prefixes are merged below + self.prefixes = [a for a in self.prefixes if a is not None] + + # We'll set run_again to True when we make changes that require + # re-evaluation of the whole list + run_again = False + + # We can merge two prefixes that have the same version, same + # prefix length and differ only on the last bit of the prefix + addrlen = len(self.prefixes) + i = 0 + while i < addrlen-1: + j = i + 1 + + try: + # The next line will throw an exception when merging + # is not possible + self.prefixes[i] += self.prefixes[j] + self.prefixes[j] = None + i = j + 1 + run_again = True + except ValueError: + # Can't be merged, see if position j can be merged + i = j + + # O(n) insertion now by prefix means faster searching on __contains__ + # when lots of ranges with the same length exist + self.prefixtable = {} + for address in self.prefixes: + try: + self.prefixtable[address._prefixlen].append(address) + except KeyError: + self.prefixtable[address._prefixlen] = [address] + +def _parseAddressIPv6(ipstr): + """ + Internal function used by parseAddress() to parse IPv6 address with ':'. + + >>> print(_parseAddressIPv6('::')) + 0 + >>> print(_parseAddressIPv6('::1')) + 1 + >>> print(_parseAddressIPv6('0:0:0:0:0:0:0:1')) + 1 + >>> print(_parseAddressIPv6('0:0:0::0:0:1')) + 1 + >>> print(_parseAddressIPv6('0:0:0:0:0:0:0:0')) + 0 + >>> print(_parseAddressIPv6('0:0:0::0:0:0')) + 0 + + >>> print(_parseAddressIPv6('FEDC:BA98:7654:3210:FEDC:BA98:7654:3210')) + 338770000845734292534325025077361652240 + >>> print(_parseAddressIPv6('1080:0000:0000:0000:0008:0800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('1080:0:0:0:8:800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('1080:0::8:800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('1080::8:800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('FF01:0:0:0:0:0:0:43')) + 338958331222012082418099330867817087043 + >>> print(_parseAddressIPv6('FF01:0:0::0:0:43')) + 338958331222012082418099330867817087043 + >>> print(_parseAddressIPv6('FF01::43')) + 338958331222012082418099330867817087043 + >>> print(_parseAddressIPv6('0:0:0:0:0:0:13.1.68.3')) + 218186755 + >>> print(_parseAddressIPv6('::13.1.68.3')) + 218186755 + >>> print(_parseAddressIPv6('0:0:0:0:0:FFFF:129.144.52.38')) + 281472855454758 + >>> print(_parseAddressIPv6('::FFFF:129.144.52.38')) + 281472855454758 + >>> print(_parseAddressIPv6('1080:0:0:0:8:800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('1080::8:800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('::1:2:3:4:5:6')) + 1208962713947218704138246 + >>> print(_parseAddressIPv6('1:2:3:4:5:6::')) + 5192455318486707404433266432802816 + """ + + # Split string into a list, example: + # '1080:200C::417A' => ['1080', '200C', '417A'] and fill_pos=2 + # and fill_pos is the position of '::' in the list + items = [] + index = 0 + fill_pos = None + while index < len(ipstr): + text = ipstr[index:] + if text.startswith("::"): + if fill_pos is not None: + # Invalid IPv6, eg. '1::2::' + raise ValueError("%r: Invalid IPv6 address: more than one '::'" % ipstr) + fill_pos = len(items) + index += 2 + continue + pos = text.find(':') + if pos == 0: + # Invalid IPv6, eg. '1::2:' + raise ValueError("%r: Invalid IPv6 address" % ipstr) + if pos != -1: + items.append(text[:pos]) + if text[pos:pos+2] == "::": + index += pos + else: + index += pos+1 + + if index == len(ipstr): + # Invalid IPv6, eg. '1::2:' + raise ValueError("%r: Invalid IPv6 address" % ipstr) + else: + items.append(text) + break + + if items and '.' in items[-1]: + # IPv6 ending with IPv4 like '::ffff:192.168.0.1' + if (fill_pos is not None) and not (fill_pos <= len(items)-1): + # Invalid IPv6: 'ffff:192.168.0.1::' + raise ValueError("%r: Invalid IPv6 address: '::' after IPv4" % ipstr) + value = parseAddress(items[-1])[0] + items = items[:-1] + ["%04x" % (value >> 16), "%04x" % (value & 0xffff)] + + # Expand fill_pos to fill with '0' + # ['1','2'] with fill_pos=1 => ['1', '0', '0', '0', '0', '0', '0', '2'] + if fill_pos is not None: + diff = 8 - len(items) + if diff <= 0: + raise ValueError("%r: Invalid IPv6 address: '::' is not needed" % ipstr) + items = items[:fill_pos] + ['0']*diff + items[fill_pos:] + + # Here we have a list of 8 strings + if len(items) != 8: + # Invalid IPv6, eg. '1:2:3' + raise ValueError("%r: Invalid IPv6 address: should have 8 hextets" % ipstr) + + # Convert strings to long integer + value = 0 + index = 0 + for item in items: + try: + item = int(item, 16) + error = not(0 <= item <= 0xffff) + except ValueError: + error = True + if error: + raise ValueError("%r: Invalid IPv6 address: invalid hexlet %r" % (ipstr, item)) + value = (value << 16) + item + index += 1 + return value + +def parseAddress(ipstr): + """ + Parse a string and return the corresponding IP address (as integer) + and a guess of the IP version. + + Following address formats are recognized: + + >>> def testParseAddress(address): + ... ip, version = parseAddress(address) + ... print(("%s (IPv%s)" % (ip, version))) + ... + >>> testParseAddress('0x0123456789abcdef') # IPv4 if <= 0xffffffff else IPv6 + 81985529216486895 (IPv6) + >>> testParseAddress('123.123.123.123') # IPv4 + 2071690107 (IPv4) + >>> testParseAddress('123.123') # 0-padded IPv4 + 2071658496 (IPv4) + >>> testParseAddress('127') + 2130706432 (IPv4) + >>> testParseAddress('255') + 4278190080 (IPv4) + >>> testParseAddress('256') + 256 (IPv4) + >>> testParseAddress('108000000000000000080800200C417A') + 21932261930451111902915077091070067066 (IPv6) + >>> testParseAddress('0x108000000000000000080800200C417A') + 21932261930451111902915077091070067066 (IPv6) + >>> testParseAddress('1080:0000:0000:0000:0008:0800:200C:417A') + 21932261930451111902915077091070067066 (IPv6) + >>> testParseAddress('1080:0:0:0:8:800:200C:417A') + 21932261930451111902915077091070067066 (IPv6) + >>> testParseAddress('1080:0::8:800:200C:417A') + 21932261930451111902915077091070067066 (IPv6) + >>> testParseAddress('::1') + 1 (IPv6) + >>> testParseAddress('::') + 0 (IPv6) + >>> testParseAddress('0:0:0:0:0:FFFF:129.144.52.38') + 281472855454758 (IPv6) + >>> testParseAddress('::13.1.68.3') + 218186755 (IPv6) + >>> testParseAddress('::FFFF:129.144.52.38') + 281472855454758 (IPv6) + """ + + try: + hexval = int(ipstr, 16) + except ValueError: + hexval = None + try: + intval = int(ipstr, 10) + except ValueError: + intval = None + + if ipstr.startswith('0x') and hexval is not None: + if hexval > MAX_IPV6_ADDRESS: + raise ValueError("IP Address can't be larger than %x: %x" % (MAX_IPV6_ADDRESS, hexval)) + if hexval <= MAX_IPV4_ADDRESS: + return (hexval, 4) + else: + return (hexval, 6) + + if ipstr.find(':') != -1: + return (_parseAddressIPv6(ipstr), 6) + + elif len(ipstr) == 32 and hexval is not None: + # assume IPv6 in pure hexadecimal notation + return (hexval, 6) + + elif ipstr.find('.') != -1 or (intval is not None and intval < 256): + # assume IPv4 ('127' gets interpreted as '127.0.0.0') + bytes = ipstr.split('.') + if len(bytes) > 4: + raise ValueError("IPv4 Address with more than 4 bytes") + bytes += ['0'] * (4 - len(bytes)) + bytes = [int(x) for x in bytes] + for x in bytes: + if x > 255 or x < 0: + raise ValueError("%r: single byte must be 0 <= byte < 256" % (ipstr)) + return ((bytes[0] << 24) + (bytes[1] << 16) + (bytes[2] << 8) + bytes[3], 4) + + elif intval is not None: + # we try to interprete it as a decimal digit - + # this ony works for numbers > 255 ... others + # will be interpreted as IPv4 first byte + if intval > MAX_IPV6_ADDRESS: + raise ValueError("IP Address can't be larger than %x: %x" % (MAX_IPV6_ADDRESS, intval)) + if intval <= MAX_IPV4_ADDRESS: + return (intval, 4) + else: + return (intval, 6) + + raise ValueError("IP Address format was invalid: %s" % ipstr) + + +def intToIp(ip, version): + """Transform an integer string into an IP address.""" + + # just to be sure and hoping for Python 2.2 + ip = int(ip) + + if ip < 0: + raise ValueError("IPs can't be negative: %d" % (ip)) + + ret = '' + if version == 4: + if ip > MAX_IPV4_ADDRESS: + raise ValueError("IPv4 Address can't be larger than %x: %x" % (MAX_IPV4_ADDRESS, ip)) + for l in xrange(4): + ret = str(ip & 0xff) + '.' + ret + ip = ip >> 8 + ret = ret[:-1] + elif version == 6: + if ip > MAX_IPV6_ADDRESS: + raise ValueError("IPv6 Address can't be larger than %x: %x" % (MAX_IPV6_ADDRESS, ip)) + l = "%032x" % ip + for x in xrange(1, 33): + ret = l[-x] + ret + if x % 4 == 0: + ret = ':' + ret + ret = ret[1:] + else: + raise ValueError("only IPv4 and IPv6 supported") + + return ret + +def _ipVersionToLen(version): + """Return number of bits in address for a certain IP version. + + >>> _ipVersionToLen(4) + 32 + >>> _ipVersionToLen(6) + 128 + >>> _ipVersionToLen(5) + Traceback (most recent call last): + File "", line 1, in ? + File "IPy.py", line 1076, in _ipVersionToLen + raise ValueError("only IPv4 and IPv6 supported") + ValueError: only IPv4 and IPv6 supported + """ + + if version == 4: + return 32 + elif version == 6: + return 128 + else: + raise ValueError("only IPv4 and IPv6 supported") + + +def _countFollowingZeros(l): + """Return number of elements containing 0 at the beginning of the list.""" + if len(l) == 0: + return 0 + elif l[0] != 0: + return 0 + else: + return 1 + _countFollowingZeros(l[1:]) + + +_BitTable = {'0': '0000', '1': '0001', '2': '0010', '3': '0011', + '4': '0100', '5': '0101', '6': '0110', '7': '0111', + '8': '1000', '9': '1001', 'a': '1010', 'b': '1011', + 'c': '1100', 'd': '1101', 'e': '1110', 'f': '1111'} + +def _intToBin(val): + """Return the binary representation of an integer as string.""" + + if val < 0: + raise ValueError("Only positive values allowed") + s = "%x" % val + ret = '' + for x in s: + ret += _BitTable[x] + # remove leading zeros + while ret[0] == '0' and len(ret) > 1: + ret = ret[1:] + return ret + +def _count1Bits(num): + """Find the highest bit set to 1 in an integer.""" + ret = 0 + while num > 0: + num = num >> 1 + ret += 1 + return ret + +def _count0Bits(num): + """Find the highest bit set to 0 in an integer.""" + + # this could be so easy if _count1Bits(~int(num)) would work as excepted + num = int(num) + if num < 0: + raise ValueError("Only positive Numbers please: %s" % (num)) + ret = 0 + while num > 0: + if num & 1 == 1: + break + num = num >> 1 + ret += 1 + return ret + + +def _checkPrefix(ip, prefixlen, version): + """Check the validity of a prefix + + Checks if the variant part of a prefix only has 0s, and the length is + correct. + + >>> _checkPrefix(0x7f000000, 24, 4) + 1 + >>> _checkPrefix(0x7f000001, 24, 4) + 0 + >>> repr(_checkPrefix(0x7f000001, -1, 4)) + 'None' + >>> repr(_checkPrefix(0x7f000001, 33, 4)) + 'None' + """ + + # TODO: unify this v4/v6/invalid code in a function + bits = _ipVersionToLen(version) + + if prefixlen < 0 or prefixlen > bits: + return None + + if ip == 0: + zbits = bits + 1 + else: + zbits = _count0Bits(ip) + if zbits < bits - prefixlen: + return 0 + else: + return 1 + + +def _checkNetmask(netmask, masklen): + """Checks if a netmask is expressable as a prefixlen.""" + + num = int(netmask) + bits = masklen + + # remove zero bits at the end + while (num & 1) == 0 and bits != 0: + num = num >> 1 + bits -= 1 + if bits == 0: + break + # now check if the rest consists only of ones + while bits > 0: + if (num & 1) == 0: + raise ValueError("Netmask 0x%x can't be expressed as an prefix." % netmask) + num = num >> 1 + bits -= 1 + + +def _checkNetaddrWorksWithPrefixlen(net, prefixlen, version): + """Check if a base addess of a network is compatible with a prefixlen""" + try: + return (net & _prefixlenToNetmask(prefixlen, version) == net) + except ValueError: + return False + + +def _netmaskToPrefixlen(netmask): + """Convert an Integer representing a netmask to a prefixlen. + + E.g. 0xffffff00 (255.255.255.0) returns 24 + """ + + netlen = _count0Bits(netmask) + masklen = _count1Bits(netmask) + _checkNetmask(netmask, masklen) + return masklen - netlen + + +def _prefixlenToNetmask(prefixlen, version): + """Return a mask of n bits as a long integer. + + From 'IP address conversion functions with the builtin socket module' + by Alex Martelli + http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66517 + """ + if prefixlen == 0: + return 0 + elif prefixlen < 0: + raise ValueError("Prefixlen must be > 0") + return ((2<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 + + + + + + + + +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 + + + + >>> 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 + + + + >>> 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") + [] + +(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 - 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 - 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: +------- + + * Paul Chakravarti (paul.chakravarti@gmail.com) + +Master Repository/Issues: +------------------------- + + * https://bitbucket.org/paulc/dnslib + (Cloned on GitHub: https://github.com/paulchakravarti/dnslib) +""" + +from dnslib.dns import * + +version = "0.9.6" + +if __name__ == '__main__': + import doctest,textwrap + doctest.testmod(optionflags=doctest.ELLIPSIS) + diff --git a/lib/dnslib/build/lib.linux-i686-2.7/dnslib/bimap.py b/lib/dnslib/build/lib.linux-i686-2.7/dnslib/bimap.py new file mode 100644 index 0000000..4cf9d37 --- /dev/null +++ b/lib/dnslib/build/lib.linux-i686-2.7/dnslib/bimap.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +""" + Bimap - bidirectional mapping between code/value +""" + +class BimapError(Exception): + pass + +class Bimap(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' + + """ + + def __init__(self,name,forward,error=KeyError): + self.name = name + self.error = error + self.forward = forward.copy() + self.reverse = dict([(v,k) for (k,v) in list(forward.items())]) + + def get(self,k,default=None): + try: + return self.forward[k] + except KeyError as e: + return default or str(k) + + def __getitem__(self,k): + try: + return self.forward[k] + except KeyError as e: + raise self.error("%s: Invalid forward lookup: [%s]" % (self.name,k)) + + def __getattr__(self,k): + try: + return self.reverse[k] + except KeyError as e: + raise self.error("%s: Invalid reverse lookup: [%s]" % (self.name,k)) + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/lib/dnslib/build/lib.linux-i686-2.7/dnslib/bit.py b/lib/dnslib/build/lib.linux-i686-2.7/dnslib/bit.py new file mode 100644 index 0000000..8a9f5fe --- /dev/null +++ b/lib/dnslib/build/lib.linux-i686-2.7/dnslib/bit.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- + +""" + Some basic bit mainpulation utilities +""" +from __future__ import print_function + +FILTER = bytearray([ (i < 32 or i > 127) and 46 or i for i in range(256) ]) + +def 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 !"#$%&' ()*+,-./ + + """ + n = 0 + left = length // 2 + right = length - left + result= [] + src = bytearray(src) + while src: + s,src = src[:length],src[length:] + l,r = s[:left],s[left:] + hexa = "%-*s" % (left*3,' '.join(["%02x"%x for x in l])) + hexb = "%-*s" % (right*3,' '.join(["%02x"%x for x in r])) + lf = l.translate(FILTER) + rf = r.translate(FILTER) + result.append("%s%04x %s %s %s %s" % (prefix, n, hexa, hexb, + lf.decode(), rf.decode())) + n += length + return "\n".join(result) + +def get_bits(data,offset,bits=1): + """ + Get specified bits from integer + + >>> bin(get_bits(0b0011100,2)) + '0b1' + >>> bin(get_bits(0b0011100,0,4)) + '0b1100' + + """ + mask = ((1 << bits) - 1) << offset + return (data & mask) >> offset + +def 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' + """ + mask = ((1 << bits) - 1) << offset + clear = 0xffff ^ mask + data = (data & clear) | ((value << offset) & mask) + return data + +def 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' + + """ + bits = [str((n >> y) & 1) for y in range(count-1, -1, -1)] + if reverse: + bits.reverse() + return "".join(bits) + +if __name__ == '__main__': + import doctest + doctest.testmod() + diff --git a/lib/dnslib/build/lib.linux-i686-2.7/dnslib/buffer.py b/lib/dnslib/build/lib.linux-i686-2.7/dnslib/buffer.py new file mode 100644 index 0000000..f8772c8 --- /dev/null +++ b/lib/dnslib/build/lib.linux-i686-2.7/dnslib/buffer.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- + +""" + Buffer - simple data buffer +""" + +import binascii,struct + +class BufferError(Exception): + pass + +class Buffer(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') + """ + + def __init__(self,data=b''): + """ + Initialise Buffer from data + """ + self.data = bytearray(data) + self.offset = 0 + + def remaining(self): + """ + Return bytes remaining + """ + return len(self.data) - self.offset + + def get(self,length): + """ + Gen len bytes at current offset (& increment offset) + """ + if length > self.remaining(): + raise BufferError("Not enough bytes [offset=%d,remaining=%d,requested=%d]" % + (self.offset,self.remaining(),length)) + start = self.offset + end = self.offset + length + self.offset += length + return bytes(self.data[start:end]) + + def hex(self): + """ + Return data as hex string + """ + return binascii.hexlify(self.data) + + def pack(self,fmt,*args): + """ + Pack data at end of data according to fmt (from struct) & increment + offset + """ + self.offset += struct.calcsize(fmt) + self.data += struct.pack(fmt,*args) + + def append(self,s): + """ + Append s to end of data & increment offset + """ + self.offset += len(s) + self.data += s + + def update(self,ptr,fmt,*args): + """ + Modify data at offset `ptr` + """ + s = struct.pack(fmt,*args) + self.data[ptr:ptr+len(s)] = s + + def unpack(self,fmt): + """ + Unpack data at current offset according to fmt (from struct) + """ + try: + data = self.get(struct.calcsize(fmt)) + return struct.unpack(fmt,data) + except struct.error as e: + raise BufferError("Error unpacking struct '%s' <%s>" % + (fmt,binascii.hexlify(data).decode())) + + def __len__(self): + return len(self.data) + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/lib/dnslib/build/lib.linux-i686-2.7/dnslib/client.py b/lib/dnslib/build/lib.linux-i686-2.7/dnslib/client.py new file mode 100644 index 0000000..9952243 --- /dev/null +++ b/lib/dnslib/build/lib.linux-i686-2.7/dnslib/client.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- + +""" + 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. +""" + +from __future__ import print_function + +try: + from subprocess import getoutput +except ImportError: + from commands import getoutput + +import binascii,code,pprint + +from dnslib.dns import DNSRecord,DNSHeader,DNSQuestion,QTYPE +from dnslib.digparser import DigParser + +if __name__ == '__main__': + + import argparse,sys,time + + p = argparse.ArgumentParser(description="DNS Client") + p.add_argument("--server","-s",default="8.8.8.8", + metavar="", + help="Server address:port (default:8.8.8.8:53) (port is optional)") + p.add_argument("--query",action='store_true',default=False, + help="Show query (default: False)") + p.add_argument("--hex",action='store_true',default=False, + help="Dump packet in hex (default: False)") + p.add_argument("--tcp",action='store_true',default=False, + help="Use TCP (default: UDP)") + p.add_argument("--noretry",action='store_true',default=False, + help="Don't retry query using TCP if truncated (default: false)") + p.add_argument("--diff",default="", + help="Compare response from alternate nameserver (format: address:port / default: false)") + p.add_argument("--dig",action='store_true',default=False, + help="Compare result with DiG - if ---diff also specified use alternative nameserver for DiG request (default: false)") + p.add_argument("--short",action='store_true',default=False, + help="Short output - rdata only (default: false)") + p.add_argument("--debug",action='store_true',default=False, + help="Drop into CLI after request (default: false)") + p.add_argument("domain",metavar="", + help="Query domain") + p.add_argument("qtype",metavar="",default="A",nargs="?", + help="Query type (default: A)") + args = p.parse_args() + + # Construct request + q = DNSRecord(q=DNSQuestion(args.domain,getattr(QTYPE,args.qtype))) + + address,_,port = args.server.partition(':') + port = int(port or 53) + + if args.query: + print(";; Sending%s:" % (" (TCP)" if args.tcp else "")) + if args.hex: + print(";; QUERY:",binascii.hexlify(q.pack()).decode()) + print(q) + print() + + a_pkt = q.send(address,port,tcp=args.tcp) + a = DNSRecord.parse(a_pkt) + + if a.header.tc and args.noretry == False: + # Truncated - retry in TCP mode + a_pkt = q.send(address,port,tcp=True) + a = DNSRecord.parse(a_pkt) + + if args.dig or args.diff: + if args.diff: + address,_,port = args.diff.partition(':') + port = int(port or 53) + + if args.dig: + dig = getoutput("dig +qr -p %d %s %s @%s" % ( + port, args.domain, args.qtype, address)) + dig_reply = list(iter(DigParser(dig))) + # DiG might have retried in TCP mode so get last q/a + q_diff = dig_reply[-2] + a_diff = dig_reply[-1] + else: + q_diff = DNSRecord(header=DNSHeader(id=q.header.id), + q=DNSQuestion(args.domain, + getattr(QTYPE,args.qtype))) + q_diff = q + diff = q_diff.send(address,port,tcp=args.tcp) + a_diff = DNSRecord.parse(diff) + if a_diff.header.tc and args.noretry == False: + diff = q_diff.send(address,port,tcp=True) + a_diff = DNSRecord.parse(diff) + + if args.short: + print(a.short()) + else: + print(";; Got answer:") + if args.hex: + print(";; RESPONSE:",binascii.hexlify(a_pkt).decode()) + if args.diff and not args.dig: + print(";; DIFF :",binascii.hexlify(diff).decode()) + print(a) + print() + + if args.dig or args.diff: + if q != q_diff: + print(";;; ERROR: Diff Question differs") + for (d1,d2) in q.diff(q_diff): + if d1: + print(";; - %s" % d1) + if d2: + print(";; + %s" % d2) + if a != a_diff: + print(";;; ERROR: Diff Response differs") + for (d1,d2) in a.diff(a_diff): + if d1: + print(";; - %s" % d1) + if d2: + print(";; + %s" % d2) + + if args.debug: + code.interact(local=locals()) + diff --git a/lib/dnslib/build/lib.linux-i686-2.7/dnslib/digparser.py b/lib/dnslib/build/lib.linux-i686-2.7/dnslib/digparser.py new file mode 100644 index 0000000..ee7c8ef --- /dev/null +++ b/lib/dnslib/build/lib.linux-i686-2.7/dnslib/digparser.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- +""" + + 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)) + --- + + + --- + + + + + + + + + + + + + + + + + + + + >>> 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)) + --- + + + --- + + + --- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + +from __future__ import print_function + +import glob,os.path,string + +from dnslib.lex import WordLexer +from dnslib.dns import (DNSRecord,DNSHeader,DNSQuestion,DNSError, + RR,RD,RDMAP,QR,RCODE,CLASS,QTYPE) + +class DigParser: + + """ + Parse Dig output + """ + + def __init__(self,dig,debug=False): + self.debug = debug + self.l = WordLexer(dig) + self.l.commentchars = ';' + self.l.nltok = ('NL',None) + self.i = iter(self.l) + + def parseHeader(self,l1,l2): + _,_,_,opcode,_,status,_,_id = l1.split() + _,flags,_ = l2.split(';') + header = DNSHeader(id=int(_id),bitmap=0) + header.opcode = getattr(QR,opcode.rstrip(',')) + header.rcode = getattr(RCODE,status.rstrip(',')) + for f in ('qr','aa','tc','rd','ra'): + if f in flags: + setattr(header,f,1) + return header + + def expect(self,expect): + t,val = next(self.i) + if t != expect: + raise ValueError("Invalid Token: %s (expecting: %s)" % (t,expect)) + return val + + def parseQuestions(self,q,dns): + for qname,qclass,qtype in q: + dns.add_question(DNSQuestion(qname, + getattr(QTYPE,qtype), + getattr(CLASS,qclass))) + + def parseAnswers(self,a,auth,ar,dns): + sect_map = {'a':'add_answer','auth':'add_auth','ar':'add_ar'} + for sect in 'a','auth','ar': + f = getattr(dns,sect_map[sect]) + for rr in locals()[sect]: + rname,ttl,rclass,rtype = rr[:4] + rdata = rr[4:] + rd = RDMAP.get(rtype,RD) + try: + if rd == RD and \ + any([ x not in string.hexdigits for x in rdata[-1]]): + # Only support hex encoded data for fallback RD + pass + else: + f(RR(rname=rname, + ttl=int(ttl), + rtype=getattr(QTYPE,rtype), + rclass=getattr(CLASS,rclass), + rdata=rd.fromZone(rdata))) + except DNSError as e: + if self.debug: + print("DNSError:",e,rr) + else: + # Skip records we dont understand + pass + + def __iter__(self): + return self.parse() + + def parse(self): + dns = None + section = None + paren = False + rr = [] + try: + while True: + tok,val = next(self.i) + if tok == 'COMMENT': + if 'Sending:' in val or 'Got answer:' in val: + if dns: + self.parseQuestions(q,dns) + self.parseAnswers(a,auth,ar,dns) + yield(dns) + dns = DNSRecord() + q,a,auth,ar = [],[],[],[] + elif val.startswith('; ->>HEADER<<-'): + self.expect('NL') + val2 = self.expect('COMMENT') + dns.header = self.parseHeader(val,val2) + elif val.startswith('; QUESTION'): + section = q + elif val.startswith('; ANSWER'): + section = a + elif val.startswith('; AUTHORITY'): + section = auth + elif val.startswith('; ADDITIONAL'): + section = ar + elif val.startswith(';') or tok[1].startswith('<<>>'): + pass + elif dns and section == q: + q.append(val.split()) + elif tok == 'ATOM': + if val == '(': + paren = True + elif val == ')': + paren = False + else: + rr.append(val) + elif tok == 'NL' and not paren and rr: + if self.debug: + print(">>",rr) + section.append(rr) + rr = [] + except StopIteration: + if rr: + self.section.append(rr) + if dns: + self.parseQuestions(q,dns) + self.parseAnswers(a,auth,ar,dns) + yield(dns) + +if __name__ == '__main__': + + import argparse,doctest,sys + + p = argparse.ArgumentParser(description="DigParser Test") + p.add_argument("--dig",action='store_true',default=False, + help="Parse DiG output (stdin)") + p.add_argument("--debug",action='store_true',default=False, + help="Debug output") + + args = p.parse_args() + + if args.dig: + l = DigParser(sys.stdin,args.debug) + for record in l: + print(repr(record)) + else: + doctest.testmod() diff --git a/lib/dnslib/build/lib.linux-i686-2.7/dnslib/dns.py b/lib/dnslib/build/lib.linux-i686-2.7/dnslib/dns.py new file mode 100644 index 0000000..e4cdd3a --- /dev/null +++ b/lib/dnslib/build/lib.linux-i686-2.7/dnslib/dns.py @@ -0,0 +1,1669 @@ + +""" + 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', 12:'PTR', 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', 55:'HIP', 99:'SPF', + 249:'TKEY', 250:'TSIG', 251:'IXFR', 252:'AXFR', 255:'ANY', + 257:'TYPE257', 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', 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 + 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) + sock.close() + 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) + 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() == "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_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' ] + if OPCODE.get(self.opcode) == 'UPDATE': + f1='zo' + f2='pr' + f3='up' + f4='ad' + else: + f1='q' + f2='a' + f3='ns' + f4='ar' + return "" % ( + 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' ] + 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','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 "" % ( + 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") + + >>> 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 "" % ( + 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 + if self.rtype == QTYPE.OPT: + self.edns_len = self.rclass + self.edns_do = get_bits(self.ttl,15) + 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 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 = ["" % ( + 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 "" % ( + 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) + + >>> 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 + + + >>> 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 "\\# " 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 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 = 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') + +# 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, + } + +## +## 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()) + [] + """ + + 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 + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/lib/dnslib/build/lib.linux-i686-2.7/dnslib/fixedresolver.py b/lib/dnslib/build/lib.linux-i686-2.7/dnslib/fixedresolver.py new file mode 100644 index 0000000..a0c50e8 --- /dev/null +++ b/lib/dnslib/build/lib.linux-i686-2.7/dnslib/fixedresolver.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +""" + FixedResolver - example resolver which responds with fixed response + to all requests +""" + +from __future__ import print_function + +import copy + +from dnslib import RR +from dnslib.server import DNSServer,DNSHandler,BaseResolver,DNSLogger + +class FixedResolver(BaseResolver): + """ + Respond with fixed response to all requests + """ + def __init__(self,zone): + # Parse RRs + self.rrs = RR.fromZone(zone) + + def resolve(self,request,handler): + reply = request.reply() + qname = request.q.qname + # Replace labels with request label + for rr in self.rrs: + a = copy.copy(rr) + a.rname = qname + reply.add_answer(a) + return reply + +if __name__ == '__main__': + + import argparse,sys,time + + p = argparse.ArgumentParser(description="Fixed DNS Resolver") + p.add_argument("--response","-r",default=". 60 IN A 127.0.0.1", + metavar="", + help="DNS response (zone format) (default: 127.0.0.1)") + p.add_argument("--zonefile","-f", + metavar="", + help="DNS response (zone file, '-' for stdin)") + p.add_argument("--port","-p",type=int,default=53, + metavar="", + help="Server port (default:53)") + p.add_argument("--address","-a",default="", + metavar="
", + help="Listen address (default:all)") + p.add_argument("--udplen","-u",type=int,default=0, + metavar="", + help="Max UDP packet length (default:0)") + p.add_argument("--tcp",action='store_true',default=False, + help="TCP server (default: UDP only)") + p.add_argument("--log",default="request,reply,truncated,error", + help="Log hooks to enable (default: +request,+reply,+truncated,+error,-recv,-send,-data)") + p.add_argument("--log-prefix",action='store_true',default=False, + help="Log prefix (timestamp/handler/resolver) (default: False)") + args = p.parse_args() + + if args.zonefile: + if args.zonefile == '-': + args.response = sys.stdin + else: + args.response = open(args.zonefile) + + resolver = FixedResolver(args.response) + logger = DNSLogger(args.log,args.log_prefix) + + print("Starting Fixed Resolver (%s:%d) [%s]" % ( + args.address or "*", + args.port, + "UDP/TCP" if args.tcp else "UDP")) + + for rr in resolver.rrs: + print(" | ",rr.toZone().strip(),sep="") + print() + + if args.udplen: + DNSHandler.udplen = args.udplen + + udp_server = DNSServer(resolver, + port=args.port, + address=args.address, + logger=logger) + udp_server.start_thread() + + if args.tcp: + tcp_server = DNSServer(resolver, + port=args.port, + address=args.address, + tcp=True, + logger=logger) + tcp_server.start_thread() + + while udp_server.isAlive(): + time.sleep(1) + diff --git a/lib/dnslib/build/lib.linux-i686-2.7/dnslib/intercept.py b/lib/dnslib/build/lib.linux-i686-2.7/dnslib/intercept.py new file mode 100644 index 0000000..85ad03a --- /dev/null +++ b/lib/dnslib/build/lib.linux-i686-2.7/dnslib/intercept.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- + +""" + InterceptResolver - proxy requests to upstream server + (optionally intercepting) + +""" +from __future__ import print_function + +import binascii,copy,socket,struct,sys + +from dnslib import DNSRecord,RR,QTYPE,RCODE,parse_time +from dnslib.server import DNSServer,DNSHandler,BaseResolver,DNSLogger +from dnslib.label import DNSLabel + +class InterceptResolver(BaseResolver): + + """ + Intercepting resolver + + Proxy requests to upstream server optionally intercepting requests + matching local records + """ + + def __init__(self,address,port,ttl,intercept,skip,nxdomain,timeout=0): + """ + address/port - upstream server + ttl - default ttl for intercept records + intercept - list of wildcard RRs to respond to (zone format) + skip - list of wildcard labels to skip + nxdomain - list of wildcard labels to retudn NXDOMAIN + timeout - timeout for upstream server + """ + self.address = address + self.port = port + self.ttl = parse_time(ttl) + self.skip = skip + self.nxdomain = nxdomain + self.timeout = timeout + self.zone = [] + for i in intercept: + if i == '-': + i = sys.stdin.read() + for rr in RR.fromZone(i,ttl=self.ttl): + self.zone.append((rr.rname,QTYPE[rr.rtype],rr)) + + def resolve(self,request,handler): + reply = request.reply() + qname = request.q.qname + qtype = QTYPE[request.q.qtype] + # Try to resolve locally unless on skip list + if not any([qname.matchGlob(s) for s in self.skip]): + for name,rtype,rr in self.zone: + if qname.matchGlob(name) and (qtype in (rtype,'ANY','CNAME')): + a = copy.copy(rr) + a.rname = qname + reply.add_answer(a) + # Check for NXDOMAIN + if any([qname.matchGlob(s) for s in self.nxdomain]): + reply.header.rcode = getattr(RCODE,'NXDOMAIN') + return reply + # Otherwise proxy + if not reply.rr: + try: + if handler.protocol == 'udp': + proxy_r = request.send(self.address,self.port, + timeout=self.timeout) + else: + proxy_r = request.send(self.address,self.port, + tcp=True,timeout=self.timeout) + reply = DNSRecord.parse(proxy_r) + except socket.timeout: + reply.header.rcode = getattr(RCODE,'NXDOMAIN') + + return reply + +if __name__ == '__main__': + + import argparse,sys,time + + p = argparse.ArgumentParser(description="DNS Intercept Proxy") + p.add_argument("--port","-p",type=int,default=53, + metavar="", + help="Local proxy port (default:53)") + p.add_argument("--address","-a",default="", + metavar="
", + help="Local proxy listen address (default:all)") + p.add_argument("--upstream","-u",default="8.8.8.8:53", + metavar="", + help="Upstream DNS server:port (default:8.8.8.8:53)") + p.add_argument("--tcp",action='store_true',default=False, + help="TCP proxy (default: UDP only)") + p.add_argument("--intercept","-i",action="append", + metavar="", + help="Intercept requests matching zone record (glob) ('-' for stdin)") + p.add_argument("--skip","-s",action="append", + metavar="