fn main() -> Result<()> {
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new(
"warn,nostr_relay_pool=off,boringtun::noise::timers=error",
)
});
tracing_subscriber::fmt().with_env_filter(env_filter).init();
let cli = Cli::parse();
run_cli(cli)
}
fn run_cli(cli: Cli) -> Result<()> {
match cli.command {
#[cfg(target_os = "windows")]
Command::Daemon(args) if args.service => run_windows_service_dispatcher(args),
command => run_command_on_runtime(command),
}
}
fn run_command_on_runtime(command: Command) -> Result<()> {
#[cfg(target_os = "windows")]
{
let handle = thread::Builder::new()
.name("nvpn-runtime".to_string())
.stack_size(16 * 1024 * 1024)
.spawn(move || run_command_with_runtime(command))
.context("failed to spawn nvpn runtime thread")?;
handle.join().map_err(|panic| {
if let Some(message) = panic.downcast_ref::<&str>() {
anyhow!("nvpn runtime thread panicked: {message}")
} else if let Some(message) = panic.downcast_ref::<String>() {
anyhow!("nvpn runtime thread panicked: {message}")
} else {
anyhow!("nvpn runtime thread panicked")
}
})?
}
#[cfg(not(target_os = "windows"))]
{
run_command_with_runtime(command)
}
}
fn run_command_with_runtime(command: Command) -> Result<()> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.context("failed to build tokio runtime")?;
runtime.block_on(run_command(command))
}
async fn run_command(command: Command) -> Result<()> {
match command {
Command::Init {
config,
force,
devices,
} => {
let path = config.unwrap_or_else(default_config_path);
init_config(&path, force, devices)?;
}
Command::Version(args) => {
print_version(args)?;
}
Command::Update(args) => {
updater::run_update(args).await?;
}
Command::InstallCli(args) => {
install_cli(args)?;
}
Command::UninstallCli(args) => {
uninstall_cli(args)?;
}
Command::Service(args) => {
service_management::run_service_command(args)?;
}
Command::Start(args) => {
start_session(args).await?;
}
Command::Stop(args) => {
stop_daemon(args)?;
}
Command::RepairNetwork(args) => {
repair_network(args)?;
}
Command::Reload(args) => {
reload_daemon(args)?;
}
Command::Pause(args) => {
control_daemon(args, DaemonControlRequest::Pause)?;
}
Command::Resume(args) => {
control_daemon(args, DaemonControlRequest::Resume)?;
}
Command::Connect(args) => {
connect_vpn(args).await?;
}
Command::Status(args) => {
let config_path = args.config.unwrap_or_else(default_config_path);
let (app, network_id) =
load_config_with_overrides(&config_path, args.network_id, args.devices)?;
let daemon = daemon_status(&config_path)?;
let daemon_peers: Option<Vec<DaemonPeerState>> = if daemon.running {
daemon.state.as_ref().map(|state| {
state
.peers
.iter()
.filter(|peer| {
!peer.node_id.is_empty()
|| !peer.tunnel_ip.is_empty()
|| !peer.endpoint.is_empty()
})
.cloned()
.collect()
})
} else {
None
};
let (peers, expected_peers, peer_count, mesh_ready, status_source) =
if let Some(daemon_peers) = daemon_peers.as_ref() {
let state = daemon.state.as_ref().expect("daemon peers implies state");
let peers = daemon_peers
.iter()
.map(|peer| PeerAnnouncement {
node_id: if peer.node_id.is_empty() {
peer.participant_pubkey.clone()
} else {
peer.node_id.clone()
},
public_key: peer.public_key.clone(),
endpoint: peer.endpoint.clone(),
local_endpoint: None,
public_endpoint: None,
tunnel_ip: peer.tunnel_ip.clone(),
advertised_routes: peer.advertised_routes.clone(),
timestamp: peer.last_mesh_seen_at,
})
.collect::<Vec<_>>();
(
peers,
state.expected_peer_count,
state.connected_peer_count,
state.mesh_ready,
"daemon",
)
} else {
let peers = configured_fips_peer_announcements(&app, &network_id);
let expected = expected_peer_count(&app);
(peers, expected, 0, false, "config")
};
if args.json {
let endpoint = status_endpoint(&app, &daemon);
let listen_port = status_listen_port(&app, &daemon);
println!(
"{}",
serde_json::to_string_pretty(&json!({
"status_source": status_source,
"network_id": network_id,
"magic_dns_suffix": app.magic_dns_suffix,
"autoconnect": app.autoconnect,
"node_id": app.node.id,
"tunnel_ip": runtime_local_tunnel_ip(&app, &network_id),
"endpoint": endpoint,
"configured_endpoint": app.node.endpoint,
"listen_port": listen_port,
"configured_listen_port": app.node.listen_port,
"exit_node": if app.exit_node.is_empty() {
None::<String>
} else {
Some(app.exit_node.clone())
},
"exit_node_leak_protection": app.exit_node_leak_protection,
"advertise_exit_node": app.node.advertise_exit_node,
"advertised_routes": app.node.advertised_routes,
"effective_advertised_routes": runtime_effective_advertised_routes(&app),
"fips_host_tunnel_enabled": app.fips_host_tunnel_enabled,
"connect_to_non_roster_fips_peers": app.connect_to_non_roster_fips_peers,
"fips_nostr_discovery_enabled": app.fips_nostr_discovery_enabled,
"fips_bootstrap_enabled": app.fips_bootstrap_enabled,
"fips_host_inbound_tcp_ports": app.fips_host_inbound_tcp_ports,
"wireguard_exit": wireguard_exit_status_json(&app),
"daemon": daemon_status_json_value(&daemon),
"expected_peer_count": expected_peers,
"peer_count": peer_count,
"mesh_ready": mesh_ready,
"peers": peers,
}))?
);
} else {
let endpoint = status_endpoint(&app, &daemon);
let listen_port = status_listen_port(&app, &daemon);
println!("network: {network_id}");
println!("magic_dns_suffix: {}", app.magic_dns_suffix);
println!("autoconnect: {}", app.autoconnect);
println!("node: {}", app.node.id);
println!("tunnel_ip: {}", runtime_local_tunnel_ip(&app, &network_id));
println!("endpoint: {endpoint}");
println!("listen_port: {listen_port}");
if endpoint != app.node.endpoint {
println!("configured_endpoint: {}", app.node.endpoint);
}
if listen_port != app.node.listen_port {
println!("configured_listen_port: {}", app.node.listen_port);
}
if app.exit_node.is_empty() {
println!("exit_node: none");
} else {
println!("exit_node: {}", app.exit_node);
}
println!(
"exit_node_leak_protection: {}",
app.exit_node_leak_protection
);
println!("advertise_exit_node: {}", app.node.advertise_exit_node);
println!("fips_host_tunnel_enabled: {}", app.fips_host_tunnel_enabled);
println!(
"connect_to_non_roster_fips_peers: {}",
app.connect_to_non_roster_fips_peers
);
println!(
"fips_nostr_discovery_enabled: {}",
app.fips_nostr_discovery_enabled
);
println!("fips_bootstrap_enabled: {}", app.fips_bootstrap_enabled);
if !app.fips_host_inbound_tcp_ports.is_empty() {
println!(
"fips_host_inbound_tcp_ports: {}",
app.fips_host_inbound_tcp_ports
.iter()
.map(u16::to_string)
.collect::<Vec<_>>()
.join(",")
);
}
println!(
"wireguard_exit: {}",
if app.wireguard_exit.enabled {
if app.wireguard_exit.configured() {
"enabled"
} else {
"enabled (incomplete)"
}
} else {
"disabled"
}
);
if app.wireguard_exit.enabled {
println!("wireguard_exit_interface: {}", app.wireguard_exit.interface);
println!("wireguard_exit_address: {}", app.wireguard_exit.address);
println!("wireguard_exit_endpoint: {}", app.wireguard_exit.endpoint);
}
let effective_routes = runtime_effective_advertised_routes(&app);
if effective_routes.is_empty() {
println!("advertised_routes: none");
} else {
println!("advertised_routes: {}", effective_routes.join(", "));
}
if daemon.running {
println!("daemon: running (pid {})", daemon.pid.unwrap_or_default());
if let Some(state) = daemon.state.as_ref() {
println!("vpn_status: {}", state.vpn_status);
}
} else {
println!("daemon: stopped");
}
println!("status_source: {status_source}");
if expected_peers > 0 {
println!("mesh_progress: {}/{}", peer_count, expected_peers);
println!("mesh_ready: {mesh_ready}");
}
if let Some(daemon_peers) = daemon_peers.as_ref() {
let reachable_count = daemon_peers.iter().filter(|p| p.reachable).count();
println!(
"peers: {} total, {reachable_count} reachable",
daemon_peers.len()
);
let now = unix_timestamp();
for peer in daemon_peers {
print_daemon_peer_line(peer, now);
}
} else {
println!("peers: {}", peers.len());
for peer in &peers {
if peer.advertised_routes.is_empty() {
println!(" {} {} {}", peer.node_id, peer.tunnel_ip, peer.endpoint);
} else {
println!(
" {} {} {} routes={}",
peer.node_id,
peer.tunnel_ip,
peer.endpoint,
peer.advertised_routes.join(",")
);
}
}
}
}
}
Command::Set(args) => {
let config_path = args.config.unwrap_or_else(default_config_path);
let mut app = load_or_default_config(&config_path)?;
if let Some(value) = args.network_id {
app.set_active_network_id(&value)?;
}
if let Some(value) = args.node_name {
app.node_name = value;
}
if let Some(value) = args.node_id {
app.node.id = value;
}
if let Some(value) = args.endpoint {
app.node.endpoint = value;
}
if let Some(value) = args.tunnel_ip {
app.node.tunnel_ip = value;
}
if let Some(value) = args.listen_port {
app.node.listen_port = value;
}
if let Some(value) = args.exit_node {
app.exit_node = parse_exit_node_arg(&value)?.unwrap_or_default();
}
if let Some(value) = args.exit_node_leak_protection {
app.exit_node_leak_protection = value;
}
if let Some(value) = args.advertise_routes {
app.node.advertised_routes = parse_advertised_routes_arg(&value)?;
}
if let Some(value) = args.advertise_exit_node {
app.node.advertise_exit_node = value;
}
if let Some(value) = args.wireguard_exit_enabled {
app.wireguard_exit.enabled = value;
}
if args.wireguard_exit_config.is_some() && args.wireguard_exit_config_file.is_some() {
return Err(anyhow!(
"use either --wireguard-exit-config or --wireguard-exit-config-file, not both"
));
}
if let Some(value) = args.wireguard_exit_config {
let enabled = app.wireguard_exit.enabled;
let mut parsed = parse_wireguard_exit_config(&value)?;
parsed.enabled = enabled;
app.wireguard_exit = parsed;
}
if let Some(path) = args.wireguard_exit_config_file {
let value = fs::read_to_string(&path).with_context(|| {
format!("failed to read WireGuard config {}", path.display())
})?;
let enabled = app.wireguard_exit.enabled;
let mut parsed = parse_wireguard_exit_config(&value)?;
parsed.enabled = enabled;
app.wireguard_exit = parsed;
}
if let Some(value) = args.wireguard_exit_interface {
app.wireguard_exit.interface = value;
}
if let Some(value) = args.wireguard_exit_address {
app.wireguard_exit.address = value;
}
if let Some(value) = args.wireguard_exit_private_key {
app.wireguard_exit.private_key = value;
}
if let Some(value) = args.wireguard_exit_peer_public_key {
app.wireguard_exit.peer_public_key = value;
}
if let Some(value) = args.wireguard_exit_peer_preshared_key {
app.wireguard_exit.peer_preshared_key = value;
}
if let Some(value) = args.wireguard_exit_endpoint {
app.wireguard_exit.endpoint = value;
}
if let Some(value) = args.wireguard_exit_allowed_ips {
app.wireguard_exit.allowed_ips = parse_advertised_routes_arg(&value)?;
}
if let Some(value) = args.wireguard_exit_dns {
app.wireguard_exit.dns = parse_csv_arg(&value);
}
if let Some(value) = args.wireguard_exit_mtu {
app.wireguard_exit.mtu = value;
}
if let Some(value) = args.wireguard_exit_keepalive {
app.wireguard_exit.persistent_keepalive_secs = value;
}
if let Some(value) = args.autoconnect {
app.autoconnect = value;
}
if let Some(value) = args.join_requests_enabled {
let network_id = app
.active_network_opt()
.ok_or_else(|| anyhow!("activate a network before changing join requests"))?
.id
.clone();
app.set_network_join_requests_enabled(&network_id, value)?;
}
if let Some(value) = args.fips_advertise_public_endpoint {
app.fips_advertise_public_endpoint = value;
}
if let Some(value) = args.fips_host_tunnel_enabled {
app.fips_host_tunnel_enabled = value;
}
if let Some(value) = args.connect_to_non_roster_fips_peers {
app.connect_to_non_roster_fips_peers = value;
}
if let Some(value) = args.fips_nostr_discovery_enabled {
app.fips_nostr_discovery_enabled = value;
}
if let Some(value) = args.fips_bootstrap_enabled {
app.fips_bootstrap_enabled = value;
}
if let Some(value) = args.fips_host_inbound_tcp_ports {
app.fips_host_inbound_tcp_ports = parse_tcp_ports_arg(&value)?;
}
if !args.fips_peer_endpoints.is_empty() {
app.fips_peer_endpoints = parse_fips_peer_endpoint_args(&args.fips_peer_endpoints)?;
}
apply_devices_override(&mut app, args.devices)?;
app.ensure_defaults();
maybe_autoconfigure_node(&mut app);
app.save(&config_path)?;
maybe_reload_running_daemon(&config_path);
if args.json {
println!("{}", serde_json::to_string_pretty(&app)?);
} else {
println!("saved {}", config_path.display());
println!("network_id={}", app.effective_network_id());
println!("node_id={}", app.node.id);
}
}
Command::CreateInvite(args) => {
let config_path = args.config.unwrap_or_else(default_config_path);
let app = load_or_default_config(&config_path)?;
let invite = active_network_invite_code(&app)?;
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&json!({
"network_id": app.effective_network_id(),
"invite": invite,
}))?
);
} else {
println!("{invite}");
}
}
Command::ImportInvite(args) => {
let config_path = args.config.unwrap_or_else(default_config_path);
let mut app = load_or_default_config(&config_path)?;
let invite = parse_network_invite(&args.invite)?;
apply_network_invite_to_active_network(&mut app, &invite)?;
let join_request_queued = queue_active_network_join_request(&mut app)?;
app.ensure_defaults();
maybe_autoconfigure_node(&mut app);
app.save(&config_path)?;
maybe_reload_running_daemon(&config_path);
if args.json {
println!("{}", serde_json::to_string_pretty(&app)?);
} else {
println!("saved {}", config_path.display());
println!("network_id={}", app.effective_network_id());
println!("invite_imported={}", app.active_network().name);
println!("join_request_queued={join_request_queued}");
}
}
Command::InviteBroadcast(args) => {
run_invite_broadcast(args)?;
}
Command::Discover(args) => {
run_discover(args)?;
}
Command::AddDevice(args) => {
update_active_network_roster(args, RosterEditAction::AddDevice).await?;
}
Command::RemoveDevice(args) => {
update_active_network_roster(args, RosterEditAction::RemoveDevice).await?;
}
Command::AddAdmin(args) => {
update_active_network_roster(args, RosterEditAction::AddAdmin).await?;
}
Command::RemoveAdmin(args) => {
update_active_network_roster(args, RosterEditAction::RemoveAdmin).await?;
}
Command::Ping(args) => {
let config_path = args.config.unwrap_or_else(default_config_path);
let (app, network_id) =
load_config_with_overrides(&config_path, args.network_id, args.devices)?;
let peers = configured_fips_peer_announcements(&app, &network_id);
let target = resolve_ping_target(&args.target, &peers).ok_or_else(|| {
anyhow!("target '{}' did not match an IP or known peer", args.target)
})?;
run_ping(&target, args.count, args.timeout_secs)?;
}
Command::Doctor(args) => {
run_doctor(args).await?;
}
Command::Ip(args) => {
let config_path = args.config.unwrap_or_else(default_config_path);
let (app, network_id) =
load_config_with_overrides(&config_path, args.network_id, args.devices)?;
if !args.peer {
let tunnel_ip = runtime_local_tunnel_ip(&app, &network_id);
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&json!({
"node_id": app.node.id,
"tunnel_ip": tunnel_ip,
"ip": strip_cidr(&tunnel_ip),
}))?
);
} else {
println!("{}", strip_cidr(&tunnel_ip));
}
} else {
let peer_ips = runtime_peer_tunnel_ips(&app, &network_id);
if args.json {
println!("{}", serde_json::to_string_pretty(&peer_ips)?);
} else {
for ip in peer_ips {
println!("{}", strip_cidr(&ip));
}
}
}
}
Command::Whois(args) => {
let config_path = args.config.unwrap_or_else(default_config_path);
let (app, network_id) =
load_config_with_overrides(&config_path, args.network_id, args.devices)?;
let peers = configured_fips_peer_announcements(&app, &network_id);
let found = peers
.iter()
.find(|peer| {
peer.node_id == args.query
|| peer.public_key == args.query
|| peer.tunnel_ip == args.query
|| strip_cidr(&peer.tunnel_ip) == args.query
})
.cloned();
let Some(peer) = found else {
return Err(anyhow!("no peer found for '{}'", args.query));
};
if args.json {
println!("{}", serde_json::to_string_pretty(&peer)?);
} else {
println!("node_id={}", peer.node_id);
println!("public_key={}", peer.public_key);
println!("tunnel_ip={}", peer.tunnel_ip);
println!("endpoint={}", peer.endpoint);
println!("timestamp={}", peer.timestamp);
}
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
Command::WgUpstreamTest(args) => {
run_wg_upstream_test(args).await?;
}
Command::ApplyConfig(args) => {
let config_path = args.config.unwrap_or_else(default_config_path);
apply_config_file(&args.source, &config_path)?;
}
Command::ApplyConfigDaemon(args) => {
let config_path = args.config.unwrap_or_else(default_config_path);
apply_config_via_running_daemon(&args.source, &config_path)?;
}
Command::Daemon(args) => daemon_vpn(args).await?,
}
Ok(())
}