Skip to main content

mctx_core/
config.rs

1use crate::error::MctxError;
2use std::net::{Ipv4Addr, SocketAddrV4};
3
4/// Configuration for one multicast publication socket.
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6pub struct PublicationConfig {
7    /// The destination multicast group.
8    pub group: Ipv4Addr,
9    /// The destination UDP port.
10    pub dst_port: u16,
11    /// The local interface to use for multicast egress, if explicitly set.
12    pub interface: Option<Ipv4Addr>,
13    /// The source UDP port to bind before sending, if explicitly set.
14    pub source_port: Option<u16>,
15    /// The source IPv4 address to bind before sending, if explicitly set.
16    pub source_addr: Option<Ipv4Addr>,
17    /// The multicast TTL for transmitted packets.
18    pub ttl: u32,
19    /// Whether outbound multicast packets should be looped back to the local host.
20    pub loopback: bool,
21}
22
23impl PublicationConfig {
24    /// Creates a basic multicast publication configuration.
25    pub fn new(group: Ipv4Addr, port: u16) -> Self {
26        Self {
27            group,
28            dst_port: port,
29            interface: None,
30            source_port: None,
31            source_addr: None,
32            ttl: 1,
33            loopback: true,
34        }
35    }
36
37    /// Validates the configuration and returns an error if it is not usable.
38    pub fn validate(&self) -> Result<(), MctxError> {
39        if self.dst_port == 0 {
40            return Err(MctxError::InvalidDestinationPort);
41        }
42
43        if !self.group.is_multicast() {
44            return Err(MctxError::InvalidMulticastGroup);
45        }
46
47        if matches!(self.source_port, Some(0)) {
48            return Err(MctxError::InvalidSourcePort);
49        }
50
51        if let Some(source_addr) = self.source_addr
52            && (source_addr.is_multicast() || source_addr.is_unspecified())
53        {
54            return Err(MctxError::InvalidSourceAddress);
55        }
56
57        if let Some(interface) = self.interface
58            && interface.is_multicast()
59        {
60            return Err(MctxError::InvalidInterfaceAddress);
61        }
62
63        Ok(())
64    }
65
66    /// Sets the multicast egress interface.
67    pub fn with_interface(mut self, interface: Ipv4Addr) -> Self {
68        self.interface = Some(interface);
69        self
70    }
71
72    /// Sets the source UDP port.
73    pub fn with_source_port(mut self, source_port: u16) -> Self {
74        self.source_port = Some(source_port);
75        self
76    }
77
78    /// Sets the exact local IPv4 source address to bind before sending.
79    pub fn with_source_addr(mut self, source_addr: Ipv4Addr) -> Self {
80        self.source_addr = Some(source_addr);
81        self
82    }
83
84    /// Sets the exact local IPv4 address and UDP port to bind before sending.
85    pub fn with_bind_addr(mut self, bind_addr: SocketAddrV4) -> Self {
86        self.source_addr = Some(*bind_addr.ip());
87        self.source_port = Some(bind_addr.port());
88        self
89    }
90
91    /// Sets the multicast TTL.
92    pub fn with_ttl(mut self, ttl: u32) -> Self {
93        self.ttl = ttl;
94        self
95    }
96
97    /// Enables or disables multicast loopback.
98    pub fn with_loopback(mut self, loopback: bool) -> Self {
99        self.loopback = loopback;
100        self
101    }
102
103    /// Returns the configured destination socket address.
104    pub fn destination(&self) -> SocketAddrV4 {
105        SocketAddrV4::new(self.group, self.dst_port)
106    }
107
108    /// Returns the exact local bind address requested by the configuration, if any.
109    pub fn bind_addr(&self) -> Option<SocketAddrV4> {
110        if self.source_addr.is_none() && self.source_port.is_none() {
111            return None;
112        }
113
114        Some(SocketAddrV4::new(
115            self.source_addr.unwrap_or(Ipv4Addr::UNSPECIFIED),
116            self.source_port.unwrap_or(0),
117        ))
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn valid_multicast_config_passes_validation() {
127        let cfg = PublicationConfig::new(Ipv4Addr::new(239, 1, 2, 3), 5000)
128            .with_source_port(5001)
129            .with_source_addr(Ipv4Addr::new(192, 168, 10, 5))
130            .with_ttl(8)
131            .with_loopback(false);
132
133        assert!(cfg.validate().is_ok());
134    }
135
136    #[test]
137    fn port_zero_fails_validation() {
138        let cfg = PublicationConfig::new(Ipv4Addr::new(239, 1, 2, 3), 0);
139
140        let result = cfg.validate();
141
142        assert!(matches!(result, Err(MctxError::InvalidDestinationPort)));
143    }
144
145    #[test]
146    fn non_multicast_group_fails_validation() {
147        let cfg = PublicationConfig::new(Ipv4Addr::new(192, 168, 1, 10), 5000);
148
149        let result = cfg.validate();
150
151        assert!(matches!(result, Err(MctxError::InvalidMulticastGroup)));
152    }
153
154    #[test]
155    fn multicast_interface_fails_validation() {
156        let cfg = PublicationConfig::new(Ipv4Addr::new(239, 1, 2, 3), 5000)
157            .with_interface(Ipv4Addr::new(239, 9, 9, 9));
158
159        let result = cfg.validate();
160
161        assert!(matches!(result, Err(MctxError::InvalidInterfaceAddress)));
162    }
163
164    #[test]
165    fn unspecified_source_addr_fails_validation() {
166        let cfg = PublicationConfig::new(Ipv4Addr::new(239, 1, 2, 3), 5000)
167            .with_source_addr(Ipv4Addr::UNSPECIFIED);
168
169        let result = cfg.validate();
170
171        assert!(matches!(result, Err(MctxError::InvalidSourceAddress)));
172    }
173
174    #[test]
175    fn bind_addr_builder_sets_source_fields() {
176        let bind_addr = SocketAddrV4::new(Ipv4Addr::new(10, 1, 2, 3), 5001);
177        let cfg =
178            PublicationConfig::new(Ipv4Addr::new(239, 1, 2, 3), 5000).with_bind_addr(bind_addr);
179
180        assert_eq!(cfg.source_addr, Some(Ipv4Addr::new(10, 1, 2, 3)));
181        assert_eq!(cfg.source_port, Some(5001));
182        assert_eq!(cfg.bind_addr(), Some(bind_addr));
183    }
184}