Allow multiple bypass IP addresses/CIDRs in routing setup

See issue #73.
This commit is contained in:
B. Blechschmidt 2023-10-29 23:01:06 +01:00
parent 9b27dd2df2
commit e08a0f683d
10 changed files with 112 additions and 123 deletions

View file

@ -12,18 +12,9 @@ RUN cargo build --release --target x86_64-unknown-linux-gnu
## Final image ## Final image
#################################################################################################### ####################################################################################################
FROM ubuntu:latest FROM ubuntu:latest
WORKDIR /app
ENV TUN=tun0 RUN apt update && apt install -y iproute2 && apt clean all
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
COPY --from=builder /worker/target/x86_64-unknown-linux-gnu/release/tun2proxy /usr/bin/tun2proxy 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"]

View file

@ -99,7 +99,7 @@ Options:
--dns-addr <IP> DNS resolver address [default: 8.8.8.8] --dns-addr <IP> DNS resolver address [default: 8.8.8.8]
-6, --ipv6-enabled IPv6 enabled -6, --ipv6-enabled IPv6 enabled
-s, --setup <method> Routing and system setup [default: none] [possible values: none, auto] -s, --setup <method> Routing and system setup [default: none] [possible values: none, auto]
-b, --bypass <IP> Public proxy IP used in routing setup which should bypassing the tunnel -b, --bypass <IP|CIDR> IPs and CIDRs used in routing setup which should bypass the tunnel
-v, --verbosity <level> Verbosity level [default: info] [possible values: off, error, warn, info, debug, trace] -v, --verbosity <level> Verbosity level [default: info] [possible values: off, error, warn, info, debug, trace]
-h, --help Print help -h, --help Print help
-V, --version Print version -V, --version Print version
@ -119,31 +119,17 @@ Next, start a container from the tun2proxy image:
```bash ```bash
docker run -d \ docker run -d \
-e PROXY=proto://[username[:password]@]host:port \
-v /dev/net/tun:/dev/net/tun \ -v /dev/net/tun:/dev/net/tun \
--sysctl net.ipv6.conf.default.disable_ipv6=0 \ --sysctl net.ipv6.conf.default.disable_ipv6=0 \
--cap-add NET_ADMIN \ --cap-add NET_ADMIN \
--name tun2proxy \ --name tun2proxy \
tun2proxy tun2proxy --proxy proto://[username[:password]@]host:port
``` ```
container env list
| container env | Default | program option | mean |
| ------------- | ------- | ----------------------- | ------------------------------------------------------------ |
| TUN | tun0 | -t, --tun <name> | Name of the tun interface [default: tun0] |
| PROXY | None | -p, --proxy <URL> | Proxy URL in the form proto://[username[:password]@]host:port |
| DNS | virtual | -d, --dns <strategy> | DNS handling strategy [default: virtual] [possible values: virtual, over-tcp, direct] |
| MODE | auto | -s, --setup <method> | Routing and system setup [default: none] [possible values: none, auto] |
| BYPASS_IP | None | -b, --bypass <IP> | Public proxy IP used in routing setup which should bypassing the tunnel |
| VERBOSITY | info | -v, --verbosity <level> | 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): You can then provide the running container's network to another worker container by sharing the network namespace (like kubernetes sidecar):
```bash ```bash
docker run -it \ docker run -it \
-d \
--network "container:tun2proxy" \ --network "container:tun2proxy" \
ubuntu:latest ubuntu:latest
``` ```

View file

@ -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!!"

View file

@ -4,6 +4,7 @@ use crate::{
socks::SocksProxyManager, socks::SocksProxyManager,
tun2proxy::{ConnectionManager, TunToProxy}, tun2proxy::{ConnectionManager, TunToProxy},
}; };
use smoltcp::wire::IpCidr;
use socks5_impl::protocol::UserKey; use socks5_impl::protocol::UserKey;
use std::{ use std::{
net::{SocketAddr, ToSocketAddrs}, net::{SocketAddr, ToSocketAddrs},
@ -17,6 +18,7 @@ mod http;
pub mod setup; pub mod setup;
mod socks; mod socks;
mod tun2proxy; mod tun2proxy;
pub mod util;
mod virtdevice; mod virtdevice;
mod virtdns; mod virtdns;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@ -104,8 +106,8 @@ pub struct Options {
dns_over_tcp: bool, dns_over_tcp: bool,
dns_addr: Option<std::net::IpAddr>, dns_addr: Option<std::net::IpAddr>,
ipv6_enabled: bool, ipv6_enabled: bool,
bypass: Option<std::net::IpAddr>,
pub setup: bool, pub setup: bool,
bypass: Vec<IpCidr>,
} }
impl Options { impl Options {
@ -140,8 +142,10 @@ impl Options {
self self
} }
pub fn with_bypass(mut self, ip: Option<std::net::IpAddr>) -> Self { pub fn with_bypass_ips<'a>(mut self, bypass_ips: impl IntoIterator<Item = &'a IpCidr>) -> Self {
self.bypass = ip; for bypass_ip in bypass_ips {
self.bypass.push(*bypass_ip);
}
self self
} }
} }

View file

@ -1,5 +1,7 @@
use clap::Parser; use clap::Parser;
use smoltcp::wire::IpCidr;
use std::{net::IpAddr, process::ExitCode}; use std::{net::IpAddr, process::ExitCode};
use tun2proxy::util::str_to_cidr;
use tun2proxy::{error::Error, main_entry, NetworkInterface, Options, Proxy}; use tun2proxy::{error::Error, main_entry, NetworkInterface, Options, Proxy};
#[cfg(target_os = "linux")] #[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" })] #[arg(short, long, value_name = "method", value_enum, default_value = if cfg!(target_os = "linux") { "none" } else { "auto" })]
setup: Option<ArgSetup>, setup: Option<ArgSetup>,
/// Public proxy IP used in routing setup which should bypassing the tunnel /// IPs used in routing setup which should bypass the tunnel
#[arg(short, long, value_name = "IP")] #[arg(short, long, value_name = "IP|CIDR")]
bypass: Option<IpAddr>, bypass: Vec<String>,
/// Verbosity level /// Verbosity level
#[arg(short, long, value_name = "level", value_enum, default_value = "info")] #[arg(short, long, value_name = "level", value_enum, default_value = "info")]
@ -53,7 +55,7 @@ struct Args {
/// DNS query handling strategy /// DNS query handling strategy
/// - Virtual: Intercept DNS queries and resolve them locally with a fake IP address /// - Virtual: Intercept DNS queries and resolve them locally with a fake IP address
/// - OverTcp: Use TCP to send DNS queries to the DNS server /// - 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)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
enum ArgDns { enum ArgDns {
Virtual, 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); options.setup = args.setup.map(|s| s == ArgSetup::Auto).unwrap_or(false);
let block = || -> Result<(), Error> { let block = || -> Result<(), Error> {
let mut bypass_ips = Vec::<IpCidr>::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")] #[cfg(target_os = "linux")]
if options.setup { {
let mut setup = Setup::new(&args.tun, &bypass_tun_ip, get_default_cidrs(), args.bypass.is_some()); let mut setup: Setup;
setup.configure()?; if options.setup {
setup.drop_privileges()?; setup = Setup::new(&args.tun, bypass_ips, get_default_cidrs());
setup.configure()?;
setup.drop_privileges()?;
}
} }
main_entry(&interface, &args.proxy, options)?; main_entry(&interface, &args.proxy, options)?;

View file

@ -8,7 +8,7 @@ use std::{
ffi::OsStr, ffi::OsStr,
fs, fs,
io::BufRead, io::BufRead,
net::{IpAddr, Ipv4Addr, Ipv6Addr}, net::{Ipv4Addr, Ipv6Addr},
os::unix::io::RawFd, os::unix::io::RawFd,
process::{Command, Output}, process::{Command, Output},
str::FromStr, str::FromStr,
@ -17,11 +17,10 @@ use std::{
#[derive(Clone)] #[derive(Clone)]
pub struct Setup { pub struct Setup {
routes: Vec<IpCidr>, routes: Vec<IpCidr>,
tunnel_bypass_addr: IpAddr, tunnel_bypass_addrs: Vec<IpCidr>,
allow_private: bool,
tun: String, tun: String,
set_up: bool, set_up: bool,
delete_proxy_route: bool, delete_proxy_routes: Vec<IpCidr>,
child: libc::pid_t, child: libc::pid_t,
unmount_resolvconf: bool, unmount_resolvconf: bool,
restore_resolvconf_data: Option<Vec<u8>>, restore_resolvconf_data: Option<Vec<u8>>,
@ -76,35 +75,41 @@ where
impl Setup { impl Setup {
pub fn new( pub fn new(
tun: impl Into<String>, tun: impl Into<String>,
tunnel_bypass_addr: &IpAddr, tunnel_bypass_addrs: impl IntoIterator<Item = IpCidr>,
routes: impl IntoIterator<Item = IpCidr>, routes: impl IntoIterator<Item = IpCidr>,
allow_private: bool,
) -> Self { ) -> Self {
let routes_cidr = routes.into_iter().collect(); let routes_cidr = routes.into_iter().collect();
let bypass_cidrs = tunnel_bypass_addrs.into_iter().collect();
Self { Self {
tun: tun.into(), tun: tun.into(),
tunnel_bypass_addr: *tunnel_bypass_addr, tunnel_bypass_addrs: bypass_cidrs,
allow_private,
routes: routes_cidr, routes: routes_cidr,
set_up: false, set_up: false,
delete_proxy_route: false, delete_proxy_routes: Vec::<IpCidr>::new(),
child: 0, child: 0,
unmount_resolvconf: false, unmount_resolvconf: false,
restore_resolvconf_data: None, restore_resolvconf_data: None,
} }
} }
fn route_proxy_address(&mut self) -> Result<bool, Error> { fn bypass_cidr(cidr: &IpCidr) -> Result<bool, Error> {
let route_show_args = if self.tunnel_bypass_addr.is_ipv6() { let is_ipv6 = match cidr {
IpCidr::Ipv4(_) => false,
IpCidr::Ipv6(_) => true,
};
let route_show_args = if is_ipv6 {
["ip", "-6", "route", "show"] ["ip", "-6", "route", "show"]
} else { } else {
["ip", "-4", "route", "show"] ["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<String>)>::new(); let mut route_info = Vec::<(IpCidr, Vec<String>)>::new();
for line in routes.stdout.lines() { for line in routes.stdout.lines() {
if line.is_err() { if line.is_err() {
break; break;
@ -117,15 +122,11 @@ impl Setup {
let mut split = line.split_whitespace(); let mut split = line.split_whitespace();
let mut dst_str = split.next().unwrap(); let mut dst_str = split.next().unwrap();
if dst_str == "default" { if dst_str == "default" {
dst_str = if self.tunnel_bypass_addr.is_ipv6() { dst_str = if is_ipv6 { "::/0" } else { "0.0.0.0/0" }
"::/0"
} else {
"0.0.0.0/0"
}
} }
let (addr_str, prefix_len_str) = match dst_str.split_once(['/']) { 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), 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. // 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())); route_info.sort_by(|entry1, entry2| entry2.0.prefix_len().cmp(&entry1.0.prefix_len()));
for (cidr, route_components) in route_info { for (route_cidr, route_components) in route_info {
if !cidr.contains_addr(&smoltcp::wire::IpAddress::from(self.tunnel_bypass_addr)) { if !route_cidr.contains_subnet(cidr) {
continue; continue;
} }
// The IP address is routed through a more specific route than the default route. // The IP address is routed through a more specific route than the default route.
// In this case, there is nothing to do. // In this case, there is nothing to do.
if cidr.prefix_len() != 0 { if route_cidr.prefix_len() != 0 {
break; break;
} }
let mut proxy_route = vec!["ip".into(), "route".into(), "add".into()]; 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()); proxy_route.extend(route_components.into_iter());
run_iproute(proxy_route, "failed to clone route for proxy", false)?; run_iproute(proxy_route, "failed to clone route for proxy", false)?;
return Ok(true); return Ok(true);
@ -235,14 +236,17 @@ impl Setup {
self.set_up = false; self.set_up = false;
log::info!("[{}] Restoring network configuration", nix::unistd::getpid()); log::info!("[{}] Restoring network configuration", nix::unistd::getpid());
let _ = Command::new("ip").args(["link", "del", self.tun.as_str()]).output(); 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") let _ = Command::new("ip")
.args(["route", "del", self.tunnel_bypass_addr.to_string().as_str()]) .args(["route", "del", cidr.to_string().as_str()])
.output(); .output();
} }
if self.unmount_resolvconf { if self.unmount_resolvconf {
nix::mount::umount("/etc/resolv.conf")?; nix::mount::umount("/etc/resolv.conf")?;
} }
if let Some(data) = &self.restore_resolvconf_data { if let Some(data) = &self.restore_resolvconf_data {
fs::write("/etc/resolv.conf", data)?; fs::write("/etc/resolv.conf", data)?;
} }
@ -259,8 +263,6 @@ impl Setup {
)?; )?;
self.set_up = true; self.set_up = true;
let _tun_name = self.tun.clone();
let _proxy_ip = self.tunnel_bypass_addr;
run_iproute( run_iproute(
["ip", "link", "set", self.tun.as_str(), "up"], ["ip", "link", "set", self.tun.as_str(), "up"],
@ -268,8 +270,13 @@ impl Setup {
true, true,
)?; )?;
let delete_proxy_route = self.route_proxy_address()?; let mut delete_proxy_route = Vec::<IpCidr>::new();
self.delete_proxy_route = delete_proxy_route; 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.setup_resolv_conf()?;
self.add_tunnel_routes()?; self.add_tunnel_routes()?;
@ -321,14 +328,6 @@ impl Setup {
return Err("Automatic setup requires root privileges".into()); 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()?; let (read_from_child, write_to_parent) = nix::unistd::pipe()?;
match fork::fork() { match fork::fork() {
Ok(Fork::Child) => { Ok(Fork::Child) => {

View file

@ -259,7 +259,7 @@ impl<'a> TunToProxy<'a> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
if options.setup { if options.setup {
tun.setup_config(options.bypass, options.dns_addr)?; tun.setup_config(&options.bypass, options.dns_addr)?;
} }
let poll = Poll::new()?; let poll = Poll::new()?;

22
src/util.rs Normal file
View file

@ -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, Error> {
// 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),
}
}

View file

@ -1,4 +1,5 @@
use mio::{event, windows::NamedPipe, Interest, Registry, Token}; use mio::{event, windows::NamedPipe, Interest, Registry, Token};
use smoltcp::wire::IpCidr;
use smoltcp::{ use smoltcp::{
phy::{self, Device, DeviceCapabilities, Medium}, phy::{self, Device, DeviceCapabilities, Medium},
time::Instant, time::Instant,
@ -225,7 +226,11 @@ impl WinTunInterface {
Ok(()) Ok(())
} }
pub fn setup_config(&mut self, bypass_ip: Option<IpAddr>, dns_addr: Option<IpAddr>) -> Result<(), io::Error> { pub fn setup_config<'a>(
&mut self,
bypass_ips: impl IntoIterator<Item = &'a IpCidr>,
dns_addr: Option<IpAddr>,
) -> Result<(), io::Error> {
let adapter = self.wintun_session.get_adapter(); let adapter = self.wintun_session.get_adapter();
// Setup the adapter's address/mask/gateway // Setup the adapter's address/mask/gateway
@ -261,7 +266,7 @@ impl WinTunInterface {
// 3. route the bypass ip to the old gateway // 3. route the bypass ip to the old gateway
// command: `route add bypass_ip old_gateway metric 1` // 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"]; let args = &["add", &bypass_ip.to_string(), &old_gateway.to_string(), "metric", "1"];
run_command("route", args)?; run_command("route", args)?;
log::info!("route {:?}", args); log::info!("route {:?}", args);

View file

@ -11,8 +11,10 @@ mod tests {
use nix::sys::signal; use nix::sys::signal;
use nix::unistd::Pid; use nix::unistd::Pid;
use serial_test::serial; use serial_test::serial;
use smoltcp::wire::IpCidr;
use tun2proxy::setup::{get_default_cidrs, Setup}; use tun2proxy::setup::{get_default_cidrs, Setup};
use tun2proxy::util::str_to_cidr;
use tun2proxy::{main_entry, NetworkInterface, Options, Proxy, ProxyType}; use tun2proxy::{main_entry, NetworkInterface, Options, Proxy, ProxyType};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -66,12 +68,17 @@ mod tests {
continue; continue;
} }
let bypass_ip = match env::var("BYPASS_IP") { let mut bypass_ips = Vec::<IpCidr>::new();
Err(_) => test.proxy.addr.ip(),
Ok(ip_str) => IpAddr::from_str(ip_str.as_str()).unwrap(), 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(); setup.configure().unwrap();
match fork::fork() { match fork::fork() {