def read_routes6(): """Return a list of IPv6 routes than can be used by Scapy.""" # Call netstat to retrieve IPv6 routes fd_netstat = os.popen("netstat -rn -f inet6") # List interfaces IPv6 addresses lifaddr = in6_getifaddr() if not lifaddr: return [] # Routes header information got_header = False mtu_present = False prio_present = False # Parse the routes routes = [] for line in fd_netstat.readlines(): # Parse the routes header and try to identify extra columns if not got_header: if "Destination" == line[:11]: got_header = True mtu_present = "Mtu" in line prio_present = "Prio" in line continue # Parse a route entry according to the operating system splitted_line = line.split() if OPENBSD or NETBSD: index = 5 + mtu_present + prio_present if len(splitted_line) < index: warning("Not enough columns in route entry !") continue destination, next_hop, flags = splitted_line[:3] dev = splitted_line[index] else: # FREEBSD or DARWIN if len(splitted_line) < 4: warning("Not enough columns in route entry !") continue destination, next_hop, flags, dev = splitted_line[:4] # XXX: TODO: add metrics for unix.py (use -e option on netstat) metric = 1 # Check flags if not "U" in flags: # usable route continue if "R" in flags: # Host or net unreachable continue if "m" in flags: # multicast address # Note: multicast routing is handled in Route6.route() continue # Replace link with the default route in next_hop if "link" in next_hop: next_hop = "::" # Default prefix length destination_plen = 128 # Extract network interface from the zone id if '%' in destination: destination, dev = destination.split('%') if '/' in dev: # Example: fe80::%lo0/64 ; dev = "lo0/64" dev, destination_plen = dev.split('/') if '%' in next_hop: next_hop, dev = next_hop.split('%') # Ensure that the next hop is a valid IPv6 address if not in6_isvalid(next_hop): # Note: the 'Gateway' column might contain a MAC address next_hop = "::" # Modify parsed routing entries # Note: these rules are OS specific and may evolve over time if destination == "default": destination, destination_plen = "::", 0 elif '/' in destination: # Example: fe80::/10 destination, destination_plen = destination.split('/') if '/' in dev: # Example: ff02::%lo0/32 ; dev = "lo0/32" dev, destination_plen = dev.split('/') # Check route entries parameters consistency if not in6_isvalid(destination): warning("Invalid destination IPv6 address in route entry !") continue try: destination_plen = int(destination_plen) except: warning("Invalid IPv6 prefix length in route entry !") continue if in6_ismlladdr(destination) or in6_ismnladdr(destination): # Note: multicast routing is handled in Route6.route() continue if LOOPBACK_NAME in dev: # Handle ::1 separately cset = ["::1"] next_hop = "::" else: # Get possible IPv6 source addresses devaddrs = (x for x in lifaddr if x[2] == dev) cset = construct_source_candidate_set(destination, destination_plen, devaddrs) if len(cset): routes.append((destination, destination_plen, next_hop, dev, cset, metric)) fd_netstat.close() return routes
def read_routes6(): """Return a list of IPv6 routes than can be used by Scapy.""" # Call netstat to retrieve IPv6 routes fd_netstat = os.popen("netstat -rn -f inet6") # List interfaces IPv6 addresses lifaddr = in6_getifaddr() if not lifaddr: return [] # Routes header information got_header = False mtu_present = False prio_present = False # Parse the routes routes = [] for line in fd_netstat.readlines(): # Parse the routes header and try to identify extra columns if not got_header: if "Destination" == line[:11]: got_header = True mtu_present = "Mtu" in line prio_present = "Prio" in line continue # Parse a route entry according to the operating system splitted_line = line.split() if OPENBSD or NETBSD: index = 5 + mtu_present + prio_present if len(splitted_line) < index: warning("Not enough columns in route entry !") continue destination, next_hop, flags = splitted_line[:3] dev = splitted_line[index] else: # FREEBSD or DARWIN if len(splitted_line) < 4: warning("Not enough columns in route entry !") continue destination, next_hop, flags, dev = splitted_line[:4] # XXX: TODO: add metrics for unix.py (use -e option on netstat) metric = 1 # Check flags if "U" not in flags: # usable route continue if "R" in flags: # Host or net unreachable continue if "m" in flags: # multicast address # Note: multicast routing is handled in Route6.route() continue # Replace link with the default route in next_hop if "link" in next_hop: next_hop = "::" # Default prefix length destination_plen = 128 # Extract network interface from the zone id if '%' in destination: destination, dev = destination.split('%') if '/' in dev: # Example: fe80::%lo0/64 ; dev = "lo0/64" dev, destination_plen = dev.split('/') if '%' in next_hop: next_hop, dev = next_hop.split('%') # Ensure that the next hop is a valid IPv6 address if not in6_isvalid(next_hop): # Note: the 'Gateway' column might contain a MAC address next_hop = "::" # Modify parsed routing entries # Note: these rules are OS specific and may evolve over time if destination == "default": destination, destination_plen = "::", 0 elif '/' in destination: # Example: fe80::/10 destination, destination_plen = destination.split('/') if '/' in dev: # Example: ff02::%lo0/32 ; dev = "lo0/32" dev, destination_plen = dev.split('/') # Check route entries parameters consistency if not in6_isvalid(destination): warning("Invalid destination IPv6 address in route entry !") continue try: destination_plen = int(destination_plen) except Exception: warning("Invalid IPv6 prefix length in route entry !") continue if in6_ismlladdr(destination) or in6_ismnladdr(destination): # Note: multicast routing is handled in Route6.route() continue if LOOPBACK_NAME in dev: # Handle ::1 separately cset = ["::1"] next_hop = "::" else: # Get possible IPv6 source addresses devaddrs = (x for x in lifaddr if x[2] == dev) cset = construct_source_candidate_set(destination, destination_plen, devaddrs) # noqa: E501 if len(cset): routes.append((destination, destination_plen, next_hop, dev, cset, metric)) # noqa: E501 fd_netstat.close() return routes
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") idx = dst.find("-") while idx >= 0: m = (dst[idx:] + ":").find(":") dst = dst[:idx] + dst[idx + m:] idx = 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 # Use the default interface while dealing with link-local addresses if dev is None and (in6_islladdr(dst) or in6_ismlladdr(dst)): dev = conf.iface # Deal with dev-specific request for cache search k = dst if dev is not None: k = dst + "%%" + (dev if isinstance(dev, six.string_types) else dev.pcap_name) # noqa: E501 if k in self.cache: return self.cache[k] paths = [] # 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, me in self.routes: if dev is not None and iface != dev: continue if in6_isincluded(dst, p, plen): paths.append((plen, me, (iface, cset, gw))) elif (in6_ismlladdr(dst) and in6_islladdr(p) and in6_islladdr(cset[0])): # noqa: E501 paths.append((plen, me, (iface, cset, gw))) if not paths: warning( "No route found for IPv6 destination %s (no default route?)", dst) # noqa: E501 return (scapy.consts.LOOPBACK_INTERFACE, "::", "::") # Sort with longest prefix first paths.sort(reverse=True, key=lambda x: x[0]) best_plen = paths[0][0] paths = [x for x in paths if x[0] == best_plen] res = [] for p in paths: # Here we select best source address for every route tmp = p[2] srcaddr = get_source_addr_from_candidate_set(dst, tmp[1]) if srcaddr is not None: res.append((p[0], p[1], (tmp[0], srcaddr, tmp[2]))) if res == []: warning( "Found a route for IPv6 destination '%s', but no possible source address.", dst) # noqa: E501 return (scapy.consts.LOOPBACK_INTERFACE, "::", "::") # Tie-breaker: Metrics paths.sort(key=lambda x: x[1]) paths = [i for i in paths if i[1] == paths[0][1]] # 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 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 = [x for x in res if in6_isaddr6to4(x[2][1])] elif in6_ismaddr(dst) or in6_islladdr(dst): # TODO : I'm sure we are not covering all addresses. Check that tmp = [x for x in res if x[2][0] == conf.iface6] if tmp: res = tmp # Fill the cache (including dev-specific request) k = dst if dev is not None: k = dst + "%%" + (dev if isinstance(dev, six.string_types) else dev.pcap_name) # noqa: E501 self.cache[k] = res[0][2] return res[0][2]
def route(self, dst="", dev=None, verbose=conf.verb): # type: (str, Optional[Any], int) -> Tuple[str, str, str] """ Provide best route to IPv6 destination address, based on Scapy 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. """ dst = dst or "::/0" # Enable route(None) to return default route # 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") idx = dst.find("-") while idx >= 0: m = (dst[idx:] + ":").find(":") dst = dst[:idx] + dst[idx + m:] idx = 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 # Choose a valid IPv6 interface while dealing with link-local addresses if dev is None and (in6_islladdr(dst) or in6_ismlladdr(dst)): dev = conf.iface # default interface # Check if the default interface supports IPv6! if dev not in self.ipv6_ifaces and self.ipv6_ifaces: tmp_routes = [route for route in self.routes if route[3] != conf.iface] default_routes = [route for route in tmp_routes if (route[0], route[1]) == ("::", 0)] ll_routes = [route for route in tmp_routes if (route[0], route[1]) == ("fe80::", 64)] if default_routes: # Fallback #1 - the first IPv6 default route dev = default_routes[0][3] elif ll_routes: # Fallback #2 - the first link-local prefix dev = ll_routes[0][3] else: # Fallback #3 - the loopback dev = conf.loopback_name warning("The conf.iface interface (%s) does not support IPv6! " "Using %s instead for routing!" % (conf.iface, dev)) # 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] paths = [] # type: List[Tuple[int, int, Tuple[str, List[str], str]]] # 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, me in self.routes: if dev is not None and iface != dev: continue if in6_isincluded(dst, p, plen): paths.append((plen, me, (iface, cset, gw))) elif (in6_ismlladdr(dst) and in6_islladdr(p) and in6_islladdr(cset[0])): # noqa: E501 paths.append((plen, me, (iface, cset, gw))) if not paths: if dst == "::1": return (conf.loopback_name, "::1", "::") else: if verbose: warning("No route found for IPv6 destination %s " "(no default route?)", dst) return (conf.loopback_name, "::", "::") # Sort with longest prefix first then use metrics as a tie-breaker paths.sort(key=lambda x: (-x[0], x[1])) best_plen = (paths[0][0], paths[0][1]) paths = [x for x in paths if (x[0], x[1]) == best_plen] res = [] # type: List[Tuple[int, int, Tuple[str, str, str]]] for path in paths: # we select best source address for every route tmp_c = path[2] srcaddr = get_source_addr_from_candidate_set(dst, tmp_c[1]) if srcaddr is not None: res.append((path[0], path[1], (tmp_c[0], srcaddr, tmp_c[2]))) if res == []: warning("Found a route for IPv6 destination '%s', but no possible source address.", dst) # noqa: E501 return (conf.loopback_name, "::", "::") # 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 if len(res) > 1: tmp = [] # type: List[Tuple[int, int, Tuple[str, str, str]]] 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 = [x for x in res if in6_isaddr6to4(x[2][1])] elif in6_ismaddr(dst) or in6_islladdr(dst): # TODO : I'm sure we are not covering all addresses. Check that tmp = [x for x in res if x[2][0] == conf.iface] 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][2] return res[0][2]
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") idx = dst.find("-") while idx >= 0: m = (dst[idx:] + ":").find(":") dst = dst[:idx] + dst[idx + m:] idx = 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 # Use the default interface while dealing with link-local addresses if dev is None and (in6_islladdr(dst) or in6_ismlladdr(dst)): dev = conf.iface # Deal with dev-specific request for cache search k = dst if dev is not None: k = dst + "%%" + (dev if isinstance(dev, six.string_types) else dev.pcap_name) # noqa: E501 if k in self.cache: return self.cache[k] paths = [] # 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, me in self.routes: if dev is not None and iface != dev: continue if in6_isincluded(dst, p, plen): paths.append((plen, me, (iface, cset, gw))) elif (in6_ismlladdr(dst) and in6_islladdr(p) and in6_islladdr(cset[0])): # noqa: E501 paths.append((plen, me, (iface, cset, gw))) if not paths: warning("No route found for IPv6 destination %s (no default route?)", dst) # noqa: E501 return (scapy.consts.LOOPBACK_INTERFACE, "::", "::") # Sort with longest prefix first paths.sort(reverse=True, key=lambda x: x[0]) best_plen = paths[0][0] paths = [x for x in paths if x[0] == best_plen] res = [] for p in paths: # Here we select best source address for every route tmp = p[2] srcaddr = get_source_addr_from_candidate_set(dst, tmp[1]) if srcaddr is not None: res.append((p[0], p[1], (tmp[0], srcaddr, tmp[2]))) if res == []: warning("Found a route for IPv6 destination '%s', but no possible source address.", dst) # noqa: E501 return (scapy.consts.LOOPBACK_INTERFACE, "::", "::") # Tie-breaker: Metrics paths.sort(key=lambda x: x[1]) paths = [i for i in paths if i[1] == paths[0][1]] # 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 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 = [x for x in res if in6_isaddr6to4(x[2][1])] elif in6_ismaddr(dst) or in6_islladdr(dst): # TODO : I'm sure we are not covering all addresses. Check that tmp = [x for x in res if x[2][0] == conf.iface6] if tmp: res = tmp # Fill the cache (including dev-specific request) k = dst if dev is not None: k = dst + "%%" + (dev if isinstance(dev, six.string_types) else dev.pcap_name) # noqa: E501 self.cache[k] = res[0][2] return res[0][2]