Use nix crate instead of interacting with libc directly, drop privileges

This commit is contained in:
B. Blechschmidt 2023-04-01 02:19:20 +02:00
parent 3dc7fde5e9
commit 15703a4823
6 changed files with 232 additions and 127 deletions

View file

@ -10,19 +10,19 @@ clap = { version = "4.1", features = ["derive"] }
ctrlc = "3.2" ctrlc = "3.2"
dotenvy = "0.15" dotenvy = "0.15"
env_logger = "0.10" env_logger = "0.10"
fork = "0.1"
hashlink = "0.8" hashlink = "0.8"
libc = "0.2" libc = "0.2"
log = "0.4" log = "0.4"
mio = { version = "0.8", features = ["os-poll", "net", "os-ext"] } mio = { version = "0.8", features = ["os-poll", "net", "os-ext"] }
nix = { version = "0.26", features = ["process", "signal"] }
prctl = "1.0"
smoltcp = { version = "0.9", git = "https://github.com/smoltcp-rs/smoltcp.git", features = ["std"] } smoltcp = { version = "0.9", git = "https://github.com/smoltcp-rs/smoltcp.git", features = ["std"] }
thiserror = "1.0" thiserror = "1.0"
url = "2.3" url = "2.3"
[dev-dependencies] [dev-dependencies]
ctor = "0.1" ctor = "0.1"
fork = "0.1"
nix = { version = "0.26", features = ["process", "signal"] }
prctl = "1.0"
reqwest = { version = "0.11", features = ["blocking", "json"] } reqwest = { version = "0.11", features = ["blocking", "json"] }
serial_test = "1.0" serial_test = "1.0"
test-log = "0.2" test-log = "0.2"

View file

@ -35,6 +35,12 @@ pub enum Error {
#[error("&String {0}")] #[error("&String {0}")]
RefString(String), RefString(String),
#[error("nix::errno::Errno {0:?}")]
OSError(#[from] nix::errno::Errno),
#[error("std::num::ParseIntError {0:?}")]
IntParseError(#[from] std::num::ParseIntError),
} }
impl From<&str> for Error { impl From<&str> for Error {

View file

@ -111,25 +111,25 @@ impl Credentials {
} }
} }
pub fn main_entry(tun: &str, proxy: Proxy, options: Options) -> Result<(), Error> { pub fn main_entry(tun: &str, proxy: &Proxy, options: Options) -> Result<(), Error> {
let mut ttp = TunToProxy::new(tun, options)?; let mut ttp = TunToProxy::new(tun, options)?;
match proxy.proxy_type { match proxy.proxy_type {
ProxyType::Socks4 => { ProxyType::Socks4 => {
ttp.add_connection_manager(SocksManager::new( ttp.add_connection_manager(SocksManager::new(
proxy.addr, proxy.addr,
SocksVersion::V4, SocksVersion::V4,
proxy.credentials, proxy.credentials.clone(),
)); ));
} }
ProxyType::Socks5 => { ProxyType::Socks5 => {
ttp.add_connection_manager(SocksManager::new( ttp.add_connection_manager(SocksManager::new(
proxy.addr, proxy.addr,
SocksVersion::V5, SocksVersion::V5,
proxy.credentials, proxy.credentials.clone(),
)); ));
} }
ProxyType::Http => { ProxyType::Http => {
ttp.add_connection_manager(HttpManager::new(proxy.addr, proxy.credentials)); ttp.add_connection_manager(HttpManager::new(proxy.addr, proxy.credentials.clone()));
} }
} }
ttp.run() ttp.run()

View file

@ -1,8 +1,10 @@
use clap::Parser; use clap::Parser;
use env_logger::Env; use env_logger::Env;
use std::net::IpAddr; use std::net::IpAddr;
use std::process::ExitCode; use std::process::ExitCode;
use tun2proxy::error::Error;
use tun2proxy::setup::{get_default_cidrs, Setup}; use tun2proxy::setup::{get_default_cidrs, Setup};
use tun2proxy::Options; use tun2proxy::Options;
use tun2proxy::{main_entry, Proxy}; use tun2proxy::{main_entry, Proxy};
@ -63,6 +65,7 @@ fn main() -> ExitCode {
options = options.with_virtual_dns(); options = options.with_virtual_dns();
} }
if let Err(e) = (|| -> Result<(), Error> {
let mut setup: Setup; let mut setup: Setup;
if args.setup == Some(ArgSetup::Auto) { if args.setup == Some(ArgSetup::Auto) {
let bypass_tun_ip = match args.setup_ip { let bypass_tun_ip = match args.setup_ip {
@ -75,15 +78,19 @@ fn main() -> ExitCode {
get_default_cidrs(), get_default_cidrs(),
args.setup_ip.is_some(), args.setup_ip.is_some(),
); );
if let Err(e) = setup.setup() {
log::error!("{e}"); setup.configure()?;
return ExitCode::FAILURE;
} setup.drop_privileges()?;
} }
if let Err(e) = main_entry(&args.tun, args.proxy, options) { main_entry(&args.tun, &args.proxy, options)?;
Ok(())
})() {
log::error!("{e}"); log::error!("{e}");
return ExitCode::FAILURE; std::process::exit(1);
} };
ExitCode::SUCCESS ExitCode::SUCCESS
} }

View file

@ -1,14 +1,19 @@
use crate::error::Error; use crate::error::Error;
use smoltcp::wire::IpCidr; use smoltcp::wire::IpCidr;
use std::ffi::{CString, OsStr}; use std::convert::TryFrom;
use std::io::{BufRead, Write};
use std::mem; use std::ffi::OsStr;
use std::io::BufRead;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::os::unix::io::FromRawFd;
use std::os::fd::RawFd;
use std::process::{Command, Output}; use std::process::{Command, Output};
use std::ptr::null;
use std::str::FromStr; use std::str::FromStr;
use fork::Fork;
#[derive(Clone)] #[derive(Clone)]
pub struct Setup { pub struct Setup {
routes: Vec<IpCidr>, routes: Vec<IpCidr>,
@ -17,6 +22,7 @@ pub struct Setup {
tun: String, tun: String,
set_up: bool, set_up: bool,
delete_proxy_route: bool, delete_proxy_route: bool,
child: libc::pid_t,
} }
pub fn get_default_cidrs() -> [IpCidr; 4] { pub fn get_default_cidrs() -> [IpCidr; 4] {
@ -54,7 +60,13 @@ where
cmdline.append(&mut args); cmdline.append(&mut args);
let command = cmdline.as_slice().join(" "); let command = cmdline.as_slice().join(" ");
match String::from_utf8(output.stderr.clone()) { match String::from_utf8(output.stderr.clone()) {
Ok(output) => Err(format!("Command `{}` failed: {}", command, output).into()), Ok(output) => Err(format!(
"[{}] Command `{}` failed: {}",
nix::unistd::getpid(),
command,
output
)
.into()),
Err(_) => Err(format!( Err(_) => Err(format!(
"Command `{:?}` failed with exit code {}", "Command `{:?}` failed with exit code {}",
command, command,
@ -80,6 +92,7 @@ impl Setup {
routes: routes_cidr, routes: routes_cidr,
set_up: false, set_up: false,
delete_proxy_route: false, delete_proxy_route: false,
child: 0,
} }
} }
@ -113,7 +126,17 @@ impl Setup {
} }
} }
let (addr_str, prefix_len_str) = dst_str.split_once(['/']).unwrap(); let (addr_str, prefix_len_str) = match dst_str.split_once(['/']) {
None => (
dst_str,
if self.tunnel_bypass_addr.is_ipv6() {
"128"
} else {
"32"
},
),
Some((addr_str, prefix_len_str)) => (addr_str, prefix_len_str),
};
let cidr: IpCidr = IpCidr::new( let cidr: IpCidr = IpCidr::new(
std::net::IpAddr::from_str(addr_str).unwrap().into(), std::net::IpAddr::from_str(addr_str).unwrap().into(),
@ -147,32 +170,29 @@ impl Setup {
} }
fn setup_resolv_conf() -> Result<(), Error> { fn setup_resolv_conf() -> Result<(), Error> {
unsafe { let fd = nix::fcntl::open(
let fd = libc::open( "/tmp/tun2proxy-resolv.conf",
CString::new("/tmp/tun2proxy-resolv.conf")?.as_ptr(), nix::fcntl::OFlag::O_RDWR | nix::fcntl::OFlag::O_CLOEXEC | nix::fcntl::OFlag::O_CREAT,
libc::O_RDWR | libc::O_CLOEXEC | libc::O_CREAT, nix::sys::stat::Mode::from_bits(0o644_u32).unwrap(),
); )?;
if fd == -1 { let data = "nameserver 198.18.0.1\n".as_bytes();
return Err("Failed to create temporary file".into()); let mut written = 0;
} loop {
let mut f = std::fs::File::from_raw_fd(fd); if written >= data.len() {
f.write_all("nameserver 198.18.0.1\n".as_bytes())?; break;
mem::forget(f);
if libc::fchmod(fd, 0o444) == -1 {
return Err("Failed to change ownership of /etc/resolv.conf".into());
}
let fd_path = format!("/proc/self/fd/{}", fd);
if libc::mount(
CString::new(fd_path)?.as_ptr(),
CString::new("/etc/resolv.conf")?.as_ptr(),
CString::new("resolvconf")?.as_ptr(),
libc::MS_BIND,
null(),
) == -1
{
return Err("Failed to mount /etc/resolv.conf".into());
} }
written += nix::unistd::write(fd, &data[written..])?;
} }
nix::sys::stat::fchmod(fd, nix::sys::stat::Mode::from_bits(0o444_u32).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(),
)?;
nix::unistd::close(fd)?;
Ok(()) Ok(())
} }
@ -194,42 +214,27 @@ impl Setup {
Ok(()) Ok(())
} }
fn shutdown(&self) { fn shutdown(&mut self) -> Result<(), Error> {
if !self.set_up { self.set_up = false;
return; log::info!(
} "[{}] Restoring network configuration",
Self::shutdown_with_args(&self.tun, self.tunnel_bypass_addr, self.delete_proxy_route); nix::unistd::getpid()
} );
fn shutdown_with_args(tun_name: &str, proxy_ip: IpAddr, delete_proxy_route: bool) {
log::info!("Restoring network configuration");
let _ = Command::new("ip").args(["link", "del", tun_name]).output();
if delete_proxy_route {
let _ = Command::new("ip") let _ = Command::new("ip")
.args(["route", "del", proxy_ip.to_string().as_str()]) .args(["link", "del", self.tun.as_str()])
.output();
if self.delete_proxy_route {
let _ = Command::new("ip")
.args(["route", "del", self.tunnel_bypass_addr.to_string().as_str()])
.output(); .output();
} }
unsafe { nix::mount::umount("/etc/resolv.conf")?;
let umount_path = CString::new("/etc/resolv.conf").unwrap(); Ok(())
libc::umount(umount_path.as_ptr());
}
}
pub fn setup(&mut self) -> Result<(), Error> {
unsafe {
if libc::getuid() != 0 {
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 --setup-ip to specify the server IP bypassing the tunnel",
self.tunnel_bypass_addr
)
} }
fn setup_and_handle_signals(&mut self, read_from_child: RawFd, write_to_parent: RawFd) {
if let Err(e) = (|| -> Result<(), Error> {
nix::unistd::close(read_from_child)?;
run_iproute( run_iproute(
[ [
"ip", "ip",
@ -245,8 +250,8 @@ impl Setup {
)?; )?;
self.set_up = true; self.set_up = true;
let tun_name = self.tun.clone(); let _tun_name = self.tun.clone();
let proxy_ip = self.tunnel_bypass_addr; 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"],
@ -256,19 +261,105 @@ impl Setup {
let delete_proxy_route = self.route_proxy_address()?; let delete_proxy_route = self.route_proxy_address()?;
self.delete_proxy_route = delete_proxy_route; self.delete_proxy_route = delete_proxy_route;
ctrlc::set_handler(move || {
Self::shutdown_with_args(&tun_name, proxy_ip, delete_proxy_route);
std::process::exit(0);
})?;
Self::setup_resolv_conf()?; Self::setup_resolv_conf()?;
self.add_tunnel_routes()?; self.add_tunnel_routes()?;
Ok(()) // Signal to child that we are done setting up everything.
if nix::unistd::write(write_to_parent, &[1])? != 1 {
return Err("Failed to write to pipe".into());
}
nix::unistd::close(write_to_parent)?;
// Now wait for the termination signals.
let mut mask = nix::sys::signal::SigSet::empty();
mask.add(nix::sys::signal::SIGINT);
mask.add(nix::sys::signal::SIGTERM);
mask.add(nix::sys::signal::SIGQUIT);
mask.thread_block().unwrap();
let mut fd = nix::sys::signalfd::SignalFd::new(&mask).unwrap();
loop {
let res = fd.read_signal().unwrap().unwrap();
let signo = nix::sys::signal::Signal::try_from(res.ssi_signo as i32).unwrap();
if signo == nix::sys::signal::SIGINT
|| signo == nix::sys::signal::SIGTERM
|| signo == nix::sys::signal::SIGQUIT
{
break;
} }
} }
impl Drop for Setup { self.shutdown()?;
fn drop(&mut self) { Ok(())
self.shutdown(); })() {
log::error!("{e}");
self.shutdown().unwrap();
};
}
pub fn drop_privileges(&self) -> Result<(), Error> {
let gid_str = match std::env::var("SUDO_GID") {
Ok(uid_str) => uid_str,
_ => String::from("65535"),
};
let gid = gid_str.parse::<u32>()?;
nix::unistd::setgid(nix::unistd::Gid::from_raw(gid))?;
let uid_str = match std::env::var("SUDO_UID") {
Ok(uid_str) => uid_str,
_ => String::from("65535"),
};
let uid = uid_str.parse::<u32>()?;
nix::unistd::setuid(nix::unistd::Uid::from_raw(uid))?;
Ok(())
}
pub fn configure(&mut self) -> Result<(), Error> {
log::info!(
"[{}] Setting up network configuration",
nix::unistd::getpid()
);
if nix::unistd::getuid() != 0.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 --setup-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) => {
prctl::set_death_signal(nix::sys::signal::SIGINT as isize).unwrap();
self.setup_and_handle_signals(read_from_child, write_to_parent);
std::process::exit(0);
}
Ok(Fork::Parent(child)) => {
self.child = child;
nix::unistd::close(write_to_parent)?;
let mut buf = [0];
if nix::unistd::read(read_from_child, &mut buf)? != 1 {
return Err("Failed to read from pipe".into());
}
nix::unistd::close(read_from_child)?;
Ok(())
}
_ => Err("Failed to fork".into()),
}
}
pub fn restore(&mut self) -> Result<(), Error> {
nix::sys::signal::kill(
nix::unistd::Pid::from_raw(self.child),
nix::sys::signal::SIGINT,
)?;
nix::sys::wait::waitpid(nix::unistd::Pid::from_raw(self.child), None)?;
Ok(())
} }
} }

View file

@ -64,28 +64,29 @@ mod tests {
continue; continue;
} }
match fork::fork() { let mut setup = Setup::new(
Ok(Fork::Parent(child)) => {
test_function();
signal::kill(Pid::from_raw(child), signal::SIGINT)
.expect("failed to kill child");
nix::sys::wait::waitpid(Pid::from_raw(child), None)
.expect("failed to wait for child");
}
Ok(Fork::Child) => {
prctl::set_death_signal(signal::SIGKILL as isize).unwrap(); // 9 == SIGKILL
let _setup = Setup::new(
TUN_TEST_DEVICE, TUN_TEST_DEVICE,
&test.proxy.addr.ip(), &test.proxy.addr.ip(),
get_default_cidrs(), get_default_cidrs(),
false, false,
); );
setup.configure().unwrap();
match fork::fork() {
Ok(Fork::Parent(child)) => {
test_function();
signal::kill(Pid::from_raw(child), signal::SIGINT)
.expect("failed to kill child");
setup.restore().unwrap();
}
Ok(Fork::Child) => {
prctl::set_death_signal(signal::SIGINT as isize).unwrap();
let _ = main_entry( let _ = main_entry(
TUN_TEST_DEVICE, TUN_TEST_DEVICE,
test.proxy, &test.proxy,
Options::new().with_virtual_dns(), Options::new().with_virtual_dns(),
); );
std::process::exit(0);
} }
Err(_) => panic!(), Err(_) => panic!(),
} }