Skip to content

Commit d284138

Browse files
committed
fix(toolkit): reassemble the first fragmented IP layer, so tunnelled fragments reassemble (#2307)
Problem: the IPv4 and IPv6 reassembly adapters read the frame's outermost IPv4 or IPv6 layer, so in an IP-in-IP tunnel a fragmented inner datagram was reassembled whole under the tunnel's header, or dropped (tunnel DF set, or no Fragment header in the tunnel's IPv6), and never reassembled itself. Fix: take the first layer of that IP version, from the outside in, that is a fragment (IPv4 MF or a non-zero offset; an IPv6 Fragment header), else the outermost as before. Applied to the default engine's PCAP and PCAP-NG adapters, and to scapy and dpkt, which picked the outermost too. Test: tests/toolkit/test_tunnelled_ip_reassembly_2307_unit.py reassembles a fragmented inner datagram (4in4 with DF clear and set, 6in6) and a fragmented tunnel carrying a whole one, per engine; the inner cases fail on main. The scapy 6in6 pin now expects the inner packet reassembled on its own header.
1 parent 4615c31 commit d284138

9 files changed

Lines changed: 401 additions & 48 deletions

File tree

‎docs/source/pcapkit/toolkit/3rdparty.rst‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ Auxiliary Functions
4646

4747
.. autofunction:: pcapkit.toolkit.scapy._within_datagrams
4848

49+
.. autofunction:: pcapkit.toolkit.scapy._fragment
50+
51+
.. autofunction:: pcapkit.toolkit.scapy._fragment_header
52+
4953
.. autofunction:: pcapkit.toolkit.scapy._tcp_carrier
5054

5155
.. autofunction:: pcapkit.toolkit.scapy._tcp_segment
@@ -91,6 +95,8 @@ Auxiliary Functions
9195

9296
.. autofunction:: pcapkit.toolkit.dpkt._capture_time
9397

98+
.. autofunction:: pcapkit.toolkit.dpkt._fragment
99+
94100
Auxiliary Data
95101
--------------
96102

‎docs/source/pcapkit/toolkit/builtin.rst‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Auxiliary Functions
2727

2828
.. autofunction:: pcapkit.toolkit.pcap.tcp_segment
2929

30+
.. autofunction:: pcapkit.toolkit.pcap._fragment
31+
3032
.. autoclass:: pcapkit.toolkit.pcap.TCPSegment
3133
:members:
3234
:show-inheritance:

‎pcapkit/toolkit/dpkt.py‎

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -529,13 +529,56 @@ def wrapper(packet: 'Packet') -> 'dict[str, Any]':
529529
}
530530

531531

532+
def _fragment(packet: 'Packet', version: 'int') -> 'Optional[IP | IP6]':
533+
"""Fetch the IPv4 or IPv6 layer of a DPKT packet that IP reassembly reads.
534+
535+
Args:
536+
packet: DPKT packet, i.e. the outermost layer.
537+
version: ``4`` or ``6``, the IP version of the layer to fetch.
538+
539+
Returns:
540+
The first layer of that version, from the network layer in, that is a
541+
fragment -- for IPv4, one with **MF** set or a non-zero fragment
542+
offset, for IPv6, one with an IPv6 Fragment header; with none, the
543+
network layer if it is of that version; otherwise :data:`None`.
544+
545+
Note:
546+
As with :func:`pcapkit.toolkit.pcap._fragment`, either datagram of an
547+
IP-in-IP tunnel may be the fragmented one, and each is reassembled on
548+
its own header (:issue:`2307`). An IPv6 layer is the one with extension
549+
headers. The walk goes on past an IP layer only where its payload
550+
protocol is IPv4 or IPv6, i.e. a tunnel, so a datagram an ICMP error
551+
quotes, which `DPKT`_ decodes as an IP layer below the ICMP one, is not
552+
walked to.
553+
554+
"""
555+
import dpkt # isort:skip
556+
557+
network = _network(packet)
558+
layer = network # type: Any
559+
while isinstance(layer, (dpkt.ip.IP, dpkt.ip6.IP6)):
560+
if isinstance(layer, dpkt.ip6.IP6):
561+
if version == 6 and layer.extension_hdrs.get(44) is not None:
562+
return layer
563+
elif version == 4 and (layer.mf or layer.offset):
564+
return layer
565+
# ``IP6.p`` is the protocol after the extension headers
566+
if getattr(layer, 'p', None) not in (Enum_TransType.IPv4, Enum_TransType.IPv6):
567+
break
568+
layer = layer.data
569+
if network is None or hasattr(network, 'extension_hdrs') is not (version == 6):
570+
return None
571+
return network
572+
573+
532574
def ipv4_reassembly(packet: 'Packet', timestamp: 'float', *,
533575
count: 'int' = -1) -> 'IP_Packet[IPv4Address] | None':
534576
"""Make data for IPv4 reassembly.
535577
536-
A packet can be reassembled if its network layer is IPv4 (:class:`dpkt.ip.IP`) --
537-
one carried in IPv6 is not read -- whose header was captured whole, and the **DF**
538-
(:attr:`dpkt.ip.IP.df`) flag is :data:`False`.
578+
A packet can be reassembled if it has an IPv4 layer (:class:`dpkt.ip.IP`) whose
579+
header was captured whole, and the **DF** (:attr:`dpkt.ip.IP.df`) flag is
580+
:data:`False`. The layer is the first that is a fragment, or with none, the network
581+
layer if it is IPv4 (see :func:`_fragment`).
539582
540583
Args:
541584
packet: DPKT packet.
@@ -581,9 +624,7 @@ def ipv4_reassembly(packet: 'Packet', timestamp: 'float', *,
581624
with ``packet`` alone fails.
582625
583626
"""
584-
network = _network(packet)
585-
# an IPv6 layer is the one with extension headers, as in ``_wire_hdr_len``
586-
ipv4 = None if hasattr(network, 'extension_hdrs') else network
627+
ipv4 = cast('Optional[IP]', _fragment(packet, 4))
587628
if ipv4 is not None:
588629
if ipv4.df: # dismiss not fragmented packet
589630
return None
@@ -628,9 +669,10 @@ def ipv6_reassembly(packet: 'Packet', timestamp: 'float', *,
628669
count: 'int' = -1) -> 'IP_Packet[IPv6Address] | None':
629670
"""Make data for IPv6 reassembly.
630671
631-
A packet can be reassembled if its network layer is IPv6 (:class:`dpkt.ip6.IP6`) --
632-
one carried in IPv4 is not read -- and has an IPv6 Fragment header
633-
(:rfc:`2460#section-4.5`, i.e., :class:`dpkt.ip6.IP6FragmentHeader`).
672+
A packet can be reassembled if it has an IPv6 layer (:class:`dpkt.ip6.IP6`) with an
673+
IPv6 Fragment header (:rfc:`2460#section-4.5`, i.e.,
674+
:class:`dpkt.ip6.IP6FragmentHeader`). The layer is the first that has one (see
675+
:func:`_fragment`).
634676
635677
Args:
636678
packet: DPKT packet.
@@ -663,8 +705,7 @@ def ipv6_reassembly(packet: 'Packet', timestamp: 'float', *,
663705
with ``packet`` alone fails.
664706
665707
"""
666-
network = _network(packet)
667-
ipv6 = cast('IP6', network) if hasattr(network, 'extension_hdrs') else None
708+
ipv6 = cast('Optional[IP6]', _fragment(packet, 6))
668709
if ipv6 is not None:
669710
# dpkt keeps each extension header under its protocol number, so 44 is a
670711
# Fragment header; the dict's value type cannot say so

‎pcapkit/toolkit/pcap.py‎

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from pcapkit.foundation.traceflow.data.tcp import Packet as TF_TCP_Packet
2222
from pcapkit.protocols.internet.ip import IP as Protocol_IP
2323
from pcapkit.protocols.internet.ipv4 import IPv4
24+
from pcapkit.protocols.internet.ipv6 import IPv6
2425
from pcapkit.protocols.misc.null import NoPayload
2526
from pcapkit.protocols.misc.raw import Raw
2627
from pcapkit.protocols.protocol import ProtocolBase
@@ -34,7 +35,6 @@
3435
from pcapkit.const.reg.linktype import LinkType
3536
from pcapkit.protocols.data.internet.ipv4 import IPv4 as Data_IPv4
3637
from pcapkit.protocols.data.internet.ipv6 import IPv6 as Data_IPv6
37-
from pcapkit.protocols.internet.ipv6 import IPv6
3838
from pcapkit.protocols.internet.ipv6_frag import IPv6_Frag
3939
from pcapkit.protocols.misc.pcap import Frame
4040

@@ -193,13 +193,51 @@ def tcp_segment(frame: 'ProtocolBase') -> 'Optional[TCPSegment]':
193193
)
194194

195195

196+
def _fragment(frame: 'ProtocolBase', version: 'type[IPv4 | IPv6]') -> 'Optional[IPv4 | IPv6]':
197+
"""Fetch the IPv4 or IPv6 layer of a frame that IP reassembly reads.
198+
199+
Args:
200+
frame: PCAP frame or PCAP-NG block.
201+
version: :class:`~pcapkit.protocols.internet.ipv4.IPv4` or
202+
:class:`~pcapkit.protocols.internet.ipv6.IPv6`, the layer to fetch.
203+
204+
Returns:
205+
The first ``version`` layer, from the outside in, that is a fragment --
206+
for IPv4, one with **MF** set or a non-zero Fragment Offset, for IPv6,
207+
one with an IPv6 Fragment header; with none, the outermost ``version``
208+
layer; with no ``version`` layer, :data:`None`.
209+
210+
Note:
211+
In an IP-in-IP tunnel (:rfc:`2003`, :rfc:`2473`, :rfc:`4213`) either
212+
datagram may be the fragmented one: the inner, each fragment of it
213+
carried whole by the tunnel, or the tunnel's own, whose first fragment
214+
alone carries the inner header. Each is reassembled on its own header
215+
(:issue:`2307`). A frame of no tunnel has one layer to choose from.
216+
217+
"""
218+
if version.__name__ not in frame:
219+
return None
220+
outermost = cast('IPv4 | IPv6', frame[version.__name__])
221+
layer = outermost # type: ProtocolBase
222+
while isinstance(layer, ProtocolBase) and not isinstance(layer, NoPayload):
223+
if isinstance(layer, IPv4) and version is IPv4:
224+
if layer.info.flags.mf or layer.info.offset:
225+
return layer
226+
elif isinstance(layer, IPv6) and version is IPv6:
227+
if Enum_ExtensionHeader.IPv6_Frag in layer.extension_headers:
228+
return layer
229+
layer = layer.payload
230+
return outermost
231+
232+
196233
def ipv4_reassembly(frame: 'Frame') -> 'IP_Packet[IPv4Address] | None':
197234
"""Make data for IPv4 reassembly.
198235
199236
A frame can be reassembled if it contains an IPv4 layer
200237
(:class:`~pcapkit.protocols.internet.ipv4.IPv4`) and the **DF**
201238
(:attr:`IPv4.flags.df <pcapkit.protocols.data.internet.ipv4.Flags.df>`) flag is
202-
:data:`False`.
239+
:data:`False`. In an IP-in-IP tunnel, the layer is the first that is a fragment
240+
(see :func:`_fragment`).
203241
204242
Args:
205243
frame: PCAP frame.
@@ -212,8 +250,7 @@ def ipv4_reassembly(frame: 'Frame') -> 'IP_Packet[IPv4Address] | None':
212250
:class:`pcapkit.foundation.reassembly.ipv4.IPv4`
213251
214252
"""
215-
if 'IPv4' in frame:
216-
ipv4 = cast('IPv4', frame['IPv4'])
253+
if (ipv4 := cast('Optional[IPv4]', _fragment(frame, IPv4))) is not None:
217254
ipv4_info = ipv4.info
218255
if ipv4_info.flags.df: # dismiss not fragmented frame
219256
return None
@@ -244,7 +281,8 @@ def ipv6_reassembly(frame: 'Frame') -> 'IP_Packet[IPv6Address] | None':
244281
A frame can be reassembled if it contains an IPv6 layer
245282
(:class:`~pcapkit.protocols.internet.ipv6.IPv6`) and an IPv6 Fragment header
246283
(:rfc:`2460#section-4.5`, i.e.,
247-
:class:`~pcapkit.protocols.internet.ipv6_frag.IPv6_Frag`).
284+
:class:`~pcapkit.protocols.internet.ipv6_frag.IPv6_Frag`). In an IP-in-IP tunnel,
285+
the layer is the first that has one (see :func:`_fragment`).
248286
249287
Args:
250288
frame: PCAP frame.
@@ -257,8 +295,7 @@ def ipv6_reassembly(frame: 'Frame') -> 'IP_Packet[IPv6Address] | None':
257295
:class:`pcapkit.foundation.reassembly.ipv6.IPv6`
258296
259297
"""
260-
if 'IPv6' in frame:
261-
ipv6 = cast('IPv6', frame['IPv6'])
298+
if (ipv6 := cast('Optional[IPv6]', _fragment(frame, IPv6))) is not None:
262299
ipv6_info = ipv6.info
263300
if (ipv6_frag := ipv6.extension_headers.get( # type: ignore[call-overload]
264301
Enum_ExtensionHeader.IPv6_Frag

‎pcapkit/toolkit/pcapng.py‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@
1818
from pcapkit.foundation.traceflow.data.tcp import Packet as TF_TCP_Packet
1919
from pcapkit.protocols.data.misc.pcap.frame import Frame as Data_Frame
2020
from pcapkit.protocols.data.misc.pcap.frame import FrameInfo as Data_FrameInfo
21+
from pcapkit.protocols.internet.ipv4 import IPv4
22+
from pcapkit.protocols.internet.ipv6 import IPv6
2123
from pcapkit.protocols.schema.misc.pcap.header import RESOLUTION
22-
from pcapkit.toolkit.pcap import tcp_segment
24+
from pcapkit.toolkit.pcap import _fragment, tcp_segment
2325

2426
if TYPE_CHECKING:
2527
from ipaddress import IPv4Address, IPv6Address
28+
from typing import Optional
2629

27-
from pcapkit.protocols.internet.ipv4 import IPv4
28-
from pcapkit.protocols.internet.ipv6 import IPv6
2930
from pcapkit.protocols.internet.ipv6_frag import IPv6_Frag
3031
from pcapkit.protocols.misc.pcapng import PCAPNG, Packet
3132

@@ -39,7 +40,8 @@ def ipv4_reassembly(frame: 'PCAPNG') -> 'IP_Packet[IPv4Address] | None':
3940
A frame can be reassembled if it contains an IPv4 layer
4041
(:class:`~pcapkit.protocols.internet.ipv4.IPv4`) and the **DF**
4142
(:attr:`IPv4.flags.df <pcapkit.protocols.data.internet.ipv4.Flags.df>`) flag is
42-
:data:`False`.
43+
:data:`False`. In an IP-in-IP tunnel, the layer is the first that is a fragment
44+
(see :func:`pcapkit.toolkit.pcap._fragment`).
4345
4446
Args:
4547
frame: PCAP-NG frame.
@@ -52,8 +54,7 @@ def ipv4_reassembly(frame: 'PCAPNG') -> 'IP_Packet[IPv4Address] | None':
5254
:class:`pcapkit.foundation.reassembly.ipv4.IPv4`
5355
5456
"""
55-
if 'IPv4' in frame:
56-
ipv4 = cast('IPv4', frame['IPv4'])
57+
if (ipv4 := cast('Optional[IPv4]', _fragment(frame, IPv4))) is not None:
5758
ipv4_info = ipv4.info
5859
if ipv4_info.flags.df: # dismiss not fragmented frame
5960
return None
@@ -85,7 +86,8 @@ def ipv6_reassembly(frame: 'PCAPNG') -> 'IP_Packet[IPv6Address] | None':
8586
A frame can be reassembled if it contains an IPv6 layer
8687
(:class:`~pcapkit.protocols.internet.ipv6.IPv6`) and an IPv6 Fragment header
8788
(:rfc:`2460#section-4.5`, i.e.,
88-
:class:`~pcapkit.protocols.internet.ipv6_frag.IPv6_Frag`).
89+
:class:`~pcapkit.protocols.internet.ipv6_frag.IPv6_Frag`). In an IP-in-IP tunnel,
90+
the layer is the first that has one (see :func:`pcapkit.toolkit.pcap._fragment`).
8991
9092
Args:
9193
frame: PCAP-NG frame.
@@ -98,8 +100,7 @@ def ipv6_reassembly(frame: 'PCAPNG') -> 'IP_Packet[IPv6Address] | None':
98100
:class:`pcapkit.foundation.reassembly.ipv6.IPv6`
99101
100102
"""
101-
if 'IPv6' in frame:
102-
ipv6 = cast('IPv6', frame['IPv6'])
103+
if (ipv6 := cast('Optional[IPv6]', _fragment(frame, IPv6))) is not None:
103104
ipv6_info = ipv6.info
104105
if (ipv6_frag := ipv6.extension_headers.get( # type: ignore[call-overload]
105106
Enum_ExtensionHeader.IPv6_Frag

0 commit comments

Comments
 (0)