Skip to main content

mcrx_core/
config.rs

1use crate::error::McrxError;
2use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
3
4/// Identifies the IP address family used by a subscription configuration.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum SubscriptionAddressFamily {
7    /// IPv4 multicast traffic.
8    Ipv4,
9    /// IPv6 multicast traffic.
10    Ipv6,
11}
12
13/// Describes whether packets from any source or only one specific source
14/// should be accepted for a multicast group.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum SourceFilter {
17    /// Accept packets from any source (Any-Source Multicast, `(*, G)`).
18    Any,
19    /// Accept packets only from one specific source (Source-Specific Multicast, `(S, G)`).
20    Source(IpAddr),
21}
22
23/// Configuration for a multicast receive subscription.
24///
25/// This defines the multicast group, source filtering mode (ASM or SSM),
26/// destination port, and optionally the local interface to join on.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct SubscriptionConfig {
29    /// The destination multicast group to join.
30    pub group: IpAddr,
31    /// The source filtering mode for the subscription.
32    pub source: SourceFilter,
33    /// The destination UDP port to receive on.
34    pub dst_port: u16,
35    /// The local interface address to join on, if explicitly specified.
36    pub interface: Option<IpAddr>,
37    /// The local IPv6 interface index to join on, if explicitly specified.
38    ///
39    /// This is primarily useful for scoped/link-local IPv6 multicast where an
40    /// interface address alone may be ambiguous across multiple adapters.
41    pub interface_index: Option<u32>,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub(crate) struct Ipv4Membership {
46    pub(crate) group: Ipv4Addr,
47    pub(crate) source: Option<Ipv4Addr>,
48    pub(crate) interface: Option<Ipv4Addr>,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub(crate) struct Ipv6Membership {
53    pub(crate) group: Ipv6Addr,
54    pub(crate) source: Option<Ipv6Addr>,
55    pub(crate) interface: Option<Ipv6Addr>,
56    pub(crate) interface_index: Option<u32>,
57}
58
59impl SubscriptionConfig {
60    /// Validates the configuration and returns an error if it is not usable.
61    pub fn validate(&self) -> Result<(), McrxError> {
62        if self.dst_port == 0 {
63            return Err(McrxError::InvalidDestinationPort);
64        }
65
66        validate_multicast_selection(
67            self.group,
68            &self.source,
69            self.interface,
70            self.interface_index,
71        )
72    }
73
74    /// Returns the configured address family.
75    pub fn family(&self) -> SubscriptionAddressFamily {
76        match self.group {
77            IpAddr::V4(_) => SubscriptionAddressFamily::Ipv4,
78            IpAddr::V6(_) => SubscriptionAddressFamily::Ipv6,
79        }
80    }
81
82    /// Returns `true` when this is an IPv4 subscription.
83    pub fn is_ipv4(&self) -> bool {
84        matches!(self.family(), SubscriptionAddressFamily::Ipv4)
85    }
86
87    /// Returns `true` when this is an IPv6 subscription.
88    pub fn is_ipv6(&self) -> bool {
89        matches!(self.family(), SubscriptionAddressFamily::Ipv6)
90    }
91
92    /// Returns the configured source address, if any.
93    pub fn source_addr(&self) -> Option<IpAddr> {
94        match self.source {
95            SourceFilter::Any => None,
96            SourceFilter::Source(source) => Some(source),
97        }
98    }
99
100    /// Creates an ASM (`(*, G)`) subscription configuration.
101    pub fn asm(group: Ipv4Addr, port: u16) -> Self {
102        Self::asm_ip(group.into(), port)
103    }
104
105    /// Creates an IPv6 ASM (`(*, G)`) subscription configuration.
106    pub fn asm_v6(group: Ipv6Addr, port: u16) -> Self {
107        Self::asm_ip(group.into(), port)
108    }
109
110    /// Creates an ASM (`(*, G)`) subscription configuration from any IP family.
111    pub fn asm_ip(group: IpAddr, port: u16) -> Self {
112        Self {
113            group,
114            source: SourceFilter::Any,
115            dst_port: port,
116            interface: None,
117            interface_index: None,
118        }
119    }
120
121    /// Creates an SSM (`(S, G)`) subscription configuration.
122    pub fn ssm(group: Ipv4Addr, source: Ipv4Addr, port: u16) -> Self {
123        Self::ssm_ip(group.into(), source.into(), port)
124    }
125
126    /// Creates an IPv6 SSM (`(S, G)`) subscription configuration.
127    pub fn ssm_v6(group: Ipv6Addr, source: Ipv6Addr, port: u16) -> Self {
128        Self::ssm_ip(group.into(), source.into(), port)
129    }
130
131    /// Creates an SSM (`(S, G)`) subscription configuration from any IP family.
132    pub fn ssm_ip(group: IpAddr, source: IpAddr, port: u16) -> Self {
133        Self {
134            group,
135            source: SourceFilter::Source(source),
136            dst_port: port,
137            interface: None,
138            interface_index: None,
139        }
140    }
141
142    pub(crate) fn ipv4_membership(&self) -> Option<Ipv4Membership> {
143        let group = match self.group {
144            IpAddr::V4(group) => group,
145            IpAddr::V6(_) => return None,
146        };
147
148        let source = match self.source {
149            SourceFilter::Any => None,
150            SourceFilter::Source(IpAddr::V4(source)) => Some(source),
151            SourceFilter::Source(IpAddr::V6(_)) => return None,
152        };
153
154        let interface = match self.interface {
155            None => None,
156            Some(IpAddr::V4(interface)) => Some(interface),
157            Some(IpAddr::V6(_)) => return None,
158        };
159
160        Some(Ipv4Membership {
161            group,
162            source,
163            interface,
164        })
165    }
166
167    pub(crate) fn ipv6_membership(&self) -> Option<Ipv6Membership> {
168        let group = match self.group {
169            IpAddr::V6(group) => group,
170            IpAddr::V4(_) => return None,
171        };
172
173        let source = match self.source {
174            SourceFilter::Any => None,
175            SourceFilter::Source(IpAddr::V6(source)) => Some(source),
176            SourceFilter::Source(IpAddr::V4(_)) => return None,
177        };
178
179        let interface = match self.interface {
180            None => None,
181            Some(IpAddr::V6(interface)) => Some(interface),
182            Some(IpAddr::V4(_)) => return None,
183        };
184
185        Some(Ipv6Membership {
186            group,
187            source,
188            interface,
189            interface_index: self.interface_index,
190        })
191    }
192}
193
194pub(crate) fn validate_multicast_selection(
195    group: IpAddr,
196    source: &SourceFilter,
197    interface: Option<IpAddr>,
198    interface_index: Option<u32>,
199) -> Result<(), McrxError> {
200    if !group.is_multicast() {
201        return Err(McrxError::InvalidMulticastGroup);
202    }
203
204    if let SourceFilter::Source(source) = source {
205        if source.is_multicast() {
206            return Err(McrxError::InvalidSourceAddress);
207        }
208
209        if !same_family(group, *source) {
210            return Err(McrxError::SourceAddressFamilyMismatch);
211        }
212
213        if let (IpAddr::V6(group), IpAddr::V6(_)) = (group, *source)
214            && !is_ipv6_ssm_group(group)
215        {
216            return Err(McrxError::InvalidIpv6SsmGroup);
217        }
218    }
219
220    if let Some(interface) = interface
221        && !same_family(group, interface)
222    {
223        return Err(McrxError::InterfaceAddressFamilyMismatch);
224    }
225
226    if let Some(interface_index) = interface_index {
227        if interface_index == 0 {
228            return Err(McrxError::InvalidInterfaceIndex);
229        }
230
231        if !matches!(group, IpAddr::V6(_)) {
232            return Err(McrxError::InterfaceIndexRequiresIpv6);
233        }
234    }
235
236    Ok(())
237}
238
239pub(crate) fn same_family(left: IpAddr, right: IpAddr) -> bool {
240    matches!(
241        (left, right),
242        (IpAddr::V4(_), IpAddr::V4(_)) | (IpAddr::V6(_), IpAddr::V6(_))
243    )
244}
245
246pub(crate) fn is_ipv6_ssm_group(group: Ipv6Addr) -> bool {
247    let octets = group.octets();
248    octets[0] == 0xff && (octets[1] >> 4) == 0x3
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn valid_multicast_config_passes_validation() {
257        let cfg = SubscriptionConfig {
258            group: Ipv4Addr::new(239, 1, 2, 3).into(),
259            source: SourceFilter::Any,
260            dst_port: 5000,
261            interface: None,
262            interface_index: None,
263        };
264
265        assert!(cfg.validate().is_ok());
266    }
267
268    #[test]
269    fn port_zero_fails_validation() {
270        let cfg = SubscriptionConfig {
271            group: Ipv4Addr::new(239, 1, 2, 3).into(),
272            source: SourceFilter::Any,
273            dst_port: 0,
274            interface: None,
275            interface_index: None,
276        };
277
278        let result = cfg.validate();
279
280        assert!(matches!(result, Err(McrxError::InvalidDestinationPort)));
281    }
282
283    #[test]
284    fn non_multicast_group_fails_validation() {
285        let cfg = SubscriptionConfig {
286            group: Ipv4Addr::new(192, 168, 1, 10).into(),
287            source: SourceFilter::Any,
288            dst_port: 5000,
289            interface: None,
290            interface_index: None,
291        };
292
293        let result = cfg.validate();
294
295        assert!(matches!(result, Err(McrxError::InvalidMulticastGroup)));
296    }
297
298    #[test]
299    fn multicast_source_fails_validation() {
300        let cfg = SubscriptionConfig {
301            group: Ipv4Addr::new(232, 1, 2, 3).into(),
302            source: SourceFilter::Source(Ipv4Addr::new(239, 1, 1, 1).into()),
303            dst_port: 5000,
304            interface: None,
305            interface_index: None,
306        };
307
308        let result = cfg.validate();
309
310        assert!(matches!(result, Err(McrxError::InvalidSourceAddress)));
311    }
312
313    #[test]
314    fn ipv6_asm_config_passes_validation() {
315        let cfg = SubscriptionConfig::asm_v6("ff3e::1234".parse().unwrap(), 5000);
316
317        assert!(cfg.validate().is_ok());
318        assert!(cfg.is_ipv6());
319    }
320
321    #[test]
322    fn ipv6_ssm_config_passes_validation() {
323        let cfg = SubscriptionConfig::ssm_v6(
324            "ff3e::1234".parse().unwrap(),
325            "2001:db8::10".parse().unwrap(),
326            5000,
327        );
328
329        assert!(cfg.validate().is_ok());
330        assert_eq!(
331            cfg.source_addr(),
332            Some("2001:db8::10".parse::<IpAddr>().unwrap())
333        );
334    }
335
336    #[test]
337    fn ipv6_ssm_requires_ff3x_group_range() {
338        let cfg = SubscriptionConfig::ssm_v6(
339            "ff12::1234".parse().unwrap(),
340            "2001:db8::10".parse().unwrap(),
341            5000,
342        );
343
344        let result = cfg.validate();
345
346        assert!(matches!(result, Err(McrxError::InvalidIpv6SsmGroup)));
347    }
348
349    #[test]
350    fn source_family_mismatch_fails_validation() {
351        let cfg = SubscriptionConfig::ssm_ip(
352            Ipv4Addr::new(232, 1, 2, 3).into(),
353            "2001:db8::10".parse().unwrap(),
354            5000,
355        );
356
357        let result = cfg.validate();
358
359        assert!(matches!(
360            result,
361            Err(McrxError::SourceAddressFamilyMismatch)
362        ));
363    }
364
365    #[test]
366    fn interface_family_mismatch_fails_validation() {
367        let mut cfg = SubscriptionConfig::asm(Ipv4Addr::new(239, 1, 2, 3), 5000);
368        cfg.interface = Some("2001:db8::20".parse().unwrap());
369
370        let result = cfg.validate();
371
372        assert!(matches!(
373            result,
374            Err(McrxError::InterfaceAddressFamilyMismatch)
375        ));
376    }
377
378    #[test]
379    fn ipv4_config_rejects_interface_index() {
380        let mut cfg = SubscriptionConfig::asm(Ipv4Addr::new(239, 1, 2, 3), 5000);
381        cfg.interface_index = Some(7);
382
383        let result = cfg.validate();
384
385        assert!(matches!(result, Err(McrxError::InterfaceIndexRequiresIpv6)));
386    }
387
388    #[test]
389    fn ipv6_config_accepts_interface_index() {
390        let mut cfg = SubscriptionConfig::asm_v6("ff01::1234".parse().unwrap(), 5000);
391        cfg.interface_index = Some(7);
392
393        assert!(cfg.validate().is_ok());
394        assert_eq!(cfg.ipv6_membership().unwrap().interface_index, Some(7));
395    }
396}