vopono_core 1.0.2

Library code for running VPN connections in network namespaces
Documentation
mod openvpn;
mod wireguard;

use super::{ConfigurationChoice, OpenVpnProvider, Password, Provider, UiClient};
use crate::config::vpn::Protocol;
use anyhow::{Context, anyhow};
use reqwest::blocking::Client;
use serde::Deserialize;
use std::sync::OnceLock;

pub struct NordVPN {}

#[derive(Deserialize)]
pub(super) struct ServiceCredentials {
    pub username: String,
    pub password: String,
    pub nordlynx_private_key: Option<String>,
}

impl NordVPN {
    fn access_token(&self, uiclient: &dyn UiClient) -> anyhow::Result<String> {
        static ACCESS_TOKEN: OnceLock<String> = OnceLock::new();
        if let Some(token) = ACCESS_TOKEN.get() {
            return Ok(token.clone());
        }

        let token = match std::env::var("NORDVPN_ACCESS_TOKEN") {
            Ok(token) if !token.trim().is_empty() => token,
            _ => uiclient.get_password(Password {
                prompt: "NordVPN access token".to_string(),
                confirm: false,
            })?,
        };
        let token = token.trim().to_string();
        if token.is_empty() {
            return Err(anyhow!("NordVPN access token must not be empty"));
        }

        Ok(ACCESS_TOKEN.get_or_init(|| token).clone())
    }

    pub(super) fn service_credentials(
        &self,
        uiclient: &dyn UiClient,
    ) -> anyhow::Result<ServiceCredentials> {
        let token = self.access_token(uiclient)?;
        let credentials: ServiceCredentials = Client::new()
            .get("https://api.nordvpn.com/v1/users/services/credentials")
            .basic_auth("token", Some(token))
            .send()
            .context("Failed to request NordVPN service credentials")?
            .error_for_status()
            .context("NordVPN rejected the access token")?
            .json()
            .context("Failed to parse NordVPN service credentials")?;

        if credentials.username.is_empty() || credentials.password.is_empty() {
            return Err(anyhow!("NordVPN returned empty service credentials"));
        }

        Ok(credentials)
    }
}

impl Provider for NordVPN {
    fn alias(&self) -> String {
        "nordvpn".to_string()
    }

    fn alias_2char(&self) -> String {
        "nd".to_string()
    }

    fn default_protocol(&self) -> Protocol {
        Protocol::OpenVpn
    }
}