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, Hash)]
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    let group_is_ssm = match group {
205        IpAddr::V4(group) => is_ipv4_ssm_group(group),
206        IpAddr::V6(group) => is_ipv6_ssm_group(group),
207    };
208
209    if matches!(source, SourceFilter::Any) && group_is_ssm {
210        return Err(McrxError::SsmGroupRequiresSource);
211    }
212
213    if let SourceFilter::Source(source) = source {
214        if !is_valid_source_address(*source) {
215            return Err(McrxError::InvalidSourceAddress);
216        }
217
218        if !same_family(group, *source) {
219            return Err(McrxError::SourceAddressFamilyMismatch);
220        }
221
222        match (group, *source) {
223            (IpAddr::V4(group), IpAddr::V4(_)) if !is_ipv4_ssm_group(group) => {
224                return Err(McrxError::InvalidIpv4SsmGroup);
225            }
226            (IpAddr::V6(group), IpAddr::V6(_)) if !is_ipv6_ssm_group(group) => {
227                return Err(McrxError::InvalidIpv6SsmGroup);
228            }
229            _ => {}
230        }
231    }
232
233    if let Some(interface) = interface
234        && !same_family(group, interface)
235    {
236        return Err(McrxError::InterfaceAddressFamilyMismatch);
237    }
238
239    if let Some(interface_index) = interface_index {
240        if interface_index == 0 {
241            return Err(McrxError::InvalidInterfaceIndex);
242        }
243
244        if !matches!(group, IpAddr::V6(_)) {
245            return Err(McrxError::InterfaceIndexRequiresIpv6);
246        }
247    }
248
249    Ok(())
250}
251
252pub(crate) fn same_family(left: IpAddr, right: IpAddr) -> bool {
253    matches!(
254        (left, right),
255        (IpAddr::V4(_), IpAddr::V4(_)) | (IpAddr::V6(_), IpAddr::V6(_))
256    )
257}
258
259pub(crate) fn is_ipv4_ssm_group(group: Ipv4Addr) -> bool {
260    group.octets()[0] == 232
261}
262
263pub(crate) fn is_ipv6_ssm_group(group: Ipv6Addr) -> bool {
264    let octets = group.octets();
265    octets[0] == 0xff && (octets[1] >> 4) == 0x3 && octets[2] == 0 && octets[3] == 0
266}
267
268fn is_valid_source_address(source: IpAddr) -> bool {
269    match source {
270        IpAddr::V4(source) => {
271            !source.is_unspecified() && !source.is_multicast() && !source.is_broadcast()
272        }
273        IpAddr::V6(source) => !source.is_unspecified() && !source.is_multicast(),
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn valid_multicast_config_passes_validation() {
283        let cfg = SubscriptionConfig {
284            group: Ipv4Addr::new(239, 1, 2, 3).into(),
285            source: SourceFilter::Any,
286            dst_port: 5000,
287            interface: None,
288            interface_index: None,
289        };
290
291        assert!(cfg.validate().is_ok());
292    }
293
294    #[test]
295    fn port_zero_fails_validation() {
296        let cfg = SubscriptionConfig {
297            group: Ipv4Addr::new(239, 1, 2, 3).into(),
298            source: SourceFilter::Any,
299            dst_port: 0,
300            interface: None,
301            interface_index: None,
302        };
303
304        let result = cfg.validate();
305
306        assert!(matches!(result, Err(McrxError::InvalidDestinationPort)));
307    }
308
309    #[test]
310    fn non_multicast_group_fails_validation() {
311        let cfg = SubscriptionConfig {
312            group: Ipv4Addr::new(192, 168, 1, 10).into(),
313            source: SourceFilter::Any,
314            dst_port: 5000,
315            interface: None,
316            interface_index: None,
317        };
318
319        let result = cfg.validate();
320
321        assert!(matches!(result, Err(McrxError::InvalidMulticastGroup)));
322    }
323
324    #[test]
325    fn multicast_source_fails_validation() {
326        let cfg = SubscriptionConfig {
327            group: Ipv4Addr::new(232, 1, 2, 3).into(),
328            source: SourceFilter::Source(Ipv4Addr::new(239, 1, 1, 1).into()),
329            dst_port: 5000,
330            interface: None,
331            interface_index: None,
332        };
333
334        let result = cfg.validate();
335
336        assert!(matches!(result, Err(McrxError::InvalidSourceAddress)));
337    }
338
339    #[test]
340    fn ipv4_ssm_config_passes_validation() {
341        let cfg = SubscriptionConfig::ssm(
342            Ipv4Addr::new(232, 1, 2, 3),
343            Ipv4Addr::new(192, 168, 1, 10),
344            5000,
345        );
346
347        assert!(cfg.validate().is_ok());
348        assert_eq!(
349            cfg.source_addr(),
350            Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)))
351        );
352    }
353
354    #[test]
355    fn ipv4_ssm_requires_232_range() {
356        let cfg = SubscriptionConfig::ssm(
357            Ipv4Addr::new(239, 1, 1, 1),
358            Ipv4Addr::new(192, 168, 1, 10),
359            5000,
360        );
361
362        let result = cfg.validate();
363
364        assert!(matches!(result, Err(McrxError::InvalidIpv4SsmGroup)));
365    }
366
367    #[test]
368    fn ipv6_asm_config_passes_validation() {
369        let cfg = SubscriptionConfig::asm_v6("ff12::1234".parse().unwrap(), 5000);
370
371        assert!(cfg.validate().is_ok());
372        assert!(cfg.is_ipv6());
373    }
374
375    #[test]
376    fn ipv6_ssm_config_passes_validation() {
377        let cfg = SubscriptionConfig::ssm_v6(
378            "ff3e::1234".parse().unwrap(),
379            "2001:db8::10".parse().unwrap(),
380            5000,
381        );
382
383        assert!(cfg.validate().is_ok());
384        assert_eq!(
385            cfg.source_addr(),
386            Some("2001:db8::10".parse::<IpAddr>().unwrap())
387        );
388    }
389
390    #[test]
391    fn ipv6_ssm_requires_ff3x_group_range() {
392        let cfg = SubscriptionConfig::ssm_v6(
393            "ff12::1234".parse().unwrap(),
394            "2001:db8::10".parse().unwrap(),
395            5000,
396        );
397
398        let result = cfg.validate();
399
400        assert!(matches!(result, Err(McrxError::InvalidIpv6SsmGroup)));
401    }
402
403    #[test]
404    fn asm_rejects_ipv4_ssm_range() {
405        let cfg = SubscriptionConfig::asm(Ipv4Addr::new(232, 1, 2, 3), 5000);
406
407        assert!(matches!(
408            cfg.validate(),
409            Err(McrxError::SsmGroupRequiresSource)
410        ));
411    }
412
413    #[test]
414    fn asm_rejects_ipv6_ssm_range() {
415        let cfg = SubscriptionConfig::asm_v6("ff3e::8000:1234".parse().unwrap(), 5000);
416
417        assert!(matches!(
418            cfg.validate(),
419            Err(McrxError::SsmGroupRequiresSource)
420        ));
421    }
422
423    #[test]
424    fn ipv6_ssm_requires_full_32_bit_prefix() {
425        let cfg = SubscriptionConfig::ssm_v6(
426            "ff3e:1::8000:1234".parse().unwrap(),
427            "2001:db8::10".parse().unwrap(),
428            5000,
429        );
430
431        assert!(matches!(
432            cfg.validate(),
433            Err(McrxError::InvalidIpv6SsmGroup)
434        ));
435    }
436
437    #[test]
438    fn unspecified_and_broadcast_sources_are_rejected() {
439        for source in [Ipv4Addr::UNSPECIFIED, Ipv4Addr::BROADCAST] {
440            let cfg = SubscriptionConfig::ssm(Ipv4Addr::new(232, 1, 2, 3), source, 5000);
441            assert!(matches!(
442                cfg.validate(),
443                Err(McrxError::InvalidSourceAddress)
444            ));
445        }
446
447        let cfg = SubscriptionConfig::ssm_v6(
448            "ff3e::8000:1234".parse().unwrap(),
449            Ipv6Addr::UNSPECIFIED,
450            5000,
451        );
452        assert!(matches!(
453            cfg.validate(),
454            Err(McrxError::InvalidSourceAddress)
455        ));
456    }
457
458    #[test]
459    fn source_family_mismatch_fails_validation() {
460        let cfg = SubscriptionConfig::ssm_ip(
461            Ipv4Addr::new(232, 1, 2, 3).into(),
462            "2001:db8::10".parse().unwrap(),
463            5000,
464        );
465
466        let result = cfg.validate();
467
468        assert!(matches!(
469            result,
470            Err(McrxError::SourceAddressFamilyMismatch)
471        ));
472    }
473
474    #[test]
475    fn interface_family_mismatch_fails_validation() {
476        let mut cfg = SubscriptionConfig::asm(Ipv4Addr::new(239, 1, 2, 3), 5000);
477        cfg.interface = Some("2001:db8::20".parse().unwrap());
478
479        let result = cfg.validate();
480
481        assert!(matches!(
482            result,
483            Err(McrxError::InterfaceAddressFamilyMismatch)
484        ));
485    }
486
487    #[test]
488    fn ipv4_config_rejects_interface_index() {
489        let mut cfg = SubscriptionConfig::asm(Ipv4Addr::new(239, 1, 2, 3), 5000);
490        cfg.interface_index = Some(7);
491
492        let result = cfg.validate();
493
494        assert!(matches!(result, Err(McrxError::InterfaceIndexRequiresIpv6)));
495    }
496
497    #[test]
498    fn ipv6_config_accepts_interface_index() {
499        let mut cfg = SubscriptionConfig::asm_v6("ff01::1234".parse().unwrap(), 5000);
500        cfg.interface_index = Some(7);
501
502        assert!(cfg.validate().is_ok());
503        assert_eq!(cfg.ipv6_membership().unwrap().interface_index, Some(7));
504    }
505}