{client,server}: allow peer/cidr creation with CLI arguments (#48)

Fixes #20
This commit is contained in:
Jake McGinty 2021-04-15 00:25:31 +09:00 committed by GitHub
parent a1818d9618
commit 6d28e7f4ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 207 additions and 73 deletions

1
Cargo.lock generated
View file

@ -1165,6 +1165,7 @@ dependencies = [
"lazy_static",
"regex",
"serde",
"structopt",
"toml",
"ureq",
"wgctrl",

View file

@ -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<Cidr> = 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<Peer> = 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)?,

View file

@ -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<S>,
) -> Result<T, Error> {
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)?)?

View file

@ -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<Mutex<Connection>>;
@ -201,8 +211,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
},
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!(
"

View file

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

View file

@ -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<bool, Error> {
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)
}

View file

@ -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<Option<CidrContents>, 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<Option<CidrContents>, 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<Option<CidrContents>, 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<Option<(PeerContents, KeyPair)>, 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<String>,
) -> 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)?;

View file

@ -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<String>,
/// Specify desired IP of new peer (within parent CIDR)
#[structopt(long, conflicts_with = "auto-ip")]
pub ip: Option<IpAddr>,
/// 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<String>,
/// Make new peer an admin
#[structopt(long)]
pub admin: Option<bool>,
/// Force confirmation
#[structopt(short, long)]
pub force: bool,
#[structopt(long)]
pub save_config: Option<String>,
}
#[derive(Debug, Clone, PartialEq, StructOpt)]
pub struct AddCidrContents {
#[structopt(long)]
pub name: Option<String>,
#[structopt(long)]
pub cidr: Option<IpNetwork>,
#[structopt(long)]
pub parent: Option<String>,
#[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(_)));
}
}
}

View file

@ -309,11 +309,14 @@ mod tests {
#[test]
fn test_interface_names() {
assert_eq!("wg-01".parse::<InterfaceName>().unwrap().as_str_lossy(), "wg-01");
assert_eq!(
"wg-01".parse::<InterfaceName>().unwrap().as_str_lossy(),
"wg-01"
);
assert!("longer-nul\0".parse::<InterfaceName>().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