Skip to main content

mcrx_core/raw/
config.rs

1use crate::config::{
2    SourceFilter, SubscriptionAddressFamily, SubscriptionConfig, validate_multicast_selection,
3};
4use crate::error::McrxError;
5use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
6
7/// Configuration for raw multicast IP datagram receive.
8///
9/// Unlike [`SubscriptionConfig`], this configuration is not tied to a UDP
10/// destination port. It represents multicast membership and interface
11/// selection only.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct RawSubscriptionConfig {
14    /// The destination multicast group to join.
15    pub group: IpAddr,
16    /// The source filtering mode for the subscription.
17    pub source: SourceFilter,
18    /// The local interface address to join on, if explicitly specified.
19    pub interface: Option<IpAddr>,
20    /// The local IPv6 interface index to join on, if explicitly specified.
21    pub interface_index: Option<u32>,
22}
23
24impl RawSubscriptionConfig {
25    /// Validates the configuration and returns an error if it is not usable.
26    pub fn validate(&self) -> Result<(), McrxError> {
27        validate_multicast_selection(
28            self.group,
29            &self.source,
30            self.interface,
31            self.interface_index,
32        )
33    }
34
35    /// Returns the configured address family.
36    pub fn family(&self) -> SubscriptionAddressFamily {
37        match self.group {
38            IpAddr::V4(_) => SubscriptionAddressFamily::Ipv4,
39            IpAddr::V6(_) => SubscriptionAddressFamily::Ipv6,
40        }
41    }
42
43    /// Returns `true` when this is an IPv4 subscription.
44    pub fn is_ipv4(&self) -> bool {
45        matches!(self.family(), SubscriptionAddressFamily::Ipv4)
46    }
47
48    /// Returns `true` when this is an IPv6 subscription.
49    pub fn is_ipv6(&self) -> bool {
50        matches!(self.family(), SubscriptionAddressFamily::Ipv6)
51    }
52
53    /// Returns the configured source address, if any.
54    pub fn source_addr(&self) -> Option<IpAddr> {
55        match self.source {
56            SourceFilter::Any => None,
57            SourceFilter::Source(source) => Some(source),
58        }
59    }
60
61    /// Creates an ASM (`(*, G)`) raw subscription configuration.
62    pub fn asm(group: Ipv4Addr) -> Self {
63        Self::asm_ip(group.into())
64    }
65
66    /// Creates an IPv6 ASM (`(*, G)`) raw subscription configuration.
67    pub fn asm_v6(group: Ipv6Addr) -> Self {
68        Self::asm_ip(group.into())
69    }
70
71    /// Creates an ASM (`(*, G)`) raw subscription configuration from any IP family.
72    pub fn asm_ip(group: IpAddr) -> Self {
73        Self {
74            group,
75            source: SourceFilter::Any,
76            interface: None,
77            interface_index: None,
78        }
79    }
80
81    /// Creates an SSM (`(S, G)`) raw subscription configuration.
82    pub fn ssm(group: Ipv4Addr, source: Ipv4Addr) -> Self {
83        Self::ssm_ip(group.into(), source.into())
84    }
85
86    /// Creates an IPv6 SSM (`(S, G)`) raw subscription configuration.
87    pub fn ssm_v6(group: Ipv6Addr, source: Ipv6Addr) -> Self {
88        Self::ssm_ip(group.into(), source.into())
89    }
90
91    /// Creates an SSM (`(S, G)`) raw subscription configuration from any IP family.
92    pub fn ssm_ip(group: IpAddr, source: IpAddr) -> Self {
93        Self {
94            group,
95            source: SourceFilter::Source(source),
96            interface: None,
97            interface_index: None,
98        }
99    }
100
101    pub(crate) fn membership_compat_config(&self) -> SubscriptionConfig {
102        SubscriptionConfig {
103            group: self.group,
104            source: self.source.clone(),
105            dst_port: 1,
106            interface: self.interface,
107            interface_index: self.interface_index,
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn valid_ipv4_raw_config_passes_validation() {
118        let cfg = RawSubscriptionConfig::asm(Ipv4Addr::new(239, 1, 2, 3));
119        assert!(cfg.validate().is_ok());
120    }
121
122    #[test]
123    fn valid_ipv4_ssm_raw_config_passes_validation() {
124        let cfg =
125            RawSubscriptionConfig::ssm(Ipv4Addr::new(232, 1, 2, 3), Ipv4Addr::new(10, 0, 0, 1));
126        assert!(cfg.validate().is_ok());
127    }
128
129    #[test]
130    fn ipv4_raw_ssm_requires_232_range() {
131        let cfg =
132            RawSubscriptionConfig::ssm(Ipv4Addr::new(239, 1, 1, 1), Ipv4Addr::new(10, 0, 0, 1));
133
134        let result = cfg.validate();
135        assert!(matches!(result, Err(McrxError::InvalidIpv4SsmGroup)));
136    }
137
138    #[test]
139    fn valid_ipv6_ssm_raw_config_passes_validation() {
140        let cfg = RawSubscriptionConfig::ssm_v6(
141            "ff3e::8000:1234".parse().unwrap(),
142            "2001:db8::10".parse().unwrap(),
143        );
144        assert!(cfg.validate().is_ok());
145    }
146
147    #[test]
148    fn ipv6_raw_ssm_requires_ff3x_group_range() {
149        let cfg = RawSubscriptionConfig::ssm_v6(
150            "ff12::1234".parse().unwrap(),
151            "2001:db8::10".parse().unwrap(),
152        );
153
154        let result = cfg.validate();
155        assert!(matches!(result, Err(McrxError::InvalidIpv6SsmGroup)));
156    }
157
158    #[test]
159    fn ipv4_raw_config_rejects_interface_index() {
160        let mut cfg = RawSubscriptionConfig::asm(Ipv4Addr::new(239, 1, 2, 3));
161        cfg.interface_index = Some(7);
162
163        let result = cfg.validate();
164        assert!(matches!(result, Err(McrxError::InterfaceIndexRequiresIpv6)));
165    }
166
167    #[test]
168    fn raw_asm_rejects_ssm_reserved_groups() {
169        let ipv4 = RawSubscriptionConfig::asm(Ipv4Addr::new(232, 1, 2, 3));
170        let ipv6 = RawSubscriptionConfig::asm_v6("ff3e::8000:1234".parse().unwrap());
171
172        assert!(matches!(
173            ipv4.validate(),
174            Err(McrxError::SsmGroupRequiresSource)
175        ));
176        assert!(matches!(
177            ipv6.validate(),
178            Err(McrxError::SsmGroupRequiresSource)
179        ));
180    }
181}