Commit e938b916 authored by Alexander Hansen Færøy's avatar Alexander Hansen Færøy
Browse files

Merge branch 'onionmasq-cli' into 'main'

onionmasq: improve CLI usability, replace netlink with fwmarks

See merge request tpo/core/onionmasq!149
parents 812bde14 4d983307
Loading
Loading
Loading
Loading
+89 −95
Original line number Diff line number Diff line
@@ -105,6 +105,54 @@ dependencies = [
 "libc",
]

[[package]]
name = "anstream"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44"
dependencies = [
 "anstyle",
 "anstyle-parse",
 "anstyle-query",
 "anstyle-wincon",
 "colorchoice",
 "utf8parse",
]

[[package]]
name = "anstyle"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"

[[package]]
name = "anstyle-parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140"
dependencies = [
 "utf8parse",
]

[[package]]
name = "anstyle-query"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
dependencies = [
 "windows-sys 0.48.0",
]

[[package]]
name = "anstyle-wincon"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628"
dependencies = [
 "anstyle",
 "windows-sys 0.48.0",
]

[[package]]
name = "anyhow"
version = "1.0.68"
@@ -478,6 +526,33 @@ dependencies = [
 "libloading",
]

[[package]]
name = "clap"
version = "4.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956"
dependencies = [
 "clap_builder",
]

[[package]]
name = "clap_builder"
version = "4.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45"
dependencies = [
 "anstream",
 "anstyle",
 "clap_lex",
 "strsim",
]

[[package]]
name = "clap_lex"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"

[[package]]
name = "coarsetime"
version = "0.1.22"
@@ -500,6 +575,12 @@ dependencies = [
 "unicode-width",
]

[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"

[[package]]
name = "colored"
version = "2.0.0"
@@ -1532,15 +1613,6 @@ dependencies = [
 "generic-array",
]

[[package]]
name = "ipnetwork"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e"
dependencies = [
 "serde",
]

[[package]]
name = "itertools"
version = "0.11.0"
@@ -1775,71 +1847,6 @@ dependencies = [
 "windows-sys 0.48.0",
]

[[package]]
name = "netlink-packet-core"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4"
dependencies = [
 "anyhow",
 "byteorder",
 "netlink-packet-utils",
]

[[package]]
name = "netlink-packet-route"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053998cea5a306971f88580d0829e90f270f940befd7cf928da179d4187a5a66"
dependencies = [
 "anyhow",
 "bitflags 1.3.2",
 "byteorder",
 "libc",
 "netlink-packet-core",
 "netlink-packet-utils",
]

[[package]]
name = "netlink-packet-utils"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34"
dependencies = [
 "anyhow",
 "byteorder",
 "paste",
 "thiserror",
]

[[package]]
name = "netlink-proto"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "842c6770fc4bb33dd902f41829c61ef872b8e38de1405aa0b938b27b8fba12c3"
dependencies = [
 "bytes",
 "futures",
 "log",
 "netlink-packet-core",
 "netlink-sys",
 "thiserror",
 "tokio",
]

[[package]]
name = "netlink-sys"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "260e21fbb6f3d253a14df90eb0000a6066780a15dd901a7519ce02d77a94985b"
dependencies = [
 "bytes",
 "futures",
 "libc",
 "log",
 "tokio",
]

[[package]]
name = "nix"
version = "0.26.2"
@@ -2031,12 +2038,11 @@ name = "onionmasq"
version = "0.1.0"
dependencies = [
 "anyhow",
 "clap",
 "futures",
 "ipnetwork",
 "libc",
 "log",
 "netlink-packet-route",
 "onion-tunnel",
 "rtnetlink",
 "simple-proc-net",
 "tokio",
 "tracing-subscriber",
@@ -2592,24 +2598,6 @@ dependencies = [
 "zeroize",
]

[[package]]
name = "rtnetlink"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a552eb82d19f38c3beed3f786bd23aa434ceb9ac43ab44419ca6d67a7e186c0"
dependencies = [
 "futures",
 "log",
 "netlink-packet-core",
 "netlink-packet-route",
 "netlink-packet-utils",
 "netlink-proto",
 "netlink-sys",
 "nix",
 "thiserror",
 "tokio",
]

[[package]]
name = "rusqlite"
version = "0.29.0"
@@ -4218,6 +4206,12 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"

[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"

[[package]]
name = "valuable"
version = "0.1.0"
+2 −3
Original line number Diff line number Diff line
@@ -11,8 +11,7 @@ onion-tunnel = { path = "../onion-tunnel", version = "0.1.0" }
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
log = "0.4"
tracing-subscriber = { version = "0.3.17", features = ["fmt", "tracing-log", "env-filter"] }
rtnetlink = "0.13"
netlink-packet-route = "0.17.1"
ipnetwork = "*"
futures = "0.3"
simple-proc-net = { path = "../simple-proc-net" }
clap = "4.3"
libc = "0.2"
+190 −27
Original line number Diff line number Diff line
use crate::netlink::Netlink;
use log::{info, warn};
use onion_tunnel::{IpEndpoint, OnionTunnel, TunnelScaffolding};
use anyhow::Context;
use clap::{Arg, Command};
use log::{info, trace, warn};
use onion_tunnel::scaffolding::{ConnectionDetails, FailedConnectionDetails};
use onion_tunnel::{CountryCode, IpEndpoint, OnionTunnel, TunnelScaffolding};
use simple_proc_net::ProcNetEntry;
use std::ffi::c_void;
use std::io;
use std::mem::size_of;
use std::net::SocketAddr;
use std::os::fd::AsRawFd;
use std::os::fd::RawFd;
use tokio::runtime::Handle;
use std::str::FromStr;
use tokio::net::TcpSocket;
use tracing_subscriber::FmtSubscriber;

mod netlink;
struct LinuxScaffolding {
    cc: Option<CountryCode>,
    can_mark: bool,
    log_connections: bool,
}

impl LinuxScaffolding {
    /// The fwmark to set on arti connections (so policy routing can avoid them being routed
    /// recursively back into the tunnel).
    ///
    /// This is just a randomly generated set of 2 bytes that hopefully won't conflict with
    /// anything else.
    pub const FWMARK: libc::c_int = 0xc185;

struct NetlinkScaffolding {
    netlink: Netlink,
    rt: Handle,
    /// Mark the provided socket file descriptor with the `FWMARK`.
    #[cfg(target_os = "linux")]
    fn mark_fd(fd: RawFd) -> io::Result<()> {
        let ret = unsafe {
            libc::setsockopt(
                fd,
                libc::SOL_SOCKET,
                libc::SO_MARK,
                &Self::FWMARK as *const libc::c_int as *const c_void,
                size_of::<libc::c_int>() as _,
            )
        };
        if ret != 0 {
            Err(io::Error::last_os_error())
        } else {
            Ok(())
        }
    }
}

impl TunnelScaffolding for NetlinkScaffolding {
    fn protect(&self, _: RawFd, addr: &SocketAddr) -> io::Result<()> {
        // HACK(eta): protect() isn't async, so we just block. Oh well!
        tokio::task::block_in_place(|| {
            if let Err(e) = self
                .rt
                .block_on(self.netlink.add_passthrough_route(&addr.ip().into()))
            {
                warn!("adding netlink passthrough route failed: {}", e);
impl TunnelScaffolding for LinuxScaffolding {
    fn protect(&self, fd: RawFd, _: &SocketAddr) -> io::Result<()> {
        #[cfg(target_os = "linux")]
        if self.can_mark {
            Self::mark_fd(fd)?;
        }
        });
        Ok(())
    }

    fn locate(&self, _: IpEndpoint, _: IpEndpoint, _: u64) -> Option<CountryCode> {
        self.cc
    }

    fn on_bootstrapped(&self) {
        info!("Connection to Tor complete!");
    }

    fn on_established(&self, details: ConnectionDetails<'_>) {
        if self.log_connections {
            let exit_identity = details
                .circuit_relays()
                .last()
                .map(|x| {
                    let cc = match x.country_code {
                        Some(v) => v.to_string(),
                        None => "??".into(),
                    };
                    if let Some(i) = x.ed_identity {
                        format!("{} ({})", i, cc)
                    } else if let Some(i) = x.rsa_identity {
                        format!("{} ({})", i, cc)
                    } else {
                        "???".into()
                    }
                })
                .unwrap_or_else(|| "???".into());

            info!(
                "New connection to '{}' (uid {}) via exit {}",
                details.tor_dst, details.isolation_key, exit_identity
            );
        }
    }

    fn on_arti_failure(&self, details: FailedConnectionDetails) {
        if self.log_connections {
            warn!(
                "Failed to connect to '{}': {}",
                details.tor_dst, details.error
            );
        }
    }

    fn isolate(&self, src: IpEndpoint, dst: IpEndpoint, ip_proto: u8) -> io::Result<u64> {
        let iter = match ip_proto {
            6 => ProcNetEntry::tcp4()?.chain(ProcNetEntry::tcp6()?),
@@ -40,7 +113,7 @@ impl TunnelScaffolding for NetlinkScaffolding {
                    if IpEndpoint::from(v.local_addr) == src
                        && IpEndpoint::from(v.remote_addr) == dst
                    {
                        info!("isolated {src} -> {dst} proto {ip_proto} as {}", v.uid);
                        trace!("isolated {src} -> {dst} proto {ip_proto} as {}", v.uid);
                        return Ok(v.uid as u64);
                    }
                }
@@ -55,22 +128,112 @@ impl TunnelScaffolding for NetlinkScaffolding {
    }
}

const ENV_FILTER_VERBOSE: &str =
    "info,smoltcp=debug,onion_tunnel=trace,arti_client=debug,tor_chanmgr=debug,tor_proto=debug";
const ENV_FILTER_TAME: &str = "info";

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    FmtSubscriber::builder()
        .with_env_filter("info,smoltcp=debug,onion_tunnel=trace,arti_client=debug,tor_chanmgr=debug,tor_proto=debug")
        .init();
    let matches = Command::new("onionmasq")
        .version(env!("CARGO_PKG_VERSION"))
        .author("eta <eta@torproject.org>")
        .about("A magical TUN device that feeds traffic via Tor.")
        .arg(
            Arg::new("tun-device")
                .short('d')
                .long("device")
                .value_name("DEVICE")
                .default_value("onion0")
                .help(
                    "Name of a TUN device to use. Will attempt to create one if it doesn't exist.",
                ),
        )
        .arg(
            Arg::new("country-code")
                .short('c')
                .long("country-code")
                .value_name("DE|NL|etc")
                .help("Make traffic come from exit nodes in this country (ISO 3166-1 alpha-2 country code)"),
        )
        /* TODO(eta): make this a thing
        .arg(Arg::new("whole-system")
            .short('s')
            .long("whole-system")
            .help("Make all system traffic go through the onionmasq TUN device. Off by default.")
            .action(clap::ArgAction::SetTrue)
        )
         */
        .arg(Arg::new("debug")
            .long("debug")
            .help("Enable detailed debug logging. Off by default.")
            .action(clap::ArgAction::SetTrue)
        )
        .arg(Arg::new("verbose")
            .short('v')
            .long("verbose")
            .help("Print information about successful and failed connections. Off by default.")
            .action(clap::ArgAction::SetTrue))
        .get_matches();

    let filter = if matches.get_flag("debug") {
        ENV_FILTER_VERBOSE
    } else {
        ENV_FILTER_TAME
    };

    let log_connections = matches.get_flag("verbose");

    let country_code = matches
        .get_one("country-code")
        .map(|x: &String| CountryCode::from_str(x))
        .transpose()
        .context("invalid country code provided")?;

    let tun_device: &String = matches.get_one("tun-device").unwrap();

    FmtSubscriber::builder().with_env_filter(filter).init();

    info!("Starting tunnel on interface 'onion0'...");
    info!(
        "Starting onionmasq {} on device '{}'...",
        env!("CARGO_PKG_VERSION"),
        tun_device
    );

    let scaffolding = NetlinkScaffolding {
        netlink: Netlink::new().await,
        rt: Handle::current(),
    // Check whether we can call setsockopt(). If we can't, don't bother doing so in future, since
    // we probably don't have the capabilities; just print an explanatory warning instead.
    #[cfg(target_os = "linux")]
    let dummy_socket = TcpSocket::new_v4()?;
    #[cfg(target_os = "linux")]
    let can_mark = match LinuxScaffolding::mark_fd(dummy_socket.as_raw_fd()) {
        Ok(_) => {
            info!(
                "Outgoing connections (to the Tor network) will use fwmark {:#x}.",
                LinuxScaffolding::FWMARK
            );
            true
        }
        Err(e) => {
            warn!("Calling setsockopt() failed: {e}");
            warn!("Setting fwmarks on outgoing sockets will be disabled.");
            warn!("Make the onionmasq binary CAP_NET_ADMIN to fix this problem.");
            false
        }
    };

    #[cfg(not(target_os = "linux"))]
    let can_mark = false;

    let scaffolding = LinuxScaffolding {
        can_mark,
        cc: country_code,
        log_connections,
    };

    #[cfg(target_os = "linux")]
    let mut onion_tunnel = OnionTunnel::new(scaffolding, "onion0", Default::default()).await?;

    info!("Connecting to Tor...");

    #[cfg(target_os = "linux")]
    tokio::select! {
        _ = onion_tunnel.run() => (),

crates/onionmasq/src/netlink.rs

deleted100644 → 0
+0 −69
Original line number Diff line number Diff line
use std::net::{IpAddr, Ipv4Addr};

use futures::stream::TryStreamExt;

use ipnetwork::IpNetwork;
use netlink_packet_route::{RTN_UNICAST, RT_SCOPE_UNIVERSE};
use rtnetlink::{new_connection, Error, Handle, IpVersion};

/*
async fn get_link_index_by_name(handle: &Handle, name: &String) -> Result<u32, Error> {
    let mut links = handle.link().get().match_name(name.clone()).execute();
    if let Some(msg) = links.try_next().await? {
        Ok(msg.header.index)
    } else {
        Err(Error::RequestFailed)
    }
}
*/

async fn get_default_gateway4(handle: &Handle) -> Result<Ipv4Addr, Error> {
    let mut routes = handle.route().get(IpVersion::V4).execute();
    while let Some(msg) = routes.try_next().await? {
        if msg.header.destination_prefix_length == 0
            && msg.header.scope == RT_SCOPE_UNIVERSE
            && msg.header.kind == RTN_UNICAST
        {
            if let IpAddr::V4(v4) = msg.gateway().unwrap() {
                return Ok(v4);
            }
        }
    }
    Err(Error::RequestFailed)
}

#[derive(Clone)]
pub struct Netlink {
    gw4_addr: Ipv4Addr,
    handle: Handle,
}

impl Netlink {
    // XXX: Handle errors.
    pub async fn new() -> Self {
        let (conn, handle, _) = new_connection().unwrap();
        tokio::spawn(conn);

        Self {
            gw4_addr: get_default_gateway4(&handle).await.unwrap(),
            handle,
        }
    }

    pub async fn add_passthrough_route(&self, dest: &IpNetwork) -> Result<(), Error> {
        match dest {
            IpNetwork::V4(v4) => {
                self.handle
                    .route()
                    .add()
                    .v4()
                    .destination_prefix(v4.ip(), v4.prefix())
                    .gateway(self.gw4_addr)
                    .execute()
                    .await?;
            }
            IpNetwork::V6(_v6) => (),
        }
        Ok(())
    }
}