Skip to content

Commit 01b7c57

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

File tree

12 files changed

+253
-101
lines changed

12 files changed

+253
-101
lines changed

doc/scapy/usage.rst

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1488,9 +1488,24 @@ NBNS Query Request (find by NetbiosName)
14881488

14891489
.. code::
14901490
1491-
>>> conf.checkIPaddr = False # Mandatory because we are using a broadcast destination
1491+
>>> conf.checkIPaddr = False # Mandatory because we are using a broadcast destination and receiving unicast
14921492
>>> sr1(IP(dst="192.168.0.255")/UDP()/NBNSHeader()/NBNSQueryRequest(QUESTION_NAME="DC1"))
14931493
1494+
mDNS Query Request
1495+
------------------
1496+
1497+
For instance, find all spotify connect devices.
1498+
1499+
.. code::
1500+
1501+
>>> # For interface 'eth0'
1502+
>>> 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)
1503+
>>> ans.show()
1504+
1505+
.. note::
1506+
1507+
As you can see, we used a scope identifier (``%eth0``) to specify on which interface we want to use the above multicast IP.
1508+
14941509
Advanced traceroute
14951510
-------------------
14961511

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: 90 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 _NetIP(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(_NetIP, self).__repr__()
123+
if self.scope is not None:
124+
return "NetIP(%s, scope=%s)" % (val, repr(self.scope))
125+
return val
126+
127+
128+
def NetIP(net: str, scope: Optional[Any] = None) -> _NetIP:
129+
"""
130+
An str that also holds extra attributes.
131+
132+
Examples::
133+
134+
>>> NetIP("224.0.0.1%eth0") # interface 'eth0'
135+
>>> NetIP("224.0.0.1%1") # interface index 1
136+
>>> NetIP("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 = _NetIP(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,15 @@ 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 = NetIP(net)
221+
self.scope = net.scope
151222
if stop is None:
152223
try:
153224
net, mask = net.split("/", 1)
@@ -174,7 +245,10 @@ def __iter__(self):
174245
# type: () -> Iterator[str]
175246
# Python 2 won't handle huge (> sys.maxint) values in range()
176247
for i in range(self.count):
177-
yield self.int2ip(self.start + i)
248+
yield NetIP(
249+
self.int2ip(self.start + i),
250+
scope=self.scope,
251+
)
178252

179253
def __len__(self):
180254
# type: () -> int
@@ -187,20 +261,28 @@ def __iterlen__(self):
187261

188262
def choice(self):
189263
# type: () -> str
190-
return self.int2ip(random.randint(self.start, self.stop))
264+
return NetIP(
265+
self.int2ip(random.randint(self.start, self.stop)),
266+
scope=self.scope,
267+
)
191268

192269
def __repr__(self):
193270
# type: () -> str
271+
scope_id_repr = ""
272+
if self.scope:
273+
scope_id_repr = ", scope=%s" % repr(self.scope)
194274
if self.mask is not None:
195-
return '%s("%s/%d")' % (
275+
return '%s("%s/%d"%s)' % (
196276
self.__class__.__name__,
197277
self.net,
198278
self.mask,
279+
scope_id_repr,
199280
)
200-
return '%s("%s", "%s")' % (
281+
return '%s("%s", "%s"%s)' % (
201282
self.__class__.__name__,
202283
self.int2ip(self.start),
203284
self.int2ip(self.stop),
285+
scope_id_repr,
204286
)
205287

206288
def __eq__(self, other):
@@ -220,7 +302,7 @@ def __ne__(self, other):
220302

221303
def __hash__(self):
222304
# type: () -> int
223-
return hash(("scapy.Net", self.family, self.start, self.stop))
305+
return hash(("scapy.Net", self.family, self.start, self.stop, self.scope))
224306

225307
def __contains__(self, other):
226308
# type: (Any) -> bool

scapy/fields.py

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,13 @@
3737
from scapy.utils import inet_aton, inet_ntoa, lhex, mac2str, str2mac, EDecimal
3838
from scapy.utils6 import in6_6to4ExtractAddr, in6_isaddr6to4, \
3939
in6_isaddrTeredo, in6_ptop, Net6, teredoAddrExtractInfo
40-
from scapy.base_classes import Gen, Net, BasePacket, Field_metaclass
41-
from scapy.error import warning
40+
from scapy.base_classes import (
41+
_NetIP,
42+
BasePacket,
43+
Field_metaclass,
44+
Net,
45+
NetIP,
46+
)
4247

4348
# Typing imports
4449
from typing import (
@@ -849,6 +854,7 @@ def h2i(self, pkt, x):
849854
if isinstance(x, bytes):
850855
x = plain_str(x) # type: ignore
851856
if isinstance(x, str):
857+
x = NetIP(x)
852858
try:
853859
inet_aton(x)
854860
except socket.error:
@@ -893,6 +899,8 @@ def any2i(self, pkt, x):
893899

894900
def i2repr(self, pkt, x):
895901
# type: (Optional[Packet], Union[str, Net]) -> str
902+
if isinstance(x, _NetIP):
903+
return repr(x)
896904
r = self.resolve(self.i2h(pkt, x))
897905
return r if isinstance(r, str) else repr(r)
898906

@@ -902,29 +910,16 @@ def randval(self):
902910

903911

904912
class SourceIPField(IPField):
905-
__slots__ = ["dstname"]
906-
907-
def __init__(self, name, dstname):
908-
# type: (str, Optional[str]) -> None
913+
def __init__(self, name):
914+
# type: (str) -> None
909915
IPField.__init__(self, name, None)
910-
self.dstname = dstname
911916

912917
def __findaddr(self, pkt):
913-
# type: (Packet) -> str
918+
# type: (Packet) -> Optional[str]
914919
if conf.route is None:
915920
# unused import, only to initialize conf.route
916921
import scapy.route # noqa: F401
917-
dst = ("0.0.0.0" if self.dstname is None
918-
else getattr(pkt, self.dstname) or "0.0.0.0")
919-
if isinstance(dst, (Gen, list)):
920-
r = {
921-
conf.route.route(str(daddr))
922-
for daddr in dst
923-
} # type: Set[Tuple[str, str, str]]
924-
if len(r) > 1:
925-
warning("More than one possible route for %r" % (dst,))
926-
return min(r)[1]
927-
return conf.route.route(dst)[1]
922+
return pkt.route()[1]
928923

929924
def i2m(self, pkt, x):
930925
# type: (Optional[Packet], Optional[Union[str, Net]]) -> bytes
@@ -945,18 +940,19 @@ def __init__(self, name, default):
945940
Field.__init__(self, name, default, "16s")
946941

947942
def h2i(self, pkt, x):
948-
# type: (Optional[Packet], Optional[str]) -> str
943+
# type: (Optional[Packet], Any) -> str
949944
if isinstance(x, bytes):
950945
x = plain_str(x)
951946
if isinstance(x, str):
947+
x = NetIP(x)
952948
try:
953-
x = in6_ptop(x)
949+
in6_ptop(x)
954950
except socket.error:
955951
return Net6(x) # type: ignore
956952
elif isinstance(x, tuple):
957953
if len(x) != 2:
958954
raise ValueError("Invalid IPv6 format")
959-
return Net6(*x)
955+
return Net6(*x) # type: ignore
960956
elif isinstance(x, list):
961957
x = [self.h2i(pkt, n) for n in x]
962958
return x # type: ignore
@@ -990,6 +986,8 @@ def i2repr(self, pkt, x):
990986
elif in6_isaddr6to4(x): # print encapsulated address
991987
vaddr = in6_6to4ExtractAddr(x)
992988
return "%s [6to4 GW: %s]" % (self.i2h(pkt, x), vaddr)
989+
elif isinstance(x, _NetIP):
990+
return repr(x)
993991
r = self.i2h(pkt, x) # No specific information to return
994992
return r if isinstance(r, str) else repr(r)
995993

@@ -999,36 +997,27 @@ def randval(self):
999997

1000998

1001999
class SourceIP6Field(IP6Field):
1002-
__slots__ = ["dstname"]
1003-
1004-
def __init__(self, name, dstname):
1005-
# type: (str, str) -> None
1000+
def __init__(self, name):
1001+
# type: (str) -> None
10061002
IP6Field.__init__(self, name, None)
1007-
self.dstname = dstname
1003+
1004+
def __findaddr(self, pkt):
1005+
# type: (Packet) -> Optional[str]
1006+
if conf.route6 is None:
1007+
# unused import, only to initialize conf.route
1008+
import scapy.route6 # noqa: F401
1009+
return pkt.route()[1]
10081010

10091011
def i2m(self, pkt, x):
10101012
# type: (Optional[Packet], Optional[Union[str, Net6]]) -> bytes
1011-
if x is None:
1012-
dst = ("::" if self.dstname is None else
1013-
getattr(pkt, self.dstname) or "::")
1014-
iff, x, nh = conf.route6.route(dst)
1013+
if x is None and pkt is not None:
1014+
x = self.__findaddr(pkt)
10151015
return super(SourceIP6Field, self).i2m(pkt, x)
10161016

10171017
def i2h(self, pkt, x):
10181018
# type: (Optional[Packet], Optional[Union[str, Net6]]) -> str
1019-
if x is None:
1020-
if conf.route6 is None:
1021-
# unused import, only to initialize conf.route6
1022-
import scapy.route6 # noqa: F401
1023-
dst = ("::" if self.dstname is None else getattr(pkt, self.dstname)) # noqa: E501
1024-
if isinstance(dst, (Gen, list)):
1025-
r = {conf.route6.route(str(daddr))
1026-
for daddr in dst}
1027-
if len(r) > 1:
1028-
warning("More than one possible route for %r" % (dst,))
1029-
x = min(r)[1]
1030-
else:
1031-
x = conf.route6.route(dst)[1]
1019+
if x is None and pkt is not None:
1020+
x = self.__findaddr(pkt)
10321021
return super(SourceIP6Field, self).i2h(pkt, x)
10331022

10341023

scapy/layers/hsrp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class HSRPmd5(Packet):
4848
ByteEnumField("algo", 0, {1: "MD5"}),
4949
ByteField("padding", 0x00),
5050
XShortField("flags", 0x00),
51-
SourceIPField("sourceip", None),
51+
SourceIPField("sourceip"),
5252
XIntField("keyid", 0x00),
5353
StrFixedLenField("authdigest", b"\00" * 16, 16)]
5454

0 commit comments

Comments
 (0)