Skip to main content

mcrx_core/raw/
shared.rs

1use crate::error::McrxError;
2use crate::packet::ReceiveMetadata;
3use crate::platform::{
4    RawCapturedDatagram, RawReceiveSocket, RawSharedCaptureKey, join_raw_multicast_group,
5    leave_raw_multicast_group, open_shared_raw_socket, recv_shared_raw_datagram,
6    shared_raw_capture_key,
7};
8use crate::raw::{RawPacket, RawSubscriptionConfig};
9use crate::subscription::{SubscriptionId, SubscriptionState};
10use std::collections::hash_map::Entry;
11use std::collections::{HashMap, VecDeque};
12use std::net::IpAddr;
13
14/// Limits for a [`SharedRawContext`].
15///
16/// The pending-packet limit bounds userspace memory when the caller is slower
17/// than the capture sockets. The subscription limit bounds the logical
18/// membership index, not the number of operating-system capture sockets.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct SharedRawContextLimits {
21    /// Maximum number of logical raw subscriptions stored in the context.
22    pub max_subscriptions: usize,
23    /// Maximum number of demultiplexed packets buffered for the caller.
24    pub max_pending_packets: usize,
25}
26
27impl Default for SharedRawContextLimits {
28    fn default() -> Self {
29        Self {
30            max_subscriptions: 4_096,
31            max_pending_packets: 1_024,
32        }
33    }
34}
35
36/// A logical raw multicast subscription managed by [`SharedRawContext`].
37///
38/// Multiple joined subscriptions can share one underlying capture socket when
39/// they use the same IP family and resolved interface. Duplicate logical
40/// subscriptions share a reference-counted kernel membership while each handle
41/// keeps its own lifecycle state.
42#[derive(Debug, Clone)]
43pub struct SharedRawSubscription {
44    id: SubscriptionId,
45    config: RawSubscriptionConfig,
46    capture_key: RawSharedCaptureKey,
47    state: SubscriptionState,
48}
49
50impl SharedRawSubscription {
51    fn new(
52        id: SubscriptionId,
53        config: RawSubscriptionConfig,
54        capture_key: RawSharedCaptureKey,
55    ) -> Self {
56        Self {
57            id,
58            config,
59            capture_key,
60            state: SubscriptionState::Bound,
61        }
62    }
63
64    /// Returns the subscription's ID.
65    pub fn id(&self) -> SubscriptionId {
66        self.id
67    }
68
69    /// Returns the immutable subscription configuration.
70    pub fn config(&self) -> &RawSubscriptionConfig {
71        &self.config
72    }
73
74    /// Returns the current lifecycle state.
75    pub fn state(&self) -> SubscriptionState {
76        self.state
77    }
78
79    /// Returns `true` when this logical subscription has joined its group.
80    pub fn is_joined(&self) -> bool {
81        matches!(self.state, SubscriptionState::Joined)
82    }
83}
84
85/// One raw IP datagram and every joined logical subscription that matched it.
86///
87/// The kernel datagram is captured once and the [`RawPacket`] uses the first
88/// matching ID in deterministic ascending order as its primary subscription.
89/// Use [`Self::matching_subscription_ids`] when overlapping logical
90/// memberships need to be distinguished.
91#[derive(Debug, Clone)]
92pub struct SharedRawPacket {
93    /// The received complete IP datagram and primary matching subscription ID.
94    pub packet: RawPacket,
95    matching_subscription_ids: MatchingSubscriptionIds,
96}
97
98impl SharedRawPacket {
99    /// Returns the captured complete IP datagram.
100    pub fn packet(&self) -> &RawPacket {
101        &self.packet
102    }
103
104    /// Returns every joined logical subscription matched by this datagram.
105    pub fn matching_subscription_ids(&self) -> &[SubscriptionId] {
106        self.matching_subscription_ids.as_slice()
107    }
108}
109
110#[derive(Debug, Clone)]
111enum MatchingSubscriptionIds {
112    One([SubscriptionId; 1]),
113    Many(Vec<SubscriptionId>),
114}
115
116impl MatchingSubscriptionIds {
117    fn from_sorted_slices(first: &[SubscriptionId], second: &[SubscriptionId]) -> Option<Self> {
118        match first.len() + second.len() {
119            0 => None,
120            1 => {
121                let id = match first.first() {
122                    Some(id) => *id,
123                    None => second[0],
124                };
125                Some(Self::One([id]))
126            }
127            total => Some(Self::Many(merge_ids(first, second, total))),
128        }
129    }
130
131    fn as_slice(&self) -> &[SubscriptionId] {
132        match self {
133            Self::One(id) => id,
134            Self::Many(ids) => ids,
135        }
136    }
137
138    fn first(&self) -> SubscriptionId {
139        self.as_slice()[0]
140    }
141
142    fn remove_existing(&mut self, index: usize) {
143        let Self::Many(ids) = self else {
144            unreachable!("a single matching subscription is removed with its queued packet");
145        };
146
147        ids.remove(index);
148        if ids.len() == 1 {
149            *self = Self::One([ids[0]]);
150        }
151    }
152}
153
154#[derive(Debug, Default)]
155struct GroupDemultiplexer {
156    any_source: Vec<SubscriptionId>,
157    sources: HashMap<IpAddr, Vec<SubscriptionId>>,
158}
159
160impl GroupDemultiplexer {
161    fn insert(&mut self, id: SubscriptionId, source: Option<IpAddr>) {
162        let ids = match source {
163            Some(source) => self.sources.entry(source).or_default(),
164            None => &mut self.any_source,
165        };
166        insert_id(ids, id);
167    }
168
169    fn remove(&mut self, id: SubscriptionId, source: Option<IpAddr>) {
170        match source {
171            Some(source) => {
172                if let Some(ids) = self.sources.get_mut(&source) {
173                    remove_id(ids, id);
174                    if ids.is_empty() {
175                        self.sources.remove(&source);
176                    }
177                }
178            }
179            None => remove_id(&mut self.any_source, id),
180        }
181    }
182
183    fn is_empty(&self) -> bool {
184        self.any_source.is_empty() && self.sources.is_empty()
185    }
186
187    fn matches(&self, source: IpAddr) -> Option<MatchingSubscriptionIds> {
188        let source_matches = self.sources.get(&source).map_or(&[][..], Vec::as_slice);
189        MatchingSubscriptionIds::from_sorted_slices(&self.any_source, source_matches)
190    }
191}
192
193#[derive(Debug, Default)]
194struct CaptureDemultiplexer {
195    groups: HashMap<IpAddr, GroupDemultiplexer>,
196}
197
198impl CaptureDemultiplexer {
199    fn insert(&mut self, id: SubscriptionId, config: &RawSubscriptionConfig) {
200        self.groups
201            .entry(config.group)
202            .or_default()
203            .insert(id, config.source_addr());
204    }
205
206    fn remove(&mut self, id: SubscriptionId, config: &RawSubscriptionConfig) {
207        let remove_group = if let Some(group) = self.groups.get_mut(&config.group) {
208            group.remove(id, config.source_addr());
209            group.is_empty()
210        } else {
211            false
212        };
213
214        if remove_group {
215            self.groups.remove(&config.group);
216        }
217    }
218
219    fn matches(&self, group: IpAddr, source: IpAddr) -> Option<MatchingSubscriptionIds> {
220        self.groups.get(&group)?.matches(source)
221    }
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
225struct KernelMembershipKey {
226    group: IpAddr,
227    source: Option<IpAddr>,
228}
229
230impl From<&RawSubscriptionConfig> for KernelMembershipKey {
231    fn from(config: &RawSubscriptionConfig) -> Self {
232        Self {
233            group: config.group,
234            source: config.source_addr(),
235        }
236    }
237}
238
239#[derive(Debug)]
240struct KernelMembership {
241    references: usize,
242    joined_config: RawSubscriptionConfig,
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
246struct CaptureSelector {
247    family: crate::SubscriptionAddressFamily,
248    interface: Option<IpAddr>,
249    interface_index: Option<u32>,
250}
251
252impl From<&RawSubscriptionConfig> for CaptureSelector {
253    fn from(config: &RawSubscriptionConfig) -> Self {
254        Self {
255            family: config.family(),
256            interface: config.interface,
257            interface_index: config.interface_index,
258        }
259    }
260}
261
262#[derive(Debug)]
263struct ResolvedCaptureKey {
264    key: RawSharedCaptureKey,
265    references: usize,
266}
267
268#[derive(Debug)]
269struct SharedCaptureSocket {
270    socket: RawReceiveSocket,
271    memberships: usize,
272    kernel_memberships: HashMap<KernelMembershipKey, KernelMembership>,
273    demultiplexer: CaptureDemultiplexer,
274}
275
276impl SharedCaptureSocket {
277    fn new(socket: RawReceiveSocket) -> Self {
278        Self {
279            socket,
280            memberships: 0,
281            kernel_memberships: HashMap::new(),
282            demultiplexer: CaptureDemultiplexer::default(),
283        }
284    }
285}
286
287#[cfg(feature = "metrics")]
288#[derive(Debug, Default)]
289struct SharedRawCaptureMetrics {
290    received_packets_total: u64,
291    unmatched_packets_total: u64,
292    demultiplex_matches_total: u64,
293}
294
295/// A point-in-time snapshot of shared raw capture activity.
296#[cfg(feature = "metrics")]
297#[derive(Debug, Clone, PartialEq, Eq)]
298pub struct SharedRawCaptureMetricsSnapshot {
299    /// Number of open capture sockets, normally one per family/interface tuple.
300    pub capture_socket_count: usize,
301    /// Number of joined logical multicast memberships.
302    pub active_memberships: usize,
303    /// Complete IP datagrams read from capture sockets since context creation.
304    pub received_packets_total: u64,
305    /// Captured datagrams whose source/group matched no logical membership.
306    pub unmatched_packets_total: u64,
307    /// Sum of logical membership matches across received datagrams.
308    pub demultiplex_matches_total: u64,
309    /// Demultiplexed packets currently queued for the caller.
310    pub pending_packets: usize,
311}
312
313/// A raw multicast context that shares one capture socket per family/interface.
314///
315/// This Linux-only opt-in transport primitive is intended for applications
316/// with many logical memberships. It manages each logical lifecycle
317/// independently, reference-counts duplicate kernel memberships, captures each
318/// kernel IP datagram once, and looks up matching subscriptions through a
319/// `(group, source)` index. macOS and Windows return
320/// [`McrxError::RawPacketReceiveUnsupported`] instead of falling back to the
321/// per-subscription backend.
322#[derive(Debug)]
323pub struct SharedRawContext {
324    subscriptions: HashMap<SubscriptionId, SharedRawSubscription>,
325    resolved_capture_keys: HashMap<CaptureSelector, ResolvedCaptureKey>,
326    captures: HashMap<RawSharedCaptureKey, SharedCaptureSocket>,
327    capture_order: Vec<RawSharedCaptureKey>,
328    pending_packets: VecDeque<SharedRawPacket>,
329    limits: SharedRawContextLimits,
330    next_subscription_id: u64,
331    next_capture_index: usize,
332    #[cfg(feature = "metrics")]
333    metrics: SharedRawCaptureMetrics,
334}
335
336impl Default for SharedRawContext {
337    fn default() -> Self {
338        Self::new()
339    }
340}
341
342impl SharedRawContext {
343    /// Creates an empty shared raw capture context with default bounds.
344    pub fn new() -> Self {
345        Self::with_limits(SharedRawContextLimits::default())
346            .expect("default shared raw capture limits are valid")
347    }
348
349    /// Creates an empty shared raw capture context with explicit bounds.
350    pub fn with_limits(limits: SharedRawContextLimits) -> Result<Self, McrxError> {
351        if limits.max_subscriptions == 0 || limits.max_pending_packets == 0 {
352            return Err(McrxError::InvalidSharedRawCaptureLimits);
353        }
354
355        Ok(Self {
356            subscriptions: HashMap::new(),
357            resolved_capture_keys: HashMap::new(),
358            captures: HashMap::new(),
359            capture_order: Vec::new(),
360            pending_packets: VecDeque::new(),
361            limits,
362            next_subscription_id: 1,
363            next_capture_index: 0,
364            #[cfg(feature = "metrics")]
365            metrics: SharedRawCaptureMetrics::default(),
366        })
367    }
368
369    /// Returns the configured shared raw capture bounds.
370    pub fn limits(&self) -> SharedRawContextLimits {
371        self.limits
372    }
373
374    /// Returns the number of stored logical subscriptions.
375    pub fn subscription_count(&self) -> usize {
376        self.subscriptions.len()
377    }
378
379    /// Returns the number of currently open shared capture sockets.
380    pub fn capture_socket_count(&self) -> usize {
381        self.captures.len()
382    }
383
384    /// Returns the number of joined logical memberships.
385    pub fn active_membership_count(&self) -> usize {
386        self.captures
387            .values()
388            .map(|capture| capture.memberships)
389            .sum()
390    }
391
392    /// Returns the number of demultiplexed packets waiting for the caller.
393    pub fn pending_packet_count(&self) -> usize {
394        self.pending_packets.len()
395    }
396
397    /// Returns true if a logical subscription with the given ID exists.
398    pub fn contains_subscription(&self, id: SubscriptionId) -> bool {
399        self.subscriptions.contains_key(&id)
400    }
401
402    /// Returns an immutable logical subscription, if present.
403    pub fn get_subscription(&self, id: SubscriptionId) -> Option<&SharedRawSubscription> {
404        self.subscriptions.get(&id)
405    }
406
407    /// Adds a bounded logical raw subscription without joining it yet.
408    ///
409    /// Unlike [`crate::RawContext`], this context permits duplicate configs.
410    /// Joined duplicates share one kernel membership and are returned as
411    /// separate IDs by [`SharedRawPacket::matching_subscription_ids`].
412    ///
413    /// On supported platforms this operation resolves the capture interface
414    /// key, but does not allocate a raw capture socket until [`Self::join_subscription`].
415    pub fn add_subscription(
416        &mut self,
417        config: RawSubscriptionConfig,
418    ) -> Result<SubscriptionId, McrxError> {
419        config.validate()?;
420        if self.subscriptions.len() >= self.limits.max_subscriptions {
421            return Err(McrxError::SharedRawCaptureSubscriptionLimitExceeded {
422                limit: self.limits.max_subscriptions,
423            });
424        }
425
426        let capture_key = self.resolve_capture_key(&config)?;
427        let id = SubscriptionId(self.next_subscription_id);
428        self.next_subscription_id += 1;
429        self.subscriptions
430            .insert(id, SharedRawSubscription::new(id, config, capture_key));
431        Ok(id)
432    }
433
434    /// Joins one logical multicast membership.
435    ///
436    /// The membership is installed on the capture socket matching its resolved
437    /// family/interface key. An identical joined membership increments its
438    /// kernel reference count instead of repeating the socket option. Other
439    /// memberships on that socket remain intact.
440    pub fn join_subscription(&mut self, id: SubscriptionId) -> Result<(), McrxError> {
441        let (capture_key, config) = {
442            let subscription = self
443                .subscriptions
444                .get(&id)
445                .ok_or(McrxError::SubscriptionNotFound)?;
446            if subscription.is_joined() {
447                return Err(McrxError::SubscriptionAlreadyJoined);
448            }
449            (subscription.capture_key, subscription.config.clone())
450        };
451        let membership_key = KernelMembershipKey::from(&config);
452
453        let created_capture = match self.captures.entry(capture_key) {
454            Entry::Occupied(_) => false,
455            Entry::Vacant(entry) => {
456                let socket = open_shared_raw_socket(capture_key)?;
457                entry.insert(SharedCaptureSocket::new(socket));
458                self.capture_order.push(capture_key);
459                true
460            }
461        };
462
463        let needs_kernel_join = !self
464            .captures
465            .get(&capture_key)
466            .expect("shared capture was inserted or already existed")
467            .kernel_memberships
468            .contains_key(&membership_key);
469        let join_result = if needs_kernel_join {
470            let capture = self
471                .captures
472                .get(&capture_key)
473                .expect("shared capture was inserted or already existed");
474            join_raw_multicast_group(&capture.socket, &config)
475        } else {
476            Ok(())
477        };
478        if let Err(error) = join_result {
479            if created_capture {
480                self.remove_capture(capture_key);
481            }
482            return Err(error);
483        }
484
485        let capture = self
486            .captures
487            .get_mut(&capture_key)
488            .expect("joined shared capture exists");
489        match capture.kernel_memberships.entry(membership_key) {
490            Entry::Occupied(mut entry) => entry.get_mut().references += 1,
491            Entry::Vacant(entry) => {
492                entry.insert(KernelMembership {
493                    references: 1,
494                    joined_config: config.clone(),
495                });
496            }
497        }
498        capture.demultiplexer.insert(id, &config);
499        capture.memberships += 1;
500        self.subscriptions
501            .get_mut(&id)
502            .expect("joined subscription exists")
503            .state = SubscriptionState::Joined;
504        Ok(())
505    }
506
507    /// Leaves one logical membership without affecting other shared memberships.
508    pub fn leave_subscription(&mut self, id: SubscriptionId) -> Result<(), McrxError> {
509        let (capture_key, config) = {
510            let subscription = self
511                .subscriptions
512                .get(&id)
513                .ok_or(McrxError::SubscriptionNotFound)?;
514            if !subscription.is_joined() {
515                return Err(McrxError::SubscriptionNotJoined);
516            }
517            (subscription.capture_key, subscription.config.clone())
518        };
519        let membership_key = KernelMembershipKey::from(&config);
520
521        let should_remove_capture = {
522            let capture = self
523                .captures
524                .get_mut(&capture_key)
525                .expect("joined subscription has a shared capture");
526            let joined_config = {
527                let membership = capture
528                    .kernel_memberships
529                    .get_mut(&membership_key)
530                    .expect("joined subscription has a kernel membership");
531                if membership.references == 1 {
532                    Some(membership.joined_config.clone())
533                } else {
534                    membership.references -= 1;
535                    None
536                }
537            };
538            if let Some(joined_config) = joined_config {
539                leave_raw_multicast_group(&capture.socket, &joined_config)?;
540                capture.kernel_memberships.remove(&membership_key);
541            }
542            capture.demultiplexer.remove(id, &config);
543            capture.memberships = capture
544                .memberships
545                .checked_sub(1)
546                .expect("membership count");
547            debug_assert_eq!(
548                capture.memberships == 0,
549                capture.kernel_memberships.is_empty()
550            );
551            capture.memberships == 0
552        };
553
554        self.subscriptions
555            .get_mut(&id)
556            .expect("joined subscription exists")
557            .state = SubscriptionState::Bound;
558        self.remove_pending_membership(id);
559        if should_remove_capture {
560            self.remove_capture(capture_key);
561        }
562        Ok(())
563    }
564
565    /// Removes one logical subscription and leaves it first when necessary.
566    ///
567    /// Returns `false` when no such subscription exists.
568    pub fn remove_subscription(&mut self, id: SubscriptionId) -> Result<bool, McrxError> {
569        if !self.subscriptions.contains_key(&id) {
570            return Ok(false);
571        }
572        if self
573            .subscriptions
574            .get(&id)
575            .expect("subscription existence checked")
576            .is_joined()
577        {
578            self.leave_subscription(id)?;
579        }
580        let removed = self
581            .subscriptions
582            .remove(&id)
583            .expect("subscription existence checked");
584        self.release_capture_key(&removed.config);
585        Ok(true)
586    }
587
588    /// Attempts to receive one complete IP datagram without blocking.
589    ///
590    /// Capture sockets are visited in round-robin order. A packet is read once
591    /// and returned with all matching logical subscription IDs.
592    pub fn try_recv_any(&mut self) -> Result<Option<SharedRawPacket>, McrxError> {
593        if let Some(packet) = self.pending_packets.pop_front() {
594            return Ok(Some(packet));
595        }
596
597        self.fill_pending(1)?;
598        Ok(self.pending_packets.pop_front())
599    }
600
601    /// Receives up to `max_packets` complete IP datagrams without blocking.
602    ///
603    /// Returns the number of packets appended to `packets`.
604    pub fn try_recv_batch_into(
605        &mut self,
606        packets: &mut Vec<SharedRawPacket>,
607        max_packets: usize,
608    ) -> Result<usize, McrxError> {
609        if max_packets == 0 {
610            return Ok(0);
611        }
612
613        let mut received = self.drain_pending_into(packets, max_packets);
614        while received < max_packets {
615            let queued = self.fill_pending(max_packets - received)?;
616            if queued == 0 {
617                break;
618            }
619            received += self.drain_pending_into(packets, max_packets - received);
620        }
621        Ok(received)
622    }
623
624    /// Returns a metrics snapshot for the shared capture backend.
625    #[cfg(feature = "metrics")]
626    pub fn metrics_snapshot(&self) -> SharedRawCaptureMetricsSnapshot {
627        SharedRawCaptureMetricsSnapshot {
628            capture_socket_count: self.capture_socket_count(),
629            active_memberships: self.active_membership_count(),
630            received_packets_total: self.metrics.received_packets_total,
631            unmatched_packets_total: self.metrics.unmatched_packets_total,
632            demultiplex_matches_total: self.metrics.demultiplex_matches_total,
633            pending_packets: self.pending_packet_count(),
634        }
635    }
636
637    fn fill_pending(&mut self, max_new_packets: usize) -> Result<usize, McrxError> {
638        let target = max_new_packets.min(
639            self.limits
640                .max_pending_packets
641                .saturating_sub(self.pending_packets.len()),
642        );
643        if self.capture_order.is_empty() || target == 0 {
644            return Ok(0);
645        }
646
647        let capture_count = self.capture_order.len();
648        let mut queued_packets = 0;
649        let mut captured_datagrams = 0;
650        let capture_budget = target.saturating_add(capture_count);
651
652        'captures: for _ in 0..capture_count {
653            let capture_key = self.capture_order[self.next_capture_index];
654            self.next_capture_index = (self.next_capture_index + 1) % capture_count;
655
656            for _ in 0..target {
657                if queued_packets == target || captured_datagrams == capture_budget {
658                    break 'captures;
659                }
660
661                let captured_and_matches = {
662                    let capture = self
663                        .captures
664                        .get(&capture_key)
665                        .expect("capture order and capture map stay in sync");
666                    let Some(captured) = recv_shared_raw_datagram(&capture.socket, capture_key)?
667                    else {
668                        break;
669                    };
670                    let matches = capture
671                        .demultiplexer
672                        .matches(captured.group, captured.source_ip);
673                    (captured, matches)
674                };
675                captured_datagrams += 1;
676                self.record_received_packet();
677
678                let (captured, Some(matching_ids)) = captured_and_matches else {
679                    self.record_unmatched_packet();
680                    continue;
681                };
682
683                self.record_demultiplex_matches(matching_ids.as_slice().len());
684                let primary_id = matching_ids.first();
685                let packet = {
686                    let primary_config = &self
687                        .subscriptions
688                        .get(&primary_id)
689                        .expect("demultiplexer only contains registered subscriptions")
690                        .config;
691                    raw_packet_from_captured(primary_id, primary_config, captured)
692                };
693                self.pending_packets.push_back(SharedRawPacket {
694                    packet,
695                    matching_subscription_ids: matching_ids,
696                });
697                queued_packets += 1;
698            }
699        }
700        Ok(queued_packets)
701    }
702
703    fn drain_pending_into(
704        &mut self,
705        packets: &mut Vec<SharedRawPacket>,
706        max_packets: usize,
707    ) -> usize {
708        let drained = max_packets.min(self.pending_packets.len());
709        packets.extend(self.pending_packets.drain(..drained));
710        drained
711    }
712
713    fn remove_capture(&mut self, capture_key: RawSharedCaptureKey) {
714        self.captures.remove(&capture_key);
715        let index = self
716            .capture_order
717            .iter()
718            .position(|key| *key == capture_key)
719            .expect("capture order contains every capture map key");
720        self.capture_order.remove(index);
721        if self.capture_order.is_empty() {
722            self.next_capture_index = 0;
723        } else {
724            self.next_capture_index %= self.capture_order.len();
725        }
726    }
727
728    fn resolve_capture_key(
729        &mut self,
730        config: &RawSubscriptionConfig,
731    ) -> Result<RawSharedCaptureKey, McrxError> {
732        let selector = CaptureSelector::from(config);
733        match self.resolved_capture_keys.entry(selector) {
734            Entry::Occupied(mut entry) => {
735                let resolved = entry.get_mut();
736                resolved.references += 1;
737                Ok(resolved.key)
738            }
739            Entry::Vacant(entry) => {
740                let key = shared_raw_capture_key(config)?;
741                entry.insert(ResolvedCaptureKey { key, references: 1 });
742                Ok(key)
743            }
744        }
745    }
746
747    fn release_capture_key(&mut self, config: &RawSubscriptionConfig) {
748        let selector = CaptureSelector::from(config);
749        let remove = {
750            let resolved = self
751                .resolved_capture_keys
752                .get_mut(&selector)
753                .expect("stored subscription has a resolved capture key");
754            resolved.references = resolved
755                .references
756                .checked_sub(1)
757                .expect("resolved capture key reference count");
758            resolved.references == 0
759        };
760        if remove {
761            self.resolved_capture_keys.remove(&selector);
762        }
763    }
764
765    fn remove_pending_membership(&mut self, id: SubscriptionId) {
766        let subscriptions = &self.subscriptions;
767        self.pending_packets.retain_mut(|shared_packet| {
768            let Some(index) = shared_packet
769                .matching_subscription_ids
770                .as_slice()
771                .iter()
772                .position(|matching_id| *matching_id == id)
773            else {
774                return true;
775            };
776
777            if shared_packet.matching_subscription_ids.as_slice().len() == 1 {
778                return false;
779            }
780
781            shared_packet
782                .matching_subscription_ids
783                .remove_existing(index);
784            let primary_id = shared_packet.matching_subscription_ids.first();
785            if shared_packet.packet.subscription_id != primary_id {
786                let primary_config = &subscriptions
787                    .get(&primary_id)
788                    .expect("queued packet only references registered subscriptions")
789                    .config;
790                shared_packet.packet.subscription_id = primary_id;
791                shared_packet.packet.metadata.configured_interface = primary_config.interface;
792                shared_packet.packet.metadata.configured_interface_index =
793                    primary_config.interface_index;
794            }
795            true
796        });
797    }
798
799    #[cfg(not(feature = "metrics"))]
800    fn record_received_packet(&mut self) {}
801
802    #[cfg(feature = "metrics")]
803    fn record_received_packet(&mut self) {
804        self.metrics.received_packets_total = self.metrics.received_packets_total.saturating_add(1);
805    }
806
807    #[cfg(not(feature = "metrics"))]
808    fn record_unmatched_packet(&mut self) {}
809
810    #[cfg(feature = "metrics")]
811    fn record_unmatched_packet(&mut self) {
812        self.metrics.unmatched_packets_total =
813            self.metrics.unmatched_packets_total.saturating_add(1);
814    }
815
816    #[cfg(not(feature = "metrics"))]
817    fn record_demultiplex_matches(&mut self, _matches: usize) {}
818
819    #[cfg(feature = "metrics")]
820    fn record_demultiplex_matches(&mut self, matches: usize) {
821        self.metrics.demultiplex_matches_total = self
822            .metrics
823            .demultiplex_matches_total
824            .saturating_add(matches as u64);
825    }
826}
827
828fn raw_packet_from_captured(
829    subscription_id: SubscriptionId,
830    config: &RawSubscriptionConfig,
831    captured: RawCapturedDatagram,
832) -> RawPacket {
833    RawPacket {
834        subscription_id,
835        datagram: captured.datagram,
836        source_ip: Some(captured.source_ip),
837        group: Some(captured.group),
838        ip_protocol: Some(captured.ip_protocol),
839        metadata: ReceiveMetadata {
840            socket_local_addr: None,
841            configured_interface: config.interface,
842            configured_interface_index: config.interface_index,
843            destination_local_ip: Some(captured.group),
844            ingress_interface_index: captured.ingress_interface_index,
845        },
846    }
847}
848
849fn insert_id(ids: &mut Vec<SubscriptionId>, id: SubscriptionId) {
850    match ids.binary_search_by_key(&id.0, |existing| existing.0) {
851        Ok(_) => {}
852        Err(index) => ids.insert(index, id),
853    }
854}
855
856fn remove_id(ids: &mut Vec<SubscriptionId>, id: SubscriptionId) {
857    if let Ok(index) = ids.binary_search_by_key(&id.0, |existing| existing.0) {
858        ids.remove(index);
859    }
860}
861
862fn merge_ids(
863    first: &[SubscriptionId],
864    second: &[SubscriptionId],
865    total: usize,
866) -> Vec<SubscriptionId> {
867    let mut matches = Vec::with_capacity(total);
868    let (mut first_index, mut second_index) = (0, 0);
869    while first_index < first.len() && second_index < second.len() {
870        if first[first_index].0 < second[second_index].0 {
871            matches.push(first[first_index]);
872            first_index += 1;
873        } else if first[first_index].0 > second[second_index].0 {
874            matches.push(second[second_index]);
875            second_index += 1;
876        } else {
877            matches.push(first[first_index]);
878            first_index += 1;
879            second_index += 1;
880        }
881    }
882    matches.extend_from_slice(&first[first_index..]);
883    matches.extend_from_slice(&second[second_index..]);
884    matches
885}
886
887#[cfg(test)]
888mod tests {
889    use super::*;
890    use crate::SourceFilter;
891    use std::net::{Ipv4Addr, Ipv6Addr};
892
893    fn asm(group: Ipv4Addr) -> RawSubscriptionConfig {
894        RawSubscriptionConfig::asm(group)
895    }
896
897    fn ssm(group: Ipv4Addr, source: Ipv4Addr) -> RawSubscriptionConfig {
898        RawSubscriptionConfig::ssm(group, source)
899    }
900
901    #[test]
902    fn demultiplexer_matches_duplicate_logical_memberships_once_each() {
903        let group = Ipv4Addr::new(239, 1, 2, 3);
904        let source = Ipv4Addr::new(192, 0, 2, 10);
905        let config = asm(group);
906        let mut demux = CaptureDemultiplexer::default();
907        demux.insert(SubscriptionId(9), &config);
908        demux.insert(SubscriptionId(3), &config);
909
910        assert_eq!(
911            demux
912                .matches(group.into(), source.into())
913                .unwrap()
914                .as_slice(),
915            &[SubscriptionId(3), SubscriptionId(9)]
916        );
917    }
918
919    #[test]
920    fn demultiplexer_uses_group_then_source_without_cross_group_matches() {
921        let first_group = Ipv4Addr::new(239, 1, 2, 3);
922        let second_group = Ipv4Addr::new(239, 1, 2, 4);
923        let source = Ipv4Addr::new(192, 0, 2, 10);
924        let mut demux = CaptureDemultiplexer::default();
925        demux.insert(SubscriptionId(1), &ssm(first_group, source));
926        demux.insert(SubscriptionId(2), &asm(second_group));
927
928        assert!(
929            demux
930                .matches(first_group.into(), Ipv4Addr::new(192, 0, 2, 11).into())
931                .is_none()
932        );
933        assert_eq!(
934            demux
935                .matches(second_group.into(), source.into())
936                .unwrap()
937                .as_slice(),
938            &[SubscriptionId(2)]
939        );
940        assert!(matches!(
941            demux.matches(second_group.into(), source.into()),
942            Some(MatchingSubscriptionIds::One(_))
943        ));
944    }
945
946    #[test]
947    fn removing_one_duplicate_does_not_disturb_the_other_logical_membership() {
948        let group = Ipv4Addr::new(239, 1, 2, 3);
949        let source = Ipv4Addr::new(192, 0, 2, 10);
950        let config = asm(group);
951        let mut demux = CaptureDemultiplexer::default();
952        demux.insert(SubscriptionId(1), &config);
953        demux.insert(SubscriptionId(2), &config);
954        demux.remove(SubscriptionId(1), &config);
955
956        assert_eq!(
957            demux
958                .matches(group.into(), source.into())
959                .unwrap()
960                .as_slice(),
961            &[SubscriptionId(2)]
962        );
963        assert_eq!(
964            demux
965                .matches(group.into(), Ipv4Addr::new(192, 0, 2, 11).into())
966                .unwrap()
967                .as_slice(),
968            &[SubscriptionId(2)]
969        );
970    }
971
972    #[test]
973    fn kernel_membership_keys_track_group_and_source_within_one_capture() {
974        let group = Ipv4Addr::new(239, 1, 2, 3);
975        let first = asm(group);
976        let mut duplicate = first.clone();
977        duplicate.interface = Some(Ipv4Addr::new(192, 0, 2, 10).into());
978
979        assert_eq!(
980            KernelMembershipKey::from(&first),
981            KernelMembershipKey::from(&duplicate)
982        );
983        assert_ne!(
984            KernelMembershipKey::from(&first),
985            KernelMembershipKey::from(&asm(Ipv4Addr::new(239, 1, 2, 4)))
986        );
987
988        let ssm_group = Ipv4Addr::new(232, 1, 2, 3);
989        assert_ne!(
990            KernelMembershipKey::from(&ssm(ssm_group, Ipv4Addr::new(192, 0, 2, 10))),
991            KernelMembershipKey::from(&ssm(ssm_group, Ipv4Addr::new(192, 0, 2, 11)))
992        );
993    }
994
995    #[test]
996    fn resolved_capture_key_cache_is_bounded_by_stored_selectors() {
997        let config = asm(Ipv4Addr::new(239, 1, 2, 3));
998        let selector = CaptureSelector::from(&config);
999        let capture_key = RawSharedCaptureKey {
1000            family: crate::SubscriptionAddressFamily::Ipv4,
1001            interface_index: 2,
1002        };
1003        let mut context = SharedRawContext::new();
1004        context.resolved_capture_keys.insert(
1005            selector,
1006            ResolvedCaptureKey {
1007                key: capture_key,
1008                references: 2,
1009            },
1010        );
1011
1012        context.release_capture_key(&config);
1013        assert_eq!(context.resolved_capture_keys[&selector].references, 1);
1014
1015        context.release_capture_key(&config);
1016        assert!(context.resolved_capture_keys.is_empty());
1017    }
1018
1019    #[test]
1020    fn capture_keys_separate_address_families_and_interfaces() {
1021        let ipv4_eth0 = RawSharedCaptureKey {
1022            family: crate::SubscriptionAddressFamily::Ipv4,
1023            interface_index: 2,
1024        };
1025        let ipv4_eth1 = RawSharedCaptureKey {
1026            family: crate::SubscriptionAddressFamily::Ipv4,
1027            interface_index: 3,
1028        };
1029        let ipv6_eth0 = RawSharedCaptureKey {
1030            family: crate::SubscriptionAddressFamily::Ipv6,
1031            interface_index: 2,
1032        };
1033
1034        assert_ne!(ipv4_eth0, ipv4_eth1);
1035        assert_ne!(ipv4_eth0, ipv6_eth0);
1036    }
1037
1038    #[test]
1039    fn limits_reject_zero_and_default_is_bounded() {
1040        assert!(matches!(
1041            SharedRawContext::with_limits(SharedRawContextLimits {
1042                max_subscriptions: 0,
1043                max_pending_packets: 1,
1044            }),
1045            Err(McrxError::InvalidSharedRawCaptureLimits)
1046        ));
1047        assert!(matches!(
1048            SharedRawContext::with_limits(SharedRawContextLimits {
1049                max_subscriptions: 1,
1050                max_pending_packets: 0,
1051            }),
1052            Err(McrxError::InvalidSharedRawCaptureLimits)
1053        ));
1054        assert!(SharedRawContextLimits::default().max_pending_packets > 0);
1055    }
1056
1057    #[test]
1058    fn subscription_limit_is_enforced_before_platform_socket_setup() {
1059        let first_config = asm(Ipv4Addr::new(239, 1, 2, 3));
1060        let mut context = SharedRawContext::with_limits(SharedRawContextLimits {
1061            max_subscriptions: 1,
1062            max_pending_packets: 1,
1063        })
1064        .unwrap();
1065        context.subscriptions.insert(
1066            SubscriptionId(1),
1067            SharedRawSubscription::new(
1068                SubscriptionId(1),
1069                first_config,
1070                RawSharedCaptureKey {
1071                    family: crate::SubscriptionAddressFamily::Ipv4,
1072                    interface_index: 2,
1073                },
1074            ),
1075        );
1076
1077        let error = context
1078            .add_subscription(asm(Ipv4Addr::new(239, 1, 2, 4)))
1079            .unwrap_err();
1080        assert!(matches!(
1081            error,
1082            McrxError::SharedRawCaptureSubscriptionLimitExceeded { limit: 1 }
1083        ));
1084    }
1085
1086    #[test]
1087    fn raw_packet_from_capture_preserves_complete_datagram_and_metadata() {
1088        let config = RawSubscriptionConfig::asm(Ipv4Addr::new(239, 1, 2, 3));
1089        let packet = raw_packet_from_captured(
1090            SubscriptionId(7),
1091            &config,
1092            RawCapturedDatagram {
1093                datagram: bytes::Bytes::from_static(&[0x45, 0, 0, 20]),
1094                source_ip: Ipv4Addr::new(192, 0, 2, 10).into(),
1095                group: config.group,
1096                ip_protocol: 17,
1097                ingress_interface_index: Some(2),
1098            },
1099        );
1100
1101        assert_eq!(packet.datagram.as_ref(), &[0x45, 0, 0, 20]);
1102        assert_eq!(packet.source_ip, Some(Ipv4Addr::new(192, 0, 2, 10).into()));
1103        assert_eq!(packet.metadata.ingress_interface_index, Some(2));
1104    }
1105
1106    #[test]
1107    fn removing_a_pending_overlap_keeps_the_remaining_membership() {
1108        let first_id = SubscriptionId(1);
1109        let second_id = SubscriptionId(2);
1110        let first_config = asm(Ipv4Addr::new(239, 1, 2, 3));
1111        let second_config = first_config.clone();
1112        let capture_key = RawSharedCaptureKey {
1113            family: crate::SubscriptionAddressFamily::Ipv4,
1114            interface_index: 2,
1115        };
1116        let mut context = SharedRawContext::new();
1117        context.subscriptions.insert(
1118            first_id,
1119            SharedRawSubscription::new(first_id, first_config.clone(), capture_key),
1120        );
1121        context.subscriptions.insert(
1122            second_id,
1123            SharedRawSubscription::new(second_id, second_config.clone(), capture_key),
1124        );
1125        context.pending_packets.push_back(SharedRawPacket {
1126            packet: raw_packet_from_captured(
1127                first_id,
1128                &first_config,
1129                RawCapturedDatagram {
1130                    datagram: bytes::Bytes::from_static(&[0x45, 0, 0, 20]),
1131                    source_ip: Ipv4Addr::new(192, 0, 2, 10).into(),
1132                    group: first_config.group,
1133                    ip_protocol: 17,
1134                    ingress_interface_index: Some(2),
1135                },
1136            ),
1137            matching_subscription_ids: MatchingSubscriptionIds::Many(vec![first_id, second_id]),
1138        });
1139
1140        context.remove_pending_membership(first_id);
1141
1142        let packet = context.pending_packets.front().expect("remaining overlap");
1143        assert_eq!(packet.packet.subscription_id, second_id);
1144        assert_eq!(packet.matching_subscription_ids(), &[second_id]);
1145    }
1146
1147    #[test]
1148    fn batch_receive_drains_queued_packets_up_to_the_requested_limit() {
1149        fn queued_packet(id: SubscriptionId) -> SharedRawPacket {
1150            SharedRawPacket {
1151                packet: RawPacket {
1152                    subscription_id: id,
1153                    datagram: bytes::Bytes::from_static(&[0x45, 0, 0, 20]),
1154                    source_ip: Some(Ipv4Addr::new(192, 0, 2, 10).into()),
1155                    group: Some(Ipv4Addr::new(239, 1, 2, 3).into()),
1156                    ip_protocol: Some(17),
1157                    metadata: ReceiveMetadata::empty(),
1158                },
1159                matching_subscription_ids: MatchingSubscriptionIds::One([id]),
1160            }
1161        }
1162
1163        let mut context = SharedRawContext::new();
1164        context
1165            .pending_packets
1166            .push_back(queued_packet(SubscriptionId(1)));
1167        context
1168            .pending_packets
1169            .push_back(queued_packet(SubscriptionId(2)));
1170        let mut packets = Vec::new();
1171
1172        let received = context.try_recv_batch_into(&mut packets, 1).unwrap();
1173
1174        assert_eq!(received, 1);
1175        assert_eq!(packets[0].packet.subscription_id, SubscriptionId(1));
1176        assert_eq!(context.pending_packet_count(), 1);
1177    }
1178
1179    #[cfg(feature = "metrics")]
1180    #[test]
1181    fn metrics_snapshot_reports_shared_capture_counters() {
1182        let mut context = SharedRawContext::new();
1183        context.record_received_packet();
1184        context.record_received_packet();
1185        context.record_unmatched_packet();
1186        context.record_demultiplex_matches(3);
1187
1188        let snapshot = context.metrics_snapshot();
1189        assert_eq!(snapshot.capture_socket_count, 0);
1190        assert_eq!(snapshot.active_memberships, 0);
1191        assert_eq!(snapshot.received_packets_total, 2);
1192        assert_eq!(snapshot.unmatched_packets_total, 1);
1193        assert_eq!(snapshot.demultiplex_matches_total, 3);
1194        assert_eq!(snapshot.pending_packets, 0);
1195    }
1196
1197    #[test]
1198    fn source_filter_enum_remains_usable_for_shared_configs() {
1199        let config = RawSubscriptionConfig {
1200            group: Ipv6Addr::from(0xff3e_0000_0000_0000_0000_0000_0000_1234u128).into(),
1201            source: SourceFilter::Source("2001:db8::1".parse().unwrap()),
1202            interface: Some("fe80::1".parse().unwrap()),
1203            interface_index: Some(2),
1204        };
1205        assert!(config.validate().is_ok());
1206    }
1207}