From 7eb1b2110c17f03a8ed1cfa896ab2ff3f6e7f9bc Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Wed, 7 Oct 2026 13:52:17 -0400 Subject: [PATCH] fix(tcp): MPTCP options round-trip byte-exactly (#1212-#1217, #1220) - Map the reserved bits of MP_JOIN, DSS, REMOVE_ADDR, MP_PRIO, MP_FAIL and MP_FASTCLOSE into the schemas and keep them in a new `reserved` data-model field. - MP_FAIL reads its 12 reserved bits, so the DSN starts at offset 4. - Unknown subtypes size their data as `length - 3`. - DSS keeps the `a`/`m` flags (`ack_wide`/`dsn_wide`) and they decide the field widths; the checksum is present only when the length has room for it. - ADD_ADDR follows RFC 8684: family, port and HMAC from the length, `reserved`/`echo` bits and an optional `hmac`; RFC 6824 options still parse and round-trip. - MP_CAPABLE keeps the C flag (`deny_join`) and bits D-G (`reserved`). New data-model fields are required arguments, so existing tests that build these models directly are updated. --- pcapkit/protocols/data/transport/tcp.py | 50 +++- pcapkit/protocols/schema/transport/tcp.py | 129 +++++++--- pcapkit/protocols/transport/tcp.py | 184 ++++++++++++--- tests/corekit/test_fields_ipaddress.py | 4 +- tests/corekit/test_fields_misc.py | 4 +- .../transport/test_mptcp_roundtrip_unit.py | 222 ++++++++++++++++++ .../transport/test_tcp_mptcp_addaddr_unit.py | 9 +- .../test_tcp_mptcp_error_message_unit.py | 6 +- .../protocols/transport/test_tcp_udp_unit.py | 69 +++--- 9 files changed, 564 insertions(+), 113 deletions(-) create mode 100644 tests/protocols/transport/test_mptcp_roundtrip_unit.py diff --git a/pcapkit/protocols/data/transport/tcp.py b/pcapkit/protocols/data/transport/tcp.py index 2793662532..4edfd4ae88 100644 --- a/pcapkit/protocols/data/transport/tcp.py +++ b/pcapkit/protocols/data/transport/tcp.py @@ -395,11 +395,15 @@ class MPTCPCapableFlag(Data): req: 'bool' #: Extensibility flag. ext: 'bool' + #: Flag for not establishing new subflows to the source address (``C``). + deny_join: 'bool' + #: Reserved flags (``D`` to ``G``). + reserved: 'int' #: HMAC-SHA1 flag. hsa: 'bool' if TYPE_CHECKING: - def __init__(self, req: 'bool', ext: 'bool', hsa: 'bool') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + def __init__(self, req: 'bool', ext: 'bool', deny_join: 'bool', reserved: 'int', hsa: 'bool') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin @info_final @@ -434,6 +438,8 @@ class MPTCPJoin(MPTCP): class MPTCPJoinSYN(MPTCPJoin): """Data model for TCP ``MP_JOIN-SYN`` option.""" + #: Reserved. + reserved: 'int' #: Backup path flag. backup: 'bool' #: Address ID. @@ -444,13 +450,15 @@ class MPTCPJoinSYN(MPTCPJoin): nonce: 'int' if TYPE_CHECKING: - def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', connection: 'TCP_Flags', backup: 'bool', addr_id: 'int', token: 'int', nonce: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', connection: 'TCP_Flags', reserved: 'int', backup: 'bool', addr_id: 'int', token: 'int', nonce: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin @info_final class MPTCPJoinSYNACK(MPTCPJoin): """Data model for TCP ``MP_JOIN-SYNACK`` option.""" + #: Reserved. + reserved: 'int' #: Backup path flag. backup: 'bool' #: Address ID. @@ -461,28 +469,36 @@ class MPTCPJoinSYNACK(MPTCPJoin): nonce: 'int' if TYPE_CHECKING: - def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', connection: 'TCP_Flags', backup: 'bool', addr_id: 'int', hmac: 'bytes', nonce: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', connection: 'TCP_Flags', reserved: 'int', backup: 'bool', addr_id: 'int', hmac: 'bytes', nonce: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin @info_final class MPTCPJoinACK(MPTCPJoin): """Data model for TCP ``MP_JOIN-ACK`` option.""" + #: Reserved. + reserved: 'int' #: HMAC value. hmac: 'bytes' if TYPE_CHECKING: - def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', connection: 'TCP_Flags', hmac: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', connection: 'TCP_Flags', reserved: 'int', hmac: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin @info_final class MPTCPDSS(MPTCP): """Data model for TCP ``DSS`` option.""" + #: Reserved. + reserved: 'int' #: ``DATA_FIN`` flag. data_fin: 'bool' + #: Data ACK is 8 octets (``a`` flag). + ack_wide: 'bool' #: Data ACK. ack: 'Optional[int]' + #: Data sequence number is 8 octets (``m`` flag). + dsn_wide: 'bool' #: Data sequence number. dsn: 'Optional[int]' #: Subflow sequence number. @@ -493,7 +509,7 @@ class MPTCPDSS(MPTCP): checksum: 'Optional[bytes]' if TYPE_CHECKING: - def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', data_fin: 'bool', ack: 'Optional[int]', dsn: 'Optional[int]', ssn: 'Optional[int]', dl_len: 'Optional[int]', checksum: 'Optional[bytes]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', reserved: 'int', data_fin: 'bool', ack_wide: 'bool', ack: 'Optional[int]', dsn_wide: 'bool', dsn: 'Optional[int]', ssn: 'Optional[int]', dl_len: 'Optional[int]', checksum: 'Optional[bytes]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin @info_final @@ -502,58 +518,72 @@ class MPTCPAddAddress(MPTCP): #: IP version. version: 'int' + #: Reserved. + reserved: 'int' + #: Echo flag. + echo: 'bool' #: Address ID. addr_id: 'int' #: Address. addr: 'IPAddress' #: Port number. port: 'Optional[int]' + #: Truncated HMAC. + hmac: 'Optional[bytes]' if TYPE_CHECKING: - def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', version: 'int', addr_id: 'int', addr: 'IPAddress', port: 'Optional[int]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', version: 'int', reserved: 'int', echo: 'bool', addr_id: 'int', addr: 'IPAddress', port: 'Optional[int]', hmac: 'Optional[bytes]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin @info_final class MPTCPRemoveAddress(MPTCP): """Data model for TCP ``REMOVE_ADDR`` option.""" + #: Reserved. + reserved: 'int' #: Address ID. addr_id: 'tuple[int, ...]' if TYPE_CHECKING: - def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', addr_id: 'tuple[int, ...]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', reserved: 'int', addr_id: 'tuple[int, ...]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin @info_final class MPTCPPriority(MPTCP): """Data model for TCP ``MP_PRIO`` option.""" + #: Reserved. + reserved: 'int' #: Backup path flag. backup: 'bool' #: Address ID. addr_id: 'Optional[int]' if TYPE_CHECKING: - def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', backup: 'bool', addr_id: 'Optional[int]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', reserved: 'int', backup: 'bool', addr_id: 'Optional[int]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin @info_final class MPTCPFallback(MPTCP): """Data model for TCP ``MP_FAIL`` option.""" + #: Reserved. + reserved: 'int' #: Data sequence number. dsn: 'int' if TYPE_CHECKING: - def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', dsn: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', reserved: 'int', dsn: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin @info_final class MPTCPFastclose(MPTCP): """Data model for TCP ``MP_FASTCLOSE`` option.""" + #: Reserved. + reserved: 'int' #: Option receiver's key. rkey: 'int' if TYPE_CHECKING: - def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', rkey: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + def __init__(self, kind: 'OptionNumber', length: 'int', subtype: 'MPTCPOption', reserved: 'int', rkey: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin diff --git a/pcapkit/protocols/schema/transport/tcp.py b/pcapkit/protocols/schema/transport/tcp.py index 69c087ec3a..4fffa71fe6 100644 --- a/pcapkit/protocols/schema/transport/tcp.py +++ b/pcapkit/protocols/schema/transport/tcp.py @@ -136,6 +136,10 @@ class MPTCPCapableFlags(TypedDict): req: int #: Extensibility flag. ext: int + #: Do not establish new subflows to the source address. + deny_join: int + #: Reserved flags (``D`` to ``G``). + reserved: int #: Use of HMAC-SHA1. hsa: int @@ -144,6 +148,8 @@ class MPTCPSubtypeJoin(TypedDict): #: Subtype. subtype: int + #: Reserved. + reserved: int #: Backup flag. backup: int @@ -152,10 +158,14 @@ class MPTCPSubtype(TypedDict): #: Subtype. subtype: int + #: Reserved. + reserved: int class MPTCPDSSFlags(TypedDict): """MPTCP-DSS flags.""" + #: Reserved. + reserved: int #: ``DATA_FIN`` flag. F: int #: Data sequence number is 8 octets (if not set, DSN is 4 octets). @@ -173,14 +183,18 @@ class MPTCPSubtypeAddAddress(TypedDict): #: Subtype. subtype: int - #: IP version. - version: int + #: Reserved. + reserved: int + #: Echo flag. + echo: int class MPTCPSubtypePriority(TypedDict): """MPTCP Priority subtype field.""" #: Subtype. subtype: int + #: Reserved. + reserved: int #: Backup flag. backup: int @@ -220,17 +234,47 @@ def mptcp_add_address_selector(pkt: 'dict[str, Any]') -> 'Field': pkt: Packet data. Returns: - * If IP version is 4, a :class:`~pcapkit.corekit.fields.ipaddress.IPv4AddressField` - instance. - * If IP version is 6, a :class:`~pcapkit.corekit.fields.ipaddress.IPv6AddressField` - instance. + * If the option length is one of the IPv4 lengths, a + :class:`~pcapkit.corekit.fields.ipaddress.IPv4AddressField` instance. + * If the option length is one of the IPv6 lengths, a + :class:`~pcapkit.corekit.fields.ipaddress.IPv6AddressField` instance. + + Note: + :rfc:`8684` section 3.4.1 figure 12 has no IP version field: the + address family follows from the option length, which is 8 or 20 for + an IPv4 or IPv6 address, plus 2 with the port, plus 8 with the + truncated HMAC. The :rfc:`6824` ``IPVer`` nibble occupies the bits + :rfc:`8684` reserves and spends on ``E``, and its lengths are the + HMAC-less ones, so both layouts are read the same way. """ - if pkt['test']['version'] == 4: + if pkt['length'] in (8, 10, 16, 18): return IPv4AddressField() - if pkt['test']['version'] == 6: + if pkt['length'] in (20, 22, 28, 30): return IPv6AddressField() - raise FieldError(f'TCP: [OptNo {Enum_Option.Multipath_TCP}] {Enum_MPTCPOption.ADD_ADDR} invalid IP version') + raise FieldError(f'TCP: [OptNo {Enum_Option.Multipath_TCP}] {Enum_MPTCPOption.ADD_ADDR} invalid length') + + +def mptcp_dss_checksum(pkt: 'dict[str, Any]') -> 'bool': + """Condition function for :attr:`MPTCPDSS.checksum` field. + + :rfc:`8684` section 3.3 makes the checksum part of the mapping only when + checksums were negotiated, so a mapping without one is 2 octets shorter + and only the option length tells the two forms apart. + + Args: + pkt: Packet data. + + Returns: + Whether the option length leaves room for the checksum after the + Data ACK and the mapping. + + """ + if not pkt['flags']['M']: + return False + size = 4 + ((8 if pkt['flags']['a'] else 4) if pkt['flags']['A'] else 0) \ + + (8 if pkt['flags']['m'] else 4) + 6 + return pkt['length'] >= size + 2 def mptcp_dss_ack_selector(pkt: 'dict[str, Any]') -> 'Field': @@ -264,7 +308,8 @@ def mptcp_dss_ack_selector(pkt: 'dict[str, Any]') -> 'Field': 8-octet form, and wire *absence* is no obstacle: :attr:`MPTCPDSS.ssn`, :attr:`MPTCPDSS.dl_len` and :attr:`MPTCPDSS.checksum` are each a ``ConditionalField`` on the sibling - ``M`` flag. The ``SwitchField`` form is kept for a narrower reason about + ``M`` flag (the checksum on the option length as well). The + ``SwitchField`` form is kept for a narrower reason about composition. A :class:`~pcapkit.corekit.fields.misc.ConditionalField`'s ``length`` forwards to the wrapped field unconditionally, never consulting the @@ -823,7 +868,7 @@ class MPTCPUnknown(MPTCP): 'data': (4, 4), }) #: Data. - data: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 2) + data: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 3) if TYPE_CHECKING: def __init__(self, kind: 'Enum_Option', length: 'int', test: 'MPTCPSubtypeUnknown', data: 'bytes') -> 'None': ... @@ -842,6 +887,8 @@ class MPTCPCapable(MPTCP, code=Enum_MPTCPOption.MP_CAPABLE): flags: 'MPTCPCapableFlags' = BitField(length=1, namespace={ 'req': (0, 1), 'ext': (1, 1), + 'deny_join': (2, 1), + 'reserved': (3, 4), 'hsa': (7, 1), }) # NOTE: :rfc:`8684` section 3.1 figure 4 gives MP_CAPABLE five lengths -- @@ -884,6 +931,7 @@ class MPTCPJoinSYN(MPTCPJoin): #: Subtype and flags. test: 'MPTCPSubtypeJoin' = BitField(length=1, namespace={ 'subtype': (0, 4), + 'reserved': (4, 3), 'backup': (7, 1), }) #: Address ID. @@ -904,6 +952,7 @@ class MPTCPJoinSYNACK(MPTCPJoin): #: Subtype and flags. test: 'MPTCPSubtypeJoin' = BitField(length=1, namespace={ 'subtype': (0, 4), + 'reserved': (4, 3), 'backup': (7, 1), }) #: Address ID. @@ -921,12 +970,11 @@ def __init__(self, kind: 'Enum_Option', length: 'int', test: 'MPTCPSubtypeJoin', class MPTCPJoinACK(MPTCPJoin): """Header schema for Multipath TCP join option for ``ACK`` connection.""" - #: Subtype. - test: 'MPTCPSubtype' = BitField(length=1, namespace={ + #: Subtype and reserved. + test: 'MPTCPSubtype' = BitField(length=2, namespace={ 'subtype': (0, 4), + 'reserved': (4, 12), }) - #: Reserved. - reserved: 'bytes' = PaddingField(length=1) #: Sender's HMAC. hmac: 'bytes' = BytesField(length=20) @@ -938,12 +986,14 @@ def __init__(self, kind: 'Enum_Option', length: 'int', test: 'MPTCPSubtype', hma class MPTCPDSS(MPTCP, code=Enum_MPTCPOption.DSS): """Header schema for Multipath TCP DSS option.""" - #: Subtype and flags. + #: Subtype and reserved. test: 'MPTCPSubtype' = BitField(length=1, namespace={ 'subtype': (0, 4), + 'reserved': (4, 4), }) #: Flags. flags: 'MPTCPDSSFlags' = BitField(length=1, namespace={ + 'reserved': (0, 3), 'F': (3, 1), 'm': (4, 1), 'M': (5, 1), @@ -976,10 +1026,10 @@ class MPTCPDSS(MPTCP, code=Enum_MPTCPOption.DSS): UInt16Field(), lambda pkt: pkt['flags']['M'], ) - #: Checksum. + #: Checksum (if negotiated, c.f. :func:`mptcp_dss_checksum`). checksum: 'bytes' = ConditionalField( BytesField(length=2), - lambda pkt: pkt['flags']['M'], + mptcp_dss_checksum, ) if TYPE_CHECKING: @@ -990,10 +1040,11 @@ def __init__(self, kind: 'Enum_Option', length: 'int', test: 'MPTCPSubtype', fla class MPTCPAddAddress(MPTCP, code=Enum_MPTCPOption.ADD_ADDR): """Header schema for Multipath TCP add address option.""" - #: Subtype and IP version. + #: Subtype and flags. test: 'MPTCPSubtypeAddAddress' = BitField(length=1, namespace={ 'subtype': (0, 4), - 'version': (4, 4), + 'reserved': (4, 3), + 'echo': (7, 1), }) #: Address ID. addr_id: 'int' = UInt8Field() @@ -1004,20 +1055,26 @@ class MPTCPAddAddress(MPTCP, code=Enum_MPTCPOption.ADD_ADDR): #: Port. port: 'int' = ConditionalField( UInt16Field(), - lambda pkt: pkt['length'] in (10, 22), + lambda pkt: pkt['length'] in (10, 18, 22, 30), + ) + #: Truncated HMAC. + hmac: 'bytes' = ConditionalField( + BytesField(length=8), + lambda pkt: pkt['length'] in (16, 18, 28, 30), ) if TYPE_CHECKING: - def __init__(self, kind: 'Enum_Option', length: 'int', test: 'MPTCPSubtypeAddAddress', addr_id: 'int', address: 'IPv4Address | IPv6Address', port: 'Optional[int]') -> 'None': ... + def __init__(self, kind: 'Enum_Option', length: 'int', test: 'MPTCPSubtypeAddAddress', addr_id: 'int', address: 'IPv4Address | IPv6Address', port: 'Optional[int]', hmac: 'Optional[bytes]') -> 'None': ... @schema_final class MPTCPRemoveAddress(MPTCP, code=Enum_MPTCPOption.REMOVE_ADDR): """Header schema for Multipath TCP remove address option.""" - #: Subtype. + #: Subtype and reserved. test: 'MPTCPSubtype' = BitField(length=1, namespace={ 'subtype': (0, 4), + 'reserved': (4, 4), }) #: Address ID. addr_id: 'list[int]' = ListField( @@ -1036,6 +1093,7 @@ class MPTCPPriority(MPTCP, code=Enum_MPTCPOption.MP_PRIO): #: Subtype. test: 'MPTCPSubtypePriority' = BitField(length=1, namespace={ 'subtype': (0, 4), + 'reserved': (4, 3), 'backup': (7, 1), }) #: Address ID. @@ -1052,9 +1110,14 @@ def __init__(self, kind: 'Enum_Option', length: 'int', test: 'MPTCPSubtypePriori class MPTCPFallback(MPTCP, code=Enum_MPTCPOption.MP_FAIL): """Header schema for Multipath TCP fallback option.""" - #: Subtype. - test: 'MPTCPSubtype' = BitField(length=1, namespace={ + #: Subtype and reserved. + #: + #: :rfc:`8684` section 3.7 figure 16 follows the subtype's 4 bits with + #: **12** reserved bits, so the Data Sequence Number starts at the fifth + #: octet of the 12-octet option. + test: 'MPTCPSubtype' = BitField(length=2, namespace={ 'subtype': (0, 4), + 'reserved': (4, 12), }) #: Data sequence number. dsn: 'int' = UInt64Field() @@ -1067,20 +1130,16 @@ def __init__(self, kind: 'Enum_Option', length: 'int', test: 'MPTCPSubtype', dsn class MPTCPFastclose(MPTCP, code=Enum_MPTCPOption.MP_FASTCLOSE): """Header schema for Multipath TCP fastclose option.""" - #: Subtype. - test: 'MPTCPSubtype' = BitField(length=1, namespace={ - 'subtype': (0, 4), - }) - #: Reserved. + #: Subtype and reserved. #: #: :rfc:`8684` section 3.5 figure 14 spends a whole 32-bit row on #: ``Kind``/``Length``/``Subtype``/``(reserved)``, i.e. the subtype's 4 bits #: are followed by **12** reserved bits, not 4 -- so the subtype-and-reserved - #: part is 2 octets and the option is 12 octets in total. Declared the same - #: way :class:`MPTCPJoinACK` declares its own reserved octet, for the same - #: reason: a wider ``test`` would make the reserved bits look like part of - #: the subtype namespace. - reserved: 'bytes' = PaddingField(length=1) + #: part is 2 octets and the option is 12 octets in total. + test: 'MPTCPSubtype' = BitField(length=2, namespace={ + 'subtype': (0, 4), + 'reserved': (4, 12), + }) #: Option receiver's key. key: 'int' = UInt64Field() diff --git a/pcapkit/protocols/transport/tcp.py b/pcapkit/protocols/transport/tcp.py index 5296c43b3c..ad02495054 100644 --- a/pcapkit/protocols/transport/tcp.py +++ b/pcapkit/protocols/transport/tcp.py @@ -1544,6 +1544,8 @@ def _read_mptcp_capable(self, schema: 'Schema_MPTCPCapable', *, options: 'Option flags=Data_MPTCPCapableFlag( req=bool(schema.flags['req']), ext=bool(schema.flags['ext']), + deny_join=bool(schema.flags['deny_join']), + reserved=schema.flags['reserved'], hsa=bool(schema.flags['hsa']), ), skey=schema.skey if schema.length > 4 else None, @@ -1611,6 +1613,7 @@ def _read_join_syn(self, schema: 'Schema_MPTCPJoinSYN', *, options: 'Option') -> length=schema.length, subtype=schema.subtype, connection=Enum_Flags.SYN, # type: ignore[arg-type] + reserved=schema.test['reserved'], backup=bool(schema.test['backup']), addr_id=schema.addr_id, token=schema.token, @@ -1663,6 +1666,7 @@ def _read_join_synack(self, schema: 'Schema_MPTCPJoinSYNACK', options: 'Option') length=schema.length, subtype=schema.subtype, connection=Enum_Flags.SYN | Enum_Flags.ACK, # type: ignore[arg-type] + reserved=schema.test['reserved'], backup=bool(schema.test['backup']), addr_id=schema.addr_id, hmac=schema.hmac, @@ -1708,6 +1712,7 @@ def _read_join_ack(self, schema: 'Schema_MPTCPJoinACK', *, options: 'Option') -> length=schema.length, subtype=schema.subtype, connection=Enum_Flags.ACK, # type: ignore[arg-type] + reserved=schema.test['reserved'], hmac=schema.hmac, ) return data @@ -1744,13 +1749,22 @@ def _read_mptcp_dss(self, schema: 'Schema_MPTCPDSS', *, options: 'Option') -> 'D Returns: Parsed option data. + Note: + The reserved field spans the 4 low bits of the subtype octet and the + 3 high bits of the flags octet; it is kept as one 7-bit value. The + checksum is absent when the option length leaves no room for it, + i.e., when checksums were not negotiated. + """ data = Data_MPTCPDSS( kind=Enum_Option.Multipath_TCP, # type: ignore[arg-type] length=schema.length, subtype=schema.subtype, + reserved=(schema.test['reserved'] << 3) | schema.flags['reserved'], data_fin=bool(schema.flags['F']), + ack_wide=bool(schema.flags['a']), ack=schema.ack, + dsn_wide=bool(schema.flags['m']), dsn=schema.dsn, ssn=schema.ssn, dl_len=schema.dl_len, @@ -1761,18 +1775,22 @@ def _read_mptcp_dss(self, schema: 'Schema_MPTCPDSS', *, options: 'Option') -> 'D def _read_mptcp_addaddr(self, schema: 'Schema_MPTCPAddAddress', *, options: 'Option') -> 'Data_MPTCPAddAddress': # pylint: disable=unused-argument """Read Add Address option. - Structure of ``ADD_ADDR`` [:rfc:`6824`]: + Structure of ``ADD_ADDR`` [:rfc:`8684`, section 3.4.1, figure 12]: .. code-block:: text 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +---------------+---------------+-------+-------+---------------+ - | Kind | Length |Subtype| IPVer | Address ID | + | Kind | Length |Subtype|(rsv)|E| Address ID | +---------------+---------------+-------+-------+---------------+ - | Address (IPv4 - 4 octets / IPv6 - 16 octets) | + | Address (IPv4: 4 octets / IPv6: 16 octets) | +-------------------------------+-------------------------------+ - | Port (2 octets, optional) | + | Port (2 octets, optional) | | + +-------------------------------+ | + | Truncated HMAC (8 octets, if E=0) | + | +-------------------------------+ + | | +-------------------------------+ Arguments: @@ -1783,20 +1801,29 @@ def _read_mptcp_addaddr(self, schema: 'Schema_MPTCPAddAddress', *, options: 'Opt Parsed option data. Raises: - ProtocolError: Invalid IP version and/or addresses. + ProtocolError: If the length is not one of the :rfc:`8684` lengths. + + Note: + The address family, the port and the HMAC all follow from the + option length, never from the ``E`` flag, so an :rfc:`6824` + option, whose ``IPVer`` nibble overlaps the reserved bits and + ``E``, parses the same way and round-trips unchanged. """ - if schema.test['version'] not in (4, 6): - raise ProtocolError(f'{self.alias}: [OptNo {schema.kind}] invalid IP version') + if schema.length not in (8, 10, 16, 18, 20, 22, 28, 30): + raise ProtocolError(f'{self.alias}: [OptNo {schema.kind}] invalid format') data = Data_MPTCPAddAddress( kind=Enum_Option.Multipath_TCP, # type: ignore[arg-type] length=schema.length, subtype=schema.subtype, - version=schema.test['version'], + version=schema.address.version, + reserved=schema.test['reserved'], + echo=bool(schema.test['echo']), addr_id=schema.addr_id, addr=schema.address, port=schema.port, + hmac=schema.hmac, ) return data @@ -1845,6 +1872,7 @@ def _read_mptcp_remove(self, schema: 'Schema_MPTCPRemoveAddress', *, options: 'O kind=Enum_Option.Multipath_TCP, # type: ignore[arg-type] length=schema.length, subtype=schema.subtype, + reserved=schema.test['reserved'], addr_id=tuple(schema.addr_id), ) @@ -1897,6 +1925,7 @@ def _read_mptcp_prio(self, schema: 'Schema_MPTCPPriority', *, options: 'Option') kind=Enum_Option.Multipath_TCP, # type: ignore[arg-type] length=schema.length, subtype=schema.subtype, + reserved=schema.test['reserved'], backup=bool(schema.test['backup']), addr_id=schema.addr_id, ) @@ -1906,7 +1935,7 @@ def _read_mptcp_prio(self, schema: 'Schema_MPTCPPriority', *, options: 'Option') def _read_mptcp_fail(self, schema: 'Schema_MPTCPFallback', *, options: 'Option') -> 'Data_MPTCPFallback': # pylint: disable=unused-argument """Read Fallback option. - Structure of ``MP_FAIL`` [:rfc:`6824`]: + Structure of ``MP_FAIL`` [:rfc:`8684`, section 3.7, figure 16]: .. code-block:: text @@ -1938,6 +1967,7 @@ def _read_mptcp_fail(self, schema: 'Schema_MPTCPFallback', *, options: 'Option') kind=Enum_Option.Multipath_TCP, # type: ignore[arg-type] length=schema.length, subtype=schema.subtype, + reserved=schema.test['reserved'], dsn=schema.dsn, ) return data @@ -1975,7 +2005,7 @@ def _read_mptcp_fastclose(self, schema: 'Schema_MPTCPFastclose', options: 'Optio subtype-and-reserved (2, being 4 subtype bits and 12 reserved) + the option receiver's key (64 bits, 8). The guard, ``_make_mptcp_fastclose`` and the schema must all agree on 12, the - schema accounting for the reserved octet with a padding field. + schema reading the 12 reserved bits along with the subtype. """ if schema.length != 12: @@ -1985,6 +2015,7 @@ def _read_mptcp_fastclose(self, schema: 'Schema_MPTCPFastclose', options: 'Optio kind=Enum_Option.Multipath_TCP, # type: ignore[arg-type] length=schema.length, subtype=schema.subtype, + reserved=schema.test['reserved'], rkey=schema.key, ) return data @@ -2726,6 +2757,8 @@ def _make_mptcp_capable(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_M version: 'int' = 0, flag_req: 'bool' = False, flag_ext: 'bool' = False, + flag_deny_join: 'bool' = False, + flag_reserved: 'int' = 0, flag_hsa: 'bool' = False, skey: 'Optional[int]' = 0, rkey: 'Optional[int]' = 0, @@ -2744,6 +2777,9 @@ def _make_mptcp_capable(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_M version: MPTCP version flag_req: checksum required flag flag_ext: extensibility flag + flag_deny_join: flag for not establishing new subflows to the + source address + flag_reserved: reserved flags (``D`` to ``G``) flag_hsa: use of HMAC-SHA1 flag skey: option sender's key rkey: option receiver's key @@ -2762,6 +2798,8 @@ def _make_mptcp_capable(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_M version = opt.version flag_req = opt.flags.req flag_ext = opt.flags.ext + flag_deny_join = opt.flags.deny_join + flag_reserved = opt.flags.reserved flag_hsa = opt.flags.hsa skey = opt.skey rkey = opt.rkey @@ -2785,6 +2823,8 @@ def _make_mptcp_capable(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_M flags={ 'req': flag_req, 'ext': flag_ext, + 'deny_join': flag_deny_join, + 'reserved': flag_reserved, 'hsa': flag_hsa, }, skey=skey, @@ -2814,6 +2854,7 @@ def _make_mptcp_join(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTC raise ProtocolError(f'{self.alias}: [OptNo {Enum_Option.Multipath_TCP}] {subtype}: invalid flags combination') def _make_join_syn(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPJoinSYN]' = None, *, + reserved: 'int' = 0, backup: 'bool' = False, addr_id: 'int' = 0, token: 'int' = 0, @@ -2824,6 +2865,7 @@ def _make_join_syn(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPJ Args: subtype: MPTCP subtype opt: option data + reserved: reserved bits backup: backup flag addr_id: address ID token: receiver's token @@ -2835,6 +2877,7 @@ def _make_join_syn(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPJ """ if opt is not None: + reserved = opt.reserved backup = opt.backup addr_id = opt.addr_id token = opt.token @@ -2845,6 +2888,7 @@ def _make_join_syn(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPJ length=12, test={ 'subtype': subtype.value, + 'reserved': reserved, 'backup': backup, }, addr_id=addr_id, @@ -2853,6 +2897,7 @@ def _make_join_syn(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPJ ) def _make_join_synack(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPJoinSYNACK]' = None, *, + reserved: 'int' = 0, backup: 'bool' = False, addr_id: 'int' = 0, hmac: 'bytes' = bytes(8), @@ -2863,6 +2908,7 @@ def _make_join_synack(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPT Args: subtype: MPTCP subtype opt: option data + reserved: reserved bits backup: backup flag addr_id: address ID hmac: sender's truncated HMAC @@ -2874,6 +2920,7 @@ def _make_join_synack(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPT """ if opt is not None: + reserved = opt.reserved backup = opt.backup addr_id = opt.addr_id # NOTE: ``hmac`` must be carried over here, or reconstructing a @@ -2892,6 +2939,7 @@ def _make_join_synack(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPT length=16, test={ 'subtype': subtype.value, + 'reserved': reserved, 'backup': backup, }, addr_id=addr_id, @@ -2900,6 +2948,7 @@ def _make_join_synack(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPT ) def _make_join_ack(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPJoinACK]' = None, *, + reserved: 'int' = 0, hmac: 'bytes' = bytes(20), **kwargs: 'Any') -> 'Schema_MPTCPJoinACK': """Make multipath TCP join ACK option. @@ -2907,6 +2956,7 @@ def _make_join_ack(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPJ Args: subtype: MPTCP subtype opt: option data + reserved: reserved bits hmac: sender's HMAC **kwargs: arbitrary keyword arguments @@ -2915,6 +2965,7 @@ def _make_join_ack(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPJ """ if opt is not None: + reserved = opt.reserved hmac = opt.hmac return Schema_MPTCPJoinACK( @@ -2926,13 +2977,17 @@ def _make_join_ack(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPJ length=24, test={ 'subtype': subtype.value, + 'reserved': reserved, }, hmac=hmac, ) def _make_mptcp_dss(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPDSS]' = None, *, + reserved: 'int' = 0, data_fin: 'bool' = False, + ack_wide: 'Optional[bool]' = None, ack: 'Optional[int]' = None, + dsn_wide: 'Optional[bool]' = None, dsn: 'Optional[int]' = None, ssn: 'Optional[int]' = None, dl_len: 'Optional[int]' = None, @@ -2943,53 +2998,70 @@ def _make_mptcp_dss(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCP Args: subtype: MPTCP subtype opt: option data + reserved: reserved bits (7 bits) data_fin: ``DATA_FIN`` flag + ack_wide: whether Data ACK is 8 octets; if not given, it is 8 octets + only when ``ack`` does not fit in 4 ack: Data ACK + dsn_wide: whether data sequence number is 8 octets; if not given, it + is 8 octets only when ``dsn`` does not fit in 4 dsn: data sequence number ssn: subflow sequence number dl_len: data-level length - checksum: checksum + checksum: checksum, or ``None`` if checksums were not negotiated **kwargs: arbitrary keyword arguments Returns: Constructed option schema. + Raises: + ProtocolError: If the mapping fields are given only in part, or a + 4-octet field is asked to hold a value wider than 32 bits. + """ if opt is not None: + reserved = opt.reserved data_fin = opt.data_fin + ack_wide = opt.ack_wide ack = opt.ack + dsn_wide = opt.dsn_wide dsn = opt.dsn ssn = opt.ssn dl_len = opt.dl_len checksum = opt.checksum flag_A = ack is not None - flag_a = cast('int', ack).bit_length() > 32 if flag_A else False + flag_a = ack_wide if ack_wide is not None else (flag_A and cast('int', ack).bit_length() > 32) flag_M = dsn is not None - flag_m = cast('int', dsn).bit_length() > 32 if flag_M else False + flag_m = dsn_wide if dsn_wide is not None else (flag_M and cast('int', dsn).bit_length() > 32) - if flag_M and (ssn is None or dl_len is None or checksum is None): + if (flag_A and not flag_a and cast('int', ack).bit_length() > 32) or \ + (flag_M and not flag_m and cast('int', dsn).bit_length() > 32): + raise ProtocolError(f'{self.alias}: [OptNo {Enum_Option.Multipath_TCP}] {subtype}: value too wide for 4 octets') + if flag_M and (ssn is None or dl_len is None): raise ProtocolError(f'{self.alias}: [OptNo {Enum_Option.Multipath_TCP}] {subtype}: missing required fields') if not flag_M and (ssn is not None or dl_len is not None or checksum is not None): raise ProtocolError(f'{self.alias}: [OptNo {Enum_Option.Multipath_TCP}] {subtype}: missing required fields') return Schema_MPTCPDSS( kind=cast('Enum_Option', Enum_Option.Multipath_TCP), - # NOTE: this arithmetic follows :rfc:`8684` section 3.3 figure 9. - # Read it as a base plus a widening increment rather than - # as one term per field: 4 for ``Kind``/``Length``/subtype/flags, then - # ``A`` contributes the 4-octet Data ACK and ``a`` a further 4 to make - # it 8; ``M`` contributes 12 (a 4-octet DSN, the 4-octet Subflow - # Sequence Number, the 2-octet Data-Level Length and the 2-octet - # Checksum) and ``m`` a further 4 to widen the DSN to 8. All flags set - # gives 4 + 4 + 4 + 12 + 4 = 28, which is the maximum the section - # states in prose. - length=4 + (4 if flag_A else 0) + (4 if flag_a else 0) + (12 if flag_M else 0) + (4 if flag_m else 0), + # NOTE: this arithmetic follows :rfc:`8684` section 3.3 figure 9: + # 4 for ``Kind``/``Length``/subtype/flags, then the Data ACK (4, or + # 8 with ``a``) when ``A`` is set, and the mapping when ``M`` is set + # -- the DSN (4, or 8 with ``m``), the 4-octet Subflow Sequence + # Number, the 2-octet Data-Level Length and, only if checksums were + # negotiated, the 2-octet Checksum. ``a`` and ``m`` widen a field + # only when it is present, so a stray flag on the wire still packs + # back without changing the length. + length=4 + ((8 if flag_a else 4) if flag_A else 0) + + ((8 if flag_m else 4) + 6 + (2 if checksum is not None else 0) if flag_M else 0), test={ 'subtype': subtype.value, + 'reserved': reserved >> 3, }, flags={ + 'reserved': reserved & 0b111, 'F': data_fin, 'A': flag_A, 'a': flag_a, @@ -3004,52 +3076,85 @@ def _make_mptcp_dss(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCP ) def _make_mptcp_addaddr(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPAddAddress]' = None, *, + reserved: 'Optional[int]' = None, + echo: 'Optional[bool]' = None, addr_id: 'int' = 0, addr: 'IPv4Address | IPv6Address | int | bytes | str' = '0.0.0.0', # nosec: B104 port: 'Optional[int]' = None, + hmac: 'Optional[bytes]' = None, **kwargs: 'Any') -> 'Schema_MPTCPAddAddress': """Make multipath TCP add address option. Args: subtype: MPTCP subtype opt: option data + reserved: reserved bits + echo: echo flag addr_id: address ID addr: address port: port number + hmac: truncated HMAC **kwargs: arbitrary keyword arguments Returns: Constructed option schema. + Raises: + ProtocolError: If ``hmac`` is not 8 octets, or is given together + with ``echo`` set. + + Note: + :rfc:`8684` section 3.4.1 allows an ADD_ADDR without the truncated + HMAC only as an echo (``E`` set). So when none of ``reserved``, + ``echo`` and ``hmac`` is given, the option is built in the + :rfc:`6824` layout instead, its ``IPVer`` nibble taking the place of + the reserved bits and ``E``; otherwise ``reserved`` defaults to 0 and + ``echo`` to whether ``hmac`` is absent. A parsed ``opt`` is rebuilt + as it was read, without those checks. + """ if opt is not None: + reserved = opt.reserved + echo = opt.echo addr_id = opt.addr_id addr_val = opt.addr port = opt.port + hmac = opt.hmac else: - # NOTE: Through ``parse_ip_address`` because the ``version`` sub-field - # and the option length below are both derived from the family here, - # ahead of the schema, so a bare ``ipaddress.ip_address`` would launder - # a ``bool`` into an ``IPv4Address`` that the schema's own guard can no - # longer tell from a real address: ``addr=True`` would reach - # ``mptcp_add_address_selector`` as ``0.0.0.1`` with ``version=4``. + # NOTE: Through ``parse_ip_address`` because the option length below + # is derived from the family here, ahead of the schema, so a bare + # ``ipaddress.ip_address`` would launder a ``bool`` into an + # ``IPv4Address`` that the schema's own guard can no longer tell from + # a real address: ``addr=True`` would reach + # ``mptcp_add_address_selector`` as ``0.0.0.1`` with an IPv4 length. addr_val = parse_ip_address( addr, f'{self.alias}: [OptNo {Enum_Option.Multipath_TCP}] invalid address') version = addr_val.version + if reserved is None and echo is None and hmac is None: + reserved, echo = version >> 1, bool(version & 1) + if reserved is None: + reserved = 0 + if echo is None: + echo = hmac is None + if opt is None and hmac is not None and (echo or len(hmac) != 8): + raise ProtocolError(f'{self.alias}: [OptNo {Enum_Option.Multipath_TCP}] {subtype}: invalid HMAC') return Schema_MPTCPAddAddress( kind=cast('Enum_Option', Enum_Option.Multipath_TCP), - length=4 + (4 if version == 4 else 16) + (2 if port is not None else 0), + length=4 + (4 if version == 4 else 16) + (2 if port is not None else 0) + (8 if hmac is not None else 0), test={ 'subtype': subtype.value, - 'version': version, + 'reserved': reserved, + 'echo': echo, }, addr_id=addr_id, address=addr_val, port=port, + hmac=hmac, ) def _make_mptcp_remove(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPRemoveAddress]' = None, *, + reserved: 'int' = 0, addr_id: 'Optional[list[int]]' = None, **kwargs: 'Any') -> 'Schema_MPTCPRemoveAddress': """Make multipath TCP remove address option. @@ -3057,6 +3162,7 @@ def _make_mptcp_remove(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MP Args: subtype: MPTCP subtype opt: option data + reserved: reserved bits addr_id: address ID list **kwargs: arbitrary keyword arguments @@ -3065,6 +3171,7 @@ def _make_mptcp_remove(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MP """ if opt is not None: + reserved = opt.reserved addr_id_list = cast('list[int]', opt.addr_id) else: addr_id_list = addr_id if addr_id is not None else [] @@ -3080,11 +3187,13 @@ def _make_mptcp_remove(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MP length=3 + len(addr_id_list), test={ 'subtype': subtype.value, + 'reserved': reserved, }, addr_id=addr_id_list, ) def _make_mptcp_prio(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPPriority]' = None, *, + reserved: 'int' = 0, backup: 'bool' = False, addr_id: 'Optional[int]' = None, **kwargs: 'Any') -> 'Schema_MPTCPPriority': @@ -3093,6 +3202,7 @@ def _make_mptcp_prio(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTC Args: subtype: MPTCP subtype opt: option data + reserved: reserved bits backup: backup flag addr_id: address ID **kwargs: arbitrary keyword arguments @@ -3105,6 +3215,7 @@ def _make_mptcp_prio(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTC # NOTE: ``backup`` must be carried over, or reconstructing a parsed # MP_PRIO would always write ``B=0``; the ``B`` flag is the entire # payload of this option. + reserved = opt.reserved backup = opt.backup addr_id = opt.addr_id @@ -3124,12 +3235,14 @@ def _make_mptcp_prio(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTC length=3 if addr_id is None else 4, test={ 'subtype': subtype.value, + 'reserved': reserved, 'backup': backup, }, addr_id=addr_id, ) def _make_mptcp_fail(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPFallback]' = None, *, + reserved: 'int' = 0, dsn: 'int' = 0, **kwargs: 'Any') -> 'Schema_MPTCPFallback': """Make multipath TCP fail option. @@ -3137,6 +3250,7 @@ def _make_mptcp_fail(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTC Args: subtype: MPTCP subtype opt: option data + reserved: reserved bits dsn: data sequence number **kwargs: arbitrary keyword arguments @@ -3145,6 +3259,7 @@ def _make_mptcp_fail(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTC """ if opt is not None: + reserved = opt.reserved dsn = opt.dsn return Schema_MPTCPFallback( @@ -3152,11 +3267,13 @@ def _make_mptcp_fail(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTC length=12, test={ 'subtype': subtype.value, + 'reserved': reserved, }, dsn=dsn, ) def _make_mptcp_fastclose(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPFastclose]' = None, *, + reserved: 'int' = 0, key: 'int' = 0, **kwargs: 'Any') -> 'Schema_MPTCPFastclose': """Make multipath TCP fastclose option. @@ -3164,6 +3281,7 @@ def _make_mptcp_fastclose(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data Args: subtype: MPTCP subtype opt: option data + reserved: reserved bits key: option receiver's key **kwargs: arbitrary keyword arguments @@ -3172,6 +3290,7 @@ def _make_mptcp_fastclose(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data """ if opt is not None: + reserved = opt.reserved key = opt.rkey return Schema_MPTCPFastclose( @@ -3179,6 +3298,7 @@ def _make_mptcp_fastclose(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data length=12, test={ 'subtype': subtype.value, + 'reserved': reserved, }, key=key, ) diff --git a/tests/corekit/test_fields_ipaddress.py b/tests/corekit/test_fields_ipaddress.py index 845a9668ae..6d8aacdbe2 100644 --- a/tests/corekit/test_fields_ipaddress.py +++ b/tests/corekit/test_fields_ipaddress.py @@ -518,8 +518,8 @@ def test_switch_backed_address_makers_reject_a_bool(self) -> None: backed by a :class:`~pcapkit.corekit.fields.misc.SwitchField`, and both makers derive the wire form from the address family before the schema exists. Measured before the fix: ``ip=True`` packed a locator of ``::1`` - with no error at all, and ``addr=True`` gave - ``MPTCPAddAddress(test={'version': 4}, address=IPv4Address('0.0.0.1'))``. + with no error at all, and ``addr=True`` gave an IPv4 ``MPTCPAddAddress`` + with ``address=IPv4Address('0.0.0.1')``. The five ``mh`` sites are covered next to the rest of that module, in ``MHUnitTests.test_mh_length_derived_addresses_reject_a_bool``. diff --git a/tests/corekit/test_fields_misc.py b/tests/corekit/test_fields_misc.py index ef07facb4a..af0312447b 100644 --- a/tests/corekit/test_fields_misc.py +++ b/tests/corekit/test_fields_misc.py @@ -55,9 +55,9 @@ 'mh tcoa IPv6', {'length': 18}), ('pcapkit.protocols.schema.transport.tcp', 'MPTCPAddAddress', 'address', - 'tcp mptcp add_addr IPv4', {'test': {'version': 4}}), + 'tcp mptcp add_addr IPv4', {'length': 8}), ('pcapkit.protocols.schema.transport.tcp', 'MPTCPAddAddress', 'address', - 'tcp mptcp add_addr IPv6', {'test': {'version': 6}}), + 'tcp mptcp add_addr IPv6', {'length': 20}), ] diff --git a/tests/protocols/transport/test_mptcp_roundtrip_unit.py b/tests/protocols/transport/test_mptcp_roundtrip_unit.py new file mode 100644 index 0000000000..fd1f11ab06 --- /dev/null +++ b/tests/protocols/transport/test_mptcp_roundtrip_unit.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +"""Multipath TCP options rebuild byte for byte. + +GitHub issues #1212, #1213, #1214, #1215, #1216, #1217 and #1220, all found by +the transport round-trip audit (#1202). Each case wraps one option in a TCP +header and checks that :meth:`~pcapkit.protocols.protocol.ProtocolBase.from_data` +over the parsed segment reproduces it: + +* #1212 -- the reserved bits of MP_JOIN, DSS, REMOVE_ADDR, MP_PRIO, MP_FAIL and + MP_FASTCLOSE were written back as zero; +* #1213 -- MP_FAIL had no room for its 12 reserved bits, so the DSN was read + one octet early; +* #1214 -- an unknown subtype read ``length - 2`` octets after the subtype + octet, one past the option; +* #1215 -- DSS chose the Data ACK and DSN widths from the values instead of the + ``a``/``m`` flags; +* #1216 -- DSS assumed a checksum whenever ``M`` was set; +* #1217 -- ADD_ADDR in the :rfc:`8684` layout (``E`` flag, optional HMAC) did + not parse; +* #1220 -- MP_CAPABLE dropped the ``C`` flag and bits ``D`` to ``G``. + +Every case builds its own octets in memory and reads no capture. :class:`TCP` +is imported inside each test, after +:func:`~tests._support.reimport_once_per_class`, not at module load. + +""" + +import unittest + +from tests._support import reimport_once_per_class + +#: TCP header without its offset/flags octets: ports 1234 and 80, sequence 1, +#: acknowledgement 2; the window, checksum and urgent pointer follow them. +HEAD = bytes.fromhex('04d2' '0050' '00000001' '00000002') +TAIL = bytes.fromhex('03e8' 'abcd' '0000') + +SYN, ACK = 0x02, 0x10 + +#: ``name -> (TCP flags, option octets)``; the octets are padded with leading +#: NOPs to a multiple of 4. +OPTIONS = { + # 1212 + 'join-syn-reserved': (SYN, '1e0c1e01 00000002 00000003'), + 'join-synack-reserved': (SYN | ACK, '1e101e01 0102030405060708 00000003'), + 'join-ack-reserved': (ACK, '1e1810ff' + '11' * 20), + 'dss-reserved-subtype-octet': (ACK, '1e082e01 00000005'), + 'dss-reserved-flags-octet': (ACK, '1e0820e1 00000005'), + 'remove-addr-reserved': (ACK, '01 1e044f01 01 01 01'), + 'mp-prio-reserved': (ACK, '01 1e035e'), + 'mp-fastclose-reserved': (ACK, '1e0c7fff 0000000000000009'), + # 1213 + 'mp-fail': (ACK, '1e0c6000 0000000000000007'), + 'mp-fail-reserved': (ACK, '1e0c6abc 0000000000000007'), + # 1214 + 'unknown-subtype': (ACK, '1e048001'), + 'unknown-subtype-longer': (ACK, '1e06f1020304 0101'), + # 1215 + 'dss-wide-small-ack': (ACK, '1e0c2003 0000000000000005'), + 'dss-wide-small-dsn': (ACK, '1e14200c 0000000000000005 00000006 0007 abcd'), + # 1216 + 'dss-no-checksum': (ACK, '1e0e2004 00000005 00000006 0007 0101'), + 'dss-checksum': (ACK, '1e102004 00000005 00000006 0007 abcd'), + # 1217 + 'add-addr-echo': (ACK, '1e083101 c0000201'), + 'add-addr-hmac': (ACK, '1e103001 c0000201 0001020304050607'), + 'add-addr-hmac-port': (ACK, '0101 1e123001 c0000201 01bb 0001020304050607'), + 'add-addr-ipv6-hmac-port': (ACK, '0101 1e1e3001 20010db8000000000000000000000001 01bb 0001020304050607'), + 'add-addr-rfc6824-ipv4-port': (ACK, '0101 1e0a3401 c0000201 01bb'), + 'add-addr-rfc6824-ipv6': (ACK, '1e143601 20010db8000000000000000000000001'), + # 1220 + 'mp-capable-all-flags': (SYN, '1e0c01ff 0000000000000001'), + 'mp-capable-c-flag': (SYN, '1e0c0121 0000000000000001'), +} + + +def segment(flags: int, option: str) -> bytes: + """Wrap option octets in a TCP header with a matching data offset.""" + opt = bytes.fromhex(option.replace(' ', '')) + assert len(opt) % 4 == 0, option + return HEAD + bytes([(5 + len(opt) // 4) << 4, flags]) + TAIL + opt + + +class TestMPTCPRoundTrip(unittest.TestCase): + """Pin the wire form of every Multipath TCP subtype the audit flagged.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def parse(self, name: str) -> 'tuple[bytes, object]': + from pcapkit.protocols.transport.tcp import TCP + + raw = segment(*OPTIONS[name]) + return raw, TCP(raw, len(raw)) + + def mptcp(self, name: str) -> 'object': + from pcapkit.const.tcp.option import Option + + _, tcp = self.parse(name) + return tcp.info.options[Option.Multipath_TCP] + + def test_from_data_rebuilds_byte_for_byte(self) -> None: + from pcapkit.protocols.transport.tcp import TCP + + for name in OPTIONS: + with self.subTest(option=name): + raw, tcp = self.parse(name) + self.assertEqual(TCP.from_data(tcp.info).data.hex(), raw.hex()) + + def test_reserved_bits_are_kept(self) -> None: + cases = { + 'join-syn-reserved': 0b111, + 'join-synack-reserved': 0b111, + 'join-ack-reserved': 0x0ff, + 'dss-reserved-subtype-octet': 0b1110000, + 'dss-reserved-flags-octet': 0b0000111, + 'remove-addr-reserved': 0xf, + 'mp-prio-reserved': 0b111, + 'mp-fastclose-reserved': 0xfff, + 'mp-fail-reserved': 0xabc, + } + for name, reserved in cases.items(): + with self.subTest(option=name): + self.assertEqual(self.mptcp(name).reserved, reserved) + + def test_mp_fail_reads_the_dsn_after_the_reserved_bits(self) -> None: + self.assertEqual(self.mptcp('mp-fail').dsn, 7) + self.assertEqual(self.mptcp('mp-fail-reserved').dsn, 7) + + def test_unknown_subtype_stays_inside_the_option(self) -> None: + opt = self.mptcp('unknown-subtype') + self.assertEqual(opt.length, 4) + self.assertEqual(opt.data, b'\x00\x01') + self.assertEqual(self.mptcp('unknown-subtype-longer').data, bytes.fromhex('01020304')) + + def test_dss_width_follows_the_flags(self) -> None: + ack = self.mptcp('dss-wide-small-ack') + self.assertTrue(ack.ack_wide) + self.assertEqual(ack.ack, 5) + dsn = self.mptcp('dss-wide-small-dsn') + self.assertTrue(dsn.dsn_wide) + self.assertEqual((dsn.dsn, dsn.ssn, dsn.dl_len, dsn.checksum), (5, 6, 7, b'\xab\xcd')) + + def test_dss_checksum_follows_the_length(self) -> None: + bare = self.mptcp('dss-no-checksum') + self.assertEqual((bare.dsn, bare.ssn, bare.dl_len), (5, 6, 7)) + self.assertIsNone(bare.checksum) + self.assertEqual(self.mptcp('dss-checksum').checksum, b'\xab\xcd') + + def test_add_addr_rfc8684_layout(self) -> None: + import ipaddress + + echo = self.mptcp('add-addr-echo') + self.assertEqual((echo.version, echo.echo, echo.reserved), (4, True, 0)) + self.assertEqual(echo.addr, ipaddress.ip_address('192.0.2.1')) + self.assertIsNone(echo.hmac) + + full = self.mptcp('add-addr-ipv6-hmac-port') + self.assertEqual((full.version, full.echo, full.port), (6, False, 443)) + self.assertEqual(full.hmac, bytes.fromhex('0001020304050607')) + + legacy = self.mptcp('add-addr-rfc6824-ipv4-port') + self.assertEqual((legacy.version, legacy.port, legacy.hmac), (4, 443, None)) + + def test_add_addr_rejects_an_unknown_length(self) -> None: + from pcapkit.protocols.transport.tcp import TCP + from pcapkit.utilities.exceptions import FieldError + + raw = segment(ACK, '1e093001 c0000201 01 010101') + with self.assertRaises(FieldError): + TCP(raw, len(raw)) + + def test_mp_capable_keeps_c_and_d_to_g(self) -> None: + flags = self.mptcp('mp-capable-all-flags').flags + self.assertEqual((flags.req, flags.ext, flags.deny_join, flags.reserved, flags.hsa), + (True, True, True, 0xf, True)) + flags = self.mptcp('mp-capable-c-flag').flags + self.assertEqual((flags.deny_join, flags.reserved), (True, 0)) + + def test_makers_build_the_new_forms(self) -> None: + from pcapkit.const.tcp.mp_tcp_option import MPTCPOption + from pcapkit.protocols.transport.tcp import TCP + + tcp = TCP.__new__(TCP) + self.assertEqual(tcp._make_mptcp_dss( # pylint: disable=protected-access + MPTCPOption.DSS, ack_wide=True, ack=5).pack().hex(), '1e0c20030000000000000005') + self.assertEqual(tcp._make_mptcp_dss( # pylint: disable=protected-access + MPTCPOption.DSS, dsn=5, ssn=6, dl_len=7).pack().hex(), '1e0e200400000005000000060007') + self.assertEqual(tcp._make_mptcp_addaddr( # pylint: disable=protected-access + MPTCPOption.ADD_ADDR, addr_id=1, addr='192.0.2.1', + hmac=bytes(range(8))).pack().hex(), '1e103001c00002010001020304050607') + self.assertEqual(tcp._make_mptcp_addaddr( # pylint: disable=protected-access + MPTCPOption.ADD_ADDR, echo=True, addr_id=1, addr='192.0.2.1').pack().hex(), '1e083101c0000201') + self.assertEqual(tcp._make_mptcp_fail( # pylint: disable=protected-access + MPTCPOption.MP_FAIL, dsn=7).pack().hex(), '1e0c60000000000000000007') + self.assertEqual(tcp._make_mptcp_capable( # pylint: disable=protected-access + MPTCPOption.MP_CAPABLE, version=1, flag_deny_join=True, flag_hsa=True, skey=1, rkey=None + ).pack().hex(), '1e0c01210000000000000001') + + def test_add_addr_maker_rejects_a_bad_hmac(self) -> None: + from pcapkit.const.tcp.mp_tcp_option import MPTCPOption + from pcapkit.protocols.transport.tcp import TCP + from pcapkit.utilities.exceptions import ProtocolError + + tcp = TCP.__new__(TCP) + for kwargs in ({'echo': True, 'hmac': bytes(8)}, {'hmac': bytes(7)}, {'hmac': bytes(9)}): + with self.subTest(**{key: repr(value) for key, value in kwargs.items()}): + with self.assertRaises(ProtocolError): + tcp._make_mptcp_addaddr( # pylint: disable=protected-access + MPTCPOption.ADD_ADDR, addr='192.0.2.1', **kwargs) + + def test_dss_maker_rejects_a_wide_value_in_a_narrow_field(self) -> None: + from pcapkit.const.tcp.mp_tcp_option import MPTCPOption + from pcapkit.protocols.transport.tcp import TCP + from pcapkit.utilities.exceptions import ProtocolError + + tcp = TCP.__new__(TCP) + with self.assertRaises(ProtocolError): + tcp._make_mptcp_dss(MPTCPOption.DSS, ack_wide=False, ack=1 << 40) # pylint: disable=protected-access + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/transport/test_tcp_mptcp_addaddr_unit.py b/tests/protocols/transport/test_tcp_mptcp_addaddr_unit.py index 5c2e8495a2..5afb122b54 100644 --- a/tests/protocols/transport/test_tcp_mptcp_addaddr_unit.py +++ b/tests/protocols/transport/test_tcp_mptcp_addaddr_unit.py @@ -69,7 +69,8 @@ from pcapkit.utilities.warnings import UnknownFieldWarning #: A spec-correct ``ADD_ADDR`` option, RFC 8684 section 3.4.1: ``Kind`` ``0x1e`` -#: (Multipath TCP), ``Length`` ``8``, ``Subtype`` ``3``/``IPVer`` ``4`` packed into one +#: (Multipath TCP), ``Length`` ``8``, ``Subtype`` ``3``/``IPVer`` ``4`` (the :rfc:`6824` +#: layout, which :rfc:`8684` reads as reserved bits and ``E``) packed into one #: octet as ``0x34``, ``Address ID`` ``1``, then the four octets of ``192.0.2.1``. Built #: by hand from the RFC figure, not through :meth:`TCP._make_mptcp_addaddr #: ` or any other maker, so a @@ -208,9 +209,12 @@ def test_opt_argument_round_trips_through_the_maker(self) -> None: length=10, subtype=Enum_MPTCPOption.ADD_ADDR, version=4, + reserved=2, + echo=False, addr_id=1, addr=ipaddress.ip_address('1.2.3.4'), port=443, + hmac=None, ) tcp = TCP.__new__(TCP) @@ -241,7 +245,8 @@ def test_add_addr_spec_octets_parse_correctly(self) -> None: ``MPTCPAddAddress.test`` read the ``kind`` octet ``0x1e`` (``0001 1110``) as its own subtype/version octet, decoding ``version=14``, which :func:`~pcapkit.protocols.schema.transport.tcp.mptcp_add_address_selector` - rejects outright since it is neither 4 nor 6. + then rejected outright since it was neither 4 nor 6. The selector now + takes the address family from the option length instead (#1217). """ from pcapkit.const.tcp.option import Option as Enum_Option diff --git a/tests/protocols/transport/test_tcp_mptcp_error_message_unit.py b/tests/protocols/transport/test_tcp_mptcp_error_message_unit.py index 478542a5b2..a6bb2fdaf4 100644 --- a/tests/protocols/transport/test_tcp_mptcp_error_message_unit.py +++ b/tests/protocols/transport/test_tcp_mptcp_error_message_unit.py @@ -231,8 +231,10 @@ def test_no_doubled_separator_survives_anywhere_in_the_module(self) -> None: # four lines are deleted outright -- so when you legitimately change the # number of such messages, update the number here rather than loosening # the assertion. The one above it, on the doubled form, is the one that - # must stay at 0 forever. 33 since #1042 added the MP_CAPABLE maker's. - self.assertEqual(source.count("f'{self.alias}: [OptNo"), 33) + # must stay at 0 forever. 33 since #1042 added the MP_CAPABLE maker's, + # 34 since #1215 added the DSS maker's 4-octet width check, 35 since + # #1217 added the ADD_ADDR maker's HMAC check. + self.assertEqual(source.count("f'{self.alias}: [OptNo"), 35) if __name__ == '__main__': diff --git a/tests/protocols/transport/test_tcp_udp_unit.py b/tests/protocols/transport/test_tcp_udp_unit.py index b4ad02d77a..b88036d38f 100644 --- a/tests/protocols/transport/test_tcp_udp_unit.py +++ b/tests/protocols/transport/test_tcp_udp_unit.py @@ -551,6 +551,7 @@ def test_tcp_option_constructors_cover_data_model_and_mapping_paths(self) -> Non length=12, subtype=MPTCPOption.MP_JOIN, connection=Flags.SYN, + reserved=0, backup=True, addr_id=1, token=2, @@ -563,6 +564,7 @@ def test_tcp_option_constructors_cover_data_model_and_mapping_paths(self) -> Non length=20, subtype=MPTCPOption.MP_JOIN, connection=Flags.SYN | Flags.ACK, + reserved=0, backup=True, addr_id=2, hmac=b'12345678', @@ -575,6 +577,7 @@ def test_tcp_option_constructors_cover_data_model_and_mapping_paths(self) -> Non length=24, subtype=MPTCPOption.MP_JOIN, connection=Flags.ACK, + reserved=0, hmac=b'1' * 20, ) self.assertEqual(mptcp_option(join_ack, ack=True).hmac, b'1' * 20) @@ -584,7 +587,7 @@ def test_tcp_option_constructors_cover_data_model_and_mapping_paths(self) -> Non length=32, subtype=MPTCPOption.MP_CAPABLE, version=0, - flags=tcp_data.MPTCPCapableFlag(req=True, ext=False, hsa=True), + flags=tcp_data.MPTCPCapableFlag(req=True, ext=False, deny_join=False, reserved=0, hsa=True), skey=1, rkey=2, dl_len=None, @@ -603,8 +606,11 @@ def test_tcp_option_constructors_cover_data_model_and_mapping_paths(self) -> Non tcp_data.MPTCPDSS(kind=Option.Multipath_TCP, length=18, subtype=MPTCPOption.DSS, + reserved=0, data_fin=True, + ack_wide=True, ack=(1 << 33), + dsn_wide=True, dsn=(1 << 33), ssn=2, dl_len=3, @@ -618,15 +624,19 @@ def test_tcp_option_constructors_cover_data_model_and_mapping_paths(self) -> Non length=10, subtype=MPTCPOption.ADD_ADDR, version=4, + reserved=0, + echo=True, addr_id=1, addr=ipaddress.ip_address('192.0.2.1'), - port=None), + port=None, + hmac=None), ).address), '192.0.2.1') self.assertEqual(proto._make_mptcp_remove( MPTCPOption.REMOVE_ADDR, tcp_data.MPTCPRemoveAddress(kind=Option.Multipath_TCP, length=4, subtype=MPTCPOption.REMOVE_ADDR, + reserved=0, addr_id=(1, 2)), ).addr_id, (1, 2)) self.assertEqual(proto._make_mptcp_prio( @@ -634,6 +644,7 @@ def test_tcp_option_constructors_cover_data_model_and_mapping_paths(self) -> Non tcp_data.MPTCPPriority(kind=Option.Multipath_TCP, length=4, subtype=MPTCPOption.MP_PRIO, + reserved=0, backup=True, addr_id=5), ).addr_id, 5) @@ -642,6 +653,7 @@ def test_tcp_option_constructors_cover_data_model_and_mapping_paths(self) -> Non tcp_data.MPTCPFallback(kind=Option.Multipath_TCP, length=12, subtype=MPTCPOption.MP_FAIL, + reserved=0, dsn=99), ).dsn, 99) self.assertEqual(proto._make_mptcp_fastclose( @@ -649,6 +661,7 @@ def test_tcp_option_constructors_cover_data_model_and_mapping_paths(self) -> Non tcp_data.MPTCPFastclose(kind=Option.Multipath_TCP, length=12, subtype=MPTCPOption.MP_FASTCLOSE, + reserved=0, rkey=123), ).key, 123) @@ -971,7 +984,7 @@ def mark(schema, length: int, subtype: MPTCPOption): # or the new split, so it stays a genuine rejection. capable_no_receiver = mark( MPTCPCapable(test={'subtype': MPTCPOption.MP_CAPABLE.value, 'version': 0}, - flags={'req': 1, 'ext': 0, 'hsa': 1}, skey=1, rkey=2), + flags={'req': 1, 'ext': 0, 'deny_join': 0, 'reserved': 0, 'hsa': 1}, skey=1, rkey=2), 12, MPTCPOption.MP_CAPABLE, ) @@ -982,7 +995,7 @@ def mark(schema, length: int, subtype: MPTCPOption): capable_with_receiver = mark( MPTCPCapable(test={'subtype': MPTCPOption.MP_CAPABLE.value, 'version': 0}, - flags={'req': 0, 'ext': 1, 'hsa': 0}, skey=1, rkey=2), + flags={'req': 0, 'ext': 1, 'deny_join': 0, 'reserved': 0, 'hsa': 0}, skey=1, rkey=2), 20, MPTCPOption.MP_CAPABLE, ) @@ -990,7 +1003,7 @@ def mark(schema, length: int, subtype: MPTCPOption): with self.assertRaises(ProtocolError): proto._read_mode_mp(mark( MPTCPCapable(test={'subtype': MPTCPOption.MP_CAPABLE.value, 'version': 0}, - flags={'req': 0, 'ext': 0, 'hsa': 0}, skey=1, rkey=None), + flags={'req': 0, 'ext': 0, 'deny_join': 0, 'reserved': 0, 'hsa': 0}, skey=1, rkey=None), 32, MPTCPOption.MP_CAPABLE, ), options=options) @@ -1013,7 +1026,7 @@ def mark(schema, length: int, subtype: MPTCPOption): # halves against real segments. proto.make(syn=True) join_syn = mark( - MPTCPJoinSYN(test={'subtype': MPTCPOption.MP_JOIN.value, 'backup': 1}, + MPTCPJoinSYN(test={'subtype': MPTCPOption.MP_JOIN.value, 'reserved': 0, 'backup': 1}, addr_id=1, token=2, nonce=3), 12, MPTCPOption.MP_JOIN, @@ -1021,7 +1034,7 @@ def mark(schema, length: int, subtype: MPTCPOption): self.assertTrue(proto._read_mode_mp(join_syn, options=options).backup) with self.assertRaises(ProtocolError): proto._read_mode_mp(mark( - MPTCPJoinSYN(test={'subtype': MPTCPOption.MP_JOIN.value, 'backup': 0}, + MPTCPJoinSYN(test={'subtype': MPTCPOption.MP_JOIN.value, 'reserved': 0, 'backup': 0}, addr_id=1, token=2, nonce=3), 11, MPTCPOption.MP_JOIN, @@ -1037,7 +1050,7 @@ def mark(schema, length: int, subtype: MPTCPOption): # genuine rejection rather than an off-by-one near the correct length. proto.make(syn=True, ack=True) join_synack = mark( - MPTCPJoinSYNACK(test={'subtype': MPTCPOption.MP_JOIN.value, 'backup': 0}, + MPTCPJoinSYNACK(test={'subtype': MPTCPOption.MP_JOIN.value, 'reserved': 0, 'backup': 0}, addr_id=1, hmac=b'12345678', nonce=3), 16, MPTCPOption.MP_JOIN, @@ -1045,7 +1058,7 @@ def mark(schema, length: int, subtype: MPTCPOption): self.assertEqual(proto._read_mode_mp(join_synack, options=options).hmac, b'12345678') with self.assertRaises(ProtocolError): proto._read_mode_mp(mark( - MPTCPJoinSYNACK(test={'subtype': MPTCPOption.MP_JOIN.value, 'backup': 0}, + MPTCPJoinSYNACK(test={'subtype': MPTCPOption.MP_JOIN.value, 'reserved': 0, 'backup': 0}, addr_id=1, hmac=b'12345678', nonce=3), 20, MPTCPOption.MP_JOIN, @@ -1053,14 +1066,14 @@ def mark(schema, length: int, subtype: MPTCPOption): proto.make(ack=True) join_ack = mark( - MPTCPJoinACK(test={'subtype': MPTCPOption.MP_JOIN.value}, hmac=b'1' * 20), + MPTCPJoinACK(test={'subtype': MPTCPOption.MP_JOIN.value, 'reserved': 0}, hmac=b'1' * 20), 24, MPTCPOption.MP_JOIN, ) self.assertEqual(proto._read_mode_mp(join_ack, options=options).hmac, b'1' * 20) with self.assertRaises(ProtocolError): proto._read_mode_mp(mark( - MPTCPJoinACK(test={'subtype': MPTCPOption.MP_JOIN.value}, hmac=b'1' * 20), + MPTCPJoinACK(test={'subtype': MPTCPOption.MP_JOIN.value, 'reserved': 0}, hmac=b'1' * 20), 23, MPTCPOption.MP_JOIN, ), options=options) @@ -1069,8 +1082,8 @@ def mark(schema, length: int, subtype: MPTCPOption): proto._read_mode_mp(join_syn, options=options) dss = mark( - MPTCPDSS(test={'subtype': MPTCPOption.DSS.value}, - flags={'F': 1, 'm': 0, 'M': 1, 'a': 0, 'A': 1}, + MPTCPDSS(test={'subtype': MPTCPOption.DSS.value, 'reserved': 0}, + flags={'reserved': 0, 'F': 1, 'm': 0, 'M': 1, 'a': 0, 'A': 1}, ack=1, dsn=2, ssn=3, dl_len=4, checksum=b'\x00\x01'), 18, MPTCPOption.DSS, @@ -1080,7 +1093,7 @@ def mark(schema, length: int, subtype: MPTCPOption): self.assertEqual(dss_data.checksum, b'\x00\x01') addaddr = mark( - MPTCPAddAddress(test={'subtype': MPTCPOption.ADD_ADDR.value, 'version': 4}, + MPTCPAddAddress(test={'subtype': MPTCPOption.ADD_ADDR.value, 'reserved': 2, 'echo': 0}, addr_id=1, address=ipaddress.ip_address('192.0.2.1'), port=443), 10, MPTCPOption.ADD_ADDR, @@ -1088,9 +1101,9 @@ def mark(schema, length: int, subtype: MPTCPOption): self.assertEqual(str(proto._read_mode_mp(addaddr, options=options).addr), '192.0.2.1') with self.assertRaises(ProtocolError): proto._read_mode_mp(mark( - MPTCPAddAddress(test={'subtype': MPTCPOption.ADD_ADDR.value, 'version': 5}, + MPTCPAddAddress(test={'subtype': MPTCPOption.ADD_ADDR.value, 'reserved': 2, 'echo': 1}, addr_id=1, address=ipaddress.ip_address('192.0.2.1'), port=None), - 8, + 9, MPTCPOption.ADD_ADDR, ), options=options) @@ -1101,45 +1114,45 @@ def mark(schema, length: int, subtype: MPTCPOption): # removed from ``_make_mptcp_remove``, which is worth not leaving in a # test as though it were correct. remove = mark( - MPTCPRemoveAddress(test={'subtype': MPTCPOption.REMOVE_ADDR.value}, addr_id=[1, 2]), + MPTCPRemoveAddress(test={'subtype': MPTCPOption.REMOVE_ADDR.value, 'reserved': 0}, addr_id=[1, 2]), 5, MPTCPOption.REMOVE_ADDR, ) self.assertEqual(proto._read_mode_mp(remove, options=options).addr_id, (1, 2)) with self.assertRaises(ProtocolError): proto._read_mode_mp(mark( - MPTCPRemoveAddress(test={'subtype': MPTCPOption.REMOVE_ADDR.value}, addr_id=[]), + MPTCPRemoveAddress(test={'subtype': MPTCPOption.REMOVE_ADDR.value, 'reserved': 0}, addr_id=[]), 2, MPTCPOption.REMOVE_ADDR, ), options=options) prio = mark( - MPTCPPriority(test={'subtype': MPTCPOption.MP_PRIO.value, 'backup': 1}, addr_id=9), + MPTCPPriority(test={'subtype': MPTCPOption.MP_PRIO.value, 'reserved': 0, 'backup': 1}, addr_id=9), 4, MPTCPOption.MP_PRIO, ) self.assertTrue(proto._read_mode_mp(prio, options=options).backup) self.assertIsNone(proto._read_mode_mp(mark( - MPTCPPriority(test={'subtype': MPTCPOption.MP_PRIO.value, 'backup': 0}, addr_id=None), + MPTCPPriority(test={'subtype': MPTCPOption.MP_PRIO.value, 'reserved': 0, 'backup': 0}, addr_id=None), 3, MPTCPOption.MP_PRIO, ), options=options).addr_id) with self.assertRaises(ProtocolError): proto._read_mode_mp(mark( - MPTCPPriority(test={'subtype': MPTCPOption.MP_PRIO.value, 'backup': 0}, addr_id=None), + MPTCPPriority(test={'subtype': MPTCPOption.MP_PRIO.value, 'reserved': 0, 'backup': 0}, addr_id=None), 5, MPTCPOption.MP_PRIO, ), options=options) fallback = mark( - MPTCPFallback(test={'subtype': MPTCPOption.MP_FAIL.value}, dsn=99), + MPTCPFallback(test={'subtype': MPTCPOption.MP_FAIL.value, 'reserved': 0}, dsn=99), 12, MPTCPOption.MP_FAIL, ) self.assertEqual(proto._read_mode_mp(fallback, options=options).dsn, 99) with self.assertRaises(ProtocolError): proto._read_mode_mp(mark( - MPTCPFallback(test={'subtype': MPTCPOption.MP_FAIL.value}, dsn=99), + MPTCPFallback(test={'subtype': MPTCPOption.MP_FAIL.value, 'reserved': 0}, dsn=99), 11, MPTCPOption.MP_FAIL, ), options=options) @@ -1152,14 +1165,14 @@ def mark(schema, length: int, subtype: MPTCPOption): # value the guard used to require and that the RFC never produces for this # option. fastclose = mark( - MPTCPFastclose(test={'subtype': MPTCPOption.MP_FASTCLOSE.value}, key=123), + MPTCPFastclose(test={'subtype': MPTCPOption.MP_FASTCLOSE.value, 'reserved': 0}, key=123), 12, MPTCPOption.MP_FASTCLOSE, ) self.assertEqual(proto._read_mode_mp(fastclose, options=options).rkey, 123) with self.assertRaises(ProtocolError): proto._read_mode_mp(mark( - MPTCPFastclose(test={'subtype': MPTCPOption.MP_FASTCLOSE.value}, key=123), + MPTCPFastclose(test={'subtype': MPTCPOption.MP_FASTCLOSE.value, 'reserved': 0}, key=123), 16, MPTCPOption.MP_FASTCLOSE, ), options=options) @@ -1215,13 +1228,13 @@ def test_transport_schema_helpers_cover_selectors_and_port_fields(self) -> None: }) self.assertEqual(type(tcp_schema.mptcp_add_address_selector({ - 'test': {'version': 4}, + 'length': 8, })).__name__, 'IPv4AddressField') self.assertEqual(type(tcp_schema.mptcp_add_address_selector({ - 'test': {'version': 6}, + 'length': 20, })).__name__, 'IPv6AddressField') with self.assertRaises(FieldError): - tcp_schema.mptcp_add_address_selector({'test': {'version': 5}}) + tcp_schema.mptcp_add_address_selector({'length': 9}) inner = tcp_schema.MPTCPUnknown( test={'subtype': MPTCPOption.Reserved_for_Private_Use.value, 'data': 1},