From cf1d2bd1e5477d2b3fa0ebd6d6080c40e4be8d75 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Thu, 17 Sep 2026 12:30:08 -0400 Subject: [PATCH 1/4] protocols: complete the Mobility Header registry -- 10 message types, 51 options, 3 CGA extensions `MH` implemented the RFC 5568 fast-handover messages and the options they need; everything else in the registry fell through to a generic handler and parsed as opaque. This fills it in, in both directions. * **All 24 registered message data types** are now dispatched. New: Home Agent Switch [RFC 5142], Heartbeat [RFC 5847], Binding Revocation [RFC 5846], Localized Routing Initiation and Acknowledgment [RFC 6705], Update Notification and its Acknowledgement [RFC 7077], Flow Binding [RFC 7109], Subscription Query and Response [RFC 7161]. * **70 of the 71 registered options** are now dispatched, spanning the PMIPv6, NEMO, flow-binding, QoS, multicast and DMM blocks, with nested sub-option registries for the flow identification, access network identifier, quality-of-service and LMA-controlled MAG parameter families. * **All 4 CGA extensions**: the three RFC 4581 experimental types join Multi-Prefix. RFC 4581 gives their extension data no structure, so it is opaque by specification rather than undecoded. * Two spec-only value sets go in `mh.py` rather than `pcapkit.const.mh`, since IANA registers neither: `LocalizedRoutingStatus` [RFC 6705] and `LMAAddressCode` [RFC 5949]. No new IANA enumeration or vendor crawler was needed -- every sub-registry was already generated. Fixes two Multi-Prefix CGA extension construction defects found on the way: `_make_ext_multiprefix` declared `1 + len(prefixes) * 16` data octets for a payload of `4 + len(prefixes) * 8`, so two prefixes claimed 33 octets where 20 were emitted and a re-parse overran the extension; and it forwarded the data model's `prefixes` tuple to a `ListField`, which rejects tuples, so a parsed extension could not be re-made at all. Every new message type, option and extension round-trips byte-for-byte, and the 14 sample captures produce identical tree and json output. The CGA Parameters option (type 12) is left on the generic handler and documented: it cannot be parsed on `main` either, and both halves of the fault are in shared field machinery rather than in the mobility header. Suite: 889 passed, 17 skipped, 1064 subtests (was 859/17 with 844 subtests). mypy unchanged at 128 errors in 41 files; pylint errors and fatals unchanged at 78. --- docs/source/pcapkit/protocols/internet/mh.rst | 1121 +- docs/source/pep.rst | 99 +- pcapkit/protocols/data/internet/mh.py | 1388 ++- pcapkit/protocols/internet/mh.py | 9064 ++++++++++++++--- pcapkit/protocols/schema/internet/mh.py | 2303 ++++- tests/protocols/internet/test_mh_unit.py | 650 +- 6 files changed, 13087 insertions(+), 1538 deletions(-) diff --git a/docs/source/pcapkit/protocols/internet/mh.rst b/docs/source/pcapkit/protocols/internet/mh.rst index 44e89b2e8f..a1bc42583d 100644 --- a/docs/source/pcapkit/protocols/internet/mh.rst +++ b/docs/source/pcapkit/protocols/internet/mh.rst @@ -21,7 +21,10 @@ Octets Bits Name Description .. todo:: - Implements extractor for message data of all MH types. + The CGA Parameters option (type 12) is the one registered mobility option + still on the generic handler. It is unreachable rather than unimplemented -- + see the Mobility Header section of :doc:`/pep` for the two faults involved, + both of which are in shared field machinery rather than here. .. autoclass:: pcapkit.protocols.internet.mh.MH :no-members: @@ -57,6 +60,16 @@ Octets Bits Name Description .. automethod:: _read_msg_emh .. automethod:: _read_msg_hi .. automethod:: _read_msg_hack + .. automethod:: _read_msg_brm + .. automethod:: _read_msg_fbm + .. automethod:: _read_msg_has + .. automethod:: _read_msg_hb + .. automethod:: _read_msg_lra + .. automethod:: _read_msg_lri + .. automethod:: _read_msg_sq + .. automethod:: _read_msg_sr + .. automethod:: _read_msg_upa + .. automethod:: _read_msg_upn .. automethod:: _make_msg_unknown .. automethod:: _make_msg_brr @@ -73,8 +86,23 @@ Octets Bits Name Description .. automethod:: _make_msg_emh .. automethod:: _make_msg_hi .. automethod:: _make_msg_hack + .. automethod:: _make_msg_brm + .. automethod:: _make_msg_fbm + .. automethod:: _make_msg_has + .. automethod:: _make_msg_hb + .. automethod:: _make_msg_lra + .. automethod:: _make_msg_lri + .. automethod:: _make_msg_sq + .. automethod:: _make_msg_sr + .. automethod:: _make_msg_upa + .. automethod:: _make_msg_upn .. automethod:: _read_mh_options + .. automethod:: _read_fid_suboptions + .. automethod:: _read_ani_suboptions + .. automethod:: _read_qos_attributes + .. automethod:: _read_lcmp_suboptions + .. automethod:: _decode_signed .. automethod:: _read_opt_none .. automethod:: _read_opt_pad .. automethod:: _read_opt_bra @@ -95,8 +123,68 @@ Octets Bits Name Description .. automethod:: _read_opt_exp .. automethod:: _read_opt_badf .. automethod:: _read_opt_ipv6_ap + .. automethod:: _read_opt_alt_ipv4_coa + .. automethod:: _read_opt_ams4 + .. automethod:: _read_opt_ams6 + .. automethod:: _read_opt_anchored + .. automethod:: _read_opt_ani + .. automethod:: _read_opt_att + .. automethod:: _read_opt_bid + .. automethod:: _read_opt_cr + .. automethod:: _read_opt_dlif_lla + .. automethod:: _read_opt_dlif_lladdr + .. automethod:: _read_opt_dmnp + .. automethod:: _read_opt_dns + .. automethod:: _read_opt_fid + .. automethod:: _read_opt_fs + .. automethod:: _read_opt_gre + .. automethod:: _read_opt_hi + .. automethod:: _read_opt_hnp + .. automethod:: _read_opt_ipv4_ack + .. automethod:: _read_opt_ipv4_coa + .. automethod:: _read_opt_ipv4_dhcp + .. automethod:: _read_opt_ipv4_hoa + .. automethod:: _read_opt_ipv4_hoa_rep + .. automethod:: _read_opt_ipv4_hoa_req + .. automethod:: _read_opt_ipv4_router + .. automethod:: _read_opt_lcmp + .. automethod:: _read_opt_lla_addr + .. automethod:: _read_opt_lma_up + .. automethod:: _read_opt_lmaa + .. automethod:: _read_opt_load + .. automethod:: _read_opt_local_prefix + .. automethod:: _read_opt_mag_addr + .. automethod:: _read_opt_mag_id + .. automethod:: _read_opt_mag_mp + .. automethod:: _read_opt_mcast + .. automethod:: _read_opt_mcast_ack + .. automethod:: _read_opt_mcast_sel + .. automethod:: _read_opt_mn_group + .. automethod:: _read_opt_mn_lla_iid + .. automethod:: _read_opt_mn_lli + .. automethod:: _read_opt_nat + .. automethod:: _read_opt_offload + .. automethod:: _read_opt_prev_maar + .. automethod:: _read_opt_qos + .. automethod:: _read_opt_rc + .. automethod:: _read_opt_redirect + .. automethod:: _read_opt_restart + .. automethod:: _read_opt_serv_maar + .. automethod:: _read_opt_service + .. automethod:: _read_opt_timestamp + .. automethod:: _read_opt_transient + .. automethod:: _read_opt_vendor .. automethod:: _make_mh_options + .. automethod:: _make_fid_suboptions + .. automethod:: _make_fid_suboption + .. automethod:: _make_ani_suboptions + .. automethod:: _make_ani_suboption + .. automethod:: _make_qos_attributes + .. automethod:: _make_qos_attribute + .. automethod:: _make_lcmp_suboptions + .. automethod:: _make_lcmp_suboption + .. automethod:: _seconds .. automethod:: _make_opt_none .. automethod:: _make_opt_pad .. automethod:: _make_opt_bra @@ -117,14 +205,67 @@ Octets Bits Name Description .. automethod:: _make_opt_exp .. automethod:: _make_opt_badf .. automethod:: _make_opt_ipv6_ap + .. automethod:: _make_opt_alt_ipv4_coa + .. automethod:: _make_opt_ams4 + .. automethod:: _make_opt_ams6 + .. automethod:: _make_opt_anchored + .. automethod:: _make_opt_ani + .. automethod:: _make_opt_att + .. automethod:: _make_opt_bid + .. automethod:: _make_opt_cr + .. automethod:: _make_opt_dlif_lla + .. automethod:: _make_opt_dlif_lladdr + .. automethod:: _make_opt_dmnp + .. automethod:: _make_opt_dns + .. automethod:: _make_opt_fid + .. automethod:: _make_opt_fs + .. automethod:: _make_opt_gre + .. automethod:: _make_opt_hi + .. automethod:: _make_opt_hnp + .. automethod:: _make_opt_ipv4_ack + .. automethod:: _make_opt_ipv4_coa + .. automethod:: _make_opt_ipv4_dhcp + .. automethod:: _make_opt_ipv4_hoa + .. automethod:: _make_opt_ipv4_hoa_rep + .. automethod:: _make_opt_ipv4_hoa_req + .. automethod:: _make_opt_ipv4_router + .. automethod:: _make_opt_lcmp + .. automethod:: _make_opt_lla_addr + .. automethod:: _make_opt_lma_up + .. automethod:: _make_opt_lmaa + .. automethod:: _make_opt_load + .. automethod:: _make_opt_local_prefix + .. automethod:: _make_opt_mag_addr + .. automethod:: _make_opt_mag_id + .. automethod:: _make_opt_mag_mp + .. automethod:: _make_opt_mcast + .. automethod:: _make_opt_mcast_ack + .. automethod:: _make_opt_mcast_sel + .. automethod:: _make_opt_mn_group + .. automethod:: _make_opt_mn_lla_iid + .. automethod:: _make_opt_mn_lli + .. automethod:: _make_opt_nat + .. automethod:: _make_opt_offload + .. automethod:: _make_opt_prev_maar + .. automethod:: _make_opt_qos + .. automethod:: _make_opt_rc + .. automethod:: _make_opt_redirect + .. automethod:: _make_opt_restart + .. automethod:: _make_opt_serv_maar + .. automethod:: _make_opt_service + .. automethod:: _make_opt_timestamp + .. automethod:: _make_opt_transient + .. automethod:: _make_opt_vendor .. automethod:: _read_cga_extensions .. automethod:: _read_ext_none .. automethod:: _read_ext_multiprefix + .. automethod:: _read_ext_exp .. automethod:: _make_cga_extensions .. automethod:: _make_ext_none .. automethod:: _make_ext_multiprefix + .. automethod:: _make_ext_exp .. autoattribute:: __message__ :no-value: @@ -163,6 +304,20 @@ Auxiliary Data :undoc-members: :show-inheritance: +.. autoclass:: pcapkit.protocols.internet.mh.PMIPv6Timestamp + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.internet.mh.LocalizedRoutingStatus + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.internet.mh.LMAAddressCode + :members: + :undoc-members: + :show-inheritance: + Header Schemas -------------- @@ -336,225 +491,1051 @@ Header Schemas :members: :show-inheritance: -Type Stubs -~~~~~~~~~~ +.. autoclass:: pcapkit.protocols.schema.internet.mh.HeartbeatMessage + :members: + :show-inheritance: -.. autoclass:: pcapkit.protocols.schema.internet.mh.ANSIKeyLengthTest +.. autoclass:: pcapkit.protocols.schema.internet.mh.HomeAgentSwitchMessage :members: - :undoc-members: :show-inheritance: -.. autoclass:: pcapkit.protocols.schema.internet.mh.MultiPrefixExtensionFlags +.. autoclass:: pcapkit.protocols.schema.internet.mh.BindingRevocationMessage :members: - :undoc-members: :show-inheritance: -.. autoclass:: pcapkit.protocols.schema.internet.mh.BindingUpdateMessageFlags +.. autoclass:: pcapkit.protocols.schema.internet.mh.LocalizedRoutingInitiationMessage :members: - :undoc-members: :show-inheritance: -.. autoclass:: pcapkit.protocols.schema.internet.mh.BindingAcknowledgementMessageFlags +.. autoclass:: pcapkit.protocols.schema.internet.mh.LocalizedRoutingAcknowledgmentMessage :members: - :undoc-members: :show-inheritance: -.. autoclass:: pcapkit.protocols.schema.internet.mh.FastBindingUpdateMessageFlags +.. autoclass:: pcapkit.protocols.schema.internet.mh.UpdateNotificationMessage :members: - :undoc-members: :show-inheritance: -.. autoclass:: pcapkit.protocols.schema.internet.mh.FastBindingAcknowledgmentMessageFlags +.. autoclass:: pcapkit.protocols.schema.internet.mh.UpdateNotificationAcknowledgementMessage :members: - :undoc-members: :show-inheritance: -.. autoclass:: pcapkit.protocols.schema.internet.mh.HandoverInitiateMessageFlags +.. autoclass:: pcapkit.protocols.schema.internet.mh.FlowBindingMessage :members: - :undoc-members: :show-inheritance: -.. autoclass:: pcapkit.protocols.schema.internet.mh.HandoverAcknowledgeMessageFlags +.. autoclass:: pcapkit.protocols.schema.internet.mh.SubscriptionQueryMessage :members: - :undoc-members: :show-inheritance: -Auxiliary Functions -~~~~~~~~~~~~~~~~~~~ +.. autoclass:: pcapkit.protocols.schema.internet.mh.SubscriptionResponseMessage + :members: + :show-inheritance: -.. autofunction:: pcapkit.protocols.schema.internet.mh.mh_data_selector -.. autofunction:: pcapkit.protocols.schema.internet.mh.mn_id_selector -.. autofunction:: pcapkit.protocols.schema.internet.mh.pad_opt_data_len +.. autoclass:: pcapkit.protocols.schema.internet.mh.HomeNetworkPrefixOption + :members: + :show-inheritance: -Data Models ------------ +.. autoclass:: pcapkit.protocols.schema.internet.mh.HandoffIndicatorOption + :members: + :show-inheritance: -.. module:: pcapkit.protocols.data.internet.mh +.. autoclass:: pcapkit.protocols.schema.internet.mh.AccessTechnologyTypeOption + :members: + :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.MH +.. autoclass:: pcapkit.protocols.schema.internet.mh.MNLLIdentifierOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.Option +.. autoclass:: pcapkit.protocols.schema.internet.mh.LinkLocalAddressOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.UnassignedOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.TimestampOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.PadOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.RestartCounterOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.BindingRefreshAdviceOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.DNSUpdateOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.AlternateCareofAddressOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.VendorSpecificOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.NonceIndicesOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.ServiceSelectionOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.AuthorizationDataOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.IPv4HomeAddressOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.MobileNetworkPrefixOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.IPv4AddressAcknowledgementOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.LinkLayerAddressOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.NATDetectionOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.MNIDOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.IPv4CareofAddressOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.AuthOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.GREKeyOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.MesgIDOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.BindingIdentifierOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.CGAParametersRequestOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.IPv4HomeAddressRequestOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.CGAExtension +.. autoclass:: pcapkit.protocols.schema.internet.mh.IPv4HomeAddressReplyOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.CGAParameter +.. autoclass:: pcapkit.protocols.schema.internet.mh.IPv4DefaultRouterAddressOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.CGAParametersOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.IPv4DHCPSupportModeOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.UnknownExtension +.. autoclass:: pcapkit.protocols.schema.internet.mh.ContextRequestOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.MultiPrefixExtension +.. autoclass:: pcapkit.protocols.schema.internet.mh.LMAAddressOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.SignatureOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.MNLLAIIDOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.PermanentHomeKeygenTokenOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.TransientBindingOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.CareofTestInitOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.FlowSummaryOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.CareofTestOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.FlowIdentificationOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.ExperimentalMobilityOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.RedirectCapabilityOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.BADFOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.RedirectOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.IPv6AddressPrefixOption +.. autoclass:: pcapkit.protocols.schema.internet.mh.LoadInformationOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.UnknownMessage +.. autoclass:: pcapkit.protocols.schema.internet.mh.AlternateIPv4CareofAddressOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.BindingRefreshRequestMessage +.. autoclass:: pcapkit.protocols.schema.internet.mh.MNGroupIdentifierOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.HomeTestInitMessage +.. autoclass:: pcapkit.protocols.schema.internet.mh.MAGIPv6AddressOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.CareofTestInitMessage +.. autoclass:: pcapkit.protocols.schema.internet.mh.AccessNetworkIdentifierOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.HomeTestMessage +.. autoclass:: pcapkit.protocols.schema.internet.mh.IPv4TrafficOffloadSelectorOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.CareofTestMessage +.. autoclass:: pcapkit.protocols.schema.internet.mh.DynamicIPMulticastSelectorOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.BindingUpdateMessage +.. autoclass:: pcapkit.protocols.schema.internet.mh.DelegatedMNPOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.BindingAcknowledgementMessage +.. autoclass:: pcapkit.protocols.schema.internet.mh.ActiveMulticastSubscriptionIPv4Option :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.BindingErrorMessage +.. autoclass:: pcapkit.protocols.schema.internet.mh.ActiveMulticastSubscriptionIPv6Option :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.FastBindingUpdateMessage +.. autoclass:: pcapkit.protocols.schema.internet.mh.QualityOfServiceOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.FastBindingAcknowledgmentMessage +.. autoclass:: pcapkit.protocols.schema.internet.mh.LMAUserPlaneAddressOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.FastNeighborAdvertisementMessage +.. autoclass:: pcapkit.protocols.schema.internet.mh.MulticastMobilityOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.ExperimentalMessage +.. autoclass:: pcapkit.protocols.schema.internet.mh.MulticastAcknowledgementOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.HandoverInitiateMessage +.. autoclass:: pcapkit.protocols.schema.internet.mh.LMAControlledMAGParametersOption :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.internet.mh.HandoverAcknowledgeMessage +.. autoclass:: pcapkit.protocols.schema.internet.mh.MAGMultipathBindingOption :members: :show-inheritance: -.. rubric:: Footnotes +.. autoclass:: pcapkit.protocols.schema.internet.mh.MAGIdentifierOption + :members: + :show-inheritance: -.. [*] https://en.wikipedia.org/wiki/Mobile_IP#Changes_in_IPv6_for_Mobile_IPv6 +.. autoclass:: pcapkit.protocols.schema.internet.mh.AnchoredPrefixOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.LocalPrefixOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.PreviousMAAROption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.ServingMAAROption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.DLIFLinkLocalAddressOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.DLIFLinkLayerAddressOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.FlowIdentificationSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.UnassignedFlowIdentificationSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.PadFlowIdentificationSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.BIDReferenceSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.TrafficSelectorSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.FlowBindingActionSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.TargetCareofAddressSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.ANISuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.UnassignedANISuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.ANINetworkIdentifierSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.ANIGeoLocationSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.ANIOperatorIdentifierSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.ANICivicLocationSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.ANIMAGGroupIdentifierSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.ANIUpdateTimerSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.QoSAttribute + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.UnassignedQoSAttribute + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.BitRateAttribute + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.PerSessionBitRateAttribute + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.AllocationRetentionPriorityAttribute + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.QoSTrafficSelectorAttribute + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.QoSVendorSpecificAttribute + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.LMAControlledMAGSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.UnassignedLMAControlledMAGSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.BindingReregistrationControlSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.HeartbeatControlSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.ExperimentalExtension + :members: + :show-inheritance: + +Type Stubs +~~~~~~~~~~ + +.. autoclass:: pcapkit.protocols.schema.internet.mh.ANSIKeyLengthTest + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.MultiPrefixExtensionFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.BindingUpdateMessageFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.BindingAcknowledgementMessageFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.FastBindingUpdateMessageFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.FastBindingAcknowledgmentMessageFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.HandoverInitiateMessageFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.HandoverAcknowledgeMessageFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.HeartbeatMessageFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.FixedPointTimestamp + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.BindingRevocationMessageFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.LocalizedRoutingAcknowledgmentMessageFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.FlowBindingMessageFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.UpdateNotificationMessageFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.SubscriptionResponseMessageFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.DNSUpdateOptionFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.IPv4HomeAddressOptionFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.PrefixLengthOnly + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.NATDetectionOptionFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.IPv4DHCPSupportModeOptionFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.RedirectOptionFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.BindingIdentifierOptionFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.TransientBindingOptionFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.ModeFlagOnly + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.DelegatedMNPOptionFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.MAGMultipathBindingOptionFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.ANINetworkIdentifierFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.GeoLocationDegrees + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.SessionBitRateFlags + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.mh.AllocationRetentionPriorityFields + :members: + :undoc-members: + :show-inheritance: + +Auxiliary Functions +~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: pcapkit.protocols.schema.internet.mh.mh_data_selector +.. autofunction:: pcapkit.protocols.schema.internet.mh.mn_id_selector +.. autofunction:: pcapkit.protocols.schema.internet.mh.pad_opt_data_len +.. autofunction:: pcapkit.protocols.schema.internet.mh.bid_address_selector +.. autofunction:: pcapkit.protocols.schema.internet.mh.br_code_selector +.. autofunction:: pcapkit.protocols.schema.internet.mh.dmnp_prefix_selector +.. autofunction:: pcapkit.protocols.schema.internet.mh.fb_code_selector +.. autofunction:: pcapkit.protocols.schema.internet.mh.lma_address_selector +.. autofunction:: pcapkit.protocols.schema.internet.mh.lma_user_plane_selector +.. autofunction:: pcapkit.protocols.schema.internet.mh.pad_subopt_data_len +.. autofunction:: pcapkit.protocols.schema.internet.mh.target_coa_selector + +Data Models +----------- + +.. module:: pcapkit.protocols.data.internet.mh + +.. autoclass:: pcapkit.protocols.data.internet.mh.MH + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.Option + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.UnassignedOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.PadOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.BindingRefreshAdviceOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.AlternateCareofAddressOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.NonceIndicesOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.AuthorizationDataOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.MobileNetworkPrefixOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.LinkLayerAddressOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.MNIDOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.AuthOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.MesgIDOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.CGAParametersRequestOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.CGAExtension + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.CGAParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.CGAParametersOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.UnknownExtension + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.MultiPrefixExtension + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.SignatureOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.PermanentHomeKeygenTokenOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.CareofTestInitOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.CareofTestOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.ExperimentalMobilityOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.BADFOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.IPv6AddressPrefixOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.UnknownMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.BindingRefreshRequestMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.HomeTestInitMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.CareofTestInitMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.HomeTestMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.CareofTestMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.BindingUpdateMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.BindingAcknowledgementMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.BindingErrorMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.FastBindingUpdateMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.FastBindingAcknowledgmentMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.FastNeighborAdvertisementMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.ExperimentalMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.HandoverInitiateMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.HandoverAcknowledgeMessage + :members: + :show-inheritance: + +.. rubric:: Footnotes + +.. [*] https://en.wikipedia.org/wiki/Mobile_IP#Changes_in_IPv6_for_Mobile_IPv6 + +.. autoclass:: pcapkit.protocols.data.internet.mh.HeartbeatMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.HomeAgentSwitchMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.BindingRevocationMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.LocalizedRoutingInitiationMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.LocalizedRoutingAcknowledgmentMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.UpdateNotificationMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.UpdateNotificationAcknowledgementMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.FlowBindingMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.SubscriptionQueryMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.SubscriptionResponseMessage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.HomeNetworkPrefixOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.HandoffIndicatorOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.AccessTechnologyTypeOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.MNLLIdentifierOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.LinkLocalAddressOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.TimestampOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.RestartCounterOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.DNSUpdateOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.VendorSpecificOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.ServiceSelectionOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.IPv4HomeAddressOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.IPv4AddressAcknowledgementOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.NATDetectionOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.IPv4CareofAddressOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.GREKeyOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.BindingIdentifierOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.IPv4HomeAddressRequestOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.IPv4HomeAddressReplyOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.IPv4DefaultRouterAddressOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.IPv4DHCPSupportModeOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.ContextRequestOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.LMAAddressOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.MNLLAIIDOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.TransientBindingOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.FlowSummaryOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.FlowIdentificationOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.RedirectCapabilityOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.RedirectOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.LoadInformationOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.AlternateIPv4CareofAddressOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.MNGroupIdentifierOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.MAGIPv6AddressOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.AccessNetworkIdentifierOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.IPv4TrafficOffloadSelectorOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.DynamicIPMulticastSelectorOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.DelegatedMNPOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.ActiveMulticastSubscriptionIPv4Option + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.ActiveMulticastSubscriptionIPv6Option + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.QualityOfServiceOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.LMAUserPlaneAddressOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.MulticastMobilityOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.MulticastAcknowledgementOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.LMAControlledMAGParametersOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.MAGMultipathBindingOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.MAGIdentifierOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.AnchoredPrefixOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.LocalPrefixOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.PreviousMAAROption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.ServingMAAROption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.DLIFLinkLocalAddressOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.DLIFLinkLayerAddressOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.ContextRequest + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.FlowIdentificationSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.UnassignedFlowIdentificationSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.PadFlowIdentificationSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.BIDReferenceSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.TrafficSelectorSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.FlowBindingActionSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.TargetCareofAddressSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.ANISuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.UnassignedANISuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.ANINetworkIdentifierSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.ANIGeoLocationSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.ANIOperatorIdentifierSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.ANICivicLocationSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.ANIMAGGroupIdentifierSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.ANIUpdateTimerSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.QoSAttribute + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.UnassignedQoSAttribute + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.BitRateAttribute + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.PerSessionBitRateAttribute + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.AllocationRetentionPriorityAttribute + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.QoSTrafficSelectorAttribute + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.QoSVendorSpecificAttribute + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.LMAControlledMAGSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.UnassignedLMAControlledMAGSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.BindingReregistrationControlSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.HeartbeatControlSuboption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.mh.ExperimentalExtension + :members: + :show-inheritance: diff --git a/docs/source/pep.rst b/docs/source/pep.rst index 8ae5400eaf..a1396aad13 100644 --- a/docs/source/pep.rst +++ b/docs/source/pep.rst @@ -146,42 +146,73 @@ rather than when a packet is read, so nothing decodes wrongly in the meantime. Mobility Header ~~~~~~~~~~~~~~~ -**Partly done**, and still the section of this page with the most work left in -it. :class:`~pcapkit.protocols.internet.mh.MH` has the FMIPv6 fast-handover -messages [:rfc:`5568`] -- Handover Initiate, Handover Acknowledge, FBU, FBack -and FNA -- along with the options they need. What remains is the rest of the -registry: - -* **10 of the 24 registered message data types**, namely Home Agent Switch, - Heartbeat, Binding Revocation, Localized Routing Initiation and - Acknowledgment, Update Notification and its Acknowledgement, Flow Binding, - Subscription Query and Subscription Response. -* **51 of the 71 registered options** -- most of the PMIPv6 and flow-binding - block, including Home Network Prefix, Handoff Indicator, Access Technology - Type, Timestamp, GRE Key, Binding Identifier and the QoS options, together - with DNS-UPDATE-TYPE, Vendor Specific and Service Selection, which belong to - none of those groups. -* **3 of the 4 CGA extensions**; only Multi-Prefix is implemented. - -Each of those falls through to a generic handler, so nothing breaks -- the -fields simply are not decoded. Read and construction are symmetric throughout, -so every gap above is a gap in both directions: a message type needs a -``_read_msg_`` and a ``_make_msg_`` handler, an option a ``_read_opt_`` and a -``_make_opt_``, and each has to be named in the matching dispatch table -- +**Done, bar one option that was already broken.** +:class:`~pcapkit.protocols.internet.mh.MH` now decodes and constructs the whole +registry: **all 24 registered message data types**, **all 4 CGA extensions**, and +**70 of the 71 registered options**. Every one of them is registered in :attr:`~pcapkit.protocols.internet.mh.MH.__message__`, :attr:`~pcapkit.protocols.internet.mh.MH.__option__` or -:attr:`~pcapkit.protocols.internet.mh.MH.__extension__`. The six ``# TODO`` -markers in ``pcapkit/protocols/internet/mh.py`` sit at the end of each handler -block rather than at the tables, so both places need editing; the file documents -the shape each handler takes. - -Less of this is groundwork than the numbers suggest. The sub-registries the -missing options and messages need -- binding revocation types and triggers, -handoff indicators, access network identifier sub-options, flow identification -and flow binding sub-options, LMA-controlled MAG parameters, DNS update status, -traffic selector formats, QoS attributes -- are already generated in full under -:doc:`pcapkit/const/mh`, and ``mh.py`` already imports 32 of them without using -them. What is missing is the handlers, not the enumerations. +:attr:`~pcapkit.protocols.internet.mh.MH.__extension__` with both a ``_read_`` +and a ``_make_`` handler, and every one round-trips byte-for-byte -- +``make`` then ``read`` then ``make`` again reproduces the same octets. + +The sub-registries turned out to be the easy half, as predicted: binding +revocation types and triggers, handoff indicators, access network identifier +sub-options, flow identification and flow binding sub-options, LMA-controlled MAG +parameters, DNS update status, traffic selector formats and QoS attributes were +already generated in full under :doc:`pcapkit/const/mh`, and **no new +enumeration or vendor crawler was needed**. Two value sets did have to be added +to ``mh.py`` itself rather than to :mod:`pcapkit.const.mh`, because IANA +registers neither: the localized routing acknowledgment status codes of +:rfc:`6705#section-10.2` +(:class:`~pcapkit.protocols.internet.mh.LocalizedRoutingStatus`) and the local +mobility anchor address option codes of :rfc:`5949#section-6.2.2` +(:class:`~pcapkit.protocols.internet.mh.LMAAddressCode`), alongside the two +:rfc:`5568` sets that were already there. + +What is left, and why: + +* **The CGA Parameters option** (type 12) is the one option still on the generic + handler, and it is unreachable rather than unimplemented: + :attr:`~pcapkit.protocols.schema.internet.mh.CGAParameter.extensions` sizes + itself from ``pkt['length']``, but :class:`CGAParameter + ` has no ``length`` field + and :class:`~pcapkit.corekit.fields.misc.SchemaField` gives a nested schema a + fresh packet context rather than the enclosing option's, so a well-formed + option raises ``KeyError: 'length'`` on parse and on construction alike. Making + the lookup optional gets past that and straight into a second fault, in how a + :class:`~pcapkit.corekit.fields.misc.ForwardMatchField` counts towards the + nested schema's length. Both halves live in shared field machinery rather than + in the mobility header, which is why this is recorded here rather than patched + around; ``test_mh_cga_parameters_option_is_unparsable_upstream`` pins the + current behaviour so the fix is noticed. +* **Payloads that belong to another protocol** are carried opaquely, deliberately. + The multicast options (54, 56, 57, 60 and 61) embed :rfc:`3810` MLD or + :rfc:`3376` IGMP address records, and the traffic selectors of :rfc:`6089` and + :rfc:`7222` embed the flag-driven range lists of :rfc:`6088`. Both are separate + registries with their own dissectors' worth of structure; the mobility options + around them are fully decoded, and each records which format its payload is in. +* **The MN-ID option's constructor mis-sizes a non-address identifier.** + ``_make_opt_mn_id`` measures ``len(identifier)`` even for the ``IPv6_Address`` + subtype, so passing a string or an integer declares that many octets while the + schema emits 16. Passing an :class:`~ipaddress.IPv6Address` is correct. This is + pre-existing and outside the registry-completion work, so it is noted rather + than fixed. + +Two wire-format traps are worth knowing before touching this code, since both +look like ordinary fields and are not: + +* :rfc:`7411`'s two multicast options measure their length field in **32-bit + words**, and exclude the option-code and status octets as well as the type and + length ones -- so the option occupies ``4 + length * 4`` octets, not + ``length + 2``. +* :rfc:`5213#section-8.8`'s timestamp is **not** an :rfc:`1305` NTP timestamp, + though the mobility header carries both. It counts from the UNIX epoch in a + 48/16 fixed-point split, where NTP counts from 1900 in a 32/32 one, so reading + one as the other is wrong in the epoch and in both field widths. They have + separate types for that reason: + :class:`~pcapkit.protocols.internet.mh.PMIPv6Timestamp` and + :class:`~pcapkit.protocols.internet.mh.NTPTimestamp`. DTLS ~~~~ diff --git a/pcapkit/protocols/data/internet/mh.py b/pcapkit/protocols/data/internet/mh.py index af2a93f7ab..af2bd7dbc0 100644 --- a/pcapkit/protocols/data/internet/mh.py +++ b/pcapkit/protocols/data/internet/mh.py @@ -10,7 +10,8 @@ if TYPE_CHECKING: from datetime import datetime as dt_type from datetime import timedelta - from ipaddress import IPv6Address, IPv6Network + from ipaddress import IPv4Address, IPv6Address, IPv6Network + from typing import Optional from pcapkit.const.mh.access_type import AccessType as Enum_AccessType from pcapkit.const.mh.ack_status_code import ACKStatusCode as Enum_ACKStatusCode @@ -65,7 +66,9 @@ from pcapkit.const.reg.transtype import TransType from pcapkit.corekit.multidict import OrderedMultiDict from pcapkit.protocols.internet.mh import (FastBindingAcknowledgmentStatus, - IPv6AddressPrefixCode, NTPTimestamp) + IPv6AddressPrefixCode, LMAAddressCode, + LocalizedRoutingStatus, NTPTimestamp, + PMIPv6Timestamp) __all__ = [ 'MH', @@ -73,7 +76,11 @@ 'HomeTestMessage', 'CareofTestMessage', 'BindingUpdateMessage', 'BindingAcknowledgementMessage', 'BindingErrorMessage', 'FastBindingUpdateMessage', 'FastBindingAcknowledgmentMessage', 'FastNeighborAdvertisementMessage', 'ExperimentalMessage', 'HandoverInitiateMessage', - 'HandoverAcknowledgeMessage', + 'HandoverAcknowledgeMessage', 'HeartbeatMessage', 'HomeAgentSwitchMessage', + 'BindingRevocationMessage', 'LocalizedRoutingInitiationMessage', + 'LocalizedRoutingAcknowledgmentMessage', 'UpdateNotificationMessage', + 'UpdateNotificationAcknowledgementMessage', 'FlowBindingMessage', + 'SubscriptionQueryMessage', 'SubscriptionResponseMessage', 'Option', 'UnassignedOption', 'PadOption', 'BindingRefreshAdviceOption', 'AlternateCareofAddressOption', @@ -81,11 +88,50 @@ 'LinkLayerAddressOption', 'MNIDOption', 'AuthOption', 'MesgIDOption', 'CGAParametersRequestOption', 'CGAParametersOption', 'SignatureOption', 'PermanentHomeKeygenTokenOption', 'CareofTestInitOption', 'CareofTestOption', 'ExperimentalMobilityOption', 'BADFOption', 'IPv6AddressPrefixOption', + 'HomeNetworkPrefixOption', 'HandoffIndicatorOption', 'AccessTechnologyTypeOption', + 'MNLLIdentifierOption', 'LinkLocalAddressOption', 'TimestampOption', 'RestartCounterOption', + 'DNSUpdateOption', 'VendorSpecificOption', 'ServiceSelectionOption', + 'IPv4HomeAddressOption', 'IPv4AddressAcknowledgementOption', 'NATDetectionOption', + 'IPv4CareofAddressOption', 'GREKeyOption', 'BindingIdentifierOption', + 'IPv4HomeAddressRequestOption', 'IPv4HomeAddressReplyOption', + 'IPv4DefaultRouterAddressOption', 'IPv4DHCPSupportModeOption', 'ContextRequestOption', + 'LMAAddressOption', 'MNLLAIIDOption', 'TransientBindingOption', 'FlowSummaryOption', + 'FlowIdentificationOption', 'RedirectCapabilityOption', 'RedirectOption', + 'LoadInformationOption', 'AlternateIPv4CareofAddressOption', 'MNGroupIdentifierOption', + 'MAGIPv6AddressOption', 'AccessNetworkIdentifierOption', 'IPv4TrafficOffloadSelectorOption', + 'DynamicIPMulticastSelectorOption', 'DelegatedMNPOption', + 'ActiveMulticastSubscriptionIPv4Option', 'ActiveMulticastSubscriptionIPv6Option', + 'QualityOfServiceOption', 'LMAUserPlaneAddressOption', 'MulticastMobilityOption', + 'MulticastAcknowledgementOption', 'LMAControlledMAGParametersOption', + 'MAGMultipathBindingOption', 'MAGIdentifierOption', 'AnchoredPrefixOption', + 'LocalPrefixOption', 'PreviousMAAROption', 'ServingMAAROption', + 'DLIFLinkLocalAddressOption', 'DLIFLinkLayerAddressOption', + + 'ContextRequest', + + 'FlowIdentificationSuboption', + 'UnassignedFlowIdentificationSuboption', 'PadFlowIdentificationSuboption', + 'BIDReferenceSuboption', 'TrafficSelectorSuboption', 'FlowBindingActionSuboption', + 'TargetCareofAddressSuboption', + + 'ANISuboption', + 'UnassignedANISuboption', 'ANINetworkIdentifierSuboption', 'ANIGeoLocationSuboption', + 'ANIOperatorIdentifierSuboption', 'ANICivicLocationSuboption', + 'ANIMAGGroupIdentifierSuboption', 'ANIUpdateTimerSuboption', + + 'QoSAttribute', + 'UnassignedQoSAttribute', 'BitRateAttribute', 'PerSessionBitRateAttribute', + 'AllocationRetentionPriorityAttribute', 'QoSTrafficSelectorAttribute', + 'QoSVendorSpecificAttribute', + + 'LMAControlledMAGSuboption', + 'UnassignedLMAControlledMAGSuboption', 'BindingReregistrationControlSuboption', + 'HeartbeatControlSuboption', 'CGAParameter', 'CGAExtension', - 'UnknownExtension', 'MultiPrefixExtension', + 'UnknownExtension', 'MultiPrefixExtension', 'ExperimentalExtension', ] @@ -380,6 +426,222 @@ def __init__(self, next: 'TransType', length: 'int', type: 'Packet', chksum: 'by options: 'OrderedMultiDict[Enum_Option, Option]') -> 'None': ... +@info_final +class HeartbeatMessage(MH): + """Data model for MH Heartbeat message type.""" + + #: Unsolicited flag. Set in an unsolicited Heartbeat response, in which case + #: :attr:`seq` is ignored and no response is expected. + unsolicited: 'bool' + #: Response flag. Unset for a Heartbeat request, set for a response. + response: 'bool' + #: Sequence number. + seq: 'int' + #: Mobility options. + options: 'OrderedMultiDict[Enum_Option, Option]' + + if TYPE_CHECKING: + def __init__(self, next: 'TransType', length: 'int', type: 'Packet', chksum: 'bytes', + unsolicited: 'bool', response: 'bool', seq: 'int', + options: 'OrderedMultiDict[Enum_Option, Option]') -> 'None': ... + + +@info_final +class HomeAgentSwitchMessage(MH): + """Data model for MH Home Agent Switch message type.""" + + #: Alternate home agent addresses. An empty tuple asks the mobile node to + #: run home agent discovery instead. + addresses: 'tuple[IPv6Address, ...]' + #: Mobility options. + options: 'OrderedMultiDict[Enum_Option, Option]' + + if TYPE_CHECKING: + def __init__(self, next: 'TransType', length: 'int', type: 'Packet', chksum: 'bytes', + addresses: 'tuple[IPv6Address, ...]', + options: 'OrderedMultiDict[Enum_Option, Option]') -> 'None': ... + + +@info_final +class BindingRevocationMessage(MH): + """Data model for MH Binding Revocation message type. + + Both forms of the message -- the Binding Revocation Indication and the + Binding Revocation Acknowledgement -- share this model, since they share a + Mobility Header type and differ only in :attr:`br_type` and in what + :attr:`code` means. + """ + + #: Binding revocation type, saying which form of the message this is. + br_type: 'Enum_BindingRevocation' + #: Revocation trigger in an indication, acknowledgement status in an + #: acknowledgement. The two draw from different registries, which is why the + #: field is not split in two. + code: 'Enum_RevocationTrigger | Enum_RevocationStatusCode' + #: Sequence number. + seq: 'int' + #: Proxy binding flag. + proxy: 'bool' + #: IPv4 home address binding only flag. + ipv4_hoa: 'bool' + #: Global revocation flag. + global_revocation: 'bool' + #: Mobility options. + options: 'OrderedMultiDict[Enum_Option, Option]' + + if TYPE_CHECKING: + def __init__(self, next: 'TransType', length: 'int', type: 'Packet', chksum: 'bytes', + br_type: 'Enum_BindingRevocation', + code: 'Enum_RevocationTrigger | Enum_RevocationStatusCode', seq: 'int', + proxy: 'bool', ipv4_hoa: 'bool', global_revocation: 'bool', + options: 'OrderedMultiDict[Enum_Option, Option]') -> 'None': ... + + +@info_final +class LocalizedRoutingInitiationMessage(MH): + """Data model for MH Localized Routing Initiation message type.""" + + #: Sequence number. + seq: 'int' + #: Lifetime. :rfc:`6705#section-10.1` counts this in seconds rather than in + #: the units of 4 seconds the binding messages use. + lifetime: 'timedelta' + #: Mobility options. + options: 'OrderedMultiDict[Enum_Option, Option]' + + if TYPE_CHECKING: + def __init__(self, next: 'TransType', length: 'int', type: 'Packet', chksum: 'bytes', + seq: 'int', lifetime: 'timedelta', + options: 'OrderedMultiDict[Enum_Option, Option]') -> 'None': ... + + +@info_final +class LocalizedRoutingAcknowledgmentMessage(MH): + """Data model for MH Localized Routing Acknowledgment message type.""" + + #: Sequence number. + seq: 'int' + #: Unsolicited flag. + unsolicited: 'bool' + #: Status. :rfc:`6705#section-10.2` defines its values inline with no IANA + #: registry behind them, so this field carries the module-local + #: :class:`~pcapkit.protocols.internet.mh.LocalizedRoutingStatus` rather than + #: :class:`~pcapkit.const.mh.status_code.StatusCode`. + status: 'LocalizedRoutingStatus' + #: Lifetime, in seconds. + lifetime: 'timedelta' + #: Mobility options. + options: 'OrderedMultiDict[Enum_Option, Option]' + + if TYPE_CHECKING: + def __init__(self, next: 'TransType', length: 'int', type: 'Packet', chksum: 'bytes', + seq: 'int', unsolicited: 'bool', status: 'LocalizedRoutingStatus', + lifetime: 'timedelta', + options: 'OrderedMultiDict[Enum_Option, Option]') -> 'None': ... + + +@info_final +class UpdateNotificationMessage(MH): + """Data model for MH Update Notification message type.""" + + #: Sequence number. + seq: 'int' + #: Notification reason. + reason: 'Enum_UpdateNotificationReason' + #: Acknowledgement requested flag. + ack: 'bool' + #: Retransmission flag. + retransmit: 'bool' + #: Mobility options. + options: 'OrderedMultiDict[Enum_Option, Option]' + + if TYPE_CHECKING: + def __init__(self, next: 'TransType', length: 'int', type: 'Packet', chksum: 'bytes', + seq: 'int', reason: 'Enum_UpdateNotificationReason', ack: 'bool', + retransmit: 'bool', + options: 'OrderedMultiDict[Enum_Option, Option]') -> 'None': ... + + +@info_final +class UpdateNotificationAcknowledgementMessage(MH): + """Data model for MH Update Notification Acknowledgement message type.""" + + #: Sequence number. + seq: 'int' + #: Status. + status: 'Enum_UpdateNotificationACKStatus' + #: Mobility options. + options: 'OrderedMultiDict[Enum_Option, Option]' + + if TYPE_CHECKING: + def __init__(self, next: 'TransType', length: 'int', type: 'Packet', chksum: 'bytes', + seq: 'int', status: 'Enum_UpdateNotificationACKStatus', + options: 'OrderedMultiDict[Enum_Option, Option]') -> 'None': ... + + +@info_final +class FlowBindingMessage(MH): + """Data model for MH Flow Binding message type. + + Both forms of the message -- the Flow Binding Indication and the Flow Binding + Acknowledgement -- share this model, since they share a Mobility Header type + and differ only in :attr:`fb_type` and in what :attr:`code` means. + """ + + #: Flow binding type, saying which form of the message this is. + fb_type: 'Enum_FlowBindingType' + #: Sequence number. + seq: 'int' + #: Indication trigger in an indication, acknowledgement status in an + #: acknowledgement. + code: 'Enum_FlowBindingIndicationTrigger | Enum_FlowBindingACKStatus' + #: Acknowledgement requested flag. Only meaningful in an indication; an + #: acknowledgement reserves the whole octet this bit is taken from. + ack: 'bool' + #: Mobility options. + options: 'OrderedMultiDict[Enum_Option, Option]' + + if TYPE_CHECKING: + def __init__(self, next: 'TransType', length: 'int', type: 'Packet', chksum: 'bytes', + fb_type: 'Enum_FlowBindingType', seq: 'int', + code: 'Enum_FlowBindingIndicationTrigger | Enum_FlowBindingACKStatus', + ack: 'bool', + options: 'OrderedMultiDict[Enum_Option, Option]') -> 'None': ... + + +@info_final +class SubscriptionQueryMessage(MH): + """Data model for MH Subscription Query message type.""" + + #: Sequence number, counted modulo 256. + seq: 'int' + #: Mobility options. + options: 'OrderedMultiDict[Enum_Option, Option]' + + if TYPE_CHECKING: + def __init__(self, next: 'TransType', length: 'int', type: 'Packet', chksum: 'bytes', + seq: 'int', + options: 'OrderedMultiDict[Enum_Option, Option]') -> 'None': ... + + +@info_final +class SubscriptionResponseMessage(MH): + """Data model for MH Subscription Response message type.""" + + #: Sequence number, echoed from the query. + seq: 'int' + #: Multicast information flag, set when the response carries active + #: multicast subscription options. + info: 'bool' + #: Mobility options. + options: 'OrderedMultiDict[Enum_Option, Option]' + + if TYPE_CHECKING: + def __init__(self, next: 'TransType', length: 'int', type: 'Packet', chksum: 'bytes', + seq: 'int', info: 'bool', + options: 'OrderedMultiDict[Enum_Option, Option]') -> 'None': ... + + # TODO: Implement other message types. @@ -550,6 +812,22 @@ class UnknownExtension(CGAExtension): def __init__(self, type: 'Enum_CGAExtension', length: 'int', data: 'bytes') -> 'None': ... +@info_final +class ExperimentalExtension(CGAExtension): + """Data model for experimental CGA extensions. + + :rfc:`4581#section-3` assigns the three experimental extension types but + gives their extension data no structure at all, so :attr:`data` is opaque by + specification rather than merely undecoded. + """ + + #: Extension data. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_CGAExtension', length: 'int', data: 'bytes') -> 'None': ... + + @info_final class MultiPrefixExtension(CGAExtension): """Data model for Multi-Prefix CGA extension.""" @@ -681,4 +959,1106 @@ def __init__(self, type: 'Enum_Option', length: 'int', code: 'IPv6AddressPrefixC prefix_length: 'int', address: 'IPv6Address') -> 'None': ... +@info_final +class HomeNetworkPrefixOption(Option): + """Data model for MH Home Network Prefix option.""" + + #: Prefix length. + prefix_length: 'int' + #: Home network prefix. Kept apart from :attr:`prefix_length` rather than + #: fused into an :class:`~ipaddress.IPv6Network`, so that a prefix whose bits + #: past :attr:`prefix_length` are non-zero survives a round trip -- masking + #: them off, which is what building a network does, would not reproduce the + #: octets that were parsed. Same reasoning as + #: :class:`IPv6AddressPrefixOption`. + prefix: 'IPv6Address' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', prefix_length: 'int', + prefix: 'IPv6Address') -> 'None': ... + + +@info_final +class HandoffIndicatorOption(Option): + """Data model for MH Handoff Indicator option.""" + + #: Handoff indicator. + hi: 'Enum_HandoffType' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', hi: 'Enum_HandoffType') -> 'None': ... + + +@info_final +class AccessTechnologyTypeOption(Option): + """Data model for MH Access Technology Type option.""" + + #: Access technology type. + att: 'Enum_AccessType' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', att: 'Enum_AccessType') -> 'None': ... + + +@info_final +class MNLLIdentifierOption(Option): + """Data model for MH Mobile Node Link-layer Identifier option.""" + + #: Link-layer identifier. + lli: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', lli: 'bytes') -> 'None': ... + + +@info_final +class LinkLocalAddressOption(Option): + """Data model for MH Link-local Address option.""" + + #: Link-local address. + address: 'IPv6Address' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', address: 'IPv6Address') -> 'None': ... + + +@info_final +class TimestampOption(Option): + """Data model for MH Timestamp option.""" + + #: Timestamp. + timestamp: 'dt_type' + #: Timestamp in the fixed-point format of :rfc:`5213#section-8.8`, kept + #: alongside :attr:`timestamp` so that the 1/65536-second resolution of the + #: wire format survives a round trip. + pmip_timestamp: 'PMIPv6Timestamp' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', timestamp: 'dt_type', + pmip_timestamp: 'PMIPv6Timestamp') -> 'None': ... + + +@info_final +class RestartCounterOption(Option): + """Data model for MH Restart Counter option.""" + + #: Restart counter. + counter: 'int' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', counter: 'int') -> 'None': ... + + +@info_final +class DNSUpdateOption(Option): + """Data model for MH DNS-UPDATE-TYPE option.""" + + #: Status. :rfc:`5026#section-8.1` draws this from a registry of its own + #: rather than from the general mobility status codes. + status: 'Enum_DNSStatusCode' + #: Remove flag. Set when the mobile node asks the home agent to remove the + #: DNS entry rather than to create or update it. + remove: 'bool' + #: Mobile node identity, in FQDN form. Kept as :obj:`bytes` because the RFC + #: does not say which of the two FQDN encodings is meant. + identity: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', status: 'Enum_DNSStatusCode', + remove: 'bool', identity: 'bytes') -> 'None': ... + + +@info_final +class VendorSpecificOption(Option): + """Data model for MH Vendor Specific mobility option.""" + + #: Vendor ID, an SMI Network Management Private Enterprise Number. + vendor: 'int' + #: Vendor-administered sub-type. + subtype: 'int' + #: Vendor-specific data. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', vendor: 'int', subtype: 'int', + data: 'bytes') -> 'None': ... + + +@info_final +class ServiceSelectionOption(Option): + """Data model for MH Service Selection mobility option.""" + + #: Service identifier. + identifier: 'str' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', identifier: 'str') -> 'None': ... + + +@info_final +class IPv4HomeAddressOption(Option): + """Data model for MH IPv4 Home Address option.""" + + #: Prefix length. ``32`` means the option carries a single address rather + #: than a prefix; ``0`` is invalid. + prefix_length: 'int' + #: IPv4 home address. ``0.0.0.0`` asks the home agent to allocate one + #: dynamically. Kept apart from :attr:`prefix_length` so that the host bits, + #: which are significant here, survive a round trip. + address: 'IPv4Address' + #: Mobile network prefix request flag. + request_prefix: 'bool' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', prefix_length: 'int', + address: 'IPv4Address', request_prefix: 'bool') -> 'None': ... + + +@info_final +class IPv4AddressAcknowledgementOption(Option): + """Data model for MH IPv4 Address Acknowledgement option.""" + + #: Status. :rfc:`5555#section-3.2.1` draws this from the DSMIPv6 IPv4 home + #: address registry, which is **not** the one the IPv4 Home Address Reply + #: option of :rfc:`5844` uses -- that one lacks value 133. + status: 'Enum_DSMIPv6HomeAddress' + #: Allocated prefix length. Meaningful only on success, and required to be + #: zero on failure. + prefix_length: 'int' + #: Assigned IPv4 home address. + address: 'IPv4Address' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + status: 'Enum_DSMIPv6HomeAddress', prefix_length: 'int', + address: 'IPv4Address') -> 'None': ... + + +@info_final +class NATDetectionOption(Option): + """Data model for MH NAT Detection option.""" + + #: Force UDP encapsulation flag. + force: 'bool' + #: Suggested NAT binding refresh interval, in seconds. ``0`` means the value + #: is to be ignored, and all ones that no keep-alives are needed. + refresh: 'timedelta' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', force: 'bool', + refresh: 'timedelta') -> 'None': ... + + +@info_final +class IPv4CareofAddressOption(Option): + """Data model for MH IPv4 Care-of Address option.""" + + #: IPv4 care-of address. + address: 'IPv4Address' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', address: 'IPv4Address') -> 'None': ... + + +@info_final +class GREKeyOption(Option): + """Data model for MH GRE Key option.""" + + #: GRE key identifier, or :obj:`None` when the option carries none -- which + #: :rfc:`5845#section-6.1` signals by an option length of 2 rather than 6. + key: 'Optional[int]' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + key: 'Optional[int]') -> 'None': ... + + +@info_final +class BindingIdentifierOption(Option): + """Data model for MH Binding Identifier (BID) option.""" + + #: Binding identifier. + bid: 'int' + #: Status, overriding the message status for this binding alone. + status: 'Enum_StatusCode' + #: Simultaneous home and foreign binding flag. + simultaneous: 'bool' + #: Binding priority. :rfc:`6089#section-4.1` renames the bits :rfc:`5648` + #: reserved; ``0`` means the sender predates :rfc:`6089`. + bid_pri: 'int' + #: Care-of address, or :obj:`None` when the option carries none. + address: 'Optional[IPv4Address | IPv6Address]' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', bid: 'int', + status: 'Enum_StatusCode', simultaneous: 'bool', bid_pri: 'int', + address: 'Optional[IPv4Address | IPv6Address]') -> 'None': ... + + +@info_final +class IPv4HomeAddressRequestOption(Option): + """Data model for MH IPv4 Home Address Request option.""" + + #: Prefix length of the requested home network. + prefix_length: 'int' + #: Requested IPv4 home address. ``0.0.0.0`` asks the local mobility anchor to + #: allocate one. + address: 'IPv4Address' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', prefix_length: 'int', + address: 'IPv4Address') -> 'None': ... + + +@info_final +class IPv4HomeAddressReplyOption(Option): + """Data model for MH IPv4 Home Address Reply option.""" + + #: Status. :rfc:`5844#section-3.3.2` draws this from a registry of its own, + #: which unlike the DSMIPv6 one of :rfc:`5555` has no value 133. + status: 'Enum_HomeAddressReply' + #: Prefix length of the assigned home network. + prefix_length: 'int' + #: Assigned IPv4 home address. + address: 'IPv4Address' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', status: 'Enum_HomeAddressReply', + prefix_length: 'int', address: 'IPv4Address') -> 'None': ... + + +@info_final +class IPv4DefaultRouterAddressOption(Option): + """Data model for MH IPv4 Default-Router Address option.""" + + #: IPv4 default-router address. + address: 'IPv4Address' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', address: 'IPv4Address') -> 'None': ... + + +@info_final +class IPv4DHCPSupportModeOption(Option): + """Data model for MH IPv4 DHCP Support Mode option.""" + + #: DHCP support mode. Unset means the mobile access gateway acts as a DHCP + #: relay, set that it acts as a DHCP server. + mode: 'Enum_DHCPSupportMode' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + mode: 'Enum_DHCPSupportMode') -> 'None': ... + + +@info_final +class ContextRequest(Data): + """Data model for one entry of an MH Context Request option.""" + + #: Requested mobility option type. + type: 'Enum_Option' + #: Length of :attr:`value`. + length: 'int' + #: Extra data identifying the requested context. + value: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', value: 'bytes') -> 'None': ... + + +@info_final +class ContextRequestOption(Option): + """Data model for MH Context Request option.""" + + #: Requested contexts. + requests: 'tuple[ContextRequest, ...]' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + requests: 'tuple[ContextRequest, ...]') -> 'None': ... + + +@info_final +class LMAAddressOption(Option): + """Data model for MH Local Mobility Anchor Address option.""" + + #: Option code, c.f., :rfc:`5949#section-6.2.2`. The codes are defined inline + #: by the RFC with no IANA registry behind them, so this field carries the + #: module-local :class:`~pcapkit.protocols.internet.mh.LMAAddressCode`. + code: 'LMAAddressCode' + #: Local mobility anchor address. + address: 'IPv4Address | IPv6Address | bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', code: 'LMAAddressCode', + address: 'IPv4Address | IPv6Address | bytes') -> 'None': ... + + +@info_final +class MNLLAIIDOption(Option): + """Data model for MH Mobile Node Link-local Address Interface Identifier option.""" + + #: Interface identifier. + iid: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', iid: 'bytes') -> 'None': ... + + +@info_final +class TransientBindingOption(Option): + """Data model for MH Transient Binding option.""" + + #: Late path switch flag. + late: 'bool' + #: Maximum lifetime of the transient state. Counted on the wire in units of + #: 100 milliseconds. + lifetime: 'timedelta' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', late: 'bool', + lifetime: 'timedelta') -> 'None': ... + + +@info_final +class FlowSummaryOption(Option): + """Data model for MH Flow Summary mobility option.""" + + #: Flow identifiers being refreshed. + fid: 'tuple[int, ...]' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + fid: 'tuple[int, ...]') -> 'None': ... + + +class FlowIdentificationSuboption(Data): + """Data model for MH Flow Identification sub-options.""" + + #: Sub-option type. + type: 'Enum_FlowIDSuboption' + #: Sub-option length (incl. type and length fields). + length: 'int' + + +@info_final +class UnassignedFlowIdentificationSuboption(FlowIdentificationSuboption): + """Data model for unassigned MH Flow Identification sub-options.""" + + #: Sub-option data. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_FlowIDSuboption', length: 'int', + data: 'bytes') -> 'None': ... + + +@info_final +class PadFlowIdentificationSuboption(FlowIdentificationSuboption): + """Data model for MH Flow Identification padding sub-options.""" + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_FlowIDSuboption', length: 'int') -> 'None': ... + + +@info_final +class BIDReferenceSuboption(FlowIdentificationSuboption): + """Data model for MH Flow Identification BID Reference sub-option.""" + + #: Referenced binding identifiers. + bid: 'tuple[int, ...]' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_FlowIDSuboption', length: 'int', + bid: 'tuple[int, ...]') -> 'None': ... + + +@info_final +class TrafficSelectorSuboption(FlowIdentificationSuboption): + """Data model for MH Flow Identification Traffic Selector sub-option.""" + + #: Traffic selector format. + ts_format: 'Enum_TrafficSelector' + #: Traffic selector. Its interior is :rfc:`6088` territory and belongs to the + #: separate traffic selector format registry, so it is kept opaque. + selector: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_FlowIDSuboption', length: 'int', + ts_format: 'Enum_TrafficSelector', selector: 'bytes') -> 'None': ... + + +@info_final +class FlowBindingActionSuboption(FlowIdentificationSuboption): + """Data model for MH Flow Identification Flow Binding Action sub-option.""" + + #: Flow binding action. + action: 'Enum_FlowBindingAction' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_FlowIDSuboption', length: 'int', + action: 'Enum_FlowBindingAction') -> 'None': ... + + +@info_final +class TargetCareofAddressSuboption(FlowIdentificationSuboption): + """Data model for MH Flow Identification Target Care-of Address sub-option.""" + + #: Target care-of address. + address: 'IPv4Address | IPv6Address | bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_FlowIDSuboption', length: 'int', + address: 'IPv4Address | IPv6Address | bytes') -> 'None': ... + + +@info_final +class FlowIdentificationOption(Option): + """Data model for MH Flow Identification mobility option.""" + + #: Flow identifier. + fid: 'int' + #: Flow priority. + fid_pri: 'int' + #: Status. + status: 'Enum_FlowIDStatus' + #: Sub-options. + suboptions: 'OrderedMultiDict[Enum_FlowIDSuboption, FlowIdentificationSuboption]' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', fid: 'int', fid_pri: 'int', + status: 'Enum_FlowIDStatus', + suboptions: 'OrderedMultiDict[Enum_FlowIDSuboption, FlowIdentificationSuboption]') -> 'None': ... + + +@info_final +class RedirectCapabilityOption(Option): + """Data model for MH Redirect-Capability mobility option.""" + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int') -> 'None': ... + + +@info_final +class RedirectOption(Option): + """Data model for MH Redirect mobility option.""" + + #: IPv6 address of the redirected-to local mobility anchor, if carried. + ipv6: 'Optional[IPv6Address]' + #: IPv4 address of the redirected-to local mobility anchor, if carried. + ipv4: 'Optional[IPv4Address]' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', ipv6: 'Optional[IPv6Address]', + ipv4: 'Optional[IPv4Address]') -> 'None': ... + + +@info_final +class LoadInformationOption(Option): + """Data model for MH Load Information mobility option.""" + + #: Priority; a lower value is a higher priority. + priority: 'int' + #: Mobility sessions currently in use. + sessions_in_use: 'int' + #: Maximum number of mobility sessions accepted. + max_sessions: 'int' + #: Used capacity, in kilobytes per second. + used_capacity: 'int' + #: Maximum capacity, in kilobytes per second. + max_capacity: 'int' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', priority: 'int', + sessions_in_use: 'int', max_sessions: 'int', used_capacity: 'int', + max_capacity: 'int') -> 'None': ... + + +@info_final +class AlternateIPv4CareofAddressOption(Option): + """Data model for MH Alternate IPv4 Care-of Address option.""" + + #: Alternate IPv4 care-of address. + address: 'IPv4Address' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', address: 'IPv4Address') -> 'None': ... + + +@info_final +class MNGroupIdentifierOption(Option): + """Data model for MH Mobile Node Group Identifier option.""" + + #: Sub-type. + subtype: 'Enum_MNGroupID' + #: Mobile node group identifier. ``1`` is the default "all sessions" group. + group_id: 'int' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', subtype: 'Enum_MNGroupID', + group_id: 'int') -> 'None': ... + + +@info_final +class MAGIPv6AddressOption(Option): + """Data model for MH MAG IPv6 Address option.""" + + #: Address length, in bits; always 128. + address_length: 'int' + #: MAG IPv6 address. + address: 'IPv6Address' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', address_length: 'int', + address: 'IPv6Address') -> 'None': ... + + +class ANISuboption(Data): + """Data model for MH Access Network Identifier sub-options.""" + + #: Sub-option type. + type: 'Enum_ANISuboption' + #: Sub-option length (incl. type and length fields). + length: 'int' + + +@info_final +class UnassignedANISuboption(ANISuboption): + """Data model for unassigned MH Access Network Identifier sub-options.""" + + #: Sub-option data. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_ANISuboption', length: 'int', data: 'bytes') -> 'None': ... + + +@info_final +class ANINetworkIdentifierSuboption(ANISuboption): + """Data model for MH Network-Identifier ANI sub-option.""" + + #: UTF-8 encoding flag for :attr:`net_name`. + utf8: 'bool' + #: Network name, e.g. an SSID or a PLMN identifier. + net_name: 'bytes' + #: Access-point name. + ap_name: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_ANISuboption', length: 'int', utf8: 'bool', + net_name: 'bytes', ap_name: 'bytes') -> 'None': ... + + +@info_final +class ANIGeoLocationSuboption(ANISuboption): + """Data model for MH Geo-Location ANI sub-option.""" + + #: Latitude, in degrees. + latitude: 'float' + #: Longitude, in degrees. + longitude: 'float' + #: Latitude as the signed 24-bit fixed-point integer on the wire, kept so + #: that the exact value survives a round trip. + raw_latitude: 'int' + #: Longitude as the signed 24-bit fixed-point integer on the wire. + raw_longitude: 'int' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_ANISuboption', length: 'int', latitude: 'float', + longitude: 'float', raw_latitude: 'int', + raw_longitude: 'int') -> 'None': ... + + +@info_final +class ANIOperatorIdentifierSuboption(ANISuboption): + """Data model for MH Operator-Identifier ANI sub-option.""" + + #: Operator identifier type. + op_id_type: 'Enum_OperatorID' + #: Operator identifier. + identifier: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_ANISuboption', length: 'int', + op_id_type: 'Enum_OperatorID', identifier: 'bytes') -> 'None': ... + + +@info_final +class ANICivicLocationSuboption(ANISuboption): + """Data model for MH Civic-Location ANI sub-option.""" + + #: Encoding format; only ``0``, the binary encoding of + #: :rfc:`4776#section-3.1`, is defined. + format: 'int' + #: Civic location. + location: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_ANISuboption', length: 'int', format: 'int', + location: 'bytes') -> 'None': ... + + +@info_final +class ANIMAGGroupIdentifierSuboption(ANISuboption): + """Data model for MH MAG-Group-Identifier ANI sub-option.""" + + #: MAG group identifier. + group_id: 'int' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_ANISuboption', length: 'int', + group_id: 'int') -> 'None': ... + + +@info_final +class ANIUpdateTimerSuboption(ANISuboption): + """Data model for MH ANI Update-Timer sub-option.""" + + #: Update timer. Counted on the wire in units of 4 seconds; zero asks for an + #: immediate update. + timer: 'timedelta' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_ANISuboption', length: 'int', + timer: 'timedelta') -> 'None': ... + + +@info_final +class AccessNetworkIdentifierOption(Option): + """Data model for MH Access Network Identifier option.""" + + #: Sub-options. + suboptions: 'OrderedMultiDict[Enum_ANISuboption, ANISuboption]' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + suboptions: 'OrderedMultiDict[Enum_ANISuboption, ANISuboption]') -> 'None': ... + + +@info_final +class IPv4TrafficOffloadSelectorOption(Option): + """Data model for MH IPv4 Traffic Offload Selector option.""" + + #: Offload mode flag. Unset offloads the flows matching the selector; set + #: offloads everything except them. + mode: 'bool' + #: Traffic selector sub-options. + selector: 'OrderedMultiDict[Enum_FlowIDSuboption, FlowIdentificationSuboption]' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', mode: 'bool', + selector: 'OrderedMultiDict[Enum_FlowIDSuboption, FlowIdentificationSuboption]') -> 'None': ... + + +@info_final +class DynamicIPMulticastSelectorOption(Option): + """Data model for MH Dynamic IP Multicast Selector option.""" + + #: MLD or IGMP protocol number identifying the record format. + protocol: 'int' + #: Selector mode flag. Set routes the group locally, unset via the home + #: network. + mode: 'bool' + #: Number of multicast address records carried in :attr:`data`. + records: 'int' + #: Multicast address records. These are MLD or IGMP structures belonging to + #: those protocols rather than to the Mobility Header, so they are kept + #: opaque. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', protocol: 'int', mode: 'bool', + records: 'int', data: 'bytes') -> 'None': ... + + +@info_final +class DelegatedMNPOption(Option): + """Data model for MH Delegated Mobile Network Prefix option.""" + + #: IPv4 prefix flag, saying which family :attr:`prefix` is in. + ipv4: 'bool' + #: Prefix length. + prefix_length: 'int' + #: Delegated mobile network prefix, IPv4 or IPv6 as :attr:`ipv4` says. + prefix: 'IPv4Address | IPv6Address' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', ipv4: 'bool', + prefix_length: 'int', + prefix: 'IPv4Address | IPv6Address') -> 'None': ... + + +@info_final +class ActiveMulticastSubscriptionIPv4Option(Option): + """Data model for MH Active Multicast Subscription IPv4 option.""" + + #: IGMP message type identifying the context format. + igmp_type: 'int' + #: Multicast membership context, an IGMP structure kept opaque. + context: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', igmp_type: 'int', + context: 'bytes') -> 'None': ... + + +@info_final +class ActiveMulticastSubscriptionIPv6Option(Option): + """Data model for MH Active Multicast Subscription IPv6 option.""" + + #: MLD message type identifying the context format. + mld_type: 'int' + #: Multicast membership context, an MLD structure kept opaque. + context: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', mld_type: 'int', + context: 'bytes') -> 'None': ... + + +class QoSAttribute(Data): + """Data model for MH Quality-of-Service attributes.""" + + #: Attribute type. + type: 'Enum_QoSAttribute' + #: Attribute length (incl. type and length fields). + length: 'int' + + +@info_final +class UnassignedQoSAttribute(QoSAttribute): + """Data model for unassigned MH Quality-of-Service attributes.""" + + #: Attribute value. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_QoSAttribute', length: 'int', data: 'bytes') -> 'None': ... + + +@info_final +class BitRateAttribute(QoSAttribute): + """Data model for the MH Quality-of-Service attributes carrying a bare bit rate.""" + + #: Bit rate, in **bits** per second. + rate: 'int' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_QoSAttribute', length: 'int', rate: 'int') -> 'None': ... + + +@info_final +class PerSessionBitRateAttribute(QoSAttribute): + """Data model for the MH per-session aggregate maximum bit-rate attributes.""" + + #: Service-identifier aggregation flag. + service: 'bool' + #: Guaranteed-bit-rate exclusion flag. + exclude: 'bool' + #: Bit rate, in **bits** per second. + rate: 'int' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_QoSAttribute', length: 'int', service: 'bool', + exclude: 'bool', rate: 'int') -> 'None': ... + + +@info_final +class AllocationRetentionPriorityAttribute(QoSAttribute): + """Data model for the MH Allocation-Retention-Priority attribute.""" + + #: Priority level, 1 through 15, where 1 is the highest. + priority_level: 'int' + #: Pre-emption capability; ``0`` enabled, ``1`` disabled. + preemption_capability: 'int' + #: Pre-emption vulnerability; ``0`` enabled, ``1`` disabled. + preemption_vulnerability: 'int' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_QoSAttribute', length: 'int', priority_level: 'int', + preemption_capability: 'int', + preemption_vulnerability: 'int') -> 'None': ... + + +@info_final +class QoSTrafficSelectorAttribute(QoSAttribute): + """Data model for the MH QoS-Traffic-Selector attribute.""" + + #: Traffic selector format. + ts_format: 'Enum_TrafficSelector' + #: Traffic selector, kept opaque as for + #: :class:`TrafficSelectorSuboption`. + selector: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_QoSAttribute', length: 'int', + ts_format: 'Enum_TrafficSelector', selector: 'bytes') -> 'None': ... + + +@info_final +class QoSVendorSpecificAttribute(QoSAttribute): + """Data model for the MH QoS-Vendor-Specific attribute.""" + + #: Vendor ID, an SMI Network Management Private Enterprise Number. + vendor: 'int' + #: Vendor-administered sub-type. + subtype: 'int' + #: Vendor-specific data. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_QoSAttribute', length: 'int', vendor: 'int', + subtype: 'int', data: 'bytes') -> 'None': ... + + +@info_final +class QualityOfServiceOption(Option): + """Data model for MH Quality-of-Service option.""" + + #: Service request identifier. + sr_id: 'int' + #: Differentiated services code point, the significant 6 bits of the traffic + #: class octet. + dscp: 'int' + #: Operational code: ``0`` response, ``1`` allocate, ``2`` de-allocate, + #: ``3`` modify, ``4`` query, ``5`` negotiate. + oc: 'int' + #: Quality-of-service attributes. + attributes: 'OrderedMultiDict[Enum_QoSAttribute, QoSAttribute]' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', sr_id: 'int', dscp: 'int', + oc: 'int', + attributes: 'OrderedMultiDict[Enum_QoSAttribute, QoSAttribute]') -> 'None': ... + + +@info_final +class LMAUserPlaneAddressOption(Option): + """Data model for MH LMA User-Plane Address option.""" + + #: LMA user-plane address, or :obj:`None` when the option carries none -- + #: which is how a mobile access gateway asks for a transport without naming + #: an address. + address: 'Optional[IPv4Address | IPv6Address]' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + address: 'Optional[IPv4Address | IPv6Address]') -> 'None': ... + + +@info_final +class MulticastMobilityOption(Option): + """Data model for MH Multicast Mobility option.""" + + #: Option code: ``1`` IGMPv3, ``2`` MLDv2, ``3`` IGMPv3 in IGMPv2 + #: compatibility mode, ``4`` MLDv2 in MLDv1 compatibility mode. + code: 'int' + #: MLD or IGMP report payload, kept opaque. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', code: 'int', + data: 'bytes') -> 'None': ... + + +@info_final +class MulticastAcknowledgementOption(Option): + """Data model for MH Multicast Acknowledgement option.""" + + #: Option code; always ``0``. + code: 'int' + #: Status: ``1`` report payload type unsupported, ``2`` requested group + #: service unsupported, ``3`` administratively prohibited. + status: 'int' + #: MLD or IGMP unsupported report payload, kept opaque. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', code: 'int', status: 'int', + data: 'bytes') -> 'None': ... + + +class LMAControlledMAGSuboption(Data): + """Data model for MH LMA-Controlled MAG Parameters sub-options.""" + + #: Sub-option type. + type: 'Enum_LMAControlledMAGSuboption' + #: Sub-option length (incl. type and length fields). + length: 'int' + + +@info_final +class UnassignedLMAControlledMAGSuboption(LMAControlledMAGSuboption): + """Data model for unassigned MH LMA-Controlled MAG Parameters sub-options.""" + + #: Sub-option data. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_LMAControlledMAGSuboption', length: 'int', + data: 'bytes') -> 'None': ... + + +@info_final +class BindingReregistrationControlSuboption(LMAControlledMAGSuboption): + """Data model for MH Binding Re-registration Control sub-option.""" + + #: Time before binding expiry at which to re-register. Counted on the wire in + #: units of 4 seconds. + start_time: 'timedelta' + #: Minimum delay before the first retransmission. + initial_retransmission: 'timedelta' + #: Maximum delay before the last retransmission. + max_retransmission: 'timedelta' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_LMAControlledMAGSuboption', length: 'int', + start_time: 'timedelta', initial_retransmission: 'timedelta', + max_retransmission: 'timedelta') -> 'None': ... + + +@info_final +class HeartbeatControlSuboption(LMAControlledMAGSuboption): + """Data model for MH Heartbeat Control sub-option.""" + + #: Delay after a successful heartbeat exchange. + interval: 'timedelta' + #: Minimum delay before a heartbeat retransmission. + retransmission_delay: 'timedelta' + #: Maximum number of heartbeat retransmissions. + max_retransmissions: 'int' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_LMAControlledMAGSuboption', length: 'int', + interval: 'timedelta', retransmission_delay: 'timedelta', + max_retransmissions: 'int') -> 'None': ... + + +@info_final +class LMAControlledMAGParametersOption(Option): + """Data model for MH LMA-Controlled MAG Parameters option.""" + + #: Sub-options. + suboptions: 'OrderedMultiDict[Enum_LMAControlledMAGSuboption, LMAControlledMAGSuboption]' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + suboptions: 'OrderedMultiDict[Enum_LMAControlledMAGSuboption, LMAControlledMAGSuboption]') -> 'None': ... + + +@info_final +class MAGMultipathBindingOption(Option): + """Data model for MH MAG Multipath Binding option.""" + + #: Interface access-technology type. + att: 'Enum_AccessType' + #: Interface label. + label: 'int' + #: Binding identifier; ``0`` and ``255`` are reserved. + bid: 'int' + #: Bulk re-registration flag. + bulk: 'bool' + #: Registration overwrite flag. + overwrite: 'bool' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', att: 'Enum_AccessType', + label: 'int', bid: 'int', bulk: 'bool', + overwrite: 'bool') -> 'None': ... + + +@info_final +class MAGIdentifierOption(Option): + """Data model for MH MAG Identifier option.""" + + #: Sub-type, drawn from the mobile node identifier subtype registry. + subtype: 'Enum_MNIDSubtype' + #: Identifier, in the form named by :attr:`subtype`. + identifier: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', subtype: 'Enum_MNIDSubtype', + identifier: 'bytes') -> 'None': ... + + +@info_final +class AnchoredPrefixOption(Option): + """Data model for MH Anchored Prefix option.""" + + #: Prefix length. + prefix_length: 'int' + #: Anchored prefix. Kept apart from :attr:`prefix_length` so that the bits + #: past it, which :rfc:`8885#section-4.3` tells a receiver to ignore rather + #: than requires a sender to zero, survive a round trip. + prefix: 'IPv6Address' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', prefix_length: 'int', + prefix: 'IPv6Address') -> 'None': ... + + +@info_final +class LocalPrefixOption(Option): + """Data model for MH Local Prefix option.""" + + #: Prefix length. + prefix_length: 'int' + #: Local prefix. + prefix: 'IPv6Address' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', prefix_length: 'int', + prefix: 'IPv6Address') -> 'None': ... + + +@info_final +class PreviousMAAROption(Option): + """Data model for MH Previous MAAR option.""" + + #: Prefix length of :attr:`prefix`. + prefix_length: 'int' + #: Previous MAAR's global address. A full address rather than a prefix, so + #: :attr:`prefix_length` does not apply to it. + maar: 'IPv6Address' + #: Mobile node's home network prefix. + prefix: 'IPv6Address' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', prefix_length: 'int', + maar: 'IPv6Address', prefix: 'IPv6Address') -> 'None': ... + + +@info_final +class ServingMAAROption(Option): + """Data model for MH Serving MAAR option.""" + + #: Serving MAAR's global address. + address: 'IPv6Address' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', address: 'IPv6Address') -> 'None': ... + + +@info_final +class DLIFLinkLocalAddressOption(Option): + """Data model for MH DLIF Link-Local Address option.""" + + #: Distributed logical interface's link-local address. + address: 'IPv6Address' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', address: 'IPv6Address') -> 'None': ... + + +@info_final +class DLIFLinkLayerAddressOption(Option): + """Data model for MH DLIF Link-Layer Address option.""" + + #: Distributed logical interface's link-layer address. + lla: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', lla: 'bytes') -> 'None': ... + + # TODO: Implement other options. diff --git a/pcapkit/protocols/internet/mh.py b/pcapkit/protocols/internet/mh.py index 31d6b2c528..aaf6c46105 100644 --- a/pcapkit/protocols/internet/mh.py +++ b/pcapkit/protocols/internet/mh.py @@ -79,20 +79,54 @@ from pcapkit.const.reg.transtype import TransType as Enum_TransType from pcapkit.corekit.multidict import OrderedMultiDict from pcapkit.protocols.data.internet.mh import MH as Data_MH +from pcapkit.protocols.data.internet.mh import \ + AccessNetworkIdentifierOption as Data_AccessNetworkIdentifierOption +from pcapkit.protocols.data.internet.mh import \ + AccessTechnologyTypeOption as Data_AccessTechnologyTypeOption +from pcapkit.protocols.data.internet.mh import \ + ActiveMulticastSubscriptionIPv4Option as Data_ActiveMulticastSubscriptionIPv4Option +from pcapkit.protocols.data.internet.mh import \ + ActiveMulticastSubscriptionIPv6Option as Data_ActiveMulticastSubscriptionIPv6Option +from pcapkit.protocols.data.internet.mh import \ + AllocationRetentionPriorityAttribute as Data_AllocationRetentionPriorityAttribute from pcapkit.protocols.data.internet.mh import \ AlternateCareofAddressOption as Data_AlternateCareofAddressOption +from pcapkit.protocols.data.internet.mh import \ + AlternateIPv4CareofAddressOption as Data_AlternateIPv4CareofAddressOption +from pcapkit.protocols.data.internet.mh import AnchoredPrefixOption as Data_AnchoredPrefixOption +from pcapkit.protocols.data.internet.mh import \ + ANICivicLocationSuboption as Data_ANICivicLocationSuboption +from pcapkit.protocols.data.internet.mh import \ + ANIGeoLocationSuboption as Data_ANIGeoLocationSuboption +from pcapkit.protocols.data.internet.mh import \ + ANIMAGGroupIdentifierSuboption as Data_ANIMAGGroupIdentifierSuboption +from pcapkit.protocols.data.internet.mh import \ + ANINetworkIdentifierSuboption as Data_ANINetworkIdentifierSuboption +from pcapkit.protocols.data.internet.mh import \ + ANIOperatorIdentifierSuboption as Data_ANIOperatorIdentifierSuboption +from pcapkit.protocols.data.internet.mh import ANISuboption as Data_ANISuboption +from pcapkit.protocols.data.internet.mh import \ + ANIUpdateTimerSuboption as Data_ANIUpdateTimerSuboption from pcapkit.protocols.data.internet.mh import AuthOption as Data_AuthOption from pcapkit.protocols.data.internet.mh import \ AuthorizationDataOption as Data_AuthorizationDataOption from pcapkit.protocols.data.internet.mh import BADFOption as Data_BADFOption +from pcapkit.protocols.data.internet.mh import BIDReferenceSuboption as Data_BIDReferenceSuboption from pcapkit.protocols.data.internet.mh import \ BindingAcknowledgementMessage as Data_BindingAcknowledgementMessage from pcapkit.protocols.data.internet.mh import BindingErrorMessage as Data_BindingErrorMessage +from pcapkit.protocols.data.internet.mh import \ + BindingIdentifierOption as Data_BindingIdentifierOption from pcapkit.protocols.data.internet.mh import \ BindingRefreshAdviceOption as Data_BindingRefreshAdviceOption from pcapkit.protocols.data.internet.mh import \ BindingRefreshRequestMessage as Data_BindingRefreshRequestMessage +from pcapkit.protocols.data.internet.mh import \ + BindingReregistrationControlSuboption as Data_BindingReregistrationControlSuboption +from pcapkit.protocols.data.internet.mh import \ + BindingRevocationMessage as Data_BindingRevocationMessage from pcapkit.protocols.data.internet.mh import BindingUpdateMessage as Data_BindingUpdateMessage +from pcapkit.protocols.data.internet.mh import BitRateAttribute as Data_BitRateAttribute from pcapkit.protocols.data.internet.mh import CareofTestInitMessage as Data_CareofTestInitMessage from pcapkit.protocols.data.internet.mh import CareofTestInitOption as Data_CareofTestInitOption from pcapkit.protocols.data.internet.mh import CareofTestMessage as Data_CareofTestMessage @@ -102,6 +136,17 @@ from pcapkit.protocols.data.internet.mh import CGAParametersOption as Data_CGAParametersOption from pcapkit.protocols.data.internet.mh import \ CGAParametersRequestOption as Data_CGAParametersRequestOption +from pcapkit.protocols.data.internet.mh import ContextRequest as Data_ContextRequest +from pcapkit.protocols.data.internet.mh import ContextRequestOption as Data_ContextRequestOption +from pcapkit.protocols.data.internet.mh import DelegatedMNPOption as Data_DelegatedMNPOption +from pcapkit.protocols.data.internet.mh import \ + DLIFLinkLayerAddressOption as Data_DLIFLinkLayerAddressOption +from pcapkit.protocols.data.internet.mh import \ + DLIFLinkLocalAddressOption as Data_DLIFLinkLocalAddressOption +from pcapkit.protocols.data.internet.mh import DNSUpdateOption as Data_DNSUpdateOption +from pcapkit.protocols.data.internet.mh import \ + DynamicIPMulticastSelectorOption as Data_DynamicIPMulticastSelectorOption +from pcapkit.protocols.data.internet.mh import ExperimentalExtension as Data_ExperimentalExtension from pcapkit.protocols.data.internet.mh import ExperimentalMessage as Data_ExperimentalMessage from pcapkit.protocols.data.internet.mh import \ ExperimentalMobilityOption as Data_ExperimentalMobilityOption @@ -111,44 +156,175 @@ FastBindingUpdateMessage as Data_FastBindingUpdateMessage from pcapkit.protocols.data.internet.mh import \ FastNeighborAdvertisementMessage as Data_FastNeighborAdvertisementMessage +from pcapkit.protocols.data.internet.mh import \ + FlowBindingActionSuboption as Data_FlowBindingActionSuboption +from pcapkit.protocols.data.internet.mh import FlowBindingMessage as Data_FlowBindingMessage +from pcapkit.protocols.data.internet.mh import \ + FlowIdentificationOption as Data_FlowIdentificationOption +from pcapkit.protocols.data.internet.mh import \ + FlowIdentificationSuboption as Data_FlowIdentificationSuboption +from pcapkit.protocols.data.internet.mh import FlowSummaryOption as Data_FlowSummaryOption +from pcapkit.protocols.data.internet.mh import GREKeyOption as Data_GREKeyOption +from pcapkit.protocols.data.internet.mh import HandoffIndicatorOption as Data_HandoffIndicatorOption from pcapkit.protocols.data.internet.mh import \ HandoverAcknowledgeMessage as Data_HandoverAcknowledgeMessage from pcapkit.protocols.data.internet.mh import \ HandoverInitiateMessage as Data_HandoverInitiateMessage +from pcapkit.protocols.data.internet.mh import \ + HeartbeatControlSuboption as Data_HeartbeatControlSuboption +from pcapkit.protocols.data.internet.mh import HeartbeatMessage as Data_HeartbeatMessage +from pcapkit.protocols.data.internet.mh import HomeAgentSwitchMessage as Data_HomeAgentSwitchMessage +from pcapkit.protocols.data.internet.mh import \ + HomeNetworkPrefixOption as Data_HomeNetworkPrefixOption from pcapkit.protocols.data.internet.mh import HomeTestInitMessage as Data_HomeTestInitMessage from pcapkit.protocols.data.internet.mh import HomeTestMessage as Data_HomeTestMessage +from pcapkit.protocols.data.internet.mh import \ + IPv4AddressAcknowledgementOption as Data_IPv4AddressAcknowledgementOption +from pcapkit.protocols.data.internet.mh import \ + IPv4CareofAddressOption as Data_IPv4CareofAddressOption +from pcapkit.protocols.data.internet.mh import \ + IPv4DefaultRouterAddressOption as Data_IPv4DefaultRouterAddressOption +from pcapkit.protocols.data.internet.mh import \ + IPv4DHCPSupportModeOption as Data_IPv4DHCPSupportModeOption +from pcapkit.protocols.data.internet.mh import IPv4HomeAddressOption as Data_IPv4HomeAddressOption +from pcapkit.protocols.data.internet.mh import \ + IPv4HomeAddressReplyOption as Data_IPv4HomeAddressReplyOption +from pcapkit.protocols.data.internet.mh import \ + IPv4HomeAddressRequestOption as Data_IPv4HomeAddressRequestOption +from pcapkit.protocols.data.internet.mh import \ + IPv4TrafficOffloadSelectorOption as Data_IPv4TrafficOffloadSelectorOption from pcapkit.protocols.data.internet.mh import \ IPv6AddressPrefixOption as Data_IPv6AddressPrefixOption from pcapkit.protocols.data.internet.mh import LinkLayerAddressOption as Data_LinkLayerAddressOption +from pcapkit.protocols.data.internet.mh import LinkLocalAddressOption as Data_LinkLocalAddressOption +from pcapkit.protocols.data.internet.mh import LMAAddressOption as Data_LMAAddressOption +from pcapkit.protocols.data.internet.mh import \ + LMAControlledMAGParametersOption as Data_LMAControlledMAGParametersOption +from pcapkit.protocols.data.internet.mh import \ + LMAControlledMAGSuboption as Data_LMAControlledMAGSuboption +from pcapkit.protocols.data.internet.mh import \ + LMAUserPlaneAddressOption as Data_LMAUserPlaneAddressOption +from pcapkit.protocols.data.internet.mh import LoadInformationOption as Data_LoadInformationOption +from pcapkit.protocols.data.internet.mh import \ + LocalizedRoutingAcknowledgmentMessage as Data_LocalizedRoutingAcknowledgmentMessage +from pcapkit.protocols.data.internet.mh import \ + LocalizedRoutingInitiationMessage as Data_LocalizedRoutingInitiationMessage +from pcapkit.protocols.data.internet.mh import LocalPrefixOption as Data_LocalPrefixOption +from pcapkit.protocols.data.internet.mh import MAGIdentifierOption as Data_MAGIdentifierOption +from pcapkit.protocols.data.internet.mh import MAGIPv6AddressOption as Data_MAGIPv6AddressOption +from pcapkit.protocols.data.internet.mh import \ + MAGMultipathBindingOption as Data_MAGMultipathBindingOption from pcapkit.protocols.data.internet.mh import MesgIDOption as Data_MesgIDOption +from pcapkit.protocols.data.internet.mh import \ + MNGroupIdentifierOption as Data_MNGroupIdentifierOption from pcapkit.protocols.data.internet.mh import MNIDOption as Data_MNIDOption +from pcapkit.protocols.data.internet.mh import MNLLAIIDOption as Data_MNLLAIIDOption +from pcapkit.protocols.data.internet.mh import MNLLIdentifierOption as Data_MNLLIdentifierOption from pcapkit.protocols.data.internet.mh import \ MobileNetworkPrefixOption as Data_MobileNetworkPrefixOption +from pcapkit.protocols.data.internet.mh import \ + MulticastAcknowledgementOption as Data_MulticastAcknowledgementOption +from pcapkit.protocols.data.internet.mh import \ + MulticastMobilityOption as Data_MulticastMobilityOption from pcapkit.protocols.data.internet.mh import MultiPrefixExtension as Data_MultiPrefixExtension +from pcapkit.protocols.data.internet.mh import NATDetectionOption as Data_NATDetectionOption from pcapkit.protocols.data.internet.mh import NonceIndicesOption as Data_NonceIndicesOption +from pcapkit.protocols.data.internet.mh import \ + PadFlowIdentificationSuboption as Data_PadFlowIdentificationSuboption from pcapkit.protocols.data.internet.mh import PadOption as Data_PadOption from pcapkit.protocols.data.internet.mh import \ PermanentHomeKeygenTokenOption as Data_PermanentHomeKeygenTokenOption +from pcapkit.protocols.data.internet.mh import \ + PerSessionBitRateAttribute as Data_PerSessionBitRateAttribute +from pcapkit.protocols.data.internet.mh import PreviousMAAROption as Data_PreviousMAAROption +from pcapkit.protocols.data.internet.mh import QoSAttribute as Data_QoSAttribute +from pcapkit.protocols.data.internet.mh import \ + QoSTrafficSelectorAttribute as Data_QoSTrafficSelectorAttribute +from pcapkit.protocols.data.internet.mh import \ + QoSVendorSpecificAttribute as Data_QoSVendorSpecificAttribute +from pcapkit.protocols.data.internet.mh import QualityOfServiceOption as Data_QualityOfServiceOption +from pcapkit.protocols.data.internet.mh import \ + RedirectCapabilityOption as Data_RedirectCapabilityOption +from pcapkit.protocols.data.internet.mh import RedirectOption as Data_RedirectOption +from pcapkit.protocols.data.internet.mh import RestartCounterOption as Data_RestartCounterOption +from pcapkit.protocols.data.internet.mh import ServiceSelectionOption as Data_ServiceSelectionOption +from pcapkit.protocols.data.internet.mh import ServingMAAROption as Data_ServingMAAROption from pcapkit.protocols.data.internet.mh import SignatureOption as Data_SignatureOption +from pcapkit.protocols.data.internet.mh import \ + SubscriptionQueryMessage as Data_SubscriptionQueryMessage +from pcapkit.protocols.data.internet.mh import \ + SubscriptionResponseMessage as Data_SubscriptionResponseMessage +from pcapkit.protocols.data.internet.mh import \ + TargetCareofAddressSuboption as Data_TargetCareofAddressSuboption +from pcapkit.protocols.data.internet.mh import TimestampOption as Data_TimestampOption +from pcapkit.protocols.data.internet.mh import \ + TrafficSelectorSuboption as Data_TrafficSelectorSuboption +from pcapkit.protocols.data.internet.mh import TransientBindingOption as Data_TransientBindingOption +from pcapkit.protocols.data.internet.mh import UnassignedANISuboption as Data_UnassignedANISuboption +from pcapkit.protocols.data.internet.mh import \ + UnassignedFlowIdentificationSuboption as Data_UnassignedFlowIdentificationSuboption +from pcapkit.protocols.data.internet.mh import \ + UnassignedLMAControlledMAGSuboption as Data_UnassignedLMAControlledMAGSuboption from pcapkit.protocols.data.internet.mh import UnassignedOption as Data_UnassignedOption +from pcapkit.protocols.data.internet.mh import UnassignedQoSAttribute as Data_UnassignedQoSAttribute from pcapkit.protocols.data.internet.mh import UnknownExtension as Data_UnknownExtension from pcapkit.protocols.data.internet.mh import UnknownMessage as Data_UnknownMessage +from pcapkit.protocols.data.internet.mh import \ + UpdateNotificationAcknowledgementMessage as Data_UpdateNotificationAcknowledgementMessage +from pcapkit.protocols.data.internet.mh import \ + UpdateNotificationMessage as Data_UpdateNotificationMessage +from pcapkit.protocols.data.internet.mh import VendorSpecificOption as Data_VendorSpecificOption from pcapkit.protocols.internet.internet import Internet from pcapkit.protocols.schema.internet.mh import MH as Schema_MH +from pcapkit.protocols.schema.internet.mh import \ + AccessNetworkIdentifierOption as Schema_AccessNetworkIdentifierOption +from pcapkit.protocols.schema.internet.mh import \ + AccessTechnologyTypeOption as Schema_AccessTechnologyTypeOption +from pcapkit.protocols.schema.internet.mh import \ + ActiveMulticastSubscriptionIPv4Option as Schema_ActiveMulticastSubscriptionIPv4Option +from pcapkit.protocols.schema.internet.mh import \ + ActiveMulticastSubscriptionIPv6Option as Schema_ActiveMulticastSubscriptionIPv6Option +from pcapkit.protocols.schema.internet.mh import \ + AllocationRetentionPriorityAttribute as Schema_AllocationRetentionPriorityAttribute from pcapkit.protocols.schema.internet.mh import \ AlternateCareofAddressOption as Schema_AlternateCareofAddressOption +from pcapkit.protocols.schema.internet.mh import \ + AlternateIPv4CareofAddressOption as Schema_AlternateIPv4CareofAddressOption +from pcapkit.protocols.schema.internet.mh import AnchoredPrefixOption as Schema_AnchoredPrefixOption +from pcapkit.protocols.schema.internet.mh import \ + ANICivicLocationSuboption as Schema_ANICivicLocationSuboption +from pcapkit.protocols.schema.internet.mh import \ + ANIGeoLocationSuboption as Schema_ANIGeoLocationSuboption +from pcapkit.protocols.schema.internet.mh import \ + ANIMAGGroupIdentifierSuboption as Schema_ANIMAGGroupIdentifierSuboption +from pcapkit.protocols.schema.internet.mh import \ + ANINetworkIdentifierSuboption as Schema_ANINetworkIdentifierSuboption +from pcapkit.protocols.schema.internet.mh import \ + ANIOperatorIdentifierSuboption as Schema_ANIOperatorIdentifierSuboption +from pcapkit.protocols.schema.internet.mh import ANISuboption as Schema_ANISuboption +from pcapkit.protocols.schema.internet.mh import \ + ANIUpdateTimerSuboption as Schema_ANIUpdateTimerSuboption from pcapkit.protocols.schema.internet.mh import AuthOption as Schema_AuthOption from pcapkit.protocols.schema.internet.mh import \ AuthorizationDataOption as Schema_AuthorizationDataOption from pcapkit.protocols.schema.internet.mh import BADFOption as Schema_BADFOption +from pcapkit.protocols.schema.internet.mh import \ + BIDReferenceSuboption as Schema_BIDReferenceSuboption from pcapkit.protocols.schema.internet.mh import \ BindingAcknowledgementMessage as Schema_BindingAcknowledgementMessage from pcapkit.protocols.schema.internet.mh import BindingErrorMessage as Schema_BindingErrorMessage +from pcapkit.protocols.schema.internet.mh import \ + BindingIdentifierOption as Schema_BindingIdentifierOption from pcapkit.protocols.schema.internet.mh import \ BindingRefreshAdviceOption as Schema_BindingRefreshAdviceOption from pcapkit.protocols.schema.internet.mh import \ BindingRefreshRequestMessage as Schema_BindingRefreshRequestMessage +from pcapkit.protocols.schema.internet.mh import \ + BindingReregistrationControlSuboption as Schema_BindingReregistrationControlSuboption +from pcapkit.protocols.schema.internet.mh import \ + BindingRevocationMessage as Schema_BindingRevocationMessage from pcapkit.protocols.schema.internet.mh import BindingUpdateMessage as Schema_BindingUpdateMessage +from pcapkit.protocols.schema.internet.mh import BitRateAttribute as Schema_BitRateAttribute from pcapkit.protocols.schema.internet.mh import \ CareofTestInitMessage as Schema_CareofTestInitMessage from pcapkit.protocols.schema.internet.mh import CareofTestInitOption as Schema_CareofTestInitOption @@ -159,6 +335,17 @@ from pcapkit.protocols.schema.internet.mh import CGAParametersOption as Schema_CGAParametersOption from pcapkit.protocols.schema.internet.mh import \ CGAParametersRequestOption as Schema_CGAParametersRequestOption +from pcapkit.protocols.schema.internet.mh import ContextRequestOption as Schema_ContextRequestOption +from pcapkit.protocols.schema.internet.mh import DelegatedMNPOption as Schema_DelegatedMNPOption +from pcapkit.protocols.schema.internet.mh import \ + DLIFLinkLayerAddressOption as Schema_DLIFLinkLayerAddressOption +from pcapkit.protocols.schema.internet.mh import \ + DLIFLinkLocalAddressOption as Schema_DLIFLinkLocalAddressOption +from pcapkit.protocols.schema.internet.mh import DNSUpdateOption as Schema_DNSUpdateOption +from pcapkit.protocols.schema.internet.mh import \ + DynamicIPMulticastSelectorOption as Schema_DynamicIPMulticastSelectorOption +from pcapkit.protocols.schema.internet.mh import \ + ExperimentalExtension as Schema_ExperimentalExtension from pcapkit.protocols.schema.internet.mh import ExperimentalMessage as Schema_ExperimentalMessage from pcapkit.protocols.schema.internet.mh import \ ExperimentalMobilityOption as Schema_ExperimentalMobilityOption @@ -168,30 +355,136 @@ FastBindingUpdateMessage as Schema_FastBindingUpdateMessage from pcapkit.protocols.schema.internet.mh import \ FastNeighborAdvertisementMessage as Schema_FastNeighborAdvertisementMessage +from pcapkit.protocols.schema.internet.mh import \ + FlowBindingActionSuboption as Schema_FlowBindingActionSuboption +from pcapkit.protocols.schema.internet.mh import FlowBindingMessage as Schema_FlowBindingMessage +from pcapkit.protocols.schema.internet.mh import \ + FlowIdentificationOption as Schema_FlowIdentificationOption +from pcapkit.protocols.schema.internet.mh import \ + FlowIdentificationSuboption as Schema_FlowIdentificationSuboption +from pcapkit.protocols.schema.internet.mh import FlowSummaryOption as Schema_FlowSummaryOption +from pcapkit.protocols.schema.internet.mh import GREKeyOption as Schema_GREKeyOption +from pcapkit.protocols.schema.internet.mh import \ + HandoffIndicatorOption as Schema_HandoffIndicatorOption from pcapkit.protocols.schema.internet.mh import \ HandoverAcknowledgeMessage as Schema_HandoverAcknowledgeMessage from pcapkit.protocols.schema.internet.mh import \ HandoverInitiateMessage as Schema_HandoverInitiateMessage +from pcapkit.protocols.schema.internet.mh import \ + HeartbeatControlSuboption as Schema_HeartbeatControlSuboption +from pcapkit.protocols.schema.internet.mh import HeartbeatMessage as Schema_HeartbeatMessage +from pcapkit.protocols.schema.internet.mh import \ + HomeAgentSwitchMessage as Schema_HomeAgentSwitchMessage +from pcapkit.protocols.schema.internet.mh import \ + HomeNetworkPrefixOption as Schema_HomeNetworkPrefixOption from pcapkit.protocols.schema.internet.mh import HomeTestInitMessage as Schema_HomeTestInitMessage from pcapkit.protocols.schema.internet.mh import HomeTestMessage as Schema_HomeTestMessage +from pcapkit.protocols.schema.internet.mh import \ + IPv4AddressAcknowledgementOption as Schema_IPv4AddressAcknowledgementOption +from pcapkit.protocols.schema.internet.mh import \ + IPv4CareofAddressOption as Schema_IPv4CareofAddressOption +from pcapkit.protocols.schema.internet.mh import \ + IPv4DefaultRouterAddressOption as Schema_IPv4DefaultRouterAddressOption +from pcapkit.protocols.schema.internet.mh import \ + IPv4DHCPSupportModeOption as Schema_IPv4DHCPSupportModeOption +from pcapkit.protocols.schema.internet.mh import \ + IPv4HomeAddressOption as Schema_IPv4HomeAddressOption +from pcapkit.protocols.schema.internet.mh import \ + IPv4HomeAddressReplyOption as Schema_IPv4HomeAddressReplyOption +from pcapkit.protocols.schema.internet.mh import \ + IPv4HomeAddressRequestOption as Schema_IPv4HomeAddressRequestOption +from pcapkit.protocols.schema.internet.mh import \ + IPv4TrafficOffloadSelectorOption as Schema_IPv4TrafficOffloadSelectorOption from pcapkit.protocols.schema.internet.mh import \ IPv6AddressPrefixOption as Schema_IPv6AddressPrefixOption from pcapkit.protocols.schema.internet.mh import \ LinkLayerAddressOption as Schema_LinkLayerAddressOption +from pcapkit.protocols.schema.internet.mh import \ + LinkLocalAddressOption as Schema_LinkLocalAddressOption +from pcapkit.protocols.schema.internet.mh import LMAAddressOption as Schema_LMAAddressOption +from pcapkit.protocols.schema.internet.mh import \ + LMAControlledMAGParametersOption as Schema_LMAControlledMAGParametersOption +from pcapkit.protocols.schema.internet.mh import \ + LMAControlledMAGSuboption as Schema_LMAControlledMAGSuboption +from pcapkit.protocols.schema.internet.mh import \ + LMAUserPlaneAddressOption as Schema_LMAUserPlaneAddressOption +from pcapkit.protocols.schema.internet.mh import \ + LoadInformationOption as Schema_LoadInformationOption +from pcapkit.protocols.schema.internet.mh import \ + LocalizedRoutingAcknowledgmentMessage as Schema_LocalizedRoutingAcknowledgmentMessage +from pcapkit.protocols.schema.internet.mh import \ + LocalizedRoutingInitiationMessage as Schema_LocalizedRoutingInitiationMessage +from pcapkit.protocols.schema.internet.mh import LocalPrefixOption as Schema_LocalPrefixOption +from pcapkit.protocols.schema.internet.mh import MAGIdentifierOption as Schema_MAGIdentifierOption +from pcapkit.protocols.schema.internet.mh import MAGIPv6AddressOption as Schema_MAGIPv6AddressOption +from pcapkit.protocols.schema.internet.mh import \ + MAGMultipathBindingOption as Schema_MAGMultipathBindingOption from pcapkit.protocols.schema.internet.mh import MesgIDOption as Schema_MesgIDOption +from pcapkit.protocols.schema.internet.mh import \ + MNGroupIdentifierOption as Schema_MNGroupIdentifierOption from pcapkit.protocols.schema.internet.mh import MNIDOption as Schema_MNIDOption +from pcapkit.protocols.schema.internet.mh import MNLLAIIDOption as Schema_MNLLAIIDOption +from pcapkit.protocols.schema.internet.mh import MNLLIdentifierOption as Schema_MNLLIdentifierOption from pcapkit.protocols.schema.internet.mh import \ MobileNetworkPrefixOption as Schema_MobileNetworkPrefixOption +from pcapkit.protocols.schema.internet.mh import \ + MulticastAcknowledgementOption as Schema_MulticastAcknowledgementOption +from pcapkit.protocols.schema.internet.mh import \ + MulticastMobilityOption as Schema_MulticastMobilityOption from pcapkit.protocols.schema.internet.mh import MultiPrefixExtension as Schema_MultiPrefixExtension +from pcapkit.protocols.schema.internet.mh import NATDetectionOption as Schema_NATDetectionOption from pcapkit.protocols.schema.internet.mh import NonceIndicesOption as Schema_NonceIndicesOption from pcapkit.protocols.schema.internet.mh import Packet as Schema_Packet +from pcapkit.protocols.schema.internet.mh import \ + PadFlowIdentificationSuboption as Schema_PadFlowIdentificationSuboption from pcapkit.protocols.schema.internet.mh import PadOption as Schema_PadOption from pcapkit.protocols.schema.internet.mh import \ PermanentHomeKeygenTokenOption as Schema_PermanentHomeKeygenTokenOption +from pcapkit.protocols.schema.internet.mh import \ + PerSessionBitRateAttribute as Schema_PerSessionBitRateAttribute +from pcapkit.protocols.schema.internet.mh import PreviousMAAROption as Schema_PreviousMAAROption +from pcapkit.protocols.schema.internet.mh import QoSAttribute as Schema_QoSAttribute +from pcapkit.protocols.schema.internet.mh import \ + QoSTrafficSelectorAttribute as Schema_QoSTrafficSelectorAttribute +from pcapkit.protocols.schema.internet.mh import \ + QoSVendorSpecificAttribute as Schema_QoSVendorSpecificAttribute +from pcapkit.protocols.schema.internet.mh import \ + QualityOfServiceOption as Schema_QualityOfServiceOption +from pcapkit.protocols.schema.internet.mh import \ + RedirectCapabilityOption as Schema_RedirectCapabilityOption +from pcapkit.protocols.schema.internet.mh import RedirectOption as Schema_RedirectOption +from pcapkit.protocols.schema.internet.mh import RestartCounterOption as Schema_RestartCounterOption +from pcapkit.protocols.schema.internet.mh import \ + ServiceSelectionOption as Schema_ServiceSelectionOption +from pcapkit.protocols.schema.internet.mh import ServingMAAROption as Schema_ServingMAAROption from pcapkit.protocols.schema.internet.mh import SignatureOption as Schema_SignatureOption +from pcapkit.protocols.schema.internet.mh import \ + SubscriptionQueryMessage as Schema_SubscriptionQueryMessage +from pcapkit.protocols.schema.internet.mh import \ + SubscriptionResponseMessage as Schema_SubscriptionResponseMessage +from pcapkit.protocols.schema.internet.mh import \ + TargetCareofAddressSuboption as Schema_TargetCareofAddressSuboption +from pcapkit.protocols.schema.internet.mh import TimestampOption as Schema_TimestampOption +from pcapkit.protocols.schema.internet.mh import \ + TrafficSelectorSuboption as Schema_TrafficSelectorSuboption +from pcapkit.protocols.schema.internet.mh import \ + TransientBindingOption as Schema_TransientBindingOption +from pcapkit.protocols.schema.internet.mh import \ + UnassignedANISuboption as Schema_UnassignedANISuboption +from pcapkit.protocols.schema.internet.mh import \ + UnassignedFlowIdentificationSuboption as Schema_UnassignedFlowIdentificationSuboption +from pcapkit.protocols.schema.internet.mh import \ + UnassignedLMAControlledMAGSuboption as Schema_UnassignedLMAControlledMAGSuboption from pcapkit.protocols.schema.internet.mh import UnassignedOption as Schema_UnassignedOption +from pcapkit.protocols.schema.internet.mh import \ + UnassignedQoSAttribute as Schema_UnassignedQoSAttribute from pcapkit.protocols.schema.internet.mh import UnknownExtension as Schema_UnknownExtension from pcapkit.protocols.schema.internet.mh import UnknownMessage as Schema_UnknownMessage +from pcapkit.protocols.schema.internet.mh import \ + UpdateNotificationAcknowledgementMessage as Schema_UpdateNotificationAcknowledgementMessage +from pcapkit.protocols.schema.internet.mh import \ + UpdateNotificationMessage as Schema_UpdateNotificationMessage +from pcapkit.protocols.schema.internet.mh import VendorSpecificOption as Schema_VendorSpecificOption from pcapkit.protocols.schema.schema import Schema from pcapkit.utilities.exceptions import ProtocolError, UnsupportedCall from pcapkit.utilities.warnings import ProtocolWarning, RegistryWarning, warn @@ -200,7 +493,7 @@ from datetime import datetime as dt_type from datetime import timedelta from enum import IntEnum as StdlibEnum - from ipaddress import IPv6Address, IPv6Network + from ipaddress import IPv4Address, IPv6Address, IPv6Network from typing import IO, Any, Callable, DefaultDict, NoReturn, Optional, Type from aenum import IntEnum as AenumEnum @@ -218,6 +511,13 @@ Option = OrderedMultiDict[Enum_Option, Data_Option] Extension = OrderedMultiDict[Enum_CGAExtension, Data_CGAExtension] + FlowIDSuboption = OrderedMultiDict[Enum_FlowIDSuboption, + Data_FlowIdentificationSuboption] + ANISuboption = OrderedMultiDict[Enum_ANISuboption, Data_ANISuboption] + QoSAttribute = OrderedMultiDict[Enum_QoSAttribute, Data_QoSAttribute] + LMAControlledMAGSuboption = OrderedMultiDict[Enum_LMAControlledMAGSuboption, + Data_LMAControlledMAGSuboption] + PacketParser = Callable[[Schema_Packet, NamedArg(Schema_MH, 'header')], Data_MH] PacketConstructor = Callable[[DefaultArg(Optional[Data_MH]), KwArg(Any)], Schema_Packet] @@ -244,6 +544,25 @@ class NTPTimestamp(collections.namedtuple('NTPTimestamp', 'seconds fraction')): fraction: int +class PMIPv6Timestamp(collections.namedtuple('PMIPv6Timestamp', 'seconds fraction')): + """Proxy Mobile IPv6 timestamp format, c.f., :rfc:`5213#section-8.8`. + + Note: + This is **not** an :rfc:`1305` NTP timestamp, which is why it is a type of + its own rather than a reuse of :class:`NTPTimestamp`. It counts from the + UNIX epoch rather than NTP's 1900 one, and splits its 64 bits 48/16 + rather than 32/32, so each field means something different in the two. + + """ + + __slots__ = () + + #: Seconds since 1 January 1970, in the leading 48 bits. + seconds: int + #: Fraction of a second, in units of 1/65536, in the trailing 16 bits. + fraction: int + + class FastBindingAcknowledgmentStatus(IntEnum): """[FastBindingAcknowledgmentStatus] Fast Binding Acknowledgment Status Codes. @@ -370,6 +689,112 @@ def _missing_(cls, value: 'int') -> 'IPv6AddressPrefixCode': return extend_enum(cls, 'Unassigned_%d' % value, value) +class LocalizedRoutingStatus(IntEnum): + """[LocalizedRoutingStatus] Localized Routing Acknowledgment Status Codes. + + Status values of the localized routing acknowledgment (LRA) message, c.f., + :rfc:`6705#section-10.2`. Values below ``128`` indicate that the initiation + was processed successfully, values of ``128`` and above that it was rejected. + + Note: + :rfc:`6705#section-10.2` defines these values inline and IANA keeps no + registry of them -- neither a dedicated one nor entries in the general + *Status Codes* registry -- so the enumeration lives here rather than in + :mod:`pcapkit.const.mh`. It is **not** interchangeable with the + registered :class:`~pcapkit.const.mh.status_code.StatusCode`, whose + ``128`` and ``129`` mean something else entirely. + + """ + + #: Success [:rfc:`6705#section-10.2`] + Success = 0 + + #: Localized Routing Not Allowed [:rfc:`6705#section-10.2`] + Localized_Routing_Not_Allowed = 128 + + #: MN Not Attached [:rfc:`6705#section-10.2`] + MN_Not_Attached = 129 + + @staticmethod + def get(key: 'int | str', default: 'int' = -1) -> 'LocalizedRoutingStatus': + """Backport support for original codes. + + Args: + key: Key to get enum item. + default: Default value if not found. + + """ + if isinstance(key, int): + return LocalizedRoutingStatus(key) + if key not in LocalizedRoutingStatus._member_map_: # pylint: disable=no-member + extend_enum(LocalizedRoutingStatus, key, default) + return LocalizedRoutingStatus[key] # type: ignore[misc] + + @classmethod + def _missing_(cls, value: 'int') -> 'LocalizedRoutingStatus': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + #: Unassigned + return extend_enum(cls, 'Unassigned_%d' % value, value) + + +class LMAAddressCode(IntEnum): + """[LMAAddressCode] Local Mobility Anchor Address Option Codes. + + Option codes of the local mobility anchor address option, which say which + address family the option carries, c.f., :rfc:`5949#section-6.2.2`. + + Note: + :rfc:`5949#section-6.2.2` defines these values inline and IANA keeps no + registry of them, so the enumeration lives here rather than in + :mod:`pcapkit.const.mh`. + + """ + + #: Reserved [:rfc:`5949#section-6.2.2`] + Reserved = 0 + + #: IPv6 local mobility anchor address [:rfc:`5949#section-6.2.2`] + IPv6_LMAA = 1 + + #: IPv4 local mobility anchor address [:rfc:`5949#section-6.2.2`] + IPv4_LMAA = 2 + + @staticmethod + def get(key: 'int | str', default: 'int' = -1) -> 'LMAAddressCode': + """Backport support for original codes. + + Args: + key: Key to get enum item. + default: Default value if not found. + + """ + if isinstance(key, int): + return LMAAddressCode(key) + if key not in LMAAddressCode._member_map_: # pylint: disable=no-member + extend_enum(LMAAddressCode, key, default) + return LMAAddressCode[key] # type: ignore[misc] + + @classmethod + def _missing_(cls, value: 'int') -> 'LMAAddressCode': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + #: Unassigned + return extend_enum(cls, 'Unassigned_%d' % value, value) + + class MH(Internet[Data_MH, Schema_MH], schema=Schema_MH, data=Data_MH): """This class implements Mobility Header. @@ -420,12 +845,42 @@ class MH(Internet[Data_MH, Schema_MH], * - :attr:`~pcapkit.const.mh.packet.Packet.Experimental_Mobility_Header` - :meth:`~pcapkit.protocols.internet.mh.MH._read_msg_emh` - :meth:`~pcapkit.protocols.internet.mh.MH._make_msg_emh` + * - :attr:`~pcapkit.const.mh.packet.Packet.Home_Agent_Switch_Message` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_msg_has` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_msg_has` + * - :attr:`~pcapkit.const.mh.packet.Packet.Heartbeat_Message` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_msg_hb` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_msg_hb` * - :attr:`~pcapkit.const.mh.packet.Packet.Handover_Initiate_Message` - :meth:`~pcapkit.protocols.internet.mh.MH._read_msg_hi` - :meth:`~pcapkit.protocols.internet.mh.MH._make_msg_hi` * - :attr:`~pcapkit.const.mh.packet.Packet.Handover_Acknowledge_Message` - :meth:`~pcapkit.protocols.internet.mh.MH._read_msg_hack` - :meth:`~pcapkit.protocols.internet.mh.MH._make_msg_hack` + * - :attr:`~pcapkit.const.mh.packet.Packet.Binding_Revocation_Message` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_msg_brm` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_msg_brm` + * - :attr:`~pcapkit.const.mh.packet.Packet.Localized_Routing_Initiation` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_msg_lri` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_msg_lri` + * - :attr:`~pcapkit.const.mh.packet.Packet.Localized_Routing_Acknowledgment` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_msg_lra` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_msg_lra` + * - :attr:`~pcapkit.const.mh.packet.Packet.Update_Notification` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_msg_upn` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_msg_upn` + * - :attr:`~pcapkit.const.mh.packet.Packet.Update_Notification_Acknowledgement` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_msg_upa` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_msg_upa` + * - :attr:`~pcapkit.const.mh.packet.Packet.Flow_Binding_Message` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_msg_fbm` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_msg_fbm` + * - :attr:`~pcapkit.const.mh.packet.Packet.Subscription_Query` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_msg_sq` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_msg_sq` + * - :attr:`~pcapkit.const.mh.packet.Packet.Subscription_Response` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_msg_sr` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_msg_sr` This class currently supports parsing the following MH options, which are registered in the :attr:`self.__option__ ` @@ -437,7 +892,6 @@ class MH(Internet[Data_MH, Schema_MH], * - Option Code - Option Parser - Option Constructor - * - :attr:`~pcapkit.const.mh.option.Option.Pad1` - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_pad` - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_pad` @@ -489,15 +943,168 @@ class MH(Internet[Data_MH, Schema_MH], * - :attr:`~pcapkit.const.mh.option.Option.Care_of_Test` - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_ct` - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_ct` + * - :attr:`~pcapkit.const.mh.option.Option.DNS_UPDATE_TYPE` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_dns` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_dns` * - :attr:`~pcapkit.const.mh.option.Option.Experimental_Mobility_Option` - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_exp` - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_exp` + * - :attr:`~pcapkit.const.mh.option.Option.Vendor_Specific_Mobility_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_vendor` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_vendor` + * - :attr:`~pcapkit.const.mh.option.Option.Service_Selection_Mobility_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_service` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_service` * - :attr:`~pcapkit.const.mh.option.Option.Binding_Authorization_Data_for_FMIPv6` - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_badf` - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_badf` + * - :attr:`~pcapkit.const.mh.option.Option.Home_Network_Prefix_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_hnp` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_hnp` + * - :attr:`~pcapkit.const.mh.option.Option.Handoff_Indicator_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_hi` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_hi` + * - :attr:`~pcapkit.const.mh.option.Option.Access_Technology_Type_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_att` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_att` + * - :attr:`~pcapkit.const.mh.option.Option.Mobile_Node_Link_layer_Identifier_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_mn_lli` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_mn_lli` + * - :attr:`~pcapkit.const.mh.option.Option.Link_local_Address_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_lla_addr` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_lla_addr` + * - :attr:`~pcapkit.const.mh.option.Option.Timestamp_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_timestamp` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_timestamp` + * - :attr:`~pcapkit.const.mh.option.Option.Restart_Counter` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_restart` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_restart` + * - :attr:`~pcapkit.const.mh.option.Option.IPv4_Home_Address` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_ipv4_hoa` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_ipv4_hoa` + * - :attr:`~pcapkit.const.mh.option.Option.IPv4_Address_Acknowledgement` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_ipv4_ack` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_ipv4_ack` + * - :attr:`~pcapkit.const.mh.option.Option.NAT_Detection` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_nat` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_nat` + * - :attr:`~pcapkit.const.mh.option.Option.IPv4_Care_of_Address` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_ipv4_coa` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_ipv4_coa` + * - :attr:`~pcapkit.const.mh.option.Option.GRE_Key_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_gre` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_gre` * - :attr:`~pcapkit.const.mh.option.Option.Mobility_Header_IPv6_Address_Prefix` - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_ipv6_ap` - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_ipv6_ap` + * - :attr:`~pcapkit.const.mh.option.Option.Binding_Identifier` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_bid` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_bid` + * - :attr:`~pcapkit.const.mh.option.Option.IPv4_Home_Address_Request` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_ipv4_hoa_req` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_ipv4_hoa_req` + * - :attr:`~pcapkit.const.mh.option.Option.IPv4_Home_Address_Reply` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_ipv4_hoa_rep` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_ipv4_hoa_rep` + * - :attr:`~pcapkit.const.mh.option.Option.IPv4_Default_Router_Address` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_ipv4_router` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_ipv4_router` + * - :attr:`~pcapkit.const.mh.option.Option.IPv4_DHCP_Support_Mode` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_ipv4_dhcp` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_ipv4_dhcp` + * - :attr:`~pcapkit.const.mh.option.Option.Context_Request_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_cr` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_cr` + * - :attr:`~pcapkit.const.mh.option.Option.Local_Mobility_Anchor_Address_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_lmaa` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_lmaa` + * - :attr:`~pcapkit.const.mh.option.Option.Mobile_Node_Link_local_Address_Interface_Identifier_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_mn_lla_iid` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_mn_lla_iid` + * - :attr:`~pcapkit.const.mh.option.Option.Transient_Binding` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_transient` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_transient` + * - :attr:`~pcapkit.const.mh.option.Option.Flow_Summary_Mobility_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_fs` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_fs` + * - :attr:`~pcapkit.const.mh.option.Option.Flow_Identification_Mobility_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_fid` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_fid` + * - :attr:`~pcapkit.const.mh.option.Option.Redirect_Capability_Mobility_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_rc` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_rc` + * - :attr:`~pcapkit.const.mh.option.Option.Redirect_Mobility_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_redirect` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_redirect` + * - :attr:`~pcapkit.const.mh.option.Option.Load_Information_Mobility_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_load` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_load` + * - :attr:`~pcapkit.const.mh.option.Option.Alternate_IPv4_Care_of_Address` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_alt_ipv4_coa` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_alt_ipv4_coa` + * - :attr:`~pcapkit.const.mh.option.Option.Mobile_Node_Group_Identifier` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_mn_group` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_mn_group` + * - :attr:`~pcapkit.const.mh.option.Option.MAG_IPv6_Address` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_mag_addr` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_mag_addr` + * - :attr:`~pcapkit.const.mh.option.Option.Access_Network_Identifier` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_ani` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_ani` + * - :attr:`~pcapkit.const.mh.option.Option.IPv4_Traffic_Offload_Selector` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_offload` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_offload` + * - :attr:`~pcapkit.const.mh.option.Option.Dynamic_IP_Multicast_Selector` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_mcast_sel` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_mcast_sel` + * - :attr:`~pcapkit.const.mh.option.Option.Delegated_Mobile_Network_Prefix` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_dmnp` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_dmnp` + * - :attr:`~pcapkit.const.mh.option.Option.Active_Multicast_Subscription_IPv4` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_ams4` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_ams4` + * - :attr:`~pcapkit.const.mh.option.Option.Active_Multicast_Subscription_IPv6` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_ams6` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_ams6` + * - :attr:`~pcapkit.const.mh.option.Option.Quality_of_Service` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_qos` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_qos` + * - :attr:`~pcapkit.const.mh.option.Option.LMA_User_Plane_Address` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_lma_up` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_lma_up` + * - :attr:`~pcapkit.const.mh.option.Option.Multicast_Mobility_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_mcast` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_mcast` + * - :attr:`~pcapkit.const.mh.option.Option.Multicast_Acknowledgement_Option` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_mcast_ack` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_mcast_ack` + * - :attr:`~pcapkit.const.mh.option.Option.LMA_Controlled_MAG_Parameters` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_lcmp` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_lcmp` + * - :attr:`~pcapkit.const.mh.option.Option.MAG_Multipath_Binding` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_mag_mp` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_mag_mp` + * - :attr:`~pcapkit.const.mh.option.Option.MAG_Identifier` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_mag_id` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_mag_id` + * - :attr:`~pcapkit.const.mh.option.Option.Anchored_Prefix` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_anchored` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_anchored` + * - :attr:`~pcapkit.const.mh.option.Option.Local_Prefix` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_local_prefix` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_local_prefix` + * - :attr:`~pcapkit.const.mh.option.Option.Previous_MAAR` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_prev_maar` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_prev_maar` + * - :attr:`~pcapkit.const.mh.option.Option.Serving_MAAR` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_serv_maar` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_serv_maar` + * - :attr:`~pcapkit.const.mh.option.Option.DLIF_Link_Local_Address` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_dlif_lla` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_dlif_lla` + * - :attr:`~pcapkit.const.mh.option.Option.DLIF_Link_Layer_Address` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_dlif_lladdr` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_opt_dlif_lladdr` This class currently supports parsing of the following MH CGA extensions, which are registered in the :attr:`self.__extension__ ` @@ -509,9 +1116,18 @@ class MH(Internet[Data_MH, Schema_MH], * - CGA Extension Code - CGA Extension Parser - CGA Extension Constructor - * - :attr:`~pcapkit.const.mh.extension.cga_extension.CGAExtension.Multi_Prefix` + * - :attr:`~pcapkit.const.mh.cga_extension.CGAExtension.Multi_Prefix` - :meth:`~pcapkit.protocols.internet.mh.MH._read_ext_multiprefix` - :meth:`~pcapkit.protocols.internet.mh.MH._make_ext_multiprefix` + * - :attr:`~pcapkit.const.mh.cga_extension.CGAExtension.Exp_FFFD` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_ext_exp` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_ext_exp` + * - :attr:`~pcapkit.const.mh.cga_extension.CGAExtension.Exp_FFFE` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_ext_exp` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_ext_exp` + * - :attr:`~pcapkit.const.mh.cga_extension.CGAExtension.Exp_FFFF` + - :meth:`~pcapkit.protocols.internet.mh.MH._read_ext_exp` + - :meth:`~pcapkit.protocols.internet.mh.MH._make_ext_exp` """ @@ -541,6 +1157,16 @@ class MH(Internet[Data_MH, Schema_MH], Enum_Packet.Experimental_Mobility_Header: 'emh', Enum_Packet.Handover_Initiate_Message: 'hi', Enum_Packet.Handover_Acknowledge_Message: 'hack', + Enum_Packet.Home_Agent_Switch_Message: 'has', + Enum_Packet.Heartbeat_Message: 'hb', + Enum_Packet.Binding_Revocation_Message: 'brm', + Enum_Packet.Localized_Routing_Initiation: 'lri', + Enum_Packet.Localized_Routing_Acknowledgment: 'lra', + Enum_Packet.Update_Notification: 'upn', + Enum_Packet.Update_Notification_Acknowledgement: 'upa', + Enum_Packet.Flow_Binding_Message: 'fbm', + Enum_Packet.Subscription_Query: 'sq', + Enum_Packet.Subscription_Response: 'sr', }, ) # type: DefaultDict[Enum_Packet | int, str | tuple[PacketParser, PacketConstructor]] @@ -572,6 +1198,57 @@ class MH(Internet[Data_MH, Schema_MH], Enum_Option.Experimental_Mobility_Option: 'exp', Enum_Option.Binding_Authorization_Data_for_FMIPv6: 'badf', Enum_Option.Mobility_Header_IPv6_Address_Prefix: 'ipv6_ap', + Enum_Option.DNS_UPDATE_TYPE: 'dns', + Enum_Option.Vendor_Specific_Mobility_Option: 'vendor', + Enum_Option.Service_Selection_Mobility_Option: 'service', + Enum_Option.Home_Network_Prefix_Option: 'hnp', + Enum_Option.Handoff_Indicator_Option: 'hi', + Enum_Option.Access_Technology_Type_Option: 'att', + Enum_Option.Mobile_Node_Link_layer_Identifier_Option: 'mn_lli', + Enum_Option.Link_local_Address_Option: 'lla_addr', + Enum_Option.Timestamp_Option: 'timestamp', + Enum_Option.Restart_Counter: 'restart', + Enum_Option.IPv4_Home_Address: 'ipv4_hoa', + Enum_Option.IPv4_Address_Acknowledgement: 'ipv4_ack', + Enum_Option.NAT_Detection: 'nat', + Enum_Option.IPv4_Care_of_Address: 'ipv4_coa', + Enum_Option.GRE_Key_Option: 'gre', + Enum_Option.Binding_Identifier: 'bid', + Enum_Option.IPv4_Home_Address_Request: 'ipv4_hoa_req', + Enum_Option.IPv4_Home_Address_Reply: 'ipv4_hoa_rep', + Enum_Option.IPv4_Default_Router_Address: 'ipv4_router', + Enum_Option.IPv4_DHCP_Support_Mode: 'ipv4_dhcp', + Enum_Option.Context_Request_Option: 'cr', + Enum_Option.Local_Mobility_Anchor_Address_Option: 'lmaa', + Enum_Option.Mobile_Node_Link_local_Address_Interface_Identifier_Option: 'mn_lla_iid', + Enum_Option.Transient_Binding: 'transient', + Enum_Option.Flow_Summary_Mobility_Option: 'fs', + Enum_Option.Flow_Identification_Mobility_Option: 'fid', + Enum_Option.Redirect_Capability_Mobility_Option: 'rc', + Enum_Option.Redirect_Mobility_Option: 'redirect', + Enum_Option.Load_Information_Mobility_Option: 'load', + Enum_Option.Alternate_IPv4_Care_of_Address: 'alt_ipv4_coa', + Enum_Option.Mobile_Node_Group_Identifier: 'mn_group', + Enum_Option.MAG_IPv6_Address: 'mag_addr', + Enum_Option.Access_Network_Identifier: 'ani', + Enum_Option.IPv4_Traffic_Offload_Selector: 'offload', + Enum_Option.Dynamic_IP_Multicast_Selector: 'mcast_sel', + Enum_Option.Delegated_Mobile_Network_Prefix: 'dmnp', + Enum_Option.Active_Multicast_Subscription_IPv4: 'ams4', + Enum_Option.Active_Multicast_Subscription_IPv6: 'ams6', + Enum_Option.Quality_of_Service: 'qos', + Enum_Option.LMA_User_Plane_Address: 'lma_up', + Enum_Option.Multicast_Mobility_Option: 'mcast', + Enum_Option.Multicast_Acknowledgement_Option: 'mcast_ack', + Enum_Option.LMA_Controlled_MAG_Parameters: 'lcmp', + Enum_Option.MAG_Multipath_Binding: 'mag_mp', + Enum_Option.MAG_Identifier: 'mag_id', + Enum_Option.Anchored_Prefix: 'anchored', + Enum_Option.Local_Prefix: 'local_prefix', + Enum_Option.Previous_MAAR: 'prev_maar', + Enum_Option.Serving_MAAR: 'serv_maar', + Enum_Option.DLIF_Link_Local_Address: 'dlif_lla', + Enum_Option.DLIF_Link_Layer_Address: 'dlif_lladdr', }, ) # type: DefaultDict[Enum_Option | int, str | tuple[OptionParser, OptionConstructor]] @@ -584,6 +1261,9 @@ class MH(Internet[Data_MH, Schema_MH], lambda: 'none', { Enum_CGAExtension.Multi_Prefix: 'multiprefix', + Enum_CGAExtension.Exp_FFFD: 'exp', + Enum_CGAExtension.Exp_FFFE: 'exp', + Enum_CGAExtension.Exp_FFFF: 'exp', }, ) # type: DefaultDict[Enum_CGAExtension | int, str | tuple[ExtensionParser, ExtensionConstructor]] @@ -1507,395 +2187,542 @@ def _read_msg_hack(self, schema: 'Schema_HandoverAcknowledgeMessage', *, ) return data - # TODO: Implement other message types. - - def _read_mh_options(self, options_schema: 'list[Schema_Option]') -> 'Option': - """Read MH options. + def _read_msg_has(self, schema: 'Schema_HomeAgentSwitchMessage', *, + header: 'Schema_MH') -> 'Data_HomeAgentSwitchMessage': + """Read MH home agent switch (HAS) message type. - Structure of MH option [:rfc:`6275`]: + Structure of MH Home Agent Switch Message [:rfc:`5142#section-4`]: .. code-block:: text - 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |# of Addresses | Reserved | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Type | Option Length | Option Data... + | | + + + + . . + . Home Agent Addresses . + . . + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + . . + . Mobility Options . + . . + + + + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Note: + The ``# of Addresses`` field is not carried into the data model, + since it is exactly ``len(addresses)`` and a second copy of a length + is a second thing to keep in step. An empty list is meaningful rather + than degenerate: :rfc:`5142#section-4` uses a count of zero to tell + the mobile node to run home agent discovery instead. + Args: - options_schema: Parsed MH options. + schema: Parsed message type schema. + header: Parsed MH header schema. Returns: - Parsed MH options data. + Parsed message type data. """ - options = OrderedMultiDict() # type: Option + if schema.count != len(schema.addresses): + raise ProtocolError(f'{self.alias}: [Type {header.type}] invalid format') - for schema in options_schema: - type = schema.type - name = self._lookup_registry(self.__option__, type) - - if isinstance(name, str): - meth_name = f'_read_opt_{name}' - meth = cast('OptionParser', - getattr(self, meth_name, self._read_opt_none)) - else: - meth = name[0] - data = meth(schema, options=options) - - # record option data - options.add(type, data) - - return options + data = Data_HomeAgentSwitchMessage( + next=header.next, + length=(header.length + 1) * 8, + type=header.type, + chksum=header.chksum, + addresses=tuple(schema.addresses), + options=self._read_mh_options(schema.options), + ) + return data - def _read_opt_none(self, schema: 'Schema_UnassignedOption', *, - options: 'Option') -> 'Data_UnassignedOption': - """Read MH unassigned option. + def _read_msg_hb(self, schema: 'Schema_HeartbeatMessage', *, + header: 'Schema_MH') -> 'Data_HeartbeatMessage': + """Read MH heartbeat message type. - Args: - schema: Parsed option schema. - options: Parsed MH options. + Structure of MH Heartbeat Message [:rfc:`5847#section-3.3`]: - Returns: - Constructed option data. + .. code-block:: text + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Reserved |U|R| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sequence Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + . . + . Mobility Options . + . . + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The ``U`` and ``R`` flags are the two **least** significant bits of + the first half-word, not the most significant ones the other mobility + messages put their flags in [:rfc:`5847#section-3.3`]. The sequence + number is 32 bits wide, also unlike the 16-bit ones elsewhere. + + Args: + schema: Parsed message type schema. + header: Parsed MH header schema. + + Returns: + Parsed message type data. """ - data = Data_UnassignedOption( - type=schema.type, - length=schema.length + 2, - data=schema.data, + data = Data_HeartbeatMessage( + next=header.next, + length=(header.length + 1) * 8, + type=header.type, + chksum=header.chksum, + unsolicited=bool(schema.flags['U']), + response=bool(schema.flags['R']), + seq=schema.seq, + options=self._read_mh_options(schema.options), ) return data - def _read_opt_pad(self, schema: 'Schema_PadOption', *, - options: 'Option') -> 'Data_PadOption': - """Read MH padding option. + def _read_msg_brm(self, schema: 'Schema_BindingRevocationMessage', *, + header: 'Schema_MH') -> 'Data_BindingRevocationMessage': + """Read MH binding revocation (BRM) message type. - Structure of MH padding option [:rfc:`6275`]: + Structure of MH Binding Revocation Indication Message + [:rfc:`5846#section-5.1`]: - * ``Pad1`` option: + .. code-block:: text - .. code-block:: text + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | B.R. Type = 1 | R. Trigger | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sequence # |P|V|G| Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + . . + . Mobility options . + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 0 - 0 1 2 3 4 5 6 7 - +-+-+-+-+-+-+-+-+ - | Type = 0 | - +-+-+-+-+-+-+-+-+ + Structure of MH Binding Revocation Acknowledgement Message + [:rfc:`5846#section-5.2`]: - * ``PadN`` option: + .. code-block:: text - .. code-block:: text + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | B.R. Type = 2 | Status | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sequence # |P|V|G| Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + . . + . Mobility options . + . . + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 0 1 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - - - | Type = 1 | Option Length | Option Data - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - - + Note: + One Mobility Header type carries both forms, told apart by the + ``B.R. Type`` octet **inside** the message rather than by the header + type. The two have identical geometry, and the only field whose + meaning differs is the octet after it -- a revocation trigger in an + indication and a status code in an acknowledgement, drawn from two + different registries -- so a single data model carries both, with + :attr:`~pcapkit.protocols.data.internet.mh.BindingRevocationMessage.br_type` + saying which it is. Args: - schema: Parsed option schema. - options: Parsed MH options. + schema: Parsed message type schema. + header: Parsed MH header schema. Returns: - Constructed option data. + Parsed message type data. """ - code, clen = schema.type, schema.length - - if code not in (Enum_Option.Pad1, Enum_Option.PadN): - raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid format') - if code == Enum_Option.Pad1 and clen != 0: - raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid format') - if code == Enum_Option.PadN and clen == 0: - raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid format') - - if code == Enum_Option.Pad1: - size = 1 - else: - size = clen + 2 - - data = Data_PadOption( - type=schema.type, - length=size, + data = Data_BindingRevocationMessage( + next=header.next, + length=(header.length + 1) * 8, + type=header.type, + chksum=header.chksum, + br_type=schema.br_type, + code=schema.code, + seq=schema.seq, + proxy=bool(schema.flags['P']), + ipv4_hoa=bool(schema.flags['V']), + global_revocation=bool(schema.flags['G']), + options=self._read_mh_options(schema.options), ) return data - def _read_opt_bra(self, schema: 'Schema_BindingRefreshAdviceOption', *, - options: 'Option') -> 'Data_BindingRefreshAdviceOption': - """Read MH binding refresh advice option. + def _read_msg_lri(self, schema: 'Schema_LocalizedRoutingInitiationMessage', *, + header: 'Schema_MH') -> 'Data_LocalizedRoutingInitiationMessage': + """Read MH localized routing initiation (LRI) message type. - Structure of MH Binding Refresh Advice option [:rfc:`6275`]: + Structure of MH Localized Routing Initiation Message + [:rfc:`6705#section-10.1`]: .. code-block:: text - 0 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type = 2 | Length = 2 | + | Sequence # | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Refresh Interval | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Reserved | Lifetime | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + . . + . Mobility options . + . . + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The lifetime is in **seconds** here, not in the units of 4 seconds + that :rfc:`6275` uses for the binding messages + [:rfc:`6705#section-10.1`], so it is not scaled on the way in. Args: - schema: Parsed option schema. - options: Parsed MH options. + schema: Parsed message type schema. + header: Parsed MH header schema. Returns: - Constructed option data. + Parsed message type data. """ - if schema.length != 2: - raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - - data = Data_BindingRefreshAdviceOption( - type=schema.type, - length=schema.length + 2, - interval=schema.interval, + data = Data_LocalizedRoutingInitiationMessage( + next=header.next, + length=(header.length + 1) * 8, + type=header.type, + chksum=header.chksum, + seq=schema.seq, + lifetime=datetime.timedelta(seconds=schema.lifetime), + options=self._read_mh_options(schema.options), ) return data - def _read_opt_aca(self, schema: 'Schema_AlternateCareofAddressOption', *, - options: 'Option') -> 'Data_AlternateCareofAddressOption': - """Read MH alternate care-of address option. + def _read_msg_lra(self, schema: 'Schema_LocalizedRoutingAcknowledgmentMessage', *, + header: 'Schema_MH') -> 'Data_LocalizedRoutingAcknowledgmentMessage': + """Read MH localized routing acknowledgment (LRA) message type. - Structure of MH Alternate Care-of Address option [:rfc:`6275`]: + Structure of MH Localized Routing Acknowledgment Message + [:rfc:`6705#section-10.2`]: .. code-block:: text - 0 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type = 3 | Length = 16 | + | Sequence # | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |U| Reserved | Status | Lifetime | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | - + + - | | - + Alternate Care-of Address + - | | - + + + . . + . Mobility options . + . . | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + The status codes are enumerated in + :class:`~pcapkit.protocols.internet.mh.LocalizedRoutingStatus`, which is + local to this module because :rfc:`6705` defines them inline and IANA + registers them nowhere -- not even in the general *Status Codes* + registry. + Args: - schema: Parsed option schema. - options: Parsed MH options. + schema: Parsed message type schema. + header: Parsed MH header schema. Returns: - Constructed option data. + Parsed message type data. """ - if schema.length != 16: - raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - - data = Data_AlternateCareofAddressOption( - type=schema.type, - length=schema.length + 2, - address=schema.address, + data = Data_LocalizedRoutingAcknowledgmentMessage( + next=header.next, + length=(header.length + 1) * 8, + type=header.type, + chksum=header.chksum, + seq=schema.seq, + unsolicited=bool(schema.flags['U']), + status=LocalizedRoutingStatus(schema.status), + lifetime=datetime.timedelta(seconds=schema.lifetime), + options=self._read_mh_options(schema.options), ) return data - def _read_opt_ni(self, schema: 'Schema_NonceIndicesOption', *, - options: 'Option') -> 'Data_NonceIndicesOption': - """Read MH nonce indices option. + def _read_msg_upn(self, schema: 'Schema_UpdateNotificationMessage', *, + header: 'Schema_MH') -> 'Data_UpdateNotificationMessage': + """Read MH update notification (UPN) message type. - Structure of MH Nonce Indices option [:rfc:`6275`]: + Structure of MH Update Notification Message [:rfc:`7077#section-4.1`]: .. code-block:: text - 0 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type = 4 | Length = 4 | + | Sequence # | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Home Nonce Index | Care-of Nonce Index | + | Notification Reason |A|D| Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + . . + . Mobility options . + . . + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Note: + The notification reason is **16** bits wide, not the 8 that a status + octet elsewhere in the Mobility Header would be. + Args: - schema: Parsed option schema. - options: Parsed MH options. + schema: Parsed message type schema. + header: Parsed MH header schema. Returns: - Constructed option data. + Parsed message type data. """ - if schema.length != 4: - raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - - data = Data_NonceIndicesOption( - type=schema.type, - length=schema.length + 2, - home=schema.home, - careof=schema.careof, + data = Data_UpdateNotificationMessage( + next=header.next, + length=(header.length + 1) * 8, + type=header.type, + chksum=header.chksum, + seq=schema.seq, + reason=schema.reason, + ack=bool(schema.flags['A']), + retransmit=bool(schema.flags['D']), + options=self._read_mh_options(schema.options), ) return data - def _read_opt_bad(self, schema: 'Schema_AuthorizationDataOption', *, - options: 'Option') -> 'Data_AuthorizationDataOption': - """Read MH binding authorization data option. + def _read_msg_upa(self, schema: 'Schema_UpdateNotificationAcknowledgementMessage', *, + header: 'Schema_MH') -> 'Data_UpdateNotificationAcknowledgementMessage': + """Read MH update notification acknowledgement (UPA) message type. - Structure of MH Binding Authorization Data option [:rfc:`6275`]: + Structure of MH Update Notification Acknowledgement Message + [:rfc:`7077#section-4.2`]: .. code-block:: text - 0 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type = 5 | Option Length | + | Sequence # | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Status Code | Reserved | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | - + + - | Authenticator | - + + + . . + . Mobility options . + . . | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - schema: Parsed option schema. - options: Parsed MH options. + schema: Parsed message type schema. + header: Parsed MH header schema. Returns: - Constructed option data. + Parsed message type data. """ - if schema.length % 8 != 0: - raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - - data = Data_AuthorizationDataOption( - type=schema.type, - length=schema.length + 2, - data=schema.data, + data = Data_UpdateNotificationAcknowledgementMessage( + next=header.next, + length=(header.length + 1) * 8, + type=header.type, + chksum=header.chksum, + seq=schema.seq, + status=schema.status, + options=self._read_mh_options(schema.options), ) return data - def _read_opt_mnp(self, schema: 'Schema_MobileNetworkPrefixOption', *, - options: 'Option') -> 'Data_MobileNetworkPrefixOption': - """Read MH mobile network prefix option. + def _read_msg_fbm(self, schema: 'Schema_FlowBindingMessage', *, + header: 'Schema_MH') -> 'Data_FlowBindingMessage': + """Read MH flow binding (FB) message type. - Structure of MH Mobile Network Prefix option [:rfc:`3963`]: + Structure of MH Flow Binding Indication Message + [:rfc:`7109#section-6.1.1`]: .. code-block:: text - 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Flow Binding Type = 1 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Length | Reserved | Prefix Length | + | Sequence # | Trigger |A| Reserved | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | - + + - | | - + Mobile Network Prefix + - | | - + + + . . + . Mobility options . | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - Args: - schema: Parsed option schema. - options: Parsed MH options. + Structure of MH Flow Binding Acknowledgement Message + [:rfc:`7109#section-6.1.2`]: - Returns: - Constructed option data. + .. code-block:: text - """ - if schema.length != 18: - raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Flow Binding Type = 2 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sequence # | Status | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + . . + . Mobility options . + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - prefix = cast('IPv6Network', - ipaddress.ip_network((schema.prefix, schema.prefix_length))) + Note: + As with the binding revocation message, one Mobility Header type + carries both forms and the ``Flow Binding Type`` field inside the + message says which. The ``A`` flag exists only in the indication; the + acknowledgement reserves the whole octet it is taken from, so a + well-formed acknowledgement reads back as ``ack=False``. - data = Data_MobileNetworkPrefixOption( - type=schema.type, - length=schema.length + 2, - prefix=prefix, + Args: + schema: Parsed message type schema. + header: Parsed MH header schema. + + Returns: + Parsed message type data. + + """ + data = Data_FlowBindingMessage( + next=header.next, + length=(header.length + 1) * 8, + type=header.type, + chksum=header.chksum, + fb_type=schema.fb_type, + seq=schema.seq, + code=schema.code, + ack=bool(schema.flags['A']), + options=self._read_mh_options(schema.options), ) return data - def _read_opt_lla(self, schema: 'Schema_LinkLayerAddressOption', *, - options: 'Option') -> 'Data_LinkLayerAddressOption': - """Read MH link-layer address (MH-LLA) option. + def _read_msg_sq(self, schema: 'Schema_SubscriptionQueryMessage', *, + header: 'Schema_MH') -> 'Data_SubscriptionQueryMessage': + """Read MH subscription query (SQ) message type. - Structure of MH Link-Layer Address option [:rfc:`5568`]: + Structure of MH Subscription Query Message [:rfc:`7161#section-4.3.1.2`]: .. code-block:: text - 0 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 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option-Code | LLA .... - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sequence # | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + . . + . Mobility Options . + . . + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The sequence number is a single octet, counted modulo 256, unlike the + 16-bit ones of most other mobility messages + [:rfc:`7161#section-4.3.1.2`]. Args: - schema: Parsed option schema. - options: Parsed MH options. + schema: Parsed message type schema. + header: Parsed MH header schema. Returns: - Constructed option data. + Parsed message type data. """ - if schema.code != Enum_LLACode.MH: - raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - - data = Data_LinkLayerAddressOption( - type=schema.type, - length=schema.length + 2, - code=schema.code, - lla=schema.lla, + data = Data_SubscriptionQueryMessage( + next=header.next, + length=(header.length + 1) * 8, + type=header.type, + chksum=header.chksum, + seq=schema.seq, + options=self._read_mh_options(schema.options), ) return data - def _read_opt_mn_id(self, schema: 'Schema_MNIDOption', *, - options: 'Option') -> 'Data_MNIDOption': - """Read MH mobile node identifier option. + def _read_msg_sr(self, schema: 'Schema_SubscriptionResponseMessage', *, + header: 'Schema_MH') -> 'Data_SubscriptionResponseMessage': + """Read MH subscription response (SR) message type. - Structure of MH Mobile Node Identifier option [:rfc:`4283`]: + Structure of MH Subscription Response Message + [:rfc:`7161#section-4.3.2.2`]: .. code-block:: text - 0 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Type | Option Length | + | Sequence # |I| Reserved | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Subtype | Identifier ... + | | + . . + . Mobility Options . + . . + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - schema: Parsed option schema. - options: Parsed MH options. + schema: Parsed message type schema. + header: Parsed MH header schema. Returns: - Constructed option data. + Parsed message type data. """ - data = Data_MNIDOption( - type=schema.type, - length=schema.length + 2, - subtype=schema.subtype, - identifier=schema.identifier, + data = Data_SubscriptionResponseMessage( + next=header.next, + length=(header.length + 1) * 8, + type=header.type, + chksum=header.chksum, + seq=schema.seq, + info=bool(schema.flags['I']), + options=self._read_mh_options(schema.options), ) return data - def _read_opt_auth(self, schema: 'Schema_AuthOption', *, - options: 'Option') -> 'Data_AuthOption': - """Read MH mobility message authentication option. + def _read_mh_options(self, options_schema: 'list[Schema_Option]') -> 'Option': + """Read MH options. - Structure of MH Mobility Message Authentication option [:rfc:`4285`]: + Structure of MH option [:rfc:`6275`]: .. code-block:: text - 0 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 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Type | Option Length | Subtype | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Mobility SPI | + 0 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Authentication Data .... + | Option Type | Option Length | Option Data... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Args: + options_schema: Parsed MH options. + + Returns: + Parsed MH options data. + + """ + options = OrderedMultiDict() # type: Option + + for schema in options_schema: + type = schema.type + name = self._lookup_registry(self.__option__, type) + + if isinstance(name, str): + meth_name = f'_read_opt_{name}' + meth = cast('OptionParser', + getattr(self, meth_name, self._read_opt_none)) + else: + meth = name[0] + data = meth(schema, options=options) + + # record option data + options.add(type, data) + + return options + + def _read_opt_none(self, schema: 'Schema_UnassignedOption', *, + options: 'Option') -> 'Data_UnassignedOption': + """Read MH unassigned option. + Args: schema: Parsed option schema. options: Parsed MH options. @@ -1904,35 +2731,38 @@ def _read_opt_auth(self, schema: 'Schema_AuthOption', *, Constructed option data. """ - if (schema.length + 1) % 4 != 0: - raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - - data = Data_AuthOption( + data = Data_UnassignedOption( type=schema.type, length=schema.length + 2, - subtype=schema.subtype, - spi=schema.spi, data=schema.data, ) return data - def _read_opt_mesg_id(self, schema: 'Schema_MesgIDOption', *, - options: 'Option') -> 'Data_MesgIDOption': - """Read MH mobility message replay protection option. + def _read_opt_pad(self, schema: 'Schema_PadOption', *, + options: 'Option') -> 'Data_PadOption': + """Read MH padding option. - Structure of MH Mobility Message Replay Protection option [:rfc:`4285`]: + Structure of MH padding option [:rfc:`6275`]: - .. code-block:: text + * ``Pad1`` option: - 0 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 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Type | Option Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Timestamp ... | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Timestamp | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + .. code-block:: text + + 0 + 0 1 2 3 4 5 6 7 + +-+-+-+-+-+-+-+-+ + | Type = 0 | + +-+-+-+-+-+-+-+-+ + + * ``PadN`` option: + + .. code-block:: text + + 0 1 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - - + | Type = 1 | Option Length | Option Data + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - - Args: schema: Parsed option schema. @@ -1942,30 +2772,41 @@ def _read_opt_mesg_id(self, schema: 'Schema_MesgIDOption', *, Constructed option data. """ - if (schema.length) % 8 != 0: - raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + code, clen = schema.type, schema.length - data = Data_MesgIDOption( + if code not in (Enum_Option.Pad1, Enum_Option.PadN): + raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid format') + if code == Enum_Option.Pad1 and clen != 0: + raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid format') + if code == Enum_Option.PadN and clen == 0: + raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid format') + + if code == Enum_Option.Pad1: + size = 1 + else: + size = clen + 2 + + data = Data_PadOption( type=schema.type, - length=schema.length + 2, - timestamp=schema.timestamp, - ntp_timestamp=NTPTimestamp(schema.seconds, schema.fraction), + length=size, ) return data - def _read_opt_cga_pr(self, schema: 'Schema_CGAParametersRequestOption', *, - options: 'Option') -> 'Data_CGAParametersRequestOption': - """Read MH CGA parameters request option. + def _read_opt_bra(self, schema: 'Schema_BindingRefreshAdviceOption', *, + options: 'Option') -> 'Data_BindingRefreshAdviceOption': + """Read MH binding refresh advice option. - Structure of MH CGA Parameters Request option [:rfc:`4866`]: + Structure of MH Binding Refresh Advice option [:rfc:`6275`]: .. code-block:: text 0 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Type | Option Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type = 2 | Length = 2 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Refresh Interval | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: schema: Parsed option schema. @@ -1975,32 +2816,35 @@ def _read_opt_cga_pr(self, schema: 'Schema_CGAParametersRequestOption', *, Constructed option data. """ - if schema.length != 0: + if schema.length != 2: raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - data = Data_CGAParametersRequestOption( + data = Data_BindingRefreshAdviceOption( type=schema.type, length=schema.length + 2, + interval=schema.interval, ) return data - def _read_opt_cga_param(self, schema: 'Schema_CGAParametersOption', *, - options: 'Option') -> 'Data_CGAParametersOption': - """Read MH CGA parameters option. + def _read_opt_aca(self, schema: 'Schema_AlternateCareofAddressOption', *, + options: 'Option') -> 'Data_AlternateCareofAddressOption': + """Read MH alternate care-of address option. - Structure of MH CGA Parameters option [:rfc:`4866`]: + Structure of MH Alternate Care-of Address option [:rfc:`6275`]: .. code-block:: text 0 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Type | Option Length | + | Type = 3 | Length = 16 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | - : : - : CGA Parameters : - : : + + + + | | + + Alternate Care-of Address + + | | + + + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ @@ -2012,41 +2856,30 @@ def _read_opt_cga_param(self, schema: 'Schema_CGAParametersOption', *, Constructed option data. """ - for param in schema.parameters: - if param.collision_count not in (0, 1, 2): - raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - - data = Data_CGAParametersOption( + if schema.length != 16: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_AlternateCareofAddressOption( type=schema.type, length=schema.length + 2, - parameters=tuple(Data_CGAParameter( - modifier=param.modifier, - prefix=param.prefix, - collision_count=param.collision_count, - public_key=param.public_key, - extensions=self._read_cga_extensions(param.extensions), - ) for param in schema.parameters), + address=schema.address, ) return data - def _read_opt_signature(self, schema: 'Schema_SignatureOption', *, - options: 'Option') -> 'Data_SignatureOption': - """Read MH signature option. + def _read_opt_ni(self, schema: 'Schema_NonceIndicesOption', *, + options: 'Option') -> 'Data_NonceIndicesOption': + """Read MH nonce indices option. - Structure of MH Signature option [:rfc:`4866`]: + Structure of MH Nonce Indices option [:rfc:`6275`]: .. code-block:: text 0 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Type | Option Length | + | Type = 4 | Length = 4 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - : : - : Signature : - : : - | | + | Home Nonce Index | Care-of Nonce Index | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: @@ -2057,30 +2890,34 @@ def _read_opt_signature(self, schema: 'Schema_SignatureOption', *, Constructed option data. """ - data = Data_SignatureOption( + if schema.length != 4: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_NonceIndicesOption( type=schema.type, length=schema.length + 2, - signature=schema.signature, + home=schema.home, + careof=schema.careof, ) return data - def _read_opt_phkt(self, schema: 'Schema_PermanentHomeKeygenTokenOption', *, - options: 'Option') -> 'Data_PermanentHomeKeygenTokenOption': - """Read MH permanent home keygen token option. + def _read_opt_bad(self, schema: 'Schema_AuthorizationDataOption', *, + options: 'Option') -> 'Data_AuthorizationDataOption': + """Read MH binding authorization data option. - Structure of MH Permanent Home Keygen Token option [:rfc:`4866`]: + Structure of MH Binding Authorization Data option [:rfc:`6275`]: .. code-block:: text 0 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Type | Option Length | + | Type = 5 | Option Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | - : : - : Permanent Home Keygen Token : - : : + + + + | Authenticator | + + + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ @@ -2092,26 +2929,37 @@ def _read_opt_phkt(self, schema: 'Schema_PermanentHomeKeygenTokenOption', *, Constructed option data. """ - data = Data_PermanentHomeKeygenTokenOption( + if schema.length % 8 != 0: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_AuthorizationDataOption( type=schema.type, length=schema.length + 2, - token=schema.token, + data=schema.data, ) return data - def _read_opt_ct_init(self, schema: 'Schema_CareofTestInitOption', *, - options: 'Option') -> 'Data_CareofTestInitOption': - """Read MH Care-of Test Init option. + def _read_opt_mnp(self, schema: 'Schema_MobileNetworkPrefixOption', *, + options: 'Option') -> 'Data_MobileNetworkPrefixOption': + """Read MH mobile network prefix option. - Structure of MH Care-of Test Init option [:rfc:`4866`]: + Structure of MH Mobile Network Prefix option [:rfc:`3963`]: .. code-block:: text 0 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 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Type | Option Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved | Prefix Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + Mobile Network Prefix + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: schema: Parsed option schema. @@ -2121,32 +2969,34 @@ def _read_opt_ct_init(self, schema: 'Schema_CareofTestInitOption', *, Constructed option data. """ - if schema.length != 0: + if schema.length != 18: raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - data = Data_CareofTestInitOption( + prefix = cast('IPv6Network', + ipaddress.ip_network((schema.prefix, schema.prefix_length))) + + data = Data_MobileNetworkPrefixOption( type=schema.type, length=schema.length + 2, + prefix=prefix, ) return data - def _read_opt_ct(self, schema: 'Schema_CareofTestOption', *, - options: 'Option') -> 'Data_CareofTestOption': - """Read MH Care-of Test option. + def _read_opt_lla(self, schema: 'Schema_LinkLayerAddressOption', *, + options: 'Option') -> 'Data_LinkLayerAddressOption': + """Read MH link-layer address (MH-LLA) option. - Structure of MH Care-of Test option [:rfc:`4866`]: + Structure of MH Link-Layer Address option [:rfc:`5568`]: .. code-block:: text 0 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 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Type | Option Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - + Care-of Keygen Token + - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option-Code | LLA .... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: schema: Parsed option schema. @@ -2156,28 +3006,31 @@ def _read_opt_ct(self, schema: 'Schema_CareofTestOption', *, Constructed option data. """ - if schema.length != 8: + if schema.code != Enum_LLACode.MH: raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - data = Data_CareofTestOption( + data = Data_LinkLayerAddressOption( type=schema.type, length=schema.length + 2, - token=schema.token, + code=schema.code, + lla=schema.lla, ) return data - def _read_opt_exp(self, schema: 'Schema_ExperimentalMobilityOption', *, - options: 'Option') -> 'Data_ExperimentalMobilityOption': - """Read MH experimental mobility option. + def _read_opt_mn_id(self, schema: 'Schema_MNIDOption', *, + options: 'Option') -> 'Data_MNIDOption': + """Read MH mobile node identifier option. - Structure of MH Experimental Mobility option [:rfc:`5096#section-4`]: + Structure of MH Mobile Node Identifier option [:rfc:`4283`]: .. code-block:: text - 0 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 + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Length | Data ..... + | Subtype | Identifier ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: @@ -2188,49 +3041,32 @@ def _read_opt_exp(self, schema: 'Schema_ExperimentalMobilityOption', *, Constructed option data. """ - data = Data_ExperimentalMobilityOption( + data = Data_MNIDOption( type=schema.type, length=schema.length + 2, - data=schema.data, + subtype=schema.subtype, + identifier=schema.identifier, ) return data - def _read_opt_badf(self, schema: 'Schema_BADFOption', *, - options: 'Option') -> 'Data_BADFOption': - """Read MH binding authorization data for FMIPv6 (BADF) option. + def _read_opt_auth(self, schema: 'Schema_AuthOption', *, + options: 'Option') -> 'Data_AuthOption': + """Read MH mobility message authentication option. - Structure of MH Binding Authorization Data for FMIPv6 option - [:rfc:`5568#section-6.4.5`]: + Structure of MH Mobility Message Authentication option [:rfc:`4285`]: .. code-block:: text - 0 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 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type = 21 | Option Length | + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Length | Subtype | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | SPI | + | Mobility SPI | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - + + - | Authenticator | - + + - | | + | Authentication Data .... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - Note: - :rfc:`5568#section-6.4.5` defines the option length as *the length of - the Authenticator in bytes*, i.e. it does **not** cover the 4-byte - SPI, unlike every other mobility option, whose length covers all of - its option data. The wording is inherited from the :rfc:`6275` - binding authorization data option (type ``5``), which carries no SPI - and for which the two readings coincide. Because :rfc:`5568` also - requires this option to be the **last** mobility option present, the - discrepancy never has to be resolved in order to find the following - option, so the literal reading is used here and the reported - :attr:`~pcapkit.protocols.data.internet.mh.Option.length` accounts - for the extra 4 bytes. - Args: schema: Parsed option schema. options: Parsed MH options. @@ -2239,52 +3075,36 @@ def _read_opt_badf(self, schema: 'Schema_BADFOption', *, Constructed option data. """ - if schema.length == 0: + if (schema.length + 1) % 4 != 0: raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - data = Data_BADFOption( + data = Data_AuthOption( type=schema.type, - length=schema.length + 6, # 2 bytes for type & length, 4 bytes for SPI + length=schema.length + 2, + subtype=schema.subtype, spi=schema.spi, data=schema.data, ) return data - def _read_opt_ipv6_ap(self, schema: 'Schema_IPv6AddressPrefixOption', *, - options: 'Option') -> 'Data_IPv6AddressPrefixOption': - """Read MH mobility header IPv6 address/prefix option. + def _read_opt_mesg_id(self, schema: 'Schema_MesgIDOption', *, + options: 'Option') -> 'Data_MesgIDOption': + """Read MH mobility message replay protection option. - Structure of MH Mobility Header IPv6 Address/Prefix option - [:rfc:`5568#section-6.4.2`]: + Structure of MH Mobility Message Replay Protection option [:rfc:`4285`]: .. code-block:: text - 0 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 + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Length | Option-Code | Prefix Length | + | Timestamp ... | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - + + - | | - + IPv6 Address/Prefix + - | | - + + - | | + | Timestamp | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - The option code identifies which address the option carries, c.f., - :class:`~pcapkit.protocols.internet.mh.IPv6AddressPrefixCode`. It, too, - is defined inline by :rfc:`5568#section-6.4.2` with no IANA registry - behind it, hence the enumeration is local to this module. - - Note: - :rfc:`5568` prints the type as ``17``, which is the *neighbor - discovery* option type of its sibling in - :rfc:`5568#section-6.4.1`. Errata ID 1816 (verified) corrects it to - the IANA-assigned mobility option type ``34``, which is what this - handler is registered against. - Args: schema: Parsed option schema. options: Parsed MH options. @@ -2293,859 +3113,6158 @@ def _read_opt_ipv6_ap(self, schema: 'Schema_IPv6AddressPrefixOption', *, Constructed option data. """ - if schema.length != 18: - raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - if schema.prefix_length > 128: + if (schema.length) % 8 != 0: raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - data = Data_IPv6AddressPrefixOption( + data = Data_MesgIDOption( type=schema.type, length=schema.length + 2, - code=IPv6AddressPrefixCode(schema.code), - prefix_length=schema.prefix_length, - address=schema.address, + timestamp=schema.timestamp, + ntp_timestamp=NTPTimestamp(schema.seconds, schema.fraction), ) return data - # TODO: Implement other options. - - def _read_cga_extensions(self, extensions_schema: 'list[Schema_CGAExtension]') -> 'Extension': - """Read CGA extensions. + def _read_opt_cga_pr(self, schema: 'Schema_CGAParametersRequestOption', *, + options: 'Option') -> 'Data_CGAParametersRequestOption': + """Read MH CGA parameters request option. - Structure of CGA extensions [:rfc:`4581`]: + Structure of MH CGA Parameters Request option [:rfc:`4866`]: .. code-block:: text 0 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 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Extension Type | Extension Data Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - ~ Extension Data ~ - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - extensions_schema: Parsed CGA extensions. + schema: Parsed option schema. + options: Parsed MH options. Returns: - Parsed CGA extensions data. + Constructed option data. """ - extensions = OrderedMultiDict() # type: Extension - - for schema in extensions_schema: - type = schema.type - name = self._lookup_registry(self.__extension__, type) + if schema.length != 0: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - if isinstance(name, str): - meth_name = f'_read_ext_{name}' - meth = cast('ExtensionParser', - getattr(self, meth_name, self._read_ext_none)) - else: - meth = name[0] - data = meth(schema, extensions=extensions) + data = Data_CGAParametersRequestOption( + type=schema.type, + length=schema.length + 2, + ) + return data - # record extension data - extensions.add(type, data) + def _read_opt_cga_param(self, schema: 'Schema_CGAParametersOption', *, + options: 'Option') -> 'Data_CGAParametersOption': + """Read MH CGA parameters option. - return extensions + Structure of MH CGA Parameters option [:rfc:`4866`]: - def _read_ext_none(self, schema: 'Schema_UnknownExtension', *, - extensions: 'Extension') -> 'Data_UnknownExtension': - """Read unknown CGA extension. + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + : : + : CGA Parameters : + : : + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - schema: Parsed extension schema. - extensions: Parsed MH CGA extensions. + schema: Parsed option schema. + options: Parsed MH options. Returns: - Constructed extension data. + Constructed option data. """ - data = Data_UnknownExtension( + for param in schema.parameters: + if param.collision_count not in (0, 1, 2): + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_CGAParametersOption( type=schema.type, length=schema.length + 2, - data=schema.data, + parameters=tuple(Data_CGAParameter( + modifier=param.modifier, + prefix=param.prefix, + collision_count=param.collision_count, + public_key=param.public_key, + extensions=self._read_cga_extensions(param.extensions), + ) for param in schema.parameters), ) return data - def _read_ext_multiprefix(self, schema: 'Schema_MultiPrefixExtension', *, - extensions: 'Extension') -> 'Data_MultiPrefixExtension': - """Read multi-prefix CGA extension. + def _read_opt_signature(self, schema: 'Schema_SignatureOption', *, + options: 'Option') -> 'Data_SignatureOption': + """Read MH signature option. - Structure of Multi-Prefix CGA extension [:rfc:`5535`]: + Structure of MH Signature option [:rfc:`4866`]: .. code-block:: text 0 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 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Extension Type | Extension Data Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |P| Reserved | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - + Prefix[1] + - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - + Prefix[2] + - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - . . . - . . . - . . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | - + Prefix[n] + + : : + : Signature : + : : | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - schema: Parsed extension schema. - extensions: Parsed MH CGA extensions. + schema: Parsed option schema. + options: Parsed MH options. Returns: - Constructed extension data. + Constructed option data. """ - data = Data_MultiPrefixExtension( + data = Data_SignatureOption( type=schema.type, length=schema.length + 2, - flag=bool(schema.flags['P']), - prefixes=tuple(schema.prefixes), + signature=schema.signature, ) return data - # TODO: Implement other CGA extensions. + def _read_opt_phkt(self, schema: 'Schema_PermanentHomeKeygenTokenOption', *, + options: 'Option') -> 'Data_PermanentHomeKeygenTokenOption': + """Read MH permanent home keygen token option. + + Structure of MH Permanent Home Keygen Token option [:rfc:`4866`]: - def _make_msg_unknown(self, message: 'Optional[Data_UnknownMessage]' = None, *, - data: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_UnknownMessage': - """Make MH unknown message type. + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + : : + : Permanent Home Keygen Token : + : : + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - message: Message data model. - data: Raw message data. - **kwargs: Arbitrary keyword arguments. + schema: Parsed option schema. + options: Parsed MH options. Returns: - Constructed message type. + Constructed option data. """ - if message is not None: - data = message.data - - return Schema_UnknownMessage( - data=data, + data = Data_PermanentHomeKeygenTokenOption( + type=schema.type, + length=schema.length + 2, + token=schema.token, ) + return data - def _make_msg_brr(self, message: 'Optional[Data_BindingRefreshRequestMessage]' = None, *, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, - **kwargs: 'Any') -> 'Schema_BindingRefreshRequestMessage': - """Make MH binding refresh request (BRR) message type. + def _read_opt_ct_init(self, schema: 'Schema_CareofTestInitOption', *, + options: 'Option') -> 'Data_CareofTestInitOption': + """Read MH Care-of Test Init option. + + Structure of MH Care-of Test Init option [:rfc:`4866`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - message: Message data model. - options: Mobility options. - **kwargs: Arbitrary keyword arguments. + schema: Parsed option schema. + options: Parsed MH options. Returns: - Constructed message type. + Constructed option data. """ - if message is not None: - options = message.options - else: - options = options or [] + if schema.length != 0: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - return Schema_BindingRefreshRequestMessage( - options=self._make_mh_options(options), + data = Data_CareofTestInitOption( + type=schema.type, + length=schema.length + 2, ) + return data - def _make_msg_hoti(self, message: 'Optional[Data_HomeTestInitMessage]' = None, *, - cookie: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, - **kwargs: 'Any') -> 'Schema_HomeTestInitMessage': - """Make MH home test init (HoTI) message type. + def _read_opt_ct(self, schema: 'Schema_CareofTestOption', *, + options: 'Option') -> 'Data_CareofTestOption': + """Read MH Care-of Test option. + + Structure of MH Care-of Test option [:rfc:`4866`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + Care-of Keygen Token + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - message: Message data model. - cookie: Home test cookie. - options: Mobility options. - **kwargs: Arbitrary keyword arguments. + schema: Parsed option schema. + options: Parsed MH options. Returns: - Constructed message type. + Constructed option data. """ - if message is not None: - cookie = message.cookie - options = message.options - else: - options = options or [] + if schema.length != 8: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - return Schema_HomeTestInitMessage( - cookie=cookie, - options=self._make_mh_options(options), + data = Data_CareofTestOption( + type=schema.type, + length=schema.length + 2, + token=schema.token, ) + return data - def _make_msg_coti(self, message: 'Optional[Data_CareofTestInitMessage]' = None, *, - cookie: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, - **kwargs: 'Any') -> 'Schema_CareofTestInitMessage': - """Make MH care-of test init (CoTI) message type. + def _read_opt_exp(self, schema: 'Schema_ExperimentalMobilityOption', *, + options: 'Option') -> 'Data_ExperimentalMobilityOption': + """Read MH experimental mobility option. + + Structure of MH Experimental Mobility option [:rfc:`5096#section-4`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Data ..... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - message: Message data model. - cookie: Care-of test cookie. - options: Mobility options. - **kwargs: Arbitrary keyword arguments. + schema: Parsed option schema. + options: Parsed MH options. Returns: - Constructed message type. + Constructed option data. """ - if message is not None: - cookie = message.cookie - options = message.options - else: - options = options or [] - - return Schema_CareofTestInitMessage( - cookie=cookie, - options=self._make_mh_options(options), + data = Data_ExperimentalMobilityOption( + type=schema.type, + length=schema.length + 2, + data=schema.data, + ) + return data + + def _read_opt_badf(self, schema: 'Schema_BADFOption', *, + options: 'Option') -> 'Data_BADFOption': + """Read MH binding authorization data for FMIPv6 (BADF) option. + + Structure of MH Binding Authorization Data for FMIPv6 option + [:rfc:`5568#section-6.4.5`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type = 21 | Option Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SPI | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | Authenticator | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + :rfc:`5568#section-6.4.5` defines the option length as *the length of + the Authenticator in bytes*, i.e. it does **not** cover the 4-byte + SPI, unlike every other mobility option, whose length covers all of + its option data. The wording is inherited from the :rfc:`6275` + binding authorization data option (type ``5``), which carries no SPI + and for which the two readings coincide. Because :rfc:`5568` also + requires this option to be the **last** mobility option present, the + discrepancy never has to be resolved in order to find the following + option, so the literal reading is used here and the reported + :attr:`~pcapkit.protocols.data.internet.mh.Option.length` accounts + for the extra 4 bytes. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length == 0: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_BADFOption( + type=schema.type, + length=schema.length + 6, # 2 bytes for type & length, 4 bytes for SPI + spi=schema.spi, + data=schema.data, + ) + return data + + def _read_opt_ipv6_ap(self, schema: 'Schema_IPv6AddressPrefixOption', *, + options: 'Option') -> 'Data_IPv6AddressPrefixOption': + """Read MH mobility header IPv6 address/prefix option. + + Structure of MH Mobility Header IPv6 Address/Prefix option + [:rfc:`5568#section-6.4.2`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Option-Code | Prefix Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + IPv6 Address/Prefix + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + The option code identifies which address the option carries, c.f., + :class:`~pcapkit.protocols.internet.mh.IPv6AddressPrefixCode`. It, too, + is defined inline by :rfc:`5568#section-6.4.2` with no IANA registry + behind it, hence the enumeration is local to this module. + + Note: + :rfc:`5568` prints the type as ``17``, which is the *neighbor + discovery* option type of its sibling in + :rfc:`5568#section-6.4.1`. Errata ID 1816 (verified) corrects it to + the IANA-assigned mobility option type ``34``, which is what this + handler is registered against. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 18: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + if schema.prefix_length > 128: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_IPv6AddressPrefixOption( + type=schema.type, + length=schema.length + 2, + code=IPv6AddressPrefixCode(schema.code), + prefix_length=schema.prefix_length, + address=schema.address, + ) + return data + + def _read_opt_dns(self, schema: 'Schema_DNSUpdateOption', *, + options: 'Option') -> 'Data_DNSUpdateOption': + """Read MH DNS-UPDATE-TYPE option. + + Structure of MH DNS-UPDATE-TYPE option [:rfc:`5026#section-8.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Status |R| Reserved | MN identity (FQDN) ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The status field draws from the *Status Codes (DNS Update Mobility + Option)* registry that :rfc:`5026#section-10` creates, **not** from + the general mobility status codes, which is why it is typed + :class:`~pcapkit.const.mh.dns_status_code.DNSStatusCode`. + + The identity is kept as :obj:`bytes` rather than decoded to + :class:`str`, because the RFC says only "FQDN format" without saying + whether that means the presentation form or the length-prefixed + label form of :rfc:`1035`. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length < 2: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_DNSUpdateOption( + type=schema.type, + length=schema.length + 2, + status=schema.status, + remove=bool(schema.flags['R']), + identity=schema.identity, + ) + return data + + def _read_opt_vendor(self, schema: 'Schema_VendorSpecificOption', *, + options: 'Option') -> 'Data_VendorSpecificOption': + """Read MH vendor specific mobility option. + + Structure of MH Vendor Specific mobility option [:rfc:`5094#section-3`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Vendor ID | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sub-Type | Data....... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The vendor ID is an SMI Network Management Private Enterprise Number, + whose space is unbounded, so it is not enumerated. The sub-type is + administered by that vendor rather than by IANA, so it is not + enumerated either. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length < 5: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_VendorSpecificOption( + type=schema.type, + length=schema.length + 2, + vendor=schema.vendor, + subtype=schema.subtype, + data=schema.data, + ) + return data + + def _read_opt_service(self, schema: 'Schema_ServiceSelectionOption', *, + options: 'Option') -> 'Data_ServiceSelectionOption': + """Read MH service selection mobility option. + + Structure of MH Service Selection mobility option [:rfc:`5149#section-3`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type = 20 | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identifier... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + Unlike the DNS identity of :meth:`_read_opt_dns`, this identifier is + decoded to :class:`str`: :rfc:`5149#section-3` states outright that it + is UTF-8 and NFKC-normalised. A length of zero is invalid. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length == 0: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_ServiceSelectionOption( + type=schema.type, + length=schema.length + 2, + identifier=schema.identifier, + ) + return data + + def _read_opt_hnp(self, schema: 'Schema_HomeNetworkPrefixOption', *, + options: 'Option') -> 'Data_HomeNetworkPrefixOption': + """Read MH home network prefix option. + + Structure of MH Home Network Prefix option [:rfc:`5213#section-8.3`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved | Prefix Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + Home Network Prefix + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 18: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + if schema.prefix_length > 128: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_HomeNetworkPrefixOption( + type=schema.type, + length=schema.length + 2, + prefix_length=schema.prefix_length, + prefix=schema.prefix, + ) + return data + + def _read_opt_hi(self, schema: 'Schema_HandoffIndicatorOption', *, + options: 'Option') -> 'Data_HandoffIndicatorOption': + """Read MH handoff indicator option. + + Structure of MH Handoff Indicator option [:rfc:`5213#section-8.4`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved (R) | HI | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 2: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_HandoffIndicatorOption( + type=schema.type, + length=schema.length + 2, + hi=schema.hi, + ) + return data + + def _read_opt_att(self, schema: 'Schema_AccessTechnologyTypeOption', *, + options: 'Option') -> 'Data_AccessTechnologyTypeOption': + """Read MH access technology type option. + + Structure of MH Access Technology Type option [:rfc:`5213#section-8.5`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved (R) | ATT | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + :rfc:`5213` itself defines only access types ``0`` through ``5``. + Values ``6`` through ``13`` were registered directly with IANA against + 3GPP and 3GPP2 specifications rather than through an updating RFC, and + :class:`~pcapkit.const.mh.access_type.AccessType` carries all of them. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 2: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_AccessTechnologyTypeOption( + type=schema.type, + length=schema.length + 2, + att=schema.att, + ) + return data + + def _read_opt_mn_lli(self, schema: 'Schema_MNLLIdentifierOption', *, + options: 'Option') -> 'Data_MNLLIdentifierOption': + """Read MH mobile node link-layer identifier option. + + Structure of MH Mobile Node Link-layer Identifier option + [:rfc:`5213#section-8.6`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + Link-layer Identifier + + . ... . + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The identifier's octet and bit ordering is that of + :rfc:`4861#section-4.6`, but only the *address encoding* is borrowed + from there -- the option keeps its own mobility-option framing, in + which the length is in octets rather than in the 8-octet units + neighbour discovery uses. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length < 2: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_MNLLIdentifierOption( + type=schema.type, + length=schema.length + 2, + lli=schema.lli, + ) + return data + + def _read_opt_lla_addr(self, schema: 'Schema_LinkLocalAddressOption', *, + options: 'Option') -> 'Data_LinkLocalAddressOption': + """Read MH link-local address option. + + Structure of MH Link-local Address option [:rfc:`5213#section-8.7`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + Link-local Address + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + :rfc:`6543` reserves ``0200:5EFF:FE00:5213`` as the interface + identifier a mobile access gateway may use here when it does not vary + the address per access link. That changes which values are expected, + not the option's shape. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 16: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_LinkLocalAddressOption( + type=schema.type, + length=schema.length + 2, + address=schema.address, + ) + return data + + def _read_opt_timestamp(self, schema: 'Schema_TimestampOption', *, + options: 'Option') -> 'Data_TimestampOption': + """Read MH timestamp option. + + Structure of MH Timestamp option [:rfc:`5213#section-8.8`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + Timestamp + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + This is **not** the :rfc:`1305` NTP timestamp that + :meth:`_read_opt_mesg_id` reads, and the two must not be conflated. + :rfc:`5213#section-8.8` counts seconds from the UNIX epoch, not NTP's + 1900 one, and splits the 64 bits 48/16 rather than 32/32. Reading it + as an NTP timestamp would be wrong in both the epoch and the field + widths, which is why it gets its own + :class:`~pcapkit.protocols.internet.mh.PMIPv6Timestamp`. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 8: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + seconds = schema.timestamp['seconds'] + fraction = schema.timestamp['fraction'] + + data = Data_TimestampOption( + type=schema.type, + length=schema.length + 2, + timestamp=datetime.datetime.fromtimestamp( + seconds + fraction / 65536, tz=datetime.timezone.utc), + pmip_timestamp=PMIPv6Timestamp(seconds, fraction), + ) + return data + + def _read_opt_restart(self, schema: 'Schema_RestartCounterOption', *, + options: 'Option') -> 'Data_RestartCounterOption': + """Read MH restart counter option. + + Structure of MH Restart Counter option [:rfc:`5847#section-3.4`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Restart Counter | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + :rfc:`5847#section-3.4` says this option is valid only in a heartbeat + *response*. That is a constraint on the sender rather than on the wire + format, so it is not enforced here -- a capture carries whatever it + carries. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 4: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_RestartCounterOption( + type=schema.type, + length=schema.length + 2, + counter=schema.counter, + ) + return data + + def _read_opt_ipv4_hoa(self, schema: 'Schema_IPv4HomeAddressOption', *, + options: 'Option') -> 'Data_IPv4HomeAddressOption': + """Read MH IPv4 home address option. + + Structure of MH IPv4 Home Address option [:rfc:`5555#section-3.1.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length |Prefix-len |P| Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | IPv4 home address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + A prefix length of ``32`` means a single address rather than a prefix, + and ``0`` is invalid [:rfc:`5555#section-3.1.1`]. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 6: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + prefix_length = schema.flags['prefix_length'] + if prefix_length == 0 or prefix_length > 32: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_IPv4HomeAddressOption( + type=schema.type, + length=schema.length + 2, + prefix_length=prefix_length, + address=schema.address, + request_prefix=bool(schema.flags['P']), + ) + return data + + def _read_opt_ipv4_ack(self, schema: 'Schema_IPv4AddressAcknowledgementOption', *, + options: 'Option') -> 'Data_IPv4AddressAcknowledgementOption': + """Read MH IPv4 address acknowledgement option. + + Structure of MH IPv4 Address Acknowledgement option + [:rfc:`5555#section-3.2.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Status |Pref-len |Res| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | IPv4 home address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The status field draws from the *DSMIPv6 IPv4 Home Address Option + Status Codes* registry of :rfc:`5555#section-8`, which despite its + name governs **this** option rather than the IPv4 home address option. + It is not the registry the similarly shaped IPv4 Home Address Reply + option of :rfc:`5844` uses: that one has no value ``133``. Nor is it + the pseudo-home-address registry of :rfc:`5726` that + :class:`~pcapkit.const.mh.ack_status_code.ACKStatusCode` carries, + whose name is the more obvious trap of the three. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 6: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_IPv4AddressAcknowledgementOption( + type=schema.type, + length=schema.length + 2, + status=schema.status, + prefix_length=schema.flags['prefix_length'], + address=schema.address, + ) + return data + + def _read_opt_nat(self, schema: 'Schema_NATDetectionOption', *, + options: 'Option') -> 'Data_NATDetectionOption': + """Read MH NAT detection option. + + Structure of MH NAT Detection option [:rfc:`5555#section-3.2.2`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length |F| Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Refresh time | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + A refresh time of ``0`` means the field is to be ignored, and all ones + that no keep-alives are needed [:rfc:`5555#section-3.2.2`]. Both are + representable as a :class:`~datetime.timedelta`, so the raw value is + recoverable from it without being stored twice. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 6: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_NATDetectionOption( + type=schema.type, + length=schema.length + 2, + force=bool(schema.flags['F']), + refresh=datetime.timedelta(seconds=schema.refresh), + ) + return data + + def _read_opt_ipv4_coa(self, schema: 'Schema_IPv4CareofAddressOption', *, + options: 'Option') -> 'Data_IPv4CareofAddressOption': + """Read MH IPv4 care-of address option. + + Structure of MH IPv4 Care-of Address option [:rfc:`5555#section-3.1.2`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | IPv4 Care-of address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + Unlike the IPv4 home address option, the whole half-word before the + address is reserved -- there is no prefix length and no ``P`` flag + carved out of it. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 6: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_IPv4CareofAddressOption( + type=schema.type, + length=schema.length + 2, + address=schema.address, + ) + return data + + def _read_opt_gre(self, schema: 'Schema_GREKeyOption', *, + options: 'Option') -> 'Data_GREKeyOption': + """Read MH GRE key option. + + Structure of MH GRE Key option [:rfc:`5845#section-6.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | GRE Key Identifier | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The key identifier is present only when the option length is ``6``; a + length of ``2`` says the option carries none + [:rfc:`5845#section-6.1`]. No flag announces that, so the length is + the only thing to branch on, and any other length is malformed. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length not in (2, 6): + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_GREKeyOption( + type=schema.type, + length=schema.length + 2, + key=schema.key if schema.length == 6 else None, + ) + return data + + def _read_opt_bid(self, schema: 'Schema_BindingIdentifierOption', *, + options: 'Option') -> 'Data_BindingIdentifierOption': + """Read MH binding identifier (BID) option. + + Structure of MH Binding Identifier option [:rfc:`5648#section-4.3`, as + updated by :rfc:`6089#section-4.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type = 35 | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Binding ID (BID) | Status |H| BID-PRI | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-------------------------------+ + + + + : IPv4 or IPv6 Care-of Address (CoA) : + + + + +---------------------------------------------------------------+ + + Note: + The care-of address is absent for a length of ``4``, an IPv4 address + for ``8`` and an IPv6 address for ``20``; no other length is valid and + the option carries no address-family flag, so the length is the only + thing to branch on [:rfc:`5648#section-4.3`]. + + :rfc:`6089#section-4.1` renames the seven bits below the ``H`` flag + from ``Reserved`` to ``BID-PRI``, a binding priority in which ``0`` + marks a sender predating :rfc:`6089`. The newer reading is used, since + a zero reads identically either way. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length not in (4, 8, 20): + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_BindingIdentifierOption( + type=schema.type, + length=schema.length + 2, + bid=schema.bid, + status=schema.status, + simultaneous=bool(schema.flags['H']), + bid_pri=schema.flags['BID_PRI'], + address=cast('Optional[IPv4Address | IPv6Address]', schema.address), + ) + return data + + def _read_opt_ipv4_hoa_req(self, schema: 'Schema_IPv4HomeAddressRequestOption', *, + options: 'Option') -> 'Data_IPv4HomeAddressRequestOption': + """Read MH IPv4 home address request option. + + Structure of MH IPv4 Home Address Request option + [:rfc:`5844#section-3.3.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length |Prefix-len | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | IPv4 home address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + Unlike the IPv4 home address option of :rfc:`5555#section-3.1.1`, + which this otherwise resembles, there is no ``P`` flag -- the reserved + field is 10 bits rather than 9. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 6: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_IPv4HomeAddressRequestOption( + type=schema.type, + length=schema.length + 2, + prefix_length=schema.flags['prefix_length'], + address=schema.address, + ) + return data + + def _read_opt_ipv4_hoa_rep(self, schema: 'Schema_IPv4HomeAddressReplyOption', *, + options: 'Option') -> 'Data_IPv4HomeAddressReplyOption': + """Read MH IPv4 home address reply option. + + Structure of MH IPv4 Home Address Reply option [:rfc:`5844#section-3.3.2`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Status |Pref-len |Res| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | IPv4 home address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The status draws from the *IPv4 Home Address Reply Status Codes* + registry of :rfc:`5844#section-6`, which is **not** the DSMIPv6 one the + identically shaped IPv4 address acknowledgement option of :rfc:`5555` + uses -- that registry additionally defines ``133``. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 6: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_IPv4HomeAddressReplyOption( + type=schema.type, + length=schema.length + 2, + status=schema.status, + prefix_length=schema.flags['prefix_length'], + address=schema.address, + ) + return data + + def _read_opt_ipv4_router(self, schema: 'Schema_IPv4DefaultRouterAddressOption', *, + options: 'Option') -> 'Data_IPv4DefaultRouterAddressOption': + """Read MH IPv4 default-router address option. + + Structure of MH IPv4 Default-Router Address option + [:rfc:`5844#section-3.3.3`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved (R) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | IPv4 Default-Router Address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 6: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_IPv4DefaultRouterAddressOption( + type=schema.type, + length=schema.length + 2, + address=schema.address, + ) + return data + + def _read_opt_ipv4_dhcp(self, schema: 'Schema_IPv4DHCPSupportModeOption', *, + options: 'Option') -> 'Data_IPv4DHCPSupportModeOption': + """Read MH IPv4 DHCP support mode option. + + Structure of MH IPv4 DHCP Support Mode option [:rfc:`5844#section-3.3.4`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved (R) |S| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The ``S`` bit is the **last** bit of the option, not the first -- the + reserved field precedes it rather than following it, which is the + other way round from every other flag in this module. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 2: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_IPv4DHCPSupportModeOption( + type=schema.type, + length=schema.length + 2, + mode=Enum_DHCPSupportMode(schema.flags['S']), + ) + return data + + def _read_opt_cr(self, schema: 'Schema_ContextRequestOption', *, + options: 'Option') -> 'Data_ContextRequestOption': + """Read MH context request option. + + Structure of MH Context Request option [:rfc:`5949#section-6.2.1`]: + + .. code-block:: text + + 0 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 + +---------------+---------------+---------------+---------------+ + | Option-Type | Option-Length | Reserved | + +---------------+---------------+-------------------------------+ + | Req-type-1 | Req-length-1 | Req-type-2 | Req-length-2 | + +---------------------------------------------------------------+ + | Req-type-3 | Req-length-3 | Req-option-3 | + +---------------------------------------------------------------+ + | ... | + + Note: + The requested types are drawn from the mobility option registry + itself, so a request for a vendor specific option (type ``19``) is + followed by five octets naming the vendor and sub-type, whereas a + request for a home network prefix (type ``22``) carries a request + length of zero. There is neither a count field nor a terminator: the + list runs to the end of the option. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length < 2: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + requests = [] # type: list[Data_ContextRequest] + buffer = schema.requests + index = 0 + while index < len(buffer): + if index + 2 > len(buffer): + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + req_type, req_len = buffer[index], buffer[index + 1] + value = buffer[index + 2:index + 2 + req_len] + if len(value) != req_len: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + requests.append(Data_ContextRequest( + type=Enum_Option.get(req_type), + length=req_len, + value=value, + )) + index += 2 + req_len + + data = Data_ContextRequestOption( + type=schema.type, + length=schema.length + 2, + requests=tuple(requests), + ) + return data + + def _read_opt_lmaa(self, schema: 'Schema_LMAAddressOption', *, + options: 'Option') -> 'Data_LMAAddressOption': + """Read MH local mobility anchor address option. + + Structure of MH Local Mobility Anchor Address option + [:rfc:`5949#section-6.2.2`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option-Type | Option-Length | Option-Code | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Local Mobility Anchor Address ... | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + The option codes are enumerated in + :class:`~pcapkit.protocols.internet.mh.LMAAddressCode`, which is local to + this module because :rfc:`5949` defines them inline with no IANA registry + behind them. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length not in (6, 18): + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_LMAAddressOption( + type=schema.type, + length=schema.length + 2, + code=LMAAddressCode(schema.code), + address=schema.address, + ) + return data + + def _read_opt_mn_lla_iid(self, schema: 'Schema_MNLLAIIDOption', *, + options: 'Option') -> 'Data_MNLLAIIDOption': + """Read MH mobile node link-local address interface identifier option. + + Structure of MH Mobile Node Link-local Address Interface Identifier option + [:rfc:`5949#section-6.2.3`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option-Type | Option-Length | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + Interface Identifier + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 10: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_MNLLAIIDOption( + type=schema.type, + length=schema.length + 2, + iid=schema.iid, + ) + return data + + def _read_opt_transient(self, schema: 'Schema_TransientBindingOption', *, + options: 'Option') -> 'Data_TransientBindingOption': + """Read MH transient binding option. + + Structure of MH Transient Binding option [:rfc:`6058#section-5.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved |L| Lifetime | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The lifetime is in units of **100 milliseconds**, which is unlike every + other lifetime in this module [:rfc:`6058#section-5.1`]. The ``L`` flag + is the last bit of the third octet, with the seven reserved bits above + rather than below it. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 2: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_TransientBindingOption( + type=schema.type, + length=schema.length + 2, + late=bool(schema.flags['L']), + lifetime=datetime.timedelta(milliseconds=schema.lifetime * 100), + ) + return data + + def _read_opt_fs(self, schema: 'Schema_FlowSummaryOption', *, + options: 'Option') -> 'Data_FlowSummaryOption': + """Read MH flow summary mobility option. + + Structure of MH Flow Summary mobility option [:rfc:`6089#section-4.2.2`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Len | FID | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | FID ........ + +-+-+-+-+-+-+-+-+-+-+- + + Note: + :rfc:`6089#section-8` prints the type codes of this option and the flow + identification option the wrong way round, contradicting both its own + figures and the IANA registry. The figures and the registry agree that + the flow summary option is type ``44``, which is what is registered + here. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length % 2 != 0: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_FlowSummaryOption( + type=schema.type, + length=schema.length + 2, + fid=tuple(schema.fid), + ) + return data + + def _read_fid_suboptions( + self, suboptions_schema: 'list[Schema_FlowIdentificationSuboption]', + ) -> 'FlowIDSuboption': + """Read MH flow identification sub-options. + + Structure of MH flow identification sub-option [:rfc:`6089#section-4.2.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sub-Opt Type |Sub-Opt Length | Sub-Option Data... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + These are sub-options of one mobility option rather than mobility options + in their own right, so they are dispatched here by schema type rather than + through :attr:`self.__option__ `. + + Args: + suboptions_schema: Parsed sub-option schemas. + + Returns: + Parsed sub-option data. + + """ + suboptions = OrderedMultiDict() # type: FlowIDSuboption + + for schema in suboptions_schema: + if isinstance(schema, Schema_PadFlowIdentificationSuboption): + size = 1 if schema.type == Enum_FlowIDSuboption.Pad else schema.length + 2 + data = Data_PadFlowIdentificationSuboption( + type=schema.type, + length=size, + ) # type: Data_FlowIdentificationSuboption + elif isinstance(schema, Schema_BIDReferenceSuboption): + data = Data_BIDReferenceSuboption( + type=schema.type, + length=schema.length + 2, + bid=tuple(schema.bid), + ) + elif isinstance(schema, Schema_TrafficSelectorSuboption): + data = Data_TrafficSelectorSuboption( + type=schema.type, + length=schema.length + 2, + ts_format=schema.ts_format, + selector=schema.selector, + ) + elif isinstance(schema, Schema_FlowBindingActionSuboption): + data = Data_FlowBindingActionSuboption( + type=schema.type, + length=schema.length + 2, + action=schema.action, + ) + elif isinstance(schema, Schema_TargetCareofAddressSuboption): + data = Data_TargetCareofAddressSuboption( + type=schema.type, + length=schema.length + 2, + address=schema.address, + ) + else: + data = Data_UnassignedFlowIdentificationSuboption( + type=schema.type, + length=schema.length + 2, + data=cast('Schema_UnassignedFlowIdentificationSuboption', schema).data, + ) + + suboptions.add(schema.type, data) + + return suboptions + + def _read_opt_fid(self, schema: 'Schema_FlowIdentificationOption', *, + options: 'Option') -> 'Data_FlowIdentificationOption': + """Read MH flow identification mobility option. + + Structure of MH Flow Identification mobility option + [:rfc:`6089#section-4.2`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Len | FID | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | FID-PRI | Reserved | Status | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sub-options (optional) ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The status draws from the *Flow Identification Mobility Option Status + Codes* registry, which is neither the general mobility status codes nor + the flow binding acknowledgement ones -- three similarly named + registries that must not be confused. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length < 6: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_FlowIdentificationOption( + type=schema.type, + length=schema.length + 2, + fid=schema.fid, + fid_pri=schema.fid_pri, + status=schema.status, + suboptions=self._read_fid_suboptions(schema.suboptions), + ) + return data + + def _read_opt_rc(self, schema: 'Schema_RedirectCapabilityOption', *, + options: 'Option') -> 'Data_RedirectCapabilityOption': + """Read MH redirect-capability mobility option. + + Structure of MH Redirect-Capability mobility option + [:rfc:`6463#section-4.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Length | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 2: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_RedirectCapabilityOption( + type=schema.type, + length=schema.length + 2, + ) + return data + + def _read_opt_redirect(self, schema: 'Schema_RedirectOption', *, + options: 'Option') -> 'Data_RedirectOption': + """Read MH redirect mobility option. + + Structure of MH Redirect mobility option [:rfc:`6463#section-4.2`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Length |K|N| Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | Optional IPv6 r2LMA Address | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Optional IPv4 r2LMA Address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + :rfc:`6463#section-4.2` forbids ``K`` and ``N`` from being both set and + both clear, so exactly one address is present and the option length is + 18 or 6 accordingly. The two encodings of the same fact are checked + against each other here, since a disagreement means the option cannot + be read either way. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + ipv6_flag = bool(schema.flags['K']) + ipv4_flag = bool(schema.flags['N']) + + if ipv6_flag == ipv4_flag: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + if schema.length != (18 if ipv6_flag else 6): + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_RedirectOption( + type=schema.type, + length=schema.length + 2, + ipv6=schema.ipv6, + ipv4=schema.ipv4, + ) + return data + + def _read_opt_load(self, schema: 'Schema_LoadInformationOption', *, + options: 'Option') -> 'Data_LoadInformationOption': + """Read MH load information mobility option. + + Structure of MH Load Information mobility option [:rfc:`6463#section-4.3`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Length | Priority | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sessions in Use | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Maximum Sessions | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Used Capacity | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Maximum Capacity | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The two capacity fields are in **kilobytes** per second, unlike the + quality-of-service bit rates, which are in bits per second + [:rfc:`6463#section-4.3`]. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 18: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_LoadInformationOption( + type=schema.type, + length=schema.length + 2, + priority=schema.priority, + sessions_in_use=schema.sessions_in_use, + max_sessions=schema.max_sessions, + used_capacity=schema.used_capacity, + max_capacity=schema.max_capacity, + ) + return data + + def _read_opt_alt_ipv4_coa(self, schema: 'Schema_AlternateIPv4CareofAddressOption', *, + options: 'Option') -> 'Data_AlternateIPv4CareofAddressOption': + """Read MH alternate IPv4 care-of address option. + + Structure of MH Alternate IPv4 Care-of Address option + [:rfc:`6463#section-4.4`]: + + .. code-block:: text + + 0 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 + | Option Type | Option Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Alternate IPv4 Care-of Address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 4: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_AlternateIPv4CareofAddressOption( + type=schema.type, + length=schema.length + 2, + address=schema.address, + ) + return data + + def _read_opt_mn_group(self, schema: 'Schema_MNGroupIdentifierOption', *, + options: 'Option') -> 'Data_MNGroupIdentifierOption': + """Read MH mobile node group identifier option. + + Structure of MH Mobile Node Group Identifier option + [:rfc:`6602#section-4.3`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Sub-type | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Mobile Node Group Identifier | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 6: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_MNGroupIdentifierOption( + type=schema.type, + length=schema.length + 2, + subtype=schema.subtype, + group_id=schema.group_id, + ) + return data + + def _read_opt_mag_addr(self, schema: 'Schema_MAGIPv6AddressOption', *, + options: 'Option') -> 'Data_MAGIPv6AddressOption': + """Read MH MAG IPv6 address option. + + Structure of MH MAG IPv6 Address option [:rfc:`6705#section-11.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved | Address Length| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + MAG IPv6 Address + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The option is modelled on the home network prefix option, which is why + it carries an address *length* field at all; :rfc:`6705#section-11.1` + requires it to be ``128``, since a full address is always carried. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 18: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_MAGIPv6AddressOption( + type=schema.type, + length=schema.length + 2, + address_length=schema.address_length, + address=schema.address, + ) + return data + + def _read_ani_suboptions( + self, suboptions_schema: 'list[Schema_ANISuboption]', + ) -> 'ANISuboption': + """Read MH access network identifier sub-options. + + Structure of MH access network identifier sub-option + [:rfc:`6757#section-3.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | ANI Type | ANI Length | Option Data ~ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + The geo-location degrees of :rfc:`6757#section-3.1.2` are 24-bit + **two's-complement** fixed-point values with nine integer bits. A + :class:`~pcapkit.corekit.fields.strings.BitField` reads them unsigned, so + the sign is applied here, and both the decoded degrees and the raw signed + integers are recorded -- the latter so that the exact wire value survives + a round trip through the data model. + + Args: + suboptions_schema: Parsed sub-option schemas. + + Returns: + Parsed sub-option data. + + """ + suboptions = OrderedMultiDict() # type: ANISuboption + + for schema in suboptions_schema: + if isinstance(schema, Schema_ANINetworkIdentifierSuboption): + data = Data_ANINetworkIdentifierSuboption( + type=schema.type, + length=schema.length + 2, + utf8=bool(schema.flags['E']), + net_name=schema.net_name, + ap_name=schema.ap_name, + ) # type: Data_ANISuboption + elif isinstance(schema, Schema_ANIGeoLocationSuboption): + raw_lat = self._decode_signed(schema.location['latitude'], 24) + raw_lon = self._decode_signed(schema.location['longitude'], 24) + data = Data_ANIGeoLocationSuboption( + type=schema.type, + length=schema.length + 2, + latitude=raw_lat / 2 ** 15, + longitude=raw_lon / 2 ** 15, + raw_latitude=raw_lat, + raw_longitude=raw_lon, + ) + elif isinstance(schema, Schema_ANIOperatorIdentifierSuboption): + data = Data_ANIOperatorIdentifierSuboption( + type=schema.type, + length=schema.length + 2, + op_id_type=schema.op_id_type, + identifier=schema.identifier, + ) + elif isinstance(schema, Schema_ANICivicLocationSuboption): + data = Data_ANICivicLocationSuboption( + type=schema.type, + length=schema.length + 2, + format=schema.format, + location=schema.location, + ) + elif isinstance(schema, Schema_ANIMAGGroupIdentifierSuboption): + data = Data_ANIMAGGroupIdentifierSuboption( + type=schema.type, + length=schema.length + 2, + group_id=schema.group_id, + ) + elif isinstance(schema, Schema_ANIUpdateTimerSuboption): + data = Data_ANIUpdateTimerSuboption( + type=schema.type, + length=schema.length + 2, + timer=datetime.timedelta(seconds=schema.timer * 4), + ) + else: + data = Data_UnassignedANISuboption( + type=schema.type, + length=schema.length + 2, + data=cast('Schema_UnassignedANISuboption', schema).data, + ) + + suboptions.add(schema.type, data) + + return suboptions + + @staticmethod + def _decode_signed(value: 'int', width: 'int') -> 'int': + """Reinterpret an unsigned integer as a two's-complement signed one. + + Args: + value: Unsigned value as read from the wire. + width: Field width, in bits. + + Returns: + The signed value the same bits denote. + + Note: + :class:`~pcapkit.corekit.fields.strings.BitField` reads a sub-field as + an unsigned integer, since that is what almost every bit-packed field + in the mobility header is. The geo-location degrees of + :rfc:`6757#section-3.1.2` are the exception, so they are converted + here rather than by teaching the field about signedness -- a change + that would touch every other user of it. + + """ + sign_bit = 1 << (width - 1) + return value - (1 << width) if value & sign_bit else value + + def _read_opt_ani(self, schema: 'Schema_AccessNetworkIdentifierOption', *, + options: 'Option') -> 'Data_AccessNetworkIdentifierOption': + """Read MH access network identifier option. + + Structure of MH Access Network Identifier option [:rfc:`6757#section-3`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ... ANI Sub-option(s) ... ~ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + Unlike most options carrying sub-options, this one has **no** reserved + octet between its own length and the first sub-option + [:rfc:`6757#section-3`], and it must carry at least one. + + IANA names the sub-option registry "Access Network *Information*" + while the RFC's prose says "Identifier"; they are the same registry. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length == 0: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_AccessNetworkIdentifierOption( + type=schema.type, + length=schema.length + 2, + suboptions=self._read_ani_suboptions(schema.suboptions), + ) + return data + + def _read_opt_offload(self, schema: 'Schema_IPv4TrafficOffloadSelectorOption', *, + options: 'Option') -> 'Data_IPv4TrafficOffloadSelectorOption': + """Read MH IPv4 traffic offload selector option. + + Structure of MH IPv4 Traffic Offload Selector option + [:rfc:`6909#section-3.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |M| Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Traffic Selector Sub-option ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The traffic selector it carries is the *flow identification* + sub-option of :rfc:`6089#section-4.2.1.4`, so it is read through the + same sub-option registry rather than through one of its own. It is + optional in a proxy binding update and mandatory in a proxy binding + acknowledgement, hence a collection rather than a single value. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_IPv4TrafficOffloadSelectorOption( + type=schema.type, + length=schema.length + 2, + mode=bool(schema.flags['M']), + selector=self._read_fid_suboptions(schema.selector), + ) + return data + + def _read_opt_mcast_sel(self, schema: 'Schema_DynamicIPMulticastSelectorOption', *, + options: 'Option') -> 'Data_DynamicIPMulticastSelectorOption': + """Read MH dynamic IP multicast selector option. + + Structure of MH Dynamic IP Multicast Selector option + [:rfc:`7028#section-5.1.2`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Protocol |M| Reserved |Nr of Mcast Address Records (N)| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + Multicast Address Record [1] + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + . . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + Multicast Address Record [N] + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The address records are :rfc:`3810#section-5.2` MLD or + :rfc:`3376#section-4.2` IGMP structures. They belong to those protocols + rather than to the mobility header, and each is self-describing through + its own auxiliary-data length and source count, so they are recorded + opaquely here rather than half-decoded. :attr:`protocol` says which of + the two they are: ``143`` for MLDv2, ``131`` for MLDv1. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_DynamicIPMulticastSelectorOption( + type=schema.type, + length=schema.length + 2, + protocol=schema.protocol, + mode=bool(schema.flags['M']), + records=schema.records, + data=schema.data, + ) + return data + + def _read_opt_dmnp(self, schema: 'Schema_DelegatedMNPOption', *, + options: 'Option') -> 'Data_DelegatedMNPOption': + """Read MH delegated mobile network prefix option. + + Structure of MH Delegated Mobile Network Prefix option + [:rfc:`7148#section-4.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length |V| Reserved | Prefix Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + . . + + IPv4 or IPv6 Delegated Mobile Network Prefix + + | (DMNP) | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + Unlike the binding identifier and target care-of address, this option + *does* carry an address-family flag, so the ``V`` bit rather than the + option length is what selects the prefix width + [:rfc:`7148#section-4.1`]. The two are checked against each other, + since a disagreement leaves the option unreadable. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + ipv4 = bool(schema.flags['V']) + if schema.length != (6 if ipv4 else 18): + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_DelegatedMNPOption( + type=schema.type, + length=schema.length + 2, + ipv4=ipv4, + prefix_length=schema.prefix_length, + prefix=schema.prefix, + ) + return data + + def _read_opt_ams4(self, schema: 'Schema_ActiveMulticastSubscriptionIPv4Option', *, + options: 'Option') -> 'Data_ActiveMulticastSubscriptionIPv4Option': + """Read MH active multicast subscription IPv4 option. + + Structure of MH Active Multicast Subscription IPv4 option + [:rfc:`7161#section-6.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | IGMP Type | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + Multicast Membership Context + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The membership context is an IGMP group address (:rfc:`1112`, + :rfc:`2236`) or an IGMPv3 group record (:rfc:`3376#section-4.2`), + which belong to IGMP rather than to the mobility header, so they are + recorded opaquely. :attr:`igmp_type` says which: ``0x12`` IGMPv1, + ``0x16`` IGMPv2, ``0x22`` IGMPv3. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length < 1: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_ActiveMulticastSubscriptionIPv4Option( + type=schema.type, + length=schema.length + 2, + igmp_type=schema.igmp_type, + context=schema.context, + ) + return data + + def _read_opt_ams6(self, schema: 'Schema_ActiveMulticastSubscriptionIPv6Option', *, + options: 'Option') -> 'Data_ActiveMulticastSubscriptionIPv6Option': + """Read MH active multicast subscription IPv6 option. + + Structure of MH Active Multicast Subscription IPv6 option + [:rfc:`7161#section-4.1.2`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | MLD Type | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + Multicast Membership Context + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + For MLDv2 the context is an :rfc:`3810#section-5.2` multicast address + record; for MLDv1 it is a reserved word followed by a multicast address + [:rfc:`7161#section-4.1.3`]. Both belong to MLD, so the context is + recorded opaquely and :attr:`mld_type` says which form it is in. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length < 1: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_ActiveMulticastSubscriptionIPv6Option( + type=schema.type, + length=schema.length + 2, + mld_type=schema.mld_type, + context=schema.context, + ) + return data + + def _read_qos_attributes( + self, attributes_schema: 'list[Schema_QoSAttribute]', + ) -> 'QoSAttribute': + """Read MH quality-of-service attributes. + + Structure of MH quality-of-service attribute [:rfc:`7222#section-4.2`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value ~ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + Every bit rate here is in **bits** per second [:rfc:`7222#section-4.2.1`], + not the kilobytes per second of the load information option's capacity + fields. + + Args: + attributes_schema: Parsed attribute schemas. + + Returns: + Parsed attribute data. + + """ + attributes = OrderedMultiDict() # type: QoSAttribute + + for schema in attributes_schema: + if isinstance(schema, Schema_PerSessionBitRateAttribute): + data = Data_PerSessionBitRateAttribute( + type=schema.type, + length=schema.length + 2, + service=bool(schema.flags['S']), + exclude=bool(schema.flags['E']), + rate=schema.rate, + ) # type: Data_QoSAttribute + elif isinstance(schema, Schema_BitRateAttribute): + data = Data_BitRateAttribute( + type=schema.type, + length=schema.length + 2, + rate=schema.rate, + ) + elif isinstance(schema, Schema_AllocationRetentionPriorityAttribute): + data = Data_AllocationRetentionPriorityAttribute( + type=schema.type, + length=schema.length + 2, + priority_level=schema.priority['PL'], + preemption_capability=schema.priority['PC'], + preemption_vulnerability=schema.priority['PV'], + ) + elif isinstance(schema, Schema_QoSTrafficSelectorAttribute): + data = Data_QoSTrafficSelectorAttribute( + type=schema.type, + length=schema.length + 2, + ts_format=schema.ts_format, + selector=schema.selector, + ) + elif isinstance(schema, Schema_QoSVendorSpecificAttribute): + data = Data_QoSVendorSpecificAttribute( + type=schema.type, + length=schema.length + 2, + vendor=schema.vendor, + subtype=schema.subtype, + data=schema.data, + ) + else: + data = Data_UnassignedQoSAttribute( + type=schema.type, + length=schema.length + 2, + data=cast('Schema_UnassignedQoSAttribute', schema).data, + ) + + attributes.add(schema.type, data) + + return attributes + + def _read_opt_qos(self, schema: 'Schema_QualityOfServiceOption', *, + options: 'Option') -> 'Data_QualityOfServiceOption': + """Read MH quality-of-service option. + + Structure of MH Quality-of-Service option [:rfc:`7222#section-4.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | SR-ID | TC | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | OC | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ~ QoS Attribute(s) ~ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The traffic class octet is a 6-bit differentiated services code point + with two reserved bits below it, so only the code point is carried into + the data model. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length < 6: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_QualityOfServiceOption( + type=schema.type, + length=schema.length + 2, + sr_id=schema.sr_id, + dscp=schema.tc >> 2, + oc=schema.oc, + attributes=self._read_qos_attributes(schema.attributes), + ) + return data + + def _read_opt_lma_up(self, schema: 'Schema_LMAUserPlaneAddressOption', *, + options: 'Option') -> 'Data_LMAUserPlaneAddressOption': + """Read MH LMA user-plane address option. + + Structure of MH LMA User-Plane Address option [:rfc:`7389#section-4`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + . . + + LMA User-Plane Address + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The address may legitimately be **absent**: a mobile access gateway + sends the option with no address, or with an all-zero one, purely to + say which transport it wants [:rfc:`7389#section-4`]. Only a local + mobility anchor's reply is required to carry a real address. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length not in (2, 6, 18): + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_LMAUserPlaneAddressOption( + type=schema.type, + length=schema.length + 2, + address=cast('Optional[IPv4Address | IPv6Address]', + schema.address if schema.length != 2 else None), + ) + return data + + def _read_opt_mcast(self, schema: 'Schema_MulticastMobilityOption', *, + options: 'Option') -> 'Data_MulticastMobilityOption': + """Read MH multicast mobility option. + + Structure of MH Multicast Mobility option [:rfc:`7411#section-5.3`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Option-Code | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | MLD or IGMP Report Payload | + ~ ~ + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + This option's length field is **not** the usual octet count. + :rfc:`7411#section-5.3` measures it in 32-bit *words* and excludes the + option code and reserved octets as well as the type and length ones, so + the option occupies ``4 + length * 4`` octets rather than + ``length + 2``. The option-collection machinery advances by the octets + the schema actually consumed rather than by the declared length, so the + unusual unit does not misframe the options that follow -- but it does + mean :attr:`~pcapkit.protocols.data.internet.mh.Option.length` here is + the true octet count, computed rather than copied. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + data = Data_MulticastMobilityOption( + type=schema.type, + length=schema.length * 4 + 4, + code=schema.code, + data=schema.data, + ) + return data + + def _read_opt_mcast_ack(self, schema: 'Schema_MulticastAcknowledgementOption', *, + options: 'Option') -> 'Data_MulticastAcknowledgementOption': + """Read MH multicast acknowledgement option. + + Structure of MH Multicast Acknowledgement option [:rfc:`7411#section-5.4`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Option-Code | Status | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | MLD or IGMP Unsupported Report Payload | + ~ ~ + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + As with :meth:`_read_opt_mcast`, the length counts 32-bit words and + excludes the option code and status octets [:rfc:`7411#section-5.4`]. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + data = Data_MulticastAcknowledgementOption( + type=schema.type, + length=schema.length * 4 + 4, + code=schema.code, + status=schema.status, + data=schema.data, + ) + return data + + def _read_lcmp_suboptions( + self, suboptions_schema: 'list[Schema_LMAControlledMAGSuboption]', + ) -> 'LMAControlledMAGSuboption': + """Read MH LMA-controlled MAG parameters sub-options. + + Structure of MH LMA-controlled MAG parameters sub-option + [:rfc:`8127#section-3.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | LCMP Type | LCMP Length | Sub-Option Data ~ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + Only the re-registration start time is in units of 4 seconds; every + other interval in these two sub-options is in whole seconds + [:rfc:`8127#section-3.1.1`, :rfc:`8127#section-3.1.2`]. + + Args: + suboptions_schema: Parsed sub-option schemas. + + Returns: + Parsed sub-option data. + + """ + suboptions = OrderedMultiDict() # type: LMAControlledMAGSuboption + + for schema in suboptions_schema: + if isinstance(schema, Schema_BindingReregistrationControlSuboption): + data = Data_BindingReregistrationControlSuboption( + type=schema.type, + length=schema.length + 2, + start_time=datetime.timedelta(seconds=schema.start_time * 4), + initial_retransmission=datetime.timedelta( + seconds=schema.initial_retransmission), + max_retransmission=datetime.timedelta(seconds=schema.max_retransmission), + ) # type: Data_LMAControlledMAGSuboption + elif isinstance(schema, Schema_HeartbeatControlSuboption): + data = Data_HeartbeatControlSuboption( + type=schema.type, + length=schema.length + 2, + interval=datetime.timedelta(seconds=schema.interval), + retransmission_delay=datetime.timedelta( + seconds=schema.retransmission_delay), + max_retransmissions=schema.max_retransmissions, + ) + else: + data = Data_UnassignedLMAControlledMAGSuboption( + type=schema.type, + length=schema.length + 2, + data=cast('Schema_UnassignedLMAControlledMAGSuboption', schema).data, + ) + + suboptions.add(schema.type, data) + + return suboptions + + def _read_opt_lcmp(self, schema: 'Schema_LMAControlledMAGParametersOption', *, + options: 'Option') -> 'Data_LMAControlledMAGParametersOption': + """Read MH LMA-controlled MAG parameters option. + + Structure of MH LMA-Controlled MAG Parameters option + [:rfc:`8127#section-3`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | LCMP Sub-Option(s) ~ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + This option is registered to :rfc:`8127`, **not** to :rfc:`7864` -- + the latter defines the flow-mobility sub-options and the + ``FLOW-MOBILITY`` update notification reason instead. The IANA registry + is the authority, and it cites :rfc:`8127`. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length == 0: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_LMAControlledMAGParametersOption( + type=schema.type, + length=schema.length + 2, + suboptions=self._read_lcmp_suboptions(schema.suboptions), + ) + return data + + def _read_opt_mag_mp(self, schema: 'Schema_MAGMultipathBindingOption', *, + options: 'Option') -> 'Data_MAGMultipathBindingOption': + """Read MH MAG multipath binding option. + + Structure of MH MAG Multipath Binding option [:rfc:`8278#section-4.1`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | If-ATT | If-Label | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Binding ID |B|O| Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + :rfc:`8278#section-4.1` makes the ``B`` and ``O`` flags mutually + exclusive -- neither may be set while the other is -- and reserves + binding identifiers ``0`` and ``255``. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 6: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + bulk = bool(schema.flags['B']) + overwrite = bool(schema.flags['O']) + if bulk and overwrite: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_MAGMultipathBindingOption( + type=schema.type, + length=schema.length + 2, + att=schema.att, + label=schema.label, + bid=schema.bid, + bulk=bulk, + overwrite=overwrite, + ) + return data + + def _read_opt_mag_id(self, schema: 'Schema_MAGIdentifierOption', *, + options: 'Option') -> 'Data_MAGIdentifierOption': + """Read MH MAG identifier option. + + Structure of MH MAG Identifier option [:rfc:`8278#section-4.2`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Subtype | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identifier ... ~ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The sub-type is borrowed from the *mobile node* identifier subtype + registry [:rfc:`4283`, :rfc:`8371`], so the identifier's encoding is + whatever that subtype prescribes. :rfc:`8278` does not restate those + encodings, so the identifier is kept as :obj:`bytes`. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length < 2: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_MAGIdentifierOption( + type=schema.type, + length=schema.length + 2, + subtype=schema.subtype, + identifier=schema.identifier, + ) + return data + + def _read_opt_anchored(self, schema: 'Schema_AnchoredPrefixOption', *, + options: 'Option') -> 'Data_AnchoredPrefixOption': + """Read MH anchored prefix option. + + Structure of MH Anchored Prefix option [:rfc:`8885#section-4.3`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved | Prefix Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + Anchored Prefix + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 18: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + if schema.prefix_length > 128: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_AnchoredPrefixOption( + type=schema.type, + length=schema.length + 2, + prefix_length=schema.prefix_length, + prefix=schema.prefix, + ) + return data + + def _read_opt_local_prefix(self, schema: 'Schema_LocalPrefixOption', *, + options: 'Option') -> 'Data_LocalPrefixOption': + """Read MH local prefix option. + + Structure of MH Local Prefix option [:rfc:`8885#section-4.4`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved | Prefix Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + Local Prefix + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 18: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + if schema.prefix_length > 128: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_LocalPrefixOption( + type=schema.type, + length=schema.length + 2, + prefix_length=schema.prefix_length, + prefix=schema.prefix, + ) + return data + + def _read_opt_prev_maar(self, schema: 'Schema_PreviousMAAROption', *, + options: 'Option') -> 'Data_PreviousMAAROption': + """Read MH previous MAAR option. + + Structure of MH Previous MAAR option [:rfc:`8885#section-4.5`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved | Prefix Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + Previous MAAR + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + Home Network Prefix + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The prefix length applies to the *home network prefix* only; the + previous MAAR's own field is a full address + [:rfc:`8885#section-4.5`]. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 34: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + if schema.prefix_length > 128: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_PreviousMAAROption( + type=schema.type, + length=schema.length + 2, + prefix_length=schema.prefix_length, + maar=schema.maar, + prefix=schema.prefix, + ) + return data + + def _read_opt_serv_maar(self, schema: 'Schema_ServingMAAROption', *, + options: 'Option') -> 'Data_ServingMAAROption': + """Read MH serving MAAR option. + + Structure of MH Serving MAAR option [:rfc:`8885#section-4.6`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + S-MAAR's Address + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + Unlike the anchored and local prefix options, this one has neither a + reserved octet nor a prefix length -- it carries a full address and + nothing else. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 16: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_ServingMAAROption( + type=schema.type, + length=schema.length + 2, + address=schema.address, + ) + return data + + def _read_opt_dlif_lla(self, schema: 'Schema_DLIFLinkLocalAddressOption', *, + options: 'Option') -> 'Data_DLIFLinkLocalAddressOption': + """Read MH DLIF link-local address option. + + Structure of MH DLIF Link-Local Address option [:rfc:`8885#section-4.7`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + DLIF Link-Local Address + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 16: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_DLIFLinkLocalAddressOption( + type=schema.type, + length=schema.length + 2, + address=schema.address, + ) + return data + + def _read_opt_dlif_lladdr(self, schema: 'Schema_DLIFLinkLayerAddressOption', *, + options: 'Option') -> 'Data_DLIFLinkLayerAddressOption': + """Read MH DLIF link-layer address option. + + Structure of MH DLIF Link-Layer Address option [:rfc:`8885#section-4.8`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + DLIF Link-Layer Address + + . ... . + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The address is encoded as in :rfc:`4861#section-4.6.2`, whose width is + link-type dependent, so no fixed length can be checked here. + :rfc:`8885#section-4.8` says outright that the option cannot be used on + links where a link-layer address is unavailable. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length < 2: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_DLIFLinkLayerAddressOption( + type=schema.type, + length=schema.length + 2, + lla=schema.lla, + ) + return data + + def _read_cga_extensions(self, extensions_schema: 'list[Schema_CGAExtension]') -> 'Extension': + """Read CGA extensions. + + Structure of CGA extensions [:rfc:`4581`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Extension Type | Extension Data Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + ~ Extension Data ~ + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + extensions_schema: Parsed CGA extensions. + + Returns: + Parsed CGA extensions data. + + """ + extensions = OrderedMultiDict() # type: Extension + + for schema in extensions_schema: + type = schema.type + name = self._lookup_registry(self.__extension__, type) + + if isinstance(name, str): + meth_name = f'_read_ext_{name}' + meth = cast('ExtensionParser', + getattr(self, meth_name, self._read_ext_none)) + else: + meth = name[0] + data = meth(schema, extensions=extensions) + + # record extension data + extensions.add(type, data) + + return extensions + + def _read_ext_none(self, schema: 'Schema_UnknownExtension', *, + extensions: 'Extension') -> 'Data_UnknownExtension': + """Read unknown CGA extension. + + Args: + schema: Parsed extension schema. + extensions: Parsed MH CGA extensions. + + Returns: + Constructed extension data. + + """ + data = Data_UnknownExtension( + type=schema.type, + length=schema.length + 2, + data=schema.data, + ) + return data + + def _read_ext_multiprefix(self, schema: 'Schema_MultiPrefixExtension', *, + extensions: 'Extension') -> 'Data_MultiPrefixExtension': + """Read multi-prefix CGA extension. + + Structure of Multi-Prefix CGA extension [:rfc:`5535`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Extension Type | Extension Data Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |P| Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + Prefix[1] + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + Prefix[2] + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + . . . + . . . + . . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + Prefix[n] + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: Parsed extension schema. + extensions: Parsed MH CGA extensions. + + Returns: + Constructed extension data. + + """ + data = Data_MultiPrefixExtension( + type=schema.type, + length=schema.length + 2, + flag=bool(schema.flags['P']), + prefixes=tuple(schema.prefixes), + ) + return data + + def _read_ext_exp(self, schema: 'Schema_ExperimentalExtension', *, + extensions: 'Extension') -> 'Data_ExperimentalExtension': + """Read experimental CGA extension. + + Structure of an experimental CGA extension [:rfc:`4581#section-2`, + :rfc:`4581#section-3`]: + + .. code-block:: text + + 0 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Extension Type | Extension Data Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + ~ Extension Data ~ + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + :rfc:`4581#section-3` assigns extension types ``0xFFFD``, ``0xFFFE`` + and ``0xFFFF`` for experimental use as :rfc:`3692` recommends, and + defines **no** structure for their extension data -- neither it nor + :rfc:`5535` gives those three a layout, and the IANA registry records + only their names. So the data is opaque *by specification* here rather + than merely left undecoded, and no better parse is available to be + written later. One handler serves all three, since they share a shape + and differ only in their type code. + + Args: + schema: Parsed extension schema. + extensions: Parsed MH CGA extensions. + + Returns: + Constructed extension data. + + """ + data = Data_ExperimentalExtension( + type=schema.type, + length=schema.length + 2, + data=schema.data, + ) + return data + + def _make_msg_unknown(self, message: 'Optional[Data_UnknownMessage]' = None, *, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_UnknownMessage': + """Make MH unknown message type. + + Args: + message: Message data model. + data: Raw message data. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + data = message.data + + return Schema_UnknownMessage( + data=data, + ) + + def _make_msg_brr(self, message: 'Optional[Data_BindingRefreshRequestMessage]' = None, *, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_BindingRefreshRequestMessage': + """Make MH binding refresh request (BRR) message type. + + Args: + message: Message data model. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + options = message.options + else: + options = options or [] + + return Schema_BindingRefreshRequestMessage( + options=self._make_mh_options(options), + ) + + def _make_msg_hoti(self, message: 'Optional[Data_HomeTestInitMessage]' = None, *, + cookie: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_HomeTestInitMessage': + """Make MH home test init (HoTI) message type. + + Args: + message: Message data model. + cookie: Home test cookie. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + cookie = message.cookie + options = message.options + else: + options = options or [] + + return Schema_HomeTestInitMessage( + cookie=cookie, + options=self._make_mh_options(options), + ) + + def _make_msg_coti(self, message: 'Optional[Data_CareofTestInitMessage]' = None, *, + cookie: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_CareofTestInitMessage': + """Make MH care-of test init (CoTI) message type. + + Args: + message: Message data model. + cookie: Care-of test cookie. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + cookie = message.cookie + options = message.options + else: + options = options or [] + + return Schema_CareofTestInitMessage( + cookie=cookie, + options=self._make_mh_options(options), + ) + + def _make_msg_hot(self, message: 'Optional[Data_HomeTestMessage]' = None, *, + nonce_index: 'int' = 0, + cookie: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', + token: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_HomeTestMessage': + """Make MH home test (HoT) message type. + + Args: + message: Message data model. + nonce_index: Home nonce index. + cookie: Home test cookie. + token: Home test token. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + nonce_index = message.nonce_index + cookie = message.cookie + token = message.token + options = message.options + else: + options = options or [] + + return Schema_HomeTestMessage( + nonce_index=nonce_index, + cookie=cookie, + token=token, + options=self._make_mh_options(options), + ) + + def _make_msg_cot(self, message: 'Optional[Data_CareofTestMessage]' = None, *, + nonce_index: 'int' = 0, + cookie: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', + token: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_CareofTestMessage': + """Make MH care-of test (CoT) message type. + + Args: + message: Message data model. + nonce_index: Care-of nonce index. + cookie: Care-of test cookie. + token: Care-of test token. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + nonce_index = message.nonce_index + cookie = message.cookie + token = message.token + options = message.options + else: + options = options or [] + + return Schema_CareofTestMessage( + nonce_index=nonce_index, + cookie=cookie, + token=token, + options=self._make_mh_options(options), + ) + + def _make_msg_bu(self, message: 'Optional[Data_BindingUpdateMessage]' = None, *, + seq: 'int' = 0, + ack: 'bool' = False, + home: 'bool' = False, + lla_compat: 'bool' = False, + key_mngt: 'bool' = False, + lifetime: 'int | timedelta' = 4, # reasonable default value + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_BindingUpdateMessage': + """Make MH binding update (BU) message type. + + Args: + message: Message data model. + seq: Sequence number. + ack: Acknowledgement flag. + home: Home registration flag. + lla_compat: LLA compatibility flag. + key_mngt: Key management mobility option flag. + lifetime: Lifetime in seconds or timedelta. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + seq = message.seq + ack = message.ack + home = message.home + lla_compat = message.lla_compat + key_mngt = message.key_mngt + lifetime_val = math.ceil(message.lifetime.total_seconds()) + options = message.options + else: + lifetime_val = lifetime if isinstance(lifetime, int) else math.ceil(lifetime.total_seconds()) + options = options or [] + + return Schema_BindingUpdateMessage( + seq=seq, + flags={ + 'A': ack, + 'H': home, + 'L': lla_compat, + 'K': key_mngt, + }, + lifetime=math.ceil(lifetime_val / 4), + options=self._make_mh_options(options), + ) + + def _make_msg_ba(self, message: 'Optional[Data_BindingAcknowledgementMessage]' = None, *, + status: 'Enum_StatusCode | StdlibEnum | AenumEnum | str | int' = Enum_StatusCode.Binding_Update_accepted_Proxy_Binding_Update_accepted, + status_default: 'Optional[int]' = None, + status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + status_reversed: 'bool' = False, + key_mngt: 'bool' = False, + seq: 'int' = 0, + lifetime: 'int | timedelta' = 4, # reasonable default value + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_BindingAcknowledgementMessage': + """Make MH binding acknowledge (BA) message type. + + Args: + message: Message data model. + status: Status code. + status_default: Default status code. + status_namespace: Status code namespace. + status_reversed: Reverse status code namespace. + key_mngt: Key management mobility option flag. + seq: Sequence number. + lifetime: Lifetime in seconds or timedelta. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + status_val = message.status + key_mngt = message.key_mngt + seq = message.seq + lifetime_val = math.ceil(message.lifetime.total_seconds()) + options = message.options + else: + status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] + reversed=status_reversed, pack=False) + lifetime_val = lifetime if isinstance(lifetime, int) else math.ceil(lifetime.total_seconds()) + options = options or [] + + return Schema_BindingAcknowledgementMessage( + status=status_val, + flags={ + 'K': key_mngt, + }, + seq=seq, + lifetime=math.ceil(lifetime_val / 4), + options=self._make_mh_options(options), + ) + + def _make_msg_be(self, message: 'Optional[Data_BindingErrorMessage]' = None, *, + status: 'Enum_StatusCode | StdlibEnum | AenumEnum | str | int' = Enum_StatusCode.Binding_Update_accepted_Proxy_Binding_Update_accepted, + status_default: 'Optional[int]' = None, + status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + status_reversed: 'bool' = False, + home: 'IPv6Address | int | str | bytes' = '::', + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_BindingErrorMessage': + """Make MH binding error (BE) message type. + + Args: + message: Message data model. + status: Status code. + status_default: Default status code. + status_namespace: Status code namespace. + status_reversed: Reverse status code namespace. + home: Home address. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + status_val = message.status + home = message.home + options = message.options + else: + status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] + reversed=status_reversed, pack=False) + options = options or [] + + return Schema_BindingErrorMessage( + status=status_val, + home=home, + options=self._make_mh_options(options), + ) + + def _make_msg_fbu(self, message: 'Optional[Data_FastBindingUpdateMessage]' = None, *, + seq: 'int' = 0, + ack: 'bool' = True, # MUST be set, c.f., RFC 5568, section 6.2.2 + home: 'bool' = True, # MUST be set, c.f., RFC 5568, section 6.2.2 + lla_compat: 'bool' = False, + key_mngt: 'bool' = False, + lifetime: 'int | timedelta' = 4, # reasonable default value + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_FastBindingUpdateMessage': + """Make MH fast binding update (FBU) message type. + + Args: + message: Message data model. + seq: Sequence number. + ack: Acknowledgement flag. + home: Home registration flag. + lla_compat: LLA compatibility flag. + key_mngt: Key management mobility option flag. + lifetime: Lifetime in seconds or timedelta. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + seq = message.seq + ack = message.ack + home = message.home + lla_compat = message.lla_compat + key_mngt = message.key_mngt + lifetime_val = math.ceil(message.lifetime.total_seconds()) + options = message.options + else: + lifetime_val = lifetime if isinstance(lifetime, int) else math.ceil(lifetime.total_seconds()) + options = options or [] + + return Schema_FastBindingUpdateMessage( + seq=seq, + flags={ + 'A': ack, + 'H': home, + 'L': lla_compat, + 'K': key_mngt, + }, + lifetime=math.ceil(lifetime_val / 4), + options=self._make_mh_options(options), + ) + + def _make_msg_fback(self, message: 'Optional[Data_FastBindingAcknowledgmentMessage]' = None, *, + status: 'FastBindingAcknowledgmentStatus | StdlibEnum | AenumEnum | str | int' = FastBindingAcknowledgmentStatus.Fast_Binding_Update_accepted, + status_default: 'Optional[int]' = None, + status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + status_reversed: 'bool' = False, + key_mngt: 'bool' = False, + seq: 'int' = 0, + lifetime: 'int | timedelta' = 4, # reasonable default value + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_FastBindingAcknowledgmentMessage': + """Make MH fast binding acknowledgment (FBack) message type. + + Args: + message: Message data model. + status: Status code, c.f., + :class:`~pcapkit.protocols.internet.mh.FastBindingAcknowledgmentStatus`. + status_default: Default status code. + status_namespace: Status code namespace. + status_reversed: Reverse status code namespace. + key_mngt: Key management mobility option flag. + seq: Sequence number. + lifetime: Lifetime in seconds or timedelta. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + status_val = message.status + key_mngt = message.key_mngt + seq = message.seq + lifetime_val = math.ceil(message.lifetime.total_seconds()) + options = message.options + else: + status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] + reversed=status_reversed, pack=False) + lifetime_val = lifetime if isinstance(lifetime, int) else math.ceil(lifetime.total_seconds()) + options = options or [] + + return Schema_FastBindingAcknowledgmentMessage( + status=status_val, + flags={ + 'K': key_mngt, + }, + seq=seq, + lifetime=math.ceil(lifetime_val / 4), + options=self._make_mh_options(options), + ) + + def _make_msg_fna(self, message: 'Optional[Data_FastNeighborAdvertisementMessage]' = None, *, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_FastNeighborAdvertisementMessage': + """Make MH fast neighbor advertisement (FNA) message type. + + Args: + message: Message data model. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + options = message.options + else: + options = options or [] + + return Schema_FastNeighborAdvertisementMessage( + options=self._make_mh_options(options), + ) + + def _make_msg_emh(self, message: 'Optional[Data_ExperimentalMessage]' = None, *, + data: 'bytes' = b'\x00\x00', # 2 bytes of padding, c.f., RFC 5096, section 3 + **kwargs: 'Any') -> 'Schema_ExperimentalMessage': + """Make MH experimental mobility header message type. + + Args: + message: Message data model. + data: Experimental message data. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + data = message.data + + return Schema_ExperimentalMessage( + data=data, + ) + + def _make_msg_hi(self, message: 'Optional[Data_HandoverInitiateMessage]' = None, *, + seq: 'int' = 0, + assign: 'bool' = False, + buffer: 'bool' = False, + proxy: 'bool' = False, + forward: 'bool' = False, + code: 'Enum_HandoverInitiateStatus | StdlibEnum | AenumEnum | str | int' = Enum_HandoverInitiateStatus.FBU_with_the_PCoA_as_source_IP_address, + code_default: 'Optional[int]' = None, + code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + code_reversed: 'bool' = False, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_HandoverInitiateMessage': + """Make MH handover initiate (HI) message type. + + Args: + message: Message data model. + seq: Sequence number. + assign: Assigned address configuration flag. + buffer: Buffer flag. + proxy: Proxy flag. + forward: Forwarding flag. + code: Code. + code_default: Default code. + code_namespace: Code namespace. + code_reversed: Reverse code namespace. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + seq = message.seq + assign = message.assign + buffer = message.buffer + proxy = message.proxy + forward = message.forward + code_val = message.code + options = message.options + else: + code_val = self._make_index(code, code_default, namespace=code_namespace, # type: ignore[assignment] + reversed=code_reversed, pack=False) + options = options or [] + + return Schema_HandoverInitiateMessage( + seq=seq, + flags={ + 'S': assign, + 'U': buffer, + 'P': proxy, + 'F': forward, + }, + code=code_val, + options=self._make_mh_options(options), + ) + + def _make_msg_hack(self, message: 'Optional[Data_HandoverAcknowledgeMessage]' = None, *, + seq: 'int' = 0, + buffer: 'bool' = False, + proxy: 'bool' = False, + forward: 'bool' = False, + code: 'Enum_HandoverACKStatus | StdlibEnum | AenumEnum | str | int' = Enum_HandoverACKStatus.Handover_Accepted_with_NCoA_valid, + code_default: 'Optional[int]' = None, + code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + code_reversed: 'bool' = False, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_HandoverAcknowledgeMessage': + """Make MH handover acknowledge (HAck) message type. + + Args: + message: Message data model. + seq: Sequence number. + buffer: Buffer flag. + proxy: Proxy flag. + forward: Forwarding flag. + code: Code. + code_default: Default code. + code_namespace: Code namespace. + code_reversed: Reverse code namespace. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + seq = message.seq + buffer = message.buffer + proxy = message.proxy + forward = message.forward + code_val = message.code + options = message.options + else: + code_val = self._make_index(code, code_default, namespace=code_namespace, # type: ignore[assignment] + reversed=code_reversed, pack=False) + options = options or [] + + return Schema_HandoverAcknowledgeMessage( + seq=seq, + flags={ + 'U': buffer, + 'P': proxy, + 'F': forward, + }, + code=code_val, + options=self._make_mh_options(options), + ) + + def _make_msg_has(self, message: 'Optional[Data_HomeAgentSwitchMessage]' = None, *, + addresses: 'Optional[list[IPv6Address | bytes | str | int]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_HomeAgentSwitchMessage': + """Make MH home agent switch (HAS) message type. + + Args: + message: Message data model. + addresses: Alternate home agent addresses. An empty list is valid and + asks the mobile node to run home agent discovery. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + addresses = cast('list[IPv6Address | bytes | str | int]', list(message.addresses)) + options = message.options + else: + addresses = addresses or [] + options = options or [] + + return Schema_HomeAgentSwitchMessage( + count=len(addresses), + addresses=addresses, + options=self._make_mh_options(options), + ) + + def _make_msg_hb(self, message: 'Optional[Data_HeartbeatMessage]' = None, *, + unsolicited: 'bool' = False, + response: 'bool' = False, + seq: 'int' = 0, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_HeartbeatMessage': + """Make MH heartbeat message type. + + Args: + message: Message data model. + unsolicited: Unsolicited flag. + response: Response flag; unset makes this a heartbeat request. + seq: Sequence number. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + unsolicited = message.unsolicited + response = message.response + seq = message.seq + options = message.options + else: + options = options or [] + + return Schema_HeartbeatMessage( + flags={ + 'U': unsolicited, + 'R': response, + }, + seq=seq, + options=self._make_mh_options(options), + ) + + def _make_msg_brm(self, message: 'Optional[Data_BindingRevocationMessage]' = None, *, + br_type: 'Enum_BindingRevocation | StdlibEnum | AenumEnum | str | int' = Enum_BindingRevocation.Binding_Revocation_Indication, + br_type_default: 'Optional[int]' = None, + br_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + br_type_reversed: 'bool' = False, + code: 'Enum_RevocationTrigger | Enum_RevocationStatusCode | StdlibEnum | AenumEnum | str | int' = Enum_RevocationTrigger.Unspecified, + code_default: 'Optional[int]' = None, + code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + code_reversed: 'bool' = False, + seq: 'int' = 0, + proxy: 'bool' = False, + ipv4_hoa: 'bool' = False, + global_revocation: 'bool' = False, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_BindingRevocationMessage': + """Make MH binding revocation (BRM) message type. + + Args: + message: Message data model. + br_type: Binding revocation type, saying which form of the message + this is. + br_type_default: Default binding revocation type. + br_type_namespace: Binding revocation type namespace. + br_type_reversed: Reverse binding revocation type namespace. + code: Revocation trigger, for an indication, or acknowledgement + status, for an acknowledgement. + code_default: Default code. + code_namespace: Code namespace. + code_reversed: Reverse code namespace. + seq: Sequence number. + proxy: Proxy binding flag. + ipv4_hoa: IPv4 home address binding only flag. + global_revocation: Global revocation flag. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + br_type_val = message.br_type # type: Enum_BindingRevocation + code_val = message.code # type: Enum_RevocationTrigger | Enum_RevocationStatusCode + seq = message.seq + proxy = message.proxy + ipv4_hoa = message.ipv4_hoa + global_revocation = message.global_revocation + options = message.options + else: + br_type_val = self._make_index(br_type, br_type_default, namespace=br_type_namespace, # type: ignore[assignment] + reversed=br_type_reversed, pack=False) + code_val = self._make_index(code, code_default, namespace=code_namespace, # type: ignore[assignment] + reversed=code_reversed, pack=False) + options = options or [] + + return Schema_BindingRevocationMessage( + br_type=br_type_val, + code=code_val, + seq=seq, + flags={ + 'P': proxy, + 'V': ipv4_hoa, + 'G': global_revocation, + }, + options=self._make_mh_options(options), + ) + + def _make_msg_lri(self, message: 'Optional[Data_LocalizedRoutingInitiationMessage]' = None, *, + seq: 'int' = 0, + lifetime: 'int | timedelta' = 0, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_LocalizedRoutingInitiationMessage': + """Make MH localized routing initiation (LRI) message type. + + Args: + message: Message data model. + seq: Sequence number. + lifetime: Lifetime, in seconds or as a + :class:`~datetime.timedelta`. Unlike the binding messages, this + one counts seconds rather than units of 4 seconds, so the value is + not scaled. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + seq = message.seq + lifetime_val = math.ceil(message.lifetime.total_seconds()) + options = message.options + else: + lifetime_val = lifetime if isinstance(lifetime, int) else math.ceil(lifetime.total_seconds()) + options = options or [] + + return Schema_LocalizedRoutingInitiationMessage( + seq=seq, + lifetime=lifetime_val, + options=self._make_mh_options(options), + ) + + def _make_msg_lra(self, message: 'Optional[Data_LocalizedRoutingAcknowledgmentMessage]' = None, *, + seq: 'int' = 0, + unsolicited: 'bool' = False, + status: 'LocalizedRoutingStatus | StdlibEnum | AenumEnum | str | int' = LocalizedRoutingStatus.Success, + status_default: 'Optional[int]' = None, + status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + status_reversed: 'bool' = False, + lifetime: 'int | timedelta' = 0, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_LocalizedRoutingAcknowledgmentMessage': + """Make MH localized routing acknowledgment (LRA) message type. + + Args: + message: Message data model. + seq: Sequence number. + unsolicited: Unsolicited flag. + status: Status, c.f., + :class:`~pcapkit.protocols.internet.mh.LocalizedRoutingStatus`. + status_default: Default status. + status_namespace: Status namespace. + status_reversed: Reverse status namespace. + lifetime: Lifetime, in seconds or as a + :class:`~datetime.timedelta`. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + seq = message.seq + unsolicited = message.unsolicited + status_val = message.status # type: int + lifetime_val = math.ceil(message.lifetime.total_seconds()) + options = message.options + else: + status_val = self._make_index(status, status_default, namespace=status_namespace, + reversed=status_reversed, pack=False) + lifetime_val = lifetime if isinstance(lifetime, int) else math.ceil(lifetime.total_seconds()) + options = options or [] + + return Schema_LocalizedRoutingAcknowledgmentMessage( + seq=seq, + flags={ + 'U': unsolicited, + }, + status=status_val, + lifetime=lifetime_val, + options=self._make_mh_options(options), + ) + + def _make_msg_upn(self, message: 'Optional[Data_UpdateNotificationMessage]' = None, *, + seq: 'int' = 0, + reason: 'Enum_UpdateNotificationReason | StdlibEnum | AenumEnum | str | int' = Enum_UpdateNotificationReason.FORCE_REREGISTRATION, + reason_default: 'Optional[int]' = None, + reason_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + reason_reversed: 'bool' = False, + ack: 'bool' = False, + retransmit: 'bool' = False, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_UpdateNotificationMessage': + """Make MH update notification (UPN) message type. + + Args: + message: Message data model. + seq: Sequence number. + reason: Notification reason. + reason_default: Default notification reason. + reason_namespace: Notification reason namespace. + reason_reversed: Reverse notification reason namespace. + ack: Acknowledgement requested flag. + retransmit: Retransmission flag. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + seq = message.seq + reason_val = message.reason # type: Enum_UpdateNotificationReason + ack = message.ack + retransmit = message.retransmit + options = message.options + else: + reason_val = self._make_index(reason, reason_default, namespace=reason_namespace, # type: ignore[assignment] + reversed=reason_reversed, pack=False) + options = options or [] + + return Schema_UpdateNotificationMessage( + seq=seq, + reason=reason_val, + flags={ + 'A': ack, + 'D': retransmit, + }, + options=self._make_mh_options(options), + ) + + def _make_msg_upa(self, message: 'Optional[Data_UpdateNotificationAcknowledgementMessage]' = None, *, + seq: 'int' = 0, + status: 'Enum_UpdateNotificationACKStatus | StdlibEnum | AenumEnum | str | int' = Enum_UpdateNotificationACKStatus.SUCCESS, + status_default: 'Optional[int]' = None, + status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + status_reversed: 'bool' = False, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_UpdateNotificationAcknowledgementMessage': + """Make MH update notification acknowledgement (UPA) message type. + + Args: + message: Message data model. + seq: Sequence number. + status: Status. + status_default: Default status. + status_namespace: Status namespace. + status_reversed: Reverse status namespace. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + seq = message.seq + status_val = message.status # type: Enum_UpdateNotificationACKStatus + options = message.options + else: + status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] + reversed=status_reversed, pack=False) + options = options or [] + + return Schema_UpdateNotificationAcknowledgementMessage( + seq=seq, + status=status_val, + options=self._make_mh_options(options), + ) + + def _make_msg_fbm(self, message: 'Optional[Data_FlowBindingMessage]' = None, *, + fb_type: 'Enum_FlowBindingType | StdlibEnum | AenumEnum | str | int' = Enum_FlowBindingType.Indication, + fb_type_default: 'Optional[int]' = None, + fb_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + fb_type_reversed: 'bool' = False, + seq: 'int' = 0, + code: 'Enum_FlowBindingIndicationTrigger | Enum_FlowBindingACKStatus | StdlibEnum | AenumEnum | str | int' = Enum_FlowBindingIndicationTrigger.Unspecified, + code_default: 'Optional[int]' = None, + code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + code_reversed: 'bool' = False, + ack: 'bool' = False, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_FlowBindingMessage': + """Make MH flow binding (FB) message type. + + Args: + message: Message data model. + fb_type: Flow binding type, saying which form of the message this is. + fb_type_default: Default flow binding type. + fb_type_namespace: Flow binding type namespace. + fb_type_reversed: Reverse flow binding type namespace. + seq: Sequence number. + code: Indication trigger, for an indication, or acknowledgement + status, for an acknowledgement. + code_default: Default code. + code_namespace: Code namespace. + code_reversed: Reverse code namespace. + ack: Acknowledgement requested flag. Meaningful only in an + indication. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + fb_type_val = message.fb_type # type: Enum_FlowBindingType + seq = message.seq + code_val = message.code # type: Enum_FlowBindingIndicationTrigger | Enum_FlowBindingACKStatus + ack = message.ack + options = message.options + else: + fb_type_val = self._make_index(fb_type, fb_type_default, namespace=fb_type_namespace, # type: ignore[assignment] + reversed=fb_type_reversed, pack=False) + code_val = self._make_index(code, code_default, namespace=code_namespace, # type: ignore[assignment] + reversed=code_reversed, pack=False) + options = options or [] + + return Schema_FlowBindingMessage( + fb_type=fb_type_val, + seq=seq, + code=code_val, + flags={ + 'A': ack, + }, + options=self._make_mh_options(options), + ) + + def _make_msg_sq(self, message: 'Optional[Data_SubscriptionQueryMessage]' = None, *, + seq: 'int' = 0, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_SubscriptionQueryMessage': + """Make MH subscription query (SQ) message type. + + Args: + message: Message data model. + seq: Sequence number, counted modulo 256. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + seq = message.seq + options = message.options + else: + options = options or [] + + return Schema_SubscriptionQueryMessage( + seq=seq, + options=self._make_mh_options(options), + ) + + def _make_msg_sr(self, message: 'Optional[Data_SubscriptionResponseMessage]' = None, *, + seq: 'int' = 0, + info: 'bool' = False, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_SubscriptionResponseMessage': + """Make MH subscription response (SR) message type. + + Args: + message: Message data model. + seq: Sequence number, echoed from the query. + info: Multicast information flag. + options: Mobility options. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + """ + if message is not None: + seq = message.seq + info = message.info + options = message.options + else: + options = options or [] + + return Schema_SubscriptionResponseMessage( + seq=seq, + flags={ + 'I': info, + }, + options=self._make_mh_options(options), + ) + + def _make_pad_options(self, offset: 'int') -> 'tuple[list[Schema_PadOption], int]': + """Make the padding options needed to align the header to 8 octets. + + Args: + offset: Number of octets emitted so far, **counting the six octets + of the fixed header** (``Payload Proto``, ``Header Len``, + ``MH Type``, ``Reserved`` and ``Checksum``) as well as the whole + message data. + + Returns: + Tuple of the padding option schemas and the number of octets they + occupy. + + Note: + The Mobility Header is a multiple of 8 octets and + :attr:`~pcapkit.protocols.schema.internet.mh.MH.length` counts those + units less one [:rfc:`6275#section-6.1.1`]. Its fixed part is + **six** octets, not the two of an IPv6 options header, so it is + ``len(data) + 6`` that has to be aligned -- which means the message + data is well formed only when it is 2 octets past a multiple of 8, + a different modulus from the one the extension headers use. + + A ``PadN`` option spends two octets on its own type and + ``Option Length`` fields before any padding data, so occupying + ``pad_len`` octets means an ``Option Length`` of ``pad_len - 2``. + One or two octets are padded with ``Pad1`` options instead, that + being the only form which can occupy a single octet + [:rfc:`6275#section-6.2.5`]; two octets need two separate ``Pad1`` + schemas, since a schema instance is mutable and must not be shared + between two entries of the option list. + + """ + if offset % 8 == 0: + return [], 0 + + pad_len = 8 - (offset % 8) + if pad_len <= 2: + return [self._make_opt_pad(Enum_Option.Pad1, length=0) # type: ignore[arg-type] + for _ in range(pad_len)], pad_len + return [self._make_opt_pad(Enum_Option.PadN, length=pad_len - 2)], pad_len # type: ignore[arg-type] + + def _pad_mh_message(self, data: 'Schema_Packet | bytes') -> 'Schema_Packet | bytes': + """Pad an MH message so that the Mobility Header aligns to 8 octets. + + Args: + data: Constructed message data. + + Returns: + The message data, with padding options appended if any were needed. + + Note: + The padding goes into the message's mobility options, which is where + :rfc:`6275#section-6.2.5` puts it. A message whose body is opaque has + nowhere to put it -- + :class:`~pcapkit.protocols.schema.internet.mh.UnknownMessage` and + :class:`~pcapkit.protocols.schema.internet.mh.ExperimentalMessage` + carry raw bytes and no options, as does a ``data`` argument given + directly as :obj:`bytes` -- so for those the padding is appended to the + message body itself and a + :class:`~pcapkit.utilities.warnings.ProtocolWarning` says so. + + Appending is necessary rather than optional: ``length`` is + ``(len(data) + 6) // 8 - 1``, which floors, so leaving an opaque body + short emitted 10, 12 or 14 octets while declaring 8, and a parser reads + 8 and misinterprets the remainder. Since the caller asked for a packet + to be built and the shortfall is recoverable, completing it beats + refusing -- the warning is there because the emitted body is then not + byte-for-byte what was handed in. + + """ + pad_opts, pad_len = self._make_pad_options(len(data) + 6) + if pad_len == 0: + return data + + options = getattr(data, 'options', None) + # NOTE: The ``isinstance`` test comes first so that the type checker can + # narrow ``data`` for the assignment below; at runtime ``getattr`` has + # already covered the :obj:`bytes` case by returning :obj:`None`. + if isinstance(data, bytes) or options is None: + warn(f'{self.alias}: message data of {len(data)} octets carries no ' + f'mobility options to hold padding, so {pad_len} octet(s) were ' + 'appended to the message body to align the header', + ProtocolWarning) + if isinstance(data, bytes): + return data + b'\x00' * pad_len + + # An opaque schema body -- UnknownMessage, ExperimentalMessage -- keeps + # its content in ``data`` rather than in options, so that is where the + # octets go. Rebound rather than mutated in place so that ``len()`` and + # ``pack()`` see the change, exactly as for the options branch below. + body = getattr(data, 'data', None) + if not isinstance(body, bytes): + raise ProtocolError( + f'{self.alias}: message data of {len(data)} octets needs ' + f'{pad_len} octet(s) of padding, but the body is neither bytes ' + 'nor a schema carrying bytes, so there is nowhere to put it') + data.data = body + b'\x00' * pad_len + return data + + # NOTE: Rebinding the attribute rather than mutating the list in place is + # what marks the schema as updated, so that ``len()`` and ``pack()`` take + # the padding into account. + data.options = list(options) + pad_opts + return data + + def _make_mh_options(self, options: 'Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]') -> 'list[Schema_Option | bytes]': + """Make options for MH. + + Args: + options: MH options. + + Returns: + Mobility options list. + + """ + if isinstance(options, list): + options_list = [] # type: list[Schema_Option | bytes] + for schema in options: + if isinstance(schema, bytes): + code = Enum_Option.get(int.from_bytes(schema[0:1], 'big', signed=False)) + + data = schema # type: Schema_Option | bytes + elif isinstance(schema, Schema): + data = schema + else: + code, args = cast('tuple[Enum_Option, dict[str, Any]]', schema) + name = self._lookup_registry(self.__option__, code) + if isinstance(name, str): + meth_name = f'_make_opt_{name}' + meth = cast('OptionConstructor', + getattr(self, meth_name, self._make_opt_none)) + else: + meth = name[1] + data = meth(code, **args) + + options_list.append(data) + return options_list + + options_list = [] + for code, option in options.items(multi=True): + name = self._lookup_registry(self.__option__, code) + if isinstance(name, str): + meth_name = f'_make_opt_{name}' + meth = cast('OptionConstructor', + getattr(self, meth_name, self._make_opt_none)) + else: + meth = name[1] + + data = meth(code, option) + options_list.append(data) + return options_list + + def _make_opt_none(self, type: 'Enum_Option', option: 'Optional[Data_UnassignedOption]' = None, *, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_UnassignedOption': + """Make MH unassigned option. + + Args: + type: Option type. + option: Option data model. + data: Option data. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + data = option.data + + return Schema_UnassignedOption( + type=type, + length=len(data), + data=data, + ) + + def _make_opt_pad(self, type: 'Enum_Option', option: 'Optional[Data_PadOption]' = None, *, + length: 'int' = 0, + **kwargs: 'Any') -> 'Schema_PadOption': + """Make MH pad option. + + Args: + type: Option type. + option: Option data model. + length: Value of the ``Option Length`` field, i.e. the number of + padding octets *after* the two octets of the option header. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + Note: + :attr:`Data_PadOption.length + ` counts the + *whole* option, whereas :attr:`Schema_PadOption.length + ` is the + ``Option Length`` field -- two octets fewer, and absent altogether + for a ``Pad1``. Copying one into the other unconverted is why + re-making a parsed ``PadN`` used to come back two octets too long. + + """ + if option is not None: + length = 0 if option.type == Enum_Option.Pad1 else option.length - 2 + + if type == Enum_Option.Pad1 and length != 0: + # raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid format') + warn(f'{self.alias}: [OptNo {type}] invalid format', ProtocolWarning) + type = Enum_Option.PadN # type: ignore[assignment] + if type == Enum_Option.PadN and length == 0: + # raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid format') + warn(f'{self.alias}: [OptNo {type}] invalid format', ProtocolWarning) + type = Enum_Option.Pad1 # type: ignore[assignment] + + return Schema_PadOption( + type=type, + length=length, + ) + + def _make_opt_bra(self, type: 'Enum_Option', option: 'Optional[Data_BindingRefreshAdviceOption]' = None, *, + interval: 'int' = 0, + **kwargs: 'Any') -> 'Schema_BindingRefreshAdviceOption': + """Make MH binding refresh advice option. + + Args: + type: Option type. + option: Option data model. + interval: Refresh interval. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + interval = option.interval + + return Schema_BindingRefreshAdviceOption( + type=type, + length=2, + interval=interval, + ) + + def _make_opt_aca(self, type: 'Enum_Option', option: 'Optional[Data_AlternateCareofAddressOption]' = None, *, + address: 'bytes | str | int | IPv6Address' = '::', + **kwargs: 'Any') -> 'Schema_AlternateCareofAddressOption': + """Make MH alternate care-of address option. + + Args: + type: Option type. + option: Option data model. + address: Alternate care-of address. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + address = option.address + + return Schema_AlternateCareofAddressOption( + type=type, + length=16, + address=address, + ) + + def _make_opt_ni(self, type: 'Enum_Option', option: 'Optional[Data_NonceIndicesOption]' = None, *, + home: 'int' = 0, + careof: 'int' = 0, + **kwargs: 'Any') -> 'Schema_NonceIndicesOption': + """Make MH nonce indices option. + + Args: + type: Option type. + option: Option data model. + home: Home nonce index. + careof: Care-of nonce index. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + home = option.home + careof = option.careof + + return Schema_NonceIndicesOption( + type=type, + length=4, + home=home, + careof=careof, + ) + + def _make_opt_bad(self, type: 'Enum_Option', option: 'Optional[Data_AuthorizationDataOption]' = None, *, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_AuthorizationDataOption': + """Make MH binding authorization data option. + + Args: + type: Option type. + option: Option data model. + data: Authenticator. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + data = option.data + + if len(data) % 8 != 0: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid format') + + return Schema_AuthorizationDataOption( + type=type, + length=len(data), + data=data, + ) + + def _make_opt_mnp(self, type: 'Enum_Option', option: 'Optional[Data_MobileNetworkPrefixOption]' = None, *, + prefix: 'bytes | str | IPv6Network' = '::/0', + **kwargs: 'Any') -> 'Schema_MobileNetworkPrefixOption': + """Make MH mobile network prefix option. + + Args: + type: Option type. + option: Option data model. + prefix: Mobile network prefix. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + prefix = option.prefix + + prefix_val = ipaddress.ip_network(prefix) + if prefix_val.version != 6: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid movile network prefix: {prefix!r}') + prefix_length = prefix_val.prefixlen + prefix_addr = prefix_val.network_address + + return Schema_MobileNetworkPrefixOption( + type=type, + length=18, + prefix_length=prefix_length, + prefix=prefix_addr, + ) + + def _make_opt_lla(self, type: 'Enum_Option', option: 'Optional[Data_LinkLayerAddressOption]' = None, *, + address: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_LinkLayerAddressOption': + """Make MH link-layer address option. + + Args: + type: Option type. + option: Option data model. + address: Link-layer address. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + address = option.lla + + return Schema_LinkLayerAddressOption( + type=type, + length=len(address) + 1, + code=Enum_LLACode.MH, # type: ignore[arg-type] + lla=address, + ) + + def _make_opt_mn_id(self, type: 'Enum_Option', option: 'Optional[Data_MNIDOption]' = None, *, + subtype: 'Enum_MNIDSubtype | StdlibEnum | AenumEnum | str | int' = Enum_MNIDSubtype.IPv6_Address, + subtype_default: 'Optional[int]' = None, + subtype_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + subtype_reversed: 'bool' = False, + identifier: 'bytes | str | IPv6Address | int' = '::', + **kwargs: 'Any') -> 'Schema_MNIDOption': + """Make MH mobile node identifier option. + + Args: + type: Option type. + option: Option data model. + subtype: MN-ID subtype. + subtype_default: MN-ID subtype default value. + subtype_namespace: MN-ID subtype namespace. + subtype_reversed: MN-ID subtype reversed flag. + identifier: Identifier. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + subtype_val = option.subtype + identifier = option.identifier + else: + subtype_val = self._make_index(subtype, subtype_default, namespace=subtype_namespace, # type: ignore[assignment] + reversed=subtype_reversed, pack=False) + + if isinstance(identifier, ipaddress.IPv6Address): + id_len = 16 + elif isinstance(identifier, int): + id_len = math.ceil(identifier.bit_length() / 8) + else: + id_len = len(identifier) + + return Schema_MNIDOption( + type=type, + length=1 + id_len, + subtype=subtype_val, + identifier=identifier, + ) + + def _make_opt_auth(self, type: 'Enum_Option', option: 'Optional[Data_AuthOption]' = None, *, + subtype: 'Enum_AuthSubtype | StdlibEnum | AenumEnum | str | int' = Enum_AuthSubtype.MN_HA, + subtype_default: 'Optional[int]' = None, + subtype_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + subtype_reversed: 'bool' = False, + spi: 'int' = 0, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_AuthOption': + """Make MH authentication option. + + Args: + type: Option type. + option: Option data model. + subtype: Authentication subtype. + subtype_default: Authentication subtype default value. + subtype_namespace: Authentication subtype namespace. + subtype_reversed: Authentication subtype reversed flag. + spi: Security parameter index. + data: Authentication data. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + subtype_val = option.subtype + spi = option.spi + data = option.data + else: + subtype_val = self._make_index(subtype, subtype_default, namespace=subtype_namespace, # type: ignore[assignment] + reversed=subtype_reversed, pack=False) + + if (len(data) + 6) % 4 != 0: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid format') + + return Schema_AuthOption( + type=type, + length=5 + len(data), + subtype=subtype_val, + spi=spi, + data=data, + ) + + def _make_opt_mesg_id(self, type: 'Enum_Option', option: 'Optional[Data_MesgIDOption]' = None, *, + timestamp: 'Optional[NTPTimestamp]' = None, + interval: 'Optional[dt_type]' = None, + **kwargs: 'Any') -> 'Schema_MesgIDOption': + """Make MH mobility message replay protection option. + + Args: + type: Option type. + option: Option data model. + timestamp: NTP timestamp, c.f., :rfc:`1305`. + interval: Timestamp interval (since UNIX-epoch). + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + timestamp = option.ntp_timestamp + + if timestamp is None: + interval = interval or datetime.datetime.now(datetime.timezone.utc) + + int_ts = interval.timestamp() + ts_sec = math.floor(int_ts) + ts_frc = math.ceil(((int_ts - ts_sec) * 1_000_000)) * 2**32 + + timestamp = NTPTimestamp(seconds=ts_sec + 2_208_988_800, # 70 years + fraction=ts_frc) + + return Schema_MesgIDOption( + type=type, + length=8, + seconds=timestamp.seconds, + fraction=timestamp.fraction, + ) + + def _make_opt_cga_pr(self, type: 'Enum_Option', option: 'Optional[Data_CGAParametersRequestOption]' = None, + **kwargs: 'Any') -> 'Schema_CGAParametersRequestOption': + """Make MH CGA parameters request option. + + Args: + type: Option type. + option: Option data model. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + return Schema_CGAParametersRequestOption( + type=type, + length=0, + ) + + def _make_opt_cga_param(self, type: 'Enum_Option', option: 'Optional[Data_CGAParametersOption]' = None, *, + parameters: 'Optional[list[Schema_CGAParameter | Data_CGAParameter | dict[str, Any] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_CGAParametersOption': + """Make MH CGA paramters option. + + Args: + type: Option type. + option: Option data model. + parameters: CGA parameters. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + parameters = cast('list[Data_CGAParameter]', option.parameters) # type: ignore[assignment] + + if parameters is None: + parameters = [] + + param = [] # type: list[Schema_CGAParameter | bytes] + length = 0 + for data in parameters: + if isinstance(data, bytes): + length += len(data) + param.append(data) + elif isinstance(data, Schema_CGAParameter): + if not hasattr(data, 'public_key_test'): + data.public_key_test = {'len': max(len(data.public_key) - 2, 0)} + _, ext_len = self._make_cga_extensions(data.extensions) + length += 25 + len(data.public_key) + ext_len + param.append(data) + elif isinstance(data, Data_CGAParameter): + ext, ext_len = self._make_cga_extensions(data.extensions) + schema = Schema_CGAParameter( + modifier=data.modifier, + prefix=data.prefix, + collision_count=data.collision_count, + public_key=data.public_key, + extensions=ext, + ) + schema.public_key_test = {'len': max(len(data.public_key) - 2, 0)} + + length += 25 + len(data.public_key) + ext_len + param.append(schema) + else: + raise ProtocolError(f'{self.alias}: [OptNo {type}] unknown CGA parameter format: {data}') + + return Schema_CGAParametersOption( + type=type, + length=length, + parameters=param, + ) + + def _make_opt_signature(self, type: 'Enum_Option', option: 'Optional[Data_SignatureOption]' = None, *, + signature: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_SignatureOption': + """Make MH signature option. + + Args: + type: Option type. + option: Option data model. + signature: Signature data. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + signature = option.signature + + return Schema_SignatureOption( + type=type, + length=len(signature), + signature=signature, + ) + + def _make_opt_phkt(self, type: 'Enum_Option', option: 'Optional[Data_PermanentHomeKeygenTokenOption]' = None, *, + token: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_PermanentHomeKeygenTokenOption': + """Make MH permanent home keygen token option. + + Args: + type: Option type. + option: Option data model. + token: Token data. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + token = option.token + + return Schema_PermanentHomeKeygenTokenOption( + type=type, + length=len(token), + token=token, + ) + + def _make_opt_ct_init(self, type: 'Enum_Option', option: 'Optional[Data_CareofTestInitOption]' = None, + **kwargs: 'Any') -> 'Schema_CareofTestInitOption': + """Make MH Care-of Test Init option. + + Args: + type: Option type. + option: Option data model. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + return Schema_CareofTestInitOption( + type=type, + length=0, + ) + + def _make_opt_ct(self, type: 'Enum_Option', option: 'Optional[Data_CareofTestOption]' = None, + token: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', + **kwargs: 'Any') -> 'Schema_CareofTestOption': + """Make MH Care-of Test option. + + Args: + type: Option type. + option: Option data model. + token: Care-of keygen token. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + token = option.token + + return Schema_CareofTestOption( + type=type, + length=8, + token=token, + ) + + def _make_opt_exp(self, type: 'Enum_Option', option: 'Optional[Data_ExperimentalMobilityOption]' = None, *, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_ExperimentalMobilityOption': + """Make MH experimental mobility option. + + Args: + type: Option type. + option: Option data model. + data: Experimental data. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + data = option.data + + return Schema_ExperimentalMobilityOption( + type=type, + length=len(data), + data=data, + ) + + def _make_opt_badf(self, type: 'Enum_Option', option: 'Optional[Data_BADFOption]' = None, *, + spi: 'int' = 0, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_BADFOption': + """Make MH binding authorization data for FMIPv6 (BADF) option. + + Args: + type: Option type. + option: Option data model. + spi: Security parameter index. + data: Authenticator. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + Note: + The ``length`` field counts the authenticator only, excluding the + SPI, c.f., :meth:`_read_opt_badf`. + + """ + if option is not None: + spi = option.spi + data = option.data + + if not data: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid format') + + return Schema_BADFOption( + type=type, + length=len(data), + spi=spi, + data=data, + ) + + def _make_opt_ipv6_ap(self, type: 'Enum_Option', option: 'Optional[Data_IPv6AddressPrefixOption]' = None, *, + code: 'IPv6AddressPrefixCode | StdlibEnum | AenumEnum | str | int' = IPv6AddressPrefixCode.New_Care_of_Address, + code_default: 'Optional[int]' = None, + code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + code_reversed: 'bool' = False, + prefix_length: 'int' = 128, + address: 'bytes | str | int | IPv6Address' = '::', + **kwargs: 'Any') -> 'Schema_IPv6AddressPrefixOption': + """Make MH mobility header IPv6 address/prefix option. + + Args: + type: Option type. + option: Option data model. + code: Option code, c.f., + :class:`~pcapkit.protocols.internet.mh.IPv6AddressPrefixCode`. + code_default: Default option code. + code_namespace: Option code namespace. + code_reversed: Reverse option code namespace. + prefix_length: Prefix length. + address: IPv6 address/prefix. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + code_val = option.code + prefix_length = option.prefix_length + address = option.address + else: + code_val = self._make_index(code, code_default, namespace=code_namespace, # type: ignore[assignment] + reversed=code_reversed, pack=False) + + if prefix_length > 128: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid prefix length: {prefix_length}') + + return Schema_IPv6AddressPrefixOption( + type=type, + length=18, + code=code_val, + prefix_length=prefix_length, + address=address, + ) + + def _make_opt_dns(self, type: 'Enum_Option', option: 'Optional[Data_DNSUpdateOption]' = None, *, + status: 'Enum_DNSStatusCode | StdlibEnum | AenumEnum | str | int' = Enum_DNSStatusCode.DNS_update_performed, + status_default: 'Optional[int]' = None, + status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + status_reversed: 'bool' = False, + remove: 'bool' = False, + identity: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_DNSUpdateOption': + """Make MH DNS-UPDATE-TYPE option. + + Args: + type: Option type. + option: Option data model. + status: Status. + status_default: Default status. + status_namespace: Status namespace. + status_reversed: Reverse status namespace. + remove: Remove flag. + identity: Mobile node identity, in FQDN form. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + status_val = option.status # type: Enum_DNSStatusCode + remove = option.remove + identity = option.identity + else: + status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] + reversed=status_reversed, pack=False) + + return Schema_DNSUpdateOption( + type=type, + length=2 + len(identity), + status=status_val, + flags={'R': int(remove)}, + identity=identity, + ) + + def _make_opt_vendor(self, type: 'Enum_Option', option: 'Optional[Data_VendorSpecificOption]' = None, *, + vendor: 'int' = 0, + subtype: 'int' = 0, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_VendorSpecificOption': + """Make MH vendor specific mobility option. + + Args: + type: Option type. + option: Option data model. + vendor: Vendor ID, an SMI Network Management Private Enterprise + Number. + subtype: Vendor-administered sub-type. + data: Vendor-specific data. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + vendor = option.vendor + subtype = option.subtype + data = option.data + + return Schema_VendorSpecificOption( + type=type, + length=5 + len(data), + vendor=vendor, + subtype=subtype, + data=data, + ) + + def _make_opt_service(self, type: 'Enum_Option', option: 'Optional[Data_ServiceSelectionOption]' = None, *, + identifier: 'str' = '', + **kwargs: 'Any') -> 'Schema_ServiceSelectionOption': + """Make MH service selection mobility option. + + Args: + type: Option type. + option: Option data model. + identifier: Service identifier. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + identifier = option.identifier + + encoded = identifier.encode() + if not encoded: + raise ProtocolError(f'{self.alias}: [OptNo {type}] service identifier must not be empty') + + return Schema_ServiceSelectionOption( + type=type, + length=len(encoded), + identifier=identifier, + ) + + def _make_opt_hnp(self, type: 'Enum_Option', option: 'Optional[Data_HomeNetworkPrefixOption]' = None, *, + prefix_length: 'int' = 64, + prefix: 'bytes | str | int | IPv6Address' = '::', + **kwargs: 'Any') -> 'Schema_HomeNetworkPrefixOption': + """Make MH home network prefix option. + + Args: + type: Option type. + option: Option data model. + prefix_length: Prefix length. + prefix: Home network prefix. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + prefix_length = option.prefix_length + prefix = option.prefix + + if prefix_length > 128: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid prefix length: {prefix_length}') + + return Schema_HomeNetworkPrefixOption( + type=type, + length=18, + prefix_length=prefix_length, + prefix=prefix, + ) + + def _make_opt_hi(self, type: 'Enum_Option', option: 'Optional[Data_HandoffIndicatorOption]' = None, *, + hi: 'Enum_HandoffType | StdlibEnum | AenumEnum | str | int' = Enum_HandoffType.Attachment_over_a_new_interface, + hi_default: 'Optional[int]' = None, + hi_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + hi_reversed: 'bool' = False, + **kwargs: 'Any') -> 'Schema_HandoffIndicatorOption': + """Make MH handoff indicator option. + + Args: + type: Option type. + option: Option data model. + hi: Handoff indicator. + hi_default: Default handoff indicator. + hi_namespace: Handoff indicator namespace. + hi_reversed: Reverse handoff indicator namespace. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + hi_val = option.hi # type: Enum_HandoffType + else: + hi_val = self._make_index(hi, hi_default, namespace=hi_namespace, # type: ignore[assignment] + reversed=hi_reversed, pack=False) + + return Schema_HandoffIndicatorOption( + type=type, + length=2, + hi=hi_val, + ) + + def _make_opt_att(self, type: 'Enum_Option', option: 'Optional[Data_AccessTechnologyTypeOption]' = None, *, + att: 'Enum_AccessType | StdlibEnum | AenumEnum | str | int' = Enum_AccessType.Virtual, + att_default: 'Optional[int]' = None, + att_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + att_reversed: 'bool' = False, + **kwargs: 'Any') -> 'Schema_AccessTechnologyTypeOption': + """Make MH access technology type option. + + Args: + type: Option type. + option: Option data model. + att: Access technology type. + att_default: Default access technology type. + att_namespace: Access technology type namespace. + att_reversed: Reverse access technology type namespace. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + att_val = option.att # type: Enum_AccessType + else: + att_val = self._make_index(att, att_default, namespace=att_namespace, # type: ignore[assignment] + reversed=att_reversed, pack=False) + + return Schema_AccessTechnologyTypeOption( + type=type, + length=2, + att=att_val, + ) + + def _make_opt_mn_lli(self, type: 'Enum_Option', option: 'Optional[Data_MNLLIdentifierOption]' = None, *, + lli: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_MNLLIdentifierOption': + """Make MH mobile node link-layer identifier option. + + Args: + type: Option type. + option: Option data model. + lli: Link-layer identifier. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + lli = option.lli + + return Schema_MNLLIdentifierOption( + type=type, + length=2 + len(lli), + lli=lli, + ) + + def _make_opt_lla_addr(self, type: 'Enum_Option', option: 'Optional[Data_LinkLocalAddressOption]' = None, *, + address: 'bytes | str | int | IPv6Address' = '::', + **kwargs: 'Any') -> 'Schema_LinkLocalAddressOption': + """Make MH link-local address option. + + Args: + type: Option type. + option: Option data model. + address: Link-local address. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + address = option.address + + return Schema_LinkLocalAddressOption( + type=type, + length=16, + address=address, + ) + + def _make_opt_timestamp(self, type: 'Enum_Option', option: 'Optional[Data_TimestampOption]' = None, *, + seconds: 'int' = 0, + fraction: 'int' = 0, + **kwargs: 'Any') -> 'Schema_TimestampOption': + """Make MH timestamp option. + + Args: + type: Option type. + option: Option data model. + seconds: Seconds since 1 January 1970, in the leading 48 bits. + fraction: Fraction of a second, in units of 1/65536. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + Note: + The fixed-point pair is taken from + :attr:`~pcapkit.protocols.data.internet.mh.TimestampOption.pmip_timestamp` + rather than recomputed from + :attr:`~pcapkit.protocols.data.internet.mh.TimestampOption.timestamp`, + since a :class:`~datetime.datetime` holds microseconds and the wire + format holds 1/65536ths -- converting between them does not round-trip. + + """ + if option is not None: + seconds, fraction = option.pmip_timestamp + + if not 0 <= seconds < 2 ** 48: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid timestamp seconds: {seconds}') + if not 0 <= fraction < 2 ** 16: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid timestamp fraction: {fraction}') + + return Schema_TimestampOption( + type=type, + length=8, + timestamp={ + 'seconds': seconds, + 'fraction': fraction, + }, ) - def _make_msg_hot(self, message: 'Optional[Data_HomeTestMessage]' = None, *, - nonce_index: 'int' = 0, - cookie: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', - token: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, - **kwargs: 'Any') -> 'Schema_HomeTestMessage': - """Make MH home test (HoT) message type. + def _make_opt_restart(self, type: 'Enum_Option', option: 'Optional[Data_RestartCounterOption]' = None, *, + counter: 'int' = 0, + **kwargs: 'Any') -> 'Schema_RestartCounterOption': + """Make MH restart counter option. Args: - message: Message data model. - nonce_index: Home nonce index. - cookie: Home test cookie. - token: Home test token. - options: Mobility options. + type: Option type. + option: Option data model. + counter: Restart counter. **kwargs: Arbitrary keyword arguments. Returns: - Constructed message type. + Constructed option schema. """ - if message is not None: - nonce_index = message.nonce_index - cookie = message.cookie - token = message.token - options = message.options + if option is not None: + counter = option.counter + + return Schema_RestartCounterOption( + type=type, + length=4, + counter=counter, + ) + + def _make_opt_ipv4_hoa(self, type: 'Enum_Option', option: 'Optional[Data_IPv4HomeAddressOption]' = None, *, + prefix_length: 'int' = 32, + address: 'bytes | str | int | IPv4Address' = '0.0.0.0', + request_prefix: 'bool' = False, + **kwargs: 'Any') -> 'Schema_IPv4HomeAddressOption': + """Make MH IPv4 home address option. + + Args: + type: Option type. + option: Option data model. + prefix_length: Prefix length; ``32`` for a single address. + address: IPv4 home address. + request_prefix: Mobile network prefix request flag. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + prefix_length = option.prefix_length + address = option.address + request_prefix = option.request_prefix + + if prefix_length == 0 or prefix_length > 32: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid prefix length: {prefix_length}') + + return Schema_IPv4HomeAddressOption( + type=type, + length=6, + flags={ + 'prefix_length': prefix_length, + 'P': int(request_prefix), + }, + address=address, + ) + + def _make_opt_ipv4_ack(self, type: 'Enum_Option', + option: 'Optional[Data_IPv4AddressAcknowledgementOption]' = None, *, + status: 'Enum_DSMIPv6HomeAddress | StdlibEnum | AenumEnum | str | int' = Enum_DSMIPv6HomeAddress.Success, + status_default: 'Optional[int]' = None, + status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + status_reversed: 'bool' = False, + prefix_length: 'int' = 32, + address: 'bytes | str | int | IPv4Address' = '0.0.0.0', + **kwargs: 'Any') -> 'Schema_IPv4AddressAcknowledgementOption': + """Make MH IPv4 address acknowledgement option. + + Args: + type: Option type. + option: Option data model. + status: Status. + status_default: Default status. + status_namespace: Status namespace. + status_reversed: Reverse status namespace. + prefix_length: Allocated prefix length. + address: Assigned IPv4 home address. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + status_val = option.status # type: Enum_DSMIPv6HomeAddress + prefix_length = option.prefix_length + address = option.address else: - options = options or [] + status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] + reversed=status_reversed, pack=False) - return Schema_HomeTestMessage( - nonce_index=nonce_index, - cookie=cookie, - token=token, - options=self._make_mh_options(options), + if prefix_length > 32: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid prefix length: {prefix_length}') + + return Schema_IPv4AddressAcknowledgementOption( + type=type, + length=6, + status=status_val, + flags={'prefix_length': prefix_length}, + address=address, ) - def _make_msg_cot(self, message: 'Optional[Data_CareofTestMessage]' = None, *, - nonce_index: 'int' = 0, - cookie: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', - token: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, - **kwargs: 'Any') -> 'Schema_CareofTestMessage': - """Make MH care-of test (CoT) message type. + def _make_opt_nat(self, type: 'Enum_Option', option: 'Optional[Data_NATDetectionOption]' = None, *, + force: 'bool' = False, + refresh: 'int | timedelta' = 0, + **kwargs: 'Any') -> 'Schema_NATDetectionOption': + """Make MH NAT detection option. + + Args: + type: Option type. + option: Option data model. + force: Force UDP encapsulation flag. + refresh: Suggested NAT binding refresh interval, in seconds or as a + :class:`~datetime.timedelta`. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + force = option.force + refresh_val = math.ceil(option.refresh.total_seconds()) + else: + refresh_val = refresh if isinstance(refresh, int) else math.ceil(refresh.total_seconds()) + + return Schema_NATDetectionOption( + type=type, + length=6, + flags={'F': int(force)}, + refresh=refresh_val, + ) + + def _make_opt_ipv4_coa(self, type: 'Enum_Option', option: 'Optional[Data_IPv4CareofAddressOption]' = None, *, + address: 'bytes | str | int | IPv4Address' = '0.0.0.0', + **kwargs: 'Any') -> 'Schema_IPv4CareofAddressOption': + """Make MH IPv4 care-of address option. + + Args: + type: Option type. + option: Option data model. + address: IPv4 care-of address. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + address = option.address + + return Schema_IPv4CareofAddressOption( + type=type, + length=6, + address=address, + ) + + def _make_opt_gre(self, type: 'Enum_Option', option: 'Optional[Data_GREKeyOption]' = None, *, + key: 'Optional[int]' = None, + **kwargs: 'Any') -> 'Schema_GREKeyOption': + """Make MH GRE key option. + + Args: + type: Option type. + option: Option data model. + key: GRE key identifier, or :obj:`None` to omit it -- which is what an + option length of 2 means on the wire. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + key = option.key + + return Schema_GREKeyOption( + type=type, + length=2 if key is None else 6, + key=key, + ) + + def _make_opt_bid(self, type: 'Enum_Option', option: 'Optional[Data_BindingIdentifierOption]' = None, *, + bid: 'int' = 1, + status: 'Enum_StatusCode | StdlibEnum | AenumEnum | str | int' = Enum_StatusCode.Binding_Update_accepted_Proxy_Binding_Update_accepted, + status_default: 'Optional[int]' = None, + status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + status_reversed: 'bool' = False, + simultaneous: 'bool' = False, + bid_pri: 'int' = 0, + address: 'Optional[bytes | str | int | IPv4Address | IPv6Address]' = None, + **kwargs: 'Any') -> 'Schema_BindingIdentifierOption': + """Make MH binding identifier (BID) option. + + Args: + type: Option type. + option: Option data model. + bid: Binding identifier. + status: Status, overriding the message status for this binding alone. + status_default: Default status. + status_namespace: Status namespace. + status_reversed: Reverse status namespace. + simultaneous: Simultaneous home and foreign binding flag. + bid_pri: Binding priority. + address: Care-of address, or :obj:`None` to omit it. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + Note: + The option length is derived from the address family, since that is + the only thing that carries it on the wire: 4 with no address, 8 for + an IPv4 one and 20 for an IPv6 one [:rfc:`5648#section-4.3`]. + + """ + if option is not None: + bid = option.bid + status_val = option.status # type: Enum_StatusCode + simultaneous = option.simultaneous + bid_pri = option.bid_pri + address = option.address + else: + status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] + reversed=status_reversed, pack=False) + + if bid_pri > 0x7F: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid binding priority: {bid_pri}') + + if address is None: + length = 4 + else: + addr = ipaddress.ip_address(address) if not isinstance( + address, (ipaddress.IPv4Address, ipaddress.IPv6Address)) else address + length = 8 if addr.version == 4 else 20 + address = addr + + return Schema_BindingIdentifierOption( + type=type, + length=length, + bid=bid, + status=status_val, + flags={ + 'H': int(simultaneous), + 'BID_PRI': bid_pri, + }, + address=address, + ) + + def _make_opt_ipv4_hoa_req(self, type: 'Enum_Option', + option: 'Optional[Data_IPv4HomeAddressRequestOption]' = None, *, + prefix_length: 'int' = 32, + address: 'bytes | str | int | IPv4Address' = '0.0.0.0', + **kwargs: 'Any') -> 'Schema_IPv4HomeAddressRequestOption': + """Make MH IPv4 home address request option. + + Args: + type: Option type. + option: Option data model. + prefix_length: Prefix length of the requested home network. + address: Requested IPv4 home address. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + prefix_length = option.prefix_length + address = option.address + + if prefix_length > 32: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid prefix length: {prefix_length}') + + return Schema_IPv4HomeAddressRequestOption( + type=type, + length=6, + flags={'prefix_length': prefix_length}, + address=address, + ) + + def _make_opt_ipv4_hoa_rep(self, type: 'Enum_Option', option: 'Optional[Data_IPv4HomeAddressReplyOption]' = None, *, + status: 'Enum_HomeAddressReply | StdlibEnum | AenumEnum | str | int' = Enum_HomeAddressReply.Success, + status_default: 'Optional[int]' = None, + status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + status_reversed: 'bool' = False, + prefix_length: 'int' = 32, + address: 'bytes | str | int | IPv4Address' = '0.0.0.0', + **kwargs: 'Any') -> 'Schema_IPv4HomeAddressReplyOption': + """Make MH IPv4 home address reply option. + + Args: + type: Option type. + option: Option data model. + status: Status. + status_default: Default status. + status_namespace: Status namespace. + status_reversed: Reverse status namespace. + prefix_length: Prefix length of the assigned home network. + address: Assigned IPv4 home address. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + status_val = option.status # type: Enum_HomeAddressReply + prefix_length = option.prefix_length + address = option.address + else: + status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] + reversed=status_reversed, pack=False) + + if prefix_length > 32: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid prefix length: {prefix_length}') + + return Schema_IPv4HomeAddressReplyOption( + type=type, + length=6, + status=status_val, + flags={'prefix_length': prefix_length}, + address=address, + ) + + def _make_opt_ipv4_router(self, type: 'Enum_Option', + option: 'Optional[Data_IPv4DefaultRouterAddressOption]' = None, *, + address: 'bytes | str | int | IPv4Address' = '0.0.0.0', + **kwargs: 'Any') -> 'Schema_IPv4DefaultRouterAddressOption': + """Make MH IPv4 default-router address option. + + Args: + type: Option type. + option: Option data model. + address: IPv4 default-router address. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + address = option.address + + return Schema_IPv4DefaultRouterAddressOption( + type=type, + length=6, + address=address, + ) + + def _make_opt_ipv4_dhcp(self, type: 'Enum_Option', option: 'Optional[Data_IPv4DHCPSupportModeOption]' = None, *, + mode: 'Enum_DHCPSupportMode | StdlibEnum | AenumEnum | str | int' = Enum_DHCPSupportMode.Unassigned_0x0, + mode_default: 'Optional[int]' = None, + mode_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + mode_reversed: 'bool' = False, + **kwargs: 'Any') -> 'Schema_IPv4DHCPSupportModeOption': + """Make MH IPv4 DHCP support mode option. + + Args: + type: Option type. + option: Option data model. + mode: DHCP support mode. + mode_default: Default DHCP support mode. + mode_namespace: DHCP support mode namespace. + mode_reversed: Reverse DHCP support mode namespace. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + mode_val = int(option.mode) + else: + mode_val = self._make_index(mode, mode_default, namespace=mode_namespace, + reversed=mode_reversed, pack=False) + + if mode_val > 1: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid DHCP support mode: {mode_val}') + + return Schema_IPv4DHCPSupportModeOption( + type=type, + length=2, + flags={'S': mode_val}, + ) + + def _make_opt_cr(self, type: 'Enum_Option', option: 'Optional[Data_ContextRequestOption]' = None, *, + requests: 'Optional[list[Data_ContextRequest | tuple[Enum_Option | int, bytes]]]' = None, + **kwargs: 'Any') -> 'Schema_ContextRequestOption': + """Make MH context request option. + + Args: + type: Option type. + option: Option data model. + requests: Requested contexts, each either a data model or a + ``(mobility option type, extra data)`` pair. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + requests = cast('list[Data_ContextRequest | tuple[Enum_Option | int, bytes]]', + list(option.requests)) + else: + requests = requests or [] + + buffer = bytearray() + for request in requests: + if isinstance(request, Data_ContextRequest): + req_type, value = int(request.type), request.value + else: + raw_type, value = request + req_type = int(raw_type) + + if len(value) > 0xFF: + raise ProtocolError(f'{self.alias}: [OptNo {type}] context request value of ' + f'{len(value)} octets does not fit its length field') + + buffer.append(req_type) + buffer.append(len(value)) + buffer.extend(value) + + return Schema_ContextRequestOption( + type=type, + length=2 + len(buffer), + requests=bytes(buffer), + ) + + def _make_opt_lmaa(self, type: 'Enum_Option', option: 'Optional[Data_LMAAddressOption]' = None, *, + code: 'LMAAddressCode | StdlibEnum | AenumEnum | str | int' = LMAAddressCode.IPv6_LMAA, + code_default: 'Optional[int]' = None, + code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + code_reversed: 'bool' = False, + address: 'bytes | str | int | IPv4Address | IPv6Address' = '::', + **kwargs: 'Any') -> 'Schema_LMAAddressOption': + """Make MH local mobility anchor address option. Args: - message: Message data model. - nonce_index: Care-of nonce index. - cookie: Care-of test cookie. - token: Care-of test token. - options: Mobility options. + type: Option type. + option: Option data model. + code: Option code, c.f., + :class:`~pcapkit.protocols.internet.mh.LMAAddressCode`. + code_default: Default option code. + code_namespace: Option code namespace. + code_reversed: Reverse option code namespace. + address: Local mobility anchor address. **kwargs: Arbitrary keyword arguments. Returns: - Constructed message type. + Constructed option schema. + + Note: + The option length is derived from the address family rather than from + :attr:`code`, so that the two cannot be emitted disagreeing. """ - if message is not None: - nonce_index = message.nonce_index - cookie = message.cookie - token = message.token - options = message.options + if option is not None: + code_val = int(option.code) + address = option.address else: - options = options or [] + code_val = self._make_index(code, code_default, namespace=code_namespace, + reversed=code_reversed, pack=False) - return Schema_CareofTestMessage( - nonce_index=nonce_index, - cookie=cookie, - token=token, - options=self._make_mh_options(options), + if isinstance(address, bytes) and len(address) not in (4, 16): + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid address: {address!r}') + + # NOTE: The address is normalised rather than passed through, so that the + # schema attribute holds the same type it would after a parse. The width is + # then taken from the address itself rather than from ``code``, so that the + # two cannot be emitted disagreeing. + if isinstance(address, bytes): + addr = ipaddress.ip_address(address) # type: IPv4Address | IPv6Address + elif isinstance(address, (ipaddress.IPv4Address, ipaddress.IPv6Address)): + addr = address + else: + addr = ipaddress.ip_address(address) + + return Schema_LMAAddressOption( + type=type, + length=6 if addr.version == 4 else 18, + code=code_val, + address=addr, ) - def _make_msg_bu(self, message: 'Optional[Data_BindingUpdateMessage]' = None, *, - seq: 'int' = 0, - ack: 'bool' = False, - home: 'bool' = False, - lla_compat: 'bool' = False, - key_mngt: 'bool' = False, - lifetime: 'int | timedelta' = 4, # reasonable default value - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, - **kwargs: 'Any') -> 'Schema_BindingUpdateMessage': - """Make MH binding update (BU) message type. + def _make_opt_mn_lla_iid(self, type: 'Enum_Option', option: 'Optional[Data_MNLLAIIDOption]' = None, *, + iid: 'bytes' = b'\x00' * 8, + **kwargs: 'Any') -> 'Schema_MNLLAIIDOption': + """Make MH mobile node link-local address interface identifier option. Args: - message: Message data model. - seq: Sequence number. - ack: Acknowledgement flag. - home: Home registration flag. - lla_compat: LLA compatibility flag. - key_mngt: Key management mobility option flag. - lifetime: Lifetime in seconds or timedelta. - options: Mobility options. + type: Option type. + option: Option data model. + iid: Interface identifier; exactly 8 octets. **kwargs: Arbitrary keyword arguments. Returns: - Constructed message type. + Constructed option schema. """ - if message is not None: - seq = message.seq - ack = message.ack - home = message.home - lla_compat = message.lla_compat - key_mngt = message.key_mngt - lifetime_val = math.ceil(message.lifetime.total_seconds()) - options = message.options - else: - lifetime_val = lifetime if isinstance(lifetime, int) else math.ceil(lifetime.total_seconds()) - options = options or [] + if option is not None: + iid = option.iid - return Schema_BindingUpdateMessage( - seq=seq, - flags={ - 'A': ack, - 'H': home, - 'L': lla_compat, - 'K': key_mngt, - }, - lifetime=math.ceil(lifetime_val / 4), - options=self._make_mh_options(options), + if len(iid) != 8: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid interface identifier: {iid!r}') + + return Schema_MNLLAIIDOption( + type=type, + length=10, + iid=iid, ) - def _make_msg_ba(self, message: 'Optional[Data_BindingAcknowledgementMessage]' = None, *, - status: 'Enum_StatusCode | StdlibEnum | AenumEnum | str | int' = Enum_StatusCode.Binding_Update_accepted_Proxy_Binding_Update_accepted, - status_default: 'Optional[int]' = None, - status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - status_reversed: 'bool' = False, - key_mngt: 'bool' = False, - seq: 'int' = 0, - lifetime: 'int | timedelta' = 4, # reasonable default value - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, - **kwargs: 'Any') -> 'Schema_BindingAcknowledgementMessage': - """Make MH binding acknowledge (BA) message type. + def _make_opt_transient(self, type: 'Enum_Option', option: 'Optional[Data_TransientBindingOption]' = None, *, + late: 'bool' = False, + lifetime: 'int | timedelta' = 0, + **kwargs: 'Any') -> 'Schema_TransientBindingOption': + """Make MH transient binding option. Args: - message: Message data model. - status: Status code. - status_default: Default status code. - status_namespace: Status code namespace. - status_reversed: Reverse status code namespace. - key_mngt: Key management mobility option flag. - seq: Sequence number. - lifetime: Lifetime in seconds or timedelta. - options: Mobility options. + type: Option type. + option: Option data model. + late: Late path switch flag. + lifetime: Maximum lifetime of the transient state, in units of 100 + milliseconds or as a :class:`~datetime.timedelta`. **kwargs: Arbitrary keyword arguments. Returns: - Constructed message type. + Constructed option schema. """ - if message is not None: - status_val = message.status - key_mngt = message.key_mngt - seq = message.seq - lifetime_val = math.ceil(message.lifetime.total_seconds()) - options = message.options + if option is not None: + late = option.late + lifetime_val = math.ceil(option.lifetime.total_seconds() * 10) else: - status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] - reversed=status_reversed, pack=False) - lifetime_val = lifetime if isinstance(lifetime, int) else math.ceil(lifetime.total_seconds()) - options = options or [] + lifetime_val = lifetime if isinstance(lifetime, int) else math.ceil( + lifetime.total_seconds() * 10) - return Schema_BindingAcknowledgementMessage( - status=status_val, - flags={ - 'K': key_mngt, - }, - seq=seq, - lifetime=math.ceil(lifetime_val / 4), - options=self._make_mh_options(options), + if lifetime_val > 0xFF: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid lifetime: {lifetime_val}') + + return Schema_TransientBindingOption( + type=type, + length=2, + flags={'L': int(late)}, + lifetime=lifetime_val, ) - def _make_msg_be(self, message: 'Optional[Data_BindingErrorMessage]' = None, *, - status: 'Enum_StatusCode | StdlibEnum | AenumEnum | str | int' = Enum_StatusCode.Binding_Update_accepted_Proxy_Binding_Update_accepted, - status_default: 'Optional[int]' = None, - status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - status_reversed: 'bool' = False, - home: 'IPv6Address | int | str | bytes' = '::', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, - **kwargs: 'Any') -> 'Schema_BindingErrorMessage': - """Make MH binding error (BE) message type. + def _make_opt_fs(self, type: 'Enum_Option', option: 'Optional[Data_FlowSummaryOption]' = None, *, + fid: 'Optional[list[int]]' = None, + **kwargs: 'Any') -> 'Schema_FlowSummaryOption': + """Make MH flow summary mobility option. Args: - message: Message data model. - status: Status code. - status_default: Default status code. - status_namespace: Status code namespace. - status_reversed: Reverse status code namespace. - home: Home address. - options: Mobility options. + type: Option type. + option: Option data model. + fid: Flow identifiers being refreshed. **kwargs: Arbitrary keyword arguments. Returns: - Constructed message type. + Constructed option schema. """ - if message is not None: - status_val = message.status - home = message.home - options = message.options + if option is not None: + fid = list(option.fid) else: - status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] - reversed=status_reversed, pack=False) - options = options or [] + fid = fid or [] - return Schema_BindingErrorMessage( - status=status_val, - home=home, - options=self._make_mh_options(options), + return Schema_FlowSummaryOption( + type=type, + length=len(fid) * 2, + fid=fid, ) - def _make_msg_fbu(self, message: 'Optional[Data_FastBindingUpdateMessage]' = None, *, - seq: 'int' = 0, - ack: 'bool' = True, # MUST be set, c.f., RFC 5568, section 6.2.2 - home: 'bool' = True, # MUST be set, c.f., RFC 5568, section 6.2.2 - lla_compat: 'bool' = False, - key_mngt: 'bool' = False, - lifetime: 'int | timedelta' = 4, # reasonable default value - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, - **kwargs: 'Any') -> 'Schema_FastBindingUpdateMessage': - """Make MH fast binding update (FBU) message type. + def _make_fid_suboptions( + self, + suboptions: 'FlowIDSuboption | list[Schema_FlowIdentificationSuboption | tuple[Enum_FlowIDSuboption, dict[str, Any]] | bytes]', + ) -> 'list[Schema_FlowIdentificationSuboption | bytes]': + """Make MH flow identification sub-options. Args: - message: Message data model. - seq: Sequence number. - ack: Acknowledgement flag. - home: Home registration flag. - lla_compat: LLA compatibility flag. - key_mngt: Key management mobility option flag. - lifetime: Lifetime in seconds or timedelta. - options: Mobility options. - **kwargs: Arbitrary keyword arguments. + suboptions: Sub-options, as parsed data, schemas, ``(type, kwargs)`` + pairs, or raw octets. Returns: - Constructed message type. + Sub-option schema list. """ - if message is not None: - seq = message.seq - ack = message.ack - home = message.home - lla_compat = message.lla_compat - key_mngt = message.key_mngt - lifetime_val = math.ceil(message.lifetime.total_seconds()) - options = message.options - else: - lifetime_val = lifetime if isinstance(lifetime, int) else math.ceil(lifetime.total_seconds()) - options = options or [] + if isinstance(suboptions, list): + entries = [] # type: list[Schema_FlowIdentificationSuboption | bytes] + for item in suboptions: + if isinstance(item, (bytes, Schema)): + entries.append(cast('Schema_FlowIdentificationSuboption | bytes', item)) + else: + code, args = cast('tuple[Enum_FlowIDSuboption, dict[str, Any]]', item) + entries.append(self._make_fid_suboption(code, **args)) + return entries - return Schema_FastBindingUpdateMessage( - seq=seq, - flags={ - 'A': ack, - 'H': home, - 'L': lla_compat, - 'K': key_mngt, - }, - lifetime=math.ceil(lifetime_val / 4), - options=self._make_mh_options(options), - ) + return [self._make_fid_suboption(code, data=data) + for code, data in suboptions.items(multi=True)] - def _make_msg_fback(self, message: 'Optional[Data_FastBindingAcknowledgmentMessage]' = None, *, - status: 'FastBindingAcknowledgmentStatus | StdlibEnum | AenumEnum | str | int' = FastBindingAcknowledgmentStatus.Fast_Binding_Update_accepted, - status_default: 'Optional[int]' = None, - status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - status_reversed: 'bool' = False, - key_mngt: 'bool' = False, - seq: 'int' = 0, - lifetime: 'int | timedelta' = 4, # reasonable default value - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, - **kwargs: 'Any') -> 'Schema_FastBindingAcknowledgmentMessage': - """Make MH fast binding acknowledgment (FBack) message type. + def _make_fid_suboption(self, code: 'Enum_FlowIDSuboption', + data: 'Optional[Data_FlowIdentificationSuboption]' = None, + **kwargs: 'Any') -> 'Schema_FlowIdentificationSuboption': + """Make one MH flow identification sub-option. Args: - message: Message data model. - status: Status code, c.f., - :class:`~pcapkit.protocols.internet.mh.FastBindingAcknowledgmentStatus`. - status_default: Default status code. - status_namespace: Status code namespace. - status_reversed: Reverse status code namespace. - key_mngt: Key management mobility option flag. - seq: Sequence number. - lifetime: Lifetime in seconds or timedelta. - options: Mobility options. - **kwargs: Arbitrary keyword arguments. + code: Sub-option type. + data: Sub-option data model. + **kwargs: Sub-option fields, when no data model is given. Returns: - Constructed message type. + Constructed sub-option schema. """ - if message is not None: - status_val = message.status - key_mngt = message.key_mngt - seq = message.seq - lifetime_val = math.ceil(message.lifetime.total_seconds()) - options = message.options - else: - status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] - reversed=status_reversed, pack=False) - lifetime_val = lifetime if isinstance(lifetime, int) else math.ceil(lifetime.total_seconds()) - options = options or [] - - return Schema_FastBindingAcknowledgmentMessage( - status=status_val, - flags={ - 'K': key_mngt, - }, - seq=seq, - lifetime=math.ceil(lifetime_val / 4), - options=self._make_mh_options(options), - ) + if isinstance(data, Data_PadFlowIdentificationSuboption) or ( + data is None and code in (Enum_FlowIDSuboption.Pad, + Enum_FlowIDSuboption.PadN)): + if code == Enum_FlowIDSuboption.Pad: + pad_len = 0 + elif data is not None: + pad_len = data.length - 2 + else: + pad_len = cast('int', kwargs.get('length', 0)) + return Schema_PadFlowIdentificationSuboption(type=code, length=pad_len) - def _make_msg_fna(self, message: 'Optional[Data_FastNeighborAdvertisementMessage]' = None, *, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, - **kwargs: 'Any') -> 'Schema_FastNeighborAdvertisementMessage': - """Make MH fast neighbor advertisement (FNA) message type. + if isinstance(data, Data_BIDReferenceSuboption): + bid = list(data.bid) + else: + bid = cast('list[int]', kwargs.get('bid') or []) + if code == Enum_FlowIDSuboption.BID_Reference: + return Schema_BIDReferenceSuboption(type=code, length=len(bid) * 2, bid=bid) + + if code == Enum_FlowIDSuboption.Traffic_Selector: + if isinstance(data, Data_TrafficSelectorSuboption): + ts_format = data.ts_format # type: Enum_TrafficSelector | int + selector = data.selector + else: + ts_format = cast('Enum_TrafficSelector | int', + kwargs.get('ts_format', + Enum_TrafficSelector.IPv6_Binary_Traffic_Selector)) + selector = cast('bytes', kwargs.get('selector', b'')) + return Schema_TrafficSelectorSuboption( + type=code, length=2 + len(selector), + ts_format=cast('Enum_TrafficSelector', ts_format), selector=selector) + + if code == Enum_FlowIDSuboption.Flow_Binding_Action: + if isinstance(data, Data_FlowBindingActionSuboption): + action = data.action # type: Enum_FlowBindingAction | int + else: + action = cast('Enum_FlowBindingAction | int', + kwargs.get('action', Enum_FlowBindingAction.Add)) + return Schema_FlowBindingActionSuboption( + type=code, length=2, action=cast('Enum_FlowBindingAction', action)) + + if code == Enum_FlowIDSuboption.Target_Care_of_Address: + if isinstance(data, Data_TargetCareofAddressSuboption): + address = data.address # type: Any + else: + address = kwargs.get('address', '::') + addr = address if isinstance( + address, (ipaddress.IPv4Address, ipaddress.IPv6Address) + ) else ipaddress.ip_address(address) + return Schema_TargetCareofAddressSuboption( + type=code, length=6 if addr.version == 4 else 18, address=addr) + + if isinstance(data, Data_UnassignedFlowIdentificationSuboption): + payload = data.data + else: + payload = cast('bytes', kwargs.get('data', b'')) + return Schema_UnassignedFlowIdentificationSuboption( + type=code, length=len(payload), data=payload) + + def _make_opt_fid(self, type: 'Enum_Option', option: 'Optional[Data_FlowIdentificationOption]' = None, *, + fid: 'int' = 1, + fid_pri: 'int' = 1, + status: 'Enum_FlowIDStatus | StdlibEnum | AenumEnum | str | int' = Enum_FlowIDStatus.Flow_binding_successful, + status_default: 'Optional[int]' = None, + status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + status_reversed: 'bool' = False, + suboptions: 'Optional[FlowIDSuboption | list[Schema_FlowIdentificationSuboption | tuple[Enum_FlowIDSuboption, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_FlowIdentificationOption': + """Make MH flow identification mobility option. Args: - message: Message data model. - options: Mobility options. + type: Option type. + option: Option data model. + fid: Flow identifier. + fid_pri: Flow priority. + status: Status. + status_default: Default status. + status_namespace: Status namespace. + status_reversed: Reverse status namespace. + suboptions: Sub-options. **kwargs: Arbitrary keyword arguments. Returns: - Constructed message type. + Constructed option schema. """ - if message is not None: - options = message.options + if option is not None: + fid = option.fid + fid_pri = option.fid_pri + status_val = option.status # type: Enum_FlowIDStatus + suboptions = option.suboptions else: - options = options or [] + status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] + reversed=status_reversed, pack=False) + suboptions = suboptions or [] - return Schema_FastNeighborAdvertisementMessage( - options=self._make_mh_options(options), + entries = self._make_fid_suboptions(suboptions) + length = 6 + sum(len(entry) if isinstance(entry, bytes) else len(entry.pack()) + for entry in entries) + + return Schema_FlowIdentificationOption( + type=type, + length=length, + fid=fid, + fid_pri=fid_pri, + status=status_val, + suboptions=entries, ) - def _make_msg_emh(self, message: 'Optional[Data_ExperimentalMessage]' = None, *, - data: 'bytes' = b'\x00\x00', # 2 bytes of padding, c.f., RFC 5096, section 3 - **kwargs: 'Any') -> 'Schema_ExperimentalMessage': - """Make MH experimental mobility header message type. + def _make_opt_rc(self, type: 'Enum_Option', option: 'Optional[Data_RedirectCapabilityOption]' = None, + **kwargs: 'Any') -> 'Schema_RedirectCapabilityOption': + """Make MH redirect-capability mobility option. Args: - message: Message data model. - data: Experimental message data. + type: Option type. + option: Option data model. **kwargs: Arbitrary keyword arguments. Returns: - Constructed message type. + Constructed option schema. """ - if message is not None: - data = message.data - - return Schema_ExperimentalMessage( - data=data, + return Schema_RedirectCapabilityOption( + type=type, + length=2, ) - def _make_msg_hi(self, message: 'Optional[Data_HandoverInitiateMessage]' = None, *, - seq: 'int' = 0, - assign: 'bool' = False, - buffer: 'bool' = False, - proxy: 'bool' = False, - forward: 'bool' = False, - code: 'Enum_HandoverInitiateStatus | StdlibEnum | AenumEnum | str | int' = Enum_HandoverInitiateStatus.FBU_with_the_PCoA_as_source_IP_address, - code_default: 'Optional[int]' = None, - code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - code_reversed: 'bool' = False, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, - **kwargs: 'Any') -> 'Schema_HandoverInitiateMessage': - """Make MH handover initiate (HI) message type. + def _make_opt_redirect(self, type: 'Enum_Option', option: 'Optional[Data_RedirectOption]' = None, *, + ipv6: 'Optional[bytes | str | int | IPv6Address]' = None, + ipv4: 'Optional[bytes | str | int | IPv4Address]' = None, + **kwargs: 'Any') -> 'Schema_RedirectOption': + """Make MH redirect mobility option. Args: - message: Message data model. - seq: Sequence number. - assign: Assigned address configuration flag. - buffer: Buffer flag. - proxy: Proxy flag. - forward: Forwarding flag. - code: Code. - code_default: Default code. - code_namespace: Code namespace. - code_reversed: Reverse code namespace. - options: Mobility options. + type: Option type. + option: Option data model. + ipv6: IPv6 address of the redirected-to local mobility anchor. + ipv4: IPv4 address of the redirected-to local mobility anchor. **kwargs: Arbitrary keyword arguments. Returns: - Constructed message type. + Constructed option schema. + + Raises: + ProtocolError: If neither or both addresses are given. + :rfc:`6463#section-4.2` requires exactly one. """ - if message is not None: - seq = message.seq - assign = message.assign - buffer = message.buffer - proxy = message.proxy - forward = message.forward - code_val = message.code - options = message.options - else: - code_val = self._make_index(code, code_default, namespace=code_namespace, # type: ignore[assignment] - reversed=code_reversed, pack=False) - options = options or [] + if option is not None: + ipv6 = option.ipv6 + ipv4 = option.ipv4 - return Schema_HandoverInitiateMessage( - seq=seq, + if (ipv6 is None) == (ipv4 is None): + raise ProtocolError(f'{self.alias}: [OptNo {type}] exactly one of the IPv6 and ' + 'IPv4 addresses must be given') + + return Schema_RedirectOption( + type=type, + length=18 if ipv6 is not None else 6, flags={ - 'S': assign, - 'U': buffer, - 'P': proxy, - 'F': forward, + 'K': int(ipv6 is not None), + 'N': int(ipv4 is not None), }, - code=code_val, - options=self._make_mh_options(options), + ipv6=ipv6, + ipv4=ipv4, ) - def _make_msg_hack(self, message: 'Optional[Data_HandoverAcknowledgeMessage]' = None, *, - seq: 'int' = 0, - buffer: 'bool' = False, - proxy: 'bool' = False, - forward: 'bool' = False, - code: 'Enum_HandoverACKStatus | StdlibEnum | AenumEnum | str | int' = Enum_HandoverACKStatus.Handover_Accepted_with_NCoA_valid, - code_default: 'Optional[int]' = None, - code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - code_reversed: 'bool' = False, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, - **kwargs: 'Any') -> 'Schema_HandoverAcknowledgeMessage': - """Make MH handover acknowledge (HAck) message type. + def _make_opt_load(self, type: 'Enum_Option', option: 'Optional[Data_LoadInformationOption]' = None, *, + priority: 'int' = 0, + sessions_in_use: 'int' = 0, + max_sessions: 'int' = 0, + used_capacity: 'int' = 0, + max_capacity: 'int' = 0, + **kwargs: 'Any') -> 'Schema_LoadInformationOption': + """Make MH load information mobility option. Args: - message: Message data model. - seq: Sequence number. - buffer: Buffer flag. - proxy: Proxy flag. - forward: Forwarding flag. - code: Code. - code_default: Default code. - code_namespace: Code namespace. - code_reversed: Reverse code namespace. - options: Mobility options. + type: Option type. + option: Option data model. + priority: Priority; a lower value is a higher priority. + sessions_in_use: Mobility sessions currently in use. + max_sessions: Maximum number of mobility sessions accepted. + used_capacity: Used capacity, in kilobytes per second. + max_capacity: Maximum capacity, in kilobytes per second. **kwargs: Arbitrary keyword arguments. Returns: - Constructed message type. + Constructed option schema. """ - if message is not None: - seq = message.seq - buffer = message.buffer - proxy = message.proxy - forward = message.forward - code_val = message.code - options = message.options - else: - code_val = self._make_index(code, code_default, namespace=code_namespace, # type: ignore[assignment] - reversed=code_reversed, pack=False) - options = options or [] + if option is not None: + priority = option.priority + sessions_in_use = option.sessions_in_use + max_sessions = option.max_sessions + used_capacity = option.used_capacity + max_capacity = option.max_capacity - return Schema_HandoverAcknowledgeMessage( - seq=seq, - flags={ - 'U': buffer, - 'P': proxy, - 'F': forward, - }, - code=code_val, - options=self._make_mh_options(options), + return Schema_LoadInformationOption( + type=type, + length=18, + priority=priority, + sessions_in_use=sessions_in_use, + max_sessions=max_sessions, + used_capacity=used_capacity, + max_capacity=max_capacity, ) - # TODO: Implement other message types. - - def _make_pad_options(self, offset: 'int') -> 'tuple[list[Schema_PadOption], int]': - """Make the padding options needed to align the header to 8 octets. + def _make_opt_alt_ipv4_coa(self, type: 'Enum_Option', + option: 'Optional[Data_AlternateIPv4CareofAddressOption]' = None, *, + address: 'bytes | str | int | IPv4Address' = '0.0.0.0', + **kwargs: 'Any') -> 'Schema_AlternateIPv4CareofAddressOption': + """Make MH alternate IPv4 care-of address option. Args: - offset: Number of octets emitted so far, **counting the six octets - of the fixed header** (``Payload Proto``, ``Header Len``, - ``MH Type``, ``Reserved`` and ``Checksum``) as well as the whole - message data. + type: Option type. + option: Option data model. + address: Alternate IPv4 care-of address. + **kwargs: Arbitrary keyword arguments. Returns: - Tuple of the padding option schemas and the number of octets they - occupy. - - Note: - The Mobility Header is a multiple of 8 octets and - :attr:`~pcapkit.protocols.schema.internet.mh.MH.length` counts those - units less one [:rfc:`6275#section-6.1.1`]. Its fixed part is - **six** octets, not the two of an IPv6 options header, so it is - ``len(data) + 6`` that has to be aligned -- which means the message - data is well formed only when it is 2 octets past a multiple of 8, - a different modulus from the one the extension headers use. - - A ``PadN`` option spends two octets on its own type and - ``Option Length`` fields before any padding data, so occupying - ``pad_len`` octets means an ``Option Length`` of ``pad_len - 2``. - One or two octets are padded with ``Pad1`` options instead, that - being the only form which can occupy a single octet - [:rfc:`6275#section-6.2.5`]; two octets need two separate ``Pad1`` - schemas, since a schema instance is mutable and must not be shared - between two entries of the option list. + Constructed option schema. """ - if offset % 8 == 0: - return [], 0 + if option is not None: + address = option.address - pad_len = 8 - (offset % 8) - if pad_len <= 2: - return [self._make_opt_pad(Enum_Option.Pad1, length=0) # type: ignore[arg-type] - for _ in range(pad_len)], pad_len - return [self._make_opt_pad(Enum_Option.PadN, length=pad_len - 2)], pad_len # type: ignore[arg-type] + return Schema_AlternateIPv4CareofAddressOption( + type=type, + length=4, + address=address, + ) - def _pad_mh_message(self, data: 'Schema_Packet | bytes') -> 'Schema_Packet | bytes': - """Pad an MH message so that the Mobility Header aligns to 8 octets. + def _make_opt_mn_group(self, type: 'Enum_Option', option: 'Optional[Data_MNGroupIdentifierOption]' = None, *, + subtype: 'Enum_MNGroupID | StdlibEnum | AenumEnum | str | int' = Enum_MNGroupID.Bulk_Binding_Update_Group, + subtype_default: 'Optional[int]' = None, + subtype_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + subtype_reversed: 'bool' = False, + group_id: 'int' = 1, + **kwargs: 'Any') -> 'Schema_MNGroupIdentifierOption': + """Make MH mobile node group identifier option. Args: - data: Constructed message data. + type: Option type. + option: Option data model. + subtype: Sub-type. + subtype_default: Default sub-type. + subtype_namespace: Sub-type namespace. + subtype_reversed: Reverse sub-type namespace. + group_id: Mobile node group identifier. + **kwargs: Arbitrary keyword arguments. Returns: - The message data, with padding options appended if any were needed. + Constructed option schema. - Note: - The padding goes into the message's mobility options, which is where - :rfc:`6275#section-6.2.5` puts it. A message whose body is opaque has - nowhere to put it -- - :class:`~pcapkit.protocols.schema.internet.mh.UnknownMessage` and - :class:`~pcapkit.protocols.schema.internet.mh.ExperimentalMessage` - carry raw bytes and no options, as does a ``data`` argument given - directly as :obj:`bytes` -- so for those the padding is appended to the - message body itself and a - :class:`~pcapkit.utilities.warnings.ProtocolWarning` says so. + """ + if option is not None: + subtype_val = option.subtype # type: Enum_MNGroupID + group_id = option.group_id + else: + subtype_val = self._make_index(subtype, subtype_default, namespace=subtype_namespace, # type: ignore[assignment] + reversed=subtype_reversed, pack=False) - Appending is necessary rather than optional: ``length`` is - ``(len(data) + 6) // 8 - 1``, which floors, so leaving an opaque body - short emitted 10, 12 or 14 octets while declaring 8, and a parser reads - 8 and misinterprets the remainder. Since the caller asked for a packet - to be built and the shortfall is recoverable, completing it beats - refusing -- the warning is there because the emitted body is then not - byte-for-byte what was handed in. + return Schema_MNGroupIdentifierOption( + type=type, + length=6, + subtype=subtype_val, + group_id=group_id, + ) - """ - pad_opts, pad_len = self._make_pad_options(len(data) + 6) - if pad_len == 0: - return data + def _make_opt_mag_addr(self, type: 'Enum_Option', option: 'Optional[Data_MAGIPv6AddressOption]' = None, *, + address_length: 'int' = 128, + address: 'bytes | str | int | IPv6Address' = '::', + **kwargs: 'Any') -> 'Schema_MAGIPv6AddressOption': + """Make MH MAG IPv6 address option. - options = getattr(data, 'options', None) - # NOTE: The ``isinstance`` test comes first so that the type checker can - # narrow ``data`` for the assignment below; at runtime ``getattr`` has - # already covered the :obj:`bytes` case by returning :obj:`None`. - if isinstance(data, bytes) or options is None: - warn(f'{self.alias}: message data of {len(data)} octets carries no ' - f'mobility options to hold padding, so {pad_len} octet(s) were ' - 'appended to the message body to align the header', - ProtocolWarning) - if isinstance(data, bytes): - return data + b'\x00' * pad_len + Args: + type: Option type. + option: Option data model. + address_length: Address length, in bits; :rfc:`6705#section-11.1` + requires 128. + address: MAG IPv6 address. + **kwargs: Arbitrary keyword arguments. - # An opaque schema body -- UnknownMessage, ExperimentalMessage -- keeps - # its content in ``data`` rather than in options, so that is where the - # octets go. Rebound rather than mutated in place so that ``len()`` and - # ``pack()`` see the change, exactly as for the options branch below. - body = getattr(data, 'data', None) - if not isinstance(body, bytes): - raise ProtocolError( - f'{self.alias}: message data of {len(data)} octets needs ' - f'{pad_len} octet(s) of padding, but the body is neither bytes ' - 'nor a schema carrying bytes, so there is nowhere to put it') - data.data = body + b'\x00' * pad_len - return data + Returns: + Constructed option schema. + + """ + if option is not None: + address_length = option.address_length + address = option.address - # NOTE: Rebinding the attribute rather than mutating the list in place is - # what marks the schema as updated, so that ``len()`` and ``pack()`` take - # the padding into account. - data.options = list(options) + pad_opts - return data + return Schema_MAGIPv6AddressOption( + type=type, + length=18, + address_length=address_length, + address=address, + ) - def _make_mh_options(self, options: 'Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]') -> 'list[Schema_Option | bytes]': - """Make options for MH. + def _make_ani_suboptions( + self, + suboptions: 'ANISuboption | list[Schema_ANISuboption | tuple[Enum_ANISuboption, dict[str, Any]] | bytes]', + ) -> 'list[Schema_ANISuboption | bytes]': + """Make MH access network identifier sub-options. Args: - options: MH options. + suboptions: Sub-options, as parsed data, schemas, ``(type, kwargs)`` + pairs, or raw octets. Returns: - Mobility options list. + Sub-option schema list. """ - if isinstance(options, list): - options_list = [] # type: list[Schema_Option | bytes] - for schema in options: - if isinstance(schema, bytes): - code = Enum_Option.get(int.from_bytes(schema[0:1], 'big', signed=False)) - - data = schema # type: Schema_Option | bytes - elif isinstance(schema, Schema): - data = schema + if isinstance(suboptions, list): + entries = [] # type: list[Schema_ANISuboption | bytes] + for item in suboptions: + if isinstance(item, (bytes, Schema)): + entries.append(cast('Schema_ANISuboption | bytes', item)) else: - code, args = cast('tuple[Enum_Option, dict[str, Any]]', schema) - name = self._lookup_registry(self.__option__, code) - if isinstance(name, str): - meth_name = f'_make_opt_{name}' - meth = cast('OptionConstructor', - getattr(self, meth_name, self._make_opt_none)) - else: - meth = name[1] - data = meth(code, **args) + code, args = cast('tuple[Enum_ANISuboption, dict[str, Any]]', item) + entries.append(self._make_ani_suboption(code, **args)) + return entries - options_list.append(data) - return options_list + return [self._make_ani_suboption(code, data=data) + for code, data in suboptions.items(multi=True)] - options_list = [] - for code, option in options.items(multi=True): - name = self._lookup_registry(self.__option__, code) - if isinstance(name, str): - meth_name = f'_make_opt_{name}' - meth = cast('OptionConstructor', - getattr(self, meth_name, self._make_opt_none)) + def _make_ani_suboption(self, code: 'Enum_ANISuboption', + data: 'Optional[Data_ANISuboption]' = None, + **kwargs: 'Any') -> 'Schema_ANISuboption': + """Make one MH access network identifier sub-option. + + Args: + code: Sub-option type. + data: Sub-option data model. + **kwargs: Sub-option fields, when no data model is given. + + Returns: + Constructed sub-option schema. + + Note: + The geo-location degrees are re-encoded from the **raw** signed + integers rather than from the decoded floats, since a float cannot + always be converted back to the same 24-bit fixed-point value. + + """ + if code == Enum_ANISuboption.Network_Identifier: + if isinstance(data, Data_ANINetworkIdentifierSuboption): + utf8, net_name, ap_name = data.utf8, data.net_name, data.ap_name else: - meth = name[1] + utf8 = cast('bool', kwargs.get('utf8', False)) + net_name = cast('bytes', kwargs.get('net_name', b'')) + ap_name = cast('bytes', kwargs.get('ap_name', b'')) + return Schema_ANINetworkIdentifierSuboption( + type=code, length=3 + len(net_name) + len(ap_name), + flags={'E': int(utf8)}, net_name_len=len(net_name), net_name=net_name, + ap_name_len=len(ap_name), ap_name=ap_name) + + if code == Enum_ANISuboption.Geo_Location: + if isinstance(data, Data_ANIGeoLocationSuboption): + raw_lat, raw_lon = data.raw_latitude, data.raw_longitude + else: + raw_lat = cast('int', kwargs.get('raw_latitude', 0)) + raw_lon = cast('int', kwargs.get('raw_longitude', 0)) + return Schema_ANIGeoLocationSuboption( + type=code, length=6, location={ + 'latitude': raw_lat & 0xFFFFFF, + 'longitude': raw_lon & 0xFFFFFF, + }) + + if code == Enum_ANISuboption.Operator_Identifier: + if isinstance(data, Data_ANIOperatorIdentifierSuboption): + op_id_type = data.op_id_type # type: Enum_OperatorID | int + identifier = data.identifier + else: + op_id_type = cast('Enum_OperatorID | int', + kwargs.get('op_id_type', Enum_OperatorID.Realm_of_the_Operator)) + identifier = cast('bytes', kwargs.get('identifier', b'')) + return Schema_ANIOperatorIdentifierSuboption( + type=code, length=1 + len(identifier), + op_id_type=cast('Enum_OperatorID', op_id_type), identifier=identifier) + + if code == Enum_ANISuboption.Civic_Location: + if isinstance(data, Data_ANICivicLocationSuboption): + fmt, location = data.format, data.location + else: + fmt = cast('int', kwargs.get('format', 0)) + location = cast('bytes', kwargs.get('location', b'')) + return Schema_ANICivicLocationSuboption( + type=code, length=2 + len(location), format=fmt, location=location) + + if code == Enum_ANISuboption.MAG_Group_Identifier: + if isinstance(data, Data_ANIMAGGroupIdentifierSuboption): + group_id = data.group_id + else: + group_id = cast('int', kwargs.get('group_id', 0)) + return Schema_ANIMAGGroupIdentifierSuboption(type=code, length=2, group_id=group_id) - data = meth(code, option) - options_list.append(data) - return options_list + if code == Enum_ANISuboption.ANI_Update_Timer: + if isinstance(data, Data_ANIUpdateTimerSuboption): + timer = math.ceil(data.timer.total_seconds() / 4) + else: + raw_timer = kwargs.get('timer', 0) + timer = raw_timer if isinstance(raw_timer, int) else math.ceil( + raw_timer.total_seconds() / 4) + return Schema_ANIUpdateTimerSuboption(type=code, length=2, timer=timer) - def _make_opt_none(self, type: 'Enum_Option', option: 'Optional[Data_UnassignedOption]' = None, *, - data: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_UnassignedOption': - """Make MH unassigned option. + if isinstance(data, Data_UnassignedANISuboption): + payload = data.data + else: + payload = cast('bytes', kwargs.get('data', b'')) + return Schema_UnassignedANISuboption(type=code, length=len(payload), data=payload) + + def _make_opt_ani(self, type: 'Enum_Option', option: 'Optional[Data_AccessNetworkIdentifierOption]' = None, *, + suboptions: 'Optional[ANISuboption | list[Schema_ANISuboption | tuple[Enum_ANISuboption, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_AccessNetworkIdentifierOption': + """Make MH access network identifier option. Args: type: Option type. option: Option data model. - data: Option data. + suboptions: Sub-options; at least one is required. **kwargs: Arbitrary keyword arguments. Returns: @@ -3153,65 +9272,74 @@ def _make_opt_none(self, type: 'Enum_Option', option: 'Optional[Data_UnassignedO """ if option is not None: - data = option.data + suboptions = option.suboptions + else: + suboptions = suboptions or [] - return Schema_UnassignedOption( + entries = self._make_ani_suboptions(suboptions) + if not entries: + raise ProtocolError(f'{self.alias}: [OptNo {type}] at least one sub-option is required') + + length = sum(len(entry) if isinstance(entry, bytes) else len(entry.pack()) + for entry in entries) + + return Schema_AccessNetworkIdentifierOption( type=type, - length=len(data), - data=data, + length=length, + suboptions=entries, ) - def _make_opt_pad(self, type: 'Enum_Option', option: 'Optional[Data_PadOption]' = None, *, - length: 'int' = 0, - **kwargs: 'Any') -> 'Schema_PadOption': - """Make MH pad option. + def _make_opt_offload(self, type: 'Enum_Option', + option: 'Optional[Data_IPv4TrafficOffloadSelectorOption]' = None, *, + mode: 'bool' = False, + selector: 'Optional[FlowIDSuboption | list[Schema_FlowIdentificationSuboption | tuple[Enum_FlowIDSuboption, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_IPv4TrafficOffloadSelectorOption': + """Make MH IPv4 traffic offload selector option. Args: type: Option type. option: Option data model. - length: Value of the ``Option Length`` field, i.e. the number of - padding octets *after* the two octets of the option header. + mode: Offload mode flag. + selector: Traffic selector sub-options. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. - Note: - :attr:`Data_PadOption.length - ` counts the - *whole* option, whereas :attr:`Schema_PadOption.length - ` is the - ``Option Length`` field -- two octets fewer, and absent altogether - for a ``Pad1``. Copying one into the other unconverted is why - re-making a parsed ``PadN`` used to come back two octets too long. - """ if option is not None: - length = 0 if option.type == Enum_Option.Pad1 else option.length - 2 + mode = option.mode + selector = option.selector + else: + selector = selector or [] - if type == Enum_Option.Pad1 and length != 0: - # raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid format') - warn(f'{self.alias}: [OptNo {type}] invalid format', ProtocolWarning) - type = Enum_Option.PadN # type: ignore[assignment] - if type == Enum_Option.PadN and length == 0: - # raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid format') - warn(f'{self.alias}: [OptNo {type}] invalid format', ProtocolWarning) - type = Enum_Option.Pad1 # type: ignore[assignment] + entries = self._make_fid_suboptions(selector) + length = 4 + sum(len(entry) if isinstance(entry, bytes) else len(entry.pack()) + for entry in entries) - return Schema_PadOption( + return Schema_IPv4TrafficOffloadSelectorOption( type=type, length=length, + flags={'M': int(mode)}, + selector=entries, ) - def _make_opt_bra(self, type: 'Enum_Option', option: 'Optional[Data_BindingRefreshAdviceOption]' = None, *, - interval: 'int' = 0, - **kwargs: 'Any') -> 'Schema_BindingRefreshAdviceOption': - """Make MH binding refresh advice option. + def _make_opt_mcast_sel(self, type: 'Enum_Option', + option: 'Optional[Data_DynamicIPMulticastSelectorOption]' = None, *, + protocol: 'int' = 143, + mode: 'bool' = False, + records: 'int' = 0, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_DynamicIPMulticastSelectorOption': + """Make MH dynamic IP multicast selector option. Args: type: Option type. option: Option data model. - interval: Refresh interval. + protocol: MLD or IGMP protocol number identifying the record format. + mode: Selector mode flag. + records: Number of multicast address records in ``data``. + data: Multicast address records, opaque to this module. **kwargs: Arbitrary keyword arguments. Returns: @@ -3219,49 +9347,74 @@ def _make_opt_bra(self, type: 'Enum_Option', option: 'Optional[Data_BindingRefre """ if option is not None: - interval = option.interval + protocol = option.protocol + mode = option.mode + records = option.records + data = option.data - return Schema_BindingRefreshAdviceOption( + return Schema_DynamicIPMulticastSelectorOption( type=type, - length=2, - interval=interval, + length=4 + len(data), + protocol=protocol, + flags={'M': int(mode)}, + records=records, + data=data, ) - def _make_opt_aca(self, type: 'Enum_Option', option: 'Optional[Data_AlternateCareofAddressOption]' = None, *, - address: 'bytes | str | int | IPv6Address' = '::', - **kwargs: 'Any') -> 'Schema_AlternateCareofAddressOption': - """Make MH alternate care-of address option. + def _make_opt_dmnp(self, type: 'Enum_Option', option: 'Optional[Data_DelegatedMNPOption]' = None, *, + prefix_length: 'int' = 64, + prefix: 'bytes | str | int | IPv4Address | IPv6Address' = '::', + **kwargs: 'Any') -> 'Schema_DelegatedMNPOption': + """Make MH delegated mobile network prefix option. Args: type: Option type. option: Option data model. - address: Alternate care-of address. + prefix_length: Prefix length. + prefix: Delegated mobile network prefix. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. + Note: + The ``V`` flag is derived from the prefix's own family rather than + taken as an argument, so that the flag and the emitted width cannot + disagree. + """ if option is not None: - address = option.address + prefix_length = option.prefix_length + prefix = option.prefix - return Schema_AlternateCareofAddressOption( + addr = prefix if isinstance( + prefix, (ipaddress.IPv4Address, ipaddress.IPv6Address) + ) else ipaddress.ip_address(prefix) + ipv4 = addr.version == 4 + + if prefix_length > (32 if ipv4 else 128): + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid prefix length: {prefix_length}') + + return Schema_DelegatedMNPOption( type=type, - length=16, - address=address, + length=6 if ipv4 else 18, + flags={'V': int(ipv4)}, + prefix_length=prefix_length, + prefix=addr, ) - def _make_opt_ni(self, type: 'Enum_Option', option: 'Optional[Data_NonceIndicesOption]' = None, *, - home: 'int' = 0, - careof: 'int' = 0, - **kwargs: 'Any') -> 'Schema_NonceIndicesOption': - """Make MH nonce indices option. + def _make_opt_ams4(self, type: 'Enum_Option', + option: 'Optional[Data_ActiveMulticastSubscriptionIPv4Option]' = None, *, + igmp_type: 'int' = 0x22, + context: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_ActiveMulticastSubscriptionIPv4Option': + """Make MH active multicast subscription IPv4 option. Args: type: Option type. option: Option data model. - home: Home nonce index. - careof: Care-of nonce index. + igmp_type: IGMP message type identifying the context format. + context: Multicast membership context, opaque to this module. **kwargs: Arbitrary keyword arguments. Returns: @@ -3269,25 +9422,28 @@ def _make_opt_ni(self, type: 'Enum_Option', option: 'Optional[Data_NonceIndicesO """ if option is not None: - home = option.home - careof = option.careof + igmp_type = option.igmp_type + context = option.context - return Schema_NonceIndicesOption( + return Schema_ActiveMulticastSubscriptionIPv4Option( type=type, - length=4, - home=home, - careof=careof, + length=1 + len(context), + igmp_type=igmp_type, + context=context, ) - def _make_opt_bad(self, type: 'Enum_Option', option: 'Optional[Data_AuthorizationDataOption]' = None, *, - data: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_AuthorizationDataOption': - """Make MH binding authorization data option. + def _make_opt_ams6(self, type: 'Enum_Option', + option: 'Optional[Data_ActiveMulticastSubscriptionIPv6Option]' = None, *, + mld_type: 'int' = 143, + context: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_ActiveMulticastSubscriptionIPv6Option': + """Make MH active multicast subscription IPv6 option. Args: type: Option type. option: Option data model. - data: Authenticator. + mld_type: MLD message type identifying the context format. + context: Multicast membership context, opaque to this module. **kwargs: Arbitrary keyword arguments. Returns: @@ -3295,26 +9451,139 @@ def _make_opt_bad(self, type: 'Enum_Option', option: 'Optional[Data_Authorizatio """ if option is not None: - data = option.data - - if len(data) % 8 != 0: - raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid format') + mld_type = option.mld_type + context = option.context - return Schema_AuthorizationDataOption( + return Schema_ActiveMulticastSubscriptionIPv6Option( type=type, - length=len(data), - data=data, + length=1 + len(context), + mld_type=mld_type, + context=context, ) - def _make_opt_mnp(self, type: 'Enum_Option', option: 'Optional[Data_MobileNetworkPrefixOption]' = None, *, - prefix: 'bytes | str | IPv6Network' = '::/0', - **kwargs: 'Any') -> 'Schema_MobileNetworkPrefixOption': - """Make MH mobile network prefix option. + def _make_qos_attributes( + self, + attributes: 'QoSAttribute | list[Schema_QoSAttribute | tuple[Enum_QoSAttribute, dict[str, Any]] | bytes]', + ) -> 'list[Schema_QoSAttribute | bytes]': + """Make MH quality-of-service attributes. + + Args: + attributes: Attributes, as parsed data, schemas, ``(type, kwargs)`` + pairs, or raw octets. + + Returns: + Attribute schema list. + + """ + if isinstance(attributes, list): + entries = [] # type: list[Schema_QoSAttribute | bytes] + for item in attributes: + if isinstance(item, (bytes, Schema)): + entries.append(cast('Schema_QoSAttribute | bytes', item)) + else: + code, args = cast('tuple[Enum_QoSAttribute, dict[str, Any]]', item) + entries.append(self._make_qos_attribute(code, **args)) + return entries + + return [self._make_qos_attribute(code, data=data) + for code, data in attributes.items(multi=True)] + + def _make_qos_attribute(self, code: 'Enum_QoSAttribute', + data: 'Optional[Data_QoSAttribute]' = None, + **kwargs: 'Any') -> 'Schema_QoSAttribute': + """Make one MH quality-of-service attribute. + + Args: + code: Attribute type. + data: Attribute data model. + **kwargs: Attribute fields, when no data model is given. + + Returns: + Constructed attribute schema. + + """ + if code in (Enum_QoSAttribute.Per_Session_Agg_Max_DL_Bit_Rate, + Enum_QoSAttribute.Per_Session_Agg_Max_UL_Bit_Rate): + if isinstance(data, Data_PerSessionBitRateAttribute): + service, exclude, rate = data.service, data.exclude, data.rate + else: + service = cast('bool', kwargs.get('service', False)) + exclude = cast('bool', kwargs.get('exclude', False)) + rate = cast('int', kwargs.get('rate', 0)) + return Schema_PerSessionBitRateAttribute( + type=code, length=6, + flags={'S': int(service), 'E': int(exclude)}, rate=rate) + + if code in (Enum_QoSAttribute.Per_MN_Agg_Max_DL_Bit_Rate, + Enum_QoSAttribute.Per_MN_Agg_Max_UL_Bit_Rate, + Enum_QoSAttribute.Aggregate_Max_DL_Bit_Rate, + Enum_QoSAttribute.Aggregate_Max_UL_Bit_Rate, + Enum_QoSAttribute.Guaranteed_DL_Bit_Rate, + Enum_QoSAttribute.Guaranteed_UL_Bit_Rate): + if isinstance(data, Data_BitRateAttribute): + rate = data.rate + else: + rate = cast('int', kwargs.get('rate', 0)) + return Schema_BitRateAttribute(type=code, length=6, rate=rate) + + if code == Enum_QoSAttribute.Allocation_Retention_Priority: + if isinstance(data, Data_AllocationRetentionPriorityAttribute): + level = data.priority_level + capability = data.preemption_capability + vulnerability = data.preemption_vulnerability + else: + level = cast('int', kwargs.get('priority_level', 1)) + capability = cast('int', kwargs.get('preemption_capability', 0)) + vulnerability = cast('int', kwargs.get('preemption_vulnerability', 0)) + return Schema_AllocationRetentionPriorityAttribute( + type=code, length=2, + priority={'PL': level, 'PC': capability, 'PV': vulnerability}) + + if code == Enum_QoSAttribute.QoS_Traffic_Selector: + if isinstance(data, Data_QoSTrafficSelectorAttribute): + ts_format = data.ts_format # type: Enum_TrafficSelector | int + selector = data.selector + else: + ts_format = cast('Enum_TrafficSelector | int', + kwargs.get('ts_format', + Enum_TrafficSelector.IPv6_Binary_Traffic_Selector)) + selector = cast('bytes', kwargs.get('selector', b'')) + return Schema_QoSTrafficSelectorAttribute( + type=code, length=2 + len(selector), + ts_format=cast('Enum_TrafficSelector', ts_format), selector=selector) + + if code == Enum_QoSAttribute.QoS_Vendor_Specific_Attribute: + if isinstance(data, Data_QoSVendorSpecificAttribute): + vendor, subtype, payload = data.vendor, data.subtype, data.data + else: + vendor = cast('int', kwargs.get('vendor', 0)) + subtype = cast('int', kwargs.get('subtype', 0)) + payload = cast('bytes', kwargs.get('data', b'')) + return Schema_QoSVendorSpecificAttribute( + type=code, length=7 + len(payload), vendor=vendor, subtype=subtype, + data=payload) + + if isinstance(data, Data_UnassignedQoSAttribute): + payload = data.data + else: + payload = cast('bytes', kwargs.get('data', b'')) + return Schema_UnassignedQoSAttribute(type=code, length=len(payload), data=payload) + + def _make_opt_qos(self, type: 'Enum_Option', option: 'Optional[Data_QualityOfServiceOption]' = None, *, + sr_id: 'int' = 0, + dscp: 'int' = 0, + oc: 'int' = 0, + attributes: 'Optional[QoSAttribute | list[Schema_QoSAttribute | tuple[Enum_QoSAttribute, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_QualityOfServiceOption': + """Make MH quality-of-service option. Args: type: Option type. option: Option data model. - prefix: Mobile network prefix. + sr_id: Service request identifier. + dscp: Differentiated services code point, 6 bits. + oc: Operational code. + attributes: Quality-of-service attributes. **kwargs: Arbitrary keyword arguments. Returns: @@ -3322,30 +9591,40 @@ def _make_opt_mnp(self, type: 'Enum_Option', option: 'Optional[Data_MobileNetwor """ if option is not None: - prefix = option.prefix + sr_id = option.sr_id + dscp = option.dscp + oc = option.oc + attributes = option.attributes + else: + attributes = attributes or [] - prefix_val = ipaddress.ip_network(prefix) - if prefix_val.version != 6: - raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid movile network prefix: {prefix!r}') - prefix_length = prefix_val.prefixlen - prefix_addr = prefix_val.network_address + if dscp > 0x3F: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid DSCP: {dscp}') - return Schema_MobileNetworkPrefixOption( + entries = self._make_qos_attributes(attributes) + length = 6 + sum(len(entry) if isinstance(entry, bytes) else len(entry.pack()) + for entry in entries) + + return Schema_QualityOfServiceOption( type=type, - length=18, - prefix_length=prefix_length, - prefix=prefix_addr, + length=length, + sr_id=sr_id, + tc=dscp << 2, + oc=oc, + attributes=entries, ) - def _make_opt_lla(self, type: 'Enum_Option', option: 'Optional[Data_LinkLayerAddressOption]' = None, *, - address: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_LinkLayerAddressOption': - """Make MH link-layer address option. + def _make_opt_lma_up(self, type: 'Enum_Option', option: 'Optional[Data_LMAUserPlaneAddressOption]' = None, *, + address: 'Optional[bytes | str | int | IPv4Address | IPv6Address]' = None, + **kwargs: 'Any') -> 'Schema_LMAUserPlaneAddressOption': + """Make MH LMA user-plane address option. Args: type: Option type. option: Option data model. - address: Link-layer address. + address: LMA user-plane address, or :obj:`None` to omit it -- which is + how a mobile access gateway asks for a transport without naming an + address. **kwargs: Arbitrary keyword arguments. Returns: @@ -3353,222 +9632,300 @@ def _make_opt_lla(self, type: 'Enum_Option', option: 'Optional[Data_LinkLayerAdd """ if option is not None: - address = option.lla + address = option.address - return Schema_LinkLayerAddressOption( + if address is None: + return Schema_LMAUserPlaneAddressOption(type=type, length=2, address=b'') + + addr = address if isinstance( + address, (ipaddress.IPv4Address, ipaddress.IPv6Address) + ) else ipaddress.ip_address(address) + + return Schema_LMAUserPlaneAddressOption( type=type, - length=len(address) + 1, - code=Enum_LLACode.MH, # type: ignore[arg-type] - lla=address, + length=6 if addr.version == 4 else 18, + address=addr, ) - def _make_opt_mn_id(self, type: 'Enum_Option', option: 'Optional[Data_MNIDOption]' = None, *, - subtype: 'Enum_MNIDSubtype | StdlibEnum | AenumEnum | str | int' = Enum_MNIDSubtype.IPv6_Address, - subtype_default: 'Optional[int]' = None, - subtype_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - subtype_reversed: 'bool' = False, - identifier: 'bytes | str | IPv6Address | int' = '::', - **kwargs: 'Any') -> 'Schema_MNIDOption': - """Make MH mobile node identifier option. + def _make_opt_mcast(self, type: 'Enum_Option', option: 'Optional[Data_MulticastMobilityOption]' = None, *, + code: 'int' = 2, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_MulticastMobilityOption': + """Make MH multicast mobility option. Args: type: Option type. option: Option data model. - subtype: MN-ID subtype. - subtype_default: MN-ID subtype default value. - subtype_namespace: MN-ID subtype namespace. - subtype_reversed: MN-ID subtype reversed flag. - identifier: Identifier. + code: Option code. + data: MLD or IGMP report payload, opaque to this module. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. + Raises: + ProtocolError: If the payload is not a whole number of 32-bit words. + :rfc:`7411#section-5.3` measures this option's length in words, so + a payload that is not a multiple of 4 octets cannot be described by + it at all. + """ if option is not None: - subtype_val = option.subtype - identifier = option.identifier - else: - subtype_val = self._make_index(subtype, subtype_default, namespace=subtype_namespace, # type: ignore[assignment] - reversed=subtype_reversed, pack=False) + code = option.code + data = option.data - if isinstance(identifier, ipaddress.IPv6Address): - id_len = 16 - elif isinstance(identifier, int): - id_len = math.ceil(identifier.bit_length() / 8) - else: - id_len = len(identifier) + if len(data) % 4 != 0: + raise ProtocolError(f'{self.alias}: [OptNo {type}] payload of {len(data)} octets is ' + 'not a whole number of 32-bit words') - return Schema_MNIDOption( + return Schema_MulticastMobilityOption( type=type, - length=1 + id_len, - subtype=subtype_val, - identifier=identifier, + length=len(data) // 4, + code=code, + data=data, ) - def _make_opt_auth(self, type: 'Enum_Option', option: 'Optional[Data_AuthOption]' = None, *, - subtype: 'Enum_AuthSubtype | StdlibEnum | AenumEnum | str | int' = Enum_AuthSubtype.MN_HA, - subtype_default: 'Optional[int]' = None, - subtype_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - subtype_reversed: 'bool' = False, - spi: 'int' = 0, - data: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_AuthOption': - """Make MH authentication option. + def _make_opt_mcast_ack(self, type: 'Enum_Option', + option: 'Optional[Data_MulticastAcknowledgementOption]' = None, *, + code: 'int' = 0, + status: 'int' = 1, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_MulticastAcknowledgementOption': + """Make MH multicast acknowledgement option. Args: type: Option type. option: Option data model. - subtype: Authentication subtype. - subtype_default: Authentication subtype default value. - subtype_namespace: Authentication subtype namespace. - subtype_reversed: Authentication subtype reversed flag. - spi: Security parameter index. - data: Authentication data. + code: Option code; always ``0``. + status: Status. + data: MLD or IGMP unsupported report payload, opaque to this module. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. + Raises: + ProtocolError: If the payload is not a whole number of 32-bit words. + """ if option is not None: - subtype_val = option.subtype - spi = option.spi + code = option.code + status = option.status data = option.data - else: - subtype_val = self._make_index(subtype, subtype_default, namespace=subtype_namespace, # type: ignore[assignment] - reversed=subtype_reversed, pack=False) - if (len(data) + 6) % 4 != 0: - raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid format') + if len(data) % 4 != 0: + raise ProtocolError(f'{self.alias}: [OptNo {type}] payload of {len(data)} octets is ' + 'not a whole number of 32-bit words') - return Schema_AuthOption( + return Schema_MulticastAcknowledgementOption( type=type, - length=5 + len(data), - subtype=subtype_val, - spi=spi, + length=len(data) // 4, + code=code, + status=status, data=data, ) - def _make_opt_mesg_id(self, type: 'Enum_Option', option: 'Optional[Data_MesgIDOption]' = None, *, - timestamp: 'Optional[NTPTimestamp]' = None, - interval: 'Optional[dt_type]' = None, - **kwargs: 'Any') -> 'Schema_MesgIDOption': - """Make MH mobility message replay protection option. + def _make_lcmp_suboptions( + self, + suboptions: 'LMAControlledMAGSuboption | list[Schema_LMAControlledMAGSuboption | tuple[Enum_LMAControlledMAGSuboption, dict[str, Any]] | bytes]', + ) -> 'list[Schema_LMAControlledMAGSuboption | bytes]': + """Make MH LMA-controlled MAG parameters sub-options. Args: - type: Option type. - option: Option data model. - timestamp: NTP timestamp, c.f., :rfc:`1305`. - interval: Timestamp interval (since UNIX-epoch). - **kwargs: Arbitrary keyword arguments. + suboptions: Sub-options, as parsed data, schemas, ``(type, kwargs)`` + pairs, or raw octets. Returns: - Constructed option schema. + Sub-option schema list. """ - if option is not None: - timestamp = option.ntp_timestamp + if isinstance(suboptions, list): + entries = [] # type: list[Schema_LMAControlledMAGSuboption | bytes] + for item in suboptions: + if isinstance(item, (bytes, Schema)): + entries.append(cast('Schema_LMAControlledMAGSuboption | bytes', item)) + else: + code, args = cast( + 'tuple[Enum_LMAControlledMAGSuboption, dict[str, Any]]', item) + entries.append(self._make_lcmp_suboption(code, **args)) + return entries - if timestamp is None: - interval = interval or datetime.datetime.now(datetime.timezone.utc) + return [self._make_lcmp_suboption(code, data=data) + for code, data in suboptions.items(multi=True)] - int_ts = interval.timestamp() - ts_sec = math.floor(int_ts) - ts_frc = math.ceil(((int_ts - ts_sec) * 1_000_000)) * 2**32 + @staticmethod + def _seconds(value: 'int | timedelta', unit: 'int' = 1) -> 'int': + """Return an interval in wire units. - timestamp = NTPTimestamp(seconds=ts_sec + 2_208_988_800, # 70 years - fraction=ts_frc) + Args: + value: Interval, already in wire units when an :obj:`int`, or as a + :class:`~datetime.timedelta`. + unit: Number of seconds one wire unit stands for. - return Schema_MesgIDOption( - type=type, - length=8, - seconds=timestamp.seconds, - fraction=timestamp.fraction, - ) + Returns: + The interval in wire units. - def _make_opt_cga_pr(self, type: 'Enum_Option', option: 'Optional[Data_CGAParametersRequestOption]' = None, - **kwargs: 'Any') -> 'Schema_CGAParametersRequestOption': - """Make MH CGA parameters request option. + """ + if isinstance(value, int): + return value + return math.ceil(value.total_seconds() / unit) + + def _make_lcmp_suboption(self, code: 'Enum_LMAControlledMAGSuboption', + data: 'Optional[Data_LMAControlledMAGSuboption]' = None, + **kwargs: 'Any') -> 'Schema_LMAControlledMAGSuboption': + """Make one MH LMA-controlled MAG parameters sub-option. + + Args: + code: Sub-option type. + data: Sub-option data model. + **kwargs: Sub-option fields, when no data model is given. + + Returns: + Constructed sub-option schema. + + """ + if code == Enum_LMAControlledMAGSuboption.Binding_Re_registration_Control: + if isinstance(data, Data_BindingReregistrationControlSuboption): + start = math.ceil(data.start_time.total_seconds() / 4) + initial = math.ceil(data.initial_retransmission.total_seconds()) + maximum = math.ceil(data.max_retransmission.total_seconds()) + else: + start = self._seconds(kwargs.get('start_time', 0), 4) + initial = self._seconds(kwargs.get('initial_retransmission', 0)) + maximum = self._seconds(kwargs.get('max_retransmission', 0)) + return Schema_BindingReregistrationControlSuboption( + type=code, length=6, start_time=start, + initial_retransmission=initial, max_retransmission=maximum) + + if code == Enum_LMAControlledMAGSuboption.Heartbeat_Control: + if isinstance(data, Data_HeartbeatControlSuboption): + interval = math.ceil(data.interval.total_seconds()) + delay = math.ceil(data.retransmission_delay.total_seconds()) + count = data.max_retransmissions + else: + interval = self._seconds(kwargs.get('interval', 0)) + delay = self._seconds(kwargs.get('retransmission_delay', 0)) + count = cast('int', kwargs.get('max_retransmissions', 0)) + return Schema_HeartbeatControlSuboption( + type=code, length=6, interval=interval, + retransmission_delay=delay, max_retransmissions=count) + + if isinstance(data, Data_UnassignedLMAControlledMAGSuboption): + payload = data.data + else: + payload = cast('bytes', kwargs.get('data', b'')) + return Schema_UnassignedLMAControlledMAGSuboption( + type=code, length=len(payload), data=payload) + + def _make_opt_lcmp(self, type: 'Enum_Option', option: 'Optional[Data_LMAControlledMAGParametersOption]' = None, *, + suboptions: 'Optional[LMAControlledMAGSuboption | list[Schema_LMAControlledMAGSuboption | tuple[Enum_LMAControlledMAGSuboption, dict[str, Any]] | bytes]]' = None, + **kwargs: 'Any') -> 'Schema_LMAControlledMAGParametersOption': + """Make MH LMA-controlled MAG parameters option. Args: type: Option type. option: Option data model. + suboptions: Sub-options; at least one is required. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. """ - return Schema_CGAParametersRequestOption( + if option is not None: + suboptions = option.suboptions + else: + suboptions = suboptions or [] + + entries = self._make_lcmp_suboptions(suboptions) + if not entries: + raise ProtocolError(f'{self.alias}: [OptNo {type}] at least one sub-option is required') + + length = sum(len(entry) if isinstance(entry, bytes) else len(entry.pack()) + for entry in entries) + + return Schema_LMAControlledMAGParametersOption( type=type, - length=0, + length=length, + suboptions=entries, ) - def _make_opt_cga_param(self, type: 'Enum_Option', option: 'Optional[Data_CGAParametersOption]' = None, *, - parameters: 'Optional[list[Schema_CGAParameter | Data_CGAParameter | dict[str, Any] | bytes]]' = None, - **kwargs: 'Any') -> 'Schema_CGAParametersOption': - """Make MH CGA paramters option. + def _make_opt_mag_mp(self, type: 'Enum_Option', option: 'Optional[Data_MAGMultipathBindingOption]' = None, *, + att: 'Enum_AccessType | StdlibEnum | AenumEnum | str | int' = Enum_AccessType.Virtual, + att_default: 'Optional[int]' = None, + att_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + att_reversed: 'bool' = False, + label: 'int' = 0, + bid: 'int' = 1, + bulk: 'bool' = False, + overwrite: 'bool' = False, + **kwargs: 'Any') -> 'Schema_MAGMultipathBindingOption': + """Make MH MAG multipath binding option. Args: type: Option type. option: Option data model. - parameters: CGA parameters. + att: Interface access-technology type. + att_default: Default access-technology type. + att_namespace: Access-technology type namespace. + att_reversed: Reverse access-technology type namespace. + label: Interface label. + bid: Binding identifier; ``0`` and ``255`` are reserved. + bulk: Bulk re-registration flag. + overwrite: Registration overwrite flag. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. + Raises: + ProtocolError: If both flags are set, which + :rfc:`8278#section-4.1` forbids. + """ if option is not None: - parameters = cast('list[Data_CGAParameter]', option.parameters) # type: ignore[assignment] - - if parameters is None: - parameters = [] - - param = [] # type: list[Schema_CGAParameter | bytes] - length = 0 - for data in parameters: - if isinstance(data, bytes): - length += len(data) - param.append(data) - elif isinstance(data, Schema_CGAParameter): - if not hasattr(data, 'public_key_test'): - data.public_key_test = {'len': max(len(data.public_key) - 2, 0)} - _, ext_len = self._make_cga_extensions(data.extensions) - length += 25 + len(data.public_key) + ext_len - param.append(data) - elif isinstance(data, Data_CGAParameter): - ext, ext_len = self._make_cga_extensions(data.extensions) - schema = Schema_CGAParameter( - modifier=data.modifier, - prefix=data.prefix, - collision_count=data.collision_count, - public_key=data.public_key, - extensions=ext, - ) - schema.public_key_test = {'len': max(len(data.public_key) - 2, 0)} + att_val = option.att # type: Enum_AccessType + label = option.label + bid = option.bid + bulk = option.bulk + overwrite = option.overwrite + else: + att_val = self._make_index(att, att_default, namespace=att_namespace, # type: ignore[assignment] + reversed=att_reversed, pack=False) - length += 25 + len(data.public_key) + ext_len - param.append(schema) - else: - raise ProtocolError(f'{self.alias}: [OptNo {type}] unknown CGA parameter format: {data}') + if bulk and overwrite: + raise ProtocolError(f'{self.alias}: [OptNo {type}] the bulk re-registration and ' + 'registration overwrite flags are mutually exclusive') - return Schema_CGAParametersOption( + return Schema_MAGMultipathBindingOption( type=type, - length=length, - parameters=param, + length=6, + att=att_val, + label=label, + bid=bid, + flags={ + 'B': int(bulk), + 'O': int(overwrite), + }, ) - def _make_opt_signature(self, type: 'Enum_Option', option: 'Optional[Data_SignatureOption]' = None, *, - signature: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_SignatureOption': - """Make MH signature option. + def _make_opt_mag_id(self, type: 'Enum_Option', option: 'Optional[Data_MAGIdentifierOption]' = None, *, + subtype: 'Enum_MNIDSubtype | StdlibEnum | AenumEnum | str | int' = Enum_MNIDSubtype.NAI, + subtype_default: 'Optional[int]' = None, + subtype_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + subtype_reversed: 'bool' = False, + identifier: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_MAGIdentifierOption': + """Make MH MAG identifier option. Args: type: Option type. option: Option data model. - signature: Signature data. + subtype: Sub-type, from the mobile node identifier subtype registry. + subtype_default: Default sub-type. + subtype_namespace: Sub-type namespace. + subtype_reversed: Reverse sub-type namespace. + identifier: Identifier. **kwargs: Arbitrary keyword arguments. Returns: @@ -3576,23 +9933,30 @@ def _make_opt_signature(self, type: 'Enum_Option', option: 'Optional[Data_Signat """ if option is not None: - signature = option.signature + subtype_val = option.subtype # type: Enum_MNIDSubtype + identifier = option.identifier + else: + subtype_val = self._make_index(subtype, subtype_default, namespace=subtype_namespace, # type: ignore[assignment] + reversed=subtype_reversed, pack=False) - return Schema_SignatureOption( + return Schema_MAGIdentifierOption( type=type, - length=len(signature), - signature=signature, + length=2 + len(identifier), + subtype=subtype_val, + identifier=identifier, ) - def _make_opt_phkt(self, type: 'Enum_Option', option: 'Optional[Data_PermanentHomeKeygenTokenOption]' = None, *, - token: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_PermanentHomeKeygenTokenOption': - """Make MH permanent home keygen token option. + def _make_opt_anchored(self, type: 'Enum_Option', option: 'Optional[Data_AnchoredPrefixOption]' = None, *, + prefix_length: 'int' = 64, + prefix: 'bytes | str | int | IPv6Address' = '::', + **kwargs: 'Any') -> 'Schema_AnchoredPrefixOption': + """Make MH anchored prefix option. Args: type: Option type. option: Option data model. - token: Token data. + prefix_length: Prefix length. + prefix: Anchored prefix. **kwargs: Arbitrary keyword arguments. Returns: @@ -3600,41 +9964,63 @@ def _make_opt_phkt(self, type: 'Enum_Option', option: 'Optional[Data_PermanentHo """ if option is not None: - token = option.token + prefix_length = option.prefix_length + prefix = option.prefix - return Schema_PermanentHomeKeygenTokenOption( + if prefix_length > 128: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid prefix length: {prefix_length}') + + return Schema_AnchoredPrefixOption( type=type, - length=len(token), - token=token, + length=18, + prefix_length=prefix_length, + prefix=prefix, ) - def _make_opt_ct_init(self, type: 'Enum_Option', option: 'Optional[Data_CareofTestInitOption]' = None, - **kwargs: 'Any') -> 'Schema_CareofTestInitOption': - """Make MH Care-of Test Init option. + def _make_opt_local_prefix(self, type: 'Enum_Option', option: 'Optional[Data_LocalPrefixOption]' = None, *, + prefix_length: 'int' = 64, + prefix: 'bytes | str | int | IPv6Address' = '::', + **kwargs: 'Any') -> 'Schema_LocalPrefixOption': + """Make MH local prefix option. Args: type: Option type. option: Option data model. + prefix_length: Prefix length. + prefix: Local prefix. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. """ - return Schema_CareofTestInitOption( + if option is not None: + prefix_length = option.prefix_length + prefix = option.prefix + + if prefix_length > 128: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid prefix length: {prefix_length}') + + return Schema_LocalPrefixOption( type=type, - length=0, + length=18, + prefix_length=prefix_length, + prefix=prefix, ) - def _make_opt_ct(self, type: 'Enum_Option', option: 'Optional[Data_CareofTestOption]' = None, - token: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', - **kwargs: 'Any') -> 'Schema_CareofTestOption': - """Make MH Care-of Test option. + def _make_opt_prev_maar(self, type: 'Enum_Option', option: 'Optional[Data_PreviousMAAROption]' = None, *, + prefix_length: 'int' = 64, + maar: 'bytes | str | int | IPv6Address' = '::', + prefix: 'bytes | str | int | IPv6Address' = '::', + **kwargs: 'Any') -> 'Schema_PreviousMAAROption': + """Make MH previous MAAR option. Args: type: Option type. option: Option data model. - token: Care-of keygen token. + prefix_length: Prefix length of ``prefix``. + maar: Previous MAAR's global address. + prefix: Mobile node's home network prefix. **kwargs: Arbitrary keyword arguments. Returns: @@ -3642,23 +10028,30 @@ def _make_opt_ct(self, type: 'Enum_Option', option: 'Optional[Data_CareofTestOpt """ if option is not None: - token = option.token + prefix_length = option.prefix_length + maar = option.maar + prefix = option.prefix - return Schema_CareofTestOption( + if prefix_length > 128: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid prefix length: {prefix_length}') + + return Schema_PreviousMAAROption( type=type, - length=8, - token=token, + length=34, + prefix_length=prefix_length, + maar=maar, + prefix=prefix, ) - def _make_opt_exp(self, type: 'Enum_Option', option: 'Optional[Data_ExperimentalMobilityOption]' = None, *, - data: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_ExperimentalMobilityOption': - """Make MH experimental mobility option. + def _make_opt_serv_maar(self, type: 'Enum_Option', option: 'Optional[Data_ServingMAAROption]' = None, *, + address: 'bytes | str | int | IPv6Address' = '::', + **kwargs: 'Any') -> 'Schema_ServingMAAROption': + """Make MH serving MAAR option. Args: type: Option type. option: Option data model. - data: Experimental data. + address: Serving MAAR's global address. **kwargs: Arbitrary keyword arguments. Returns: @@ -3666,69 +10059,47 @@ def _make_opt_exp(self, type: 'Enum_Option', option: 'Optional[Data_Experimental """ if option is not None: - data = option.data + address = option.address - return Schema_ExperimentalMobilityOption( + return Schema_ServingMAAROption( type=type, - length=len(data), - data=data, + length=16, + address=address, ) - def _make_opt_badf(self, type: 'Enum_Option', option: 'Optional[Data_BADFOption]' = None, *, - spi: 'int' = 0, - data: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_BADFOption': - """Make MH binding authorization data for FMIPv6 (BADF) option. + def _make_opt_dlif_lla(self, type: 'Enum_Option', option: 'Optional[Data_DLIFLinkLocalAddressOption]' = None, *, + address: 'bytes | str | int | IPv6Address' = '::', + **kwargs: 'Any') -> 'Schema_DLIFLinkLocalAddressOption': + """Make MH DLIF link-local address option. Args: type: Option type. option: Option data model. - spi: Security parameter index. - data: Authenticator. + address: Distributed logical interface's link-local address. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. - Note: - The ``length`` field counts the authenticator only, excluding the - SPI, c.f., :meth:`_read_opt_badf`. - """ if option is not None: - spi = option.spi - data = option.data - - if not data: - raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid format') + address = option.address - return Schema_BADFOption( + return Schema_DLIFLinkLocalAddressOption( type=type, - length=len(data), - spi=spi, - data=data, + length=16, + address=address, ) - def _make_opt_ipv6_ap(self, type: 'Enum_Option', option: 'Optional[Data_IPv6AddressPrefixOption]' = None, *, - code: 'IPv6AddressPrefixCode | StdlibEnum | AenumEnum | str | int' = IPv6AddressPrefixCode.New_Care_of_Address, - code_default: 'Optional[int]' = None, - code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - code_reversed: 'bool' = False, - prefix_length: 'int' = 128, - address: 'bytes | str | int | IPv6Address' = '::', - **kwargs: 'Any') -> 'Schema_IPv6AddressPrefixOption': - """Make MH mobility header IPv6 address/prefix option. + def _make_opt_dlif_lladdr(self, type: 'Enum_Option', option: 'Optional[Data_DLIFLinkLayerAddressOption]' = None, *, + lla: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_DLIFLinkLayerAddressOption': + """Make MH DLIF link-layer address option. Args: type: Option type. option: Option data model. - code: Option code, c.f., - :class:`~pcapkit.protocols.internet.mh.IPv6AddressPrefixCode`. - code_default: Default option code. - code_namespace: Option code namespace. - code_reversed: Reverse option code namespace. - prefix_length: Prefix length. - address: IPv6 address/prefix. + lla: Distributed logical interface's link-layer address. **kwargs: Arbitrary keyword arguments. Returns: @@ -3736,26 +10107,14 @@ def _make_opt_ipv6_ap(self, type: 'Enum_Option', option: 'Optional[Data_IPv6Addr """ if option is not None: - code_val = option.code - prefix_length = option.prefix_length - address = option.address - else: - code_val = self._make_index(code, code_default, namespace=code_namespace, # type: ignore[assignment] - reversed=code_reversed, pack=False) - - if prefix_length > 128: - raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid prefix length: {prefix_length}') + lla = option.lla - return Schema_IPv6AddressPrefixOption( + return Schema_DLIFLinkLayerAddressOption( type=type, - length=18, - code=code_val, - prefix_length=prefix_length, - address=address, + length=2 + len(lla), + lla=lla, ) - # TODO: Implement other options. - def _make_cga_extensions(self, extensions: 'Extension | list[Schema_CGAExtension | tuple[Enum_CGAExtension, dict[str, Any]] | bytes]') -> 'tuple[list[Schema_CGAExtension | bytes], int]': """Make CGA extensions for MH. @@ -3855,17 +10214,52 @@ def _make_ext_multiprefix(self, type: 'Enum_CGAExtension', option: 'Optional[Dat """ if option is not None: flag = option.flag - prefixes = cast('list[int]', option.prefixes) + # NOTE: ``list()`` rather than a cast: the data model stores the + # prefixes as a :obj:`tuple`, which + # :class:`~pcapkit.corekit.fields.collections.ListField` refuses to + # pack -- it raises ``ProtocolUnbound: unsupported type ``. The cast this replaced was a no-op at runtime, so + # re-making a parsed Multi-Prefix extension could not work at all. + prefixes = list(option.prefixes) else: prefixes = prefixes or [] return Schema_MultiPrefixExtension( type=type, - length=1 + len(prefixes) * 16, + # NOTE: The extension data is the 4-octet flags word followed by one + # **8**-octet prefix apiece, since + # :attr:`~pcapkit.protocols.schema.internet.mh.MultiPrefixExtension.prefixes` + # is a list of :class:`~pcapkit.corekit.fields.numbers.UInt64Field`. + # This used to read ``1 + len(prefixes) * 16``, which declared 33 + # octets where 20 were emitted for two prefixes, so a re-parse ran off + # the end of the extension. + length=4 + len(prefixes) * 8, flags={ 'P': int(flag), }, prefixes=prefixes, ) - # TODO: Implement other CGA extensions. + def _make_ext_exp(self, type: 'Enum_CGAExtension', option: 'Optional[Data_ExperimentalExtension]' = None, *, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_ExperimentalExtension': + """Make experimental CGA extension. + + Args: + type: Extension type. + option: Extension data model. + data: Extension data, which :rfc:`4581#section-3` gives no structure. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed extension schema. + + """ + if option is not None: + data = option.data + + return Schema_ExperimentalExtension( + type=type, + length=len(data), + data=data, + ) diff --git a/pcapkit/protocols/schema/internet/mh.py b/pcapkit/protocols/schema/internet/mh.py index 4b4dea99eb..4becf0daa5 100644 --- a/pcapkit/protocols/schema/internet/mh.py +++ b/pcapkit/protocols/schema/internet/mh.py @@ -55,7 +55,7 @@ from pcapkit.const.mh.upn_reason import UpdateNotificationReason as Enum_UpdateNotificationReason from pcapkit.const.reg.transtype import TransType as Enum_TransType from pcapkit.corekit.fields.collections import ListField, OptionField -from pcapkit.corekit.fields.ipaddress import IPv6AddressField +from pcapkit.corekit.fields.ipaddress import IPv4AddressField, IPv6AddressField from pcapkit.corekit.fields.misc import (ConditionalField, ForwardMatchField, PayloadField, SchemaField, SwitchField) from pcapkit.corekit.fields.numbers import (EnumField, UInt8Field, UInt16Field, UInt32Field, @@ -72,7 +72,11 @@ 'HomeTestMessage', 'CareofTestMessage', 'BindingUpdateMessage', 'BindingAcknowledgementMessage', 'BindingErrorMessage', 'FastBindingUpdateMessage', 'FastBindingAcknowledgmentMessage', 'FastNeighborAdvertisementMessage', 'ExperimentalMessage', 'HandoverInitiateMessage', - 'HandoverAcknowledgeMessage', + 'HandoverAcknowledgeMessage', 'HeartbeatMessage', 'HomeAgentSwitchMessage', + 'BindingRevocationMessage', 'LocalizedRoutingInitiationMessage', + 'LocalizedRoutingAcknowledgmentMessage', 'UpdateNotificationMessage', + 'UpdateNotificationAcknowledgementMessage', 'FlowBindingMessage', + 'SubscriptionQueryMessage', 'SubscriptionResponseMessage', 'Option', 'UnassignedOption', 'PadOption', 'BindingRefreshAdviceOption', 'AlternateCareofAddressOption', @@ -80,17 +84,54 @@ 'LinkLayerAddressOption', 'MNIDOption', 'AuthOption', 'MesgIDOption', 'CGAParametersRequestOption', 'CGAParametersOption', 'SignatureOption', 'PermanentHomeKeygenTokenOption', 'CareofTestInitOption', 'CareofTestOption', 'ExperimentalMobilityOption', 'BADFOption', 'IPv6AddressPrefixOption', + 'HomeNetworkPrefixOption', 'HandoffIndicatorOption', 'AccessTechnologyTypeOption', + 'MNLLIdentifierOption', 'LinkLocalAddressOption', 'TimestampOption', 'RestartCounterOption', + 'DNSUpdateOption', 'VendorSpecificOption', 'ServiceSelectionOption', + 'IPv4HomeAddressOption', 'IPv4AddressAcknowledgementOption', 'NATDetectionOption', + 'IPv4CareofAddressOption', 'GREKeyOption', 'BindingIdentifierOption', + 'IPv4HomeAddressRequestOption', 'IPv4HomeAddressReplyOption', + 'IPv4DefaultRouterAddressOption', 'IPv4DHCPSupportModeOption', 'ContextRequestOption', + 'LMAAddressOption', 'MNLLAIIDOption', 'TransientBindingOption', 'FlowSummaryOption', + 'FlowIdentificationOption', 'RedirectCapabilityOption', 'RedirectOption', + 'LoadInformationOption', 'AlternateIPv4CareofAddressOption', 'MNGroupIdentifierOption', + 'MAGIPv6AddressOption', 'AccessNetworkIdentifierOption', 'IPv4TrafficOffloadSelectorOption', + 'DynamicIPMulticastSelectorOption', 'DelegatedMNPOption', + 'ActiveMulticastSubscriptionIPv4Option', 'ActiveMulticastSubscriptionIPv6Option', + 'QualityOfServiceOption', 'LMAUserPlaneAddressOption', 'MulticastMobilityOption', + 'MulticastAcknowledgementOption', 'LMAControlledMAGParametersOption', + 'MAGMultipathBindingOption', 'MAGIdentifierOption', 'AnchoredPrefixOption', + 'LocalPrefixOption', 'PreviousMAAROption', 'ServingMAAROption', + 'DLIFLinkLocalAddressOption', 'DLIFLinkLayerAddressOption', + + 'FlowIdentificationSuboption', + 'UnassignedFlowIdentificationSuboption', 'PadFlowIdentificationSuboption', + 'BIDReferenceSuboption', 'TrafficSelectorSuboption', 'FlowBindingActionSuboption', + 'TargetCareofAddressSuboption', + + 'ANISuboption', + 'UnassignedANISuboption', 'ANINetworkIdentifierSuboption', 'ANIGeoLocationSuboption', + 'ANIOperatorIdentifierSuboption', 'ANICivicLocationSuboption', + 'ANIMAGGroupIdentifierSuboption', 'ANIUpdateTimerSuboption', + + 'QoSAttribute', + 'UnassignedQoSAttribute', 'BitRateAttribute', 'PerSessionBitRateAttribute', + 'AllocationRetentionPriorityAttribute', 'QoSTrafficSelectorAttribute', + 'QoSVendorSpecificAttribute', + + 'LMAControlledMAGSuboption', + 'UnassignedLMAControlledMAGSuboption', 'BindingReregistrationControlSuboption', + 'HeartbeatControlSuboption', 'CGAParameter', 'CGAExtension', - 'UnknownExtension', 'MultiPrefixExtension', + 'UnknownExtension', 'MultiPrefixExtension', 'ExperimentalExtension', ] if TYPE_CHECKING: from datetime import datetime as dt_type - from ipaddress import IPv6Address - from typing import Any + from ipaddress import IPv4Address, IPv6Address + from typing import Any, Optional from pcapkit.corekit.fields.field import FieldBase as Field from pcapkit.protocols.protocol import ProtocolBase as Protocol @@ -150,6 +191,134 @@ class HandoverAcknowledgeMessageFlags(TypedDict): P: 'int' F: 'int' + class HeartbeatMessageFlags(TypedDict): + """Flags for :attr:`HeartbeatMessage.flags`.""" + + U: 'int' + R: 'int' + + class FixedPointTimestamp(TypedDict): + """Fixed-point timestamp of :attr:`TimestampOption.timestamp`.""" + + seconds: 'int' + fraction: 'int' + + class BindingRevocationMessageFlags(TypedDict): + """Flags for :attr:`BindingRevocationMessage.flags`.""" + + P: 'int' + V: 'int' + G: 'int' + + class LocalizedRoutingAcknowledgmentMessageFlags(TypedDict): + """Flags for :attr:`LocalizedRoutingAcknowledgmentMessage.flags`.""" + + U: 'int' + + class FlowBindingMessageFlags(TypedDict): + """Flags for :attr:`FlowBindingMessage.flags`.""" + + A: 'int' + + class UpdateNotificationMessageFlags(TypedDict): + """Flags for :attr:`UpdateNotificationMessage.flags`.""" + + A: 'int' + D: 'int' + + class SubscriptionResponseMessageFlags(TypedDict): + """Flags for :attr:`SubscriptionResponseMessage.flags`.""" + + I: 'int' + + class DNSUpdateOptionFlags(TypedDict): + """Flags for :attr:`DNSUpdateOption.flags`.""" + + R: 'int' + + class IPv4HomeAddressOptionFlags(TypedDict): + """Prefix length and flags of :attr:`IPv4HomeAddressOption.flags`.""" + + prefix_length: 'int' + P: 'int' + + class PrefixLengthOnly(TypedDict): + """Prefix length packed alone in its own word, as in + :attr:`IPv4AddressAcknowledgementOption.flags`, + :attr:`IPv4HomeAddressRequestOption.flags` and + :attr:`IPv4HomeAddressReplyOption.flags`.""" + + prefix_length: 'int' + + class NATDetectionOptionFlags(TypedDict): + """Flags for :attr:`NATDetectionOption.flags`.""" + + F: 'int' + + class IPv4DHCPSupportModeOptionFlags(TypedDict): + """Flags for :attr:`IPv4DHCPSupportModeOption.flags`.""" + + S: 'int' + + class RedirectOptionFlags(TypedDict): + """Flags for :attr:`RedirectOption.flags`.""" + + K: 'int' + N: 'int' + + class BindingIdentifierOptionFlags(TypedDict): + """Flag and priority of :attr:`BindingIdentifierOption.flags`.""" + + H: 'int' + BID_PRI: 'int' + + class TransientBindingOptionFlags(TypedDict): + """Flags for :attr:`TransientBindingOption.flags`.""" + + L: 'int' + + class ModeFlagOnly(TypedDict): + """A lone ``M`` mode flag, as in + :attr:`DynamicIPMulticastSelectorOption.flags` and + :attr:`IPv4TrafficOffloadSelectorOption.flags`.""" + + M: 'int' + + class DelegatedMNPOptionFlags(TypedDict): + """Flags for :attr:`DelegatedMNPOption.flags`.""" + + V: 'int' + + class MAGMultipathBindingOptionFlags(TypedDict): + """Flags for :attr:`MAGMultipathBindingOption.flags`.""" + + B: 'int' + O: 'int' + + class ANINetworkIdentifierFlags(TypedDict): + """Flags for :attr:`ANINetworkIdentifierSuboption.flags`.""" + + E: 'int' + + class GeoLocationDegrees(TypedDict): + """Latitude and longitude of :attr:`ANIGeoLocationSuboption.location`.""" + + latitude: 'int' + longitude: 'int' + + class SessionBitRateFlags(TypedDict): + """Flags for :attr:`PerSessionBitRateAttribute.flags`.""" + + S: 'int' + E: 'int' + + class AllocationRetentionPriorityFields(TypedDict): + """Priority fields of :attr:`AllocationRetentionPriorityAttribute.priority`.""" + + PL: 'int' + PC: 'int' + PV: 'int' + def mh_data_selector(pkt: 'dict[str, Any]') -> 'Field': """Selector function for :attr:`MH.data` field. @@ -221,6 +390,169 @@ def pad_opt_data_len(pkt: 'dict[str, Any]') -> 'int': return length +def pad_subopt_data_len(pkt: 'dict[str, Any]') -> 'int': + """Return the length of the padding data of a flow identification padding + sub-option. + + Args: + pkt: Padding sub-option unpacked schema. + + Returns: + Number of padding octets carried after the sub-option header, i.e. the + value of the ``Sub-Opt Length`` field of a ``PadN`` sub-option, and zero + for the one-octet ``Pad`` sub-option, which carries no such field. + + See Also: + :func:`pad_opt_data_len`, which does the same job for the mobility + options themselves and documents why the test has to be on the field + *value* rather than on the presence of its key. + + """ + length = pkt.get('length', 0) + if not isinstance(length, int): # ``NoValue`` (skipped) or :obj:`None` (unset) + return 0 + return length + + +def bid_address_selector(pkt: 'dict[str, Any]') -> 'Field': + """Selector function for :attr:`BindingIdentifierOption.address` field. + + Args: + pkt: Packet data. + + Returns: + Returns a :class:`~pcapkit.corekit.fields.field.FieldBase` instance + sized from the option length, which is the only thing on the wire that + says whether the care-of address is an IPv4 or an IPv6 one + [:rfc:`5648#section-4.3`]. + + """ + length = pkt['length'] + if length == 8: + return IPv4AddressField() + if length == 20: + return IPv6AddressField() + return BytesField(length=max(length - 4, 0)) + + +def lma_address_selector(pkt: 'dict[str, Any]') -> 'Field': + """Selector function for :attr:`LMAAddressOption.address` field. + + Args: + pkt: Packet data. + + Returns: + Returns a :class:`~pcapkit.corekit.fields.field.FieldBase` instance + sized from the option length -- 6 for an IPv4 address and 18 for an IPv6 + one [:rfc:`5949#section-6.2.2`]. + + """ + length = pkt['length'] + if length == 6: + return IPv4AddressField() + if length == 18: + return IPv6AddressField() + return BytesField(length=max(length - 2, 0)) + + +def target_coa_selector(pkt: 'dict[str, Any]') -> 'Field': + """Selector function for :attr:`TargetCareofAddressSuboption.address` field. + + Args: + pkt: Packet data. + + Returns: + Returns a :class:`~pcapkit.corekit.fields.field.FieldBase` instance + sized from the sub-option length -- 6 for an IPv4 address and 18 for an + IPv6 one [:rfc:`7109#section-6.2.2`]. + + """ + length = pkt['length'] + if length == 6: + return IPv4AddressField() + if length == 18: + return IPv6AddressField() + return BytesField(length=max(length - 2, 0)) + + +def dmnp_prefix_selector(pkt: 'dict[str, Any]') -> 'Field': + """Selector function for :attr:`DelegatedMNPOption.prefix` field. + + Args: + pkt: Packet data. + + Returns: + Returns a :class:`~pcapkit.corekit.fields.field.FieldBase` instance + selected by the ``V`` flag, which :rfc:`7148#section-4.1` makes + authoritative for the address family -- unlike the options that carry no + such flag and have to be sized from their length. + + """ + if pkt['flags']['V']: + return IPv4AddressField() + return IPv6AddressField() + + +def lma_user_plane_selector(pkt: 'dict[str, Any]') -> 'Field': + """Selector function for :attr:`LMAUserPlaneAddressOption.address` field. + + Args: + pkt: Packet data. + + Returns: + Returns a :class:`~pcapkit.corekit.fields.field.FieldBase` instance + sized from the option length. :rfc:`7389#section-4` lets the address be + absent as well as IPv4 or IPv6 -- a mobile access gateway sends the + option with no address at all, or with an all-zero one, purely to say + which transport it wants. + + """ + length = pkt['length'] + if length == 6: + return IPv4AddressField() + if length == 18: + return IPv6AddressField() + return BytesField(length=max(length - 2, 0)) + + +def br_code_selector(pkt: 'dict[str, Any]') -> 'Field': + """Selector function for :attr:`BindingRevocationMessage.code` field. + + Args: + pkt: Packet data. + + Returns: + Returns an :class:`~pcapkit.corekit.fields.numbers.EnumField` over the + registry the octet draws from, which depends on which of the two forms + of the message this is: a revocation trigger in a Binding Revocation + Indication and a status code in a Binding Revocation Acknowledgement + [:rfc:`5846#section-5.1`, :rfc:`5846#section-5.2`]. + + """ + if pkt['br_type'] == Enum_BindingRevocation.Binding_Revocation_Acknowledgement: + return EnumField(length=1, namespace=Enum_RevocationStatusCode) + return EnumField(length=1, namespace=Enum_RevocationTrigger) + + +def fb_code_selector(pkt: 'dict[str, Any]') -> 'Field': + """Selector function for :attr:`FlowBindingMessage.code` field. + + Args: + pkt: Packet data. + + Returns: + Returns an :class:`~pcapkit.corekit.fields.numbers.EnumField` over the + registry the octet draws from, which depends on which of the two forms of + the message this is: an indication trigger in a Flow Binding Indication + and a status code in a Flow Binding Acknowledgement + [:rfc:`7109#section-6.1.1`, :rfc:`7109#section-6.1.2`]. + + """ + if pkt['fb_type'] == Enum_FlowBindingType.Acknowledgement: + return EnumField(length=1, namespace=Enum_FlowBindingACKStatus) + return EnumField(length=1, namespace=Enum_FlowBindingIndicationTrigger) + + @schema_final class MH(Schema): """Header schema for MH packets.""" @@ -495,6 +827,31 @@ class MultiPrefixExtension(CGAExtension, code=Enum_CGAExtension.Multi_Prefix): def __init__(self, type: 'Enum_CGAExtension', length: 'int', flags: 'MultiPrefixExtensionFlags', prefixes: 'list[int]') -> 'None': ... +@schema_final +class ExperimentalExtension(CGAExtension, code=[Enum_CGAExtension.Exp_FFFD, + Enum_CGAExtension.Exp_FFFE, + Enum_CGAExtension.Exp_FFFF]): + """Header schema for experimental CGA extensions. + + :rfc:`4581#section-3` assigns extension types ``0xFFFD``, ``0xFFFE`` and + ``0xFFFF`` for experimental use as :rfc:`3692` recommends, and defines **no** + structure for their extension data -- neither it nor :rfc:`5535` gives those + three a layout, and the IANA registry records only their names. So the data + is genuinely opaque rather than merely undecoded here, and a parser cannot do + better than the extension header and a byte string. + + The three share one schema because they share one shape; they are told apart + by :attr:`CGAExtension.type`. + + """ + + #: Extension data. + data: 'bytes' = BytesField(length=lambda pkt: pkt['length']) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_CGAExtension', length: 'int', data: 'bytes') -> 'None': ... + + @schema_final class CGAParameter(Schema): """Header schema for CGA Parameters.""" @@ -646,138 +1003,1586 @@ def __init__(self, type: 'Enum_Option', length: 'int', code: 'int', prefix_lengt address: 'IPv6Address | int | bytes | str') -> 'None': ... -# TODO: Implement other options. +@schema_final +class DNSUpdateOption(Option, code=Enum_Option.DNS_UPDATE_TYPE): + """Header schema for MH DNS-UPDATE-TYPE options.""" + + #: Status. + status: 'Enum_DNSStatusCode' = EnumField(length=1, namespace=Enum_DNSStatusCode) + #: Flags. + flags: 'DNSUpdateOptionFlags' = BitField(length=1, namespace={ + 'R': (0, 1), + }) + #: Mobile node identity, in FQDN form. + identity: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 2) + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', status: 'Enum_DNSStatusCode', + flags: 'DNSUpdateOptionFlags', identity: 'bytes') -> 'None': ... -class Packet(EnumSchema[Enum_Packet]): - """Header schema for MH packet data.""" - __default__ = lambda: UnknownMessage +@schema_final +class VendorSpecificOption(Option, code=Enum_Option.Vendor_Specific_Mobility_Option): + """Header schema for MH Vendor Specific mobility options.""" + + #: Vendor ID, an SMI Network Management Private Enterprise Number. + vendor: 'int' = UInt32Field() + #: Vendor-administered sub-type. + subtype: 'int' = UInt8Field() + #: Vendor-specific data. + data: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 5) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', vendor: 'int', subtype: 'int', + data: 'bytes') -> 'None': ... @schema_final -class UnknownMessage(Packet): - """Header schema for MH unknown message type.""" +class ServiceSelectionOption(Option, code=Enum_Option.Service_Selection_Mobility_Option): + """Header schema for MH Service Selection mobility options.""" - #: Message data. - data: 'bytes' = BytesField(length=lambda pkt: pkt['__length__']) + #: Service identifier. + identifier: 'str' = StringField(length=lambda pkt: pkt['length']) if TYPE_CHECKING: - def __init__(self, data: 'bytes') -> 'None': ... + def __init__(self, type: 'Enum_Option', length: 'int', identifier: 'str | bytes') -> 'None': ... @schema_final -class BindingRefreshRequestMessage(Packet, code=Enum_Packet.Binding_Refresh_Request): - """Header schema for MH Binding Refresh Request (BRR) message.""" +class HomeNetworkPrefixOption(Option, code=Enum_Option.Home_Network_Prefix_Option): + """Header schema for MH Home Network Prefix options.""" #: Reserved. - reserved: 'bytes' = PaddingField(length=2) - #: Mobility options. - options: 'list[Option]' = OptionField( - length=lambda pkt: pkt['__length__'], - base_schema=Option, - type_name='type', - registry=Option.registry, - eool=None, - ) + reserved: 'bytes' = PaddingField(length=1) + #: Prefix length. + prefix_length: 'int' = UInt8Field() + #: Home network prefix. + prefix: 'IPv6Address' = IPv6AddressField() if TYPE_CHECKING: - def __init__(self, options: 'list[Option | bytes]') -> 'None': ... + def __init__(self, type: 'Enum_Option', length: 'int', prefix_length: 'int', + prefix: 'IPv6Address | int | bytes | str') -> 'None': ... @schema_final -class HomeTestInitMessage(Packet, code=Enum_Packet.Home_Test_Init): - """Header schema for MH Home Test Init (HoTI) message.""" +class HandoffIndicatorOption(Option, code=Enum_Option.Handoff_Indicator_Option): + """Header schema for MH Handoff Indicator options.""" #: Reserved. - reserved: 'bytes' = PaddingField(length=2) - #: Home init cookie. - cookie: 'bytes' = BytesField(length=8) - #: Mobility options. - options: 'list[Option]' = OptionField( - length=lambda pkt: pkt['__length__'], - base_schema=Option, - type_name='type', - registry=Option.registry, - eool=None, - ) + reserved: 'bytes' = PaddingField(length=1) + #: Handoff indicator. + hi: 'Enum_HandoffType' = EnumField(length=1, namespace=Enum_HandoffType) if TYPE_CHECKING: - def __init__(self, cookie: 'bytes', options: 'list[Option | bytes]') -> 'None': ... + def __init__(self, type: 'Enum_Option', length: 'int', hi: 'Enum_HandoffType') -> 'None': ... @schema_final -class CareofTestInitMessage(Packet, code=Enum_Packet.Care_of_Test_Init): - """Header schema for MH Care-of Test Init (CoTI) messages.""" +class AccessTechnologyTypeOption(Option, code=Enum_Option.Access_Technology_Type_Option): + """Header schema for MH Access Technology Type options.""" #: Reserved. - reserved: 'bytes' = PaddingField(length=2) - #: Care-of init cookie. - cookie: 'bytes' = BytesField(length=8) - #: Mobility options. - options: 'list[Option]' = OptionField( - length=lambda pkt: pkt['__length__'], - base_schema=Option, - type_name='type', - registry=Option.registry, - eool=None, - ) + reserved: 'bytes' = PaddingField(length=1) + #: Access technology type. + att: 'Enum_AccessType' = EnumField(length=1, namespace=Enum_AccessType) if TYPE_CHECKING: - def __init__(self, cookie: 'bytes', options: 'list[Option | bytes]') -> 'None': ... + def __init__(self, type: 'Enum_Option', length: 'int', att: 'Enum_AccessType') -> 'None': ... @schema_final -class HomeTestMessage(Packet, code=Enum_Packet.Home_Test): - """Header schema for MH Home Test (HoT) message.""" +class MNLLIdentifierOption(Option, code=Enum_Option.Mobile_Node_Link_layer_Identifier_Option): + """Header schema for MH Mobile Node Link-layer Identifier options.""" - #: Home nonce index. - nonce_index: 'int' = UInt16Field() - #: Home init cookie. - cookie: 'bytes' = BytesField(length=8) - #: Home keygen token. - token: 'bytes' = BytesField(length=8) - #: Mobility options. - options: 'list[Option]' = OptionField( - length=lambda pkt: pkt['__length__'], - base_schema=Option, - type_name='type', - registry=Option.registry, - eool=None, - ) + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + #: Link-layer identifier. + lli: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 2) if TYPE_CHECKING: - def __init__(self, nonce_index: 'int', cookie: 'bytes', token: 'bytes', - options: 'list[Option | bytes]') -> 'None': ... + def __init__(self, type: 'Enum_Option', length: 'int', lli: 'bytes') -> 'None': ... @schema_final -class CareofTestMessage(Packet, code=Enum_Packet.Care_of_Test): - """Header schema for MH Care-of Test (CoT) message.""" +class LinkLocalAddressOption(Option, code=Enum_Option.Link_local_Address_Option): + """Header schema for MH Link-local Address options.""" - #: Care-of nonce index. - nonce_index: 'int' = UInt16Field() - #: Care-of init cookie. - cookie: 'bytes' = BytesField(length=8) - #: Care-of keygen token. - token: 'bytes' = BytesField(length=8) - #: Mobility options. - options: 'list[Option]' = OptionField( - length=lambda pkt: pkt['__length__'], - base_schema=Option, - type_name='type', - registry=Option.registry, - eool=None, - ) + #: Link-local address. + address: 'IPv6Address' = IPv6AddressField() if TYPE_CHECKING: - def __init__(self, nonce_index: 'int', cookie: 'bytes', token: 'bytes', - options: 'list[Option | bytes]') -> 'None': ... + def __init__(self, type: 'Enum_Option', length: 'int', + address: 'IPv6Address | int | bytes | str') -> 'None': ... @schema_final -class BindingUpdateMessage(Packet, code=Enum_Packet.Binding_Update): - """Header schema for MH Binding Update (BU) messages.""" +class TimestampOption(Option, code=Enum_Option.Timestamp_Option): + """Header schema for MH Timestamp options. + + Note: + The 64-bit timestamp of :rfc:`5213#section-8.8` is **not** an + :rfc:`1305` NTP timestamp, which is why it is not shared with + :class:`MesgIDOption`. It counts seconds since 1 January 1970 00:00 UTC + -- the UNIX epoch, not NTP's 1900 one -- in a fixed-point format whose + integer part occupies the leading **48** bits and whose fractional part + occupies the trailing **16**, each unit of the latter being 1/65536 of a + second. NTP splits its 64 bits 32/32 from a different epoch, so reusing + :class:`~pcapkit.protocols.internet.mh.NTPTimestamp` here would misread + both halves. + + """ + + #: Timestamp, as the 48-bit second count and 16-bit fraction of + #: :rfc:`5213#section-8.8`. + timestamp: 'FixedPointTimestamp' = BitField(length=8, namespace={ + 'seconds': (0, 48), + 'fraction': (48, 16), + }) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + timestamp: 'FixedPointTimestamp') -> 'None': ... + + +@schema_final +class RestartCounterOption(Option, code=Enum_Option.Restart_Counter): + """Header schema for MH Restart Counter options.""" + + #: Restart counter. + counter: 'int' = UInt32Field() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', counter: 'int') -> 'None': ... + + +@schema_final +class IPv4HomeAddressOption(Option, code=Enum_Option.IPv4_Home_Address): + """Header schema for MH IPv4 Home Address options.""" + + #: Prefix length and flags. :rfc:`5555#section-3.1.1` packs the 6-bit + #: ``Prefix-len``, the ``P`` flag and 9 reserved bits into one 16-bit word. + flags: 'IPv4HomeAddressOptionFlags' = BitField(length=2, namespace={ + 'prefix_length': (0, 6), + 'P': (6, 1), + }) + #: IPv4 home address. + address: 'IPv4Address' = IPv4AddressField() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', flags: 'IPv4HomeAddressOptionFlags', + address: 'IPv4Address | int | bytes | str') -> 'None': ... + + +@schema_final +class IPv4AddressAcknowledgementOption(Option, code=Enum_Option.IPv4_Address_Acknowledgement): + """Header schema for MH IPv4 Address Acknowledgement options.""" + + #: Status. + status: 'Enum_DSMIPv6HomeAddress' = EnumField(length=1, namespace=Enum_DSMIPv6HomeAddress) + #: Prefix length, with 2 reserved bits below it. + flags: 'PrefixLengthOnly' = BitField(length=1, namespace={ + 'prefix_length': (0, 6), + }) + #: IPv4 home address. + address: 'IPv4Address' = IPv4AddressField() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', status: 'Enum_DSMIPv6HomeAddress', + flags: 'PrefixLengthOnly', + address: 'IPv4Address | int | bytes | str') -> 'None': ... + + +@schema_final +class NATDetectionOption(Option, code=Enum_Option.NAT_Detection): + """Header schema for MH NAT Detection options.""" + + #: Flags. + flags: 'NATDetectionOptionFlags' = BitField(length=2, namespace={ + 'F': (0, 1), + }) + #: Suggested NAT binding refresh time, in seconds. + refresh: 'int' = UInt32Field() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', flags: 'NATDetectionOptionFlags', + refresh: 'int') -> 'None': ... + + +@schema_final +class IPv4CareofAddressOption(Option, code=Enum_Option.IPv4_Care_of_Address): + """Header schema for MH IPv4 Care-of Address options.""" + + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + #: IPv4 care-of address. + address: 'IPv4Address' = IPv4AddressField() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + address: 'IPv4Address | int | bytes | str') -> 'None': ... + + +@schema_final +class GREKeyOption(Option, code=Enum_Option.GRE_Key_Option): + """Header schema for MH GRE Key options. + + Note: + The ``GRE Key Identifier`` field is **absent** when + :attr:`Option.length` is ``2`` and present when it is ``6`` + [:rfc:`5845#section-6.1`]. There is no flag saying so, so + :attr:`key` is sized from the option length alone. + + """ + + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + #: GRE key identifier, carried only when the option length is 6. + key: 'Optional[int]' = ConditionalField( + UInt32Field(default=0), + lambda pkt: pkt['length'] >= 6, + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + key: 'Optional[int]') -> 'None': ... + + +@schema_final +class BindingIdentifierOption(Option, code=Enum_Option.Binding_Identifier): + """Header schema for MH Binding Identifier (BID) options. + + Note: + The care-of address is absent when :attr:`Option.length` is ``4``, an + IPv4 address when it is ``8``, and an IPv6 address when it is ``20`` + [:rfc:`5648#section-4.3`]. The option carries **no** address-family + flag, so :attr:`address` is selected from the option length alone. + + :rfc:`6089#section-4.1` renames the 7 bits below the ``H`` flag from + ``Reserved`` to ``BID-PRI``, a binding priority in which ``0`` means the + sender predates :rfc:`6089`. The field is parsed under the newer name. + + """ + + #: Binding identifier. + bid: 'int' = UInt16Field() + #: Status, overriding the message status for this binding alone. + status: 'Enum_StatusCode' = EnumField(length=1, namespace=Enum_StatusCode) + #: Simultaneous home and foreign binding flag, and binding priority. + flags: 'BindingIdentifierOptionFlags' = BitField(length=1, namespace={ + 'H': (0, 1), + 'BID_PRI': (1, 7), + }) + #: Care-of address, absent for an option length of 4. + address: 'Optional[IPv4Address | IPv6Address | bytes]' = ConditionalField( + SwitchField(selector=bid_address_selector), + lambda pkt: pkt['length'] > 4, + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', bid: 'int', + status: 'Enum_StatusCode', flags: 'BindingIdentifierOptionFlags', + address: 'Optional[IPv4Address | IPv6Address | bytes | int | str]') -> 'None': ... + + +@schema_final +class IPv4HomeAddressRequestOption(Option, code=Enum_Option.IPv4_Home_Address_Request): + """Header schema for MH IPv4 Home Address Request options.""" + + #: Prefix length, with 10 reserved bits below it. + flags: 'PrefixLengthOnly' = BitField(length=2, namespace={ + 'prefix_length': (0, 6), + }) + #: Requested IPv4 home address. + address: 'IPv4Address' = IPv4AddressField() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', flags: 'PrefixLengthOnly', + address: 'IPv4Address | int | bytes | str') -> 'None': ... + + +@schema_final +class IPv4HomeAddressReplyOption(Option, code=Enum_Option.IPv4_Home_Address_Reply): + """Header schema for MH IPv4 Home Address Reply options.""" + + #: Status. + status: 'Enum_HomeAddressReply' = EnumField(length=1, namespace=Enum_HomeAddressReply) + #: Prefix length, with 2 reserved bits below it. + flags: 'PrefixLengthOnly' = BitField(length=1, namespace={ + 'prefix_length': (0, 6), + }) + #: Assigned IPv4 home address. + address: 'IPv4Address' = IPv4AddressField() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', status: 'Enum_HomeAddressReply', + flags: 'PrefixLengthOnly', + address: 'IPv4Address | int | bytes | str') -> 'None': ... + + +@schema_final +class IPv4DefaultRouterAddressOption(Option, code=Enum_Option.IPv4_Default_Router_Address): + """Header schema for MH IPv4 Default-Router Address options.""" + + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + #: IPv4 default-router address. + address: 'IPv4Address' = IPv4AddressField() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + address: 'IPv4Address | int | bytes | str') -> 'None': ... + + +@schema_final +class IPv4DHCPSupportModeOption(Option, code=Enum_Option.IPv4_DHCP_Support_Mode): + """Header schema for MH IPv4 DHCP Support Mode options.""" + + #: Flags. The ``S`` bit is the **last** bit of the option, not the first + #: [:rfc:`5844#section-3.3.4`]. + flags: 'IPv4DHCPSupportModeOptionFlags' = BitField(length=2, namespace={ + 'S': (15, 1), + }) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + flags: 'IPv4DHCPSupportModeOptionFlags') -> 'None': ... + + +@schema_final +class ContextRequestOption(Option, code=Enum_Option.Context_Request_Option): + """Header schema for MH Context Request options. + + Note: + The request entries are carried as one opaque run here and walked by + :meth:`~pcapkit.protocols.internet.mh.MH._read_opt_cr`, rather than as a + :class:`~pcapkit.corekit.fields.collections.ListField` of a nested schema. + Each entry is self-describing -- a type octet, a length octet and that many + octets of value [:rfc:`5949#section-6.2.1`] -- so a nested schema would need + a :class:`~pcapkit.corekit.fields.misc.SchemaField` with no fixed length, + and such a field is handed ``-1`` as its length and hands that on as the + nested schema's ``__length__``. Every field then decrements a budget that + started negative, and parsing a perfectly valid option emits a + :class:`~pcapkit.utilities.warnings.SchemaWarning` per field saying + ``packet length < 0``. Decoding the run directly costs a few lines and + keeps a valid packet quiet. + + """ + + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + #: Requested contexts, as a run of type/length/value entries. + requests: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 2) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + requests: 'bytes') -> 'None': ... + + +@schema_final +class LMAAddressOption(Option, code=Enum_Option.Local_Mobility_Anchor_Address_Option): + """Header schema for MH Local Mobility Anchor Address options. + + Note: + :attr:`code` says whether an IPv6 (``1``) or an IPv4 (``2``) address + follows [:rfc:`5949#section-6.2.2`]. Those codes are defined inline by + the RFC with no IANA registry behind them, so their enumeration lives in + :class:`pcapkit.protocols.internet.mh.LMAAddressCode` rather than in + :mod:`pcapkit.const.mh`, and :attr:`code` carries the raw wire integer. + The address field is nonetheless sized from :attr:`Option.length`, which + is the authoritative width on the wire. + + """ + + #: Option code, c.f., + #: :class:`pcapkit.protocols.internet.mh.LMAAddressCode`. + code: 'int' = UInt8Field() + #: Reserved. + reserved: 'bytes' = PaddingField(length=1) + #: Local mobility anchor address. + address: 'IPv4Address | IPv6Address | bytes' = SwitchField(selector=lma_address_selector) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', code: 'int', + address: 'IPv4Address | IPv6Address | bytes | int | str') -> 'None': ... + + +@schema_final +class MNLLAIIDOption(Option, + code=Enum_Option.Mobile_Node_Link_local_Address_Interface_Identifier_Option): + """Header schema for MH Mobile Node Link-local Address Interface Identifier options.""" + + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + #: Interface identifier. + iid: 'bytes' = BytesField(length=8) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', iid: 'bytes') -> 'None': ... + + +@schema_final +class TransientBindingOption(Option, code=Enum_Option.Transient_Binding): + """Header schema for MH Transient Binding options.""" + + #: Flags. + flags: 'TransientBindingOptionFlags' = BitField(length=1, namespace={ + 'L': (7, 1), + }) + #: Maximum lifetime of the transient state, in units of 100 milliseconds. + lifetime: 'int' = UInt8Field() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + flags: 'TransientBindingOptionFlags', lifetime: 'int') -> 'None': ... + + +@schema_final +class FlowSummaryOption(Option, code=Enum_Option.Flow_Summary_Mobility_Option): + """Header schema for MH Flow Summary mobility options.""" + + #: Flow identifiers being refreshed. + fid: 'list[int]' = ListField( + length=lambda pkt: pkt['length'], + item_type=UInt16Field(), + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', fid: 'list[int]') -> 'None': ... + + +class FlowIdentificationSuboption(EnumSchema[Enum_FlowIDSuboption]): + """Header schema for MH Flow Identification sub-options.""" + + __default__ = lambda: UnassignedFlowIdentificationSuboption + + #: Sub-option type. + type: 'Enum_FlowIDSuboption' = EnumField(length=1, namespace=Enum_FlowIDSuboption) + #: Sub-option length, conditional in case of the one-octet ``Pad`` sub-option. + length: 'int' = ConditionalField( + UInt8Field(default=0), + lambda pkt: pkt['type'] != Enum_FlowIDSuboption.Pad, + ) + + def post_process(self, packet: 'dict[str, Any]') -> 'FlowIdentificationSuboption': + """Revise ``schema`` data after unpacking process. + + Args: + packet: Unpacked data. + + Returns: + Revised schema. + + """ + # the one-octet ``Pad`` sub-option carries no length field at all + if self.type == Enum_FlowIDSuboption.Pad: + self.length = 0 + return self + + +@schema_final +class UnassignedFlowIdentificationSuboption(FlowIdentificationSuboption): + """Header schema for unassigned MH Flow Identification sub-options.""" + + #: Sub-option data. + data: 'bytes' = BytesField(length=lambda pkt: pkt['length']) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_FlowIDSuboption', length: 'int', data: 'bytes') -> 'None': ... + + +@schema_final +class PadFlowIdentificationSuboption(FlowIdentificationSuboption, + code=[Enum_FlowIDSuboption.Pad, + Enum_FlowIDSuboption.PadN]): + """Header schema for MH Flow Identification padding sub-options. + + As with the mobility options themselves, the one-octet ``Pad`` sub-option + carries neither a length field nor any data, whereas ``PadN`` carries both + [:rfc:`6089#section-4.2.1.1`, :rfc:`6089#section-4.2.1.2`]. + + """ + + #: Sub-option data. + data: 'bytes' = PaddingField(length=pad_subopt_data_len) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_FlowIDSuboption', length: 'int') -> 'None': ... + + +@schema_final +class BIDReferenceSuboption(FlowIdentificationSuboption, + code=Enum_FlowIDSuboption.BID_Reference): + """Header schema for MH Flow Identification BID Reference sub-options.""" + + #: Referenced binding identifiers. + bid: 'list[int]' = ListField( + length=lambda pkt: pkt['length'], + item_type=UInt16Field(), + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_FlowIDSuboption', length: 'int', + bid: 'list[int]') -> 'None': ... + + +@schema_final +class TrafficSelectorSuboption(FlowIdentificationSuboption, + code=Enum_FlowIDSuboption.Traffic_Selector): + """Header schema for MH Flow Identification Traffic Selector sub-options. + + Note: + The interior of :attr:`selector` is defined by :rfc:`6088` -- a flag + word followed by whichever of its sixteen range bounds the flags select + -- and belongs to the separate *Traffic Selector Format* registry rather + than to this sub-option. It is therefore carried opaquely here, and + :attr:`ts_format` says which format it is in. + + """ + + #: Traffic selector format. + ts_format: 'Enum_TrafficSelector' = EnumField(length=1, namespace=Enum_TrafficSelector) + #: Reserved. + reserved: 'bytes' = PaddingField(length=1) + #: Traffic selector, in the format named by :attr:`ts_format`. + selector: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 2) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_FlowIDSuboption', length: 'int', + ts_format: 'Enum_TrafficSelector', selector: 'bytes') -> 'None': ... + + +@schema_final +class FlowBindingActionSuboption(FlowIdentificationSuboption, + code=Enum_FlowIDSuboption.Flow_Binding_Action): + """Header schema for MH Flow Identification Flow Binding Action sub-options.""" + + #: Reserved. + reserved: 'bytes' = PaddingField(length=1) + #: Flow binding action. + action: 'Enum_FlowBindingAction' = EnumField(length=1, namespace=Enum_FlowBindingAction) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_FlowIDSuboption', length: 'int', + action: 'Enum_FlowBindingAction') -> 'None': ... + + +@schema_final +class TargetCareofAddressSuboption(FlowIdentificationSuboption, + code=Enum_FlowIDSuboption.Target_Care_of_Address): + """Header schema for MH Flow Identification Target Care-of Address sub-options. + + Note: + As with the Binding Identifier option, the sub-option carries no + address-family flag, so the address is an IPv4 one when the sub-option + length is 6 and an IPv6 one when it is 18 + [:rfc:`7109#section-6.2.2`]. + + """ + + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + #: Target care-of address. + address: 'IPv4Address | IPv6Address | bytes' = SwitchField(selector=target_coa_selector) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_FlowIDSuboption', length: 'int', + address: 'IPv4Address | IPv6Address | bytes | int | str') -> 'None': ... + + +@schema_final +class FlowIdentificationOption(Option, code=Enum_Option.Flow_Identification_Mobility_Option): + """Header schema for MH Flow Identification mobility options.""" + + #: Flow identifier. + fid: 'int' = UInt16Field() + #: Flow priority. + fid_pri: 'int' = UInt16Field() + #: Reserved. + reserved: 'bytes' = PaddingField(length=1) + #: Status. + status: 'Enum_FlowIDStatus' = EnumField(length=1, namespace=Enum_FlowIDStatus) + #: Sub-options. + suboptions: 'list[FlowIdentificationSuboption]' = OptionField( + length=lambda pkt: pkt['length'] - 6, + base_schema=FlowIdentificationSuboption, + type_name='type', + registry=FlowIdentificationSuboption.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', fid: 'int', fid_pri: 'int', + status: 'Enum_FlowIDStatus', + suboptions: 'list[FlowIdentificationSuboption | bytes]') -> 'None': ... + + +@schema_final +class RedirectCapabilityOption(Option, code=Enum_Option.Redirect_Capability_Mobility_Option): + """Header schema for MH Redirect-Capability mobility options.""" + + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int') -> 'None': ... + + +@schema_final +class RedirectOption(Option, code=Enum_Option.Redirect_Mobility_Option): + """Header schema for MH Redirect mobility options. + + Note: + Exactly one of the ``K`` and ``N`` flags is set -- :rfc:`6463#section-4.2` + forbids both being set and both being clear -- so exactly one of + :attr:`ipv6` and :attr:`ipv4` is present, and the option length is + correspondingly 18 or 6. + + """ + + #: Flags. + flags: 'RedirectOptionFlags' = BitField(length=2, namespace={ + 'K': (0, 1), + 'N': (1, 1), + }) + #: IPv6 address of the redirected-to LMA, present when ``K`` is set. + ipv6: 'Optional[IPv6Address]' = ConditionalField( + IPv6AddressField(), + lambda pkt: bool(pkt['flags']['K']), + ) + #: IPv4 address of the redirected-to LMA, present when ``N`` is set. + ipv4: 'Optional[IPv4Address]' = ConditionalField( + IPv4AddressField(), + lambda pkt: bool(pkt['flags']['N']), + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', flags: 'RedirectOptionFlags', + ipv6: 'Optional[IPv6Address | int | bytes | str]', + ipv4: 'Optional[IPv4Address | int | bytes | str]') -> 'None': ... + + +@schema_final +class LoadInformationOption(Option, code=Enum_Option.Load_Information_Mobility_Option): + """Header schema for MH Load Information mobility options.""" + + #: Priority; a lower value is a higher priority. + priority: 'int' = UInt16Field() + #: Mobility sessions currently in use. + sessions_in_use: 'int' = UInt32Field() + #: Maximum number of mobility sessions accepted. + max_sessions: 'int' = UInt32Field() + #: Used capacity, in kilobytes per second. + used_capacity: 'int' = UInt32Field() + #: Maximum capacity, in kilobytes per second. + max_capacity: 'int' = UInt32Field() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', priority: 'int', + sessions_in_use: 'int', max_sessions: 'int', used_capacity: 'int', + max_capacity: 'int') -> 'None': ... + + +@schema_final +class AlternateIPv4CareofAddressOption(Option, code=Enum_Option.Alternate_IPv4_Care_of_Address): + """Header schema for MH Alternate IPv4 Care-of Address options.""" + + #: Alternate IPv4 care-of address. + address: 'IPv4Address' = IPv4AddressField() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + address: 'IPv4Address | int | bytes | str') -> 'None': ... + + +@schema_final +class MNGroupIdentifierOption(Option, code=Enum_Option.Mobile_Node_Group_Identifier): + """Header schema for MH Mobile Node Group Identifier options.""" + + #: Sub-type. + subtype: 'Enum_MNGroupID' = EnumField(length=1, namespace=Enum_MNGroupID) + #: Reserved. + reserved: 'bytes' = PaddingField(length=1) + #: Mobile node group identifier. + group_id: 'int' = UInt32Field() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', subtype: 'Enum_MNGroupID', + group_id: 'int') -> 'None': ... + + +@schema_final +class MAGIPv6AddressOption(Option, code=Enum_Option.MAG_IPv6_Address): + """Header schema for MH MAG IPv6 Address options.""" + + #: Reserved. + reserved: 'bytes' = PaddingField(length=1) + #: Address length, in bits; always 128. + address_length: 'int' = UInt8Field() + #: MAG IPv6 address. + address: 'IPv6Address' = IPv6AddressField() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', address_length: 'int', + address: 'IPv6Address | int | bytes | str') -> 'None': ... + + +class ANISuboption(EnumSchema[Enum_ANISuboption]): + """Header schema for MH Access Network Identifier sub-options.""" + + __default__ = lambda: UnassignedANISuboption + + #: Sub-option type. + type: 'Enum_ANISuboption' = EnumField(length=1, namespace=Enum_ANISuboption) + #: Sub-option length. + length: 'int' = UInt8Field() + + +@schema_final +class UnassignedANISuboption(ANISuboption): + """Header schema for unassigned MH Access Network Identifier sub-options.""" + + #: Sub-option data. + data: 'bytes' = BytesField(length=lambda pkt: pkt['length']) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_ANISuboption', length: 'int', data: 'bytes') -> 'None': ... + + +@schema_final +class ANINetworkIdentifierSuboption(ANISuboption, code=Enum_ANISuboption.Network_Identifier): + """Header schema for MH Network-Identifier ANI sub-options.""" + + #: Flags. + flags: 'ANINetworkIdentifierFlags' = BitField(length=1, namespace={ + 'E': (0, 1), + }) + #: Network name length. + net_name_len: 'int' = UInt8Field() + #: Network name, e.g. an SSID or a PLMN identifier. + net_name: 'bytes' = BytesField(length=lambda pkt: pkt['net_name_len']) + #: Access-point name length. + ap_name_len: 'int' = UInt8Field() + #: Access-point name. + ap_name: 'bytes' = BytesField(length=lambda pkt: pkt['ap_name_len']) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_ANISuboption', length: 'int', + flags: 'ANINetworkIdentifierFlags', net_name_len: 'int', net_name: 'bytes', + ap_name_len: 'int', ap_name: 'bytes') -> 'None': ... + + +@schema_final +class ANIGeoLocationSuboption(ANISuboption, code=Enum_ANISuboption.Geo_Location): + """Header schema for MH Geo-Location ANI sub-options. + + Note: + Both degree fields are 24-bit **two's-complement** fixed-point values + with 9 integer bits [:rfc:`6757#section-3.1.2`]. A + :class:`~pcapkit.corekit.fields.strings.BitField` reads them unsigned, + so the sign is applied when + :meth:`~pcapkit.protocols.internet.mh.MH._read_ani_geo` builds the data + model. + + """ + + #: Latitude and longitude, in the fixed-point format of + #: :rfc:`6757#section-3.1.2`. + location: 'GeoLocationDegrees' = BitField(length=6, namespace={ + 'latitude': (0, 24), + 'longitude': (24, 24), + }) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_ANISuboption', length: 'int', + location: 'GeoLocationDegrees') -> 'None': ... + + +@schema_final +class ANIOperatorIdentifierSuboption(ANISuboption, code=Enum_ANISuboption.Operator_Identifier): + """Header schema for MH Operator-Identifier ANI sub-options.""" + + #: Operator identifier type. + op_id_type: 'Enum_OperatorID' = EnumField(length=1, namespace=Enum_OperatorID) + #: Operator identifier. + identifier: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 1) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_ANISuboption', length: 'int', + op_id_type: 'Enum_OperatorID', identifier: 'bytes') -> 'None': ... + + +@schema_final +class ANICivicLocationSuboption(ANISuboption, code=Enum_ANISuboption.Civic_Location): + """Header schema for MH Civic-Location ANI sub-options.""" + + #: Encoding format; only ``0``, the binary encoding of :rfc:`4776#section-3.1`, + #: is defined. + format: 'int' = UInt8Field() + #: Reserved. + reserved: 'bytes' = PaddingField(length=1) + #: Civic location. + location: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 2) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_ANISuboption', length: 'int', format: 'int', + location: 'bytes') -> 'None': ... + + +@schema_final +class ANIMAGGroupIdentifierSuboption(ANISuboption, code=Enum_ANISuboption.MAG_Group_Identifier): + """Header schema for MH MAG-Group-Identifier ANI sub-options. + + Note: + :rfc:`7563#section-3.2` contradicts itself about this field's width: the + diagram places ``group identifier`` in bits 16 to 31 of the word, and the + prose fixes ``ANI Length`` at ``2``, but the field description then calls + it "a 3-octet unsigned integer value". Two of the three statements agree + on **two** octets and are the ones that constrain the wire, so that is + what is parsed here. No erratum has been filed against the RFC. + + """ + + #: MAG group identifier. + group_id: 'int' = UInt16Field() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_ANISuboption', length: 'int', + group_id: 'int') -> 'None': ... + + +@schema_final +class ANIUpdateTimerSuboption(ANISuboption, code=Enum_ANISuboption.ANI_Update_Timer): + """Header schema for MH ANI Update-Timer sub-options.""" + + #: Update timer. One time unit is 4 seconds; ``0`` asks for an immediate + #: update. + timer: 'int' = UInt16Field() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_ANISuboption', length: 'int', + timer: 'int') -> 'None': ... + + +@schema_final +class AccessNetworkIdentifierOption(Option, code=Enum_Option.Access_Network_Identifier): + """Header schema for MH Access Network Identifier options. + + Note: + Unlike most options that carry sub-options, this one has **no** reserved + octet between its own length and the first sub-option + [:rfc:`6757#section-3`]. + + """ + + #: Sub-options. + suboptions: 'list[ANISuboption]' = OptionField( + length=lambda pkt: pkt['length'], + base_schema=ANISuboption, + type_name='type', + registry=ANISuboption.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + suboptions: 'list[ANISuboption | bytes]') -> 'None': ... + + +@schema_final +class IPv4TrafficOffloadSelectorOption(Option, code=Enum_Option.IPv4_Traffic_Offload_Selector): + """Header schema for MH IPv4 Traffic Offload Selector options.""" + + #: Flags. + flags: 'ModeFlagOnly' = BitField(length=4, namespace={ + 'M': (0, 1), + }) + #: Traffic selector, carried as the traffic selector sub-option of + #: :rfc:`6089#section-4.2.1.4`. Optional in a proxy binding update and + #: mandatory in a proxy binding acknowledgement, hence a list. + selector: 'list[FlowIdentificationSuboption]' = OptionField( + length=lambda pkt: pkt['length'] - 4, + base_schema=FlowIdentificationSuboption, + type_name='type', + registry=FlowIdentificationSuboption.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', flags: 'ModeFlagOnly', + selector: 'list[FlowIdentificationSuboption | bytes]') -> 'None': ... + + +@schema_final +class DynamicIPMulticastSelectorOption(Option, code=Enum_Option.Dynamic_IP_Multicast_Selector): + """Header schema for MH Dynamic IP Multicast Selector options. + + Note: + The multicast address records are :rfc:`3810#section-5.2` MLD or + :rfc:`3376#section-4.2` IGMP structures, which belong to those protocols + rather than to the Mobility Header, so they are carried opaquely here. + :attr:`protocol` says which of the two they are in -- ``143`` for MLDv2 + and ``131`` for MLDv1. + + """ + + #: MLD or IGMP protocol number identifying the record format. + protocol: 'int' = UInt8Field() + #: Flags. + flags: 'ModeFlagOnly' = BitField(length=1, namespace={ + 'M': (0, 1), + }) + #: Number of multicast address records. + records: 'int' = UInt16Field() + #: Multicast address records. + data: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 4) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', protocol: 'int', + flags: 'ModeFlagOnly', records: 'int', data: 'bytes') -> 'None': ... + + +@schema_final +class DelegatedMNPOption(Option, code=Enum_Option.Delegated_Mobile_Network_Prefix): + """Header schema for MH Delegated Mobile Network Prefix options.""" + + #: Flags. + flags: 'DelegatedMNPOptionFlags' = BitField(length=1, namespace={ + 'V': (0, 1), + }) + #: Prefix length. + prefix_length: 'int' = UInt8Field() + #: Delegated mobile network prefix, an IPv4 one when ``V`` is set. + prefix: 'IPv4Address | IPv6Address' = SwitchField(selector=dmnp_prefix_selector) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + flags: 'DelegatedMNPOptionFlags', prefix_length: 'int', + prefix: 'IPv4Address | IPv6Address | int | bytes | str') -> 'None': ... + + +@schema_final +class ActiveMulticastSubscriptionIPv4Option( + Option, code=Enum_Option.Active_Multicast_Subscription_IPv4): + """Header schema for MH Active Multicast Subscription IPv4 options. + + Note: + The membership context is an IGMP group address or group record + [:rfc:`1112`, :rfc:`2236`, :rfc:`3376#section-4.2`], which belongs to + IGMP rather than to the Mobility Header, so it is carried opaquely here. + + """ + + #: IGMP message type identifying the context format: ``0x12`` for IGMPv1, + #: ``0x16`` for IGMPv2 and ``0x22`` for IGMPv3. + igmp_type: 'int' = UInt8Field() + #: Multicast membership context. + context: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 1) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', igmp_type: 'int', + context: 'bytes') -> 'None': ... + + +@schema_final +class ActiveMulticastSubscriptionIPv6Option( + Option, code=Enum_Option.Active_Multicast_Subscription_IPv6): + """Header schema for MH Active Multicast Subscription IPv6 options. + + Note: + The membership context is an :rfc:`3810#section-5.2` multicast address + record for MLDv2, or a reserved word followed by a multicast address for + MLDv1 [:rfc:`2710`]. Both belong to MLD rather than to the Mobility + Header, so the context is carried opaquely here. + + """ + + #: MLD message type identifying the context format: ``143`` for MLDv2 and + #: ``131`` for MLDv1. + mld_type: 'int' = UInt8Field() + #: Multicast membership context. + context: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 1) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', mld_type: 'int', + context: 'bytes') -> 'None': ... + + +class QoSAttribute(EnumSchema[Enum_QoSAttribute]): + """Header schema for MH Quality-of-Service attributes.""" + + __default__ = lambda: UnassignedQoSAttribute + + #: Attribute type. + type: 'Enum_QoSAttribute' = EnumField(length=1, namespace=Enum_QoSAttribute) + #: Attribute length. + length: 'int' = UInt8Field() + + +@schema_final +class UnassignedQoSAttribute(QoSAttribute): + """Header schema for unassigned MH Quality-of-Service attributes.""" + + #: Attribute value. + data: 'bytes' = BytesField(length=lambda pkt: pkt['length']) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_QoSAttribute', length: 'int', data: 'bytes') -> 'None': ... + + +@schema_final +class BitRateAttribute(QoSAttribute, code=[Enum_QoSAttribute.Per_MN_Agg_Max_DL_Bit_Rate, + Enum_QoSAttribute.Per_MN_Agg_Max_UL_Bit_Rate, + Enum_QoSAttribute.Aggregate_Max_DL_Bit_Rate, + Enum_QoSAttribute.Aggregate_Max_UL_Bit_Rate, + Enum_QoSAttribute.Guaranteed_DL_Bit_Rate, + Enum_QoSAttribute.Guaranteed_UL_Bit_Rate]): + """Header schema for the MH Quality-of-Service attributes that carry a bare + bit rate. + + Six of the eleven registered attributes share this shape -- two reserved + octets and a 32-bit rate [:rfc:`7222#section-4.2.1`, + :rfc:`7222#section-4.2.2`, :rfc:`7222#section-4.2.6` to + :rfc:`7222#section-4.2.9`] -- so one schema is registered against all six. + + Note: + The rate is in **bits** per second, not kilobits + [:rfc:`7222#section-4.2.1`]. + + """ + + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + #: Bit rate, in bits per second. + rate: 'int' = UInt32Field() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_QoSAttribute', length: 'int', rate: 'int') -> 'None': ... + + +@schema_final +class PerSessionBitRateAttribute( + QoSAttribute, code=[Enum_QoSAttribute.Per_Session_Agg_Max_DL_Bit_Rate, + Enum_QoSAttribute.Per_Session_Agg_Max_UL_Bit_Rate]): + """Header schema for the MH per-session aggregate maximum bit-rate + Quality-of-Service attributes [:rfc:`7222#section-4.2.3`, + :rfc:`7222#section-4.2.4`].""" + + #: Flags. + flags: 'SessionBitRateFlags' = BitField(length=2, namespace={ + 'S': (0, 1), + 'E': (1, 1), + }) + #: Bit rate, in bits per second. + rate: 'int' = UInt32Field() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_QoSAttribute', length: 'int', + flags: 'SessionBitRateFlags', rate: 'int') -> 'None': ... + + +@schema_final +class AllocationRetentionPriorityAttribute( + QoSAttribute, code=Enum_QoSAttribute.Allocation_Retention_Priority): + """Header schema for the MH Allocation-Retention-Priority Quality-of-Service + attribute [:rfc:`7222#section-4.2.5`].""" + + #: Reserved. + reserved: 'bytes' = PaddingField(length=1) + #: Priority level, pre-emption capability and pre-emption vulnerability. + priority: 'AllocationRetentionPriorityFields' = BitField(length=1, namespace={ + 'PL': (0, 4), + 'PC': (4, 2), + 'PV': (6, 2), + }) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_QoSAttribute', length: 'int', + priority: 'AllocationRetentionPriorityFields') -> 'None': ... + + +@schema_final +class QoSTrafficSelectorAttribute(QoSAttribute, code=Enum_QoSAttribute.QoS_Traffic_Selector): + """Header schema for the MH QoS-Traffic-Selector Quality-of-Service attribute. + + Note: + The reserved octet comes **before** the traffic selector format here, + which is the other way round from the traffic selector sub-option of + :rfc:`6089#section-4.2.1.4` [:rfc:`7222#section-4.2.10`]. The two are + otherwise the same shape, which makes them easy to confuse. + + """ + + #: Reserved. + reserved: 'bytes' = PaddingField(length=1) + #: Traffic selector format. + ts_format: 'Enum_TrafficSelector' = EnumField(length=1, namespace=Enum_TrafficSelector) + #: Traffic selector, in the format named by :attr:`ts_format`. + selector: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 2) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_QoSAttribute', length: 'int', + ts_format: 'Enum_TrafficSelector', selector: 'bytes') -> 'None': ... + + +@schema_final +class QoSVendorSpecificAttribute( + QoSAttribute, code=Enum_QoSAttribute.QoS_Vendor_Specific_Attribute): + """Header schema for the MH QoS-Vendor-Specific Quality-of-Service attribute + [:rfc:`7222#section-4.2.11`].""" + + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + #: Vendor ID, an SMI Network Management Private Enterprise Number. + vendor: 'int' = UInt32Field() + #: Vendor-administered sub-type. + subtype: 'int' = UInt8Field() + #: Vendor-specific data. + data: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 7) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_QoSAttribute', length: 'int', vendor: 'int', + subtype: 'int', data: 'bytes') -> 'None': ... + + +@schema_final +class QualityOfServiceOption(Option, code=Enum_Option.Quality_of_Service): + """Header schema for MH Quality-of-Service options.""" + + #: Service request identifier. + sr_id: 'int' = UInt8Field() + #: Traffic class, holding a 6-bit DSCP and 2 reserved bits. + tc: 'int' = UInt8Field() + #: Operational code. + oc: 'int' = UInt8Field() + #: Reserved. + reserved: 'bytes' = PaddingField(length=3) + #: Quality-of-service attributes. + attributes: 'list[QoSAttribute]' = OptionField( + length=lambda pkt: pkt['length'] - 6, + base_schema=QoSAttribute, + type_name='type', + registry=QoSAttribute.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', sr_id: 'int', tc: 'int', + oc: 'int', attributes: 'list[QoSAttribute | bytes]') -> 'None': ... + + +@schema_final +class LMAUserPlaneAddressOption(Option, code=Enum_Option.LMA_User_Plane_Address): + """Header schema for MH LMA User-Plane Address options.""" + + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + #: LMA user-plane address, absent in a proxy binding update. + address: 'IPv4Address | IPv6Address | bytes' = SwitchField(selector=lma_user_plane_selector) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + address: 'IPv4Address | IPv6Address | bytes | int | str') -> 'None': ... + + +@schema_final +class MulticastMobilityOption(Option, code=Enum_Option.Multicast_Mobility_Option): + """Header schema for MH Multicast Mobility options. + + Note: + This option's length field is **not** the usual octet count. + :rfc:`7411#section-5.3` measures it in 32-bit *words* and excludes the + option code and reserved octets as well as the type and length ones, so + the whole option occupies ``4 + length * 4`` octets. The report payload + itself is an MLD or IGMP structure belonging to those protocols, so it is + carried opaquely. + + """ + + #: Option code: ``1`` for IGMPv3, ``2`` for MLDv2, ``3`` for IGMPv3 in + #: IGMPv2 compatibility mode and ``4`` for MLDv2 in MLDv1 compatibility mode. + code: 'int' = UInt8Field() + #: Reserved. + reserved: 'bytes' = PaddingField(length=1) + #: MLD or IGMP report payload. + data: 'bytes' = BytesField(length=lambda pkt: pkt['length'] * 4) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', code: 'int', + data: 'bytes') -> 'None': ... + + +@schema_final +class MulticastAcknowledgementOption(Option, code=Enum_Option.Multicast_Acknowledgement_Option): + """Header schema for MH Multicast Acknowledgement options. + + Note: + As with :class:`MulticastMobilityOption`, the length field counts 32-bit + words rather than octets and excludes the option code and status octets + [:rfc:`7411#section-5.4`]. + + """ + + #: Option code; always ``0``. + code: 'int' = UInt8Field() + #: Status: ``1`` report payload type unsupported, ``2`` requested group + #: service unsupported, ``3`` requested group service administratively + #: prohibited. + status: 'int' = UInt8Field() + #: MLD or IGMP unsupported report payload. + data: 'bytes' = BytesField(length=lambda pkt: pkt['length'] * 4) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', code: 'int', status: 'int', + data: 'bytes') -> 'None': ... + + +class LMAControlledMAGSuboption(EnumSchema[Enum_LMAControlledMAGSuboption]): + """Header schema for MH LMA-Controlled MAG Parameters sub-options.""" + + __default__ = lambda: UnassignedLMAControlledMAGSuboption + + #: Sub-option type. + type: 'Enum_LMAControlledMAGSuboption' = EnumField( + length=1, namespace=Enum_LMAControlledMAGSuboption) + #: Sub-option length. + length: 'int' = UInt8Field() + + +@schema_final +class UnassignedLMAControlledMAGSuboption(LMAControlledMAGSuboption): + """Header schema for unassigned MH LMA-Controlled MAG Parameters sub-options.""" + + #: Sub-option data. + data: 'bytes' = BytesField(length=lambda pkt: pkt['length']) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_LMAControlledMAGSuboption', length: 'int', + data: 'bytes') -> 'None': ... + + +@schema_final +class BindingReregistrationControlSuboption( + LMAControlledMAGSuboption, + code=Enum_LMAControlledMAGSuboption.Binding_Re_registration_Control): + """Header schema for MH Binding Re-registration Control sub-options + [:rfc:`8127#section-3.1.1`].""" + + #: Time before binding expiry at which to re-register. One time unit is + #: 4 seconds. + start_time: 'int' = UInt16Field() + #: Minimum delay before the first retransmission, in seconds. + initial_retransmission: 'int' = UInt16Field() + #: Maximum delay before the last retransmission, in seconds. + max_retransmission: 'int' = UInt16Field() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_LMAControlledMAGSuboption', length: 'int', + start_time: 'int', initial_retransmission: 'int', + max_retransmission: 'int') -> 'None': ... + + +@schema_final +class HeartbeatControlSuboption(LMAControlledMAGSuboption, + code=Enum_LMAControlledMAGSuboption.Heartbeat_Control): + """Header schema for MH Heartbeat Control sub-options [:rfc:`8127#section-3.1.2`].""" + + #: Delay after a successful heartbeat exchange, in seconds. + interval: 'int' = UInt16Field() + #: Minimum delay before a heartbeat retransmission, in seconds. + retransmission_delay: 'int' = UInt16Field() + #: Maximum number of heartbeat retransmissions. + max_retransmissions: 'int' = UInt16Field() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_LMAControlledMAGSuboption', length: 'int', + interval: 'int', retransmission_delay: 'int', + max_retransmissions: 'int') -> 'None': ... + + +@schema_final +class LMAControlledMAGParametersOption(Option, code=Enum_Option.LMA_Controlled_MAG_Parameters): + """Header schema for MH LMA-Controlled MAG Parameters options. + + Note: + The option is registered to :rfc:`8127`, not to :rfc:`7864` -- the latter + defines the flow-mobility options and the ``FLOW-MOBILITY`` update + notification reason instead. + + """ + + #: Sub-options. + suboptions: 'list[LMAControlledMAGSuboption]' = OptionField( + length=lambda pkt: pkt['length'], + base_schema=LMAControlledMAGSuboption, + type_name='type', + registry=LMAControlledMAGSuboption.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + suboptions: 'list[LMAControlledMAGSuboption | bytes]') -> 'None': ... + + +@schema_final +class MAGMultipathBindingOption(Option, code=Enum_Option.MAG_Multipath_Binding): + """Header schema for MH MAG Multipath Binding options.""" + + #: Interface access-technology type. + att: 'Enum_AccessType' = EnumField(length=1, namespace=Enum_AccessType) + #: Interface label. + label: 'int' = UInt8Field() + #: Binding identifier; ``0`` and ``255`` are reserved. + bid: 'int' = UInt8Field() + #: Flags. + flags: 'MAGMultipathBindingOptionFlags' = BitField(length=3, namespace={ + 'B': (0, 1), + 'O': (1, 1), + }) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', att: 'Enum_AccessType', + label: 'int', bid: 'int', + flags: 'MAGMultipathBindingOptionFlags') -> 'None': ... + + +@schema_final +class MAGIdentifierOption(Option, code=Enum_Option.MAG_Identifier): + """Header schema for MH MAG Identifier options.""" + + #: Sub-type, drawn from the mobile node identifier subtype registry. + subtype: 'Enum_MNIDSubtype' = EnumField(length=1, namespace=Enum_MNIDSubtype) + #: Reserved. + reserved: 'bytes' = PaddingField(length=1) + #: Identifier, in the form named by :attr:`subtype`. + identifier: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 2) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', subtype: 'Enum_MNIDSubtype', + identifier: 'bytes') -> 'None': ... + + +@schema_final +class AnchoredPrefixOption(Option, code=Enum_Option.Anchored_Prefix): + """Header schema for MH Anchored Prefix options [:rfc:`8885#section-4.3`].""" + + #: Reserved. + reserved: 'bytes' = PaddingField(length=1) + #: Prefix length. + prefix_length: 'int' = UInt8Field() + #: Anchored prefix. + prefix: 'IPv6Address' = IPv6AddressField() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', prefix_length: 'int', + prefix: 'IPv6Address | int | bytes | str') -> 'None': ... + + +@schema_final +class LocalPrefixOption(Option, code=Enum_Option.Local_Prefix): + """Header schema for MH Local Prefix options [:rfc:`8885#section-4.4`].""" + + #: Reserved. + reserved: 'bytes' = PaddingField(length=1) + #: Prefix length. + prefix_length: 'int' = UInt8Field() + #: Local prefix. + prefix: 'IPv6Address' = IPv6AddressField() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', prefix_length: 'int', + prefix: 'IPv6Address | int | bytes | str') -> 'None': ... + + +@schema_final +class PreviousMAAROption(Option, code=Enum_Option.Previous_MAAR): + """Header schema for MH Previous MAAR options [:rfc:`8885#section-4.5`].""" + + #: Reserved. + reserved: 'bytes' = PaddingField(length=1) + #: Prefix length of :attr:`prefix`. + prefix_length: 'int' = UInt8Field() + #: Previous MAAR's global address. + maar: 'IPv6Address' = IPv6AddressField() + #: Mobile node's home network prefix. + prefix: 'IPv6Address' = IPv6AddressField() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', prefix_length: 'int', + maar: 'IPv6Address | int | bytes | str', + prefix: 'IPv6Address | int | bytes | str') -> 'None': ... + + +@schema_final +class ServingMAAROption(Option, code=Enum_Option.Serving_MAAR): + """Header schema for MH Serving MAAR options [:rfc:`8885#section-4.6`].""" + + #: Serving MAAR's global address. + address: 'IPv6Address' = IPv6AddressField() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + address: 'IPv6Address | int | bytes | str') -> 'None': ... + + +@schema_final +class DLIFLinkLocalAddressOption(Option, code=Enum_Option.DLIF_Link_Local_Address): + """Header schema for MH DLIF Link-Local Address options [:rfc:`8885#section-4.7`].""" + + #: Distributed logical interface's link-local address. + address: 'IPv6Address' = IPv6AddressField() + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', + address: 'IPv6Address | int | bytes | str') -> 'None': ... + + +@schema_final +class DLIFLinkLayerAddressOption(Option, code=Enum_Option.DLIF_Link_Layer_Address): + """Header schema for MH DLIF Link-Layer Address options [:rfc:`8885#section-4.8`].""" + + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + #: Distributed logical interface's link-layer address, encoded as in + #: :rfc:`4861#section-4.6.2` and therefore link-type dependent in width. + lla: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 2) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', lla: 'bytes') -> 'None': ... + + +# TODO: Implement other options. + + +class Packet(EnumSchema[Enum_Packet]): + """Header schema for MH packet data.""" + + __default__ = lambda: UnknownMessage + + +@schema_final +class UnknownMessage(Packet): + """Header schema for MH unknown message type.""" + + #: Message data. + data: 'bytes' = BytesField(length=lambda pkt: pkt['__length__']) + + if TYPE_CHECKING: + def __init__(self, data: 'bytes') -> 'None': ... + + +@schema_final +class BindingRefreshRequestMessage(Packet, code=Enum_Packet.Binding_Refresh_Request): + """Header schema for MH Binding Refresh Request (BRR) message.""" + + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + #: Mobility options. + options: 'list[Option]' = OptionField( + length=lambda pkt: pkt['__length__'], + base_schema=Option, + type_name='type', + registry=Option.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, options: 'list[Option | bytes]') -> 'None': ... + + +@schema_final +class HomeTestInitMessage(Packet, code=Enum_Packet.Home_Test_Init): + """Header schema for MH Home Test Init (HoTI) message.""" + + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + #: Home init cookie. + cookie: 'bytes' = BytesField(length=8) + #: Mobility options. + options: 'list[Option]' = OptionField( + length=lambda pkt: pkt['__length__'], + base_schema=Option, + type_name='type', + registry=Option.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, cookie: 'bytes', options: 'list[Option | bytes]') -> 'None': ... + + +@schema_final +class CareofTestInitMessage(Packet, code=Enum_Packet.Care_of_Test_Init): + """Header schema for MH Care-of Test Init (CoTI) messages.""" + + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + #: Care-of init cookie. + cookie: 'bytes' = BytesField(length=8) + #: Mobility options. + options: 'list[Option]' = OptionField( + length=lambda pkt: pkt['__length__'], + base_schema=Option, + type_name='type', + registry=Option.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, cookie: 'bytes', options: 'list[Option | bytes]') -> 'None': ... + + +@schema_final +class HomeTestMessage(Packet, code=Enum_Packet.Home_Test): + """Header schema for MH Home Test (HoT) message.""" + + #: Home nonce index. + nonce_index: 'int' = UInt16Field() + #: Home init cookie. + cookie: 'bytes' = BytesField(length=8) + #: Home keygen token. + token: 'bytes' = BytesField(length=8) + #: Mobility options. + options: 'list[Option]' = OptionField( + length=lambda pkt: pkt['__length__'], + base_schema=Option, + type_name='type', + registry=Option.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, nonce_index: 'int', cookie: 'bytes', token: 'bytes', + options: 'list[Option | bytes]') -> 'None': ... + + +@schema_final +class CareofTestMessage(Packet, code=Enum_Packet.Care_of_Test): + """Header schema for MH Care-of Test (CoT) message.""" + + #: Care-of nonce index. + nonce_index: 'int' = UInt16Field() + #: Care-of init cookie. + cookie: 'bytes' = BytesField(length=8) + #: Care-of keygen token. + token: 'bytes' = BytesField(length=8) + #: Mobility options. + options: 'list[Option]' = OptionField( + length=lambda pkt: pkt['__length__'], + base_schema=Option, + type_name='type', + registry=Option.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, nonce_index: 'int', cookie: 'bytes', token: 'bytes', + options: 'list[Option | bytes]') -> 'None': ... + + +@schema_final +class BindingUpdateMessage(Packet, code=Enum_Packet.Binding_Update): + """Header schema for MH Binding Update (BU) messages.""" #: Sequence number. seq: 'int' = UInt16Field() @@ -1014,4 +2819,318 @@ def __init__(self, seq: 'int', flags: 'HandoverAcknowledgeMessageFlags', code: 'Enum_HandoverACKStatus', options: 'list[Option | bytes]') -> 'None': ... +@schema_final +class HeartbeatMessage(Packet, code=Enum_Packet.Heartbeat_Message): + """Header schema for MH Heartbeat messages.""" + + #: Flags. + flags: 'HeartbeatMessageFlags' = BitField(length=2, namespace={ + 'U': (14, 1), + 'R': (15, 1), + }) + #: Sequence number. + seq: 'int' = UInt32Field() + #: Mobility options. + options: 'list[Option]' = OptionField( + length=lambda pkt: pkt['__length__'], + base_schema=Option, + type_name='type', + registry=Option.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, flags: 'HeartbeatMessageFlags', seq: 'int', + options: 'list[Option | bytes]') -> 'None': ... + + +@schema_final +class HomeAgentSwitchMessage(Packet, code=Enum_Packet.Home_Agent_Switch_Message): + """Header schema for MH Home Agent Switch messages.""" + + #: Number of home agent addresses that follow. + count: 'int' = UInt8Field() + #: Reserved. + reserved: 'bytes' = PaddingField(length=1) + #: Home agent addresses. + addresses: 'list[IPv6Address]' = ListField( + length=lambda pkt: pkt['count'] * 16, + item_type=IPv6AddressField(), + ) + #: Mobility options. + options: 'list[Option]' = OptionField( + length=lambda pkt: pkt['__length__'], + base_schema=Option, + type_name='type', + registry=Option.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, count: 'int', addresses: 'list[IPv6Address | int | bytes | str]', + options: 'list[Option | bytes]') -> 'None': ... + + +@schema_final +class BindingRevocationMessage(Packet, code=Enum_Packet.Binding_Revocation_Message): + """Header schema for MH Binding Revocation messages. + + The one message type carries two forms, told apart by :attr:`br_type` rather + than by the Mobility Header type: a Binding Revocation Indication + [:rfc:`5846#section-5.1`] and a Binding Revocation Acknowledgement + [:rfc:`5846#section-5.2`]. Both have the same geometry, and the only field + that differs is the octet after :attr:`br_type` -- a revocation trigger in + the former and a status code in the latter -- so :attr:`code` selects its + enumeration from :attr:`br_type`. + + """ + + #: Binding revocation type, saying which form this is. + br_type: 'Enum_BindingRevocation' = EnumField(length=1, namespace=Enum_BindingRevocation) + #: Revocation trigger, or acknowledgement status. + code: 'Enum_RevocationTrigger | Enum_RevocationStatusCode' = SwitchField( + selector=br_code_selector) + #: Sequence number. + seq: 'int' = UInt16Field() + #: Flags. + flags: 'BindingRevocationMessageFlags' = BitField(length=2, namespace={ + 'P': (0, 1), + 'V': (1, 1), + 'G': (2, 1), + }) + #: Mobility options. + options: 'list[Option]' = OptionField( + length=lambda pkt: pkt['__length__'], + base_schema=Option, + type_name='type', + registry=Option.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, br_type: 'Enum_BindingRevocation', + code: 'Enum_RevocationTrigger | Enum_RevocationStatusCode | int', + seq: 'int', flags: 'BindingRevocationMessageFlags', + options: 'list[Option | bytes]') -> 'None': ... + + +@schema_final +class LocalizedRoutingInitiationMessage(Packet, code=Enum_Packet.Localized_Routing_Initiation): + """Header schema for MH Localized Routing Initiation messages.""" + + #: Sequence number. + seq: 'int' = UInt16Field() + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + #: Lifetime, **in seconds** rather than in the units of 4 seconds that the + #: binding messages use. ``0xFFFF`` is an infinite lifetime and ``0`` asks + #: for localized routing to stop [:rfc:`6705#section-10.1`]. + lifetime: 'int' = UInt16Field() + #: Mobility options. + options: 'list[Option]' = OptionField( + length=lambda pkt: pkt['__length__'], + base_schema=Option, + type_name='type', + registry=Option.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, seq: 'int', lifetime: 'int', + options: 'list[Option | bytes]') -> 'None': ... + + +@schema_final +class LocalizedRoutingAcknowledgmentMessage( + Packet, code=Enum_Packet.Localized_Routing_Acknowledgment): + """Header schema for MH Localized Routing Acknowledgment messages. + + Note: + :rfc:`6705#section-10.2` defines this message's three status values + inline and IANA keeps no registry for them -- they are neither in the + general *Status Codes* registry nor in one of their own -- so their + enumeration lives in + :class:`pcapkit.protocols.internet.mh.LocalizedRoutingStatus` rather than + in :mod:`pcapkit.const.mh`. Being module-local, it cannot be imported + here without a circular import, so :attr:`status` carries the raw wire + integer. + + """ + + #: Sequence number. + seq: 'int' = UInt16Field() + #: Flags. + flags: 'LocalizedRoutingAcknowledgmentMessageFlags' = BitField(length=1, namespace={ + 'U': (0, 1), + }) + #: Status, c.f., + #: :class:`pcapkit.protocols.internet.mh.LocalizedRoutingStatus`. + status: 'int' = UInt8Field() + #: Lifetime, in seconds. + lifetime: 'int' = UInt16Field() + #: Mobility options. + options: 'list[Option]' = OptionField( + length=lambda pkt: pkt['__length__'], + base_schema=Option, + type_name='type', + registry=Option.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, seq: 'int', flags: 'LocalizedRoutingAcknowledgmentMessageFlags', + status: 'int', lifetime: 'int', + options: 'list[Option | bytes]') -> 'None': ... + + +@schema_final +class UpdateNotificationMessage(Packet, code=Enum_Packet.Update_Notification): + """Header schema for MH Update Notification messages.""" + + #: Sequence number. + seq: 'int' = UInt16Field() + #: Notification reason. + reason: 'Enum_UpdateNotificationReason' = EnumField( + length=2, namespace=Enum_UpdateNotificationReason) + #: Flags. + flags: 'UpdateNotificationMessageFlags' = BitField(length=2, namespace={ + 'A': (0, 1), + 'D': (1, 1), + }) + #: Mobility options. + options: 'list[Option]' = OptionField( + length=lambda pkt: pkt['__length__'], + base_schema=Option, + type_name='type', + registry=Option.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, seq: 'int', reason: 'Enum_UpdateNotificationReason', + flags: 'UpdateNotificationMessageFlags', + options: 'list[Option | bytes]') -> 'None': ... + + +@schema_final +class UpdateNotificationAcknowledgementMessage( + Packet, code=Enum_Packet.Update_Notification_Acknowledgement): + """Header schema for MH Update Notification Acknowledgement messages.""" + + #: Sequence number. + seq: 'int' = UInt16Field() + #: Status. + status: 'Enum_UpdateNotificationACKStatus' = EnumField( + length=1, namespace=Enum_UpdateNotificationACKStatus) + #: Reserved. + reserved: 'bytes' = PaddingField(length=3) + #: Mobility options. + options: 'list[Option]' = OptionField( + length=lambda pkt: pkt['__length__'], + base_schema=Option, + type_name='type', + registry=Option.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, seq: 'int', status: 'Enum_UpdateNotificationACKStatus', + options: 'list[Option | bytes]') -> 'None': ... + + +@schema_final +class FlowBindingMessage(Packet, code=Enum_Packet.Flow_Binding_Message): + """Header schema for MH Flow Binding messages. + + As with :class:`BindingRevocationMessage`, the one message type carries two + forms told apart by a field inside the message rather than by the Mobility + Header type: a Flow Binding Indication [:rfc:`7109#section-6.1.1`] and a Flow + Binding Acknowledgement [:rfc:`7109#section-6.1.2`], selected by + :attr:`fb_type`. + + Note: + The octet after :attr:`code` holds the ``A`` flag in an indication and is + wholly reserved in an acknowledgement, so :attr:`flags` reads as all-zero + for a well-formed acknowledgement. + + """ + + #: Flow binding type, saying which form this is. + fb_type: 'Enum_FlowBindingType' = EnumField(length=2, namespace=Enum_FlowBindingType) + #: Sequence number. + seq: 'int' = UInt16Field() + #: Indication trigger, or acknowledgement status. + code: 'Enum_FlowBindingIndicationTrigger | Enum_FlowBindingACKStatus' = SwitchField( + selector=fb_code_selector) + #: Flags. + flags: 'FlowBindingMessageFlags' = BitField(length=1, namespace={ + 'A': (0, 1), + }) + #: Mobility options. + options: 'list[Option]' = OptionField( + length=lambda pkt: pkt['__length__'], + base_schema=Option, + type_name='type', + registry=Option.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, fb_type: 'Enum_FlowBindingType', seq: 'int', + code: 'Enum_FlowBindingIndicationTrigger | Enum_FlowBindingACKStatus | int', + flags: 'FlowBindingMessageFlags', + options: 'list[Option | bytes]') -> 'None': ... + + +@schema_final +class SubscriptionQueryMessage(Packet, code=Enum_Packet.Subscription_Query): + """Header schema for MH Subscription Query messages. + + Note: + The sequence number is a single octet here, unlike the two-octet one of + most other mobility messages [:rfc:`7161#section-4.3.1.2`]. + + """ + + #: Sequence number, counted modulo 256. + seq: 'int' = UInt8Field() + #: Reserved. + reserved: 'bytes' = PaddingField(length=1) + #: Mobility options. + options: 'list[Option]' = OptionField( + length=lambda pkt: pkt['__length__'], + base_schema=Option, + type_name='type', + registry=Option.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, seq: 'int', options: 'list[Option | bytes]') -> 'None': ... + + +@schema_final +class SubscriptionResponseMessage(Packet, code=Enum_Packet.Subscription_Response): + """Header schema for MH Subscription Response messages.""" + + #: Sequence number, echoed from the query. + seq: 'int' = UInt8Field() + #: Flags. + flags: 'SubscriptionResponseMessageFlags' = BitField(length=1, namespace={ + 'I': (0, 1), + }) + #: Mobility options. + options: 'list[Option]' = OptionField( + length=lambda pkt: pkt['__length__'], + base_schema=Option, + type_name='type', + registry=Option.registry, + eool=None, + ) + + if TYPE_CHECKING: + def __init__(self, seq: 'int', flags: 'SubscriptionResponseMessageFlags', + options: 'list[Option | bytes]') -> 'None': ... + + # TODO: Implement other message types. diff --git a/tests/protocols/internet/test_mh_unit.py b/tests/protocols/internet/test_mh_unit.py index 989198e7bc..67e2baa4d1 100644 --- a/tests/protocols/internet/test_mh_unit.py +++ b/tests/protocols/internet/test_mh_unit.py @@ -124,9 +124,13 @@ def parse(hexstr: str) -> None: # checksum, then ten octets of message body nothing can interpret ('message', MH.register_message, Packet(200), MH.__dict__['__message__'], lambda: parse('1101' 'c8' '00' '1234' '00000000000000000000')), - # a Binding Refresh Request carrying one 8-octet option of type 0x40 - ('option', MH.register_option, Option(0x40), MH.__dict__['__option__'], - lambda: parse('1101' '00' '00' '1234' '0000' '4006' '000000000000')), + # a Binding Refresh Request carrying one 8-octet option of type 0xC8. + # The code has to be outside the registry itself, not merely one this + # module happens not to handle: every one of the 71 registered option + # types now has a dispatch entry, so a registered code would already be + # in the table and prove nothing about insertion on a miss. + ('option', MH.register_option, Option(0xC8), MH.__dict__['__option__'], + lambda: parse('1101' '00' '00' '1234' '0000' 'c806' '000000000000')), ('extension', MH.register_extension, CGAExtension(0xFF), MH.__dict__['__extension__'], lambda: object.__new__(MH)._make_cga_extensions( @@ -1703,6 +1707,646 @@ def test_mh_opaque_message_body_cannot_be_padded(self) -> None: chksum=b'\x00\x00', data={'options': []})) self.assertEqual(len(raw) % 8, 0) + def test_mh_every_registered_code_has_both_handlers(self) -> None: + """Every registry entry must name a parser *and* a constructor. + + The dispatch tables map a code to a bare method-name stem, and a stem + naming a method that does not exist falls back to the generic handler + silently -- no import fails, no test fails, the option simply stops being + decoded. So the mapping is checked against the class rather than trusted. + """ + from pcapkit.const.mh.cga_extension import CGAExtension + from pcapkit.const.mh.option import Option + from pcapkit.const.mh.packet import Packet + from pcapkit.protocols.internet.mh import MH + + for enum, registry, read_pre, make_pre in ( + (Packet, '__message__', '_read_msg_', '_make_msg_'), + (Option, '__option__', '_read_opt_', '_make_opt_'), + (CGAExtension, '__extension__', '_read_ext_', '_make_ext_'), + ): + table = MH.__dict__[registry] + # every registered code, not just the ones the table happens to hold + self.assertEqual(sorted(table), sorted(enum), + f'{registry} does not cover its whole registry') + for member in enum: + stem = table[member] + with self.subTest(registry=registry, code=member.name): + self.assertIsInstance(stem, str) + self.assertTrue(hasattr(MH, f'{read_pre}{stem}'), + f'{read_pre}{stem} is missing') + self.assertTrue(hasattr(MH, f'{make_pre}{stem}'), + f'{make_pre}{stem} is missing') + + def test_mh_pmipv6_timestamp_is_not_an_ntp_timestamp(self) -> None: + """:rfc:`5213#section-8.8` is not :rfc:`1305`, and must not be read as it. + + The two are both 64-bit timestamps carried in a mobility option, which is + exactly why they are easy to conflate. They agree on nothing else: the + replay-protection option counts from 1900 in a 32/32 split, and the + timestamp option counts from 1970 in a 48/16 one. + """ + import datetime as dt + + from pcapkit.const.mh.option import Option + from pcapkit.protocols.internet.mh import MH + + proto = object.__new__(MH) + + # 1 January 2000 00:00:00 UTC, exactly, with no fractional part + epoch_2000 = 946_684_800 + schema = proto._make_opt_timestamp( # type: ignore[arg-type] + Option.Timestamp_Option, seconds=epoch_2000, fraction=0) + self.assertEqual(schema.length, 8) + self.assertEqual(schema.timestamp, {'seconds': epoch_2000, 'fraction': 0}) + + data = proto._read_opt_timestamp(schema, options=None) # type: ignore[arg-type] + self.assertEqual(data.timestamp, + dt.datetime(2000, 1, 1, tzinfo=dt.timezone.utc)) + self.assertEqual(tuple(data.pmip_timestamp), (epoch_2000, 0)) + + # a half-second is 0x8000 of the 16-bit fraction, and survives the trip + half = proto._make_opt_timestamp( # type: ignore[arg-type] + Option.Timestamp_Option, seconds=epoch_2000, fraction=0x8000) + parsed = proto._read_opt_timestamp(half, options=None) # type: ignore[arg-type] + self.assertEqual(tuple(parsed.pmip_timestamp), (epoch_2000, 0x8000)) + self.assertEqual( + proto._make_opt_timestamp(Option.Timestamp_Option, parsed).pack(), # type: ignore[arg-type] + half.pack()) + + def test_mh_geo_location_degrees_are_signed(self) -> None: + """The geo-location degrees are 24-bit two's complement. + + A :class:`~pcapkit.corekit.fields.strings.BitField` reads a sub-field + unsigned, which is right for every other bit-packed field in the mobility + header and wrong for these two, so a southern latitude read as an unsigned + integer comes out as a number just under 2**24 rather than a negative one. + """ + from pcapkit.const.mh.ani_suboption import ANISuboption + from pcapkit.const.mh.option import Option + from pcapkit.protocols.internet.mh import MH + + proto = object.__new__(MH) + + # 33 degrees 52 minutes south, 151 degrees 12 minutes east + raw_lat, raw_lon = -1_109_852, 4_953_047 + schema = proto._make_opt_ani( # type: ignore[arg-type] + Option.Access_Network_Identifier, + suboptions=[(ANISuboption.Geo_Location, + {'raw_latitude': raw_lat, 'raw_longitude': raw_lon})]) + + # the wire carries them unsigned, as two 24-bit fields + sub = schema.suboptions[0] + self.assertEqual(sub.location['latitude'], raw_lat & 0xFFFFFF) + self.assertEqual(sub.location['longitude'], raw_lon) + + data = proto._read_opt_ani(schema, options=None) # type: ignore[arg-type] + geo = data.suboptions[ANISuboption.Geo_Location] + self.assertEqual(geo.raw_latitude, raw_lat) + self.assertEqual(geo.raw_longitude, raw_lon) + self.assertLess(geo.latitude, 0) + self.assertGreater(geo.longitude, 0) + self.assertAlmostEqual(geo.latitude, raw_lat / 2 ** 15, places=6) + + # and the signed values, not the decoded floats, are what gets re-encoded + again = proto._make_opt_ani(Option.Access_Network_Identifier, data) # type: ignore[arg-type] + self.assertEqual(again.pack(), schema.pack()) + + def test_mh_multiprefix_extension_length_matches_its_payload(self) -> None: + """The Multi-Prefix CGA extension declared a length it did not emit. + + ``_make_ext_multiprefix`` computed ``1 + len(prefixes) * 16`` for an + extension whose data is a 4-octet flag word followed by one **8**-octet + prefix apiece, so two prefixes declared 33 data octets where 20 were + written and a re-parse ran off the end. Re-making a *parsed* extension was + broken separately: the data model holds the prefixes as a :obj:`tuple`, + which :class:`~pcapkit.corekit.fields.collections.ListField` refuses. + """ + from pcapkit.const.mh.cga_extension import CGAExtension + from pcapkit.protocols.internet.mh import MH + + proto = object.__new__(MH) + + for count in (0, 1, 2, 5): + with self.subTest(prefixes=count): + schema = proto._make_ext_multiprefix( # type: ignore[arg-type] + CGAExtension.Multi_Prefix, flag=True, prefixes=list(range(count))) + packed = schema.pack() + + # the declared data length is exactly the data emitted + self.assertEqual(schema.length, 4 + count * 8) + self.assertEqual(len(packed), schema.length + 4) + + data = proto._read_ext_multiprefix(schema, extensions=None) # type: ignore[arg-type] + self.assertEqual(data.prefixes, tuple(range(count))) + self.assertTrue(data.flag) + + # and a parsed extension can be re-made, tuple prefixes and all + again = proto._make_ext_multiprefix( # type: ignore[arg-type] + CGAExtension.Multi_Prefix, data) + self.assertEqual(again.pack(), packed) + + def test_mh_experimental_cga_extensions_round_trip(self) -> None: + """:rfc:`4581#section-3`'s three experimental extension types. + + The RFC assigns the codepoints and gives their extension data no structure + at all, so an opaque payload is the whole of the correct parse rather than + a placeholder for a better one. All three share a handler. + """ + from pcapkit.const.mh.cga_extension import CGAExtension + from pcapkit.protocols.internet.mh import MH + + proto = object.__new__(MH) + + for code in (CGAExtension.Exp_FFFD, CGAExtension.Exp_FFFE, CGAExtension.Exp_FFFF): + for payload in (b'', b'\x01', bytes(range(16))): + with self.subTest(code=code.name, size=len(payload)): + schema = proto._make_ext_exp(code, data=payload) # type: ignore[arg-type] + self.assertEqual(schema.length, len(payload)) + self.assertEqual(len(schema.pack()), len(payload) + 4) + + data = proto._read_ext_exp(schema, extensions=None) # type: ignore[arg-type] + self.assertEqual(data.type, code) + self.assertEqual(data.length, len(payload) + 2) + self.assertEqual(data.data, payload) + + again = proto._make_ext_exp(code, data) # type: ignore[arg-type] + self.assertEqual(again.pack(), schema.pack()) + + def test_mh_pmipv6_options_round_trip_byte_for_byte(self) -> None: + """Every mobility option this module decodes must survive a round trip. + + ``make`` then ``read`` then ``make`` again has to give identical octets, + and the parse must land on a real handler rather than falling through to + :class:`~pcapkit.protocols.data.internet.mh.UnassignedOption` -- a + fall-through is what a missing dispatch entry looks like, and it does not + raise. + + Note: + :attr:`~pcapkit.const.mh.option.Option.CGA_Parameters` is absent, and + deliberately so: it cannot be parsed at all, on this branch or on + ``main``. See + :meth:`test_mh_cga_parameters_option_is_unparsable_upstream`. + """ + import ipaddress + + from pcapkit.const.mh.ani_suboption import ANISuboption + from pcapkit.const.mh.flow_id_suboption import FlowIDSuboption + from pcapkit.const.mh.lma_mag_suboption import LMAControlledMAGSuboption + from pcapkit.const.mh.option import Option + from pcapkit.const.mh.packet import Packet + from pcapkit.const.mh.qos_attribute import QoSAttribute + from pcapkit.const.reg.transtype import TransType + from pcapkit.protocols.data.internet.mh import UnassignedOption + from pcapkit.protocols.internet.mh import MH + + v6 = '2001:db8::1' + v4 = '198.51.100.7' + + cases = { + Option.Pad1: {'length': 0}, + Option.PadN: {'length': 4}, + Option.Binding_Refresh_Advice: {'interval': 300}, + Option.Alternate_Care_of_Address: {'address': v6}, + Option.Nonce_Indices: {'home': 1, 'careof': 2}, + Option.Authorization_Data: {'data': bytes(range(8))}, + Option.Mobile_Network_Prefix_Option: {'prefix': '2001:db8:1::/64'}, + Option.Mobility_Header_Link_Layer_Address_option: {'address': b'\x00\x11\x22\x33\x44\x55'}, + Option.MN_ID_OPTION_TYPE: {'identifier': ipaddress.ip_address(v6)}, + Option.AUTH_OPTION_TYPE: {'spi': 0xdeadbeef, 'data': bytes(range(10))}, + Option.MESG_ID_OPTION_TYPE: {'seconds': 0x83aa7e80, 'fraction': 0x40000000}, + Option.CGA_Parameters_Request: {}, + Option.Signature: {'signature': bytes(range(16))}, + Option.Permanent_Home_Keygen_Token: {'token': bytes(range(8))}, + Option.Care_of_Test_Init: {}, + Option.Care_of_Test: {'token': bytes(range(8))}, + Option.DNS_UPDATE_TYPE: {'remove': True, 'identity': b'mn.example.com'}, + Option.Experimental_Mobility_Option: {'data': b'\x01\x02\x03\x04'}, + Option.Vendor_Specific_Mobility_Option: {'vendor': 32473, 'subtype': 3, + 'data': b'\xaa\xbb'}, + Option.Service_Selection_Mobility_Option: {'identifier': 'ims'}, + Option.Binding_Authorization_Data_for_FMIPv6: {'spi': 1, 'data': bytes(range(12))}, + Option.Home_Network_Prefix_Option: {'prefix_length': 64, 'prefix': '2001:db8:1::'}, + Option.Handoff_Indicator_Option: {'hi': 2}, + Option.Access_Technology_Type_Option: {'att': 4}, + Option.Mobile_Node_Link_layer_Identifier_Option: {'lli': b'\x00\x11\x22\x33\x44\x55'}, + Option.Link_local_Address_Option: {'address': 'fe80::1'}, + Option.Timestamp_Option: {'seconds': 1_700_000_000, 'fraction': 0x8000}, + Option.Restart_Counter: {'counter': 7}, + Option.IPv4_Home_Address: {'prefix_length': 24, 'address': v4, + 'request_prefix': True}, + Option.IPv4_Address_Acknowledgement: {'status': 0, 'prefix_length': 24, + 'address': v4}, + Option.NAT_Detection: {'force': True, 'refresh': 110}, + Option.IPv4_Care_of_Address: {'address': v4}, + Option.GRE_Key_Option: {'key': 0x11223344}, + Option.Mobility_Header_IPv6_Address_Prefix: {'code': 2, 'prefix_length': 64, + 'address': '2001:db8:2::'}, + Option.Binding_Identifier: {'bid': 5, 'status': 0, 'simultaneous': True, + 'bid_pri': 3, 'address': v6}, + Option.IPv4_Home_Address_Request: {'prefix_length': 32, 'address': v4}, + Option.IPv4_Home_Address_Reply: {'status': 0, 'prefix_length': 32, 'address': v4}, + Option.IPv4_Default_Router_Address: {'address': v4}, + Option.IPv4_DHCP_Support_Mode: {'mode': 1}, + Option.Context_Request_Option: {'requests': [(22, b''), + (19, b'\x00\x00~\xd9\x03')]}, + Option.Local_Mobility_Anchor_Address_Option: {'code': 1, 'address': v6}, + Option.Mobile_Node_Link_local_Address_Interface_Identifier_Option: { + 'iid': bytes(range(8))}, + Option.Transient_Binding: {'late': True, 'lifetime': 5}, + Option.Flow_Summary_Mobility_Option: {'fid': [1, 2, 3]}, + Option.Flow_Identification_Mobility_Option: { + 'fid': 7, 'fid_pri': 2, 'status': 0, + 'suboptions': [ + (FlowIDSuboption.BID_Reference, {'bid': [1, 2]}), + (FlowIDSuboption.Traffic_Selector, {'ts_format': 2, + 'selector': b'\x00\x01\x02\x03'}), + (FlowIDSuboption.Flow_Binding_Action, {'action': 11}), + (FlowIDSuboption.Target_Care_of_Address, {'address': v6}), + (FlowIDSuboption.PadN, {'length': 2}), + (FlowIDSuboption.Pad, {}), + ], + }, + Option.Redirect_Capability_Mobility_Option: {}, + Option.Redirect_Mobility_Option: {'ipv6': v6}, + Option.Load_Information_Mobility_Option: { + 'priority': 10, 'sessions_in_use': 100, 'max_sessions': 1000, + 'used_capacity': 55, 'max_capacity': 999}, + Option.Alternate_IPv4_Care_of_Address: {'address': v4}, + Option.Mobile_Node_Group_Identifier: {'subtype': 1, 'group_id': 42}, + Option.MAG_IPv6_Address: {'address_length': 128, 'address': v6}, + Option.Access_Network_Identifier: { + 'suboptions': [ + (ANISuboption.Network_Identifier, {'utf8': True, 'net_name': b'wifi', + 'ap_name': b'\x00\x11\x22\x33\x44\x55'}), + (ANISuboption.Geo_Location, {'raw_latitude': -1234567, + 'raw_longitude': 987654}), + (ANISuboption.Operator_Identifier, {'op_id_type': 2, + 'identifier': b'example.com'}), + (ANISuboption.Civic_Location, {'format': 0, 'location': b'GB\x01\x02'}), + (ANISuboption.MAG_Group_Identifier, {'group_id': 9}), + (ANISuboption.ANI_Update_Timer, {'timer': 15}), + ], + }, + Option.IPv4_Traffic_Offload_Selector: { + 'mode': True, + 'selector': [(FlowIDSuboption.Traffic_Selector, + {'ts_format': 1, 'selector': b'\x00\x01\x02\x03'})], + }, + Option.Dynamic_IP_Multicast_Selector: {'protocol': 143, 'mode': True, + 'records': 1, 'data': bytes(range(8))}, + Option.Delegated_Mobile_Network_Prefix: {'prefix_length': 56, + 'prefix': '2001:db8:3::'}, + Option.Active_Multicast_Subscription_IPv4: {'igmp_type': 0x22, + 'context': bytes(range(8))}, + Option.Active_Multicast_Subscription_IPv6: {'mld_type': 143, + 'context': bytes(range(20))}, + Option.Quality_of_Service: { + 'sr_id': 3, 'dscp': 46, 'oc': 1, + 'attributes': [ + (QoSAttribute.Per_MN_Agg_Max_DL_Bit_Rate, {'rate': 1_000_000}), + (QoSAttribute.Per_Session_Agg_Max_UL_Bit_Rate, + {'service': True, 'exclude': True, 'rate': 500_000}), + (QoSAttribute.Allocation_Retention_Priority, + {'priority_level': 5, 'preemption_capability': 1, + 'preemption_vulnerability': 0}), + (QoSAttribute.QoS_Traffic_Selector, {'ts_format': 2, + 'selector': b'\x01\x02'}), + (QoSAttribute.QoS_Vendor_Specific_Attribute, + {'vendor': 32473, 'subtype': 1, 'data': b'\xff'}), + ], + }, + Option.LMA_User_Plane_Address: {'address': v6}, + Option.Multicast_Mobility_Option: {'code': 2, 'data': bytes(range(8))}, + Option.Multicast_Acknowledgement_Option: {'code': 0, 'status': 1, + 'data': bytes(range(4))}, + Option.LMA_Controlled_MAG_Parameters: { + 'suboptions': [ + (LMAControlledMAGSuboption.Binding_Re_registration_Control, + {'start_time': 10, 'initial_retransmission': 2, + 'max_retransmission': 30}), + (LMAControlledMAGSuboption.Heartbeat_Control, + {'interval': 60, 'retransmission_delay': 3, + 'max_retransmissions': 5}), + ], + }, + Option.MAG_Multipath_Binding: {'att': 4, 'label': 2, 'bid': 3, 'bulk': True}, + Option.MAG_Identifier: {'subtype': 1, 'identifier': b'mag@example.com'}, + Option.Anchored_Prefix: {'prefix_length': 64, 'prefix': '2001:db8:4::'}, + Option.Local_Prefix: {'prefix_length': 64, 'prefix': '2001:db8:5::'}, + Option.Previous_MAAR: {'prefix_length': 64, 'maar': v6, + 'prefix': '2001:db8:6::'}, + Option.Serving_MAAR: {'address': v6}, + Option.DLIF_Link_Local_Address: {'address': 'fe80::2'}, + Option.DLIF_Link_Layer_Address: {'lla': b'\x00\x11\x22\x33\x44\x55'}, + } + + # the whole registry bar the one option that cannot be parsed at all + self.assertEqual(sorted([*cases, Option.CGA_Parameters]), sorted(Option)) + + for code, args in cases.items(): + with self.subTest(option=code.name): + raw = bytes(MH(next=TransType.UDP, chksum=b'\x12\x34', + type=Packet.Binding_Refresh_Request, + data={'options': [(code, args)]})) + self.assertEqual(len(raw) % 8, 0) + + parsed = MH(io.BytesIO(raw), len(raw), extension=True).info + option = parsed.options[code] + self.assertNotIsInstance(option, UnassignedOption) + self.assertEqual(option.type, code) + + rebuilt = bytes(MH(next=parsed.next, type=parsed.type, + chksum=parsed.chksum, data=parsed)) + self.assertEqual(rebuilt, raw) + + def test_mh_message_types_round_trip_byte_for_byte(self) -> None: + """Every one of the 24 registered message types must survive a round trip.""" + from pcapkit.const.mh.option import Option + from pcapkit.const.mh.packet import Packet + from pcapkit.const.reg.transtype import TransType + from pcapkit.protocols.data.internet.mh import UnknownMessage + from pcapkit.protocols.internet.mh import MH + + v6 = '2001:db8::1' + + # message types that carry mobility options, and the fields they add + with_options = { + Packet.Binding_Refresh_Request: {}, + Packet.Home_Test_Init: {'cookie': bytes(range(8))}, + Packet.Care_of_Test_Init: {'cookie': bytes(range(8))}, + Packet.Home_Test: {'nonce_index': 1, 'cookie': bytes(range(8)), + 'token': bytes(range(8))}, + Packet.Care_of_Test: {'nonce_index': 1, 'cookie': bytes(range(8)), + 'token': bytes(range(8))}, + Packet.Binding_Update: {'seq': 9, 'ack': True, 'lifetime': 40}, + Packet.Binding_Acknowledgement: {'status': 0, 'seq': 9, 'lifetime': 40}, + Packet.Binding_Error: {'status': 1, 'home': v6}, + Packet.Fast_Binding_Update: {'seq': 9, 'ack': True, 'lifetime': 40}, + Packet.Fast_Binding_Acknowledgment: {'status': 0, 'seq': 9, 'lifetime': 40}, + Packet.Fast_Neighbor_Advertisement: {}, + Packet.Handover_Initiate_Message: {'seq': 3, 'assign': True, 'code': 0}, + Packet.Handover_Acknowledge_Message: {'seq': 3, 'buffer': True, 'code': 0}, + Packet.Home_Agent_Switch_Message: {'addresses': [v6, '2001:db8::2']}, + Packet.Heartbeat_Message: {'unsolicited': True, 'response': True, 'seq': 12345}, + Packet.Binding_Revocation_Message: {'br_type': 1, 'code': 2, 'seq': 77, + 'proxy': True, 'global_revocation': True}, + Packet.Localized_Routing_Initiation: {'seq': 4, 'lifetime': 600}, + Packet.Localized_Routing_Acknowledgment: {'seq': 4, 'unsolicited': True, + 'status': 128, 'lifetime': 600}, + Packet.Update_Notification: {'seq': 5, 'reason': 2, 'ack': True, + 'retransmit': True}, + Packet.Update_Notification_Acknowledgement: {'seq': 5, 'status': 128}, + Packet.Flow_Binding_Message: {'fb_type': 2, 'seq': 6, 'code': 128}, + Packet.Subscription_Query: {'seq': 200}, + Packet.Subscription_Response: {'seq': 200, 'info': True}, + } + # ... and the one whose body is opaque, so has nowhere to put an option + opaque = {Packet.Experimental_Mobility_Header: {'data': bytes(range(10))}} + + self.assertEqual(sorted({**with_options, **opaque}), sorted(Packet)) + + for code, args in with_options.items(): + with self.subTest(message=code.name): + payload = dict(args) + payload['options'] = [(Option.Alternate_Care_of_Address, {'address': v6})] + + raw = bytes(MH(next=TransType.UDP, chksum=b'\x12\x34', type=code, + data=payload)) + self.assertEqual(len(raw) % 8, 0) + + parsed = MH(io.BytesIO(raw), len(raw), extension=True).info + self.assertEqual(parsed.type, code) + self.assertNotIsInstance(parsed, UnknownMessage) + self.assertEqual(parsed.length, len(raw)) + + rebuilt = bytes(MH(next=parsed.next, type=parsed.type, + chksum=parsed.chksum, data=parsed)) + self.assertEqual(rebuilt, raw) + + for code, args in opaque.items(): + with self.subTest(message=code.name): + raw = bytes(MH(next=TransType.UDP, chksum=b'\x12\x34', type=code, data=args)) + parsed = MH(io.BytesIO(raw), len(raw), extension=True).info + rebuilt = bytes(MH(next=parsed.next, type=parsed.type, + chksum=parsed.chksum, data=parsed)) + self.assertEqual(rebuilt, raw) + + def test_mh_two_form_messages_switch_on_their_inner_type(self) -> None: + """Binding revocation and flow binding each carry two forms under one type. + + Neither is distinguished by the Mobility Header type, so the octet after + the inner type field means different things in the two forms and draws from + a different registry in each. Reading it against the wrong registry yields + a plausible-looking wrong name rather than an error, so both forms are + pinned. + """ + from pcapkit.const.mh.binding_revocation import BindingRevocation + from pcapkit.const.mh.fb_ack_status import FlowBindingACKStatus + from pcapkit.const.mh.fb_indication_trigger import FlowBindingIndicationTrigger + from pcapkit.const.mh.fb_type import FlowBindingType + from pcapkit.const.mh.packet import Packet + from pcapkit.const.mh.revocation_status_code import RevocationStatusCode + from pcapkit.const.mh.revocation_trigger import RevocationTrigger + from pcapkit.const.reg.transtype import TransType + from pcapkit.protocols.internet.mh import MH + + def build(packet_type, payload): + payload = dict(payload, options=[]) + raw = bytes(MH(next=TransType.UDP, chksum=b'\x00\x00', + type=packet_type, data=payload)) + return raw, MH(io.BytesIO(raw), len(raw), extension=True).info + + # a revocation indication reads its octet as a trigger ... + raw, bri = build(Packet.Binding_Revocation_Message, + {'br_type': BindingRevocation.Binding_Revocation_Indication, + 'code': RevocationTrigger.Per_Peer_Policy, 'seq': 1, + 'proxy': True}) + self.assertEqual(bri.br_type, BindingRevocation.Binding_Revocation_Indication) + self.assertEqual(bri.code, RevocationTrigger.Per_Peer_Policy) + self.assertIsInstance(bri.code, RevocationTrigger) + self.assertTrue(bri.proxy) + self.assertEqual( + bytes(MH(next=bri.next, type=bri.type, chksum=bri.chksum, data=bri)), raw) + + # ... and an acknowledgement reads the same octet as a status code + raw, bra = build(Packet.Binding_Revocation_Message, + {'br_type': BindingRevocation.Binding_Revocation_Acknowledgement, + 'code': RevocationStatusCode.Binding_Does_NOT_Exist, 'seq': 1}) + self.assertEqual(bra.code, RevocationStatusCode.Binding_Does_NOT_Exist) + self.assertIsInstance(bra.code, RevocationStatusCode) + self.assertEqual( + bytes(MH(next=bra.next, type=bra.type, chksum=bra.chksum, data=bra)), raw) + + # the flow binding message does the same, one registry apart + raw, fbi = build(Packet.Flow_Binding_Message, + {'fb_type': FlowBindingType.Indication, + 'code': FlowBindingIndicationTrigger.Administrative_Reason, + 'ack': True, 'seq': 2}) + self.assertEqual(fbi.code, FlowBindingIndicationTrigger.Administrative_Reason) + self.assertIsInstance(fbi.code, FlowBindingIndicationTrigger) + self.assertTrue(fbi.ack) + self.assertEqual( + bytes(MH(next=fbi.next, type=fbi.type, chksum=fbi.chksum, data=fbi)), raw) + + raw, fba = build(Packet.Flow_Binding_Message, + {'fb_type': FlowBindingType.Acknowledgement, + 'code': FlowBindingACKStatus.Action_NOT_Authorized, 'seq': 2}) + self.assertEqual(fba.code, FlowBindingACKStatus.Action_NOT_Authorized) + self.assertIsInstance(fba.code, FlowBindingACKStatus) + self.assertEqual( + bytes(MH(next=fba.next, type=fba.type, chksum=fba.chksum, data=fba)), raw) + + def test_mh_word_counted_option_lengths_are_not_octet_counts(self) -> None: + """The two multicast options of :rfc:`7411` count 32-bit words. + + Their length field is in words rather than octets, and excludes the option + code and status octets as well as the type and length ones, so the option + occupies ``4 + length * 4`` octets. Treating the field as the usual octet + count would under-read the payload by a factor of four. + """ + from pcapkit.const.mh.option import Option + from pcapkit.protocols.internet.mh import MH + from pcapkit.utilities.exceptions import ProtocolError + + proto = object.__new__(MH) + + payload = bytes(range(12)) + schema = proto._make_opt_mcast( # type: ignore[arg-type] + Option.Multicast_Mobility_Option, code=2, data=payload) + self.assertEqual(schema.length, 3) # 12 octets == 3 words + self.assertEqual(len(schema.pack()), 16) # 4 + 12 + + data = proto._read_opt_mcast(schema, options=None) # type: ignore[arg-type] + self.assertEqual(data.length, 16) # the true octet count + self.assertEqual(data.data, payload) + + # a payload that is not a whole number of words cannot be described at all + with self.assertRaises(ProtocolError): + proto._make_opt_mcast(Option.Multicast_Mobility_Option, # type: ignore[arg-type] + code=2, data=b'\x00\x01\x02') + + ack = proto._make_opt_mcast_ack( # type: ignore[arg-type] + Option.Multicast_Acknowledgement_Option, code=0, status=1, data=payload) + self.assertEqual(ack.length, 3) + self.assertEqual( + proto._read_opt_mcast_ack(ack, options=None).length, 16) # type: ignore[arg-type] + + def test_mh_length_derived_addresses_pick_their_family(self) -> None: + """Three fields carry an address whose family only the length reveals. + + The binding identifier option, the local mobility anchor address option and + the target care-of address sub-option each carry an IPv4 or an IPv6 address + with no flag saying which, so the option length is the only thing to branch + on. The delegated mobile network prefix option is the exception that *does* + carry a flag, and is checked here alongside so the two shapes stay distinct. + """ + import ipaddress + + from pcapkit.const.mh.option import Option + from pcapkit.protocols.internet.mh import MH + + proto = object.__new__(MH) + + for address, expected in (('198.51.100.7', 8), ('2001:db8::1', 20)): + with self.subTest(option='bid', address=address): + schema = proto._make_opt_bid( # type: ignore[arg-type] + Option.Binding_Identifier, bid=1, address=address) + self.assertEqual(schema.length, expected) + data = proto._read_opt_bid(schema, options=None) # type: ignore[arg-type] + self.assertEqual(data.address, ipaddress.ip_address(address)) + + # no address at all is a length of 4, and comes back as ``None`` + bare = proto._make_opt_bid(Option.Binding_Identifier, bid=1) # type: ignore[arg-type] + self.assertEqual(bare.length, 4) + self.assertIsNone(proto._read_opt_bid(bare, options=None).address) # type: ignore[arg-type] + + for address, expected in (('198.51.100.7', 6), ('2001:db8::1', 18)): + with self.subTest(option='lmaa', address=address): + schema = proto._make_opt_lmaa( # type: ignore[arg-type] + Option.Local_Mobility_Anchor_Address_Option, address=address) + self.assertEqual(schema.length, expected) + data = proto._read_opt_lmaa(schema, options=None) # type: ignore[arg-type] + self.assertEqual(data.address, ipaddress.ip_address(address)) + + # the LMA user-plane address may be absent entirely, which is how a mobile + # access gateway names a transport without naming an address + empty = proto._make_opt_lma_up(Option.LMA_User_Plane_Address) # type: ignore[arg-type] + self.assertEqual(empty.length, 2) + self.assertIsNone( + proto._read_opt_lma_up(empty, options=None).address) # type: ignore[arg-type] + + # the delegated prefix option carries a flag, so the flag drives the width + for prefix, expected, ipv4 in (('198.51.100.0', 6, True), + ('2001:db8:3::', 18, False)): + with self.subTest(option='dmnp', prefix=prefix): + schema = proto._make_opt_dmnp( # type: ignore[arg-type] + Option.Delegated_Mobile_Network_Prefix, prefix_length=24 if ipv4 else 56, + prefix=prefix) + self.assertEqual(schema.length, expected) + self.assertEqual(bool(schema.flags['V']), ipv4) + data = proto._read_opt_dmnp(schema, options=None) # type: ignore[arg-type] + self.assertEqual(data.ipv4, ipv4) + self.assertEqual(data.prefix, ipaddress.ip_address(prefix)) + + def test_mh_redirect_option_rejects_contradictory_flags(self) -> None: + """:rfc:`6463#section-4.2` allows exactly one of the ``K`` and ``N`` flags. + + Both set, or both clear, leaves the option's own length undetermined, so it + cannot be read either way -- and the flags and the length are two encodings + of the same fact, which a parser has to see agree. + """ + from pcapkit.const.mh.option import Option + from pcapkit.protocols.internet.mh import MH + from pcapkit.protocols.schema.internet.mh import RedirectOption + from pcapkit.utilities.exceptions import ProtocolError + + proto = object.__new__(MH) + + with self.assertRaises(ProtocolError): + proto._make_opt_redirect(Option.Redirect_Mobility_Option) # type: ignore[arg-type] + with self.assertRaises(ProtocolError): + proto._make_opt_redirect( # type: ignore[arg-type] + Option.Redirect_Mobility_Option, ipv6='2001:db8::1', ipv4='198.51.100.7') + + # a hand-built option with both flags clear is rejected on the way in + bogus = RedirectOption(type=Option.Redirect_Mobility_Option, length=6, + flags={'K': 0, 'N': 0}, ipv6=None, ipv4=None) + with self.assertRaises(ProtocolError): + proto._read_opt_redirect(bogus, options=None) # type: ignore[arg-type] + + def test_mh_cga_parameters_option_is_unparsable_upstream(self) -> None: + """The CGA Parameters option cannot be parsed, and this is not new. + + :attr:`~pcapkit.protocols.schema.internet.mh.CGAParameter.extensions` sizes + itself from ``pkt['length']``, but :class:`CGAParameter + ` has no ``length`` + field of its own and + :class:`~pcapkit.corekit.fields.misc.SchemaField` hands a nested schema a + fresh packet dict rather than the enclosing option's, so the lookup fails. + A well-formed option therefore raises :exc:`KeyError` on parse. + + This is recorded rather than fixed: the remaining half of the fault is in + how a :class:`~pcapkit.corekit.fields.misc.ForwardMatchField` is counted + towards the nested schema's length, which is shared field machinery well + outside the mobility header. The test pins the *current* behaviour so that + whoever fixes it finds out here. + """ + from pcapkit.protocols.internet.mh import MH + + # type 12, 30 octets: a 16-octet modifier, 8-octet subnet prefix, one + # collision count octet and a 5-octet ASN.1 public key -- no extensions + raw = bytes.fromhex('11040000123400000c1e' + '0000000000000000000000000000086f' + '0000000020010db8' + '00' + '3003010203') + self.assertEqual(len(raw), 40) + + with self.assertRaises(KeyError) as caught: + MH(io.BytesIO(raw), len(raw), extension=True) + self.assertEqual(caught.exception.args[0], 'length') + if __name__ == '__main__': unittest.main() From afcedfaeafed6f50fe5508590b34d457e3c83911 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Thu, 17 Sep 2026 13:18:44 -0400 Subject: [PATCH 2/4] protocols: dispatch MH sub-options on their type code, not on isinstance Fixes six Python 3.10 failures in the new nested sub-option handlers. One root cause, not six. Every schema class descends from `collections.abc.Mapping`, and on Python 3.10 and older they do **not** each get their own `_abc_impl` -- they share `Schema`'s. A single `abc.ABCMeta` cache therefore serves the whole family, and it is keyed only on the class being *tested*, not on the class it is tested *against*, so one answer poisons every later question about that class: >>> issubclass(ANIGeoLocationSuboption, Schema) # True, cached True >>> issubclass(ANIGeoLocationSuboption, ANINetworkIdentifierSuboption) True # wrong -- the cached True for Schema is handed back That produced both shapes of failure. A false positive sent a sub-option down a sibling's branch (`AttributeError: ... has no attribute 'flags'`), and the mirror case -- a correct `False` against a sibling, cached and then returned for `Schema` -- made `ListField.pack`'s `isinstance(item, Schema)` false, so packing a valid option raised `FieldValueError` and reconstruction emitted zeros where payload belonged. Python 3.11+ gives each class its own cache, which is why this was invisible on 3.14. The eight nested helpers now dispatch on the sub-option type code, which is on the wire, is what the registry keys on, is what every other dispatch in this module already uses, and cannot be poisoned. The shared `_abc_impl` is pre-existing and untouched here; no MH code depended on those checks before. Also fixes a version-independent defect the round-trip tests could not see: the four `_make_*_suboption` helpers took the data model as a parameter named `data`, which shadowed the field of that name on the vendor-specific quality-of-service attribute [RFC 7222 section 4.2.11]. A caller's `data=` bound to the parameter instead of reaching `**kwargs`, so the payload was dropped with no exception and the length written as though empty, while `vendor` and `subtype` arrived intact. The parameter is now `option`, matching `_make_opt_*`. Covered by a new test on the raw-kwargs construction path, which the round-trip tests cannot reach because rebuilding from a parsed model takes the other branch. And two missing citations: the Civic-Location and ANI Update-Timer sub-options are RFC 7563 sections 3.1 and 3.3, as their MAG-Group-Identifier sibling already recorded for section 3.2. Suite on 3.10: 34 passed, 262 subtests in test_mh_unit (was 6 failed). All 70 options, 24 message types and 4 extensions still round-trip byte-identically on both 3.10 and 3.14. --- pcapkit/protocols/internet/mh.py | 497 +++++++++++++++-------- pcapkit/protocols/schema/internet/mh.py | 6 +- tests/protocols/internet/test_mh_unit.py | 68 ++++ 3 files changed, 389 insertions(+), 182 deletions(-) diff --git a/pcapkit/protocols/internet/mh.py b/pcapkit/protocols/internet/mh.py index aaf6c46105..edfee69b2e 100644 --- a/pcapkit/protocols/internet/mh.py +++ b/pcapkit/protocols/internet/mh.py @@ -4560,8 +4560,42 @@ def _read_fid_suboptions( +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ These are sub-options of one mobility option rather than mobility options - in their own right, so they are dispatched here by schema type rather than - through :attr:`self.__option__ `. + in their own right, so they are dispatched here rather than through + :attr:`self.__option__ `. + + Note: + Dispatch is on the sub-option **type code**, never on ``isinstance`` of + the schema class, and that is load-bearing rather than a matter of + taste. Every schema class descends from + :class:`collections.abc.Mapping`, and on Python 3.10 and older they do + **not** each get their own ``_abc_impl`` -- they share + :class:`~pcapkit.protocols.schema.schema.Schema`'s. One + :class:`abc.ABCMeta` cache therefore serves the whole family, and it is + keyed only on the class being *tested*, not on the class being tested + *against*. So one ``issubclass`` answer poisons every later question + about the same class: + + .. code-block:: python + + >>> issubclass(ANIGeoLocationSuboption, Schema) # True, cached + True + >>> issubclass(ANIGeoLocationSuboption, ANINetworkIdentifierSuboption) + True # wrong -- the cached True for Schema is handed back + + The wrong answer goes both ways: a correct ``False`` against a sibling + then makes ``isinstance(sub, Schema)`` ``False``, which is what + :meth:`ListField.pack + ` consults, so + packing a perfectly good option fails with + :exc:`~pcapkit.utilities.exceptions.FieldValueError`. Python 3.11 and + newer give each class its own cache and the checks behave, which is why + this was invisible on a modern interpreter. + + The code is on the wire and the registry keys on it, so it is both the + cheaper discriminator and the only one that cannot be poisoned. The + same reasoning applies to :meth:`_read_ani_suboptions`, + :meth:`_read_qos_attributes`, :meth:`_read_lcmp_suboptions` and their + four constructor counterparts. Args: suboptions_schema: Parsed sub-option schemas. @@ -4573,45 +4607,53 @@ def _read_fid_suboptions( suboptions = OrderedMultiDict() # type: FlowIDSuboption for schema in suboptions_schema: - if isinstance(schema, Schema_PadFlowIdentificationSuboption): - size = 1 if schema.type == Enum_FlowIDSuboption.Pad else schema.length + 2 + code = schema.type + + if code in (Enum_FlowIDSuboption.Pad, Enum_FlowIDSuboption.PadN): + pad = cast('Schema_PadFlowIdentificationSuboption', schema) + size = 1 if code == Enum_FlowIDSuboption.Pad else pad.length + 2 data = Data_PadFlowIdentificationSuboption( - type=schema.type, + type=code, length=size, ) # type: Data_FlowIdentificationSuboption - elif isinstance(schema, Schema_BIDReferenceSuboption): + elif code == Enum_FlowIDSuboption.BID_Reference: + bid_ref = cast('Schema_BIDReferenceSuboption', schema) data = Data_BIDReferenceSuboption( - type=schema.type, - length=schema.length + 2, - bid=tuple(schema.bid), + type=code, + length=bid_ref.length + 2, + bid=tuple(bid_ref.bid), ) - elif isinstance(schema, Schema_TrafficSelectorSuboption): + elif code == Enum_FlowIDSuboption.Traffic_Selector: + selector = cast('Schema_TrafficSelectorSuboption', schema) data = Data_TrafficSelectorSuboption( - type=schema.type, - length=schema.length + 2, - ts_format=schema.ts_format, - selector=schema.selector, + type=code, + length=selector.length + 2, + ts_format=selector.ts_format, + selector=selector.selector, ) - elif isinstance(schema, Schema_FlowBindingActionSuboption): + elif code == Enum_FlowIDSuboption.Flow_Binding_Action: + action = cast('Schema_FlowBindingActionSuboption', schema) data = Data_FlowBindingActionSuboption( - type=schema.type, - length=schema.length + 2, - action=schema.action, + type=code, + length=action.length + 2, + action=action.action, ) - elif isinstance(schema, Schema_TargetCareofAddressSuboption): + elif code == Enum_FlowIDSuboption.Target_Care_of_Address: + target = cast('Schema_TargetCareofAddressSuboption', schema) data = Data_TargetCareofAddressSuboption( - type=schema.type, - length=schema.length + 2, - address=schema.address, + type=code, + length=target.length + 2, + address=target.address, ) else: + unknown = cast('Schema_UnassignedFlowIdentificationSuboption', schema) data = Data_UnassignedFlowIdentificationSuboption( - type=schema.type, - length=schema.length + 2, - data=cast('Schema_UnassignedFlowIdentificationSuboption', schema).data, + type=code, + length=unknown.length + 2, + data=unknown.data, ) - suboptions.add(schema.type, data) + suboptions.add(code, data) return suboptions @@ -4933,6 +4975,11 @@ def _read_ani_suboptions( integers are recorded -- the latter so that the exact wire value survives a round trip through the data model. + Note: + Dispatch is on the sub-option type code rather than on ``isinstance`` + of the schema class, for the reason given in + :meth:`_read_fid_suboptions`. + Args: suboptions_schema: Parsed sub-option schemas. @@ -4943,59 +4990,68 @@ def _read_ani_suboptions( suboptions = OrderedMultiDict() # type: ANISuboption for schema in suboptions_schema: - if isinstance(schema, Schema_ANINetworkIdentifierSuboption): + code = schema.type + + if code == Enum_ANISuboption.Network_Identifier: + net = cast('Schema_ANINetworkIdentifierSuboption', schema) data = Data_ANINetworkIdentifierSuboption( - type=schema.type, - length=schema.length + 2, - utf8=bool(schema.flags['E']), - net_name=schema.net_name, - ap_name=schema.ap_name, + type=code, + length=net.length + 2, + utf8=bool(net.flags['E']), + net_name=net.net_name, + ap_name=net.ap_name, ) # type: Data_ANISuboption - elif isinstance(schema, Schema_ANIGeoLocationSuboption): - raw_lat = self._decode_signed(schema.location['latitude'], 24) - raw_lon = self._decode_signed(schema.location['longitude'], 24) + elif code == Enum_ANISuboption.Geo_Location: + geo = cast('Schema_ANIGeoLocationSuboption', schema) + raw_lat = self._decode_signed(geo.location['latitude'], 24) + raw_lon = self._decode_signed(geo.location['longitude'], 24) data = Data_ANIGeoLocationSuboption( - type=schema.type, - length=schema.length + 2, + type=code, + length=geo.length + 2, latitude=raw_lat / 2 ** 15, longitude=raw_lon / 2 ** 15, raw_latitude=raw_lat, raw_longitude=raw_lon, ) - elif isinstance(schema, Schema_ANIOperatorIdentifierSuboption): + elif code == Enum_ANISuboption.Operator_Identifier: + operator = cast('Schema_ANIOperatorIdentifierSuboption', schema) data = Data_ANIOperatorIdentifierSuboption( - type=schema.type, - length=schema.length + 2, - op_id_type=schema.op_id_type, - identifier=schema.identifier, + type=code, + length=operator.length + 2, + op_id_type=operator.op_id_type, + identifier=operator.identifier, ) - elif isinstance(schema, Schema_ANICivicLocationSuboption): + elif code == Enum_ANISuboption.Civic_Location: + civic = cast('Schema_ANICivicLocationSuboption', schema) data = Data_ANICivicLocationSuboption( - type=schema.type, - length=schema.length + 2, - format=schema.format, - location=schema.location, + type=code, + length=civic.length + 2, + format=civic.format, + location=civic.location, ) - elif isinstance(schema, Schema_ANIMAGGroupIdentifierSuboption): + elif code == Enum_ANISuboption.MAG_Group_Identifier: + group = cast('Schema_ANIMAGGroupIdentifierSuboption', schema) data = Data_ANIMAGGroupIdentifierSuboption( - type=schema.type, - length=schema.length + 2, - group_id=schema.group_id, + type=code, + length=group.length + 2, + group_id=group.group_id, ) - elif isinstance(schema, Schema_ANIUpdateTimerSuboption): + elif code == Enum_ANISuboption.ANI_Update_Timer: + timer = cast('Schema_ANIUpdateTimerSuboption', schema) data = Data_ANIUpdateTimerSuboption( - type=schema.type, - length=schema.length + 2, - timer=datetime.timedelta(seconds=schema.timer * 4), + type=code, + length=timer.length + 2, + timer=datetime.timedelta(seconds=timer.timer * 4), ) else: + unknown = cast('Schema_UnassignedANISuboption', schema) data = Data_UnassignedANISuboption( - type=schema.type, - length=schema.length + 2, - data=cast('Schema_UnassignedANISuboption', schema).data, + type=code, + length=unknown.length + 2, + data=unknown.data, ) - suboptions.add(schema.type, data) + suboptions.add(code, data) return suboptions @@ -5326,6 +5382,10 @@ def _read_qos_attributes( not the kilobytes per second of the load information option's capacity fields. + Dispatch is on the attribute type code rather than on ``isinstance`` of + the schema class, for the reason given in + :meth:`_read_fid_suboptions`. + Args: attributes_schema: Parsed attribute schemas. @@ -5336,51 +5396,65 @@ def _read_qos_attributes( attributes = OrderedMultiDict() # type: QoSAttribute for schema in attributes_schema: - if isinstance(schema, Schema_PerSessionBitRateAttribute): + code = schema.type + + if code in (Enum_QoSAttribute.Per_Session_Agg_Max_DL_Bit_Rate, + Enum_QoSAttribute.Per_Session_Agg_Max_UL_Bit_Rate): + session = cast('Schema_PerSessionBitRateAttribute', schema) data = Data_PerSessionBitRateAttribute( - type=schema.type, - length=schema.length + 2, - service=bool(schema.flags['S']), - exclude=bool(schema.flags['E']), - rate=schema.rate, + type=code, + length=session.length + 2, + service=bool(session.flags['S']), + exclude=bool(session.flags['E']), + rate=session.rate, ) # type: Data_QoSAttribute - elif isinstance(schema, Schema_BitRateAttribute): + elif code in (Enum_QoSAttribute.Per_MN_Agg_Max_DL_Bit_Rate, + Enum_QoSAttribute.Per_MN_Agg_Max_UL_Bit_Rate, + Enum_QoSAttribute.Aggregate_Max_DL_Bit_Rate, + Enum_QoSAttribute.Aggregate_Max_UL_Bit_Rate, + Enum_QoSAttribute.Guaranteed_DL_Bit_Rate, + Enum_QoSAttribute.Guaranteed_UL_Bit_Rate): + rate = cast('Schema_BitRateAttribute', schema) data = Data_BitRateAttribute( - type=schema.type, - length=schema.length + 2, - rate=schema.rate, + type=code, + length=rate.length + 2, + rate=rate.rate, ) - elif isinstance(schema, Schema_AllocationRetentionPriorityAttribute): + elif code == Enum_QoSAttribute.Allocation_Retention_Priority: + arp = cast('Schema_AllocationRetentionPriorityAttribute', schema) data = Data_AllocationRetentionPriorityAttribute( - type=schema.type, - length=schema.length + 2, - priority_level=schema.priority['PL'], - preemption_capability=schema.priority['PC'], - preemption_vulnerability=schema.priority['PV'], + type=code, + length=arp.length + 2, + priority_level=arp.priority['PL'], + preemption_capability=arp.priority['PC'], + preemption_vulnerability=arp.priority['PV'], ) - elif isinstance(schema, Schema_QoSTrafficSelectorAttribute): + elif code == Enum_QoSAttribute.QoS_Traffic_Selector: + selector = cast('Schema_QoSTrafficSelectorAttribute', schema) data = Data_QoSTrafficSelectorAttribute( - type=schema.type, - length=schema.length + 2, - ts_format=schema.ts_format, - selector=schema.selector, + type=code, + length=selector.length + 2, + ts_format=selector.ts_format, + selector=selector.selector, ) - elif isinstance(schema, Schema_QoSVendorSpecificAttribute): + elif code == Enum_QoSAttribute.QoS_Vendor_Specific_Attribute: + vendor = cast('Schema_QoSVendorSpecificAttribute', schema) data = Data_QoSVendorSpecificAttribute( - type=schema.type, - length=schema.length + 2, - vendor=schema.vendor, - subtype=schema.subtype, - data=schema.data, + type=code, + length=vendor.length + 2, + vendor=vendor.vendor, + subtype=vendor.subtype, + data=vendor.data, ) else: + unknown = cast('Schema_UnassignedQoSAttribute', schema) data = Data_UnassignedQoSAttribute( - type=schema.type, - length=schema.length + 2, - data=cast('Schema_UnassignedQoSAttribute', schema).data, + type=code, + length=unknown.length + 2, + data=unknown.data, ) - attributes.add(schema.type, data) + attributes.add(code, data) return attributes @@ -5585,6 +5659,10 @@ def _read_lcmp_suboptions( other interval in these two sub-options is in whole seconds [:rfc:`8127#section-3.1.1`, :rfc:`8127#section-3.1.2`]. + Dispatch is on the sub-option type code rather than on ``isinstance`` + of the schema class, for the reason given in + :meth:`_read_fid_suboptions`. + Args: suboptions_schema: Parsed sub-option schemas. @@ -5595,32 +5673,37 @@ def _read_lcmp_suboptions( suboptions = OrderedMultiDict() # type: LMAControlledMAGSuboption for schema in suboptions_schema: - if isinstance(schema, Schema_BindingReregistrationControlSuboption): + code = schema.type + + if code == Enum_LMAControlledMAGSuboption.Binding_Re_registration_Control: + rereg = cast('Schema_BindingReregistrationControlSuboption', schema) data = Data_BindingReregistrationControlSuboption( - type=schema.type, - length=schema.length + 2, - start_time=datetime.timedelta(seconds=schema.start_time * 4), + type=code, + length=rereg.length + 2, + start_time=datetime.timedelta(seconds=rereg.start_time * 4), initial_retransmission=datetime.timedelta( - seconds=schema.initial_retransmission), - max_retransmission=datetime.timedelta(seconds=schema.max_retransmission), + seconds=rereg.initial_retransmission), + max_retransmission=datetime.timedelta(seconds=rereg.max_retransmission), ) # type: Data_LMAControlledMAGSuboption - elif isinstance(schema, Schema_HeartbeatControlSuboption): + elif code == Enum_LMAControlledMAGSuboption.Heartbeat_Control: + heartbeat = cast('Schema_HeartbeatControlSuboption', schema) data = Data_HeartbeatControlSuboption( - type=schema.type, - length=schema.length + 2, - interval=datetime.timedelta(seconds=schema.interval), + type=code, + length=heartbeat.length + 2, + interval=datetime.timedelta(seconds=heartbeat.interval), retransmission_delay=datetime.timedelta( - seconds=schema.retransmission_delay), - max_retransmissions=schema.max_retransmissions, + seconds=heartbeat.retransmission_delay), + max_retransmissions=heartbeat.max_retransmissions, ) else: + unknown = cast('Schema_UnassignedLMAControlledMAGSuboption', schema) data = Data_UnassignedLMAControlledMAGSuboption( - type=schema.type, - length=schema.length + 2, - data=cast('Schema_UnassignedLMAControlledMAGSuboption', schema).data, + type=code, + length=unknown.length + 2, + data=unknown.data, ) - suboptions.add(schema.type, data) + suboptions.add(code, data) return suboptions @@ -8829,45 +8912,57 @@ def _make_fid_suboptions( entries.append(self._make_fid_suboption(code, **args)) return entries - return [self._make_fid_suboption(code, data=data) - for code, data in suboptions.items(multi=True)] + return [self._make_fid_suboption(code, option) + for code, option in suboptions.items(multi=True)] def _make_fid_suboption(self, code: 'Enum_FlowIDSuboption', - data: 'Optional[Data_FlowIdentificationSuboption]' = None, + option: 'Optional[Data_FlowIdentificationSuboption]' = None, **kwargs: 'Any') -> 'Schema_FlowIdentificationSuboption': """Make one MH flow identification sub-option. Args: code: Sub-option type. - data: Sub-option data model. + option: Sub-option data model. **kwargs: Sub-option fields, when no data model is given. Returns: Constructed sub-option schema. + Note: + The data model parameter is named ``option`` rather than ``data`` to + match :meth:`_make_opt_fid` and the rest of this module -- and because + naming it ``data`` made it **shadow a field**. The traffic selector and + unassigned sub-options both carry a field of their own called ``data``, + so a caller's ``data=`` bound to the model parameter instead of landing + in ``**kwargs``, and the ``kwargs.get('data')`` fallback below could + never see it: the payload was silently dropped and the length written as + though it were empty. No exception, just a lost field. + + Dispatch is on ``code`` rather than on ``isinstance`` of the schema or + data class, for the reason given in :meth:`_read_fid_suboptions`. + """ - if isinstance(data, Data_PadFlowIdentificationSuboption) or ( - data is None and code in (Enum_FlowIDSuboption.Pad, - Enum_FlowIDSuboption.PadN)): + if code in (Enum_FlowIDSuboption.Pad, Enum_FlowIDSuboption.PadN): if code == Enum_FlowIDSuboption.Pad: pad_len = 0 - elif data is not None: - pad_len = data.length - 2 + elif option is not None: + pad_len = option.length - 2 else: pad_len = cast('int', kwargs.get('length', 0)) return Schema_PadFlowIdentificationSuboption(type=code, length=pad_len) - if isinstance(data, Data_BIDReferenceSuboption): - bid = list(data.bid) - else: - bid = cast('list[int]', kwargs.get('bid') or []) if code == Enum_FlowIDSuboption.BID_Reference: + if option is not None: + bid = list(cast('Data_BIDReferenceSuboption', option).bid) + else: + bid = cast('list[int]', kwargs.get('bid') or []) return Schema_BIDReferenceSuboption(type=code, length=len(bid) * 2, bid=bid) if code == Enum_FlowIDSuboption.Traffic_Selector: - if isinstance(data, Data_TrafficSelectorSuboption): - ts_format = data.ts_format # type: Enum_TrafficSelector | int - selector = data.selector + if option is not None: + selector_opt = cast('Data_TrafficSelectorSuboption', option) + ts_format = selector_opt.ts_format # type: Enum_TrafficSelector | int + selector = selector_opt.selector else: ts_format = cast('Enum_TrafficSelector | int', kwargs.get('ts_format', @@ -8878,8 +8973,9 @@ def _make_fid_suboption(self, code: 'Enum_FlowIDSuboption', ts_format=cast('Enum_TrafficSelector', ts_format), selector=selector) if code == Enum_FlowIDSuboption.Flow_Binding_Action: - if isinstance(data, Data_FlowBindingActionSuboption): - action = data.action # type: Enum_FlowBindingAction | int + if option is not None: + action = cast('Data_FlowBindingActionSuboption', + option).action # type: Enum_FlowBindingAction | int else: action = cast('Enum_FlowBindingAction | int', kwargs.get('action', Enum_FlowBindingAction.Add)) @@ -8887,8 +8983,8 @@ def _make_fid_suboption(self, code: 'Enum_FlowIDSuboption', type=code, length=2, action=cast('Enum_FlowBindingAction', action)) if code == Enum_FlowIDSuboption.Target_Care_of_Address: - if isinstance(data, Data_TargetCareofAddressSuboption): - address = data.address # type: Any + if option is not None: + address = cast('Data_TargetCareofAddressSuboption', option).address # type: Any else: address = kwargs.get('address', '::') addr = address if isinstance( @@ -8897,8 +8993,8 @@ def _make_fid_suboption(self, code: 'Enum_FlowIDSuboption', return Schema_TargetCareofAddressSuboption( type=code, length=6 if addr.version == 4 else 18, address=addr) - if isinstance(data, Data_UnassignedFlowIdentificationSuboption): - payload = data.data + if option is not None: + payload = cast('Data_UnassignedFlowIdentificationSuboption', option).data else: payload = cast('bytes', kwargs.get('data', b'')) return Schema_UnassignedFlowIdentificationSuboption( @@ -9167,17 +9263,17 @@ def _make_ani_suboptions( entries.append(self._make_ani_suboption(code, **args)) return entries - return [self._make_ani_suboption(code, data=data) - for code, data in suboptions.items(multi=True)] + return [self._make_ani_suboption(code, option) + for code, option in suboptions.items(multi=True)] def _make_ani_suboption(self, code: 'Enum_ANISuboption', - data: 'Optional[Data_ANISuboption]' = None, + option: 'Optional[Data_ANISuboption]' = None, **kwargs: 'Any') -> 'Schema_ANISuboption': """Make one MH access network identifier sub-option. Args: code: Sub-option type. - data: Sub-option data model. + option: Sub-option data model. **kwargs: Sub-option fields, when no data model is given. Returns: @@ -9188,10 +9284,17 @@ def _make_ani_suboption(self, code: 'Enum_ANISuboption', integers rather than from the decoded floats, since a float cannot always be converted back to the same 24-bit fixed-point value. + The data model parameter is named ``option`` rather than ``data`` so + that it cannot shadow a sub-option field of that name -- see + :meth:`_make_fid_suboption`, where it did. Dispatch is on ``code`` + rather than on ``isinstance``, for the reason given in + :meth:`_read_fid_suboptions`. + """ if code == Enum_ANISuboption.Network_Identifier: - if isinstance(data, Data_ANINetworkIdentifierSuboption): - utf8, net_name, ap_name = data.utf8, data.net_name, data.ap_name + if option is not None: + net = cast('Data_ANINetworkIdentifierSuboption', option) + utf8, net_name, ap_name = net.utf8, net.net_name, net.ap_name else: utf8 = cast('bool', kwargs.get('utf8', False)) net_name = cast('bytes', kwargs.get('net_name', b'')) @@ -9202,8 +9305,9 @@ def _make_ani_suboption(self, code: 'Enum_ANISuboption', ap_name_len=len(ap_name), ap_name=ap_name) if code == Enum_ANISuboption.Geo_Location: - if isinstance(data, Data_ANIGeoLocationSuboption): - raw_lat, raw_lon = data.raw_latitude, data.raw_longitude + if option is not None: + geo = cast('Data_ANIGeoLocationSuboption', option) + raw_lat, raw_lon = geo.raw_latitude, geo.raw_longitude else: raw_lat = cast('int', kwargs.get('raw_latitude', 0)) raw_lon = cast('int', kwargs.get('raw_longitude', 0)) @@ -9214,9 +9318,10 @@ def _make_ani_suboption(self, code: 'Enum_ANISuboption', }) if code == Enum_ANISuboption.Operator_Identifier: - if isinstance(data, Data_ANIOperatorIdentifierSuboption): - op_id_type = data.op_id_type # type: Enum_OperatorID | int - identifier = data.identifier + if option is not None: + operator = cast('Data_ANIOperatorIdentifierSuboption', option) + op_id_type = operator.op_id_type # type: Enum_OperatorID | int + identifier = operator.identifier else: op_id_type = cast('Enum_OperatorID | int', kwargs.get('op_id_type', Enum_OperatorID.Realm_of_the_Operator)) @@ -9226,8 +9331,9 @@ def _make_ani_suboption(self, code: 'Enum_ANISuboption', op_id_type=cast('Enum_OperatorID', op_id_type), identifier=identifier) if code == Enum_ANISuboption.Civic_Location: - if isinstance(data, Data_ANICivicLocationSuboption): - fmt, location = data.format, data.location + if option is not None: + civic = cast('Data_ANICivicLocationSuboption', option) + fmt, location = civic.format, civic.location else: fmt = cast('int', kwargs.get('format', 0)) location = cast('bytes', kwargs.get('location', b'')) @@ -9235,23 +9341,24 @@ def _make_ani_suboption(self, code: 'Enum_ANISuboption', type=code, length=2 + len(location), format=fmt, location=location) if code == Enum_ANISuboption.MAG_Group_Identifier: - if isinstance(data, Data_ANIMAGGroupIdentifierSuboption): - group_id = data.group_id + if option is not None: + group_id = cast('Data_ANIMAGGroupIdentifierSuboption', option).group_id else: group_id = cast('int', kwargs.get('group_id', 0)) return Schema_ANIMAGGroupIdentifierSuboption(type=code, length=2, group_id=group_id) if code == Enum_ANISuboption.ANI_Update_Timer: - if isinstance(data, Data_ANIUpdateTimerSuboption): - timer = math.ceil(data.timer.total_seconds() / 4) + if option is not None: + timer = math.ceil(cast('Data_ANIUpdateTimerSuboption', + option).timer.total_seconds() / 4) else: raw_timer = kwargs.get('timer', 0) timer = raw_timer if isinstance(raw_timer, int) else math.ceil( raw_timer.total_seconds() / 4) return Schema_ANIUpdateTimerSuboption(type=code, length=2, timer=timer) - if isinstance(data, Data_UnassignedANISuboption): - payload = data.data + if option is not None: + payload = cast('Data_UnassignedANISuboption', option).data else: payload = cast('bytes', kwargs.get('data', b'')) return Schema_UnassignedANISuboption(type=code, length=len(payload), data=payload) @@ -9485,27 +9592,44 @@ def _make_qos_attributes( entries.append(self._make_qos_attribute(code, **args)) return entries - return [self._make_qos_attribute(code, data=data) - for code, data in attributes.items(multi=True)] + return [self._make_qos_attribute(code, option) + for code, option in attributes.items(multi=True)] def _make_qos_attribute(self, code: 'Enum_QoSAttribute', - data: 'Optional[Data_QoSAttribute]' = None, + option: 'Optional[Data_QoSAttribute]' = None, **kwargs: 'Any') -> 'Schema_QoSAttribute': """Make one MH quality-of-service attribute. Args: code: Attribute type. - data: Attribute data model. + option: Attribute data model. **kwargs: Attribute fields, when no data model is given. Returns: Constructed attribute schema. + Note: + The data model parameter is named ``option`` rather than ``data``, and + that is not cosmetic. The vendor-specific attribute of + :rfc:`7222#section-4.2.11` has a field of its own called ``data``, so + with the parameter named ``data`` a caller's ``data=`` bound to the + model parameter instead of reaching ``**kwargs`` -- and the + ``kwargs.get('data')`` fallback then always saw nothing. Building the + attribute the natural way, mirroring the data model's own field names, + silently dropped the vendor payload and wrote the length as though it + were empty. ``vendor`` and ``subtype`` survived because those names do + not collide, which made the loss look like a partial success rather + than a bug. + + Dispatch is on ``code`` rather than on ``isinstance``, for the reason + given in :meth:`_read_fid_suboptions`. + """ if code in (Enum_QoSAttribute.Per_Session_Agg_Max_DL_Bit_Rate, Enum_QoSAttribute.Per_Session_Agg_Max_UL_Bit_Rate): - if isinstance(data, Data_PerSessionBitRateAttribute): - service, exclude, rate = data.service, data.exclude, data.rate + if option is not None: + session = cast('Data_PerSessionBitRateAttribute', option) + service, exclude, rate = session.service, session.exclude, session.rate else: service = cast('bool', kwargs.get('service', False)) exclude = cast('bool', kwargs.get('exclude', False)) @@ -9520,17 +9644,18 @@ def _make_qos_attribute(self, code: 'Enum_QoSAttribute', Enum_QoSAttribute.Aggregate_Max_UL_Bit_Rate, Enum_QoSAttribute.Guaranteed_DL_Bit_Rate, Enum_QoSAttribute.Guaranteed_UL_Bit_Rate): - if isinstance(data, Data_BitRateAttribute): - rate = data.rate + if option is not None: + rate = cast('Data_BitRateAttribute', option).rate else: rate = cast('int', kwargs.get('rate', 0)) return Schema_BitRateAttribute(type=code, length=6, rate=rate) if code == Enum_QoSAttribute.Allocation_Retention_Priority: - if isinstance(data, Data_AllocationRetentionPriorityAttribute): - level = data.priority_level - capability = data.preemption_capability - vulnerability = data.preemption_vulnerability + if option is not None: + arp = cast('Data_AllocationRetentionPriorityAttribute', option) + level = arp.priority_level + capability = arp.preemption_capability + vulnerability = arp.preemption_vulnerability else: level = cast('int', kwargs.get('priority_level', 1)) capability = cast('int', kwargs.get('preemption_capability', 0)) @@ -9540,9 +9665,10 @@ def _make_qos_attribute(self, code: 'Enum_QoSAttribute', priority={'PL': level, 'PC': capability, 'PV': vulnerability}) if code == Enum_QoSAttribute.QoS_Traffic_Selector: - if isinstance(data, Data_QoSTrafficSelectorAttribute): - ts_format = data.ts_format # type: Enum_TrafficSelector | int - selector = data.selector + if option is not None: + ts_attr = cast('Data_QoSTrafficSelectorAttribute', option) + ts_format = ts_attr.ts_format # type: Enum_TrafficSelector | int + selector = ts_attr.selector else: ts_format = cast('Enum_TrafficSelector | int', kwargs.get('ts_format', @@ -9553,8 +9679,10 @@ def _make_qos_attribute(self, code: 'Enum_QoSAttribute', ts_format=cast('Enum_TrafficSelector', ts_format), selector=selector) if code == Enum_QoSAttribute.QoS_Vendor_Specific_Attribute: - if isinstance(data, Data_QoSVendorSpecificAttribute): - vendor, subtype, payload = data.vendor, data.subtype, data.data + if option is not None: + vendor_attr = cast('Data_QoSVendorSpecificAttribute', option) + vendor, subtype, payload = (vendor_attr.vendor, vendor_attr.subtype, + vendor_attr.data) else: vendor = cast('int', kwargs.get('vendor', 0)) subtype = cast('int', kwargs.get('subtype', 0)) @@ -9563,8 +9691,8 @@ def _make_qos_attribute(self, code: 'Enum_QoSAttribute', type=code, length=7 + len(payload), vendor=vendor, subtype=subtype, data=payload) - if isinstance(data, Data_UnassignedQoSAttribute): - payload = data.data + if option is not None: + payload = cast('Data_UnassignedQoSAttribute', option).data else: payload = cast('bytes', kwargs.get('data', b'')) return Schema_UnassignedQoSAttribute(type=code, length=len(payload), data=payload) @@ -9750,8 +9878,8 @@ def _make_lcmp_suboptions( entries.append(self._make_lcmp_suboption(code, **args)) return entries - return [self._make_lcmp_suboption(code, data=data) - for code, data in suboptions.items(multi=True)] + return [self._make_lcmp_suboption(code, option) + for code, option in suboptions.items(multi=True)] @staticmethod def _seconds(value: 'int | timedelta', unit: 'int' = 1) -> 'int': @@ -9771,24 +9899,32 @@ def _seconds(value: 'int | timedelta', unit: 'int' = 1) -> 'int': return math.ceil(value.total_seconds() / unit) def _make_lcmp_suboption(self, code: 'Enum_LMAControlledMAGSuboption', - data: 'Optional[Data_LMAControlledMAGSuboption]' = None, + option: 'Optional[Data_LMAControlledMAGSuboption]' = None, **kwargs: 'Any') -> 'Schema_LMAControlledMAGSuboption': """Make one MH LMA-controlled MAG parameters sub-option. Args: code: Sub-option type. - data: Sub-option data model. + option: Sub-option data model. **kwargs: Sub-option fields, when no data model is given. Returns: Constructed sub-option schema. + Note: + The data model parameter is named ``option`` rather than ``data`` so + that it cannot shadow a sub-option field of that name -- see + :meth:`_make_fid_suboption`, where it did. Dispatch is on ``code`` + rather than on ``isinstance``, for the reason given in + :meth:`_read_fid_suboptions`. + """ if code == Enum_LMAControlledMAGSuboption.Binding_Re_registration_Control: - if isinstance(data, Data_BindingReregistrationControlSuboption): - start = math.ceil(data.start_time.total_seconds() / 4) - initial = math.ceil(data.initial_retransmission.total_seconds()) - maximum = math.ceil(data.max_retransmission.total_seconds()) + if option is not None: + rereg = cast('Data_BindingReregistrationControlSuboption', option) + start = math.ceil(rereg.start_time.total_seconds() / 4) + initial = math.ceil(rereg.initial_retransmission.total_seconds()) + maximum = math.ceil(rereg.max_retransmission.total_seconds()) else: start = self._seconds(kwargs.get('start_time', 0), 4) initial = self._seconds(kwargs.get('initial_retransmission', 0)) @@ -9798,10 +9934,11 @@ def _make_lcmp_suboption(self, code: 'Enum_LMAControlledMAGSuboption', initial_retransmission=initial, max_retransmission=maximum) if code == Enum_LMAControlledMAGSuboption.Heartbeat_Control: - if isinstance(data, Data_HeartbeatControlSuboption): - interval = math.ceil(data.interval.total_seconds()) - delay = math.ceil(data.retransmission_delay.total_seconds()) - count = data.max_retransmissions + if option is not None: + heartbeat = cast('Data_HeartbeatControlSuboption', option) + interval = math.ceil(heartbeat.interval.total_seconds()) + delay = math.ceil(heartbeat.retransmission_delay.total_seconds()) + count = heartbeat.max_retransmissions else: interval = self._seconds(kwargs.get('interval', 0)) delay = self._seconds(kwargs.get('retransmission_delay', 0)) @@ -9810,8 +9947,8 @@ def _make_lcmp_suboption(self, code: 'Enum_LMAControlledMAGSuboption', type=code, length=6, interval=interval, retransmission_delay=delay, max_retransmissions=count) - if isinstance(data, Data_UnassignedLMAControlledMAGSuboption): - payload = data.data + if option is not None: + payload = cast('Data_UnassignedLMAControlledMAGSuboption', option).data else: payload = cast('bytes', kwargs.get('data', b'')) return Schema_UnassignedLMAControlledMAGSuboption( diff --git a/pcapkit/protocols/schema/internet/mh.py b/pcapkit/protocols/schema/internet/mh.py index 4becf0daa5..c6ba345fb7 100644 --- a/pcapkit/protocols/schema/internet/mh.py +++ b/pcapkit/protocols/schema/internet/mh.py @@ -1813,7 +1813,8 @@ def __init__(self, type: 'Enum_ANISuboption', length: 'int', @schema_final class ANICivicLocationSuboption(ANISuboption, code=Enum_ANISuboption.Civic_Location): - """Header schema for MH Civic-Location ANI sub-options.""" + """Header schema for MH Civic-Location ANI sub-options + [:rfc:`7563#section-3.1`].""" #: Encoding format; only ``0``, the binary encoding of :rfc:`4776#section-3.1`, #: is defined. @@ -1852,7 +1853,8 @@ def __init__(self, type: 'Enum_ANISuboption', length: 'int', @schema_final class ANIUpdateTimerSuboption(ANISuboption, code=Enum_ANISuboption.ANI_Update_Timer): - """Header schema for MH ANI Update-Timer sub-options.""" + """Header schema for MH ANI Update-Timer sub-options + [:rfc:`7563#section-3.3`].""" #: Update timer. One time unit is 4 seconds; ``0`` asks for an immediate #: update. diff --git a/tests/protocols/internet/test_mh_unit.py b/tests/protocols/internet/test_mh_unit.py index 67e2baa4d1..4a52c56799 100644 --- a/tests/protocols/internet/test_mh_unit.py +++ b/tests/protocols/internet/test_mh_unit.py @@ -2315,6 +2315,74 @@ def test_mh_redirect_option_rejects_contradictory_flags(self) -> None: with self.assertRaises(ProtocolError): proto._read_opt_redirect(bogus, options=None) # type: ignore[arg-type] + def test_mh_nested_suboptions_build_from_raw_kwargs(self) -> None: + """A nested sub-option built from keyword arguments must keep every field. + + The round-trip tests cannot catch this: rebuilding from an already-parsed + data model goes down the ``option is not None`` branch, which reads the + model. Only *fresh* construction from keyword arguments reaches the + ``kwargs`` branch, and a field whose name collides with one of the maker's + own parameters never arrives there. + + That is what happened. The makers took the data model as a parameter named + ``data``, and the vendor-specific quality-of-service attribute of + :rfc:`7222#section-4.2.11` has a *field* called ``data``, so a caller's + ``data=`` bound to the model parameter and ``kwargs.get('data')`` always saw + nothing. The payload was dropped with no exception and the length written as + though it were empty -- ``vendor`` and ``subtype`` arrived intact, which + made it look like a partial success. The parameter is now ``option``, which + no sub-option field is called. + """ + from pcapkit.const.mh.ani_suboption import ANISuboption + from pcapkit.const.mh.flow_id_suboption import FlowIDSuboption + from pcapkit.const.mh.lma_mag_suboption import LMAControlledMAGSuboption + from pcapkit.const.mh.option import Option + from pcapkit.const.mh.qos_attribute import QoSAttribute + from pcapkit.protocols.internet.mh import MH + + proto = object.__new__(MH) + payload = b'\xde\xad\xbe\xef' + + # the registered vendor-specific attribute: the case that was broken + attr = proto._make_qos_attribute( # type: ignore[arg-type] + QoSAttribute.QoS_Vendor_Specific_Attribute, + vendor=32473, subtype=3, data=payload) + self.assertEqual(attr.vendor, 32473) + self.assertEqual(attr.subtype, 3) + self.assertEqual(attr.data, payload, 'the vendor payload was dropped') + self.assertEqual(attr.length, 7 + len(payload)) + self.assertEqual(len(attr.pack()), attr.length + 2) + + # every family's unassigned fallback takes a ``data`` keyword too + unassigned = [ + ('qos attribute', proto._make_qos_attribute, QoSAttribute(200)), # type: ignore[arg-type] + ('flow id sub-option', proto._make_fid_suboption, FlowIDSuboption(200)), # type: ignore[arg-type] + ('ani sub-option', proto._make_ani_suboption, ANISuboption(200)), # type: ignore[arg-type] + ('lcmp sub-option', proto._make_lcmp_suboption, # type: ignore[arg-type] + LMAControlledMAGSuboption(200)), + ] + for label, maker, code in unassigned: + with self.subTest(label): + built = maker(code, data=payload) + self.assertEqual(built.data, payload, 'the sub-option data was dropped') + self.assertEqual(built.length, len(payload)) + + # and the traffic selector sub-option, whose payload field is ``selector`` + # rather than ``data`` -- included so the two spellings stay distinguished + selector = proto._make_fid_suboption( # type: ignore[arg-type] + FlowIDSuboption.Traffic_Selector, ts_format=1, selector=payload) + self.assertEqual(selector.selector, payload) + self.assertEqual(selector.length, 2 + len(payload)) + + # finally the same thing through the public interface, since that is how a + # caller meets it: the attribute has to survive being nested in an option + # and packed + option = proto._make_opt_qos( # type: ignore[arg-type] + Option.Quality_of_Service, sr_id=1, dscp=46, oc=1, + attributes=[(QoSAttribute.QoS_Vendor_Specific_Attribute, + {'vendor': 32473, 'subtype': 3, 'data': payload})]) + self.assertIn(payload, option.pack()) + def test_mh_cga_parameters_option_is_unparsable_upstream(self) -> None: """The CGA Parameters option cannot be parsed, and this is not new. From d306dcacc4120fe5a86c06584b4a11517440b39c Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Thu, 17 Sep 2026 18:46:35 -0400 Subject: [PATCH 3/4] docs: file the mobility-header defects rather than describing them on the wish list The Help Wanted page tracks feature requests. Three of the things this PR added to it are defects in code that already exists, which belong in the issue tracker where they can be triaged, not in prose on a wish list where they cannot. - The two faults blocking the CGA Parameters option are now #445 (a nested schema cannot reach the enclosing packet's fields by name) and #446 (a ForwardMatchField's non-consuming bytes count towards the schema's length). Both are shared field machinery, and both must be fixed for the option to parse -- which is why the half-fix was reverted. The pinning test stays. - The MN-ID constructor mis-sizing is now #448. Measuring it turned out worse than reported: it sizes from the Python type of `identifier` rather than from `subtype`, so the method's own default arguments emit a 19-octet option declaring length 3. - `http.HTTP`'s explicit `version=` path is now #447. That paragraph is edited to match what has landed on `main`, so the two do not conflict on merge; the UDP/TCP port asymmetry above it stays, being a decision rather than a bug. The opaque-payload item stays, because it is a genuine request, and now records the approach rather than only the situation: carry those payloads as `Raw` rather than bare `bytes`, dispatched through a per-payload registry in the style of `MH.__option__`, keyed on the field that already names the format. `Raw` is what an unregistered dispatch falls back to everywhere else, so registering a real dissector later needs no change at the option site. It deliberately does not use `_decode_next_layer`: that is only called at a layer boundary and appends to the frame's protocol chain, and an MLD record inside a mobility option did not follow MH on the wire, so it would make `layer=` and `protocol=` limits behave wrongly. Docs only -- no code, no tests, no behaviour change. --- docs/source/pep.rst | 64 +++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/docs/source/pep.rst b/docs/source/pep.rst index a1396aad13..befc91f3ee 100644 --- a/docs/source/pep.rst +++ b/docs/source/pep.rst @@ -173,31 +173,45 @@ mobility anchor address option codes of :rfc:`5949#section-6.2.2` What is left, and why: * **The CGA Parameters option** (type 12) is the one option still on the generic - handler, and it is unreachable rather than unimplemented: - :attr:`~pcapkit.protocols.schema.internet.mh.CGAParameter.extensions` sizes - itself from ``pkt['length']``, but :class:`CGAParameter - ` has no ``length`` field - and :class:`~pcapkit.corekit.fields.misc.SchemaField` gives a nested schema a - fresh packet context rather than the enclosing option's, so a well-formed - option raises ``KeyError: 'length'`` on parse and on construction alike. Making - the lookup optional gets past that and straight into a second fault, in how a - :class:`~pcapkit.corekit.fields.misc.ForwardMatchField` counts towards the - nested schema's length. Both halves live in shared field machinery rather than - in the mobility header, which is why this is recorded here rather than patched - around; ``test_mh_cga_parameters_option_is_unparsable_upstream`` pins the - current behaviour so the fix is noticed. -* **Payloads that belong to another protocol** are carried opaquely, deliberately. - The multicast options (54, 56, 57, 60 and 61) embed :rfc:`3810` MLD or - :rfc:`3376` IGMP address records, and the traffic selectors of :rfc:`6089` and - :rfc:`7222` embed the flag-driven range lists of :rfc:`6088`. Both are separate - registries with their own dissectors' worth of structure; the mobility options - around them are fully decoded, and each records which format its payload is in. -* **The MN-ID option's constructor mis-sizes a non-address identifier.** - ``_make_opt_mn_id`` measures ``len(identifier)`` even for the ``IPv6_Address`` - subtype, so passing a string or an integer declares that many octets while the - schema emits 16. Passing an :class:`~ipaddress.IPv6Address` is correct. This is - pre-existing and outside the registry-completion work, so it is noted rather - than fixed. + handler, and it is unreachable rather than unimplemented. Two faults in shared + field machinery stand in the way, both outside the mobility header and both now + tracked as defects rather than described here: + `#445 `__, a nested schema + cannot reach the enclosing packet's fields by name, and + `#446 `__, a + :class:`~pcapkit.corekit.fields.misc.ForwardMatchField`'s non-consuming bytes + count towards the schema's length. Both have to be fixed for this option to + parse, which is why the half-fix was reverted rather than shipped; + ``test_mh_cga_parameters_option_is_unparsable_upstream`` pins the current + behaviour so the day it starts working is visible. +* **Payloads that belong to another protocol** are carried opaquely for now. The + multicast options (54, 56, 57, 60 and 61) embed :rfc:`3810` MLD or :rfc:`3376` + IGMP address records, and the traffic selectors of :rfc:`6089` and :rfc:`7222` + embed the flag-driven range lists of :rfc:`6088`. Each is a separate registry + with its own dissector's worth of structure; the mobility options around them + are fully decoded, and each records which format its payload is in. + + What is wanted is to carry these as :class:`~pcapkit.protocols.misc.raw.Raw` + rather than as bare :obj:`bytes`, dispatched through a **per-payload registry** + in the style of :attr:`MH.__option__ `, + keyed on the field that already names the format -- + :attr:`~pcapkit.protocols.schema.internet.mh.TrafficSelectorSuboption.ts_format` + for the traffic selectors, and the mode flag for MLD against IGMP on the + multicast options. ``Raw`` is already what an unregistered dispatch falls back + to everywhere else in the package, so this makes the mobility header consistent + with the rest rather than inventing a convention; and once a dissector for one + of these formats exists, registering it needs no change at the option site. + + It wants a registry of its own rather than + :meth:`~pcapkit.protocols.protocol.Protocol._decode_next_layer`, which is + only ever called at a layer boundary and appends to the frame's protocol chain. + An MLD address record inside a mobility option did not follow MH on the wire, + so putting it in that chain would make ``layer=`` and ``protocol=`` limits + behave wrongly. Changing the parsed shape from :obj:`bytes` to ``Raw`` also + changes what existing captures dump to, so it is its own change. +* **The MN-ID option's constructor mis-sizes a non-address identifier**, tracked + as `#448 `__. Pre-existing + and outside the registry-completion work, so it is filed rather than fixed here. Two wire-format traps are worth knowing before touching this code, since both look like ordinary fields and are not: From 8a4929ccf89314f2ae0cc012bb66a2c8fe409afc Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Thu, 17 Sep 2026 20:27:22 -0400 Subject: [PATCH 4/4] tests: give four MH options their required minimum content, and record the CGA extension carrier PR #440's round-trip harness enumerates the registries, so completing `MH.__option__` and `MH.__extension__` added 64 cases to it -- and seven of them were red. Two different reasons, and only one of them is a defect. Four are the harness constructing an option with no arguments at all, which for these four is not a well-formed instance of the option: * Service Selection: `Length` of 0 "is not allowed" and the identifier is 1-255 octets [RFC 5149 section 3]. * Redirect: "Both the 'K' and 'N' flags cannot be set or unset simultaneously" [RFC 6463 section 4.2], so with neither address given the option's own length is undetermined. * Access Network Identifier: "MUST contain at least one ANI sub-option" [RFC 6757 section 3]; it is a pure container. * LMA-Controlled MAG Parameters: likewise at least one sub-option [RFC 8127 section 3]. The constructors refuse all four correctly, so the validations stay and `_mh_option_overrides` gains the arguments that make the codes reachable -- which is what that table is already for, alongside the eleven MH options whose no-argument default is likewise invalid. This raises coverage rather than avoiding anything: all four now round-trip carrying real content, e.g. the ANI option emits `340f 010d 0004 77696669 06 001122334455`. The other three are `mh-extension/Exp_FFF*`, and they are #445 rather than mine. Measured: all four extension codes -- including `Multi_Prefix`, which the table already records -- fail identically with `PARSE / KeyError: 'length'`, raised from `SchemaField.unpack` at `corekit/fields/misc.py:619` by way of `CGAParameter.extensions`, before any extension schema is unpacked at all. A CGA extension has no carrier but the CGA Parameters option, so the whole registry is unreachable until #445 and #446 land. Three `EXPECTED_FAILURES` entries record that, grouped with the existing one and naming the same site; that entry's `file:line` is also refreshed, since this branch moved the lambda from :516 to :873. `pep.rst` claimed all four CGA extensions round-trip byte-for-byte. Their handlers do, when driven directly, but nothing can reach them through the public API, so the claim is narrowed to what is actually true and points at the recorded gap instead. Suite on 3.14: 973 passed, 17 skipped, 1544 subtests. On 3.10: 899 passed, 91 skipped, 1446 subtests. Baseline e2d8ed6d1 on 3.14: 942 passed, 1256 subtests. The option harness is 7 passed / 363 subtests on both interpreters, and `make_samples.py` regenerates all 24 captures byte-identically. --- docs/source/pep.rst | 20 +++++++++++-- examples/generators/options.py | 28 +++++++++++++++++++ tests/protocols/test_option_roundtrip_unit.py | 19 +++++++++++-- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/docs/source/pep.rst b/docs/source/pep.rst index b36471b9bd..69cb75224a 100644 --- a/docs/source/pep.rst +++ b/docs/source/pep.rst @@ -153,8 +153,19 @@ registry: **all 24 registered message data types**, **all 4 CGA extensions**, an :attr:`~pcapkit.protocols.internet.mh.MH.__message__`, :attr:`~pcapkit.protocols.internet.mh.MH.__option__` or :attr:`~pcapkit.protocols.internet.mh.MH.__extension__` with both a ``_read_`` -and a ``_make_`` handler, and every one round-trips byte-for-byte -- -``make`` then ``read`` then ``make`` again reproduces the same octets. +and a ``_make_`` handler. + +Every message type and every one of those options round-trips byte-for-byte +through the public API -- ``make`` then ``read`` then ``make`` again reproduces +the same octets, which +:file:`tests/protocols/test_option_roundtrip_unit.py` checks for the whole +registry. The **four CGA extensions are the exception, and not because of their +own handlers**: those round-trip when driven directly, but the CGA Parameters +option is the only thing that can carry a CGA extension on the wire, and that +option cannot be parsed at all for the reasons below. So all four are recorded +in that test's ``EXPECTED_FAILURES`` as ``PARSE`` failures against +`#445 `__ rather than claimed +as working end to end. The sub-registries turned out to be the easy half, as predicted: binding revocation types and triggers, handoff indicators, access network identifier @@ -183,7 +194,10 @@ What is left, and why: count towards the schema's length. Both have to be fixed for this option to parse, which is why the half-fix was reverted rather than shipped; ``test_mh_cga_parameters_option_is_unparsable_upstream`` pins the current - behaviour so the day it starts working is visible. + behaviour so the day it starts working is visible. This option is also what + makes the whole :attr:`~pcapkit.protocols.internet.mh.MH.__extension__` + registry unreachable, since it is the only carrier a CGA extension has -- + fixing it turns four ``EXPECTED_FAILURES`` entries green at once. * **Payloads that belong to another protocol** are carried opaquely for now. The multicast options (54, 56, 57, 60 and 61) embed :rfc:`3810` MLD or :rfc:`3376` IGMP address records, and the traffic selectors of :rfc:`6089` and :rfc:`7222` diff --git a/examples/generators/options.py b/examples/generators/options.py index b7b09b57da..bd0b050641 100644 --- a/examples/generators/options.py +++ b/examples/generators/options.py @@ -797,6 +797,9 @@ def _mh_option_registry() -> 'Any': def _mh_option_overrides() -> 'dict[Any, dict[str, Any]]': + from pcapkit.const.mh.ani_suboption import ANISuboption as Enum_ANISuboption + from pcapkit.const.mh.lma_mag_suboption import \ + LMAControlledMAGSuboption as Enum_LMAControlledMAGSuboption from pcapkit.const.mh.option import Option as Enum_Option import ipaddress return { @@ -820,6 +823,31 @@ def _mh_option_overrides() -> 'dict[Any, dict[str, Any]]': Enum_Option.Permanent_Home_Keygen_Token: {'token': b'\xbb' * 8}, Enum_Option.Experimental_Mobility_Option: {'data': b'\xcc' * 4}, Enum_Option.Binding_Authorization_Data_for_FMIPv6: {'data': b'\xb5'}, + # The four below are not workarounds for defects: each option has a + # minimum content its RFC requires, and the no-argument default is not a + # well-formed instance of it. The constructors refuse it, correctly, so + # the arguments here are what make the code reachable at all. + # + # ``Length`` of 0 "is not allowed" and the identifier is 1-255 octets + # [RFC 5149 section 3]. ``'ims'`` is that RFC's own example. + Enum_Option.Service_Selection_Mobility_Option: {'identifier': 'ims'}, + # "Both the 'K' and 'N' flags cannot be set or unset simultaneously" + # [RFC 6463 section 4.2], so exactly one address is present and the + # option is 18 octets long for IPv6 or 6 for IPv4. With neither given + # the option's own length is undetermined. + Enum_Option.Redirect_Mobility_Option: { + 'ipv6': ipaddress.IPv6Address('2001:db8::1')}, + # The option "MUST contain at least one ANI sub-option" + # [RFC 6757 section 3]; it is a pure container and carries nothing else. + Enum_Option.Access_Network_Identifier: { + 'suboptions': [(Enum_ANISuboption.Network_Identifier, + {'net_name': b'wifi', + 'ap_name': b'\x00\x11\x22\x33\x44\x55'})]}, + # Likewise at least one LCMP sub-option [RFC 8127 section 3]. + Enum_Option.LMA_Controlled_MAG_Parameters: { + 'suboptions': [(Enum_LMAControlledMAGSuboption.Heartbeat_Control, + {'interval': 60, 'retransmission_delay': 3, + 'max_retransmissions': 5})]}, } diff --git a/tests/protocols/test_option_roundtrip_unit.py b/tests/protocols/test_option_roundtrip_unit.py index f7830b7a65..e04553f456 100644 --- a/tests/protocols/test_option_roundtrip_unit.py +++ b/tests/protocols/test_option_roundtrip_unit.py @@ -328,10 +328,25 @@ class Gap(NamedTuple): # is present while packing and absent while unpacking: ``SchemaField.unpack`` # starts the nested schema with a fresh context whose parent is under # ``__packet__``. A CGA extension has no other carrier, so the whole - # ``MH.__extension__`` registry is unreachable through the public API. + # ``MH.__extension__`` registry is unreachable through the public API -- + # every code in it fails here, identically, before its own schema is ever + # unpacked. That is #445, and it is why all four entries below name one site + # in ``CGAParameter`` rather than anything in the extensions themselves. 'mh-extension/Multi_Prefix': Gap( 'PARSE', "KeyError: 'length'", - 'pcapkit/protocols/schema/internet/mh.py:516 -- needs ' + 'pcapkit/protocols/schema/internet/mh.py:873 -- #445; needs ' + "pkt['__packet__']['length'] on the unpack path"), + 'mh-extension/Exp_FFFD': Gap( + 'PARSE', "KeyError: 'length'", + 'pcapkit/protocols/schema/internet/mh.py:873 -- #445; needs ' + "pkt['__packet__']['length'] on the unpack path"), + 'mh-extension/Exp_FFFE': Gap( + 'PARSE', "KeyError: 'length'", + 'pcapkit/protocols/schema/internet/mh.py:873 -- #445; needs ' + "pkt['__packet__']['length'] on the unpack path"), + 'mh-extension/Exp_FFFF': Gap( + 'PARSE', "KeyError: 'length'", + 'pcapkit/protocols/schema/internet/mh.py:873 -- #445; needs ' "pkt['__packet__']['length'] on the unpack path"), # -- HIP ------------------------------------------------------------------