use crate::util::all_installed;
use anyhow::{anyhow, bail, Error};
use clap::{ArgAction, Parser, Subcommand};
use colored::*;
use dialoguer::{Confirm, Input};
use indoc::eprintdoc;
use innernet_client_core::{
data_store::DataStore,
interface::{fetch, redeem_invite},
peer::create_peer,
rest_client::{RestClient, RestError},
DEFAULT_CONFIG_DIR, DEFAULT_DATA_DIR,
};
use innernet_shared::{
interface_config::InterfaceConfig, prompts, wg, wg::PeerInfoExt, AddCidrOpts,
AddDeleteAssociationOpts, AddPeerOpts, Association, AssociationContents, Cidr, CidrTree,
DeleteCidrOpts, EnableDisablePeerOpts, Endpoint, EndpointContents, HostsOpts, InstallOpts,
Interface, IoErrorContext, ListenPortOpts, NatOpts, NetworkOpts, OverrideEndpointOpts,
OverridePeerEndpointOpts, Peer, RenameCidrOpts, RenamePeerOpts, ServerCapabilities,
WrappedIoError,
};
use std::{
io,
path::{Path, PathBuf},
thread,
time::Duration,
};
use util::{human_duration, human_size};
use wireguard_control::{Device, InterfaceName, PeerInfo};
mod util;
struct PeerState<'a> {
peer: &'a Peer,
info: Option<&'a PeerInfo>,
}
macro_rules! println_pad {
($pad:expr, $($arg:tt)*) => {
print!("{:pad$}", "", pad = $pad);
println!($($arg)*);
}
}
#[derive(Clone, Debug, Parser)]
#[command(name = "innernet", author, version, about)]
struct Opts {
#[clap(subcommand)]
command: Option<Command>,
#[clap(short, long, action = ArgAction::Count)]
verbose: u8,
#[clap(short, long, default_value = DEFAULT_CONFIG_DIR)]
config_dir: PathBuf,
#[clap(short, long, default_value = DEFAULT_DATA_DIR)]
data_dir: PathBuf,
#[clap(flatten)]
network: NetworkOpts,
}
#[derive(Clone, Debug, Subcommand)]
enum Command {
#[clap(alias = "redeem")]
Install {
invite: PathBuf,
#[clap(flatten)]
hosts: HostsOpts,
#[clap(flatten)]
install_opts: InstallOpts,
#[clap(flatten)]
nat: NatOpts,
},
#[clap(alias = "list")]
Show {
#[clap(short, long)]
short: bool,
#[clap(short, long)]
tree: bool,
interface: Option<Interface>,
},
Up {
#[clap(short, long)]
daemon: bool,
#[clap(long, default_value = "60")]
interval: u64,
#[clap(flatten)]
hosts: HostsOpts,
#[clap(flatten)]
nat: NatOpts,
interface: Option<Interface>,
},
Fetch {
interface: Interface,
#[clap(flatten)]
hosts: HostsOpts,
#[clap(flatten)]
nat: NatOpts,
},
Uninstall {
interface: Interface,
#[clap(long)]
yes: bool,
},
Down { interface: Interface },
AddPeer {
interface: Interface,
#[clap(flatten)]
sub_opts: AddPeerOpts,
},
RenamePeer {
interface: Interface,
#[clap(flatten)]
sub_opts: RenamePeerOpts,
},
AddCidr {
interface: Interface,
#[clap(flatten)]
sub_opts: AddCidrOpts,
},
RenameCidr {
interface: Interface,
#[clap(flatten)]
sub_opts: RenameCidrOpts,
},
DeleteCidr {
interface: Interface,
#[clap(flatten)]
sub_opts: DeleteCidrOpts,
},
ListCidrs {
interface: Interface,
#[clap(short, long)]
tree: bool,
},
DisablePeer {
interface: Interface,
#[clap(flatten)]
sub_opts: EnableDisablePeerOpts,
},
EnablePeer {
interface: Interface,
#[clap(flatten)]
sub_opts: EnableDisablePeerOpts,
},
AddAssociation {
interface: Interface,
#[clap(flatten)]
sub_opts: AddDeleteAssociationOpts,
},
DeleteAssociation {
interface: Interface,
#[clap(flatten)]
sub_opts: AddDeleteAssociationOpts,
},
ListAssociations { interface: Interface },
SetListenPort {
interface: Interface,
#[clap(flatten)]
sub_opts: ListenPortOpts,
},
OverrideEndpoint {
interface: Interface,
#[clap(flatten)]
sub_opts: OverrideEndpointOpts,
},
OverridePeerEndpoint {
interface: Interface,
#[clap(flatten)]
sub_opts: OverridePeerEndpointOpts,
},
Completions {
#[clap(value_enum)]
shell: clap_complete::Shell,
},
}
fn install(
opts: &Opts,
hosts_opts: &HostsOpts,
install_opts: &InstallOpts,
nat_opts: &NatOpts,
invite: &Path,
) -> Result<(), Error> {
let config = InterfaceConfig::from_file(invite)?;
let interface_name = if install_opts.default_name {
config.interface.network_name.clone()
} else if let Some(ref iface) = install_opts.name {
iface.clone()
} else {
Input::with_theme(&*prompts::THEME)
.with_prompt("Interface name")
.default(config.interface.network_name.clone())
.interact()?
};
let interface_name = interface_name.parse()?;
redeem_invite(&opts.config_dir, &opts.network, &interface_name, config)?;
let mut fetch_success = false;
for _ in 0..3 {
if fetch(
&opts.config_dir,
&opts.data_dir,
&opts.network,
hosts_opts,
nat_opts,
&interface_name,
true,
)
.is_ok()
{
fetch_success = true;
break;
}
thread::sleep(Duration::from_secs(1));
}
if !fetch_success {
log::warn!(
"Failed to fetch peers from server, you will need to manually run the 'up' command.",
);
}
if install_opts.delete_invite
|| Confirm::with_theme(&*prompts::THEME)
.wait_for_newline(true)
.with_prompt(format!(
"Delete invitation file \"{}\" now? (It's no longer needed)",
invite.to_string_lossy().yellow()
))
.default(true)
.interact()?
{
std::fs::remove_file(invite).with_path(invite)?;
}
eprintdoc!(
"
{star} Done!
{interface} has been {installed}.
By default, innernet will write to your /etc/hosts file for peer name
resolution. To disable this behavior, use the --no-write-hosts or --write-hosts [PATH]
options.
See the manpage or innernet GitHub repo for more detailed instruction on managing your
interface and network. Have fun!
",
star = "[*]".dimmed(),
interface = interface_name.to_string().yellow(),
installed = "installed".green(),
);
if cfg!(target_os = "linux") {
eprintdoc!(
"
It's recommended to now keep the interface automatically refreshing via systemd:
{systemctl_enable}{interface}
",
interface = interface_name.to_string().yellow(),
systemctl_enable = "systemctl enable --now innernet@".yellow(),
);
} else if cfg!(target_os = "macos") {
eprintdoc!("
It's recommended to now keep the interface automatically refreshing, which you can
do via a launchd script (easier macOS helpers to be added to innernet in a later version).
Ex. to run innernet in a 60s update loop:
{daemon_mode} {interface}
",
interface = interface_name.to_string().yellow(),
daemon_mode = "innernet up -d --interval 60".yellow());
} else {
eprintdoc!(
"
It's recommended to now keep the interface automatically refreshing via whatever service
system your distribution provides.
Ex. to run innernet in a 60s update loop:
{daemon_mode} {interface}
",
interface = interface_name.to_string().yellow(),
daemon_mode = "innernet up -d --interval 60".yellow()
);
}
Ok(())
}
fn up(
specific_interface: Option<Interface>,
opts: &Opts,
loop_interval: Option<Duration>,
hosts_opts: HostsOpts,
nat_opts: &NatOpts,
) -> Result<(), Error> {
loop {
let interfaces = match &specific_interface {
Some(iface) => vec![iface.clone()],
None => all_installed(&opts.config_dir)?,
};
for interface in interfaces {
fetch(
&opts.config_dir,
&opts.data_dir,
&opts.network,
&hosts_opts,
nat_opts,
&interface,
true,
)?;
}
match loop_interval {
Some(interval) => thread::sleep(interval),
None => break,
}
}
Ok(())
}
fn uninstall(interface: &InterfaceName, opts: &Opts, yes: bool) -> Result<(), Error> {
let config = InterfaceConfig::get_path(&opts.config_dir, interface);
let data = DataStore::get_path(&opts.data_dir, interface);
if !config.exists() && !data.exists() {
bail!(
"No network named \"{}\" exists.",
interface.as_str_lossy().yellow()
);
}
if yes
|| Confirm::with_theme(&*prompts::THEME)
.with_prompt(format!(
"Permanently delete network \"{}\"?",
interface.as_str_lossy().yellow()
))
.default(false)
.wait_for_newline(true)
.interact()?
{
log::info!("bringing down interface (if up).");
wg::down(interface, opts.network.backend).ok();
std::fs::remove_file(&config)
.with_path(&config)
.map_err(|e| log::warn!("{}", e.to_string().yellow()))
.ok();
std::fs::remove_file(&data)
.with_path(&data)
.map_err(|e| log::warn!("{}", e.to_string().yellow()))
.ok();
log::info!(
"network {} is uninstalled.",
interface.as_str_lossy().yellow()
);
}
Ok(())
}
fn add_cidr(interface: &InterfaceName, opts: &Opts, sub_opts: AddCidrOpts) -> Result<(), Error> {
let InterfaceConfig { server, .. } =
InterfaceConfig::from_interface(&opts.config_dir, interface)?;
log::info!("Fetching CIDRs");
let rest_client = RestClient::new(&server);
let cidrs: Vec<Cidr> = rest_client.http("GET", "/admin/cidrs")?;
if let Some(cidr_request) = prompts::add_cidr(&cidrs, &sub_opts)? {
log::info!("Creating CIDR...");
let cidr: Cidr = rest_client.http_form("POST", "/admin/cidrs", cidr_request)?;
eprintdoc!(
"
CIDR \"{cidr_name}\" added.
Right now, peers within {cidr_name} can only see peers in the same CIDR
, and in the special \"infra\" CIDR that includes the innernet server peer.
You'll need to add more associations for peers in diffent CIDRs to communicate.
",
cidr_name = cidr.name.bold()
);
} else {
log::info!("exited without creating CIDR.");
}
Ok(())
}
fn rename_cidr(
interface: &InterfaceName,
opts: &Opts,
sub_opts: RenameCidrOpts,
) -> Result<(), Error> {
let InterfaceConfig { server, .. } =
InterfaceConfig::from_interface(&opts.config_dir, interface)?;
let rest_client = RestClient::new(&server);
log::info!("Fetching CIDRs");
let cidrs: Vec<Cidr> = rest_client.http("GET", "/admin/cidrs")?;
if let Some((cidr_request, old_name)) = prompts::rename_cidr(&cidrs, &sub_opts)? {
log::info!("Renaming CIDR...");
let id = cidrs
.iter()
.find(|c| c.name == old_name)
.ok_or_else(|| anyhow!("CIDR not found."))?
.id;
rest_client.http_form::<_, ()>("PUT", &format!("/admin/cidrs/{id}"), cidr_request)?;
log::info!("CIDR renamed.");
} else {
log::info!("Exited without renaming CIDR.");
}
Ok(())
}
fn delete_cidr(
interface: &InterfaceName,
opts: &Opts,
sub_opts: DeleteCidrOpts,
) -> Result<(), Error> {
let InterfaceConfig { server, .. } =
InterfaceConfig::from_interface(&opts.config_dir, interface)?;
println!("Fetching eligible CIDRs");
let rest_client = RestClient::new(&server);
let cidrs: Vec<Cidr> = rest_client.http("GET", "/admin/cidrs")?;
let peers: Vec<Peer> = rest_client.http("GET", "/admin/peers")?;
let cidr_id = prompts::delete_cidr(&cidrs, &peers, &sub_opts)?;
println!("Deleting CIDR...");
rest_client.http::<()>("DELETE", &format!("/admin/cidrs/{cidr_id}"))?;
println!("CIDR deleted.");
Ok(())
}
fn list_cidrs(interface: &InterfaceName, opts: &Opts, tree: bool) -> Result<(), Error> {
let data_store = DataStore::open(&opts.data_dir, interface)?;
let config = InterfaceConfig::from_interface(&opts.config_dir, interface)?;
if tree {
let cidr_tree = CidrTree::new(data_store.cidrs());
colored::control::set_override(false);
print_tree(&cidr_tree, &[], 0, false, &config);
colored::control::unset_override();
} else {
for cidr in data_store.cidrs() {
println!("{} {}", cidr.cidr, cidr.name);
}
}
Ok(())
}
fn add_peer(interface: &InterfaceName, opts: &Opts, sub_opts: AddPeerOpts) -> Result<(), Error> {
let InterfaceConfig { server, .. } =
InterfaceConfig::from_interface(&opts.config_dir, interface)?;
let rest_client = RestClient::new(&server);
log::info!("Fetching CIDRs");
let cidrs = rest_client.get_cidrs()?;
log::info!("Fetching peers");
let peers = rest_client.get_peers()?;
let cidr_tree = CidrTree::new(&cidrs[..]);
if let Some((new_peer_info, target_path)) =
prompts::gather_new_peer_info(&peers, &cidr_tree, &sub_opts)?
{
log::info!("Creating peer...");
let (peer, invitation) =
create_peer(&opts.config_dir, interface, &cidrs, &peers, new_peer_info)?;
invitation.save_new(&target_path)?;
prompts::print_invitation_info(&peer, &target_path);
} else {
log::info!("Exited without creating peer.");
}
Ok(())
}
fn rename_peer(
interface: &InterfaceName,
opts: &Opts,
sub_opts: RenamePeerOpts,
) -> Result<(), Error> {
let InterfaceConfig { server, .. } =
InterfaceConfig::from_interface(&opts.config_dir, interface)?;
let rest_client = RestClient::new(&server);
log::info!("Fetching peers");
let peers: Vec<Peer> = rest_client.http("GET", "/admin/peers")?;
if let Some((peer_request, old_name)) = prompts::rename_peer(&peers, &sub_opts)? {
log::info!("Renaming peer...");
let id = peers
.iter()
.filter(|p| p.name == old_name)
.map(|p| p.id)
.next()
.ok_or_else(|| anyhow!("Peer not found."))?;
rest_client.http_form::<_, ()>("PUT", &format!("/admin/peers/{id}"), peer_request)?;
log::info!("Peer renamed.");
} else {
log::info!("exited without renaming peer.");
}
Ok(())
}
fn enable_or_disable_peer(
interface: &InterfaceName,
opts: &Opts,
sub_opts: EnableDisablePeerOpts,
enable: bool,
) -> Result<(), Error> {
let InterfaceConfig { server, .. } =
InterfaceConfig::from_interface(&opts.config_dir, interface)?;
let rest_client = RestClient::new(&server);
log::info!("Fetching peers.");
let peers: Vec<Peer> = rest_client.http("GET", "/admin/peers")?;
if let Some(peer) = prompts::enable_or_disable_peer(&peers[..], &sub_opts, enable)? {
let Peer { id, mut contents } = peer;
contents.is_disabled = !enable;
rest_client.http_form::<_, ()>("PUT", &format!("/admin/peers/{id}"), contents)?;
} else {
log::info!("exiting without enabling or disabling peer.");
}
Ok(())
}
fn add_association(
interface: &InterfaceName,
opts: &Opts,
sub_opts: AddDeleteAssociationOpts,
) -> Result<(), Error> {
let InterfaceConfig { server, .. } =
InterfaceConfig::from_interface(&opts.config_dir, interface)?;
let rest_client = RestClient::new(&server);
log::info!("Fetching CIDRs");
let cidrs: Vec<Cidr> = rest_client.http("GET", "/admin/cidrs")?;
let association = if let (Some(ref cidr1), Some(ref cidr2)) = (&sub_opts.cidr1, &sub_opts.cidr2)
{
let cidr1 = cidrs
.iter()
.find(|c| &c.name == cidr1)
.ok_or_else(|| anyhow!("can't find cidr '{}'", cidr1))?;
let cidr2 = cidrs
.iter()
.find(|c| &c.name == cidr2)
.ok_or_else(|| anyhow!("can't find cidr '{}'", cidr2))?;
(cidr1, cidr2)
} else if let Some((cidr1, cidr2)) = prompts::add_association(&cidrs[..], &sub_opts)? {
(cidr1, cidr2)
} else {
log::info!("exiting without adding association.");
return Ok(());
};
rest_client.http_form::<_, ()>(
"POST",
"/admin/associations",
AssociationContents {
cidr_id_1: association.0.id,
cidr_id_2: association.1.id,
},
)?;
Ok(())
}
fn delete_association(
interface: &InterfaceName,
opts: &Opts,
sub_opts: AddDeleteAssociationOpts,
) -> Result<(), Error> {
let InterfaceConfig { server, .. } =
InterfaceConfig::from_interface(&opts.config_dir, interface)?;
let rest_client = RestClient::new(&server);
log::info!("Fetching CIDRs");
let cidrs: Vec<Cidr> = rest_client.http("GET", "/admin/cidrs")?;
log::info!("Fetching associations");
let associations: Vec<Association> = rest_client.http("GET", "/admin/associations")?;
if let Some(association) =
prompts::delete_association(&associations[..], &cidrs[..], &sub_opts)?
{
rest_client.http::<()>("DELETE", &format!("/admin/associations/{}", association.id))?;
} else {
log::info!("exiting without adding association.");
}
Ok(())
}
fn list_associations(interface: &InterfaceName, opts: &Opts) -> Result<(), Error> {
let InterfaceConfig { server, .. } =
InterfaceConfig::from_interface(&opts.config_dir, interface)?;
let rest_client = RestClient::new(&server);
log::info!("Fetching CIDRs");
let cidrs: Vec<Cidr> = rest_client.http("GET", "/admin/cidrs")?;
log::info!("Fetching associations");
let associations: Vec<Association> = rest_client.http("GET", "/admin/associations")?;
for association in associations {
println!(
"{}: {} <=> {}",
association.id,
&cidrs
.iter()
.find(|c| c.id == association.cidr_id_1)
.unwrap()
.name
.yellow(),
&cidrs
.iter()
.find(|c| c.id == association.cidr_id_2)
.unwrap()
.name
.yellow()
);
}
Ok(())
}
fn set_listen_port(
interface: &InterfaceName,
opts: &Opts,
sub_opts: ListenPortOpts,
) -> Result<(), Error> {
let mut config = InterfaceConfig::from_interface(&opts.config_dir, interface)?;
let listen_port = prompts::set_listen_port(&config.interface, sub_opts)?;
if let Some(listen_port) = listen_port {
innernet_client_core::set_listen_port(
opts.network.backend,
&opts.config_dir,
interface,
&mut config,
listen_port,
)?;
} else {
log::info!("exiting without updating the listen port.");
}
Ok(())
}
fn override_endpoint(
interface: &InterfaceName,
opts: &Opts,
sub_opts: OverrideEndpointOpts,
) -> Result<(), Error> {
let config = InterfaceConfig::from_interface(&opts.config_dir, interface)?;
let endpoint_contents = if sub_opts.unset {
prompt_unset_override_endpoint(&sub_opts)?.then_some(EndpointContents::Unset)
} else {
let server_capabilities = get_server_capabilities(&config)?;
let port = match config.interface.listen_port {
Some(port) => port,
None => bail!("you need to set a listen port with set-listen-port before overriding the endpoint (otherwise port randomization on the interface would make it useless).")
};
let endpoint = prompt_override_endpoint(&server_capabilities, &sub_opts, port)?;
endpoint.map(EndpointContents::Set)
};
if let Some(contents) = endpoint_contents {
log::info!("requesting endpoint update...");
RestClient::new(&config.server).http_form::<_, ()>("PUT", "/user/endpoint", contents)?;
log::info!(
"endpoint override {}",
if sub_opts.unset { "unset" } else { "set" }
);
} else {
log::info!("exiting without overriding endpoint");
}
Ok(())
}
fn override_peer_endpoint(
interface: &InterfaceName,
opts: &Opts,
sub_opts: OverridePeerEndpointOpts,
) -> Result<(), Error> {
let mut config = InterfaceConfig::from_interface(&opts.config_dir, interface)?;
let data_store = DataStore::open(&opts.data_dir, interface)?;
if let Some((peer, endpoint_opt)) = prompts::override_peer_endpoint_prompt(
data_store.peers(),
config.peer_endpoint_overrides(),
&sub_opts,
)? {
if let Some(endpoint) = endpoint_opt {
log::info!(
"overriding endpoint for peer IP {} with endpoint {}",
peer.ip,
endpoint
);
innernet_client_core::set_endpoint_override_for_peer(
opts.network.backend,
&opts.config_dir,
interface,
&mut config,
&peer,
endpoint,
)?;
} else {
log::info!("unsetting endpoint override for peer IP {}", peer.ip);
innernet_client_core::unset_endpoint_override_for_peer(
&opts.config_dir,
interface,
&mut config,
&peer,
)?;
log::info!("normal peer endpoint/NAT traversal will be restored on the next call to 'innernet fetch'");
}
} else {
log::info!("exiting without overriding peer endpoint");
}
Ok(())
}
fn prompt_override_endpoint(
server_capabilities: &ServerCapabilities,
args: &OverrideEndpointOpts,
listen_port: u16,
) -> Result<Option<Endpoint>, Error> {
let unspecified_ip_supported = server_capabilities.unspecified_ip_in_override_endpoint;
let endpoint = match &args.endpoint {
Some(endpoint) => endpoint.clone(),
None => {
let external_ip = if unspecified_ip_supported {
prompts::unspecified_ip_and_auto_detection_flow()?
} else {
prompts::ip_auto_detection_flow()?
};
prompts::input_external_endpoint(external_ip, listen_port)?
},
};
if endpoint.is_host_unspecified() && !unspecified_ip_supported {
bail!(
"Attempted to use an unspecified IP (all zeros) but the innernet server does
not have the capability to resolve it, likely because its version is older."
)
}
if args.yes || prompts::confirm(&format!("Set external endpoint to {endpoint}?"))? {
Ok(Some(endpoint))
} else {
Ok(None)
}
}
fn prompt_unset_override_endpoint(args: &OverrideEndpointOpts) -> Result<bool, Error> {
Ok(args.yes
|| prompts::confirm("Unset external endpoint to enable automatic endpoint discovery?")?)
}
fn get_server_capabilities(config: &InterfaceConfig) -> Result<ServerCapabilities, Error> {
let rest_client = RestClient::new(&config.server);
let maybe_info: Result<ServerCapabilities, RestError> =
rest_client.http("GET", "/user/capabilities");
match maybe_info {
Ok(info) => Ok(info),
Err(e) => {
if e.has_status_of(404) {
log::debug!(
"innernet server endpoint capabilities not found, assuming default capabilities"
);
Ok(Default::default())
} else {
bail!(e)
}
},
}
}
fn show(opts: &Opts, short: bool, tree: bool, interface: Option<Interface>) -> Result<(), Error> {
let interfaces = interface.map_or_else(
|| Device::list(opts.network.backend),
|interface| Ok(vec![*interface]),
)?;
let devices = interfaces
.into_iter()
.map(|name| -> Result<Option<_>, Error> {
match DataStore::open(&opts.data_dir, &name) {
Ok(store) => {
let device =
Device::get(&name, opts.network.backend).with_str(name.as_str_lossy())?;
let config = InterfaceConfig::from_interface(&opts.config_dir, &name)?;
Ok(Some((device, store, config)))
},
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
})
.filter_map(Result::transpose)
.collect::<Result<Vec<_>, _>>()?;
if devices.is_empty() {
log::info!("No innernet networks currently running.");
return Ok(());
}
for (device_info, store, interface_config) in devices {
let public_key = match &device_info.public_key {
Some(key) => key.to_base64(),
None => {
log::warn!(
"network {} is missing public key.",
device_info.name.to_string().yellow()
);
continue;
},
};
let peers = store.peers();
let cidrs = store.cidrs();
let me = peers
.iter()
.find(|p| p.public_key == public_key)
.ok_or_else(|| anyhow!("missing peer info"))?;
let mut peer_states = device_info
.peers
.iter()
.map(|info| {
let public_key = info.config.public_key.to_base64();
match peers.iter().find(|p| p.public_key == public_key) {
Some(peer) => Ok(PeerState {
peer,
info: Some(info),
}),
None => Err(anyhow!("peer {} isn't an innernet peer.", public_key)),
}
})
.collect::<Result<Vec<PeerState>, _>>()?;
peer_states.push(PeerState {
peer: me,
info: None,
});
print_interface(&device_info, short || tree)?;
peer_states.sort_by_key(|peer| peer.peer.ip);
let verbose = opts.verbose > 0;
if tree {
let cidr_tree = CidrTree::new(cidrs);
print_tree(&cidr_tree, &peer_states, 1, verbose, &interface_config);
} else {
for peer_state in peer_states {
let endpoint_has_local_override = interface_config
.peer_endpoint_overrides()
.contains_key(&peer_state.peer.contents.ip);
print_peer(&peer_state, short, 1, verbose, endpoint_has_local_override);
}
}
}
Ok(())
}
fn print_tree(
cidr: &CidrTree,
peers: &[PeerState],
level: usize,
verbose: bool,
interface_config: &InterfaceConfig,
) {
println_pad!(
level * 2,
"{} {}",
cidr.cidr.to_string().bold().blue(),
cidr.name.blue(),
);
let mut children: Vec<_> = cidr.children().collect();
children.sort();
children
.iter()
.for_each(|child| print_tree(child, peers, level + 1, verbose, interface_config));
for peer in peers.iter().filter(|p| p.peer.cidr_id == cidr.id) {
let endpoint_has_local_override = interface_config
.peer_endpoint_overrides()
.contains_key(&peer.peer.contents.ip);
print_peer(peer, true, level, verbose, endpoint_has_local_override);
}
}
fn print_interface(device_info: &Device, short: bool) -> Result<(), Error> {
if short {
let listen_port_str = device_info
.listen_port
.map(|p| format!("(:{p}) "))
.unwrap_or_default();
println!(
"{} {}",
device_info.name.to_string().green().bold(),
listen_port_str.dimmed(),
);
} else {
println!(
"{}: {}",
"network".green().bold(),
device_info.name.to_string().green(),
);
if let Some(listen_port) = device_info.listen_port {
println!(" {}: {}", "listening port".bold(), listen_port);
}
}
Ok(())
}
fn print_peer(
peer: &PeerState,
short: bool,
level: usize,
verbose: bool,
endpoint_has_local_override: bool,
) {
let pad = level * 2;
let PeerState { peer, info } = peer;
let public_key = if verbose {
&peer.public_key
} else {
&format!("{}…", &peer.public_key[..10])
};
let endpoint_override_msg = if endpoint_has_local_override {
" (endpoint overridden locally)"
} else {
""
};
if short {
let connected = info
.map(|info| info.is_recently_connected())
.unwrap_or_default();
let is_you = info.is_none();
println_pad!(
pad,
"| {} {}: {} ({}{}){endpoint_override_msg}",
if connected || is_you {
"â—‰".bold()
} else {
"â—¯".dimmed()
},
peer.ip.to_string().yellow().bold(),
peer.name.yellow(),
if is_you { "you, " } else { "" },
public_key.dimmed(),
);
} else {
println_pad!(
pad,
"{}: {} ({})",
"peer".yellow().bold(),
peer.name.yellow(),
public_key.yellow(),
);
println_pad!(pad, " {}: {}", "ip".bold(), peer.ip);
if let Some(info) = info {
if let Some(endpoint) = info.config.endpoint {
println_pad!(
pad,
" {}: {endpoint}{endpoint_override_msg}",
"endpoint".bold(),
);
}
if let Some(last_handshake) = info.stats.last_handshake_time {
let duration = last_handshake.elapsed().expect("horrible clock problem");
println_pad!(
pad,
" {}: {}",
"last handshake".bold(),
human_duration(duration),
);
}
if info.stats.tx_bytes > 0 || info.stats.rx_bytes > 0 {
println_pad!(
pad,
" {}: {} received, {} sent",
"transfer".bold(),
human_size(info.stats.rx_bytes),
human_size(info.stats.tx_bytes),
);
}
}
}
}
fn main() {
let opts = Opts::parse();
util::init_logger(opts.verbose);
if let Err(e) = run(&opts) {
println!();
log::error!("{}\n", e);
if let Some(e) = e.downcast_ref::<WrappedIoError>() {
util::permissions_helptext(&opts.config_dir, &opts.data_dir, e);
}
if let Some(e) = e.downcast_ref::<io::Error>() {
util::permissions_helptext(&opts.config_dir, &opts.data_dir, e);
}
std::process::exit(1);
}
}
fn run(opts: &Opts) -> Result<(), Error> {
let command = opts.command.clone().unwrap_or(Command::Show {
short: false,
tree: false,
interface: None,
});
match command {
Command::Install {
invite,
hosts,
install_opts,
nat,
} => install(opts, &hosts, &install_opts, &nat, &invite)?,
Command::Show {
short,
tree,
interface,
} => show(opts, short, tree, interface)?,
Command::Fetch {
interface,
hosts,
nat,
} => {
fetch(
&opts.config_dir,
&opts.data_dir,
&opts.network,
&hosts,
&nat,
&interface,
false,
)?;
},
Command::Up {
interface,
daemon,
hosts,
nat,
interval,
} => up(
interface,
opts,
daemon.then(|| Duration::from_secs(interval)),
hosts,
&nat,
)?,
Command::Down { interface } => wg::down(&interface, opts.network.backend)?,
Command::Uninstall { interface, yes } => uninstall(&interface, opts, yes)?,
Command::AddPeer {
interface,
sub_opts,
} => add_peer(&interface, opts, sub_opts)?,
Command::RenamePeer {
interface,
sub_opts,
} => rename_peer(&interface, opts, sub_opts)?,
Command::AddCidr {
interface,
sub_opts,
} => add_cidr(&interface, opts, sub_opts)?,
Command::RenameCidr {
interface,
sub_opts,
} => rename_cidr(&interface, opts, sub_opts)?,
Command::DeleteCidr {
interface,
sub_opts,
} => delete_cidr(&interface, opts, sub_opts)?,
Command::ListCidrs { interface, tree } => list_cidrs(&interface, opts, tree)?,
Command::DisablePeer {
interface,
sub_opts,
} => enable_or_disable_peer(&interface, opts, sub_opts, false)?,
Command::EnablePeer {
interface,
sub_opts,
} => enable_or_disable_peer(&interface, opts, sub_opts, true)?,
Command::AddAssociation {
interface,
sub_opts,
} => add_association(&interface, opts, sub_opts)?,
Command::DeleteAssociation {
interface,
sub_opts,
} => delete_association(&interface, opts, sub_opts)?,
Command::ListAssociations { interface } => list_associations(&interface, opts)?,
Command::SetListenPort {
interface,
sub_opts,
} => {
set_listen_port(&interface, opts, sub_opts)?;
},
Command::OverrideEndpoint {
interface,
sub_opts,
} => {
override_endpoint(&interface, opts, sub_opts)?;
},
Command::OverridePeerEndpoint {
interface,
sub_opts,
} => {
override_peer_endpoint(&interface, opts, sub_opts)?;
},
Command::Completions { shell } => {
use clap::CommandFactory;
let mut app = Opts::command();
let app_name = app.get_name().to_string();
clap_complete::generate(shell, &mut app, app_name, &mut std::io::stdout());
std::process::exit(0);
},
}
Ok(())
}