1use crate::error::MctxError;
2use crate::raw::{RawPublication, RawPublicationConfig, RawPublicationId, RawSendReport};
3use std::collections::HashMap;
4
5#[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 pub fn new() -> Self {
16 Self {
17 publications: Vec::new(),
18 publication_indices: HashMap::new(),
19 next_publication_id: 1,
20 }
21 }
22
23 pub fn publication_count(&self) -> usize {
25 self.publications.len()
26 }
27
28 pub fn contains_publication(&self, id: RawPublicationId) -> bool {
30 self.publication_indices.contains_key(&id)
31 }
32
33 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 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 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 pub fn remove_publication(&mut self, id: RawPublicationId) -> bool {
79 self.take_publication(id).is_some()
80 }
81
82 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 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}