Skip to content

Commit d97baad

Browse files
committed
Support for Scope Identifiers in IP addresses
1 parent a1afb9a commit d97baad

File tree

14 files changed

+352
-107
lines changed

14 files changed

+352
-107
lines changed

doc/scapy/usage.rst

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,31 @@ Now that we know how to manipulate packets. Let's see how to send them. The send
252252
Sent 1 packets.
253253
<PacketList: TCP:0 UDP:0 ICMP:0 Other:1>
254254

255+
.. _multicast:
256+
257+
Multicast on layer 3: Scope Identifiers
258+
---------------------------------------
259+
260+
.. index::
261+
single: Multicast
262+
263+
.. note:: This feature is only available since Scapy 2.6.0.
264+
265+
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).
266+
267+
.. code:: python
268+
269+
>>> conf.checkIPaddr = False # answer IP will be != from the one we requested
270+
# send on interface 'eth0'
271+
>>> sr(IP(dst="224.0.0.1%eth0")/ICMP(), multi=True)
272+
>>> sr(IPv6(dst="ff02::2%eth0")/ICMPv6EchoRequest(), multi=True)
273+
274+
You can use both ``%eth0`` format or ``%15`` (the interface id) format. You can query those using ``conf.ifaces``.
275+
276+
.. note::
277+
278+
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::
279+
>>> pkt = IP(dst=ScopedIP("224.0.0.1", scope=conf.iface))/ICMP()
255280

256281
Fuzzing
257282
-------
@@ -1488,9 +1513,24 @@ NBNS Query Request (find by NetbiosName)
14881513

14891514
.. code::
14901515
1491-
>>> conf.checkIPaddr = False # Mandatory because we are using a broadcast destination
1516+
>>> conf.checkIPaddr = False # Mandatory because we are using a broadcast destination and receiving unicast
14921517
>>> sr1(IP(dst="192.168.0.255")/UDP()/NBNSHeader()/NBNSQueryRequest(QUESTION_NAME="DC1"))
14931518
1519+
mDNS Query Request
1520+
------------------
1521+
1522+
For instance, find all spotify connect devices.
1523+
1524+
.. code::
1525+
1526+
>>> # For interface 'eth0'
1527+
>>> 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)
1528+
>>> ans.show()
1529+
1530+
.. note::
1531+
1532+
As you can see, we used a scope identifier (``%eth0``) to specify on which interface we want to use the above multicast IP.
1533+
14941534
Advanced traceroute
14951535
-------------------
14961536

scapy/arch/linux/rtnetlink.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,7 @@ def _sr1_rtrequest(pkt: Packet) -> List[Packet]:
733733
if msg.nlmsg_type == 3 and nlmsgerr in msg and msg.status != 0:
734734
# NLMSG_DONE with errors
735735
if msg.data and msg.data[0].rta_type == 1:
736-
log_loading.warning(
736+
log_loading.debug(
737737
"Scapy RTNETLINK error on %s: '%s'. Please report !",
738738
pkt.sprintf("%nlmsg_type%"),
739739
msg.data[0].rta_data.decode(),
@@ -900,6 +900,20 @@ def read_routes():
900900
elif attr.rta_type == 0x07: # RTA_PREFSRC
901901
addr = attr.rta_data
902902
routes.append((net, mask, gw, iface, addr, metric))
903+
# Add multicast routes, as those are missing by default
904+
for _iface in ifaces.values():
905+
if _iface['flags'].MULTICAST:
906+
try:
907+
addr = next(
908+
x["address"]
909+
for x in _iface["ips"]
910+
if x["af_family"] == socket.AF_INET
911+
)
912+
except StopIteration:
913+
continue
914+
routes.append((
915+
0xe0000000, 0xf0000000, "0.0.0.0", _iface["name"], addr, 250
916+
))
903917
return routes
904918

905919

@@ -937,4 +951,17 @@ def read_routes6():
937951
cset = scapy.utils6.construct_source_candidate_set(prefix, plen, devaddrs)
938952
if cset:
939953
routes.append((prefix, plen, nh, iface, cset, metric))
954+
# Add multicast routes, as those are missing by default
955+
for _iface in ifaces.values():
956+
if _iface['flags'].MULTICAST:
957+
addrs = [
958+
x["address"]
959+
for x in _iface["ips"]
960+
if x["af_family"] == socket.AF_INET6
961+
]
962+
if not addrs:
963+
continue
964+
routes.append((
965+
"ff00::", 8, "::", _iface["name"], addrs, 250
966+
))
940967
return routes

scapy/base_classes.py

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,75 @@ def __repr__(self):
109109
return "<SetGen %r>" % self.values
110110

111111

112+
class _ScopedIP(str):
113+
"""
114+
An str that also holds extra attributes.
115+
"""
116+
__slots__ = ["scope"]
117+
118+
def __init__(self, _: str) -> None:
119+
self.scope = None
120+
121+
def __repr__(self) -> str:
122+
val = super(_ScopedIP, self).__repr__()
123+
if self.scope is not None:
124+
return "ScopedIP(%s, scope=%s)" % (val, repr(self.scope))
125+
return val
126+
127+
128+
def ScopedIP(net: str, scope: Optional[Any] = None) -> _ScopedIP:
129+
"""
130+
An str that also holds extra attributes.
131+
132+
Examples::
133+
134+
>>> ScopedIP("224.0.0.1%eth0") # interface 'eth0'
135+
>>> ScopedIP("224.0.0.1%1") # interface index 1
136+
>>> ScopedIP("224.0.0.1", scope=conf.iface)
137+
"""
138+
if "%" in net:
139+
try:
140+
net, scope = net.split("%", 1)
141+
except ValueError:
142+
raise Scapy_Exception("Scope identifier can only be present once !")
143+
if scope is not None:
144+
from scapy.interfaces import resolve_iface, network_name, dev_from_index
145+
try:
146+
iface = dev_from_index(int(scope))
147+
except (ValueError, TypeError):
148+
iface = resolve_iface(scope)
149+
if not iface.is_valid():
150+
raise Scapy_Exception(
151+
"RFC6874 scope identifier '%s' could not be resolved to a "
152+
"valid interface !" % scope
153+
)
154+
scope = network_name(iface)
155+
x = _ScopedIP(net)
156+
x.scope = scope
157+
return x
158+
159+
112160
class Net(Gen[str]):
113-
"""Network object from an IP address or hostname and mask"""
161+
"""
162+
Network object from an IP address or hostname and mask
163+
164+
Examples:
165+
166+
- With mask::
167+
168+
>>> list(Net("192.168.0.1/24"))
169+
['192.168.0.0', '192.168.0.1', ..., '192.168.0.255']
170+
171+
- With 'end'::
172+
173+
>>> list(Net("192.168.0.100", "192.168.0.200"))
174+
['192.168.0.100', '192.168.0.101', ..., '192.168.0.200']
175+
176+
- With 'scope' (for multicast)::
177+
178+
>>> Net("224.0.0.1%lo")
179+
>>> Net("224.0.0.1", scope=conf.iface)
180+
"""
114181
name = "Net" # type: str
115182
family = socket.AF_INET # type: int
116183
max_mask = 32 # type: int
@@ -143,11 +210,16 @@ def int2ip(val):
143210
# type: (int) -> str
144211
return socket.inet_ntoa(struct.pack('!I', val))
145212

146-
def __init__(self, net, stop=None):
147-
# type: (str, Union[None, str]) -> None
213+
def __init__(self, net, stop=None, scope=None):
214+
# type: (str, Optional[str], Optional[str]) -> None
148215
if "*" in net:
149216
raise Scapy_Exception("Wildcards are no longer accepted in %s()" %
150217
self.__class__.__name__)
218+
self.scope = None
219+
if "%" in net:
220+
net = ScopedIP(net)
221+
if isinstance(net, _ScopedIP):
222+
self.scope = net.scope
151223
if stop is None:
152224
try:
153225
net, mask = net.split("/", 1)
@@ -174,7 +246,10 @@ def __iter__(self):
174246
# type: () -> Iterator[str]
175247
# Python 2 won't handle huge (> sys.maxint) values in range()
176248
for i in range(self.count):
177-
yield self.int2ip(self.start + i)
249+
yield ScopedIP(
250+
self.int2ip(self.start + i),
251+
scope=self.scope,
252+
)
178253

179254
def __len__(self):
180255
# type: () -> int
@@ -187,20 +262,28 @@ def __iterlen__(self):
187262

188263
def choice(self):
189264
# type: () -> str
190-
return self.int2ip(random.randint(self.start, self.stop))
265+
return ScopedIP(
266+
self.int2ip(random.randint(self.start, self.stop)),
267+
scope=self.scope,
268+
)
191269

192270
def __repr__(self):
193271
# type: () -> str
272+
scope_id_repr = ""
273+
if self.scope:
274+
scope_id_repr = ", scope=%s" % repr(self.scope)
194275
if self.mask is not None:
195-
return '%s("%s/%d")' % (
276+
return '%s("%s/%d"%s)' % (
196277
self.__class__.__name__,
197278
self.net,
198279
self.mask,
280+
scope_id_repr,
199281
)
200-
return '%s("%s", "%s")' % (
282+
return '%s("%s", "%s"%s)' % (
201283
self.__class__.__name__,
202284
self.int2ip(self.start),
203285
self.int2ip(self.stop),
286+
scope_id_repr,
204287
)
205288

206289
def __eq__(self, other):
@@ -220,7 +303,7 @@ def __ne__(self, other):
220303

221304
def __hash__(self):
222305
# type: () -> int
223-
return hash(("scapy.Net", self.family, self.start, self.stop))
306+
return hash(("scapy.Net", self.family, self.start, self.stop, self.scope))
224307

225308
def __contains__(self, other):
226309
# type: (Any) -> bool

0 commit comments

Comments
 (0)