Skip to main content

mctx_core/raw/
config.rs

1use crate::config::{OutgoingInterface, PublicationAddressFamily};
2use crate::error::MctxError;
3use std::net::IpAddr;
4
5/// Validation behavior applied to outbound raw IP datagrams.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
7pub enum RawValidationMode {
8    /// Require the parsed destination IP address to be multicast.
9    #[default]
10    StrictMulticastDestination,
11    /// Allow non-multicast destinations through validation.
12    ///
13    /// Individual platform backends can still return an explicit unsupported
14    /// error when they cannot route a non-multicast raw datagram faithfully.
15    AllowAnyDestination,
16}
17
18/// Configuration for one raw multicast transmit publication.
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub struct RawPublicationConfig {
21    /// The expected IP family for outbound datagrams, if fixed in advance.
22    /// Otherwise it is inferred from the local bind or interface selector when
23    /// the publication is created.
24    pub family: Option<PublicationAddressFamily>,
25    /// The explicit egress interface selector, if set.
26    pub outgoing_interface: Option<OutgoingInterface>,
27    /// The local IP address used to select and validate the egress interface.
28    ///
29    /// The source IP seen by receivers comes from the supplied datagram. This
30    /// field only identifies a local egress address. A matching IPv6 source can
31    /// use the host IP stack; a distinct source requires a link-layer backend.
32    pub bind_addr: Option<IpAddr>,
33    /// Optional TTL or hop-limit override applied during transmit.
34    pub ttl: Option<u8>,
35    /// Optional loopback preference.
36    pub loopback: Option<bool>,
37    /// Validation behavior for outbound datagrams.
38    pub validation_mode: RawValidationMode,
39}
40
41impl Default for RawPublicationConfig {
42    fn default() -> Self {
43        Self::new()
44    }
45}
46
47impl RawPublicationConfig {
48    /// Creates a raw publication config with family inferred from its local
49    /// bind or outgoing-interface selector.
50    pub fn new() -> Self {
51        Self {
52            family: None,
53            outgoing_interface: None,
54            bind_addr: None,
55            ttl: None,
56            loopback: None,
57            validation_mode: RawValidationMode::StrictMulticastDestination,
58        }
59    }
60
61    /// Creates a config fixed to IPv4 datagrams.
62    pub fn ipv4() -> Self {
63        Self::new().with_family(PublicationAddressFamily::Ipv4)
64    }
65
66    /// Creates a config fixed to IPv6 datagrams.
67    pub fn ipv6() -> Self {
68        Self::new().with_family(PublicationAddressFamily::Ipv6)
69    }
70
71    /// Validates the configuration and returns an error if it is not usable.
72    pub fn validate(&self) -> Result<(), MctxError> {
73        if self.bind_addr.is_none() && self.outgoing_interface.is_none() {
74            return Err(MctxError::RawInterfaceRequired);
75        }
76
77        if let Some(bind_addr) = self.bind_addr {
78            if bind_addr.is_multicast() || bind_addr.is_unspecified() {
79                return Err(MctxError::InvalidRawBindAddress);
80            }
81
82            if let Some(family) = self.family
83                && !family_matches_ip(family, bind_addr)
84            {
85                return Err(MctxError::RawBindAddressFamilyMismatch);
86            }
87        }
88
89        if let Some(outgoing_interface) = self.outgoing_interface {
90            match outgoing_interface {
91                OutgoingInterface::Ipv4Addr(interface) => {
92                    if interface.is_multicast() || interface.is_unspecified() {
93                        return Err(MctxError::InvalidInterfaceAddress);
94                    }
95
96                    if matches!(self.family, Some(PublicationAddressFamily::Ipv6)) {
97                        return Err(MctxError::OutgoingInterfaceFamilyMismatch);
98                    }
99                }
100                OutgoingInterface::Ipv6Addr(interface) => {
101                    if interface.is_multicast() || interface.is_unspecified() {
102                        return Err(MctxError::InvalidInterfaceAddress);
103                    }
104
105                    if matches!(self.family, Some(PublicationAddressFamily::Ipv4)) {
106                        return Err(MctxError::OutgoingInterfaceFamilyMismatch);
107                    }
108                }
109                OutgoingInterface::Ipv6Index(index) => {
110                    if index == 0 {
111                        return Err(MctxError::InvalidIpv6InterfaceIndex);
112                    }
113
114                    if matches!(self.family, Some(PublicationAddressFamily::Ipv4)) {
115                        return Err(MctxError::OutgoingInterfaceFamilyMismatch);
116                    }
117                }
118            }
119
120            if let Some(bind_addr) = self.bind_addr
121                && !interface_matches_ip(outgoing_interface, bind_addr)
122            {
123                return Err(MctxError::OutgoingInterfaceFamilyMismatch);
124            }
125        }
126
127        Ok(())
128    }
129
130    /// Pins the expected IP family for datagrams sent through this publication.
131    pub fn with_family(mut self, family: PublicationAddressFamily) -> Self {
132        self.family = Some(family);
133        self
134    }
135
136    /// Sets the outgoing interface selector.
137    pub fn with_outgoing_interface(
138        mut self,
139        outgoing_interface: impl Into<OutgoingInterface>,
140    ) -> Self {
141        self.outgoing_interface = Some(outgoing_interface.into());
142        self
143    }
144
145    /// Sets the IPv4-oriented interface convenience selector.
146    pub fn with_interface(self, interface: std::net::Ipv4Addr) -> Self {
147        self.with_outgoing_interface(interface)
148    }
149
150    /// Sets the IPv6 interface selector by interface index.
151    pub fn with_ipv6_interface_index(mut self, interface_index: u32) -> Self {
152        self.outgoing_interface = Some(OutgoingInterface::Ipv6Index(interface_index));
153        self
154    }
155
156    /// Sets the local IP address used to select and validate the egress interface.
157    pub fn with_bind_addr(mut self, bind_addr: impl Into<IpAddr>) -> Self {
158        self.bind_addr = Some(bind_addr.into());
159        self
160    }
161
162    /// Sets an optional TTL or hop-limit override.
163    pub fn with_ttl(mut self, ttl: u8) -> Self {
164        self.ttl = Some(ttl);
165        self
166    }
167
168    /// Requests an explicit loopback preference.
169    pub fn with_loopback(mut self, loopback: bool) -> Self {
170        self.loopback = Some(loopback);
171        self
172    }
173
174    /// Adjusts outbound datagram validation behavior.
175    pub fn with_validation_mode(mut self, validation_mode: RawValidationMode) -> Self {
176        self.validation_mode = validation_mode;
177        self
178    }
179}
180
181fn family_matches_ip(family: PublicationAddressFamily, ip: IpAddr) -> bool {
182    matches!(
183        (family, ip),
184        (PublicationAddressFamily::Ipv4, IpAddr::V4(_))
185            | (PublicationAddressFamily::Ipv6, IpAddr::V6(_))
186    )
187}
188
189fn interface_matches_ip(interface: OutgoingInterface, ip: IpAddr) -> bool {
190    matches!(
191        (interface, ip),
192        (OutgoingInterface::Ipv4Addr(_), IpAddr::V4(_))
193            | (
194                OutgoingInterface::Ipv6Addr(_) | OutgoingInterface::Ipv6Index(_),
195                IpAddr::V6(_)
196            )
197    )
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use std::net::{Ipv4Addr, Ipv6Addr};
204
205    #[test]
206    fn valid_ipv4_raw_config_passes_validation() {
207        let cfg = RawPublicationConfig::ipv4()
208            .with_bind_addr(Ipv4Addr::new(192, 168, 1, 20))
209            .with_outgoing_interface(Ipv4Addr::new(192, 168, 1, 20))
210            .with_ttl(8);
211
212        assert!(cfg.validate().is_ok());
213    }
214
215    #[test]
216    fn valid_ipv6_raw_config_passes_validation() {
217        let cfg = RawPublicationConfig::ipv6()
218            .with_bind_addr("2001:db8::10".parse::<Ipv6Addr>().unwrap())
219            .with_ipv6_interface_index(7)
220            .with_validation_mode(RawValidationMode::AllowAnyDestination);
221
222        assert!(cfg.validate().is_ok());
223    }
224
225    #[test]
226    fn raw_bind_address_must_be_unicast() {
227        let cfg = RawPublicationConfig::new().with_bind_addr(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
228
229        assert!(matches!(
230            cfg.validate(),
231            Err(MctxError::InvalidRawBindAddress)
232        ));
233    }
234
235    #[test]
236    fn raw_bind_address_family_must_match_config_family() {
237        let cfg = RawPublicationConfig::ipv6().with_bind_addr(Ipv4Addr::new(10, 0, 0, 1));
238
239        assert!(matches!(
240            cfg.validate(),
241            Err(MctxError::RawBindAddressFamilyMismatch)
242        ));
243    }
244
245    #[test]
246    fn ipv4_raw_config_rejects_ipv6_interface_index() {
247        let cfg = RawPublicationConfig::ipv4().with_ipv6_interface_index(7);
248
249        assert!(matches!(
250            cfg.validate(),
251            Err(MctxError::OutgoingInterfaceFamilyMismatch)
252        ));
253    }
254
255    #[test]
256    fn inferred_family_rejects_mismatched_bind_and_interface() {
257        let cfg = RawPublicationConfig::new()
258            .with_bind_addr(Ipv4Addr::new(10, 0, 0, 1))
259            .with_ipv6_interface_index(7);
260
261        assert!(matches!(
262            cfg.validate(),
263            Err(MctxError::OutgoingInterfaceFamilyMismatch)
264        ));
265    }
266
267    #[test]
268    fn raw_config_requires_an_egress_selector() {
269        assert!(matches!(
270            RawPublicationConfig::ipv4().validate(),
271            Err(MctxError::RawInterfaceRequired)
272        ));
273    }
274}