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: std::sync::Arc<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_handle() 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    direct_fmp_endpoint_allowed: bool,
174}
175
176impl EndpointDataPayload {
177    pub(crate) fn new(bytes: Vec<u8>) -> Self {
178        let traffic_class = classify_endpoint_payload(&bytes);
179        Self {
180            bytes,
181            traffic_class,
182            direct_fmp_endpoint_allowed: false,
183        }
184    }
185
186    pub(crate) fn from_classified_with_direct_fmp_endpoint_allowed(
187        bytes: Vec<u8>,
188        traffic_class: EndpointPayloadClass,
189        direct_fmp_endpoint_allowed: bool,
190    ) -> Self {
191        Self {
192            bytes,
193            traffic_class,
194            direct_fmp_endpoint_allowed,
195        }
196    }
197
198    #[cfg(test)]
199    pub(crate) fn allow_direct_fmp_endpoint_data(mut self) -> Self {
200        self.direct_fmp_endpoint_allowed = true;
201        self
202    }
203
204    pub(crate) fn lane(&self) -> EndpointCommandLane {
205        self.traffic_class.lane().command_lane()
206    }
207
208    pub(crate) fn bulk_endpoint_data(&self) -> bool {
209        self.traffic_class.lane() == EndpointPayloadLane::Bulk
210    }
211
212    pub(crate) fn drop_on_backpressure(&self) -> bool {
213        self.traffic_class.drop_on_backpressure()
214    }
215
216    pub(crate) fn direct_fmp_endpoint_allowed(&self) -> bool {
217        self.direct_fmp_endpoint_allowed
218    }
219
220    pub(crate) fn as_slice(&self) -> &[u8] {
221        &self.bytes
222    }
223
224    pub(crate) fn len(&self) -> usize {
225        self.bytes.len()
226    }
227}
228
229impl From<Vec<u8>> for EndpointDataPayload {
230    fn from(bytes: Vec<u8>) -> Self {
231        Self::new(bytes)
232    }
233}
234
235/// Outbound endpoint data plus the peer identity it is bound to.
236#[derive(Debug)]
237pub(crate) struct EndpointDataSend {
238    dest_addr: NodeAddr,
239    dest_pubkey: secp256k1::PublicKey,
240    payload: EndpointDataPayload,
241}
242
243impl EndpointDataSend {
244    pub(crate) fn new(remote: PeerIdentity, payload: EndpointDataPayload) -> Self {
245        Self {
246            dest_addr: *remote.node_addr(),
247            dest_pubkey: remote.pubkey_full(),
248            payload,
249        }
250    }
251
252    pub(crate) fn dest_addr(&self) -> NodeAddr {
253        self.dest_addr
254    }
255
256    pub(crate) fn dest_pubkey(&self) -> secp256k1::PublicKey {
257        self.dest_pubkey
258    }
259
260    pub(crate) fn payload(&self) -> &EndpointDataPayload {
261        &self.payload
262    }
263
264    pub(crate) fn into_payload(self) -> EndpointDataPayload {
265        self.payload
266    }
267}
268
269/// Admission result for a bounded pending endpoint-data queue.
270#[derive(Clone, Copy, Debug, PartialEq, Eq)]
271pub(crate) struct PendingEndpointDataQueueAdmission {
272    dropped_oldest: bool,
273}
274
275impl PendingEndpointDataQueueAdmission {
276    pub(crate) fn dropped_oldest(&self) -> bool {
277        self.dropped_oldest
278    }
279}
280
281/// Per-destination endpoint payloads waiting for session establishment.
282#[derive(Debug, Default)]
283pub(crate) struct PendingEndpointDataQueue {
284    payloads: VecDeque<EndpointDataPayload>,
285}
286
287impl PendingEndpointDataQueue {
288    pub(crate) fn push_bounded(
289        &mut self,
290        payload: EndpointDataPayload,
291        capacity: usize,
292    ) -> PendingEndpointDataQueueAdmission {
293        let dropped_oldest = self.payloads.len() >= capacity;
294        if dropped_oldest {
295            self.payloads.pop_front();
296        }
297        self.payloads.push_back(payload);
298        PendingEndpointDataQueueAdmission { dropped_oldest }
299    }
300
301    pub(crate) fn len(&self) -> usize {
302        self.payloads.len()
303    }
304
305    pub(crate) fn into_payloads(self) -> VecDeque<EndpointDataPayload> {
306        self.payloads
307    }
308
309    #[cfg(test)]
310    pub(crate) fn iter(&self) -> impl Iterator<Item = &EndpointDataPayload> {
311        self.payloads.iter()
312    }
313}
314
315/// Admission result for a bounded pending TUN packet queue.
316#[derive(Clone, Copy, Debug, PartialEq, Eq)]
317pub(crate) struct PendingTunPacketQueueAdmission {
318    dropped_oldest: bool,
319}
320
321impl PendingTunPacketQueueAdmission {
322    pub(crate) fn dropped_oldest(&self) -> bool {
323        self.dropped_oldest
324    }
325}
326
327/// Per-destination TUN packets waiting for session establishment.
328#[derive(Debug, Default)]
329pub(crate) struct PendingTunPacketQueue {
330    packets: VecDeque<Vec<u8>>,
331}
332
333impl PendingTunPacketQueue {
334    pub(crate) fn push_bounded(
335        &mut self,
336        packet: Vec<u8>,
337        capacity: usize,
338    ) -> PendingTunPacketQueueAdmission {
339        let dropped_oldest = self.packets.len() >= capacity;
340        if dropped_oldest {
341            self.packets.pop_front();
342        }
343        self.packets.push_back(packet);
344        PendingTunPacketQueueAdmission { dropped_oldest }
345    }
346
347    pub(crate) fn len(&self) -> usize {
348        self.packets.len()
349    }
350
351    pub(crate) fn into_packets(self) -> VecDeque<Vec<u8>> {
352        self.packets
353    }
354
355    #[cfg(test)]
356    pub(crate) fn iter(&self) -> impl Iterator<Item = &Vec<u8>> {
357        self.packets.iter()
358    }
359}
360
361/// Admission result for pending session-establishment traffic.
362#[derive(Clone, Copy, Debug, PartialEq, Eq)]
363pub(crate) struct PendingSessionTrafficAdmission {
364    destination_dropped: bool,
365    dropped_oldest: bool,
366}
367
368impl PendingSessionTrafficAdmission {
369    pub(crate) fn destination_dropped(&self) -> bool {
370        self.destination_dropped
371    }
372
373    pub(crate) fn dropped_oldest(&self) -> bool {
374        self.dropped_oldest
375    }
376}
377
378/// Queued TUN and endpoint traffic removed for one destination.
379#[derive(Debug, Default)]
380pub(crate) struct PendingDestinationTraffic {
381    tun_packets: Option<PendingTunPacketQueue>,
382    endpoint_data: Option<PendingEndpointDataQueue>,
383}
384
385impl PendingDestinationTraffic {
386    pub(crate) fn tun_packets(&self) -> Option<&PendingTunPacketQueue> {
387        self.tun_packets.as_ref()
388    }
389
390    pub(crate) fn into_tun_packets(self) -> Option<PendingTunPacketQueue> {
391        self.tun_packets
392    }
393
394    pub(crate) fn endpoint_data(&self) -> Option<&PendingEndpointDataQueue> {
395        self.endpoint_data.as_ref()
396    }
397}
398
399/// Pending traffic waiting for session establishment.
400#[derive(Debug, Default)]
401pub(crate) struct PendingSessionTrafficQueues {
402    pending_destinations: HashSet<NodeAddr>,
403    tun_packets: HashMap<NodeAddr, PendingTunPacketQueue>,
404    endpoint_data: HashMap<NodeAddr, PendingEndpointDataQueue>,
405}
406
407impl PendingSessionTrafficQueues {
408    pub(crate) fn push_tun_packet(
409        &mut self,
410        dest_addr: NodeAddr,
411        packet: Vec<u8>,
412        max_destinations: usize,
413        packets_per_dest: usize,
414    ) -> PendingSessionTrafficAdmission {
415        if !self.tun_packets.contains_key(&dest_addr) && self.tun_packets.len() >= max_destinations
416        {
417            return PendingSessionTrafficAdmission {
418                destination_dropped: true,
419                dropped_oldest: false,
420            };
421        }
422
423        let admission = self
424            .tun_packets
425            .entry(dest_addr)
426            .or_default()
427            .push_bounded(packet, packets_per_dest);
428        self.pending_destinations.insert(dest_addr);
429        PendingSessionTrafficAdmission {
430            destination_dropped: false,
431            dropped_oldest: admission.dropped_oldest(),
432        }
433    }
434
435    pub(crate) fn push_endpoint_data(
436        &mut self,
437        dest_addr: NodeAddr,
438        payload: impl Into<EndpointDataPayload>,
439        max_destinations: usize,
440        packets_per_dest: usize,
441    ) -> PendingSessionTrafficAdmission {
442        if !self.endpoint_data.contains_key(&dest_addr)
443            && self.endpoint_data.len() >= max_destinations
444        {
445            return PendingSessionTrafficAdmission {
446                destination_dropped: true,
447                dropped_oldest: false,
448            };
449        }
450
451        let admission = self
452            .endpoint_data
453            .entry(dest_addr)
454            .or_default()
455            .push_bounded(payload.into(), packets_per_dest);
456        self.pending_destinations.insert(dest_addr);
457        PendingSessionTrafficAdmission {
458            destination_dropped: false,
459            dropped_oldest: admission.dropped_oldest(),
460        }
461    }
462
463    pub(crate) fn remove_destination(&mut self, dest_addr: &NodeAddr) -> PendingDestinationTraffic {
464        self.pending_destinations.remove(dest_addr);
465        PendingDestinationTraffic {
466            tun_packets: self.tun_packets.remove(dest_addr),
467            endpoint_data: self.endpoint_data.remove(dest_addr),
468        }
469    }
470
471    pub(crate) fn take_tun_packets(
472        &mut self,
473        dest_addr: &NodeAddr,
474    ) -> Option<PendingTunPacketQueue> {
475        let packets = self.tun_packets.remove(dest_addr);
476        if packets.is_some() && !self.endpoint_data.contains_key(dest_addr) {
477            self.pending_destinations.remove(dest_addr);
478        }
479        packets
480    }
481
482    pub(crate) fn take_endpoint_data(
483        &mut self,
484        dest_addr: &NodeAddr,
485    ) -> Option<PendingEndpointDataQueue> {
486        let payloads = self.endpoint_data.remove(dest_addr);
487        if payloads.is_some() && !self.tun_packets.contains_key(dest_addr) {
488            self.pending_destinations.remove(dest_addr);
489        }
490        payloads
491    }
492
493    pub(crate) fn has_traffic_for(&self, dest_addr: &NodeAddr) -> bool {
494        self.pending_destinations.contains(dest_addr)
495    }
496
497    pub(crate) fn tun_packets_for(&self, dest_addr: &NodeAddr) -> Option<&PendingTunPacketQueue> {
498        self.tun_packets.get(dest_addr)
499    }
500
501    pub(crate) fn endpoint_data_for(
502        &self,
503        dest_addr: &NodeAddr,
504    ) -> Option<&PendingEndpointDataQueue> {
505        self.endpoint_data.get(dest_addr)
506    }
507
508    pub(crate) fn tun_destination_count(&self) -> usize {
509        self.tun_packets.len()
510    }
511
512    pub(crate) fn tun_packet_count(&self) -> usize {
513        self.tun_packets.values().map(|q| q.len()).sum()
514    }
515}
516
517fn endpoint_tcp_payload_is_latency_sensitive(payload: &[u8], tcp_offset: usize) -> bool {
518    const TCP_MIN_HEADER_LEN: usize = 20;
519    const TCP_FLAG_FIN: u8 = 0x01;
520    const TCP_FLAG_SYN: u8 = 0x02;
521    const TCP_FLAG_RST: u8 = 0x04;
522    const INTERACTIVE_TCP_PAYLOAD_MAX: usize = 256;
523
524    if payload.len() < tcp_offset + TCP_MIN_HEADER_LEN {
525        return true;
526    }
527
528    let tcp_header_len = usize::from(payload[tcp_offset + 12] >> 4) * 4;
529    if tcp_header_len < TCP_MIN_HEADER_LEN || payload.len() < tcp_offset + tcp_header_len {
530        return true;
531    }
532
533    let flags = payload[tcp_offset + 13];
534    if flags & (TCP_FLAG_FIN | TCP_FLAG_SYN | TCP_FLAG_RST) != 0 {
535        return true;
536    }
537
538    let payload_len = endpoint_ip_payload_len(payload)
539        .and_then(|ip_payload_len| ip_payload_len.checked_sub(tcp_header_len))
540        .unwrap_or_else(|| payload.len().saturating_sub(tcp_offset + tcp_header_len));
541    payload_len <= INTERACTIVE_TCP_PAYLOAD_MAX
542}
543
544fn endpoint_ip_payload_len(payload: &[u8]) -> Option<usize> {
545    const IPV4_MIN_HEADER_LEN: usize = 20;
546    const IPV6_HEADER_LEN: usize = 40;
547
548    let version_ihl = payload.first().copied()?;
549    match version_ihl >> 4 {
550        4 => {
551            if payload.len() < IPV4_MIN_HEADER_LEN {
552                return None;
553            }
554            let header_len = usize::from(version_ihl & 0x0f) * 4;
555            if header_len < IPV4_MIN_HEADER_LEN || payload.len() < header_len {
556                return None;
557            }
558            let total_len = usize::from(u16::from_be_bytes([payload[2], payload[3]]));
559            total_len.checked_sub(header_len)
560        }
561        6 => {
562            if payload.len() < IPV6_HEADER_LEN {
563                return None;
564            }
565            Some(usize::from(u16::from_be_bytes([payload[4], payload[5]])))
566        }
567        _ => None,
568    }
569}
570
571fn parse_endpoint_payload_ip_proto(payload: &[u8]) -> Option<(u8, usize)> {
572    const IPV4_MIN_HEADER_LEN: usize = 20;
573
574    let version_ihl = payload.first().copied()?;
575
576    match version_ihl >> 4 {
577        4 => {
578            if payload.len() < IPV4_MIN_HEADER_LEN {
579                return None;
580            }
581            let header_len = usize::from(version_ihl & 0x0f) * 4;
582            if header_len >= IPV4_MIN_HEADER_LEN && payload.len() >= header_len {
583                Some((payload[9], header_len))
584            } else {
585                None
586            }
587        }
588        6 => ipv6_payload_next_header(payload),
589        _ => None,
590    }
591}
592
593#[derive(Clone, Copy)]
594struct EndpointFlowParts<'a> {
595    version: u8,
596    proto: u8,
597    src: &'a [u8],
598    dst: &'a [u8],
599    ports: Option<[u8; 4]>,
600}
601
602impl EndpointFlowParts<'_> {
603    fn hash(self) -> u64 {
604        let mut h = EndpointFlowHasher::default();
605        h.write_u8(self.version);
606        h.write_u8(self.proto);
607        h.write(self.src);
608        h.write(self.dst);
609        if let Some(ports) = self.ports {
610            h.write(&ports);
611        }
612        h.finish()
613    }
614}
615
616#[derive(Clone, Copy)]
617struct EndpointFlowHasher(u64);
618
619impl Default for EndpointFlowHasher {
620    fn default() -> Self {
621        Self(0x9ae1_6a3b_2f90_404f)
622    }
623}
624
625impl EndpointFlowHasher {
626    fn write_u8(&mut self, value: u8) {
627        self.write(&[value]);
628    }
629
630    fn write(&mut self, bytes: &[u8]) {
631        for byte in bytes {
632            self.0 ^= u64::from(*byte);
633            self.0 = self.0.wrapping_mul(0x1000_0000_01b3);
634            self.0 ^= self.0 >> 32;
635        }
636    }
637
638    fn finish(self) -> u64 {
639        self.0
640    }
641}
642
643fn endpoint_payload_flow_parts(payload: &[u8]) -> Option<EndpointFlowParts<'_>> {
644    const IPV4_MIN_HEADER_LEN: usize = 20;
645    const IPV6_HEADER_LEN: usize = 40;
646
647    let version = payload.first().copied()? >> 4;
648    match version {
649        4 => {
650            if payload.len() < IPV4_MIN_HEADER_LEN {
651                return None;
652            }
653            let header_len = usize::from(payload[0] & 0x0f) * 4;
654            if header_len < IPV4_MIN_HEADER_LEN || payload.len() < header_len {
655                return None;
656            }
657            let fragment_bits = u16::from_be_bytes([payload[6], payload[7]]) & 0x3fff;
658            Some(EndpointFlowParts {
659                version,
660                proto: payload[9],
661                src: &payload[12..16],
662                dst: &payload[16..20],
663                ports: if fragment_bits == 0 {
664                    endpoint_transport_ports(payload, payload[9], header_len)
665                } else {
666                    None
667                },
668            })
669        }
670        6 => {
671            if payload.len() < IPV6_HEADER_LEN {
672                return None;
673            }
674            let (proto, offset, fragmented) = ipv6_payload_next_header_with_fragment(payload)?;
675            Some(EndpointFlowParts {
676                version,
677                proto,
678                src: &payload[8..24],
679                dst: &payload[24..40],
680                ports: if fragmented {
681                    None
682                } else {
683                    endpoint_transport_ports(payload, proto, offset)
684                },
685            })
686        }
687        _ => None,
688    }
689}
690
691fn endpoint_transport_ports(payload: &[u8], proto: u8, transport_offset: usize) -> Option<[u8; 4]> {
692    const IPPROTO_TCP: u8 = 6;
693    const IPPROTO_UDP: u8 = 17;
694    const IPPROTO_SCTP: u8 = 132;
695
696    if !matches!(proto, IPPROTO_TCP | IPPROTO_UDP | IPPROTO_SCTP) {
697        return None;
698    }
699    let ports = payload.get(transport_offset..transport_offset + 4)?;
700    Some([ports[0], ports[1], ports[2], ports[3]])
701}
702
703#[cfg(test)]
704pub(in crate::node) fn endpoint_payload_is_tcp(payload: &[u8]) -> bool {
705    const IPPROTO_TCP: u8 = 6;
706    parse_endpoint_payload_ip_proto(payload).is_some_and(|(proto, _)| proto == IPPROTO_TCP)
707}
708
709fn ipv6_payload_next_header(payload: &[u8]) -> Option<(u8, usize)> {
710    ipv6_payload_next_header_with_fragment(payload)
711        .map(|(next_header, offset, _)| (next_header, offset))
712}
713
714fn ipv6_payload_next_header_with_fragment(payload: &[u8]) -> Option<(u8, usize, bool)> {
715    const IPV6_HEADER_LEN: usize = 40;
716    const IPV6_FRAGMENT_HEADER_LEN: usize = 8;
717
718    if payload.len() < IPV6_HEADER_LEN || payload[0] >> 4 != 6 {
719        return None;
720    }
721
722    let mut next_header = payload[6];
723    let mut offset = IPV6_HEADER_LEN;
724    let mut extension_count = 0usize;
725    let mut fragmented = false;
726    while ipv6_extension_header_is_skippable(next_header) {
727        if next_header == 44 {
728            if payload.len() < offset + IPV6_FRAGMENT_HEADER_LEN {
729                return None;
730            }
731            fragmented = true;
732            next_header = payload[offset];
733            offset += IPV6_FRAGMENT_HEADER_LEN;
734        } else if next_header == 51 {
735            if payload.len() < offset + 2 {
736                return None;
737            }
738            let header_len = (usize::from(payload[offset + 1]) + 2) * 4;
739            if payload.len() < offset + header_len {
740                return None;
741            }
742            next_header = payload[offset];
743            offset += header_len;
744        } else {
745            if payload.len() < offset + 2 {
746                return None;
747            }
748            let header_len = (usize::from(payload[offset + 1]) + 1) * 8;
749            if payload.len() < offset + header_len {
750                return None;
751            }
752            next_header = payload[offset];
753            offset += header_len;
754        }
755        extension_count += 1;
756        if extension_count > 8 {
757            return None;
758        }
759    }
760
761    Some((next_header, offset, fragmented))
762}
763
764fn ipv6_extension_header_is_skippable(next_header: u8) -> bool {
765    matches!(next_header, 0 | 43 | 44 | 51 | 60 | 135)
766}