From 3b501781031e6b49382b3ea0fe7b0e186080793e Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sat, 13 Jul 2024 16:31:11 +0200 Subject: [PATCH] Support for Scope Identifiers in IP addresses --- doc/scapy/usage.rst | 42 ++++++++++++++- scapy/arch/linux/rtnetlink.py | 29 +++++++++- scapy/base_classes.py | 99 ++++++++++++++++++++++++++++++++--- scapy/fields.py | 85 ++++++++++++++---------------- scapy/layers/dns.py | 20 +++---- scapy/layers/hsrp.py | 2 +- scapy/layers/inet.py | 11 ++-- scapy/layers/inet6.py | 13 +++-- scapy/layers/l2.py | 15 +++--- scapy/libs/ethertypes.py | 2 + scapy/main.py | 2 +- scapy/route.py | 21 +++++--- scapy/route6.py | 6 +-- scapy/sendrecv.py | 87 ++++++++++++++++++++++-------- test/fields.uts | 2 +- test/linux.uts | 53 +++++++++++++++++-- 16 files changed, 370 insertions(+), 119 deletions(-) diff --git a/doc/scapy/usage.rst b/doc/scapy/usage.rst index d4782e5b49f..a32792dbeaf 100644 --- a/doc/scapy/usage.rst +++ b/doc/scapy/usage.rst @@ -252,6 +252,31 @@ Now that we know how to manipulate packets. Let's see how to send them. The send Sent 1 packets. +.. _multicast: + +Multicast on layer 3: Scope Identifiers +--------------------------------------- + +.. index:: + single: Multicast + +.. note:: This feature is only available since Scapy 2.6.0. + +If you try to use multicast addresses (IPv4) or link-local addresses (IPv6), you'll notice that Scapy follows the routing table and takes the first entry. In order to specify which interface to use when looking through the routing table, Scapy supports scope identifiers (similar to RFC6874 but for both IPv6 and IPv4). + +.. code:: python + + >>> conf.checkIPaddr = False # answer IP will be != from the one we requested + # send on interface 'eth0' + >>> sr(IP(dst="224.0.0.1%eth0")/ICMP(), multi=True) + >>> sr(IPv6(dst="ff02::1%eth0")/ICMPv6EchoRequest(), multi=True) + +You can use both ``%eth0`` format or ``%15`` (the interface id) format. You can query those using ``conf.ifaces``. + +.. note:: + + Behind the scene, calling ``IP(dst="224.0.0.1%eth0")`` creates a ``ScopedIP`` object that contains ``224.0.0.1`` on the scope of the interface ``eth0``. If you are using an interface object (for instance ``conf.iface``), you can also craft that object. For instance:: + >>> pkt = IP(dst=ScopedIP("224.0.0.1", scope=conf.iface))/ICMP() Fuzzing ------- @@ -1488,9 +1513,24 @@ NBNS Query Request (find by NetbiosName) .. code:: - >>> conf.checkIPaddr = False # Mandatory because we are using a broadcast destination + >>> conf.checkIPaddr = False # Mandatory because we are using a broadcast destination and receiving unicast >>> sr1(IP(dst="192.168.0.255")/UDP()/NBNSHeader()/NBNSQueryRequest(QUESTION_NAME="DC1")) +mDNS Query Request +------------------ + +For instance, find all spotify connect devices. + +.. code:: + + >>> # For interface 'eth0' + >>> ans, _ = sr(IPv6(dst="ff02::fb%eth0")/UDP(sport=5353, dport=5353)/DNS(rd=0, qd=[DNSQR(qname='_spotify-connect._tcp.local', qtype="PTR")]), multi=True, timeout=2) + >>> ans.show() + +.. note:: + + As you can see, we used a scope identifier (``%eth0``) to specify on which interface we want to use the above multicast IP. + Advanced traceroute ------------------- diff --git a/scapy/arch/linux/rtnetlink.py b/scapy/arch/linux/rtnetlink.py index 7241949f720..9a920e2ce71 100644 --- a/scapy/arch/linux/rtnetlink.py +++ b/scapy/arch/linux/rtnetlink.py @@ -741,7 +741,7 @@ def _sr1_rtrequest(pkt: Packet) -> List[Packet]: if msg.nlmsg_type == 3 and nlmsgerr in msg and msg.status != 0: # NLMSG_DONE with errors if msg.data and msg.data[0].rta_type == 1: - log_loading.warning( + log_loading.debug( "Scapy RTNETLINK error on %s: '%s'. Please report !", pkt.sprintf("%nlmsg_type%"), msg.data[0].rta_data.decode(), @@ -908,6 +908,20 @@ def read_routes(): elif attr.rta_type == 0x07: # RTA_PREFSRC addr = attr.rta_data routes.append((net, mask, gw, iface, addr, metric)) + # Add multicast routes, as those are missing by default + for _iface in ifaces.values(): + if _iface['flags'].MULTICAST: + try: + addr = next( + x["address"] + for x in _iface["ips"] + if x["af_family"] == socket.AF_INET + ) + except StopIteration: + continue + routes.append(( + 0xe0000000, 0xf0000000, "0.0.0.0", _iface["name"], addr, 250 + )) return routes @@ -945,4 +959,17 @@ def read_routes6(): cset = scapy.utils6.construct_source_candidate_set(prefix, plen, devaddrs) if cset: routes.append((prefix, plen, nh, iface, cset, metric)) + # Add multicast routes, as those are missing by default + for _iface in ifaces.values(): + if _iface['flags'].MULTICAST: + addrs = [ + x["address"] + for x in _iface["ips"] + if x["af_family"] == socket.AF_INET6 + ] + if not addrs: + continue + routes.append(( + "ff00::", 8, "::", _iface["name"], addrs, 250 + )) return routes diff --git a/scapy/base_classes.py b/scapy/base_classes.py index a85df45d9e9..6940223dc3e 100644 --- a/scapy/base_classes.py +++ b/scapy/base_classes.py @@ -109,8 +109,75 @@ def __repr__(self): return "" % self.values +class _ScopedIP(str): + """ + A str that also holds extra attributes. + """ + __slots__ = ["scope"] + + def __init__(self, _: str) -> None: + self.scope = None + + def __repr__(self) -> str: + val = super(_ScopedIP, self).__repr__() + if self.scope is not None: + return "ScopedIP(%s, scope=%s)" % (val, repr(self.scope)) + return val + + +def ScopedIP(net: str, scope: Optional[Any] = None) -> _ScopedIP: + """ + An str that also holds extra attributes. + + Examples:: + + >>> ScopedIP("224.0.0.1%eth0") # interface 'eth0' + >>> ScopedIP("224.0.0.1%1") # interface index 1 + >>> ScopedIP("224.0.0.1", scope=conf.iface) + """ + if "%" in net: + try: + net, scope = net.split("%", 1) + except ValueError: + raise Scapy_Exception("Scope identifier can only be present once !") + if scope is not None: + from scapy.interfaces import resolve_iface, network_name, dev_from_index + try: + iface = dev_from_index(int(scope)) + except (ValueError, TypeError): + iface = resolve_iface(scope) + if not iface.is_valid(): + raise Scapy_Exception( + "RFC6874 scope identifier '%s' could not be resolved to a " + "valid interface !" % scope + ) + scope = network_name(iface) + x = _ScopedIP(net) + x.scope = scope + return x + + class Net(Gen[str]): - """Network object from an IP address or hostname and mask""" + """ + Network object from an IP address or hostname and mask + + Examples: + + - With mask:: + + >>> list(Net("192.168.0.1/24")) + ['192.168.0.0', '192.168.0.1', ..., '192.168.0.255'] + + - With 'end':: + + >>> list(Net("192.168.0.100", "192.168.0.200")) + ['192.168.0.100', '192.168.0.101', ..., '192.168.0.200'] + + - With 'scope' (for multicast):: + + >>> Net("224.0.0.1%lo") + >>> Net("224.0.0.1", scope=conf.iface) + """ name = "Net" # type: str family = socket.AF_INET # type: int max_mask = 32 # type: int @@ -143,11 +210,16 @@ def int2ip(val): # type: (int) -> str return socket.inet_ntoa(struct.pack('!I', val)) - def __init__(self, net, stop=None): - # type: (str, Union[None, str]) -> None + def __init__(self, net, stop=None, scope=None): + # type: (str, Optional[str], Optional[str]) -> None if "*" in net: raise Scapy_Exception("Wildcards are no longer accepted in %s()" % self.__class__.__name__) + self.scope = None + if "%" in net: + net = ScopedIP(net) + if isinstance(net, _ScopedIP): + self.scope = net.scope if stop is None: try: net, mask = net.split("/", 1) @@ -174,7 +246,10 @@ def __iter__(self): # type: () -> Iterator[str] # Python 2 won't handle huge (> sys.maxint) values in range() for i in range(self.count): - yield self.int2ip(self.start + i) + yield ScopedIP( + self.int2ip(self.start + i), + scope=self.scope, + ) def __len__(self): # type: () -> int @@ -187,20 +262,28 @@ def __iterlen__(self): def choice(self): # type: () -> str - return self.int2ip(random.randint(self.start, self.stop)) + return ScopedIP( + self.int2ip(random.randint(self.start, self.stop)), + scope=self.scope, + ) def __repr__(self): # type: () -> str + scope_id_repr = "" + if self.scope: + scope_id_repr = ", scope=%s" % repr(self.scope) if self.mask is not None: - return '%s("%s/%d")' % ( + return '%s("%s/%d"%s)' % ( self.__class__.__name__, self.net, self.mask, + scope_id_repr, ) - return '%s("%s", "%s")' % ( + return '%s("%s", "%s"%s)' % ( self.__class__.__name__, self.int2ip(self.start), self.int2ip(self.stop), + scope_id_repr, ) def __eq__(self, other): @@ -220,7 +303,7 @@ def __ne__(self, other): def __hash__(self): # type: () -> int - return hash(("scapy.Net", self.family, self.start, self.stop)) + return hash(("scapy.Net", self.family, self.start, self.stop, self.scope)) def __contains__(self, other): # type: (Any) -> bool diff --git a/scapy/fields.py b/scapy/fields.py index 3e459bf5615..e65843de766 100644 --- a/scapy/fields.py +++ b/scapy/fields.py @@ -37,8 +37,13 @@ from scapy.utils import inet_aton, inet_ntoa, lhex, mac2str, str2mac, EDecimal from scapy.utils6 import in6_6to4ExtractAddr, in6_isaddr6to4, \ in6_isaddrTeredo, in6_ptop, Net6, teredoAddrExtractInfo -from scapy.base_classes import Gen, Net, BasePacket, Field_metaclass -from scapy.error import warning +from scapy.base_classes import ( + _ScopedIP, + BasePacket, + Field_metaclass, + Net, + ScopedIP, +) # Typing imports from typing import ( @@ -848,7 +853,10 @@ def h2i(self, pkt, x): # type: (Optional[Packet], Union[AnyStr, List[AnyStr]]) -> Any if isinstance(x, bytes): x = plain_str(x) # type: ignore - if isinstance(x, str): + if isinstance(x, _ScopedIP): + return x + elif isinstance(x, str): + x = ScopedIP(x) try: inet_aton(x) except socket.error: @@ -893,6 +901,8 @@ def any2i(self, pkt, x): def i2repr(self, pkt, x): # type: (Optional[Packet], Union[str, Net]) -> str + if isinstance(x, _ScopedIP) and x.scope: + return repr(x) r = self.resolve(self.i2h(pkt, x)) return r if isinstance(r, str) else repr(r) @@ -902,29 +912,16 @@ def randval(self): class SourceIPField(IPField): - __slots__ = ["dstname"] - - def __init__(self, name, dstname): - # type: (str, Optional[str]) -> None + def __init__(self, name): + # type: (str) -> None IPField.__init__(self, name, None) - self.dstname = dstname def __findaddr(self, pkt): - # type: (Packet) -> str + # type: (Packet) -> Optional[str] if conf.route is None: # unused import, only to initialize conf.route import scapy.route # noqa: F401 - dst = ("0.0.0.0" if self.dstname is None - else getattr(pkt, self.dstname) or "0.0.0.0") - if isinstance(dst, (Gen, list)): - r = { - conf.route.route(str(daddr)) - for daddr in dst - } # type: Set[Tuple[str, str, str]] - if len(r) > 1: - warning("More than one possible route for %r" % (dst,)) - return min(r)[1] - return conf.route.route(dst)[1] + return pkt.route()[1] or conf.route.route()[1] def i2m(self, pkt, x): # type: (Optional[Packet], Optional[Union[str, Net]]) -> bytes @@ -945,18 +942,21 @@ def __init__(self, name, default): Field.__init__(self, name, default, "16s") def h2i(self, pkt, x): - # type: (Optional[Packet], Optional[str]) -> str + # type: (Optional[Packet], Any) -> str if isinstance(x, bytes): x = plain_str(x) - if isinstance(x, str): + if isinstance(x, _ScopedIP): + return x + elif isinstance(x, str): + x = ScopedIP(x) try: - x = in6_ptop(x) + x = ScopedIP(in6_ptop(x), scope=x.scope) except socket.error: return Net6(x) # type: ignore elif isinstance(x, tuple): if len(x) != 2: raise ValueError("Invalid IPv6 format") - return Net6(*x) + return Net6(*x) # type: ignore elif isinstance(x, list): x = [self.h2i(pkt, n) for n in x] return x # type: ignore @@ -990,6 +990,8 @@ def i2repr(self, pkt, x): elif in6_isaddr6to4(x): # print encapsulated address vaddr = in6_6to4ExtractAddr(x) return "%s [6to4 GW: %s]" % (self.i2h(pkt, x), vaddr) + elif isinstance(x, _ScopedIP) and x.scope: + return repr(x) r = self.i2h(pkt, x) # No specific information to return return r if isinstance(r, str) else repr(r) @@ -999,36 +1001,27 @@ def randval(self): class SourceIP6Field(IP6Field): - __slots__ = ["dstname"] - - def __init__(self, name, dstname): - # type: (str, str) -> None + def __init__(self, name): + # type: (str) -> None IP6Field.__init__(self, name, None) - self.dstname = dstname + + def __findaddr(self, pkt): + # type: (Packet) -> Optional[str] + if conf.route6 is None: + # unused import, only to initialize conf.route + import scapy.route6 # noqa: F401 + return pkt.route()[1] def i2m(self, pkt, x): # type: (Optional[Packet], Optional[Union[str, Net6]]) -> bytes - if x is None: - dst = ("::" if self.dstname is None else - getattr(pkt, self.dstname) or "::") - iff, x, nh = conf.route6.route(dst) + if x is None and pkt is not None: + x = self.__findaddr(pkt) return super(SourceIP6Field, self).i2m(pkt, x) def i2h(self, pkt, x): # type: (Optional[Packet], Optional[Union[str, Net6]]) -> str - if x is None: - if conf.route6 is None: - # unused import, only to initialize conf.route6 - import scapy.route6 # noqa: F401 - dst = ("::" if self.dstname is None else getattr(pkt, self.dstname)) # noqa: E501 - if isinstance(dst, (Gen, list)): - r = {conf.route6.route(str(daddr)) - for daddr in dst} - if len(r) > 1: - warning("More than one possible route for %r" % (dst,)) - x = min(r)[1] - else: - x = conf.route6.route(dst)[1] + if x is None and pkt is not None: + x = self.__findaddr(pkt) return super(SourceIP6Field, self).i2h(pkt, x) diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py index 0f1b3deb623..eb566f63bdd 100644 --- a/scapy/layers/dns.py +++ b/scapy/layers/dns.py @@ -27,7 +27,7 @@ read_nameservers, ) from scapy.ansmachine import AnsweringMachine -from scapy.base_classes import Net +from scapy.base_classes import Net, ScopedIP from scapy.config import conf from scapy.compat import orb, raw, chb, bytes_encode, plain_str from scapy.error import log_runtime, warning, Scapy_Exception @@ -1918,16 +1918,14 @@ def __init__(self, ): SndRcvList.__init__(self, res, name, stats) - def show(self, *args, **kwargs): - # type: (*Any, **Any) -> None + def show(self, types=['PTR', 'SRV'], alltypes=False): + # type: (List[str], bool) -> None """ Print the list of discovered services. :param types: types to show. Default ['PTR', 'SRV'] :param alltypes: show all types. Default False """ - types = kwargs.get("types", ['PTR', 'SRV']) - alltypes = kwargs.get("alltypes", False) if alltypes: types = None data = list() # type: List[Tuple[str | List[str], ...]] @@ -1969,8 +1967,10 @@ def show(self, *args, **kwargs): @conf.commands.register def dnssd(service="_services._dns-sd._udp.local", - af=socket.AF_INET6, + af=socket.AF_INET, qtype="PTR", + iface=None, + verbose=2, timeout=3): """ Performs a DNS-SD (RFC6763) request @@ -1978,15 +1978,15 @@ def dnssd(service="_services._dns-sd._udp.local", :param service: the service name to query (e.g. _spotify-connect._tcp.local) :param af: the transport to use. socket.AF_INET or socket.AF_INET6 :param qtype: the type to use in the mDNS. Either TXT, PTR or SRV. - :param ret: return instead of printing + :param iface: the interface to do this discovery on. """ if af == socket.AF_INET: - pkt = IP(dst="224.0.0.251") + pkt = IP(dst=ScopedIP("224.0.0.251", iface), ttl=255) elif af == socket.AF_INET6: - pkt = IPv6(dst="ff02::fb") + pkt = IPv6(dst=ScopedIP("ff02::fb", iface)) else: return pkt /= UDP(sport=5353, dport=5353) pkt /= DNS(rd=0, qd=[DNSQR(qname=service, qtype=qtype)]) - ans, _ = sr(pkt, multi=True, timeout=timeout) + ans, _ = sr(pkt, multi=True, timeout=timeout, verbose=verbose) return DNSSDResult(ans.res) diff --git a/scapy/layers/hsrp.py b/scapy/layers/hsrp.py index ad9554382f9..82e82606357 100644 --- a/scapy/layers/hsrp.py +++ b/scapy/layers/hsrp.py @@ -48,7 +48,7 @@ class HSRPmd5(Packet): ByteEnumField("algo", 0, {1: "MD5"}), ByteField("padding", 0x00), XShortField("flags", 0x00), - SourceIPField("sourceip", None), + SourceIPField("sourceip"), XIntField("keyid", 0x00), StrFixedLenField("authdigest", b"\00" * 16, 16)] diff --git a/scapy/layers/inet.py b/scapy/layers/inet.py index ed8ce7e2ae9..a361664a681 100644 --- a/scapy/layers/inet.py +++ b/scapy/layers/inet.py @@ -18,7 +18,7 @@ from scapy.utils import checksum, do_graph, incremental_label, \ linehexdump, strxor, whois, colgen from scapy.ansmachine import AnsweringMachine -from scapy.base_classes import Gen, Net +from scapy.base_classes import Gen, Net, _ScopedIP from scapy.data import ETH_P_IP, ETH_P_ALL, DLT_RAW, DLT_RAW_ALT, DLT_IPV4, \ IP_PROTOS, TCP_SERVICES, UDP_SERVICES from scapy.layers.l2 import ( @@ -543,7 +543,7 @@ class IP(Packet, IPTools): ByteEnumField("proto", 0, IP_PROTOS), XShortField("chksum", None), # IPField("src", "127.0.0.1"), - Emph(SourceIPField("src", "dst")), + Emph(SourceIPField("src")), Emph(DestIPField("dst", "127.0.0.1")), PacketListField("options", [], IPOption, length_from=lambda p:p.ihl * 4 - 20)] # noqa: E501 @@ -569,12 +569,15 @@ def extract_padding(self, s): def route(self): dst = self.dst - if isinstance(dst, Gen): + scope = None + if isinstance(dst, (Net, _ScopedIP)): + scope = dst.scope + if isinstance(dst, (Gen, list)): dst = next(iter(dst)) if conf.route is None: # unused import, only to initialize conf.route import scapy.route # noqa: F401 - return conf.route.route(dst) + return conf.route.route(dst, dev=scope) def hashret(self): if ((self.proto == socket.IPPROTO_ICMP) and diff --git a/scapy/layers/inet6.py b/scapy/layers/inet6.py index 18d5f2f2fda..f1ecc210c06 100644 --- a/scapy/layers/inet6.py +++ b/scapy/layers/inet6.py @@ -20,7 +20,7 @@ from scapy.arch import get_if_hwaddr from scapy.as_resolvers import AS_resolver_riswhois -from scapy.base_classes import Gen +from scapy.base_classes import Gen, _ScopedIP from scapy.compat import chb, orb, raw, plain_str, bytes_encode from scapy.consts import WINDOWS from scapy.config import conf @@ -149,7 +149,7 @@ def neighsol(addr, src, iface, timeout=1, chainCC=0): def getmacbyip6(ip6, chainCC=0): # type: (str, int) -> Optional[str] """ - Returns the MAC address used to reach a given IPv6 address. + Returns the MAC address of the next hop used to reach a given IPv6 address. neighborCache.get() method is used on instantiated neighbor cache. Resolution mechanism is described in associated doc string. @@ -319,15 +319,18 @@ class IPv6(_IPv6GuessPayload, Packet, IPTools): ShortField("plen", None), ByteEnumField("nh", 59, ipv6nh), ByteField("hlim", 64), - SourceIP6Field("src", "dst"), # dst is for src @ selection + SourceIP6Field("src"), DestIP6Field("dst", "::1")] def route(self): """Used to select the L2 address""" dst = self.dst - if isinstance(dst, Gen): + scope = None + if isinstance(dst, (Net6, _ScopedIP)): + scope = dst.scope + if isinstance(dst, (Gen, list)): dst = next(iter(dst)) - return conf.route6.route(dst) + return conf.route6.route(dst, dev=scope) def mysummary(self): return "%s > %s (%i)" % (self.src, self.dst, self.nh) diff --git a/scapy/layers/l2.py b/scapy/layers/l2.py index 12c302dfd18..48dfbf5d5fd 100644 --- a/scapy/layers/l2.py +++ b/scapy/layers/l2.py @@ -14,7 +14,7 @@ from scapy.ansmachine import AnsweringMachine from scapy.arch import get_if_addr, get_if_hwaddr -from scapy.base_classes import Gen, Net +from scapy.base_classes import Gen, Net, _ScopedIP from scapy.compat import chb from scapy.config import conf from scapy import consts @@ -134,7 +134,7 @@ def __repr__(self): def getmacbyip(ip, chainCC=0): # type: (str, int) -> Optional[str] """ - Returns the MAC address used to reach a given IP address. + Returns the destination MAC address used to reach a given IP address. This will follow the routing table and will issue an ARP request if necessary. Special cases (multicast, etc.) are also handled. @@ -492,13 +492,13 @@ class ARP(Packet): ), MultipleTypeField( [ - (SourceIPField("psrc", "pdst"), + (SourceIPField("psrc"), (lambda pkt: pkt.ptype == 0x0800 and pkt.plen == 4, lambda pkt, val: pkt.ptype == 0x0800 and ( pkt.plen == 4 or (pkt.plen is None and (val is None or valid_net(val))) ))), - (SourceIP6Field("psrc", "pdst"), + (SourceIP6Field("psrc"), (lambda pkt: pkt.ptype == 0x86dd and pkt.plen == 16, lambda pkt, val: pkt.ptype == 0x86dd and ( pkt.plen == 16 or (pkt.plen is None and @@ -561,12 +561,15 @@ def route(self): fld, dst = cast(Tuple[MultipleTypeField, str], self.getfield_and_val("pdst")) fld_inner, dst = fld._find_fld_pkt_val(self, dst) + scope = None + if isinstance(dst, (Net, _ScopedIP)): + scope = dst.scope if isinstance(dst, Gen): dst = next(iter(dst)) if isinstance(fld_inner, IP6Field): - return conf.route6.route(dst) + return conf.route6.route(dst, dev=scope) elif isinstance(fld_inner, IPField): - return conf.route.route(dst) + return conf.route.route(dst, dev=scope) else: return None, None, None diff --git a/scapy/libs/ethertypes.py b/scapy/libs/ethertypes.py index c93aaeac0bf..6ce6850294b 100644 --- a/scapy/libs/ethertypes.py +++ b/scapy/libs/ethertypes.py @@ -35,6 +35,8 @@ */ """ +# To quote Python's get-pip: + # Hi There! # # You may be wondering what this giant blob of binary data here is, you might diff --git a/scapy/main.py b/scapy/main.py index 0fdd4a1982a..d84c6e3f8c4 100644 --- a/scapy/main.py +++ b/scapy/main.py @@ -61,7 +61,7 @@ "the wires and in the waves.", "Jean-Claude Van Damme"), ("We are in France, we say Skappee. OK? Merci.", "Sebastien Chabal"), ("Wanna support scapy? Star us on GitHub!", "Satoshi Nakamoto"), - ("What is dead may never die!", "Python 2"), + ("I'll be back.", "Python 2"), ] diff --git a/scapy/route.py b/scapy/route.py index af1411aa329..9e078bfbbc7 100644 --- a/scapy/route.py +++ b/scapy/route.py @@ -38,7 +38,7 @@ def __init__(self): def invalidate_cache(self): # type: () -> None - self.cache = {} # type: Dict[str, Tuple[str, str, str]] + self.cache = {} # type: Dict[Tuple[str, Optional[str]], Tuple[str, str, str]] def resync(self): # type: () -> None @@ -165,11 +165,13 @@ def ifadd(self, iff, addr): the_net = the_rawaddr & the_msk self.routes.append((the_net, the_msk, '0.0.0.0', iff, the_addr, 1)) - def route(self, dst=None, verbose=conf.verb, _internal=False): - # type: (Optional[str], int, bool) -> Tuple[str, str, str] + def route(self, dst=None, dev=None, verbose=conf.verb, _internal=False): + # type: (Optional[str], Optional[str], int, bool) -> Tuple[str, str, str] """Returns the IPv4 routes to a host. :param dst: the IPv4 of the destination host + :param dev: (optional) filtering is performed to limit search to route + associated to that interface. :returns: tuple (iface, output_ip, gateway_ip) where - ``iface``: the interface used to connect to the host @@ -182,8 +184,8 @@ def route(self, dst=None, verbose=conf.verb, _internal=False): dst = plain_str(dst) except UnicodeDecodeError: raise TypeError("Unknown IP address input (bytes)") - if dst in self.cache: - return self.cache[dst] + if (dst, dev) in self.cache: + return self.cache[(dst, dev)] # Transform "192.168.*.1-5" to one IP of the set _dst = dst.split("/")[0].replace("*", "0") while True: @@ -198,6 +200,8 @@ def route(self, dst=None, verbose=conf.verb, _internal=False): for d, m, gw, i, a, me in self.routes: if not a: # some interfaces may not currently be connected continue + if dev is not None and i != dev: + continue aa = atol(a) if aa == atol_dst: paths.append( @@ -208,8 +212,9 @@ def route(self, dst=None, verbose=conf.verb, _internal=False): if not paths: if verbose: - warning("No route found (no default route?)") - return conf.loopback_name, "0.0.0.0", "0.0.0.0" + warning("No route found for IPv4 destination %s " + "(no default route?)", dst) + return (dev or conf.loopback_name, "0.0.0.0", "0.0.0.0") # Choose the more specific route # Sort by greatest netmask and use metrics as a tie-breaker paths.sort(key=lambda x: (-x[0], x[1])) @@ -219,7 +224,7 @@ def route(self, dst=None, verbose=conf.verb, _internal=False): if ret[1] == "0.0.0.0" and not _internal: # Then get the source from route(gw) ret = (ret[0], self.route(ret[2], _internal=True)[1], ret[2]) - self.cache[dst] = ret + self.cache[(dst, dev)] = ret return ret def get_if_bcast(self, iff): diff --git a/scapy/route6.py b/scapy/route6.py index dd86b26ca6b..3862644c96a 100644 --- a/scapy/route6.py +++ b/scapy/route6.py @@ -220,7 +220,7 @@ def ifadd(self, iff, addr): self.ipv6_ifaces.add(iff) def route(self, dst="", dev=None, verbose=conf.verb): - # type: (str, Optional[Any], int) -> Tuple[str, str, str] + # type: (str, Optional[str], int) -> Tuple[str, str, str] """ Provide best route to IPv6 destination address, based on Scapy internal routing table content. @@ -254,7 +254,7 @@ def route(self, dst="", dev=None, verbose=conf.verb): # 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 + dev = str(conf.iface) # default interface # Check if the default interface supports IPv6! if dev not in self.ipv6_ifaces and self.ipv6_ifaces: @@ -309,7 +309,7 @@ def route(self, dst="", dev=None, verbose=conf.verb): if verbose: warning("No route found for IPv6 destination %s " "(no default route?)", dst) - return (conf.loopback_name, "::", "::") + return (dev or conf.loopback_name, "::", "::") # Sort with longest prefix first then use metrics as a tie-breaker paths.sort(key=lambda x: (-x[0], x[1])) diff --git a/scapy/sendrecv.py b/scapy/sendrecv.py index 4e020d70dba..18775d50291 100644 --- a/scapy/sendrecv.py +++ b/scapy/sendrecv.py @@ -14,6 +14,7 @@ import socket import subprocess import time +import warnings from scapy.compat import plain_str from scapy.data import ETH_P_ALL @@ -452,13 +453,15 @@ def _send(x, # type: _PacketIterable @conf.commands.register def send(x, # type: _PacketIterable - iface=None, # type: Optional[_GlobInterfaceType] **kargs # type: Any ): # type: (...) -> Optional[PacketList] """ Send packets at layer 3 + This determines the interface (or L2 source to use) based on the routing + table: conf.route / conf.route6 + :param x: the packets :param inter: time (in s) between two packets (default 0) :param loop: send packet indefinitely (default 0) @@ -467,11 +470,18 @@ def send(x, # type: _PacketIterable :param realtime: check that a packet was sent before sending the next one :param return_packets: return the sent packets :param socket: the socket to use (default is conf.L3socket(kargs)) - :param iface: the interface to send the packets on :param monitor: (not on linux) send in monitor mode :returns: None """ - iface, ipv6 = _interface_selection(iface, x) + if "iface" in kargs: + # Warn that it isn't used. + warnings.warn( + "'iface' has no effect on L3 I/O send(). For multicast/link-local " + "see https://scapy.readthedocs.io/en/latest/usage.html#multicast", + SyntaxWarning, + ) + del kargs["iface"] + iface, ipv6 = _interface_selection(x) return _send( x, lambda iface: iface.l3socket(ipv6), @@ -649,10 +659,7 @@ def _parse_tcpreplay_result(stdout_b, stderr_b, argv): return {} -def _interface_selection(iface, # type: Optional[_GlobInterfaceType] - packet # type: _PacketIterable - ): - # type: (...) -> Tuple[NetworkInterface, bool] +def _interface_selection(packet: _PacketIterable) -> Tuple[NetworkInterface, bool]: """ Select the network interface according to the layer 3 destination """ @@ -664,21 +671,17 @@ def _interface_selection(iface, # type: Optional[_GlobInterfaceType] ipv6 = True except (ValueError, OSError): pass - if iface is None: - try: - iff = resolve_iface(_iff or conf.iface) - except AttributeError: - iff = None - return iff or conf.iface, ipv6 - - return resolve_iface(iface), ipv6 + try: + iff = resolve_iface(_iff or conf.iface) + except AttributeError: + iff = None + return iff or conf.iface, ipv6 @conf.commands.register def sr(x, # type: _PacketIterable promisc=None, # type: Optional[bool] filter=None, # type: Optional[str] - iface=None, # type: Optional[_GlobInterfaceType] nofilter=0, # type: int *args, # type: Any **kargs # type: Any @@ -686,8 +689,19 @@ def sr(x, # type: _PacketIterable # type: (...) -> Tuple[SndRcvList, PacketList] """ Send and receive packets at layer 3 + + This determines the interface (or L2 source to use) based on the routing + table: conf.route / conf.route6 """ - iface, ipv6 = _interface_selection(iface, x) + if "iface" in kargs: + # Warn that it isn't used. + warnings.warn( + "'iface' has no effect on L3 I/O sr(). For multicast/link-local " + "see https://scapy.readthedocs.io/en/latest/usage.html#multicast", + SyntaxWarning, + ) + del kargs["iface"] + iface, ipv6 = _interface_selection(x) s = iface.l3socket(ipv6)( promisc=promisc, filter=filter, iface=iface, nofilter=nofilter, @@ -702,7 +716,18 @@ def sr1(*args, **kargs): # type: (*Any, **Any) -> Optional[Packet] """ Send packets at layer 3 and return only the first answer + + This determines the interface (or L2 source to use) based on the routing + table: conf.route / conf.route6 """ + if "iface" in kargs: + # Warn that it isn't used. + warnings.warn( + "'iface' has no effect on L3 I/O sr1(). For multicast/link-local " + "see https://scapy.readthedocs.io/en/latest/usage.html#multicast", + SyntaxWarning, + ) + del kargs["iface"] ans, _ = sr(*args, **kargs) if ans: return cast(Packet, ans[0][1]) @@ -926,13 +951,23 @@ def srflood(x, # type: _PacketIterable # type: (...) -> Tuple[SndRcvList, PacketList] """Flood and receive packets at layer 3 + This determines the interface (or L2 source to use) based on the routing + table: conf.route / conf.route6 + :param prn: function applied to packets received :param unique: only consider packets whose print :param nofilter: put 1 to avoid use of BPF filters :param filter: provide a BPF filter - :param iface: listen answers only on the given interface """ - iface, ipv6 = _interface_selection(iface, x) + if "iface" in kargs: + # Warn that it isn't used. + warnings.warn( + "'iface' has no effect on L3 I/O srflood(). For multicast/link-local " + "see https://scapy.readthedocs.io/en/latest/usage.html#multicast", + SyntaxWarning, + ) + del kargs["iface"] + iface, ipv6 = _interface_selection(x) s = iface.l3socket(ipv6)( promisc=promisc, filter=filter, iface=iface, nofilter=nofilter, @@ -946,7 +981,6 @@ def srflood(x, # type: _PacketIterable def sr1flood(x, # type: _PacketIterable promisc=None, # type: Optional[bool] filter=None, # type: Optional[str] - iface=None, # type: Optional[_GlobInterfaceType] nofilter=0, # type: int *args, # type: Any **kargs # type: Any @@ -954,13 +988,24 @@ def sr1flood(x, # type: _PacketIterable # type: (...) -> Optional[Packet] """Flood and receive packets at layer 3 and return only the first answer + This determines the interface (or L2 source to use) based on the routing + table: conf.route / conf.route6 + :param prn: function applied to packets received :param verbose: set verbosity level :param nofilter: put 1 to avoid use of BPF filters :param filter: provide a BPF filter :param iface: listen answers only on the given interface """ - iface, ipv6 = _interface_selection(iface, x) + if "iface" in kargs: + # Warn that it isn't used. + warnings.warn( + "'iface' has no effect on L3 I/O sr1flood(). For multicast/link-local " + "see https://scapy.readthedocs.io/en/latest/usage.html#multicast", + SyntaxWarning, + ) + del kargs["iface"] + iface, ipv6 = _interface_selection(x) s = iface.l3socket(ipv6)( promisc=promisc, filter=filter, nofilter=nofilter, iface=iface, diff --git a/test/fields.uts b/test/fields.uts index 81b2566683c..b64300b8279 100644 --- a/test/fields.uts +++ b/test/fields.uts @@ -139,7 +139,7 @@ assert r == b"FOO\x01\x02\x03\x04" = SourceIPField ~ core field defaddr = conf.route.route('0.0.0.0')[1] -class Test(Packet): fields_desc = [SourceIPField("sourceip", None)] +class Test(Packet): fields_desc = [SourceIPField("sourceip")] assert Test().sourceip == defaddr assert Test(raw(Test())).sourceip == defaddr diff --git a/test/linux.uts b/test/linux.uts index 8afe94962b0..76a4e3da832 100644 --- a/test/linux.uts +++ b/test/linux.uts @@ -63,6 +63,53 @@ if exit_status == 0: else: assert True + += Test scoped interface addresses +~ linux needs_root + +import os +exit_status = os.system("ip link add name scapy0 type dummy") +exit_status = os.system("ip link add name scapy1 type dummy") +exit_status |= os.system("ip addr add 192.0.2.1/24 dev scapy0") +exit_status |= os.system("ip addr add 192.0.3.1/24 dev scapy1") +exit_status |= os.system("ip link set scapy0 address 00:01:02:03:04:05 multicast on up") +exit_status |= os.system("ip link set scapy1 address 06:07:08:09:10:11 multicast on up") +assert exit_status == 0 + +conf.ifaces.reload() +conf.route.resync() +conf.route6.resync() + +conf.route6 + +try: + # IPv4 + a = Ether()/IP(dst="224.0.0.1%scapy0") + assert a[Ether].src == "00:01:02:03:04:05" + assert a[IP].src == "192.0.2.1" + b = Ether()/IP(dst="224.0.0.1%scapy1") + assert b[Ether].src == "06:07:08:09:10:11" + assert b[IP].src == "192.0.3.1" + c = Ether()/IP(dst="224.0.0.1/24%scapy1") + assert c[Ether].src == "06:07:08:09:10:11" + assert c[IP].src == "192.0.3.1" + # IPv6 + a = Ether()/IPv6(dst="ff02::fb%scapy0") + assert a[Ether].src == "00:01:02:03:04:05" + assert a[IPv6].src == "fe80::201:2ff:fe03:405" + b = Ether()/IPv6(dst="ff02::fb%scapy1") + assert b[Ether].src == "06:07:08:09:10:11" + assert b[IPv6].src == "fe80::407:8ff:fe09:1011" + c = Ether()/IPv6(dst="ff02::fb/30%scapy1") + assert c[Ether].src == "06:07:08:09:10:11" + assert c[IPv6].src == "fe80::407:8ff:fe09:1011" +finally: + exit_status = os.system("ip link del scapy0") + exit_status = os.system("ip link del scapy1") + conf.ifaces.reload() + conf.route.resync() + conf.route6.resync() + = catch loopback device missing ~ linux needs_root @@ -310,7 +357,7 @@ assert test_L3PacketSocket_sendto_python3() import os from scapy.sendrecv import _interface_selection -assert _interface_selection(None, IP(dst="8.8.8.8")/UDP()) == (conf.iface, False) +assert _interface_selection(IP(dst="8.8.8.8")/UDP()) == (conf.iface, False) exit_status = os.system("ip link add name scapy0 type dummy") exit_status = os.system("ip addr add 192.0.2.1/24 dev scapy0") exit_status = os.system("ip addr add fc00::/24 dev scapy0") @@ -318,8 +365,8 @@ exit_status = os.system("ip link set scapy0 up") conf.ifaces.reload() conf.route.resync() conf.route6.resync() -assert _interface_selection(None, IP(dst="192.0.2.42")/UDP()) == ("scapy0", False) -assert _interface_selection(None, IPv6(dst="fc00::ae0d")/UDP()) == ("scapy0", True) +assert _interface_selection(IP(dst="192.0.2.42")/UDP()) == ("scapy0", False) +assert _interface_selection(IPv6(dst="fc00::ae0d")/UDP()) == ("scapy0", True) exit_status = os.system("ip link del name dev scapy0") conf.ifaces.reload() conf.route.resync()