From c4e369ee545ed864fd143114d4adef9d9021372f Mon Sep 17 00:00:00 2001 From: Jake McGinty Date: Sun, 18 Apr 2021 01:12:15 +0900 Subject: [PATCH] server: non-interactive network creation --- Cargo.lock | 162 +++++++++++++++++++++++++++++++++++++-- client/src/main.rs | 9 ++- server/Cargo.toml | 5 +- server/src/initialize.rs | 96 ++++++++++++++++------- server/src/main.rs | 24 +++--- server/src/test.rs | 16 ++-- shared/src/prompts.rs | 26 ++++--- 7 files changed, 272 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49a45b1..d8c63a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" + [[package]] name = "byteorder" version = "1.4.3" @@ -666,6 +672,15 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +[[package]] +name = "js-sys" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -686,9 +701,9 @@ checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714" [[package]] name = "libsqlite3-sys" -version = "0.20.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d31059f22935e6c31830db5249ba2b7ecd54fd73a9909286f0a67aa55c2fbd" +checksum = "2f6332d94daa84478d55a6aa9dbb3b305ed6500fb0cb9400cb9e1525d0e0e188" dependencies = [ "pkg-config", "vcpkg", @@ -1027,10 +1042,25 @@ dependencies = [ ] [[package]] -name = "rusqlite" -version = "0.24.2" +name = "ring" +version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38ee71cbab2c827ec0ac24e76f82eca723cee92c509a65f67dee393c25112" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rusqlite" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48381bf52627e7b0e02c4c0e4c0c88fc1cf2228a2eb7461d9499b1372399f1da" dependencies = [ "bitflags", "fallible-iterator", @@ -1047,6 +1077,19 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustls" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +dependencies = [ + "base64", + "log", + "ring", + "sct", + "webpki", +] + [[package]] name = "ryu" version = "1.0.5" @@ -1065,6 +1108,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sct" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "serde" version = "1.0.125" @@ -1137,6 +1190,7 @@ dependencies = [ "thiserror", "tokio", "toml", + "ureq", "warp", "wgctrl", ] @@ -1199,6 +1253,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "strsim" version = "0.8.0" @@ -1496,6 +1556,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "ureq" version = "2.1.0" @@ -1506,9 +1572,12 @@ dependencies = [ "chunked_transfer", "log", "once_cell", + "rustls", "serde", "serde_json", "url", + "webpki", + "webpki-roots", ] [[package]] @@ -1583,6 +1652,89 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "wasm-bindgen" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489" + +[[package]] +name = "web-sys" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +dependencies = [ + "webpki", +] + [[package]] name = "wgctrl" version = "1.1.0" diff --git a/client/src/main.rs b/client/src/main.rs index f539c12..7152e5e 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -52,7 +52,8 @@ enum Command { /// Install a new innernet config. #[structopt(alias = "redeem")] Install { - config: PathBuf, + /// Path to the invitation file + invite: PathBuf, #[structopt(flatten)] hosts: HostsOpt, @@ -64,9 +65,11 @@ enum Command { /// Enumerate all innernet connections. #[structopt(alias = "list")] Show { + /// One-line peer list #[structopt(short, long)] short: bool, + /// Display peers in a tree based on the CIDRs #[structopt(short, long)] tree: bool, @@ -841,10 +844,10 @@ fn run(opt: Opt) -> Result<(), Error> { match command { Command::Install { - config, + invite, hosts, opts, - } => install(&config, hosts.into(), opts)?, + } => install(&invite, hosts.into(), opts)?, Command::Show { short, tree, diff --git a/server/Cargo.toml b/server/Cargo.toml index 5e58772..8aa0cff 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -21,18 +21,19 @@ hyper = "0.14" indoc = "1" ipnetwork = { git = "https://github.com/mcginty/ipnetwork" } # pending https://github.com/achanda/ipnetwork/pull/129 libc = "0.2" -libsqlite3-sys = "0.20" +libsqlite3-sys = "0.22" log = "0.4" parking_lot = "0.11" pretty_env_logger = "0.4" regex = { version = "1", default-features = false, features = ["std"] } -rusqlite = "0.24" +rusqlite = "0.25" serde = { version = "1", features = ["derive"] } serde_json = "1" shared = { path = "../shared" } subtle = "2" structopt = "0.3" thiserror = "1" +ureq = "2" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } toml = "0.5" warp = { git = "https://github.com/tonarino/warp", default-features = false } # pending https://github.com/seanmonstar/warp/issues/830 diff --git a/server/src/initialize.rs b/server/src/initialize.rs index 883d306..24c6b80 100644 --- a/server/src/initialize.rs +++ b/server/src/initialize.rs @@ -20,9 +20,32 @@ fn create_database>( Ok(conn) } +#[derive(Debug, Default, Clone, PartialEq, StructOpt)] +pub struct InitializeOpts { + /// The network name (ex: evilcorp) + #[structopt(long)] + pub network_name: Option, + + /// The network CIDR (ex: 10.42.0.0/16) + #[structopt(long)] + pub network_cidr: Option, + + /// This server's external endpoint (ex: 100.100.100.100:51820) + #[structopt(long, conflicts_with = "auto-external-endpoint")] + pub external_endpoint: Option, + + /// Auto-resolve external endpoint + #[structopt(long = "auto-external-endpoint")] + pub auto_external_endpoint: bool, + + /// Port to listen on (for the WireGuard interface) + #[structopt(long)] + pub listen_port: Option, +} + struct DbInitData { - root_cidr_name: String, - root_cidr: IpNetwork, + network_name: String, + network_cidr: IpNetwork, server_cidr: IpNetwork, our_ip: IpAddr, public_key_base64: String, @@ -35,8 +58,8 @@ fn populate_database(conn: &Connection, db_init_data: DbInitData) -> Result<(), let root_cidr = DatabaseCidr::create( &conn, CidrContents { - name: db_init_data.root_cidr_name.clone(), - cidr: db_init_data.root_cidr, + name: db_init_data.network_name.clone(), + cidr: db_init_data.network_cidr, parent: None, }, ) @@ -71,7 +94,7 @@ fn populate_database(conn: &Connection, db_init_data: DbInitData) -> Result<(), Ok(()) } -pub fn init_wizard(conf: &ServerConfig) -> Result<(), Error> { +pub fn init_wizard(conf: &ServerConfig, opts: InitializeOpts) -> Result<(), Error> { let theme = ColorfulTheme::default(); shared::ensure_dirs_exist(&[conf.config_dir(), conf.database_dir()]).map_err(|_| { @@ -81,42 +104,57 @@ pub fn init_wizard(conf: &ServerConfig) -> Result<(), Error> { ) })?; - let (name, root_cidr) = conf.root_cidr.clone().unwrap_or_else(|| { - println!("Please specify a root CIDR, which will define the entire network."); - let name: String = Input::with_theme(&theme) + let name: String = if let Some(name) = opts.network_name { + name.clone() + } else { + println!("Here you'll specify the network CIDR, which will encompass the entire network."); + Input::with_theme(&theme) .with_prompt("Network name") .validate_with(hostname_validator) - .interact() - .map_err(|_| println!("failed to get name.")) - .unwrap(); + .interact()? + }; - let root_cidr: IpNetwork = Input::with_theme(&theme) + let root_cidr: IpNetwork = if let Some(cidr) = opts.network_cidr { + cidr.clone() + } else { + Input::with_theme(&theme) .with_prompt("Network CIDR") .with_initial_text("10.42.0.0/16") - .interact() - .map_err(|_| println!("failed to get cidr.")) - .unwrap(); - - (name, root_cidr) - }); + .interact()? + }; // This probably won't error because of the `hostname_validator` regex. let name = name.parse()?; - let endpoint: SocketAddr = conf.endpoint.unwrap_or_else(|| { - prompts::ask_endpoint() - .map_err(|_| println!("failed to get endpoint.")) - .unwrap() - }); + let endpoint: SocketAddr = if let Some(endpoint) = opts.external_endpoint { + endpoint.clone() + } else { + let external_ip: Option = ureq::get("http://4.icanhazip.com") + .call() + .ok() + .map(|res| res.into_string().ok()) + .flatten() + .map(|body| body.trim().to_string()) + .and_then(|body| body.parse().ok()); - let listen_port: u16 = conf.listen_port.unwrap_or_else(|| { + if opts.auto_external_endpoint { + let ip = external_ip.ok_or("couldn't get external IP")?; + (ip, 51820).into() + } else { + prompts::ask_endpoint(external_ip)? + } + }; + + let listen_port: u16 = if let Some(listen_port) = opts.listen_port { + listen_port + } else { Input::with_theme(&theme) .with_prompt("Listen port") .default(51820) .interact() - .map_err(|_| println!("failed to get listen port.")) - .unwrap() - }); + .map_err(|_| "failed to get listen port.")? + }; + let our_ip = root_cidr .iter() .find(|ip| root_cidr.is_assignable(*ip)) @@ -134,8 +172,8 @@ pub fn init_wizard(conf: &ServerConfig) -> Result<(), Error> { config.write_to_path(&config_path)?; let db_init_data = DbInitData { - root_cidr_name: name.to_string(), - root_cidr, + network_name: name.to_string(), + network_cidr: root_cidr, server_cidr, our_ip, public_key_base64: our_keypair.public.to_base64(), diff --git a/server/src/main.rs b/server/src/main.rs index 5f5e7a2..48c8c6c 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -37,6 +37,7 @@ pub use endpoints::Endpoints; pub use error::ServerError; use shared::{prompts, wg, CidrTree, Error, Interface, SERVER_CONFIG_DIR, SERVER_DATABASE_DIR}; pub use shared::{Association, AssociationContents}; +use initialize::InitializeOpts; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -51,7 +52,10 @@ struct Opt { enum Command { /// Create a new network. #[structopt(alias = "init")] - New, + New { + #[structopt(flatten)] + opts: InitializeOpts + }, /// Permanently uninstall a created network, rendering it unusable. Use with care. Uninstall { interface: Interface }, @@ -153,10 +157,6 @@ impl ConfigFile { pub struct ServerConfig { wg_manage_dir_override: Option, wg_dir_override: Option, - root_cidr: Option<(String, IpNetwork)>, - endpoint: Option, - listen_port: Option, - noninteractive: bool, } impl ServerConfig { @@ -204,8 +204,8 @@ async fn main() -> Result<(), Box> { let conf = ServerConfig::default(); match opt.command { - Command::New => { - if let Err(e) = initialize::init_wizard(&conf) { + Command::New { opts } => { + if let Err(e) = initialize::init_wizard(&conf, opts) { println!("{}: {}.", "creation failed".red(), e); } }, @@ -237,7 +237,7 @@ fn open_database_connection( fn add_peer( interface: &InterfaceName, conf: &ServerConfig, - args: AddPeerOpts, + opts: AddPeerOpts, ) -> Result<(), Error> { let config = ConfigFile::from_file(conf.config_path(interface))?; let conn = open_database_connection(interface, conf)?; @@ -248,7 +248,7 @@ fn add_peer( let cidrs = DatabaseCidr::list(&conn)?; let cidr_tree = CidrTree::new(&cidrs[..]); - if let Some((peer_request, keypair)) = shared::prompts::add_peer(&peers, &cidr_tree, &args)? { + 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() { // Update the current WireGuard interface with the new peers. @@ -268,7 +268,7 @@ fn add_peer( &cidr_tree, keypair, &SocketAddr::new(config.address, config.listen_port), - &args.save_config, + &opts.save_config, )?; } else { println!("exited without creating peer."); @@ -280,11 +280,11 @@ fn add_peer( fn add_cidr( interface: &InterfaceName, conf: &ServerConfig, - args: AddCidrOpts, + opts: AddCidrOpts, ) -> Result<(), Error> { let conn = open_database_connection(interface, conf)?; let cidrs = DatabaseCidr::list(&conn)?; - if let Some(cidr_request) = shared::prompts::add_cidr(&cidrs, &args)? { + if let Some(cidr_request) = shared::prompts::add_cidr(&cidrs, &opts)? { let cidr = DatabaseCidr::create(&conn, cidr_request)?; printdoc!( " diff --git a/server/src/test.rs b/server/src/test.rs index 696bf2d..62430dd 100644 --- a/server/src/test.rs +++ b/server/src/test.rs @@ -2,7 +2,7 @@ use crate::{ db::{DatabaseCidr, DatabasePeer}, endpoints::Endpoints, - initialize::init_wizard, + initialize::{init_wizard, InitializeOpts}, Context, ServerConfig, }; use anyhow::{anyhow, Result}; @@ -64,12 +64,16 @@ impl Server { let conf = ServerConfig { wg_manage_dir_override: Some(test_dir_path.to_path_buf()), wg_dir_override: Some(test_dir_path.to_path_buf()), - root_cidr: Some((interface.clone(), ROOT_CIDR.parse()?)), - endpoint: Some("155.155.155.155:54321".parse()?), - listen_port: Some(54321), - noninteractive: true, }; - init_wizard(&conf).map_err(|_| anyhow!("init_wizard failed"))?; + + let opts = InitializeOpts { + network_name: Some(interface.clone()), + network_cidr: Some(ROOT_CIDR.parse()?), + external_endpoint: Some("155.155.155.155:54321".parse()?), + listen_port: Some(54321), + auto_external_endpoint: false, + }; + init_wizard(&conf, opts).map_err(|_| anyhow!("init_wizard failed"))?; let interface = interface.parse().unwrap(); // Add developer CIDR and user CIDR and some peers for testing. diff --git a/shared/src/prompts.rs b/shared/src/prompts.rs index 73d7a56..19b770a 100644 --- a/shared/src/prompts.rs +++ b/shared/src/prompts.rs @@ -362,16 +362,20 @@ pub fn set_listen_port( } } -pub fn ask_endpoint() -> Result { +pub fn ask_endpoint(external_ip: Option) -> Result { println!("getting external IP address."); - let external_ip: Option = ureq::get("http://4.icanhazip.com") - .call() - .ok() - .map(|res| res.into_string().ok()) - .flatten() - .map(|body| body.trim().to_string()) - .and_then(|body| body.parse().ok()); + let external_ip = if external_ip.is_some() { + external_ip + } else { + ureq::get("http://4.icanhazip.com") + .call() + .ok() + .map(|res| res.into_string().ok()) + .flatten() + .map(|body| body.trim().to_string()) + .and_then(|body| body.parse().ok()) + }; let mut endpoint_builder = Input::with_theme(&*THEME); if let Some(ip) = external_ip { @@ -386,7 +390,11 @@ pub fn ask_endpoint() -> Result { } pub fn override_endpoint(unset: bool) -> Result>, Error> { - let endpoint = if !unset { Some(ask_endpoint()?) } else { None }; + let endpoint = if !unset { + Some(ask_endpoint(None)?) + } else { + None + }; Ok( if Confirm::with_theme(&*THEME)