Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion pcapkit/protocols/internet/hip.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,10 @@ def read(self, length: 'Optional[int]' = None, *, extension: bool = False, **kwa
raise ProtocolError('HIP: invalid format')
if schema.ver['bit_1'] != 1:
raise ProtocolError('HIP: invalid format')
# Header Length below 4 cannot cover the two HITs, and would leave a
# negative parameter length below.
if schema.len < 4:
raise ProtocolError(f'HIP: invalid header length: {schema.len}')

hip = Data_HIP(
next=schema.next,
Expand Down Expand Up @@ -649,7 +653,9 @@ def make(self,
parameters_value, total_length = self._make_hip_param(parameters, version=version)
length = total_length // 8 + 4
else:
parameters_value, length = [], 0
# NOTE: Header Length excludes the first 8 octets but always
# counts the two 16-octet HITs, hence the minimum of 4.
parameters_value, length = [], 4

return Schema_HIP(
next=next_value, # type: ignore[arg-type]
Expand Down
71 changes: 71 additions & 0 deletions tests/protocols/internet/test_hip_parameterless_length_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
"""HIP writes Header Length 4 when it carries no parameters.

GitHub issue #1189: :meth:`HIP.make <pcapkit.protocols.internet.hip.HIP.make>`
wrote Header Length ``0`` for ``parameters=None``. RFC 7401 §5.1 counts the
field in 8-octet units excluding the first 8, so the two 16-octet HITs alone
make 4 the minimum. The header's own re-parse then computed a parameter length
of ``-32`` and raised ``ProtocolError``. :meth:`HIP.read
<pcapkit.protocols.internet.hip.HIP.read>` now rejects a wire Header Length
below 4 up front.

Every case builds its own octets in memory and reads no capture.

:class:`HIP` is imported inside each test, after
:func:`~tests._support.reimport_once_per_class`, not at module load, so it
always belongs to the live :mod:`pcapkit` import.

"""

import unittest

from tests._support import reimport_once_per_class

#: Keyword arguments for a parameterless HIP header with no payload.
KWARGS = dict(next=59, packet=1, checksum=b'\x00\x00', controls_anonymous=False,
shit=0, rhit=0, payload=b'')


class TestHIPParameterlessLength(unittest.TestCase):
"""Pin Header Length for a HIP header without parameters."""

def setUp(self) -> None:
reimport_once_per_class(self)

def test_make_without_parameters_writes_length_4(self) -> None:
from pcapkit.protocols.internet.hip import HIP

for version in (1, 2):
with self.subTest(version=version):
proto = HIP(parameters=None, version=version, **KWARGS)
self.assertEqual(len(proto.data), 40)
self.assertEqual(proto.data[1], 4)
self.assertEqual(proto.info.length, 40)
self.assertNotIn('parameters', proto.info)

def test_parameterless_header_parses_back(self) -> None:
from pcapkit.protocols.internet.hip import HIP

for version in (1, 2):
with self.subTest(version=version):
data = HIP(parameters=None, version=version, **KWARGS).data
parsed = HIP(data)
self.assertEqual(parsed.info.length, 40)
self.assertEqual(parsed.info.version, version)
self.assertEqual(parsed.data, data)

def test_read_rejects_header_length_below_4(self) -> None:
from pcapkit.protocols.internet.hip import HIP
from pcapkit.utilities.exceptions import ProtocolError

for value in (0, 3):
with self.subTest(len=value):
# Next Header, Header Length, Packet Type 1, Version 2, then
# checksum, controls and the two HITs, all zero.
data = bytes([59, value, 0x01, 0x21]) + bytes(36)
with self.assertRaisesRegex(ProtocolError, 'invalid header length'):
HIP(data)


if __name__ == '__main__':
unittest.main()
2 changes: 1 addition & 1 deletion tests/protocols/internet/test_hip_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ def test_hip_read_make_post_init_and_parameter_list_edges(self) -> None:
parsed_with_param = proto.read(extension=True)
read_params.assert_called_once_with(8, version=2)
self.assertEqual(parsed_with_param.parameters[Parameter.ESP_INFO].index, 1)
self.assertEqual(proto.make(parameters=None).len, 0)
self.assertEqual(proto.make(parameters=None).len, 4)

def test_hip_register_parameter_warns_on_overwrite(self) -> None:
from pcapkit.const.hip.parameter import Parameter
Expand Down
Loading