From c0ae52849d8287e63ed3bebc36af2da3ff8aba3c Mon Sep 17 00:00:00 2001 From: Phil <phil@secdev.org> Date: Thu, 28 Aug 2008 13:27:28 +0200 Subject: [PATCH] Integration of scapy6 (rev b8d161460576) --- scapy/layers/inet6.py | 5967 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 5967 insertions(+) create mode 100644 scapy/layers/inet6.py diff --git a/scapy/layers/inet6.py b/scapy/layers/inet6.py new file mode 100644 index 00000000..1931fd41 --- /dev/null +++ b/scapy/layers/inet6.py @@ -0,0 +1,5967 @@ +#! /usr/bin/env python +############################################################################# +## ## +## scapy6.py --- IPv6 support for Scapy ## +## see http://namabiiru.hongo.wide.ad.jp/scapy6/ ## +## for more informations ## +## ## +## Copyright (C) 2005 Guillaume Valadon <guedou@hongo.wide.ad.jp> ## +## Arnaud Ebalard <arnaud.ebalard@eads.net> ## +## ## +## This program is free software; you can redistribute it and/or modify it ## +## under the terms of the GNU General Public License version 2 as ## +## published by the Free Software Foundation; version 2. ## +## ## +## This program is distributed in the hope that it will be useful, but ## +## WITHOUT ANY WARRANTY; without even the implied warranty of ## +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## +## General Public License for more details. ## +## ## +############################################################################# + +from scapy import * +import __builtin__ + + +############################################################################# +# Helpers ## +############################################################################# + +def get_cls(name, fallback_cls): + return __builtin__.__dict__.get(name, fallback_cls) + + +############################################################################# +## Constants ## +############################################################################# + +ETH_P_IPV6 = 0x86dd +OPENBSD=sys.platform.startswith("openbsd") +FREEBSD=sys.platform.startswith("freebsd") +NETBSD = sys.platform.startswith("netbsd") +DARWIN=sys.platform.startswith("darwin") +WINDOWS = sys.platform.startswith("win") + +if OPENBSD or FREEBSD or NETBSD or DARWIN: + loname = "lo0" +else: + loname = "lo" + +# From net/ipv6.h on Linux (+ Additions) +IPV6_ADDR_UNICAST = 0x01 +IPV6_ADDR_MULTICAST = 0x02 +IPV6_ADDR_CAST_MASK = 0x0F +IPV6_ADDR_LOOPBACK = 0x10 +IPV6_ADDR_GLOBAL = 0x00 +IPV6_ADDR_LINKLOCAL = 0x20 +IPV6_ADDR_SITELOCAL = 0x40 # deprecated since Sept. 2004 by RFC 3879 +IPV6_ADDR_SCOPE_MASK = 0xF0 +#IPV6_ADDR_COMPATv4 = 0x80 # deprecated; i.e. ::/96 +#IPV6_ADDR_MAPPED = 0x1000 # i.e.; ::ffff:0.0.0.0/96 +IPV6_ADDR_6TO4 = 0x0100 # Added to have more specific info (should be 0x0101 ?) +IPV6_ADDR_UNSPECIFIED = 0x10000 + + +############################################################################# +############################################################################# +### Routing/Interfaces stuff ### +############################################################################# +############################################################################# + +def construct_source_candidate_set(addr, plen, laddr): + """ + Given all addresses assigned to a specific interface ('laddr' parameter), + this function returns the "candidate set" associated with 'addr/plen'. + + Basically, the function filters all interface addresses to keep only those + that have the same scope as provided prefix. + + This is on this list of addresses that the source selection mechanism + will then be performed to select the best source address associated + with some specific destination that uses this prefix. + """ + + cset = [] + if in6_isgladdr(addr): + cset = filter(lambda x: x[1] == IPV6_ADDR_GLOBAL, laddr) + elif in6_islladdr(addr): + cset = filter(lambda x: x[1] == IPV6_ADDR_LINKLOCAL, laddr) + elif in6_issladdr(addr): + cset = filter(lambda x: x[1] == IPV6_ADDR_SITELOCAL, laddr) + elif in6_ismaddr(addr): + if in6_ismnladdr(addr): + cset = [('::1', 16, loname)] + elif in6_ismgladdr(addr): + cset = filter(lambda x: x[1] == IPV6_ADDR_GLOBAL, laddr) + elif in6_ismlladdr(addr): + cset = filter(lambda x: x[1] == IPV6_ADDR_LINKLOCAL, laddr) + elif in6_ismsladdr(addr): + cset = filter(lambda x: x[1] == IPV6_ADDR_SITELOCAL, laddr) + elif addr == '::' and plen == 0: + cset = filter(lambda x: x[1] == IPV6_ADDR_GLOBAL, laddr) + cset = map(lambda x: x[0], cset) + return cset + +def get_source_addr_from_candidate_set(dst, candidate_set): + """ + This function implement a limited version of source address selection + algorithm defined in section 5 of RFC 3484. The format is very different + from that described in the document because it operates on a set + of candidate source address for some specific route. + + Rationale behind the implementation is to be able to make the right + choice for a 6to4 destination when both a 6to4 address and a IPv6 native + address are available for that interface. + """ + + if len(candidate_set) == 0: + # Should not happen + return None + + if in6_isaddr6to4(dst): + tmp = filter(lambda x: in6_isaddr6to4(x), candidate_set) + if len(tmp) != 0: + return tmp[0] + + return candidate_set[0] + +class Route6: + + def __init__(self): + self.invalidate_cache() + self.resync() + + def invalidate_cache(self): + self.cache = {} + + def flush(self): + self.invalidate_cache() + self.routes = [] + + def resync(self): + # TODO : At the moment, resync will drop existing Teredo routes + # if any. Change that ... + self.invalidate_cache() + self.routes = read_routes6() + if self.routes == []: + log_loading.info("No IPv6 support in kernel") + + def __repr__(self): + rtlst = [('Destination', 'Next Hop', "iface", "src candidates")] + + for net,msk,gw,iface,cset in self.routes: + rtlst.append(('%s/%i'% (net,msk), gw, iface, ", ".join(cset))) + + colwidth = map(lambda x: max(map(lambda y: len(y), x)), apply(zip, rtlst)) + fmt = " ".join(map(lambda x: "%%-%ds"%x, colwidth)) + rt = "\n".join(map(lambda x: fmt % x, rtlst)) + + return rt + + + # Unlike Scapy's Route.make_route() function, we do not have 'host' and 'net' + # parameters. We only have a 'dst' parameter that accepts 'prefix' and + # 'prefix/prefixlen' values. + # WARNING: Providing a specific device will at the moment not work correctly. + def make_route(self, dst, gw=None, dev=None): + """Internal function : create a route for 'dst' via 'gw'. + """ + prefix, plen = (dst.split("/")+["128"])[:2] + plen = int(plen) + + if gw is None: + gw = "::" + if dev is None: + dev, ifaddr, x = self.route(gw) + else: + # TODO: do better than that + # replace that unique address by the list of all addresses + lifaddr = in6_getifaddr() + devaddrs = filter(lambda x: x[2] == dev, lifaddr) + ifaddr = construct_source_candidate_set(prefix, plen, devaddrs) + + return (prefix, plen, gw, dev, ifaddr) + + + def add(self, *args, **kargs): + """Ex: + add(dst="2001:db8:cafe:f000::/56") + add(dst="2001:db8:cafe:f000::/56", gw="2001:db8:cafe::1") + add(dst="2001:db8:cafe:f000::/64", gw="2001:db8:cafe::1", dev="eth0") + """ + self.invalidate_cache() + self.routes.append(self.make_route(*args, **kargs)) + + + def delt(self, dst, gw=None): + """ Ex: + delt(dst="::/0") + delt(dst="2001:db8:cafe:f000::/56") + delt(dst="2001:db8:cafe:f000::/56", gw="2001:db8:deca::1") + """ + tmp = dst+"/128" + dst, plen = tmp.split('/')[:2] + dst = in6_ptop(dst) + plen = int(plen) + l = filter(lambda x: in6_ptop(x[0]) == dst and x[1] == plen, self.routes) + if gw: + gw = in6_ptop(gw) + l = filter(lambda x: in6_ptop(x[0]) == gw, self.routes) + if len(l) == 0: + warning("No matching route found") + elif len(l) > 1: + warning("Found more than one match. Aborting.") + else: + i=self.routes.index(l[0]) + self.invalidate_cache() + del(self.routes[i]) + + def ifchange(self, iff, addr): + the_addr, the_plen = (addr.split("/")+["128"])[:2] + the_plen = int(the_plen) + + naddr = inet_pton(socket.AF_INET6, the_addr) + nmask = in6_cidr2mask(the_plen) + the_net = inet_ntop(socket.AF_INET6, in6_and(nmask,naddr)) + + for i in range(len(self.routes)): + net,plen,gw,iface,addr = self.routes[i] + if iface != iff: + continue + if gw == '::': + self.routes[i] = (the_net,the_plen,gw,iface,the_addr) + else: + self.routes[i] = (net,the_plen,gw,iface,the_addr) + self.invalidate_cache() + ip6_neigh_cache.flush() + + def ifdel(self, iff): + """ removes all route entries that uses 'iff' interface. """ + new_routes=[] + for rt in self.routes: + if rt[3] != iff: + new_routes.append(rt) + self.invalidate_cache() + self.routes = new_routes + + + def ifadd(self, iff, addr): + """ + Add an interface 'iff' with provided address into routing table. + + Ex: ifadd('eth0', '2001:bd8:cafe:1::1/64') will add following entry into + Scapy6 internal routing table: + + Destination Next Hop iface Def src @ + 2001:bd8:cafe:1::/64 :: eth0 2001:bd8:cafe:1::1 + + prefix length value can be omitted. In that case, a value of 128 + will be used. + """ + addr, plen = (addr.split("/")+["128"])[:2] + addr = in6_ptop(addr) + plen = int(plen) + naddr = inet_pton(socket.AF_INET6, addr) + nmask = in6_cidr2mask(plen) + prefix = inet_ntop(socket.AF_INET6, in6_and(nmask,naddr)) + self.invalidate_cache() + self.routes.append((prefix,plen,'::',iff,[addr])) + + def route(self, dst, dev=None): + """ + Provide best route to IPv6 destination address, based on Scapy6 + internal routing table content. + + When a set of address is passed (e.g. 2001:db8:cafe:*::1-5) an address + of the set is used. Be aware of that behavior when using wildcards in + upper parts of addresses ! + + If 'dst' parameter is a FQDN, name resolution is performed and result + is used. + + if optional 'dev' parameter is provided a specific interface, filtering + is performed to limit search to route associated to that interface. + """ + # Transform "2001:db8:cafe:*::1-5:0/120" to one IPv6 address of the set + dst = dst.split("/")[0] + savedst = dst # In case following inet_pton() fails + dst = dst.replace("*","0") + l = dst.find("-") + while l >= 0: + m = (dst[l:]+":").find(":") + dst = dst[:l]+dst[l+m:] + l = dst.find("-") + + try: + inet_pton(socket.AF_INET6, dst) + except socket.error: + dst = socket.getaddrinfo(savedst, None, socket.AF_INET6)[0][-1][0] + # TODO : Check if name resolution went well + + # Deal with dev-specific request for cache search + k = dst + if dev is not None: + k = dst + "%%" + dev + if k in self.cache: + return self.cache[k] + + pathes = [] + + # TODO : review all kinds of addresses (scope and *cast) to see + # if we are able to cope with everything possible. I'm convinced + # it's not the case. + # -- arnaud + for p, plen, gw, iface, cset in self.routes: + if dev is not None and iface != dev: + continue + if in6_isincluded(dst, p, plen): + pathes.append((plen, (iface, cset, gw))) + elif (in6_ismlladdr(dst) and in6_islladdr(p) and in6_islladdr(cset[0])): + pathes.append((plen, (iface, cset, gw))) + + if not pathes: + warning("No route found for IPv6 destination %s (no default route?)" % dst) + return (loname, "::", "::") # XXX Linux specific + + pathes.sort() + pathes.reverse() + + best_plen = pathes[0][0] + pathes = filter(lambda x: x[0] == best_plen, pathes) + + res = [] + for p in pathes: # Here we select best source address for every route + tmp = p[1] + srcaddr = get_source_addr_from_candidate_set(dst, p[1][1]) + if srcaddr is not None: + res.append((p[0], (tmp[0], srcaddr, tmp[2]))) + + # Symptom : 2 routes with same weight (our weight is plen) + # Solution : + # - dst is unicast global. Check if it is 6to4 and we have a source + # 6to4 address in those available + # - dst is link local (unicast or multicast) and multiple output + # interfaces are available. Take main one (conf.iface) + # - if none of the previous or ambiguity persists, be lazy and keep + # first one + # XXX TODO : in a _near_ future, include metric in the game + + if len(res) > 1: + tmp = [] + if in6_isgladdr(dst) and in6_isaddr6to4(dst): + # TODO : see if taking the longest match between dst and + # every source addresses would provide better results + tmp = filter(lambda x: in6_isaddr6to4(x[1][1]), res) + elif in6_ismaddr(dst) or in6_islladdr(dst): + # TODO : I'm sure we are not covering all addresses. Check that + tmp = filter(lambda x: x[1][0] == conf.iface, res) + + if tmp: + res = tmp + + # Fill the cache (including dev-specific request) + k = dst + if dev is not None: + k = dst + "%%" + dev + self.cache[k] = res[0][1] + + return res[0][1] + +def get_if_raw_addr6(iff): + """ + Returns the main global unicast address associated with provided + interface, in network format. If no global address is found, None + is returned. + """ + r = filter(lambda x: x[2] == iff and x[1] == IPV6_ADDR_GLOBAL, in6_getifaddr()) + if len(r) == 0: + return None + else: + r = r[0][0] + return inet_pton(socket.AF_INET6, r) + +if LINUX: + + def in6_getifaddr(): + """ + Returns a list of 3-tuples of the form (addr, scope, iface) where + 'addr' is the address of scope 'scope' associated to the interface + 'ifcace'. + + This is the list of all addresses of all interfaces available on + the system. + """ + ret = [] + try: + f = open("/proc/net/if_inet6","r") + except IOError, err: + return ret + l = f.readlines() + for i in l: + # addr, index, plen, scope, flags, ifname + tmp = i.split() + addr = struct.unpack('4s4s4s4s4s4s4s4s', tmp[0]) + addr = in6_ptop(':'.join(addr)) + ret.append((addr, int(tmp[3], 16), tmp[5])) # (addr, scope, iface) + return ret + + def read_routes6(): + try: + f = open("/proc/net/ipv6_route","r") + except IOError, err: + return [] + # 1. destination network + # 2. destination prefix length + # 3. source network displayed + # 4. source prefix length + # 5. next hop + # 6. metric + # 7. reference counter (?!?) + # 8. use counter (?!?) + # 9. flags + # 10. device name + routes = [] + def proc2r(p): + ret = struct.unpack('4s4s4s4s4s4s4s4s', p) + ret = ':'.join(ret) + return in6_ptop(ret) + + lifaddr = in6_getifaddr() + for l in f.readlines(): + d,dp,s,sp,nh,m,rc,us,fl,dev = l.split() + fl = int(fl, 16) + + if fl & RTF_UP == 0: + continue + if fl & RTF_REJECT: + continue + + d = proc2r(d) ; dp = int(dp, 16) + s = proc2r(s) ; sp = int(sp, 16) + nh = proc2r(nh) + + cset = [] # candidate set (possible source addresses) + if dev == loname: + if d == '::': + continue + cset = ['::1'] + else: + devaddrs = filter(lambda x: x[2] == dev, lifaddr) + cset = construct_source_candidate_set(d, dp, devaddrs) + + if len(cset) != 0: + routes.append((d, dp, nh, dev, cset)) + f.close() + return routes + +elif WINDOWS: + def in6_getifaddr(): + """ + Returns a list of 3-tuples of the form (addr, scope, iface) where + 'addr' is the address of scope 'scope' associated to the interface + 'ifcace'. + + This is the list of all addresses of all interfaces available on + the system. + """ + ret = [] + # Just some dummy values for now + xx = "::1" + scope = 128 + ifname = loname + ret.append(xx, scope, ifname) + return ret + + def read_routes6(): + routes = [] + # Just some dummy values for now + d = '::' + dp = 0 + nh = '::' + dev = loname + cset = ['::1'] + routes.append((d, dp, nh, dev, cset)) + return routes + +else: + def in6_getifaddr(): + """ + Returns a list of 3-tuples of the form (addr, scope, iface) where + 'addr' is the address of scope 'scope' associated to the interface + 'ifcace'. + + This is the list of all addresses of all interfaces available on + the system. + """ + + ret = [] + i = dnet.intf() + for int in i: + ifname = int['name'] + v6 = [] + if int.has_key('alias_addrs'): + v6 = int['alias_addrs'] + for a in v6: + if a.type != dnet.ADDR_TYPE_IP6: + continue + + xx = str(a).split('/')[0] + addr = in6_ptop(xx) + + scope = in6_getscope(addr) + + ret.append((xx, scope, ifname)) + return ret + + def read_routes6(): + f = os.popen("netstat -rn -f inet6") + ok = -1 + routes = [] + lifaddr = in6_getifaddr() + for l in f.readlines(): + if not l: + break + l = l.strip() + if ok < 0: + ok = l.find('Destination') + continue + # gv 12/12/06: under debugging + if NETBSD or OPENBSD: + d,nh,fl,_,_,_,dev = l.split()[:7] + else: # FREEBSD or DARWIN + d,nh,fl,dev = l.split()[:4] + if filter(lambda x: x[2] == dev, lifaddr) == []: + continue + if 'L' in fl: # drop MAC addresses + continue + + if 'link' in nh: + nh = '::' + + cset = [] # candidate set (possible source addresses) + dp = 128 + if d == 'default': + d = '::' + dp = 0 + if '/' in d: + d,dp = d.split("/") + dp = int(dp) + if '%' in d: + d,dev = d.split('%') + if '%' in nh: + nh,dev = nh.split('%') + if loname in dev: + cset = ['::1'] + nh = '::' + else: + devaddrs = filter(lambda x: x[2] == dev, lifaddr) + cset = construct_source_candidate_set(d, dp, devaddrs) + + if len(cset) != 0: + routes.append((d, dp, nh, dev, cset)) + + f.close() + return routes + + +########################## +## Neighbor cache stuff ## +########################## + +NEIGHTIMEOUT=120 + +def neighsol(addr, src, iface, timeout=1, chainCC=0): + """ + Sends an ICMPv6 Neighbor Solicitation message to get the MAC address + of the neighbor with specified IPv6 address addr. 'src' address is + used as source of the message. Message is sent on iface. By default, + timeout waiting for an answer is 1 second. + + If no answer is gathered, None is returned. Else, the answer is + returned (ethernet frame). + """ + + nsma = in6_getnsma(inet_pton(socket.AF_INET6, addr)) + d = inet_ntop(socket.AF_INET6, nsma) + dm = in6_getnsmac(nsma) + p = Ether(dst=dm)/IPv6(dst=d, src=src, hlim=255) + p /= ICMPv6ND_NS(tgt=addr) + p /= ICMPv6NDOptSrcLLAddr(lladdr=get_if_hwaddr(iface)) + res = srp1(p,type=ETH_P_IPV6, iface=iface, timeout=1, verbose=0, + chainCC=chainCC) + + return res + +class neighborCache: + + # TODO : add some method to modify default value for timeout + # TODO : See what we can do for updating the neighbor cache + # when receiving a packet. + + # Note: internally, our neighbor cache is scapy's arp_cache. This allows us + # to have it updated when returning from sr() (a fork is done where a + # fork is done and the updated cache returned at the end. + + def __init__(self): + self.neighcache = arp_cache + + def flush(self, statictoo=True): + self.neighcache = {} + + def __repr__(self): + res = [("Peer", "Link layer address", "State")] + for addr in self.neighcache.keys(): + try: + inet_pton(socket.AF_INET6, addr) + except: + continue + cur_entry = self.neighcache[addr] + status = "REACHABLE" + last_contact = cur_entry[1] + if last_contact == 0: + status = "STATIC" + elif ((time.time() - last_contact) < NEIGHTIMEOUT): + status = "REACHABLE" + else: + status = "STALE" + res.append((addr, cur_entry[0], status)) + + colwidth = map(lambda x: max(map(lambda y: len(y), x)), apply(zip, res)) + fmt = " ".join(map(lambda x: "%%-%ds"%x, colwidth)) + res = "\n".join(map(lambda x: fmt % x, res)) + return res + + def addNeighbor(self, ip6, mac, static=False): + """ + Add a neighbor to the cache. If optional parameter 'static' is not + set to True (the default), the entry will expire in 2 minutes. If + 'static' is set to True, the entry in the neighbor cache is made + static. This is practical in those cases : + + - peer's address is not advertised to be on-link + - peer doed not answer to NS + - you don't want to make queries to keep time or be stealthy, ... + """ + t = 0 + if not static: + t = time.time() + self.neighcache[ip6] = (mac, t) + + def makeStatic(self, ip6): + """ + make the entry static in Scapy6 internal neighbor cache for + 'ip6' neighbor. + """ + if self.neighcache.has_key(ip6): + mac = self.neighcache[ip6][0] + self.neighcache[ip6] = (mac, 0) + else: + warning("Unable to make neighbor cache entry for %s static. It does not exist." % ip6) + + def removeStatic(self, ip6): + """ + remove the static status for 'ip6' entry in Scapy6 internal + neighbor cache. + """ + if self.neighcache.has_key(ip6): + mac = self.neighcache[ip6][0] + self.neighcache[ip6] = (mac, time.time()) + else: + warning("Unable to make neighbor cache entry for %s static. It does not exist." % ip6) + + def get(self, ip6, chainCC=0): + """ + Returns the link layer address to use for IPv6 traffic to 'ip6' address. + If searched IPv6 address is multicast, then, ethernet address is computed. + If that's not the case, Scapy6 routing table is used to find next hop for + provided address. If one is found, cache is searched. If a valid (REACHABLE + or STATIC) entry exist, content is returned. Else, resolution is performed + by sending a Neighbor Solicitation. + + In all cases, if lookup fails, None is returned. + """ + + if in6_ismaddr(ip6): # Multicast + mac = in6_getnsmac(inet_pton(socket.AF_INET6, ip6)) + return mac + + iff,a,nh = conf.route6.route(ip6, dev=conf.iface) + + if iff == loname: + return "ff:ff:ff:ff:ff:ff" + + if nh != '::': + ip6 = nh # Found next hop + + if self.neighcache.has_key(ip6): # search the cache + mac, timeout = self.neighcache[ip6] + if timeout and (time.time()-timeout < NEIGHTIMEOUT): + return mac + + res = neighsol(ip6, a, iff, chainCC=chainCC) + + if res is not None: + mac = res.src + self.neighcache[ip6] = (mac,time.time()) + return mac + + return None + +ip6_neigh_cache = neighborCache() + +def getmacbyip6(ip6, chainCC=0): + """ + Returns the mac address to be used for provided 'ip6' peer. + neighborCache.get() method is used on instantiated neighbor cache. + Resolution mechanism is described in associated doc string. + + (chainCC parameter value ends up being passed to sending function + used to perform the resolution, if needed) + """ + return ip6_neigh_cache.get(ip6, chainCC=chainCC) + + +############################################################################# +############################################################################# +### IPv6 addresses manipulation routines ### +############################################################################# +############################################################################# + +class Net6(Gen): # syntax ex. fec0::/126 + """Generate a list of IPv6s from a network address or a name""" + name = "ipv6" + ipaddress = re.compile(r"^([a-fA-F0-9:]+)(/[1]?[0-3]?[0-9])?$") + + def __init__(self, net): + self.repr = net + + tmp = net.split('/')+["128"] + if not self.ipaddress.match(net): + tmp[0]=socket.getaddrinfo(tmp[0], None, socket.AF_INET6)[0][-1][0] + + netmask = int(tmp[1]) + self.net = inet_pton(socket.AF_INET6, tmp[0]) + self.mask = in6_cidr2mask(netmask) + self.plen = netmask + + def __iter__(self): + def m8(i): + if i % 8 == 0: + return i + tuple = filter(lambda x: m8(x), xrange(8, 129)) + + a = in6_and(self.net, self.mask) + tmp = map(lambda x: x, struct.unpack('16B', a)) + + def parse_digit(a, netmask): + netmask = min(8,max(netmask,0)) + a = (int(a) & (0xffL<<netmask),(int(a) | (0xffL>>(8-netmask)))+1) + return a + self.parsed = map(lambda x,y: parse_digit(x,y), tmp, map(lambda x,nm=self.plen: x-nm, tuple)) + + def rec(n, l): + if n and n % 2 == 0: + sep = ':' + else: + sep = '' + if n == 16: + return l + else: + ll = [] + for i in xrange(*self.parsed[n]): + for y in l: + ll += [y+sep+'%.2x'%i] + return rec(n+1, ll) + + return iter(rec(0, [''])) + + def __repr__(self): + return "<Net6 %s>" % self.repr + + +# Think before modify it : for instance, FE::1 does exist and is unicast +# there are many others like that. +# TODO : integrate Unique Local Addresses +def in6_getAddrType(addr): + naddr = inet_pton(socket.AF_INET6, addr) + paddr = inet_ntop(socket.AF_INET6, naddr) # normalize + addrType = 0 + # _Assignable_ Global Unicast Address space + # is defined in RFC 3513 as those in 2000::/3 + if ((struct.unpack("B", naddr[0])[0] & 0xE0) == 0x20): + addrType = (IPV6_ADDR_UNICAST | IPV6_ADDR_GLOBAL) + if naddr[:2] == ' \x02': # Mark 6to4 @ + addrType |= IPV6_ADDR_6TO4 + elif naddr[0] == '\xff': # multicast + addrScope = paddr[3] + if addrScope == '2': + addrType = (IPV6_ADDR_LINKLOCAL | IPV6_ADDR_MULTICAST) + elif addrScope == 'e': + addrType = (IPV6_ADDR_GLOBAL | IPV6_ADDR_MULTICAST) + else: + addrType = (IPV6_ADDR_GLOBAL | IPV6_ADDR_MULTICAST) + elif ((naddr[0] == '\xfe') and ((int(paddr[2], 16) & 0xC) == 0x8)): + addrType = (IPV6_ADDR_UNICAST | IPV6_ADDR_LINKLOCAL) + elif paddr == "::1": + addrType = IPV6_ADDR_LOOPBACK + elif paddr == "::": + addrType = IPV6_ADDR_UNSPECIFIED + else: + # Everything else is global unicast (RFC 3513) + # Even old deprecated (RFC3879) Site-Local addresses + addrType = (IPV6_ADDR_GLOBAL | IPV6_ADDR_UNICAST) + + return addrType + +def find_ifaddr2(addr, plen, laddr): + dstAddrType = in6_getAddrType(addr) + + if dstAddrType == IPV6_ADDR_UNSPECIFIED: # Shouldn't happen as dst addr + return None + + if dstAddrType == IPV6_ADDR_LOOPBACK: + return None + + tmp = [[]] + map(lambda (x,y,z): (in6_getAddrType(x), x, y, z), laddr) + def filterSameScope(l, t): + if (t[0] & dstAddrType & IPV6_ADDR_SCOPE_MASK) == 0: + l.append(t) + return l + sameScope = reduce(filterSameScope, tmp) + + l = len(sameScope) + if l == 1: # Only one address for our scope + return sameScope[0][1] + + elif l > 1: # Muliple addresses for our scope + stfAddr = filter(lambda x: x[0] & IPV6_ADDR_6TO4, sameScope) + nativeAddr = filter(lambda x: not (x[0] & IPV6_ADDR_6TO4), sameScope) + + if not (dstAddrType & IPV6_ADDR_6TO4): # destination is not 6to4 + if len(nativeAddr) != 0: + return nativeAddr[0][1] + return stfAddr[0][1] + + else: # Destination is 6to4, try to use source 6to4 addr if any + if len(stfAddr) != 0: + return stfAddr[0][1] + return nativeAddr[0][1] + else: + return None + + +def in6_mactoifaceid(mac, ulbit=None): + """ + Compute the interface ID in modified EUI-64 format associated + to the Ethernet address provided as input. + value taken by U/L bit in the interface identifier is basically + the reversed value of that in given MAC address it can be forced + to a specific value by using optional 'ulbit' parameter. + """ + if len(mac) != 17: return None + m = "".join(mac.split(':')) + if len(m) != 12: return None + first = int(m[0:2], 16) + if ulbit is None or not (ulbit == 0 or ulbit == 1): + ulbit = [1,'-',0][first & 0x02] + ulbit *= 2 + first = "%.02x" % ((first & 0xFD) | ulbit) + eui64 = first + m[2:4] + ":" + m[4:6] + "FF:FE" + m[6:8] + ":" + m[8:12] + return eui64.upper() + +def in6_ifaceidtomac(ifaceid): # TODO: finish commenting function behavior + """ + Extract the mac address from provided iface ID. Iface ID is provided + in printable format ("XXXX:XXFF:FEXX:XXXX", eventually compressed). None + is returned on error. + """ + try: + ifaceid = inet_pton(socket.AF_INET6, "::"+ifaceid)[8:16] + except: + return None + if ifaceid[3:5] != '\xff\xfe': + return None + first = struct.unpack("B", ifaceid[:1])[0] + ulbit = 2*[1,'-',0][first & 0x02] + first = struct.pack("B", ((first & 0xFD) | ulbit)) + oui = first + ifaceid[1:3] + end = ifaceid[5:] + l = map(lambda x: "%.02x" % struct.unpack("B", x)[0], list(oui+end)) + return ":".join(l) + +def in6_addrtomac(addr): + """ + Extract the mac address from provided address. None is returned + on error. + """ + mask = inet_pton(socket.AF_INET6, "::ffff:ffff:ffff:ffff") + x = in6_and(mask, inet_pton(socket.AF_INET6, addr)) + ifaceid = inet_ntop(socket.AF_INET6, x)[2:] + return in6_ifaceidtomac(ifaceid) + +def in6_addrtovendor(addr): + """ + Extract the MAC address from a modified EUI-64 constructed IPv6 + address provided and use the IANA oui.txt file to get the vendor. + The database used for the conversion is the one loaded by Scapy, + based on Wireshark (/usr/share/wireshark/wireshark/manuf) None + is returned on error, "UNKNOWN" if the vendor is unknown. + """ + mac = in6_addrtomac(addr) + if mac is None: + return None + + res = conf.manufdb._get_manuf(mac) + if len(res) == 17 and res.count(':') != 5: # Mac address, i.e. unknown + res = "UNKNOWN" + + return res + +def in6_getLinkScopedMcastAddr(addr, grpid=None, scope=2): + """ + Generate a Link-Scoped Multicast Address as described in RFC 4489. + Returned value is in printable notation. + + 'addr' parameter specifies the link-local address to use for generating + Link-scoped multicast address IID. + + By default, the function returns a ::/96 prefix (aka last 32 bits of + returned address are null). If a group id is provided through 'grpid' + parameter, last 32 bits of the address are set to that value (accepted + formats : '\x12\x34\x56\x78' or '12345678' or 0x12345678 or 305419896). + + By default, generated address scope is Link-Local (2). That value can + be modified by passing a specific 'scope' value as an argument of the + function. RFC 4489 only authorizes scope values <= 2. Enforcement + is performed by the function (None will be returned). + + If no link-local address can be used to generate the Link-Scoped IPv6 + Multicast address, or if another error occurs, None is returned. + """ + if not scope in [0, 1, 2]: + return None + try: + if not in6_islladdr(addr): + return None + addr = inet_pton(socket.AF_INET6, addr) + except: + warning("in6_getLinkScopedMcastPrefix(): Invalid address provided") + return None + + iid = addr[8:] + + if grpid is None: + grpid = '\x00\x00\x00\x00' + else: + if type(grpid) is str: + if len(grpid) == 8: + try: + grpid = int(grpid, 16) & 0xffffffff + except: + warning("in6_getLinkScopedMcastPrefix(): Invalid group id provided") + return None + elif len(grpid) == 4: + try: + grpid = struct.unpack("!I", grpid)[0] + except: + warning("in6_getLinkScopedMcastPrefix(): Invalid group id provided") + return None + grpid = struct.pack("!I", grpid) + + flgscope = struct.pack("B", 0xff & ((0x3 << 4) | scope)) + plen = '\xff' + res = '\x00' + a = '\xff' + flgscope + res + plen + iid + grpid + + return inet_ntop(socket.AF_INET6, a) + +def in6_get6to4Prefix(addr): + """ + Returns the /48 6to4 prefix associated with provided IPv4 address + On error, None is returned. No check is performed on public/private + status of the address + """ + try: + addr = inet_pton(socket.AF_INET, addr) + addr = inet_ntop(socket.AF_INET6, '\x20\x02'+addr+'\x00'*10) + except: + return None + return addr + +def in6_6to4ExtractAddr(addr): + """ + Extract IPv4 address embbeded in 6to4 address. Passed address must be + a 6to4 addrees. None is returned on error. + """ + try: + addr = inet_pton(socket.AF_INET6, addr) + except: + return None + if addr[:2] != " \x02": + return None + return inet_ntop(socket.AF_INET, addr[2:6]) + + +def in6_getLocalUniquePrefix(): + """ + Returns a pseudo-randomly generated Local Unique prefix. Function + follows recommandation of Section 3.2.2 of RFC 4193 for prefix + generation. + """ + # Extracted from RFC 1305 (NTP) : + # NTP timestamps are represented as a 64-bit unsigned fixed-point number, + # in seconds relative to 0h on 1 January 1900. The integer part is in the + # first 32 bits and the fraction part in the last 32 bits. + + # epoch = (1900, 1, 1, 0, 0, 0, 5, 1, 0) + # x = time.time() + # from time import gmtime, strftime, gmtime, mktime + # delta = mktime(gmtime(0)) - mktime(self.epoch) + # x = x-delta + + tod = time.time() # time of day. Will bother with epoch later + i = int(tod) + j = int((tod - i)*(2**32)) + tod = struct.pack("!II", i,j) + # TODO: Add some check regarding system address gathering + rawmac = get_if_raw_hwaddr(conf.iface)[1] + mac = ":".join(map(lambda x: "%.02x" % ord(x), list(rawmac))) + # construct modified EUI-64 ID + eui64 = inet_pton(socket.AF_INET6, '::' + in6_mactoifaceid(mac))[8:] + import sha + globalid = sha.new(tod+eui64).digest()[:5] + return inet_ntop(socket.AF_INET6, '\xfd' + globalid + '\x00'*10) + +def in6_getRandomizedIfaceId(ifaceid, previous=None): + """ + Implements the interface ID generation algorithm described in RFC 3041. + The function takes the Modified EUI-64 interface identifier generated + as described in RFC 4291 and an optional previous history value (the + first element of the output of this function). If no previous interface + identifier is provided, a random one is generated. The function returns + a tuple containing the randomized interface identifier and the history + value (for possible future use). Input and output values are provided in + a "printable" format as depicted below. + + ex: + + >>> in6_getRandomizedIfaceId('20b:93ff:feeb:2d3') + ('4c61:76ff:f46a:a5f3', 'd006:d540:db11:b092') + + >>> in6_getRandomizedIfaceId('20b:93ff:feeb:2d3', + previous='d006:d540:db11:b092') + ('fe97:46fe:9871:bd38', 'eeed:d79c:2e3f:62e') + """ + + s = "" + if previous is None: + d = "".join(map(chr, range(256))) + for i in range(8): + s += random.choice(d) + previous = s + s = inet_pton(socket.AF_INET6, "::"+ifaceid)[8:] + previous + import md5 + s = md5.new(s).digest() + s1,s2 = s[:8],s[8:] + s1 = chr(ord(s1[0]) | 0x04) + s1[1:] + s1 = inet_ntop(socket.AF_INET6, "\xff"*8 + s1)[20:] + s2 = inet_ntop(socket.AF_INET6, "\xff"*8 + s2)[20:] + return (s1, s2) + + +_rfc1924map = [ '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E', + 'F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T', + 'U','V','W','X','Y','Z','a','b','c','d','e','f','g','h','i', + 'j','k','l','m','n','o','p','q','r','s','t','u','v','w','x', + 'y','z','!','#','$','%','&','(',')','*','+','-',';','<','=', + '>','?','@','^','_','`','{','|','}','~' ] + +def in6_ctop(addr): + """ + Convert an IPv6 address in Compact Representation Notation + (RFC 1924) to printable representation ;-) + Returns None on error. + """ + if len(addr) != 20 or not reduce(lambda x,y: x and y, + map(lambda x: x in _rfc1924map, addr)): + return None + i = 0 + for c in addr: + j = _rfc1924map.index(c) + i = 85*i + j + res = [] + for j in range(4): + res.append(struct.pack("!I", i%2**32)) + i = i/(2**32) + res.reverse() + return inet_ntop(socket.AF_INET6, "".join(res)) + +def in6_ptoc(addr): + """ + Converts an IPv6 address in printable representation to RFC + 1924 Compact Representation ;-) + Returns None on error. + """ + try: + d=struct.unpack("!IIII", inet_pton(socket.AF_INET6, addr)) + except: + return None + res = 0 + m = [2**96, 2**64, 2**32, 1] + for i in range(4): + res += d[i]*m[i] + rem = res + res = [] + while rem: + res.append(_rfc1924map[rem%85]) + rem = rem/85 + res.reverse() + return "".join(res) + + +def in6_isaddr6to4(x): + """ + Return True if provided address (in printable format) is a 6to4 + address (being in 2002::/16). + """ + x = inet_pton(socket.AF_INET6, x) + return x[:2] == ' \x02' + +conf.teredoPrefix = "2001::" # old one was 3ffe:831f (it is a /32) +conf.teredoServerPort = 3544 + +def in6_isaddrTeredo(x): + """ + Return True if provided address is a Teredo, meaning it is under + the /32 conf.teredoPrefix prefix value (by default, 2001::). + Otherwise, False is returned. Address must be passed in printable + format. + """ + our = inet_pton(socket.AF_INET6, x)[0:4] + teredoPrefix = inet_pton(socket.AF_INET6, conf.teredoPrefix)[0:4] + return teredoPrefix == our + +def teredoAddrExtractInfo(x): + """ + Extract information from a Teredo address. Return value is + a 4-tuple made of IPv4 address of Teredo server, flag value (int), + mapped address (non obfuscated) and mapped port (non obfuscated). + No specific checks are performed on passed address. + """ + addr = inet_pton(socket.AF_INET6, x) + server = inet_ntop(socket.AF_INET, addr[4:8]) + flag = struct.unpack("!H",addr[8:10])[0] + mappedport = struct.unpack("!H",strxor(addr[10:12],'\xff'*2))[0] + mappedaddr = inet_ntop(socket.AF_INET, strxor(addr[12:16],'\xff'*4)) + return server, flag, mappedaddr, mappedport + +def in6_iseui64(x): + """ + Return True if provided address has an interface identifier part + created in modified EUI-64 format (meaning it matches *::*:*ff:fe*:*). + Otherwise, False is returned. Address must be passed in printable + format. + """ + eui64 = inet_pton(socket.AF_INET6, '::ff:fe00:0') + x = in6_and(inet_pton(socket.AF_INET6, x), eui64) + return x == eui64 + +def in6_isanycast(x): # RFC 2526 + if in6_iseui64(x): + s = '::fdff:ffff:ffff:ff80' + x = in6_and(x, inet_pton(socket.AF_INET6, '::ffff:ffff:ffff:ff80')) + x = in6_and(x, inet_pton(socket.AF_INET6, s)) + return x == inet_pton(socket.AF_INET6, s) + else: + # not EUI-64 + #| n bits | 121-n bits | 7 bits | + #+---------------------------------+------------------+------------+ + #| subnet prefix | 1111111...111111 | anycast ID | + #+---------------------------------+------------------+------------+ + # | interface identifier field | + warning('in6_isanycast(): TODO not EUI-64') + return 0 + +def _in6_bitops(a1, a2, operator=0): + a1 = struct.unpack('4I', a1) + a2 = struct.unpack('4I', a2) + fop = [ lambda x,y: x | y, + lambda x,y: x & y, + lambda x,y: x ^ y + ] + ret = map(fop[operator%len(fop)], a1, a2) + t = ''.join(map(lambda x: struct.pack('I', x), ret)) + return t + +def in6_or(a1, a2): + """ + Provides a bit to bit OR of provided addresses. They must be + passed in network format. Return value is also an IPv6 address + in network format. + """ + return _in6_bitops(a1, a2, 0) + +def in6_and(a1, a2): + """ + Provides a bit to bit AND of provided addresses. They must be + passed in network format. Return value is also an IPv6 address + in network format. + """ + return _in6_bitops(a1, a2, 1) + +def in6_xor(a1, a2): + """ + Provides a bit to bit XOR of provided addresses. They must be + passed in network format. Return value is also an IPv6 address + in network format. + """ + return _in6_bitops(a1, a2, 2) + +def in6_cidr2mask(m): + """ + Return the mask (bitstring) associated with provided length + value. For instance if function is called on 48, return value is + '\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'. + + """ + if m > 128 or m < 0: + raise Scapy_Exception("value provided to in6_cidr2mask outside [0, 128] domain (%d)" % m) + + t = [] + for i in xrange(0, 4): + t.append(max(0, 2**32 - 2**(32-min(32, m)))) + m -= 32 + + return ''.join(map(lambda x: struct.pack('!I', x), t)) + +def in6_getnsma(a): + """ + Return link-local solicited-node multicast address for given + address. Passed address must be provided in network format. + Returned value is also in network format. + """ + + r = in6_and(a, inet_pton(socket.AF_INET6, '::ff:ffff')) + r = in6_or(inet_pton(socket.AF_INET6, 'ff02::1:ff00:0'), r) + return r + +def in6_getnsmac(a): # return multicast Ethernet address associated with multicast v6 destination + """ + Return the multicast mac address associated with provided + IPv6 address. Passed address must be in network format. + """ + + a = struct.unpack('16B', a)[-4:] + mac = '33:33:' + mac += ':'.join(map(lambda x: '%.2x' %x, a)) + return mac + +def in6_getha(prefix): + """ + Return the anycast address associated with all home agents on a given + subnet. + """ + r = in6_and(inet_pton(socket.AF_INET6, prefix), in6_cidr2mask(64)) + r = in6_or(r, inet_pton(socket.AF_INET6, '::fdff:ffff:ffff:fffe')) + return inet_ntop(socket.AF_INET6, r) + +def in6_ptop(str): + """ + Normalizes IPv6 addresses provided in printable format, returning the + same address in printable format. (2001:0db8:0:0::1 -> 2001:db8::1) + """ + return inet_ntop(socket.AF_INET6, inet_pton(socket.AF_INET6, str)) + +def in6_isincluded(addr, prefix, plen): + """ + Returns True when 'addr' belongs to prefix/plen. False otherwise. + """ + temp = inet_pton(socket.AF_INET6, addr) + pref = in6_cidr2mask(plen) + zero = inet_pton(socket.AF_INET6, prefix) + return zero == in6_and(temp, pref) + +def in6_isdocaddr(str): + """ + Returns True if provided address in printable format belongs to + 2001:db8::/32 address space reserved for documentation (as defined + in RFC 3849). + """ + return in6_isincluded(str, '2001:db8::', 32) + +def in6_islladdr(str): + """ + Returns True if provided address in printable format belongs to + _allocated_ link-local unicast address space (fe80::/10) + """ + return in6_isincluded(str, 'fe80::', 10) + +def in6_issladdr(str): + """ + Returns True if provided address in printable format belongs to + _allocated_ site-local address space (fec0::/10). This prefix has + been deprecated, address being now reserved by IANA. Function + will remain for historic reasons. + """ + return in6_isincluded(str, 'fec0::', 10) + +def in6_isuladdr(str): + """ + Returns True if provided address in printable format belongs to + Unique local address space (fc00::/7). + """ + return in6_isincluded(str, 'fc::', 7) + +# TODO : we should see the status of Unique Local addresses against +# global address space. +# Up-to-date information is available through RFC 3587. +# We should review function behavior based on its content. +def in6_isgladdr(str): + """ + Returns True if provided address in printable format belongs to + _allocated_ global address space (2000::/3). Please note that, + Unique Local addresses (FC00::/7) are not part of global address + space, and won't match. + """ + return in6_isincluded(str, '2000::', 3) + +def in6_ismaddr(str): + """ + Returns True if provided address in printable format belongs to + allocated Multicast address space (ff00::/8). + """ + return in6_isincluded(str, 'ff00::', 8) + +def in6_ismnladdr(str): + """ + Returns True if address belongs to node-local multicast address + space (ff01::/16) as defined in RFC + """ + return in6_isincluded(str, 'ff01::', 16) + +def in6_ismgladdr(str): + """ + Returns True if address belongs to global multicast address + space (ff0e::/16). + """ + return in6_isincluded(str, 'ff0e::', 16) + +def in6_ismlladdr(str): + """ + Returns True if address balongs to link-local multicast address + space (ff02::/16) + """ + return in6_isincluded(str, 'ff02::', 16) + +def in6_ismsladdr(str): + """ + Returns True if address belongs to site-local multicast address + space (ff05::/16). Site local address space has been deprecated. + Function remains for historic reasons. + """ + return in6_isincluded(str, 'ff05::', 16) + +def in6_isaddrllallnodes(str): + """ + Returns True if address is the link-local all-nodes multicast + address (ff02::1). + """ + return (inet_pton(socket.AF_INET6, "ff02::1") == + inet_pton(socket.AF_INET6, str)) + +def in6_isaddrllallservers(str): + """ + Returns True if address is the link-local all-servers multicast + address (ff02::2). + """ + return (inet_pton(socket.AF_INET6, "ff02::2") == + inet_pton(socket.AF_INET6, str)) + + +def in6_getscope(addr): + """ + Returns the scope of the address. + """ + if in6_isgladdr(addr): + scope = IPV6_ADDR_GLOBAL + elif in6_islladdr(addr): + scope = IPV6_ADDR_LINKLOCAL + elif in6_issladdr(addr): + scope = IPV6_ADDR_SITELOCAL + elif in6_ismaddr(addr): + scope = IPV6_ADDR_MULTICAST + elif addr == '::1': + scope = IPV6_ADDR_LOOPBACK + else: + scope = -1 + return scope + +############################################################################# +############################################################################# +### IPv6 Class ### +############################################################################# +############################################################################# + +class IP6Field(Field): + def __init__(self, name, default): + Field.__init__(self, name, default, "16s") + def h2i(self, pkt, x): + if type(x) is str: + try: + x = in6_ptop(x) + except socket.error: + x = Net6(x) + elif type(x) is list: + x = map(Net6, x) + return x + def i2m(self, pkt, x): + return inet_pton(socket.AF_INET6, x) + def m2i(self, pkt, x): + return inet_ntop(socket.AF_INET6, x) + def any2i(self, pkt, x): + return self.h2i(pkt,x) + def i2repr(self, pkt, x): + if x is None: + return self.i2h(pkt,x) + elif not isinstance(x, Net6) and not type(x) is list: + if in6_isaddrTeredo(x): # print Teredo info + server, flag, maddr, mport = teredoAddrExtractInfo(x) + return "%s [Teredo srv: %s cli: %s:%s]" % (self.i2h(pkt, x), server, maddr,mport) + elif in6_isaddr6to4(x): # print encapsulated address + vaddr = in6_6to4ExtractAddr(x) + return "%s [6to4 GW: %s]" % (self.i2h(pkt, x), vaddr) + return self.i2h(pkt, x) # No specific information to return + +class SourceIP6Field(IP6Field): + def __init__(self, name, dstname): + IP6Field.__init__(self, name, None) + self.dstname = dstname + def i2m(self, pkt, x): + if x is None: + dst=getattr(pkt,self.dstname) + iff,x,nh = conf.route6.route(dst) + return IP6Field.i2m(self, pkt, x) + def i2h(self, pkt, x): + if x is None: + dst=getattr(pkt,self.dstname) + if isinstance(dst,Gen): + r = map(conf.route6.route, dst) + r.sort() + if r[0] == r[-1]: + x=r[0][1] + else: + warning("More than one possible route for %s"%repr(dst)) + return None + else: + iff,x,nh = conf.route6.route(dst) + return IP6Field.i2h(self, pkt, x) + +ipv6nh = { 0:"Hop-by-Hop Option Header", + 4:"IP", + 6:"TCP", + 17:"UDP", + 41:"IPv6", + 43:"Routing Header", + 44:"Fragment Header", + 47:"GRE", + 50:"ESP Header", + 51:"AH Header", + 58:"ICMPv6", + 59:"No Next Header", + 60:"Destination Option Header", + 135:"Mobility Header"} + +ipv6nhcls = { 0: "IPv6ExtHdrHopByHop", + 4: "IP", + 6: "TCP", + 17: "UDP", + 43: "IPv6ExtHdrRouting", + 44: "IPv6ExtHdrFragment", + #50: "IPv6ExtHrESP", + #51: "IPv6ExtHdrAH", + 58: "ICMPv6Unknown", + 59: "Raw", + 60: "IPv6ExtHdrDestOpt" } + +class IP6ListField(StrField): + islist = 1 + def __init__(self, name, default, count_from=None, length_from=None): + if default is None: + default = [] + StrField.__init__(self, name, default) + self.count_from = count_from + self.length_from = length_from + + def i2len(self, pkt, i): + return 16*len(i) + + def i2count(self, pkt, i): + if type(i) is list: + return len(i) + return 0 + + def getfield(self, pkt, s): + c = l = None + if self.length_from is not None: + l = self.length_from(pkt) + elif self.count_from is not None: + c = self.count_from(pkt) + + lst = [] + ret = "" + remain = s + if l is not None: + remain,ret = s[:l],s[l:] + while remain: + if c is not None: + if c <= 0: + break + c -= 1 + addr = inet_ntop(socket.AF_INET6, remain[:16]) + lst.append(addr) + remain = remain[16:] + return remain+ret,lst + + def i2m(self, pkt, x): + s = '' + for y in x: + try: + y = inet_pton(socket.AF_INET6, y) + except: + y = socket.getaddrinfo(y, None, socket.AF_INET6)[0][-1][0] + y = inet_pton(socket.AF_INET6, y) + s += y + return s + + def i2repr(self,pkt,x): + s = [] + if x == None: + return "[]" + for y in x: + s.append('%s' % y) + return "[ %s ]" % (", ".join(s)) + +class _IPv6GuessPayload: + name = "Dummy class that implements guess_payload_class() for IPv6" + def default_payload_class(self,p): + if self.nh == 58 and len(p) > 2: + t = ord(p[0]) + if t == 139 or t == 140: # Node Info Query + return _niquery_guesser(p) + return get_cls(icmp6typescls.get(t,"Raw"), "Raw") + elif self.nh == 135 and len(p) > 3: + return _mip6_mhtype2cls.get(ord(p[2]), MIP6MH_Generic) + else: + return get_cls(ipv6nhcls.get(self.nh,"Raw"), "Raw") + +class IPv6(_IPv6GuessPayload, Packet, IPTools): + name = "IPv6" + fields_desc = [ BitField("version" , 6 , 4), + BitField("tc", 0, 8), #TODO: IPv6, ByteField ? + BitField("fl", 0, 20), + ShortField("plen", None), + ByteEnumField("nh", 59, ipv6nh), + ByteField("hlim", 64), + SourceIP6Field("src", "dst"), # dst is for src @ selection + IP6Field("dst", "::1") ] + def mysummary(self): + return "%s > %s (%i)" % (self.src,self.dst, self.nh) + + def post_build(self, p, pay): + p += pay + if self.plen is None: + l = len(p) - 40 + p = p[:4]+struct.pack("!H", l)+p[6:] + return p + + def extract_padding(self, s): + l = self.plen + return s[:l], s[l:] + + def hashret(self): + if self.nh == 58 and isinstance(self.payload, _ICMPv6): + if self.payload.type < 128: + return self.payload.payload.hashret() + elif (self.payload.type in [133,134,135,136,144,145]): + return struct.pack("B", self.nh)+self.payload.hashret() + + nh = self.nh + sd = self.dst + ss = self.src + if self.nh == 43 and isinstance(self.payload, IPv6ExtHdrRouting): + # With routing header, the destination is the last + # address of the IPv6 list if segleft > 0 + nh = self.payload.nh + try: + sd = self.addresses[-1] + except IndexError: + sd = '::1' + # TODO: big bug with ICMPv6 error messages as the destination of IPerror6 + # could be anything from the original list ... + if 1: + sd = inet_pton(socket.AF_INET6, sd) + for a in self.addresses: + a = inet_pton(socket.AF_INET6, a) + sd = strxor(sd, a) + sd = inet_ntop(socket.AF_INET6, sd) + + if self.nh == 44 and isinstance(self.payload, IPv6ExtHdrFragment): + nh = self.payload.nh + + if self.nh == 0 and isinstance(self.payload, IPv6ExtHdrHopByHop): + nh = self.payload.nh + + if self.nh == 60 and isinstance(self.payload, IPv6ExtHdrDestOpt): + foundhao = None + for o in self.payload.options: + if isinstance(o, HAO): + foundhao = o + if foundhao: + nh = self.payload.nh # XXX what if another extension follows ? + ss = foundhao.hoa + + if conf.checkIPsrc and conf.checkIPaddr: + sd = inet_pton(socket.AF_INET6, sd) + ss = inet_pton(socket.AF_INET6, self.src) + return struct.pack("B",nh)+self.payload.hashret() + else: + return struct.pack("B", nh)+self.payload.hashret() + + def answers(self, other): + if not isinstance(other, IPv6): # self is reply, other is request + return False + if conf.checkIPaddr: + ss = inet_pton(socket.AF_INET6, self.src) + sd = inet_pton(socket.AF_INET6, self.dst) + os = inet_pton(socket.AF_INET6, other.src) + od = inet_pton(socket.AF_INET6, other.dst) + # request was sent to a multicast address (other.dst) + # Check reply destination addr matches request source addr (i.e + # sd == os) except when reply is multicasted too + # XXX test mcast scope matching ? + if in6_ismaddr(other.dst): + if in6_ismaddr(self.dst): + if ((od == sd) or + (in6_isaddrllallnodes(self.dst) and in6_isaddrllallservers(other.dst))): + return self.payload.answers(other.payload) + return False + if (os == sd): + return self.payload.answers(other.payload) + return False + elif (sd != os): # or ss != od): <- removed for ICMP errors + return False + if self.nh == 58 and isinstance(self.payload, _ICMPv6) and self.payload.type < 128: + # ICMPv6 Error message -> generated by IPv6 packet + # Note : at the moment, we jump the ICMPv6 specific class + # to call answers() method of erroneous packet (over + # initial packet). There can be cases where an ICMPv6 error + # class could implement a specific answers method that perform + # a specific task. Currently, don't see any use ... + return self.payload.payload.answers(other) + elif other.nh == 0 and isinstance(other.payload, IPv6ExtHdrHopByHop): + return self.payload.answers(other.payload.payload) + elif other.nh == 44 and isinstance(other.payload, IPv6ExtHdrFragment): + return self.payload.answers(other.payload.payload) + elif other.nh == 43 and isinstance(other.payload, IPv6ExtHdrRouting): + return self.payload.answers(other.payload.payload) # Buggy if self.payload is a IPv6ExtHdrRouting + elif other.nh == 60 and isinstance(other.payload, IPv6ExtHdrDestOpt): + return self.payload.payload.answers(other.payload.payload) + elif self.nh == 60 and isinstance(self.payload, IPv6ExtHdrDestOpt): # BU in reply to BRR, for instance + return self.payload.payload.answers(other.payload) + else: + if (self.nh != other.nh): + return False + return self.payload.answers(other.payload) + +import scapy +scapy.IPv6 = IPv6 + +class IPerror6(IPv6): + name = "IPv6 in ICMPv6" + def answers(self, other): + if not isinstance(other, IPv6): + return False + sd = inet_pton(socket.AF_INET6, self.dst) + ss = inet_pton(socket.AF_INET6, self.src) + od = inet_pton(socket.AF_INET6, other.dst) + os = inet_pton(socket.AF_INET6, other.src) + + # Make sure that the ICMPv6 error is related to the packet scapy sent + if isinstance(self.underlayer, _ICMPv6) and self.underlayer.type < 128: + + # find upper layer for self (possible citation) + selfup = self.payload + while selfup is not None and isinstance(selfup, _IPv6ExtHdr): + selfup = selfup.payload + + # find upper layer for other (initial packet). Also look for RH + otherup = other.payload + request_has_rh = False + while otherup is not None and isinstance(otherup, _IPv6ExtHdr): + if isinstance(otherup, IPv6ExtHdrRouting): + request_has_rh = True + otherup = otherup.payload + + if ((ss == os and sd == od) or # <- Basic case + (ss == os and request_has_rh)): # <- Request has a RH : + # don't check dst address + + # Let's deal with possible MSS Clamping + if (isinstance(selfup, TCP) and + isinstance(otherup, TCP) and + selfup.options != otherup.options): # seems clamped + + # Save fields modified by MSS clamping + old_otherup_opts = otherup.options + old_otherup_cksum = otherup.chksum + old_otherup_dataofs = otherup.dataofs + old_selfup_opts = selfup.options + old_selfup_cksum = selfup.chksum + old_selfup_dataofs = selfup.dataofs + + # Nullify them + otherup.options = [] + otherup.chksum = 0 + otherup.dataofs = 0 + selfup.options = [] + selfup.chksum = 0 + selfup.dataofs = 0 + + # Test it and save result + s1 = str(selfup) + s2 = str(otherup) + l = min(len(s1), len(s2)) + res = s1[:l] == s2[:l] + + # recall saved values + otherup.options = old_otherup_opts + otherup.chksum = old_otherup_cksum + otherup.dataofs = old_otherup_dataofs + selfup.options = old_selfup_opts + selfup.chksum = old_selfup_cksum + selfup.dataofs = old_selfup_dataofs + + return res + + s1 = str(selfup) + s2 = str(otherup) + l = min(len(s1), len(s2)) + return s1[:l] == s2[:l] + + return False + + def mysummary(self): + return Packet.mysummary(self) + + +############################################################################# +############################################################################# +### Upper Layer Checksum computation ### +############################################################################# +############################################################################# + +class PseudoIPv6(Packet): # IPv6 Pseudo-header for checksum computation + name = "Pseudo IPv6 Header" + fields_desc = [ IP6Field("src", "::"), + IP6Field("dst", "::"), + ShortField("uplen", None), + BitField("zero", 0, 24), + ByteField("nh", 0) ] + +def in6_chksum(nh, u, p): + """ + Performs IPv6 Upper Layer checksum computation. Provided parameters are: + + - 'nh' : value of upper layer protocol + - 'u' : upper layer instance (TCP, UDP, ICMPv6*, ). Instance must be + provided with all under layers (IPv6 and all extension headers, + for example) + - 'p' : the payload of the upper layer provided as a string + + Functions operate by filling a pseudo header class instance (PseudoIPv6) + with + - Next Header value + - the address of _final_ destination (if some Routing Header with non + segleft field is present in underlayer classes, last address is used.) + - the address of _real_ source (basically the source address of an + IPv6 class instance available in the underlayer or the source address + in HAO option if some Destination Option header found in underlayer + includes this option). + - the length is the length of provided payload string ('p') + """ + + ph6 = PseudoIPv6() + ph6.nh = nh + rthdr = 0 + hahdr = 0 + final_dest_addr_found = 0 + while u != None and not isinstance(u, IPv6): + if (isinstance(u, IPv6ExtHdrRouting) and + u.segleft != 0 and len(u.addresses) != 0 and + final_dest_addr_found == 0): + rthdr = u.addresses[-1] + final_dest_addr_found = 1 + elif (isinstance(u, IPv6ExtHdrDestOpt) and (len(u.options) == 1) and + isinstance(u.options[0], HAO)): + hahdr = u.options[0].hoa + u = u.underlayer + if u is None: + warning("No IPv6 underlayer to compute checksum. Leaving null.") + return 0 + if hahdr: + ph6.src = hahdr + else: + ph6.src = u.src + if rthdr: + ph6.dst = rthdr + else: + ph6.dst = u.dst + ph6.uplen = len(p) + ph6s = str(ph6) + return checksum(ph6s+p) + + +############################################################################# +############################################################################# +### Extension Headers ### +############################################################################# +############################################################################# + + +# Inherited by all extension header classes +class _IPv6ExtHdr(_IPv6GuessPayload, Packet): + name = 'Abstract IPV6 Option Header' + aliastypes = [IPv6, IPerror6] # TODO ... + +scapy._IPv6OptionHeader = _IPv6ExtHdr + + +#################### IPv6 options for Extension Headers ##################### + +_hbhopts = { 0x00: "Pad1", + 0x01: "PadN", + 0x04: "Tunnel Encapsulation Limit", + 0x05: "Router Alert", + 0x06: "Quick-Start", + 0xc2: "Jumbo Payload", + 0xc9: "Home Address Option" } + +class _OTypeField(ByteEnumField): + """ + Modified BytEnumField that displays information regarding the IPv6 option + based on its option type value (What should be done by nodes that process + the option if they do not understand it ...) + + It is used by Jumbo, Pad1, PadN, RouterAlert, HAO options + """ + pol = {0x00: "00: skip", + 0x40: "01: discard", + 0x80: "10: discard+ICMP", + 0xC0: "11: discard+ICMP not mcast"} + + enroutechange = {0x00: "0: Don't change en-route", + 0x20: "1: May change en-route" } + + def i2repr(self, pkt, x): + s = self.i2s.get(x, repr(x)) + polstr = self.pol[(x & 0xC0)] + enroutechangestr = self.enroutechange[(x & 0x20)] + return "%s [%s, %s]" % (s, polstr, enroutechangestr) + +class HBHOptUnknown(Packet): # IPv6 Hop-By-Hop Option + name = "Scapy6 Unknown Option" + fields_desc = [_OTypeField("otype", 0x01, _hbhopts), + FieldLenField("optlen", None, length_of="optdata", fmt="B"), + StrLenField("optdata", "", + length_from = lambda pkt: pkt.optlen) ] + def alignment_delta(self, curpos): # By default, no alignment requirement + """ + As specified in section 4.2 of RFC 2460, every options has + an alignment requirement ususally expressed xn+y, meaning + the Option Type must appear at an integer multiple of x octest + from the start of the header, plus y octet. + + That function is provided the current position from the + start of the header and returns required padding length. + """ + return 0 + +class Pad1(Packet): # IPv6 Hop-By-Hop Option + name = "Pad1" + fields_desc = [ _OTypeField("otype", 0x00, _hbhopts) ] + def alignment_delta(self, curpos): # No alignment requirement + return 0 + +class PadN(Packet): # IPv6 Hop-By-Hop Option + name = "PadN" + fields_desc = [_OTypeField("otype", 0x01, _hbhopts), + FieldLenField("optlen", None, length_of="optdata", fmt="B"), + StrLenField("optdata", "", + length_from = lambda pkt: pkt.optlen)] + def alignment_delta(self, curpos): # No alignment requirement + return 0 + +class RouterAlert(Packet): # RFC 2711 - IPv6 Hop-By-Hop Option + name = "Router Alert" + fields_desc = [_OTypeField("otype", 0x05, _hbhopts), + ByteField("optlen", 2), + ShortEnumField("value", None, + { 0: "Datagram contains a MLD message", + 1: "Datagram contains RSVP message", + 2: "Datagram contains an Active Network message" }) ] + # TODO : Check IANA has not defined new values for value field of RouterAlertOption + # TODO : now that we have that option, we should do something in MLD class that need it + def alignment_delta(self, curpos): # alignment requirement : 2n+0 + x = 2 ; y = 0 + delta = x*((curpos - y + x - 1)/x) + y - curpos + return delta + +class Jumbo(Packet): # IPv6 Hop-By-Hop Option + name = "Jumbo Payload" + fields_desc = [_OTypeField("otype", 0xC2, _hbhopts), + ByteField("optlen", 4), + IntField("jumboplen", None) ] + def alignment_delta(self, curpos): # alignment requirement : 4n+2 + x = 4 ; y = 2 + delta = x*((curpos - y + x - 1)/x) + y - curpos + return delta + +class HAO(Packet): # IPv6 Destination Options Header Option + name = "Home Address Option" + fields_desc = [_OTypeField("otype", 0xC9, _hbhopts), + ByteField("optlen", 16), + IP6Field("hoa", "::") ] + def alignment_delta(self, curpos): # alignment requirement : 8n+6 + x = 8 ; y = 6 + delta = x*((curpos - y + x - 1)/x) + y - curpos + return delta + +_hbhoptcls = { 0x00: Pad1, + 0x01: PadN, + 0x05: RouterAlert, + 0xC2: Jumbo, + 0xC9: HAO } + + +######################## Hop-by-Hop Extension Header ######################## + +class _HopByHopOptionsField(PacketListField): + islist = 1 + holds_packet = 1 + def __init__(self, name, default, cls, curpos, count_from=None, length_from=None): + self.curpos = curpos + PacketListField.__init__(self, name, default, cls, count_from=count_from, length_from=length_from) + + def i2len(self, pkt, i): + l = len(self.i2m(pkt, i)) + return l + + def i2count(self, pkt, i): + if type(i) is list: + return len(i) + return 0 + + def getfield(self, pkt, s): + c = l = None + if self.length_from is not None: + l = self.length_from(pkt) + elif self.count_from is not None: + c = self.count_from(pkt) + + opt = [] + ret = "" + x = s + if l is not None: + x,ret = s[:l],s[l:] + while x: + if c is not None: + if c <= 0: + break + c -= 1 + o = ord(x[0]) # Option type + cls = self.cls + if _hbhoptcls.has_key(o): + cls = _hbhoptcls[o] + try: + op = cls(x) + except: + op = self.cls(x) + opt.append(op) + if isinstance(op.payload, Raw): + x = op.payload.load + del(op.payload) + else: + x = "" + return x+ret,opt + + def i2m(self, pkt, x): + autopad = None + try: + autopad = getattr(pkt, "autopad") # Hack : 'autopad' phantom field + except: + autopad = 1 + + if not autopad: + return "".join(map(str, x)) + + curpos = self.curpos + s = "" + for p in x: + d = p.alignment_delta(curpos) + curpos += d + if d == 1: + s += str(Pad1()) + elif d != 0: + s += str(PadN(optdata='\x00'*(d-2))) + pstr = str(p) + curpos += len(pstr) + s += pstr + + # Let's make the class including our option field + # a multiple of 8 octets long + d = curpos % 8 + if d == 0: + return s + d = 8 - d + if d == 1: + s += str(Pad1()) + elif d != 0: + s += str(PadN(optdata='\x00'*(d-2))) + + return s + + def addfield(self, pkt, s, val): + return s+self.i2m(pkt, val) + +class _PhantomAutoPadField(ByteField): + def addfield(self, pkt, s, val): + return s + + def getfield(self, pkt, s): + return s, 1 + + def i2repr(self, pkt, x): + if x: + return "On" + return "Off" + + +class IPv6ExtHdrHopByHop(_IPv6ExtHdr): + name = "IPv6 Extension Header - Hop-by-Hop Options Header" + fields_desc = [ ByteEnumField("nh", 59, ipv6nh), + FieldLenField("len", None, length_of="options", fmt="B", + adjust = lambda pkt,x: (x+2+7)/8 - 1), + _PhantomAutoPadField("autopad", 1), # autopad activated by default + _HopByHopOptionsField("options", [], HBHOptUnknown, 2, + length_from = lambda pkt: (8*(pkt.len+1))-2) ] + overload_fields = {IPv6: { "nh": 0 }} + + +######################## Destination Option Header ########################## + +class IPv6ExtHdrDestOpt(_IPv6ExtHdr): + name = "IPv6 Extension Header - Destination Options Header" + fields_desc = [ ByteEnumField("nh", 59, ipv6nh), + FieldLenField("len", None, length_of="options", fmt="B", + adjust = lambda pkt,x: (x+2+7)/8 - 1), + _PhantomAutoPadField("autopad", 1), # autopad activated by default + _HopByHopOptionsField("options", [], HBHOptUnknown, 2, + length_from = lambda pkt: (8*(pkt.len+1))-2) ] + overload_fields = {IPv6: { "nh": 60 }} + + +############################# Routing Header ################################ + +class IPv6ExtHdrRouting(_IPv6ExtHdr): + name = "IPv6 Option Header Routing" + fields_desc = [ ByteEnumField("nh", 59, ipv6nh), + FieldLenField("len", None, count_of="addresses", fmt="B", + adjust = lambda pkt,x:2*x), # in 8 bytes blocks + ByteField("type", 0), + ByteField("segleft", None), + BitField("reserved", 0, 32), # There is meaning in this field ... + IP6ListField("addresses", [], + length_from = lambda pkt: 8*pkt.len)] + overload_fields = {IPv6: { "nh": 43 }} + + def post_build(self, pkt, pay): + if self.segleft is None: + pkt = pkt[:3]+struct.pack("B", len(self.addresses))+pkt[4:] + return _IPv6ExtHdr.post_build(self, pkt, pay) + +########################### Fragmentation Header ############################ + +class IPv6ExtHdrFragment(_IPv6ExtHdr): + name = "IPv6 Extension Header - Fragmentation header" + fields_desc = [ ByteEnumField("nh", 59, ipv6nh), + BitField("res1", 0, 8), + BitField("offset", 0, 13), + BitField("res2", 0, 2), + BitField("m", 0, 1), + IntField("id", None) ] + overload_fields = {IPv6: { "nh": 44 }} + + +def defragment6(pktlist): + """ + Performs defragmentation of a list of IPv6 packets. Packets are reordered. + Crap is dropped. What lacks is completed by 'X' characters. + """ + + l = filter(lambda x: IPv6ExtHdrFragment in x, pktlist) # remove non fragments + if not l: + return [] + + id = l[0][IPv6ExtHdrFragment].id + + llen = len(l) + l = filter(lambda x: x[IPv6ExtHdrFragment].id == id, l) + if len(l) != llen: + warning("defragment6: some fragmented packets have been removed from list") + llen = len(l) + + # reorder fragments + i = 0 + res = [] + while l: + min_pos = 0 + min_offset = l[0][IPv6ExtHdrFragment].offset + for p in l: + cur_offset = p[IPv6ExtHdrFragment].offset + if cur_offset < min_offset: + min_pos = 0 + min_offset = cur_offset + res.append(l[min_pos]) + del(l[min_pos]) + + # regenerate the fragmentable part + fragmentable = "" + for p in res: + q=p[IPv6ExtHdrFragment] + offset = 8*q.offset + if offset != len(fragmentable): + warning("Expected an offset of %d. Found %d. Padding with XXXX" % (len(fragmentable), offset)) + fragmentable += "X"*(offset - len(fragmentable)) + fragmentable += str(q.payload) + + # Regenerate the unfragmentable part. + q = res[0] + nh = q[IPv6ExtHdrFragment].nh + q[IPv6ExtHdrFragment].underlayer.nh = nh + q[IPv6ExtHdrFragment].underlayer.payload = None + q /= Raw(load=fragmentable) + + return IPv6(str(q)) + + +def fragment6(pkt, fragSize): + """ + Performs fragmentation of an IPv6 packet. Provided packet ('pkt') must already + contain an IPv6ExtHdrFragment() class. 'fragSize' argument is the expected + maximum size of fragments (MTU). The list of packets is returned. + + If packet does not contain an IPv6ExtHdrFragment class, it is returned in + result list. + """ + + pkt = pkt.copy() + s = str(pkt) # for instantiation to get upper layer checksum right + + if len(s) <= fragSize: + return [pkt] + + if not IPv6ExtHdrFragment in pkt: + # TODO : automatically add a fragment before upper Layer + # at the moment, we do nothing and return initial packet + # as single element of a list + return [pkt] + + # Fragmentable part : fake IPv6 for Fragmentable part length computation + fragPart = pkt[IPv6ExtHdrFragment].payload + tmp = str(IPv6(src="::1", dst="::1")/fragPart) + fragPartLen = len(tmp) - 40 # basic IPv6 header length + fragPartStr = s[-fragPartLen:] + + # Grab Next Header for use in Fragment Header + nh = IPv6(tmp[:40]).nh + + # Keep fragment header + fragHeader = pkt[IPv6ExtHdrFragment] + fragHeader.payload = None # detach payload + + # Unfragmentable Part + unfragPartLen = len(s) - fragPartLen - 8 + unfragPart = pkt + pkt[IPv6ExtHdrFragment].underlayer.payload = None # detach payload + + # Cut the fragmentable part to fit fragSize. Inner fragments have + # a length that is an integer multiple of 8 octets. last Frag MTU + # can be anything below MTU + lastFragSize = fragSize - unfragPartLen - 8 + innerFragSize = lastFragSize - (lastFragSize % 8) + + if lastFragSize <= 0 or innerFragSize == 0: + warning("Provided fragment size value is too low. " + + "Should be more than %d" % (unfragPartLen + 8)) + return [unfragPart/fragHeader/fragPart] + + remain = fragPartStr + res = [] + fragOffset = 0 # offset, incremeted during creation + fragId = random.randint(0,0xffffffff) # random id ... + if fragHeader.id is not None: # ... except id provided by user + fragId = fragHeader.id + fragHeader.m = 1 + fragHeader.id = fragId + fragHeader.nh = nh + + # Main loop : cut, fit to FRAGSIZEs, fragOffset, Id ... + while True: + if (len(remain) > lastFragSize): + tmp = remain[:innerFragSize] + remain = remain[innerFragSize:] + fragHeader.offset = fragOffset # update offset + fragOffset += (innerFragSize / 8) # compute new one + if IPv6 in unfragPart: + unfragPart[IPv6].plen = None + tempo = unfragPart/fragHeader/Raw(load=tmp) + res.append(tempo) + else: + fragHeader.offset = fragOffset # update offSet + fragHeader.m = 0 + if IPv6 in unfragPart: + unfragPart[IPv6].plen = None + tempo = unfragPart/fragHeader/Raw(load=remain) + res.append(tempo) + break + return res + + +############################### AH Header ################################### + +# class _AHFieldLenField(FieldLenField): +# def getfield(self, pkt, s): +# l = getattr(pkt, self.fld) +# l = (l*8)-self.shift +# i = self.m2i(pkt, s[:l]) +# return s[l:],i + +# class _AHICVStrLenField(StrLenField): +# def i2len(self, pkt, x): + + + +# class IPv6ExtHdrAH(_IPv6ExtHdr): +# name = "IPv6 Extension Header - AH" +# fields_desc = [ ByteEnumField("nh", 59, ipv6nh), +# _AHFieldLenField("len", None, "icv"), +# ShortField("res", 0), +# IntField("spi", 0), +# IntField("sn", 0), +# _AHICVStrLenField("icv", None, "len", shift=2) ] +# overload_fields = {IPv6: { "nh": 51 }} + +# def post_build(self, pkt, pay): +# if self.len is None: +# pkt = pkt[0]+struct.pack("!B", 2*len(self.addresses))+pkt[2:] +# if self.segleft is None: +# pkt = pkt[:3]+struct.pack("!B", len(self.addresses))+pkt[4:] +# return _IPv6ExtHdr.post_build(self, pkt, pay) + + +############################### ESP Header ################################## + +# class IPv6ExtHdrESP(_IPv6extHdr): +# name = "IPv6 Extension Header - ESP" +# fields_desc = [ IntField("spi", 0), +# IntField("sn", 0), +# # there is things to extract from IKE work +# ] +# overloads_fields = {IPv6: { "nh": 50 }} + + + +############################################################################# +############################################################################# +### ICMPv6* Classes ### +############################################################################# +############################################################################# + +icmp6typescls = { 1: "ICMPv6DestUnreach", + 2: "ICMPv6PacketTooBig", + 3: "ICMPv6TimeExceeded", + 4: "ICMPv6ParamProblem", + 128: "ICMPv6EchoRequest", + 129: "ICMPv6EchoReply", + 130: "ICMPv6MLQuery", + 131: "ICMPv6MLReport", + 132: "ICMPv6MLDone", + 133: "ICMPv6ND_RS", + 134: "ICMPv6ND_RA", + 135: "ICMPv6ND_NS", + 136: "ICMPv6ND_NA", + 137: "ICMPv6ND_Redirect", + #138: Do Me - RFC 2894 - Seems painful + 139: "ICMPv6NIQuery", + 140: "ICMPv6NIReply", + 141: "ICMPv6ND_INDSol", + 142: "ICMPv6ND_INDAdv", + #143: Do Me - RFC 3810 + 144: "ICMPv6HAADRequest", + 145: "ICMPv6HAADReply", + 146: "ICMPv6MPSol", + 147: "ICMPv6MPAdv", + #148: Do Me - SEND related - RFC 3971 + #149: Do Me - SEND related - RFC 3971 + 151: "ICMPv6MRD_Advertisement", + 152: "ICMPv6MRD_Solicitation", + 153: "ICMPv6MRD_Termination", + } + +icmp6types = { 1 : "Destination unreachable", + 2 : "Packet too big", + 3 : "Time exceeded", + 4 : "Parameter problem", + 100 : "Private Experimentation", + 101 : "Private Experimentation", + 128 : "Echo Request", + 129 : "Echo Reply", + 130 : "MLD Query", + 131 : "MLD Report", + 132 : "MLD Done", + 133 : "Router Solicitation", + 134 : "Router Advertisement", + 135 : "Neighbor Solicitation", + 136 : "Neighbor Advertisement", + 137 : "Redirect Message", + 138 : "Router Renumbering", + 139 : "ICMP Node Information Query", + 140 : "ICMP Node Information Response", + 141 : "Inverse Neighbor Discovery Solicitation Message", + 142 : "Inverse Neighbor Discovery Advertisement Message", + 143 : "Version 2 Multicast Listener Report", + 144 : "Home Agent Address Discovery Request Message", + 145 : "Home Agent Address Discovery Reply Message", + 146 : "Mobile Prefix Solicitation", + 147 : "Mobile Prefix Advertisement", + 148 : "Certification Path Solicitation", + 149 : "Certification Path Advertisement", + 151 : "Multicast Router Advertisement", + 152 : "Multicast Router Solicitation", + 153 : "Multicast Router Termination", + 200 : "Private Experimentation", + 201 : "Private Experimentation" } + + +class _ICMPv6(Packet): + name = "ICMPv6 dummy class" + overload_fields = {IPv6: {"nh": 58}} + def post_build(self, p, pay): + p += pay + if self.cksum == None: + chksum = in6_chksum(58, self.underlayer, p) + p = p[:2]+struct.pack("!H", chksum)+p[4:] + return p + + def hashret(self): + return self.payload.hashret() + + def answers(self, other): + # isinstance(self.underlayer, _IPv6ExtHdr) may introduce a bug ... + if (isinstance(self.underlayer, IPerror6) or + isinstance(self.underlayer, _IPv6ExtHdr) and + isinstance(other, _ICMPv6)): + if not ((self.type == other.type) and + (self.code == other.code)): + return 0 + return 1 + return 0 + + +class _ICMPv6Error(_ICMPv6): + name = "ICMPv6 errors dummy class" + def guess_payload_class(self,p): + return IPerror6 + +class ICMPv6Unknown(_ICMPv6): + name = "Scapy6 ICMPv6 fallback class" + fields_desc = [ ByteEnumField("type",1, icmp6types), + ByteField("code",0), + XShortField("cksum", None), + StrField("msgbody", "")] + + +################################## RFC 2460 ################################# + +class ICMPv6DestUnreach(_ICMPv6Error): + name = "ICMPv6 Destination Unreachable" + fields_desc = [ ByteEnumField("type",1, icmp6types), + ByteEnumField("code",0, { 0: "No route to destination", + 1: "Communication with destination administratively prohibited", + 2: "Beyond scope of source address", + 3: "Address unreachable", + 4: "Port unreachable" }), + XShortField("cksum", None), + XIntField("unused",0x00000000)] + +class ICMPv6PacketTooBig(_ICMPv6Error): + name = "ICMPv6 Packet Too Big" + fields_desc = [ ByteEnumField("type",2, icmp6types), + ByteField("code",0), + XShortField("cksum", None), + IntField("mtu",1280)] + +class ICMPv6TimeExceeded(_ICMPv6Error): + name = "ICMPv6 Time Exceeded" + fields_desc = [ ByteEnumField("type",3, icmp6types), + ByteField("code",{ 0: "hop limit exceeded in transit", + 1: "fragment reassembly time exceeded"}), + XShortField("cksum", None), + XIntField("unused",0x00000000)] + +# The default pointer value is set to the next header field of +# the encapsulated IPv6 packet +class ICMPv6ParamProblem(_ICMPv6Error): + name = "ICMPv6 Parameter Problem" + fields_desc = [ ByteEnumField("type",4, icmp6types), + ByteEnumField("code",0, {0: "erroneous header field encountered", + 1: "unrecognized Next Header type encountered", + 2: "unrecognized IPv6 option encountered"}), + XShortField("cksum", None), + IntField("ptr",6)] + +class ICMPv6EchoRequest(_ICMPv6): + name = "ICMPv6 Echo Request" + fields_desc = [ ByteEnumField("type", 128, icmp6types), + ByteField("code", 0), + XShortField("cksum", None), + XShortField("id",0), + XShortField("seq",0), + StrField("data", "")] + def mysummary(self): + return self.sprintf("%name% (id: %id% seq: %seq%)") + def hashret(self): + return struct.pack("HH",self.id,self.seq)+self.payload.hashret() + + +class ICMPv6EchoReply(ICMPv6EchoRequest): + name = "ICMPv6 Echo Reply" + __metaclass__ = NewDefaultValues + type = 129 + def answers(self, other): + # We could match data content between request and reply. + return (isinstance(other, ICMPv6EchoRequest) and + self.id == other.id and self.seq == other.seq and + self.data == other.data) + + +############ ICMPv6 Multicast Listener Discovery (RFC3810) ################## + +# tous les messages MLD sont emis avec une adresse source lien-locale +# -> Y veiller dans le post_build si aucune n'est specifiee +# La valeur de Hop-Limit doit etre de 1 +# "and an IPv6 Router Alert option in a Hop-by-Hop Options +# header. (The router alert option is necessary to cause routers to +# examine MLD messages sent to multicast addresses in which the router +# itself has no interest" +class _ICMPv6ML(_ICMPv6): + fields_desc = [ ByteEnumField("type", 130, icmp6types), + ByteField("code", 0), + XShortField("cksum", None), + ShortField("mrd", 0), + ShortField("reserved", 0), + IP6Field("mladdr",None)] + +# general queries are sent to the link-scope all-nodes multicast +# address ff02::1, with a multicast address field of 0 and a MRD of +# [Query Response Interval] +# Default value for mladdr is set to 0 for a General Query, and +# overloaded by the user for a Multicast Address specific query +# TODO : See what we can do to automatically include a Router Alert +# Option in a Destination Option Header. +class ICMPv6MLQuery(_ICMPv6ML): # RFC 2710 + name = "MLD - Multicast Listener Query" + __metaclass__ = NewDefaultValues + type = 130 + mrd = 10000 + mladdr = "::" # 10s for mrd + overload_fields = {IPv6: { "dst": "ff02::1", "hlim": 1 }} + def hashret(self): + if self.mladdr != "::": + return struct.pack("HH",self.mladdr)+self.payload.hashret() + else: + return self.payload.hashret() + + +# TODO : See what we can do to automatically include a Router Alert +# Option in a Destination Option Header. +class ICMPv6MLReport(_ICMPv6ML): # RFC 2710 + name = "MLD - Multicast Listener Report" + __metaclass__ = NewDefaultValues + type = 131 + overload_fields = {IPv6: {"hlim": 1}} + # implementer le hashret et le answers + +# When a node ceases to listen to a multicast address on an interface, +# it SHOULD send a single Done message to the link-scope all-routers +# multicast address (FF02::2), carrying in its multicast address field +# the address to which it is ceasing to listen +# TODO : See what we can do to automatically include a Router Alert +# Option in a Destination Option Header. +class ICMPv6MLDone(_ICMPv6ML): # RFC 2710 + name = "MLD - Multicast Listener Done" + __metaclass__ = NewDefaultValues + type = 132 + overload_fields = {IPv6: { "dst": "ff02::2", "hlim": 1}} + + +########## ICMPv6 MRD - Multicast Router Discovery (RFC 4286) ############### + +# TODO: +# - 04/09/06 troglocan : find a way to automatically add a router alert +# option for all MRD packets. This could be done in a specific +# way when IPv6 is the under layer with some specific keyword +# like 'exthdr'. This would allow to keep compatibility with +# providing IPv6 fields to be overloaded in fields_desc. +# +# At the moment, if user inserts an IPv6 Router alert option +# none of the IPv6 default values of IPv6 layer will be set. + +class ICMPv6MRD_Advertisement(_ICMPv6): + name = "ICMPv6 Multicast Router Discovery Advertisement" + fields_desc = [ByteEnumField("type", 151, icmp6types), + ByteField("advinter", 20), + XShortField("cksum", None), + ShortField("queryint", 0), + ShortField("robustness", 0)] + overload_fields = {IPv6: { "nh": 58, "hlim": 1, "dst": "ff02::2"}} + # IPv6 Router Alert requires manual inclusion + def extract_padding(self, s): + return s[:8], s[8:] + +class ICMPv6MRD_Solicitation(_ICMPv6): + name = "ICMPv6 Multicast Router Discovery Solicitation" + fields_desc = [ByteEnumField("type", 152, icmp6types), + ByteField("res", 0), + XShortField("cksum", None) ] + overload_fields = {IPv6: { "nh": 58, "hlim": 1, "dst": "ff02::2"}} + # IPv6 Router Alert requires manual inclusion + def extract_padding(self, s): + return s[:4], s[4:] + +class ICMPv6MRD_Termination(_ICMPv6): + name = "ICMPv6 Multicast Router Discovery Termination" + fields_desc = [ByteEnumField("type", 153, icmp6types), + ByteField("res", 0), + XShortField("cksum", None) ] + overload_fields = {IPv6: { "nh": 58, "hlim": 1, "dst": "ff02::6A"}} + # IPv6 Router Alert requires manual inclusion + def extract_padding(self, s): + return s[:4], s[4:] + + +################### ICMPv6 Neighbor Discovery (RFC 2461) #################### + +icmp6ndopts = { 1: "Source Link-Layer Address", + 2: "Target Link-Layer Address", + 3: "Prefix Information", + 4: "Redirected Header", + 5: "MTU", + 6: "NBMA Shortcut Limit Option", # RFC2491 + 7: "Advertisement Interval Option", + 8: "Home Agent Information Option", + 9: "Source Address List", + 10: "Target Address List", + 11: "CGA Option", # RFC 3971 + 12: "RSA Signature Option", # RFC 3971 + 13: "Timestamp Option", # RFC 3971 + 14: "Nonce option", # RFC 3971 + 15: "Trust Anchor Option", # RFC 3971 + 16: "Certificate Option", # RFC 3971 + 17: "IP Address Option", # RFC 4068 + 18: "New Router Prefix Information Option", # RFC 4068 + 19: "Link-layer Address Option", # RFC 4068 + 20: "Neighbor Advertisement Acknowledgement Option", + 21: "CARD Request Option", # RFC 4065/4066/4067 + 22: "CARD Reply Option", # RFC 4065/4066/4067 + 23: "MAP Option", # RFC 4140 + 24: "Route Information Option", # RFC 4191 + 25: "Recusive DNS Server Option", + 26: "IPv6 Router Advertisement Flags Option" + } + +icmp6ndoptscls = { 1: "ICMPv6NDOptSrcLLAddr", + 2: "ICMPv6NDOptDstLLAddr", + 3: "ICMPv6NDOptPrefixInfo", + 4: "ICMPv6NDOptRedirectedHdr", + 5: "ICMPv6NDOptMTU", + 6: "ICMPv6NDOptShortcutLimit", + 7: "ICMPv6NDOptAdvInterval", + 8: "ICMPv6NDOptHAInfo", + 9: "ICMPv6NDOptSrcAddrList", + 10: "ICMPv6NDOptTgtAddrList", + #11: Do Me, + #12: Do Me, + #13: Do Me, + #14: Do Me, + #15: Do Me, + #16: Do Me, + 17: "ICMPv6NDOptIPAddr", + 18: "ICMPv6NDOptNewRtrPrefix", + 19: "ICMPv6NDOptLLA", + #18: Do Me, + #19: Do Me, + #20: Do Me, + #21: Do Me, + #22: Do Me, + 23: "ICMPv6NDOptMAP", + 24: "ICMPv6NDOptRouteInfo", + 25: "ICMPv6NDOptRDNSS", + 26: "ICMPv6NDOptEFA" + } + +class _ICMPv6NDGuessPayload: + name = "Dummy ND class that implements guess_payload_class()" + def guess_payload_class(self,p): + if len(p) > 1: + return get_cls(icmp6ndoptscls.get(ord(p[0]),"Raw"), "Raw") # s/Raw/ICMPv6NDOptUnknown/g ? + + +# Beginning of ICMPv6 Neighbor Discovery Options. + +class ICMPv6NDOptUnknown(_ICMPv6NDGuessPayload, Packet): + name = "ICMPv6 Neighbor Discovery Option - Scapy Unimplemented" + fields_desc = [ ByteField("type",None), + FieldLenField("len",None,length_of="data",fmt="B", + adjust = lambda pkt,x: x+2), + StrLenField("data","", + length_from = lambda pkt: pkt.len-2) ] + +# NOTE: len includes type and len field. Expressed in unit of 8 bytes +# TODO: Revoir le coup du ETHER_ANY +class ICMPv6NDOptSrcLLAddr(_ICMPv6NDGuessPayload, Packet): + name = "ICMPv6 Neighbor Discovery Option - Source Link-Layer Address" + fields_desc = [ ByteField("type", 1), + ByteField("len", 1), + MACField("lladdr", ETHER_ANY) ] + def mysummary(self): + return self.sprintf("%name% %lladdr%") + +class ICMPv6NDOptDstLLAddr(ICMPv6NDOptSrcLLAddr): + name = "ICMPv6 Neighbor Discovery Option - Destination Link-Layer Address" + __metaclass__ = NewDefaultValues + type = 2 + +class ICMPv6NDOptPrefixInfo(_ICMPv6NDGuessPayload, Packet): + name = "ICMPv6 Neighbor Discovery Option - Prefix Information" + fields_desc = [ ByteField("type",3), + ByteField("len",4), + ByteField("prefixlen",None), + BitField("L",1,1), + BitField("A",1,1), + BitField("R",0,1), + BitField("res1",0,5), + XIntField("validlifetime",0xffffffffL), + XIntField("preferredlifetime",0xffffffffL), + XIntField("res2",0x00000000), + IP6Field("prefix","::") ] + def mysummary(self): + return self.sprintf("%name% %prefix%") + +# TODO: We should also limit the size of included packet to something +# like (initiallen - 40 - 2) +class TruncPktLenField(PacketLenField): + + def __init__(self, name, default, cls, cur_shift, length_from=None, shift=0): + PacketLenField.__init__(self, name, default, cls, length_from=length_from) + self.cur_shift = cur_shift + + def getfield(self, pkt, s): + l = self.length_from(pkt) + i = self.m2i(pkt, s[:l]) + return s[l:],i + + def m2i(self, pkt, m): + s = None + try: # It can happen we have sth shorter than 40 bytes + s = self.cls(m) + except: + return Raw(m) + return s + + def i2m(self, pkt, x): + s = str(x) + l = len(s) + r = (l + self.cur_shift) % 8 + l = l - r + return s[:l] + + def i2len(self, pkt, i): + return len(self.i2m(pkt, i)) + + +# Faire un post_build pour le recalcul de la taille (en multiple de 8 octets) +class ICMPv6NDOptRedirectedHdr(_ICMPv6NDGuessPayload, Packet): + name = "ICMPv6 Neighbor Discovery Option - Redirected Header" + fields_desc = [ ByteField("type",4), + FieldLenField("len", None, length_of="pkt", fmt="B", + adjust = lambda pkt,x:(x+4)/8), + XShortField("res",0), + TruncPktLenField("pkt", "", IPv6, 4, + length_from = lambda pkt: 8*pkt.len-4) ] + +# See which value should be used for default MTU instead of 1280 +class ICMPv6NDOptMTU(_ICMPv6NDGuessPayload, Packet): + name = "ICMPv6 Neighbor Discovery Option - MTU" + fields_desc = [ ByteField("type",5), + ByteField("len",1), + XShortField("res",0), + IntField("mtu",1280)] + +class ICMPv6NDOptShortcutLimit(_ICMPv6NDGuessPayload, Packet): # RFC 2491 + name = "ICMPv6 Neighbor Discovery Option - NBMA Shortcut Limit" + fields_desc = [ ByteField("type", 6), + ByteField("len", 1), + ByteField("shortcutlim", 40), # XXX + ByteField("res1", 0), + IntField("res2", 0) ] + +class ICMPv6NDOptAdvInterval(_ICMPv6NDGuessPayload, Packet): + name = "ICMPv6 Neighbor Discovery - Interval Advertisement" + fields_desc = [ ByteField("type",7), + ByteField("len",1), + ShortField("res", 0), + IntField("advint", 0) ] + def mysummary(self): + return self.sprintf("%name% %advint% milliseconds") + +class ICMPv6NDOptHAInfo(_ICMPv6NDGuessPayload, Packet): + name = "ICMPv6 Neighbor Discovery - Home Agent Information" + fields_desc = [ ByteField("type",8), + ByteField("len",1), + ShortField("res", 0), + ShortField("pref", 0), + ShortField("lifetime", 1)] + def mysummary(self): + return self.sprintf("%name% %pref% %lifetime% seconds") + +# type 9 : See ICMPv6NDOptSrcAddrList class below in IND (RFC 3122) support + +# type 10 : See ICMPv6NDOptTgtAddrList class below in IND (RFC 3122) support + +class ICMPv6NDOptIPAddr(_ICMPv6NDGuessPayload, Packet): # RFC 4068 + name = "ICMPv6 Neighbor Discovery - IP Address Option (FH for MIPv6)" + fields_desc = [ ByteField("type",17), + ByteField("len", 3), + ByteEnumField("optcode", 1, {1: "Old Care-Of Address", + 2: "New Care-Of Address", + 3: "NAR's IP address" }), + ByteField("plen", 64), + IntField("res", 0), + IP6Field("addr", "::") ] + +class ICMPv6NDOptNewRtrPrefix(_ICMPv6NDGuessPayload, Packet): # RFC 4068 + name = "ICMPv6 Neighbor Discovery - New Router Prefix Information Option (FH for MIPv6)" + fields_desc = [ ByteField("type",18), + ByteField("len", 3), + ByteField("optcode", 0), + ByteField("plen", 64), + IntField("res", 0), + IP6Field("prefix", "::") ] + +_rfc4068_lla_optcode = {0: "Wildcard requesting resolution for all nearby AP", + 1: "LLA for the new AP", + 2: "LLA of the MN", + 3: "LLA of the NAR", + 4: "LLA of the src of TrSolPr or PrRtAdv msg", + 5: "AP identified by LLA belongs to current iface of router", + 6: "No preifx info available for AP identified by the LLA", + 7: "No fast handovers support for AP identified by the LLA" } + +class ICMPv6NDOptLLA(_ICMPv6NDGuessPayload, Packet): # RFC 4068 + name = "ICMPv6 Neighbor Discovery - Link-Layer Address (LLA) Option (FH for MIPv6)" + fields_desc = [ ByteField("type", 19), + ByteField("len", 1), + ByteEnumField("optcode", 0, _rfc4068_lla_optcode), + MACField("lla", ETHER_ANY) ] # We only support ethernet + +class ICMPv6NDOptMAP(_ICMPv6NDGuessPayload, Packet): # RFC 4140 + name = "ICMPv6 Neighbor Discovery - MAP Option" + fields_desc = [ ByteField("type", 23), + ByteField("len", 3), + BitField("dist", 1, 4), + BitField("pref", 15, 4), # highest availability + BitField("R", 1, 1), + BitField("res", 0, 7), + IntField("validlifetime", 0xffffffff), + IP6Field("addr", "::") ] + + +class IP6PrefixField(IP6Field): + def __init__(self, name, default): + IP6Field.__init__(self, name, default) + self.length_from = lambda pkt: 8*(pkt.len - 1) + + def addfield(self, pkt, s, val): + return s + self.i2m(pkt, val) + + def getfield(self, pkt, s): + l = self.length_from(pkt) + p = s[:l] + if l < 16: + p += '\x00'*(16-l) + return s[l:], self.m2i(pkt,p) + + def i2len(self, pkt, x): + return len(self.i2m(pkt, x)) + + def i2m(self, pkt, x): + l = pkt.len + + if x is None: + x = "::" + if l is None: + l = 1 + x = inet_pton(socket.AF_INET6, x) + + if l is None: + return x + if l in [0, 1]: + return "" + if l in [2, 3]: + return x[:8*(l-1)] + + return x + '\x00'*8*(l-3) + +class ICMPv6NDOptRouteInfo(_ICMPv6NDGuessPayload, Packet): # RFC 4191 + name = "ICMPv6 Neighbor Discovery Option - Route Information Option" + fields_desc = [ ByteField("type",24), + FieldLenField("len", None, length_of="prefix", fmt="B", + adjust = lambda pkt,x: x/8 + 1), + ByteField("plen", None), + BitField("res1",0,3), + BitField("prf",0,2), + BitField("res2",0,3), + IntField("rtlifetime", 0xffffffff), + IP6PrefixField("prefix", None) ] + +class ICMPv6NDOptRDNSS(_ICMPv6NDGuessPayload, Packet): # RFC 5006 + name = "ICMPv6 Neighbor Discovery Option - Recursive DNS Server Option" + fields_desc = [ ByteField("type", 25), + FieldLenField("len", None, count_of="dns", fmt="B", + adjust = lambda pkt,x: 2*x+1), + ShortField("res", None), + IntField("lifetime", 0xffffffff), + IP6ListField("dns", [], + length_from = lambda pkt: 8*(pkt.len-1)) ] + +class ICMPv6NDOptEFA(_ICMPv6NDGuessPayload, Packet): # RFC 5175 (prev. 5075) + name = "ICMPv6 Neighbor Discovery Option - Expanded Flags Option" + fields_desc = [ ByteField("type", 26), + ByteField("len", 1), + BitField("res", 0, 48) ] + +# End of ICMPv6 Neighbor Discovery Options. + +class ICMPv6ND_RS(_ICMPv6NDGuessPayload, _ICMPv6): + name = "ICMPv6 Neighbor Discovery - Router Solicitation" + fields_desc = [ ByteEnumField("type", 133, icmp6types), + ByteField("code",0), + XShortField("cksum", None), + IntField("res",0) ] + overload_fields = {IPv6: { "nh": 58, "dst": "ff02::2", "hlim": 255 }} + +class ICMPv6ND_RA(_ICMPv6NDGuessPayload, _ICMPv6): + name = "ICMPv6 Neighbor Discovery - Router Advertisement" + fields_desc = [ ByteEnumField("type", 134, icmp6types), + ByteField("code",0), + XShortField("cksum", None), + ByteField("chlim",0), + BitField("M",0,1), + BitField("O",0,1), + BitField("H",0,1), + BitEnumField("prf",1,2, { 0: "Medium (default)", + 1: "High", + 2: "Reserved", + 3: "Low" } ), # RFC 4191 + BitField("P",0,1), + BitField("res",0,2), + ShortField("routerlifetime",1800), + IntField("reachabletime",0), + IntField("retranstimer",0) ] + overload_fields = {IPv6: { "nh": 58, "dst": "ff02::1", "hlim": 255 }} + + def answers(self, other): + return isinstance(other, ICMPv6ND_RS) + +class ICMPv6ND_NS(_ICMPv6NDGuessPayload, _ICMPv6, Packet): + name = "ICMPv6 Neighbor Discovery - Neighbor Solicitation" + fields_desc = [ ByteEnumField("type",135, icmp6types), + ByteField("code",0), + XShortField("cksum", None), + BitField("R",0,1), + BitField("S",0,1), + BitField("O",0,1), + XBitField("res",0,29), + IP6Field("tgt","::") ] + overload_fields = {IPv6: { "nh": 58, "dst": "ff02::1", "hlim": 255 }} + + def mysummary(self): + return self.sprintf("%name% (tgt: %tgt%)") + + def hashret(self): + return self.tgt+self.payload.hashret() + +class ICMPv6ND_NA(ICMPv6ND_NS): + name = "ICMPv6 Neighbor Discovery - Neighbor Advertisement" + __metaclass__ = NewDefaultValues + type = 136 + R = 1 + O = 1 + + def answers(self, other): + return isinstance(other, ICMPv6ND_NS) and self.tgt == other.tgt + +# associated possible options : target link-layer option, Redirected header +class ICMPv6ND_Redirect(_ICMPv6NDGuessPayload, _ICMPv6, Packet): + name = "ICMPv6 Neighbor Discovery - Redirect" + fields_desc = [ ByteEnumField("type",137, icmp6types), + ByteField("code",0), + XShortField("cksum", None), + XIntField("res",0), + IP6Field("tgt","::"), + IP6Field("dst","::") ] + overload_fields = {IPv6: { "nh": 58, "dst": "ff02::1", "hlim": 255 }} + + + +################ ICMPv6 Inverse Neighbor Discovery (RFC 3122) ############### + +class ICMPv6NDOptSrcAddrList(_ICMPv6NDGuessPayload, Packet): + name = "ICMPv6 Inverse Neighbor Discovery Option - Source Address List" + fields_desc = [ ByteField("type",9), + FieldLenField("len", None, count_of="addrlist", fmt="B", + adjust = lambda pkt,x: 2*x+1), + StrFixedLenField("res", "\x00"*6, 6), + IP6ListField("addrlist", [], + length_from = lambda pkt: 8*(pkt.len-1)) ] + +class ICMPv6NDOptTgtAddrList(ICMPv6NDOptSrcAddrList): + name = "ICMPv6 Inverse Neighbor Discovery Option - Target Address List" + __metaclass__ = NewDefaultValues + type = 10 + + +# RFC3122 +# Options requises : source lladdr et target lladdr +# Autres options valides : source address list, MTU +# - Comme precise dans le document, il serait bien de prendre l'adresse L2 +# demandee dans l'option requise target lladdr et l'utiliser au niveau +# de l'adresse destination ethernet si aucune adresse n'est precisee +# - ca semble pas forcement pratique si l'utilisateur doit preciser toutes +# les options. +# Ether() must use the target lladdr as destination +class ICMPv6ND_INDSol(_ICMPv6NDGuessPayload, _ICMPv6): + name = "ICMPv6 Inverse Neighbor Discovery Solicitation" + fields_desc = [ ByteEnumField("type",141, icmp6types), + ByteField("code",0), + XShortField("cksum",None), + XIntField("reserved",0) ] + overload_fields = {IPv6: { "nh": 58, "dst": "ff02::1", "hlim": 255 }} + +# Options requises : target lladdr, target address list +# Autres options valides : MTU +class ICMPv6ND_INDAdv(_ICMPv6NDGuessPayload, _ICMPv6): + name = "ICMPv6 Inverse Neighbor Discovery Advertisement" + fields_desc = [ ByteEnumField("type",142, icmp6types), + ByteField("code",0), + XShortField("cksum",None), + XIntField("reserved",0) ] + overload_fields = {IPv6: { "nh": 58, "dst": "ff02::1", "hlim": 255 }} + +############################################################################# +### LLMNR (RFC4795) ### +############################################################################# +# LLMNR is based on the DNS packet format (RFC1035 Section 4) +# RFC also envisions LLMNR over TCP. Like vista, we don't support it -- arno + +_LLMNR_IPv6_mcast_Addr = "FF02:0:0:0:0:0:1:3" +_LLMNR_IPv4_mcast_addr = "224.0.0.252" + +class LLMNRQuery(Packet): + name = "Link Local Multicast Node Resolution - Query" + fields_desc = [ ShortField("id", 0), + BitField("qr", 0, 1), + BitEnumField("opcode", 0, 4, { 0:"QUERY" }), + BitField("c", 0, 1), + BitField("tc", 0, 2), + BitField("z", 0, 4), + BitEnumField("rcode", 0, 4, { 0:"ok" }), + DNSRRCountField("qdcount", None, "qd"), + DNSRRCountField("ancount", None, "an"), + DNSRRCountField("nscount", None, "ns"), + DNSRRCountField("arcount", None, "ar"), + DNSQRField("qd", "qdcount"), + DNSRRField("an", "ancount"), + DNSRRField("ns", "nscount"), + DNSRRField("ar", "arcount",0)] + overload_fields = {UDP: {"sport": 5355, "dport": 5355 }} + def hashret(self): + return struct.pack("!H", id) + +class LLMNRResponse(LLMNRQuery): + name = "Link Local Multicast Node Resolution - Response" + __metaclass__ = NewDefaultValues + qr = 1 + fields_desc = [] + + def answers(self, other): + return (isinstance(other, LLMNRQuery) and + self.id == other.id and + self.qr == 1 and + other.qr == 0) + +def _llmnr_dispatcher(x, *args, **kargs): + cls = Raw + if len(x) >= 3: + if (ord(x[4]) & 0x80): # Response + cls = LLMNRResponse + else: # Query + cls = LLMNRQuery + return cls(x, *args, **kargs) + +bind_bottom_up(UDP, _llmnr_dispatcher, { "dport": 5355 }) +bind_bottom_up(UDP, _llmnr_dispatcher, { "sport": 5355 }) + +# LLMNRQuery(id=RandShort(), qd=DNSQR(qname="vista."))) + + + + +############################################################################### +# ICMPv6 Node Information Queries (RFC 4620) +############################################################################### + +# [ ] Add automatic destination address computation using computeNIGroupAddr +# in IPv6 class (Scapy6 modification when integrated) if : +# - it is not provided +# - upper layer is ICMPv6NIQueryName() with a valid value +# [ ] Try to be liberal in what we accept as internal values for _explicit_ +# DNS elements provided by users. Any string should be considered +# valid and kept like it has been provided. At the moment, i2repr() will +# crash on many inputs +# [ ] Do the documentation +# [ ] Add regression tests +# [ ] Perform test against real machines (NOOP reply is proof of implementation). +# [ ] Check if there are differences between different stacks. Among *BSD, +# with others. +# [ ] Deal with flags in a consistent way. +# [ ] Implement compression in names2dnsrepr() and decompresiion in +# dnsrepr2names(). Should be deactivable. + +icmp6_niqtypes = { 0: "NOOP", + 2: "Node Name", + 3: "IPv6 Address", + 4: "IPv4 Address" } + + +class _ICMPv6NIHashret: + def hashret(self): + return self.nonce + +class _ICMPv6NIAnswers: + def answers(self, other): + return self.nonce == other.nonce + +# Buggy; always returns the same value during a session +class NonceField(StrFixedLenField): + def __init__(self, name, default=None): + StrFixedLenField.__init__(self, name, default, 8) + if default is None: + self.default = self.randval() + +# Compute the NI group Address. Can take a FQDN as input parameter +def computeNIGroupAddr(name): + import md5 + name = name.lower().split(".")[0] + record = chr(len(name))+name + h = md5.new(record) + h = h.digest() + addr = "ff02::2:%2x%2x:%2x%2x" % struct.unpack("BBBB", h[:4]) + return addr + + +# Here is the deal. First, that protocol is a piece of shit. Then, we +# provide 4 classes for the different kinds of Requests (one for every +# valid qtype: NOOP, Node Name, IPv6@, IPv4@). They all share the same +# data field class that is made to be smart by guessing the specifc +# type of value provided : +# +# - IPv6 if acceptable for inet_pton(AF_INET6, ): code is set to 0, +# if not overriden by user +# - IPv4 if acceptable for inet_pton(AF_INET, ): code is set to 2, +# if not overriden +# - Name in the other cases: code is set to 0, if not overriden by user +# +# Internal storage, is not only the value, but the a pair providing +# the type and the value (1 is IPv6@, 1 is Name or string, 2 is IPv4@) +# +# Note : I merged getfield() and m2i(). m2i() should not be called +# directly anyway. Same remark for addfield() and i2m() +# +# -- arno + +# "The type of information present in the Data field of a query is +# declared by the ICMP Code, whereas the type of information in a +# Reply is determined by the Qtype" + +def names2dnsrepr(x): + """ + Take as input a list of DNS names or a single DNS name + and encode it in DNS format (with possible compression) + If a string that is already a DNS name in DNS format + is passed, it is returned unmodified. Result is a string. + !!! At the moment, compression is not implemented !!! + """ + + if type(x) is str: + if x and x[-1] == '\x00': # stupid heuristic + return x + x = [x] + + res = [] + for n in x: + termin = "\x00" + if n.count('.') == 0: # single-component gets one more + termin += '\x00' + n = "".join(map(lambda y: chr(len(y))+y, n.split("."))) + termin + res.append(n) + return "".join(res) + + +def dnsrepr2names(x): + """ + Take as input a DNS encoded string (possibly compressed) + and returns a list of DNS names contained in it. + If provided string is already in printable format + (does not end with a null character, a one element list + is returned). Result is a list. + """ + res = [] + cur = "" + while x: + l = ord(x[0]) + x = x[1:] + if l == 0: + if cur and cur[-1] == '.': + cur = cur[:-1] + res.append(cur) + cur = "" + if x and ord(x[0]) == 0: # single component + x = x[1:] + continue + if l & 0xc0: # XXX TODO : work on that -- arno + raise Exception("DNS message can't be compressed at this point!") + else: + cur += x[:l]+"." + x = x[l:] + return res + + +class NIQueryDataField(StrField): + def __init__(self, name, default): + StrField.__init__(self, name, default) + + def i2h(self, pkt, x): + if x is None: + return x + t,val = x + if t == 1: + val = dnsrepr2names(val)[0] + return val + + def h2i(self, pkt, x): + if x is tuple and type(x[0]) is int: + return x + + val = None + try: # Try IPv6 + inet_pton(socket.AF_INET6, x) + val = (0, x) + except: + try: # Try IPv4 + inet_pton(socket.AF_INET, x) + val = (2, x) + except: # Try DNS + if x is None: + x = "" + x = names2dnsrepr(x) + val = (1, x) + return val + + def i2repr(self, pkt, x): + t,val = x + if t == 1: # DNS Name + # we don't use dnsrepr2names() to deal with + # possible weird data extracted info + res = [] + weird = None + while val: + l = ord(val[0]) + val = val[1:] + if l == 0: + if (len(res) > 1 and val): # fqdn with data behind + weird = val + elif len(val) > 1: # single label with data behind + weird = val[1:] + break + res.append(val[:l]+".") + val = val[l:] + tmp = "".join(res) + if tmp and tmp[-1] == '.': + tmp = tmp[:-1] + return tmp + return repr(val) + + def getfield(self, pkt, s): + qtype = getattr(pkt, "qtype") + if qtype == 0: # NOOP + return s, (0, "") + else: + code = getattr(pkt, "code") + if code == 0: # IPv6 Addr + return s[16:], (0, inet_ntop(socket.AF_INET6, s[:16])) + elif code == 2: # IPv4 Addr + return s[4:], (2, inet_ntop(socket.AF_INET, s[:4])) + else: # Name or Unknown + return "", (1, s) + + def addfield(self, pkt, s, val): + if ((type(val) is tuple and val[1] is None) or + val is None): + val = (1, "") + t = val[0] + if t == 1: + return s + val[1] + elif t == 0: + return s + inet_pton(socket.AF_INET6, val[1]) + else: + return s + inet_pton(socket.AF_INET, val[1]) + +class NIQueryCodeField(ByteEnumField): + def i2m(self, pkt, x): + if x is None: + d = pkt.getfieldval("data") + if d is None: + return 1 + elif d[0] == 0: # IPv6 address + return 0 + elif d[0] == 1: # Name + return 1 + elif d[0] == 2: # IPv4 address + return 2 + else: + return 1 + return x + + +_niquery_code = {0: "IPv6 Query", 1: "Name Query", 2: "IPv4 Query"} + +#_niquery_flags = { 2: "All unicast addresses", 4: "IPv4 addresses", +# 8: "Link-local addresses", 16: "Site-local addresses", +# 32: "Global addresses" } + +# "This NI type has no defined flags and never has a Data Field". Used +# to know if the destination is up and implements NI protocol. +class ICMPv6NIQueryNOOP(_ICMPv6NIHashret, _ICMPv6): + name = "ICMPv6 Node Information Query - NOOP Query" + fields_desc = [ ByteEnumField("type", 139, icmp6types), + NIQueryCodeField("code", None, _niquery_code), + XShortField("cksum", None), + ShortEnumField("qtype", 0, icmp6_niqtypes), + BitField("unused", 0, 10), + FlagsField("flags", 0, 6, "TACLSG"), + NonceField("nonce", None), + NIQueryDataField("data", None) ] + +class ICMPv6NIQueryName(ICMPv6NIQueryNOOP): + name = "ICMPv6 Node Information Query - IPv6 Name Query" + __metaclass__ = NewDefaultValues + qtype = 2 + +# We ask for the IPv6 address of the peer +class ICMPv6NIQueryIPv6(ICMPv6NIQueryNOOP): + name = "ICMPv6 Node Information Query - IPv6 Address Query" + __metaclass__ = NewDefaultValues + qtype = 3 + flags = 0x3E + +class ICMPv6NIQueryIPv4(ICMPv6NIQueryNOOP): + name = "ICMPv6 Node Information Query - IPv4 Address Query" + __metaclass__ = NewDefaultValues + qtype = 4 + +_nireply_code = { 0: "Successful Reply", + 1: "Response Refusal", + 3: "Unknown query type" } + +_nireply_flags = { 1: "Reply set incomplete", + 2: "All unicast addresses", + 4: "IPv4 addresses", + 8: "Link-local addresses", + 16: "Site-local addresses", + 32: "Global addresses" } + +# Internal repr is one of those : +# (0, "some string") : unknow qtype value are mapped to that one +# (3, [ (ttl, ip6), ... ]) +# (4, [ (ttl, ip4), ... ]) +# (2, [ttl, dns_names]) : dns_names is one string that contains +# all the DNS names. Internally it is kept ready to be sent +# (undissected). i2repr() decode it for user. This is to +# make build after dissection bijective. +# +# I also merged getfield() and m2i(), and addfield() and i2m(). +class NIReplyDataField(StrField): + + def i2h(self, pkt, x): + if x is None: + return x + t,val = x + if t == 2: + ttl, dnsnames = val + val = [ttl] + dnsrepr2names(dnsnames) + return val + + def h2i(self, pkt, x): + qtype = 0 # We will decode it as string if not + # overridden through 'qtype' in pkt + + # No user hint, let's use 'qtype' value for that purpose + if type(x) is not tuple: + if pkt is not None: + qtype = getattr(pkt, "qtype") + else: + qtype = x[0] + x = x[1] + + # From that point on, x is the value (second element of the tuple) + + if qtype == 2: # DNS name + if type(x) is str: # listify the string + x = [x] + if type(x) is list and x and type(x[0]) is not int: # ttl was omitted : use 0 + x = [0] + x + ttl = x[0] + names = x[1:] + return (2, [ttl, names2dnsrepr(names)]) + + elif qtype in [3, 4]: # IPv4 or IPv6 addr + if type(x) is str: + x = [x] # User directly provided an IP, instead of list + + # List elements are not tuples, user probably + # omitted ttl value : we will use 0 instead + def addttl(x): + if type(x) is str: + return (0, x) + return x + + return (qtype, map(addttl, x)) + + return (qtype, x) + + + def addfield(self, pkt, s, val): + t,tmp = val + if tmp is None: + tmp = "" + if t == 2: + ttl,dnsstr = tmp + return s+ struct.pack("!I", ttl) + dnsstr + elif t == 3: + return s + "".join(map(lambda (x,y): struct.pack("!I", x)+inet_pton(socket.AF_INET6, y), tmp)) + elif t == 4: + return s + "".join(map(lambda (x,y): struct.pack("!I", x)+inet_pton(socket.AF_INET, y), tmp)) + else: + return s + tmp + + def getfield(self, pkt, s): + code = getattr(pkt, "code") + if code != 0: + return s, (0, "") + + qtype = getattr(pkt, "qtype") + if qtype == 0: # NOOP + return s, (0, "") + + elif qtype == 2: + if len(s) < 4: + return s, (0, "") + ttl = struct.unpack("!I", s[:4])[0] + return "", (2, [ttl, s[4:]]) + + elif qtype == 3: # IPv6 addresses with TTLs + # XXX TODO : get the real length + res = [] + while len(s) >= 20: # 4 + 16 + ttl = struct.unpack("!I", s[:4])[0] + ip = inet_ntop(socket.AF_INET6, s[4:20]) + res.append((ttl, ip)) + s = s[20:] + return s, (3, res) + + elif qtype == 4: # IPv4 addresses with TTLs + # XXX TODO : get the real length + res = [] + while len(s) >= 8: # 4 + 4 + ttl = struct.unpack("!I", s[:4])[0] + ip = inet_ntop(socket.AF_INET, s[4:8]) + res.append((ttl, ip)) + s = s[8:] + return s, (4, res) + else: + # XXX TODO : implement me and deal with real length + return "", (0, s) + + def i2repr(self, pkt, x): + if x is None: + return "[]" + + if type(x) is tuple and len(x) == 2: + t, val = x + if t == 2: # DNS names + ttl,l = val + l = dnsrepr2names(l) + return "ttl:%d %s" % (ttl, ", ".join(l)) + elif t == 3 or t == 4: + return "[ %s ]" % (", ".join(map(lambda (x,y): "(%d, %s)" % (x, y), val))) + return repr(val) + return repr(x) # XXX should not happen + +# By default, sent responses have code set to 0 (successful) +class ICMPv6NIReplyNOOP(_ICMPv6NIAnswers, _ICMPv6NIHashret, _ICMPv6): + name = "ICMPv6 Node Information Reply - NOOP Reply" + fields_desc = [ ByteEnumField("type", 140, icmp6types), + ByteEnumField("code", 0, _nireply_code), + XShortField("cksum", None), + ShortEnumField("qtype", 0, icmp6_niqtypes), + BitField("unused", 0, 10), + FlagsField("flags", 0, 6, "TACLSG"), + NonceField("nonce", None), + NIReplyDataField("data", None)] + +class ICMPv6NIReplyName(ICMPv6NIReplyNOOP): + name = "ICMPv6 Node Information Reply - Node Names" + __metaclass__ = NewDefaultValues + qtype = 2 + +class ICMPv6NIReplyIPv6(ICMPv6NIReplyNOOP): + name = "ICMPv6 Node Information Reply - IPv6 addresses" + __metaclass__ = NewDefaultValues + qtype = 3 + +class ICMPv6NIReplyIPv4(ICMPv6NIReplyNOOP): + name = "ICMPv6 Node Information Reply - IPv4 addresses" + __metaclass__ = NewDefaultValues + qtype = 4 + +class ICMPv6NIReplyRefuse(ICMPv6NIReplyNOOP): + name = "ICMPv6 Node Information Reply - Responder refuses to supply answer" + __metaclass__ = NewDefaultValues + code = 1 + +class ICMPv6NIReplyUnknown(ICMPv6NIReplyNOOP): + name = "ICMPv6 Node Information Reply - Qtype unknown to the responder" + __metaclass__ = NewDefaultValues + code = 2 + + +def _niquery_guesser(p): + cls = Raw + type = ord(p[0]) + if type == 139: # Node Info Query specific stuff + if len(p) > 6: + qtype, = struct.unpack("!H", p[4:6]) + cls = { 0: ICMPv6NIQueryNOOP, + 2: ICMPv6NIQueryName, + 3: ICMPv6NIQueryIPv6, + 4: ICMPv6NIQueryIPv4 }.get(qtype, Raw) + elif type == 140: # Node Info Reply specific stuff + code = ord(p[1]) + if code == 0: + if len(p) > 6: + qtype, = struct.unpack("!H", p[4:6]) + cls = { 2: ICMPv6NIReplyName, + 3: ICMPv6NIReplyIPv6, + 4: ICMPv6NIReplyIPv4 }.get(qtype, ICMPv6NIReplyNOOP) + elif code == 1: + cls = ICMPv6NIReplyRefuse + elif code == 2: + cls = ICMPv6NIReplyUnknown + return cls + +############################################################################# +############################################################################# +### DHCPv6 ### +############################################################################# +############################################################################# + +All_DHCP_Relay_Agents_and_Servers = "ff02::1:2" +All_DHCP_Servers = "ff05::1:3" # Site-Local scope : deprecated by 3879 + +dhcp6opts = { 1: "CLIENTID", + 2: "SERVERID", + 3: "IA_NA", + 4: "IA_TA", + 5: "IAADDR", + 6: "ORO", + 7: "PREFERENCE", + 8: "ELAPSED_TIME", + 9: "RELAY_MSG", + 11: "AUTH", + 12: "UNICAST", + 13: "STATUS_CODE", + 14: "RAPID_COMMIT", + 15: "USER_CLASS", + 16: "VENDOR_CLASS", + 17: "VENDOR_OPTS", + 18: "INTERFACE_ID", + 19: "RECONF_MSG", + 20: "RECONF_ACCEPT", + 21: "SIP Servers Domain Name List", #RFC3319 + 22: "SIP Servers IPv6 Address List", #RFC3319 + 23: "DNS Recursive Name Server Option", #RFC3646 + 24: "Domain Search List option", #RFC3646 + 25: "OPTION_IA_PD", #RFC3633 + 26: "OPTION_IAPREFIX", #RFC3633 + 27: "OPTION_NIS_SERVERS", #RFC3898 + 28: "OPTION_NISP_SERVERS", #RFC3898 + 29: "OPTION_NIS_DOMAIN_NAME", #RFC3898 + 30: "OPTION_NISP_DOMAIN_NAME", #RFC3898 + 31: "OPTION_SNTP_SERVERS", #RFC4075 + 32: "OPTION_INFORMATION_REFRESH_TIME", #RFC4242 + 33: "OPTION_BCMCS_SERVER_D", #RFC4280 + 34: "OPTION_BCMCS_SERVER_A", #RFC4280 + 36: "OPTION_GEOCONF_CIVIC", #RFC-ietf-geopriv-dhcp-civil-09.txt + 37: "OPTION_REMOTE_ID", #RFC4649 + 38: "OPTION_SUBSCRIBER_ID", #RFC4580 + 39: "OPTION_CLIENT_FQDN" } #RFC4704 + +dhcp6opts_by_code = { 1: "DHCP6OptClientId", + 2: "DHCP6OptServerId", + 3: "DHCP6OptIA_NA", + 4: "DHCP6OptIA_TA", + 5: "DHCP6OptIAAddress", + 6: "DHCP6OptOptReq", + 7: "DHCP6OptPref", + 8: "DHCP6OptElapsedTime", + 9: "DHCP6OptRelayMsg", + 11: "DHCP6OptAuth", + 12: "DHCP6OptServerUnicast", + 13: "DHCP6OptStatusCode", + 14: "DHCP6OptRapidCommit", + 15: "DHCP6OptUserClass", + 16: "DHCP6OptVendorClass", + 17: "DHCP6OptVendorSpecificInfo", + 18: "DHCP6OptIfaceId", + 19: "DHCP6OptReconfMsg", + 20: "DHCP6OptReconfAccept", + 21: "DHCP6OptSIPDomains", #RFC3319 + 22: "DHCP6OptSIPServers", #RFC3319 + 23: "DHCP6OptDNSServers", #RFC3646 + 24: "DHCP6OptDNSDomains", #RFC3646 + 25: "DHCP6OptIA_PD", #RFC3633 + 26: "DHCP6OptIAPrefix", #RFC3633 + 27: "DHCP6OptNISServers", #RFC3898 + 28: "DHCP6OptNISPServers", #RFC3898 + 29: "DHCP6OptNISDomain", #RFC3898 + 30: "DHCP6OptNISPDomain", #RFC3898 + 31: "DHCP6OptSNTPServers", #RFC4075 + 32: "DHCP6OptInfoRefreshTime", #RFC4242 + 33: "DHCP6OptBCMCSDomains", #RFC4280 + 34: "DHCP6OptBCMCSServers", #RFC4280 + #36: "DHCP6OptGeoConf", #RFC-ietf-geopriv-dhcp-civil-09.txt + 37: "DHCP6OptRemoteID", #RFC4649 + 38: "DHCP6OptSubscriberID", #RFC4580 + 39: "DHCP6OptClientFQDN", #RFC4704 + #40: "DHCP6OptPANAAgent", #RFC-ietf-dhc-paa-option-05.txt + #41: "DHCP6OptNewPOSIXTimeZone, #RFC4833 + #42: "DHCP6OptNewTZDBTimeZone, #RFC4833 + 43: "DHCP6OptRelayAgentERO" #RFC4994 + #44: "DHCP6OptLQQuery", #RFC5007 + #45: "DHCP6OptLQClientData", #RFC5007 + #46: "DHCP6OptLQClientTime", #RFC5007 + #47: "DHCP6OptLQRelayData", #RFC5007 + #48: "DHCP6OptLQClientLink", #RFC5007 +} + + +# sect 5.3 RFC 3315 : DHCP6 Messages types +dhcp6types = { 1:"SOLICIT", + 2:"ADVERTISE", + 3:"REQUEST", + 4:"CONFIRM", + 5:"RENEW", + 6:"REBIND", + 7:"REPLY", + 8:"RELEASE", + 9:"DECLINE", + 10:"RECONFIGURE", + 11:"INFORMATION-REQUEST", + 12:"RELAY-FORW", + 13:"RELAY-REPL" } + + +##################################################################### +### DHCPv6 DUID related stuff ### +##################################################################### + +duidtypes = { 1: "Link-layer address plus time", + 2: "Vendor-assigned unique ID based on Enterprise Number", + 3: "Link-layer Address" } + +# DUID hardware types - RFC 826 - Extracted from +# http://www.iana.org/assignments/arp-parameters on 31/10/06 +# We should add the length of every kind of address. +duidhwtypes = { 0: "NET/ROM pseudo", # Not referenced by IANA + 1: "Ethernet (10Mb)", + 2: "Experimental Ethernet (3Mb)", + 3: "Amateur Radio AX.25", + 4: "Proteon ProNET Token Ring", + 5: "Chaos", + 6: "IEEE 802 Networks", + 7: "ARCNET", + 8: "Hyperchannel", + 9: "Lanstar", + 10: "Autonet Short Address", + 11: "LocalTalk", + 12: "LocalNet (IBM PCNet or SYTEK LocalNET)", + 13: "Ultra link", + 14: "SMDS", + 15: "Frame Relay", + 16: "Asynchronous Transmission Mode (ATM)", + 17: "HDLC", + 18: "Fibre Channel", + 19: "Asynchronous Transmission Mode (ATM)", + 20: "Serial Line", + 21: "Asynchronous Transmission Mode (ATM)", + 22: "MIL-STD-188-220", + 23: "Metricom", + 24: "IEEE 1394.1995", + 25: "MAPOS", + 26: "Twinaxial", + 27: "EUI-64", + 28: "HIPARP", + 29: "IP and ARP over ISO 7816-3", + 30: "ARPSec", + 31: "IPsec tunnel", + 32: "InfiniBand (TM)", + 33: "TIA-102 Project 25 Common Air Interface (CAI)" } + +class UTCTimeField(IntField): + epoch = (2000, 1, 1, 0, 0, 0, 5, 1, 0) # required Epoch + def i2repr(self, pkt, x): + x = self.i2h(pkt, x) + from time import gmtime, strftime, mktime + delta = mktime(self.epoch) - mktime(gmtime(0)) + x = x + delta + t = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime(x)) + return "%s (%d)" % (t, x) + +class _LLAddrField(MACField): + pass + +# XXX We only support Ethernet addresses at the moment. _LLAddrField +# will be modified when needed. Ask us. --arno +class DUID_LLT(Packet): # sect 9.2 RFC 3315 + name = "DUID - Link-layer address plus time" + fields_desc = [ ShortEnumField("type", 1, duidtypes), + XShortEnumField("hwtype", 1, duidhwtypes), + UTCTimeField("timeval", 0), # i.e. 01 Jan 2000 + _LLAddrField("lladdr", ETHER_ANY) ] + +# In fact, IANA enterprise-numbers file available at +# http//www.iana.org/asignments/enterprise-numbers) +# is simply huge (more than 2Mo and 600Ko in bz2). I'll +# add only most common vendors, and encountered values. +# -- arno +iana_enterprise_num = { 9: "ciscoSystems", + 35: "Nortel Networks", + 43: "3Com", + 311: "Microsoft", + 2636: "Juniper Networks, Inc.", + 4526: "Netgear", + 5771: "Cisco Systems, Inc.", + 5842: "Cisco Systems", + 16885: "Nortel Networks" } + +class DUID_EN(Packet): # sect 9.3 RFC 3315 + name = "DUID - Assigned by Vendor Based on Enterprise Number" + fields_desc = [ ShortEnumField("type", 2, duidtypes), + IntEnumField("enterprisenum", 311, iana_enterprise_num), + StrField("id","") ] + +class DUID_LL(Packet): # sect 9.4 RFC 3315 + name = "DUID - Based on Link-layer Address" + fields_desc = [ ShortEnumField("type", 3, duidtypes), + XShortEnumField("hwtype", 1, duidhwtypes), + _LLAddrField("lladdr", ETHER_ANY) ] + +duid_cls = { 1: "DUID_LLT", + 2: "DUID_EN", + 3: "DUID_LL"} + +##################################################################### +### DHCPv6 Options classes ### +##################################################################### + +class _DHCP6OptGuessPayload(Packet): + def guess_payload_class(self, payload): + cls = Raw + if len(payload) > 2 : + opt = struct.unpack("!H", payload[:2])[0] + cls = get_cls(dhcp6opts_by_code.get(opt, "DHCP6OptUnknown"), DHCP6OptUnknown) + return cls + +class DHCP6OptUnknown(_DHCP6OptGuessPayload): # A generic DHCPv6 Option + name = "Unknown DHCPv6 OPtion" + fields_desc = [ ShortEnumField("optcode", 0, dhcp6opts), + FieldLenField("optlen", None, length_of="data", fmt="!H"), + StrLenField("data", "", + length_from = lambda pkt: pkt.optlen)] + +class _DUIDField(PacketField): + holds_packets=1 + def __init__(self, name, default, length_from=None): + StrField.__init__(self, name, default) + self.length_from = length_from + + def i2m(self, pkt, i): + return str(i) + + def m2i(self, pkt, x): + cls = Raw + if len(x) > 4: + o = struct.unpack("!H", x[:2])[0] + cls = get_cls(duid_cls.get(o, Raw), "Raw") + return cls(x) + + def getfield(self, pkt, s): + l = self.length_from(pkt) + return s[l:], self.m2i(pkt,s[:l]) + + +class DHCP6OptClientId(_DHCP6OptGuessPayload): # RFC sect 22.2 + name = "DHCP6 Client Identifier Option" + fields_desc = [ ShortEnumField("optcode", 1, dhcp6opts), + FieldLenField("optlen", None, length_of="duid", fmt="!H"), + _DUIDField("duid", "", + length_from = lambda pkt: pkt.optlen) ] + + +class DHCP6OptServerId(DHCP6OptClientId): # RFC sect 22.3 + name = "DHCP6 Server Identifier Option" + __metaclass__ = NewDefaultValues + optcode = 2 + +# Should be encapsulated in the option field of IA_NA or IA_TA options +# Can only appear at that location. +# TODO : last field IAaddr-options is not defined in the reference document +class DHCP6OptIAAddress(_DHCP6OptGuessPayload): # RFC sect 22.6 + name = "DHCP6 IA Address Option (IA_TA or IA_NA suboption)" + fields_desc = [ ShortEnumField("optcode", 5, dhcp6opts), + FieldLenField("optlen", None, length_of="iaaddropts", + fmt="!H", adjust = lambda pkt,x: x+24), + IP6Field("addr", "::"), + IntField("preflft", 0), + IntField("validlft", 0), + XIntField("iaid", None), + StrLenField("iaaddropts", "", + length_from = lambda pkt: pkt.optlen - 24) ] + def guess_payload_class(self, payload): + return Padding + +class _IANAOptField(PacketListField): + def i2len(self, pkt, z): + if z is None or z == []: + return 0 + return sum(map(lambda x: len(str(x)) ,z)) + + def getfield(self, pkt, s): + l = self.length_from(pkt) + lst = [] + remain, payl = s[:l], s[l:] + while len(remain)>0: + p = self.m2i(pkt,remain) + if Padding in p: + pad = p[Padding] + remain = pad.load + del(pad.underlayer.payload) + else: + remain = "" + lst.append(p) + return payl,lst + +class DHCP6OptIA_NA(_DHCP6OptGuessPayload): # RFC sect 22.4 + name = "DHCP6 Identity Association for Non-temporary Addresses Option" + fields_desc = [ ShortEnumField("optcode", 3, dhcp6opts), + FieldLenField("optlen", None, length_of="ianaopts", + fmt="!H", adjust = lambda pkt,x: x+12), + XIntField("iaid", None), + IntField("T1", None), + IntField("T2", None), + _IANAOptField("ianaopts", [], DHCP6OptIAAddress, + length_from = lambda pkt: pkt.optlen-12) ] + +class _IATAOptField(_IANAOptField): + pass + +class DHCP6OptIA_TA(_DHCP6OptGuessPayload): # RFC sect 22.5 + name = "DHCP6 Identity Association for Temporary Addresses Option" + fields_desc = [ ShortEnumField("optcode", 4, dhcp6opts), + FieldLenField("optlen", None, length_of="iataopts", + fmt="!H", adjust = lambda pkt,x: x+4), + XIntField("iaid", None), + _IATAOptField("iataopts", [], DHCP6OptIAAddress, + length_from = lambda pkt: pkt.optlen-4) ] + + +#### DHCPv6 Option Request Option ################################### + +class _OptReqListField(StrLenField): + islist = 1 + def i2h(self, pkt, x): + if x is None: + return [] + return x + + def i2len(self, pkt, x): + return 2*len(x) + + def any2i(self, pkt, x): + return x + + def i2repr(self, pkt, x): + s = [] + for y in self.i2h(pkt, x): + if dhcp6opts.has_key(y): + s.append(dhcp6opts[y]) + else: + s.append("%d" % y) + return "[%s]" % ", ".join(s) + + def m2i(self, pkt, x): + r = [] + while len(x) != 0: + if len(x)<2: + warning("Odd length for requested option field. Rejecting last byte") + return r + r.append(struct.unpack("!H", x[:2])[0]) + x = x[2:] + return r + + def i2m(self, pkt, x): + return "".join(map(lambda y: struct.pack("!H", y), x)) + +# A client may include an ORO in a solicit, Request, Renew, Rebind, +# Confirm or Information-request +class DHCP6OptOptReq(_DHCP6OptGuessPayload): # RFC sect 22.7 + name = "DHCP6 Option Request Option" + fields_desc = [ ShortEnumField("optcode", 6, dhcp6opts), + FieldLenField("optlen", None, length_of="reqopts", fmt="!H"), + _OptReqListField("reqopts", [23, 24], + length_from = lambda pkt: pkt.optlen) ] + + +#### DHCPv6 Preference Option ####################################### + +# emise par un serveur pour affecter le choix fait par le client. Dans +# les messages Advertise, a priori +class DHCP6OptPref(_DHCP6OptGuessPayload): # RFC sect 22.8 + name = "DHCP6 Preference Option" + fields_desc = [ ShortEnumField("optcode", 7, dhcp6opts), + ShortField("optlen", 1 ), + ByteField("prefval",255) ] + + +#### DHCPv6 Elapsed Time Option ##################################### + +class _ElapsedTimeField(ShortField): + def i2repr(self, pkt, x): + if x == 0xffff: + return "infinity (0xffff)" + return "%.2f sec" % (self.i2h(pkt, x)/100.) + +class DHCP6OptElapsedTime(_DHCP6OptGuessPayload):# RFC sect 22.9 + name = "DHCP6 Elapsed Time Option" + fields_desc = [ ShortEnumField("optcode", 8, dhcp6opts), + ShortField("optlen", 2), + _ElapsedTimeField("elapsedtime", 0) ] + + +#### DHCPv6 Relay Message Option #################################### + +# Relayed message is seen as a payload. +class DHCP6OptRelayMsg(_DHCP6OptGuessPayload):# RFC sect 22.10 + name = "DHCP6 Relay Message Option" + fields_desc = [ ShortEnumField("optcode", 9, dhcp6opts), + ShortField("optlen", None ) ] + def post_build(self, p, pay): + if self.optlen is None: + l = len(pay) + p = p[:2]+struct.pack("!H", l) + return p + pay + + +#### DHCPv6 Authentication Option ################################### + +# The following fields are set in an Authentication option for the +# Reconfigure Key Authentication Protocol: +# +# protocol 3 +# +# algorithm 1 +# +# RDM 0 +# +# The format of the Authentication information for the Reconfigure Key +# Authentication Protocol is: +# +# 0 1 2 3 +# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Type | Value (128 bits) | +# +-+-+-+-+-+-+-+-+ | +# . . +# . . +# . +-+-+-+-+-+-+-+-+ +# | | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# +# Type Type of data in Value field carried in this option: +# +# 1 Reconfigure Key value (used in Reply message). +# +# 2 HMAC-MD5 digest of the message (used in Reconfigure +# message). +# +# Value Data as defined by field. + + +# TODO : Decoding only at the moment +class DHCP6OptAuth(_DHCP6OptGuessPayload): # RFC sect 22.11 + name = "DHCP6 Option - Authentication" + fields_desc = [ ShortEnumField("optcode", 11, dhcp6opts), + FieldLenField("optlen", None, length_of="authinfo", + adjust = lambda pkt,x: x+11), + ByteField("proto", 3), # TODO : XXX + ByteField("alg", 1), # TODO : XXX + ByteField("rdm", 0), # TODO : XXX + StrFixedLenField("replay", "A"*8, 8), # TODO: XXX + StrLenField("authinfo", "", + length_from = lambda pkt: pkt.optlen - 11) ] + +#### DHCPv6 Server Unicast Option ################################### + +class _SrvAddrField(IP6Field): + def i2h(self, pkt, x): + if x is None: + return "::" + return x + + def i2m(self, pkt, x): + return inet_pton(socket.AF_INET6, self.i2h(pkt,x)) + +class DHCP6OptServerUnicast(_DHCP6OptGuessPayload):# RFC sect 22.12 + name = "DHCP6 Server Unicast Option" + fields_desc = [ ShortEnumField("optcode", 12, dhcp6opts), + ShortField("optlen", 16 ), + _SrvAddrField("srvaddr",None) ] + + +#### DHCPv6 Status Code Option ###################################### + +dhcp6statuscodes = { 0:"Success", # sect 24.4 + 1:"UnspecFail", + 2:"NoAddrsAvail", + 3:"NoBinding", + 4:"NotOnLink", + 5:"UseMulticast", + 6:"NoPrefixAvail"} # From RFC3633 + +class DHCP6OptStatusCode(_DHCP6OptGuessPayload):# RFC sect 22.13 + name = "DHCP6 Status Code Option" + fields_desc = [ ShortEnumField("optcode", 13, dhcp6opts), + FieldLenField("optlen", None, length_of="statusmsg", + fmt="!H", adjust = lambda pkt,x:x+2), + ShortEnumField("statuscode",None,dhcp6statuscodes), + StrLenField("statusmsg", "", + length_from = lambda pkt: pkt.optlen-2) ] + + +#### DHCPv6 Rapid Commit Option ##################################### + +class DHCP6OptRapidCommit(_DHCP6OptGuessPayload): # RFC sect 22.14 + name = "DHCP6 Rapid Commit Option" + fields_desc = [ ShortEnumField("optcode", 14, dhcp6opts), + ShortField("optlen", 0)] + + +#### DHCPv6 User Class Option ####################################### + +class _UserClassDataField(PacketListField): + def i2len(self, pkt, z): + if z is None or z == []: + return 0 + return sum(map(lambda x: len(str(x)) ,z)) + + def getfield(self, pkt, s): + l = self.length_from(pkt) + lst = [] + remain, payl = s[:l], s[l:] + while len(remain)>0: + p = self.m2i(pkt,remain) + if Padding in p: + pad = p[Padding] + remain = pad.load + del(pad.underlayer.payload) + else: + remain = "" + lst.append(p) + return payl,lst + + +class USER_CLASS_DATA(Packet): + name = "user class data" + fields_desc = [ FieldLenField("len", None, length_of="data"), + StrLenField("data", "", + length_from = lambda pkt: pkt.len) ] + def guess_payload_class(self, payload): + return Padding + +class DHCP6OptUserClass(_DHCP6OptGuessPayload):# RFC sect 22.15 + name = "DHCP6 User Class Option" + fields_desc = [ ShortEnumField("optcode", 15, dhcp6opts), + FieldLenField("optlen", None, fmt="!H", + length_of="userclassdata"), + _UserClassDataField("userclassdata", [], USER_CLASS_DATA, + length_from = lambda pkt: pkt.optlen) ] + + +#### DHCPv6 Vendor Class Option ##################################### + +class _VendorClassDataField(_UserClassDataField): + pass + +class VENDOR_CLASS_DATA(USER_CLASS_DATA): + name = "vendor class data" + +class DHCP6OptVendorClass(_DHCP6OptGuessPayload):# RFC sect 22.16 + name = "DHCP6 Vendor Class Option" + fields_desc = [ ShortEnumField("optcode", 16, dhcp6opts), + FieldLenField("optlen", None, length_of="vcdata", fmt="!H", + adjust = lambda pkt,x: x+4), + IntEnumField("enterprisenum",None , iana_enterprise_num ), + _VendorClassDataField("vcdata", [], VENDOR_CLASS_DATA, + length_from = lambda pkt: pkt.optlen-4) ] + +#### DHCPv6 Vendor-Specific Information Option ###################### + +class VENDOR_SPECIFIC_OPTION(_DHCP6OptGuessPayload): + name = "vendor specific option data" + fields_desc = [ ShortField("optcode", None), + FieldLenField("optlen", None, length_of="optdata"), + StrLenField("optdata", "", + length_from = lambda pkt: pkt.optlen) ] + def guess_payload_class(self, payload): + return Padding + +# The third one that will be used for nothing interesting +class DHCP6OptVendorSpecificInfo(_DHCP6OptGuessPayload):# RFC sect 22.17 + name = "DHCP6 Vendor-specific Information Option" + fields_desc = [ ShortEnumField("optcode", 17, dhcp6opts), + FieldLenField("optlen", None, length_of="vso", fmt="!H", + adjust = lambda pkt,x: x+4), + IntEnumField("enterprisenum",None , iana_enterprise_num), + _VendorClassDataField("vso", [], VENDOR_SPECIFIC_OPTION, + length_from = lambda pkt: pkt.optlen-4) ] + +#### DHCPv6 Interface-ID Option ##################################### + +# Repasser sur cette option a la fin. Elle a pas l'air d'etre des +# masses critique. +class DHCP6OptIfaceId(_DHCP6OptGuessPayload):# RFC sect 22.18 + name = "DHCP6 Interface-Id Option" + fields_desc = [ ShortEnumField("optcode", 18, dhcp6opts), + FieldLenField("optlen", None, fmt="!H", + length_of="ifaceid"), + StrLenField("ifaceid", "", + length_from = lambda pkt: pkt.optlen) ] + + +#### DHCPv6 Reconfigure Message Option ############################## + +# A server includes a Reconfigure Message option in a Reconfigure +# message to indicate to the client whether the client responds with a +# renew message or an Informatiion-request message. +class DHCP6OptReconfMsg(_DHCP6OptGuessPayload): # RFC sect 22.19 + name = "DHCP6 Reconfigure Message Option" + fields_desc = [ ShortEnumField("optcode", 19, dhcp6opts), + ShortField("optlen", 1 ), + ByteEnumField("msgtype", 11, { 5:"Renew Message", + 11:"Information Request"}) ] + + +#### DHCPv6 Reconfigure Accept Option ############################### + +# A client uses the Reconfigure Accept option to announce to the +# server whether the client is willing to accept Recoonfigure +# messages, and a server uses this option to tell the client whether +# or not to accept Reconfigure messages. The default behavior in the +# absence of this option, means unwillingness to accept reconfigure +# messages, or instruction not to accept Reconfigure messages, for the +# client and server messages, respectively. +class DHCP6OptReconfAccept(_DHCP6OptGuessPayload): # RFC sect 22.20 + name = "DHCP6 Reconfigure Accept Option" + fields_desc = [ ShortEnumField("optcode", 20, dhcp6opts), + ShortField("optlen", 0)] + +# As required in Sect 8. of RFC 3315, Domain Names must be encoded as +# described in section 3.1 of RFC 1035 +# XXX Label should be at most 63 octets in length : we do not enforce it +# Total length of domain should be 255 : we do not enforce it either +class DomainNameListField(StrLenField): + islist = 1 + + def i2len(self, pkt, x): + return len(self.i2m(pkt, x)) + + def m2i(self, pkt, x): + res = [] + while x: + cur = [] + while x and x[0] != '\x00': + l = ord(x[0]) + cur.append(x[1:l+1]) + x = x[l+1:] + res.append(".".join(cur)) + if x and x[0] == '\x00': + x = x[1:] + return res + + def i2m(self, pkt, x): + def conditionalTrailingDot(z): + if z and z[-1] == '\x00': + return z + return z+'\x00' + res = "" + tmp = map(lambda y: map((lambda z: chr(len(z))+z), y.split('.')), x) + return "".join(map(lambda x: conditionalTrailingDot("".join(x)), tmp)) + +class DHCP6OptSIPDomains(_DHCP6OptGuessPayload): #RFC3319 + name = "DHCP6 Option - SIP Servers Domain Name List" + fields_desc = [ ShortEnumField("optcode", 21, dhcp6opts), + FieldLenField("optlen", None, length_of="sipdomains"), + DomainNameListField("sipdomains", [], + length_from = lambda pkt: pkt.optlen) ] + +class DHCP6OptSIPServers(_DHCP6OptGuessPayload): #RFC3319 + name = "DHCP6 Option - SIP Servers IPv6 Address List" + fields_desc = [ ShortEnumField("optcode", 22, dhcp6opts), + FieldLenField("optlen", None, length_of="sipservers"), + IP6ListField("sipservers", [], + length_from = lambda pkt: pkt.optlen) ] + +class DHCP6OptDNSServers(_DHCP6OptGuessPayload): #RFC3646 + name = "DHCP6 Option - DNS Recursive Name Server" + fields_desc = [ ShortEnumField("optcode", 23, dhcp6opts), + FieldLenField("optlen", None, length_of="dnsservers"), + IP6ListField("dnsservers", [], + length_from = lambda pkt: pkt.optlen) ] + +class DHCP6OptDNSDomains(_DHCP6OptGuessPayload): #RFC3646 + name = "DHCP6 Option - Domain Search List option" + fields_desc = [ ShortEnumField("optcode", 24, dhcp6opts), + FieldLenField("optlen", None, length_of="dnsdomains"), + DomainNameListField("dnsdomains", [], + length_from = lambda pkt: pkt.optlen) ] + +# TODO: Implement iaprefopts correctly when provided with more +# information about it. +class DHCP6OptIAPrefix(_DHCP6OptGuessPayload): #RFC3633 + name = "DHCP6 Option - IA_PD Prefix option" + fields_desc = [ ShortEnumField("optcode", 26, dhcp6opts), + FieldLenField("optlen", None, length_of="iaprefopts", + adjust = lambda pkt,x: x+26), + IntField("preflft", 0), + IntField("validlft", 0), + ByteField("plen", 48), # TODO: Challenge that default value + IP6Field("prefix", "2001:db8::"), # At least, global and won't hurt + StrLenField("iaprefopts", "", + length_from = lambda pkt: pkt.optlen-26) ] + +class DHCP6OptIA_PD(_DHCP6OptGuessPayload): #RFC3633 + name = "DHCP6 Option - Identity Association for Prefix Delegation" + fields_desc = [ ShortEnumField("optcode", 25, dhcp6opts), + FieldLenField("optlen", None, length_of="iapdopt", + adjust = lambda pkt,x: x+12), + IntField("iaid", 0), + IntField("T1", 0), + IntField("T2", 0), + PacketListField("iapdopt", [], DHCP6OptIAPrefix, + length_from = lambda pkt: pkt.optlen-12) ] + +class DHCP6OptNISServers(_DHCP6OptGuessPayload): #RFC3898 + name = "DHCP6 Option - NIS Servers" + fields_desc = [ ShortEnumField("optcode", 27, dhcp6opts), + FieldLenField("optlen", None, length_of="nisservers"), + IP6ListField("nisservers", [], + length_from = lambda pkt: pkt.optlen) ] + +class DHCP6OptNISPServers(_DHCP6OptGuessPayload): #RFC3898 + name = "DHCP6 Option - NIS+ Servers" + fields_desc = [ ShortEnumField("optcode", 28, dhcp6opts), + FieldLenField("optlen", None, length_of="nispservers"), + IP6ListField("nispservers", [], + length_from = lambda pkt: pkt.optlen) ] + +class DomainNameField(StrLenField): + def getfield(self, pkt, s): + l = self.length_from(pkt) + return s[l:], self.m2i(pkt,s[:l]) + + def i2len(self, pkt, x): + return len(self.i2m(pkt, x)) + + def m2i(self, pkt, x): + save = x + cur = [] + while x and x[0] != '\x00': + l = ord(x[0]) + cur.append(x[1:1+l]) + x = x[l+1:] + if x[0] != '\x00': + print "Found weird domain: '%s'. Keeping %s" % (save, x) + return ".".join(cur) + + def i2m(self, pkt, x): + def conditionalTrailingDot(z): + if (z and z[-1] == '\x00'): + return z + return z+'\x00' + if not x: + return "" + tmp = "".join(map(lambda z: chr(len(z))+z, x.split('.'))) + return conditionalTrailingDot(tmp) + +class DHCP6OptNISDomain(_DHCP6OptGuessPayload): #RFC3898 + name = "DHCP6 Option - NIS Domain Name" + fields_desc = [ ShortEnumField("optcode", 29, dhcp6opts), + FieldLenField("optlen", None, length_of="nisdomain"), + DomainNameField("nisdomain", "", + length_from = lambda pkt: pkt.optlen) ] + +class DHCP6OptNISPDomain(_DHCP6OptGuessPayload): #RFC3898 + name = "DHCP6 Option - NIS+ Domain Name" + fields_desc = [ ShortEnumField("optcode", 30, dhcp6opts), + FieldLenField("optlen", None, length_of="nispdomain"), + DomainNameField("nispdomain", "", + length_from= lambda pkt: pkt.optlen) ] + +class DHCP6OptSNTPServers(_DHCP6OptGuessPayload): #RFC4075 + name = "DHCP6 option - SNTP Servers" + fields_desc = [ ShortEnumField("optcode", 31, dhcp6opts), + FieldLenField("optlen", None, length_of="sntpservers"), + IP6ListField("sntpservers", [], + length_from = lambda pkt: pkt.optlen) ] + +IRT_DEFAULT=86400 +IRT_MINIMUM=600 +class DHCP6OptInfoRefreshTime(_DHCP6OptGuessPayload): #RFC4242 + name = "DHCP6 Option - Information Refresh Time" + fields_desc = [ ShortEnumField("optcode", 32, dhcp6opts), + ShortField("optlen", 4), + IntField("reftime", IRT_DEFAULT)] # One day + +class DHCP6OptBCMCSDomains(_DHCP6OptGuessPayload): #RFC4280 + name = "DHCP6 Option - BCMCS Domain Name List" + fields_desc = [ ShortEnumField("optcode", 33, dhcp6opts), + FieldLenField("optlen", None, length_of="bcmcsdomains"), + DomainNameListField("bcmcsdomains", [], + length_from = lambda pkt: pkt.optlen) ] + +class DHCP6OptBCMCSServers(_DHCP6OptGuessPayload): #RFC4280 + name = "DHCP6 Option - BCMCS Addresses List" + fields_desc = [ ShortEnumField("optcode", 34, dhcp6opts), + FieldLenField("optlen", None, length_of="bcmcsservers"), + IP6ListField("bcmcsservers", [], + length_from= lambda pkt: pkt.optlen) ] + +# TODO : Does Nothing at the moment +class DHCP6OptGeoConf(_DHCP6OptGuessPayload): #RFC-ietf-geopriv-dhcp-civil-09.txt + name = "" + fields_desc = [ ShortEnumField("optcode", 36, dhcp6opts), + FieldLenField("optlen", None, length_of="optdata"), + StrLenField("optdata", "", + length_from = lambda pkt: pkt.optlen) ] + +# TODO: see if we encounter opaque values from vendor devices +class DHCP6OptRemoteID(_DHCP6OptGuessPayload): #RFC4649 + name = "DHCP6 Option - Relay Agent Remote-ID" + fields_desc = [ ShortEnumField("optcode", 37, dhcp6opts), + FieldLenField("optlen", None, length_of="remoteid", + adjust = lambda pkt,x: x+4), + IntEnumField("enterprisenum", None, iana_enterprise_num), + StrLenField("remoteid", "", + length_from = lambda pkt: pkt.optlen-4) ] + +# TODO : 'subscriberid' default value should be at least 1 byte long +class DHCP6OptSubscriberID(_DHCP6OptGuessPayload): #RFC4580 + name = "DHCP6 Option - Subscriber ID" + fields_desc = [ ShortEnumField("optcode", 38, dhcp6opts), + FieldLenField("optlen", None, length_of="subscriberid"), + StrLenField("subscriberid", "", + length_from = lambda pkt: pkt.optlen) ] + +# TODO : "The data in the Domain Name field MUST be encoded +# as described in Section 8 of [5]" +class DHCP6OptClientFQDN(_DHCP6OptGuessPayload): #RFC4704 + name = "DHCP6 Option - Client FQDN" + fields_desc = [ ShortEnumField("optcode", 39, dhcp6opts), + FieldLenField("optlen", None, length_of="fqdn", + adjust = lambda pkt,x: x+1), + BitField("res", 0, 5), + FlagsField("flags", 0, 3, "SON" ), + DomainNameField("fqdn", "", + length_from = lambda pkt: pkt.optlen-1) ] + +class DHCP6OptRelayAgentERO(_DHCP6OptGuessPayload): # RFC4994 + name = "DHCP6 Option - RelayRequest Option" + fields_desc = [ ShortEnumField("optcode", 43, dhcp6opts), + FieldLenField("optlen", None, length_of="reqopts", fmt="!H"), + _OptReqListField("reqopts", [23, 24], + length_from = lambda pkt: pkt.optlen) ] + +##################################################################### +### DHCPv6 messages ### +##################################################################### + +# Some state parameters of the protocols that should probably be +# useful to have in the configuration (and keep up-to-date) +DHCP6RelayAgentUnicastAddr="" +DHCP6RelayHopCount="" +DHCP6ServerUnicastAddr="" +DHCP6ClientUnicastAddr="" +DHCP6ClientIA_TA="" +DHCP6ClientIA_NA="" +DHCP6ClientIAID="" +T1="" # Voir 2462 +T2="" # Voir 2462 +DHCP6ServerDUID="" +DHCP6CurrentTransactionID="" # devrait etre utilise pour matcher une +# reponse et mis a jour en mode client par une valeur aleatoire pour +# laquelle on attend un retour de la part d'un serveur. +DHCP6PrefVal="" # la valeur de preference a utiliser dans +# les options preference + +# Emitted by : +# - server : ADVERTISE, REPLY, RECONFIGURE, RELAY-REPL (vers relay) +# - client : SOLICIT, REQUEST, CONFIRM, RENEW, REBIND, RELEASE, DECLINE, +# INFORMATION REQUEST +# - relay : RELAY-FORW (toward server) + +class _DHCP6GuessPayload(Packet): + def guess_payload_class(self, payload): + if len(payload) > 1 : + print ord(payload[0]) + return get_cls(dhcp6opts.get(ord(payload[0]),"DHCP6OptUnknown"), Raw) + return Raw + +##################################################################### +## DHCPv6 messages sent between Clients and Servers (types 1 to 11) +# Comme specifie en section 15.1 de la RFC 3315, les valeurs de +# transaction id sont selectionnees de maniere aleatoire par le client +# a chaque emission et doivent matcher dans les reponses faites par +# les clients +class DHCP6(_DHCP6OptGuessPayload): + name = "DHCPv6 Generic Message)" + fields_desc = [ ByteEnumField("msgtype",None,dhcp6types), + X3BytesField("trid",0x000000) ] + overload_fields = { UDP: {"sport": 546, "dport": 547} } + + def hashret(self): + return struct.pack("!I", self.trid)[1:4] + +##################################################################### +# Solicit Message : sect 17.1.1 RFC3315 +# - sent by client +# - must include a client identifier option +# - the client may include IA options for any IAs to which it wants the +# server to assign address +# - The client use IA_NA options to request the assignment of +# non-temporary addresses and uses IA_TA options to request the +# assignment of temporary addresses +# - The client should include an Option Request option to indicate the +# options the client is interested in receiving (eventually +# including hints) +# - The client includes a Reconfigure Accept option if is willing to +# accept Reconfigure messages from the server. +# Le cas du send and reply est assez particulier car suivant la +# presence d'une option rapid commit dans le solicit, l'attente +# s'arrete au premier message de reponse recu ou alors apres un +# timeout. De la meme maniere, si un message Advertise arrive avec une +# valeur de preference de 255, il arrete l'attente et envoie une +# Request. +# - The client announces its intention to use DHCP authentication by +# including an Authentication option in its solicit message. The +# server selects a key for the client based on the client's DUID. The +# client and server use that key to authenticate all DHCP messages +# exchanged during the session + +class DHCP6_Solicit(DHCP6): + name = "DHCPv6 Solicit Message" + __metaclass__ = NewDefaultValues + msgtype = 1 + overload_fields = { UDP: {"sport": 546, "dport": 547} } + +##################################################################### +# Advertise Message +# - sent by server +# - Includes a server identifier option +# - Includes a client identifier option +# - the client identifier option must match the client's DUID +# - transaction ID must match + +class DHCP6_Advertise(DHCP6): + name = "DHCPv6 Advertise Message" + __metaclass__ = NewDefaultValues + msgtype = 2 + overload_fields = { UDP: {"sport": 547, "dport": 546} } + + def answers(self, other): + return (isinstance(other,DHCP6_Solicit) and + other.msgtype == 1 and + self.trid == other.trid) + +##################################################################### +# Request Message +# - sent by clients +# - includes a server identifier option +# - the content of Server Identifier option must match server's DUID +# - includes a client identifier option +# - must include an ORO Option (even with hints) p40 +# - can includes a reconfigure Accept option indicating whether or +# not the client is willing to accept Reconfigure messages from +# the server (p40) +# - When the server receives a Request message via unicast from a +# client to which the server has not sent a unicast option, the server +# discards the Request message and responds with a Reply message +# containinig Status Code option with the value UseMulticast, a Server +# Identifier Option containing the server's DUID, the client +# Identifier option from the client message and no other option. + +class DHCP6_Request(DHCP6): + name = "DHCPv6 Request Message" + __metaclass__ = NewDefaultValues + msgtype = 3 + +##################################################################### +# Confirm Message +# - sent by clients +# - must include a clien identifier option +# - When the server receives a Confirm Message, the server determines +# whether the addresses in the Confirm message are appropriate for the +# link to which the client is attached. cf p50 + +class DHCP6_Confirm(DHCP6): + name = "DHCPv6 Confirm Message" + __metaclass__ = NewDefaultValues + msgtype = 4 + +##################################################################### +# Renew Message +# - sent by clients +# - must include a server identifier option +# - content of server identifier option must match the server's identifier +# - must include a client identifier option +# - the clients includes any IA assigned to the interface that may +# have moved to a new link, along with the addresses associated with +# those IAs in its confirm messages +# - When the server receives a Renew message that contains an IA +# option from a client, it locates the client's binding and verifies +# that the information in the IA from the client matches the +# information for that client. If the server cannot find a client +# entry for the IA the server returns the IA containing no addresses +# with a status code option est to NoBinding in the Reply message. cf +# p51 pour le reste. + +class DHCP6_Renew(DHCP6): + name = "DHCPv6 Renew Message" + __metaclass__ = NewDefaultValues + msgtype = 5 + +##################################################################### +# Rebind Message +# - sent by clients +# - must include a client identifier option +# cf p52 + +class DHCP6_Rebind(DHCP6): + name = "DHCPv6 Rebind Message" + __metaclass__ = NewDefaultValues + msgtype = 6 + +##################################################################### +# Reply Message +# - sent by servers +# - the message must include a server identifier option +# - transaction-id field must match the value of original message +# The server includes a Rapid Commit option in the Reply message to +# indicate that the reply is in response to a solicit message +# - if the client receives a reply message with a Status code option +# with the value UseMulticast, the client records the receipt of the +# message and sends subsequent messages to the server through the +# interface on which the message was received using multicast. The +# client resends the original message using multicast +# - When the client receives a NotOnLink status from the server in +# response to a Confirm message, the client performs DHCP server +# solicitation as described in section 17 and client-initiated +# configuration as descrribed in section 18 (RFC 3315) +# - when the client receives a NotOnLink status from the server in +# response to a Request, the client can either re-issue the Request +# without specifying any addresses or restart the DHCP server +# discovery process. +# - the server must include a server identifier option containing the +# server's DUID in the Reply message + +class DHCP6_Reply(DHCP6): + name = "DHCPv6 Reply Message" + __metaclass__ = NewDefaultValues + msgtype = 7 + + def answers(self, other): + return (isinstance(other, DHCP6_InfoRequest) and + self.trid == other.trid) + +##################################################################### +# Release Message +# - sent by clients +# - must include a server identifier option +# cf p53 + +class DHCP6_Release(DHCP6): + name = "DHCPv6 Release Message" + __metaclass__ = NewDefaultValues + msgtype = 8 + +##################################################################### +# Decline Message +# - sent by clients +# - must include a client identifier option +# - Server identifier option must match server identifier +# - The addresses to be declined must be included in the IAs. Any +# addresses for the IAs the client wishes to continue to use should +# not be in added to the IAs. +# - cf p54 + +class DHCP6_Decline(DHCP6): + name = "DHCPv6 Decline Message" + __metaclass__ = NewDefaultValues + msgtype = 9 + +##################################################################### +# Reconfigure Message +# - sent by servers +# - must be unicast to the client +# - must include a server identifier option +# - must include a client identifier option that contains the client DUID +# - must contain a Reconfigure Message Option and the message type +# must be a valid value +# - the server sets the transaction-id to 0 +# - The server must use DHCP Authentication in the Reconfigure +# message. Autant dire que ca va pas etre le type de message qu'on va +# voir le plus souvent. + +class DHCP6_Reconf(DHCP6): + name = "DHCPv6 Reconfigure Message" + __metaclass__ = NewDefaultValues + msgtype = 10 + overload_fields = { UDP: { "sport": 547, "dport": 546 } } + + +##################################################################### +# Information-Request Message +# - sent by clients when needs configuration information but no +# addresses. +# - client should include a client identifier option to identify +# itself. If it doesn't the server is not able to return client +# specific options or the server can choose to not respond to the +# message at all. The client must include a client identifier option +# if the message will be authenticated. +# - client must include an ORO of option she's interested in receiving +# (can include hints) + +class DHCP6_InfoRequest(DHCP6): + name = "DHCPv6 Information Request Message" + __metaclass__ = NewDefaultValues + msgtype = 11 + + def hashret(self): + return struct.pack("!I", self.trid)[1:3] + +##################################################################### +# sent between Relay Agents and Servers +# +# Normalement, doit inclure une option "Relay Message Option" +# peut en inclure d'autres. +# voir section 7.1 de la 3315 + +# Relay-Forward Message +# - sent by relay agents to servers +# If the relay agent relays messages to the All_DHCP_Servers multicast +# address or other multicast addresses, it sets the Hop Limit field to +# 32. + +class DHCP6_RelayForward(_DHCP6GuessPayload,Packet): + name = "DHCPv6 Relay Forward Message (Relay Agent/Server Message)" + fields_desc = [ ByteEnumField("msgtype", 12, dhcp6types), + ShortField("hopcount", None), + IP6Field("linkaddr", "::"), + IP6Field("peeraddr", "::") ] + def hashret(self): # we filter on peer address field + return inet_pton(socket.AF_INET6, self.peeraddr) + +##################################################################### +# sent between Relay Agents and Servers +# Normalement, doit inclure une option "Relay Message Option" +# peut en inclure d'autres. +# Les valeurs des champs hop-count, link-addr et peer-addr +# sont copiees du messsage Forward associe. POur le suivi de session. +# Pour le moment, comme decrit dans le commentaire, le hashret +# se limite au contenu du champ peer address. +# Voir section 7.2 de la 3315. + +# Relay-Reply Message +# - sent by servers to relay agents +# - if the solicit message was received in a Relay-Forward message, +# the server constructs a relay-reply message with the Advertise +# message in the payload of a relay-message. cf page 37/101. Envoie de +# ce message en unicast au relay-agent. utilisation de l'adresse ip +# presente en ip source du paquet recu + +class DHCP6_RelayReply(DHCP6_RelayForward): + name = "DHCPv6 Relay Reply Message (Relay Agent/Server Message)" + __metaclass__= NewDefaultValues + msgtype = 13 + def hashret(self): # We filter on peer address field. + return inet_pton(socket.AF_INET6, self.peeraddr) + def answers(self, other): + return (isinstance(other, DHCP6_RelayForward) and + self.count == other.count and + self.linkaddr == other.linkaddr and + self.peeraddr == other.peeraddr ) + + +dhcp6_cls_by_type = { 1: "DHCP6_Solicit", + 2: "DHCP6_Advertise", + 3: "DHCP6_Request", + 4: "DHCP6_Confirm", + 5: "DHCP6_Renew", + 6: "DHCP6_Rebind", + 7: "DHCP6_Reply", + 8: "DHCP6_Release", + 9: "DHCP6_Decline", + 10: "DHCP6_Reconf", + 11: "DHCP6_InfoRequest", + 12: "DHCP6_RelayForward", + 13: "DHCP6_RelayReply" } + +def _dhcp6_dispatcher(x, *args, **kargs): + cls = Raw + if len(x) >= 2: + cls = get_cls(dhcp6_cls_by_type.get(ord(x[0]), "Raw"), Raw) + return cls(x, *args, **kargs) + +bind_bottom_up(UDP, _dhcp6_dispatcher, { "dport": 547 } ) +bind_bottom_up(UDP, _dhcp6_dispatcher, { "dport": 546 } ) + + + +class DHCPv6_am(AnsweringMachine): + function_name = "dhcp6d" + filter = "udp and port 546 and port 547" + send_function = staticmethod(send) + def usage(self): + msg = """ +dhcp6d( dns="2001:500::1035", domain="localdomain, local", duid=None) + iface=conf.iface, advpref=255, sntpservers=None, + sipdomains=None, sipservers=None, + nisdomain=None, nisservers=None, + nispdomain=None, nispservers=None, + bcmcsdomain=None, bcmcsservers=None) + + debug : When set, additional debugging information is printed. + + duid : some DUID class (DUID_LLT, DUID_LL or DUID_EN). If none + is provided a DUID_LLT is constructed based on the MAC + address of the sending interface and launch time of dhcp6d + answering machine. + + iface : the interface to listen/reply on if you do not want to use + conf.iface. + + advpref : Value in [0,255] given to Advertise preference field. + By default, 255 is used. Be aware that this specific + value makes clients stops waiting for further Advertise + messages from other servers. + + dns : list of recursive DNS servers addresses (as a string or list). + By default, it is set empty and the associated DHCP6OptDNSServers + option is inactive. See RFC 3646 for details. + domain : a list of DNS search domain (as a string or list). By default, + it is empty and the associated DHCP6OptDomains option is inactive. + See RFC 3646 for details. + + sntpservers : a list of SNTP servers IPv6 addresses. By default, + it is empty and the associated DHCP6OptSNTPServers option + is inactive. + + sipdomains : a list of SIP domains. By default, it is empty and the + associated DHCP6OptSIPDomains option is inactive. See RFC 3319 + for details. + sipservers : a list of SIP servers IPv6 addresses. By default, it is + empty and the associated DHCP6OptSIPDomains option is inactive. + See RFC 3319 for details. + + nisdomain : a list of NIS domains. By default, it is empty and the + associated DHCP6OptNISDomains option is inactive. See RFC 3898 + for details. See RFC 3646 for details. + nisservers : a list of NIS servers IPv6 addresses. By default, it is + empty and the associated DHCP6OptNISServers option is inactive. + See RFC 3646 for details. + + nispdomain : a list of NIS+ domains. By default, it is empty and the + associated DHCP6OptNISPDomains option is inactive. See RFC 3898 + for details. + nispservers : a list of NIS+ servers IPv6 addresses. By default, it is + empty and the associated DHCP6OptNISServers option is inactive. + See RFC 3898 for details. + + bcmcsdomain : a list of BCMCS domains. By default, it is empty and the + associated DHCP6OptBCMCSDomains option is inactive. See RFC 4280 + for details. + bcmcsservers : a list of BCMCS servers IPv6 addresses. By default, it is + empty and the associated DHCP6OptBCMCSServers option is inactive. + See RFC 4280 for details. + + If you have a need for others, just ask ... or provide a patch.""" + print msg + + def parse_options(self, dns="2001:500::1035", domain="localdomain, local", + startip="2001:db8::1", endip="2001:db8::20", duid=None, + sntpservers=None, sipdomains=None, sipservers=None, + nisdomain=None, nisservers=None, nispdomain=None, + nispservers=None, bcmcsservers=None, bcmcsdomains=None, + iface=conf.iface, debug=0, advpref=255): + def norm_list(val, param_name): + if val is None: + return None + if type(val) is list: + return val + elif type(val) is str: + l = val.split(',') + return map(lambda x: x.strip(), l) + else: + print "Bad '%s' parameter provided." % param_name + self.usage() + return -1 + + self.debug = debug + + # Dictionary of provided DHCPv6 options, keyed by option type + self.dhcpv6_options={} + + for o in [(dns, "dns", 23, lambda x: DHCP6OptDNSServers(dnsservers=x)), + (domain, "domain", 24, lambda x: DHCP6OptDNSDomains(dnsdomains=x)), + (sntpservers, "sntpservers", 31, lambda x: DHCP6OptSNTPServers(sntpservers=x)), + (sipservers, "sipservers", 22, lambda x: DHCP6OptSIPServers(sipservers=x)), + (sipdomains, "sipdomains", 21, lambda x: DHCP6OptSIPDomains(sipdomains=x)), + (nisservers, "nisservers", 27, lambda x: DHCP6OptNISServers(nisservers=x)), + (nisdomain, "nisdomain", 29, lambda x: DHCP6OptNISDomain(nisdomain=(x+[""])[0])), + (nispservers, "nispservers", 28, lambda x: DHCP6OptNISPServers(nispservers=x)), + (nispdomain, "nispdomain", 30, lambda x: DHCP6OptNISPDomain(nispdomain=(x+[""])[0])), + (bcmcsservers, "bcmcsservers", 33, lambda x: DHCP6OptBCMCSServers(bcmcsservers=x)), + (bcmcsdomains, "bcmcsdomains", 34, lambda x: DHCP6OptBCMCSDomains(bcmcsdomains=x))]: + + opt = norm_list(o[0], o[1]) + if opt == -1: # Usage() was triggered + return False + elif opt is None: # We won't return that option + pass + else: + self.dhcpv6_options[o[2]] = o[3](opt) + + if self.debug: + print "\n[+] List of active DHCPv6 options:" + opts = self.dhcpv6_options.keys() + opts.sort() + for i in opts: + print " %d: %s" % (i, repr(self.dhcpv6_options[i])) + + # Preference value used in Advertise. + self.advpref = advpref + + # IP Pool + self.startip = startip + self.endip = endip + # XXX TODO Check IPs are in same subnet + + #### + # The interface we are listening/replying on + self.iface = iface + + #### + # Generate a server DUID + if duid is not None: + self.duid = duid + else: + # Timeval + from time import gmtime, strftime, mktime + epoch = (2000, 1, 1, 0, 0, 0, 5, 1, 0) + delta = mktime(epoch) - mktime(gmtime(0)) + timeval = time.time() - delta + + # Mac Address + rawmac = get_if_raw_hwaddr(iface)[1] + mac = ":".join(map(lambda x: "%.02x" % ord(x), list(rawmac))) + + self.duid = DUID_LLT(timeval = timeval, lladdr = mac) + + if self.debug: + print "\n[+] Our server DUID:" + self.duid.show(label_lvl=" "*4) + + #### + # Find the source address we will use + l = filter(lambda x: x[2] == iface and in6_islladdr(x[0]), + in6_getifaddr()) + if not l: + warning("Unable to get a Link-Local address") + return + + self.src_addr = l[0][0] + + #### + # Our leases + self.leases = {} + + + if self.debug: + print "\n[+] Starting DHCPv6 service on %s:" % self.iface + + def is_request(self, p): + if not IPv6 in p: + return False + + src = p[IPv6].src + dst = p[IPv6].dst + + p = p[IPv6].payload + if not isinstance(p, UDP) or p.sport != 546 or p.dport != 547 : + return False + + p = p.payload + if not isinstance(p, DHCP6): + return False + + # Message we considered client messages : + # Solicit (1), Request (3), Confirm (4), Renew (5), Rebind (6) + # Decline (9), Release (8), Information-request (11), + if not (p.msgtype in [1, 3, 4, 5, 6, 8, 9, 11]): + return False + + # Message validation following section 15 of RFC 3315 + + if ((p.msgtype == 1) or # Solicit + (p.msgtype == 6) or # Rebind + (p.msgtype == 4)): # Confirm + if ((not DHCP6OptClientId in p) or + DHCP6OptServerId in p): + return False + + if (p.msgtype == 6 or # Rebind + p.msgtype == 4): # Confirm + # XXX We do not reply to Confirm or Rebind as we + # XXX do not support address assignment + return False + + elif (p.msgtype == 3 or # Request + p.msgtype == 5 or # Renew + p.msgtype == 8): # Release + + # Both options must be present + if ((not DHCP6OptServerId in p) or + (not DHCP6OptClientId in p)): + return False + # provided server DUID must match ours + duid = p[DHCP6OptServerId].duid + if (type(duid) != type(self.duid)): + return False + if str(duid) != str(self.duid): + return False + + if (p.msgtype == 5 or # Renew + p.msgtype == 8): # Release + # XXX We do not reply to Renew or Release as we + # XXX do not support address assignment + return False + + elif p.msgtype == 9: # Decline + # XXX We should check if we are tracking that client + if not self.debug: + return False + + bo = Color.bold + g = Color.green + bo + b = Color.blue + bo + n = Color.normal + r = Color.red + + vendor = in6_addrtovendor(src) + if (vendor and vendor != "UNKNOWN"): + vendor = " [" + b + vendor + n + "]" + else: + vendor = "" + src = bo + src + n + + it = p + addrs = [] + while it: + l = [] + if isinstance(it, DHCP6OptIA_NA): + l = it.ianaopts + elif isinstance(it, DHCP6OptIA_TA): + l = it.iataopts + + opsaddr = filter(lambda x: isinstance(x, DHCP6OptIAAddress),l) + a=map(lambda x: x.addr, opsaddr) + addrs += a + it = it.payload + + addrs = map(lambda x: bo + x + n, addrs) + if debug: + msg = r + "[DEBUG]" + n + " Received " + g + "Decline" + n + msg += " from " + bo + src + vendor + " for " + msg += ", ".join(addrs)+ n + print msg + + # See sect 18.1.7 + + # Sent by a client to warn us she has determined + # one or more addresses assigned to her is already + # used on the link. + # We should simply log that fact. No messaged should + # be sent in return. + + # - Message must include a Server identifier option + # - the content of the Server identifier option must + # match the server's identifier + # - the message must include a Client Identifier option + return False + + elif p.msgtype == 11: # Information-Request + if DHCP6OptServerId in p: + duid = p[DHCP6OptServerId].duid + if (type(duid) != type(self.duid)): + return False + if str(duid) != str(self.duid): + return False + if ((DHCP6OptIA_NA in p) or + (DHCP6OptIA_TA in p) or + (DHCP6OptIA_PD in p)): + return False + else: + return False + + return True + + def print_reply(self, req, reply): + def norm(s): + if s.startswith("DHCPv6 "): + s = s[7:] + if s.endswith(" Message"): + s = s[:-8] + return s + + if reply is None: + return + + bo = Color.bold + g = Color.green + bo + b = Color.blue + bo + n = Color.normal + reqtype = g + norm(req.getlayer(UDP).payload.name) + n + reqsrc = req.getlayer(IPv6).src + vendor = in6_addrtovendor(reqsrc) + if (vendor and vendor != "UNKNOWN"): + vendor = " [" + b + vendor + n + "]" + else: + vendor = "" + reqsrc = bo + reqsrc + n + reptype = g + norm(reply.getlayer(UDP).payload.name) + n + + print "Sent %s answering to %s from %s%s" % (reptype, reqtype, reqsrc, vendor) + + def make_reply(self, req): + req_mac_src = req.src + req_mac_dst = req.dst + + p = req[IPv6] + req_src = p.src + req_dst = p.dst + + p = p.payload.payload + + msgtype = p.msgtype + trid = p.trid + + if msgtype == 1: # SOLICIT (See Sect 17.1 and 17.2 of RFC 3315) + + # XXX We don't support address or prefix assignment + # XXX We also do not support relay function --arno + + client_duid = p[DHCP6OptClientId].duid + resp = IPv6(src=self.src_addr, dst=req_src) + resp /= UDP(sport=547, dport=546) + + if p.haslayer(DHCP6OptRapidCommit): + # construct a Reply packet + resp /= DHCP6_Reply(trid=trid) + resp /= DHCP6OptRapidCommit() # See 17.1.2 + resp /= DHCP6OptServerId(duid = self.duid) + resp /= DHCP6OptClientId(duid = client_duid) + + else: # No Rapid Commit in the packet. Reply with an Advertise + + if (p.haslayer(DHCP6OptIA_NA) or + p.haslayer(DHCP6OptIA_TA)): + # XXX We don't assign addresses at the moment + msg = "Scapy6 dhcp6d does not support address assignment" + resp /= DHCP6_Advertise(trid = trid) + resp /= DHCP6OptStatusCode(statuscode=2, statusmsg=msg) + resp /= DHCP6OptServerId(duid = self.duid) + resp /= DHCP6OptClientId(duid = client_duid) + + elif p.haslayer(DHCP6OptIA_PD): + # XXX We don't assign prefixes at the moment + msg = "Scapy6 dhcp6d does not support prefix assignment" + resp /= DHCP6_Advertise(trid = trid) + resp /= DHCP6OptStatusCode(statuscode=6, statusmsg=msg) + resp /= DHCP6OptServerId(duid = self.duid) + resp /= DHCP6OptClientId(duid = client_duid) + + else: # Usual case, no request for prefixes or addresse + resp /= DHCP6_Advertise(trid = trid) + resp /= DHCP6OptPref(prefval = self.advpref) + resp /= DHCP6OptServerId(duid = self.duid) + resp /= DHCP6OptClientId(duid = client_duid) + resp /= DHCP6OptReconfAccept() + + # See which options should be included + reqopts = [] + if p.haslayer(DHCP6OptOptReq): # add only asked ones + reqopts = p[DHCP6OptOptReq].reqopts + for o in self.dhcpv6_options.keys(): + if o in reqopts: + resp /= self.dhcpv6_options[o] + else: # advertise everything we have available + for o in self.dhcpv6_options.keys(): + resp /= self.dhcpv6_options[o] + + return resp + + elif msgtype == 3: #REQUEST (INFO-REQUEST is further below) + client_duid = p[DHCP6OptClientId].duid + resp = IPv6(src=self.src_addr, dst=req_src) + resp /= UDP(sport=547, dport=546) + resp /= DHCP6_Solicit(trid=trid) + resp /= DHCP6OptServerId(duid = self.duid) + resp /= DHCP6OptClientId(duid = client_duid) + + # See which options should be included + reqopts = [] + if p.haslayer(DHCP6OptOptReq): # add only asked ones + reqopts = p[DHCP6OptOptReq].reqopts + for o in self.dhcpv6_options.keys(): + if o in reqopts: + resp /= self.dhcpv6_options[o] + else: + # advertise everything we have available. + # Should not happen has clients MUST include + # and ORO in requests (sec 18.1.1) -- arno + for o in self.dhcpv6_options.keys(): + resp /= self.dhcpv6_options[o] + + return resp + + elif msgtype == 4: # CONFIRM + # see Sect 18.1.2 + + # Client want to check if addresses it was assigned + # are still appropriate + + # Server must discard any Confirm messages that + # do not include a Client Identifier option OR + # THAT DO INCLUDE a Server Identifier Option + + # XXX we must discard the SOLICIT if it is received with + # a unicast destination address + + pass + + elif msgtype == 5: # RENEW + # see Sect 18.1.3 + + # Clients want to extend lifetime of assigned addresses + # and update configuration parameters. This message is sent + # specifically to the server that provided her the info + + # - Received message must include a Server Identifier + # option. + # - the content of server identifier option must match + # the server's identifier. + # - the message must include a Client identifier option + + pass + + elif msgtype == 6: # REBIND + # see Sect 18.1.4 + + # Same purpose as the Renew message but sent to any + # available server after he received no response + # to its previous Renew message. + + + # - Message must include a Client Identifier Option + # - Message can't include a Server identifier option + + # XXX we must discard the SOLICIT if it is received with + # a unicast destination address + + pass + + elif msgtype == 8: # RELEASE + # See section 18.1.6 + + # Message is sent to the server to indicate that + # she will no longer use the addresses that was assigned + # We should parse the message and verify our dictionary + # to log that fact. + + + # - The message must include a server identifier option + # - The content of the Server Identifier option must + # match the server's identifier + # - the message must include a Client Identifier option + + pass + + elif msgtype == 9: # DECLINE + # See section 18.1.7 + pass + + elif msgtype == 11: # INFO-REQUEST + client_duid = None + if not p.haslayer(DHCP6OptClientId): + if self.debug: + warning("Received Info Request message without Client Id option") + else: + client_duid = p[DHCP6OptClientId].duid + + resp = IPv6(src=self.src_addr, dst=req_src) + resp /= UDP(sport=547, dport=546) + resp /= DHCP6_Reply(trid=trid) + resp /= DHCP6OptServerId(duid = self.duid) + + if client_duid: + resp /= DHCP6OptClientId(duid = client_duid) + + # Stack requested options if available + reqopts = [] + if p.haslayer(DHCP6OptOptReq): + reqopts = p[DHCP6OptOptReq].reqopts + for o in self.dhcpv6_options.keys(): + resp /= self.dhcpv6_options[o] + + return resp + + else: + # what else ? + pass + + # - We won't support reemission + # - We won't support relay role, nor relay forwarded messages + # at the beginning + +############################################################################# +############################################################################# +### Mobile IPv6 (RFC 3775) and Nemo (RFC 3963) ### +############################################################################# +############################################################################# + +# Mobile IPv6 ICMPv6 related classes + +class ICMPv6HAADRequest(_ICMPv6): + name = 'ICMPv6 Home Agent Address Discovery Request' + fields_desc = [ ByteEnumField("type", 144, icmp6types), + ByteField("code", 0), + XShortField("cksum", None), + XShortField("id", None), + BitEnumField("R", 1, 1, {1: 'MR'}), + XBitField("res", 0, 15) ] + def hashret(self): + return struct.pack("!H",self.id)+self.payload.hashret() + +class ICMPv6HAADReply(_ICMPv6): + name = 'ICMPv6 Home Agent Address Discovery Reply' + fields_desc = [ ByteEnumField("type", 145, icmp6types), + ByteField("code", 0), + XShortField("cksum", None), + XShortField("id", None), + BitEnumField("R", 1, 1, {1: 'MR'}), + XBitField("res", 0, 15), + IP6ListField('addresses', None) ] + def hashret(self): + return struct.pack("!H",self.id)+self.payload.hashret() + + def answers(self, other): + if not isinstance(other, ICMPv6HAADRequest): + return 0 + return self.id == other.id + +class ICMPv6MPSol(_ICMPv6): + name = 'ICMPv6 Mobile Prefix Solicitation' + fields_desc = [ ByteEnumField("type", 146, icmp6types), + ByteField("code", 0), + XShortField("cksum", None), + XShortField("id", None), + XShortField("res", 0) ] + def _hashret(self): + return struct.pack("!H",self.id) + +class ICMPv6MPAdv(_ICMPv6NDGuessPayload, _ICMPv6): + name = 'ICMPv6 Mobile Prefix Advertisement' + fields_desc = [ ByteEnumField("type", 147, icmp6types), + ByteField("code", 0), + XShortField("cksum", None), + XShortField("id", None), + BitEnumField("flags", 2, 2, {2: 'M', 1:'O'}), + XBitField("res", 0, 14) ] + def hashret(self): + return struct.pack("!H",self.id) + + def answers(self, other): + return isinstance(other, ICMPv6MPSol) + +# Mobile IPv6 Options classes + + +_mobopttypes = { 2: "Binding Refresh Advice", + 3: "Alternate Care-of Address", + 4: "Nonce Indices", + 5: "Binding Authorization Data", + 6: "Mobile Network Prefix (RFC3963)", + 7: "Link-Layer Address (RFC4068)", + 8: "Mobile Node Identifier (RFC4283)", + 9: "Mobility Message Authentication (RFC4285)", + 10: "Replay Protection (RFC4285)", + 11: "CGA Parameters Request (RFC4866)", + 12: "CGA Parameters (RFC4866)", + 13: "Signature (RFC4866)", + 14: "Home Keygen Token (RFC4866)", + 15: "Care-of Test Init (RFC4866)", + 16: "Care-of Test (RFC4866)" } + + +class _MIP6OptAlign: + """ Mobile IPv6 options have alignment requirements of the form x*n+y. + This class is inherited by all MIPv6 options to help in computing the + required Padding for that option, i.e. the need for a Pad1 or PadN + option before it. They only need to provide x and y as class + parameters. (x=0 and y=0 are used when no alignment is required)""" + def alignment_delta(self, curpos): + x = self.x ; y = self.y + if x == 0 and y ==0: + return 0 + delta = x*((curpos - y + x - 1)/x) + y - curpos + return delta + + +class MIP6OptBRAdvice(_MIP6OptAlign, Packet): + name = 'Mobile IPv6 Option - Binding Refresh Advice' + fields_desc = [ ByteEnumField('otype', 2, _mobopttypes), + ByteField('olen', 2), + ShortField('rinter', 0) ] + x = 2 ; y = 0# alignment requirement: 2n + +class MIP6OptAltCoA(_MIP6OptAlign, Packet): + name = 'MIPv6 Option - Alternate Care-of Address' + fields_desc = [ ByteEnumField('otype', 3, _mobopttypes), + ByteField('olen', 16), + IP6Field("acoa", "::") ] + x = 8 ; y = 6 # alignment requirement: 8n+6 + +class MIP6OptNonceIndices(_MIP6OptAlign, Packet): + name = 'MIPv6 Option - Nonce Indices' + fields_desc = [ ByteEnumField('otype', 4, _mobopttypes), + ByteField('olen', 16), + ShortField('hni', 0), + ShortField('coni', 0) ] + x = 2 ; y = 0 # alignment requirement: 2n + +class MIP6OptBindingAuthData(_MIP6OptAlign, Packet): + name = 'MIPv6 Option - Binding Authorization Data' + fields_desc = [ ByteEnumField('otype', 5, _mobopttypes), + ByteField('olen', 16), + BitField('authenticator', 0, 96) ] + x = 8 ; y = 2 # alignment requirement: 8n+2 + +class MIP6OptMobNetPrefix(_MIP6OptAlign, Packet): # NEMO - RFC 3963 + name = 'NEMO Option - Mobile Network Prefix' + fields_desc = [ ByteEnumField("otype", 6, _mobopttypes), + ByteField("olen", 16), + ByteField("reserved", 0), + ByteField("plen", 64), + IP6Field("prefix", "::") ] + x = 8 ; y = 4 # alignment requirement: 8n+4 + +class MIP6OptLLAddr(_MIP6OptAlign, Packet): # Sect 6.4.4 of RFC 4068 + name = "MIPv6 Option - Link-Layer Address (MH-LLA)" + fields_desc = [ ByteEnumField("otype", 7, _mobopttypes), + ByteField("olen", 7), + ByteEnumField("ocode", 2, _rfc4068_lla_optcode), + ByteField("pad", 0), + MACField("lla", ETHER_ANY) ] # Only support ethernet + x = 0 ; y = 0 # alignment requirement: none + +class MIP6OptMNID(_MIP6OptAlign, Packet): # RFC 4283 + name = "MIPv6 Option - Mobile Node Identifier" + fields_desc = [ ByteEnumField("otype", 8, _mobopttypes), + FieldLenField("olen", None, length_of="id", fmt="B", + adjust = lambda pkt,x: x+1), + ByteEnumField("subtype", 1, {1: "NAI"}), + StrLenField("id", "", + length_from = lambda pkt: pkt.olen-1) ] + x = 0 ; y = 0 # alignment requirement: none + +# We only support decoding and basic build. Automatic HMAC computation is +# too much work for our current needs. It is left to the user (I mean ... +# you). --arno +class MIP6OptMsgAuth(_MIP6OptAlign, Packet): # RFC 4285 (Sect. 5) + name = "MIPv6 Option - Mobility Message Authentication" + fields_desc = [ ByteEnumField("otype", 9, _mobopttypes), + FieldLenField("olen", None, length_of="authdata", fmt="B", + adjust = lambda pkt,x: x+5), + ByteEnumField("subtype", 1, {1: "MN-HA authentication mobility option", + 2: "MN-AAA authentication mobility option"}), + IntField("mspi", None), + StrLenField("authdata", "A"*12, + length_from = lambda pkt: pkt.olen-5) ] + x = 4 ; y = 1 # alignment requirement: 4n+1 + +# Extracted from RFC 1305 (NTP) : +# NTP timestamps are represented as a 64-bit unsigned fixed-point number, +# in seconds relative to 0h on 1 January 1900. The integer part is in the +# first 32 bits and the fraction part in the last 32 bits. +class NTPTimestampField(LongField): + epoch = (1900, 1, 1, 0, 0, 0, 5, 1, 0) + def i2repr(self, pkt, x): + if x < ((50*31536000)<<32): + return "Some date a few decades ago (%d)" % x + + # delta from epoch (= (1900, 1, 1, 0, 0, 0, 5, 1, 0)) to + # January 1st 1970 : + delta = -2209075761 + i = int(x >> 32) + j = float(x & 0xffffffff) * 2.0**-32 + res = i + j + delta + from time import strftime + t = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime(res)) + + return "%s (%d)" % (t, x) + +class MIP6OptReplayProtection(_MIP6OptAlign, Packet): # RFC 4285 (Sect. 6) + name = "MIPv6 option - Replay Protection" + fields_desc = [ ByteEnumField("otype", 10, _mobopttypes), + ByteField("olen", 8), + NTPTimestampField("timestamp", 0) ] + x = 8 ; y = 2 # alignment requirement: 8n+2 + +class MIP6OptCGAParamsReq(_MIP6OptAlign, Packet): # RFC 4866 (Sect. 5.6) + name = "MIPv6 option - CGA Parameters Request" + fields_desc = [ ByteEnumField("otype", 11, _mobopttypes), + ByteField("olen", 0) ] + x = 0 ; y = 0 # alignment requirement: none + +# XXX TODO: deal with CGA param fragmentation and build of defragmented +# XXX version. Passing of a big CGAParam structure should be +# XXX simplified. Make it hold packets, by the way --arno +class MIP6OptCGAParams(_MIP6OptAlign, Packet): # RFC 4866 (Sect. 5.1) + name = "MIPv6 option - CGA Parameters" + fields_desc = [ ByteEnumField("otype", 12, _mobopttypes), + FieldLenField("olen", None, length_of="cgaparams", fmt="B"), + StrLenField("cgaparams", "", + length_from = lambda pkt: pkt.olen) ] + x = 0 ; y = 0 # alignment requirement: none + +class MIP6OptSignature(_MIP6OptAlign, Packet): # RFC 4866 (Sect. 5.2) + name = "MIPv6 option - Signature" + fields_desc = [ ByteEnumField("otype", 13, _mobopttypes), + FieldLenField("olen", None, length_of="sig", fmt="B"), + StrLenField("sig", "", + length_from = lambda pkt: pkt.olen) ] + x = 0 ; y = 0 # alignment requirement: none + +class MIP6OptHomeKeygenToken(_MIP6OptAlign, Packet): # RFC 4866 (Sect. 5.3) + name = "MIPv6 option - Home Keygen Token" + fields_desc = [ ByteEnumField("otype", 14, _mobopttypes), + FieldLenField("olen", None, length_of="hkt", fmt="B"), + StrLenField("hkt", "", + length_from = lambda pkt: pkt.olen) ] + x = 0 ; y = 0 # alignment requirement: none + +class MIP6OptCareOfTestInit(_MIP6OptAlign, Packet): # RFC 4866 (Sect. 5.4) + name = "MIPv6 option - Care-of Test Init" + fields_desc = [ ByteEnumField("otype", 15, _mobopttypes), + ByteField("olen", 0) ] + x = 0 ; y = 0 # alignment requirement: none + +class MIP6OptCareOfTest(_MIP6OptAlign, Packet): # RFC 4866 (Sect. 5.5) + name = "MIPv6 option - Care-of Test" + fields_desc = [ ByteEnumField("otype", 16, _mobopttypes), + FieldLenField("olen", None, length_of="cokt", fmt="B"), + StrLenField("cokt", '\x00'*8, + length_from = lambda pkt: pkt.olen) ] + x = 0 ; y = 0 # alignment requirement: none + +class MIP6OptUnknown(_MIP6OptAlign, Packet): + name = 'Scapy6 - Unknown Mobility Option' + fields_desc = [ ByteEnumField("otype", 6, _mobopttypes), + FieldLenField("olen", None, length_of="odata", fmt="B"), + StrLenField("odata", "", + length_from = lambda pkt: pkt.olen) ] + x = 0 ; y = 0 # alignment requirement: none + +moboptcls = { 0: Pad1, + 1: PadN, + 2: MIP6OptBRAdvice, + 3: MIP6OptAltCoA, + 4: MIP6OptNonceIndices, + 5: MIP6OptBindingAuthData, + 6: MIP6OptMobNetPrefix, + 7: MIP6OptLLAddr, + 8: MIP6OptMNID, + 9: MIP6OptMsgAuth, + 10: MIP6OptReplayProtection, + 11: MIP6OptCGAParamsReq, + 12: MIP6OptCGAParams, + 13: MIP6OptSignature, + 14: MIP6OptHomeKeygenToken, + 15: MIP6OptCareOfTestInit, + 16: MIP6OptCareOfTest } + + +# Main Mobile IPv6 Classes + +mhtypes = { 0: 'BRR', + 1: 'HoTI', + 2: 'CoTI', + 3: 'HoT', + 4: 'CoT', + 5: 'BU', + 6: 'BA', + 7: 'BE', + 8: 'Fast BU', + 9: 'Fast BA', + 10: 'Fast NA' } + +# From http://www.iana.org/assignments/mobility-parameters +bastatus = { 0: 'Binding Update accepted', + 1: 'Accepted but prefix discovery necessary', + 128: 'Reason unspecified', + 129: 'Administratively prohibited', + 130: 'Insufficient resources', + 131: 'Home registration not supported', + 132: 'Not home subnet', + 133: 'Not home agent for this mobile node', + 134: 'Duplicate Address Detection failed', + 135: 'Sequence number out of window', + 136: 'Expired home nonce index', + 137: 'Expired care-of nonce index', + 138: 'Expired nonces', + 139: 'Registration type change disallowed', + 140: 'Mobile Router Operation not permitted', + 141: 'Invalid Prefix', + 142: 'Not Authorized for Prefix', + 143: 'Forwarding Setup failed (prefixes missing)', + 144: 'MIPV6-ID-MISMATCH', + 145: 'MIPV6-MESG-ID-REQD', + 146: 'MIPV6-AUTH-FAIL', + 147: 'Permanent home keygen token unavailable', + 148: 'CGA and signature verification failed', + 149: 'Permanent home keygen token exists', + 150: 'Non-null home nonce index expected' } + + +class _MobilityHeader(Packet): + name = 'Dummy IPv6 Mobility Header' + overload_fields = { IPv6: { "nh": 135 }} + + def post_build(self, p, pay): + p += pay + l = self.len + if self.len is None: + l = (len(p)-8)/8 + p = p[0] + struct.pack("B", l) + p[2:] + if self.cksum is None: + cksum = in6_chksum(135, self.underlayer, p) + else: + cksum = self.cksum + p = p[:4]+struct.pack("!H", cksum)+p[6:] + return p + + +class MIP6MH_Generic(_MobilityHeader): # Mainly for decoding of unknown msg + name = "IPv6 Mobility Header - Generic Message" + fields_desc = [ ByteEnumField("nh", 59, ipv6nh), + ByteField("len", None), + ByteEnumField("mhtype", None, mhtypes), + ByteField("res", None), + XShortField("cksum", None), + StrLenField("msg", "\x00"*2, + length_from = lambda pkt: 8*pkt.len-6) ] + + + +# TODO: make a generic _OptionsField +class _MobilityOptionsField(PacketListField): + islist = 1 + holds_packet = 1 + + def __init__(self, name, default, cls, curpos, count_from=None, length_from=None): + self.curpos = curpos + PacketListField.__init__(self, name, default, cls, count_from=count_from, length_from=length_from) + + def getfield(self, pkt, s): + l = self.length_from(pkt) + return s[l:],self.m2i(pkt, s[:l]) + + def i2len(self, pkt, i): + return len(self.i2m(pkt, i)) + + def m2i(self, pkt, x): + opt = [] + while x: + o = ord(x[0]) # Option type + cls = self.cls + if moboptcls.has_key(o): + cls = moboptcls[o] + try: + op = cls(x) + except: + op = self.cls(x) + opt.append(op) + if isinstance(op.payload, Raw): + x = op.payload.load + del(op.payload) + else: + x = "" + return opt + + def i2m(self, pkt, x): + autopad = None + try: + autopad = getattr(pkt, "autopad") # Hack : 'autopad' phantom field + except: + autopad = 1 + + if not autopad: + return "".join(map(str, x)) + + curpos = self.curpos + s = "" + for p in x: + d = p.alignment_delta(curpos) + curpos += d + if d == 1: + s += str(Pad1()) + elif d != 0: + s += str(PadN(optdata='\x00'*(d-2))) + pstr = str(p) + curpos += len(pstr) + s += pstr + + # Let's make the class including our option field + # a multiple of 8 octets long + d = curpos % 8 + if d == 0: + return s + d = 8 - d + if d == 1: + s += str(Pad1()) + elif d != 0: + s += str(PadN(optdata='\x00'*(d-2))) + + return s + + def addfield(self, pkt, s, val): + return s+self.i2m(pkt, val) + +class MIP6MH_BRR(_MobilityHeader): + name = "IPv6 Mobility Header - Binding Refresh Request" + fields_desc = [ ByteEnumField("nh", 59, ipv6nh), + ByteField("len", None), + ByteEnumField("mhtype", 0, mhtypes), + ByteField("res", None), + XShortField("cksum", None), + ShortField("res2", None), + _PhantomAutoPadField("autopad", 1), # autopad activated by default + _MobilityOptionsField("options", [], MIP6OptUnknown, 8, + length_from = lambda pkt: 8*pkt.len) ] + overload_fields = { IPv6: { "nh": 135 } } + def hashret(self): + # Hack: BRR, BU and BA have the same hashret that returns the same + # value "\x00\x08\x09" (concatenation of mhtypes). This is + # because we need match BA with BU and BU with BRR. --arno + return "\x00\x08\x09" + +class MIP6MH_HoTI(_MobilityHeader): + name = "IPv6 Mobility Header - Home Test Init" + fields_desc = [ ByteEnumField("nh", 59, ipv6nh), + ByteField("len", None), + ByteEnumField("mhtype", 1, mhtypes), + ByteField("res", None), + XShortField("cksum", None), + StrFixedLenField("cookie", "\x00"*8, 8), + _PhantomAutoPadField("autopad", 1), # autopad activated by default + _MobilityOptionsField("options", [], MIP6OptUnknown, 16, + length_from = lambda pkt: 8*(pkt.len-1)) ] + overload_fields = { IPv6: { "nh": 135 } } + def hashret(self): + return self.cookie + +class MIP6MH_CoTI(MIP6MH_HoTI): + name = "IPv6 Mobility Header - Care-of Test Init" + __metaclass__ = NewDefaultValues + mhtype = 2 + def hashret(self): + return self.cookie + +class MIP6MH_HoT(_MobilityHeader): + name = "IPv6 Mobility Header - Home Test" + fields_desc = [ ByteEnumField("nh", 59, ipv6nh), + ByteField("len", None), + ByteEnumField("mhtype", 3, mhtypes), + ByteField("res", None), + XShortField("cksum", None), + ShortField("index", None), + StrFixedLenField("cookie", "\x00"*8, 8), + StrFixedLenField("token", "\x00"*8, 8), + _PhantomAutoPadField("autopad", 1), # autopad activated by default + _MobilityOptionsField("options", [], MIP6OptUnknown, 24, + length_from = lambda pkt: 8*(pkt.len-2)) ] + overload_fields = { IPv6: { "nh": 135 } } + def hashret(self): + return self.cookie + def answers(self): + if (isinstance(other, MIP6MH_HoTI) and + self.cookie == other.cookie): + return 1 + return 0 + +class MIP6MH_CoT(MIP6MH_HoT): + name = "IPv6 Mobility Header - Care-of Test" + __metaclass__ = NewDefaultValues + mhtype = 4 + def hashret(self): + return self.cookie + + def answers(self): + if (isinstance(other, MIP6MH_CoTI) and + self.cookie == other.cookie): + return 1 + return 0 + +class LifetimeField(ShortField): + def i2repr(self, pkt, x): + return "%d sec" % (4*x) + +class MIP6MH_BU(_MobilityHeader): + name = "IPv6 Mobility Header - Binding Update" + fields_desc = [ ByteEnumField("nh", 59, ipv6nh), + ByteField("len", None), # unit == 8 bytes (excluding the first 8 bytes) + ByteEnumField("mhtype", 5, mhtypes), + ByteField("res", None), + XShortField("cksum", None), + XShortField("seq", None), # TODO: ShortNonceField + FlagsField("flags", 49, 6, "AHLKMR"), + XBitField("reserved", 0, 10), + LifetimeField("mhtime", 3), # unit == 4 seconds + _PhantomAutoPadField("autopad", 1), # autopad activated by default + _MobilityOptionsField("options", [], MIP6OptUnknown, 12, + length_from = lambda pkt: 8*pkt.len - 4) ] + overload_fields = { IPv6: { "nh": 135 } } + + def hashret(self): # Hack: see comment in MIP6MH_BRR.hashret() + return "\x00\x08\x09" + + def answers(self, other): + if isinstance(other, MIP6MH_BRR): + return 1 + return 0 + +class MIP6MH_BA(_MobilityHeader): + name = "IPv6 Mobility Header - Binding ACK" + fields_desc = [ ByteEnumField("nh", 59, ipv6nh), + ByteField("len", None), # unit == 8 bytes (excluding the first 8 bytes) + ByteEnumField("mhtype", 6, mhtypes), + ByteField("res", None), + XShortField("cksum", None), + ByteEnumField("status", 0, bastatus), + FlagsField("flags", 2, 2, "KR"), + XBitField("res2", None, 6), + XShortField("seq", None), # TODO: ShortNonceField + XShortField("mhtime", 0), # unit == 4 seconds + _PhantomAutoPadField("autopad", 1), # autopad activated by default + _MobilityOptionsField("options", [], MIP6OptUnknown, 12, + length_from = lambda pkt: 8*pkt.len-4) ] + overload_fields = { IPv6: { "nh": 135 }} + + def hashret(self): # Hack: see comment in MIP6MH_BRR.hashret() + return "\x00\x08\x09" + + def answers(self, other): + if (isinstance(other, MIP6MH_BU) and + other.mhtype == 5 and + self.mhtype == 6 and + other.flags & 0x1 and # Ack request flags is set + self.seq == other.seq): + return 1 + return 0 + +_bestatus = { 1: 'Unknown binding for Home Address destination option', + 2: 'Unrecognized MH Type value' } + +# TODO: match Binding Error to its stimulus +class MIP6MH_BE(_MobilityHeader): + name = "IPv6 Mobility Header - Binding Error" + fields_desc = [ ByteEnumField("nh", 59, ipv6nh), + ByteField("len", None), # unit == 8 bytes (excluding the first 8 bytes) + ByteEnumField("mhtype", 7, mhtypes), + ByteField("res", 0), + XShortField("cksum", None), + ByteEnumField("status", 0, _bestatus), + ByteField("reserved", 0), + IP6Field("ha", "::"), + _MobilityOptionsField("options", [], MIP6OptUnknown, 24, + length_from = lambda pkt: 8*(pkt.len-2)) ] + overload_fields = { IPv6: { "nh": 135 }} + +_mip6_mhtype2cls = { 0: MIP6MH_BRR, + 1: MIP6MH_HoTI, + 2: MIP6MH_CoTI, + 3: MIP6MH_HoT, + 4: MIP6MH_CoT, + 5: MIP6MH_BU, + 6: MIP6MH_BA, + 7: MIP6MH_BE } + + +############################################################################# +############################################################################# +### Traceroute6 ### +############################################################################# +############################################################################# + +class AS_resolver6(AS_resolver_riswhois): + def _resolve_one(self, ip): + """ + overloaded version to provide a Whois resolution on the + embedded IPv4 address if the address is 6to4 or Teredo. + Otherwise, the native IPv6 address is passed. + """ + + if in6_isaddr6to4(ip): # for 6to4, use embedded @ + tmp = inet_pton(socket.AF_INET6, ip) + addr = inet_ntop(socket.AF_INET, tmp[2:6]) + elif in6_isaddrTeredo(ip): # for Teredo, use mapped address + addr = teredoAddrExtractInfo(ip)[2] + else: + addr = ip + + _, asn, desc = AS_resolver_riswhois._resolve_one(self, addr) + + return ip,asn,desc + +class TracerouteResult6(TracerouteResult): + def show(self): + return self.make_table(lambda (s,r): (s.sprintf("%-42s,IPv6.dst%:{TCP:tcp%TCP.dport%}{UDP:udp%UDP.dport%}{ICMPv6EchoRequest:IER}"), # TODO: ICMPv6 ! + s.hlim, + r.sprintf("%-42s,IPv6.src% {TCP:%TCP.flags%}"+ + "{ICMPv6DestUnreach:%ir,type%}{ICMPv6PacketTooBig:%ir,type%}"+ + "{ICMPv6TimeExceeded:%ir,type%}{ICMPv6ParamProblem:%ir,type%}"+ + "{ICMPv6EchoReply:%ir,type%}"))) + + def get_trace(self): + trace = {} + + for s,r in self.res: + if IPv6 not in s: + continue + d = s[IPv6].dst + if d not in trace: + trace[d] = {} + + t = not (ICMPv6TimeExceeded in r or + ICMPv6DestUnreach in r or + ICMPv6PacketTooBig in r or + ICMPv6ParamProblem in r) + + trace[d][s[IPv6].hlim] = r[IPv6].src, t + + for k in trace.values(): + m = filter(lambda x: k[x][1], k.keys()) + if not m: + continue + m = min(m) + for l in k.keys(): + if l > m: + del(k[l]) + + return trace + + def graph(self, ASres=AS_resolver6(), **kargs): + TracerouteResult.graph(self, ASres=ASres, **kargs) + +def traceroute6(target, dport=80, minttl=1, maxttl=30, sport=RandShort(), + l4 = None, timeout=2, verbose=None, **kargs): + """ + Instant TCP traceroute using IPv6 : + traceroute6(target, [maxttl=30], [dport=80], [sport=80]) -> None + """ + if verbose is None: + verbose = conf.verb + + if l4 is None: + a,b = sr(IPv6(dst=target, hlim=(minttl,maxttl))/TCP(seq=RandInt(),sport=sport, dport=dport), + timeout=timeout, filter="icmp6 or tcp", verbose=verbose, **kargs) + else: + a,b = sr(IPv6(dst=target, hlim=(minttl,maxttl))/l4, + timeout=timeout, verbose=verbose, **kargs) + + a = TracerouteResult6(a.res) + + if verbose: + a.display() + + return a,b + +############################################################################# +############################################################################# +### Sockets ### +############################################################################# +############################################################################# + +class L3RawSocket6(L3RawSocket): + def __init__(self, type = ETH_P_IPV6, filter=None, iface=None, promisc=None, nofilter=0): + L3RawSocket.__init__(self, type, filter, iface, promisc) + # NOTE: if fragmentation is needed, it will be done by the kernel (RFC 2292) + self.outs = socket.socket(socket.AF_INET6, socket.SOCK_RAW, socket.IPPROTO_RAW) + self.ins = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(type)) + +def IPv6inIP(dst='203.178.135.36', src=None): + _IPv6inIP.dst = dst + _IPv6inIP.src = src + if not conf.L3socket == _IPv6inIP: + _IPv6inIP.cls = conf.L3socket + else: + del(conf.L3socket) + return _IPv6inIP + +class _IPv6inIP(SuperSocket): + dst = '127.0.0.1' + src = None + cls = None + + def __init__(self, family=socket.AF_INET6, type=socket.SOCK_STREAM, proto=0, **args): + SuperSocket.__init__(self, family, type, proto) + self.worker = self.cls(**args) + + def set(self, dst, src=None): + _IPv6inIP.src = src + _IPv6inIP.dst = dst + + def nonblock_recv(self): + p = self.worker.nonblock_recv() + return self._recv(p) + + def recv(self, x): + p = self.worker.recv(x) + return self._recv(p, x) + + def _recv(self, p, x=MTU): + if p is None: + return p + elif isinstance(p, IP): + # TODO: verify checksum + if p.src == self.dst and p.proto == socket.IPPROTO_IPV6: + if isinstance(p.payload, IPv6): + return p.payload + return p + + def send(self, x): + return self.worker.send(IP(dst=self.dst, src=self.src, proto=socket.IPPROTO_IPV6)/x) + + +############################################################################# +############################################################################# +### Layers binding ### +############################################################################# +############################################################################# + +L3Types[ETH_P_IPV6] = IPv6 +LLTypes[31] = IPv6 +LLNumTypes[IPv6] = 31 + +bind_layers(Ether, IPv6, type = 0x86dd ) +bind_layers(IPerror6, TCPerror, nh = socket.IPPROTO_TCP ) +bind_layers(IPerror6, UDPerror, nh = socket.IPPROTO_UDP ) +bind_layers(IPv6, TCP, nh = socket.IPPROTO_TCP ) +bind_layers(IPv6, UDP, nh = socket.IPPROTO_UDP ) +bind_layers(IP, IPv6, proto = socket.IPPROTO_IPV6 ) +bind_layers(IPv6, IPv6, nh = socket.IPPROTO_IPV6 ) + +############################################################################# +### Conf overloading ### +############################################################################# + +def get_working_if6(): + """ + try to guess the best interface for conf.iface by looking for the + one used by default route if any. + """ + res = conf.route6.route("::/0") + if res: + iff, gw, addr = res + return iff + return get_working_if() + +conf.route6 = Route6() +conf.iface = get_working_if6() + +if __name__ == '__main__': + interact(mydict=globals(), mybanner="IPv6 enabled") +else: + import __builtin__ + __builtin__.__dict__.update(globals()) -- GitLab