innisfree 0.4.3

Exposes local services on public IPv4 address, via cloud server.
Documentation
//! End-to-end UDP forwarding test.
//!
//! Exercises the `innisfree proxy --ports …/UDP` flow end to end:
//! a Rust echo upstream, the cargo-built `innisfree` binary as the
//! in-process proxy, and `socat` (the quintessential UDP sysadmin
//! CLI) as the client. Confirms a datagram fired at the proxy's
//! listener round-trips through the upstream and back.
//!
//! `nc` (nmap's `ncat`) was the obvious first pick but doesn't
//! flush received UDP to stdout reliably with the OpenBSD-style
//! `-w` / `-i` / `--idle-timeout` flags on this distro; socat is
//! both more predictable and equally canonical for UDP work.
//!
//! Gated behind the `udp` feature so default `cargo test` runs
//! aren't held to having `socat` on PATH. Run with:
//!
//!     cargo test --features udp --test proxy_udp
#![cfg(feature = "udp")]

use std::io::Write;
use std::net::UdpSocket;
use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;

#[test]
fn udp_proxy_round_trips_via_socat() {
    let runner = escargot::CargoBuild::new()
        .bin("innisfree")
        .run()
        .expect("building innisfree via escargot");
    let bin = runner.path().to_path_buf();

    let listen_port = pick_ephemeral_udp_port();
    let upstream_port = pick_ephemeral_udp_port();

    // Echo upstream: bind, accept one datagram, reply with the same
    // payload, exit. Run on a real OS thread so the test main thread
    // can keep its tokio-free `std::process` shape.
    let server = UdpSocket::bind(("127.0.0.1", upstream_port)).expect("binding echo upstream");
    server
        .set_read_timeout(Some(Duration::from_secs(5)))
        .expect("setting upstream timeout");
    let server_thread = thread::spawn(move || {
        let mut buf = vec![0u8; 1500];
        let (n, src) = server.recv_from(&mut buf).expect("upstream recv");
        server.send_to(&buf[..n], src).expect("upstream send reply");
    });

    // ServicePort spec is `<port>:<local_port>/PROTO`, where
    // `<local_port>` is what the in-process proxy LISTENS on and
    // `<port>` is where it FORWARDS — i.e. the upstream. (See
    // ServicePort::try_from in src/config.rs.)
    let mut proxy = Command::new(&bin)
        .args([
            "proxy",
            "--dest-ip",
            "127.0.0.1",
            "--ports",
            &format!("{upstream_port}:{listen_port}/UDP"),
        ])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .expect("spawning innisfree proxy");

    // Brief sleep to let the proxy bind. Polling the listen port
    // would be more robust, but 200ms is plenty in practice and
    // keeps the test simple.
    thread::sleep(Duration::from_millis(200));

    // socat as the UDP client. `-T 1` exits after 1s of total
    // inactivity; the reply arrives within ms, then the timer
    // fires and socat exits with the echoed payload on stdout.
    let mut socat = Command::new("socat")
        .args(["-T", "1", "-", &format!("UDP:127.0.0.1:{listen_port}")])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawning socat; is it installed and on PATH?");

    socat
        .stdin
        .as_mut()
        .unwrap()
        .write_all(b"hello-udp\n")
        .expect("writing to socat stdin");
    // Close stdin so socat's inactivity timer can fire.
    drop(socat.stdin.take());

    let socat_out = socat.wait_with_output().expect("waiting for socat");

    // Cleanup: tear down the proxy, drain the upstream thread.
    let _ = proxy.kill();
    let _ = proxy.wait();
    server_thread.join().expect("joining upstream thread");

    let stdout = String::from_utf8_lossy(&socat_out.stdout);
    let stderr = String::from_utf8_lossy(&socat_out.stderr);
    assert!(
        stdout.contains("hello-udp"),
        "socat didn't receive the echoed payload.\n\
         status: {}\nstdout: {stdout:?}\nstderr: {stderr:?}",
        socat_out.status,
    );
}

/// Bind UDP/127.0.0.1:0 to grab an OS-assigned port, then drop the
/// socket so the proxy can rebind it. There's a brief race window
/// where another process could take the port, but this is the
/// pattern tokio's own test suite uses and it's reliable enough on
/// a normal dev/CI box.
fn pick_ephemeral_udp_port() -> u16 {
    let s = UdpSocket::bind("127.0.0.1:0").expect("ephemeral UDP bind");
    s.local_addr().expect("local addr").port()
}