Files
nx9-wg/src/main.rs
T

3058 lines
119 KiB
Rust

//! NX9 WireGuard - Native Rust WireGuard Management Application CLI and Daemon
use clap::{Args, Parser, Subcommand, ValueEnum};
use nx9_wg_api::auth::{AuthService, BootstrapOptions, bootstrap_admin};
use nx9_wg_api::backup::BackupService;
use nx9_wg_api::error::ApiError;
use nx9_wg_api::reconciliation::ReconciliationEngine;
use nx9_wg_api::routes::build_api_router;
use nx9_wg_api::state::AppState;
use nx9_wg_api::{DiagnosticsService, IpAllocator};
use nx9_wg_core::config::AppConfig;
use nx9_wg_core::crypto::generate_secure_password;
use nx9_wg_core::types::audit::AuditEventType;
use nx9_wg_core::types::diagnostics::DiagnosticSubsystem;
use nx9_wg_core::types::firewall::{
FirewallAction, FirewallDirection, FirewallProtocol, FirewallRule,
};
use nx9_wg_core::types::network::{Network, Route};
use nx9_wg_core::types::settings::Setting;
use nx9_wg_core::types::wireguard::{Interface, Peer, PeerProfile, PeerState, PeerType};
use nx9_wg_core::validation::{
validate_cidr, validate_interface_name, validate_listen_port, validate_peer_name,
validate_port_spec,
};
use nx9_wg_db::Store;
#[allow(unused_imports)]
use nx9_wg_network::{
IpForwardingStatus, NativeLinuxNetworkEngine, NetworkEngine, SimulatedNetworkEngine,
};
#[allow(unused_imports)]
use nx9_wireguard::{
ClientConfigBuilder, NativeLinuxWireGuardEngine, SimulatedWireGuardEngine, WireGuardEngine,
generate_qr_ascii, generate_qr_png_bytes, generate_qr_svg,
};
use serde::Serialize;
use std::io::{self, Read};
use std::net::{IpAddr, SocketAddr};
use std::os::unix::fs::OpenOptionsExt;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
use uuid::Uuid;
#[derive(Parser)]
#[command(
name = "nx9-wg",
author = "NX9 Systems",
version,
about = "Native Rust WireGuard Appliance and Management Platform",
long_about = "A high-performance, native Rust WireGuard management system with minimal, explicitly documented Linux runtime dependencies."
)]
struct Cli {
#[arg(
short,
long,
global = true,
env = "NX9_WG_CONFIG",
help = "Path to configuration file"
)]
config: Option<PathBuf>,
#[arg(
short,
long,
global = true,
env = "NX9_WG_DATA_DIR",
help = "Path to database data directory"
)]
data_dir: Option<PathBuf>,
#[arg(
long,
global = true,
env = "NX9_WG_DATABASE",
help = "Specific SQLite database file path or URL"
)]
database: Option<PathBuf>,
#[arg(
long,
global = true,
value_enum,
default_value_t = OutputFormat::Table,
help = "Output format"
)]
format: OutputFormat,
#[arg(long, global = true, help = "Output strictly in JSON format")]
json: bool,
#[arg(short, long, global = true, help = "Suppress status output")]
quiet: bool,
#[arg(short, long, global = true, help = "Enable verbose output")]
verbose: bool,
#[arg(
long,
global = true,
env = "NX9_WG_LOG_LEVEL",
help = "Log verbosity level (trace, debug, info, warn, error)"
)]
log_level: Option<String>,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq, Default)]
enum OutputFormat {
#[default]
Table,
Json,
Yaml,
Csv,
}
#[derive(Subcommand)]
enum Commands {
#[command(about = "Start the nx9-wg REST API and WebSocket daemon")]
Serve(ServeArgs),
#[command(about = "Initialize the administrator account")]
Init(InitArgs),
#[command(about = "System status, health, and settings")]
System(SystemArgs),
#[command(about = "Administrator, session, and token management")]
Admin(AdminArgs),
#[command(about = "WireGuard interface management")]
Interface(InterfaceArgs),
#[command(about = "WireGuard peer enrollment and operations")]
Peer(PeerArgs),
#[command(about = "Subnet network management")]
Network(NetworkArgs),
#[command(about = "Kernel routing table management")]
Route(RouteArgs),
#[command(about = "nftables firewall management")]
Firewall(FirewallArgs),
#[command(about = "NAT masquerade management")]
Nat(NatArgs),
#[command(about = "Kernel IP packet forwarding management")]
Forwarding(ForwardingArgs),
#[command(about = "Reconciliation between SQLite desired state and live kernel state")]
Reconcile(ReconcileArgs),
#[command(about = "Database backup and restore operations")]
Backup(BackupArgs),
#[command(about = "Security and operational audit trail")]
Audit(AuditArgs),
#[command(about = "Query live Linux kernel state")]
Live(LiveArgs),
#[command(about = "Native Linux and WireGuard diagnostics inspection")]
Diagnostics(DiagnosticsArgs),
#[command(about = "Client environment and MTU profile management")]
Profile(ProfileArgs),
#[command(about = "Display version and build information")]
Version,
}
// ── Command Arguments Definitions ───────────────────────────────────────────
#[derive(Args)]
struct DiagnosticsArgs {
#[arg(
default_value = "all",
help = "Subsystem to inspect (system, network, wan, wireguard, peer, routing, forwarding, firewall, nat, mtu, reconciliation, all)"
)]
subsystem: String,
#[arg(long, help = "Optional peer UUID for single peer diagnostics")]
peer: Option<String>,
}
#[derive(Args)]
struct ServeArgs {
#[arg(
short,
long,
env = "NX9_WG_LISTEN_ADDR",
help = "Bind address for HTTP/WebSocket server"
)]
bind: Option<SocketAddr>,
}
#[derive(Args)]
struct InitArgs {
#[arg(
short,
long,
env = "NX9_WG_ADMIN_USERNAME",
help = "Administrator username [default: admin]"
)]
username: Option<String>,
#[arg(
long,
env = "NX9_WG_ADMIN_PASSWORD",
help = "Administrator password via argument"
)]
password: Option<String>,
#[arg(long, help = "Read administrator password from standard input")]
password_stdin: bool,
#[arg(
long,
env = "NX9_WG_ADMIN_PASSWORD_FILE",
help = "Read administrator password from file"
)]
password_file: Option<PathBuf>,
#[arg(long, help = "Generate a cryptographically secure random password")]
generate_password: bool,
#[arg(long, help = "Write generated password to specified file")]
write_password_file: Option<PathBuf>,
}
#[derive(Args)]
struct SystemArgs {
#[command(subcommand)]
subcommand: SystemSubcommands,
}
#[derive(Subcommand)]
enum SystemSubcommands {
#[command(about = "Display overall system status and counts")]
Status,
#[command(about = "Perform system and database health check")]
Health,
#[command(about = "Display system platform and runtime environment info")]
Info,
#[command(about = "Manage system settings key-value store")]
Settings(SettingsArgs),
}
#[derive(Args)]
struct SettingsArgs {
#[command(subcommand)]
subcommand: SettingsSubcommands,
}
#[derive(Subcommand)]
enum SettingsSubcommands {
#[command(about = "List all configuration settings")]
List,
#[command(about = "Get value of a setting")]
Get { key: String },
#[command(about = "Set or update a configuration setting")]
Set {
key: String,
value: String,
#[arg(long, help = "Flag setting as secret")]
secret: bool,
},
#[command(about = "Delete a configuration setting")]
Delete { key: String },
}
#[derive(Args)]
struct AdminArgs {
#[command(subcommand)]
subcommand: AdminSubcommands,
}
#[derive(Subcommand)]
enum AdminSubcommands {
#[command(about = "Display administrator metadata and status")]
Status,
#[command(about = "Create/bootstrap administrator if not initialized")]
Create(InitArgs),
#[command(about = "Reset or update administrator password")]
Password(AdminPasswordArgs),
#[command(about = "Manage active sessions")]
Sessions(SessionArgs),
#[command(about = "Manage API tokens")]
Tokens(AdminTokenArgs),
}
#[derive(Args)]
struct AdminPasswordArgs {
#[arg(long, help = "New administrator password via argument")]
new_password: Option<String>,
#[arg(long, help = "Read new password from standard input")]
stdin: bool,
#[arg(long, help = "Read new password from file")]
password_file: Option<PathBuf>,
#[arg(long, help = "Generate a secure random password")]
generate: bool,
}
#[derive(Args)]
struct SessionArgs {
#[command(subcommand)]
subcommand: SessionSubcommands,
}
#[derive(Subcommand)]
enum SessionSubcommands {
#[command(about = "List active sessions")]
List,
#[command(about = "Revoke an active session")]
Revoke { id: String },
#[command(about = "Invalidate all active sessions")]
RevokeAll,
}
#[derive(Args)]
struct AdminTokenArgs {
#[command(subcommand)]
subcommand: TokenSubcommands,
}
#[derive(Subcommand)]
enum TokenSubcommands {
#[command(about = "Create a new API token")]
Create {
#[arg(short, long, help = "Descriptive name for the API token")]
name: String,
#[arg(long, help = "Validity in days (omit for never expiring)")]
days: Option<i64>,
#[arg(
long,
help = "Write the plaintext token to a file (restricted mode 0600)"
)]
write_token_file: Option<PathBuf>,
},
#[command(about = "List all API tokens")]
List,
#[command(about = "Revoke an API token")]
Revoke {
#[arg(help = "API Token ID to revoke")]
id: String,
},
}
#[derive(Args)]
struct InterfaceArgs {
#[command(subcommand)]
subcommand: InterfaceSubcommands,
}
#[derive(Subcommand)]
enum InterfaceSubcommands {
#[command(about = "List all WireGuard interfaces")]
List,
#[command(about = "Show interface details")]
Show { interface: String },
#[command(about = "Create a new WireGuard interface")]
Create {
name: String,
#[arg(short, long, default_value_t = 51820, help = "UDP listen port")]
port: u16,
#[arg(
short = '4',
long,
help = "IPv4 network CIDR address (e.g. 10.0.0.1/24)"
)]
address_v4: String,
#[arg(short = '6', long, help = "IPv6 network CIDR address (optional)")]
address_v6: Option<String>,
#[arg(short, long, help = "Interface MTU [default: 1420]")]
mtu: Option<u16>,
#[arg(long, help = "DNS server addresses")]
dns: Option<String>,
},
#[command(about = "Update an existing WireGuard interface")]
Update {
interface: String,
#[arg(short, long, help = "UDP listen port")]
port: Option<u16>,
#[arg(short = '4', long, help = "IPv4 network CIDR address")]
address_v4: Option<String>,
#[arg(short = '6', long, help = "IPv6 network CIDR address")]
address_v6: Option<String>,
#[arg(short, long, help = "Interface MTU")]
mtu: Option<u16>,
#[arg(long, help = "DNS server addresses")]
dns: Option<String>,
#[arg(long, help = "Enable or disable interface")]
enabled: Option<bool>,
},
#[command(about = "Delete a WireGuard interface")]
Delete { interface: String },
#[command(about = "Enable a WireGuard interface")]
Enable { interface: String },
#[command(about = "Disable a WireGuard interface")]
Disable { interface: String },
#[command(about = "Show live interface status and peer metrics")]
Status { interface: String },
#[command(about = "Reconcile a specific interface with kernel")]
Reconcile { interface: String },
}
#[derive(Args)]
struct PeerArgs {
#[command(subcommand)]
subcommand: PeerSubcommands,
}
#[derive(Subcommand)]
enum PeerSubcommands {
#[command(about = "List enrolled WireGuard peers")]
List {
#[arg(short, long, help = "Filter peers by interface name or ID")]
interface: Option<String>,
},
#[command(about = "Show peer details")]
Show { id: String },
#[command(about = "Create and enroll a new peer")]
Create {
#[arg(short, long, help = "Interface name or ID")]
interface: String,
#[arg(short, long, help = "Peer display name")]
name: String,
#[arg(long, default_value = "road_warrior", help = "Peer type")]
peer_type: String,
#[arg(long, default_value = "full_tunnel", help = "Tunnel profile")]
profile: String,
#[arg(long, help = "Subnet network name or ID for automatic IP allocation")]
network: Option<String>,
#[arg(short = '4', long, help = "IPv4 peer address CIDR")]
address_v4: Option<String>,
#[arg(long, default_value = "0.0.0.0/0, ::/0", help = "Allowed IPs")]
allowed_ips: String,
#[arg(long, help = "Optional fixed remote endpoint (IP:PORT)")]
endpoint: Option<String>,
#[arg(long, help = "Persistent keepalive interval in seconds")]
persistent_keepalive: Option<u16>,
#[arg(long, help = "DNS servers")]
dns: Option<String>,
#[arg(long, help = "Client MTU")]
mtu: Option<u16>,
#[arg(
long,
help = "Peer expiration timestamp (RFC3339 or 'YYYY-MM-DD HH:MM:SS')"
)]
expires_at: Option<String>,
},
#[command(about = "Update an enrolled peer")]
Update {
id: String,
#[arg(short, long, help = "Peer display name")]
name: Option<String>,
#[arg(long, help = "Allowed IPs")]
allowed_ips: Option<String>,
#[arg(long, help = "Endpoint")]
endpoint: Option<String>,
#[arg(long, help = "Persistent keepalive interval")]
persistent_keepalive: Option<u16>,
#[arg(long, help = "DNS servers")]
dns: Option<String>,
#[arg(long, help = "MTU")]
mtu: Option<u16>,
#[arg(long, help = "Peer expiration timestamp")]
expires_at: Option<String>,
#[arg(long, help = "Enable or disable peer")]
enabled: Option<bool>,
},
#[command(about = "Delete an enrolled peer")]
Delete { id: String },
#[command(about = "Enable an enrolled peer")]
Enable { id: String },
#[command(about = "Disable an enrolled peer")]
Disable { id: String },
#[command(about = "Revoke an enrolled peer")]
Revoke { id: String },
#[command(about = "Mark a peer as immediately expired")]
Expire { id: String },
#[command(about = "Show peer lifecycle and expiration metadata")]
Lifecycle { id: String },
#[command(about = "Show live peer status and telemetry")]
Status { id: String },
#[command(about = "Generate standard client .conf configuration")]
Config {
id: String,
#[arg(long, help = "Network provider (e.g. tmobile, verizon, jio, starlink)")]
provider: Option<String>,
#[arg(
long,
help = "Device category (android, ios, linux, windows, macos, other)"
)]
device: Option<String>,
#[arg(
long,
help = "Connection environment (web, mobile, wifi, wired, other)"
)]
connection: Option<String>,
#[arg(long, help = "NAT condition (direct, cgnat, unknown)")]
nat: Option<String>,
#[arg(long, help = "Explicit manual MTU override")]
mtu: Option<u16>,
#[arg(long, help = "Explicit client profile ID")]
profile: Option<String>,
#[arg(long, help = "WireGuard server endpoint host or IP")]
endpoint: Option<String>,
#[arg(short, long, help = "Write configuration to file")]
output: Option<PathBuf>,
},
#[command(about = "Generate enrollment QR code")]
Qr {
id: String,
#[arg(long, help = "Network provider (e.g. tmobile, verizon, jio, starlink)")]
provider: Option<String>,
#[arg(
long,
help = "Device category (android, ios, linux, windows, macos, other)"
)]
device: Option<String>,
#[arg(
long,
help = "Connection environment (web, mobile, wifi, wired, other)"
)]
connection: Option<String>,
#[arg(long, help = "NAT condition (direct, cgnat, unknown)")]
nat: Option<String>,
#[arg(long, help = "Explicit manual MTU override")]
mtu: Option<u16>,
#[arg(long, help = "Explicit client profile ID")]
profile: Option<String>,
#[arg(long, help = "WireGuard server endpoint host or IP")]
endpoint: Option<String>,
#[arg(
long = "qr-format",
default_value = "terminal",
help = "QR code output format (terminal, svg, png)"
)]
qr_format: String,
},
}
#[derive(Args)]
struct ProfileArgs {
#[command(subcommand)]
subcommand: ProfileSubcommands,
}
#[derive(Subcommand)]
enum ProfileSubcommands {
#[command(about = "List client configuration profiles")]
List {
#[arg(long, help = "Filter by network provider")]
provider: Option<String>,
#[arg(
long,
help = "Filter by device category (android, ios, linux, windows, macos, other)"
)]
device: Option<String>,
#[arg(
long,
help = "Filter by connection type (web, mobile, wifi, wired, other)"
)]
connection: Option<String>,
#[arg(long, help = "Filter by NAT type (direct, cgnat, unknown)")]
nat: Option<String>,
},
#[command(about = "Show details of a specific client profile")]
Show {
#[arg(help = "Profile identifier (e.g. default-mobile, default-cgnat, android-mobile)")]
id: String,
},
#[command(about = "Validate a client MTU against safe operational limits")]
Validate {
#[arg(help = "MTU value in bytes (e.g. 1280, 1360, 1420)")]
mtu: u16,
},
#[command(
about = "Resolve the optimal client profile and MTU given client environment parameters"
)]
Resolve {
#[arg(long, help = "Network provider (e.g. tmobile, verizon, jio, starlink)")]
provider: Option<String>,
#[arg(
long,
help = "Device category (android, ios, linux, windows, macos, other)"
)]
device: Option<String>,
#[arg(
long,
help = "Connection environment (web, mobile, wifi, wired, other)"
)]
connection: Option<String>,
#[arg(long, help = "NAT condition (direct, cgnat, unknown)")]
nat: Option<String>,
#[arg(long, help = "Explicit manual MTU override")]
mtu: Option<u16>,
#[arg(long, help = "Explicit profile ID override")]
profile: Option<String>,
},
}
#[derive(Args)]
struct NetworkArgs {
#[command(subcommand)]
subcommand: NetworkSubcommands,
}
#[derive(Subcommand)]
enum NetworkSubcommands {
#[command(about = "List defined subnet networks")]
List,
#[command(about = "Show network details")]
Show { id: String },
#[command(about = "Create a new subnet network")]
Create {
name: String,
cidr: String,
#[arg(long, help = "Optional network description")]
description: Option<String>,
},
#[command(about = "Show available unallocated IP addresses in a network")]
Available {
id: String,
#[arg(short, long, default_value_t = 10, help = "Number of IPs to display")]
limit: usize,
#[arg(long, help = "Optional interface name or ID for exclusion")]
interface: Option<String>,
},
#[command(about = "Show allocated IP addresses and peer mappings in a network")]
Allocations { id: String },
#[command(about = "Update an existing network")]
Update {
id: String,
#[arg(short, long, help = "Network name")]
name: Option<String>,
#[arg(long, help = "Network CIDR")]
cidr: Option<String>,
#[arg(long, help = "Description")]
description: Option<String>,
#[arg(long, help = "Enable or disable network")]
enabled: Option<bool>,
},
#[command(about = "Delete a subnet network")]
Delete { id: String },
}
#[derive(Args)]
struct RouteArgs {
#[command(subcommand)]
subcommand: RouteSubcommands,
}
#[derive(Subcommand)]
enum RouteSubcommands {
#[command(about = "List configured routing rules")]
List,
#[command(about = "Show route details")]
Show { id: String },
#[command(about = "Add a kernel routing rule")]
Add {
#[arg(long, help = "Destination subnet CIDR (e.g. 10.50.0.0/24)")]
destination: String,
#[arg(short, long, help = "Gateway IP address")]
gateway: Option<String>,
#[arg(short, long, help = "Egress interface name")]
interface_name: Option<String>,
#[arg(short, long, help = "Route priority metric")]
metric: Option<u32>,
},
#[command(about = "Update an existing route")]
Update {
id: String,
#[arg(long, help = "Destination CIDR")]
destination: Option<String>,
#[arg(short, long, help = "Gateway IP")]
gateway: Option<String>,
#[arg(short, long, help = "Interface name")]
interface_name: Option<String>,
#[arg(short, long, help = "Metric")]
metric: Option<u32>,
#[arg(long, help = "Enable or disable route")]
enabled: Option<bool>,
},
#[command(about = "Delete a routing rule")]
Delete { id: String },
#[command(about = "Show current kernel route status")]
Status,
#[command(about = "Synchronize routes with kernel routing table")]
Sync,
}
#[derive(Args)]
struct FirewallArgs {
#[command(subcommand)]
subcommand: FirewallSubcommands,
}
#[derive(Subcommand)]
enum FirewallSubcommands {
#[command(about = "List configured firewall rules")]
List {
#[arg(long, help = "Filter rules for specific peer name or UUID")]
peer: Option<String>,
},
#[command(about = "Show firewall rule details")]
Show { id: String },
#[command(about = "Add a packet filter firewall rule")]
Add {
#[arg(short, long, help = "Rule descriptive name")]
name: String,
#[arg(long, default_value = "in", help = "Direction (in, out, forward)")]
direction: String,
#[arg(short, long, help = "Source CIDR")]
source: Option<String>,
#[arg(long, help = "Destination CIDR")]
destination: Option<String>,
#[arg(long, help = "Associate with specific peer name or UUID")]
peer: Option<String>,
#[arg(
long,
default_value = "any",
help = "Protocol (tcp, udp, tcp_udp, icmp, any)"
)]
protocol: String,
#[arg(short, long, help = "Target port")]
port: Option<u16>,
#[arg(
long,
help = "Port specification (single '443', range '8000-8100', or list '53,80,443')"
)]
port_range: Option<String>,
#[arg(
short,
long,
default_value = "accept",
help = "Action (accept, drop, reject)"
)]
action: String,
#[arg(long, default_value_t = 100, help = "Priority order")]
priority: i32,
},
#[command(about = "Update an existing firewall rule")]
Update {
id: String,
#[arg(short, long, help = "Rule name")]
name: Option<String>,
#[arg(short, long, help = "Action")]
action: Option<String>,
#[arg(long, help = "Priority")]
priority: Option<i32>,
#[arg(long, help = "Enable or disable rule")]
enabled: Option<bool>,
},
#[command(about = "Delete a firewall rule")]
Delete { id: String },
#[command(about = "Enable a firewall rule")]
Enable { id: String },
#[command(about = "Disable a firewall rule")]
Disable { id: String },
#[command(about = "Synchronize nftables ruleset")]
Sync,
#[command(about = "Show active nftables table and ruleset status")]
Status,
}
#[derive(Args)]
struct NatArgs {
#[command(subcommand)]
subcommand: NatSubcommands,
}
#[derive(Subcommand)]
enum NatSubcommands {
#[command(about = "Inspect NAT masquerade status")]
Status,
#[command(about = "Enable NAT masquerade")]
Enable,
#[command(about = "Disable NAT masquerade")]
Disable,
#[command(about = "List subnets configured for NAT masquerade")]
List,
#[command(about = "Synchronize NAT rules with kernel")]
Sync,
}
#[derive(Args)]
struct ForwardingArgs {
#[command(subcommand)]
subcommand: ForwardingSubcommands,
}
#[derive(Subcommand)]
enum ForwardingSubcommands {
#[command(about = "Inspect Linux kernel IP forwarding status")]
Status,
#[command(about = "Enable Linux kernel IP forwarding")]
Enable,
#[command(about = "Disable Linux kernel IP forwarding")]
Disable,
#[command(about = "Synchronize IP forwarding setting with kernel")]
Sync,
}
#[derive(Args)]
struct ReconcileArgs {
#[command(subcommand)]
subcommand: ReconcileSubcommands,
}
#[derive(Subcommand)]
enum ReconcileSubcommands {
#[command(about = "Inspect reconciliation status and statistics")]
Status,
#[command(about = "Generate reconciliation dry-run plan")]
Plan {
#[arg(short, long, help = "Target specific interface")]
interface: Option<String>,
},
#[command(about = "Apply reconciliation plan to live kernel state")]
Apply {
#[arg(short, long, help = "Target specific interface")]
interface: Option<String>,
},
#[command(about = "Verify zero drift between SQLite and kernel")]
Verify,
}
#[derive(Args)]
struct BackupArgs {
#[command(subcommand)]
subcommand: BackupSubcommands,
}
#[derive(Subcommand)]
enum BackupSubcommands {
#[command(about = "Create a consistent SQLite database backup snapshot")]
Create {
#[arg(long, help = "Optional backup note or description")]
description: Option<String>,
},
#[command(about = "List available database backup snapshots")]
List,
#[command(about = "Show backup details and manifest")]
Show { id: String },
#[command(about = "Verify integrity and checksum of a backup file")]
Verify { path: PathBuf },
#[command(about = "Restore database from backup file")]
Restore {
path: PathBuf,
#[arg(short = 'y', long, help = "Confirm destructive restore")]
yes: bool,
},
#[command(about = "Delete a backup record and archive")]
Delete { id: String },
}
#[derive(Args)]
struct AuditArgs {
#[command(subcommand)]
subcommand: AuditSubcommands,
}
#[derive(Subcommand)]
enum AuditSubcommands {
#[command(about = "Query audit log records")]
List {
#[arg(long, help = "Filter by audit event type")]
event_type: Option<String>,
#[arg(long, help = "Filter by actor")]
actor: Option<String>,
#[arg(long, help = "Filter by resource type")]
resource_type: Option<String>,
#[arg(long, default_value_t = 50, help = "Maximum records to return")]
limit: u32,
#[arg(long, default_value_t = 0, help = "Offset for pagination")]
offset: u32,
},
#[command(about = "Show full details for an audit event")]
Show { id: i64 },
}
#[derive(Args)]
struct LiveArgs {
#[command(subcommand)]
subcommand: LiveSubcommands,
}
#[derive(Subcommand)]
enum LiveSubcommands {
#[command(about = "Query live WireGuard interfaces from kernel")]
Interface(LiveInterfaceArgs),
#[command(about = "Query live connected peers from kernel")]
Peer(LivePeerArgs),
#[command(about = "Query live Linux kernel routing table")]
Routes,
#[command(about = "Query live active nftables ruleset")]
Firewall,
#[command(about = "Query live IP packet forwarding status")]
Forwarding,
#[command(about = "Query live NAT masquerade status")]
Nat,
}
#[derive(Args)]
struct LiveInterfaceArgs {
#[command(subcommand)]
subcommand: LiveInterfaceSubcommands,
}
#[derive(Subcommand)]
enum LiveInterfaceSubcommands {
#[command(about = "List live WireGuard interface names")]
List,
#[command(about = "Show live interface statistics and status")]
Show { name: String },
}
#[derive(Args)]
struct LivePeerArgs {
#[command(subcommand)]
subcommand: LivePeerSubcommands,
}
#[derive(Subcommand)]
enum LivePeerSubcommands {
#[command(about = "List live peers on an interface")]
List { interface: String },
#[command(about = "Show live telemetry for a peer")]
Show { id: String },
}
fn create_wireguard_engine() -> Arc<dyn WireGuardEngine> {
#[cfg(target_os = "linux")]
{
Arc::new(NativeLinuxWireGuardEngine::new())
}
#[cfg(not(target_os = "linux"))]
{
Arc::new(SimulatedWireGuardEngine::new())
}
}
fn create_network_engine() -> Arc<dyn NetworkEngine> {
#[cfg(target_os = "linux")]
{
Arc::new(NativeLinuxNetworkEngine::new())
}
#[cfg(not(target_os = "linux"))]
{
Arc::new(SimulatedNetworkEngine::new())
}
}
// ── Output Formatter ────────────────────────────────────────────────────────
fn print_output<T: Serialize>(
data: &T,
format: OutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let json_val = serde_json::to_value(data)?;
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&json_val)?);
}
OutputFormat::Yaml => {
print_json_as_yaml(&json_val, 0);
}
OutputFormat::Csv => {
print_json_as_csv(&json_val);
}
OutputFormat::Table => {
print_json_as_table(&json_val);
}
}
Ok(())
}
fn print_json_as_yaml(val: &serde_json::Value, indent: usize) {
let pad = " ".repeat(indent);
match val {
serde_json::Value::Object(map) => {
for (k, v) in map {
match v {
serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
println!("{pad}{k}:");
print_json_as_yaml(v, indent + 1);
}
_ => {
println!("{pad}{k}: {v}");
}
}
}
}
serde_json::Value::Array(arr) => {
for item in arr {
println!("{pad}-");
print_json_as_yaml(item, indent + 1);
}
}
_ => {
println!("{pad}{val}");
}
}
}
fn print_json_as_csv(val: &serde_json::Value) {
match val {
serde_json::Value::Array(arr) => {
if arr.is_empty() {
return;
}
if let Some(first) = arr.first().and_then(|v| v.as_object()) {
let headers: Vec<&str> = first.keys().map(|k| k.as_str()).collect();
println!("{}", headers.join(","));
for row in arr {
if let Some(obj) = row.as_object() {
let values: Vec<String> = headers
.iter()
.map(|h| {
obj.get(*h)
.map(|v| match v {
serde_json::Value::String(s) => s.clone(),
_ => v.to_string(),
})
.unwrap_or_default()
})
.collect();
println!("{}", values.join(","));
}
}
}
}
serde_json::Value::Object(map) => {
let headers: Vec<&str> = map.keys().map(|k| k.as_str()).collect();
let values: Vec<String> = map
.values()
.map(|v| match v {
serde_json::Value::String(s) => s.clone(),
_ => v.to_string(),
})
.collect();
println!("{}", headers.join(","));
println!("{}", values.join(","));
}
_ => {
println!("{val}");
}
}
}
fn print_json_as_table(val: &serde_json::Value) {
match val {
serde_json::Value::Array(arr) => {
if arr.is_empty() {
println!("(No items found)");
return;
}
println!("{}", serde_json::to_string_pretty(val).unwrap_or_default());
}
serde_json::Value::Object(map) => {
for (k, v) in map {
match v {
serde_json::Value::String(s) => println!(" {k: <24}: {s}"),
_ => println!(" {k: <24}: {v}"),
}
}
}
_ => {
println!("{val}");
}
}
}
// ── Application Entry Point ─────────────────────────────────────────────────
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let format = if cli.json {
OutputFormat::Json
} else {
cli.format
};
let log_level = cli.log_level.as_deref().unwrap_or("info");
let filter = format!(
"nx9_wg={log_level},nx9_wg_api={log_level},nx9_wg_db={log_level},nx9_wireguard={log_level},nx9_wg_network={log_level}"
);
tracing_subscriber::registry()
.with(EnvFilter::try_new(&filter).unwrap_or_else(|_| EnvFilter::new(&filter)))
.with(tracing_subscriber::fmt::layer())
.init();
let config_path = cli
.config
.clone()
.unwrap_or_else(|| PathBuf::from("/etc/nx9-wg/config.toml"));
let mut config = AppConfig::load(&config_path).unwrap_or_default();
if let Some(ref data_dir) = cli.data_dir {
config.backup.dir = data_dir.join("backups");
config.data_dir = data_dir.clone();
}
let command = match cli.command {
Some(cmd) => cmd,
None => {
println!(
"NX9 WireGuard Appliance (nx9-wg) v{}",
env!("CARGO_PKG_VERSION")
);
println!("Run 'nx9-wg --help' for available commands.");
return Ok(());
}
};
// Avoid initializing or creating data directories for the simple 'version' command.
let db_url = if matches!(&command, Commands::Version) {
String::new()
} else if let Some(ref db_path) = cli.database {
// If an explicit database path is provided, ensure its parent directories exist
if cli.data_dir.is_none()
&& let Some(parent) = db_path.parent()
{
if !parent.exists()
&& let Err(e) = std::fs::create_dir_all(parent)
{
eprintln!(
"Failed to create parent directory for database '{}': {}",
parent.display(),
e
);
std::process::exit(1);
}
config.data_dir = parent.to_path_buf();
config.backup.dir = parent.join("backups");
}
db_path.to_string_lossy().to_string()
} else {
if !config.data_dir.exists()
&& let Err(e) = std::fs::create_dir_all(&config.data_dir)
{
eprintln!(
"Failed to create data directory '{}': {}",
config.data_dir.display(),
e
);
std::process::exit(1);
}
config
.data_dir
.join("nx9-wg.db")
.to_string_lossy()
.to_string()
};
match command {
Commands::Version => {
let info = serde_json::json!({
"name": "nx9-wg",
"version": env!("CARGO_PKG_VERSION"),
"architecture": std::env::consts::ARCH,
"os": std::env::consts::OS,
"edition": "2024",
"native_wireguard": true,
"single_admin_security": true
});
print_output(&info, format)?;
}
Commands::Serve(args) => {
let bind_addr = args.bind.unwrap_or(config.bind_address);
if !cli.quiet {
tracing::info!("Connecting to SQLite store at '{db_url}'");
}
let store = Store::connect(&db_url).await?;
store.migrate().await?;
let app_state = AppState::new(store.clone());
let app = build_api_router(app_state.clone());
let listener = tokio::net::TcpListener::bind(bind_addr).await?;
if !cli.quiet {
tracing::info!("NX9 WireGuard daemon listening on http://{bind_addr}");
}
// Start background periodic reconciliation
let wg_engine = Arc::new(NativeLinuxWireGuardEngine::new());
let net_engine = Arc::new(NativeLinuxNetworkEngine::new());
let reconciler = Arc::new(ReconciliationEngine::new(app_state, wg_engine, net_engine));
reconciler.start_background_loop(config.reconciliation_interval_secs);
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await?;
}
Commands::Init(args) => {
let store = Store::connect(&db_url).await?;
store.migrate().await?;
let stdin_password = if args.password_stdin {
let mut buf = String::new();
io::stdin().read_to_string(&mut buf)?;
Some(buf.trim().to_string())
} else {
None
};
let opts = BootstrapOptions {
admin_username: args.username,
cli_password: args.password,
stdin_password,
password_file: args.password_file.map(|p| p.to_string_lossy().to_string()),
generate_password: args.generate_password,
write_password_file: args
.write_password_file
.map(|p| p.to_string_lossy().to_string()),
};
match bootstrap_admin(&store, &config, &opts).await {
Ok(res) => {
if !cli.quiet {
println!("Administrator initialized successfully.");
println!(" Username: {}", res.admin.username);
println!(" Source: {}", res.source.description());
if let Some(_pw) = res.generated_plaintext {
if let Some(ref path) = opts.write_password_file {
println!("Generated password written to file: {}", path);
} else {
println!("Generated password created (redacted)");
}
}
}
// Sanitize admin output to avoid leaking password hashes or secrets
let admin_sanitized = serde_json::json!({
"id": res.admin.id,
"username": res.admin.username,
"created_at": res.admin.created_at,
"last_login_at": res.admin.last_login_at,
"last_login_ip": res.admin.last_login_ip,
"totp_enabled": res.admin.totp_enabled,
"password_hash": "[REDACTED]"
});
print_output(&admin_sanitized, format)?;
}
Err(ApiError::Conflict(_)) => {
if !cli.quiet {
println!(
"Administrator already initialized. Use 'nx9-wg admin password' to reset."
);
}
}
Err(e) => {
eprintln!("Error bootstrapping administrator: {e}");
std::process::exit(1);
}
}
}
Commands::System(args) => {
let store = Store::connect(&db_url).await?;
store.migrate().await?;
match args.subcommand {
SystemSubcommands::Status => {
let admin = store.get_admin().await?;
let ifaces = store.list_interfaces().await?.len();
let peers = store.list_all_peers().await?.len();
let networks = store.list_networks().await?.len();
let routes = store.list_routes().await?.len();
let rules = store.list_firewall_rules().await?.len();
let backups = store.list_backups().await?.len();
let stats = serde_json::json!({
"admin_initialized": admin.is_some(),
"interfaces_count": ifaces,
"peers_count": peers,
"networks_count": networks,
"routes_count": routes,
"firewall_rules_count": rules,
"backups_count": backups,
"status": "operational"
});
print_output(&stats, format)?;
}
SystemSubcommands::Health => {
let healthy = store.admin_exists().await.is_ok();
let health_data = serde_json::json!({
"status": if healthy { "healthy" } else { "unhealthy" },
"database": if healthy { "connected" } else { "disconnected" },
"timestamp": chrono::Utc::now().to_rfc3339()
});
print_output(&health_data, format)?;
if !healthy {
std::process::exit(1);
}
}
SystemSubcommands::Info => {
let info = serde_json::json!({
"version": env!("CARGO_PKG_VERSION"),
"os": std::env::consts::OS,
"arch": std::env::consts::ARCH,
"database_path": db_url,
"data_dir": config.data_dir.display().to_string(),
"config_file": config.config_file.display().to_string(),
"log_level": config.log_level,
"session_expiry_hours": config.session_expiry_hours,
"reconciliation_interval_secs": config.reconciliation_interval_secs
});
print_output(&info, format)?;
}
SystemSubcommands::Settings(s_args) => match s_args.subcommand {
SettingsSubcommands::List => {
let settings = store.list_settings().await?;
let sanitized: Vec<Setting> = settings
.into_iter()
.map(|mut s| {
if s.is_secret {
s.value = "[REDACTED]".to_string();
}
s
})
.collect();
print_output(&sanitized, format)?;
}
SettingsSubcommands::Get { key } => {
let s = store.get_setting(&key).await?;
match s {
Some(mut setting) => {
if setting.is_secret {
setting.value = "[REDACTED]".to_string();
}
print_output(&setting, format)?;
}
None => {
eprintln!("Setting '{key}' not found");
std::process::exit(1);
}
}
}
SettingsSubcommands::Set { key, value, secret } => {
let key_trimmed = key.trim();
let val_trimmed = value.trim();
if key_trimmed == nx9_wg_core::types::settings::SETTING_SERVER_HOST {
if !val_trimmed.is_empty() {
nx9_wg_core::validation::validate_server_host(val_trimmed)?;
}
} else if key_trimmed == nx9_wg_core::types::settings::SETTING_SERVER_PORT {
let port: u16 = val_trimmed.parse().map_err(
|_| "Invalid server port: must be an integer between 1 and 65535",
)?;
nx9_wg_core::validation::validate_server_port(port)?;
} else if key_trimmed
== nx9_wg_core::types::settings::SETTING_SERVER_ENDPOINT_ENABLED
&& val_trimmed != "true"
&& val_trimmed != "false"
&& val_trimmed != "1"
&& val_trimmed != "0"
{
return Err("Setting wireguard.server_endpoint_enabled must be 'true' or 'false'".into());
}
store.set_setting(key_trimmed, val_trimmed, secret).await?;
if (key_trimmed == nx9_wg_core::types::settings::SETTING_SERVER_HOST
|| key_trimmed == nx9_wg_core::types::settings::SETTING_SERVER_PORT
|| key_trimmed
== nx9_wg_core::types::settings::SETTING_SERVER_ENDPOINT_ENABLED)
&& let Ok(settings) = store.get_server_endpoint_settings().await
&& settings.enabled
&& !settings.host.trim().is_empty()
{
let formatted = nx9_wg_core::validation::format_endpoint(
&settings.host,
settings.port,
);
let _ = store
.set_setting(
nx9_wg_core::types::settings::LEGACY_SETTING_SERVER_ENDPOINT,
&formatted,
false,
)
.await;
}
println!("Setting '{key}' saved.");
}
SettingsSubcommands::Delete { key } => {
store.delete_setting(&key).await?;
println!("Setting '{key}' deleted.");
}
},
}
}
Commands::Admin(args) => {
let store = Store::connect(&db_url).await?;
store.migrate().await?;
let auth = AuthService::new(store.clone());
match args.subcommand {
AdminSubcommands::Status => {
let admin = store.get_admin().await?;
match admin {
Some(a) => {
let val = serde_json::json!({
"username": a.username,
"totp_enabled": a.totp_enabled,
"last_login_at": a.last_login_at,
"last_login_ip": a.last_login_ip,
"created_at": a.created_at
});
print_output(&val, format)?;
}
None => {
println!("Administrator has not been initialized yet.");
}
}
}
AdminSubcommands::Create(init_args) => {
let stdin_password = if init_args.password_stdin {
let mut buf = String::new();
io::stdin().read_to_string(&mut buf)?;
Some(buf.trim().to_string())
} else {
None
};
let opts = BootstrapOptions {
admin_username: init_args.username,
cli_password: init_args.password,
stdin_password,
password_file: init_args
.password_file
.map(|p| p.to_string_lossy().to_string()),
generate_password: init_args.generate_password,
write_password_file: init_args
.write_password_file
.map(|p| p.to_string_lossy().to_string()),
};
match bootstrap_admin(&store, &config, &opts).await {
Ok(res) => {
println!(
"Administrator created successfully ({}).",
res.source.description()
);
if let Some(_pw) = res.generated_plaintext {
if let Some(ref path) = opts.write_password_file {
println!("Generated password written to file: {}", path);
} else {
println!("Generated password created (redacted)");
}
}
// Sanitize admin output to avoid leaking password hashes or secrets
let admin_sanitized = serde_json::json!({
"id": res.admin.id,
"username": res.admin.username,
"created_at": res.admin.created_at,
"last_login_at": res.admin.last_login_at,
"last_login_ip": res.admin.last_login_ip,
"totp_enabled": res.admin.totp_enabled,
"password_hash": "[REDACTED]"
});
print_output(&admin_sanitized, format)?;
}
Err(ApiError::Conflict(_)) => {
eprintln!("Administrator already exists.");
std::process::exit(1);
}
Err(e) => {
eprintln!("Error creating admin: {e}");
std::process::exit(1);
}
}
}
AdminSubcommands::Password(pw_args) => {
let new_pw = if let Some(p) = pw_args.new_password {
p
} else if pw_args.stdin {
let mut buf = String::new();
io::stdin().read_to_string(&mut buf)?;
buf.trim().to_string()
} else if let Some(ref path) = pw_args.password_file {
std::fs::read_to_string(path)?.trim().to_string()
} else if pw_args.generate {
let generated = generate_secure_password(24);
println!("Generated password created (redacted)");
generated
} else {
eprintln!(
"Please provide --new-password, --stdin, --password-file, or --generate"
);
std::process::exit(1);
};
match auth.change_password(&new_pw, Some("cli")).await {
Ok(()) => {
println!("Administrator password updated successfully.");
println!("All active sessions have been invalidated.");
}
Err(e) => {
eprintln!("Error changing password: {e}");
std::process::exit(1);
}
}
}
AdminSubcommands::Sessions(sess_args) => match sess_args.subcommand {
SessionSubcommands::List => {
let sessions = store.list_sessions().await?;
print_output(&sessions, format)?;
}
SessionSubcommands::Revoke { id } => {
store.delete_session(&id).await?;
println!("Session '{id}' revoked.");
}
SessionSubcommands::RevokeAll => {
store.delete_all_sessions().await?;
println!("All active sessions revoked.");
}
},
AdminSubcommands::Tokens(token_args) => match token_args.subcommand {
TokenSubcommands::Create {
name,
days,
write_token_file,
} => {
let exp = days
.map(|d| chrono::Utc::now().naive_utc() + chrono::Duration::days(d));
match auth.create_api_token(&name, exp, Some("cli")).await {
Ok((meta, raw_token)) => {
println!("API Token Created:");
// One-time delivery: either write to a restricted file, or display once to stdout
if let Some(path) = write_token_file {
if let Some(parent) = path.parent()
&& !parent.exists()
&& let Err(e) = std::fs::create_dir_all(parent)
{
eprintln!(
"Failed to create parent directory for token file '{}': {}",
parent.display(),
e
);
std::process::exit(1);
}
use std::io::Write;
match std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.mode(0o600)
.open(&path)
{
Ok(mut f) => {
if let Err(e) = f.write_all(raw_token.as_bytes()) {
eprintln!(
"Failed to write token to file '{}': {}",
path.display(),
e
);
std::process::exit(1);
}
}
Err(e) => {
eprintln!(
"Failed to create token file '{}': {}",
path.display(),
e
);
std::process::exit(1);
}
}
println!("Token written to file: {}", path.display());
} else {
println!(" ID: {}", meta.id);
println!(" Name: {}", meta.name);
println!(" Expires: {:?}", meta.expires_at);
println!("\n Secret Token (SAVE THIS NOW):");
println!(" ========================================");
println!(" {raw_token}");
println!(" ========================================\n");
}
// Sanitize token metadata for output (never expose token_hash)
let mut meta_val = serde_json::to_value(&meta)?;
if let serde_json::Value::Object(ref mut obj) = meta_val {
obj.insert(
"token_hash".to_string(),
serde_json::Value::String("[REDACTED]".to_string()),
);
}
print_output(&meta_val, format)?;
}
Err(e) => {
eprintln!("Error creating token: {e}");
std::process::exit(1);
}
}
}
TokenSubcommands::List => {
let tokens = store.list_tokens().await?;
let mut tokens_val = serde_json::to_value(&tokens)?;
if let serde_json::Value::Array(ref mut arr) = tokens_val {
for item in arr.iter_mut() {
if let serde_json::Value::Object(obj) = item {
obj.insert(
"token_hash".to_string(),
serde_json::Value::String("[REDACTED]".to_string()),
);
}
}
}
print_output(&tokens_val, format)?;
}
TokenSubcommands::Revoke { id } => {
auth.revoke_api_token(&id, Some("cli")).await?;
println!("API token '{id}' revoked.");
}
},
}
}
Commands::Interface(args) => {
let store = Store::connect(&db_url).await?;
store.migrate().await?;
match args.subcommand {
InterfaceSubcommands::List => {
let ifaces = store.list_interfaces().await?;
print_output(&ifaces, format)?;
}
InterfaceSubcommands::Show { interface } => {
let iface = if let Ok(id) = Uuid::parse_str(&interface) {
store.get_interface(id).await?
} else {
store.get_interface_by_name(&interface).await?
};
match iface {
Some(i) => print_output(&i, format)?,
None => {
eprintln!("Interface '{interface}' not found");
std::process::exit(1);
}
}
}
InterfaceSubcommands::Create {
name,
port,
address_v4,
address_v6,
mtu,
dns,
} => {
validate_interface_name(&name)?;
validate_listen_port(port)?;
let v4_net = validate_cidr(&address_v4)?;
let v6_net = address_v6.as_deref().map(validate_cidr).transpose()?;
let (priv_key, pub_key) = nx9_wg_core::crypto::generate_keypair();
let now = chrono::Utc::now().naive_utc();
let iface = Interface {
id: Uuid::new_v4(),
name,
private_key: priv_key,
public_key: pub_key,
listen_port: port,
address_v4: v4_net,
address_v6: v6_net,
mtu,
dns,
enabled: true,
pre_up: None,
post_up: None,
pre_down: None,
post_down: None,
created_at: now,
updated_at: now,
};
store.create_interface(&iface).await?;
if !cli.quiet && format == OutputFormat::Table {
println!("Interface '{}' ({}) created.", iface.name, iface.id);
}
print_output(&iface, format)?;
}
InterfaceSubcommands::Update {
interface,
port,
address_v4,
address_v6,
mtu,
dns,
enabled,
} => {
let mut iface = if let Ok(id) = Uuid::parse_str(&interface) {
store
.get_interface(id)
.await?
.ok_or("Interface not found")?
} else {
store
.get_interface_by_name(&interface)
.await?
.ok_or("Interface not found")?
};
if let Some(p) = port {
validate_listen_port(p)?;
iface.listen_port = p;
}
if let Some(ref v4) = address_v4 {
iface.address_v4 = validate_cidr(v4)?;
}
if let Some(ref v6) = address_v6 {
iface.address_v6 = Some(validate_cidr(v6)?);
}
if let Some(m) = mtu {
iface.mtu = Some(m);
}
if let Some(d) = dns {
iface.dns = Some(d);
}
if let Some(e) = enabled {
iface.enabled = e;
}
iface.updated_at = chrono::Utc::now().naive_utc();
store.update_interface(&iface).await?;
if !cli.quiet && format == OutputFormat::Table {
println!("Interface '{}' updated.", iface.name);
}
print_output(&iface, format)?;
}
InterfaceSubcommands::Delete { interface } => {
let id = if let Ok(uuid) = Uuid::parse_str(&interface) {
uuid
} else {
let iface = store
.get_interface_by_name(&interface)
.await?
.ok_or("Interface not found")?;
iface.id
};
store.delete_interface(id).await?;
println!("Interface '{interface}' deleted.");
}
InterfaceSubcommands::Enable { interface } => {
let id = if let Ok(uuid) = Uuid::parse_str(&interface) {
uuid
} else {
let iface = store
.get_interface_by_name(&interface)
.await?
.ok_or("Interface not found")?;
iface.id
};
store.set_interface_enabled(id, true).await?;
println!("Interface '{interface}' enabled.");
}
InterfaceSubcommands::Disable { interface } => {
let id = if let Ok(uuid) = Uuid::parse_str(&interface) {
uuid
} else {
let iface = store
.get_interface_by_name(&interface)
.await?
.ok_or("Interface not found")?;
iface.id
};
store.set_interface_enabled(id, false).await?;
println!("Interface '{interface}' disabled.");
}
InterfaceSubcommands::Status { interface } => {
let wg = create_wireguard_engine();
let stats = wg.get_interface_stats(&interface).await?;
match stats {
Some(s) => print_output(&s, format)?,
None => println!("No live kernel stats available for '{interface}'."),
}
}
InterfaceSubcommands::Reconcile { interface: _ } => {
let state = AppState::new(store);
let wg = create_wireguard_engine();
let net = create_network_engine();
let reconciler = ReconciliationEngine::new(state, wg, net);
let report = reconciler.apply().await?;
print_output(&report, format)?;
}
}
}
Commands::Peer(args) => {
let store = Store::connect(&db_url).await?;
store.migrate().await?;
match args.subcommand {
PeerSubcommands::List { interface } => {
let peers = if let Some(iface_id_str) = interface {
if let Ok(id) = Uuid::parse_str(&iface_id_str) {
store.list_peers_for_interface(id).await?
} else if let Some(iface) =
store.get_interface_by_name(&iface_id_str).await?
{
store.list_peers_for_interface(iface.id).await?
} else {
Vec::new()
}
} else {
store.list_all_peers().await?
};
print_output(&peers, format)?;
}
PeerSubcommands::Show { id } => {
let peer_id = Uuid::parse_str(&id)?;
let peer = store.get_peer(peer_id).await?;
match peer {
Some(p) => print_output(&p, format)?,
None => {
eprintln!("Peer '{id}' not found");
std::process::exit(1);
}
}
}
PeerSubcommands::Create {
interface,
name,
peer_type,
profile,
network,
address_v4,
allowed_ips,
endpoint,
persistent_keepalive,
dns,
mtu,
expires_at,
} => {
let iface = if let Ok(uuid) = Uuid::parse_str(&interface) {
store
.get_interface(uuid)
.await?
.ok_or("Interface not found")?
} else {
store
.get_interface_by_name(&interface)
.await?
.ok_or("Interface not found")?
};
validate_peer_name(&name)?;
let (priv_key, pub_key) = nx9_wg_core::crypto::generate_keypair();
let psk = nx9_wg_core::crypto::generate_preshared_key();
let mut v4_net = address_v4.as_deref().map(validate_cidr).transpose()?;
if v4_net.is_none() {
let target_net = match network {
Some(ref n_str) => {
if let Ok(n_uuid) = Uuid::parse_str(n_str) {
store
.get_network(n_uuid)
.await?
.ok_or("Network not found")?
} else {
store
.get_network_by_name(n_str)
.await?
.ok_or("Network not found")?
}
}
None => Network {
id: Uuid::nil(),
name: format!("{}-subnet", iface.name),
cidr: iface.address_v4,
enabled: true,
description: None,
created_at: chrono::Utc::now().naive_utc(),
updated_at: chrono::Utc::now().naive_utc(),
},
};
v4_net = Some(
IpAllocator::allocate_next_ip(&store, &target_net, Some(&iface), None)
.await?,
);
}
let p_type = PeerType::from_str(&peer_type).unwrap_or(PeerType::RoadWarrior);
let p_profile =
PeerProfile::from_str(&profile).unwrap_or(PeerProfile::FullTunnel);
let parsed_expires_at = match expires_at {
Some(ref s) => {
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
Some(dt.naive_utc())
} else {
Some(chrono::NaiveDateTime::parse_from_str(
s,
"%Y-%m-%d %H:%M:%S",
)?)
}
}
None => None,
};
let now = chrono::Utc::now().naive_utc();
let peer = Peer {
id: Uuid::new_v4(),
interface_id: iface.id,
name,
peer_type: p_type,
state: PeerState::Active,
public_key: pub_key,
private_key: Some(priv_key),
preshared_key: Some(psk),
endpoint,
allowed_ips: if allowed_ips == "0.0.0.0/0, ::/0" {
if let Some(v4) = v4_net {
v4.to_string()
} else {
allowed_ips
}
} else {
allowed_ips
},
server_allowed_ips: None,
address_v4: v4_net,
address_v6: None,
dns: dns.or_else(|| Some("1.1.1.1".to_string())),
mtu: mtu.or(Some(1420)),
persistent_keepalive: persistent_keepalive.or(Some(25)),
profile: p_profile,
expires_at: parsed_expires_at,
last_handshake_at: None,
created_at: now,
updated_at: now,
};
store.create_peer(&peer).await?;
if !cli.quiet && format == OutputFormat::Table {
println!("Peer '{}' ({}) created.", peer.name, peer.id);
}
print_output(&peer, format)?;
}
PeerSubcommands::Update {
id,
name,
allowed_ips,
endpoint,
persistent_keepalive,
dns,
mtu,
expires_at,
enabled,
} => {
let peer_id = Uuid::parse_str(&id)?;
let mut peer = store.get_peer(peer_id).await?.ok_or("Peer not found")?;
if let Some(n) = name {
validate_peer_name(&n)?;
peer.name = n;
}
if let Some(ips) = allowed_ips {
peer.allowed_ips = ips;
}
if let Some(ep) = endpoint {
peer.endpoint = Some(ep);
}
if let Some(ka) = persistent_keepalive {
peer.persistent_keepalive = Some(ka);
}
if let Some(d) = dns {
peer.dns = Some(d);
}
if let Some(m) = mtu {
peer.mtu = Some(m);
}
if let Some(ref exp_s) = expires_at {
peer.expires_at =
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(exp_s) {
Some(dt.naive_utc())
} else {
Some(chrono::NaiveDateTime::parse_from_str(
exp_s,
"%Y-%m-%d %H:%M:%S",
)?)
};
}
if let Some(e) = enabled {
peer.state = if e {
PeerState::Active
} else {
PeerState::Disabled
};
}
peer.updated_at = chrono::Utc::now().naive_utc();
store.update_peer(&peer).await?;
if !cli.quiet && format == OutputFormat::Table {
println!("Peer '{}' updated.", peer.name);
}
print_output(&peer, format)?;
}
PeerSubcommands::Delete { id } => {
let peer_id = Uuid::parse_str(&id)?;
store.delete_peer(peer_id).await?;
println!("Peer '{id}' deleted.");
}
PeerSubcommands::Enable { id } => {
let peer_id = Uuid::parse_str(&id)?;
store.set_peer_state(peer_id, PeerState::Active).await?;
println!("Peer '{id}' enabled.");
}
PeerSubcommands::Disable { id } => {
let peer_id = Uuid::parse_str(&id)?;
store.set_peer_state(peer_id, PeerState::Disabled).await?;
println!("Peer '{id}' disabled.");
}
PeerSubcommands::Revoke { id } => {
let peer_id = Uuid::parse_str(&id)?;
store.set_peer_state(peer_id, PeerState::Revoked).await?;
println!("Peer '{id}' revoked.");
}
PeerSubcommands::Expire { id } => {
let peer_id = Uuid::parse_str(&id)?;
store.mark_peer_expired(peer_id).await?;
println!("Peer '{id}' marked as expired.");
}
PeerSubcommands::Lifecycle { id } => {
let peer_id = Uuid::parse_str(&id)?;
let peer = store.get_peer(peer_id).await?.ok_or("Peer not found")?;
let now = chrono::Utc::now().naive_utc();
let is_expired = peer.state == PeerState::Expired
|| peer.expires_at.map(|e| e <= now).unwrap_or(false);
let info = serde_json::json!({
"id": peer.id,
"name": peer.name,
"state": peer.state,
"expires_at": peer.expires_at,
"is_expired": is_expired,
"last_handshake_at": peer.last_handshake_at,
"created_at": peer.created_at,
"updated_at": peer.updated_at
});
print_output(&info, format)?;
}
PeerSubcommands::Status { id } => {
let peer_id = Uuid::parse_str(&id)?;
let peer = store.get_peer(peer_id).await?.ok_or("Peer not found")?;
let iface = store
.get_interface(peer.interface_id)
.await?
.ok_or("Interface not found")?;
let wg = create_wireguard_engine();
let iface_stats = wg.get_interface_stats(&iface.name).await?;
let peer_stat = iface_stats.and_then(|s| {
s.peers
.into_iter()
.find(|p| p.public_key == peer.public_key.as_str())
});
match peer_stat {
Some(s) => print_output(&s, format)?,
None => println!("No live kernel stats for peer '{id}'"),
}
}
PeerSubcommands::Config {
id,
provider,
device,
connection,
nat,
mtu,
profile,
endpoint,
output,
} => {
let peer_id = Uuid::parse_str(&id)?;
let peer = store.get_peer(peer_id).await?.ok_or("Peer not found")?;
let iface = store
.get_interface(peer.interface_id)
.await?
.ok_or("Interface not found")?;
let resolved_profile = if provider.is_some()
|| device.is_some()
|| connection.is_some()
|| nat.is_some()
|| mtu.is_some()
|| profile.is_some()
{
let dev = device
.as_deref()
.map(nx9_wg_core::types::client_profile::DeviceCategory::from_str)
.transpose()?;
let conn = connection
.as_deref()
.map(nx9_wg_core::types::client_profile::ConnectionType::from_str)
.transpose()?;
let nat_t = nat
.as_deref()
.map(nx9_wg_core::types::client_profile::NatType::from_str)
.transpose()?;
let res = nx9_wg_api::profile_resolver::ClientProfileResolver::resolve(
&store,
provider.as_deref(),
dev,
conn,
nat_t,
mtu,
profile.as_deref(),
iface.mtu,
)
.await?;
if let Some(w) = res
.warning
.as_ref()
.filter(|_| !cli.quiet && format == OutputFormat::Table)
{
eprintln!("Warning: {w}");
}
Some(res)
} else {
None
};
let server_host = store.resolve_server_endpoint(endpoint.as_deref()).await?;
let conf = ClientConfigBuilder::build_with_profile(
&peer,
&iface,
&server_host,
resolved_profile.as_ref(),
)?;
if let Some(out_path) = output {
std::fs::write(&out_path, &conf)?;
println!("Configuration written to '{}'", out_path.display());
} else {
println!("{conf}");
}
}
PeerSubcommands::Qr {
id,
provider,
device,
connection,
nat,
mtu,
profile,
endpoint,
qr_format,
} => {
let peer_id = Uuid::parse_str(&id)?;
let peer = store.get_peer(peer_id).await?.ok_or("Peer not found")?;
let iface = store
.get_interface(peer.interface_id)
.await?
.ok_or("Interface not found")?;
let resolved_profile = if provider.is_some()
|| device.is_some()
|| connection.is_some()
|| nat.is_some()
|| mtu.is_some()
|| profile.is_some()
{
let dev = device
.as_deref()
.map(nx9_wg_core::types::client_profile::DeviceCategory::from_str)
.transpose()?;
let conn = connection
.as_deref()
.map(nx9_wg_core::types::client_profile::ConnectionType::from_str)
.transpose()?;
let nat_t = nat
.as_deref()
.map(nx9_wg_core::types::client_profile::NatType::from_str)
.transpose()?;
let res = nx9_wg_api::profile_resolver::ClientProfileResolver::resolve(
&store,
provider.as_deref(),
dev,
conn,
nat_t,
mtu,
profile.as_deref(),
iface.mtu,
)
.await?;
if let Some(w) = res
.warning
.as_ref()
.filter(|_| !cli.quiet && format == OutputFormat::Table)
{
eprintln!("Warning: {w}");
}
Some(res)
} else {
None
};
let server_host = store.resolve_server_endpoint(endpoint.as_deref()).await?;
let conf = ClientConfigBuilder::build_with_profile(
&peer,
&iface,
&server_host,
resolved_profile.as_ref(),
)?;
match qr_format.to_lowercase().as_str() {
"svg" => {
let svg = generate_qr_svg(&conf)?;
println!("{svg}");
}
"png" => {
let png_bytes = generate_qr_png_bytes(&conf)?;
println!(
"PNG Bytes (base64): {}",
base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
png_bytes
)
);
}
_ => {
let qr_ascii = generate_qr_ascii(&conf)?;
println!("{qr_ascii}");
}
}
}
}
}
Commands::Network(args) => {
let store = Store::connect(&db_url).await?;
store.migrate().await?;
match args.subcommand {
NetworkSubcommands::List => {
let list = store.list_networks().await?;
print_output(&list, format)?;
}
NetworkSubcommands::Show { id } => {
let net_id = Uuid::parse_str(&id)?;
let net = store.get_network(net_id).await?;
match net {
Some(n) => print_output(&n, format)?,
None => {
eprintln!("Network '{id}' not found");
std::process::exit(1);
}
}
}
NetworkSubcommands::Create {
name,
cidr,
description,
} => {
let net_cidr = validate_cidr(&cidr)?;
let now = chrono::Utc::now().naive_utc();
let net = Network {
id: Uuid::new_v4(),
name,
cidr: net_cidr,
enabled: true,
description,
created_at: now,
updated_at: now,
};
store.create_network(&net).await?;
if !cli.quiet && format == OutputFormat::Table {
println!("Network '{}' created.", net.name);
}
print_output(&net, format)?;
}
NetworkSubcommands::Available {
id,
limit,
interface,
} => {
let net = if let Ok(u) = Uuid::parse_str(&id) {
store.get_network(u).await?.ok_or("Network not found")?
} else {
store
.get_network_by_name(&id)
.await?
.ok_or("Network not found")?
};
let iface = match interface {
Some(ref i_str) => {
if let Ok(u) = Uuid::parse_str(i_str) {
store.get_interface(u).await?
} else {
store.get_interface_by_name(i_str).await?
}
}
None => None,
};
let available =
IpAllocator::list_available_ips(&store, &net, iface.as_ref(), limit)
.await?;
let res: Vec<serde_json::Value> = available
.iter()
.map(|ip| serde_json::json!({ "available_ip": ip.to_string(), "network": net.name }))
.collect();
print_output(&res, format)?;
}
NetworkSubcommands::Allocations { id } => {
let net = if let Ok(u) = Uuid::parse_str(&id) {
store.get_network(u).await?.ok_or("Network not found")?
} else {
store
.get_network_by_name(&id)
.await?
.ok_or("Network not found")?
};
let allocs = IpAllocator::list_allocations(&store, &net).await?;
print_output(&allocs, format)?;
}
NetworkSubcommands::Update {
id,
name,
cidr,
description,
enabled,
} => {
let net_id = Uuid::parse_str(&id)?;
let mut net = store
.get_network(net_id)
.await?
.ok_or("Network not found")?;
if let Some(n) = name {
net.name = n;
}
if let Some(c) = cidr {
net.cidr = validate_cidr(&c)?;
}
if let Some(d) = description {
net.description = Some(d);
}
if let Some(e) = enabled {
net.enabled = e;
}
net.updated_at = chrono::Utc::now().naive_utc();
store.update_network(&net).await?;
if !cli.quiet && format == OutputFormat::Table {
println!("Network '{}' updated.", net.name);
}
print_output(&net, format)?;
}
NetworkSubcommands::Delete { id } => {
let net_id = Uuid::parse_str(&id)?;
store.delete_network(net_id).await?;
println!("Network '{id}' deleted.");
}
}
}
Commands::Route(args) => {
let store = Store::connect(&db_url).await?;
store.migrate().await?;
match args.subcommand {
RouteSubcommands::List => {
let list = store.list_routes().await?;
print_output(&list, format)?;
}
RouteSubcommands::Show { id } => {
let r_id = Uuid::parse_str(&id)?;
let route = store.get_route(r_id).await?;
match route {
Some(r) => print_output(&r, format)?,
None => {
eprintln!("Route '{id}' not found");
std::process::exit(1);
}
}
}
RouteSubcommands::Add {
destination,
gateway,
interface_name,
metric,
} => {
let dst = validate_cidr(&destination)?;
let gw = gateway.as_deref().map(IpAddr::from_str).transpose()?;
let now = chrono::Utc::now().naive_utc();
let route = Route {
id: Uuid::new_v4(),
network_id: None,
interface_id: None,
destination: dst,
gateway: gw,
interface_name,
metric,
enabled: true,
description: None,
created_at: now,
updated_at: now,
};
store.create_route(&route).await?;
if !cli.quiet && format == OutputFormat::Table {
println!("Route to '{}' added.", route.destination);
}
print_output(&route, format)?;
}
RouteSubcommands::Update {
id,
destination,
gateway,
interface_name,
metric,
enabled,
} => {
let r_id = Uuid::parse_str(&id)?;
let mut route = store.get_route(r_id).await?.ok_or("Route not found")?;
if let Some(d) = destination {
route.destination = validate_cidr(&d)?;
}
if let Some(g) = gateway {
route.gateway = Some(IpAddr::from_str(&g)?);
}
if let Some(i) = interface_name {
route.interface_name = Some(i);
}
if let Some(m) = metric {
route.metric = Some(m);
}
if let Some(e) = enabled {
route.enabled = e;
}
route.updated_at = chrono::Utc::now().naive_utc();
store.update_route(&route).await?;
if !cli.quiet && format == OutputFormat::Table {
println!("Route updated.");
}
print_output(&route, format)?;
}
RouteSubcommands::Delete { id } => {
let r_id = Uuid::parse_str(&id)?;
store.delete_route(r_id).await?;
println!("Route '{id}' deleted.");
}
RouteSubcommands::Status => {
let status = serde_json::json!({
"routes_managed": store.list_routes().await?.len(),
"engine": "linux_netlink_routing"
});
print_output(&status, format)?;
}
RouteSubcommands::Sync => {
let routes = store.list_routes().await?;
let net = NativeLinuxNetworkEngine::new();
net.sync_routes(&routes).await?;
println!("Routes synchronized successfully with kernel.");
}
}
}
Commands::Firewall(args) => {
let store = Store::connect(&db_url).await?;
store.migrate().await?;
match args.subcommand {
FirewallSubcommands::List { peer } => {
let list = if let Some(ref p_str) = peer {
let peer_id = if let Ok(u) = Uuid::parse_str(p_str) {
u
} else {
let all_peers = store.list_all_peers().await?;
let p = all_peers
.into_iter()
.find(|p| &p.name == p_str)
.ok_or("Peer not found")?;
p.id
};
store.list_firewall_rules_for_peer(peer_id).await?
} else {
store.list_firewall_rules().await?
};
print_output(&list, format)?;
}
FirewallSubcommands::Show { id } => {
let rule_id = Uuid::parse_str(&id)?;
let rule = store.get_firewall_rule(rule_id).await?;
match rule {
Some(r) => print_output(&r, format)?,
None => {
eprintln!("Firewall rule '{id}' not found");
std::process::exit(1);
}
}
}
FirewallSubcommands::Add {
name,
direction,
source,
destination,
peer,
protocol,
port,
port_range,
action,
priority,
} => {
let dir =
FirewallDirection::from_str(&direction).unwrap_or(FirewallDirection::In);
let proto =
FirewallProtocol::from_str(&protocol).unwrap_or(FirewallProtocol::Any);
let act = FirewallAction::from_str(&action).unwrap_or(FirewallAction::Accept);
let peer_id = match peer {
Some(ref p_str) => {
if let Ok(u) = Uuid::parse_str(p_str) {
Some(u)
} else {
let all = store.list_all_peers().await?;
let p = all
.into_iter()
.find(|p| &p.name == p_str)
.ok_or("Peer not found")?;
Some(p.id)
}
}
None => None,
};
if let Some(ref pr) = port_range {
validate_port_spec(pr)?;
}
let now = chrono::Utc::now().naive_utc();
let rule = FirewallRule {
id: Uuid::new_v4(),
name,
interface_id: None,
peer_id,
direction: dir,
source,
destination,
protocol: proto,
source_port: None,
destination_port: port,
port_range,
action: act,
priority,
enabled: true,
description: None,
created_at: now,
updated_at: now,
};
store.create_firewall_rule(&rule).await?;
if !cli.quiet && format == OutputFormat::Table {
println!("Firewall rule '{}' added.", rule.name);
}
print_output(&rule, format)?;
}
FirewallSubcommands::Update {
id,
name,
action,
priority,
enabled,
} => {
let r_id = Uuid::parse_str(&id)?;
let mut rule = store
.get_firewall_rule(r_id)
.await?
.ok_or("Rule not found")?;
if let Some(n) = name {
rule.name = n;
}
if let Some(a) = action {
rule.action = FirewallAction::from_str(&a)?;
}
if let Some(p) = priority {
rule.priority = p;
}
if let Some(e) = enabled {
rule.enabled = e;
}
rule.updated_at = chrono::Utc::now().naive_utc();
store.update_firewall_rule(&rule).await?;
if !cli.quiet && format == OutputFormat::Table {
println!("Firewall rule '{}' updated.", rule.name);
}
print_output(&rule, format)?;
}
FirewallSubcommands::Delete { id } => {
let rule_id = Uuid::parse_str(&id)?;
store.delete_firewall_rule(rule_id).await?;
println!("Firewall rule '{id}' deleted.");
}
FirewallSubcommands::Enable { id } => {
let rule_id = Uuid::parse_str(&id)?;
store.set_firewall_rule_enabled(rule_id, true).await?;
println!("Firewall rule '{id}' enabled.");
}
FirewallSubcommands::Disable { id } => {
let rule_id = Uuid::parse_str(&id)?;
store.set_firewall_rule_enabled(rule_id, false).await?;
println!("Firewall rule '{id}' disabled.");
}
FirewallSubcommands::Sync => {
let rules = store.list_firewall_rules().await?;
let ifaces = store.list_interfaces().await?;
let subnets: Vec<_> = ifaces.into_iter().map(|i| i.address_v4).collect();
let net = NativeLinuxNetworkEngine::new();
net.sync_firewall(&rules, true, &subnets).await?;
println!("Firewall ruleset synchronized successfully.");
}
FirewallSubcommands::Status => {
let net = NativeLinuxNetworkEngine::new();
let ruleset = net.get_active_nftables_ruleset().await?;
let status = serde_json::json!({
"rules_count": store.list_firewall_rules().await?.len(),
"table": "inet nx9_wg",
"ruleset": ruleset
});
print_output(&status, format)?;
}
}
}
Commands::Nat(args) => {
let store = Store::connect(&db_url).await?;
store.migrate().await?;
match args.subcommand {
NatSubcommands::Status => {
let ifaces = store.list_interfaces().await?;
let subnets: Vec<String> = ifaces
.into_iter()
.map(|i| i.address_v4.to_string())
.collect();
let status = serde_json::json!({
"nat_masquerade_enabled": true,
"table": "inet nx9_wg",
"managed_subnets": subnets
});
print_output(&status, format)?;
}
NatSubcommands::Enable => {
store.set_setting("nat_enabled", "true", false).await?;
println!("NAT masquerade enabled in settings.");
}
NatSubcommands::Disable => {
store.set_setting("nat_enabled", "false", false).await?;
println!("NAT masquerade disabled in settings.");
}
NatSubcommands::List => {
let ifaces = store.list_interfaces().await?;
let subnets: Vec<String> = ifaces
.into_iter()
.map(|i| i.address_v4.to_string())
.collect();
print_output(&subnets, format)?;
}
NatSubcommands::Sync => {
let rules = store.list_firewall_rules().await?;
let ifaces = store.list_interfaces().await?;
let subnets: Vec<_> = ifaces.into_iter().map(|i| i.address_v4).collect();
let net = NativeLinuxNetworkEngine::new();
net.sync_firewall(&rules, true, &subnets).await?;
println!("NAT masquerade rules synchronized with nftables.");
}
}
}
Commands::Forwarding(args) => match args.subcommand {
ForwardingSubcommands::Status => {
let status = IpForwardingStatus::detect().unwrap_or_default();
print_output(&status, format)?;
}
ForwardingSubcommands::Enable => {
let _ = IpForwardingStatus::set_ipv4(true);
let _ = IpForwardingStatus::set_ipv6(true);
println!("Linux kernel IP packet forwarding enabled.");
}
ForwardingSubcommands::Disable => {
let _ = IpForwardingStatus::set_ipv4(false);
let _ = IpForwardingStatus::set_ipv6(false);
println!("Linux kernel IP packet forwarding disabled.");
}
ForwardingSubcommands::Sync => {
let _ = IpForwardingStatus::set_ipv4(true);
println!("IP packet forwarding synchronized with kernel.");
}
},
Commands::Reconcile(args) => {
let store = Store::connect(&db_url).await?;
store.migrate().await?;
let state = AppState::new(store);
let wg = Arc::new(NativeLinuxWireGuardEngine::new());
let net = Arc::new(NativeLinuxNetworkEngine::new());
let reconciler = ReconciliationEngine::new(state, wg, net);
match args.subcommand {
ReconcileSubcommands::Status => {
let plan = reconciler.plan().await?;
let status = serde_json::json!({
"drift_detected": plan.has_drift,
"interface_changes": plan.interface_changes,
"peer_changes": plan.peer_changes,
"route_changes": plan.route_changes,
"firewall_changes": plan.firewall_changes
});
print_output(&status, format)?;
}
ReconcileSubcommands::Plan { .. } => {
let plan = reconciler.plan().await?;
print_output(&plan, format)?;
}
ReconcileSubcommands::Apply { .. } => {
let report = reconciler.apply().await?;
print_output(&report, format)?;
}
ReconcileSubcommands::Verify => {
let plan = reconciler.plan().await?;
if plan.has_drift {
eprintln!("Drift detected between SQLite desired state and kernel state.");
print_output(&plan, format)?;
std::process::exit(1);
} else {
println!(
"Zero drift detected: SQLite and kernel state are in full synchronization."
);
}
}
}
}
Commands::Backup(args) => {
let store = Store::connect(&db_url).await?;
store.migrate().await?;
match args.subcommand {
BackupSubcommands::Create { description } => {
let backup_dir = config.backup.dir;
let (meta, path) = BackupService::create_backup(
&store,
&backup_dir,
description.as_deref(),
"cli",
None,
)
.await?;
if !cli.quiet && format == OutputFormat::Table {
println!("Backup created at '{}'", path.display());
}
print_output(&meta, format)?;
}
BackupSubcommands::List => {
let list = store.list_backups().await?;
print_output(&list, format)?;
}
BackupSubcommands::Show { id } => {
let b_id = Uuid::parse_str(&id)?;
let meta = store.get_backup_meta(b_id).await?;
match meta {
Some(m) => print_output(&m, format)?,
None => {
eprintln!("Backup record '{id}' not found");
std::process::exit(1);
}
}
}
BackupSubcommands::Verify { path } => {
let valid = BackupService::verify_backup(&path, None)?;
if valid {
println!("Backup file '{}' is VALID.", path.display());
} else {
eprintln!("Backup file '{}' is INVALID or corrupted.", path.display());
std::process::exit(1);
}
}
BackupSubcommands::Restore { path, yes } => {
if !yes {
eprintln!("WARNING: Restoring database is a destructive operation.");
eprintln!("Please re-run with '--yes' to confirm.");
std::process::exit(1);
}
let active_db = PathBuf::from(&db_url);
let safety_dir = config.backup.dir.join("safety");
BackupService::restore_backup(
&store,
&path,
&active_db,
&safety_dir,
"cli",
None,
)
.await?;
println!("Database successfully restored from '{}'", path.display());
}
BackupSubcommands::Delete { id } => {
let b_id = Uuid::parse_str(&id)?;
store.delete_backup_meta(b_id).await?;
println!("Backup record '{id}' deleted.");
}
}
}
Commands::Audit(args) => {
let store = Store::connect(&db_url).await?;
store.migrate().await?;
match args.subcommand {
AuditSubcommands::List {
event_type,
actor,
resource_type,
limit,
offset,
} => {
let mut filter = nx9_wg_db::AuditFilter::default();
if let Some(et) = event_type {
filter.event_type = Some(AuditEventType::from_str(&et)?);
}
if let Some(act) = actor {
filter.resource_id = Some(act);
}
if let Some(rt) = resource_type {
filter.resource_type = Some(rt);
}
let events = store.list_audit_events(&filter, limit, offset).await?;
print_output(&events, format)?;
}
AuditSubcommands::Show { id } => {
let event = store.get_audit_event(id).await?;
match event {
Some(e) => print_output(&e, format)?,
None => {
eprintln!("Audit event '{id}' not found");
std::process::exit(1);
}
}
}
}
}
Commands::Live(args) => match args.subcommand {
LiveSubcommands::Interface(i_args) => match i_args.subcommand {
LiveInterfaceSubcommands::List => {
let wg = create_wireguard_engine();
let list = wg.list_interfaces().await?;
print_output(&list, format)?;
}
LiveInterfaceSubcommands::Show { name } => {
let wg = create_wireguard_engine();
let stats = wg.get_interface_stats(&name).await?;
match stats {
Some(s) => print_output(&s, format)?,
None => {
eprintln!("Live interface '{name}' not found");
std::process::exit(1);
}
}
}
},
LiveSubcommands::Peer(p_args) => match p_args.subcommand {
LivePeerSubcommands::List { interface } => {
let wg = create_wireguard_engine();
let stats = wg.get_interface_stats(&interface).await?;
let peers = stats.map(|s| s.peers).unwrap_or_default();
print_output(&peers, format)?;
}
LivePeerSubcommands::Show { id } => {
let wg = create_wireguard_engine();
let ifaces = wg.list_interfaces().await?;
let mut found = None;
for iface in ifaces {
if let Ok(Some(stats)) = wg.get_interface_stats(&iface).await {
for p in stats.peers {
if p.public_key == id || p.endpoint.as_deref() == Some(&id) {
found = Some(p);
break;
}
}
if found.is_some() {
break;
}
}
}
match found {
Some(s) => print_output(&s, format)?,
None => {
eprintln!("Live peer '{id}' not found in kernel");
std::process::exit(1);
}
}
}
},
LiveSubcommands::Routes => {
let status = serde_json::json!({
"live_kernel_routes": "synchronized",
"engine": "linux_netlink"
});
print_output(&status, format)?;
}
LiveSubcommands::Firewall => {
let net = create_network_engine();
let ruleset = net.get_active_nftables_ruleset().await?;
print_output(&ruleset, format)?;
}
LiveSubcommands::Forwarding => {
let status = IpForwardingStatus::detect().unwrap_or_default();
print_output(&status, format)?;
}
LiveSubcommands::Nat => {
let net = create_network_engine();
let ruleset = net.get_active_nftables_ruleset().await.unwrap_or_default();
let nat_active = ruleset.contains("masquerade");
let status = serde_json::json!({
"nat_masquerade_active": nat_active,
"table": "inet nx9_wg",
"chain": "postrouting"
});
print_output(&status, format)?;
}
},
Commands::Diagnostics(args) => {
let store = Store::connect(&db_url).await?;
store.migrate().await?;
let state = AppState::new(store);
let wg = Arc::new(NativeLinuxWireGuardEngine::new());
let net = Arc::new(NativeLinuxNetworkEngine::new());
let reconciler = Arc::new(ReconciliationEngine::new(
state.clone(),
wg.clone(),
net.clone(),
));
let service = DiagnosticsService::new(state, wg, net, reconciler);
let peer_id = args.peer.as_deref().map(Uuid::parse_str).transpose()?;
let subsystem = DiagnosticSubsystem::from_str(&args.subsystem)?;
let reports = service.run_diagnostic(subsystem, peer_id).await?;
print_output(&reports, format)?;
}
Commands::Profile(args) => {
let store = Store::connect(&db_url).await?;
store.migrate().await?;
match args.subcommand {
ProfileSubcommands::List {
provider,
device,
connection,
nat,
} => {
let dev = device
.as_deref()
.map(nx9_wg_core::types::client_profile::DeviceCategory::from_str)
.transpose()?;
let conn = connection
.as_deref()
.map(nx9_wg_core::types::client_profile::ConnectionType::from_str)
.transpose()?;
let nat_t = nat
.as_deref()
.map(nx9_wg_core::types::client_profile::NatType::from_str)
.transpose()?;
let profiles = if provider.is_some()
|| dev.is_some()
|| conn.is_some()
|| nat_t.is_some()
{
store
.find_matching_client_profiles(provider.as_deref(), dev, conn, nat_t)
.await?
} else {
store.list_client_profiles().await?
};
print_output(&profiles, format)?;
}
ProfileSubcommands::Show { id } => {
let profile = store
.get_client_profile(&id)
.await?
.ok_or_else(|| format!("Client profile '{id}' not found"))?;
print_output(&profile, format)?;
}
ProfileSubcommands::Validate { mtu } => {
match nx9_wg_core::validation::validate_client_mtu(mtu) {
Ok(valid_mtu) => {
let res = serde_json::json!({
"mtu": valid_mtu,
"valid": true,
"is_jumbo": valid_mtu > 1500,
"message": if valid_mtu > 1500 {
"MTU is in valid jumbo frame range (1501-9000)"
} else {
"MTU is in valid standard range (1280-1500)"
}
});
print_output(&res, format)?;
}
Err(e) => {
return Err(format!("Invalid MTU: {e}").into());
}
}
}
ProfileSubcommands::Resolve {
provider,
device,
connection,
nat,
mtu,
profile,
} => {
let dev = device
.as_deref()
.map(nx9_wg_core::types::client_profile::DeviceCategory::from_str)
.transpose()?;
let conn = connection
.as_deref()
.map(nx9_wg_core::types::client_profile::ConnectionType::from_str)
.transpose()?;
let nat_t = nat
.as_deref()
.map(nx9_wg_core::types::client_profile::NatType::from_str)
.transpose()?;
let resolved = nx9_wg_api::profile_resolver::ClientProfileResolver::resolve(
&store,
provider.as_deref(),
dev,
conn,
nat_t,
mtu,
profile.as_deref(),
None,
)
.await?;
print_output(&resolved, format)?;
}
}
}
}
Ok(())
}