//! 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, #[arg( short, long, global = true, env = "NX9_WG_DATA_DIR", help = "Path to database data directory" )] data_dir: Option, #[arg( long, global = true, env = "NX9_WG_DATABASE", help = "Specific SQLite database file path or URL" )] database: Option, #[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, #[command(subcommand)] command: Option, } #[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, } #[derive(Args)] struct ServeArgs { #[arg( short, long, env = "NX9_WG_LISTEN_ADDR", help = "Bind address for HTTP/WebSocket server" )] bind: Option, } #[derive(Args)] struct InitArgs { #[arg( short, long, env = "NX9_WG_ADMIN_USERNAME", help = "Administrator username [default: admin]" )] username: Option, #[arg( long, env = "NX9_WG_ADMIN_PASSWORD", help = "Administrator password via argument" )] password: Option, #[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, #[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, } #[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, #[arg(long, help = "Read new password from standard input")] stdin: bool, #[arg(long, help = "Read new password from file")] password_file: Option, #[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, #[arg( long, help = "Write the plaintext token to a file (restricted mode 0600)" )] write_token_file: Option, }, #[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, #[arg(short, long, help = "Interface MTU [default: 1420]")] mtu: Option, #[arg(long, help = "DNS server addresses")] dns: Option, }, #[command(about = "Update an existing WireGuard interface")] Update { interface: String, #[arg(short, long, help = "UDP listen port")] port: Option, #[arg(short = '4', long, help = "IPv4 network CIDR address")] address_v4: Option, #[arg(short = '6', long, help = "IPv6 network CIDR address")] address_v6: Option, #[arg(short, long, help = "Interface MTU")] mtu: Option, #[arg(long, help = "DNS server addresses")] dns: Option, #[arg(long, help = "Enable or disable interface")] enabled: Option, }, #[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, }, #[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, #[arg(short = '4', long, help = "IPv4 peer address CIDR")] address_v4: Option, #[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, #[arg(long, help = "Persistent keepalive interval in seconds")] persistent_keepalive: Option, #[arg(long, help = "DNS servers")] dns: Option, #[arg(long, help = "Client MTU")] mtu: Option, #[arg( long, help = "Peer expiration timestamp (RFC3339 or 'YYYY-MM-DD HH:MM:SS')" )] expires_at: Option, }, #[command(about = "Update an enrolled peer")] Update { id: String, #[arg(short, long, help = "Peer display name")] name: Option, #[arg(long, help = "Allowed IPs")] allowed_ips: Option, #[arg(long, help = "Endpoint")] endpoint: Option, #[arg(long, help = "Persistent keepalive interval")] persistent_keepalive: Option, #[arg(long, help = "DNS servers")] dns: Option, #[arg(long, help = "MTU")] mtu: Option, #[arg(long, help = "Peer expiration timestamp")] expires_at: Option, #[arg(long, help = "Enable or disable peer")] enabled: Option, }, #[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, #[arg( long, help = "Device category (android, ios, linux, windows, macos, other)" )] device: Option, #[arg( long, help = "Connection environment (web, mobile, wifi, wired, other)" )] connection: Option, #[arg(long, help = "NAT condition (direct, cgnat, unknown)")] nat: Option, #[arg(long, help = "Explicit manual MTU override")] mtu: Option, #[arg(long, help = "Explicit client profile ID")] profile: Option, #[arg(long, help = "WireGuard server endpoint host or IP")] endpoint: Option, #[arg(short, long, help = "Write configuration to file")] output: Option, }, #[command(about = "Generate enrollment QR code")] Qr { id: String, #[arg(long, help = "Network provider (e.g. tmobile, verizon, jio, starlink)")] provider: Option, #[arg( long, help = "Device category (android, ios, linux, windows, macos, other)" )] device: Option, #[arg( long, help = "Connection environment (web, mobile, wifi, wired, other)" )] connection: Option, #[arg(long, help = "NAT condition (direct, cgnat, unknown)")] nat: Option, #[arg(long, help = "Explicit manual MTU override")] mtu: Option, #[arg(long, help = "Explicit client profile ID")] profile: Option, #[arg(long, help = "WireGuard server endpoint host or IP")] endpoint: Option, #[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, #[arg( long, help = "Filter by device category (android, ios, linux, windows, macos, other)" )] device: Option, #[arg( long, help = "Filter by connection type (web, mobile, wifi, wired, other)" )] connection: Option, #[arg(long, help = "Filter by NAT type (direct, cgnat, unknown)")] nat: Option, }, #[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, #[arg( long, help = "Device category (android, ios, linux, windows, macos, other)" )] device: Option, #[arg( long, help = "Connection environment (web, mobile, wifi, wired, other)" )] connection: Option, #[arg(long, help = "NAT condition (direct, cgnat, unknown)")] nat: Option, #[arg(long, help = "Explicit manual MTU override")] mtu: Option, #[arg(long, help = "Explicit profile ID override")] profile: Option, }, } #[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, }, #[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, }, #[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, #[arg(long, help = "Network CIDR")] cidr: Option, #[arg(long, help = "Description")] description: Option, #[arg(long, help = "Enable or disable network")] enabled: Option, }, #[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, #[arg(short, long, help = "Egress interface name")] interface_name: Option, #[arg(short, long, help = "Route priority metric")] metric: Option, }, #[command(about = "Update an existing route")] Update { id: String, #[arg(long, help = "Destination CIDR")] destination: Option, #[arg(short, long, help = "Gateway IP")] gateway: Option, #[arg(short, long, help = "Interface name")] interface_name: Option, #[arg(short, long, help = "Metric")] metric: Option, #[arg(long, help = "Enable or disable route")] enabled: Option, }, #[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, }, #[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, #[arg(long, help = "Destination CIDR")] destination: Option, #[arg(long, help = "Associate with specific peer name or UUID")] peer: Option, #[arg( long, default_value = "any", help = "Protocol (tcp, udp, tcp_udp, icmp, any)" )] protocol: String, #[arg(short, long, help = "Target port")] port: Option, #[arg( long, help = "Port specification (single '443', range '8000-8100', or list '53,80,443')" )] port_range: Option, #[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, #[arg(short, long, help = "Action")] action: Option, #[arg(long, help = "Priority")] priority: Option, #[arg(long, help = "Enable or disable rule")] enabled: Option, }, #[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, }, #[command(about = "Apply reconciliation plan to live kernel state")] Apply { #[arg(short, long, help = "Target specific interface")] interface: Option, }, #[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, }, #[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, #[arg(long, help = "Filter by actor")] actor: Option, #[arg(long, help = "Filter by resource type")] resource_type: Option, #[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 { #[cfg(target_os = "linux")] { Arc::new(NativeLinuxWireGuardEngine::new()) } #[cfg(not(target_os = "linux"))] { Arc::new(SimulatedWireGuardEngine::new()) } } fn create_network_engine() -> Arc { #[cfg(target_os = "linux")] { Arc::new(NativeLinuxNetworkEngine::new()) } #[cfg(not(target_os = "linux"))] { Arc::new(SimulatedNetworkEngine::new()) } } // ── Output Formatter ──────────────────────────────────────────────────────── fn print_output( data: &T, format: OutputFormat, ) -> Result<(), Box> { 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 = 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 = 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> { 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::(), ) .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 = 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 = 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 = 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 = 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(()) }