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
pull/172/head
Jake McGinty 2021-11-12 14:42:10 +09:00
parent 9a59ac3094
commit d7c491c8f3
5 changed files with 107 additions and 39 deletions

View File

@ -9,8 +9,8 @@ use shared::{
prompts, prompts,
wg::{DeviceExt, PeerInfoExt}, wg::{DeviceExt, PeerInfoExt},
AddAssociationOpts, AddCidrOpts, AddPeerOpts, Association, AssociationContents, Cidr, CidrTree, AddAssociationOpts, AddCidrOpts, AddPeerOpts, Association, AssociationContents, Cidr, CidrTree,
DeleteCidrOpts, Endpoint, EndpointContents, InstallOpts, Interface, IoErrorContext, NetworkOpt, DeleteCidrOpts, Endpoint, EndpointContents, InstallOpts, Interface, IoErrorContext, NatOpts,
Peer, RedeemContents, RenamePeerOpts, State, WrappedIoError, CLIENT_CONFIG_DIR, NetworkOpts, Peer, RedeemContents, RenamePeerOpts, State, WrappedIoError, CLIENT_CONFIG_DIR,
REDEEM_TRANSITION_WAIT, REDEEM_TRANSITION_WAIT,
}; };
use std::{ use std::{
@ -55,7 +55,7 @@ struct Opts {
verbose: u64, verbose: u64,
#[structopt(flatten)] #[structopt(flatten)]
network: NetworkOpt, network: NetworkOpts,
} }
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]
@ -88,6 +88,9 @@ enum Command {
#[structopt(flatten)] #[structopt(flatten)]
opts: InstallOpts, opts: InstallOpts,
#[structopt(flatten)]
nat: NatOpts,
}, },
/// Enumerate all innernet connections. /// Enumerate all innernet connections.
@ -119,6 +122,9 @@ enum Command {
#[structopt(flatten)] #[structopt(flatten)]
hosts: HostsOpt, hosts: HostsOpt,
#[structopt(flatten)]
nat: NatOpts,
interface: Interface, interface: Interface,
}, },
@ -128,6 +134,9 @@ enum Command {
#[structopt(flatten)] #[structopt(flatten)]
hosts: HostsOpt, hosts: HostsOpt,
#[structopt(flatten)]
nat: NatOpts,
}, },
/// Uninstall an innernet network. /// Uninstall an innernet network.
@ -273,7 +282,8 @@ fn install(
invite: &Path, invite: &Path,
hosts_file: Option<PathBuf>, hosts_file: Option<PathBuf>,
opts: InstallOpts, opts: InstallOpts,
network: NetworkOpt, network: NetworkOpts,
nat: &NatOpts,
) -> Result<(), Error> { ) -> Result<(), Error> {
shared::ensure_dirs_exist(&[*CLIENT_CONFIG_DIR])?; shared::ensure_dirs_exist(&[*CLIENT_CONFIG_DIR])?;
let config = InterfaceConfig::from_file(invite)?; let config = InterfaceConfig::from_file(invite)?;
@ -320,7 +330,15 @@ fn install(
let mut fetch_success = false; let mut fetch_success = false;
for _ in 0..3 { 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; fetch_success = true;
break; break;
} }
@ -405,7 +423,7 @@ fn redeem_invite(
iface: &InterfaceName, iface: &InterfaceName,
mut config: InterfaceConfig, mut config: InterfaceConfig,
target_conf: PathBuf, target_conf: PathBuf,
network: NetworkOpt, network: NetworkOpts,
) -> Result<(), Error> { ) -> Result<(), Error> {
log::info!("bringing up the interface."); log::info!("bringing up the interface.");
let resolved_endpoint = config let resolved_endpoint = config
@ -463,10 +481,11 @@ fn up(
interface: &InterfaceName, interface: &InterfaceName,
loop_interval: Option<Duration>, loop_interval: Option<Duration>,
hosts_path: Option<PathBuf>, hosts_path: Option<PathBuf>,
routing: NetworkOpt, routing: NetworkOpts,
nat: &NatOpts,
) -> Result<(), Error> { ) -> Result<(), Error> {
loop { loop {
fetch(interface, true, hosts_path.clone(), routing)?; fetch(interface, true, hosts_path.clone(), routing, nat)?;
match loop_interval { match loop_interval {
Some(interval) => thread::sleep(interval), Some(interval) => thread::sleep(interval),
None => break, None => break,
@ -480,7 +499,8 @@ fn fetch(
interface: &InterfaceName, interface: &InterfaceName,
bring_up_interface: bool, bring_up_interface: bool,
hosts_path: Option<PathBuf>, hosts_path: Option<PathBuf>,
network: NetworkOpt, network: NetworkOpts,
nat: &NatOpts,
) -> Result<(), Error> { ) -> Result<(), Error> {
let config = InterfaceConfig::from_interface(interface)?; let config = InterfaceConfig::from_interface(interface)?;
let interface_up = match Device::list(network.backend) { let interface_up = match Device::list(network.backend) {
@ -553,6 +573,7 @@ fn fetch(
store.write().with_str(interface.to_string())?; store.write().with_str(interface.to_string())?;
let candidates: Vec<Endpoint> = get_local_addrs()? let candidates: Vec<Endpoint> = get_local_addrs()?
.filter(|ip| !nat.is_excluded(*ip))
.map(|addr| SocketAddr::from((addr, device.listen_port.unwrap_or(51820))).into()) .map(|addr| SocketAddr::from((addr, device.listen_port.unwrap_or(51820))).into())
.collect::<Vec<Endpoint>>(); .collect::<Vec<Endpoint>>();
log::info!( log::info!(
@ -569,8 +590,12 @@ fn fetch(
} }
log::debug!("reported candidates: {:?}", candidates); log::debug!("reported candidates: {:?}", candidates);
if nat.no_nat_traversal {
log::debug!("NAT traversal explicitly disabled, not attempting.");
} else {
let mut nat_traverse = NatTraverse::new(interface, network.backend, &modifications)?; let mut nat_traverse = NatTraverse::new(interface, network.backend, &modifications)?;
// Give time for handshakes with recently changed endpoints to complete before attempting traversal.
if !nat_traverse.is_finished() { if !nat_traverse.is_finished() {
thread::sleep(nat::STEP_INTERVAL - interface_updated_time.elapsed()); thread::sleep(nat::STEP_INTERVAL - interface_updated_time.elapsed());
} }
@ -584,11 +609,12 @@ fn fetch(
); );
nat_traverse.step()?; nat_traverse.step()?;
} }
}
Ok(()) Ok(())
} }
fn uninstall(interface: &InterfaceName, network: NetworkOpt) -> Result<(), Error> { fn uninstall(interface: &InterfaceName, network: NetworkOpts) -> Result<(), Error> {
if Confirm::with_theme(&*prompts::THEME) if Confirm::with_theme(&*prompts::THEME)
.with_prompt(&format!( .with_prompt(&format!(
"Permanently delete network \"{}\"?", "Permanently delete network \"{}\"?",
@ -841,7 +867,7 @@ fn list_associations(interface: &InterfaceName) -> Result<(), Error> {
fn set_listen_port( fn set_listen_port(
interface: &InterfaceName, interface: &InterfaceName,
unset: bool, unset: bool,
network: NetworkOpt, network: NetworkOpts,
) -> Result<Option<u16>, Error> { ) -> Result<Option<u16>, Error> {
let mut config = InterfaceConfig::from_interface(interface)?; let mut config = InterfaceConfig::from_interface(interface)?;
@ -863,7 +889,7 @@ fn set_listen_port(
fn override_endpoint( fn override_endpoint(
interface: &InterfaceName, interface: &InterfaceName,
unset: bool, unset: bool,
network: NetworkOpt, network: NetworkOpts,
) -> Result<(), Error> { ) -> Result<(), Error> {
let config = InterfaceConfig::from_interface(interface)?; let config = InterfaceConfig::from_interface(interface)?;
let endpoint_contents = if unset { let endpoint_contents = if unset {
@ -900,7 +926,7 @@ fn show(
short: bool, short: bool,
tree: bool, tree: bool,
interface: Option<Interface>, interface: Option<Interface>,
network: NetworkOpt, network: NetworkOpts,
) -> Result<(), Error> { ) -> Result<(), Error> {
let interfaces = interface.map_or_else( let interfaces = interface.map_or_else(
|| Device::list(network.backend), || Device::list(network.backend),
@ -1100,23 +1126,30 @@ fn run(opt: Opts) -> Result<(), Error> {
invite, invite,
hosts, hosts,
opts, opts,
} => install(&invite, hosts.into(), opts, opt.network)?, nat,
} => install(&invite, hosts.into(), opts, opt.network, &nat)?,
Command::Show { Command::Show {
short, short,
tree, tree,
interface, interface,
} => show(short, tree, interface, opt.network)?, } => 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 { Command::Up {
interface, interface,
daemon, daemon,
hosts, hosts,
nat,
interval, interval,
} => up( } => up(
&interface, &interface,
daemon.then(|| Duration::from_secs(interval)), daemon.then(|| Duration::from_secs(interval)),
hosts.into(), hosts.into(),
opt.network, opt.network,
&nat,
)?, )?,
Command::Down { interface } => wg::down(&interface, opt.network.backend)?, Command::Down { interface } => wg::down(&interface, opt.network.backend)?,
Command::Uninstall { interface } => uninstall(&interface, opt.network)?, Command::Uninstall { interface } => uninstall(&interface, opt.network)?,

View File

@ -9,7 +9,7 @@ use rusqlite::Connection;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use shared::{ use shared::{
get_local_addrs, AddCidrOpts, AddPeerOpts, DeleteCidrOpts, Endpoint, IoErrorContext, get_local_addrs, AddCidrOpts, AddPeerOpts, DeleteCidrOpts, Endpoint, IoErrorContext,
NetworkOpt, PeerContents, RenamePeerOpts, INNERNET_PUBKEY_HEADER, NetworkOpts, PeerContents, RenamePeerOpts, INNERNET_PUBKEY_HEADER,
}; };
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
@ -51,7 +51,7 @@ struct Opt {
command: Command, command: Command,
#[structopt(flatten)] #[structopt(flatten)]
network: NetworkOpt, network: NetworkOpts,
} }
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]
@ -71,7 +71,7 @@ enum Command {
interface: Interface, interface: Interface,
#[structopt(flatten)] #[structopt(flatten)]
network: NetworkOpt, network: NetworkOpts,
}, },
/// Add a peer to an existing network. /// Add a peer to an existing network.
@ -282,7 +282,7 @@ fn add_peer(
interface: &InterfaceName, interface: &InterfaceName,
conf: &ServerConfig, conf: &ServerConfig,
opts: AddPeerOpts, opts: AddPeerOpts,
network: NetworkOpt, network: NetworkOpts,
) -> Result<(), Error> { ) -> Result<(), Error> {
let config = ConfigFile::from_file(conf.config_path(interface))?; let config = ConfigFile::from_file(conf.config_path(interface))?;
let conn = open_database_connection(interface, conf)?; let conn = open_database_connection(interface, conf)?;
@ -400,7 +400,7 @@ fn delete_cidr(
fn uninstall( fn uninstall(
interface: &InterfaceName, interface: &InterfaceName,
conf: &ServerConfig, conf: &ServerConfig,
network: NetworkOpt, network: NetworkOpts,
) -> Result<(), Error> { ) -> Result<(), Error> {
if Confirm::with_theme(&*prompts::THEME) if Confirm::with_theme(&*prompts::THEME)
.with_prompt(&format!( .with_prompt(&format!(
@ -431,7 +431,7 @@ fn uninstall(
Ok(()) 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())); let endpoints = Arc::new(RwLock::new(HashMap::new()));
tokio::task::spawn({ tokio::task::spawn({
let endpoints = endpoints.clone(); let endpoints = endpoints.clone();
@ -473,7 +473,7 @@ fn spawn_expired_invite_sweeper(db: Db) {
async fn serve( async fn serve(
interface: InterfaceName, interface: InterfaceName,
conf: &ServerConfig, conf: &ServerConfig,
network: NetworkOpt, network: NetworkOpts,
) -> Result<(), Error> { ) -> Result<(), Error> {
let config = ConfigFile::from_file(conf.config_path(&interface))?; let config = ConfigFile::from_file(conf.config_path(&interface))?;
log::debug!("opening database connection..."); log::debug!("opening database connection...");

View File

@ -34,7 +34,7 @@ fn netlink_call(
req.serialize(&mut buf); req.serialize(&mut buf);
let len = req.buffer_len(); let len = req.buffer_len();
log::debug!("netlink request: {:?}", req); log::trace!("netlink request: {:?}", req);
let socket = Socket::new(NETLINK_ROUTE)?; let socket = Socket::new(NETLINK_ROUTE)?;
let kernel_addr = SocketAddr::new(0, 0); let kernel_addr = SocketAddr::new(0, 0);
socket.connect(&kernel_addr)?; socket.connect(&kernel_addr)?;
@ -66,7 +66,6 @@ fn netlink_call(
if offset == n_received || response.header.length == 0 { if offset == n_received || response.header.length == 0 {
// We've fully parsed the datagram, but there may be further datagrams // We've fully parsed the datagram, but there may be further datagrams
// with additional netlink response parts. // with additional netlink response parts.
log::debug!("breaking inner loop");
break; break;
} }
} }

View File

@ -387,8 +387,44 @@ pub struct AddAssociationOpts {
pub cidr2: Option<String>, pub cidr2: Option<String>,
} }
#[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<IpNetwork>,
#[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)] #[derive(Debug, Clone, Copy, StructOpt)]
pub struct NetworkOpt { pub struct NetworkOpts {
#[structopt(long)] #[structopt(long)]
/// Whether the routing should be done by innernet or is done by an /// Whether the routing should be done by innernet or is done by an
/// external tool like e.g. babeld. /// external tool like e.g. babeld.

View File

@ -1,4 +1,4 @@
use crate::{Error, IoErrorContext, NetworkOpt, Peer, PeerDiff}; use crate::{Error, IoErrorContext, NetworkOpts, Peer, PeerDiff};
use ipnetwork::IpNetwork; use ipnetwork::IpNetwork;
use std::{ use std::{
io, io,
@ -75,7 +75,7 @@ pub fn up(
address: IpNetwork, address: IpNetwork,
listen_port: Option<u16>, listen_port: Option<u16>,
peer: Option<(&str, IpAddr, SocketAddr)>, peer: Option<(&str, IpAddr, SocketAddr)>,
network: NetworkOpt, network: NetworkOpts,
) -> Result<(), io::Error> { ) -> Result<(), io::Error> {
let mut device = DeviceUpdate::new(); let mut device = DeviceUpdate::new();
if let Some((public_key, address, endpoint)) = peer { if let Some((public_key, address, endpoint)) = peer {