Skip to main content

fips_core/node/
endpoint_traffic.rs

1use super::*;
2
3#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
4pub(in crate::node) struct FmpPlaintextTrafficClass {
5    pub(in crate::node) bulk_endpoint_data: bool,
6    pub(in crate::node) drop_on_backpressure: bool,
7}
8
9/// Priority/bulk lane selected for an app-owned endpoint payload.
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11pub enum EndpointPayloadLane {
12    #[default]
13    Priority,
14    Bulk,
15}
16
17impl EndpointPayloadLane {
18    fn command_lane(self) -> EndpointCommandLane {
19        match self {
20            Self::Priority => EndpointCommandLane::Priority,
21            Self::Bulk => EndpointCommandLane::Bulk,
22        }
23    }
24}
25
26/// Traffic policy selected for an app-owned endpoint payload.
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
28pub struct EndpointPayloadClass {
29    lane: EndpointPayloadLane,
30    drop_on_backpressure: bool,
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub(in crate::node) struct EndpointFlowDispatchKey(u64);
35
36impl EndpointFlowDispatchKey {
37    pub(in crate::node) fn get(self) -> u64 {
38        self.0
39    }
40}
41
42impl EndpointPayloadClass {
43    pub fn lane(self) -> EndpointPayloadLane {
44        self.lane
45    }
46
47    pub fn is_latency_sensitive(self) -> bool {
48        self.lane == EndpointPayloadLane::Priority
49    }
50
51    pub fn drop_on_backpressure(self) -> bool {
52        self.drop_on_backpressure
53    }
54}
55
56#[cfg(unix)]
57pub(in crate::node) struct FmpWorkerSendReservation {
58    pub(in crate::node) counter: u64,
59    pub(in crate::node) header: [u8; ESTABLISHED_HEADER_SIZE],
60    pub(in crate::node) cipher: ring::aead::LessSafeKey,
61}
62
63#[cfg(unix)]
64pub(in crate::node) fn reserve_fmp_worker_send(
65    session: &mut crate::noise::NoiseSession,
66    their_index: crate::utils::index::SessionIndex,
67    flags: u8,
68    payload_len: u16,
69) -> Result<Option<FmpWorkerSendReservation>, crate::noise::NoiseError> {
70    let Some(cipher) = session.send_cipher_clone() else {
71        return Ok(None);
72    };
73    let counter = session.take_send_counter()?;
74    let header = build_established_header(their_index, counter, flags, payload_len);
75    Ok(Some(FmpWorkerSendReservation {
76        counter,
77        header,
78        cipher,
79    }))
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub(crate) enum EndpointCommandLane {
84    Priority,
85    Bulk,
86}
87
88pub(in crate::node) fn classify_fmp_plaintext_traffic(
89    plaintext: &[u8],
90) -> FmpPlaintextTrafficClass {
91    let bulk_endpoint_data = fmp_plaintext_is_bulk_session_datagram(plaintext);
92    // At this layer established FSP payloads are already end-to-end encrypted,
93    // so a bulk SessionDatagram may still be TCP endpoint traffic. Keep it out
94    // of the control lane, but only the pre-FSP endpoint path may mark known
95    // non-TCP packets as discardable under sender backpressure.
96    FmpPlaintextTrafficClass {
97        bulk_endpoint_data,
98        drop_on_backpressure: false,
99    }
100}
101
102pub(in crate::node) fn fmp_plaintext_is_bulk_session_datagram(plaintext: &[u8]) -> bool {
103    if plaintext
104        .first()
105        .is_none_or(|ty| *ty != LinkMessageType::SessionDatagram.to_byte())
106    {
107        return false;
108    }
109    let Some(fsp_payload) = plaintext.get(crate::protocol::SESSION_DATAGRAM_HEADER_SIZE..) else {
110        return false;
111    };
112    FspCommonPrefix::parse(fsp_payload).is_some_and(|prefix| {
113        prefix.phase == FSP_PHASE_ESTABLISHED && !prefix.is_unencrypted() && !prefix.has_coords()
114    })
115}
116
117pub(in crate::node) fn endpoint_flow_dispatch_key(
118    payload: &[u8],
119) -> Option<EndpointFlowDispatchKey> {
120    endpoint_payload_flow_parts(payload).map(|parts| EndpointFlowDispatchKey(parts.hash()))
121}
122
123/// Classify an app-owned endpoint payload for queue admission and pressure policy.
124pub fn classify_endpoint_payload(payload: &[u8]) -> EndpointPayloadClass {
125    const IPPROTO_ICMP: u8 = 1;
126    const IPPROTO_TCP: u8 = 6;
127    const IPPROTO_ICMPV6: u8 = 58;
128
129    match parse_endpoint_payload_ip_proto(payload) {
130        Some((IPPROTO_ICMP, _)) => EndpointPayloadClass::default(),
131        Some((IPPROTO_ICMPV6, _)) => EndpointPayloadClass::default(),
132        Some((IPPROTO_TCP, offset)) => {
133            let latency_sensitive = endpoint_tcp_payload_is_latency_sensitive(payload, offset);
134            EndpointPayloadClass {
135                lane: if latency_sensitive {
136                    EndpointPayloadLane::Priority
137                } else {
138                    EndpointPayloadLane::Bulk
139                },
140                drop_on_backpressure: false,
141            }
142        }
143        _ => EndpointPayloadClass {
144            lane: EndpointPayloadLane::Bulk,
145            drop_on_backpressure: true,
146        },
147    }
148}
149
150/// Return true when an app-owned endpoint payload should retain priority-lane progress.
151///
152/// Embedders that stage packets before calling `FipsEndpoint::send*_to_peer`
153/// can use this to apply the same priority/bulk policy as the FIPS endpoint
154/// command queue without duplicating IP/TCP parsing.
155pub fn endpoint_payload_is_latency_sensitive(payload: &[u8]) -> bool {
156    classify_endpoint_payload(payload).is_latency_sensitive()
157}
158
159#[cfg(test)]
160pub(crate) fn endpoint_command_lane_for_payload(payload: &[u8]) -> EndpointCommandLane {
161    if endpoint_payload_is_latency_sensitive(payload) {
162        EndpointCommandLane::Priority
163    } else {
164        EndpointCommandLane::Bulk
165    }
166}
167
168/// Endpoint payload bytes plus the traffic policy selected at app ingress.
169#[derive(Clone, Debug, PartialEq, Eq)]
170pub(crate) struct EndpointDataPayload {
171    bytes: Vec<u8>,
172    traffic_class: EndpointPayloadClass,
173}
174
175impl EndpointDataPayload {
176    pub(crate) fn new(bytes: Vec<u8>) -> Self {
177        let traffic_class = classify_endpoint_payload(&bytes);
178        Self {
179            bytes,
180            traffic_class,
181        }
182    }
183
184    pub(crate) fn from_classified(bytes: Vec<u8>, traffic_class: EndpointPayloadClass) -> Self {
185        Self {
186            bytes,
187            traffic_class,
188        }
189    }
190
191    pub(crate) fn lane(&self) -> EndpointCommandLane {
192        self.traffic_class.lane().command_lane()
193    }
194
195    pub(crate) fn bulk_endpoint_data(&self) -> bool {
196        self.traffic_class.lane() == EndpointPayloadLane::Bulk
197    }
198
199    pub(crate) fn drop_on_backpressure(&self) -> bool {
200        self.traffic_class.drop_on_backpressure()
201    }
202
203    pub(crate) fn as_slice(&self) -> &[u8] {
204        &self.bytes
205    }
206
207    pub(crate) fn len(&self) -> usize {
208        self.bytes.len()
209    }
210}
211
212impl From<Vec<u8>> for EndpointDataPayload {
213    fn from(bytes: Vec<u8>) -> Self {
214        Self::new(bytes)
215    }
216}
217
218/// Outbound endpoint data plus the peer identity it is bound to.
219#[derive(Debug)]
220pub(crate) struct EndpointDataSend {
221    dest_addr: NodeAddr,
222    dest_pubkey: secp256k1::PublicKey,
223    payload: EndpointDataPayload,
224}
225
226impl EndpointDataSend {
227    pub(crate) fn new(remote: PeerIdentity, payload: EndpointDataPayload) -> Self {
228        Self {
229            dest_addr: *remote.node_addr(),
230            dest_pubkey: remote.pubkey_full(),
231            payload,
232        }
233    }
234
235    pub(crate) fn dest_addr(&self) -> NodeAddr {
236        self.dest_addr
237    }
238
239    pub(crate) fn dest_pubkey(&self) -> secp256k1::PublicKey {
240        self.dest_pubkey
241    }
242
243    pub(crate) fn payload(&self) -> &EndpointDataPayload {
244        &self.payload
245    }
246
247    pub(crate) fn into_payload(self) -> EndpointDataPayload {
248        self.payload
249    }
250}
251
252/// Admission result for a bounded pending endpoint-data queue.
253#[derive(Clone, Copy, Debug, PartialEq, Eq)]
254pub(crate) struct PendingEndpointDataQueueAdmission {
255    dropped_oldest: bool,
256}
257
258impl PendingEndpointDataQueueAdmission {
259    pub(crate) fn dropped_oldest(&self) -> bool {
260        self.dropped_oldest
261    }
262}
263
264/// Per-destination endpoint payloads waiting for session establishment.
265#[derive(Debug, Default)]
266pub(crate) struct PendingEndpointDataQueue {
267    payloads: VecDeque<EndpointDataPayload>,
268}
269
270impl PendingEndpointDataQueue {
271    pub(crate) fn push_bounded(
272        &mut self,
273        payload: EndpointDataPayload,
274        capacity: usize,
275    ) -> PendingEndpointDataQueueAdmission {
276        let dropped_oldest = self.payloads.len() >= capacity;
277        if dropped_oldest {
278            self.payloads.pop_front();
279        }
280        self.payloads.push_back(payload);
281        PendingEndpointDataQueueAdmission { dropped_oldest }
282    }
283
284    pub(crate) fn len(&self) -> usize {
285        self.payloads.len()
286    }
287
288    pub(crate) fn into_payloads(self) -> VecDeque<EndpointDataPayload> {
289        self.payloads
290    }
291
292    #[cfg(test)]
293    pub(crate) fn iter(&self) -> impl Iterator<Item = &EndpointDataPayload> {
294        self.payloads.iter()
295    }
296}
297
298/// Admission result for a bounded pending TUN packet queue.
299#[derive(Clone, Copy, Debug, PartialEq, Eq)]
300pub(crate) struct PendingTunPacketQueueAdmission {
301    dropped_oldest: bool,
302}
303
304impl PendingTunPacketQueueAdmission {
305    pub(crate) fn dropped_oldest(&self) -> bool {
306        self.dropped_oldest
307    }
308}
309
310/// Per-destination TUN packets waiting for session establishment.
311#[derive(Debug)]
312pub(crate) struct PendingTunPacket {
313    packet: Vec<u8>,
314    queued_at_ms: u64,
315}
316
317impl PendingTunPacket {
318    fn new(packet: Vec<u8>, queued_at_ms: u64) -> Self {
319        Self {
320            packet,
321            queued_at_ms,
322        }
323    }
324
325    fn is_stale(&self, now_ms: u64, max_age_ms: u64) -> bool {
326        now_ms.saturating_sub(self.queued_at_ms) > max_age_ms
327    }
328}
329
330/// Per-destination TUN packets waiting for session establishment.
331#[derive(Debug, Default)]
332pub(crate) struct PendingTunPacketQueue {
333    packets: VecDeque<PendingTunPacket>,
334}
335
336impl PendingTunPacketQueue {
337    pub(crate) fn push_bounded(
338        &mut self,
339        packet: Vec<u8>,
340        queued_at_ms: u64,
341        capacity: usize,
342    ) -> PendingTunPacketQueueAdmission {
343        let dropped_oldest = self.packets.len() >= capacity;
344        if dropped_oldest {
345            self.packets.pop_front();
346        }
347        self.packets
348            .push_back(PendingTunPacket::new(packet, queued_at_ms));
349        PendingTunPacketQueueAdmission { dropped_oldest }
350    }
351
352    pub(crate) fn len(&self) -> usize {
353        self.packets.len()
354    }
355
356    pub(crate) fn into_packets(self) -> VecDeque<Vec<u8>> {
357        self.packets
358            .into_iter()
359            .map(|packet| packet.packet)
360            .collect()
361    }
362
363    pub(crate) fn into_fresh_packets(
364        self,
365        now_ms: u64,
366        max_age_ms: u64,
367    ) -> (VecDeque<Vec<u8>>, usize) {
368        let mut fresh = VecDeque::with_capacity(self.packets.len());
369        let mut stale = 0usize;
370        for packet in self.packets {
371            if packet.is_stale(now_ms, max_age_ms) {
372                stale = stale.saturating_add(1);
373            } else {
374                fresh.push_back(packet.packet);
375            }
376        }
377        (fresh, stale)
378    }
379
380    #[cfg(test)]
381    pub(crate) fn iter(&self) -> impl Iterator<Item = &Vec<u8>> {
382        self.packets.iter().map(|packet| &packet.packet)
383    }
384}
385
386/// Admission result for pending session-establishment traffic.
387#[derive(Clone, Copy, Debug, PartialEq, Eq)]
388pub(crate) struct PendingSessionTrafficAdmission {
389    destination_dropped: bool,
390    dropped_oldest: bool,
391}
392
393impl PendingSessionTrafficAdmission {
394    pub(crate) fn destination_dropped(&self) -> bool {
395        self.destination_dropped
396    }
397
398    pub(crate) fn dropped_oldest(&self) -> bool {
399        self.dropped_oldest
400    }
401}
402
403/// Queued TUN and endpoint traffic removed for one destination.
404#[derive(Debug, Default)]
405pub(crate) struct PendingDestinationTraffic {
406    tun_packets: Option<PendingTunPacketQueue>,
407    endpoint_data: Option<PendingEndpointDataQueue>,
408}
409
410impl PendingDestinationTraffic {
411    pub(crate) fn tun_packets(&self) -> Option<&PendingTunPacketQueue> {
412        self.tun_packets.as_ref()
413    }
414
415    pub(crate) fn into_tun_packets(self) -> Option<PendingTunPacketQueue> {
416        self.tun_packets
417    }
418
419    pub(crate) fn endpoint_data(&self) -> Option<&PendingEndpointDataQueue> {
420        self.endpoint_data.as_ref()
421    }
422}
423
424/// Pending traffic waiting for session establishment.
425#[derive(Debug, Default)]
426pub(crate) struct PendingSessionTrafficQueues {
427    pending_destinations: HashSet<NodeAddr>,
428    tun_packets: HashMap<NodeAddr, PendingTunPacketQueue>,
429    endpoint_data: HashMap<NodeAddr, PendingEndpointDataQueue>,
430}
431
432impl PendingSessionTrafficQueues {
433    pub(crate) fn push_tun_packet(
434        &mut self,
435        dest_addr: NodeAddr,
436        packet: Vec<u8>,
437        max_destinations: usize,
438        packets_per_dest: usize,
439    ) -> PendingSessionTrafficAdmission {
440        if !self.tun_packets.contains_key(&dest_addr) && self.tun_packets.len() >= max_destinations
441        {
442            return PendingSessionTrafficAdmission {
443                destination_dropped: true,
444                dropped_oldest: false,
445            };
446        }
447
448        let admission = self.tun_packets.entry(dest_addr).or_default().push_bounded(
449            packet,
450            crate::time::now_ms(),
451            packets_per_dest,
452        );
453        self.pending_destinations.insert(dest_addr);
454        PendingSessionTrafficAdmission {
455            destination_dropped: false,
456            dropped_oldest: admission.dropped_oldest(),
457        }
458    }
459
460    pub(crate) fn push_endpoint_data(
461        &mut self,
462        dest_addr: NodeAddr,
463        payload: impl Into<EndpointDataPayload>,
464        max_destinations: usize,
465        packets_per_dest: usize,
466    ) -> PendingSessionTrafficAdmission {
467        if !self.endpoint_data.contains_key(&dest_addr)
468            && self.endpoint_data.len() >= max_destinations
469        {
470            return PendingSessionTrafficAdmission {
471                destination_dropped: true,
472                dropped_oldest: false,
473            };
474        }
475
476        let admission = self
477            .endpoint_data
478            .entry(dest_addr)
479            .or_default()
480            .push_bounded(payload.into(), packets_per_dest);
481        self.pending_destinations.insert(dest_addr);
482        PendingSessionTrafficAdmission {
483            destination_dropped: false,
484            dropped_oldest: admission.dropped_oldest(),
485        }
486    }
487
488    pub(crate) fn remove_destination(&mut self, dest_addr: &NodeAddr) -> PendingDestinationTraffic {
489        self.pending_destinations.remove(dest_addr);
490        PendingDestinationTraffic {
491            tun_packets: self.tun_packets.remove(dest_addr),
492            endpoint_data: self.endpoint_data.remove(dest_addr),
493        }
494    }
495
496    pub(crate) fn take_tun_packets(
497        &mut self,
498        dest_addr: &NodeAddr,
499    ) -> Option<PendingTunPacketQueue> {
500        let packets = self.tun_packets.remove(dest_addr);
501        if packets.is_some() && !self.endpoint_data.contains_key(dest_addr) {
502            self.pending_destinations.remove(dest_addr);
503        }
504        packets
505    }
506
507    pub(crate) fn take_endpoint_data(
508        &mut self,
509        dest_addr: &NodeAddr,
510    ) -> Option<PendingEndpointDataQueue> {
511        let payloads = self.endpoint_data.remove(dest_addr);
512        if payloads.is_some() && !self.tun_packets.contains_key(dest_addr) {
513            self.pending_destinations.remove(dest_addr);
514        }
515        payloads
516    }
517
518    pub(crate) fn has_traffic_for(&self, dest_addr: &NodeAddr) -> bool {
519        self.pending_destinations.contains(dest_addr)
520    }
521
522    pub(crate) fn tun_packets_for(&self, dest_addr: &NodeAddr) -> Option<&PendingTunPacketQueue> {
523        self.tun_packets.get(dest_addr)
524    }
525
526    pub(crate) fn endpoint_data_for(
527        &self,
528        dest_addr: &NodeAddr,
529    ) -> Option<&PendingEndpointDataQueue> {
530        self.endpoint_data.get(dest_addr)
531    }
532
533    pub(crate) fn tun_destination_count(&self) -> usize {
534        self.tun_packets.len()
535    }
536
537    pub(crate) fn tun_packet_count(&self) -> usize {
538        self.tun_packets.values().map(|q| q.len()).sum()
539    }
540}
541
542fn endpoint_tcp_payload_is_latency_sensitive(payload: &[u8], tcp_offset: usize) -> bool {
543    const TCP_MIN_HEADER_LEN: usize = 20;
544    const TCP_FLAG_FIN: u8 = 0x01;
545    const TCP_FLAG_SYN: u8 = 0x02;
546    const TCP_FLAG_RST: u8 = 0x04;
547    const INTERACTIVE_TCP_PAYLOAD_MAX: usize = 256;
548
549    if payload.len() < tcp_offset + TCP_MIN_HEADER_LEN {
550        return true;
551    }
552
553    let tcp_header_len = usize::from(payload[tcp_offset + 12] >> 4) * 4;
554    if tcp_header_len < TCP_MIN_HEADER_LEN || payload.len() < tcp_offset + tcp_header_len {
555        return true;
556    }
557
558    let flags = payload[tcp_offset + 13];
559    if flags & (TCP_FLAG_FIN | TCP_FLAG_SYN | TCP_FLAG_RST) != 0 {
560        return true;
561    }
562
563    let payload_len = endpoint_ip_payload_len(payload)
564        .and_then(|ip_payload_len| ip_payload_len.checked_sub(tcp_header_len))
565        .unwrap_or_else(|| payload.len().saturating_sub(tcp_offset + tcp_header_len));
566    payload_len <= INTERACTIVE_TCP_PAYLOAD_MAX
567}
568
569fn endpoint_ip_payload_len(payload: &[u8]) -> Option<usize> {
570    const IPV4_MIN_HEADER_LEN: usize = 20;
571    const IPV6_HEADER_LEN: usize = 40;
572
573    let version_ihl = payload.first().copied()?;
574    match version_ihl >> 4 {
575        4 => {
576            if payload.len() < IPV4_MIN_HEADER_LEN {
577                return None;
578            }
579            let header_len = usize::from(version_ihl & 0x0f) * 4;
580            if header_len < IPV4_MIN_HEADER_LEN || payload.len() < header_len {
581                return None;
582            }
583            let total_len = usize::from(u16::from_be_bytes([payload[2], payload[3]]));
584            total_len.checked_sub(header_len)
585        }
586        6 => {
587            if payload.len() < IPV6_HEADER_LEN {
588                return None;
589            }
590            Some(usize::from(u16::from_be_bytes([payload[4], payload[5]])))
591        }
592        _ => None,
593    }
594}
595
596fn parse_endpoint_payload_ip_proto(payload: &[u8]) -> Option<(u8, usize)> {
597    const IPV4_MIN_HEADER_LEN: usize = 20;
598
599    let version_ihl = payload.first().copied()?;
600
601    match version_ihl >> 4 {
602        4 => {
603            if payload.len() < IPV4_MIN_HEADER_LEN {
604                return None;
605            }
606            let header_len = usize::from(version_ihl & 0x0f) * 4;
607            if header_len >= IPV4_MIN_HEADER_LEN && payload.len() >= header_len {
608                Some((payload[9], header_len))
609            } else {
610                None
611            }
612        }
613        6 => ipv6_payload_next_header(payload),
614        _ => None,
615    }
616}
617
618#[derive(Clone, Copy)]
619struct EndpointFlowParts<'a> {
620    version: u8,
621    proto: u8,
622    src: &'a [u8],
623    dst: &'a [u8],
624    ports: Option<[u8; 4]>,
625}
626
627impl EndpointFlowParts<'_> {
628    fn hash(self) -> u64 {
629        let mut h = EndpointFlowHasher::default();
630        h.write_u8(self.version);
631        h.write_u8(self.proto);
632        h.write(self.src);
633        h.write(self.dst);
634        if let Some(ports) = self.ports {
635            h.write(&ports);
636        }
637        h.finish()
638    }
639}
640
641#[derive(Clone, Copy)]
642struct EndpointFlowHasher(u64);
643
644impl Default for EndpointFlowHasher {
645    fn default() -> Self {
646        Self(0x9ae1_6a3b_2f90_404f)
647    }
648}
649
650impl EndpointFlowHasher {
651    fn write_u8(&mut self, value: u8) {
652        self.write(&[value]);
653    }
654
655    fn write(&mut self, bytes: &[u8]) {
656        for byte in bytes {
657            self.0 ^= u64::from(*byte);
658            self.0 = self.0.wrapping_mul(0x1000_0000_01b3);
659            self.0 ^= self.0 >> 32;
660        }
661    }
662
663    fn finish(self) -> u64 {
664        self.0
665    }
666}
667
668fn endpoint_payload_flow_parts(payload: &[u8]) -> Option<EndpointFlowParts<'_>> {
669    const IPV4_MIN_HEADER_LEN: usize = 20;
670    const IPV6_HEADER_LEN: usize = 40;
671
672    let version = payload.first().copied()? >> 4;
673    match version {
674        4 => {
675            if payload.len() < IPV4_MIN_HEADER_LEN {
676                return None;
677            }
678            let header_len = usize::from(payload[0] & 0x0f) * 4;
679            if header_len < IPV4_MIN_HEADER_LEN || payload.len() < header_len {
680                return None;
681            }
682            let fragment_bits = u16::from_be_bytes([payload[6], payload[7]]) & 0x3fff;
683            Some(EndpointFlowParts {
684                version,
685                proto: payload[9],
686                src: &payload[12..16],
687                dst: &payload[16..20],
688                ports: if fragment_bits == 0 {
689                    endpoint_transport_ports(payload, payload[9], header_len)
690                } else {
691                    None
692                },
693            })
694        }
695        6 => {
696            if payload.len() < IPV6_HEADER_LEN {
697                return None;
698            }
699            let (proto, offset, fragmented) = ipv6_payload_next_header_with_fragment(payload)?;
700            Some(EndpointFlowParts {
701                version,
702                proto,
703                src: &payload[8..24],
704                dst: &payload[24..40],
705                ports: if fragmented {
706                    None
707                } else {
708                    endpoint_transport_ports(payload, proto, offset)
709                },
710            })
711        }
712        _ => None,
713    }
714}
715
716fn endpoint_transport_ports(payload: &[u8], proto: u8, transport_offset: usize) -> Option<[u8; 4]> {
717    const IPPROTO_TCP: u8 = 6;
718    const IPPROTO_UDP: u8 = 17;
719    const IPPROTO_SCTP: u8 = 132;
720
721    if !matches!(proto, IPPROTO_TCP | IPPROTO_UDP | IPPROTO_SCTP) {
722        return None;
723    }
724    let ports = payload.get(transport_offset..transport_offset + 4)?;
725    Some([ports[0], ports[1], ports[2], ports[3]])
726}
727
728#[cfg(test)]
729pub(in crate::node) fn endpoint_payload_is_tcp(payload: &[u8]) -> bool {
730    const IPPROTO_TCP: u8 = 6;
731    parse_endpoint_payload_ip_proto(payload).is_some_and(|(proto, _)| proto == IPPROTO_TCP)
732}
733
734fn ipv6_payload_next_header(payload: &[u8]) -> Option<(u8, usize)> {
735    ipv6_payload_next_header_with_fragment(payload)
736        .map(|(next_header, offset, _)| (next_header, offset))
737}
738
739fn ipv6_payload_next_header_with_fragment(payload: &[u8]) -> Option<(u8, usize, bool)> {
740    const IPV6_HEADER_LEN: usize = 40;
741    const IPV6_FRAGMENT_HEADER_LEN: usize = 8;
742
743    if payload.len() < IPV6_HEADER_LEN || payload[0] >> 4 != 6 {
744        return None;
745    }
746
747    let mut next_header = payload[6];
748    let mut offset = IPV6_HEADER_LEN;
749    let mut extension_count = 0usize;
750    let mut fragmented = false;
751    while ipv6_extension_header_is_skippable(next_header) {
752        if next_header == 44 {
753            if payload.len() < offset + IPV6_FRAGMENT_HEADER_LEN {
754                return None;
755            }
756            fragmented = true;
757            next_header = payload[offset];
758            offset += IPV6_FRAGMENT_HEADER_LEN;
759        } else if next_header == 51 {
760            if payload.len() < offset + 2 {
761                return None;
762            }
763            let header_len = (usize::from(payload[offset + 1]) + 2) * 4;
764            if payload.len() < offset + header_len {
765                return None;
766            }
767            next_header = payload[offset];
768            offset += header_len;
769        } else {
770            if payload.len() < offset + 2 {
771                return None;
772            }
773            let header_len = (usize::from(payload[offset + 1]) + 1) * 8;
774            if payload.len() < offset + header_len {
775                return None;
776            }
777            next_header = payload[offset];
778            offset += header_len;
779        }
780        extension_count += 1;
781        if extension_count > 8 {
782            return None;
783        }
784    }
785
786    Some((next_header, offset, fragmented))
787}
788
789fn ipv6_extension_header_is_skippable(next_header: u8) -> bool {
790    matches!(next_header, 0 | 43 | 44 | 51 | 60 | 135)
791}