From d7c491c8f309f1818d0faf598d6a4a96e2a4fce0 Mon Sep 17 00:00:00 2001 From: Jake McGinty Date: Fri, 12 Nov 2021 14:42:10 +0900 Subject: [PATCH] client: granular control over NAT traversal added to `innernet {up,fetch,install}`: --no-nat-traversal: Doesn't attempt NAT traversal (prevents long time delays in execution of command) --exclude-nat-candidates: Exclude a list of CIDRs from being considered candidates --no-nat-candidates: Don't report NAT candidates. (shorthand for '--exclude-nat-candidates 0.0.0.0/0') Closes #160 --- client/src/main.rs | 87 +++++++++++++++++++++++++++++-------------- server/src/main.rs | 14 +++---- shared/src/netlink.rs | 3 +- shared/src/types.rs | 38 ++++++++++++++++++- shared/src/wg.rs | 4 +- 5 files changed, 107 insertions(+), 39 deletions(-) diff --git a/client/src/main.rs b/client/src/main.rs index bacd30d..7991cb8 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -9,8 +9,8 @@ use shared::{ prompts, wg::{DeviceExt, PeerInfoExt}, AddAssociationOpts, AddCidrOpts, AddPeerOpts, Association, AssociationContents, Cidr, CidrTree, - DeleteCidrOpts, Endpoint, EndpointContents, InstallOpts, Interface, IoErrorContext, NetworkOpt, - Peer, RedeemContents, RenamePeerOpts, State, WrappedIoError, CLIENT_CONFIG_DIR, + DeleteCidrOpts, Endpoint, EndpointContents, InstallOpts, Interface, IoErrorContext, NatOpts, + NetworkOpts, Peer, RedeemContents, RenamePeerOpts, State, WrappedIoError, CLIENT_CONFIG_DIR, REDEEM_TRANSITION_WAIT, }; use std::{ @@ -55,7 +55,7 @@ struct Opts { verbose: u64, #[structopt(flatten)] - network: NetworkOpt, + network: NetworkOpts, } #[derive(Debug, StructOpt)] @@ -88,6 +88,9 @@ enum Command { #[structopt(flatten)] opts: InstallOpts, + + #[structopt(flatten)] + nat: NatOpts, }, /// Enumerate all innernet connections. @@ -119,6 +122,9 @@ enum Command { #[structopt(flatten)] hosts: HostsOpt, + #[structopt(flatten)] + nat: NatOpts, + interface: Interface, }, @@ -128,6 +134,9 @@ enum Command { #[structopt(flatten)] hosts: HostsOpt, + + #[structopt(flatten)] + nat: NatOpts, }, /// Uninstall an innernet network. @@ -273,7 +282,8 @@ fn install( invite: &Path, hosts_file: Option, opts: InstallOpts, - network: NetworkOpt, + network: NetworkOpts, + nat: &NatOpts, ) -> Result<(), Error> { shared::ensure_dirs_exist(&[*CLIENT_CONFIG_DIR])?; let config = InterfaceConfig::from_file(invite)?; @@ -320,7 +330,15 @@ fn install( let mut fetch_success = false; for _ in 0..3 { - if fetch(&iface, true, hosts_file.clone(), network).is_ok() { + if fetch( + &iface, + true, + hosts_file.clone(), + network, + nat, + ) + .is_ok() + { fetch_success = true; break; } @@ -405,7 +423,7 @@ fn redeem_invite( iface: &InterfaceName, mut config: InterfaceConfig, target_conf: PathBuf, - network: NetworkOpt, + network: NetworkOpts, ) -> Result<(), Error> { log::info!("bringing up the interface."); let resolved_endpoint = config @@ -463,10 +481,11 @@ fn up( interface: &InterfaceName, loop_interval: Option, hosts_path: Option, - routing: NetworkOpt, + routing: NetworkOpts, + nat: &NatOpts, ) -> Result<(), Error> { loop { - fetch(interface, true, hosts_path.clone(), routing)?; + fetch(interface, true, hosts_path.clone(), routing, nat)?; match loop_interval { Some(interval) => thread::sleep(interval), None => break, @@ -480,7 +499,8 @@ fn fetch( interface: &InterfaceName, bring_up_interface: bool, hosts_path: Option, - network: NetworkOpt, + network: NetworkOpts, + nat: &NatOpts, ) -> Result<(), Error> { let config = InterfaceConfig::from_interface(interface)?; let interface_up = match Device::list(network.backend) { @@ -553,6 +573,7 @@ fn fetch( store.write().with_str(interface.to_string())?; let candidates: Vec = get_local_addrs()? + .filter(|ip| !nat.is_excluded(*ip)) .map(|addr| SocketAddr::from((addr, device.listen_port.unwrap_or(51820))).into()) .collect::>(); log::info!( @@ -569,26 +590,31 @@ fn fetch( } log::debug!("reported candidates: {:?}", candidates); - let mut nat_traverse = NatTraverse::new(interface, network.backend, &modifications)?; + if nat.no_nat_traversal { + log::debug!("NAT traversal explicitly disabled, not attempting."); + } else { + let mut nat_traverse = NatTraverse::new(interface, network.backend, &modifications)?; - if !nat_traverse.is_finished() { - thread::sleep(nat::STEP_INTERVAL - interface_updated_time.elapsed()); - } - loop { - if nat_traverse.is_finished() { - break; + // Give time for handshakes with recently changed endpoints to complete before attempting traversal. + if !nat_traverse.is_finished() { + thread::sleep(nat::STEP_INTERVAL - interface_updated_time.elapsed()); + } + loop { + if nat_traverse.is_finished() { + break; + } + log::info!( + "Attempting to establish connection with {} remaining unconnected peers...", + nat_traverse.remaining() + ); + nat_traverse.step()?; } - log::info!( - "Attempting to establish connection with {} remaining unconnected peers...", - nat_traverse.remaining() - ); - nat_traverse.step()?; } Ok(()) } -fn uninstall(interface: &InterfaceName, network: NetworkOpt) -> Result<(), Error> { +fn uninstall(interface: &InterfaceName, network: NetworkOpts) -> Result<(), Error> { if Confirm::with_theme(&*prompts::THEME) .with_prompt(&format!( "Permanently delete network \"{}\"?", @@ -841,7 +867,7 @@ fn list_associations(interface: &InterfaceName) -> Result<(), Error> { fn set_listen_port( interface: &InterfaceName, unset: bool, - network: NetworkOpt, + network: NetworkOpts, ) -> Result, Error> { let mut config = InterfaceConfig::from_interface(interface)?; @@ -863,7 +889,7 @@ fn set_listen_port( fn override_endpoint( interface: &InterfaceName, unset: bool, - network: NetworkOpt, + network: NetworkOpts, ) -> Result<(), Error> { let config = InterfaceConfig::from_interface(interface)?; let endpoint_contents = if unset { @@ -900,7 +926,7 @@ fn show( short: bool, tree: bool, interface: Option, - network: NetworkOpt, + network: NetworkOpts, ) -> Result<(), Error> { let interfaces = interface.map_or_else( || Device::list(network.backend), @@ -1100,23 +1126,30 @@ fn run(opt: Opts) -> Result<(), Error> { invite, hosts, opts, - } => install(&invite, hosts.into(), opts, opt.network)?, + nat, + } => install(&invite, hosts.into(), opts, opt.network, &nat)?, Command::Show { short, tree, interface, } => show(short, tree, interface, opt.network)?, - Command::Fetch { interface, hosts } => fetch(&interface, false, hosts.into(), opt.network)?, + Command::Fetch { + interface, + hosts, + nat, + } => fetch(&interface, false, hosts.into(), opt.network, &nat)?, Command::Up { interface, daemon, hosts, + nat, interval, } => up( &interface, daemon.then(|| Duration::from_secs(interval)), hosts.into(), opt.network, + &nat, )?, Command::Down { interface } => wg::down(&interface, opt.network.backend)?, Command::Uninstall { interface } => uninstall(&interface, opt.network)?, diff --git a/server/src/main.rs b/server/src/main.rs index 45ca4cd..d85523b 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -9,7 +9,7 @@ use rusqlite::Connection; use serde::{Deserialize, Serialize}; use shared::{ get_local_addrs, AddCidrOpts, AddPeerOpts, DeleteCidrOpts, Endpoint, IoErrorContext, - NetworkOpt, PeerContents, RenamePeerOpts, INNERNET_PUBKEY_HEADER, + NetworkOpts, PeerContents, RenamePeerOpts, INNERNET_PUBKEY_HEADER, }; use std::{ collections::{HashMap, VecDeque}, @@ -51,7 +51,7 @@ struct Opt { command: Command, #[structopt(flatten)] - network: NetworkOpt, + network: NetworkOpts, } #[derive(Debug, StructOpt)] @@ -71,7 +71,7 @@ enum Command { interface: Interface, #[structopt(flatten)] - network: NetworkOpt, + network: NetworkOpts, }, /// Add a peer to an existing network. @@ -282,7 +282,7 @@ fn add_peer( interface: &InterfaceName, conf: &ServerConfig, opts: AddPeerOpts, - network: NetworkOpt, + network: NetworkOpts, ) -> Result<(), Error> { let config = ConfigFile::from_file(conf.config_path(interface))?; let conn = open_database_connection(interface, conf)?; @@ -400,7 +400,7 @@ fn delete_cidr( fn uninstall( interface: &InterfaceName, conf: &ServerConfig, - network: NetworkOpt, + network: NetworkOpts, ) -> Result<(), Error> { if Confirm::with_theme(&*prompts::THEME) .with_prompt(&format!( @@ -431,7 +431,7 @@ fn uninstall( Ok(()) } -fn spawn_endpoint_refresher(interface: InterfaceName, network: NetworkOpt) -> Endpoints { +fn spawn_endpoint_refresher(interface: InterfaceName, network: NetworkOpts) -> Endpoints { let endpoints = Arc::new(RwLock::new(HashMap::new())); tokio::task::spawn({ let endpoints = endpoints.clone(); @@ -473,7 +473,7 @@ fn spawn_expired_invite_sweeper(db: Db) { async fn serve( interface: InterfaceName, conf: &ServerConfig, - network: NetworkOpt, + network: NetworkOpts, ) -> Result<(), Error> { let config = ConfigFile::from_file(conf.config_path(&interface))?; log::debug!("opening database connection..."); diff --git a/shared/src/netlink.rs b/shared/src/netlink.rs index 58a2802..4e329dc 100644 --- a/shared/src/netlink.rs +++ b/shared/src/netlink.rs @@ -34,7 +34,7 @@ fn netlink_call( req.serialize(&mut buf); let len = req.buffer_len(); - log::debug!("netlink request: {:?}", req); + log::trace!("netlink request: {:?}", req); let socket = Socket::new(NETLINK_ROUTE)?; let kernel_addr = SocketAddr::new(0, 0); socket.connect(&kernel_addr)?; @@ -66,7 +66,6 @@ fn netlink_call( if offset == n_received || response.header.length == 0 { // We've fully parsed the datagram, but there may be further datagrams // with additional netlink response parts. - log::debug!("breaking inner loop"); break; } } diff --git a/shared/src/types.rs b/shared/src/types.rs index 0bbcfa0..7bac86d 100644 --- a/shared/src/types.rs +++ b/shared/src/types.rs @@ -387,8 +387,44 @@ pub struct AddAssociationOpts { pub cidr2: Option, } +#[derive(Debug, Clone, StructOpt)] +pub struct NatOpts { + #[structopt(long)] + /// Don't attempt NAT traversal. Note that this still will report candidates + /// unless you also specify to exclude all NAT candidates. + pub no_nat_traversal: bool, + + #[structopt(long)] + /// Exclude one or more CIDRs from NAT candidate reporting. + /// ex. --exclude-nat-candidates '0/0' would report no candidates. + pub exclude_nat_candidates: Vec, + + #[structopt(long, conflicts_with = "exclude-nat-candidates")] + /// Don't report any candidates to coordinating server. + /// Shorthand for --exclude-nat-candidates '0.0.0.0/0'. + pub no_nat_candidates: bool, +} + +impl NatOpts { + pub fn all_disabled() -> Self { + Self { + no_nat_traversal: true, + exclude_nat_candidates: vec![], + no_nat_candidates: true, + } + } + /// Check if an IP is allowed to be reported as a candidate. + pub fn is_excluded(&self, ip: IpAddr) -> bool { + self.no_nat_candidates + || self + .exclude_nat_candidates + .iter() + .any(|network| network.contains(ip)) + } +} + #[derive(Debug, Clone, Copy, StructOpt)] -pub struct NetworkOpt { +pub struct NetworkOpts { #[structopt(long)] /// Whether the routing should be done by innernet or is done by an /// external tool like e.g. babeld. diff --git a/shared/src/wg.rs b/shared/src/wg.rs index 82ba8ee..5a75317 100644 --- a/shared/src/wg.rs +++ b/shared/src/wg.rs @@ -1,4 +1,4 @@ -use crate::{Error, IoErrorContext, NetworkOpt, Peer, PeerDiff}; +use crate::{Error, IoErrorContext, NetworkOpts, Peer, PeerDiff}; use ipnetwork::IpNetwork; use std::{ io, @@ -75,7 +75,7 @@ pub fn up( address: IpNetwork, listen_port: Option, peer: Option<(&str, IpAddr, SocketAddr)>, - network: NetworkOpt, + network: NetworkOpts, ) -> Result<(), io::Error> { let mut device = DeviceUpdate::new(); if let Some((public_key, address, endpoint)) = peer {