use crate::logging::{log_info, log_success, log_warn};
use crate::utils::command_exists;
use colored::Colorize;
use dialoguer::Input;
use regex::Regex;
use std::process::Stdio;
use tokio::process::Command;
const NORDVPN_BIN: &str = "nordvpn";
pub async fn run_nordvpn(args: Vec<String>, debug: bool) -> Result<(), String> {
if args.is_empty()
|| (args.len() == 1 && (args[0] == "help" || args[0] == "--help" || args[0] == "-h"))
{
return show_help().await;
}
if args[0] == "setup" {
return run_setup(debug).await;
}
run_passthrough(&args, debug).await
}
async fn show_help() -> Result<(), String> {
println!("{}", "NordVPN (xbp nordvpn)".bright_cyan().bold());
println!("{}", "─".repeat(50).dimmed());
println!();
println!(
" {} Run full setup (nordvpn, meshnet, SSH, optional Azure CLI)",
"setup".bright_cyan()
);
println!(
" {} Passthrough to nordvpn CLI",
"meshnet peer list".dimmed()
);
println!();
println!("{}", "Examples:".bright_blue());
println!(" xbp nordvpn setup");
println!(" xbp nordvpn meshnet peer list");
println!(" xbp nordvpn meshnet peer list -filter external");
println!(" xbp nordvpn meshnet invite send user@example.com");
println!(" xbp nordvpn meshnet invite list");
println!(" xbp nordvpn meshnet invite accept user@example.com");
println!();
Ok(())
}
async fn run_setup(debug: bool) -> Result<(), String> {
let _ = log_info("nordvpn", "Starting NordVPN setup", None).await;
ensure_nordvpn_installed(debug).await?;
login_with_token(debug).await?;
set_meshnet_on(debug).await?;
let nickname = ensure_meshnet_routing(debug).await?;
ensure_ssh_access(debug).await?;
maybe_install_azure_cli(debug).await?;
let _ = log_success(
"nordvpn",
&format!("Setup complete. This device: {}", nickname),
None,
)
.await;
let send_invite: String = Input::new()
.with_prompt("Send meshnet invite to email? (leave empty to skip)")
.allow_empty(true)
.interact_text()
.unwrap_or_default();
let send_invite = send_invite.trim();
if !send_invite.is_empty() {
let _ = log_info(
"nordvpn",
&format!("Sending invite to {}", send_invite),
None,
)
.await;
let out = Command::new(NORDVPN_BIN)
.args(["meshnet", "invite", "send", send_invite])
.output()
.await;
match out {
Ok(o) if o.status.success() => {
let _ = log_success("nordvpn", "Invite sent", None).await;
}
Ok(o) => {
let stderr = String::from_utf8_lossy(&o.stderr);
let _ = log_warn("nordvpn", &format!("Invite send failed: {}", stderr), None).await;
}
Err(e) => {
let _ = log_warn("nordvpn", &format!("Invite send failed: {}", e), None).await;
}
}
}
println!("\n{}", "Optional next steps:".bright_blue());
println!(" xbp nordvpn meshnet peer list -filter external");
println!(" xbp nordvpn meshnet invite list");
println!(" xbp nordvpn meshnet invite accept <email>");
Ok(())
}
async fn ensure_nordvpn_installed(_debug: bool) -> Result<(), String> {
if command_exists(NORDVPN_BIN) {
let _ = log_info("nordvpn", "NordVPN is already installed", None).await;
return Ok(());
}
#[cfg(target_os = "linux")]
{
let _ = log_info(
"nordvpn",
"NordVPN not found. Installing from snap...",
None,
)
.await;
if !command_exists("snap") {
return Err("snap is required to install NordVPN. Install snapd first.".to_string());
}
let output = Command::new("sudo")
.args(["snap", "install", "nordvpn"])
.output()
.await
.map_err(|e| format!("Failed to run snap install: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("snap install nordvpn failed: {}", stderr));
}
let _ = log_success("nordvpn", "NordVPN installed from snap", None).await;
Ok(())
}
#[cfg(not(target_os = "linux"))]
{
Err(
"NordVPN is not installed. On Linux, run: sudo snap install nordvpn\n\
On Windows/macOS, install NordVPN from the official website."
.to_string(),
)
}
}
async fn login_with_token(_debug: bool) -> Result<(), String> {
let token: String = Input::new()
.with_prompt("NordVPN login token")
.interact_text()
.map_err(|e| format!("Failed to read token: {}", e))?;
let token = token.trim();
if token.is_empty() {
return Err("Token cannot be empty".to_string());
}
let _ = log_info("nordvpn", "Logging in with token...", None).await;
let output = Command::new(NORDVPN_BIN)
.args(["login", "--token", token])
.output()
.await
.map_err(|e| format!("nordvpn login failed: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
return Err(format!(
"nordvpn login failed: {}{}",
stdout,
if stderr.is_empty() {
stderr.to_string()
} else {
format!("\n{}", stderr)
}
));
}
let _ = log_success("nordvpn", "Logged in successfully", None).await;
Ok(())
}
async fn set_meshnet_on(_debug: bool) -> Result<(), String> {
let _ = log_info("nordvpn", "Enabling meshnet...", None).await;
let output = Command::new(NORDVPN_BIN)
.args(["set", "meshnet", "on"])
.output()
.await
.map_err(|e| format!("nordvpn set meshnet on failed: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("nordvpn set meshnet on failed: {}", stderr));
}
let _ = log_success("nordvpn", "Meshnet enabled", None).await;
Ok(())
}
#[cfg(target_os = "linux")]
async fn ensure_ssh_access(_debug: bool) -> Result<(), String> {
let _ = log_info("nordvpn", "Enabling SSH access...", None).await;
let apt_update = Command::new("sudo")
.args(["apt", "update"])
.output()
.await
.map_err(|e| format!("apt update failed: {}", e))?;
if !apt_update.status.success() {
let stderr = String::from_utf8_lossy(&apt_update.stderr);
return Err(format!("apt update failed: {}", stderr));
}
let apt_install = Command::new("sudo")
.args(["apt", "install", "-y", "openssh-server"])
.output()
.await
.map_err(|e| format!("apt install openssh-server failed: {}", e))?;
if !apt_install.status.success() {
let stderr = String::from_utf8_lossy(&apt_install.stderr);
return Err(format!("apt install openssh-server failed: {}", stderr));
}
let _ = log_info("nordvpn", "Checking SSH service status...", None).await;
let _ = Command::new("sudo")
.args(["systemctl", "status", "ssh"])
.output()
.await;
let ufw = Command::new("sudo")
.args(["ufw", "allow", "ssh"])
.output()
.await;
if let Ok(o) = ufw {
if o.status.success() {
let _ = log_success("nordvpn", "SSH allowed in UFW", None).await;
} else {
let _ = log_warn(
"nordvpn",
"ufw allow ssh may have failed (ufw might not be active)",
None,
)
.await;
}
} else {
let _ = log_warn("nordvpn", "ufw not available or failed", None).await;
}
let _ = log_success("nordvpn", "SSH access enabled", None).await;
Ok(())
}
#[cfg(not(target_os = "linux"))]
async fn ensure_ssh_access(_debug: bool) -> Result<(), String> {
let _ = log_info("nordvpn", "SSH setup skipped (Linux only)", None).await;
Ok(())
}
#[cfg(target_os = "linux")]
async fn maybe_install_azure_cli(_debug: bool) -> Result<(), String> {
if command_exists("az") {
let _ = log_info("nordvpn", "Azure CLI already installed", None).await;
return Ok(());
}
let install: String = Input::new()
.with_prompt("Install Azure CLI? (y/N)")
.default("N".into())
.allow_empty(true)
.interact_text()
.unwrap_or_else(|_| "n".to_string());
if !install.trim().eq_ignore_ascii_case("y") && !install.trim().eq_ignore_ascii_case("yes") {
return Ok(());
}
let _ = log_info("nordvpn", "Installing Azure CLI...", None).await;
let output = Command::new("sh")
.args([
"-c",
"curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash",
])
.output()
.await
.map_err(|e| format!("Azure CLI install failed: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Azure CLI install failed: {}", stderr));
}
let _ = log_success("nordvpn", "Azure CLI installed", None).await;
Ok(())
}
#[cfg(not(target_os = "linux"))]
async fn maybe_install_azure_cli(_debug: bool) -> Result<(), String> {
Ok(())
}
async fn ensure_meshnet_routing(_debug: bool) -> Result<String, String> {
let _ = log_info("nordvpn", "Fetching meshnet peer list...", None).await;
let output = Command::new("sudo")
.args([NORDVPN_BIN, "meshnet", "peer", "list"])
.output()
.await
.map_err(|e| format!("nordvpn meshnet peer list failed: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("nordvpn meshnet peer list failed: {}", stderr));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let nickname = parse_this_device_nickname(&stdout)
.ok_or_else(|| format!("Could not parse 'This device' nickname from:\n{}", stdout))?;
let _ = log_info(
"nordvpn",
&format!("This device nickname: {}", nickname),
None,
)
.await;
println!(
"\n{}",
"Enabling meshnet peer routing for this device..."
.bright_blue()
.dimmed()
);
let allow_output = Command::new("sudo")
.args([
NORDVPN_BIN,
"meshnet",
"peer",
"routing",
"allow",
&nickname,
])
.output()
.await
.map_err(|e| format!("nordvpn meshnet peer routing allow failed: {}", e))?;
if !allow_output.status.success() {
let stderr = String::from_utf8_lossy(&allow_output.stderr);
let _ = log_warn(
"nordvpn",
&format!("peer routing allow may have failed: {}", stderr),
None,
)
.await;
} else {
let _ = log_success("nordvpn", "Meshnet peer routing enabled", None).await;
}
println!("\n{}", "Meshnet peers:".bright_blue().dimmed());
println!("{}", stdout);
Ok(nickname)
}
fn parse_this_device_nickname(output: &str) -> Option<String> {
let re = Regex::new(r"(?m)Nickname:\s*(\S+)").ok()?;
let mut found_this_device = false;
for line in output.lines() {
if line.trim().starts_with("This device:") {
found_this_device = true;
continue;
}
if found_this_device {
if let Some(cap) = re.captures(line) {
return Some(cap.get(1)?.as_str().to_string());
}
if line.trim().is_empty() {
break;
}
}
}
re.captures(output)
.map(|cap| cap.get(1).unwrap().as_str().to_string())
}
async fn run_passthrough(args: &[String], debug: bool) -> Result<(), String> {
if debug {
let _ = log_info(
"nordvpn",
&format!("Running: nordvpn {}", args.join(" ")),
None,
)
.await;
}
let mut cmd = Command::new(NORDVPN_BIN);
cmd.args(args);
let status = cmd
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.await
.map_err(|e| format!("Failed to run nordvpn: {}", e))?;
if !status.success() {
return Err(format!(
"nordvpn exited with code {}",
status.code().unwrap_or(-1)
));
}
Ok(())
}