Skip to main content

mctx_core/raw/
context.rs

1use crate::error::MctxError;
2use crate::raw::{RawPublication, RawPublicationConfig, RawPublicationId, RawSendReport};
3use std::collections::HashMap;
4
5/// Owns and manages the set of active raw multicast publications.
6#[derive(Debug, Default)]
7pub struct RawContext {
8    publications: Vec<RawPublication>,
9    publication_indices: HashMap<RawPublicationId, usize>,
10    next_publication_id: u64,
11}
12
13impl RawContext {
14    /// Creates an empty raw context with no publications.
15    pub fn new() -> Self {
16        Self {
17            publications: Vec::new(),
18            publication_indices: HashMap::new(),
19            next_publication_id: 1,
20        }
21    }
22
23    /// Returns the number of active raw publications.
24    pub fn publication_count(&self) -> usize {
25        self.publications.len()
26    }
27
28    /// Returns true if a raw publication with the given ID exists.
29    pub fn contains_publication(&self, id: RawPublicationId) -> bool {
30        self.publication_indices.contains_key(&id)
31    }
32
33    /// Returns the raw publication with the given ID, if present.
34    pub fn get_publication(&self, id: RawPublicationId) -> Option<&RawPublication> {
35        let index = *self.publication_indices.get(&id)?;
36        self.publications.get(index)
37    }
38
39    /// Returns the raw publication mutably with the given ID, if present.
40    pub fn get_publication_mut(&mut self, id: RawPublicationId) -> Option<&mut RawPublication> {
41        let index = *self.publication_indices.get(&id)?;
42        self.publications.get_mut(index)
43    }
44
45    fn ensure_publication_config_is_unique(
46        &self,
47        config: &RawPublicationConfig,
48    ) -> Result<(), MctxError> {
49        if self
50            .publications
51            .iter()
52            .any(|publication| publication.config() == config)
53        {
54            return Err(MctxError::DuplicatePublication);
55        }
56
57        Ok(())
58    }
59
60    /// Adds a new raw publication to the context.
61    pub fn add_publication(
62        &mut self,
63        config: RawPublicationConfig,
64    ) -> Result<RawPublicationId, MctxError> {
65        self.ensure_publication_config_is_unique(&config)?;
66
67        let id = RawPublicationId(self.next_publication_id);
68        self.next_publication_id += 1;
69
70        let publication = RawPublication::new(id, config)?;
71        let index = self.publications.len();
72        self.publications.push(publication);
73        self.publication_indices.insert(id, index);
74        Ok(id)
75    }
76
77    /// Removes one raw publication and drops its socket.
78    pub fn remove_publication(&mut self, id: RawPublicationId) -> bool {
79        self.take_publication(id).is_some()
80    }
81
82    /// Extracts one raw publication from the context.
83    pub fn take_publication(&mut self, id: RawPublicationId) -> Option<RawPublication> {
84        let index = self.publication_indices.remove(&id)?;
85        let publication = self.publications.swap_remove(index);
86        if index < self.publications.len() {
87            let moved_id = self.publications[index].id();
88            self.publication_indices.insert(moved_id, index);
89        }
90
91        Some(publication)
92    }
93
94    /// Sends one full IP datagram through the selected raw publication.
95    pub fn send_raw(
96        &self,
97        id: RawPublicationId,
98        ip_datagram: &[u8],
99    ) -> Result<RawSendReport, MctxError> {
100        self.get_publication(id)
101            .ok_or(MctxError::PublicationNotFound)?
102            .send_raw(ip_datagram)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    #[cfg(any(target_os = "linux", target_os = "macos", windows))]
110    use crate::test_support::TEST_GROUP;
111    #[cfg(any(target_os = "linux", target_os = "macos"))]
112    use crate::test_support::recv_payload;
113    #[cfg(target_os = "macos")]
114    use crate::test_support::test_multicast_receiver;
115    #[cfg(any(target_os = "linux", target_os = "macos"))]
116    use crate::test_support::test_multicast_receiver_v6;
117    #[cfg(any(target_os = "linux", target_os = "macos"))]
118    use std::net::Ipv6Addr;
119    #[cfg(any(target_os = "linux", target_os = "macos", windows))]
120    use std::net::{IpAddr, Ipv4Addr};
121
122    #[cfg(any(target_os = "linux", target_os = "macos", windows))]
123    #[test]
124    fn raw_context_requires_explicit_interface_selection_before_socket_setup() {
125        let mut ctx = RawContext::new();
126
127        let err = ctx
128            .add_publication(RawPublicationConfig::ipv4())
129            .unwrap_err();
130        assert!(matches!(err, MctxError::RawInterfaceRequired));
131    }
132
133    #[cfg(any(target_os = "linux", target_os = "macos"))]
134    #[test]
135    fn raw_context_updates_id_lookup_after_swap_remove() {
136        let mut ctx = RawContext::new();
137        let first = ctx
138            .add_publication(RawPublicationConfig::ipv6().with_ipv6_interface_index(7))
139            .unwrap();
140        let second = ctx
141            .add_publication(RawPublicationConfig::ipv6().with_ipv6_interface_index(8))
142            .unwrap();
143
144        assert!(ctx.remove_publication(first));
145        assert!(!ctx.contains_publication(first));
146        assert!(ctx.contains_publication(second));
147        assert_eq!(
148            ctx.get_publication(second).map(RawPublication::id),
149            Some(second)
150        );
151    }
152
153    #[cfg(windows)]
154    #[test]
155    fn windows_raw_ipv6_support_is_explicitly_unsupported() {
156        let mut ctx = RawContext::new();
157
158        let err = ctx
159            .add_publication(RawPublicationConfig::ipv6().with_ipv6_interface_index(7))
160            .unwrap_err();
161        assert!(matches!(err, MctxError::RawPacketTransmitUnsupported(_)));
162    }
163
164    #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
165    #[test]
166    fn unsupported_platforms_report_explicit_raw_transmit_unsupported_errors() {
167        let mut ctx = RawContext::new();
168
169        let err = ctx
170            .add_publication(RawPublicationConfig::ipv6().with_ipv6_interface_index(7))
171            .unwrap_err();
172        assert!(matches!(err, MctxError::RawPacketTransmitUnsupported(_)));
173    }
174
175    #[cfg(target_os = "linux")]
176    #[test]
177    #[ignore = "requires CAP_NET_RAW and MCTX_RAW_TEST_SOURCE_V4 set to a local IPv4 address; validates send success/report only"]
178    fn linux_raw_ipv4_send_report_smoke_test() {
179        run_raw_ipv4_send_report_smoke_test();
180    }
181
182    #[cfg(target_os = "macos")]
183    #[test]
184    #[ignore = "requires root and MCTX_RAW_TEST_SOURCE_V4 set to a local IPv4 address"]
185    fn macos_raw_ipv4_send_smoke_test() {
186        run_raw_ipv4_send_smoke_test();
187    }
188
189    #[cfg(windows)]
190    #[test]
191    #[ignore = "requires Administrator privileges and MCTX_RAW_TEST_SOURCE_V4 set to a local IPv4 address; validates send success/report only"]
192    fn windows_raw_ipv4_send_report_smoke_test() {
193        run_raw_ipv4_send_report_smoke_test();
194    }
195
196    #[cfg(target_os = "macos")]
197    fn run_raw_ipv4_send_smoke_test() {
198        let Some(source) = std::env::var("MCTX_RAW_TEST_SOURCE_V4")
199            .ok()
200            .and_then(|raw| raw.parse::<Ipv4Addr>().ok())
201        else {
202            return;
203        };
204
205        let (receiver, port) = test_multicast_receiver();
206        let mut ctx = RawContext::new();
207        let id = ctx
208            .add_publication(RawPublicationConfig::ipv4().with_bind_addr(source))
209            .unwrap();
210
211        let payload = b"raw-smoke";
212        let datagram = build_ipv4_udp_datagram(source, TEST_GROUP, 4000, port, payload);
213        let report = ctx.send_raw(id, &datagram).unwrap();
214
215        assert_eq!(report.source_ip, Some(IpAddr::V4(source)));
216        assert_eq!(report.destination_ip, Some(IpAddr::V4(TEST_GROUP)));
217        assert_eq!(recv_payload(&receiver), payload);
218    }
219
220    #[cfg(any(target_os = "linux", windows))]
221    fn run_raw_ipv4_send_report_smoke_test() {
222        let Some(source) = std::env::var("MCTX_RAW_TEST_SOURCE_V4")
223            .ok()
224            .and_then(|raw| raw.parse::<Ipv4Addr>().ok())
225        else {
226            return;
227        };
228
229        let mut ctx = RawContext::new();
230        let id = ctx
231            .add_publication(
232                RawPublicationConfig::ipv4()
233                    .with_bind_addr(source)
234                    .with_outgoing_interface(source),
235            )
236            .unwrap();
237
238        let payload = b"raw-smoke";
239        let datagram = build_ipv4_udp_datagram(source, TEST_GROUP, 4000, 5000, payload);
240        let report = ctx.send_raw(id, &datagram).unwrap();
241
242        assert_eq!(report.source_ip, Some(IpAddr::V4(source)));
243        assert_eq!(report.destination_ip, Some(IpAddr::V4(TEST_GROUP)));
244        assert_eq!(report.ip_protocol, Some(17));
245        assert_eq!(report.bytes_sent, datagram.len());
246    }
247
248    #[cfg(target_os = "linux")]
249    #[test]
250    #[ignore = "requires CAP_NET_RAW or root; validates same-host IPv6 ASM raw receive on loopback"]
251    fn linux_raw_ipv6_same_host_smoke_test() {
252        run_raw_ipv6_same_host_smoke_test();
253    }
254
255    #[cfg(target_os = "macos")]
256    #[test]
257    #[ignore = "requires root; validates same-host IPv6 ASM raw receive on loopback"]
258    fn macos_raw_ipv6_same_host_smoke_test() {
259        run_raw_ipv6_same_host_smoke_test();
260    }
261
262    #[cfg(any(target_os = "linux", target_os = "macos"))]
263    fn run_raw_ipv6_same_host_smoke_test() {
264        let group = "ff01::1234".parse::<Ipv6Addr>().unwrap();
265        let source = Ipv6Addr::LOCALHOST;
266        let (receiver, port) = test_multicast_receiver_v6(group, source);
267        let mut ctx = RawContext::new();
268        let id = ctx
269            .add_publication(
270                RawPublicationConfig::ipv6()
271                    .with_bind_addr(source)
272                    .with_outgoing_interface(source)
273                    .with_loopback(true),
274            )
275            .unwrap();
276
277        let payload = b"raw-v6-smoke";
278        let datagram = build_ipv6_udp_datagram(source, group, 4000, port, payload);
279        let report = ctx.send_raw(id, &datagram).unwrap();
280
281        assert_eq!(report.source_ip, Some(IpAddr::V6(source)));
282        assert_eq!(report.destination_ip, Some(IpAddr::V6(group)));
283        assert_eq!(report.ip_protocol, Some(17));
284        assert_eq!(report.bytes_sent, datagram.len());
285        assert_eq!(recv_payload(&receiver), payload);
286    }
287
288    #[cfg(any(target_os = "linux", target_os = "macos", windows))]
289    fn build_ipv4_udp_datagram(
290        source: Ipv4Addr,
291        destination: Ipv4Addr,
292        source_port: u16,
293        destination_port: u16,
294        payload: &[u8],
295    ) -> Vec<u8> {
296        let total_len = 20 + 8 + payload.len();
297        let mut datagram = vec![0u8; total_len];
298
299        datagram[0] = 0x45;
300        datagram[2..4].copy_from_slice(&(total_len as u16).to_be_bytes());
301        datagram[8] = 1;
302        datagram[9] = 17;
303        datagram[12..16].copy_from_slice(&source.octets());
304        datagram[16..20].copy_from_slice(&destination.octets());
305
306        let udp_len = (8 + payload.len()) as u16;
307        datagram[20..22].copy_from_slice(&source_port.to_be_bytes());
308        datagram[22..24].copy_from_slice(&destination_port.to_be_bytes());
309        datagram[24..26].copy_from_slice(&udp_len.to_be_bytes());
310        datagram[26..28].copy_from_slice(&0u16.to_be_bytes());
311        datagram[28..].copy_from_slice(payload);
312
313        let checksum = ipv4_header_checksum(&datagram[..20]);
314        datagram[10..12].copy_from_slice(&checksum.to_be_bytes());
315        datagram
316    }
317
318    #[cfg(any(target_os = "linux", target_os = "macos"))]
319    fn build_ipv6_udp_datagram(
320        source: Ipv6Addr,
321        destination: Ipv6Addr,
322        source_port: u16,
323        destination_port: u16,
324        payload: &[u8],
325    ) -> Vec<u8> {
326        let payload_len = 8 + payload.len();
327        let mut datagram = vec![0u8; 40 + payload_len];
328
329        datagram[0] = 0x60;
330        datagram[4..6].copy_from_slice(&(payload_len as u16).to_be_bytes());
331        datagram[6] = 17;
332        datagram[7] = 1;
333        datagram[8..24].copy_from_slice(&source.octets());
334        datagram[24..40].copy_from_slice(&destination.octets());
335        datagram[40..42].copy_from_slice(&source_port.to_be_bytes());
336        datagram[42..44].copy_from_slice(&destination_port.to_be_bytes());
337        datagram[44..46].copy_from_slice(&(payload_len as u16).to_be_bytes());
338        datagram[46..48].copy_from_slice(&0u16.to_be_bytes());
339        datagram[48..].copy_from_slice(payload);
340
341        let checksum = udp_checksum_v6(source, destination, &datagram[40..]);
342        datagram[46..48].copy_from_slice(&checksum.to_be_bytes());
343        datagram
344    }
345
346    #[cfg(any(target_os = "linux", target_os = "macos", windows))]
347    fn ipv4_header_checksum(header: &[u8]) -> u16 {
348        let mut sum = 0u32;
349
350        for chunk in header.chunks_exact(2) {
351            sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
352        }
353
354        while (sum >> 16) != 0 {
355            sum = (sum & 0xffff) + (sum >> 16);
356        }
357
358        !(sum as u16)
359    }
360
361    #[cfg(any(target_os = "linux", target_os = "macos"))]
362    fn udp_checksum_v6(source: Ipv6Addr, destination: Ipv6Addr, udp_packet: &[u8]) -> u16 {
363        let mut pseudo = Vec::with_capacity(40 + udp_packet.len() + (udp_packet.len() % 2));
364        pseudo.extend_from_slice(&source.octets());
365        pseudo.extend_from_slice(&destination.octets());
366        pseudo.extend_from_slice(&(udp_packet.len() as u32).to_be_bytes());
367        pseudo.extend_from_slice(&[0, 0, 0, 17]);
368        pseudo.extend_from_slice(udp_packet);
369
370        let checksum = ones_complement_checksum(&pseudo);
371        if checksum == 0 { 0xffff } else { checksum }
372    }
373
374    #[cfg(any(target_os = "linux", target_os = "macos"))]
375    fn ones_complement_checksum(bytes: &[u8]) -> u16 {
376        let mut sum = 0u32;
377
378        for chunk in bytes.chunks_exact(2) {
379            sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
380        }
381
382        if !bytes.len().is_multiple_of(2) {
383            sum += (bytes[bytes.len() - 1] as u32) << 8;
384        }
385
386        while (sum >> 16) != 0 {
387            sum = (sum & 0xffff) + (sum >> 16);
388        }
389
390        !(sum as u16)
391    }
392}