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 7f78eec4c8..a848eafbf0 100644 --- a/docs/source/pep.rst +++ b/docs/source/pep.rst @@ -146,42 +146,101 @@ 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. + +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 +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. 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. 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` + 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: + +* :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/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/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..edfee69b2e 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,6265 @@ 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 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. + + Returns: + Parsed sub-option data. + + """ + suboptions = OrderedMultiDict() # type: FlowIDSuboption + + for schema in suboptions_schema: + 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=code, + length=size, + ) # type: Data_FlowIdentificationSuboption + elif code == Enum_FlowIDSuboption.BID_Reference: + bid_ref = cast('Schema_BIDReferenceSuboption', schema) + data = Data_BIDReferenceSuboption( + type=code, + length=bid_ref.length + 2, + bid=tuple(bid_ref.bid), + ) + elif code == Enum_FlowIDSuboption.Traffic_Selector: + selector = cast('Schema_TrafficSelectorSuboption', schema) + data = Data_TrafficSelectorSuboption( + type=code, + length=selector.length + 2, + ts_format=selector.ts_format, + selector=selector.selector, + ) + elif code == Enum_FlowIDSuboption.Flow_Binding_Action: + action = cast('Schema_FlowBindingActionSuboption', schema) + data = Data_FlowBindingActionSuboption( + type=code, + length=action.length + 2, + action=action.action, + ) + elif code == Enum_FlowIDSuboption.Target_Care_of_Address: + target = cast('Schema_TargetCareofAddressSuboption', schema) + data = Data_TargetCareofAddressSuboption( + type=code, + length=target.length + 2, + address=target.address, + ) + else: + unknown = cast('Schema_UnassignedFlowIdentificationSuboption', schema) + data = Data_UnassignedFlowIdentificationSuboption( + type=code, + length=unknown.length + 2, + data=unknown.data, + ) + + suboptions.add(code, 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. + + 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. + + Returns: + Parsed sub-option data. + + """ + suboptions = OrderedMultiDict() # type: ANISuboption + + for schema in suboptions_schema: + code = schema.type + + if code == Enum_ANISuboption.Network_Identifier: + net = cast('Schema_ANINetworkIdentifierSuboption', schema) + data = Data_ANINetworkIdentifierSuboption( + 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 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=code, + length=geo.length + 2, + latitude=raw_lat / 2 ** 15, + longitude=raw_lon / 2 ** 15, + raw_latitude=raw_lat, + raw_longitude=raw_lon, + ) + elif code == Enum_ANISuboption.Operator_Identifier: + operator = cast('Schema_ANIOperatorIdentifierSuboption', schema) + data = Data_ANIOperatorIdentifierSuboption( + type=code, + length=operator.length + 2, + op_id_type=operator.op_id_type, + identifier=operator.identifier, + ) + elif code == Enum_ANISuboption.Civic_Location: + civic = cast('Schema_ANICivicLocationSuboption', schema) + data = Data_ANICivicLocationSuboption( + type=code, + length=civic.length + 2, + format=civic.format, + location=civic.location, + ) + elif code == Enum_ANISuboption.MAG_Group_Identifier: + group = cast('Schema_ANIMAGGroupIdentifierSuboption', schema) + data = Data_ANIMAGGroupIdentifierSuboption( + type=code, + length=group.length + 2, + group_id=group.group_id, + ) + elif code == Enum_ANISuboption.ANI_Update_Timer: + timer = cast('Schema_ANIUpdateTimerSuboption', schema) + data = Data_ANIUpdateTimerSuboption( + type=code, + length=timer.length + 2, + timer=datetime.timedelta(seconds=timer.timer * 4), + ) + else: + unknown = cast('Schema_UnassignedANISuboption', schema) + data = Data_UnassignedANISuboption( + type=code, + length=unknown.length + 2, + data=unknown.data, + ) + + suboptions.add(code, 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. + + 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. + + Returns: + Parsed attribute data. + + """ + attributes = OrderedMultiDict() # type: QoSAttribute + + for schema in attributes_schema: + 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=code, + length=session.length + 2, + service=bool(session.flags['S']), + exclude=bool(session.flags['E']), + rate=session.rate, + ) # type: Data_QoSAttribute + 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=code, + length=rate.length + 2, + rate=rate.rate, + ) + elif code == Enum_QoSAttribute.Allocation_Retention_Priority: + arp = cast('Schema_AllocationRetentionPriorityAttribute', schema) + data = Data_AllocationRetentionPriorityAttribute( + type=code, + length=arp.length + 2, + priority_level=arp.priority['PL'], + preemption_capability=arp.priority['PC'], + preemption_vulnerability=arp.priority['PV'], + ) + elif code == Enum_QoSAttribute.QoS_Traffic_Selector: + selector = cast('Schema_QoSTrafficSelectorAttribute', schema) + data = Data_QoSTrafficSelectorAttribute( + type=code, + length=selector.length + 2, + ts_format=selector.ts_format, + selector=selector.selector, + ) + elif code == Enum_QoSAttribute.QoS_Vendor_Specific_Attribute: + vendor = cast('Schema_QoSVendorSpecificAttribute', schema) + data = Data_QoSVendorSpecificAttribute( + 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=code, + length=unknown.length + 2, + data=unknown.data, + ) + + attributes.add(code, 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`]. + + 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. + + Returns: + Parsed sub-option data. + + """ + suboptions = OrderedMultiDict() # type: LMAControlledMAGSuboption + + for schema in suboptions_schema: + code = schema.type + + if code == Enum_LMAControlledMAGSuboption.Binding_Re_registration_Control: + rereg = cast('Schema_BindingReregistrationControlSuboption', schema) + data = Data_BindingReregistrationControlSuboption( + type=code, + length=rereg.length + 2, + start_time=datetime.timedelta(seconds=rereg.start_time * 4), + initial_retransmission=datetime.timedelta( + seconds=rereg.initial_retransmission), + max_retransmission=datetime.timedelta(seconds=rereg.max_retransmission), + ) # type: Data_LMAControlledMAGSuboption + elif code == Enum_LMAControlledMAGSuboption.Heartbeat_Control: + heartbeat = cast('Schema_HeartbeatControlSuboption', schema) + data = Data_HeartbeatControlSuboption( + type=code, + length=heartbeat.length + 2, + interval=datetime.timedelta(seconds=heartbeat.interval), + retransmission_delay=datetime.timedelta( + seconds=heartbeat.retransmission_delay), + max_retransmissions=heartbeat.max_retransmissions, + ) + else: + unknown = cast('Schema_UnassignedLMAControlledMAGSuboption', schema) + data = Data_UnassignedLMAControlledMAGSuboption( + type=code, + length=unknown.length + 2, + data=unknown.data, + ) + + suboptions.add(code, 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_opt_restart(self, type: 'Enum_Option', option: 'Optional[Data_RestartCounterOption]' = None, *, + counter: 'int' = 0, + **kwargs: 'Any') -> 'Schema_RestartCounterOption': + """Make MH restart counter option. + + Args: + type: Option type. + option: Option data model. + counter: Restart counter. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + 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_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_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: - 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. + 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 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: + status_val = option.status # type: Enum_DSMIPv6HomeAddress + 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_IPv4AddressAcknowledgementOption( + type=type, + length=6, + status=status_val, + flags={'prefix_length': prefix_length}, + address=address, + ) + + 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: + 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 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 option is not None: + code_val = int(option.code) + address = option.address + else: + code_val = self._make_index(code, code_default, namespace=code_namespace, + reversed=code_reversed, pack=False) + + 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: - options = options or [] + addr = ipaddress.ip_address(address) - return Schema_HomeTestMessage( - nonce_index=nonce_index, - cookie=cookie, - token=token, - options=self._make_mh_options(options), + return Schema_LMAAddressOption( + type=type, + length=6 if addr.version == 4 else 18, + code=code_val, + address=addr, ) - 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_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. - 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. + iid: Interface identifier; exactly 8 octets. **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 - else: - options = options or [] + if option is not None: + iid = option.iid - return Schema_CareofTestMessage( - nonce_index=nonce_index, - cookie=cookie, - token=token, - 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_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_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. - 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. + 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: - 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 + if option is not None: + late = option.late + lifetime_val = math.ceil(option.lifetime.total_seconds() * 10) else: - 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_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 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_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_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. - 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. + 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 - key_mngt = message.key_mngt - seq = message.seq - lifetime_val = math.ceil(message.lifetime.total_seconds()) - 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) - lifetime_val = lifetime if isinstance(lifetime, int) else math.ceil(lifetime.total_seconds()) - options = options or [] + fid = fid 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), + return Schema_FlowSummaryOption( + type=type, + length=len(fid) * 2, + fid=fid, ) - 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_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. - 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. + 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: - 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 [] + 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_BindingErrorMessage( - status=status_val, - home=home, - options=self._make_mh_options(options), - ) + return [self._make_fid_suboption(code, option) + for code, option in suboptions.items(multi=True)] - 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_suboption(self, code: 'Enum_FlowIDSuboption', + option: 'Optional[Data_FlowIdentificationSuboption]' = None, + **kwargs: 'Any') -> 'Schema_FlowIdentificationSuboption': + """Make one MH flow identification sub-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. - **kwargs: Arbitrary keyword arguments. + code: Sub-option type. + option: Sub-option data model. + **kwargs: Sub-option fields, when no data model is given. Returns: - Constructed message type. + 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 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 code in (Enum_FlowIDSuboption.Pad, Enum_FlowIDSuboption.PadN): + if code == Enum_FlowIDSuboption.Pad: + pad_len = 0 + 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) - 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), - ) + 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 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', + 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 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)) + return Schema_FlowBindingActionSuboption( + type=code, length=2, action=cast('Enum_FlowBindingAction', action)) + + if code == Enum_FlowIDSuboption.Target_Care_of_Address: + if option is not None: + address = cast('Data_TargetCareofAddressSuboption', option).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) - 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. + if option is not None: + payload = cast('Data_UnassignedFlowIdentificationSuboption', option).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. - 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. + 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: - 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: + fid = option.fid + fid_pri = option.fid_pri + status_val = option.status # type: Enum_FlowIDStatus + suboptions = option.suboptions 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 [] + suboptions = suboptions or [] - return Schema_FastBindingAcknowledgmentMessage( + 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, - flags={ - 'K': key_mngt, - }, - seq=seq, - lifetime=math.ceil(lifetime_val / 4), - options=self._make_mh_options(options), + suboptions=entries, ) - 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. + 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. - options: Mobility options. + type: Option type. + option: Option data model. **kwargs: Arbitrary keyword arguments. Returns: - Constructed message type. + Constructed option schema. """ - if message is not None: - options = message.options - else: - options = options or [] - - return Schema_FastNeighborAdvertisementMessage( - options=self._make_mh_options(options), + return Schema_RedirectCapabilityOption( + type=type, + length=2, ) - 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_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. - data: Experimental message data. + 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: - data = message.data + if option is not None: + ipv6 = option.ipv6 + ipv4 = option.ipv4 - return Schema_ExperimentalMessage( - data=data, + 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={ + 'K': int(ipv6 is not None), + 'N': int(ipv4 is not None), + }, + ipv6=ipv6, + ipv4=ipv4, ) - 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_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. - 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. + 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 - 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: + 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_HandoverInitiateMessage( - seq=seq, - flags={ - 'S': assign, - '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, ) - 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_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: - 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. + address: Alternate IPv4 care-of address. **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: + address = option.address - return Schema_HandoverAcknowledgeMessage( - seq=seq, - flags={ - 'U': buffer, - 'P': proxy, - 'F': forward, - }, - code=code_val, - options=self._make_mh_options(options), + return Schema_AlternateIPv4CareofAddressOption( + type=type, + length=4, + address=address, ) - # 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_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: - 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. + 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: - 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: + 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) - 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_MNGroupIdentifierOption( + type=type, + length=6, + subtype=subtype_val, + group_id=group_id, + ) - 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_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. Args: - data: Constructed message data. + 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. 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: + address_length = option.address_length + address = option.address - 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_MAGIPv6AddressOption( + type=type, + length=18, + address_length=address_length, + address=address, + ) - """ - pad_opts, pad_len = self._make_pad_options(len(data) + 6) - if pad_len == 0: - return data + 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. - 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: + suboptions: Sub-options, as parsed data, schemas, ``(type, kwargs)`` + pairs, or raw octets. - # 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: + Sub-option schema list. - # 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 + """ + 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_ANISuboption, dict[str, Any]]', item) + entries.append(self._make_ani_suboption(code, **args)) + return entries - 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. + return [self._make_ani_suboption(code, option) + for code, option in suboptions.items(multi=True)] + + def _make_ani_suboption(self, code: 'Enum_ANISuboption', + option: 'Optional[Data_ANISuboption]' = None, + **kwargs: 'Any') -> 'Schema_ANISuboption': + """Make one MH access network identifier sub-option. Args: - options: MH options. + code: Sub-option type. + option: Sub-option data model. + **kwargs: Sub-option fields, when no data model is given. Returns: - Mobility options list. + Constructed sub-option schema. - """ - 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)) + 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. - 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) + 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`. - options_list.append(data) - return options_list + """ + if code == Enum_ANISuboption.Network_Identifier: + 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'')) + 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 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)) + return Schema_ANIGeoLocationSuboption( + type=code, length=6, location={ + 'latitude': raw_lat & 0xFFFFFF, + 'longitude': raw_lon & 0xFFFFFF, + }) + + if code == Enum_ANISuboption.Operator_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)) + 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 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'')) + return Schema_ANICivicLocationSuboption( + type=code, length=2 + len(location), format=fmt, location=location) + + if code == Enum_ANISuboption.MAG_Group_Identifier: + 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) - 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)) + if code == Enum_ANISuboption.ANI_Update_Timer: + if option is not None: + timer = math.ceil(cast('Data_ANIUpdateTimerSuboption', + option).timer.total_seconds() / 4) else: - meth = name[1] + 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) - data = meth(code, option) - options_list.append(data) - return options_list + 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) - def _make_opt_none(self, type: 'Enum_Option', option: 'Optional[Data_UnassignedOption]' = None, *, - data: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_UnassignedOption': - """Make MH unassigned option. + 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 +9379,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 +9454,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 +9529,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,57 +9558,160 @@ 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: - type: Option type. - option: Option data model. - prefix: Mobile network prefix. - **kwargs: Arbitrary keyword arguments. + attributes: Attributes, as parsed data, schemas, ``(type, kwargs)`` + pairs, or raw octets. Returns: - Constructed option schema. + Attribute schema list. """ - if option is not None: - prefix = option.prefix + 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 - 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 [self._make_qos_attribute(code, option) + for code, option in attributes.items(multi=True)] - return Schema_MobileNetworkPrefixOption( - type=type, - length=18, - prefix_length=prefix_length, - prefix=prefix_addr, - ) + def _make_qos_attribute(self, code: 'Enum_QoSAttribute', + option: 'Optional[Data_QoSAttribute]' = None, + **kwargs: 'Any') -> 'Schema_QoSAttribute': + """Make one MH quality-of-service attribute. - 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: + code: Attribute type. + 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 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)) + 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 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 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)) + 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 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', + 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 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)) + payload = cast('bytes', kwargs.get('data', b'')) + return Schema_QoSVendorSpecificAttribute( + type=code, length=7 + len(payload), vendor=vendor, subtype=subtype, + data=payload) + + 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) + + 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. - address: Link-layer address. + 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: @@ -3353,32 +9719,40 @@ def _make_opt_lla(self, type: 'Enum_Option', option: 'Optional[Data_LinkLayerAdd """ if option is not None: - address = option.lla + sr_id = option.sr_id + dscp = option.dscp + oc = option.oc + attributes = option.attributes + else: + attributes = attributes or [] - return Schema_LinkLayerAddressOption( + if dscp > 0x3F: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid DSCP: {dscp}') + + 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=len(address) + 1, - code=Enum_LLACode.MH, # type: ignore[arg-type] - lla=address, + length=length, + sr_id=sr_id, + tc=dscp << 2, + oc=oc, + attributes=entries, ) - 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_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. - 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. + 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: @@ -3386,189 +9760,309 @@ def _make_opt_mn_id(self, type: 'Enum_Option', option: 'Optional[Data_MNIDOption """ 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) + address = option.address - 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 address is None: + return Schema_LMAUserPlaneAddressOption(type=type, length=2, address=b'') - return Schema_MNIDOption( + addr = address if isinstance( + address, (ipaddress.IPv4Address, ipaddress.IPv6Address) + ) else ipaddress.ip_address(address) + + return Schema_LMAUserPlaneAddressOption( type=type, - length=1 + id_len, - subtype=subtype_val, - identifier=identifier, + length=6 if addr.version == 4 else 18, + address=addr, ) - 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(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: 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. + 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 - spi = option.spi + code = option.code 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_MulticastMobilityOption( type=type, - length=5 + len(data), - subtype=subtype_val, - spi=spi, + length=len(data) // 4, + code=code, 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_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. - timestamp: NTP timestamp, c.f., :rfc:`1305`. - interval: Timestamp interval (since UNIX-epoch). + 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: - 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 + code = option.code + status = option.status + data = option.data - timestamp = NTPTimestamp(seconds=ts_sec + 2_208_988_800, # 70 years - fraction=ts_frc) + 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_MesgIDOption( + return Schema_MulticastAcknowledgementOption( type=type, - length=8, - seconds=timestamp.seconds, - fraction=timestamp.fraction, + length=len(data) // 4, + code=code, + status=status, + data=data, ) - def _make_opt_cga_pr(self, type: 'Enum_Option', option: 'Optional[Data_CGAParametersRequestOption]' = None, - **kwargs: 'Any') -> 'Schema_CGAParametersRequestOption': - """Make MH CGA parameters request 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: + suboptions: Sub-options, as parsed data, schemas, ``(type, kwargs)`` + pairs, or raw octets. + + Returns: + Sub-option schema list. + + """ + 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 + + 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': + """Return an interval in wire units. + + 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. + + Returns: + The interval in wire units. + + """ + if isinstance(value, int): + return value + return math.ceil(value.total_seconds() / unit) + + def _make_lcmp_suboption(self, code: 'Enum_LMAControlledMAGSuboption', + option: 'Optional[Data_LMAControlledMAGSuboption]' = None, + **kwargs: 'Any') -> 'Schema_LMAControlledMAGSuboption': + """Make one MH LMA-controlled MAG parameters sub-option. + + Args: + code: Sub-option type. + 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 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)) + 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 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)) + count = cast('int', kwargs.get('max_retransmissions', 0)) + return Schema_HeartbeatControlSuboption( + type=code, length=6, interval=interval, + retransmission_delay=delay, max_retransmissions=count) + + if option is not None: + payload = cast('Data_UnassignedLMAControlledMAGSuboption', option).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 +10070,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 +10101,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 +10165,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 +10196,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 +10244,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 +10351,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..c6ba345fb7 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,1588 @@ 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 + [:rfc:`7563#section-3.1`].""" + + #: 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 + [:rfc:`7563#section-3.3`].""" + + #: 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 +2821,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..4a52c56799 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,714 @@ 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_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. + + :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() diff --git a/tests/protocols/test_option_roundtrip_unit.py b/tests/protocols/test_option_roundtrip_unit.py index d1669d4cac..9eda19c983 100644 --- a/tests/protocols/test_option_roundtrip_unit.py +++ b/tests/protocols/test_option_roundtrip_unit.py @@ -324,10 +324,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 ------------------------------------------------------------------