From 3892a991564289c42a95d9604354a475076d6197 Mon Sep 17 00:00:00 2001 From: Jake McGinty Date: Wed, 19 May 2021 16:54:07 +0900 Subject: [PATCH] wgctrl: use wireguard backends explicitly (with OS-specific defaults) (#85) Based on the conversation from #5 (comment) - this changes innernet's behavior on Linux from automatically falling back to the userspace, instead requiring --backend userspace to be specified. This should help people avoid weird situations in environments like Docker. --- Cargo.lock | 41 ++--- client/src/data_store.rs | 6 +- client/src/main.rs | 114 +++++++------ server/src/api/admin/peer.rs | 6 +- server/src/api/user.rs | 12 +- server/src/main.rs | 51 +++--- server/src/test.rs | 6 +- shared/src/interface_config.rs | 6 +- shared/src/lib.rs | 11 +- shared/src/types.rs | 11 +- shared/src/wg.rs | 34 ++-- wgctrl-rs/src/backends/kernel.rs | 48 ++---- wgctrl-rs/src/backends/userspace.rs | 17 +- wgctrl-rs/src/config.rs | 200 +---------------------- wgctrl-rs/src/device.rs | 243 +++++++++++++++++++++++----- wgctrl-rs/src/lib.rs | 59 +++++++ 16 files changed, 450 insertions(+), 415 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc81283..4209c96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -278,31 +278,32 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce79c6a52a299137a6013061e0cf0e688fce5d7f1bc60125f520912fdb29ec25" +checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "098cd1c6dda6ca01650f1a37a794245eb73181d0d4d4e955e2f3c37db7af1815" +checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" [[package]] name = "futures-task" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba7aa51095076f3ba6d9a1f702f74bd05ec65f555d70d2033d55ba8d69f581bc" +checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" [[package]] name = "futures-util" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c144ad54d60f23927f0a6b6d816e4271278b64f005ad65e4e35291d2de9c025" +checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" dependencies = [ + "autocfg", "futures-core", "futures-task", "pin-project-lite", @@ -409,9 +410,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1ce40d6fc9764887c2fdc7305c3dcc429ba11ff981c1509416afd5697e4437" +checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" [[package]] name = "httpdate" @@ -862,18 +863,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.125" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.125" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" dependencies = [ "proc-macro2", "quote", @@ -1048,9 +1049,9 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ca8ced750734db02076f44132d802af0b33b09942331f4459dde8636fd2406" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" dependencies = [ "libc", "winapi", @@ -1102,9 +1103,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f0c8e7c0addab50b663055baf787d0af7f413a46e6e7fb9559a4e4db7137a5" +checksum = "bd3076b5c8cc18138b8f8814895c11eb4de37114a5d127bafdc5e55798ceef37" dependencies = [ "autocfg", "libc", @@ -1116,9 +1117,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57" +checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" dependencies = [ "proc-macro2", "quote", diff --git a/client/src/data_store.rs b/client/src/data_store.rs index 17649c5..1f07829 100644 --- a/client/src/data_store.rs +++ b/client/src/data_store.rs @@ -1,7 +1,7 @@ use crate::Error; use colored::*; use serde::{Deserialize, Serialize}; -use shared::{ensure_dirs_exist, Cidr, IoErrorContext, Peer, WrappedIoError, CLIENT_DATA_PATH}; +use shared::{ensure_dirs_exist, Cidr, IoErrorContext, Peer, WrappedIoError, CLIENT_DATA_DIR}; use std::{ fs::{File, OpenOptions}, io::{Read, Seek, SeekFrom, Write}, @@ -54,13 +54,13 @@ impl DataStore { } pub fn get_path(interface: &InterfaceName) -> PathBuf { - CLIENT_DATA_PATH + CLIENT_DATA_DIR .join(interface.to_string()) .with_extension("json") } fn _open(interface: &InterfaceName, create: bool) -> Result { - ensure_dirs_exist(&[*CLIENT_DATA_PATH])?; + ensure_dirs_exist(&[*CLIENT_DATA_DIR])?; Self::open_with_path(Self::get_path(interface), create) } diff --git a/client/src/main.rs b/client/src/main.rs index fffebf5..f4724c6 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -5,7 +5,7 @@ use indoc::printdoc; use shared::{ interface_config::InterfaceConfig, prompts, AddAssociationOpts, AddCidrOpts, AddPeerOpts, Association, AssociationContents, Cidr, CidrTree, EndpointContents, InstallOpts, Interface, - IoErrorContext, Peer, RedeemContents, RoutingOpt, State, CLIENT_CONFIG_PATH, + IoErrorContext, NetworkOpt, Peer, RedeemContents, State, CLIENT_CONFIG_DIR, REDEEM_TRANSITION_WAIT, }; use std::{ @@ -15,7 +15,7 @@ use std::{ time::{Duration, SystemTime}, }; use structopt::StructOpt; -use wgctrl::{DeviceConfigBuilder, DeviceInfo, InterfaceName, PeerConfigBuilder, PeerInfo}; +use wgctrl::{Device, DeviceUpdate, InterfaceName, PeerConfigBuilder, PeerInfo}; mod data_store; mod util; @@ -41,6 +41,9 @@ macro_rules! println_pad { struct Opts { #[structopt(subcommand)] command: Option, + + #[structopt(flatten)] + network: NetworkOpt, } #[derive(Debug, StructOpt)] @@ -73,9 +76,6 @@ enum Command { #[structopt(flatten)] opts: InstallOpts, - - #[structopt(flatten)] - routing: RoutingOpt, }, /// Enumerate all innernet connections. @@ -107,9 +107,6 @@ enum Command { #[structopt(flatten)] hosts: HostsOpt, - #[structopt(flatten)] - routing: RoutingOpt, - interface: Interface, }, @@ -119,9 +116,6 @@ enum Command { #[structopt(flatten)] hosts: HostsOpt, - - #[structopt(flatten)] - routing: RoutingOpt, }, /// Uninstall an innernet network. @@ -233,9 +227,9 @@ fn install( invite: &Path, hosts_file: Option, opts: InstallOpts, - routing: RoutingOpt, + network: NetworkOpt, ) -> Result<(), Error> { - shared::ensure_dirs_exist(&[*CLIENT_CONFIG_PATH])?; + shared::ensure_dirs_exist(&[*CLIENT_CONFIG_DIR])?; let config = InterfaceConfig::from_file(invite)?; let iface = if opts.default_name { @@ -249,15 +243,15 @@ fn install( .interact()? }; - let target_conf = CLIENT_CONFIG_PATH.join(&iface).with_extension("conf"); + let target_conf = CLIENT_CONFIG_DIR.join(&iface).with_extension("conf"); if target_conf.exists() { return Err("An interface with this name already exists in innernet.".into()); } let iface = iface.parse()?; - redeem_invite(&iface, config, target_conf, routing).map_err(|e| { + redeem_invite(&iface, config, target_conf, network).map_err(|e| { println!("{} bringing down the interface.", "[*]".dimmed()); - if let Err(e) = wg::down(&iface) { + if let Err(e) = wg::down(&iface, network.backend) { println!("{} failed to bring down interface: {}.", "[*]".yellow(), e.to_string()); }; println!("{} Failed to redeem invite. Now's a good time to make sure the server is started and accessible!", "[!]".red()); @@ -266,7 +260,7 @@ fn install( let mut fetch_success = false; for _ in 0..3 { - if fetch(&iface, false, hosts_file.clone(), routing).is_ok() { + if fetch(&iface, false, hosts_file.clone(), network).is_ok() { fetch_success = true; break; } @@ -320,7 +314,7 @@ fn redeem_invite( iface: &InterfaceName, mut config: InterfaceConfig, target_conf: PathBuf, - routing: RoutingOpt, + network: NetworkOpt, ) -> Result<(), Error> { println!("{} bringing up the interface.", "[*]".dimmed()); let resolved_endpoint = config.server.external_endpoint.resolve()?; @@ -334,7 +328,7 @@ fn redeem_invite( config.server.internal_endpoint.ip(), resolved_endpoint, )), - !routing.no_routing, + network, )?; println!("{} Generating new keypair.", "[*]".dimmed()); @@ -365,9 +359,9 @@ fn redeem_invite( "{} Changing keys and waiting for server's WireGuard interface to transition.", "[*]".dimmed(), ); - DeviceConfigBuilder::new() + DeviceUpdate::new() .set_private_key(keypair.private) - .apply(&iface)?; + .apply(&iface, network.backend)?; thread::sleep(*REDEEM_TRANSITION_WAIT); Ok(()) @@ -377,7 +371,7 @@ fn up( interface: &InterfaceName, loop_interval: Option, hosts_path: Option, - routing: RoutingOpt, + routing: NetworkOpt, ) -> Result<(), Error> { loop { fetch(interface, true, hosts_path.clone(), routing)?; @@ -394,10 +388,10 @@ fn fetch( interface: &InterfaceName, bring_up_interface: bool, hosts_path: Option, - routing: RoutingOpt, + network: NetworkOpt, ) -> Result<(), Error> { let config = InterfaceConfig::from_interface(interface)?; - let interface_up = if let Ok(interfaces) = DeviceInfo::enumerate() { + let interface_up = if let Ok(interfaces) = Device::list(network.backend) { interfaces.iter().any(|name| name == interface) } else { false @@ -424,7 +418,7 @@ fn fetch( config.server.internal_endpoint.ip(), resolved_endpoint, )), - !routing.no_routing, + network, )? } @@ -432,7 +426,8 @@ fn fetch( let mut store = DataStore::open_or_create(&interface)?; let State { peers, cidrs } = Api::new(&config.server).http("GET", "/user/state")?; - let device_info = DeviceInfo::get_by_name(&interface).with_str(interface.as_str_lossy())?; + let device_info = + Device::get(&interface, network.backend).with_str(interface.as_str_lossy())?; let interface_public_key = device_info .public_key .as_ref() @@ -467,7 +462,7 @@ fn fetch( }) .collect::>(); - let mut device_config_builder = DeviceConfigBuilder::new(); + let mut device_config_builder = DeviceUpdate::new(); let mut device_config_changed = false; if !peer_configs_diff.is_empty() { @@ -491,7 +486,7 @@ fn fetch( } if device_config_changed { - device_config_builder.apply(&interface)?; + device_config_builder.apply(&interface, network.backend)?; if let Some(path) = hosts_path { update_hosts_file(interface, path, &peers)?; @@ -512,7 +507,7 @@ fn fetch( Ok(()) } -fn uninstall(interface: &InterfaceName) -> Result<(), Error> { +fn uninstall(interface: &InterfaceName, network: NetworkOpt) -> Result<(), Error> { if Confirm::with_theme(&*prompts::THEME) .with_prompt(&format!( "Permanently delete network \"{}\"?", @@ -522,7 +517,7 @@ fn uninstall(interface: &InterfaceName) -> Result<(), Error> { .interact()? { println!("{} bringing down interface (if up).", "[*]".dimmed()); - wg::down(interface).ok(); + wg::down(interface, network.backend).ok(); let config = InterfaceConfig::get_path(interface); let data = DataStore::get_path(interface); std::fs::remove_file(&config) @@ -701,11 +696,15 @@ fn list_associations(interface: &InterfaceName) -> Result<(), Error> { Ok(()) } -fn set_listen_port(interface: &InterfaceName, unset: bool) -> Result<(), Error> { +fn set_listen_port( + interface: &InterfaceName, + unset: bool, + network: NetworkOpt, +) -> Result<(), Error> { let mut config = InterfaceConfig::from_interface(interface)?; if let Some(listen_port) = prompts::set_listen_port(&config.interface, unset)? { - wg::set_listen_port(interface, listen_port)?; + wg::set_listen_port(interface, listen_port, network.backend)?; println!("{} the interface is updated", "[*]".dimmed(),); config.interface.listen_port = listen_port; @@ -718,14 +717,18 @@ fn set_listen_port(interface: &InterfaceName, unset: bool) -> Result<(), Error> Ok(()) } -fn override_endpoint(interface: &InterfaceName, unset: bool) -> Result<(), Error> { +fn override_endpoint( + interface: &InterfaceName, + unset: bool, + network: NetworkOpt, +) -> Result<(), Error> { let config = InterfaceConfig::from_interface(interface)?; if !unset && config.interface.listen_port.is_none() { println!( "{}: you need to set a listen port for your interface first.", "note".bold().yellow() ); - set_listen_port(interface, unset)?; + set_listen_port(interface, unset, network)?; } if let Some(endpoint) = prompts::override_endpoint(unset)? { @@ -742,9 +745,16 @@ fn override_endpoint(interface: &InterfaceName, unset: bool) -> Result<(), Error Ok(()) } -fn show(short: bool, tree: bool, interface: Option) -> Result<(), Error> { - let interfaces = - interface.map_or_else(DeviceInfo::enumerate, |interface| Ok(vec![*interface]))?; +fn show( + short: bool, + tree: bool, + interface: Option, + network: NetworkOpt, +) -> Result<(), Error> { + let interfaces = interface.map_or_else( + || Device::list(network.backend), + |interface| Ok(vec![*interface]), + )?; let devices = interfaces .into_iter() @@ -752,7 +762,7 @@ fn show(short: bool, tree: bool, interface: Option) -> Result<(), Err DataStore::open(&name) .and_then(|store| { Ok(( - DeviceInfo::get_by_name(&name).with_str(name.as_str_lossy())?, + Device::get(&name, network.backend).with_str(name.as_str_lossy())?, store, )) }) @@ -826,7 +836,7 @@ fn print_tree(cidr: &CidrTree, peers: &[PeerState], level: usize) { } } -fn print_interface(device_info: &DeviceInfo, short: bool) -> Result<(), Error> { +fn print_interface(device_info: &Device, short: bool) -> Result<(), Error> { if short { let listen_port_str = device_info .listen_port @@ -930,32 +940,26 @@ fn run(opt: Opts) -> Result<(), Error> { invite, hosts, opts, - routing, - } => install(&invite, hosts.into(), opts, routing)?, + } => install(&invite, hosts.into(), opts, opt.network)?, Command::Show { short, tree, interface, - } => show(short, tree, interface)?, - Command::Fetch { - interface, - hosts, - routing, - } => fetch(&interface, false, hosts.into(), routing)?, + } => show(short, tree, interface, opt.network)?, + Command::Fetch { interface, hosts } => fetch(&interface, false, hosts.into(), opt.network)?, Command::Up { interface, daemon, hosts, - routing, interval, } => up( &interface, daemon.then(|| Duration::from_secs(interval)), hosts.into(), - routing, + opt.network, )?, - Command::Down { interface } => wg::down(&interface)?, - Command::Uninstall { interface } => uninstall(&interface)?, + Command::Down { interface } => wg::down(&interface, opt.network.backend)?, + Command::Uninstall { interface } => uninstall(&interface, opt.network)?, Command::AddPeer { interface, opts } => add_peer(&interface, opts)?, Command::AddCidr { interface, opts } => add_cidr(&interface, opts)?, Command::DisablePeer { interface } => enable_or_disable_peer(&interface, false)?, @@ -963,8 +967,12 @@ fn run(opt: Opts) -> Result<(), Error> { Command::AddAssociation { interface, opts } => add_association(&interface, opts)?, Command::DeleteAssociation { interface } => delete_association(&interface)?, Command::ListAssociations { interface } => list_associations(&interface)?, - Command::SetListenPort { interface, unset } => set_listen_port(&interface, unset)?, - Command::OverrideEndpoint { interface, unset } => override_endpoint(&interface, unset)?, + Command::SetListenPort { interface, unset } => { + set_listen_port(&interface, unset, opt.network)? + }, + Command::OverrideEndpoint { interface, unset } => { + override_endpoint(&interface, unset, opt.network)? + }, } Ok(()) diff --git a/server/src/api/admin/peer.rs b/server/src/api/admin/peer.rs index 98eb3a2..0eaefd1 100644 --- a/server/src/api/admin/peer.rs +++ b/server/src/api/admin/peer.rs @@ -8,7 +8,7 @@ use crate::{ }; use hyper::{Body, Method, Request, Response, StatusCode}; use shared::PeerContents; -use wgctrl::DeviceConfigBuilder; +use wgctrl::DeviceUpdate; pub async fn routes( req: Request, @@ -49,9 +49,9 @@ mod handlers { if cfg!(not(test)) { // Update the current WireGuard interface with the new peers. - DeviceConfigBuilder::new() + DeviceUpdate::new() .add_peer((&*peer).into()) - .apply(&session.context.interface) + .apply(&session.context.interface, session.context.backend) .map_err(|_| ServerError::WireGuard)?; log::info!("updated WireGuard interface, adding {}", &*peer); } diff --git a/server/src/api/user.rs b/server/src/api/user.rs index ddb0b27..781517c 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -4,11 +4,11 @@ use crate::{ api::inject_endpoints, db::{DatabaseCidr, DatabasePeer}, util::{form_body, json_response, status_response}, - ServerError, Session, + Context, ServerError, Session, }; use hyper::{Body, Method, Request, Response, StatusCode}; use shared::{EndpointContents, PeerContents, RedeemContents, State, REDEEM_TRANSITION_WAIT}; -use wgctrl::DeviceConfigBuilder; +use wgctrl::DeviceUpdate; pub async fn routes( req: Request, @@ -83,7 +83,9 @@ mod handlers { selected_peer.redeem(&conn, &form.public_key)?; if cfg!(not(test)) { - let interface = session.context.interface; + let Context { + interface, backend, .. + } = session.context; // If we were to modify the WireGuard interface immediately, the HTTP response wouldn't // get through. Instead, we need to wait a reasonable amount for the HTTP response to @@ -102,10 +104,10 @@ mod handlers { &*selected_peer, old_public_key.to_base64() ); - DeviceConfigBuilder::new() + DeviceUpdate::new() .remove_peer_by_key(&old_public_key) .add_peer((&*selected_peer).into()) - .apply(&interface) + .apply(&interface, backend) .map_err(|e| log::error!("{:?}", e)) .ok(); }); diff --git a/server/src/main.rs b/server/src/main.rs index d6a1fd0..8d5267a 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -6,7 +6,7 @@ use ipnetwork::IpNetwork; use parking_lot::{Mutex, RwLock}; use rusqlite::Connection; use serde::{Deserialize, Serialize}; -use shared::{AddCidrOpts, AddPeerOpts, IoErrorContext, RoutingOpt, INNERNET_PUBKEY_HEADER}; +use shared::{AddCidrOpts, AddPeerOpts, IoErrorContext, NetworkOpt, INNERNET_PUBKEY_HEADER}; use std::{ collections::{HashMap, VecDeque}, convert::TryInto, @@ -21,7 +21,7 @@ use std::{ }; use structopt::StructOpt; use subtle::ConstantTimeEq; -use wgctrl::{DeviceConfigBuilder, DeviceInfo, InterfaceName, Key, PeerConfigBuilder}; +use wgctrl::{Backend, Device, DeviceUpdate, InterfaceName, Key, PeerConfigBuilder}; pub mod api; pub mod db; @@ -45,6 +45,9 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); struct Opt { #[structopt(subcommand)] command: Command, + + #[structopt(flatten)] + network: NetworkOpt, } #[derive(Debug, StructOpt)] @@ -64,7 +67,7 @@ enum Command { interface: Interface, #[structopt(flatten)] - routing: RoutingOpt, + network: NetworkOpt, }, /// Add a peer to an existing network. @@ -92,6 +95,7 @@ pub struct Context { pub db: Db, pub endpoints: Arc>>, pub interface: InterfaceName, + pub backend: Backend, pub public_key: Key, } @@ -209,10 +213,13 @@ async fn main() -> Result<(), Box> { if let Err(e) = initialize::init_wizard(&conf, opts) { println!("{}: {}.", "creation failed".red(), e); } - } - Command::Uninstall { interface } => uninstall(&interface, &conf)?, - Command::Serve { interface, routing } => serve(*interface, &conf, routing).await?, - Command::AddPeer { interface, args } => add_peer(&interface, &conf, args)?, + }, + Command::Uninstall { interface } => uninstall(&interface, &conf, opt.network)?, + Command::Serve { + interface, + network: routing, + } => serve(*interface, &conf, routing).await?, + Command::AddPeer { interface, args } => add_peer(&interface, &conf, args, opt.network)?, Command::AddCidr { interface, args } => add_cidr(&interface, &conf, args)?, } @@ -243,6 +250,7 @@ fn add_peer( interface: &InterfaceName, conf: &ServerConfig, opts: AddPeerOpts, + network: NetworkOpt, ) -> Result<(), Error> { let config = ConfigFile::from_file(conf.config_path(interface))?; let conn = open_database_connection(interface, conf)?; @@ -255,11 +263,11 @@ fn add_peer( if let Some((peer_request, keypair)) = shared::prompts::add_peer(&peers, &cidr_tree, &opts)? { let peer = DatabasePeer::create(&conn, peer_request)?; - if cfg!(not(test)) && DeviceInfo::get_by_name(interface).is_ok() { + if cfg!(not(test)) && Device::get(interface, network.backend).is_ok() { // Update the current WireGuard interface with the new peers. - DeviceConfigBuilder::new() + DeviceUpdate::new() .add_peer((&*peer).into()) - .apply(interface) + .apply(interface, network.backend) .map_err(|_| ServerError::WireGuard)?; println!("adding to WireGuard interface: {}", &*peer); @@ -309,7 +317,11 @@ fn add_cidr( Ok(()) } -fn uninstall(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Error> { +fn uninstall( + interface: &InterfaceName, + conf: &ServerConfig, + network: NetworkOpt, +) -> Result<(), Error> { if Confirm::with_theme(&*prompts::THEME) .with_prompt(&format!( "Permanently delete network \"{}\"?", @@ -319,7 +331,7 @@ fn uninstall(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Error .interact()? { println!("{} bringing down interface (if up).", "[*]".dimmed()); - wg::down(interface).ok(); + wg::down(interface, network.backend).ok(); let config = conf.config_path(interface); let data = conf.database_path(interface); std::fs::remove_file(&config) @@ -339,7 +351,7 @@ fn uninstall(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Error Ok(()) } -fn spawn_endpoint_refresher(interface: InterfaceName) -> Endpoints { +fn spawn_endpoint_refresher(interface: InterfaceName, network: NetworkOpt) -> Endpoints { let endpoints = Arc::new(RwLock::new(HashMap::new())); tokio::task::spawn({ let endpoints = endpoints.clone(); @@ -347,7 +359,7 @@ fn spawn_endpoint_refresher(interface: InterfaceName) -> Endpoints { let mut interval = tokio::time::interval(Duration::from_secs(10)); loop { interval.tick().await; - if let Ok(info) = DeviceInfo::get_by_name(&interface) { + if let Ok(info) = Device::get(&interface, network.backend) { for peer in info.peers { if let Some(endpoint) = peer.config.endpoint { endpoints @@ -378,7 +390,7 @@ fn spawn_expired_invite_sweeper(db: Db) { async fn serve( interface: InterfaceName, conf: &ServerConfig, - routing: RoutingOpt, + network: NetworkOpt, ) -> Result<(), Error> { let config = ConfigFile::from_file(conf.config_path(&interface))?; let conn = open_database_connection(&interface, conf)?; @@ -396,18 +408,18 @@ async fn serve( IpNetwork::new(config.address, config.network_cidr_prefix)?, Some(config.listen_port), None, - !routing.no_routing, + network, )?; - DeviceConfigBuilder::new() + DeviceUpdate::new() .add_peers(&peer_configs) - .apply(&interface)?; + .apply(&interface, network.backend)?; log::info!("{} peers added to wireguard interface.", peers.len()); let public_key = wgctrl::Key::from_base64(&config.private_key)?.generate_public(); let db = Arc::new(Mutex::new(conn)); - let endpoints = spawn_endpoint_refresher(interface); + let endpoints = spawn_endpoint_refresher(interface, network); spawn_expired_invite_sweeper(db.clone()); let context = Context { @@ -415,6 +427,7 @@ async fn serve( endpoints, interface, public_key, + backend: network.backend, }; log::info!("innernet-server {} starting.", VERSION); diff --git a/server/src/test.rs b/server/src/test.rs index 943b543..921a21f 100644 --- a/server/src/test.rs +++ b/server/src/test.rs @@ -12,7 +12,7 @@ use serde::Serialize; use shared::{Cidr, CidrContents, Error, PeerContents}; use std::{collections::HashMap, net::SocketAddr, path::PathBuf, sync::Arc}; use tempfile::TempDir; -use wgctrl::{InterfaceName, Key, KeyPair}; +use wgctrl::{Backend, InterfaceName, Key, KeyPair}; pub const ROOT_CIDR: &str = "10.80.0.0/15"; pub const SERVER_CIDR: &str = "10.80.0.1/32"; @@ -137,6 +137,10 @@ impl Server { interface: self.interface.clone(), endpoints: self.endpoints.clone(), public_key: self.public_key.clone(), + #[cfg(target_os = "linux")] + backend: Backend::Kernel, + #[cfg(not(target_os = "linux"))] + backend: Backend::Userspace, } } diff --git a/shared/src/interface_config.rs b/shared/src/interface_config.rs index 10febaa..74510ee 100644 --- a/shared/src/interface_config.rs +++ b/shared/src/interface_config.rs @@ -1,4 +1,4 @@ -use crate::{ensure_dirs_exist, Endpoint, Error, IoErrorContext, CLIENT_CONFIG_PATH}; +use crate::{ensure_dirs_exist, Endpoint, Error, IoErrorContext, CLIENT_CONFIG_DIR}; use colored::*; use indoc::writedoc; use ipnetwork::IpNetwork; @@ -128,13 +128,13 @@ impl InterfaceConfig { } pub fn get_path(interface: &InterfaceName) -> PathBuf { - CLIENT_CONFIG_PATH + CLIENT_CONFIG_DIR .join(interface.to_string()) .with_extension("conf") } fn build_config_file_path(interface: &InterfaceName) -> Result { - ensure_dirs_exist(&[*CLIENT_CONFIG_PATH])?; + ensure_dirs_exist(&[*CLIENT_CONFIG_DIR])?; Ok(Self::get_path(interface)) } } diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 691f826..b2c395e 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -16,8 +16,8 @@ pub mod wg; pub use types::*; lazy_static! { - pub static ref CLIENT_CONFIG_PATH: &'static Path = Path::new("/etc/innernet"); - pub static ref CLIENT_DATA_PATH: &'static Path = Path::new("/var/lib/innernet"); + pub static ref CLIENT_CONFIG_DIR: &'static Path = Path::new("/etc/innernet"); + pub static ref CLIENT_DATA_DIR: &'static Path = Path::new("/var/lib/innernet"); pub static ref SERVER_CONFIG_DIR: &'static Path = Path::new("/etc/innernet-server"); pub static ref SERVER_DATABASE_DIR: &'static Path = Path::new("/var/lib/innernet-server"); pub static ref REDEEM_TRANSITION_WAIT: Duration = Duration::from_secs(5); @@ -28,15 +28,12 @@ pub const INNERNET_PUBKEY_HEADER: &str = "X-Innernet-Server-Key"; pub type Error = Box; -pub static WG_MANAGE_DIR: &str = "/etc/innernet"; -pub static WG_DIR: &str = "/etc/wireguard"; - pub fn ensure_dirs_exist(dirs: &[&Path]) -> Result<(), WrappedIoError> { for dir in dirs { match fs::create_dir(dir).with_path(dir) { Err(e) if e.kind() != io::ErrorKind::AlreadyExists => { return Err(e); - } + }, _ => { let target_file = File::open(dir).with_path(dir)?; if chmod(&target_file, 0o700).with_path(dir)? { @@ -46,7 +43,7 @@ pub fn ensure_dirs_exist(dirs: &[&Path]) -> Result<(), WrappedIoError> { dir.display() ); } - } + }, } } Ok(()) diff --git a/shared/src/types.rs b/shared/src/types.rs index 51ac87a..0f84579 100644 --- a/shared/src/types.rs +++ b/shared/src/types.rs @@ -13,7 +13,7 @@ use std::{ }; use structopt::StructOpt; use url::Host; -use wgctrl::{InterfaceName, InvalidInterfaceName, Key, PeerConfig, PeerConfigBuilder}; +use wgctrl::{Backend, InterfaceName, InvalidInterfaceName, Key, PeerConfig, PeerConfigBuilder}; #[derive(Debug, Clone)] pub struct Interface { @@ -73,7 +73,7 @@ impl FromStr for Endpoint { let port = port.parse().map_err(|_| "couldn't parse port")?; let host = Host::parse(host).map_err(|_| "couldn't parse host")?; Ok(Endpoint { host, port }) - } + }, _ => Err("couldn't parse in form of 'host:port'"), } } @@ -347,11 +347,16 @@ pub struct AddAssociationOpts { } #[derive(Debug, Clone, Copy, StructOpt)] -pub struct RoutingOpt { +pub struct NetworkOpt { #[structopt(long)] /// Whether the routing should be done by innernet or is done by an /// external tool like e.g. babeld. pub no_routing: bool, + + #[structopt(long, default_value, possible_values = Backend::variants())] + /// Specify a WireGuard backend to use. + /// If not set, innernet will auto-select based on availability. + pub backend: Backend, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] diff --git a/shared/src/wg.rs b/shared/src/wg.rs index 4db8978..fb8eca3 100644 --- a/shared/src/wg.rs +++ b/shared/src/wg.rs @@ -1,10 +1,10 @@ -use crate::{Error, IoErrorContext}; +use crate::{Error, IoErrorContext, NetworkOpt}; use ipnetwork::IpNetwork; use std::{ net::{IpAddr, SocketAddr}, process::{self, Command}, }; -use wgctrl::{DeviceConfigBuilder, InterfaceName, PeerConfigBuilder}; +use wgctrl::{Backend, Device, DeviceUpdate, InterfaceName, PeerConfigBuilder}; fn cmd(bin: &str, args: &[&str]) -> Result { let output = Command::new(bin).args(args).output()?; @@ -67,9 +67,9 @@ pub fn up( address: IpNetwork, listen_port: Option, peer: Option<(&str, IpAddr, SocketAddr)>, - do_routing: bool, + network: NetworkOpt, ) -> Result<(), Error> { - let mut device = DeviceConfigBuilder::new(); + let mut device = DeviceUpdate::new(); if let Some((public_key, address, endpoint)) = peer { let prefix = if address.is_ipv4() { 32 } else { 128 }; let peer_config = PeerConfigBuilder::new(&wgctrl::Key::from_base64(&public_key)?) @@ -82,36 +82,32 @@ pub fn up( } device .set_private_key(wgctrl::Key::from_base64(&private_key).unwrap()) - .apply(interface)?; + .apply(interface, network.backend)?; set_addr(interface, address)?; - if do_routing { + if !network.no_routing { add_route(interface, address)?; } Ok(()) } -pub fn set_listen_port(interface: &InterfaceName, listen_port: Option) -> Result<(), Error> { - let mut device = DeviceConfigBuilder::new(); +pub fn set_listen_port( + interface: &InterfaceName, + listen_port: Option, + backend: Backend, +) -> Result<(), Error> { + let mut device = DeviceUpdate::new(); if let Some(listen_port) = listen_port { device = device.set_listen_port(listen_port); } else { device = device.randomize_listen_port(); } - device.apply(interface)?; + device.apply(interface, backend)?; Ok(()) } -#[cfg(target_os = "linux")] -pub fn down(interface: &InterfaceName) -> Result<(), Error> { - Ok(wgctrl::delete_interface(&interface).with_str(interface.to_string())?) -} - -#[cfg(not(target_os = "linux"))] -pub fn down(interface: &InterfaceName) -> Result<(), Error> { - wgctrl::backends::userspace::delete_interface(interface) - .with_str(interface.to_string()) - .map_err(Error::from) +pub fn down(interface: &InterfaceName, backend: Backend) -> Result<(), Error> { + Ok(Device::get(interface, backend)?.delete()?) } /// Add a route in the OS's routing table to get traffic flowing through this interface. diff --git a/wgctrl-rs/src/backends/kernel.rs b/wgctrl-rs/src/backends/kernel.rs index 23392fa..a846d66 100644 --- a/wgctrl-rs/src/backends/kernel.rs +++ b/wgctrl-rs/src/backends/kernel.rs @@ -1,5 +1,5 @@ use crate::{ - device::AllowedIp, DeviceConfigBuilder, DeviceInfo, InterfaceName, InvalidInterfaceName, + device::AllowedIp, Backend, Device, DeviceUpdate, InterfaceName, InvalidInterfaceName, InvalidKey, PeerConfig, PeerConfigBuilder, PeerInfo, PeerStats, }; use wgctrl_sys::{timespec64, wg_device_flags as wgdf, wg_peer_flags as wgpf}; @@ -9,15 +9,10 @@ use std::{ io, net::{IpAddr, SocketAddr}, os::raw::c_char, - path::Path, - process::Command, ptr, str, - sync::Once, time::{Duration, SystemTime}, }; -static MODPROBE: Once = Once::new(); - impl<'a> From<&'a wgctrl_sys::wg_allowedip> for AllowedIp { fn from(raw: &wgctrl_sys::wg_allowedip) -> AllowedIp { let addr = match i32::from(raw.family) { @@ -69,11 +64,11 @@ impl<'a> From<&'a wgctrl_sys::wg_peer> for PeerInfo { } } -impl<'a> From<&'a wgctrl_sys::wg_device> for DeviceInfo { - fn from(raw: &wgctrl_sys::wg_device) -> DeviceInfo { +impl<'a> From<&'a wgctrl_sys::wg_device> for Device { + fn from(raw: &wgctrl_sys::wg_device) -> Device { // SAFETY: The name string buffer came directly from wgctrl so its NUL terminated. let name = unsafe { InterfaceName::from_wg(raw.name) }; - DeviceInfo { + Device { name, public_key: if (raw.flags & wgdf::WGDEVICE_HAS_PUBLIC_KEY).0 > 0 { Some(Key::from_raw(raw.public_key)) @@ -95,6 +90,7 @@ impl<'a> From<&'a wgctrl_sys::wg_device> for DeviceInfo { }, peers: parse_peers(&raw), linked_name: None, + backend: Backend::Kernel, __cant_construct_me: (), } } @@ -214,7 +210,7 @@ fn encode_endpoint(endpoint: Option) -> wgctrl_sys::wg_peer__bindgen peer.addr4 = wgctrl_sys::sockaddr_in { sin_family: libc::AF_INET as u16, sin_addr: wgctrl_sys::in_addr { - s_addr: u32::from_be(s.ip().clone().into()), + s_addr: u32::from_be((*s.ip()).into()), }, sin_port: u16::to_be(s.port()), sin_zero: [0; 8], @@ -223,10 +219,12 @@ fn encode_endpoint(endpoint: Option) -> wgctrl_sys::wg_peer__bindgen }, Some(SocketAddr::V6(s)) => { let mut peer = wgctrl_sys::wg_peer__bindgen_ty_1::default(); - let in6_addr = wgctrl_sys::in6_addr__bindgen_ty_1{__u6_addr8: s.ip().octets()}; + let in6_addr = wgctrl_sys::in6_addr__bindgen_ty_1 { + __u6_addr8: s.ip().octets(), + }; peer.addr6 = wgctrl_sys::sockaddr_in6 { sin6_family: libc::AF_INET6 as u16, - sin6_addr: wgctrl_sys::in6_addr{__in6_u: in6_addr}, + sin6_addr: wgctrl_sys::in6_addr { __in6_u: in6_addr }, sin6_port: u16::to_be(s.port()), sin6_flowinfo: 0, sin6_scope_id: 0, @@ -238,7 +236,7 @@ fn encode_endpoint(endpoint: Option) -> wgctrl_sys::wg_peer__bindgen } fn encode_peers( - peers: Vec, + peers: &[PeerConfigBuilder], ) -> (*mut wgctrl_sys::wg_peer, *mut wgctrl_sys::wg_peer) { let mut first_peer = ptr::null_mut(); let mut last_peer: *mut wgctrl_sys::wg_peer = ptr::null_mut(); @@ -290,20 +288,6 @@ fn encode_peers( (first_peer, last_peer) } -pub fn exists() -> bool { - // Try to load the wireguard module if it isn't already. - // This is only called once per lifetime of the process. - MODPROBE.call_once(|| { - Command::new("/sbin/modprobe") - .arg("wireguard") - .output() - .ok(); - }); - - // Check that the wireguard module is loaded. - Path::new("/sys/module/wireguard").is_dir() -} - pub fn enumerate() -> Result, io::Error> { let base = unsafe { wgctrl_sys::wg_list_device_names() }; @@ -337,8 +321,8 @@ pub fn enumerate() -> Result, io::Error> { Ok(result) } -pub fn apply(builder: DeviceConfigBuilder, iface: &InterfaceName) -> io::Result<()> { - let (first_peer, last_peer) = encode_peers(builder.peers); +pub fn apply(builder: &DeviceUpdate, iface: &InterfaceName) -> io::Result<()> { + let (first_peer, last_peer) = encode_peers(&builder.peers); let result = unsafe { wgctrl_sys::wg_add_device(iface.as_ptr()) }; match result { @@ -394,7 +378,7 @@ pub fn apply(builder: DeviceConfigBuilder, iface: &InterfaceName) -> io::Result< } } -pub fn get_by_name(name: &InterfaceName) -> Result { +pub fn get_by_name(name: &InterfaceName) -> Result { let mut device: *mut wgctrl_sys::wg_device = ptr::null_mut(); let result = unsafe { @@ -405,7 +389,7 @@ pub fn get_by_name(name: &InterfaceName) -> Result { }; let result = if result == 0 { - Ok(DeviceInfo::from(unsafe { &*device })) + Ok(Device::from(unsafe { &*device })) } else { Err(io::Error::last_os_error()) }; @@ -539,4 +523,4 @@ mod tests { assert_eq!(endpoint6, parse_endpoint(&encoded6)); Ok(()) } -} \ No newline at end of file +} diff --git a/wgctrl-rs/src/backends/userspace.rs b/wgctrl-rs/src/backends/userspace.rs index 8d863ef..6b321d5 100644 --- a/wgctrl-rs/src/backends/userspace.rs +++ b/wgctrl-rs/src/backends/userspace.rs @@ -1,4 +1,4 @@ -use crate::{DeviceConfigBuilder, DeviceInfo, InterfaceName, PeerConfig, PeerInfo, PeerStats}; +use crate::{Backend, Device, DeviceUpdate, InterfaceName, PeerConfig, PeerInfo, PeerStats}; #[cfg(target_os = "linux")] use crate::Key; @@ -89,11 +89,11 @@ fn new_peer_info(public_key: Key) -> PeerInfo { } struct ConfigParser { - device_info: DeviceInfo, + device_info: Device, current_peer: Option, } -impl From for DeviceInfo { +impl From for Device { fn from(parser: ConfigParser) -> Self { parser.device_info } @@ -102,7 +102,7 @@ impl From for DeviceInfo { impl ConfigParser { /// Returns `None` if an invalid device name was provided. fn new(name: &InterfaceName) -> Self { - let device_info = DeviceInfo { + let device_info = Device { name: *name, public_key: None, private_key: None, @@ -110,6 +110,7 @@ impl ConfigParser { listen_port: None, peers: vec![], linked_name: resolve_tun(name).ok(), + backend: Backend::Userspace, __cant_construct_me: (), }; @@ -229,7 +230,7 @@ impl ConfigParser { } } -pub fn get_by_name(name: &InterfaceName) -> Result { +pub fn get_by_name(name: &InterfaceName) -> Result { let mut sock = open_socket(name)?; sock.write_all(b"get=1\n\n")?; let mut reader = BufReader::new(sock); @@ -263,7 +264,7 @@ fn get_userspace_implementation() -> String { .unwrap_or_else(|_| "wireguard-go".to_string()) } -pub fn apply(builder: DeviceConfigBuilder, iface: &InterfaceName) -> io::Result<()> { +pub fn apply(builder: &DeviceUpdate, iface: &InterfaceName) -> io::Result<()> { // If we can't open a configuration socket to an existing interface, try starting it. let mut sock = match open_socket(iface) { Err(_) => { @@ -302,7 +303,7 @@ pub fn apply(builder: DeviceConfigBuilder, iface: &InterfaceName) -> io::Result< request.push_str("replace_peers=true\n"); } - for peer in builder.peers { + for peer in &builder.peers { request.push_str(&format!("public_key={}\n", hex::encode(peer.public_key.0))); if peer.replace_allowed_ips { @@ -328,7 +329,7 @@ pub fn apply(builder: DeviceConfigBuilder, iface: &InterfaceName) -> io::Result< )); } - for allowed_ip in peer.allowed_ips { + for allowed_ip in &peer.allowed_ips { request.push_str(&format!( "allowed_ip={}/{}\n", allowed_ip.address, allowed_ip.cidr diff --git a/wgctrl-rs/src/config.rs b/wgctrl-rs/src/config.rs index c3e231d..ac5f439 100644 --- a/wgctrl-rs/src/config.rs +++ b/wgctrl-rs/src/config.rs @@ -1,195 +1,9 @@ use crate::{ - backends, - device::{AllowedIp, InterfaceName, PeerConfig}, - key::{Key, KeyPair}, + device::{AllowedIp, PeerConfig}, + key::Key, }; -use std::{ - io, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, -}; - -/// Builds and represents a configuration that can be applied to a WireGuard interface. -/// -/// This is the primary way of changing the settings of an interface. -/// -/// Note that if an interface exists, the configuration is applied _on top_ of the existing -/// settings, and missing parts are not overwritten or set to defaults. -/// -/// If this is not what you want, use [`delete_interface`](delete_interface) -/// to remove the interface entirely before applying the new configuration. -/// -/// # Example -/// ```rust -/// # use wgctrl::*; -/// # use std::net::AddrParseError; -/// # fn try_main() -> Result<(), AddrParseError> { -/// let our_keypair = KeyPair::generate(); -/// let peer_keypair = KeyPair::generate(); -/// let server_addr = "192.168.1.1:51820".parse()?; -/// -/// DeviceConfigBuilder::new() -/// .set_keypair(our_keypair) -/// .replace_peers() -/// .add_peer_with(&peer_keypair.public, |peer| { -/// peer.set_endpoint(server_addr) -/// .replace_allowed_ips() -/// .allow_all_ips() -/// }).apply(&"wg-example".parse().unwrap()); -/// -/// println!("Send these keys to your peer: {:#?}", peer_keypair); -/// -/// # Ok(()) -/// # } -/// # fn main() { try_main(); } -/// ``` -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct DeviceConfigBuilder { - pub(crate) public_key: Option, - pub(crate) private_key: Option, - pub(crate) fwmark: Option, - pub(crate) listen_port: Option, - pub(crate) peers: Vec, - pub(crate) replace_peers: bool, -} - -impl DeviceConfigBuilder { - /// Creates a new `DeviceConfigBuilder` that does nothing when applied. - pub fn new() -> Self { - DeviceConfigBuilder { - public_key: None, - private_key: None, - fwmark: None, - listen_port: None, - peers: vec![], - replace_peers: false, - } - } - - /// Sets a new keypair to be applied to the interface. - /// - /// This is a convenience method that simply wraps - /// [`set_public_key`](DeviceConfigBuilder::set_public_key) - /// and [`set_private_key`](DeviceConfigBuilder::set_private_key). - pub fn set_keypair(self, keypair: KeyPair) -> Self { - self.set_public_key(keypair.public) - .set_private_key(keypair.private) - } - - /// Specifies a new public key to be applied to the interface. - pub fn set_public_key(mut self, key: Key) -> Self { - self.public_key = Some(key); - self - } - - /// Specifies that the public key for this interface should be unset. - pub fn unset_public_key(self) -> Self { - self.set_public_key(Key::zero()) - } - - /// Sets a new private key to be applied to the interface. - pub fn set_private_key(mut self, key: Key) -> Self { - self.private_key = Some(key); - self - } - - /// Specifies that the private key for this interface should be unset. - pub fn unset_private_key(self) -> Self { - self.set_private_key(Key::zero()) - } - - /// Specifies the fwmark value that should be applied to packets coming from the interface. - pub fn set_fwmark(mut self, fwmark: u32) -> Self { - self.fwmark = Some(fwmark); - self - } - - /// Specifies that fwmark should not be set on packets from the interface. - pub fn unset_fwmark(self) -> Self { - self.set_fwmark(0) - } - - /// Specifies the port to listen for incoming packets on. - /// - /// This is useful for a server configuration that listens on a fixed endpoint. - pub fn set_listen_port(mut self, port: u16) -> Self { - self.listen_port = Some(port); - self - } - - /// Specifies that a random port should be used for incoming packets. - /// - /// This is probably what you want in client configurations. - pub fn randomize_listen_port(self) -> Self { - self.set_listen_port(0) - } - - /// Specifies a new peer configuration to be added to the interface. - /// - /// See [`PeerConfigBuilder`](PeerConfigBuilder) for details on building - /// peer configurations. This method can be called more than once, and all - /// peers will be added to the configuration. - pub fn add_peer(mut self, peer: PeerConfigBuilder) -> Self { - self.peers.push(peer); - self - } - - /// Specifies a new peer configuration using a builder function. - /// - /// This is simply a convenience method to make adding peers more fluent. - /// This method can be called more than once, and all peers will be added - /// to the configuration. - pub fn add_peer_with( - self, - pubkey: &Key, - builder: impl Fn(PeerConfigBuilder) -> PeerConfigBuilder, - ) -> Self { - self.add_peer(builder(PeerConfigBuilder::new(pubkey))) - } - - /// Specifies multiple peer configurations to be added to the interface. - pub fn add_peers(mut self, peers: &[PeerConfigBuilder]) -> Self { - self.peers.extend_from_slice(peers); - self - } - - /// Specifies that the peer configurations in this `DeviceConfigBuilder` should - /// replace the existing configurations on the interface, not modify or append to them. - pub fn replace_peers(mut self) -> Self { - self.replace_peers = true; - self - } - - /// Specifies that the peer with this public key should be removed from the interface. - pub fn remove_peer_by_key(self, public_key: &Key) -> Self { - let mut peer = PeerConfigBuilder::new(public_key); - peer.remove_me = true; - self.add_peer(peer) - } - - /// Build and apply the configuration to a WireGuard interface by name. - /// - /// An interface with the provided name will be created if one does not exist already. - #[cfg(target_os = "linux")] - pub fn apply(self, iface: &InterfaceName) -> io::Result<()> { - if backends::kernel::exists() { - backends::kernel::apply(self, &iface) - } else { - backends::userspace::apply(self, iface) - } - } - - #[cfg(not(target_os = "linux"))] - pub fn apply(self, iface: &InterfaceName) -> io::Result<()> { - backends::userspace::apply(self, iface) - } -} - -impl Default for DeviceConfigBuilder { - fn default() -> Self { - Self::new() - } -} +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; /// Builds and represents a single peer in a WireGuard interface configuration. /// @@ -215,7 +29,7 @@ impl Default for DeviceConfigBuilder { /// .add_allowed_ip("192.168.1.2".parse()?, 32); /// /// // update our existing configuration with the new peer -/// DeviceConfigBuilder::new().add_peer(peer).apply(&"wg-example".parse().unwrap()); +/// DeviceUpdate::new().add_peer(peer).apply(&"wg-example".parse().unwrap(), Backend::Userspace); /// /// println!("Send these keys to your peer: {:#?}", peer_keypair); /// @@ -350,9 +164,3 @@ impl PeerConfigBuilder { self } } - -/// Deletes an existing WireGuard interface by name. -#[cfg(target_os = "linux")] -pub fn delete_interface(iface: &InterfaceName) -> io::Result<()> { - backends::kernel::delete_interface(iface) -} diff --git a/wgctrl-rs/src/device.rs b/wgctrl-rs/src/device.rs index 674c1ee..b470396 100644 --- a/wgctrl-rs/src/device.rs +++ b/wgctrl-rs/src/device.rs @@ -1,21 +1,17 @@ use libc::c_char; -use crate::{backends, key::Key}; +use crate::{backends, key::Key, Backend, KeyPair, PeerConfigBuilder}; use std::{ borrow::Cow, ffi::CStr, - fmt, + fmt, io, net::{IpAddr, SocketAddr}, str::FromStr, time::SystemTime, }; /// Represents an IP address a peer is allowed to have, in CIDR notation. -/// -/// This may have unexpected semantics - refer to the -/// [WireGuard documentation](https://www.wireguard.com/#cryptokey-routing) -/// for more information on how routing is implemented. #[derive(Debug, PartialEq, Eq, Clone)] pub struct AllowedIp { /// The IP address. @@ -90,7 +86,7 @@ pub struct PeerInfo { /// The peer statistics are retrieved once at construction time, /// and need to be updated manually by calling [`get_by_name`](DeviceInfo::get_by_name). #[derive(Debug, PartialEq, Eq, Clone)] -pub struct DeviceInfo { +pub struct Device { /// The interface name of this device pub name: InterfaceName, /// The public encryption key of this interface (if present) @@ -105,6 +101,8 @@ pub struct DeviceInfo { pub peers: Vec, /// The associated "real name" of the interface (ex. "utun8" on macOS). pub linked_name: Option, + /// The backend the device exists on (userspace or kernel). + pub backend: Backend, pub(crate) __cant_construct_me: (), } @@ -227,57 +225,216 @@ impl From for std::io::Error { impl std::error::Error for InvalidInterfaceName {} -impl DeviceInfo { - /// Enumerates all WireGuard interfaces currently present in the system - /// and returns their names. +impl Device { + /// Enumerates all WireGuard interfaces currently present in the system, + /// both with kernel and userspace backends. /// /// You can use [`get_by_name`](DeviceInfo::get_by_name) to retrieve more /// detailed information on each interface. - #[cfg(target_os = "linux")] - pub fn enumerate() -> Result, std::io::Error> { - if backends::kernel::exists() { - backends::kernel::enumerate() - } else { - backends::userspace::enumerate() + pub fn list(backend: Backend) -> Result, std::io::Error> { + match backend { + #[cfg(target_os = "linux")] + Backend::Kernel => backends::kernel::enumerate(), + Backend::Userspace => backends::userspace::enumerate(), } } - #[cfg(not(target_os = "linux"))] - pub fn enumerate() -> Result, std::io::Error> { - crate::backends::userspace::enumerate() - } - - #[cfg(target_os = "linux")] - pub fn get_by_name(name: &InterfaceName) -> Result { - if backends::kernel::exists() { - backends::kernel::get_by_name(name) - } else { - println!("kernel module not detected. falling back to userspace backend."); - backends::userspace::get_by_name(name) + pub fn get(name: &InterfaceName, backend: Backend) -> Result { + match backend { + #[cfg(target_os = "linux")] + Backend::Kernel => backends::kernel::get_by_name(name), + Backend::Userspace => backends::userspace::get_by_name(name), } } - #[cfg(not(target_os = "linux"))] - pub fn get_by_name(name: &InterfaceName) -> Result { - backends::userspace::get_by_name(name) + pub fn delete(self) -> Result<(), std::io::Error> { + match self.backend { + #[cfg(target_os = "linux")] + Backend::Kernel => backends::kernel::delete_interface(&self.name), + Backend::Userspace => backends::userspace::delete_interface(&self.name), + } + } +} + +/// Builds and represents a configuration that can be applied to a WireGuard interface. +/// +/// This is the primary way of changing the settings of an interface. +/// +/// Note that if an interface exists, the configuration is applied _on top_ of the existing +/// settings, and missing parts are not overwritten or set to defaults. +/// +/// If this is not what you want, use [`delete_interface`](delete_interface) +/// to remove the interface entirely before applying the new configuration. +/// +/// # Example +/// ```rust +/// # use wgctrl::*; +/// # use std::net::AddrParseError; +/// # fn try_main() -> Result<(), AddrParseError> { +/// let our_keypair = KeyPair::generate(); +/// let peer_keypair = KeyPair::generate(); +/// let server_addr = "192.168.1.1:51820".parse()?; +/// +/// DeviceUpdate::new() +/// .set_keypair(our_keypair) +/// .replace_peers() +/// .add_peer_with(&peer_keypair.public, |peer| { +/// peer.set_endpoint(server_addr) +/// .replace_allowed_ips() +/// .allow_all_ips() +/// }).apply(&"wg-example".parse().unwrap(), Backend::Userspace); +/// +/// println!("Send these keys to your peer: {:#?}", peer_keypair); +/// +/// # Ok(()) +/// # } +/// # fn main() { try_main(); } +/// ``` +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct DeviceUpdate { + pub(crate) public_key: Option, + pub(crate) private_key: Option, + pub(crate) fwmark: Option, + pub(crate) listen_port: Option, + pub(crate) peers: Vec, + pub(crate) replace_peers: bool, +} + +impl DeviceUpdate { + /// Creates a new `DeviceConfigBuilder` that does nothing when applied. + pub fn new() -> Self { + DeviceUpdate { + public_key: None, + private_key: None, + fwmark: None, + listen_port: None, + peers: vec![], + replace_peers: false, + } } - #[cfg(target_os = "linux")] - pub fn delete(self) -> Result<(), std::io::Error> { - backends::kernel::delete_interface(&self.name) + /// Sets a new keypair to be applied to the interface. + /// + /// This is a convenience method that simply wraps + /// [`set_public_key`](DeviceConfigBuilder::set_public_key) + /// and [`set_private_key`](DeviceConfigBuilder::set_private_key). + pub fn set_keypair(self, keypair: KeyPair) -> Self { + self.set_public_key(keypair.public) + .set_private_key(keypair.private) } - #[cfg(not(target_os = "linux"))] - pub fn delete(self) -> Result<(), std::io::Error> { - backends::userspace::delete_interface(&self.name) + /// Specifies a new public key to be applied to the interface. + pub fn set_public_key(mut self, key: Key) -> Self { + self.public_key = Some(key); + self + } + + /// Specifies that the public key for this interface should be unset. + pub fn unset_public_key(self) -> Self { + self.set_public_key(Key::zero()) + } + + /// Sets a new private key to be applied to the interface. + pub fn set_private_key(mut self, key: Key) -> Self { + self.private_key = Some(key); + self + } + + /// Specifies that the private key for this interface should be unset. + pub fn unset_private_key(self) -> Self { + self.set_private_key(Key::zero()) + } + + /// Specifies the fwmark value that should be applied to packets coming from the interface. + pub fn set_fwmark(mut self, fwmark: u32) -> Self { + self.fwmark = Some(fwmark); + self + } + + /// Specifies that fwmark should not be set on packets from the interface. + pub fn unset_fwmark(self) -> Self { + self.set_fwmark(0) + } + + /// Specifies the port to listen for incoming packets on. + /// + /// This is useful for a server configuration that listens on a fixed endpoint. + pub fn set_listen_port(mut self, port: u16) -> Self { + self.listen_port = Some(port); + self + } + + /// Specifies that a random port should be used for incoming packets. + /// + /// This is probably what you want in client configurations. + pub fn randomize_listen_port(self) -> Self { + self.set_listen_port(0) + } + + /// Specifies a new peer configuration to be added to the interface. + /// + /// See [`PeerConfigBuilder`](PeerConfigBuilder) for details on building + /// peer configurations. This method can be called more than once, and all + /// peers will be added to the configuration. + pub fn add_peer(mut self, peer: PeerConfigBuilder) -> Self { + self.peers.push(peer); + self + } + + /// Specifies a new peer configuration using a builder function. + /// + /// This is simply a convenience method to make adding peers more fluent. + /// This method can be called more than once, and all peers will be added + /// to the configuration. + pub fn add_peer_with( + self, + pubkey: &Key, + builder: impl Fn(PeerConfigBuilder) -> PeerConfigBuilder, + ) -> Self { + self.add_peer(builder(PeerConfigBuilder::new(pubkey))) + } + + /// Specifies multiple peer configurations to be added to the interface. + pub fn add_peers(mut self, peers: &[PeerConfigBuilder]) -> Self { + self.peers.extend_from_slice(peers); + self + } + + /// Specifies that the peer configurations in this `DeviceConfigBuilder` should + /// replace the existing configurations on the interface, not modify or append to them. + pub fn replace_peers(mut self) -> Self { + self.replace_peers = true; + self + } + + /// Specifies that the peer with this public key should be removed from the interface. + pub fn remove_peer_by_key(self, public_key: &Key) -> Self { + let mut peer = PeerConfigBuilder::new(public_key); + peer.remove_me = true; + self.add_peer(peer) + } + + /// Build and apply the configuration to a WireGuard interface by name. + /// + /// An interface with the provided name will be created if one does not exist already. + pub fn apply(self, iface: &InterfaceName, backend: Backend) -> io::Result<()> { + match backend { + #[cfg(target_os = "linux")] + Backend::Kernel => backends::kernel::apply(&self, &iface), + Backend::Userspace => backends::userspace::apply(&self, &iface), + } + } +} + +impl Default for DeviceUpdate { + fn default() -> Self { + Self::new() } } #[cfg(test)] mod tests { - use crate::{ - DeviceConfigBuilder, InterfaceName, InvalidInterfaceName, KeyPair, PeerConfigBuilder, - }; + use crate::{DeviceUpdate, InterfaceName, InvalidInterfaceName, KeyPair, PeerConfigBuilder}; const TEST_INTERFACE: &str = "wgctrl-test"; use super::*; @@ -289,14 +446,14 @@ mod tests { } let keypairs: Vec<_> = (0..10).map(|_| KeyPair::generate()).collect(); - let mut builder = DeviceConfigBuilder::new(); + let mut builder = DeviceUpdate::new(); for keypair in &keypairs { builder = builder.add_peer(PeerConfigBuilder::new(&keypair.public)) } let interface = TEST_INTERFACE.parse().unwrap(); - builder.apply(&interface).unwrap(); + builder.apply(&interface, Backend::Userspace).unwrap(); - let device = DeviceInfo::get_by_name(&interface).unwrap(); + let device = Device::get(&interface, Backend::Userspace).unwrap(); for keypair in &keypairs { assert!(device diff --git a/wgctrl-rs/src/lib.rs b/wgctrl-rs/src/lib.rs index ab021ec..47dcacc 100644 --- a/wgctrl-rs/src/lib.rs +++ b/wgctrl-rs/src/lib.rs @@ -3,4 +3,63 @@ mod config; mod device; mod key; +use std::{ + fmt::{self, Display, Formatter}, + str::FromStr, +}; + pub use crate::{config::*, device::*, key::*}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Backend { + #[cfg(target_os = "linux")] + Kernel, + Userspace, +} + +impl Default for Backend { + fn default() -> Self { + #[cfg(target_os = "linux")] + { + Self::Kernel + } + + #[cfg(not(target_os = "linux"))] + { + Self::Userspace + } + } +} + +impl Display for Backend { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + #[cfg(target_os = "linux")] + Self::Kernel => write!(f, "kernel"), + Self::Userspace => write!(f, "userspace"), + } + } +} + +impl FromStr for Backend { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_ascii_lowercase().as_str() { + #[cfg(target_os = "linux")] + "kernel" => Ok(Self::Kernel), + "userspace" => Ok(Self::Userspace), + _ => Err(format!("valid values: {}.", Self::variants().join(", "))), + } + } +} + +impl Backend { + pub fn variants() -> &'static [&'static str] { + #[cfg(target_os = "linux")] + { &["kernel", "userspace"] } + + #[cfg(not(target_os = "linux"))] + { &["userspace"] } + } +}