1use crate::error::McrxError;
2use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
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 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}