1use crate::error::MctxError;
2use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum PublicationAddressFamily {
7 Ipv4,
8 Ipv6,
9}
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum OutgoingInterface {
14 Ipv4Addr(Ipv4Addr),
16 Ipv6Addr(Ipv6Addr),
21 Ipv6Index(u32),
23}
24
25impl From<Ipv4Addr> for OutgoingInterface {
26 fn from(value: Ipv4Addr) -> Self {
27 Self::Ipv4Addr(value)
28 }
29}
30
31impl From<Ipv6Addr> for OutgoingInterface {
32 fn from(value: Ipv6Addr) -> Self {
33 Self::Ipv6Addr(value)
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Ipv6MulticastScope {
40 InterfaceLocal,
41 LinkLocal,
42 RealmLocal,
43 AdminLocal,
44 SiteLocal,
45 OrganizationLocal,
46 Global,
47 Other(u8),
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Hash)]
52pub struct PublicationConfig {
53 pub group: IpAddr,
55 pub dst_port: u16,
57 pub outgoing_interface: Option<OutgoingInterface>,
59 pub source_port: Option<u16>,
61 pub source_addr: Option<IpAddr>,
63 pub ttl: u32,
65 pub loopback: bool,
67}
68
69impl PublicationConfig {
70 pub fn new(group: impl Into<IpAddr>, port: u16) -> Self {
72 Self {
73 group: group.into(),
74 dst_port: port,
75 outgoing_interface: None,
76 source_port: None,
77 source_addr: None,
78 ttl: 1,
79 loopback: true,
80 }
81 }
82
83 pub fn family(&self) -> PublicationAddressFamily {
85 match self.group {
86 IpAddr::V4(_) => PublicationAddressFamily::Ipv4,
87 IpAddr::V6(_) => PublicationAddressFamily::Ipv6,
88 }
89 }
90
91 pub fn is_ipv4(&self) -> bool {
93 matches!(self.family(), PublicationAddressFamily::Ipv4)
94 }
95
96 pub fn is_ipv6(&self) -> bool {
98 matches!(self.family(), PublicationAddressFamily::Ipv6)
99 }
100
101 pub fn validate(&self) -> Result<(), MctxError> {
103 if self.dst_port == 0 {
104 return Err(MctxError::InvalidDestinationPort);
105 }
106
107 if !self.group.is_multicast() {
108 return Err(MctxError::InvalidMulticastGroup);
109 }
110
111 if matches!(self.source_port, Some(0)) {
112 return Err(MctxError::InvalidSourcePort);
113 }
114
115 if let Some(source_addr) = self.source_addr {
116 if source_addr.is_multicast() || source_addr.is_unspecified() {
117 return Err(MctxError::InvalidSourceAddress);
118 }
119
120 if !same_family_ip(self.group, source_addr) {
121 return Err(MctxError::SourceAddressFamilyMismatch);
122 }
123 }
124
125 if let Some(interface) = self.outgoing_interface {
126 match (self.family(), interface) {
127 (PublicationAddressFamily::Ipv4, OutgoingInterface::Ipv4Addr(interface)) => {
128 if interface.is_multicast() || interface.is_unspecified() {
129 return Err(MctxError::InvalidInterfaceAddress);
130 }
131 }
132 (PublicationAddressFamily::Ipv4, OutgoingInterface::Ipv6Addr(_))
133 | (PublicationAddressFamily::Ipv4, OutgoingInterface::Ipv6Index(_)) => {
134 return Err(MctxError::OutgoingInterfaceFamilyMismatch);
135 }
136 (PublicationAddressFamily::Ipv6, OutgoingInterface::Ipv4Addr(_)) => {
137 return Err(MctxError::OutgoingInterfaceFamilyMismatch);
138 }
139 (PublicationAddressFamily::Ipv6, OutgoingInterface::Ipv6Addr(interface)) => {
140 if interface.is_multicast() || interface.is_unspecified() {
141 return Err(MctxError::InvalidInterfaceAddress);
142 }
143 }
144 (PublicationAddressFamily::Ipv6, OutgoingInterface::Ipv6Index(index)) => {
145 if index == 0 {
146 return Err(MctxError::InvalidIpv6InterfaceIndex);
147 }
148 }
149 }
150 }
151
152 Ok(())
153 }
154
155 pub fn with_outgoing_interface(
157 mut self,
158 outgoing_interface: impl Into<OutgoingInterface>,
159 ) -> Self {
160 self.outgoing_interface = Some(outgoing_interface.into());
161 self
162 }
163
164 pub fn with_interface(self, interface: Ipv4Addr) -> Self {
167 self.with_outgoing_interface(interface)
168 }
169
170 pub fn with_ipv6_interface_index(mut self, interface_index: u32) -> Self {
172 self.outgoing_interface = Some(OutgoingInterface::Ipv6Index(interface_index));
173 self
174 }
175
176 pub fn with_source_port(mut self, source_port: u16) -> Self {
178 self.source_port = Some(source_port);
179 self
180 }
181
182 pub fn with_source_addr(mut self, source_addr: impl Into<IpAddr>) -> Self {
184 self.source_addr = Some(source_addr.into());
185 self
186 }
187
188 pub fn with_bind_addr(mut self, bind_addr: impl Into<SocketAddr>) -> Self {
190 let bind_addr = bind_addr.into();
191 self.source_addr = Some(bind_addr.ip());
192 self.source_port = Some(bind_addr.port());
193
194 if let SocketAddr::V6(bind_addr_v6) = bind_addr
197 && bind_addr_v6.scope_id() != 0
198 {
199 self.outgoing_interface = Some(OutgoingInterface::Ipv6Index(bind_addr_v6.scope_id()));
200 }
201
202 self
203 }
204
205 pub fn with_ttl(mut self, ttl: u32) -> Self {
207 self.ttl = ttl;
208 self
209 }
210
211 pub fn with_loopback(mut self, loopback: bool) -> Self {
213 self.loopback = loopback;
214 self
215 }
216
217 pub fn ipv6_scope(&self) -> Option<Ipv6MulticastScope> {
219 match self.group {
220 IpAddr::V6(group) => ipv6_multicast_scope(group),
221 IpAddr::V4(_) => None,
222 }
223 }
224}
225
226fn same_family_ip(left: IpAddr, right: IpAddr) -> bool {
227 matches!(
228 (left, right),
229 (IpAddr::V4(_), IpAddr::V4(_)) | (IpAddr::V6(_), IpAddr::V6(_))
230 )
231}
232
233pub fn is_ipv6_ssm_group(group: Ipv6Addr) -> bool {
235 let octets = group.octets();
236 group.is_multicast() && (octets[1] & 0xf0) == 0x30 && octets[2] == 0 && octets[3] == 0
237}
238
239pub(crate) fn ipv6_multicast_scope(group: Ipv6Addr) -> Option<Ipv6MulticastScope> {
240 if !group.is_multicast() {
241 return None;
242 }
243
244 let scope = group.octets()[1] & 0x0f;
245 Some(match scope {
246 0x1 => Ipv6MulticastScope::InterfaceLocal,
247 0x2 => Ipv6MulticastScope::LinkLocal,
248 0x3 => Ipv6MulticastScope::RealmLocal,
249 0x4 => Ipv6MulticastScope::AdminLocal,
250 0x5 => Ipv6MulticastScope::SiteLocal,
251 0x8 => Ipv6MulticastScope::OrganizationLocal,
252 0xe => Ipv6MulticastScope::Global,
253 other => Ipv6MulticastScope::Other(other),
254 })
255}
256
257pub(crate) fn ipv6_destination_scope_id(group: Ipv6Addr, interface_index: u32) -> u32 {
258 match ipv6_multicast_scope(group) {
259 Some(Ipv6MulticastScope::InterfaceLocal | Ipv6MulticastScope::LinkLocal) => interface_index,
260 _ => 0,
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use std::net::{SocketAddrV4, SocketAddrV6};
268
269 #[test]
270 fn valid_ipv4_multicast_config_passes_validation() {
271 let cfg = PublicationConfig::new(Ipv4Addr::new(239, 1, 2, 3), 5000)
272 .with_source_port(5001)
273 .with_source_addr(Ipv4Addr::new(192, 168, 10, 5))
274 .with_ttl(8)
275 .with_loopback(false);
276
277 assert!(cfg.validate().is_ok());
278 }
279
280 #[test]
281 fn valid_ipv6_multicast_config_passes_validation() {
282 let cfg = PublicationConfig::new("ff31::8000:1234".parse::<Ipv6Addr>().unwrap(), 5000)
283 .with_source_addr("::1".parse::<Ipv6Addr>().unwrap())
284 .with_outgoing_interface("::1".parse::<Ipv6Addr>().unwrap())
285 .with_ttl(4);
286
287 assert!(cfg.validate().is_ok());
288 assert!(cfg.is_ipv6());
289 }
290
291 #[test]
292 fn port_zero_fails_validation() {
293 let cfg = PublicationConfig::new(Ipv4Addr::new(239, 1, 2, 3), 0);
294
295 let result = cfg.validate();
296
297 assert!(matches!(result, Err(MctxError::InvalidDestinationPort)));
298 }
299
300 #[test]
301 fn non_multicast_group_fails_validation() {
302 let cfg = PublicationConfig::new(Ipv4Addr::new(192, 168, 1, 10), 5000);
303
304 let result = cfg.validate();
305
306 assert!(matches!(result, Err(MctxError::InvalidMulticastGroup)));
307 }
308
309 #[test]
310 fn family_mismatched_source_fails_validation() {
311 let cfg = PublicationConfig::new(Ipv4Addr::new(239, 1, 2, 3), 5000)
312 .with_source_addr("::1".parse::<Ipv6Addr>().unwrap());
313
314 let result = cfg.validate();
315
316 assert!(matches!(
317 result,
318 Err(MctxError::SourceAddressFamilyMismatch)
319 ));
320 }
321
322 #[test]
323 fn family_mismatched_interface_fails_validation() {
324 let cfg = PublicationConfig::new("ff31::8000:1234".parse::<Ipv6Addr>().unwrap(), 5000)
325 .with_interface(Ipv4Addr::new(192, 168, 1, 10));
326
327 let result = cfg.validate();
328
329 assert!(matches!(
330 result,
331 Err(MctxError::OutgoingInterfaceFamilyMismatch)
332 ));
333 }
334
335 #[test]
336 fn unspecified_source_addr_fails_validation() {
337 let cfg = PublicationConfig::new(Ipv4Addr::new(239, 1, 2, 3), 5000)
338 .with_source_addr(Ipv4Addr::UNSPECIFIED);
339
340 let result = cfg.validate();
341
342 assert!(matches!(result, Err(MctxError::InvalidSourceAddress)));
343 }
344
345 #[test]
346 fn zero_ipv6_interface_index_fails_validation() {
347 let cfg = PublicationConfig::new("ff31::8000:1234".parse::<Ipv6Addr>().unwrap(), 5000)
348 .with_ipv6_interface_index(0);
349
350 let result = cfg.validate();
351
352 assert!(matches!(result, Err(MctxError::InvalidIpv6InterfaceIndex)));
353 }
354
355 #[test]
356 fn bind_addr_builder_sets_source_fields_for_ipv4() {
357 let bind_addr = SocketAddrV4::new(Ipv4Addr::new(10, 1, 2, 3), 5001);
358 let cfg =
359 PublicationConfig::new(Ipv4Addr::new(239, 1, 2, 3), 5000).with_bind_addr(bind_addr);
360
361 assert_eq!(
362 cfg.source_addr,
363 Some(IpAddr::V4(Ipv4Addr::new(10, 1, 2, 3)))
364 );
365 assert_eq!(cfg.source_port, Some(5001));
366 }
367
368 #[test]
369 fn bind_addr_builder_sets_source_fields_for_ipv6() {
370 let bind_addr = SocketAddrV6::new("fd00::10".parse().unwrap(), 5001, 0, 0);
371 let cfg = PublicationConfig::new("ff3e::8000:1234".parse::<Ipv6Addr>().unwrap(), 5000)
372 .with_bind_addr(bind_addr);
373
374 assert_eq!(
375 cfg.source_addr,
376 Some(IpAddr::V6("fd00::10".parse::<Ipv6Addr>().unwrap()))
377 );
378 assert_eq!(cfg.source_port, Some(5001));
379 }
380
381 #[test]
382 fn bind_addr_builder_preserves_ipv6_scope_as_interface_index() {
383 let bind_addr = SocketAddrV6::new("fe80::1234".parse().unwrap(), 5001, 0, 7);
384 let cfg = PublicationConfig::new("ff32::8000:1234".parse::<Ipv6Addr>().unwrap(), 5000)
385 .with_bind_addr(bind_addr);
386
387 assert_eq!(
388 cfg.outgoing_interface,
389 Some(OutgoingInterface::Ipv6Index(7))
390 );
391 }
392
393 #[test]
394 fn ipv6_ssm_detection_only_matches_ff3x_groups() {
395 assert!(is_ipv6_ssm_group("ff31::8000:1234".parse().unwrap()));
396 assert!(is_ipv6_ssm_group("ff3e::8000:1234".parse().unwrap()));
397 assert!(!is_ipv6_ssm_group("ff12::1234".parse().unwrap()));
398 assert!(!is_ipv6_ssm_group("ff31:1234::1".parse().unwrap()));
399 }
400
401 #[test]
402 fn link_local_ipv6_group_keeps_interface_index_in_destination_scope() {
403 let group = "ff32::8000:1234".parse::<Ipv6Addr>().unwrap();
404
405 assert_eq!(ipv6_destination_scope_id(group, 7), 7);
406 }
407
408 #[test]
409 fn wider_scope_ipv6_group_clears_destination_scope() {
410 let group = "ff3e::8000:1234".parse::<Ipv6Addr>().unwrap();
411
412 assert_eq!(ipv6_destination_scope_id(group, 7), 0);
413 }
414}