proxychains-masq 0.1.5

TUN-based proxy chain engine — routes TCP flows through SOCKS4/5, HTTP CONNECT, and HTTPS CONNECT proxy chains via a userspace network stack.
Documentation
//! Integration tests for the TUN tunnel loop.
//!
//! These tests require a Linux kernel with:
//!   - `/dev/net/tun` available
//!   - Unprivileged user namespaces (`kernel.unprivileged_userns_clone=1`)
//!   - `ip` (iproute2) in PATH
//!
//! Run with: `cargo test -p proxychains-masq tunnel -- --test-threads=1`
//!
//! Tagged `[needs-net]` in the TASKS.md plan.

#![cfg(target_os = "linux")]

mod helpers;

use std::{
    os::unix::{io::AsRawFd, io::FromRawFd, net::UnixDatagram},
    sync::{atomic::AtomicU32, Arc},
    time::Duration,
};

use nix::{
    sched::CloneFlags,
    sys::wait::{waitpid, WaitStatus},
    unistd::{Gid, Uid},
};
use proxychains_masq::{
    chain::{ChainConfig, ChainType, ProxyEntry, ProxyType},
    dns::DnsMap,
    tunnel::{ProxyChainTunnel, TunnelConfig},
};
use sendfd::{RecvWithFd, SendWithFd};
use smoltcp::phy::{Medium, TunTapInterface};

// ─── Helpers ──────────────────────────────────────────────────────────────────

/// Run `ip <args>` as a subprocess, returning true on success.
fn ip(args: &[&str]) -> bool {
    std::process::Command::new("ip")
        .args(args)
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

// ─── Child namespace entry point ──────────────────────────────────────────────

/// Runs inside the cloned user+net namespace.
///
/// 1. Writes uid/gid maps so the child has a proper identity inside the namespace.
/// 2. Creates a TUN interface (`tun0`) and sends its fd to the parent.
/// 3. Configures loopback and `tun0` with default route via `ip`.
/// 4. Waits for the parent to signal "tunnel ready".
/// 5. Connects to `100.0.0.1:<echo_port>` — the fake IP that the parent's
///    `DnsMap` resolves to the real echo server hostname.
/// 6. Performs an echo round-trip, returns 0 on success.
fn child_run(sock: UnixDatagram, uid: Uid, gid: Gid, echo_port: u16) -> isize {
    // ── 1. User namespace mappings ─────────────────────────────────────────
    // "deny" must precede gid_map for unprivileged processes.
    if std::fs::write("/proc/self/setgroups", "deny\n").is_err()
        || std::fs::write("/proc/self/uid_map", format!("0 {uid} 1\n")).is_err()
        || std::fs::write("/proc/self/gid_map", format!("0 {gid} 1\n")).is_err()
    {
        eprintln!("child: failed to write user namespace maps");
        return 1;
    }

    // ── 2. Create TUN interface ────────────────────────────────────────────
    let tun = match TunTapInterface::new("tun0", Medium::Ip) {
        Ok(t) => t,
        Err(e) => {
            eprintln!("child: TunTapInterface::new failed: {e}");
            return 1;
        }
    };

    // ── 3. Send TUN fd to parent before configuring the interface ──────────
    // The parent receives a dup of the fd and takes over reading/writing it.
    if sock.send_with_fd(&[0u8], &[tun.as_raw_fd()]).is_err() {
        eprintln!("child: send_with_fd failed");
        return 1;
    }

    // ── 4. Configure interfaces via `ip` ───────────────────────────────────
    // Loopback: needed so the child's own TCP stack is functional.
    if !ip(&["addr", "add", "127.0.0.1/8", "dev", "lo"]) || !ip(&["link", "set", "lo", "up"]) {
        eprintln!("child: lo setup failed");
        return 1;
    }
    // tun0: give it an IP and set as the default gateway so all TCP traffic
    // originating in this namespace flows through it to smoltcp.
    if !ip(&["addr", "add", "10.99.0.1/30", "dev", "tun0"])
        || !ip(&["link", "set", "tun0", "up"])
        || !ip(&["route", "add", "default", "dev", "tun0"])
    {
        eprintln!("child: tun0 setup failed");
        return 1;
    }

    // ── 5. Wait for parent's "tunnel ready" signal ─────────────────────────
    let mut buf = [0u8; 1];
    if sock.recv(&mut buf).is_err() {
        eprintln!("child: recv ready signal failed");
        return 1;
    }

    // ── 6. Connect through the TUN to the fake IP ──────────────────────────
    // 100.0.0.1 was pre-allocated by the parent for the echo server's hostname.
    // The packet is routed through tun0 → smoltcp → SOCKS5 → echo server.
    let addr = format!("100.0.0.1:{echo_port}");
    let stream = match std::net::TcpStream::connect(&addr) {
        Ok(s) => s,
        Err(e) => {
            eprintln!("child: connect {addr} failed: {e}");
            return 1;
        }
    };
    stream
        .set_read_timeout(Some(std::time::Duration::from_secs(10)))
        .ok();

    use std::io::{Read, Write};
    let mut s = stream;
    if s.write_all(b"hello-tun").is_err() {
        eprintln!("child: write failed");
        return 1;
    }
    let mut resp = [0u8; 9];
    if s.read_exact(&mut resp).is_err() {
        eprintln!("child: read_exact failed");
        return 1;
    }
    if &resp != b"hello-tun" {
        eprintln!("child: wrong echo: {resp:?}");
        return 1;
    }

    drop(tun);
    0
}

// ─── Shared test setup ────────────────────────────────────────────────────────

/// Spawn a clone child in a new user+net namespace, run the tunnel in the
/// parent, and wait for the child to finish.
///
/// `fake_hostname` is the hostname mapped to `100.0.0.1` in the DnsMap.
/// When the child connects to `100.0.0.1:<echo_port>`, smoltcp resolves the
/// fake IP to `fake_hostname` and passes it to the SOCKS5 proxy as a domain
/// CONNECT.  The SOCKS5 proxy resolves `fake_hostname` to the echo server.
fn run_tunnel_test(fake_hostname: &str) -> WaitStatus {
    let rt = tokio::runtime::Runtime::new().unwrap();
    let (echo_port, _echo_hits) = rt.block_on(helpers::echo_server());
    let socks_hits = Arc::new(AtomicU32::new(0));
    let socks_port = rt.block_on(helpers::socks5_server(None, socks_hits.clone()));

    let (parent_sock, child_sock) = UnixDatagram::pair().unwrap();
    let uid = Uid::current();
    let gid = Gid::current();

    // Clone child into new user+net namespace.
    // `clone(2)` takes an `FnMut`, so we wrap the non-Copy socket in an Option
    // and take it on first (and only) invocation.
    let mut stack = vec![0u8; 4 * 1024 * 1024];
    let mut child_sock_opt = Some(child_sock);
    let child_pid = unsafe {
        nix::sched::clone(
            Box::new(move || {
                let sock = child_sock_opt
                    .take()
                    .expect("clone callback called more than once");
                child_run(sock, uid, gid, echo_port)
            }),
            &mut stack,
            CloneFlags::CLONE_NEWNET | CloneFlags::CLONE_NEWUSER,
            Some(nix::libc::SIGCHLD),
        )
        .expect("clone(2) failed — is kernel.unprivileged_userns_clone=1?")
    };

    // Receive the TUN fd from the child.
    let mut dummy = [0u8; 1];
    let mut raw_fds = [-1i32; 1];
    parent_sock
        .recv_with_fd(&mut dummy, &mut raw_fds)
        .expect("recv_with_fd");
    assert!(raw_fds[0] >= 0, "received valid TUN fd from child");

    // Set O_NONBLOCK so the smoltcp poll loop doesn't block.
    {
        use nix::fcntl::{fcntl, FcntlArg, OFlag};
        let current = OFlag::from_bits_truncate(fcntl(raw_fds[0], FcntlArg::F_GETFL).unwrap());
        fcntl(raw_fds[0], FcntlArg::F_SETFL(current | OFlag::O_NONBLOCK)).unwrap();
    }
    // SAFETY: `raw_fds[0]` is a fresh fd received via sendfd; we have exclusive ownership.
    let tun_file = unsafe { std::fs::File::from_raw_fd(raw_fds[0]) };

    // DnsMap: 100.0.0.1 → fake_hostname
    // 100.x.x.x is unicast (not multicast like 100.x.x.x) so the kernel will
    // route it through the TUN default gateway.  The relay task translates the
    // fake IP back to `fake_hostname` and sends a domain-type CONNECT to SOCKS5.
    let dns_map = DnsMap::new(100);
    dns_map.get_or_alloc(fake_hostname).unwrap(); // 100.0.0.1

    let config = TunnelConfig {
        chain: ChainConfig {
            chain_type: ChainType::Strict,
            proxies: vec![ProxyEntry {
                proxy_type: ProxyType::Socks5,
                addr: "127.0.0.1".parse().unwrap(),
                port: socks_port,
                username: None,
                password: None,
            }],
            chain_len: 1,
            chain_retries: 3,
            connect_timeout: Duration::from_secs(8),
            localnets: vec![],
            dnats: vec![],
            tls_skip_verify: false,
            proxy_dead_threshold: 0,
        },
        dns_map,
        tun_ip: "10.99.0.2".parse().unwrap(), // smoltcp side of the /30
        prefix_len: 30,
        dns_ip: None,
    };

    let tunnel = ProxyChainTunnel::new(config);
    let _thread = tunnel.spawn(tun_file, rt.handle().clone());

    // Signal the child: tunnel is ready, go ahead and connect.
    parent_sock.send(&[1u8]).unwrap();

    waitpid(child_pid, None).unwrap()
}

// ─── T-29: Tunnel relays TCP data end-to-end ─────────────────────────────────

#[test]
fn test_tunnel_relays_tcp() {
    // Use "127.0.0.1" as the hostname so the SOCKS5 proxy can reach the echo
    // server via a direct numeric-IP connection.
    match run_tunnel_test("127.0.0.1") {
        WaitStatus::Exited(_, 0) => {}
        other => panic!("child failed: {other:?}"),
    }
}

// ─── T-30: Fake-IP → hostname forwarding ─────────────────────────────────────

#[test]
fn test_flow_resolves_fake_ip_to_hostname() {
    // Use "localhost" so the SOCKS5 server receives a domain-type CONNECT
    // (not a numeric IP), proving that the DnsMap lookup triggered hostname
    // forwarding rather than raw IP forwarding.
    match run_tunnel_test("localhost") {
        WaitStatus::Exited(_, 0) => {}
        other => panic!("child failed: {other:?}"),
    }
}