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
This commit is contained in:
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,
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<PathBuf>,
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<Duration>,
hosts_path: Option<PathBuf>,
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<PathBuf>,
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<Endpoint> = get_local_addrs()?
.filter(|ip| !nat.is_excluded(*ip))
.map(|addr| SocketAddr::from((addr, device.listen_port.unwrap_or(51820))).into())
.collect::<Vec<Endpoint>>();
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<Option<u16>, 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<Interface>,
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)?,

View file

@ -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...");

View file

@ -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;
}
}

View file

@ -387,8 +387,44 @@ pub struct AddAssociationOpts {
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)]
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.

View file

@ -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<u16>,
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 {