Skip to content

Commit 7aeeaf8

Browse files
authored
fix(toolkit): scapy reads nothing in a zero-length frame, and a hand-built packet off its octets (#1676) (#1683)
1 parent cd90688 commit 7aeeaf8

2 files changed

Lines changed: 376 additions & 17 deletions

File tree

‎pcapkit/toolkit/scapy.py‎

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ def packet2dict(packet: 'Packet') -> 'dict[str, Any]':
150150
packet: Scapy packet.
151151
152152
Returns:
153-
A :obj:`dict` mapping of packet data. Each layer carries only the fields
153+
A :obj:`dict` mapping of packet data. Its ``packet`` is the octets of
154+
the frame (see :func:`_frame`), none for a zero-length one. Each layer
155+
carries only the fields
154156
set on it (for a dissected packet, those read from the wire, with no
155157
defaults filled in). Field values that are themselves
156158
Scapy packets (e.g. IP options, DNS records), including those inside
@@ -182,7 +184,7 @@ def wrapper(packet: 'Packet') -> 'dict[str, Any]':
182184
return dict_
183185

184186
return {
185-
'packet': bytes(packet),
187+
'packet': _frame(packet),
186188
packet.name: wrapper(packet),
187189
}
188190

@@ -204,8 +206,10 @@ def packet2frame(packet: 'Packet') -> 'FrameRecord':
204206
205207
Note:
206208
The octets are ``packet.original``, which `Scapy`_'s readers keep as read,
207-
rather than ``bytes(packet)``, which rebuilds them. ``orig_len`` is
208-
``packet.wirelen``, the record's original length.
209+
rather than ``bytes(packet)``, which rebuilds them; for a packet not read
210+
from a capture, such as one built by hand, they are the octets it builds
211+
into (see :func:`_frame`). ``orig_len`` is ``packet.wirelen``, the
212+
record's original length.
209213
210214
The fraction in ``frame_info.ts_usec`` is in the capture's own resolution,
211215
as the default engine's is. That resolution is the one
@@ -220,8 +224,7 @@ def packet2frame(packet: 'Packet') -> 'FrameRecord':
220224
raise ModuleNotFound("No module named 'scapy'", name='scapy')
221225
mapping = packet2dict(packet)
222226

223-
original = getattr(packet, 'original', None)
224-
octets = bytes(original) if isinstance(original, (bytes, bytearray)) else bytes(packet)
227+
octets = _frame(packet)
225228
wirelen = getattr(packet, 'wirelen', None)
226229
orig_len = len(octets) if wirelen is None else int(wirelen)
227230

@@ -240,6 +243,34 @@ def packet2frame(packet: 'Packet') -> 'FrameRecord':
240243
), time_epoch=decimal.Decimal(epoch))
241244

242245

246+
def _frame(packet: 'Packet') -> 'bytes':
247+
"""Return the octets of the frame a Scapy packet stands for.
248+
249+
Args:
250+
packet: Scapy packet.
251+
252+
Returns:
253+
For a packet read from a capture, which every `Scapy`_ reader gives a
254+
``wirelen``, the record's original length, ``packet.original``: the
255+
octets of the record, which the readers keep as read. For any other,
256+
such as one built by hand, ``bytes(packet)``: the octets it builds into.
257+
258+
Note:
259+
`Scapy`_ dissects a packet only from a non-empty string, so a
260+
zero-length frame comes back as the first layer its link type names,
261+
every field left to its default, as one built by hand with no fields
262+
would be. ``bytes()`` of it builds a header out of nothing -- 20 octets
263+
at ``RAW`` and ``IPV4``, of an IPv4 layer whose ``ihl``, ``len`` and
264+
``chksum`` are :data:`None` until then -- where the record has none
265+
(:issue:`1676`).
266+
267+
"""
268+
original = getattr(packet, 'original', None)
269+
if getattr(packet, 'wirelen', None) is not None and isinstance(original, (bytes, bytearray)):
270+
return bytes(original)
271+
return bytes(packet)
272+
273+
243274
def _within_datagrams(layer: 'Packet') -> 'bytes':
244275
"""Return the octets from ``layer`` on that lie inside every IP datagram carrying it.
245276
@@ -282,7 +313,10 @@ def _within_datagrams(layer: 'Packet') -> 'bytes':
282313
# a zero is a jumbogram's, or else it declares nothing
283314
length = carrier.plen or jumbo_payload_length(carrier.nh, bytes(carrier.payload)) or 0
284315
elif isinstance(carrier, IP) and carrier.len: # a zero is TSO's
285-
length = max(carrier.len - carrier.ihl * 4, 0)
316+
ihl = carrier.ihl
317+
if ihl is None: # on a layer built by hand, given a Total Length but no IHL
318+
ihl = bytes(carrier)[0] & 0x0F
319+
length = max(carrier.len - ihl * 4, 0)
286320
if length is not None:
287321
# ``bytes()`` of a layer spans it and every layer after it, padding
288322
# included, so the difference is the octets between the end of the
@@ -306,10 +340,16 @@ def _tcp_segment(tcp: 'TCP') -> 'tuple[bytes, bytes] | None':
306340
(:rfc:`9293#section-3.1`), since the default engine then reads no
307341
segment at all.
308342
343+
Note:
344+
The Data Offset is read off the octets, which for a dissected layer
345+
carry the ``dataofs`` `Scapy`_ read, and for a layer built by hand the
346+
one it builds, as it leaves ``dataofs`` :data:`None` until then
347+
(:issue:`1676`).
348+
309349
"""
310350
segment = _within_datagrams(tcp)
311-
hdr_len = tcp.dataofs * 4
312-
if len(segment) < 20 or hdr_len < 20:
351+
hdr_len = (segment[12] >> 4) * 4 if len(segment) >= TCP_MIN_HEADER_LEN else 0
352+
if hdr_len < TCP_MIN_HEADER_LEN:
313353
return None
314354
return segment[:hdr_len], segment[hdr_len:]
315355

@@ -423,8 +463,9 @@ def _network(packet: 'Packet') -> 'Optional[tuple[Packet, int]]':
423463
for a raw IP link type; and for a BSD loopback header the IPv4 or IPv6
424464
packet its address family names, as the default engine reads it (see
425465
:data:`LOOPBACK_FAMILIES`). :data:`None` for any other link type, whose
426-
frames the default engine leaves raw (:issue:`1650`), or a loopback
427-
family it dissects nothing behind.
466+
frames the default engine leaves raw (:issue:`1650`), a loopback
467+
family it dissects nothing behind, or a zero-length frame, of which it
468+
dissects nothing at all (:issue:`1676`).
428469
429470
Note:
430471
The default engine dissects the first layer of the ``NULL``,
@@ -434,6 +475,10 @@ def _network(packet: 'Packet') -> 'Optional[tuple[Packet, int]]':
434475
`Scapy`_ dissects IP straight behind the headers of many others, such as
435476
``LINUX_SLL``, ``LINUX_SLL2``, ``PPP`` and the ``DLT_RAW`` of 12.
436477
478+
`Scapy`_ hands a zero-length frame back as a layer of defaults, which
479+
at ``RAW`` and ``IPV4`` is an IPv4 header with no IHL (see
480+
:func:`_frame`), so it is told by the frame's octets, not its layers.
481+
437482
A loopback header's family is read as
438483
:func:`~pcapkit.protocols.link.loopback.family_byteorder` reads it --
439484
``LOOP``'s in network byte order, ``NULL``'s in the byte order its
@@ -447,16 +492,19 @@ def _network(packet: 'Packet') -> 'Optional[tuple[Packet, int]]':
447492
from scapy.layers.inet6 import IPv6
448493

449494
linktype = _linktype(packet)
495+
if linktype not in (Enum_LinkType.NULL, Enum_LinkType.ETHERNET, Enum_LinkType.LOOP,
496+
Enum_LinkType.RAW, Enum_LinkType.IPV4, Enum_LinkType.IPV6):
497+
return None
498+
octets = _frame(packet)
499+
if not octets:
500+
return None
450501
if linktype == Enum_LinkType.ETHERNET:
451502
return packet.payload, 2
452503
if linktype in (Enum_LinkType.RAW, Enum_LinkType.IPV4, Enum_LinkType.IPV6):
453504
return packet, 1
454-
if linktype not in (Enum_LinkType.NULL, Enum_LinkType.LOOP):
455-
return None
456505

457506
from pcapkit.protocols.link.loopback import family_byteorder # isort:skip
458507

459-
octets = bytes(packet)
460508
if len(octets) < 4:
461509
return None
462510
version = LOOPBACK_FAMILIES.get(int.from_bytes(octets[:4], family_byteorder(octets[:4], linktype)))
@@ -853,9 +901,12 @@ def ipv4_reassembly(packet: 'Packet', *, count: 'int' = -1) -> 'IP_Packet[IPv4Ad
853901
# the default engine hands it only the octets inside the Payload Length
854902
# of each IPv6 packet above it, so those are all it keeps (:issue:`1623`)
855903
octets = _within_datagrams(ipv4)
856-
ihl = ipv4.ihl * 4
904+
# NOTE: the IHL and Total Length are read off the octets, as Scapy
905+
# leaves ``ihl`` and ``len`` None on a layer built by hand until it is
906+
# built (:issue:`1676`)
907+
ihl = (octets[0] & 0x0F) * 4 if octets else IPV4_HEADER_LEN
857908
# the default engine dissects no IPv4 header those octets cut short
858-
if len(octets) < ihl or not _ipv4_accepted(ipv4):
909+
if len(octets) < max(ihl, IPV4_HEADER_LEN) or not _ipv4_accepted(ipv4):
859910
return None
860911

861912
data = IP_Packet(
@@ -876,7 +927,7 @@ def ipv4_reassembly(packet: 'Packet', *, count: 'int' = -1) -> 'IP_Packet[IPv4Ad
876927
fo=ipv4.frag * 8, # fragment offset
877928
ihl=ihl, # internet header length
878929
mf=bool(ipv4.flags.MF), # more fragment flag
879-
tl=ipv4.len, # total length, header includes
930+
tl=int.from_bytes(octets[2:4], 'big'), # total length, header includes
880931
header=octets[:ihl], # raw bytes type header
881932
# NOTE: this keeps the octets past the Total Length, as the default
882933
# engine's adapter does, since the reassembly machinery cuts the

0 commit comments

Comments
 (0)