//! Tests for the `local` feature: real-socket halves through the real //! establisher path (`local_dial` injected as the `DialFn`), TCP/UDP/ //! unix round-trips against real local echo servers, the OQ-TN-13 //! truncation fail-loud probe, and the 1400-byte MTU datagram. //! //! The forward topology (`wire_forward`) carries the full path: the //! consumer opens (`TunnelSession::open`), the producer's establisher //! dials via `local_dial`, the pump handler pumps — real sockets at //! both ends. #![cfg(feature = "local")] mod harness; use std::sync::Arc; use alktunnels::params::{Substrate, TunnelParams}; use alktunnels::producer::ResourceRegistry; use alktunnels::TunnelSession; use harness::wire_forward; /// A TCP echo server on an ephemeral port. async fn tcp_echo_server() -> std::net::SocketAddr { let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)) .await .expect("bind echo"); let addr = listener.local_addr().expect("addr"); tokio::spawn(async move { loop { let Ok((sock, _)) = listener.accept().await else { return; }; tokio::spawn(async move { let (mut r, mut w) = tokio::io::split(sock); let _ = tokio::io::copy(&mut r, &mut w).await; }); } }); addr } /// A UDP echo server on an ephemeral port (one task, echo loop). async fn udp_echo_server() -> std::net::SocketAddr { let sock = tokio::net::UdpSocket::bind(("127.0.0.1", 0)) .await .expect("bind udp echo"); let addr = sock.local_addr().expect("addr"); tokio::spawn(async move { let mut buf = vec![0u8; 65535]; loop { let Ok((n, peer)) = sock.recv_from(&mut buf).await else { return; }; if sock.send_to(&buf[..n], peer).await.is_err() { return; } } }); addr } #[tokio::test] async fn tcp_dial_round_trips_through_the_real_establisher_path() { let addr = tcp_echo_server().await; let registry = ResourceRegistry::new(); registry .register("echo", Substrate::Tcp, &addr.to_string()) .await; let topo = wire_forward(registry, alktunnels::local::local_dial()).await; let mut session = TunnelSession::open(&topo.consumer, params("echo", Substrate::Tcp)) .await .expect("tcp open"); use tokio::io::{AsyncReadExt, AsyncWriteExt}; let (read, write) = session.stream_halves().expect("halves"); write.write_all(b"tcp-local").await.expect("write"); write.flush().await.expect("flush"); let mut buf = vec![0u8; 9]; tokio::time::timeout(std::time::Duration::from_secs(5), read.read_exact(&mut buf)) .await .expect("round trip") .expect("read"); assert_eq!(&buf, b"tcp-local"); session.close().await; } #[tokio::test] async fn udp_datagram_round_trip_through_the_framed_adapter() { // The FRAMED adapter end-to-end (ADR-003): session sends the codec // frame; the pump copies raw bytes; the dialed UdpHalf strips the // frame on recv and delivers ONE datagram to the target; the echo // comes back through the same path. let addr = udp_echo_server().await; let registry = ResourceRegistry::new(); registry .register("dns", Substrate::Udp, &addr.to_string()) .await; let topo = wire_forward(registry, alktunnels::local::local_dial()).await; let mut session = TunnelSession::open(&topo.consumer, params("dns", Substrate::Udp)) .await .expect("udp open"); session.send_datagram(b"query").await.expect("send"); let dg = tokio::time::timeout(std::time::Duration::from_secs(5), session.recv_datagram()) .await .expect("recv timed out") .expect("io ok") .expect("datagram"); assert_eq!(&dg[..], b"query"); session.close().await; } #[tokio::test] async fn udp_large_datagram_through_bounded_buffers() { // The POC's sizing test: a 1400-byte datagram (max ethernet MTU // payload) round-trips through the codec + the bounded-buffer path. let addr = udp_echo_server().await; let registry = ResourceRegistry::new(); registry .register("big", Substrate::Udp, &addr.to_string()) .await; let topo = wire_forward(registry, alktunnels::local::local_dial()).await; let mut session = TunnelSession::open(&topo.consumer, params("big", Substrate::Udp)) .await .expect("udp open"); let payload: Vec = (0..1400u32).map(|i| (i % 7) as u8).collect(); session.send_datagram(&payload).await.expect("send"); let got = tokio::time::timeout(std::time::Duration::from_secs(5), session.recv_datagram()) .await .expect("recv timed out") .expect("io ok") .expect("datagram"); assert_eq!(got.len(), payload.len()); assert_eq!(&got[..], &payload[..]); session.close().await; } #[tokio::test] async fn unix_dial_round_trips() { // OQ-TN-14: unix ships with local v1 (same halves shape as TCP). let dir = std::env::temp_dir().join(format!("alktunnels-test-{}", std::process::id())); let _ = std::fs::create_dir_all(&dir); let path = dir.join("echo.sock"); let _ = std::fs::remove_file(&path); let listener = tokio::net::UnixListener::bind(&path).expect("bind unix echo"); tokio::spawn(async move { loop { let Ok((sock, _)) = listener.accept().await else { return; }; tokio::spawn(async move { let (mut r, mut w) = tokio::io::split(sock); let _ = tokio::io::copy(&mut r, &mut w).await; }); } }); let registry = ResourceRegistry::new(); registry .register("uds", Substrate::Unix, &path.to_string_lossy()) .await; let topo = wire_forward(registry, alktunnels::local::local_dial()).await; let mut session = TunnelSession::open(&topo.consumer, params("uds", Substrate::Unix)) .await .expect("unix open"); use tokio::io::{AsyncReadExt, AsyncWriteExt}; let (read, write) = session.stream_halves().expect("halves"); write.write_all(b"unix-local").await.expect("write"); write.flush().await.expect("flush"); let mut buf = vec![0u8; 10]; tokio::time::timeout(std::time::Duration::from_secs(5), read.read_exact(&mut buf)) .await .expect("round trip") .expect("read"); assert_eq!(&buf, b"unix-local"); session.close().await; let _ = std::fs::remove_file(&path); } #[tokio::test] async fn truncation_fails_loud_oq_tn_13() { // The fail-loud posture (OQ-TN-13) at the FRAMED adapter: the // socket-facing recv uses the full 65535-byte scratch (try_recv // truncates silently at the buffer size), so a well-formed // >caller-buffer datagram is received whole and STAGED — never // silently truncated into a shorter frame. The staging surface is // pinned by `oversized_frame_stages_across_reads` (below); this // test pins the fail-loud path directly at the unit level: a // scratch smaller than the datagram WOULD have corrupted the // framing (the U-3 re-validation's warning), so the scratch must // be full-size. A datagram at the IP/UDP max (65507) is within // the u16 frame bound (65535) — it must arrive intact. let sock_a = tokio::net::UdpSocket::bind(("127.0.0.1", 0)) .await .expect("bind a"); let sock_b = tokio::net::UdpSocket::bind(("127.0.0.1", 0)) .await .expect("bind b"); sock_a .connect(sock_b.local_addr().expect("b addr")) .await .expect("connect a"); sock_b .connect(sock_a.local_addr().expect("a addr")) .await .expect("connect b"); // The producer-side adapter (what the establisher dials): sock_a // wrapped as halves. A 65507-byte datagram (the UDP theoretical // max — a well-formed wire's largest possible frame) from the // wire side. let (read, _write) = alktunnels::local::UdpHalf::split(sock_a); let mut read = read; let payload: Vec = (0..65507u32).map(|i| (i % 251) as u8).collect(); sock_b.send(&payload).await.expect("send max-size"); // The pump reads with the codec's chunk size (16 KiB) — far // smaller than the datagram: the adapter must STAGE, never // truncate. Drain through small reads and reassemble the frame. use tokio::io::AsyncReadExt; let mut got = Vec::new(); let mut chunk = vec![0u8; 16 * 1024]; while got.len() < payload.len() + 2 { let n = tokio::time::timeout(std::time::Duration::from_secs(5), read.read(&mut chunk)) .await .expect("read timed out (datagram truncated?)") .expect("read ok"); assert!(n > 0, "zero-length read before the frame completed"); got.extend_from_slice(&chunk[..n]); } assert_eq!(&got[..2], &[0xff, 0xe3], "declared length = 65507"); assert_eq!( &got[2..], &payload[..], "full payload intact — no truncation" ); } /// The >caller-buffer (but within-frame-bound) datagram: the framed /// adapter STAGES the frame and hands it out across reads — the pump's /// far-side de-framer reassembles the true length (no silent /// truncation, no shorter-datagram misread). The reassembly is pinned /// by the end-to-end gates below; here the staging surface is pinned /// directly: successive reads yield the full frame. #[tokio::test] async fn oversized_frame_stages_across_reads() { let sock_a = tokio::net::UdpSocket::bind(("127.0.0.1", 0)) .await .expect("bind a"); let sock_b = tokio::net::UdpSocket::bind(("127.0.0.1", 0)) .await .expect("bind b"); sock_a .connect(sock_b.local_addr().expect("b addr")) .await .expect("connect a"); sock_b .connect(sock_a.local_addr().expect("a addr")) .await .expect("connect b"); let (read, _write) = alktunnels::local::UdpHalf::split(sock_a); let mut read = read; let payload: Vec = (0..20000u32).map(|i| (i % 251) as u8).collect(); sock_b.send(&payload).await.expect("send 20k"); // Read through a small caller buffer (16 KiB — smaller than the // ~20002-byte frame): the first read fills it completely, the // second hands out the staged remainder. use tokio::io::AsyncReadExt; let mut got = Vec::new(); let mut chunk = vec![0u8; 16 * 1024]; let n1 = tokio::time::timeout(std::time::Duration::from_secs(5), read.read(&mut chunk)) .await .expect("first read") .expect("read ok"); got.extend_from_slice(&chunk[..n1]); let n2 = tokio::time::timeout(std::time::Duration::from_secs(5), read.read(&mut chunk)) .await .expect("second read") .expect("read ok"); got.extend_from_slice(&chunk[..n2]); assert_eq!(got.len(), payload.len() + 2, "frame length read out"); assert_eq!(&got[..2], &[len_hi(payload.len()), len_lo(payload.len())]); assert_eq!(&got[2..], &payload[..], "payload bytes intact"); } fn len_hi(n: usize) -> u8 { (n >> 8) as u8 } fn len_lo(n: usize) -> u8 { (n & 0xff) as u8 } /// A RECORDING UDP "target" (review 001's verification-gate probe /// shape, made permanent): it records every datagram it receives /// (byte-for-byte, to the shared log) and replies with canned, /// configurable datagrams — including EMPTY and RAW-UNFRAMED ones the /// echo server can't produce. struct RecordingTarget { addr: std::net::SocketAddr, received: Arc>>>, } async fn recording_udp_target(replies: Vec>) -> RecordingTarget { let sock = tokio::net::UdpSocket::bind(("127.0.0.1", 0)) .await .expect("bind recording target"); let addr = sock.local_addr().expect("addr"); let received = Arc::new(tokio::sync::Mutex::new(Vec::new())); let log = Arc::clone(&received); tokio::spawn(async move { let mut buf = vec![0u8; 65535]; // Reply cyclically (raw, as a real target would) to each // datagram that arrives, then keep recording. let mut next = 0usize; loop { let Ok((n, peer)) = sock.recv_from(&mut buf).await else { return; }; log.lock().await.push(buf[..n].to_vec()); if !replies.is_empty() { let reply = &replies[next % replies.len()]; next += 1; if sock.send_to(reply, peer).await.is_err() { return; } } } }); RecordingTarget { addr, received } } fn params(resource: &str, substrate: Substrate) -> TunnelParams { TunnelParams { resource: resource.to_string(), substrate, } } /// GATE 1a (review 001): the target receives PAYLOAD BYTES ONLY — no /// `[len: u16 BE]` codec prefix (the U-1 defect's primary face). #[tokio::test] async fn gate_target_receives_payload_bytes_only() { let target = recording_udp_target(vec![]).await; let registry = ResourceRegistry::new(); registry .register("rec", Substrate::Udp, &target.addr.to_string()) .await; let topo = wire_forward(registry, alktunnels::local::local_dial()).await; let mut session = TunnelSession::open(&topo.consumer, params("rec", Substrate::Udp)) .await .expect("udp open"); session.send_datagram(b"query").await.expect("send"); // Give the datagram time to cross the pump. for _ in 0..50 { if !target.received.lock().await.is_empty() { break; } tokio::time::sleep(std::time::Duration::from_millis(20)).await; } let received = target.received.lock().await; assert_eq!(received.len(), 1, "exactly one datagram at the target"); assert_eq!( &received[0][..], b"query", "the target sees the payload, not the codec frame (no 00 05 prefix)" ); session.close().await; } /// GATE 1b: an EMPTY datagram round-trips over the REAL socket path — /// `recv_datagram` yields `Some(b"")` and the channel stays alive /// (the F-2 fix end-to-end; the raw adapter collapsed it to EOF). #[tokio::test] async fn gate_empty_datagram_round_trips_over_the_real_socket_path() { // The target replies cyclically: empty datagram first (the // round-trip under test), then `still-alive` (the liveness probe). let target = recording_udp_target(vec![vec![], b"still-alive".to_vec()]).await; let registry = ResourceRegistry::new(); registry .register("empty", Substrate::Udp, &target.addr.to_string()) .await; let topo = wire_forward(registry, alktunnels::local::local_dial()).await; let mut session = TunnelSession::open(&topo.consumer, params("empty", Substrate::Udp)) .await .expect("udp open"); session.send_datagram(b"").await.expect("send empty"); let dg = tokio::time::timeout(std::time::Duration::from_secs(5), session.recv_datagram()) .await .expect("recv timed out (empty datagram collapsed to EOF?)") .expect("io ok") .expect("empty datagram is Some, NOT None (EOF)"); assert!(dg.is_empty(), "the empty datagram round-trips"); // The channel is still alive: a second exchange works (the // canned reply cycles — the target sends b"still-alive" back). session.send_datagram(b"second").await.expect("send 2"); let dg2 = tokio::time::timeout(std::time::Duration::from_secs(5), session.recv_datagram()) .await .expect("recv 2 timed out") .expect("io ok 2") .expect("datagram 2"); assert_eq!(&dg2[..], b"still-alive"); session.close().await; } /// GATE 2: a >8 KiB datagram round-trips as ONE datagram at the /// target (the 8 KiB copy-buffer split is gone — the frame reassembles /// at the adapter before crossing the pump). #[tokio::test] async fn gate_large_datagram_arrives_as_one_datagram() { let target = recording_udp_target(vec![]).await; let registry = ResourceRegistry::new(); registry .register("big", Substrate::Udp, &target.addr.to_string()) .await; let topo = wire_forward(registry, alktunnels::local::local_dial()).await; let mut session = TunnelSession::open(&topo.consumer, params("big", Substrate::Udp)) .await .expect("udp open"); // 9000 bytes — the review's probe size that split as 8192 + 810. let payload: Vec = (0..9000u32).map(|i| (i % 251) as u8).collect(); session.send_datagram(&payload).await.expect("send 9000"); for _ in 0..100 { if !target.received.lock().await.is_empty() { break; } tokio::time::sleep(std::time::Duration::from_millis(20)).await; } let received = target.received.lock().await; assert_eq!(received.len(), 1, "ONE datagram at the target, not two"); assert_eq!( received[0].len(), 9000, "full payload in one datagram (no 8192 split)" ); assert_eq!(&received[0][..], &payload[..], "payload intact"); session.close().await; } /// GATE 3: an unframed target-initiated datagram resolves at the /// session (the session codec decodes the framed bytes the adapter /// produced — real-target traffic is not parked mid-frame). #[tokio::test] async fn gate_unframed_target_datagram_resolves_at_the_session() { // A raw "HELLO" — a real server's greeting the raw adapter parked // mid-frame (the probe's 2s timeout). let target = recording_udp_target(vec![b"HELLO".to_vec()]).await; let registry = ResourceRegistry::new(); registry .register("greet", Substrate::Udp, &target.addr.to_string()) .await; let topo = wire_forward(registry, alktunnels::local::local_dial()).await; let mut session = TunnelSession::open(&topo.consumer, params("greet", Substrate::Udp)) .await .expect("udp open"); session.send_datagram(b"ping").await.expect("send"); let dg = tokio::time::timeout(std::time::Duration::from_secs(5), session.recv_datagram()) .await .expect("recv timed out — unframed target traffic was parked/dropped") .expect("io ok") .expect("datagram"); assert_eq!(&dg[..], b"HELLO"); session.close().await; } /// U-2 (review 001): `connect_udp`'s bind is family-correct — a v6 /// target dials from a v6-bound socket (the hardcoded `0.0.0.0` bind /// broke every v6-only target at connect). A real ::1 target /// round-trips through the full path. #[tokio::test] async fn connect_udp_binds_the_matching_family_v6_targets_dial() { let sock = tokio::net::UdpSocket::bind(("::1", 0)) .await .expect("bind v6 echo"); let addr = sock.local_addr().expect("v6 addr"); tokio::spawn(async move { let mut buf = vec![0u8; 65535]; loop { let Ok((n, peer)) = sock.recv_from(&mut buf).await else { return; }; if sock.send_to(&buf[..n], peer).await.is_err() { return; } } }); let registry = ResourceRegistry::new(); registry .register("v6", Substrate::Udp, &addr.to_string()) .await; let topo = wire_forward(registry, alktunnels::local::local_dial()).await; let mut session = TunnelSession::open(&topo.consumer, params("v6", Substrate::Udp)) .await .expect("v6 udp open (family-correct bind)"); session.send_datagram(b"v6-query").await.expect("send v6"); let dg = tokio::time::timeout(std::time::Duration::from_secs(5), session.recv_datagram()) .await .expect("recv timed out") .expect("io ok") .expect("datagram"); assert_eq!(&dg[..], b"v6-query"); session.close().await; } #[tokio::test] async fn dial_refusal_maps_to_dial_failed() { // Error mapping: a refused dial (nothing listening on the port) is // `dial_failed` on the wire (`channel:open_failed` reason). let registry = ResourceRegistry::new(); registry .register("dead", Substrate::Tcp, "127.0.0.1:1") .await; let topo = wire_forward(registry, alktunnels::local::local_dial()).await; let err = match TunnelSession::open(&topo.consumer, params("dead", Substrate::Tcp)).await { Ok(_) => panic!("refused dial must fail"), Err(e) => e, }; assert_eq!(err.open_ref().establishment_reason(), Some("dial_failed")); } #[tokio::test] async fn listen_producer_over_a_real_tcp_listener() { // The listen shape + local halves compose: a real TcpListenerHalves // accept loop feeds an AcceptQueue; a listen-registered producer // pops accepted sockets; the consumer pumps the tunnel against the // accepted local socket. let queue = alktunnels::producer::AcceptQueue::new(); let listener = alktunnels::local::bind_tcp("127.0.0.1:0") .await .expect("bind"); let addr = listener.local_addr().expect("addr"); let accept_loop = tokio::spawn({ let queue = queue.clone(); async move { listener.accept_loop(queue).await } }); // A local client connects (the accepted connection's source). The // driving end comes from `try_clone` — the pump owns the accepted // handle; the driver pushes/reads the echo through the same real // socket. // std-level try_clone BEFORE converting to tokio streams: one fd // for the driver, one wrapped for the pump (two independent // handles to the same socket). O_NONBLOCK is shared across the // dup, so BOTH ends go nonblocking and the driver is driven // async too. let local_std = std::net::TcpStream::connect(addr).expect("std connect"); let driver_std = local_std.try_clone().expect("try_clone driver"); local_std.set_nonblocking(true).expect("nonblocking"); let mut driver = tokio::net::TcpStream::from_std(driver_std).expect("tokio driver"); let registry = ResourceRegistry::new(); registry .register("listener", Substrate::Tcp, "assembly-owned") .await; let accept_fn: alktunnels::producer::AcceptFn = Arc::new(move || { let queue = queue.clone(); Box::pin(async move { queue.pop().await.ok_or_else(|| { alktunnels::producer::TunnelEstablishError::DialFailed( "listener closed".to_string(), ) }) }) }); let topo = harness::wire_listen(registry, accept_fn).await; let channel_id = alktunnels::open_reverse_channel( &topo.consumer_call, ¶ms("listener", Substrate::Tcp), None, ) .await .expect("listen open"); let session = TunnelSession::adopt( &topo.consumer_manager, channel_id, Substrate::Tcp, alktunnels::TUNNEL_ALPN, ) .await .expect("adopt"); // The producer's pump (wrapper-managed) copies channel <-> the // accepted handle; the accepted handle IS the client socket — so // the session's borrowed halves drive it directly: the driver end // writes into the client socket, the pump carries it over the // channel, and the session halves read it (and back). let mut session = session; use tokio::io::{AsyncReadExt, AsyncWriteExt}; driver.write_all(b"listen-local").await.expect("write"); driver.flush().await.expect("flush"); let (read, _write) = session.stream_halves().expect("halves"); let mut buf = vec![0u8; 12]; tokio::time::timeout(std::time::Duration::from_secs(5), read.read_exact(&mut buf)) .await .expect("round trip") .expect("read"); assert_eq!(&buf, b"listen-local"); session.close().await; accept_loop.abort(); } // ===================================================================== // UdpHalf write-side machinery (review 001's U-1 invariants, the // never-blocks paths the end-to-end gates don't reach): WouldBlock // stashing, the accepted-byte high-water backpressure, the flush // drain, and the shutdown best-effort drain — driven with poll_fn // over a shrunken-SO_SNDBUF socket (deterministic, no timing). // ===================================================================== /// An UNCONNECTED UdpHalf write half: `try_send` fails /// deterministically with `EDESTADDRREQ` ("destination address /// required") — the deterministic probe for the write path's ERROR /// arms on this host, where no UDP sender-blocking shape was /// reachable (loopback/veth drop silently; see the host-notes on the /// drain test). fn unconnected_write_half() -> Box { let std_sock = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind unconnected"); std_sock.set_nonblocking(true).expect("nonblocking"); let sock = tokio::net::UdpSocket::from_std(std_sock).expect("tokio socket"); let (_read, write) = alktunnels::local::UdpHalf::split(sock); write } /// Drive one `poll_write` with the given buffer (one poll — the /// pump's copy shape). async fn poll_write_once( write: &mut W, buf: &[u8], ) -> std::io::Result { futures::future::poll_fn(|cx| tokio::io::AsyncWrite::poll_write(Pin::new(write), cx, buf)).await } /// Drive one `poll_flush` to completion (the copy's flush). async fn poll_flush_once(write: &mut W) -> std::io::Result<()> { futures::future::poll_fn(|cx| tokio::io::AsyncWrite::poll_flush(Pin::new(write), cx)).await } /// Drive one `poll_shutdown`. async fn poll_shutdown_once( write: &mut W, ) -> std::io::Result<()> { futures::future::poll_fn(|cx| tokio::io::AsyncWrite::poll_shutdown(Pin::new(write), cx)).await } use std::pin::Pin; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn udp_half_write_queue_drains_on_flush_and_shutdown() { // The queue/flush machinery on a WORKING socket: poll_write // accepts frames (the de-framer's queue + accepted accounting), // flush drains them onto the socket (Ready(Ok)), and shutdown's // best-effort drain reports Ready(Ok) (UDP has no shutdown). // The peer reads (a real probe) — the datagrams land. // // Host-notes (probed 2026-09-09): (a) a fresh-socket first // `try_send` may report WouldBlock spuriously on this kernel // (one readiness+retry cycle clears it, ~20% of runs) — the // flush loop tolerates it, which also exercises the `sending` // stash + re-offer path whenever it fires. (b) The test runs // multi-thread: on a current-thread runtime the kernel's // socket-registration timing left datagrams undelivered at // bound-but-unread peers (tokio 1.53 probe result), so the // delivery assertion is only trustworthy multi-threaded. let peer = tokio::net::UdpSocket::bind(("127.0.0.1", 0)) .await .expect("bind peer"); let snd_sock = socket2::Socket::new( socket2::Domain::IPV4, socket2::Type::DGRAM, Some(socket2::Protocol::UDP), ) .expect("raw sender socket"); snd_sock.set_nonblocking(true).expect("nonblocking"); snd_sock .bind(&socket2::SockAddr::from( "127.0.0.1:0" .parse::() .expect("parse"), )) .expect("bind sender"); let snd_std: std::net::UdpSocket = snd_sock.into(); let snd = tokio::net::UdpSocket::from_std(snd_std).expect("tokio sender"); snd.connect(peer.local_addr().expect("peer addr")) .await .expect("connect sender"); let (_read, mut write) = alktunnels::local::UdpHalf::split(snd); // Accept three frames (the de-framer queue holds them). for i in 0..3u8 { let frame = [0, 3, i, i, i]; let res = poll_write_once(&mut write, &frame).await; assert!( matches!(res, Ok(5)), "poll_write accepted the frame: {res:?}" ); } // Flush drains the queue onto the socket — bounded cycles: the // quirk's stash+Pending resolves on the next flush (poll_send_ready // is Ok), a saturated socket stays queued (not this host's case). let mut flushed = false; for _ in 0..8 { match tokio::time::timeout( std::time::Duration::from_secs(5), poll_flush_once(&mut write), ) .await .expect("flush timed out") { Ok(()) => { flushed = true; break; } Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {} Err(e) => panic!("unexpected flush error: {e:?}"), } } assert!(flushed, "flush drained the queue within 8 cycles"); // The peer saw all three datagrams (the accounting released). let mut buf = vec![0u8; 65535]; let mut received = 0; for _ in 0..50 { match peer.try_recv(&mut buf) { Ok(_) => received += 1, Err(_) if received < 3 => { tokio::time::sleep(std::time::Duration::from_millis(20)).await; } Err(_) => break, } } assert_eq!(received, 3, "the peer saw the flushed datagrams"); // Shutdown: the best-effort drain (the queue is already empty) → // Ready(Ok). let res = poll_shutdown_once(&mut write).await; assert!(res.is_ok(), "shutdown is Ready(Ok): {res:?}"); drop(peer); } #[tokio::test] async fn udp_half_write_error_maps_loud_on_unconnected_socket() { // The ERROR arms: an unconnected socket's `try_send` fails // deterministically (`EDESTADDRREQ`) — poll_write's drain maps // the socket error loud (the `pump_queue_to_socket` Err arm), // and flush re-raises it (the `poll_flush` error surface). The // high-water backpressure and the WouldBlock stash remain // host-dependent (see connected_udp_pair's note) — covered by the // leftover-coverage task. let mut write = unconnected_write_half(); let frame = [0, 3, b'x', b'y', b'z']; let res = poll_write_once(&mut write, &frame).await; match res { // The opportunistic drain swallows the error on the accept // path (`let _ =`); the queue still holds the datagram. Ok(_) => {} Err(e) => panic!("poll_write must ACCEPT (the drain error is swallowed): {e:?}"), } // Flush surfaces the socket error loud (the drain's Err arm). let res = tokio::time::timeout( std::time::Duration::from_secs(5), poll_flush_once(&mut write), ) .await .expect("flush timed out"); assert!(res.is_err(), "flush must surface the unconnected error"); } #[tokio::test] async fn udp_half_poll_shutdown_drains_best_effort_and_is_idempotent() { // UDP has no shutdown: poll_shutdown drains the queue best-effort // and reports Ready(Ok) — the drop-is-close posture. On a // saturated socket the drain is best-effort too (the error is // swallowed — shutdown never fails the pump). let mut write = unconnected_write_half(); use tokio::io::AsyncWriteExt; write .write_all(&[0, 3, b'x', b'y', b'z']) .await .expect("accept"); let res = poll_shutdown_once(&mut write).await; assert!( res.is_ok(), "shutdown is Ready(Ok) even when the drain errors: {res:?}" ); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn udp_half_flush_drains_repeatedly_until_the_queue_is_empty() { // The flush loop's pump-again shape (tokio's copy polls flush // after every window drain): each flush drains that window's // queued datagrams; five write+flush cycles → five datagrams at // the peer (the fresh-socket quirk tolerated per cycle — see the // host-notes on the drain test; multi-thread runtime for the same // delivery-timing reason). let peer = tokio::net::UdpSocket::bind(("127.0.0.1", 0)) .await .expect("bind peer"); let snd_sock = socket2::Socket::new( socket2::Domain::IPV4, socket2::Type::DGRAM, Some(socket2::Protocol::UDP), ) .expect("raw sender socket"); snd_sock.set_nonblocking(true).expect("nonblocking"); snd_sock .bind(&socket2::SockAddr::from( "127.0.0.1:0" .parse::() .expect("parse"), )) .expect("bind sender"); let snd_std: std::net::UdpSocket = snd_sock.into(); let snd = tokio::net::UdpSocket::from_std(snd_std).expect("tokio sender"); snd.connect(peer.local_addr().expect("peer addr")) .await .expect("connect sender"); let (_read, mut write) = alktunnels::local::UdpHalf::split(snd); for i in 0..5u8 { let frame = [0, 1, i]; poll_write_once(&mut write, &frame).await.expect("accept"); // Each cycle flushes (bounded retries — the fresh-socket // quirk's stash clears on the second readiness cycle). let mut flushed = false; for _ in 0..8 { match poll_flush_once(&mut write).await { Ok(()) => { flushed = true; break; } Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {} Err(e) => panic!("unexpected flush {i} error: {e:?}"), } } assert!(flushed, "flush cycle {i} drained"); } let mut buf = vec![0u8; 65535]; let mut received = 0; for _ in 0..50 { match peer.try_recv(&mut buf) { Ok(_) => received += 1, Err(_) if received < 5 => { tokio::time::sleep(std::time::Duration::from_millis(20)).await; } Err(_) => break, } } assert_eq!(received, 5, "five flush cycles, five datagrams"); drop(peer); } /// The resolve arm (`connect_udp`'s no-addresses mapping): a target /// whose name cannot be resolved (an RFC 6761 `.invalid` name — /// guaranteed NXDOMAIN, resolved instantly and without network I/O) /// maps to `dial_failed` (the resolve step's error posture, ADR-004's /// `DialFn` contract). Rootless, deterministic. #[tokio::test] async fn connect_udp_unresolvable_target_maps_to_dial_failed() { let err = match alktunnels::local::connect_udp("definitely-not-a-real-host-xyz.invalid:1").await { Ok(_) => panic!("an unresolvable target must fail"), Err(e) => e, }; assert!( matches!( err, alktunnels::producer::TunnelEstablishError::DialFailed(_) ), "resolve failure maps to dial_failed: {err:?}" ); } // ===================================================================== // The sender-block machinery (the coverage-weak-spots task's option 1, // CLOSED — the recipe was found): a veth fixture with the peer // address UNASSIGNED and the neighbor table flushed puts the // connected UDP sender into a PERMANENT WouldBlock/ENOSPC state // (the unresolved neighbor never resolves; the blocked write path's // skb stays charged to the socket's wmem with a shrunken SO_SNDBUF). // Assigning the peer address + binding the peer resolves ARP; the // queued/stashed datagrams drain and DELIVER. Validated on this host // (kernel 6.8.0-110-generic, tokio 1.53.1, socket2 0.6.5) — 3/3 // deterministic probe runs, 2026-09-09. // // Root-gated (`ip link add`): auto-skipped without root. Never // touches the default route or any real interface. // ===================================================================== /// The veth fixture (created and torn down per run): `alktx/alktx-p` /// with 10.240.0.1/24 (the sender side) and 10.240.0.9 (the peer /// side, UNASSIGNED during the block phase — its absence is what /// pins the sender). The peer address is assigned mid-test (the /// resolve step). A dead-simple guard skips the test when `ip` is /// unavailable or the command fails (non-root hosts). struct VethFixture { peer_addr_assigned: bool, } /// Run a privileged `ip` command: bare `ip` first (uid-0 test runs), /// `sudo ip` second (the uid-1000 dev runs). `None` = unavailable. async fn ip_cmd(args: &[&str]) -> Option { let mut spawned = false; for prog in ["ip", "sudo"] { let mut argv = Vec::with_capacity(args.len() + 1); if prog == "sudo" { argv.push("ip"); } argv.extend_from_slice(args); match tokio::process::Command::new(prog) .args(&argv) .output() .await { Ok(o) => { spawned = true; if o.status.success() { return Some(true); } } Err(_) => continue, } } spawned.then_some(false) } impl VethFixture { async fn try_create() -> Option { // Skip loud-free: the coverage task documents the host boundary. let created = ip_cmd(&[ "link", "add", "alktx", "type", "veth", "peer", "name", "alktx-p", ]) .await .unwrap_or(false); if !created { eprintln!("skipping: veth fixture unavailable (non-root host)"); return None; } let _ = ip_cmd(&["addr", "add", "10.240.0.1/24", "dev", "alktx"]).await; // A jumbo MTU (65536): a 60 KB datagram crosses as ONE frame — // no IP fragmentation, so no fragment-reassembly races at the // peer (fragment-ID collisions under the drain burst merged // datagrams on an MTU-1500 veth during the probe runs). let _ = ip_cmd(&["link", "set", "alktx", "mtu", "65535"]).await; let _ = ip_cmd(&["link", "set", "alktx", "up"]).await; let _ = ip_cmd(&["link", "set", "alktx-p", "up"]).await; Some(Self { peer_addr_assigned: false, }) } fn assign_peer(&mut self) -> bool { if self.peer_addr_assigned { return true; } let ok = std::process::Command::new("ip") .args(["addr", "add", "10.240.0.9/24", "dev", "alktx-p"]) .output() .ok() .map(|o| o.status.success()) .unwrap_or(false) || std::process::Command::new("sudo") .args(["ip", "addr", "add", "10.240.0.9/24", "dev", "alktx-p"]) .output() .ok() .map(|o| o.status.success()) .unwrap_or(false); self.peer_addr_assigned = ok; ok } } impl Drop for VethFixture { fn drop(&mut self) { let _ = std::process::Command::new("ip") .args(["link", "del", "alktx"]) .output(); let _ = std::process::Command::new("sudo") .args(["ip", "link", "del", "alktx"]) .output(); } } /// A sender socket blocked on the unresolved neighbor: connect to the /// UNASSIGNED peer address, flush the neighbor table, shrunken /// SO_SNDBUF. `try_send` on a 60 KB datagram (truesize beyond the /// buffer) then reports WouldBlock/ENOSPC persistently — the state /// the write-side machinery is for. Returns the tokio socket ready /// for `UdpHalf::split`. async fn blocked_sender_socket(peer_port: u16) -> Option { // The BLOCK-STATE PROBE runs on a THROWAWAY socket: an oversized // probe datagram's partially-built skb chain stays corked in // sk_write_queue on EAGAIN (the kernel flushes it on the next // successful send) — poisoning the socket with a stale partial // datagram. The test socket below is created fresh afterwards. let probe = socket2::Socket::new( socket2::Domain::IPV4, socket2::Type::DGRAM, Some(socket2::Protocol::UDP), ) .ok()?; probe.set_nonblocking(true).ok()?; probe.set_send_buffer_size(32768).ok()?; probe .bind(&"10.240.0.1:0".parse::().ok()?.into()) .ok()?; let probe_std: std::net::UdpSocket = probe.into(); let probe_sock = tokio::net::UdpSocket::from_std(probe_std).ok()?; probe_sock.connect(("10.240.0.9", peer_port)).await.ok()?; // Confirm the block actually holds (the host invariant): with a // 64 KiB SO_SNDBUF, the FIRST 60 KB datagram queues on the // unresolved neighbor (Ok — its skb is charged to wmem); every // SUBSEQUENT send EAGAINs (the charge exceeds the buffer) until // the neighbor resolves. That persistent EAGAIN/ENOBUFS pair IS // the block state. A resolved neighbor would send Ok throughout. let big = vec![0u8; 60000]; let mut blocked = 0; for _ in 0..3 { match probe_sock.try_send(&big) { Ok(_) => {} // the first datagram queues; expected Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => blocked += 1, Err(e) if e.raw_os_error() == Some(105) => blocked += 1, Err(_) => return None, } } if blocked < 1 { return None; } drop(probe_sock); // The test socket: fresh (no corked state), same posture. let sock = socket2::Socket::new( socket2::Domain::IPV4, socket2::Type::DGRAM, Some(socket2::Protocol::UDP), ) .ok()?; sock.set_nonblocking(true).ok()?; sock.set_send_buffer_size(32768).ok()?; sock.bind(&"10.240.0.1:0".parse::().ok()?.into()) .ok()?; let snd_std: std::net::UdpSocket = sock.into(); let snd = tokio::net::UdpSocket::from_std(snd_std).ok()?; snd.connect(("10.240.0.9", peer_port)).await.ok()?; Some(snd) } /// THE sender-block machinery, end to end (option 1 — one test, the /// coherent behavior): the WouldBlock stash (`poll_write`'s /// opportunistic drain stashes the mid-send datagram and registers /// the send waker — `pump_queue_to_socket`'s Pending arms), the /// accepted-byte high-water backpressure (`poll_write` Pends when the /// queue stays over [`alktunnels::local::WRITE_QUEUE_HIGH_WATER`]), /// the flush re-pump (readiness → drain), and the post-resolve /// delivery (the queued datagrams reach the peer). #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn udp_half_write_side_blocks_stashes_and_drains_on_resolve() { let Some(mut veth) = VethFixture::try_create().await else { eprintln!("skipping: veth fixture unavailable (non-root host)"); return; }; // The peer port is EPHEMERAL (stale datagrams from earlier runs // of this test family target any fixed port; an ephemeral port // sidesteps the leftovers). let probe = tokio::net::UdpSocket::bind(("10.240.0.1", 0)) .await .expect("port probe bind"); let peer_port = probe.local_addr().expect("port probe addr").port(); drop(probe); let snd = blocked_sender_socket(peer_port) .await .expect("blocked sender state"); let (_read, mut write) = alktunnels::local::UdpHalf::split(snd); // Phase A: poll_write ACCEPTS everything (the accept-all // contract) while the socket blocks; the queued bytes cross the // high-water line and the next poll_write Pends. // Drive single-shot poll_write polls with a noop waker (the // pump's copy shape): each returns Ok (accepted) or Pending // (the high-water). The neighbor's INCOMPLETE→FAILED cycle // drops queued skbs periodically (charge released → sends // succeed again), so the drive loops until a Pend is observed. let mut body = vec![0xABu8; 60002]; body[0] = 0; // len = 60000 BE body[1] = 0xea; body[2] = 0x60; let noop_waker = futures::task::noop_waker(); let mut cx = std::task::Context::from_waker(&noop_waker); let mut pending_observed = false; for _ in 0..60 { let fut = std::future::poll_fn(|cx| { tokio::io::AsyncWrite::poll_write(Pin::new(&mut write), cx, &body) }); let mut fut = std::pin::pin!(fut); match std::future::Future::poll(fut.as_mut(), &mut cx) { std::task::Poll::Ready(Ok(60002)) => {} // accepted std::task::Poll::Ready(Ok(n)) => panic!("poll_write accepted {n}?"), std::task::Poll::Ready(Err(e)) => panic!("poll_write error: {e:?}"), std::task::Poll::Pending => { pending_observed = true; break; } } } assert!( pending_observed, "the high-water Pends once the queue rides over the line" ); // Flush: the blocked socket → the stash + readiness-Pending arms // (bounded retries; the resolve below unblocks the LAST one). let mut pending_flushes = 0usize; for _ in 0..10 { match tokio::time::timeout( std::time::Duration::from_millis(300), poll_flush_once(&mut write), ) .await { Err(_elapsed) => { pending_flushes += 1; } // readiness stayed Pending (blocked socket) Ok(Ok(())) => break, // drained (only possible after resolve) Ok(Err(e)) => panic!("flush error: {e:?}"), } } assert!(pending_flushes > 0, "the blocked socket Pends the flush"); // Phase B: resolve — assign the peer address (the neighbor // completes), bind the peer, wait for ARP; the queued datagrams // drain and DELIVER. assert!(veth.assign_peer(), "assign the peer address"); let peer = tokio::net::UdpSocket::bind(("10.240.0.9", peer_port)) .await .expect("bind peer (resolve)"); let mut flushed = false; for _ in 0..20 { match tokio::time::timeout( std::time::Duration::from_millis(300), poll_flush_once(&mut write), ) .await { Ok(Ok(())) => { flushed = true; break; } Ok(Err(e)) => panic!("post-resolve flush error: {e:?}"), Err(_) => {} } } assert!(flushed, "the queued datagrams drained after the resolve"); // The peer received the queued datagrams (delivery across the // blocked→resolved window — the stash/queue never lost them; at // least the stashed tail must arrive — the periodic // INCOMPLETE→FAILED drops may consume some queued ones, which // is kernel-side loss, not adapter loss). tokio::time::sleep(std::time::Duration::from_millis(1500)).await; let mut got = 0usize; let mut buf = vec![0u8; 65535]; while peer.try_recv(&mut buf).is_ok() { got += 1; } assert!( got >= 1, "the stashed/queued datagrams delivered after resolve" ); drop(peer); }