From cea0e0fa271d669d1af0770f6d8482bc7ea03814 Mon Sep 17 00:00:00 2001 From: "B. Blechschmidt" Date: Sun, 8 Oct 2023 12:27:32 +0200 Subject: [PATCH 01/16] Resort to writing to /etc/resolv.conf directly if mount permissions are missing --- src/setup.rs | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/setup.rs b/src/setup.rs index c273ea5..a969649 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -155,12 +155,7 @@ impl Setup { Ok(false) } - fn setup_resolv_conf() -> Result<(), Error> { - let fd = nix::fcntl::open( - "/tmp/tun2proxy-resolv.conf", - nix::fcntl::OFlag::O_RDWR | nix::fcntl::OFlag::O_CLOEXEC | nix::fcntl::OFlag::O_CREAT, - nix::sys::stat::Mode::from_bits(0o644).unwrap(), - )?; + fn write_nameserver(fd: RawFd) -> Result<(), Error> { let data = "nameserver 198.18.0.1\n".as_bytes(); let mut written = 0; loop { @@ -170,14 +165,35 @@ impl Setup { written += nix::unistd::write(fd, &data[written..])?; } nix::sys::stat::fchmod(fd, nix::sys::stat::Mode::from_bits(0o444).unwrap())?; - let source = format!("/proc/self/fd/{}", fd); - nix::mount::mount( - source.as_str().into(), - "/etc/resolv.conf", - "".into(), - nix::mount::MsFlags::MS_BIND, - "".into(), + Ok(()) + } + + fn setup_resolv_conf() -> Result<(), Error> { + let mut fd = nix::fcntl::open( + "/tmp/tun2proxy-resolv.conf", + nix::fcntl::OFlag::O_RDWR | nix::fcntl::OFlag::O_CLOEXEC | nix::fcntl::OFlag::O_CREAT, + nix::sys::stat::Mode::from_bits(0o644).unwrap(), )?; + Self::write_nameserver(fd)?; + let source = format!("/proc/self/fd/{}", fd); + if Ok(()) + != nix::mount::mount( + source.as_str().into(), + "/etc/resolv.conf", + "".into(), + nix::mount::MsFlags::MS_BIND, + "".into(), + ) + { + log::warn!("failed to bind mount custom resolv.conf onto /etc/resolv.conf, resorting to direct write"); + nix::unistd::close(fd)?; + fd = nix::fcntl::open( + "/etc/resolv.conf", + nix::fcntl::OFlag::O_WRONLY | nix::fcntl::OFlag::O_CLOEXEC | nix::fcntl::OFlag::O_TRUNC, + nix::sys::stat::Mode::from_bits(0o644).unwrap(), + )?; + Self::write_nameserver(fd)?; + } nix::unistd::close(fd)?; Ok(()) } From 299b51667d719f960772b3435096ff336b91c733 Mon Sep 17 00:00:00 2001 From: "B. Blechschmidt" Date: Sun, 8 Oct 2023 13:09:37 +0200 Subject: [PATCH 02/16] Restore /etc/resolv.conf if it was written directly --- src/setup.rs | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/setup.rs b/src/setup.rs index a969649..51e004f 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -6,6 +6,7 @@ use smoltcp::wire::IpCidr; use std::{ convert::TryFrom, ffi::OsStr, + fs, io::BufRead, net::{IpAddr, Ipv4Addr, Ipv6Addr}, os::unix::io::RawFd, @@ -22,6 +23,8 @@ pub struct Setup { set_up: bool, delete_proxy_route: bool, child: libc::pid_t, + unmount_resolvconf: bool, + restore_resolvconf_data: Option>, } pub fn get_default_cidrs() -> [IpCidr; 4] { @@ -86,6 +89,8 @@ impl Setup { set_up: false, delete_proxy_route: false, child: 0, + unmount_resolvconf: false, + restore_resolvconf_data: None, } } @@ -155,8 +160,7 @@ impl Setup { Ok(false) } - fn write_nameserver(fd: RawFd) -> Result<(), Error> { - let data = "nameserver 198.18.0.1\n".as_bytes(); + fn write_buffer_to_fd(fd: RawFd, data: &[u8]) -> Result<(), Error> { let mut written = 0; loop { if written >= data.len() { @@ -164,11 +168,17 @@ impl Setup { } written += nix::unistd::write(fd, &data[written..])?; } + Ok(()) + } + + fn write_nameserver(fd: RawFd) -> Result<(), Error> { + let data = "nameserver 198.18.0.1\n".as_bytes(); + Self::write_buffer_to_fd(fd, data)?; nix::sys::stat::fchmod(fd, nix::sys::stat::Mode::from_bits(0o444).unwrap())?; Ok(()) } - fn setup_resolv_conf() -> Result<(), Error> { + fn setup_resolv_conf(&mut self) -> Result<(), Error> { let mut fd = nix::fcntl::open( "/tmp/tun2proxy-resolv.conf", nix::fcntl::OFlag::O_RDWR | nix::fcntl::OFlag::O_CLOEXEC | nix::fcntl::OFlag::O_CREAT, @@ -187,12 +197,17 @@ impl Setup { { log::warn!("failed to bind mount custom resolv.conf onto /etc/resolv.conf, resorting to direct write"); nix::unistd::close(fd)?; + + self.restore_resolvconf_data = Some(fs::read("/etc/resolv.conf")?); + fd = nix::fcntl::open( "/etc/resolv.conf", nix::fcntl::OFlag::O_WRONLY | nix::fcntl::OFlag::O_CLOEXEC | nix::fcntl::OFlag::O_TRUNC, nix::sys::stat::Mode::from_bits(0o644).unwrap(), )?; Self::write_nameserver(fd)?; + } else { + self.unmount_resolvconf = true; } nix::unistd::close(fd)?; Ok(()) @@ -225,7 +240,12 @@ impl Setup { .args(["route", "del", self.tunnel_bypass_addr.to_string().as_str()]) .output(); } - nix::mount::umount("/etc/resolv.conf")?; + if self.unmount_resolvconf { + nix::mount::umount("/etc/resolv.conf")?; + } + if let Some(data) = &self.restore_resolvconf_data { + fs::write("/etc/resolv.conf", data)?; + } Ok(()) } @@ -250,7 +270,7 @@ impl Setup { let delete_proxy_route = self.route_proxy_address()?; self.delete_proxy_route = delete_proxy_route; - Self::setup_resolv_conf()?; + self.setup_resolv_conf()?; self.add_tunnel_routes()?; // Signal to child that we are done setting up everything. From b50cac82c07149b32bae8a128f2bad618dbab2d5 Mon Sep 17 00:00:00 2001 From: ssrlive <30760636+ssrlive@users.noreply.github.com> Date: Tue, 10 Oct 2023 14:22:33 +0800 Subject: [PATCH 03/16] Memory exhaustion (#69) --- src/http.rs | 10 +++++----- src/socks.rs | 10 +++++----- src/tun2proxy.rs | 25 ++++++++++++------------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/http.rs b/src/http.rs index 4d3e5bb..7ebd02e 100644 --- a/src/http.rs +++ b/src/http.rs @@ -366,15 +366,15 @@ impl ProxyHandler for HttpConnection { self.state == HttpState::Established } - fn have_data(&mut self, dir: Direction) -> bool { + fn data_len(&self, dir: Direction) -> usize { match dir { Direction::Incoming(incoming) => match incoming { - IncomingDirection::FromServer => !self.server_inbuf.is_empty(), - IncomingDirection::FromClient => !self.client_inbuf.is_empty() || !self.data_buf.is_empty(), + IncomingDirection::FromServer => self.server_inbuf.len(), + IncomingDirection::FromClient => self.client_inbuf.len().max(self.data_buf.len()), }, Direction::Outgoing(outgoing) => match outgoing { - OutgoingDirection::ToServer => !self.server_outbuf.is_empty(), - OutgoingDirection::ToClient => !self.client_outbuf.is_empty(), + OutgoingDirection::ToServer => self.server_outbuf.len(), + OutgoingDirection::ToClient => self.client_outbuf.len(), }, } } diff --git a/src/socks.rs b/src/socks.rs index 7c62aa9..c7b60aa 100644 --- a/src/socks.rs +++ b/src/socks.rs @@ -314,15 +314,15 @@ impl ProxyHandler for SocksProxyImpl { self.state == SocksState::Established } - fn have_data(&mut self, dir: Direction) -> bool { + fn data_len(&self, dir: Direction) -> usize { match dir { Direction::Incoming(incoming) => match incoming { - IncomingDirection::FromServer => !self.server_inbuf.is_empty(), - IncomingDirection::FromClient => !self.client_inbuf.is_empty() || !self.data_buf.is_empty(), + IncomingDirection::FromServer => self.server_inbuf.len(), + IncomingDirection::FromClient => self.client_inbuf.len().max(self.data_buf.len()), }, Direction::Outgoing(outgoing) => match outgoing { - OutgoingDirection::ToServer => !self.server_outbuf.is_empty(), - OutgoingDirection::ToClient => !self.client_outbuf.is_empty(), + OutgoingDirection::ToServer => self.server_outbuf.len(), + OutgoingDirection::ToClient => self.client_outbuf.len(), }, } } diff --git a/src/tun2proxy.rs b/src/tun2proxy.rs index 6ae46ba..652631b 100644 --- a/src/tun2proxy.rs +++ b/src/tun2proxy.rs @@ -174,6 +174,7 @@ const CLIENT_WRITE_CLOSED: u8 = 2; const UDP_ASSO_TIMEOUT: u64 = 10; // seconds const DNS_PORT: u16 = 53; +const IP_PACKAGE_MAX_SIZE: usize = 0xFFFF; struct ConnectionState { smoltcp_handle: SocketHandle, @@ -197,7 +198,7 @@ pub(crate) trait ProxyHandler { fn consume_data(&mut self, dir: OutgoingDirection, size: usize); fn peek_data(&mut self, dir: OutgoingDirection) -> OutgoingDataEvent; fn connection_established(&self) -> bool; - fn have_data(&mut self, dir: Direction) -> bool; + fn data_len(&self, dir: Direction) -> usize; fn reset_connection(&self) -> bool; fn get_udp_associate(&self) -> Option; } @@ -395,13 +396,10 @@ impl<'a> TunToProxy<'a> { None => return Ok(()), }; let mut closed_ends = 0; + let handler = state.proxy_handler.as_ref(); if (state.close_state & SERVER_WRITE_CLOSED) == SERVER_WRITE_CLOSED - && !state - .proxy_handler - .have_data(Direction::Incoming(IncomingDirection::FromServer)) - && !state - .proxy_handler - .have_data(Direction::Outgoing(OutgoingDirection::ToClient)) + && handler.data_len(Direction::Incoming(IncomingDirection::FromServer)) == 0 + && handler.data_len(Direction::Outgoing(OutgoingDirection::ToClient)) == 0 { // Close tun interface let socket = self.sockets.get_mut::(state.smoltcp_handle); @@ -411,12 +409,8 @@ impl<'a> TunToProxy<'a> { } if (state.close_state & CLIENT_WRITE_CLOSED) == CLIENT_WRITE_CLOSED - && !state - .proxy_handler - .have_data(Direction::Incoming(IncomingDirection::FromClient)) - && !state - .proxy_handler - .have_data(Direction::Outgoing(OutgoingDirection::ToServer)) + && handler.data_len(Direction::Incoming(IncomingDirection::FromClient)) == 0 + && handler.data_len(Direction::Outgoing(OutgoingDirection::ToServer)) == 0 { // Close remote server if let Err(err) = state.mio_stream.shutdown(Shutdown::Write) { @@ -443,6 +437,11 @@ impl<'a> TunToProxy<'a> { let socket = self.sockets.get_mut::(state.smoltcp_handle); let mut error = Ok(()); while socket.can_recv() && error.is_ok() { + let dir = Direction::Outgoing(OutgoingDirection::ToServer); + if state.proxy_handler.data_len(dir) >= IP_PACKAGE_MAX_SIZE { + break; + } + socket.recv(|data| { let event = IncomingDataEvent { direction: IncomingDirection::FromClient, From 5d722fc2a3fe92e47f69dc427406b48675eb9171 Mon Sep 17 00:00:00 2001 From: PaperDragon-SH <2678885646@qq.com> Date: Tue, 10 Oct 2023 16:04:13 +0800 Subject: [PATCH 04/16] optimize docker --- Dockerfile | 1 + README.md | 17 ++++++++++++++--- docker/entrypoint.sh | 34 ++++++++++++++++++++-------------- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index 59479b6..912fdca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,7 @@ ENV PROXY= ENV DNS=virtual ENV MODE=auto ENV BYPASS_IP= +ENV VERBOSITY=info RUN apt update && apt install -y iproute2 curl && apt clean all diff --git a/README.md b/README.md index db598a7..b83837a 100644 --- a/README.md +++ b/README.md @@ -119,16 +119,27 @@ Next, start a container from the tun2proxy image: ```bash docker run -d \ - -e PROXY=PROXY_TYPE://PROXY_IP:PROXY_PORT \ + -e PROXY=proto://[username[:password]@]host:port \ -v /dev/net/tun:/dev/net/tun \ - --sysctl net.ipv6.conf.all.disable_ipv6=0 \ --sysctl net.ipv6.conf.default.disable_ipv6=0 \ --cap-add NET_ADMIN \ --name tun2proxy \ tun2proxy ``` -You can then provide the running container's network to another worker container by sharing the network namespace: +container env list + +| container env | Default | program option | mean | +| ------------- | ------- | ----------------------- | ------------------------------------------------------------ | +| TUN | tun0 | -t, --tun | Name of the tun interface [default: tun0] | +| PROXY | None | -p, --proxy | Proxy URL in the form proto://[username[:password]@]host:port | +| DNS | virtual | -d, --dns | DNS handling strategy [default: virtual] [possible values: virtual, over-tcp, direct] | +| MODE | auto | -s, --setup | Routing and system setup [possible values: auto] | +| BYPASS_IP | None | -b, --bypass | Public proxy IP used in routing setup which should bypassing the tunnel | +| VERBOSITY | info | -v, --verbosity | Verbosity level [default: info] [possible values: off, error, warn, info, debug, trace] | +| | | | | + +You can then provide the running container's network to another worker container by sharing the network namespace (like kubernetes sidecar): ```bash docker run -it \ diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index f0e9e5c..661380c 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -2,28 +2,34 @@ run() { - if [ -n "$BYPASS_IP" ]; then - BYPASS_IP="--bypass $BYPASS_IP" - fi - - if [ -n "$DNS" ]; then - DNS="--dns $DNS" - fi - - if [ -n "$MODE" ]; then - MODE="--setup $MODE" + if [ -n "$TUN" ]; then + TUN="--tun $TUN" fi if [ -n "$PROXY" ]; then PROXY="--proxy $PROXY" fi - if [ -n "$TUN" ]; then - TUN="--tun $TUN" + if [ -n "$DNS" ]; then + DNS="--dns $DNS" fi - exec tun2proxy $TUN $PROXY $DNS $MODE $BYPASS_IP + if [ -n "$BYPASS_IP" ]; then + BYPASS_IP="--bypass $BYPASS_IP" + fi + + if [ -n "$VERBOSITY" ]; then + VERBOSITY="-v $VERBOSITY" + fi + + if [ -n "$MODE" ]; then + MODE="--setup $MODE" + fi + + echo "Bootstrap ready!! Exec Command: tun2proxy $TUN $PROXY $DNS $VERBOSITY $MODE $BYPASS_IP $@" + + exec tun2proxy $TUN $PROXY $DNS $VERBOSITY $MODE $BYPASS_IP $@ } -run || echo "Runing ERROR!!" +run $@ || echo "Runing ERROR!!" From a9a562029f79cd72d183658b336f85c4754f759f Mon Sep 17 00:00:00 2001 From: "B. Blechschmidt" Date: Tue, 10 Oct 2023 21:04:11 +0200 Subject: [PATCH 05/16] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 9b5214e..80b3987 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) B. Blechschmidt and contributors +Copyright (c) @ssrlive, B. Blechschmidt and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 989c42ee615a644f9f1d0c9681f2a34dec34d0c9 Mon Sep 17 00:00:00 2001 From: ssrlive <30760636+ssrlive@users.noreply.github.com> Date: Mon, 23 Oct 2023 09:44:27 +0800 Subject: [PATCH 06/16] Windows support (#72) --- .github/workflows/publish-exe.yml | 6 +- Cargo.toml | 14 + README.md | 4 +- build.rs | 84 +++++ src/lib.rs | 3 + src/main.rs | 24 +- src/tun2proxy.rs | 84 +++-- src/wintuninterface.rs | 551 ++++++++++++++++++++++++++++++ 8 files changed, 721 insertions(+), 49 deletions(-) create mode 100644 build.rs create mode 100644 src/wintuninterface.rs diff --git a/.github/workflows/publish-exe.yml b/.github/workflows/publish-exe.yml index c376706..7bad5a7 100644 --- a/.github/workflows/publish-exe.yml +++ b/.github/workflows/publish-exe.yml @@ -63,11 +63,11 @@ jobs: cargo build --all-features --release --target ${{ matrix.target }} fi if [[ "${{ matrix.host_os }}" == "windows-latest" ]]; then - powershell Compress-Archive -Path target/${{ matrix.target }}/release/tun2proxy.exe -DestinationPath publishdir/tun2proxy-${{ matrix.target }}.zip + powershell Compress-Archive -Path target/${{ matrix.target }}/release/tun2proxy.exe, README.md, target/${{ matrix.target }}/release/wintun.dll -DestinationPath publishdir/tun2proxy-${{ matrix.target }}.zip elif [[ "${{ matrix.host_os }}" == "macos-latest" ]]; then - zip -j publishdir/tun2proxy-${{ matrix.target }}.zip target/${{ matrix.target }}/release/tun2proxy + zip -j publishdir/tun2proxy-${{ matrix.target }}.zip target/${{ matrix.target }}/release/tun2proxy README.md elif [[ "${{ matrix.host_os }}" == "ubuntu-latest" ]]; then - zip -j publishdir/tun2proxy-${{ matrix.target }}.zip target/${{ matrix.target }}/release/tun2proxy + zip -j publishdir/tun2proxy-${{ matrix.target }}.zip target/${{ matrix.target }}/release/tun2proxy README.md fi - name: Publish diff --git a/Cargo.toml b/Cargo.toml index 193efad..416eb6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,3 +50,17 @@ reqwest = { version = "0.11", default-features = false, features = [ ] } serial_test = "2.0" test-log = "0.2" + +[target.'cfg(target_os="windows")'.dependencies] +rand = "0.8" +windows = { version = "0.51", features = [ + "Win32_Storage_FileSystem", + "Win32_NetworkManagement_IpHelper", + "Win32_NetworkManagement_Ndis", + "Win32_Networking_WinSock", + "Win32_Foundation", +] } +wintun = { git = "https://github.com/ssrlive/wintun.git", branch = "main" } + +[build-dependencies] +serde_json = "1.0" diff --git a/README.md b/README.md index b83837a..cfa1450 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Options: -d, --dns DNS handling strategy [default: virtual] [possible values: virtual, over-tcp, direct] --dns-addr DNS resolver address [default: 8.8.8.8] -6, --ipv6-enabled IPv6 enabled - -s, --setup Routing and system setup [possible values: auto] + -s, --setup Routing and system setup [default: none] [possible values: none, auto] -b, --bypass Public proxy IP used in routing setup which should bypassing the tunnel -v, --verbosity Verbosity level [default: info] [possible values: off, error, warn, info, debug, trace] -h, --help Print help @@ -134,7 +134,7 @@ container env list | TUN | tun0 | -t, --tun | Name of the tun interface [default: tun0] | | PROXY | None | -p, --proxy | Proxy URL in the form proto://[username[:password]@]host:port | | DNS | virtual | -d, --dns | DNS handling strategy [default: virtual] [possible values: virtual, over-tcp, direct] | -| MODE | auto | -s, --setup | Routing and system setup [possible values: auto] | +| MODE | auto | -s, --setup | Routing and system setup [default: none] [possible values: none, auto] | | BYPASS_IP | None | -b, --bypass | Public proxy IP used in routing setup which should bypassing the tunnel | | VERBOSITY | info | -v, --verbosity | Verbosity level [default: info] [possible values: off, error, warn, info, debug, trace] | | | | | | diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..0c9159c --- /dev/null +++ b/build.rs @@ -0,0 +1,84 @@ +fn main() -> Result<(), Box> { + #[cfg(target_os = "windows")] + if let Ok(cargo_target_dir) = get_cargo_target_dir() { + let mut f = std::fs::File::create(cargo_target_dir.join("build.log"))?; + use std::io::Write; + f.write_all(format!("CARGO_TARGET_DIR: '{}'\r\n", cargo_target_dir.display()).as_bytes())?; + + // The wintun crate's root directory + let crate_dir = get_crate_dir("wintun")?; + + // The path to the DLL file, relative to the crate root, depending on the target architecture + let dll_path = get_wintun_bin_relative_path()?; + let src_path = crate_dir.join(dll_path); + + let dst_path = cargo_target_dir.join("wintun.dll"); + + f.write_all(format!("Source path: '{}'\r\n", src_path.display()).as_bytes())?; + f.write_all(format!("Target path: '{}'\r\n", dst_path.display()).as_bytes())?; + + // Copy to the target directory + if let Err(e) = std::fs::copy(src_path, &dst_path) { + f.write_all(format!("Failed to copy 'wintun.dll': {}\r\n", e).as_bytes())?; + } else { + f.write_all(format!("Copied 'wintun.dll' to '{}'\r\n", dst_path.display()).as_bytes())?; + } + } + Ok(()) +} + +#[allow(dead_code)] +fn get_cargo_target_dir() -> Result> { + let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?); + let profile = std::env::var("PROFILE")?; + let mut target_dir = None; + let mut sub_path = out_dir.as_path(); + while let Some(parent) = sub_path.parent() { + if parent.ends_with(&profile) { + target_dir = Some(parent); + break; + } + sub_path = parent; + } + Ok(target_dir.ok_or("not found")?.to_path_buf()) +} + +#[cfg(target_os = "windows")] +fn get_wintun_bin_relative_path() -> Result> { + let dll_path = if cfg!(target_arch = "x86") { + "wintun/bin/x86/wintun.dll" + } else if cfg!(target_arch = "x86_64") { + "wintun/bin/amd64/wintun.dll" + } else if cfg!(target_arch = "arm") { + "wintun/bin/arm/wintun.dll" + } else if cfg!(target_arch = "aarch64") { + "wintun/bin/arm64/wintun.dll" + } else { + return Err("Unsupported architecture".into()); + }; + Ok(dll_path.into()) +} + +#[allow(dead_code)] +fn get_crate_dir(crate_name: &str) -> Result> { + let output = std::process::Command::new("cargo") + .arg("metadata") + .arg("--format-version=1") + .output()?; + + let metadata = serde_json::from_slice::(&output.stdout)?; + let packages = metadata["packages"].as_array().ok_or("packages")?; + + let mut crate_dir = None; + + for package in packages { + let name = package["name"].as_str().ok_or("name")?; + if name == crate_name { + let path = package["manifest_path"].as_str().ok_or("manifest_path")?; + let path = std::path::PathBuf::from(path); + crate_dir = Some(path.parent().ok_or("parent")?.to_path_buf()); + break; + } + } + Ok(crate_dir.ok_or("crate_dir")?) +} diff --git a/src/lib.rs b/src/lib.rs index 537f0c2..00977dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,8 @@ mod socks; mod tun2proxy; mod virtdevice; mod virtdns; +#[cfg(target_os = "windows")] +mod wintuninterface; #[derive(Clone, Debug)] pub struct Proxy { @@ -103,6 +105,7 @@ pub struct Options { dns_addr: Option, ipv6_enabled: bool, bypass: Option, + pub setup: bool, } impl Options { diff --git a/src/main.rs b/src/main.rs index 404c341..8f18e57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,7 +38,7 @@ struct Args { ipv6_enabled: bool, /// Routing and system setup - #[arg(short, long, value_name = "method", value_enum)] + #[arg(short, long, value_name = "method", value_enum, default_value = if cfg!(target_os = "linux") { "none" } else { "auto" })] setup: Option, /// Public proxy IP used in routing setup which should bypassing the tunnel @@ -63,6 +63,7 @@ enum ArgDns { #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] enum ArgSetup { + None, Auto, } @@ -122,21 +123,20 @@ fn main() -> ExitCode { }; options = options.with_bypass(Some(bypass_tun_ip)); + options.setup = args.setup.map(|s| s == ArgSetup::Auto).unwrap_or(false); + let block = || -> Result<(), Error> { #[cfg(target_os = "linux")] - { - let mut setup: Setup; - if args.setup == Some(ArgSetup::Auto) { - let bypass_tun_ip = match args.bypass { - Some(addr) => addr, - None => args.proxy.addr.ip(), - }; - setup = Setup::new(&args.tun, &bypass_tun_ip, get_default_cidrs(), args.bypass.is_some()); + if options.setup { + let bypass_tun_ip = match args.bypass { + Some(addr) => addr, + None => args.proxy.addr.ip(), + }; + let mut setup = Setup::new(&args.tun, &bypass_tun_ip, get_default_cidrs(), args.bypass.is_some()); - setup.configure()?; + setup.configure()?; - setup.drop_privileges()?; - } + setup.drop_privileges()?; } main_entry(&interface, &args.proxy, options)?; diff --git a/src/tun2proxy.rs b/src/tun2proxy.rs index 652631b..09bb7cb 100644 --- a/src/tun2proxy.rs +++ b/src/tun2proxy.rs @@ -1,19 +1,18 @@ #![allow(dead_code)] +#[cfg(target_os = "windows")] +use crate::wintuninterface::{self, NamedPipeSource, WinTunInterface}; use crate::{dns, error::Error, error::Result, virtdevice::VirtualTunDevice, NetworkInterface, Options}; #[cfg(target_family = "unix")] use mio::unix::SourceFd; use mio::{event::Event, net::TcpStream, net::UdpSocket, Events, Interest, Poll, Token}; -#[cfg(not(target_family = "unix"))] -use smoltcp::phy::DeviceCapabilities; #[cfg(any(target_os = "macos", target_os = "ios"))] use smoltcp::phy::RawSocket; #[cfg(any(target_os = "linux", target_os = "android"))] use smoltcp::phy::TunTapInterface; -#[cfg(target_family = "unix")] -use smoltcp::phy::{Device, Medium, RxToken, TxToken}; use smoltcp::{ iface::{Config, Interface, SocketHandle, SocketSet}, + phy::{Device, Medium, RxToken, TxToken}, socket::{tcp, tcp::State, udp, udp::UdpMetadata}, time::Instant, wire::{IpCidr, IpProtocol, Ipv4Packet, Ipv6Packet, TcpPacket, UdpPacket, UDP_HEADER_LEN}, @@ -218,6 +217,8 @@ pub struct TunToProxy<'a> { tun: TunTapInterface, #[cfg(any(target_os = "macos", target_os = "ios"))] tun: RawSocket, + #[cfg(target_os = "windows")] + tun: WinTunInterface, poll: Poll, iface: Interface, connection_map: HashMap, @@ -231,6 +232,10 @@ pub struct TunToProxy<'a> { exit_receiver: mio::unix::pipe::Receiver, #[cfg(target_family = "unix")] exit_trigger: Option, + #[cfg(target_os = "windows")] + exit_receiver: mio::windows::NamedPipe, + #[cfg(target_os = "windows")] + exit_trigger: Option, } impl<'a> TunToProxy<'a> { @@ -247,35 +252,47 @@ impl<'a> TunToProxy<'a> { NetworkInterface::Fd(_fd) => panic!("Not supported"), }; + #[cfg(target_os = "windows")] + let mut tun = match _interface { + NetworkInterface::Named(name) => WinTunInterface::new(name.as_str(), Medium::Ip)?, + }; + + #[cfg(target_os = "windows")] + if options.setup { + tun.setup_config(options.bypass, options.dns_addr)?; + } + let poll = Poll::new()?; #[cfg(target_family = "unix")] poll.registry() .register(&mut SourceFd(&tun.as_raw_fd()), TUN_TOKEN, Interest::READABLE)?; - #[cfg(target_family = "unix")] - let (mut exit_trigger, mut exit_receiver) = mio::unix::pipe::new()?; + #[cfg(target_os = "windows")] + { + let interest = Interest::READABLE | Interest::WRITABLE; + poll.registry().register(&mut tun, TUN_TOKEN, interest)?; + let mut pipe = NamedPipeSource(tun.pipe_client()); + poll.registry().register(&mut pipe, PIPE_TOKEN, interest)?; + } #[cfg(target_family = "unix")] + let (mut exit_trigger, mut exit_receiver) = mio::unix::pipe::new()?; + #[cfg(target_family = "windows")] + let (mut exit_trigger, mut exit_receiver) = wintuninterface::pipe()?; + poll.registry() .register(&mut exit_trigger, EXIT_TRIGGER_TOKEN, Interest::WRITABLE)?; - #[cfg(target_family = "unix")] poll.registry() .register(&mut exit_receiver, EXIT_TOKEN, Interest::READABLE)?; - #[cfg(target_family = "unix")] let config = match tun.capabilities().medium { Medium::Ethernet => Config::new(smoltcp::wire::EthernetAddress([0x02, 0, 0, 0, 0, 0x01]).into()), Medium::Ip => Config::new(smoltcp::wire::HardwareAddress::Ip), Medium::Ieee802154 => todo!(), }; - #[cfg(not(target_family = "unix"))] - let config = Config::new(smoltcp::wire::HardwareAddress::Ip); - #[cfg(target_family = "unix")] let mut device = VirtualTunDevice::new(tun.capabilities()); - #[cfg(not(target_family = "unix"))] - let mut device = VirtualTunDevice::new(DeviceCapabilities::default()); let gateway4: Ipv4Addr = Ipv4Addr::from_str("0.0.0.1")?; let gateway6: Ipv6Addr = Ipv6Addr::from_str("::1")?; @@ -289,7 +306,6 @@ impl<'a> TunToProxy<'a> { iface.set_any_ip(true); let tun = Self { - #[cfg(target_family = "unix")] tun, poll, iface, @@ -300,9 +316,7 @@ impl<'a> TunToProxy<'a> { device, options, write_sockets: HashSet::default(), - #[cfg(target_family = "unix")] exit_receiver, - #[cfg(target_family = "unix")] exit_trigger: Some(exit_trigger), }; Ok(tun) @@ -325,7 +339,6 @@ impl<'a> TunToProxy<'a> { let _slice = vec.as_slice(); // TODO: Actual write. Replace. - #[cfg(target_family = "unix")] self.tun .transmit(Instant::now()) .ok_or("tx token not available")? @@ -773,17 +786,24 @@ impl<'a> TunToProxy<'a> { proxy_handler: Box, udp_associate: bool, ) -> Result { + #[cfg(any(target_os = "linux", target_os = "android"))] let mut socket = tcp::Socket::new( tcp::SocketBuffer::new(vec![0; 1024 * 128]), tcp::SocketBuffer::new(vec![0; 1024 * 128]), ); + #[cfg(any(target_os = "ios", target_os = "macos", target_os = "windows"))] + let mut socket = tcp::Socket::new( + // TODO: Look into how the buffer size affects IP header length and fragmentation + tcp::SocketBuffer::new(vec![0; 1024 * 2]), + tcp::SocketBuffer::new(vec![0; 1024 * 2]), + ); socket.set_ack_delay(None); socket.listen(dst)?; let handle = self.sockets.add(socket); let mut client = TcpStream::connect(server_addr)?; let token = self.new_token(); - let i = Interest::READABLE; + let i = Interest::READABLE | Interest::WRITABLE; self.poll.registry().register(&mut client, token, i)?; let expiry = if udp_associate { @@ -808,7 +828,7 @@ impl<'a> TunToProxy<'a> { proxy_handler, close_state: 0, wait_read: true, - wait_write: false, + wait_write: true, udp_acco_expiry: expiry, udp_socket, udp_token, @@ -876,8 +896,8 @@ impl<'a> TunToProxy<'a> { state.wait_write = true; Self::update_mio_socket_interest(&mut self.poll, state)?; } - Err(error) => { - return Err(error.into()); + Err(_) => { + return Ok(()); } } } @@ -921,15 +941,23 @@ impl<'a> TunToProxy<'a> { fn tun_event(&mut self, event: &Event) -> Result<(), Error> { if event.is_readable() { - #[cfg(target_family = "unix")] while let Some((rx_token, _)) = self.tun.receive(Instant::now()) { rx_token.consume(|frame| self.receive_tun(frame))?; } } + #[cfg(target_os = "windows")] + if event.is_writable() { + // log::trace!("Tun writable"); + let tx_token = self.tun.transmit(Instant::now()).ok_or("tx token not available")?; + // Just consume the cached packets, do nothing else. + tx_token.consume(0, |_buf| {}); + } Ok(()) } fn pipe_event(&mut self, _event: &Event) -> Result<(), Error> { + #[cfg(target_os = "windows")] + self.tun.pipe_client_event(_event)?; Ok(()) } @@ -1132,7 +1160,7 @@ impl<'a> TunToProxy<'a> { Ok(()) } - #[cfg(any(target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] fn prepare_exiting_signal_trigger(&mut self) -> Result<()> { let mut exit_trigger = self.exit_trigger.take().ok_or("Already running")?; ctrlc::set_handler(move || { @@ -1163,7 +1191,7 @@ impl<'a> TunToProxy<'a> { } pub fn run(&mut self) -> Result<(), Error> { - #[cfg(any(target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] self.prepare_exiting_signal_trigger()?; let mut events = Events::with_capacity(1024); @@ -1183,7 +1211,6 @@ impl<'a> TunToProxy<'a> { } } EXIT_TRIGGER_TOKEN => { - #[cfg(target_family = "unix")] log::trace!("Exiting trigger is ready, {:?}", self.exit_trigger); } TUN_TOKEN => self.tun_event(event)?, @@ -1197,7 +1224,6 @@ impl<'a> TunToProxy<'a> { } } - #[cfg(target_family = "unix")] fn exiting_event_handler(&mut self) -> Result { let mut buffer = vec![0; 100]; match self.exit_receiver.read(&mut buffer) { @@ -1214,12 +1240,6 @@ impl<'a> TunToProxy<'a> { } } - #[cfg(target_os = "windows")] - fn exiting_event_handler(&mut self) -> Result { - Ok(true) - } - - #[cfg(target_family = "unix")] pub fn shutdown(&mut self) -> Result<(), Error> { log::debug!("Shutdown tun2proxy..."); _ = self.exit_trigger.as_mut().ok_or("Already triggered")?.write(b"EXIT")?; diff --git a/src/wintuninterface.rs b/src/wintuninterface.rs new file mode 100644 index 0000000..ab4e4eb --- /dev/null +++ b/src/wintuninterface.rs @@ -0,0 +1,551 @@ +use mio::{event, windows::NamedPipe, Interest, Registry, Token}; +use smoltcp::{ + phy::{self, Device, DeviceCapabilities, Medium}, + time::Instant, +}; +use std::{ + cell::RefCell, + fs::OpenOptions, + io::{self, Read, Write}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + os::windows::prelude::{FromRawHandle, IntoRawHandle, OpenOptionsExt}, + rc::Rc, + sync::{Arc, Mutex}, + thread::JoinHandle, + vec::Vec, +}; +use windows::{ + core::{GUID, PWSTR}, + Win32::{ + Foundation::{ERROR_BUFFER_OVERFLOW, WIN32_ERROR}, + NetworkManagement::{ + IpHelper::{ + GetAdaptersAddresses, SetInterfaceDnsSettings, DNS_INTERFACE_SETTINGS, DNS_INTERFACE_SETTINGS_VERSION1, + DNS_SETTING_NAMESERVER, GAA_FLAG_INCLUDE_GATEWAYS, GAA_FLAG_INCLUDE_PREFIX, IF_TYPE_ETHERNET_CSMACD, + IF_TYPE_IEEE80211, IP_ADAPTER_ADDRESSES_LH, + }, + Ndis::IfOperStatusUp, + }, + Networking::WinSock::{AF_INET, AF_INET6, AF_UNSPEC, SOCKADDR, SOCKADDR_IN, SOCKADDR_IN6}, + Storage::FileSystem::FILE_FLAG_OVERLAPPED, + }, +}; + +fn server() -> io::Result<(NamedPipe, String)> { + use rand::Rng; + let num: u64 = rand::thread_rng().gen(); + let name = format!(r"\\.\pipe\my-pipe-{}", num); + let pipe = NamedPipe::new(&name)?; + Ok((pipe, name)) +} + +fn client(name: &str) -> io::Result { + let mut opts = OpenOptions::new(); + opts.read(true).write(true).custom_flags(FILE_FLAG_OVERLAPPED.0); + let file = opts.open(name)?; + unsafe { Ok(NamedPipe::from_raw_handle(file.into_raw_handle())) } +} + +pub(crate) fn pipe() -> io::Result<(NamedPipe, NamedPipe)> { + let (pipe, name) = server()?; + Ok((pipe, client(&name)?)) +} + +/// A virtual TUN (IP) interface. +pub struct WinTunInterface { + wintun_session: Arc, + mtu: usize, + medium: Medium, + pipe_server: Rc>, + pipe_server_cache: Rc>>, + pipe_client: Arc>, + pipe_client_cache: Arc>>, + wintun_reader_thread: Option>, + old_gateway: Option, +} + +impl event::Source for WinTunInterface { + fn register(&mut self, registry: &Registry, token: Token, interests: Interest) -> io::Result<()> { + self.pipe_server.borrow_mut().register(registry, token, interests)?; + Ok(()) + } + + fn reregister(&mut self, registry: &Registry, token: Token, interests: Interest) -> io::Result<()> { + self.pipe_server.borrow_mut().reregister(registry, token, interests)?; + Ok(()) + } + + fn deregister(&mut self, registry: &Registry) -> io::Result<()> { + self.pipe_server.borrow_mut().deregister(registry)?; + Ok(()) + } +} + +impl WinTunInterface { + pub fn new(tun_name: &str, medium: Medium) -> io::Result { + let wintun = unsafe { wintun::load() }.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let guid = 324435345345345345_u128; + let adapter = match wintun::Adapter::open(&wintun, tun_name) { + Ok(a) => a, + Err(_) => wintun::Adapter::create(&wintun, tun_name, tun_name, Some(guid)) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?, + }; + + let session = adapter + .start_session(wintun::MAX_RING_CAPACITY) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let wintun_session = Arc::new(session); + + let (pipe_server, pipe_client) = pipe()?; + + let pipe_client = Arc::new(Mutex::new(pipe_client)); + let pipe_client_cache = Arc::new(Mutex::new(Vec::new())); + + let mtu = adapter.get_mtu().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + let reader_session = wintun_session.clone(); + let pipe_client_clone = pipe_client.clone(); + let pipe_client_cache_clone = pipe_client_cache.clone(); + let reader_thread = std::thread::spawn(move || { + let block = || -> Result<(), Box> { + loop { + // Take the old data from pipe_client_cache and append the new data + let cached_data = pipe_client_cache_clone.lock()?.drain(..).collect::>(); + let bytes = if cached_data.len() >= mtu { + // if the cached data is greater than mtu, then sleep 1ms and return the data + std::thread::sleep(std::time::Duration::from_millis(1)); + cached_data + } else { + // read data from tunnel interface + let packet = reader_session.receive_blocking()?; + let bytes = packet.bytes().to_vec(); + // and append to the end of cached data + cached_data.into_iter().chain(bytes).collect::>() + }; + + if bytes.is_empty() { + continue; + } + let len = bytes.len(); + + // write data to named pipe_server + let result = { pipe_client_clone.lock()?.write(&bytes) }; + match result { + Ok(n) => { + if n < len { + log::trace!("Wintun pipe_client write data {} less than buffer {}", n, len); + pipe_client_cache_clone.lock()?.extend_from_slice(&bytes[n..]); + } + } + Err(err) if err.kind() == io::ErrorKind::WouldBlock => { + log::trace!("Wintun pipe_client write WouldBlock (1) len {}", len); + pipe_client_cache_clone.lock()?.extend_from_slice(&bytes); + } + Err(err) => log::error!("Wintun pipe_client write data len {} error \"{}\"", len, err), + } + } + }; + if let Err(err) = block() { + log::trace!("Reader {}", err); + } + }); + + Ok(WinTunInterface { + wintun_session, + mtu, + medium, + pipe_server: Rc::new(RefCell::new(pipe_server)), + pipe_server_cache: Rc::new(RefCell::new(Vec::new())), + pipe_client, + pipe_client_cache, + wintun_reader_thread: Some(reader_thread), + old_gateway: None, + }) + } + + pub fn pipe_client(&self) -> Arc> { + self.pipe_client.clone() + } + + pub fn pipe_client_event(&self, event: &event::Event) -> Result<(), io::Error> { + if event.is_readable() { + self.pipe_client_event_readable() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + } else if event.is_writable() { + self.pipe_client_event_writable() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + } + Ok(()) + } + + fn pipe_client_event_readable(&self) -> Result<(), Box> { + let mut reader = self.pipe_client.lock()?; + let mut buffer = vec![0; self.mtu]; + loop { + // some data arieved to pipe_client from pipe_server + match reader.read(&mut buffer[..]) { + Ok(len) => match self.wintun_session.allocate_send_packet(len as u16) { + Ok(mut write_pack) => { + write_pack.bytes_mut().copy_from_slice(&buffer[..len]); + // write data to tunnel interface + self.wintun_session.send_packet(write_pack); + } + Err(err) => { + log::error!("Wintun: failed to allocate send packet: {}", err); + } + }, + Err(err) if err.kind() == io::ErrorKind::WouldBlock => break, + Err(err) if err.kind() == io::ErrorKind::Interrupted => continue, + Err(err) => return Err(err.into()), + } + } + Ok(()) + } + + fn pipe_client_event_writable(&self) -> Result<(), Box> { + let cache = self.pipe_client_cache.lock()?.drain(..).collect::>(); + if cache.is_empty() { + return Ok(()); + } + let len = cache.len(); + let result = self.pipe_client.lock()?.write(&cache[..]); + match result { + Ok(n) => { + if n < len { + log::trace!("Wintun pipe_client write data {} less than buffer {}", n, len); + self.pipe_client_cache.lock()?.extend_from_slice(&cache[n..]); + } + } + Err(err) if err.kind() == io::ErrorKind::WouldBlock => { + log::trace!("Wintun pipe_client write WouldBlock (2) len {}", len); + self.pipe_client_cache.lock()?.extend_from_slice(&cache); + } + Err(err) => log::error!("Wintun pipe_client write data len {} error \"{}\"", len, err), + } + Ok(()) + } + + pub fn setup_config(&mut self, bypass_ip: Option, dns_addr: Option) -> Result<(), io::Error> { + let adapter = self.wintun_session.get_adapter(); + + // Setup the adapter's address/mask/gateway + let address = "10.1.0.33".parse::().unwrap(); + let mask = "255.255.255.0".parse::().unwrap(); + let gateway = "10.1.0.1".parse::().unwrap(); + adapter + .set_network_addresses_tuple(address, mask, Some(gateway)) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + // 1. Setup the adapter's DNS + let interface = GUID::from(adapter.get_guid()); + let dns = dns_addr.unwrap_or("8.8.8.8".parse::().unwrap()); + let dns2 = "8.8.4.4".parse::().unwrap(); + set_interface_dns_settings(interface, &[dns, dns2])?; + + // 2. Route all traffic to the adapter, here the destination is adapter's gateway + // command: `route add 0.0.0.0 mask 0.0.0.0 10.1.0.1 metric 6` + let unspecified = Ipv4Addr::UNSPECIFIED.to_string(); + let gateway = gateway.to_string(); + let args = &["add", &unspecified, "mask", &unspecified, &gateway, "metric", "6"]; + run_command("route", args)?; + log::info!("route {:?}", args); + + let old_gateways = get_active_network_interface_gateways()?; + // find ipv4 gateway address, or error return + let old_gateway = old_gateways + .iter() + .find(|addr| addr.is_ipv4()) + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "No ipv4 gateway found"))?; + let old_gateway = old_gateway.ip(); + self.old_gateway = Some(old_gateway); + + // 3. route the bypass ip to the old gateway + // command: `route add bypass_ip old_gateway metric 1` + if let Some(bypass_ip) = bypass_ip { + let args = &["add", &bypass_ip.to_string(), &old_gateway.to_string(), "metric", "1"]; + run_command("route", args)?; + log::info!("route {:?}", args); + } + + Ok(()) + } + + pub fn restore_config(&mut self) -> Result<(), io::Error> { + if self.old_gateway.is_none() { + return Ok(()); + } + let unspecified = Ipv4Addr::UNSPECIFIED.to_string(); + + // 1. Remove current adapter's route + // command: `route delete 0.0.0.0 mask 0.0.0.0` + let args = &["delete", &unspecified, "mask", &unspecified]; + run_command("route", args)?; + + // 2. Add back the old gateway route + // command: `route add 0.0.0.0 mask 0.0.0.0 old_gateway metric 200` + let old_gateway = self.old_gateway.take().unwrap().to_string(); + let args = &["add", &unspecified, "mask", &unspecified, &old_gateway, "metric", "200"]; + run_command("route", args)?; + + Ok(()) + } +} + +impl Drop for WinTunInterface { + fn drop(&mut self) { + if let Err(e) = self.restore_config() { + log::error!("Faild to unsetup config: {}", e); + } + if let Err(e) = self.wintun_session.shutdown() { + log::error!("phy: failed to shutdown interface: {}", e); + } + if let Some(thread) = self.wintun_reader_thread.take() { + if let Err(e) = thread.join() { + log::error!("phy: failed to join reader thread: {:?}", e); + } + } + } +} + +impl Device for WinTunInterface { + type RxToken<'a> = RxToken; + type TxToken<'a> = TxToken; + + fn capabilities(&self) -> DeviceCapabilities { + let mut v = DeviceCapabilities::default(); + v.max_transmission_unit = self.mtu; + v.medium = self.medium; + v + } + + fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { + let mut buffer = vec![0; self.mtu]; + match self.pipe_server.borrow_mut().read(&mut buffer[..]) { + Ok(size) => { + buffer.resize(size, 0); + let rx = RxToken { buffer }; + let tx = TxToken { + pipe_server: self.pipe_server.clone(), + pipe_server_cache: self.pipe_server_cache.clone(), + }; + Some((rx, tx)) + } + Err(err) if err.kind() == io::ErrorKind::WouldBlock => None, + Err(err) => panic!("{}", err), + } + } + + fn transmit(&mut self, _timestamp: Instant) -> Option> { + Some(TxToken { + pipe_server: self.pipe_server.clone(), + pipe_server_cache: self.pipe_server_cache.clone(), + }) + } +} + +#[doc(hidden)] +pub struct RxToken { + buffer: Vec, +} + +impl phy::RxToken for RxToken { + fn consume(mut self, f: F) -> R + where + F: FnOnce(&mut [u8]) -> R, + { + f(&mut self.buffer[..]) + } +} + +#[doc(hidden)] +pub struct TxToken { + pipe_server: Rc>, + pipe_server_cache: Rc>>, +} + +impl phy::TxToken for TxToken { + fn consume(self, len: usize, f: F) -> R + where + F: FnOnce(&mut [u8]) -> R, + { + let mut buffer = vec![0; len]; + let result = f(&mut buffer); + + let buffer = self + .pipe_server_cache + .borrow_mut() + .drain(..) + .chain(buffer) + .collect::>(); + if buffer.is_empty() { + // log::trace!("Wintun TxToken (pipe_server) is empty"); + return result; + } + let len = buffer.len(); + + match self.pipe_server.borrow_mut().write(&buffer[..]) { + Ok(n) => { + if n < len { + log::trace!("Wintun TxToken (pipe_server) sent {} less than buffer len {}", n, len); + self.pipe_server_cache.borrow_mut().extend_from_slice(&buffer[n..]); + } + } + Err(err) if err.kind() == io::ErrorKind::WouldBlock => { + self.pipe_server_cache.borrow_mut().extend_from_slice(&buffer[..]); + log::trace!("Wintun TxToken (pipe_server) WouldBlock data len: {}", len) + } + Err(err) => log::error!("Wintun TxToken (pipe_server) len {} error \"{}\"", len, err), + } + result + } +} + +pub struct NamedPipeSource(pub Arc>); + +impl event::Source for NamedPipeSource { + fn register(&mut self, registry: &Registry, token: Token, interests: Interest) -> io::Result<()> { + self.0 + .lock() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))? + .register(registry, token, interests) + } + + fn reregister(&mut self, registry: &Registry, token: Token, interests: Interest) -> io::Result<()> { + self.0 + .lock() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))? + .reregister(registry, token, interests) + } + + fn deregister(&mut self, registry: &Registry) -> io::Result<()> { + self.0 + .lock() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))? + .deregister(registry) + } +} + +pub(crate) fn run_command(command: &str, args: &[&str]) -> io::Result<()> { + let out = std::process::Command::new(command).args(args).output()?; + if !out.status.success() { + let err = String::from_utf8_lossy(if out.stderr.is_empty() { + &out.stdout + } else { + &out.stderr + }); + let info = format!("{} failed with: \"{}\"", command, err); + return Err(std::io::Error::new(std::io::ErrorKind::Other, info)); + } + Ok(()) +} + +pub(crate) fn set_interface_dns_settings(interface: GUID, dns: &[IpAddr]) -> io::Result<()> { + // format L"1.1.1.1 8.8.8.8", or L"1.1.1.1,8.8.8.8". + let dns = dns.iter().map(|ip| ip.to_string()).collect::>().join(","); + let dns = dns.encode_utf16().chain(std::iter::once(0)).collect::>(); + + let settings = DNS_INTERFACE_SETTINGS { + Version: DNS_INTERFACE_SETTINGS_VERSION1, + Flags: DNS_SETTING_NAMESERVER as _, + NameServer: PWSTR(dns.as_ptr() as _), + ..DNS_INTERFACE_SETTINGS::default() + }; + + unsafe { SetInterfaceDnsSettings(interface, &settings as *const _)? }; + Ok(()) +} + +pub(crate) fn get_active_network_interface_gateways() -> io::Result> { + let mut addrs = vec![]; + get_adapters_addresses(|adapter| { + if adapter.OperStatus == IfOperStatusUp + && [IF_TYPE_ETHERNET_CSMACD, IF_TYPE_IEEE80211].contains(&adapter.IfType) + { + let mut current_gateway = adapter.FirstGatewayAddress; + while !current_gateway.is_null() { + let gateway = unsafe { &*current_gateway }; + { + let sockaddr_ptr = gateway.Address.lpSockaddr; + let sockaddr = unsafe { &*(sockaddr_ptr as *const SOCKADDR) }; + let a = unsafe { sockaddr_to_socket_addr(sockaddr) }?; + addrs.push(a); + } + current_gateway = gateway.Next; + } + } + Ok(()) + })?; + Ok(addrs) +} + +pub(crate) fn get_adapters_addresses(mut callback: F) -> io::Result<()> +where + F: FnMut(IP_ADAPTER_ADDRESSES_LH) -> io::Result<()>, +{ + let mut size = 0; + let flags = GAA_FLAG_INCLUDE_PREFIX | GAA_FLAG_INCLUDE_GATEWAYS; + let family = AF_UNSPEC.0 as u32; + + // Make an initial call to GetAdaptersAddresses to get the + // size needed into the size variable + let result = unsafe { GetAdaptersAddresses(family, flags, None, None, &mut size) }; + + if WIN32_ERROR(result) != ERROR_BUFFER_OVERFLOW { + WIN32_ERROR(result).ok()?; + } + // Allocate memory for the buffer + let mut addresses: Vec = vec![0; (size + 4) as usize]; + + // Make a second call to GetAdaptersAddresses to get the actual data we want + let result = unsafe { + let addr = Some(addresses.as_mut_ptr() as *mut IP_ADAPTER_ADDRESSES_LH); + GetAdaptersAddresses(family, flags, None, addr, &mut size) + }; + + WIN32_ERROR(result).ok()?; + + // If successful, output some information from the data we received + let mut current_addresses = addresses.as_ptr() as *const IP_ADAPTER_ADDRESSES_LH; + while !current_addresses.is_null() { + unsafe { + callback(*current_addresses)?; + current_addresses = (*current_addresses).Next; + } + } + Ok(()) +} + +pub(crate) unsafe fn sockaddr_to_socket_addr(sock_addr: *const SOCKADDR) -> io::Result { + let address = match (*sock_addr).sa_family { + AF_INET => sockaddr_in_to_socket_addr(&*(sock_addr as *const SOCKADDR_IN)), + AF_INET6 => sockaddr_in6_to_socket_addr(&*(sock_addr as *const SOCKADDR_IN6)), + _ => return Err(io::Error::new(io::ErrorKind::Other, "Unsupported address type")), + }; + Ok(address) +} + +pub(crate) unsafe fn sockaddr_in_to_socket_addr(sockaddr_in: &SOCKADDR_IN) -> SocketAddr { + let ip = Ipv4Addr::new( + sockaddr_in.sin_addr.S_un.S_un_b.s_b1, + sockaddr_in.sin_addr.S_un.S_un_b.s_b2, + sockaddr_in.sin_addr.S_un.S_un_b.s_b3, + sockaddr_in.sin_addr.S_un.S_un_b.s_b4, + ); + let port = u16::from_be(sockaddr_in.sin_port); + SocketAddr::new(ip.into(), port) +} + +pub(crate) unsafe fn sockaddr_in6_to_socket_addr(sockaddr_in6: &SOCKADDR_IN6) -> SocketAddr { + let ip = IpAddr::V6(Ipv6Addr::new( + u16::from_be(sockaddr_in6.sin6_addr.u.Word[0]), + u16::from_be(sockaddr_in6.sin6_addr.u.Word[1]), + u16::from_be(sockaddr_in6.sin6_addr.u.Word[2]), + u16::from_be(sockaddr_in6.sin6_addr.u.Word[3]), + u16::from_be(sockaddr_in6.sin6_addr.u.Word[4]), + u16::from_be(sockaddr_in6.sin6_addr.u.Word[5]), + u16::from_be(sockaddr_in6.sin6_addr.u.Word[6]), + u16::from_be(sockaddr_in6.sin6_addr.u.Word[7]), + )); + let port = u16::from_be(sockaddr_in6.sin6_port); + SocketAddr::new(ip, port) +} From c6f9610eb320e6e0b9721e885da6f58619cba46a Mon Sep 17 00:00:00 2001 From: ssrlive <30760636+ssrlive@users.noreply.github.com> Date: Mon, 23 Oct 2023 10:03:35 +0800 Subject: [PATCH 07/16] Bump version 0.1.9 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 416eb6c..7fa53cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ authors = ["B. Blechschmidt"] edition = "2021" name = "tun2proxy" -version = "0.1.8" +version = "0.1.9" [lib] crate-type = ["cdylib", "lib"] From 9b27dd2df2ecaa0ed21c6c1e83883096e52b8572 Mon Sep 17 00:00:00 2001 From: ssrlive <30760636+ssrlive@users.noreply.github.com> Date: Mon, 23 Oct 2023 14:49:31 +0800 Subject: [PATCH 08/16] refine code --- src/main.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8f18e57..c680e85 100644 --- a/src/main.rs +++ b/src/main.rs @@ -128,14 +128,8 @@ fn main() -> ExitCode { let block = || -> Result<(), Error> { #[cfg(target_os = "linux")] if options.setup { - let bypass_tun_ip = match args.bypass { - Some(addr) => addr, - None => args.proxy.addr.ip(), - }; let mut setup = Setup::new(&args.tun, &bypass_tun_ip, get_default_cidrs(), args.bypass.is_some()); - setup.configure()?; - setup.drop_privileges()?; } From e08a0f683dd6cb7d5af2fb4c27ab9282992f51e7 Mon Sep 17 00:00:00 2001 From: "B. Blechschmidt" Date: Sun, 29 Oct 2023 23:01:06 +0100 Subject: [PATCH 09/16] Allow multiple bypass IP addresses/CIDRs in routing setup See issue #73. --- Dockerfile | 13 ++------ README.md | 18 ++--------- docker/entrypoint.sh | 35 -------------------- src/lib.rs | 10 ++++-- src/main.rs | 38 ++++++++++++++-------- src/setup.rs | 73 +++++++++++++++++++++--------------------- src/tun2proxy.rs | 2 +- src/util.rs | 22 +++++++++++++ src/wintuninterface.rs | 9 ++++-- tests/proxy.rs | 15 ++++++--- 10 files changed, 112 insertions(+), 123 deletions(-) delete mode 100755 docker/entrypoint.sh create mode 100644 src/util.rs diff --git a/Dockerfile b/Dockerfile index 912fdca..114ef12 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,18 +12,9 @@ RUN cargo build --release --target x86_64-unknown-linux-gnu ## Final image #################################################################################################### FROM ubuntu:latest -WORKDIR /app -ENV TUN=tun0 -ENV PROXY= -ENV DNS=virtual -ENV MODE=auto -ENV BYPASS_IP= -ENV VERBOSITY=info - -RUN apt update && apt install -y iproute2 curl && apt clean all +RUN apt update && apt install -y iproute2 && apt clean all COPY --from=builder /worker/target/x86_64-unknown-linux-gnu/release/tun2proxy /usr/bin/tun2proxy -COPY --from=builder /worker/docker/entrypoint.sh /app -ENTRYPOINT ["/app/entrypoint.sh"] +ENTRYPOINT ["/usr/bin/tun2proxy", "--setup", "auto"] diff --git a/README.md b/README.md index cfa1450..78fb403 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Options: --dns-addr DNS resolver address [default: 8.8.8.8] -6, --ipv6-enabled IPv6 enabled -s, --setup Routing and system setup [default: none] [possible values: none, auto] - -b, --bypass Public proxy IP used in routing setup which should bypassing the tunnel + -b, --bypass IPs and CIDRs used in routing setup which should bypass the tunnel -v, --verbosity Verbosity level [default: info] [possible values: off, error, warn, info, debug, trace] -h, --help Print help -V, --version Print version @@ -119,31 +119,17 @@ Next, start a container from the tun2proxy image: ```bash docker run -d \ - -e PROXY=proto://[username[:password]@]host:port \ -v /dev/net/tun:/dev/net/tun \ --sysctl net.ipv6.conf.default.disable_ipv6=0 \ --cap-add NET_ADMIN \ --name tun2proxy \ - tun2proxy + tun2proxy --proxy proto://[username[:password]@]host:port ``` -container env list - -| container env | Default | program option | mean | -| ------------- | ------- | ----------------------- | ------------------------------------------------------------ | -| TUN | tun0 | -t, --tun | Name of the tun interface [default: tun0] | -| PROXY | None | -p, --proxy | Proxy URL in the form proto://[username[:password]@]host:port | -| DNS | virtual | -d, --dns | DNS handling strategy [default: virtual] [possible values: virtual, over-tcp, direct] | -| MODE | auto | -s, --setup | Routing and system setup [default: none] [possible values: none, auto] | -| BYPASS_IP | None | -b, --bypass | Public proxy IP used in routing setup which should bypassing the tunnel | -| VERBOSITY | info | -v, --verbosity | Verbosity level [default: info] [possible values: off, error, warn, info, debug, trace] | -| | | | | - You can then provide the running container's network to another worker container by sharing the network namespace (like kubernetes sidecar): ```bash docker run -it \ - -d \ --network "container:tun2proxy" \ ubuntu:latest ``` diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh deleted file mode 100755 index 661380c..0000000 --- a/docker/entrypoint.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - - -run() { - if [ -n "$TUN" ]; then - TUN="--tun $TUN" - fi - - if [ -n "$PROXY" ]; then - PROXY="--proxy $PROXY" - fi - - if [ -n "$DNS" ]; then - DNS="--dns $DNS" - fi - - if [ -n "$BYPASS_IP" ]; then - BYPASS_IP="--bypass $BYPASS_IP" - fi - - if [ -n "$VERBOSITY" ]; then - VERBOSITY="-v $VERBOSITY" - fi - - if [ -n "$MODE" ]; then - MODE="--setup $MODE" - fi - - echo "Bootstrap ready!! Exec Command: tun2proxy $TUN $PROXY $DNS $VERBOSITY $MODE $BYPASS_IP $@" - - exec tun2proxy $TUN $PROXY $DNS $VERBOSITY $MODE $BYPASS_IP $@ -} - - -run $@ || echo "Runing ERROR!!" diff --git a/src/lib.rs b/src/lib.rs index 00977dd..285b5ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ use crate::{ socks::SocksProxyManager, tun2proxy::{ConnectionManager, TunToProxy}, }; +use smoltcp::wire::IpCidr; use socks5_impl::protocol::UserKey; use std::{ net::{SocketAddr, ToSocketAddrs}, @@ -17,6 +18,7 @@ mod http; pub mod setup; mod socks; mod tun2proxy; +pub mod util; mod virtdevice; mod virtdns; #[cfg(target_os = "windows")] @@ -104,8 +106,8 @@ pub struct Options { dns_over_tcp: bool, dns_addr: Option, ipv6_enabled: bool, - bypass: Option, pub setup: bool, + bypass: Vec, } impl Options { @@ -140,8 +142,10 @@ impl Options { self } - pub fn with_bypass(mut self, ip: Option) -> Self { - self.bypass = ip; + pub fn with_bypass_ips<'a>(mut self, bypass_ips: impl IntoIterator) -> Self { + for bypass_ip in bypass_ips { + self.bypass.push(*bypass_ip); + } self } } diff --git a/src/main.rs b/src/main.rs index c680e85..06d42c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ use clap::Parser; +use smoltcp::wire::IpCidr; use std::{net::IpAddr, process::ExitCode}; +use tun2proxy::util::str_to_cidr; use tun2proxy::{error::Error, main_entry, NetworkInterface, Options, Proxy}; #[cfg(target_os = "linux")] @@ -41,9 +43,9 @@ struct Args { #[arg(short, long, value_name = "method", value_enum, default_value = if cfg!(target_os = "linux") { "none" } else { "auto" })] setup: Option, - /// Public proxy IP used in routing setup which should bypassing the tunnel - #[arg(short, long, value_name = "IP")] - bypass: Option, + /// IPs used in routing setup which should bypass the tunnel + #[arg(short, long, value_name = "IP|CIDR")] + bypass: Vec, /// Verbosity level #[arg(short, long, value_name = "level", value_enum, default_value = "info")] @@ -53,7 +55,7 @@ struct Args { /// DNS query handling strategy /// - Virtual: Intercept DNS queries and resolve them locally with a fake IP address /// - OverTcp: Use TCP to send DNS queries to the DNS server -/// - Direct: Looks as general UDP traffic but change the destination to the DNS server +/// - Direct: Do not handle DNS by relying on DNS server bypassing #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] enum ArgDns { Virtual, @@ -117,20 +119,28 @@ fn main() -> ExitCode { } }; - let bypass_tun_ip = match args.bypass { - Some(addr) => addr, - None => args.proxy.addr.ip(), - }; - options = options.with_bypass(Some(bypass_tun_ip)); - options.setup = args.setup.map(|s| s == ArgSetup::Auto).unwrap_or(false); let block = || -> Result<(), Error> { + let mut bypass_ips = Vec::::new(); + for cidr_str in args.bypass { + bypass_ips.push(str_to_cidr(&cidr_str)?); + } + if bypass_ips.is_empty() { + let prefix_len = if args.proxy.addr.ip().is_ipv6() { 128 } else { 32 }; + bypass_ips.push(IpCidr::new(args.proxy.addr.ip().into(), prefix_len)) + } + + options = options.with_bypass_ips(&bypass_ips); + #[cfg(target_os = "linux")] - if options.setup { - let mut setup = Setup::new(&args.tun, &bypass_tun_ip, get_default_cidrs(), args.bypass.is_some()); - setup.configure()?; - setup.drop_privileges()?; + { + let mut setup: Setup; + if options.setup { + setup = Setup::new(&args.tun, bypass_ips, get_default_cidrs()); + setup.configure()?; + setup.drop_privileges()?; + } } main_entry(&interface, &args.proxy, options)?; diff --git a/src/setup.rs b/src/setup.rs index 51e004f..228653b 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -8,7 +8,7 @@ use std::{ ffi::OsStr, fs, io::BufRead, - net::{IpAddr, Ipv4Addr, Ipv6Addr}, + net::{Ipv4Addr, Ipv6Addr}, os::unix::io::RawFd, process::{Command, Output}, str::FromStr, @@ -17,11 +17,10 @@ use std::{ #[derive(Clone)] pub struct Setup { routes: Vec, - tunnel_bypass_addr: IpAddr, - allow_private: bool, + tunnel_bypass_addrs: Vec, tun: String, set_up: bool, - delete_proxy_route: bool, + delete_proxy_routes: Vec, child: libc::pid_t, unmount_resolvconf: bool, restore_resolvconf_data: Option>, @@ -76,35 +75,41 @@ where impl Setup { pub fn new( tun: impl Into, - tunnel_bypass_addr: &IpAddr, + tunnel_bypass_addrs: impl IntoIterator, routes: impl IntoIterator, - allow_private: bool, ) -> Self { let routes_cidr = routes.into_iter().collect(); + let bypass_cidrs = tunnel_bypass_addrs.into_iter().collect(); Self { tun: tun.into(), - tunnel_bypass_addr: *tunnel_bypass_addr, - allow_private, + tunnel_bypass_addrs: bypass_cidrs, routes: routes_cidr, set_up: false, - delete_proxy_route: false, + delete_proxy_routes: Vec::::new(), child: 0, unmount_resolvconf: false, restore_resolvconf_data: None, } } - fn route_proxy_address(&mut self) -> Result { - let route_show_args = if self.tunnel_bypass_addr.is_ipv6() { + fn bypass_cidr(cidr: &IpCidr) -> Result { + let is_ipv6 = match cidr { + IpCidr::Ipv4(_) => false, + IpCidr::Ipv6(_) => true, + }; + let route_show_args = if is_ipv6 { ["ip", "-6", "route", "show"] } else { ["ip", "-4", "route", "show"] }; - let routes = run_iproute(route_show_args, "failed to get routing table", true)?; + let routes = run_iproute( + route_show_args, + "failed to get routing table through the ip command", + true, + )?; let mut route_info = Vec::<(IpCidr, Vec)>::new(); - for line in routes.stdout.lines() { if line.is_err() { break; @@ -117,15 +122,11 @@ impl Setup { let mut split = line.split_whitespace(); let mut dst_str = split.next().unwrap(); if dst_str == "default" { - dst_str = if self.tunnel_bypass_addr.is_ipv6() { - "::/0" - } else { - "0.0.0.0/0" - } + dst_str = if is_ipv6 { "::/0" } else { "0.0.0.0/0" } } let (addr_str, prefix_len_str) = match dst_str.split_once(['/']) { - None => (dst_str, if self.tunnel_bypass_addr.is_ipv6() { "128" } else { "32" }), + None => (dst_str, if is_ipv6 { "128" } else { "32" }), Some((addr_str, prefix_len_str)) => (addr_str, prefix_len_str), }; @@ -140,19 +141,19 @@ impl Setup { // Sort routes by prefix length, the most specific route comes first. route_info.sort_by(|entry1, entry2| entry2.0.prefix_len().cmp(&entry1.0.prefix_len())); - for (cidr, route_components) in route_info { - if !cidr.contains_addr(&smoltcp::wire::IpAddress::from(self.tunnel_bypass_addr)) { + for (route_cidr, route_components) in route_info { + if !route_cidr.contains_subnet(cidr) { continue; } // The IP address is routed through a more specific route than the default route. // In this case, there is nothing to do. - if cidr.prefix_len() != 0 { + if route_cidr.prefix_len() != 0 { break; } let mut proxy_route = vec!["ip".into(), "route".into(), "add".into()]; - proxy_route.push(self.tunnel_bypass_addr.to_string()); + proxy_route.push(cidr.to_string()); proxy_route.extend(route_components.into_iter()); run_iproute(proxy_route, "failed to clone route for proxy", false)?; return Ok(true); @@ -235,14 +236,17 @@ impl Setup { self.set_up = false; log::info!("[{}] Restoring network configuration", nix::unistd::getpid()); let _ = Command::new("ip").args(["link", "del", self.tun.as_str()]).output(); - if self.delete_proxy_route { + + for cidr in &self.delete_proxy_routes { let _ = Command::new("ip") - .args(["route", "del", self.tunnel_bypass_addr.to_string().as_str()]) + .args(["route", "del", cidr.to_string().as_str()]) .output(); } + if self.unmount_resolvconf { nix::mount::umount("/etc/resolv.conf")?; } + if let Some(data) = &self.restore_resolvconf_data { fs::write("/etc/resolv.conf", data)?; } @@ -259,8 +263,6 @@ impl Setup { )?; self.set_up = true; - let _tun_name = self.tun.clone(); - let _proxy_ip = self.tunnel_bypass_addr; run_iproute( ["ip", "link", "set", self.tun.as_str(), "up"], @@ -268,8 +270,13 @@ impl Setup { true, )?; - let delete_proxy_route = self.route_proxy_address()?; - self.delete_proxy_route = delete_proxy_route; + let mut delete_proxy_route = Vec::::new(); + for cidr in &self.tunnel_bypass_addrs { + if Self::bypass_cidr(cidr)? { + delete_proxy_route.push(*cidr); + } + } + self.delete_proxy_routes = delete_proxy_route; self.setup_resolv_conf()?; self.add_tunnel_routes()?; @@ -321,14 +328,6 @@ impl Setup { return Err("Automatic setup requires root privileges".into()); } - if self.tunnel_bypass_addr.is_loopback() && !self.allow_private { - log::warn!( - "The proxy address {} is a loopback address. You may need to manually \ - provide --bypass-ip to specify the server IP bypassing the tunnel", - self.tunnel_bypass_addr - ) - } - let (read_from_child, write_to_parent) = nix::unistd::pipe()?; match fork::fork() { Ok(Fork::Child) => { diff --git a/src/tun2proxy.rs b/src/tun2proxy.rs index 09bb7cb..8c4bdc5 100644 --- a/src/tun2proxy.rs +++ b/src/tun2proxy.rs @@ -259,7 +259,7 @@ impl<'a> TunToProxy<'a> { #[cfg(target_os = "windows")] if options.setup { - tun.setup_config(options.bypass, options.dns_addr)?; + tun.setup_config(&options.bypass, options.dns_addr)?; } let poll = Poll::new()?; diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..dff0b53 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,22 @@ +use crate::error::Error; +use smoltcp::wire::IpCidr; +use std::net::IpAddr; +use std::str::FromStr; + +pub fn str_to_cidr(s: &str) -> Result { + // IpCidr's FromString implementation requires the netmask to be specified. + // Try to parse as IP address without netmask before falling back. + match IpAddr::from_str(s) { + Err(_) => (), + Ok(cidr) => { + let prefix_len = if cidr.is_ipv4() { 32 } else { 128 }; + return Ok(IpCidr::new(cidr.into(), prefix_len)); + } + }; + + let cidr = IpCidr::from_str(s); + match cidr { + Err(()) => Err("Invalid CIDR: ".into()), + Ok(cidr) => Ok(cidr), + } +} diff --git a/src/wintuninterface.rs b/src/wintuninterface.rs index ab4e4eb..9706043 100644 --- a/src/wintuninterface.rs +++ b/src/wintuninterface.rs @@ -1,4 +1,5 @@ use mio::{event, windows::NamedPipe, Interest, Registry, Token}; +use smoltcp::wire::IpCidr; use smoltcp::{ phy::{self, Device, DeviceCapabilities, Medium}, time::Instant, @@ -225,7 +226,11 @@ impl WinTunInterface { Ok(()) } - pub fn setup_config(&mut self, bypass_ip: Option, dns_addr: Option) -> Result<(), io::Error> { + pub fn setup_config<'a>( + &mut self, + bypass_ips: impl IntoIterator, + dns_addr: Option, + ) -> Result<(), io::Error> { let adapter = self.wintun_session.get_adapter(); // Setup the adapter's address/mask/gateway @@ -261,7 +266,7 @@ impl WinTunInterface { // 3. route the bypass ip to the old gateway // command: `route add bypass_ip old_gateway metric 1` - if let Some(bypass_ip) = bypass_ip { + for bypass_ip in bypass_ips { let args = &["add", &bypass_ip.to_string(), &old_gateway.to_string(), "metric", "1"]; run_command("route", args)?; log::info!("route {:?}", args); diff --git a/tests/proxy.rs b/tests/proxy.rs index 3ced894..a2521ac 100644 --- a/tests/proxy.rs +++ b/tests/proxy.rs @@ -11,8 +11,10 @@ mod tests { use nix::sys::signal; use nix::unistd::Pid; use serial_test::serial; + use smoltcp::wire::IpCidr; use tun2proxy::setup::{get_default_cidrs, Setup}; + use tun2proxy::util::str_to_cidr; use tun2proxy::{main_entry, NetworkInterface, Options, Proxy, ProxyType}; #[derive(Clone, Debug)] @@ -66,12 +68,17 @@ mod tests { continue; } - let bypass_ip = match env::var("BYPASS_IP") { - Err(_) => test.proxy.addr.ip(), - Ok(ip_str) => IpAddr::from_str(ip_str.as_str()).unwrap(), + let mut bypass_ips = Vec::::new(); + + match env::var("BYPASS_IP") { + Err(_) => { + let prefix_len = if test.proxy.addr.ip().is_ipv6() { 128 } else { 32 }; + bypass_ips.push(IpCidr::new(test.proxy.addr.ip().into(), prefix_len)); + } + Ok(ip_str) => bypass_ips.push(str_to_cidr(&ip_str).expect("Invalid bypass IP")), }; - let mut setup = Setup::new(TUN_TEST_DEVICE, &bypass_ip, get_default_cidrs(), false); + let mut setup = Setup::new(TUN_TEST_DEVICE, bypass_ips, get_default_cidrs()); setup.configure().unwrap(); match fork::fork() { From 0ab52c623b2360dafc8344d6fbad874442929a6b Mon Sep 17 00:00:00 2001 From: "B. Blechschmidt" Date: Mon, 30 Oct 2023 20:44:28 +0100 Subject: [PATCH 10/16] Fix virtual DNS --- src/dns.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dns.rs b/src/dns.rs index be95646..1c46dab 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -8,6 +8,7 @@ use trust_dns_proto::{ op::{Message, ResponseCode}, rr::{record_type::RecordType, Name, RData, Record}, }; +use trust_dns_proto::op::{Edns, MessageType}; #[cfg(feature = "use-rand")] pub fn build_dns_request(domain: &str, query_type: RecordType, used_by_tcp: bool) -> Result, String> { @@ -46,6 +47,7 @@ pub fn build_dns_response(mut request: Message, domain: &str, ip: IpAddr, ttl: u record } }; + request.set_message_type(MessageType::Response); request.add_answer(record); Ok(request) } From e3494d921cd61726bba631b34151f2ed4792b1ec Mon Sep 17 00:00:00 2001 From: "B. Blechschmidt" Date: Mon, 30 Oct 2023 20:48:01 +0100 Subject: [PATCH 11/16] Add comment for DNS fix --- src/dns.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dns.rs b/src/dns.rs index 1c46dab..1d9ee61 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -4,11 +4,11 @@ use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, str::FromStr, }; +use trust_dns_proto::op::MessageType; use trust_dns_proto::{ op::{Message, ResponseCode}, rr::{record_type::RecordType, Name, RData, Record}, }; -use trust_dns_proto::op::{Edns, MessageType}; #[cfg(feature = "use-rand")] pub fn build_dns_request(domain: &str, query_type: RecordType, used_by_tcp: bool) -> Result, String> { @@ -47,7 +47,11 @@ pub fn build_dns_response(mut request: Message, domain: &str, ip: IpAddr, ttl: u record } }; + + // We must indicate that this message is a response. Otherwise, implementations may not + // recognize it. request.set_message_type(MessageType::Response); + request.add_answer(record); Ok(request) } From 980ae0172e96b56fce4a38511725a4f6977e7def Mon Sep 17 00:00:00 2001 From: "B. Blechschmidt" Date: Mon, 30 Oct 2023 22:57:16 +0100 Subject: [PATCH 12/16] Bump version 0.1.10 --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7fa53cf..09db757 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] -authors = ["B. Blechschmidt"] +authors = ["B. Blechschmidt", "ssrlive"] edition = "2021" name = "tun2proxy" -version = "0.1.9" +version = "0.1.10" [lib] crate-type = ["cdylib", "lib"] From 286ce0ca6d9aa8510d3dc635aabeffbc5c49ed43 Mon Sep 17 00:00:00 2001 From: "B. Blechschmidt" Date: Fri, 3 Nov 2023 20:28:31 +0100 Subject: [PATCH 13/16] Add very basic and dirty iperf test --- tests/iperf/dante.conf | 24 ++++++++++++++++++++++++ tests/iperf/test.sh | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 tests/iperf/dante.conf create mode 100755 tests/iperf/test.sh diff --git a/tests/iperf/dante.conf b/tests/iperf/dante.conf new file mode 100644 index 0000000..b723f5b --- /dev/null +++ b/tests/iperf/dante.conf @@ -0,0 +1,24 @@ +# logoutput: /var/log/socks.log +internal: 10.0.0.3 +external: 10.0.0.3 +clientmethod: none +socksmethod: none +user.privileged: root +user.notprivileged: nobody + +client pass { + from: 0/0 to: 0/0 + log: error connect disconnect +} + +socks pass { + from: 0/0 to: 0/0 + command: bind connect udpassociate + log: error connect disconnect + socksmethod: none +} + +socks pass { + from: 0.0.0.0/0 to: 0.0.0.0/0 + command: bindreply udpreply +} diff --git a/tests/iperf/test.sh b/tests/iperf/test.sh new file mode 100755 index 0000000..29e1a6f --- /dev/null +++ b/tests/iperf/test.sh @@ -0,0 +1,39 @@ +netns="test" +dante="sockd" +tun2proxy="../../target/release/tun2proxy" + +ip netns add "$netns" + +ip link add veth0 type veth peer name veth0 netns "$netns" + +# Configure veth0 in default ns +ip addr add 10.0.0.2/24 dev veth0 +ip link set dev veth0 up + +# Configure veth0 in child ns +ip netns exec "$netns" ip addr add 10.0.0.3/24 dev veth0 +ip netns exec "$netns" ip addr add 10.0.0.4/24 dev veth0 +ip netns exec "$netns" ip link set dev veth0 up + +# Configure lo interface in child ns +ip netns exec "$netns" ip addr add 127.0.0.1/8 dev lo +ip netns exec "$netns" ip link set dev lo up + +echo "Starting Dante in background ..." +ip netns exec "$netns" "$dante" -f dante.conf & + +# Start iperf server in netns +ip netns exec "$netns" iperf -s -B 10.0.0.4 & + +sleep 1 + +# Prepare tun2proxy +ip tuntap add name tun0 mode tun +ip link set tun0 up +ip route add 10.0.0.4 dev tun0 +"$tun2proxy" --proxy socks5://10.0.0.3:1080 & + +# Run iperf client through tun2proxy +iperf -c 10.0.0.4 + +iperf -c 10.0.0.4 -R From c4ed29b234974224bc9a302e0fbe4caac6eea889 Mon Sep 17 00:00:00 2001 From: "B. Blechschmidt" Date: Fri, 3 Nov 2023 22:45:27 +0100 Subject: [PATCH 14/16] Remove unnecessary SOCKS buffer --- src/socks.rs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/socks.rs b/src/socks.rs index c7b60aa..4ec8570 100644 --- a/src/socks.rs +++ b/src/socks.rs @@ -27,7 +27,6 @@ struct SocksProxyImpl { server_inbuf: VecDeque, client_outbuf: VecDeque, server_outbuf: VecDeque, - data_buf: VecDeque, version: Version, credentials: Option, command: protocol::Command, @@ -48,7 +47,6 @@ impl SocksProxyImpl { server_inbuf: VecDeque::default(), client_outbuf: VecDeque::default(), server_outbuf: VecDeque::default(), - data_buf: VecDeque::default(), version, credentials, command, @@ -131,8 +129,6 @@ impl SocksProxyImpl { } self.server_inbuf.drain(0..8); - self.server_outbuf.append(&mut self.data_buf); - self.data_buf.clear(); self.state = SocksState::Established; self.state_change() @@ -230,13 +226,9 @@ impl SocksProxyImpl { } if self.command == protocol::Command::UdpAssociate { self.udp_associate = Some(SocketAddr::try_from(&response.address)?); - assert!(self.data_buf.is_empty()); log::trace!("UDP associate recieved address {}", response.address); } - self.server_outbuf.append(&mut self.data_buf); - self.data_buf.clear(); - self.state = SocksState::Established; self.state_change() } @@ -280,11 +272,7 @@ impl ProxyHandler for SocksProxyImpl { self.server_inbuf.extend(buffer.iter()); } IncomingDirection::FromClient => { - if self.state == SocksState::Established { - self.client_inbuf.extend(buffer.iter()); - } else { - self.data_buf.extend(buffer.iter()); - } + self.client_inbuf.extend(buffer.iter()); } } @@ -318,7 +306,7 @@ impl ProxyHandler for SocksProxyImpl { match dir { Direction::Incoming(incoming) => match incoming { IncomingDirection::FromServer => self.server_inbuf.len(), - IncomingDirection::FromClient => self.client_inbuf.len().max(self.data_buf.len()), + IncomingDirection::FromClient => self.client_inbuf.len(), }, Direction::Outgoing(outgoing) => match outgoing { OutgoingDirection::ToServer => self.server_outbuf.len(), From fe85ecd15c7e9598a720acd0620230eac904e1e8 Mon Sep 17 00:00:00 2001 From: ssrlive <30760636+ssrlive@users.noreply.github.com> Date: Sat, 4 Nov 2023 12:28:38 +0800 Subject: [PATCH 15/16] iperf3 testing script --- tests/iperf/test.sh | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/iperf/test.sh b/tests/iperf/test.sh index 29e1a6f..09fd0b4 100755 --- a/tests/iperf/test.sh +++ b/tests/iperf/test.sh @@ -1,6 +1,13 @@ +#!/bin/bash + +# sudo apt install iperf3 dante-server + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +echo $SCRIPT_DIR + netns="test" -dante="sockd" -tun2proxy="../../target/release/tun2proxy" +dante="danted" +tun2proxy="${SCRIPT_DIR}/../../target/release/tun2proxy" ip netns add "$netns" @@ -20,10 +27,10 @@ ip netns exec "$netns" ip addr add 127.0.0.1/8 dev lo ip netns exec "$netns" ip link set dev lo up echo "Starting Dante in background ..." -ip netns exec "$netns" "$dante" -f dante.conf & +ip netns exec "$netns" "$dante" -f ${SCRIPT_DIR}/dante.conf & -# Start iperf server in netns -ip netns exec "$netns" iperf -s -B 10.0.0.4 & +# Start iperf3 server in netns +ip netns exec "$netns" iperf3 -s -B 10.0.0.4 & sleep 1 @@ -34,6 +41,6 @@ ip route add 10.0.0.4 dev tun0 "$tun2proxy" --proxy socks5://10.0.0.3:1080 & # Run iperf client through tun2proxy -iperf -c 10.0.0.4 +iperf3 -c 10.0.0.4 -iperf -c 10.0.0.4 -R +iperf3 -c 10.0.0.4 -R From 9396db4a5221e0952436c4a86ce5c1b50be28293 Mon Sep 17 00:00:00 2001 From: ssrlive <30760636+ssrlive@users.noreply.github.com> Date: Sat, 4 Nov 2023 14:34:47 +0800 Subject: [PATCH 16/16] test code --- src/tun2proxy.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/tun2proxy.rs b/src/tun2proxy.rs index 8c4bdc5..6792490 100644 --- a/src/tun2proxy.rs +++ b/src/tun2proxy.rs @@ -189,6 +189,7 @@ struct ConnectionState { udp_token: Option, udp_data_cache: LinkedList>, dns_over_tcp_expiry: Option<::std::time::Instant>, + is_tcp_closed: bool, } pub(crate) trait ProxyHandler { @@ -580,7 +581,7 @@ impl<'a> TunToProxy<'a> { state.dns_over_tcp_expiry = Some(Self::common_udp_life_timeout()); let mut vecbuf = vec![]; - Self::read_data_from_tcp_stream(&mut state.mio_stream, |data| { + Self::read_data_from_tcp_stream(&mut state.mio_stream, &mut state.is_tcp_closed, |data| { vecbuf.extend_from_slice(data); Ok(()) })?; @@ -835,6 +836,7 @@ impl<'a> TunToProxy<'a> { origin_dst: dst, udp_data_cache: LinkedList::new(), dns_over_tcp_expiry: None, + is_tcp_closed: false, }; Ok(state) } @@ -852,13 +854,25 @@ impl<'a> TunToProxy<'a> { false } - fn clearup_expired_udp_associate(&mut self) -> Result<()> { + fn tcp_is_closed(&self, info: &ConnectionInfo) -> bool { + if let Some(state) = self.connection_map.get(info) { + return state.is_tcp_closed; + } + false + } + + fn clearup_expired_connection(&mut self) -> Result<()> { let keys = self.connection_map.keys().cloned().collect::>(); for key in keys { if self.udp_associate_timeout_expired(&key) { log::trace!("UDP associate timeout: {}", key); self.remove_connection(&key)?; } + + if self.tcp_is_closed(&key) { + log::trace!("TCP closed: {}", key); + self.remove_connection(&key)?; + } } Ok(()) } @@ -1061,7 +1075,7 @@ impl<'a> TunToProxy<'a> { // TODO: Move this reading process to its own function. let mut vecbuf = vec![]; - Self::read_data_from_tcp_stream(&mut state.mio_stream, |data| { + Self::read_data_from_tcp_stream(&mut state.mio_stream, &mut state.is_tcp_closed, |data| { vecbuf.extend_from_slice(data); Ok(()) })?; @@ -1130,7 +1144,7 @@ impl<'a> TunToProxy<'a> { Ok(()) } - fn read_data_from_tcp_stream(stream: &mut TcpStream, mut callback: F) -> Result<()> + fn read_data_from_tcp_stream(stream: &mut TcpStream, is_closed: &mut bool, mut callback: F) -> Result<()> where F: FnMut(&mut [u8]) -> Result<()>, { @@ -1139,6 +1153,7 @@ impl<'a> TunToProxy<'a> { match stream.read(&mut tmp) { Ok(0) => { // The tcp connection closed + *is_closed = true; break; } Ok(read_result) => { @@ -1219,7 +1234,7 @@ impl<'a> TunToProxy<'a> { } } self.send_to_smoltcp()?; - self.clearup_expired_udp_associate()?; + self.clearup_expired_connection()?; self.clearup_expired_dns_over_tcp()?; } }