1use crate::config::{OutgoingInterface, PublicationAddressFamily};
2use crate::error::MctxError;
3use std::net::IpAddr;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
7pub enum RawValidationMode {
8 #[default]
10 StrictMulticastDestination,
11 AllowAnyDestination,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub struct RawPublicationConfig {
21 pub family: Option<PublicationAddressFamily>,
25 pub outgoing_interface: Option<OutgoingInterface>,
27 pub bind_addr: Option<IpAddr>,
33 pub ttl: Option<u8>,
35 pub loopback: Option<bool>,
37 pub validation_mode: RawValidationMode,
39}
40
41impl Default for RawPublicationConfig {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47impl RawPublicationConfig {
48 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 pub fn ipv4() -> Self {
63 Self::new().with_family(PublicationAddressFamily::Ipv4)
64 }
65
66 pub fn ipv6() -> Self {
68 Self::new().with_family(PublicationAddressFamily::Ipv6)
69 }
70
71 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 pub fn with_family(mut self, family: PublicationAddressFamily) -> Self {
132 self.family = Some(family);
133 self
134 }
135
136 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 pub fn with_interface(self, interface: std::net::Ipv4Addr) -> Self {
147 self.with_outgoing_interface(interface)
148 }
149
150 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 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 pub fn with_ttl(mut self, ttl: u8) -> Self {
164 self.ttl = Some(ttl);
165 self
166 }
167
168 pub fn with_loopback(mut self, loopback: bool) -> Self {
170 self.loopback = Some(loopback);
171 self
172 }
173
174 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}