From e08a0f683dd6cb7d5af2fb4c27ab9282992f51e7 Mon Sep 17 00:00:00 2001 From: "B. Blechschmidt" Date: Sun, 29 Oct 2023 23:01:06 +0100 Subject: [PATCH] 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() {