diff --git a/docs/source/pcapkit/protocols/transport/tcp.rst b/docs/source/pcapkit/protocols/transport/tcp.rst index 6492017a36..3106e6b358 100644 --- a/docs/source/pcapkit/protocols/transport/tcp.rst +++ b/docs/source/pcapkit/protocols/transport/tcp.rst @@ -18,7 +18,7 @@ Octets Bits Name Description 8 64 ``tcp.ack`` Acknowledgement Number (if ACK set) 12 96 ``tcp.hdr_len`` Data Offset 12 100 Reserved (must be ``\x00``) - 12 103 ``tcp.flags.ns`` ECN Concealment Protection (NS) [*]_ + 12 103 ``tcp.flags.ns`` ECN Concealment Protection (NS) 13 104 ``tcp.flags.cwr`` Congestion Window Reduced (CWR) 13 105 ``tcp.flags.ece`` ECN-Echo (ECE) 13 106 ``tcp.flags.urg`` Urgent (URG) @@ -535,7 +535,3 @@ Data Models .. rubric:: Footnotes .. [*] https://en.wikipedia.org/wiki/Transmission_Control_Protocol -.. [*] The NS bit is read off the wire but is **not** surfaced in - :class:`~pcapkit.protocols.data.transport.tcp.Flags`. :rfc:`3540` was - reclassified as Historic, so the field is left unexposed; the row is kept - because the bit is part of the header as the RFC defines it. diff --git a/pcapkit/protocols/data/transport/tcp.py b/pcapkit/protocols/data/transport/tcp.py index ea31e9471e..2793662532 100644 --- a/pcapkit/protocols/data/transport/tcp.py +++ b/pcapkit/protocols/data/transport/tcp.py @@ -49,7 +49,7 @@ class Flags(Data): """Data model for TCP flags.""" #: ECN-nonce concealment protection. - #ns: 'bool' + ns: 'bool' #: Congestion window reduced. cwr: 'bool' #: ECN-Echo. @@ -68,7 +68,7 @@ class Flags(Data): fin: 'bool' if TYPE_CHECKING: - def __init__(self, cwr: 'bool', ece: 'bool', urg: 'bool', ack: 'bool', + def __init__(self, ns: 'bool', cwr: 'bool', ece: 'bool', urg: 'bool', ack: 'bool', psh: 'bool', rst: 'bool', syn: 'bool', fin: 'bool') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin diff --git a/pcapkit/protocols/transport/tcp.py b/pcapkit/protocols/transport/tcp.py index 48a9107201..692cba24e6 100644 --- a/pcapkit/protocols/transport/tcp.py +++ b/pcapkit/protocols/transport/tcp.py @@ -20,7 +20,7 @@ 8 64 ``tcp.ack`` Acknowledgement Number (if ACK set) 12 96 ``tcp.hdr_len`` Data Offset 12 100 Reserved (must be ``\\x00``) - 12 103 ECN Concealment Protection (NS), not parsed + 12 103 ``tcp.flags.ns`` ECN Concealment Protection (NS) 13 104 ``tcp.flags.cwr`` Congestion Window Reduced (CWR) 13 105 ``tcp.flags.ece`` ECN-Echo (ECE) 13 106 ``tcp.flags.urg`` Urgent (URG) @@ -471,7 +471,7 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_TCP': ack=schema.ack, hdr_len=schema.offset['offset'] * 4, flags=Data_Flags( - #ns=bool(schema.offset['ns']), + ns=bool(schema.offset['ns']), cwr=bool(schema.flags['cwr']), ece=bool(schema.flags['ece']), urg=bool(schema.flags['urg']), @@ -692,7 +692,7 @@ def _make_data(cls, data: 'Data_TCP') -> 'dict[str, Any]': # type: ignore[overr 'dstport': data.dstport, 'seq_no': data.seq, 'ack_no': data.ack, - #'ns': data.flags.ns, + 'ns': data.flags.ns, 'cwr': data.flags.cwr, 'ece': data.flags.ece, 'urg': data.flags.urg, diff --git a/tests/protocols/transport/test_tcp_ns_flag_roundtrip_unit.py b/tests/protocols/transport/test_tcp_ns_flag_roundtrip_unit.py new file mode 100644 index 0000000000..ee77af3a03 --- /dev/null +++ b/tests/protocols/transport/test_tcp_ns_flag_roundtrip_unit.py @@ -0,0 +1,65 @@ +"""#1098 -- the TCP NS flag round-trips through read, ``make()`` and ``from_data``. + +:meth:`TCP.make ` accepts ``ns`` and +packs it into bit 103 (the low bit of byte 12), and +:class:`~pcapkit.protocols.schema.transport.tcp.OffsetFlag` declares the bit. The +reader and :meth:`TCP._make_data ` +must carry it too, so a parsed segment exposes ``flags.ns`` and a +:meth:`~pcapkit.protocols.protocol.ProtocolBase.from_data` rebuild keeps the bit +on the wire rather than clearing it silently. + +""" +from __future__ import annotations + +import importlib.util +import unittest + +from tests._support import reimport_once_per_class + +RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') +HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) + + +@unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') +class TCPNSFlagRoundTripUnitTests(unittest.TestCase): + def setUp(self) -> None: + reimport_once_per_class(self) + + def test_make_sets_ns_on_the_wire(self) -> None: + """``make(ns=True)`` sets the low bit of byte 12, next to a data offset of 5.""" + from pcapkit.protocols.transport.tcp import TCP + + self.assertEqual(TCP(srcport=1, dstport=2, ns=True).data[12], 0x51) + self.assertEqual(TCP(srcport=1, dstport=2, ns=False).data[12], 0x50) + + def test_read_exposes_ns(self) -> None: + """A parsed segment reports ``flags.ns`` as read from bit 103.""" + from pcapkit.protocols.transport.tcp import TCP + + for ns in (True, False): + with self.subTest(ns=ns): + raw = TCP(srcport=1, dstport=2, ns=ns).data + self.assertIs(TCP(raw).info.flags.ns, ns) + + def test_from_data_keeps_ns(self) -> None: + """A ``from_data`` rebuild of a parsed segment reproduces its bytes, NS included.""" + from pcapkit.protocols.transport.tcp import TCP + + for ns in (True, False): + with self.subTest(ns=ns): + raw = TCP(srcport=1, dstport=2, ns=ns, ack=True).data + rebuilt = TCP.from_data(TCP(raw).info) + self.assertEqual(rebuilt.data, raw) + self.assertIs(rebuilt.info.flags.ns, ns) + + def test_flags_field_order(self) -> None: + """``ns`` leads the flags, in wire order, ahead of ``cwr``.""" + from pcapkit.protocols.transport.tcp import TCP + + flags = TCP(TCP(srcport=1, dstport=2).data).info.flags + self.assertEqual(list(flags.keys()), + ['ns', 'cwr', 'ece', 'urg', 'ack', 'psh', 'rst', 'syn', 'fin']) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/transport/test_tcp_udp_unit.py b/tests/protocols/transport/test_tcp_udp_unit.py index 1a8344864a..07efc13d81 100644 --- a/tests/protocols/transport/test_tcp_udp_unit.py +++ b/tests/protocols/transport/test_tcp_udp_unit.py @@ -119,7 +119,7 @@ def test_tcp_index_and_make_data_preserve_flags_and_ports(self) -> None: from pcapkit.const.reg.transtype import TransType from pcapkit.protocols.transport.tcp import TCP - flags = DummyData(cwr=False, ece=False, urg=False, ack=True, psh=True, rst=False, syn=False, fin=True) + flags = DummyData(ns=False, cwr=False, ece=False, urg=False, ack=True, psh=True, rst=False, syn=False, fin=True) data = DummyData( srcport=443, dstport=59000,