@@ -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+
243274def _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