From 6d28e7f4ab20800e30ca759727e9cfbd9d6da17c Mon Sep 17 00:00:00 2001 From: Jake McGinty Date: Thu, 15 Apr 2021 00:25:31 +0900 Subject: [PATCH] {client,server}: allow peer/cidr creation with CLI arguments (#48) Fixes #20 --- Cargo.lock | 1 + client/src/main.rs | 33 ++++++++---- client/src/util.rs | 14 +++--- server/src/main.rs | 37 ++++++++++---- shared/Cargo.toml | 1 + shared/src/lib.rs | 28 ++++++----- shared/src/prompts.rs | 109 ++++++++++++++++++++++++++++------------ shared/src/types.rs | 50 +++++++++++++++++- wgctrl-rs/src/device.rs | 7 ++- 9 files changed, 207 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 588b60b..49a45b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1165,6 +1165,7 @@ dependencies = [ "lazy_static", "regex", "serde", + "structopt", "toml", "ureq", "wgctrl", diff --git a/client/src/main.rs b/client/src/main.rs index c41e00f..f90db2f 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -3,9 +3,9 @@ use dialoguer::{Confirm, Input}; use hostsfile::HostsBuilder; use indoc::printdoc; use shared::{ - interface_config::InterfaceConfig, prompts, Association, AssociationContents, Cidr, CidrTree, - EndpointContents, Interface, IoErrorContext, Peer, RedeemContents, State, CLIENT_CONFIG_PATH, - REDEEM_TRANSITION_WAIT, + interface_config::InterfaceConfig, prompts, AddCidrContents, AddPeerContents, Association, + AssociationContents, Cidr, CidrTree, EndpointContents, Interface, IoErrorContext, Peer, + RedeemContents, State, CLIENT_CONFIG_PATH, REDEEM_TRANSITION_WAIT, }; use std::{ fmt, @@ -103,10 +103,20 @@ enum Command { Down { interface: Interface }, /// Add a new peer. - AddPeer { interface: Interface }, + AddPeer { + interface: Interface, + + #[structopt(flatten)] + args: AddPeerContents, + }, /// Add a new CIDR. - AddCidr { interface: Interface }, + AddCidr { + interface: Interface, + + #[structopt(flatten)] + args: AddCidrContents, + }, /// Disable an enabled peer. DisablePeer { interface: Interface }, @@ -463,13 +473,13 @@ fn uninstall(interface: &InterfaceName) -> Result<(), Error> { Ok(()) } -fn add_cidr(interface: &InterfaceName) -> Result<(), Error> { +fn add_cidr(interface: &InterfaceName, args: AddCidrContents) -> Result<(), Error> { let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?; println!("Fetching CIDRs"); let api = Api::new(&server); let cidrs: Vec = api.http("GET", "/admin/cidrs")?; - let cidr_request = prompts::add_cidr(&cidrs)?; + let cidr_request = prompts::add_cidr(&cidrs, &args)?; println!("Creating CIDR..."); let cidr: Cidr = api.http_form("POST", "/admin/cidrs", cidr_request)?; @@ -489,7 +499,7 @@ fn add_cidr(interface: &InterfaceName) -> Result<(), Error> { Ok(()) } -fn add_peer(interface: &InterfaceName) -> Result<(), Error> { +fn add_peer(interface: &InterfaceName, args: AddPeerContents) -> Result<(), Error> { let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?; let api = Api::new(&server); @@ -499,7 +509,7 @@ fn add_peer(interface: &InterfaceName) -> Result<(), Error> { let peers: Vec = api.http("GET", "/admin/peers")?; let cidr_tree = CidrTree::new(&cidrs[..]); - if let Some((peer_request, keypair)) = prompts::add_peer(&peers, &cidr_tree)? { + if let Some((peer_request, keypair)) = prompts::add_peer(&peers, &cidr_tree, &args)? { println!("Creating peer..."); let peer: Peer = api.http_form("POST", "/admin/peers", peer_request)?; let server_peer = peers.iter().find(|p| p.id == 1).unwrap(); @@ -510,6 +520,7 @@ fn add_peer(interface: &InterfaceName) -> Result<(), Error> { &cidr_tree, keypair, &server.internal_endpoint, + &args.save_config, )?; } else { println!("exited without creating peer."); @@ -838,8 +849,8 @@ fn run(opt: Opt) -> Result<(), Error> { )?, Command::Down { interface } => wg::down(&interface)?, Command::Uninstall { interface } => uninstall(&interface)?, - Command::AddPeer { interface } => add_peer(&interface)?, - Command::AddCidr { interface } => add_cidr(&interface)?, + Command::AddPeer { interface, args } => add_peer(&interface, args)?, + Command::AddCidr { interface, args } => add_cidr(&interface, args)?, Command::DisablePeer { interface } => enable_or_disable_peer(&interface, false)?, Command::EnablePeer { interface } => enable_or_disable_peer(&interface, true)?, Command::AddAssociation { interface } => add_association(&interface)?, diff --git a/client/src/util.rs b/client/src/util.rs index 75c7c2b..7153faa 100644 --- a/client/src/util.rs +++ b/client/src/util.rs @@ -2,8 +2,8 @@ use crate::{ClientError, Error}; use colored::*; use serde::{de::DeserializeOwned, Serialize}; use shared::{interface_config::ServerInfo, INNERNET_PUBKEY_HEADER}; -use ureq::{Agent, AgentBuilder}; use std::time::Duration; +use ureq::{Agent, AgentBuilder}; pub fn human_duration(duration: Duration) -> String { match duration.as_secs() { @@ -81,11 +81,13 @@ impl<'a> Api<'a> { endpoint: &str, form: Option, ) -> Result { - let request = self.agent.request( - verb, - &format!("http://{}/v1{}", self.server.internal_endpoint, endpoint), - ) - .set(INNERNET_PUBKEY_HEADER, &self.server.public_key); + let request = self + .agent + .request( + verb, + &format!("http://{}/v1{}", self.server.internal_endpoint, endpoint), + ) + .set(INNERNET_PUBKEY_HEADER, &self.server.public_key); let response = if let Some(form) = form { request.send_json(serde_json::to_value(form)?)? diff --git a/server/src/main.rs b/server/src/main.rs index 2a1a5bc..6435d39 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -7,7 +7,7 @@ use ipnetwork::IpNetwork; use parking_lot::Mutex; use rusqlite::Connection; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use shared::{IoErrorContext, INNERNET_PUBKEY_HEADER}; +use shared::{AddCidrContents, AddPeerContents, IoErrorContext, INNERNET_PUBKEY_HEADER}; use std::{ convert::Infallible, env, @@ -60,10 +60,20 @@ enum Command { Serve { interface: Interface }, /// Add a peer to an existing network. - AddPeer { interface: Interface }, + AddPeer { + interface: Interface, + + #[structopt(flatten)] + args: AddPeerContents, + }, /// Add a new CIDR to an existing network. - AddCidr { interface: Interface }, + AddCidr { + interface: Interface, + + #[structopt(flatten)] + args: AddCidrContents, + }, } pub type Db = Arc>; @@ -201,8 +211,8 @@ async fn main() -> Result<(), Box> { }, Command::Uninstall { interface } => uninstall(&interface, &conf)?, Command::Serve { interface } => serve(&interface, &conf).await?, - Command::AddPeer { interface } => add_peer(&interface, &conf)?, - Command::AddCidr { interface } => add_cidr(&interface, &conf)?, + Command::AddPeer { interface, args } => add_peer(&interface, &conf, args)?, + Command::AddCidr { interface, args } => add_cidr(&interface, &conf, args)?, } Ok(()) @@ -224,7 +234,11 @@ fn open_database_connection( Ok(Connection::open(&database_path)?) } -fn add_peer(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Error> { +fn add_peer( + interface: &InterfaceName, + conf: &ServerConfig, + args: AddPeerContents, +) -> Result<(), Error> { let config = ConfigFile::from_file(conf.config_path(interface))?; let conn = open_database_connection(interface, conf)?; let peers = DatabasePeer::list(&conn)? @@ -234,7 +248,7 @@ fn add_peer(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Error> let cidrs = DatabaseCidr::list(&conn)?; let cidr_tree = CidrTree::new(&cidrs[..]); - if let Some((peer_request, keypair)) = shared::prompts::add_peer(&peers, &cidr_tree)? { + if let Some((peer_request, keypair)) = shared::prompts::add_peer(&peers, &cidr_tree, &args)? { 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. @@ -254,6 +268,7 @@ fn add_peer(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Error> &cidr_tree, keypair, &SocketAddr::new(config.address, config.listen_port), + &args.save_config, )?; } else { println!("exited without creating peer."); @@ -262,10 +277,14 @@ fn add_peer(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Error> Ok(()) } -fn add_cidr(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Error> { +fn add_cidr( + interface: &InterfaceName, + conf: &ServerConfig, + args: AddCidrContents, +) -> Result<(), Error> { let conn = open_database_connection(interface, conf)?; let cidrs = DatabaseCidr::list(&conn)?; - if let Some(cidr_request) = shared::prompts::add_cidr(&cidrs)? { + if let Some(cidr_request) = shared::prompts::add_cidr(&cidrs, &args)? { let cidr = DatabaseCidr::create(&conn, cidr_request)?; printdoc!( " diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 31a9755..1a7a5a4 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -14,6 +14,7 @@ ipnetwork = { git = "https://github.com/mcginty/ipnetwork" } # pending https://g lazy_static = "1" regex = "1" serde = { version = "1", features = ["derive"] } +structopt = "0.3" toml = "0.5" ureq = { version = "2", default-features = false } wgctrl = { path = "../wgctrl-rs" } diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 026b1c3..cc02908 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -40,7 +40,11 @@ pub fn ensure_dirs_exist(dirs: &[&Path]) -> Result<(), Error> { _ => { let target_file = File::open(dir).with_path(dir)?; if chmod(&target_file, 0o700)? { - println!("{} updated permissions for {} to 0700.", "[!]".yellow(), dir.display()); + println!( + "{} updated permissions for {} to 0700.", + "[!]".yellow(), + dir.display() + ); } }, } @@ -52,16 +56,16 @@ pub fn ensure_dirs_exist(dirs: &[&Path]) -> Result<(), Error> { /// permissions had to be changed, `Ok(false)` if permissions were already /// correct. pub fn chmod(file: &File, new_mode: u32) -> Result { - let metadata = file.metadata()?; - let mut permissions = metadata.permissions(); - let mode = permissions.mode() & 0o777; - let updated = if mode != new_mode { - permissions.set_mode(new_mode); - file.set_permissions(permissions)?; - true - } else { - false - }; + let metadata = file.metadata()?; + let mut permissions = metadata.permissions(); + let mode = permissions.mode() & 0o777; + let updated = if mode != new_mode { + permissions.set_mode(new_mode); + file.set_permissions(permissions)?; + true + } else { + false + }; - Ok(updated) + Ok(updated) } diff --git a/shared/src/prompts.rs b/shared/src/prompts.rs index 650ae04..0934a74 100644 --- a/shared/src/prompts.rs +++ b/shared/src/prompts.rs @@ -1,7 +1,7 @@ use crate::{ interface_config::{InterfaceConfig, InterfaceInfo, ServerInfo}, - Association, Cidr, CidrContents, CidrTree, Error, Peer, PeerContents, - PERSISTENT_KEEPALIVE_INTERVAL_SECS, + AddCidrContents, AddPeerContents, Association, Cidr, CidrContents, CidrTree, Error, Peer, + PeerContents, PERSISTENT_KEEPALIVE_INTERVAL_SECS, }; use colored::*; use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select}; @@ -32,10 +32,27 @@ pub fn hostname_validator(name: &String) -> Result<(), &'static str> { } /// Bring up a prompt to create a new CIDR. Returns the peer request. -pub fn add_cidr(cidrs: &[Cidr]) -> Result, Error> { - let parent_cidr = choose_cidr(cidrs, "Parent CIDR")?; - let name: String = Input::with_theme(&*THEME).with_prompt("Name").interact()?; - let cidr: IpNetwork = Input::with_theme(&*THEME).with_prompt("CIDR").interact()?; +pub fn add_cidr(cidrs: &[Cidr], request: &AddCidrContents) -> Result, Error> { + let parent_cidr = if let Some(ref parent_name) = request.parent { + cidrs + .iter() + .find(|cidr| &cidr.name == parent_name) + .ok_or("No parent CIDR with that name exists.")? + } else { + choose_cidr(cidrs, "Parent CIDR")? + }; + + let name = if let Some(ref name) = request.name { + name.clone() + } else { + Input::with_theme(&*THEME).with_prompt("Name").interact()? + }; + + let cidr = if let Some(cidr) = request.cidr { + cidr + } else { + Input::with_theme(&*THEME).with_prompt("CIDR").interact()? + }; let cidr_request = CidrContents { name, @@ -44,10 +61,11 @@ pub fn add_cidr(cidrs: &[Cidr]) -> Result, Error> { }; Ok( - if Confirm::with_theme(&*THEME) - .with_prompt(&format!("Create CIDR \"{}\"?", cidr_request.name)) - .default(false) - .interact()? + if request.force + || Confirm::with_theme(&*THEME) + .with_prompt(&format!("Create CIDR \"{}\"?", cidr_request.name)) + .default(false) + .interact()? { Some(cidr_request) } else { @@ -143,10 +161,18 @@ pub fn delete_association<'a>( pub fn add_peer( peers: &[Peer], cidr_tree: &CidrTree, + args: &AddPeerContents, ) -> Result, Error> { let leaves = cidr_tree.leaves(); - let cidr = choose_cidr(&leaves[..], "Eligible CIDRs for peer")?; + let cidr = if let Some(ref parent_name) = args.cidr { + leaves + .iter() + .find(|cidr| &cidr.name == parent_name) + .ok_or("No eligible CIDR with that name exists.")? + } else { + choose_cidr(&leaves[..], "Eligible CIDRs for peer")? + }; let mut available_ip = None; let candidate_ips = cidr.iter().filter(|ip| cidr.is_assignable(*ip)); @@ -159,20 +185,35 @@ pub fn add_peer( let available_ip = available_ip.expect("No IPs in this CIDR are avavilable"); - let ip = Input::with_theme(&*THEME) - .with_prompt("IP") - .default(available_ip) - .interact()?; + let ip = if let Some(ip) = args.ip { + ip + } else if args.auto_ip { + available_ip + } else { + Input::with_theme(&*THEME) + .with_prompt("IP") + .default(available_ip) + .interact()? + }; - let name: String = Input::with_theme(&*THEME) - .with_prompt("Name") - .validate_with(hostname_validator) - .interact()?; + let name = if let Some(ref name) = args.name { + name.clone() + } else { + Input::with_theme(&*THEME) + .with_prompt("Name") + .validate_with(hostname_validator) + .interact()? + }; + + let is_admin = if let Some(is_admin) = args.admin { + is_admin + } else { + Confirm::with_theme(&*THEME) + .with_prompt(&format!("Make {} an admin?", name)) + .default(false) + .interact()? + }; - let is_admin = Confirm::with_theme(&*THEME) - .with_prompt(&format!("Make {} an admin?", name)) - .default(false) - .interact()?; let default_keypair = KeyPair::generate(); let peer_request = PeerContents { name, @@ -187,10 +228,11 @@ pub fn add_peer( }; Ok( - if Confirm::with_theme(&*THEME) - .with_prompt(&format!("Create peer {}?", peer_request.name.yellow())) - .default(false) - .interact()? + if args.force + || Confirm::with_theme(&*THEME) + .with_prompt(&format!("Create peer {}?", peer_request.name.yellow())) + .default(false) + .interact()? { Some((peer_request, default_keypair)) } else { @@ -245,6 +287,7 @@ pub fn save_peer_invitation( root_cidr: &Cidr, keypair: KeyPair, server_api_addr: &SocketAddr, + config_location: &Option, ) -> Result<(), Error> { let peer_invitation = InterfaceConfig { interface: InterfaceInfo { @@ -262,10 +305,14 @@ pub fn save_peer_invitation( }, }; - let invitation_save_path = Input::with_theme(&*THEME) - .with_prompt("Save peer invitation file as") - .default(format!("{}.toml", peer.name)) - .interact()?; + let invitation_save_path = if let Some(location) = config_location { + location.clone() + } else { + Input::with_theme(&*THEME) + .with_prompt("Save peer invitation file as") + .default(format!("{}.toml", peer.name)) + .interact()? + }; peer_invitation.write_to_path(&invitation_save_path, true, None)?; diff --git a/shared/src/types.rs b/shared/src/types.rs index 72e6526..b205ee4 100644 --- a/shared/src/types.rs +++ b/shared/src/types.rs @@ -1,5 +1,5 @@ -use ipnetwork::IpNetwork; use crate::prompts::hostname_validator; +use ipnetwork::IpNetwork; use serde::{Deserialize, Serialize}; use std::{ fmt::{Display, Formatter}, @@ -8,6 +8,7 @@ use std::{ path::Path, str::FromStr, }; +use structopt::StructOpt; use wgctrl::{InterfaceName, InvalidInterfaceName, Key, PeerConfig, PeerConfigBuilder}; #[derive(Debug, Clone)] @@ -165,6 +166,51 @@ pub struct RedeemContents { pub public_key: String, } +#[derive(Debug, Clone, PartialEq, StructOpt)] +pub struct AddPeerContents { + /// Name of new peer + #[structopt(long)] + pub name: Option, + + /// Specify desired IP of new peer (within parent CIDR) + #[structopt(long, conflicts_with = "auto-ip")] + pub ip: Option, + + /// Auto-assign the peer the first available IP within the CIDR + #[structopt(long = "auto-ip")] + pub auto_ip: bool, + + /// Name of CIDR to add new peer under + #[structopt(long)] + pub cidr: Option, + + /// Make new peer an admin + #[structopt(long)] + pub admin: Option, + + /// Force confirmation + #[structopt(short, long)] + pub force: bool, + + #[structopt(long)] + pub save_config: Option, +} + +#[derive(Debug, Clone, PartialEq, StructOpt)] +pub struct AddCidrContents { + #[structopt(long)] + pub name: Option, + + #[structopt(long)] + pub cidr: Option, + + #[structopt(long)] + pub parent: Option, + + #[structopt(short, long)] + pub force: bool, +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct PeerContents { pub name: String, @@ -393,4 +439,4 @@ mod tests { println!("{:?}", config); assert!(matches!(peer.diff(&config), Some(_))); } -} \ No newline at end of file +} diff --git a/wgctrl-rs/src/device.rs b/wgctrl-rs/src/device.rs index 1ff835d..5a9db5e 100644 --- a/wgctrl-rs/src/device.rs +++ b/wgctrl-rs/src/device.rs @@ -309,11 +309,14 @@ mod tests { #[test] fn test_interface_names() { - assert_eq!("wg-01".parse::().unwrap().as_str_lossy(), "wg-01"); + assert_eq!( + "wg-01".parse::().unwrap().as_str_lossy(), + "wg-01" + ); assert!("longer-nul\0".parse::().is_err()); let invalid_names = &[ - ("", InvalidInterfaceName::Empty), // Empty Rust string + ("", InvalidInterfaceName::Empty), // Empty Rust string ("\0", InvalidInterfaceName::InvalidChars), // Empty C string ("ifname\0nul", InvalidInterfaceName::InvalidChars), // Contains interior NUL ("if name", InvalidInterfaceName::InvalidChars), // Contains a space