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
####################################################################################################
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"]

View file

@ -99,7 +99,7 @@ Options:
--dns-addr <IP> DNS resolver address [default: 8.8.8.8]
-6, --ipv6-enabled IPv6 enabled
-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]
-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> | 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):
```bash
docker run -it \
-d \
--network "container:tun2proxy" \
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,
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<std::net::IpAddr>,
ipv6_enabled: bool,
bypass: Option<std::net::IpAddr>,
pub setup: bool,
bypass: Vec<IpCidr>,
}
impl Options {
@ -140,8 +142,10 @@ impl Options {
self
}
pub fn with_bypass(mut self, ip: Option<std::net::IpAddr>) -> Self {
self.bypass = ip;
pub fn with_bypass_ips<'a>(mut self, bypass_ips: impl IntoIterator<Item = &'a IpCidr>) -> Self {
for bypass_ip in bypass_ips {
self.bypass.push(*bypass_ip);
}
self
}
}

View file

@ -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<ArgSetup>,
/// Public proxy IP used in routing setup which should bypassing the tunnel
#[arg(short, long, value_name = "IP")]
bypass: Option<IpAddr>,
/// IPs used in routing setup which should bypass the tunnel
#[arg(short, long, value_name = "IP|CIDR")]
bypass: Vec<String>,
/// 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,21 +119,29 @@ 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::<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")]
{
let mut setup: Setup;
if options.setup {
let mut setup = Setup::new(&args.tun, &bypass_tun_ip, get_default_cidrs(), args.bypass.is_some());
setup = Setup::new(&args.tun, bypass_ips, get_default_cidrs());
setup.configure()?;
setup.drop_privileges()?;
}
}
main_entry(&interface, &args.proxy, options)?;

View file

@ -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<IpCidr>,
tunnel_bypass_addr: IpAddr,
allow_private: bool,
tunnel_bypass_addrs: Vec<IpCidr>,
tun: String,
set_up: bool,
delete_proxy_route: bool,
delete_proxy_routes: Vec<IpCidr>,
child: libc::pid_t,
unmount_resolvconf: bool,
restore_resolvconf_data: Option<Vec<u8>>,
@ -76,35 +75,41 @@ where
impl Setup {
pub fn new(
tun: impl Into<String>,
tunnel_bypass_addr: &IpAddr,
tunnel_bypass_addrs: impl IntoIterator<Item = IpCidr>,
routes: impl IntoIterator<Item = IpCidr>,
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::<IpCidr>::new(),
child: 0,
unmount_resolvconf: false,
restore_resolvconf_data: None,
}
}
fn route_proxy_address(&mut self) -> Result<bool, Error> {
let route_show_args = if self.tunnel_bypass_addr.is_ipv6() {
fn bypass_cidr(cidr: &IpCidr) -> Result<bool, Error> {
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<String>)>::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::<IpCidr>::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) => {

View file

@ -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()?;

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 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<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();
// 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);

View file

@ -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::<IpCidr>::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() {