1use crate::error::McrxError;
2use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum SubscriptionAddressFamily {
7 Ipv4,
9 Ipv6,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum SourceFilter {
17 Any,
19 Source(IpAddr),
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct SubscriptionConfig {
29 pub group: IpAddr,
31 pub source: SourceFilter,
33 pub dst_port: u16,
35 pub interface: Option<IpAddr>,
37 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 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 pub fn family(&self) -> SubscriptionAddressFamily {
76 match self.group {
77 IpAddr::V4(_) => SubscriptionAddressFamily::Ipv4,
78 IpAddr::V6(_) => SubscriptionAddressFamily::Ipv6,
79 }
80 }
81
82 pub fn is_ipv4(&self) -> bool {
84 matches!(self.family(), SubscriptionAddressFamily::Ipv4)
85 }
86
87 pub fn is_ipv6(&self) -> bool {
89 matches!(self.family(), SubscriptionAddressFamily::Ipv6)
90 }
91
92 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 pub fn asm(group: Ipv4Addr, port: u16) -> Self {
102 Self::asm_ip(group.into(), port)
103 }
104
105 pub fn asm_v6(group: Ipv6Addr, port: u16) -> Self {
107 Self::asm_ip(group.into(), port)
108 }
109
110 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 pub fn ssm(group: Ipv4Addr, source: Ipv4Addr, port: u16) -> Self {
123 Self::ssm_ip(group.into(), source.into(), port)
124 }
125
126 pub fn ssm_v6(group: Ipv6Addr, source: Ipv6Addr, port: u16) -> Self {
128 Self::ssm_ip(group.into(), source.into(), port)
129 }
130
131 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}