Skip to main content

microsandbox_network/
udp_relay.rs

1//! Non-DNS UDP relay: handles UDP traffic outside smoltcp.
2//!
3//! smoltcp has no wildcard port binding, so non-DNS UDP is intercepted at
4//! the device level, relayed through host UDP sockets via tokio, and
5//! responses are injected back into `rx_ring` as constructed ethernet frames.
6
7use std::collections::{HashMap, VecDeque};
8use std::io;
9use std::net::{IpAddr, Ipv4Addr, SocketAddr};
10use std::ops::Range;
11#[cfg(target_os = "linux")]
12use std::os::fd::AsRawFd;
13use std::sync::Arc;
14use std::sync::atomic::{AtomicU16, AtomicU32, AtomicUsize, Ordering};
15use std::time::{Duration, Instant};
16
17use bytes::Bytes;
18use smoltcp::wire::{
19    EthernetAddress, EthernetFrame, EthernetProtocol, EthernetRepr, IpProtocol, Ipv4Packet,
20    Ipv6Packet, UdpPacket,
21};
22use socket2::{Domain, Protocol as SocketProtocol, Socket, Type};
23use tokio::io::Interest;
24use tokio::net::UdpSocket;
25use tokio::sync::mpsc;
26
27use crate::icmp_error::{construct_packet_too_big, ethernet_ip_payload};
28use crate::shared::SharedState;
29
30//--------------------------------------------------------------------------------------------------
31// Constants
32//--------------------------------------------------------------------------------------------------
33
34/// Session idle timeout.
35const SESSION_TIMEOUT: Duration = Duration::from_secs(60);
36
37/// Channel capacity for outbound datagrams to the relay task.
38const OUTBOUND_CHANNEL_CAPACITY: usize = 64;
39
40/// Default max concurrent UDP relay sessions per sandbox.
41const MAX_UDP_SESSIONS: usize = 256;
42
43/// Maximum queued bytes for one UDP relay session.
44const MAX_QUEUED_BYTES_PER_SESSION: usize = 512 * 1024;
45
46/// Maximum number of sent packet contexts kept for async PMTU errors.
47const MAX_PMTU_CONTEXTS: usize = OUTBOUND_CHANNEL_CAPACITY;
48
49/// Maximum UDP payload carried by an IPv4 packet.
50const MAX_IPV4_UDP_PAYLOAD_LEN: usize = 65_507;
51
52/// Maximum UDP payload carried by a non-jumbo IPv6 packet.
53const MAX_IPV6_UDP_PAYLOAD_LEN: usize = 65_527;
54
55/// Buffer size for receiving responses from the real server.
56const RECV_BUF_SIZE: usize = MAX_IPV6_UDP_PAYLOAD_LEN;
57
58/// Ethernet header length.
59const ETH_HDR_LEN: usize = 14;
60
61/// IPv4 header length (no options).
62const IPV4_HDR_LEN: usize = 20;
63
64/// IPv6 header length.
65const IPV6_HDR_LEN: usize = 40;
66
67/// IPv6 Fragment extension header length.
68const IPV6_FRAGMENT_HDR_LEN: usize = 8;
69
70/// UDP header length.
71const UDP_HDR_LEN: usize = 8;
72
73/// IPv4 identification sequence for fragmented guest-bound UDP responses.
74static NEXT_IPV4_RESPONSE_IDENT: AtomicU16 = AtomicU16::new(1);
75
76/// IPv6 fragment identification sequence for guest-bound UDP responses.
77static NEXT_IPV6_RESPONSE_IDENT: AtomicU32 = AtomicU32::new(1);
78
79//--------------------------------------------------------------------------------------------------
80// Types
81//--------------------------------------------------------------------------------------------------
82
83/// Relays non-DNS UDP traffic between the guest and the real network.
84///
85/// Each unique `(guest_src, guest_dst)` pair gets a host-side UDP socket
86/// and a tokio relay task. The poll loop calls [`relay_outbound()`] to
87/// send guest datagrams; response frames are injected directly into
88/// `rx_ring`.
89///
90/// [`relay_outbound()`]: UdpRelay::relay_outbound
91pub struct UdpRelay {
92    shared: Arc<SharedState>,
93    sessions: HashMap<(SocketAddr, SocketAddr), UdpSession>,
94    gateway_mac: EthernetAddress,
95    guest_mac: EthernetAddress,
96    mtu: usize,
97    tokio_handle: tokio::runtime::Handle,
98}
99
100/// A single UDP relay session.
101struct UdpSession {
102    /// Channel to send outbound datagrams to the relay task.
103    outbound_tx: mpsc::Sender<OutboundDatagram>,
104    /// Approximate bytes currently queued on `outbound_tx`.
105    queued_bytes: Arc<AtomicUsize>,
106    /// Last time this session was used.
107    last_active: Instant,
108}
109
110/// Outbound datagram plus enough original IP context to synthesize ICMP errors.
111struct OutboundDatagram {
112    /// UDP payload to send through the host socket.
113    payload: Bytes,
114    /// Original guest IP packet, without the Ethernet header.
115    original_ip_packet: Bytes,
116}
117
118impl OutboundDatagram {
119    /// Bytes charged against the per-session queue budget.
120    fn queued_len(&self) -> usize {
121        self.payload.len() + self.original_ip_packet.len()
122    }
123}
124
125//--------------------------------------------------------------------------------------------------
126// Methods
127//--------------------------------------------------------------------------------------------------
128
129impl UdpRelay {
130    /// Build a new UDP relay.
131    ///
132    /// # Arguments
133    ///
134    /// * `shared` - Stack-wide shared state used to inject response frames into `rx_ring`
135    ///   and wake the poll thread.
136    /// * `gateway_mac` - MAC address stamped as the source on synthesized response frames.
137    /// * `guest_mac` - MAC address stamped as the destination on synthesized response frames.
138    /// * `mtu` - Guest IP-level MTU. Large UDP replies are fragmented to fit it.
139    /// * `tokio_handle` - Runtime the per-session relay tasks are spawned on.
140    pub fn new(
141        shared: Arc<SharedState>,
142        gateway_mac: [u8; 6],
143        guest_mac: [u8; 6],
144        mtu: usize,
145        tokio_handle: tokio::runtime::Handle,
146    ) -> Self {
147        Self {
148            shared,
149            sessions: HashMap::new(),
150            gateway_mac: EthernetAddress(gateway_mac),
151            guest_mac: EthernetAddress(guest_mac),
152            mtu,
153            tokio_handle,
154        }
155    }
156
157    /// Relay an outbound UDP datagram from the guest.
158    ///
159    /// # Arguments
160    ///
161    /// * `frame` - Raw ethernet frame captured from the guest.
162    /// * `src` - Guest source address; keys the session and becomes the destination on
163    ///   response frames.
164    /// * `guest_dst` - Destination the guest wrote on the datagram. Retained as the session
165    ///   key and the source IP on replies.
166    /// * `host_dst` - Address the host socket actually connects to. Usually equal to
167    ///   `guest_dst`; the caller substitutes loopback when `guest_dst` matches the gateway IP.
168    pub fn relay_outbound(
169        &mut self,
170        frame: &[u8],
171        src: SocketAddr,
172        guest_dst: SocketAddr,
173        host_dst: SocketAddr,
174    ) {
175        let key = (src, guest_dst);
176        if !self.ensure_session(key, src, guest_dst, host_dst) {
177            return;
178        }
179
180        // Extract after session admission, so rejected session floods do not
181        // pay the large-payload copy cost.
182        let Some(mut datagram) = extract_udp_datagram(frame) else {
183            return;
184        };
185
186        for attempt in 0..2 {
187            let Some(session) = self.sessions.get_mut(&key) else {
188                return;
189            };
190            let queued_len = datagram.queued_len();
191            if !session.try_reserve(queued_len) {
192                tracing::debug!(
193                    guest_src = %src,
194                    guest_dst = %guest_dst,
195                    queued_len,
196                    "UDP relay datagram dropped because session queue budget is full",
197                );
198                return;
199            }
200
201            match session.outbound_tx.try_send(datagram) {
202                Ok(()) => {
203                    session.last_active = Instant::now();
204                    return;
205                }
206                Err(mpsc::error::TrySendError::Full(returned)) => {
207                    session.release(queued_len);
208                    tracing::debug!(
209                        guest_src = %src,
210                        guest_dst = %guest_dst,
211                        "UDP relay datagram dropped because outbound queue is full",
212                    );
213                    drop(returned);
214                    return;
215                }
216                Err(mpsc::error::TrySendError::Closed(returned)) => {
217                    session.release(queued_len);
218                    self.sessions.remove(&key);
219                    datagram = returned;
220                    if attempt == 0 && self.ensure_session(key, src, guest_dst, host_dst) {
221                        continue;
222                    }
223                    tracing::debug!(
224                        guest_src = %src,
225                        guest_dst = %guest_dst,
226                        "UDP relay datagram dropped because session task is closed",
227                    );
228                    return;
229                }
230            }
231        }
232    }
233
234    /// Remove expired sessions.
235    pub fn cleanup_expired(&mut self) {
236        self.sessions
237            .retain(|_, session| session.last_active.elapsed() <= SESSION_TIMEOUT);
238    }
239}
240
241impl UdpRelay {
242    /// Ensure a relay session exists and is fresh for `key`.
243    fn ensure_session(
244        &mut self,
245        key: (SocketAddr, SocketAddr),
246        guest_src: SocketAddr,
247        guest_dst: SocketAddr,
248        host_dst: SocketAddr,
249    ) -> bool {
250        if self
251            .sessions
252            .get(&key)
253            .is_some_and(|s| s.last_active.elapsed() <= SESSION_TIMEOUT)
254        {
255            return true;
256        }
257
258        self.sessions.remove(&key);
259        if self.sessions.len() >= MAX_UDP_SESSIONS {
260            self.evict_oldest();
261        }
262
263        let Some(session) = self.create_session(guest_src, guest_dst, host_dst) else {
264            return false;
265        };
266        self.sessions.insert(key, session);
267        true
268    }
269
270    /// Evict the least recently active relay session.
271    fn evict_oldest(&mut self) {
272        let Some(oldest_key) = self
273            .sessions
274            .iter()
275            .min_by_key(|(_, session)| session.last_active)
276            .map(|(key, _)| *key)
277        else {
278            return;
279        };
280        self.sessions.remove(&oldest_key);
281    }
282
283    /// Create a new relay session: bind a host UDP socket and spawn a task.
284    fn create_session(
285        &self,
286        guest_src: SocketAddr,
287        guest_dst: SocketAddr,
288        host_dst: SocketAddr,
289    ) -> Option<UdpSession> {
290        let (outbound_tx, outbound_rx) = mpsc::channel(OUTBOUND_CHANNEL_CAPACITY);
291        let queued_bytes = Arc::new(AtomicUsize::new(0));
292
293        let shared = self.shared.clone();
294        let gateway_mac = self.gateway_mac;
295        let guest_mac = self.guest_mac;
296        let mtu = self.mtu;
297        let task_queued_bytes = queued_bytes.clone();
298
299        self.tokio_handle.spawn(async move {
300            if let Err(e) = udp_relay_task(
301                outbound_rx,
302                task_queued_bytes,
303                guest_src,
304                guest_dst,
305                host_dst,
306                shared,
307                gateway_mac,
308                guest_mac,
309                mtu,
310            )
311            .await
312            {
313                tracing::debug!(
314                    guest_src = %guest_src,
315                    guest_dst = %guest_dst,
316                    error = %e,
317                    "UDP relay task ended",
318                );
319            }
320        });
321
322        Some(UdpSession {
323            outbound_tx,
324            queued_bytes,
325            last_active: Instant::now(),
326        })
327    }
328}
329
330impl UdpSession {
331    /// Reserve queued bytes for a datagram before it is sent to the task.
332    fn try_reserve(&self, len: usize) -> bool {
333        let mut current = self.queued_bytes.load(Ordering::Acquire);
334        loop {
335            let Some(next) = current.checked_add(len) else {
336                return false;
337            };
338            if next > MAX_QUEUED_BYTES_PER_SESSION {
339                return false;
340            }
341            match self.queued_bytes.compare_exchange_weak(
342                current,
343                next,
344                Ordering::AcqRel,
345                Ordering::Acquire,
346            ) {
347                Ok(_) => return true,
348                Err(observed) => current = observed,
349            }
350        }
351    }
352
353    /// Release a prior queued-byte reservation.
354    fn release(&self, len: usize) {
355        self.queued_bytes.fetch_sub(len, Ordering::AcqRel);
356    }
357}
358
359//--------------------------------------------------------------------------------------------------
360// Functions
361//--------------------------------------------------------------------------------------------------
362
363/// Per-session UDP relay loop: forwards guest datagrams to a host socket, stamps the replies
364/// back into frames the guest accepts, and exits on idle timeout or channel close.
365///
366/// Binds an ephemeral host UDP socket in the address family of `host_dst` and `connect()`s it
367/// to that peer. The `connect` restricts the socket to that peer's datagrams, which both sets
368/// the default send target and filters spoofed inbound traffic. Responses are wrapped in a
369/// synthesised ethernet frame (src IP = `guest_dst`, dst = `guest_src`) and pushed into
370/// `rx_ring`.
371///
372/// # Arguments
373///
374/// * `outbound_rx` - Receives UDP payloads from the poll-loop side. Channel close signals
375///   session drop.
376/// * `guest_src` - Guest source address; stamped as the destination on reply frames.
377/// * `guest_dst` - Destination the guest wrote on the datagram. Stamped as the source IP on
378///   reply frames so the guest sees replies from the same address it dialed.
379/// * `host_dst` - Address the host socket connects to. Equal to `guest_dst` for external
380///   destinations; rewritten to loopback by [`crate::stack::resolve_host_dst`] when the guest
381///   addressed the gateway.
382/// * `shared` - Shared state; reply frames go into `rx_ring` and wake the poll thread.
383/// * `gateway_mac` - Source MAC on reply frames (guest sees replies from the gateway's MAC).
384/// * `guest_mac` - Destination MAC on reply frames.
385/// * `mtu` - Guest IP-level MTU used to fragment large replies before injection.
386///
387/// # Errors
388///
389/// Returns [`std::io::Error`] when the initial `bind` or `connect` on
390/// the host UDP socket fails, or when the host-side `recv` fails after
391/// the socket was established.
392#[allow(clippy::too_many_arguments)]
393async fn udp_relay_task(
394    mut outbound_rx: mpsc::Receiver<OutboundDatagram>,
395    queued_bytes: Arc<AtomicUsize>,
396    guest_src: SocketAddr,
397    guest_dst: SocketAddr,
398    host_dst: SocketAddr,
399    shared: Arc<SharedState>,
400    gateway_mac: EthernetAddress,
401    guest_mac: EthernetAddress,
402    mtu: usize,
403) -> std::io::Result<()> {
404    let socket = open_udp_socket(host_dst)?;
405    // Connect to the destination to restrict accepted source addresses,
406    // preventing host-network entities from injecting spoofed datagrams.
407    socket.connect(host_dst).await?;
408
409    let mut recv_buf = vec![0u8; RECV_BUF_SIZE];
410    let mut pmtu_contexts = VecDeque::new();
411    let timeout = SESSION_TIMEOUT;
412
413    loop {
414        tokio::select! {
415            // Outbound: guest → server.
416            data = outbound_rx.recv() => {
417                match data {
418                    Some(datagram) => {
419                        queued_bytes.fetch_sub(datagram.queued_len(), Ordering::AcqRel);
420                        match socket.send(&datagram.payload).await {
421                            Ok(_) => {
422                                remember_pmtu_context(&mut pmtu_contexts, datagram.original_ip_packet);
423                            }
424                            Err(e) if is_message_size_error(&e) => {
425                                inject_packet_too_big(
426                                    &shared,
427                                    datagram.original_ip_packet.as_ref(),
428                                    socket_path_mtu(&socket, host_dst).ok(),
429                                    gateway_mac,
430                                    guest_mac,
431                                );
432                            }
433                            Err(e) => {
434                                tracing::debug!(error = %e, "UDP relay send failed");
435                            }
436                        }
437                    }
438                    // Channel closed — session dropped by poll loop.
439                    None => break,
440                }
441            }
442
443            // Inbound/error readiness: server → guest data or host PMTU feedback.
444            ready = socket.ready(Interest::READABLE | Interest::ERROR) => {
445                let ready = ready?;
446
447                #[cfg(target_os = "linux")]
448                if ready.is_error() {
449                    match drain_pmtu_errors(&socket) {
450                        Ok(updates) => {
451                            for mtu in updates {
452                                if let Some(original_ip_packet) =
453                                    take_pmtu_context(&mut pmtu_contexts, mtu)
454                                {
455                                    inject_packet_too_big(
456                                        &shared,
457                                        original_ip_packet.as_ref(),
458                                        Some(mtu),
459                                        gateway_mac,
460                                        guest_mac,
461                                    );
462                                }
463                            }
464                        }
465                        Err(e) => tracing::debug!(error = %e, "UDP relay error queue drain failed"),
466                    }
467                }
468
469                if ready.is_readable() {
470                    match socket.try_recv(&mut recv_buf) {
471                        Ok(n) => {
472                            if let Some(frames) = construct_udp_response_frames(
473                                guest_dst,
474                                guest_src,
475                                &recv_buf[..n],
476                                gateway_mac,
477                                guest_mac,
478                                mtu,
479                            ) {
480                                for frame in frames {
481                                    if !shared.push_rx_frame_and_wake(frame) {
482                                        tracing::debug!("UDP relay response dropped because rx_ring is full");
483                                        break;
484                                    }
485                                }
486                            }
487                        }
488                        Err(e) if e.kind() == io::ErrorKind::WouldBlock => {}
489                        Err(e) if is_message_size_error(&e) => {
490                            if let Some(original_ip_packet) =
491                                take_pmtu_context_without_mtu(&mut pmtu_contexts)
492                            {
493                                inject_packet_too_big(
494                                    &shared,
495                                    original_ip_packet.as_ref(),
496                                    socket_path_mtu(&socket, host_dst).ok(),
497                                    gateway_mac,
498                                    guest_mac,
499                                );
500                            }
501                        }
502                        Err(e) => {
503                            tracing::debug!(error = %e, "UDP relay recv failed");
504                            break;
505                        }
506                    }
507                }
508            }
509
510            // Idle timeout.
511            () = tokio::time::sleep(timeout) => {
512                break;
513            }
514        }
515    }
516
517    Ok(())
518}
519
520/// Construct an ethernet frame containing a UDP response for the guest.
521///
522/// Builds Ethernet + IPv4/IPv6 + UDP headers using smoltcp's wire module.
523pub(crate) fn construct_udp_response(
524    src: SocketAddr,
525    dst: SocketAddr,
526    payload: &[u8],
527    gateway_mac: EthernetAddress,
528    guest_mac: EthernetAddress,
529) -> Option<Vec<u8>> {
530    match (src.ip(), dst.ip()) {
531        (IpAddr::V4(src_ip), IpAddr::V4(dst_ip)) => Some(construct_udp_response_v4(
532            src_ip,
533            src.port(),
534            dst_ip,
535            dst.port(),
536            payload,
537            gateway_mac,
538            guest_mac,
539        )?),
540        (IpAddr::V6(src_ip), IpAddr::V6(dst_ip)) => Some(construct_udp_response_v6(
541            src_ip,
542            src.port(),
543            dst_ip,
544            dst.port(),
545            payload,
546            gateway_mac,
547            guest_mac,
548        )?),
549        _ => None, // Mismatched address families — shouldn't happen.
550    }
551}
552
553/// Construct one or more ethernet frames containing a UDP response for the guest.
554fn construct_udp_response_frames(
555    src: SocketAddr,
556    dst: SocketAddr,
557    payload: &[u8],
558    gateway_mac: EthernetAddress,
559    guest_mac: EthernetAddress,
560    mtu: usize,
561) -> Option<Vec<Vec<u8>>> {
562    match (src.ip(), dst.ip()) {
563        (IpAddr::V4(src_ip), IpAddr::V4(dst_ip)) => construct_udp_response_v4_frames(
564            src_ip,
565            src.port(),
566            dst_ip,
567            dst.port(),
568            payload,
569            gateway_mac,
570            guest_mac,
571            mtu,
572        ),
573        (IpAddr::V6(src_ip), IpAddr::V6(dst_ip)) => construct_udp_response_v6_frames(
574            src_ip,
575            src.port(),
576            dst_ip,
577            dst.port(),
578            payload,
579            gateway_mac,
580            guest_mac,
581            mtu,
582        ),
583        _ => None,
584    }
585}
586
587/// Construct an Ethernet + IPv4 + UDP frame.
588fn construct_udp_response_v4(
589    src_ip: Ipv4Addr,
590    src_port: u16,
591    dst_ip: Ipv4Addr,
592    dst_port: u16,
593    payload: &[u8],
594    gateway_mac: EthernetAddress,
595    guest_mac: EthernetAddress,
596) -> Option<Vec<u8>> {
597    if payload.len() > MAX_IPV4_UDP_PAYLOAD_LEN {
598        return None;
599    }
600
601    let udp_len = UDP_HDR_LEN + payload.len();
602    let ip_total_len = IPV4_HDR_LEN + udp_len;
603    let frame_len = ETH_HDR_LEN + ip_total_len;
604    let mut buf = vec![0u8; frame_len];
605
606    // Ethernet header.
607    let eth_repr = EthernetRepr {
608        src_addr: gateway_mac,
609        dst_addr: guest_mac,
610        ethertype: EthernetProtocol::Ipv4,
611    };
612    let mut eth_frame = EthernetFrame::new_unchecked(&mut buf);
613    eth_repr.emit(&mut eth_frame);
614
615    // IPv4 header.
616    let ip_buf = &mut buf[ETH_HDR_LEN..];
617    let mut ip_pkt = Ipv4Packet::new_unchecked(ip_buf);
618    ip_pkt.set_version(4);
619    ip_pkt.set_header_len(20);
620    ip_pkt.set_total_len(ip_total_len as u16);
621    ip_pkt.clear_flags();
622    ip_pkt.set_dont_frag(true);
623    ip_pkt.set_hop_limit(64);
624    ip_pkt.set_next_header(IpProtocol::Udp);
625    ip_pkt.set_src_addr(src_ip);
626    ip_pkt.set_dst_addr(dst_ip);
627    ip_pkt.fill_checksum();
628
629    // UDP header + payload.
630    let udp_buf = &mut buf[ETH_HDR_LEN + IPV4_HDR_LEN..];
631    let mut udp_pkt = UdpPacket::new_unchecked(udp_buf);
632    udp_pkt.set_src_port(src_port);
633    udp_pkt.set_dst_port(dst_port);
634    udp_pkt.set_len(udp_len as u16);
635    udp_pkt.set_checksum(0); // Optional for UDP over IPv4.
636    udp_pkt.payload_mut()[..payload.len()].copy_from_slice(payload);
637
638    Some(buf)
639}
640
641/// Construct IPv4 UDP response frames, fragmenting when the guest MTU requires it.
642#[allow(clippy::too_many_arguments)]
643fn construct_udp_response_v4_frames(
644    src_ip: Ipv4Addr,
645    src_port: u16,
646    dst_ip: Ipv4Addr,
647    dst_port: u16,
648    payload: &[u8],
649    gateway_mac: EthernetAddress,
650    guest_mac: EthernetAddress,
651    mtu: usize,
652) -> Option<Vec<Vec<u8>>> {
653    if payload.len() > MAX_IPV4_UDP_PAYLOAD_LEN {
654        return None;
655    }
656
657    let udp_len = UDP_HDR_LEN.checked_add(payload.len())?;
658    if IPV4_HDR_LEN.checked_add(udp_len)? <= mtu {
659        return construct_udp_response_v4(
660            src_ip,
661            src_port,
662            dst_ip,
663            dst_port,
664            payload,
665            gateway_mac,
666            guest_mac,
667        )
668        .map(|frame| vec![frame]);
669    }
670
671    let max_fragment_payload_len = fragment_payload_limit(mtu, IPV4_HDR_LEN)?;
672    let ident = NEXT_IPV4_RESPONSE_IDENT.fetch_add(1, Ordering::Relaxed);
673    let udp_datagram = build_udp_datagram_v4(src_port, dst_port, payload)?;
674    let mut frames = Vec::new();
675    let mut offset = 0usize;
676
677    while offset < udp_datagram.len() {
678        let remaining = udp_datagram.len() - offset;
679        let take = remaining.min(max_fragment_payload_len);
680        let more_frags = offset + take < udp_datagram.len();
681        frames.push(construct_ipv4_udp_fragment(
682            src_ip,
683            dst_ip,
684            ident,
685            offset,
686            more_frags,
687            &udp_datagram[offset..offset + take],
688            gateway_mac,
689            guest_mac,
690        )?);
691        offset += take;
692    }
693
694    Some(frames)
695}
696
697/// Construct an Ethernet + IPv6 + UDP frame.
698fn construct_udp_response_v6(
699    src_ip: std::net::Ipv6Addr,
700    src_port: u16,
701    dst_ip: std::net::Ipv6Addr,
702    dst_port: u16,
703    payload: &[u8],
704    gateway_mac: EthernetAddress,
705    guest_mac: EthernetAddress,
706) -> Option<Vec<u8>> {
707    if payload.len() > MAX_IPV6_UDP_PAYLOAD_LEN {
708        return None;
709    }
710
711    let udp_len = UDP_HDR_LEN + payload.len();
712    let ipv6_hdr_len = 40;
713    let frame_len = ETH_HDR_LEN + ipv6_hdr_len + udp_len;
714    let mut buf = vec![0u8; frame_len];
715
716    // Ethernet header.
717    let eth_repr = EthernetRepr {
718        src_addr: gateway_mac,
719        dst_addr: guest_mac,
720        ethertype: EthernetProtocol::Ipv6,
721    };
722    let mut eth_frame = EthernetFrame::new_unchecked(&mut buf);
723    eth_repr.emit(&mut eth_frame);
724
725    // IPv6 header.
726    let ip_buf = &mut buf[ETH_HDR_LEN..];
727    let mut ip_pkt = Ipv6Packet::new_unchecked(ip_buf);
728    ip_pkt.set_version(6);
729    ip_pkt.set_payload_len(udp_len as u16);
730    ip_pkt.set_next_header(IpProtocol::Udp);
731    ip_pkt.set_hop_limit(64);
732    ip_pkt.set_src_addr(src_ip);
733    ip_pkt.set_dst_addr(dst_ip);
734
735    // UDP header + payload.
736    let udp_buf = &mut buf[ETH_HDR_LEN + ipv6_hdr_len..];
737    let mut udp_pkt = UdpPacket::new_unchecked(udp_buf);
738    udp_pkt.set_src_port(src_port);
739    udp_pkt.set_dst_port(dst_port);
740    udp_pkt.set_len(udp_len as u16);
741    // Copy payload BEFORE computing checksum — fill_checksum reads the
742    // payload bytes, so they must be in place first.
743    udp_pkt.payload_mut()[..payload.len()].copy_from_slice(payload);
744    // IPv6 UDP checksum is mandatory per RFC 8200 section 8.1.
745    // A zero checksum causes the receiver to discard the packet.
746    udp_pkt.fill_checksum(
747        &smoltcp::wire::IpAddress::from(src_ip),
748        &smoltcp::wire::IpAddress::from(dst_ip),
749    );
750
751    Some(buf)
752}
753
754/// Construct IPv6 UDP response frames, fragmenting when the guest MTU requires it.
755#[allow(clippy::too_many_arguments)]
756fn construct_udp_response_v6_frames(
757    src_ip: std::net::Ipv6Addr,
758    src_port: u16,
759    dst_ip: std::net::Ipv6Addr,
760    dst_port: u16,
761    payload: &[u8],
762    gateway_mac: EthernetAddress,
763    guest_mac: EthernetAddress,
764    mtu: usize,
765) -> Option<Vec<Vec<u8>>> {
766    if payload.len() > MAX_IPV6_UDP_PAYLOAD_LEN {
767        return None;
768    }
769
770    let udp_len = UDP_HDR_LEN.checked_add(payload.len())?;
771    if IPV6_HDR_LEN.checked_add(udp_len)? <= mtu {
772        return construct_udp_response_v6(
773            src_ip,
774            src_port,
775            dst_ip,
776            dst_port,
777            payload,
778            gateway_mac,
779            guest_mac,
780        )
781        .map(|frame| vec![frame]);
782    }
783
784    let max_fragment_payload_len =
785        fragment_payload_limit(mtu, IPV6_HDR_LEN + IPV6_FRAGMENT_HDR_LEN)?;
786    let ident = NEXT_IPV6_RESPONSE_IDENT.fetch_add(1, Ordering::Relaxed);
787    let udp_datagram = build_udp_datagram_v6(src_ip, src_port, dst_ip, dst_port, payload)?;
788    let mut frames = Vec::new();
789    let mut offset = 0usize;
790
791    while offset < udp_datagram.len() {
792        let remaining = udp_datagram.len() - offset;
793        let take = remaining.min(max_fragment_payload_len);
794        let more_frags = offset + take < udp_datagram.len();
795        frames.push(construct_ipv6_udp_fragment(
796            src_ip,
797            dst_ip,
798            ident,
799            offset,
800            more_frags,
801            &udp_datagram[offset..offset + take],
802            gateway_mac,
803            guest_mac,
804        )?);
805        offset += take;
806    }
807
808    Some(frames)
809}
810
811/// Build a UDP datagram buffer for IPv4.
812fn build_udp_datagram_v4(src_port: u16, dst_port: u16, payload: &[u8]) -> Option<Vec<u8>> {
813    let udp_len = UDP_HDR_LEN.checked_add(payload.len())?;
814    if udp_len > u16::MAX as usize {
815        return None;
816    }
817
818    let mut buf = vec![0u8; udp_len];
819    let mut udp = UdpPacket::new_unchecked(&mut buf);
820    udp.set_src_port(src_port);
821    udp.set_dst_port(dst_port);
822    udp.set_len(udp_len as u16);
823    udp.set_checksum(0);
824    udp.payload_mut()[..payload.len()].copy_from_slice(payload);
825    Some(buf)
826}
827
828/// Build a UDP datagram buffer for IPv6, including its mandatory checksum.
829fn build_udp_datagram_v6(
830    src_ip: std::net::Ipv6Addr,
831    src_port: u16,
832    dst_ip: std::net::Ipv6Addr,
833    dst_port: u16,
834    payload: &[u8],
835) -> Option<Vec<u8>> {
836    let udp_len = UDP_HDR_LEN.checked_add(payload.len())?;
837    if udp_len > u16::MAX as usize {
838        return None;
839    }
840
841    let mut buf = vec![0u8; udp_len];
842    let mut udp = UdpPacket::new_unchecked(&mut buf);
843    udp.set_src_port(src_port);
844    udp.set_dst_port(dst_port);
845    udp.set_len(udp_len as u16);
846    udp.payload_mut()[..payload.len()].copy_from_slice(payload);
847    udp.fill_checksum(
848        &smoltcp::wire::IpAddress::from(src_ip),
849        &smoltcp::wire::IpAddress::from(dst_ip),
850    );
851    Some(buf)
852}
853
854/// Return the largest non-final fragment payload that fits in an IP MTU.
855fn fragment_payload_limit(mtu: usize, header_len: usize) -> Option<usize> {
856    let available = mtu.checked_sub(header_len)?;
857    let limit = available - (available % 8);
858    (limit > 0).then_some(limit)
859}
860
861/// Construct one Ethernet + IPv4 fragment carrying a UDP datagram slice.
862#[allow(clippy::too_many_arguments)]
863fn construct_ipv4_udp_fragment(
864    src_ip: Ipv4Addr,
865    dst_ip: Ipv4Addr,
866    ident: u16,
867    fragment_offset: usize,
868    more_frags: bool,
869    fragment_payload: &[u8],
870    gateway_mac: EthernetAddress,
871    guest_mac: EthernetAddress,
872) -> Option<Vec<u8>> {
873    let ip_total_len = IPV4_HDR_LEN.checked_add(fragment_payload.len())?;
874    if ip_total_len > u16::MAX as usize || fragment_offset > u16::MAX as usize {
875        return None;
876    }
877
878    let mut buf = vec![0u8; ETH_HDR_LEN + ip_total_len];
879    let mut eth_frame = EthernetFrame::new_unchecked(&mut buf);
880    EthernetRepr {
881        src_addr: gateway_mac,
882        dst_addr: guest_mac,
883        ethertype: EthernetProtocol::Ipv4,
884    }
885    .emit(&mut eth_frame);
886
887    let mut ip = Ipv4Packet::new_unchecked(&mut buf[ETH_HDR_LEN..]);
888    ip.set_version(4);
889    ip.set_header_len(IPV4_HDR_LEN as u8);
890    ip.set_total_len(ip_total_len as u16);
891    ip.set_ident(ident);
892    ip.clear_flags();
893    ip.set_more_frags(more_frags);
894    ip.set_frag_offset(fragment_offset as u16);
895    ip.set_hop_limit(64);
896    ip.set_next_header(IpProtocol::Udp);
897    ip.set_src_addr(src_ip);
898    ip.set_dst_addr(dst_ip);
899    ip.payload_mut().copy_from_slice(fragment_payload);
900    ip.fill_checksum();
901
902    Some(buf)
903}
904
905/// Construct one Ethernet + IPv6 Fragment packet carrying a UDP datagram slice.
906#[allow(clippy::too_many_arguments)]
907fn construct_ipv6_udp_fragment(
908    src_ip: std::net::Ipv6Addr,
909    dst_ip: std::net::Ipv6Addr,
910    ident: u32,
911    fragment_offset: usize,
912    more_frags: bool,
913    fragment_payload: &[u8],
914    gateway_mac: EthernetAddress,
915    guest_mac: EthernetAddress,
916) -> Option<Vec<u8>> {
917    let ipv6_payload_len = IPV6_FRAGMENT_HDR_LEN.checked_add(fragment_payload.len())?;
918    if ipv6_payload_len > u16::MAX as usize || fragment_offset > u16::MAX as usize {
919        return None;
920    }
921
922    let mut buf = vec![0u8; ETH_HDR_LEN + IPV6_HDR_LEN + ipv6_payload_len];
923    let mut eth_frame = EthernetFrame::new_unchecked(&mut buf);
924    EthernetRepr {
925        src_addr: gateway_mac,
926        dst_addr: guest_mac,
927        ethertype: EthernetProtocol::Ipv6,
928    }
929    .emit(&mut eth_frame);
930
931    {
932        let mut ip = Ipv6Packet::new_unchecked(&mut buf[ETH_HDR_LEN..]);
933        ip.set_version(6);
934        ip.set_payload_len(ipv6_payload_len as u16);
935        ip.set_next_header(IpProtocol::Ipv6Frag);
936        ip.set_hop_limit(64);
937        ip.set_src_addr(src_ip);
938        ip.set_dst_addr(dst_ip);
939    }
940
941    let fragment_start = ETH_HDR_LEN + IPV6_HDR_LEN;
942    let fragment = &mut buf[fragment_start..][..IPV6_FRAGMENT_HDR_LEN];
943    fragment[0] = IpProtocol::Udp.into();
944    fragment[1] = 0;
945    let offset_units = u16::try_from(fragment_offset / 8).ok()?;
946    let raw = (offset_units << 3) | u16::from(more_frags);
947    fragment[2..4].copy_from_slice(&raw.to_be_bytes());
948    fragment[4..8].copy_from_slice(&ident.to_be_bytes());
949    buf[fragment_start + IPV6_FRAGMENT_HDR_LEN..].copy_from_slice(fragment_payload);
950
951    Some(buf)
952}
953
954/// Extract the UDP payload from a raw ethernet frame.
955pub(crate) fn extract_udp_payload(frame: &[u8]) -> Option<&[u8]> {
956    let eth = EthernetFrame::new_checked(frame).ok()?;
957    match eth.ethertype() {
958        EthernetProtocol::Ipv4 => {
959            let ipv4 = Ipv4Packet::new_checked(eth.payload()).ok()?;
960            let udp = UdpPacket::new_checked(ipv4.payload()).ok()?;
961            Some(udp.payload())
962        }
963        EthernetProtocol::Ipv6 => {
964            let ipv6 = Ipv6Packet::new_checked(eth.payload()).ok()?;
965            let udp = UdpPacket::new_checked(ipv6.payload()).ok()?;
966            Some(udp.payload())
967        }
968        _ => None,
969    }
970}
971
972/// Extract the outbound UDP payload and original IP packet from a raw ethernet frame.
973fn extract_udp_datagram(frame: &[u8]) -> Option<OutboundDatagram> {
974    let original = ethernet_ip_payload(frame)?;
975    let payload_range = udp_payload_range(original)?;
976    let original_ip_packet = Bytes::copy_from_slice(original);
977    let payload = original_ip_packet.slice(payload_range);
978
979    Some(OutboundDatagram {
980        payload,
981        original_ip_packet,
982    })
983}
984
985/// Return the UDP payload range inside one Ethernet-stripped IP packet.
986fn udp_payload_range(ip_packet: &[u8]) -> Option<Range<usize>> {
987    match ip_packet.first()? >> 4 {
988        4 => {
989            let ipv4 = Ipv4Packet::new_checked(ip_packet).ok()?;
990            if ipv4.next_header() != IpProtocol::Udp {
991                return None;
992            }
993            let udp_offset = ipv4.header_len() as usize;
994            let udp = UdpPacket::new_checked(&ip_packet[udp_offset..]).ok()?;
995            let payload_start = udp_offset + UDP_HDR_LEN;
996            let payload_end = udp_offset + usize::from(udp.len());
997            (payload_start <= payload_end && payload_end <= ip_packet.len())
998                .then_some(payload_start..payload_end)
999        }
1000        6 => {
1001            let ipv6 = Ipv6Packet::new_checked(ip_packet).ok()?;
1002            if ipv6.next_header() != IpProtocol::Udp {
1003                return None;
1004            }
1005            let udp_offset = 40;
1006            let udp = UdpPacket::new_checked(&ip_packet[udp_offset..]).ok()?;
1007            let payload_start = udp_offset + UDP_HDR_LEN;
1008            let payload_end = udp_offset + usize::from(udp.len());
1009            (payload_start <= payload_end && payload_end <= ip_packet.len())
1010                .then_some(payload_start..payload_end)
1011        }
1012        _ => None,
1013    }
1014}
1015
1016/// Open a host UDP socket with PMTU feedback options enabled when available.
1017fn open_udp_socket(host_dst: SocketAddr) -> io::Result<UdpSocket> {
1018    let domain = match host_dst {
1019        SocketAddr::V4(_) => Domain::IPV4,
1020        SocketAddr::V6(_) => Domain::IPV6,
1021    };
1022    let socket = Socket::new(domain, Type::DGRAM, Some(SocketProtocol::UDP))?;
1023    socket.set_nonblocking(true)?;
1024
1025    let bind_addr: SocketAddr = match host_dst {
1026        SocketAddr::V4(_) => (Ipv4Addr::UNSPECIFIED, 0u16).into(),
1027        SocketAddr::V6(_) => (std::net::Ipv6Addr::UNSPECIFIED, 0u16).into(),
1028    };
1029    socket.bind(&bind_addr.into())?;
1030
1031    #[cfg(target_os = "linux")]
1032    enable_linux_pmtu_errors(&socket, host_dst)?;
1033
1034    UdpSocket::from_std(socket.into())
1035}
1036
1037/// Return true when an I/O error represents a datagram exceeding the path MTU.
1038fn is_message_size_error(error: &io::Error) -> bool {
1039    error.raw_os_error() == Some(libc::EMSGSIZE)
1040}
1041
1042/// Inject an ICMP too-big error toward the guest.
1043fn inject_packet_too_big(
1044    shared: &SharedState,
1045    original_ip_packet: &[u8],
1046    next_hop_mtu: Option<u32>,
1047    gateway_mac: EthernetAddress,
1048    guest_mac: EthernetAddress,
1049) {
1050    let Some(next_hop_mtu) =
1051        next_hop_mtu.filter(|mtu| valid_packet_too_big_mtu(original_ip_packet, *mtu))
1052    else {
1053        tracing::debug!("UDP relay skipped ICMP too-big because no valid MTU is available");
1054        return;
1055    };
1056
1057    let Some(frame) =
1058        construct_packet_too_big(original_ip_packet, next_hop_mtu, gateway_mac, guest_mac)
1059    else {
1060        return;
1061    };
1062
1063    if !shared.push_rx_frame_and_wake(frame) {
1064        tracing::debug!("UDP relay ICMP too-big response dropped because rx_ring is full");
1065    }
1066}
1067
1068/// Return whether an MTU value is usable in a guest-facing too-big error.
1069fn valid_packet_too_big_mtu(original_ip_packet: &[u8], mtu: u32) -> bool {
1070    if mtu == 0 {
1071        return false;
1072    }
1073
1074    match original_ip_packet.first().map(|byte| byte >> 4) {
1075        // IPv6 Packet Too Big must carry an actionable MTU. IPv6 links have a
1076        // minimum MTU of 1280, so lower values are not useful PMTU feedback.
1077        Some(6) => mtu >= 1280,
1078        Some(4) => true,
1079        _ => false,
1080    }
1081}
1082
1083/// Remember one sent packet for later async PMTU attribution.
1084fn remember_pmtu_context(contexts: &mut VecDeque<Bytes>, original_ip_packet: Bytes) {
1085    if contexts.len() >= MAX_PMTU_CONTEXTS {
1086        contexts.pop_front();
1087    }
1088    contexts.push_back(original_ip_packet);
1089}
1090
1091/// Take the most likely packet that triggered a PMTU update.
1092#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
1093fn take_pmtu_context(contexts: &mut VecDeque<Bytes>, mtu: u32) -> Option<Bytes> {
1094    if mtu > 0
1095        && let Some(position) = contexts.iter().position(|packet| {
1096            original_ip_packet_len(packet.as_ref()).is_some_and(|len| len > mtu as usize)
1097        })
1098    {
1099        return contexts.remove(position);
1100    }
1101
1102    contexts.pop_front()
1103}
1104
1105/// Take the oldest PMTU context when the host did not provide an MTU.
1106fn take_pmtu_context_without_mtu(contexts: &mut VecDeque<Bytes>) -> Option<Bytes> {
1107    contexts.pop_front()
1108}
1109
1110/// Return the wire length declared by an Ethernet-stripped IP packet.
1111#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
1112fn original_ip_packet_len(packet: &[u8]) -> Option<usize> {
1113    match packet.first()? >> 4 {
1114        4 => {
1115            let ipv4 = Ipv4Packet::new_checked(packet).ok()?;
1116            Some(usize::from(ipv4.total_len()).min(packet.len()))
1117        }
1118        6 => {
1119            let ipv6 = Ipv6Packet::new_checked(packet).ok()?;
1120            Some((40 + usize::from(ipv6.payload_len())).min(packet.len()))
1121        }
1122        _ => None,
1123    }
1124}
1125
1126/// Return the connected socket's current path MTU when the host exposes it.
1127#[cfg(target_os = "linux")]
1128fn socket_path_mtu(socket: &UdpSocket, host_dst: SocketAddr) -> io::Result<u32> {
1129    let fd = socket.as_raw_fd();
1130    let (level, optname) = match host_dst {
1131        SocketAddr::V4(_) => (libc::IPPROTO_IP, libc::IP_MTU),
1132        SocketAddr::V6(_) => (libc::IPPROTO_IPV6, libc::IPV6_MTU),
1133    };
1134
1135    let mut mtu: libc::c_int = 0;
1136    let mut len = std::mem::size_of_val(&mtu) as libc::socklen_t;
1137    // SAFETY: `mtu` and `len` point to valid writable storage for getsockopt.
1138    let rc = unsafe {
1139        libc::getsockopt(
1140            fd,
1141            level,
1142            optname,
1143            (&mut mtu as *mut libc::c_int).cast(),
1144            &mut len,
1145        )
1146    };
1147    if rc == -1 {
1148        return Err(io::Error::last_os_error());
1149    }
1150
1151    if mtu <= 0 {
1152        return Err(io::Error::new(
1153            io::ErrorKind::InvalidData,
1154            "connected socket reported a non-positive path MTU",
1155        ));
1156    }
1157
1158    Ok(mtu as u32)
1159}
1160
1161/// Return no MTU on platforms without connected-socket path-MTU queries.
1162#[cfg(not(target_os = "linux"))]
1163fn socket_path_mtu(_socket: &UdpSocket, _host_dst: SocketAddr) -> io::Result<u32> {
1164    Err(io::Error::new(
1165        io::ErrorKind::Unsupported,
1166        "path MTU query is not supported on this platform",
1167    ))
1168}
1169
1170/// Enable Linux per-socket extended errors for PMTU feedback.
1171#[cfg(target_os = "linux")]
1172fn enable_linux_pmtu_errors(socket: &Socket, host_dst: SocketAddr) -> io::Result<()> {
1173    let fd = socket.as_raw_fd();
1174    match host_dst {
1175        SocketAddr::V4(_) => set_socket_bool(fd, libc::IPPROTO_IP, libc::IP_RECVERR, true),
1176        SocketAddr::V6(_) => set_socket_bool(fd, libc::IPPROTO_IPV6, libc::IPV6_RECVERR, true),
1177    }
1178}
1179
1180/// Set a boolean socket option using libc constants not exposed by socket2.
1181#[cfg(target_os = "linux")]
1182fn set_socket_bool(
1183    fd: libc::c_int,
1184    level: libc::c_int,
1185    optname: libc::c_int,
1186    value: bool,
1187) -> io::Result<()> {
1188    let value: libc::c_int = i32::from(value);
1189    // SAFETY: `value` points to a valid c_int option payload.
1190    let rc = unsafe {
1191        libc::setsockopt(
1192            fd,
1193            level,
1194            optname,
1195            (&value as *const libc::c_int).cast(),
1196            std::mem::size_of_val(&value) as libc::socklen_t,
1197        )
1198    };
1199    if rc == -1 {
1200        return Err(io::Error::last_os_error());
1201    }
1202    Ok(())
1203}
1204
1205/// Drain Linux's UDP error queue and return PMTU updates in queue order.
1206#[cfg(target_os = "linux")]
1207fn drain_pmtu_errors(socket: &UdpSocket) -> io::Result<Vec<u32>> {
1208    socket.try_io(Interest::ERROR, || {
1209        drain_pmtu_errors_from_fd(socket.as_raw_fd())
1210    })
1211}
1212
1213/// Drain all currently queued extended errors from one socket fd.
1214#[cfg(target_os = "linux")]
1215fn drain_pmtu_errors_from_fd(fd: libc::c_int) -> io::Result<Vec<u32>> {
1216    let mut mtus = Vec::new();
1217    let mut drained_any = false;
1218
1219    loop {
1220        match recv_one_pmtu_error(fd) {
1221            Ok(mtu) => {
1222                drained_any = true;
1223                if let Some(mtu) = mtu {
1224                    mtus.push(mtu);
1225                }
1226            }
1227            Err(e) if e.kind() == io::ErrorKind::WouldBlock && drained_any => return Ok(mtus),
1228            Err(e) => return Err(e),
1229        }
1230    }
1231}
1232
1233/// Receive and parse one Linux MSG_ERRQUEUE entry.
1234#[cfg(target_os = "linux")]
1235fn recv_one_pmtu_error(fd: libc::c_int) -> io::Result<Option<u32>> {
1236    let mut data = [0u8; 1];
1237    let mut iov = libc::iovec {
1238        iov_base: data.as_mut_ptr().cast(),
1239        iov_len: data.len(),
1240    };
1241    let mut control = [0u8; 512];
1242    // SAFETY: zeroed msghdr is filled with valid pointers immediately below.
1243    let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
1244    msg.msg_iov = &mut iov;
1245    msg.msg_iovlen = 1;
1246    msg.msg_control = control.as_mut_ptr().cast();
1247    msg.msg_controllen = control.len();
1248
1249    // SAFETY: `msg` contains valid iovec/control buffers and `fd` is a UDP socket.
1250    let rc = unsafe { libc::recvmsg(fd, &mut msg, libc::MSG_ERRQUEUE | libc::MSG_DONTWAIT) };
1251    if rc == -1 {
1252        return Err(io::Error::last_os_error());
1253    }
1254
1255    let control_len = msg.msg_controllen.min(control.len());
1256    Ok(parse_pmtu_from_control_messages(&control[..control_len]))
1257}
1258
1259/// Parse a PMTU value out of one recvmsg control-message list.
1260#[cfg(target_os = "linux")]
1261fn parse_pmtu_from_control_messages(control: &[u8]) -> Option<u32> {
1262    let header_len = std::mem::size_of::<libc::cmsghdr>();
1263    let data_offset = cmsg_align(header_len)?;
1264    let mut offset = 0usize;
1265
1266    while offset.checked_add(header_len)? <= control.len() {
1267        let header = control.get(offset..offset + header_len)?;
1268        let cmsg_len = read_native_usize(header)?;
1269        if cmsg_len < data_offset {
1270            return None;
1271        }
1272
1273        let message_end = offset.checked_add(cmsg_len)?;
1274        if message_end > control.len() {
1275            return None;
1276        }
1277
1278        let cmsg_level = read_native_c_int(header.get(std::mem::size_of::<usize>()..)?)?;
1279        let cmsg_type = read_native_c_int(
1280            header.get(std::mem::size_of::<usize>() + std::mem::size_of::<libc::c_int>()..)?,
1281        )?;
1282        let is_extended_error = (cmsg_level == libc::IPPROTO_IP && cmsg_type == libc::IP_RECVERR)
1283            || (cmsg_level == libc::IPPROTO_IPV6 && cmsg_type == libc::IPV6_RECVERR);
1284
1285        if is_extended_error {
1286            let data_start = offset.checked_add(data_offset)?;
1287            if data_start > message_end {
1288                return None;
1289            }
1290
1291            if let Some(mtu) = parse_sock_extended_err_mtu(control.get(data_start..message_end)?) {
1292                return Some(mtu);
1293            }
1294        }
1295
1296        offset = offset.checked_add(cmsg_align(cmsg_len)?)?;
1297    }
1298
1299    None
1300}
1301
1302/// Parse Linux's `sock_extended_err` control payload without pointer casts.
1303#[cfg(target_os = "linux")]
1304fn parse_sock_extended_err_mtu(data: &[u8]) -> Option<u32> {
1305    let error_len = std::mem::size_of::<libc::sock_extended_err>();
1306    if data.len() < error_len {
1307        return None;
1308    }
1309
1310    let ee_errno = read_native_u32(data)?;
1311    let ee_origin = *data.get(4)?;
1312    let ee_info = read_native_u32(data.get(8..)?)?;
1313
1314    if ee_errno == libc::EMSGSIZE as u32
1315        && matches!(
1316            ee_origin,
1317            libc::SO_EE_ORIGIN_ICMP | libc::SO_EE_ORIGIN_ICMP6 | libc::SO_EE_ORIGIN_LOCAL
1318        )
1319    {
1320        return Some(ee_info);
1321    }
1322
1323    None
1324}
1325
1326/// Read a native-endian C `size_t` from the start of a byte slice.
1327#[cfg(target_os = "linux")]
1328fn read_native_usize(bytes: &[u8]) -> Option<usize> {
1329    match std::mem::size_of::<usize>() {
1330        4 => Some(u32::from_ne_bytes(bytes.get(..4)?.try_into().ok()?) as usize),
1331        8 => Some(u64::from_ne_bytes(bytes.get(..8)?.try_into().ok()?) as usize),
1332        _ => None,
1333    }
1334}
1335
1336/// Read a native-endian C `int` from the start of a byte slice.
1337#[cfg(target_os = "linux")]
1338fn read_native_c_int(bytes: &[u8]) -> Option<libc::c_int> {
1339    match std::mem::size_of::<libc::c_int>() {
1340        4 => Some(i32::from_ne_bytes(bytes.get(..4)?.try_into().ok()?) as libc::c_int),
1341        _ => None,
1342    }
1343}
1344
1345/// Read a native-endian `u32` from the start of a byte slice.
1346#[cfg(target_os = "linux")]
1347fn read_native_u32(bytes: &[u8]) -> Option<u32> {
1348    Some(u32::from_ne_bytes(bytes.get(..4)?.try_into().ok()?))
1349}
1350
1351/// Linux control messages are aligned to pointer width.
1352#[cfg(target_os = "linux")]
1353fn cmsg_align(len: usize) -> Option<usize> {
1354    let align = std::mem::size_of::<usize>();
1355    len.checked_add(align - 1).map(|value| value & !(align - 1))
1356}
1357
1358//--------------------------------------------------------------------------------------------------
1359// Tests
1360//--------------------------------------------------------------------------------------------------
1361
1362#[cfg(test)]
1363mod tests {
1364    use super::*;
1365
1366    #[test]
1367    fn construct_v4_response_has_correct_structure() {
1368        let payload = b"hello";
1369        let frame = construct_udp_response_v4(
1370            Ipv4Addr::new(8, 8, 8, 8),
1371            53,
1372            Ipv4Addr::new(100, 96, 0, 2),
1373            12345,
1374            payload,
1375            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
1376            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
1377        )
1378        .unwrap();
1379
1380        assert_eq!(frame.len(), ETH_HDR_LEN + IPV4_HDR_LEN + UDP_HDR_LEN + 5);
1381
1382        // Parse back.
1383        let eth = EthernetFrame::new_checked(&frame).unwrap();
1384        assert_eq!(eth.ethertype(), EthernetProtocol::Ipv4);
1385        assert_eq!(
1386            eth.dst_addr(),
1387            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02])
1388        );
1389
1390        let ipv4 = Ipv4Packet::new_checked(eth.payload()).unwrap();
1391        assert_eq!(ipv4.src_addr(), Ipv4Addr::new(8, 8, 8, 8));
1392        assert_eq!(ipv4.dst_addr(), Ipv4Addr::new(100, 96, 0, 2));
1393        assert_eq!(ipv4.next_header(), IpProtocol::Udp);
1394
1395        let udp = UdpPacket::new_checked(ipv4.payload()).unwrap();
1396        assert_eq!(udp.src_port(), 53);
1397        assert_eq!(udp.dst_port(), 12345);
1398        assert_eq!(udp.payload(), b"hello");
1399    }
1400
1401    #[test]
1402    fn construct_v6_response_has_correct_structure() {
1403        let payload = b"hello ipv6";
1404        let src = "2001:db8::1".parse::<std::net::Ipv6Addr>().unwrap();
1405        let dst = "fd42:6d73:62::2".parse::<std::net::Ipv6Addr>().unwrap();
1406        let frame = construct_udp_response_v6(
1407            src,
1408            53,
1409            dst,
1410            12345,
1411            payload,
1412            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
1413            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
1414        )
1415        .unwrap();
1416
1417        let ipv6_hdr_len = 40;
1418        assert_eq!(
1419            frame.len(),
1420            ETH_HDR_LEN + ipv6_hdr_len + UDP_HDR_LEN + payload.len()
1421        );
1422
1423        // Parse back.
1424        let eth = EthernetFrame::new_checked(&frame).unwrap();
1425        assert_eq!(eth.ethertype(), EthernetProtocol::Ipv6);
1426
1427        let ipv6 = Ipv6Packet::new_checked(eth.payload()).unwrap();
1428        assert_eq!(ipv6.next_header(), IpProtocol::Udp);
1429
1430        let udp = UdpPacket::new_checked(ipv6.payload()).unwrap();
1431        assert_eq!(udp.src_port(), 53);
1432        assert_eq!(udp.dst_port(), 12345);
1433        assert_eq!(udp.payload(), b"hello ipv6");
1434        // Verify checksum is non-zero (mandatory for IPv6 UDP per RFC 8200).
1435        assert_ne!(udp.checksum(), 0, "IPv6 UDP checksum must not be zero");
1436        // Verify checksum is correct.
1437        assert!(
1438            udp.verify_checksum(
1439                &smoltcp::wire::IpAddress::from(src),
1440                &smoltcp::wire::IpAddress::from(dst),
1441            ),
1442            "IPv6 UDP checksum must be valid"
1443        );
1444    }
1445
1446    #[test]
1447    fn extract_payload_from_v6_udp_frame() {
1448        let src = "2001:db8::1".parse::<std::net::Ipv6Addr>().unwrap();
1449        let dst = "fd42:6d73:62::2".parse::<std::net::Ipv6Addr>().unwrap();
1450        let frame = construct_udp_response_v6(
1451            src,
1452            80,
1453            dst,
1454            54321,
1455            b"v6 data",
1456            EthernetAddress([0; 6]),
1457            EthernetAddress([0; 6]),
1458        )
1459        .unwrap();
1460        let payload = extract_udp_payload(&frame).unwrap();
1461        assert_eq!(payload, b"v6 data");
1462    }
1463
1464    #[test]
1465    fn extract_payload_from_v4_udp_frame() {
1466        // Build a frame then extract the payload.
1467        let frame = construct_udp_response_v4(
1468            Ipv4Addr::new(1, 2, 3, 4),
1469            80,
1470            Ipv4Addr::new(10, 0, 0, 2),
1471            54321,
1472            b"test data",
1473            EthernetAddress([0; 6]),
1474            EthernetAddress([0; 6]),
1475        )
1476        .unwrap();
1477        let payload = extract_udp_payload(&frame).unwrap();
1478        assert_eq!(payload, b"test data");
1479    }
1480
1481    #[test]
1482    fn construct_v4_response_rejects_payload_over_ipv4_limit() {
1483        let payload = vec![0u8; MAX_IPV4_UDP_PAYLOAD_LEN + 1];
1484        assert!(
1485            construct_udp_response_v4(
1486                Ipv4Addr::new(8, 8, 8, 8),
1487                53,
1488                Ipv4Addr::new(100, 96, 0, 2),
1489                12345,
1490                &payload,
1491                EthernetAddress([0; 6]),
1492                EthernetAddress([0; 6]),
1493            )
1494            .is_none()
1495        );
1496    }
1497
1498    #[test]
1499    fn construct_v4_response_frames_fragment_large_payload_to_mtu() {
1500        let payload = vec![b'x'; 2000];
1501        let frames = construct_udp_response_frames(
1502            SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 53),
1503            SocketAddr::new(IpAddr::V4(Ipv4Addr::new(100, 96, 0, 2)), 12345),
1504            &payload,
1505            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
1506            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
1507            1500,
1508        )
1509        .unwrap();
1510
1511        assert_eq!(frames.len(), 2);
1512        assert!(frames.iter().all(|frame| frame.len() <= ETH_HDR_LEN + 1500));
1513
1514        let first_eth = EthernetFrame::new_checked(frames[0].as_slice()).unwrap();
1515        let first_ip = Ipv4Packet::new_checked(first_eth.payload()).unwrap();
1516        assert!(first_ip.more_frags());
1517        assert_eq!(first_ip.frag_offset(), 0);
1518        assert_eq!(&first_ip.payload()[..2], &53u16.to_be_bytes());
1519        assert_eq!(&first_ip.payload()[2..4], &12345u16.to_be_bytes());
1520        assert_eq!(
1521            u16::from_be_bytes([first_ip.payload()[4], first_ip.payload()[5]]) as usize,
1522            UDP_HDR_LEN + payload.len()
1523        );
1524
1525        let second_eth = EthernetFrame::new_checked(frames[1].as_slice()).unwrap();
1526        let second_ip = Ipv4Packet::new_checked(second_eth.payload()).unwrap();
1527        assert!(!second_ip.more_frags());
1528        assert_eq!(second_ip.frag_offset(), 1480);
1529    }
1530
1531    #[test]
1532    fn construct_v6_response_frames_fragment_large_payload_to_mtu() {
1533        let src = "2001:db8::1".parse::<std::net::Ipv6Addr>().unwrap();
1534        let dst = "fd42:6d73:62::2".parse::<std::net::Ipv6Addr>().unwrap();
1535        let payload = vec![b'x'; 2000];
1536        let frames = construct_udp_response_frames(
1537            SocketAddr::new(IpAddr::V6(src), 53),
1538            SocketAddr::new(IpAddr::V6(dst), 12345),
1539            &payload,
1540            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
1541            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
1542            1500,
1543        )
1544        .unwrap();
1545
1546        assert_eq!(frames.len(), 2);
1547        assert!(frames.iter().all(|frame| frame.len() <= ETH_HDR_LEN + 1500));
1548
1549        let first_eth = EthernetFrame::new_checked(frames[0].as_slice()).unwrap();
1550        let first_ip = Ipv6Packet::new_checked(first_eth.payload()).unwrap();
1551        assert_eq!(first_ip.next_header(), IpProtocol::Ipv6Frag);
1552        let first_fragment = &first_ip.payload()[..IPV6_FRAGMENT_HDR_LEN];
1553        let first_raw = u16::from_be_bytes([first_fragment[2], first_fragment[3]]);
1554        assert_eq!(IpProtocol::from(first_fragment[0]), IpProtocol::Udp);
1555        assert_eq!(first_raw >> 3, 0);
1556        assert_eq!(first_raw & 1, 1);
1557
1558        let second_eth = EthernetFrame::new_checked(frames[1].as_slice()).unwrap();
1559        let second_ip = Ipv6Packet::new_checked(second_eth.payload()).unwrap();
1560        assert_eq!(second_ip.next_header(), IpProtocol::Ipv6Frag);
1561        let second_fragment = &second_ip.payload()[..IPV6_FRAGMENT_HDR_LEN];
1562        let second_raw = u16::from_be_bytes([second_fragment[2], second_fragment[3]]);
1563        assert_eq!(usize::from(second_raw >> 3) * 8, 1448);
1564        assert_eq!(second_raw & 1, 0);
1565        assert_eq!(
1566            &second_ip.payload()[IPV6_FRAGMENT_HDR_LEN..][..2],
1567            &payload[1440..1442]
1568        );
1569    }
1570
1571    #[test]
1572    fn packet_too_big_mtu_validation_rejects_unusable_ipv6_values() {
1573        let frame = construct_udp_response_v6(
1574            "2001:db8::1".parse().unwrap(),
1575            443,
1576            "fd42:6d73:62::2".parse().unwrap(),
1577            12345,
1578            b"payload",
1579            EthernetAddress([0; 6]),
1580            EthernetAddress([0; 6]),
1581        )
1582        .unwrap();
1583        let original = ethernet_ip_payload(&frame).unwrap();
1584
1585        assert!(!valid_packet_too_big_mtu(original, 0));
1586        assert!(!valid_packet_too_big_mtu(original, 1279));
1587        assert!(valid_packet_too_big_mtu(original, 1280));
1588    }
1589
1590    #[test]
1591    fn pmtu_context_prefers_packet_larger_than_reported_mtu() {
1592        let small = Bytes::from(build_ipv4_udp_packet_for_test(100));
1593        let large = Bytes::from(build_ipv4_udp_packet_for_test(1400));
1594        let mut contexts = VecDeque::from([small.clone(), large.clone()]);
1595
1596        let selected = take_pmtu_context(&mut contexts, 1280).unwrap();
1597        assert_eq!(selected, large);
1598        assert_eq!(contexts.pop_front().unwrap(), small);
1599    }
1600
1601    #[cfg(target_os = "linux")]
1602    #[test]
1603    fn parses_linux_extended_error_control_message_without_pointer_walk() {
1604        let mut error = vec![0u8; std::mem::size_of::<libc::sock_extended_err>()];
1605        write_native_u32_for_test(&mut error, libc::EMSGSIZE as u32);
1606        error[4] = libc::SO_EE_ORIGIN_ICMP;
1607        write_native_u32_for_test(&mut error[8..], 1280);
1608
1609        let mut control = Vec::new();
1610        push_control_message_for_test(&mut control, libc::IPPROTO_IP, libc::IP_RECVERR, &error);
1611
1612        assert_eq!(parse_pmtu_from_control_messages(&control), Some(1280));
1613        assert_eq!(parse_pmtu_from_control_messages(&control[..8]), None);
1614    }
1615
1616    fn build_ipv4_udp_packet_for_test(payload_len: usize) -> Vec<u8> {
1617        let payload = vec![0u8; payload_len];
1618        let udp_len = UDP_HDR_LEN + payload.len();
1619        let ip_total_len = IPV4_HDR_LEN + udp_len;
1620        let mut packet = vec![0u8; ip_total_len];
1621
1622        {
1623            let mut ip = Ipv4Packet::new_unchecked(&mut packet);
1624            ip.set_version(4);
1625            ip.set_header_len(IPV4_HDR_LEN as u8);
1626            ip.set_total_len(ip_total_len as u16);
1627            ip.clear_flags();
1628            ip.set_hop_limit(64);
1629            ip.set_next_header(IpProtocol::Udp);
1630            ip.set_src_addr(Ipv4Addr::new(100, 96, 0, 2));
1631            ip.set_dst_addr(Ipv4Addr::new(203, 0, 113, 10));
1632            ip.fill_checksum();
1633        }
1634
1635        let mut udp = UdpPacket::new_unchecked(&mut packet[IPV4_HDR_LEN..]);
1636        udp.set_src_port(12345);
1637        udp.set_dst_port(443);
1638        udp.set_len(udp_len as u16);
1639        udp.payload_mut().copy_from_slice(&payload);
1640
1641        packet
1642    }
1643
1644    #[cfg(target_os = "linux")]
1645    fn push_control_message_for_test(
1646        control: &mut Vec<u8>,
1647        level: libc::c_int,
1648        message_type: libc::c_int,
1649        data: &[u8],
1650    ) {
1651        let header_len = std::mem::size_of::<libc::cmsghdr>();
1652        let data_offset = cmsg_align(header_len).unwrap();
1653        let cmsg_len = data_offset + data.len();
1654        let aligned_len = cmsg_align(cmsg_len).unwrap();
1655        let start = control.len();
1656        control.resize(start + aligned_len, 0);
1657
1658        write_native_usize_for_test(&mut control[start..], cmsg_len);
1659        write_native_c_int_for_test(&mut control[start + std::mem::size_of::<usize>()..], level);
1660        write_native_c_int_for_test(
1661            &mut control
1662                [start + std::mem::size_of::<usize>() + std::mem::size_of::<libc::c_int>()..],
1663            message_type,
1664        );
1665        control[start + data_offset..start + data_offset + data.len()].copy_from_slice(data);
1666    }
1667
1668    #[cfg(target_os = "linux")]
1669    fn write_native_usize_for_test(bytes: &mut [u8], value: usize) {
1670        match std::mem::size_of::<usize>() {
1671            4 => bytes[..4].copy_from_slice(&(value as u32).to_ne_bytes()),
1672            8 => bytes[..8].copy_from_slice(&(value as u64).to_ne_bytes()),
1673            _ => unreachable!("unsupported size_t width"),
1674        }
1675    }
1676
1677    #[cfg(target_os = "linux")]
1678    fn write_native_c_int_for_test(bytes: &mut [u8], value: libc::c_int) {
1679        match std::mem::size_of::<libc::c_int>() {
1680            4 => bytes[..4].copy_from_slice(&(value as i32).to_ne_bytes()),
1681            _ => unreachable!("unsupported c_int width"),
1682        }
1683    }
1684
1685    #[cfg(target_os = "linux")]
1686    fn write_native_u32_for_test(bytes: &mut [u8], value: u32) {
1687        bytes[..4].copy_from_slice(&value.to_ne_bytes());
1688    }
1689}