Skip to content

Commit

Permalink
Support for Scope Identifiers in IP addresses
Browse files Browse the repository at this point in the history
  • Loading branch information
gpotter2 committed Jul 15, 2024
1 parent 4a852fe commit ec89f7b
Show file tree
Hide file tree
Showing 14 changed files with 352 additions and 107 deletions.
42 changes: 41 additions & 1 deletion doc/scapy/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,31 @@ Now that we know how to manipulate packets. Let's see how to send them. The send
Sent 1 packets.
<PacketList: TCP:0 UDP:0 ICMP:0 Other:1>

.. _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
-------
Expand Down Expand Up @@ -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
-------------------

Expand Down
29 changes: 28 additions & 1 deletion scapy/arch/linux/rtnetlink.py
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,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(),
Expand Down Expand Up @@ -900,6 +900,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


Expand Down Expand Up @@ -937,4 +951,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
99 changes: 91 additions & 8 deletions scapy/base_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,75 @@ def __repr__(self):
return "<SetGen %r>" % 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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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):
Expand All @@ -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
Expand Down
Loading

0 comments on commit ec89f7b

Please sign in to comment.