From 3e8f938d16aa931848f854c13bb1ad63fac06f83 Mon Sep 17 00:00:00 2001 From: Victor Wildner Date: Mon, 1 Dec 2025 18:29:58 +0100 Subject: [PATCH] chore(release): bump version to 1.2.3 - Update version to 1.2.3 in Cargo.toml files - Enhance setup command output with clearer messages - Improve error handling and user feedback in VPN commands - Refactor comments for better clarity in main.rs --- Cargo.toml | 2 +- akon-core/Cargo.toml | 26 ++-- src/cli/setup.rs | 63 +++++--- src/cli/vpn.rs | 332 +++++++++++++++++++++++++------------------ src/main.rs | 20 +-- 5 files changed, 264 insertions(+), 179 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c0004bb..3d951f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [package] name = "akon" -version = "1.2.2" +version = "1.2.3" edition = "2021" authors = ["vcwild"] description = "A CLI tool for managing VPN connections with OpenConnect" diff --git a/akon-core/Cargo.toml b/akon-core/Cargo.toml index e1e34c2..68439f9 100644 --- a/akon-core/Cargo.toml +++ b/akon-core/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "akon-core" -version = "1.2.2" +version = "1.2.3" [features] default = [] @@ -15,35 +15,35 @@ dead_code = "deny" # Workspace dependencies anyhow.workspace = true base32.workspace = true +chrono = "0.4" +data-encoding = "2.9.0" keyring.workspace = true +nix.workspace = true +regex = "1.10" secrecy.workspace = true serde.workspace = true +sha1 = "0.10.6" thiserror.workspace = true +tokio.workspace = true toml.workspace = true totp-lite.workspace = true tracing-journald.workspace = true tracing-subscriber.workspace = true tracing.workspace = true -tokio.workspace = true -nix.workspace = true -data-encoding = "2.9.0" -sha1 = "0.10.6" -regex = "1.10" -chrono = "0.4" # lazy_static is optional and enabled via the `mock-keyring` feature -lazy_static = { version = "1.5", optional = true } +lazy_static = {version = "1.5", optional = true} # Network interruption detection dependencies -zbus = "4.0" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +reqwest = {version = "0.12", default-features = false, features = ["rustls-tls"]} url = "2.5" +zbus = "4.0" [dev-dependencies] cargo-tarpaulin = "0.27" -serde_json = "1.0" +criterion = "0.5" hex = "0.4" +lazy_static = "1.5" +serde_json = "1.0" tempfile = "3.0" -criterion = "0.5" tokio-test = "0.4" wiremock = "0.6" -lazy_static = "1.5" diff --git a/src/cli/setup.rs b/src/cli/setup.rs index bb81d19..226f7a1 100644 --- a/src/cli/setup.rs +++ b/src/cli/setup.rs @@ -13,12 +13,11 @@ use std::io::{self, Write}; /// Run the setup command pub fn run_setup() -> Result<(), AkonError> { + println!("[SETUP] {}", "akon VPN Setup".bright_white().bold()); println!( - "{} {}", - "๐Ÿ”".bright_magenta(), - "akon VPN Setup".bright_white().bold() + "{}", + "========================================".bright_white() ); - println!("{}", "=================".bright_white()); println!(); println!( "{}", @@ -38,7 +37,7 @@ pub fn run_setup() -> Result<(), AkonError> { if let Ok(true) = toml_config::config_exists() { println!( "{} {}", - "โš ".bright_yellow(), + "[WARN]".bright_yellow(), "Existing configuration detected.".bright_yellow() ); if !prompt_yes_no("Overwrite existing setup?", false)? { @@ -72,7 +71,7 @@ pub fn run_setup() -> Result<(), AkonError> { println!(); println!( "{} {}", - "๐Ÿ’พ".bright_cyan(), + "[SAVE]".bright_cyan(), "Saving configuration...".bright_white() ); @@ -85,15 +84,15 @@ pub fn run_setup() -> Result<(), AkonError> { println!( "{} {}", - "โœ…".bright_green(), + "[OK]".bright_green().bold(), "Setup complete!".bright_green().bold() ); println!(); println!("{}", "You can now use:".bright_white()); - println!(" {} - Connect to VPN", "akon vpn on".bright_cyan()); - println!(" {} - Disconnect from VPN", "akon vpn off".bright_cyan()); + println!(" {} - Connect to VPN", "akon vpn on".bright_cyan()); + println!(" {} - Disconnect from VPN", "akon vpn off".bright_cyan()); println!( - " {} - Generate OTP token manually", + " {} - Generate OTP token manually", "akon get-password".bright_cyan() ); @@ -110,9 +109,13 @@ fn check_keyring_availability() -> Result<(), AkonError> { Ok(()) } Err(AkonError::Keyring(_)) => { - println!("โŒ Keyring is not available or locked."); - println!("Please ensure your system keyring is unlocked and available."); - println!("On GNOME systems, this is usually handled automatically."); + println!( + "{} {}", + "[ERROR]".bright_red().bold(), + "Keyring is not available or locked.".bright_red().bold() + ); + println!(" Please ensure your system keyring is unlocked and available."); + println!(" On GNOME systems, this is usually handled automatically."); Err(AkonError::Keyring( akon_core::error::KeyringError::ServiceUnavailable, )) @@ -299,7 +302,7 @@ fn collect_reconnection_config( println!(); println!( - "{} {}", + " {} {}", "โœ“".bright_green(), "Reconnection configuration validated".bright_green() ); @@ -321,7 +324,13 @@ fn collect_otp_secret() -> Result { let secret = prompt_password("TOTP Secret")?; if secret.trim().is_empty() { - println!("โŒ Secret cannot be empty. Please try again."); + println!( + "{} {}", + "[ERROR]".bright_red().bold(), + "Secret cannot be empty. Please try again." + .bright_red() + .bold() + ); continue; } @@ -330,8 +339,14 @@ fn collect_otp_secret() -> Result { match otp_secret.validate_base32() { Ok(_) => return Ok(otp_secret), Err(_) => { - println!("โŒ Invalid Base32 format. Please check your secret and try again."); - println!(" Valid characters: A-Z, 2-7, =, /"); + println!( + "{} {}", + "[ERROR]".bright_red().bold(), + "Invalid Base32 format. Please check your secret and try again." + .bright_red() + .bold() + ); + println!(" Valid characters: A-Z, 2-7, =, /"); continue; } } @@ -354,7 +369,11 @@ fn collect_pin() -> Result { let candidate = pin_str.trim().to_string(); if candidate.is_empty() { - println!("โŒ PIN cannot be empty. Please try again."); + println!( + "{} {}", + "[ERROR]".bright_red().bold(), + "PIN cannot be empty. Please try again.".bright_red().bold() + ); continue; } @@ -385,7 +404,13 @@ fn prompt_required(prompt: &str, default: &str) -> Result { if !default.is_empty() { return Ok(default.to_string()); } - println!("โŒ This field is required. Please enter a value."); + println!( + "{} {}", + "[ERROR]".bright_red().bold(), + "This field is required. Please enter a value." + .bright_red() + .bold() + ); continue; } diff --git a/src/cli/vpn.rs b/src/cli/vpn.rs index e7986e5..f0f2383 100644 --- a/src/cli/vpn.rs +++ b/src/cli/vpn.rs @@ -27,12 +27,12 @@ fn state_file_path() -> PathBuf { fn handle_cleanup_result(result: Result, context: &str) { match result { Ok(0) => { - println!(" {} No orphaned processes found", "โœ“".bright_green()); + println!(" {} No orphaned processes found", "โœ“".bright_green()); debug!("{}: No orphaned OpenConnect processes to clean up", context); } Ok(count) => { println!( - " {} Terminated {} orphaned process(es)", + " {} Terminated {} orphaned process(es)", "โœ“".bright_green(), count.to_string().bright_yellow() ); @@ -45,7 +45,7 @@ fn handle_cleanup_result(result: Result, context: &str) { warn!("{}: Orphan cleanup failed: {}", context, e); println!( " {} Warning: Could not verify all processes cleaned up", - "โš ".bright_yellow() + "[WARN]".bright_yellow() ); } } @@ -57,86 +57,102 @@ fn print_error_suggestions(error: &VpnError) { VpnError::AuthenticationFailed => { eprintln!( "\n{} {}", - "๐Ÿ’ก".bright_yellow(), + "[TIP]".bright_yellow(), "Suggestions:".bright_white().bold() ); - eprintln!(" {} Verify your PIN is correct", "โ€ข".bright_blue()); + eprintln!(" {} Verify your PIN is correct", "-".bright_blue()); eprintln!( " {} Check if your TOTP secret is valid", - "โ€ข".bright_blue() + "-".bright_blue() ); eprintln!( " {} Run {} to reconfigure credentials", - "โ€ข".bright_blue(), + "-".bright_blue(), "akon setup".bright_cyan() ); - eprintln!(" {} Ensure your account is not locked", "โ€ข".bright_blue()); + eprintln!(" {} Ensure your account is not locked", "-".bright_blue()); } VpnError::NetworkError { reason } if reason.contains("SSL") || reason.contains("TLS") => { - eprintln!("\n๐Ÿ’ก Suggestions:"); - eprintln!(" โ€ข Check your internet connection"); - eprintln!(" โ€ข Verify the VPN server address is correct"); - eprintln!(" โ€ข The server may be experiencing issues"); - eprintln!(" โ€ข Try again in a few moments"); + eprintln!( + "\n{} {}", + "[TIP]".bright_yellow(), + "Suggestions:".bright_white().bold() + ); + eprintln!(" - Check your internet connection"); + eprintln!(" - Verify the VPN server address is correct"); + eprintln!(" - The server may be experiencing issues"); + eprintln!(" - Try again in a few moments"); } VpnError::NetworkError { reason } if reason.contains("Certificate") => { - eprintln!("\n๐Ÿ’ก Suggestions:"); - eprintln!(" โ€ข The server certificate may be self-signed"); - eprintln!(" โ€ข Contact your VPN administrator for certificate details"); - eprintln!(" โ€ข You may need to add the certificate to your trusted store"); + eprintln!( + "\n{} {}", + "[TIP]".bright_yellow(), + "Suggestions:".bright_white().bold() + ); + eprintln!(" - The server certificate may be self-signed"); + eprintln!(" - Contact your VPN administrator for certificate details"); + eprintln!(" - You may need to add the certificate to your trusted store"); } VpnError::NetworkError { reason } if reason.contains("DNS") => { - eprintln!("\n๐Ÿ’ก Suggestions:"); - eprintln!(" โ€ข Check your DNS configuration"); - eprintln!(" โ€ข Verify the VPN server hostname in config.toml"); - eprintln!(" โ€ข Try using the server's IP address instead"); - eprintln!(" โ€ข Check /etc/resolv.conf for DNS settings"); + eprintln!( + "\n{} {}", + "[TIP]".bright_yellow(), + "Suggestions:".bright_white().bold() + ); + eprintln!(" - Check your DNS configuration"); + eprintln!(" - Verify the VPN server hostname in config.toml"); + eprintln!(" - Try using the server's IP address instead"); + eprintln!(" - Check /etc/resolv.conf for DNS settings"); } VpnError::ConnectionFailed { reason } if reason.contains("TUN") || reason.contains("sudo") => { - eprintln!("\n๐Ÿ’ก Suggestions:"); - eprintln!(" โ€ข VPN requires root privileges to create TUN device"); - eprintln!(" โ€ข Run with: sudo akon vpn on"); - eprintln!(" โ€ข Ensure the 'tun' kernel module is loaded"); - eprintln!(" โ€ข Check: lsmod | grep tun"); + eprintln!( + "\n{} {}", + "[TIP]".bright_yellow(), + "Suggestions:".bright_white().bold() + ); + eprintln!(" - VPN requires root privileges to create TUN device"); + eprintln!(" - Run with: sudo akon vpn on"); + eprintln!(" - Ensure the 'tun' kernel module is loaded"); + eprintln!(" - Check: lsmod | grep tun"); } VpnError::ProcessSpawnError { .. } => { eprintln!( "\n{} {}", - "๐Ÿ’ก".bright_yellow(), + "[TIP]".bright_yellow(), "Suggestions:".bright_white().bold() ); - eprintln!(" {} OpenConnect may not be installed", "โ€ข".bright_blue()); + eprintln!(" {} OpenConnect may not be installed", "-".bright_blue()); eprintln!( " {} Install with: {}", - "โ€ข".bright_blue(), + "-".bright_blue(), "sudo apt install openconnect".bright_cyan() ); eprintln!( " {} Or for RHEL/Fedora: {}", - "โ€ข".bright_blue(), + "-".bright_blue(), "sudo dnf install openconnect".bright_cyan() ); eprintln!( " {} Verify installation: {}", - "โ€ข".bright_blue(), + "-".bright_blue(), "which openconnect".bright_cyan() ); } VpnError::ConnectionFailed { reason } if reason.contains("Permission denied") => { eprintln!( "\n{} {}", - "๐Ÿ’ก".bright_yellow(), + "[TIP]".bright_yellow(), "Suggestions:".bright_white().bold() ); eprintln!( " {} This command requires elevated privileges", - "โ€ข".bright_blue() + "-".bright_blue() ); eprintln!( " {} Run with: {}", - "โ€ข".bright_blue(), + "-".bright_blue(), "sudo akon vpn on".bright_cyan() ); } @@ -144,22 +160,22 @@ fn print_error_suggestions(error: &VpnError) { // Generic suggestions for other errors eprintln!( "\n{} {}", - "๐Ÿ’ก".bright_yellow(), + "[TIP]".bright_yellow(), "Suggestions:".bright_white().bold() ); eprintln!( " {} Check system logs: {}", - "โ€ข".bright_blue(), + "-".bright_blue(), "journalctl -xe".bright_cyan() ); eprintln!( " {} Verify configuration: {}", - "โ€ข".bright_blue(), + "-".bright_blue(), "cat ~/.config/akon/config.toml".bright_cyan() ); eprintln!( " {} Try reconnecting: {}", - "โ€ข".bright_blue(), + "-".bright_blue(), "akon vpn on".bright_cyan() ); } @@ -615,7 +631,7 @@ pub async fn run_vpn_on(force: bool) -> Result<(), AkonError> { ); println!( "{} {}", - "๐Ÿ”„".bright_yellow(), + "[FORCE]".bright_yellow(), "Force reconnection requested - disconnecting and resetting..." .bright_yellow() ); @@ -645,24 +661,24 @@ pub async fn run_vpn_on(force: bool) -> Result<(), AkonError> { // Clean up state file (reset functionality) let _ = fs::remove_file(&state_path); - println!(" {} Cleared connection state", "โœ“".bright_green()); + println!(" {} Cleared connection state", "โœ“".bright_green()); info!("Force flag cleared state file (reset)"); } else { // Connection is already active - return early println!( "{} {}", - "โœ“".bright_green().bold(), + "[OK]".bright_green().bold(), "VPN is already connected".bright_green() ); if let Some(ip) = state.get("ip") { println!( - " {} {}", + " {} {}", "IP address:".bright_white(), ip.as_str().unwrap_or("unknown").bright_cyan().bold() ); } println!( - "\n{} {} to see full status", + "\n {} {} to see full status", "Run".dimmed(), "akon vpn status".bright_cyan() ); @@ -673,7 +689,7 @@ pub async fn run_vpn_on(force: bool) -> Result<(), AkonError> { info!("Found stale connection state (PID: {}), cleaning up", pid); println!( "{} {}", - "โš ".bright_yellow(), + "[WARN]".bright_yellow(), "Cleaning up stale connection...".dimmed() ); let _ = fs::remove_file(&state_path); @@ -710,7 +726,7 @@ pub async fn run_vpn_on(force: bool) -> Result<(), AkonError> { // Start connection println!( "{} {} {}", - "๐Ÿ”Œ".bright_cyan(), + ">>".bright_cyan(), "Connecting to VPN server:".bright_white().bold(), config.server.bright_yellow() ); @@ -730,7 +746,7 @@ pub async fn run_vpn_on(force: bool) -> Result<(), AkonError> { info!(pid = pid, "VPN process spawned"); } ConnectionEvent::Authenticating { message } => { - println!("{} {}", "๐Ÿ”".bright_magenta(), message.bright_white()); + println!("{} {}", "[AUTH]".bright_magenta(), message.bright_white()); info!(phase = "authentication", message = %message, "Authentication in progress"); } ConnectionEvent::F5SessionEstablished { .. } => { @@ -742,7 +758,7 @@ pub async fn run_vpn_on(force: bool) -> Result<(), AkonError> { info!(device = %device, ip = %ip, "TUN device configured"); } ConnectionEvent::Connected { ip, device } => { - println!("{} {}", "โœ“".bright_green().bold(), "VPN connection established".bright_green().bold()); + println!("{} {}", "[OK]".bright_green().bold(), "VPN connection established".bright_green().bold()); info!(ip = %ip, device = %device, "VPN connection fully established"); // Get PID from connector for state persistence @@ -784,7 +800,7 @@ pub async fn run_vpn_on(force: bool) -> Result<(), AkonError> { error!("Failed to spawn reconnection manager daemon: {}", e); warn!("Continuing without reconnection manager"); } else { - println!("{} {}", "๐Ÿ”„".bright_cyan(), "Reconnection manager started in background".dimmed()); + println!("{} {}", "[AUTO]".bright_cyan(), "Reconnection manager started in background".dimmed()); } } else { warn!("Cannot start reconnection manager: no PID available"); @@ -797,7 +813,7 @@ pub async fn run_vpn_on(force: bool) -> Result<(), AkonError> { } ConnectionEvent::Error { kind, raw_output } => { error!("VPN error: {} - {}", kind, raw_output); - eprintln!("{} {}", "โŒ".bright_red(), format!("Error: {}", kind).bright_red().bold()); + eprintln!("[ERROR] {}", format!("Error: {}", kind).bright_red().bold()); if !raw_output.is_empty() { eprintln!(" {} {}", "Details:".bright_yellow(), raw_output.dimmed()); } @@ -809,7 +825,7 @@ pub async fn run_vpn_on(force: bool) -> Result<(), AkonError> { } ConnectionEvent::Disconnected { reason } => { info!("VPN disconnected: {:?}", reason); - println!("{} VPN disconnected: {:?}", "โš ".bright_yellow(), reason); + println!("{} VPN disconnected: {:?}", "[WARN]".bright_yellow(), reason); return Ok(()); } ConnectionEvent::UnknownOutput { line } => { @@ -838,12 +854,16 @@ pub async fn run_vpn_off() -> Result<(), AkonError> { let state_path = state_file_path(); if !state_path.exists() { - println!("No active VPN connection found"); + println!( + "{} {}", + "[WARN]".bright_yellow(), + "No active VPN connection found".bright_white() + ); // Still check for and clean up any orphaned OpenConnect processes println!( "{} {}", - "๐Ÿงน".bright_yellow(), + "[CLEAN]".bright_yellow(), "Checking for orphaned OpenConnect processes...".bright_white() ); @@ -891,7 +911,7 @@ pub async fn run_vpn_off() -> Result<(), AkonError> { // Process exists, try graceful termination println!( "{} {} (PID: {})...", - "๐Ÿ”Œ".bright_cyan(), + ">>".bright_cyan(), "Disconnecting VPN".bright_white().bold(), pid.to_string().bright_yellow() ); @@ -928,8 +948,8 @@ pub async fn run_vpn_off() -> Result<(), AkonError> { // Process no longer exists println!( "{} {}", - "โœ“".bright_green().bold(), - "VPN disconnected gracefully".bright_green() + "[OK]".bright_green().bold(), + "VPN disconnected gracefully".bright_green().bold() ); info!("OpenConnect process terminated gracefully"); break; @@ -938,7 +958,7 @@ pub async fn run_vpn_off() -> Result<(), AkonError> { warn!("Graceful shutdown timeout, force killing process"); println!( "{} {}", - "โš ".bright_yellow(), + "[WARN]".bright_yellow(), "Process not responding, force killing...".bright_yellow() ); @@ -955,7 +975,7 @@ pub async fn run_vpn_off() -> Result<(), AkonError> { tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; println!( "{} {}", - "โœ“".bright_green().bold(), + "[OK]".bright_green().bold(), "VPN disconnected (forced)".bright_green() ); info!("OpenConnect process force-killed"); @@ -967,7 +987,7 @@ pub async fn run_vpn_off() -> Result<(), AkonError> { // Process not running, stale state (edge case from vpn-off-command.md) println!( "{} {}", - "โš ".bright_yellow(), + "[WARN]".bright_yellow(), "VPN process no longer running (stale state)".dimmed() ); info!(pid = pid.as_raw(), "Cleaning up stale connection state"); @@ -990,7 +1010,7 @@ pub async fn run_vpn_off() -> Result<(), AkonError> { // Comprehensive cleanup: Terminate any orphaned OpenConnect processes println!( "{} {}", - "๐Ÿงน".bright_yellow(), + "[CLEAN]".bright_yellow(), "Cleaning up any orphaned OpenConnect processes...".bright_white() ); @@ -1001,7 +1021,7 @@ pub async fn run_vpn_off() -> Result<(), AkonError> { println!( "{} {}", - "โœ“".bright_green(), + "[OK]".bright_green().bold(), "Disconnect complete".bright_green().bold() ); @@ -1016,9 +1036,16 @@ pub fn run_vpn_status() -> Result<(), AkonError> { if !state_path.exists() { println!( - "{} {}", + "{} {} - {}", "โ—".bright_red(), - "Status: Not connected".bright_white().bold() + "akon-vpn.service".bright_white().bold(), + "Akon VPN Connection".bright_white() + ); + println!( + " {} {} ({})", + "Active:".bright_white(), + "inactive (dead)".bright_red(), + "not connected".dimmed() ); std::process::exit(1); } @@ -1044,41 +1071,47 @@ pub fn run_vpn_status() -> Result<(), AkonError> { // T053: Check for Error state and suggest manual intervention if is_error { println!( - "{} {}", + "{} {} - {}", "โ—".bright_red(), - "Status: Error - Max reconnection attempts exceeded" - .bright_red() - .bold() + "akon-vpn.service".bright_white().bold(), + "Akon VPN Connection".bright_white() + ); + println!( + " {} {} ({})", + "Active:".bright_white(), + "failed".bright_red().bold(), + "max reconnection attempts exceeded".dimmed() ); if let Some(error_msg) = state.get("error").and_then(|e| e.as_str()) { println!( - " {} {}", - "Last error:".bright_white(), - error_msg.bright_yellow() + " {} {}", + "Error:".bright_white(), + error_msg.bright_red() ); } if let Some(attempts) = state.get("max_attempts").and_then(|a| a.as_u64()) { println!( - " {} Failed after {} reconnection attempts", - "โŒ".bright_red(), + " {} {} attempts", + "Retries:".bright_white(), attempts.to_string().bright_yellow() ); } + println!(); println!( - "\n{} {}", - "โš ".bright_yellow(), + "{} {}", + "[WARN]".bright_yellow(), "Manual intervention required:".bright_white().bold() ); println!( - " {} Run {} to disconnect", + " {} Run {} to disconnect", "1.".bright_yellow(), "akon vpn off".bright_cyan() ); println!( - " {} Run {} to reconnect with reset", + " {} Run {} to reconnect", "2.".bright_yellow(), "akon vpn on --force".bright_cyan() ); @@ -1096,13 +1129,15 @@ pub fn run_vpn_status() -> Result<(), AkonError> { let next_retry_at = state.get("next_retry_at").and_then(|n| n.as_u64()); println!( - "{} {}", + "{} {} - {}", "โ—".bright_yellow(), - "Status: Reconnecting".bright_yellow().bold() + "akon-vpn.service".bright_white().bold(), + "Akon VPN Connection".bright_white() ); println!( - " {} Attempt {} of {}", - "๐Ÿ”„".bright_yellow(), + " {} {} (attempt {}/{})", + "Active:".bright_white(), + "reconnecting".bright_yellow().bold(), attempt.to_string().bright_cyan(), max_attempts.to_string().bright_cyan() ); @@ -1114,16 +1149,16 @@ pub fn run_vpn_status() -> Result<(), AkonError> { .unwrap_or_else(|| "unknown".to_string()); println!( - " {} Next retry at {}", - "โฑ".dimmed(), + " {} {}", + "Retry:".bright_white(), retry_time.bright_cyan() ); } if let Some(ip) = state.get("last_ip") { println!( - " {} {}", - "Last known IP:".dimmed(), + " {} {}", + "Last IP:".dimmed(), ip.as_str().unwrap_or("unknown").bright_cyan() ); } @@ -1150,88 +1185,113 @@ pub fn run_vpn_status() -> Result<(), AkonError> { if !process_running { // Stale state println!( - "{} {}", + "{} {} - {}", "โ—".bright_yellow(), - "Status: Stale connection state".bright_yellow().bold() + "akon-vpn.service".bright_white().bold(), + "Akon VPN Connection".bright_white() ); println!( - " {} {}", - "โš ".bright_yellow(), - "Process no longer running".dimmed() + " {} {} ({})", + "Active:".bright_white(), + "inactive (stale)".bright_yellow().bold(), + "process no longer running".dimmed() ); if let Some(ip) = state.get("ip") { println!( - " {} {}", - "Last known IP:".dimmed(), + " {} {}", + "Last IP:".dimmed(), ip.as_str().unwrap_or("unknown").bright_cyan() ); } + println!(); println!( - "\n{} {} to clean up the stale state", - "Run".dimmed(), - "akon vpn off".bright_white().bold() + " {} Run {} to clean up stale state", + "[TIP]".bright_yellow(), + "akon vpn off".bright_cyan() ); std::process::exit(2); } // Connected and process running + // Get connected_at timestamp for display + let connected_at_info = state + .get("connected_at") + .and_then(|v| v.as_str()) + .and_then(|s| s.parse::>().ok()); + + let duration_str = connected_at_info.map(|connected_at| { + let now = Utc::now(); + let duration = now.signed_duration_since(connected_at); + if duration.num_days() > 0 { + format!("{} days", duration.num_days()) + } else if duration.num_hours() > 0 { + format!( + "{}h {}min", + duration.num_hours(), + duration.num_minutes() % 60 + ) + } else if duration.num_minutes() > 0 { + format!( + "{}min {}s", + duration.num_minutes(), + duration.num_seconds() % 60 + ) + } else { + format!("{}s", duration.num_seconds()) + } + }); + + let active_since = connected_at_info + .map(|dt| dt.with_timezone(&chrono::Local)) + .map(|dt| dt.format("%a %Y-%m-%d %H:%M:%S %Z").to_string()) + .unwrap_or_else(|| "unknown".to_string()); + println!( - "{} {}", + "{} {} - {}", "โ—".bright_green(), - "Status: Connected".bright_green().bold() + "akon-vpn.service".bright_white().bold(), + "Akon VPN Connection".bright_white() ); - if let Some(ip) = state.get("ip") { + + // Active line with duration + if let Some(dur) = &duration_str { println!( - " {} {}", - "IP address:".bright_white(), - ip.as_str().unwrap_or("unknown").bright_cyan().bold() + " {} {} since {}; {} ago", + "Active:".bright_white(), + "active (running)".bright_green().bold(), + active_since.bright_white(), + dur.bright_magenta() ); - } - if let Some(device) = state.get("device") { + } else { println!( - " {} {}", - "Device:".bright_white(), - device.as_str().unwrap_or("unknown").bright_cyan() + " {} {}", + "Active:".bright_white(), + "active (running)".bright_green().bold() ); } + if let Some(pid_num) = pid { println!( - " {} {}", - "Process ID:".bright_white(), + " {} {} (openconnect)", + "Main PID:".bright_white(), pid_num.to_string().bright_yellow() ); } - // Calculate and display duration - if let Some(connected_at_str) = state.get("connected_at").and_then(|v| v.as_str()) { - if let Ok(connected_at) = connected_at_str.parse::>() { - let now = Utc::now(); - let duration = now.signed_duration_since(connected_at); - - let duration_str = if duration.num_days() > 0 { - format!("{} days", duration.num_days()) - } else if duration.num_hours() > 0 { - format!("{} hours", duration.num_hours()) - } else if duration.num_minutes() > 0 { - format!("{} minutes", duration.num_minutes()) - } else { - format!("{} seconds", duration.num_seconds()) - }; + if let Some(ip) = state.get("ip") { + println!( + " {} {}", + "IP:".bright_white(), + ip.as_str().unwrap_or("unknown").bright_cyan().bold() + ); + } - println!( - " {} {}", - "Duration:".bright_white(), - duration_str.bright_magenta() - ); - println!( - " {} {}", - "Connected at:".bright_white(), - connected_at - .format("%Y-%m-%d %H:%M:%S UTC") - .to_string() - .dimmed() - ); - } + if let Some(device) = state.get("device") { + println!( + " {} {}", + "Device:".bright_white(), + device.as_str().unwrap_or("unknown").bright_cyan() + ); } Ok(()) diff --git a/src/main.rs b/src/main.rs index 12219b7..2a21ba1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,16 +27,16 @@ enum Commands { /// /// CONFIGURATION FIELDS: /// - /// โ€ข Server: VPN server hostname or IP address (e.g., vpn.example.com) + /// - Server: VPN server hostname or IP address (e.g., vpn.example.com) /// - /// โ€ข Username: Your VPN account username + /// - Username: Your VPN account username /// - /// โ€ข PIN: Numeric PIN for authentication (stored securely in keyring) + /// - PIN: Numeric PIN for authentication (stored securely in keyring) /// - /// โ€ข TOTP Secret: Base32-encoded secret for generating time-based one-time passwords + /// - TOTP Secret: Base32-encoded secret for generating time-based one-time passwords /// (stored securely in keyring) /// - /// โ€ข Protocol: VPN protocol type + /// - Protocol: VPN protocol type /// - anyconnect: Cisco AnyConnect SSL VPN /// - gp: Palo Alto Networks GlobalProtect /// - nc: Juniper Network Connect @@ -45,18 +45,18 @@ enum Commands { /// - fortinet: Fortinet FortiGate SSL VPN /// - array: Array Networks SSL VPN /// - /// โ€ข Timeout: Connection timeout in seconds (default: 30) + /// - Timeout: Connection timeout in seconds (default: 30) /// - /// โ€ข No DTLS: Disable DTLS and use only TCP/TLS (default: false) + /// - No DTLS: Disable DTLS and use only TCP/TLS (default: false) /// Use this if DTLS is blocked by firewall or causes connection issues /// - /// โ€ข Lazy Mode: When enabled, running 'akon' without arguments automatically + /// - Lazy Mode: When enabled, running 'akon' without arguments automatically /// connects to VPN. When disabled, you must use 'akon vpn on' (default: false) /// /// STORAGE: /// - /// โ€ข Config file: ~/.config/akon/config.toml (non-sensitive settings) - /// โ€ข Credentials: GNOME Keyring (PIN and TOTP secret, encrypted) + /// - Config file: ~/.config/akon/config.toml (non-sensitive settings) + /// - Credentials: GNOME Keyring (PIN and TOTP secret, encrypted) /// /// EXAMPLES: ///