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 #160pull/172/head
parent
9a59ac3094
commit
d7c491c8f3
|
@ -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)?,
|
||||||
|
|
|
@ -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...");
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue