1515import io
1616from typing import TYPE_CHECKING , Generic , cast
1717
18+ from pcapkit .const .ipv6 .extension_header import ExtensionHeader as Enum_ExtensionHeader
1819from pcapkit .const .reg .transtype import TransType as Enum_TransType
1920from pcapkit .corekit .module import ModuleDescriptor
2021from pcapkit .corekit .protochain import ProtoChain
2122from pcapkit .protocols .protocol import _PT , _ST , ProtocolBase
2223from pcapkit .protocols .schema .schema import keep_short_read , replay_short_read
2324from pcapkit .utilities .decorators import beholder
24- from pcapkit .utilities .warnings import RegistryWarning , warn
25+ from pcapkit .utilities .exceptions import stacklevel
26+ from pcapkit .utilities .warnings import ProtocolWarning , RegistryWarning , warn
2527
2628if TYPE_CHECKING :
2729 from typing import IO , Any , Optional , Type
3032
3133__all__ = ['Internet' ]
3234
35+ #: Most IPv6 extension headers dissected in one chain, i.e. one after another
36+ #: with no other layer between them (:issue:`1604`). The walk stops there, and
37+ #: what follows the last of them is kept as
38+ #: :class:`~pcapkit.protocols.misc.raw.Raw`, with one
39+ #: :exc:`~pcapkit.utilities.warnings.ProtocolWarning`. That holds whether the
40+ #: chain follows IPv6, which walks it in a loop
41+ #: (:meth:`IPv6._decode_next_layer
42+ #: <pcapkit.protocols.internet.ipv6.IPv6._decode_next_layer>`), or another
43+ #: layer such as IPv4, where each header dissects the next one itself
44+ #: (:meth:`Internet._import_next_layer`). The toolkit adapters that walk a
45+ #: chain themselves stop at the same depth.
46+ #:
47+ #: :rfc:`8200#section-4.1` allows each extension header at most once, and the
48+ #: Destination Options header at most twice, so no compliant chain gets close.
49+ #: Without the bound, a chain dissected header by header stops wherever
50+ #: Python's recursion limit falls, which depends on the interpreter.
51+ EXTENSION_HEADER_LIMIT = 32
52+
3353
3454class Internet (ProtocolBase [_PT , _ST ], Generic [_PT , _ST ]): # pylint: disable=abstract-method
3555 """Abstract base class for internet layer protocol family.
@@ -118,6 +138,12 @@ class Internet(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=ab
118138 },
119139 )
120140
141+ #: int: How many IPv6 extension headers in a row the next layer follows:
142+ #: this layer's place in such a chain, or how many headers
143+ #: :class:`~pcapkit.protocols.internet.ipv6.IPv6` walked, else ``0``, c.f.
144+ #: :data:`EXTENSION_HEADER_LIMIT` and :meth:`_next_exthdr_depth`.
145+ _exthdr_depth = 0
146+
121147 ##########################################################################
122148 # Properties.
123149 ##########################################################################
@@ -197,6 +223,10 @@ def __post_init__(self, file: 'Optional[IO[bytes] | bytes]' = None,
197223 file: Source packet stream.
198224 length: Length of packet data.
199225 extension: If the protocol is used as an IPv6 extension header.
226+ _exthdr_depth (int): Where this layer is in a chain of IPv6
227+ extension headers, counting from ``1``, or ``0`` if it is not
228+ in one. Handed over by :meth:`_import_next_layer` while
229+ parsing, c.f. :data:`EXTENSION_HEADER_LIMIT`.
200230 **kwargs: Arbitrary keyword arguments.
201231
202232 Notes:
@@ -211,6 +241,9 @@ def __post_init__(self, file: 'Optional[IO[bytes] | bytes]' = None,
211241 (:issue:`1458`).
212242
213243 """
244+ # NOTE: Set before parsing, since parsing is what dissects the next layer.
245+ self ._exthdr_depth = cast ('int' , kwargs .pop ('_exthdr_depth' , 0 ))
246+
214247 super ().__post_init__ (file , length , extension = extension , ** kwargs ) # type: ignore[arg-type]
215248 keep_short_read (self )
216249
@@ -237,6 +270,77 @@ def _read_protos(self, size: 'int') -> 'Enum_TransType':
237270 _prot = Enum_TransType .get (_byte )
238271 return _prot
239272
273+ def _next_exthdr_depth (self , proto : 'Optional[int]' , extension : 'bool' = False ) -> 'int' :
274+ """Place the next layer in its chain of IPv6 extension headers.
275+
276+ Arguments:
277+ proto: next layer protocol index
278+ extension: if the next layer is parsed as an extension header by
279+ :class:`~pcapkit.protocols.internet.ipv6.IPv6`'s walk, which
280+ counts the chain itself, c.f. :meth:`IPv6._decode_next_layer
281+ <pcapkit.protocols.internet.ipv6.IPv6._decode_next_layer>`
282+
283+ Returns:
284+ One more than :attr:`_exthdr_depth` if ``proto`` names an IPv6
285+ extension header, or ``0`` if it names none, so that another layer
286+ between two headers starts a new chain. Past
287+ :data:`EXTENSION_HEADER_LIMIT`, the next layer is left raw.
288+
289+ """
290+ if extension :
291+ return 0
292+ try :
293+ Enum_ExtensionHeader (proto )
294+ except ValueError :
295+ return 0
296+ return self ._exthdr_depth + 1
297+
298+ def _next_layer_class (self , proto : 'Optional[int]' , file_ : 'bytes' , length : 'int' ,
299+ extension : 'bool' = False ) -> 'tuple[Type[ProtocolBase], dict[str, Any]]' :
300+ r"""Pick the class the next layer is parsed with.
301+
302+ Arguments:
303+ proto: next layer protocol index
304+ file\_: payload of the current layer
305+ length: valid (*non-padding*) length
306+ extension: if is extension header, c.f. :meth:`_next_exthdr_depth`
307+
308+ Returns:
309+ :class:`~pcapkit.protocols.misc.null.NoPayload` if there is
310+ nothing to parse; :class:`~pcapkit.protocols.misc.raw.Raw` if the
311+ parse stops at this layer, or the next layer is an IPv6 extension
312+ header past :data:`EXTENSION_HEADER_LIMIT` in a row; otherwise the
313+ class registered for ``proto``. With it, the keywords only that next
314+ layer is constructed with: the warning given, as ``error``, or the
315+ next layer's place in its chain, as ``_exthdr_depth``.
316+
317+ Warns:
318+ ProtocolWarning: If the next layer is past
319+ :data:`EXTENSION_HEADER_LIMIT` (:issue:`1604`).
320+
321+ """
322+ if min (length , len (file_ )) == 0 :
323+ from pcapkit .protocols .misc .null import NoPayload # isort: skip # pylint: disable=import-outside-toplevel
324+ return NoPayload , {}
325+ if self ._sigterm :
326+ from pcapkit .protocols .misc .raw import Raw # isort: skip # pylint: disable=import-outside-toplevel
327+ return Raw , {}
328+
329+ depth = self ._next_exthdr_depth (proto , extension )
330+ if depth > EXTENSION_HEADER_LIMIT :
331+ from pcapkit .protocols .misc .raw import Raw # isort: skip # pylint: disable=import-outside-toplevel
332+
333+ message = (f'{ self .alias } : a chain of IPv6 extension headers reached the limit of '
334+ f'{ EXTENSION_HEADER_LIMIT } headers; the rest of the packet, from Next '
335+ f'Header { int (cast ("int" , proto ))} , is left undissected' )
336+ warn (message , ProtocolWarning , stacklevel = stacklevel ())
337+ return Raw , {'error' : message }
338+
339+ protocol = self ._lookup_next_layer (self .__proto__ , cast ('int' , proto ))
340+ if depth and issubclass (protocol , Internet ):
341+ return protocol , {'_exthdr_depth' : depth }
342+ return protocol , {}
343+
240344 def _decode_next_layer (self , dict_ : '_PT' , proto : 'Optional[int]' = None , # pylint: disable=arguments-differ
241345 length : 'Optional[int]' = None , * , packet : 'Optional[dict[str, Any]]' = None ,
242346 version : 'Literal[4, 6]' = 4 , ipv6_exthdr : 'Optional[ProtoChain]' = None ,
@@ -304,24 +408,23 @@ def _import_next_layer(self, proto: 'int', length: 'Optional[int]' = None, *, #
304408 Returns:
305409 Instance of next layer.
306410
307- """
308- if TYPE_CHECKING :
309- protocol : 'Type[ProtocolBase]'
411+ Notes:
412+ An IPv6 extension header past :data:`EXTENSION_HEADER_LIMIT` in a
413+ row is not dissected: it and the rest of the packet are kept as
414+ :class:`~pcapkit.protocols.misc.raw.Raw`, with one
415+ :exc:`~pcapkit.utilities.warnings.ProtocolWarning` (:issue:`1604`).
416+ Each header dissects the next one from here, so the chain's depth
417+ is handed to the next layer, c.f. :meth:`_next_layer_class`.
310418
419+ """
311420 if payload is None :
312421 file_ = self .__header__ .get_payload ()
313422 else :
314423 file_ = payload
315424 if length is None :
316425 length = len (file_ )
317426
318- if min (length , len (file_ )) == 0 :
319- from pcapkit .protocols .misc .null import NoPayload as protocol # isort: skip # pylint: disable=import-outside-toplevel
320- elif self ._sigterm :
321- from pcapkit .protocols .misc .raw import Raw as protocol # isort: skip # pylint: disable=import-outside-toplevel
322- else :
323- protocol = self ._lookup_next_layer (self .__proto__ , proto )
324-
427+ protocol , extra = self ._next_layer_class (proto , file_ , length , extension )
325428 return self ._parse_next_layer (protocol , file_ , length , version = version , extension = extension ,
326429 alias = proto , packet = packet , layer = self ._exlayer ,
327- protocol = self ._exproto , __context__ = self ._exctx )
430+ protocol = self ._exproto , __context__ = self ._exctx , ** extra )
0 commit comments