From 8f9468ba694b07d74b261070fef0691e74ffc4f1 Mon Sep 17 00:00:00 2001 From: Phil <phil@secdev.org> Date: Fri, 12 Sep 2008 17:57:20 +0200 Subject: [PATCH] Split IPv6 route management --- scapy/all.py | 2 + scapy/arch/linux.py | 2 +- scapy/config.py | 1 + scapy/layers/inet6.py | 331 ------------------------------------------ scapy/route6.py | 271 ++++++++++++++++++++++++++++++++++ scapy/utils6.py | 57 ++++++++ 6 files changed, 332 insertions(+), 332 deletions(-) create mode 100644 scapy/route6.py diff --git a/scapy/all.py b/scapy/all.py index dd4f84b3..c76d08b5 100644 --- a/scapy/all.py +++ b/scapy/all.py @@ -20,6 +20,8 @@ from asn1packet import * from utils import * from route import * +from utils6 import * +from route6 import * from sendrecv import * from supersocket import * from volatile import * diff --git a/scapy/arch/linux.py b/scapy/arch/linux.py index fed3efc5..5f99507a 100644 --- a/scapy/arch/linux.py +++ b/scapy/arch/linux.py @@ -250,7 +250,7 @@ def read_routes6(): cset = ['::1'] else: devaddrs = filter(lambda x: x[2] == dev, lifaddr) - cset = construct_source_candidate_set(d, dp, devaddrs) + cset = scapy.utils6.construct_source_candidate_set(d, dp, devaddrs) if len(cset) != 0: routes.append((d, dp, nh, dev, cset)) diff --git a/scapy/config.py b/scapy/config.py index bb8d1d01..a213cd2f 100644 --- a/scapy/config.py +++ b/scapy/config.py @@ -293,6 +293,7 @@ extensions_paths: path or list of paths where extensions are to be looked for debug_match = 0 wepkey = "" route = None # Filed by route.py + route6 = None # Filed by route6.py auto_fragment = 1 debug_dissector = 0 color_theme = themes.DefaultTheme() diff --git a/scapy/layers/inet6.py b/scapy/layers/inet6.py index f23db6ee..115f3278 100644 --- a/scapy/layers/inet6.py +++ b/scapy/layers/inet6.py @@ -42,314 +42,6 @@ def get_cls(name, fallback_cls): return globals().get(name, fallback_cls) - -############################################################################# -############################################################################# -### 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, LOOPBACK_NAME)] - 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 (LOOPBACK_NAME, "::", "::") # 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.iface6) - # - 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.iface6, 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] - - - ########################## ## Neighbor cache stuff ## ########################## @@ -5123,26 +4815,3 @@ 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.iface6 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.iface6 = get_working_if6() - -if __name__ == '__main__': - interact(mydict=globals(), mybanner="IPv6 enabled") -else: - import __builtin__ - __builtin__.__dict__.update(globals()) diff --git a/scapy/route6.py b/scapy/route6.py new file mode 100644 index 00000000..3cdf9ab2 --- /dev/null +++ b/scapy/route6.py @@ -0,0 +1,271 @@ +## This file is part of Scapy +## See http://www.secdev.org/projects/scapy for more informations +## Copyright (C) Philippe Biondi <phil@secdev.org> +## This program is published under a GPLv2 license + +## Copyright (C) 2005 Guillaume Valadon <guedou@hongo.wide.ad.jp> +## Arnaud Ebalard <arnaud.ebalard@eads.net> + + +############################################################################# +############################################################################# +### Routing/Interfaces stuff ### +############################################################################# +############################################################################# + +import socket +from config import conf +from utils6 import * +from arch import * + + +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 (LOOPBACK_NAME, "::", "::") # 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.iface6) + # - 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.iface6, 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] + +conf.route6 = Route6() + +_res = conf.route6.route("::/0") +if _res: + iff, gw, addr = _res + conf.iface6 = iff +del(_res) + diff --git a/scapy/utils6.py b/scapy/utils6.py index c3d67e38..51965428 100644 --- a/scapy/utils6.py +++ b/scapy/utils6.py @@ -12,6 +12,63 @@ from data import * from utils import * +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, LOOPBACK_NAME)] + 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] + def find_ifaddr2(addr, plen, laddr): dstAddrType = in6_getAddrType(addr) -- GitLab