mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-12 06:00:23 +02:00
feat: daemon support, general improvement, and preparation for Windows release
This commit is contained in:
@@ -0,0 +1,489 @@
|
||||
//! VPN configuration types and parsing.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
/// VPN-related errors
|
||||
#[derive(Error, Debug)]
|
||||
pub enum VpnError {
|
||||
#[error("Unknown VPN config format")]
|
||||
UnknownFormat,
|
||||
#[error("Invalid WireGuard config: {0}")]
|
||||
InvalidWireGuard(String),
|
||||
#[error("Invalid OpenVPN config: {0}")]
|
||||
InvalidOpenVpn(String),
|
||||
#[error("Storage error: {0}")]
|
||||
Storage(String),
|
||||
#[error("Connection error: {0}")]
|
||||
Connection(String),
|
||||
#[error("Encryption error: {0}")]
|
||||
Encryption(String),
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("VPN not found: {0}")]
|
||||
NotFound(String),
|
||||
#[error("Tunnel error: {0}")]
|
||||
Tunnel(String),
|
||||
}
|
||||
|
||||
/// The type of VPN configuration
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum VpnType {
|
||||
WireGuard,
|
||||
OpenVPN,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VpnType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
VpnType::WireGuard => write!(f, "WireGuard"),
|
||||
VpnType::OpenVPN => write!(f, "OpenVPN"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A stored VPN configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VpnConfig {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub vpn_type: VpnType,
|
||||
pub config_data: String, // Raw config content (encrypted at rest)
|
||||
pub created_at: i64,
|
||||
pub last_used: Option<i64>,
|
||||
}
|
||||
|
||||
/// Parsed WireGuard configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WireGuardConfig {
|
||||
pub private_key: String,
|
||||
pub address: String,
|
||||
pub dns: Option<String>,
|
||||
pub mtu: Option<u16>,
|
||||
pub peer_public_key: String,
|
||||
pub peer_endpoint: String,
|
||||
pub allowed_ips: Vec<String>,
|
||||
pub persistent_keepalive: Option<u16>,
|
||||
pub preshared_key: Option<String>,
|
||||
}
|
||||
|
||||
/// Parsed OpenVPN configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OpenVpnConfig {
|
||||
pub raw_config: String,
|
||||
pub remote_host: String,
|
||||
pub remote_port: u16,
|
||||
pub protocol: String, // "udp" or "tcp"
|
||||
pub dev_type: String, // "tun" or "tap"
|
||||
pub has_inline_ca: bool,
|
||||
pub has_inline_cert: bool,
|
||||
pub has_inline_key: bool,
|
||||
}
|
||||
|
||||
/// Result of importing a VPN configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VpnImportResult {
|
||||
pub success: bool,
|
||||
pub vpn_id: Option<String>,
|
||||
pub vpn_type: Option<VpnType>,
|
||||
pub name: String,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// VPN connection status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VpnStatus {
|
||||
pub connected: bool,
|
||||
pub vpn_id: String,
|
||||
pub connected_at: Option<i64>,
|
||||
pub bytes_sent: Option<u64>,
|
||||
pub bytes_received: Option<u64>,
|
||||
pub last_handshake: Option<i64>,
|
||||
}
|
||||
|
||||
/// Detect the VPN type from file content and filename
|
||||
pub fn detect_vpn_type(content: &str, filename: &str) -> Result<VpnType, VpnError> {
|
||||
let filename_lower = filename.to_lowercase();
|
||||
|
||||
// Check file extension first
|
||||
if filename_lower.ends_with(".conf") {
|
||||
// .conf could be WireGuard - check content
|
||||
if content.contains("[Interface]") && content.contains("[Peer]") {
|
||||
return Ok(VpnType::WireGuard);
|
||||
}
|
||||
}
|
||||
|
||||
if filename_lower.ends_with(".ovpn") {
|
||||
return Ok(VpnType::OpenVPN);
|
||||
}
|
||||
|
||||
// Check content patterns
|
||||
if content.contains("[Interface]") && content.contains("PrivateKey") && content.contains("[Peer]")
|
||||
{
|
||||
return Ok(VpnType::WireGuard);
|
||||
}
|
||||
|
||||
if content.contains("remote ") && (content.contains("client") || content.contains("dev tun")) {
|
||||
return Ok(VpnType::OpenVPN);
|
||||
}
|
||||
|
||||
Err(VpnError::UnknownFormat)
|
||||
}
|
||||
|
||||
/// Parse a WireGuard configuration file
|
||||
pub fn parse_wireguard_config(content: &str) -> Result<WireGuardConfig, VpnError> {
|
||||
let mut interface: HashMap<String, String> = HashMap::new();
|
||||
let mut peer: HashMap<String, String> = HashMap::new();
|
||||
let mut current_section: Option<&str> = None;
|
||||
|
||||
for line in content.lines() {
|
||||
let line = line.trim();
|
||||
|
||||
// Skip empty lines and comments
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for section headers
|
||||
if line == "[Interface]" {
|
||||
current_section = Some("interface");
|
||||
continue;
|
||||
}
|
||||
if line == "[Peer]" {
|
||||
current_section = Some("peer");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse key-value pairs
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
let key = key.trim().to_string();
|
||||
let value = value.trim().to_string();
|
||||
|
||||
match current_section {
|
||||
Some("interface") => {
|
||||
interface.insert(key, value);
|
||||
}
|
||||
Some("peer") => {
|
||||
peer.insert(key, value);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
let private_key = interface
|
||||
.get("PrivateKey")
|
||||
.ok_or_else(|| VpnError::InvalidWireGuard("Missing PrivateKey in [Interface]".to_string()))?
|
||||
.clone();
|
||||
|
||||
let address = interface
|
||||
.get("Address")
|
||||
.ok_or_else(|| VpnError::InvalidWireGuard("Missing Address in [Interface]".to_string()))?
|
||||
.clone();
|
||||
|
||||
let peer_public_key = peer
|
||||
.get("PublicKey")
|
||||
.ok_or_else(|| VpnError::InvalidWireGuard("Missing PublicKey in [Peer]".to_string()))?
|
||||
.clone();
|
||||
|
||||
let peer_endpoint = peer
|
||||
.get("Endpoint")
|
||||
.ok_or_else(|| VpnError::InvalidWireGuard("Missing Endpoint in [Peer]".to_string()))?
|
||||
.clone();
|
||||
|
||||
let allowed_ips = peer
|
||||
.get("AllowedIPs")
|
||||
.map(|s| s.split(',').map(|ip| ip.trim().to_string()).collect())
|
||||
.unwrap_or_else(|| vec!["0.0.0.0/0".to_string()]);
|
||||
|
||||
let persistent_keepalive = peer.get("PersistentKeepalive").and_then(|s| s.parse().ok());
|
||||
|
||||
let dns = interface.get("DNS").cloned();
|
||||
let mtu = interface.get("MTU").and_then(|s| s.parse().ok());
|
||||
let preshared_key = peer.get("PresharedKey").cloned();
|
||||
|
||||
Ok(WireGuardConfig {
|
||||
private_key,
|
||||
address,
|
||||
dns,
|
||||
mtu,
|
||||
peer_public_key,
|
||||
peer_endpoint,
|
||||
allowed_ips,
|
||||
persistent_keepalive,
|
||||
preshared_key,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse an OpenVPN configuration file
|
||||
pub fn parse_openvpn_config(content: &str) -> Result<OpenVpnConfig, VpnError> {
|
||||
let mut remote_host = String::new();
|
||||
let mut remote_port: u16 = 1194; // Default OpenVPN port
|
||||
let mut protocol = "udp".to_string();
|
||||
let mut dev_type = "tun".to_string();
|
||||
|
||||
let has_inline_ca = content.contains("<ca>") && content.contains("</ca>");
|
||||
let has_inline_cert = content.contains("<cert>") && content.contains("</cert>");
|
||||
let has_inline_key = content.contains("<key>") && content.contains("</key>");
|
||||
|
||||
for line in content.lines() {
|
||||
let line = line.trim();
|
||||
|
||||
// Skip empty lines and comments
|
||||
if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match parts[0] {
|
||||
"remote" => {
|
||||
if parts.len() >= 2 {
|
||||
remote_host = parts[1].to_string();
|
||||
}
|
||||
if parts.len() >= 3 {
|
||||
if let Ok(port) = parts[2].parse() {
|
||||
remote_port = port;
|
||||
}
|
||||
}
|
||||
if parts.len() >= 4 {
|
||||
protocol = parts[3].to_string();
|
||||
}
|
||||
}
|
||||
"proto" => {
|
||||
if parts.len() >= 2 {
|
||||
protocol = parts[1].to_string();
|
||||
}
|
||||
}
|
||||
"port" => {
|
||||
if parts.len() >= 2 {
|
||||
if let Ok(port) = parts[1].parse() {
|
||||
remote_port = port;
|
||||
}
|
||||
}
|
||||
}
|
||||
"dev" => {
|
||||
if parts.len() >= 2 {
|
||||
dev_type = parts[1].to_string();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if remote_host.is_empty() {
|
||||
return Err(VpnError::InvalidOpenVpn(
|
||||
"Missing 'remote' directive".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(OpenVpnConfig {
|
||||
raw_config: content.to_string(),
|
||||
remote_host,
|
||||
remote_port,
|
||||
protocol,
|
||||
dev_type,
|
||||
has_inline_ca,
|
||||
has_inline_cert,
|
||||
has_inline_key,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_detect_wireguard_by_extension() {
|
||||
let content = "[Interface]\nPrivateKey = test\n[Peer]\nPublicKey = test";
|
||||
assert_eq!(
|
||||
detect_vpn_type(content, "test.conf").unwrap(),
|
||||
VpnType::WireGuard
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_openvpn_by_extension() {
|
||||
let content = "client\nremote vpn.example.com 1194";
|
||||
assert_eq!(
|
||||
detect_vpn_type(content, "test.ovpn").unwrap(),
|
||||
VpnType::OpenVPN
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_wireguard_by_content() {
|
||||
let content = "[Interface]\nPrivateKey = testkey123\nAddress = 10.0.0.2/24\n\n[Peer]\nPublicKey = peerkey456\nEndpoint = vpn.example.com:51820";
|
||||
assert_eq!(
|
||||
detect_vpn_type(content, "config").unwrap(),
|
||||
VpnType::WireGuard
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_openvpn_by_content() {
|
||||
let content = "client\ndev tun\nproto udp\nremote vpn.example.com 1194";
|
||||
assert_eq!(
|
||||
detect_vpn_type(content, "config").unwrap(),
|
||||
VpnType::OpenVPN
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_unknown_format() {
|
||||
let content = "random text that is not a vpn config";
|
||||
assert!(detect_vpn_type(content, "random.txt").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_wireguard_config() {
|
||||
let content = r#"
|
||||
[Interface]
|
||||
PrivateKey = WGTestPrivateKey123456789012345678901234567890
|
||||
Address = 10.0.0.2/24
|
||||
DNS = 1.1.1.1
|
||||
MTU = 1420
|
||||
|
||||
[Peer]
|
||||
PublicKey = WGTestPublicKey1234567890123456789012345678901
|
||||
Endpoint = vpn.example.com:51820
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
PersistentKeepalive = 25
|
||||
"#;
|
||||
|
||||
let config = parse_wireguard_config(content).unwrap();
|
||||
assert_eq!(
|
||||
config.private_key,
|
||||
"WGTestPrivateKey123456789012345678901234567890"
|
||||
);
|
||||
assert_eq!(config.address, "10.0.0.2/24");
|
||||
assert_eq!(config.dns, Some("1.1.1.1".to_string()));
|
||||
assert_eq!(config.mtu, Some(1420));
|
||||
assert_eq!(
|
||||
config.peer_public_key,
|
||||
"WGTestPublicKey1234567890123456789012345678901"
|
||||
);
|
||||
assert_eq!(config.peer_endpoint, "vpn.example.com:51820");
|
||||
assert_eq!(config.allowed_ips, vec!["0.0.0.0/0", "::/0"]);
|
||||
assert_eq!(config.persistent_keepalive, Some(25));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_wireguard_config_minimal() {
|
||||
let content = r#"
|
||||
[Interface]
|
||||
PrivateKey = minimalkey
|
||||
Address = 10.0.0.2/32
|
||||
|
||||
[Peer]
|
||||
PublicKey = peerpubkey
|
||||
Endpoint = 1.2.3.4:51820
|
||||
"#;
|
||||
|
||||
let config = parse_wireguard_config(content).unwrap();
|
||||
assert_eq!(config.private_key, "minimalkey");
|
||||
assert_eq!(config.address, "10.0.0.2/32");
|
||||
assert!(config.dns.is_none());
|
||||
assert!(config.mtu.is_none());
|
||||
assert_eq!(config.peer_public_key, "peerpubkey");
|
||||
assert_eq!(config.peer_endpoint, "1.2.3.4:51820");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_wireguard_missing_private_key() {
|
||||
let content = r#"
|
||||
[Interface]
|
||||
Address = 10.0.0.2/24
|
||||
|
||||
[Peer]
|
||||
PublicKey = key
|
||||
Endpoint = 1.2.3.4:51820
|
||||
"#;
|
||||
|
||||
let result = parse_wireguard_config(content);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("PrivateKey"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_openvpn_config() {
|
||||
let content = r#"
|
||||
client
|
||||
dev tun
|
||||
proto udp
|
||||
remote vpn.example.com 1194
|
||||
resolv-retry infinite
|
||||
nobind
|
||||
persist-key
|
||||
persist-tun
|
||||
<ca>
|
||||
-----BEGIN CERTIFICATE-----
|
||||
...certificate data...
|
||||
-----END CERTIFICATE-----
|
||||
</ca>
|
||||
<cert>
|
||||
-----BEGIN CERTIFICATE-----
|
||||
...cert data...
|
||||
-----END CERTIFICATE-----
|
||||
</cert>
|
||||
<key>
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
...key data...
|
||||
-----END PRIVATE KEY-----
|
||||
</key>
|
||||
"#;
|
||||
|
||||
let config = parse_openvpn_config(content).unwrap();
|
||||
assert_eq!(config.remote_host, "vpn.example.com");
|
||||
assert_eq!(config.remote_port, 1194);
|
||||
assert_eq!(config.protocol, "udp");
|
||||
assert_eq!(config.dev_type, "tun");
|
||||
assert!(config.has_inline_ca);
|
||||
assert!(config.has_inline_cert);
|
||||
assert!(config.has_inline_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_openvpn_config_minimal() {
|
||||
let content = r#"
|
||||
client
|
||||
remote vpn.example.com
|
||||
"#;
|
||||
|
||||
let config = parse_openvpn_config(content).unwrap();
|
||||
assert_eq!(config.remote_host, "vpn.example.com");
|
||||
assert_eq!(config.remote_port, 1194); // Default
|
||||
assert_eq!(config.protocol, "udp"); // Default
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_openvpn_config_with_port_and_proto() {
|
||||
let content = r#"
|
||||
client
|
||||
remote vpn.example.com 443 tcp
|
||||
"#;
|
||||
|
||||
let config = parse_openvpn_config(content).unwrap();
|
||||
assert_eq!(config.remote_host, "vpn.example.com");
|
||||
assert_eq!(config.remote_port, 443);
|
||||
assert_eq!(config.protocol, "tcp");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_openvpn_missing_remote() {
|
||||
let content = r#"
|
||||
client
|
||||
dev tun
|
||||
proto udp
|
||||
"#;
|
||||
|
||||
let result = parse_openvpn_config(content);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("remote"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//! VPN support module for WireGuard and OpenVPN configurations.
|
||||
//!
|
||||
//! This module provides:
|
||||
//! - VPN config parsing (WireGuard .conf and OpenVPN .ovpn files)
|
||||
//! - Encrypted storage for VPN configurations
|
||||
//! - Tunnel management with userspace WireGuard (boringtun) and OpenVPN process management
|
||||
|
||||
mod config;
|
||||
mod openvpn;
|
||||
mod storage;
|
||||
mod tunnel;
|
||||
mod wireguard;
|
||||
|
||||
pub use config::{
|
||||
detect_vpn_type, parse_openvpn_config, parse_wireguard_config, OpenVpnConfig, VpnConfig,
|
||||
VpnError, VpnImportResult, VpnStatus, VpnType, WireGuardConfig,
|
||||
};
|
||||
pub use openvpn::OpenVpnTunnel;
|
||||
pub use storage::VpnStorage;
|
||||
pub use tunnel::{TunnelManager, VpnTunnel};
|
||||
pub use wireguard::WireGuardTunnel;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Global VPN storage instance
|
||||
pub static VPN_STORAGE: Lazy<Mutex<VpnStorage>> = Lazy::new(|| Mutex::new(VpnStorage::new()));
|
||||
|
||||
/// Global tunnel manager instance
|
||||
pub static TUNNEL_MANAGER: Lazy<tokio::sync::Mutex<TunnelManager>> =
|
||||
Lazy::new(|| tokio::sync::Mutex::new(TunnelManager::new()));
|
||||
@@ -0,0 +1,343 @@
|
||||
//! OpenVPN tunnel implementation using system openvpn binary.
|
||||
|
||||
use super::config::{OpenVpnConfig, VpnError, VpnStatus};
|
||||
use super::tunnel::VpnTunnel;
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
/// OpenVPN tunnel implementation
|
||||
pub struct OpenVpnTunnel {
|
||||
vpn_id: String,
|
||||
config: OpenVpnConfig,
|
||||
process: Arc<Mutex<Option<Child>>>,
|
||||
config_file: Option<NamedTempFile>,
|
||||
connected: AtomicBool,
|
||||
connected_at: Option<i64>,
|
||||
bytes_sent: AtomicU64,
|
||||
bytes_received: AtomicU64,
|
||||
}
|
||||
|
||||
impl OpenVpnTunnel {
|
||||
/// Create a new OpenVPN tunnel
|
||||
pub fn new(vpn_id: String, config: OpenVpnConfig) -> Self {
|
||||
Self {
|
||||
vpn_id,
|
||||
config,
|
||||
process: Arc::new(Mutex::new(None)),
|
||||
config_file: None,
|
||||
connected: AtomicBool::new(false),
|
||||
connected_at: None,
|
||||
bytes_sent: AtomicU64::new(0),
|
||||
bytes_received: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the openvpn binary
|
||||
fn find_openvpn_binary() -> Result<PathBuf, VpnError> {
|
||||
// Check common locations
|
||||
let locations = [
|
||||
"/usr/sbin/openvpn",
|
||||
"/usr/local/sbin/openvpn",
|
||||
"/opt/homebrew/bin/openvpn",
|
||||
"/usr/bin/openvpn",
|
||||
"C:\\Program Files\\OpenVPN\\bin\\openvpn.exe",
|
||||
"C:\\Program Files (x86)\\OpenVPN\\bin\\openvpn.exe",
|
||||
];
|
||||
|
||||
for loc in &locations {
|
||||
let path = PathBuf::from(loc);
|
||||
if path.exists() {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find via which/where command
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if let Ok(output) = Command::new("which").arg("openvpn").output() {
|
||||
if output.status.success() {
|
||||
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if !path.is_empty() {
|
||||
return Ok(PathBuf::from(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if let Ok(output) = Command::new("where").arg("openvpn").output() {
|
||||
if output.status.success() {
|
||||
let path = String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
if !path.is_empty() {
|
||||
return Ok(PathBuf::from(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(VpnError::Connection(
|
||||
"OpenVPN binary not found. Please install OpenVPN.".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Write config to temporary file
|
||||
fn write_config_file(&mut self) -> Result<PathBuf, VpnError> {
|
||||
let temp_file =
|
||||
NamedTempFile::new().map_err(|e| VpnError::Io(std::io::Error::other(e.to_string())))?;
|
||||
|
||||
std::fs::write(temp_file.path(), &self.config.raw_config).map_err(VpnError::Io)?;
|
||||
|
||||
let path = temp_file.path().to_path_buf();
|
||||
self.config_file = Some(temp_file);
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Start the OpenVPN process
|
||||
async fn start_process(&mut self) -> Result<(), VpnError> {
|
||||
let openvpn_bin = Self::find_openvpn_binary()?;
|
||||
let config_path = self.write_config_file()?;
|
||||
|
||||
log::info!(
|
||||
"[vpn] Starting OpenVPN with config: {}",
|
||||
config_path.display()
|
||||
);
|
||||
|
||||
// Build command with common options
|
||||
let mut cmd = Command::new(&openvpn_bin);
|
||||
cmd
|
||||
.arg("--config")
|
||||
.arg(&config_path)
|
||||
.arg("--verb")
|
||||
.arg("3") // Verbosity level
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
// On Unix, try to avoid requiring root if possible
|
||||
#[cfg(unix)]
|
||||
{
|
||||
cmd.arg("--script-security").arg("2");
|
||||
}
|
||||
|
||||
let child = cmd
|
||||
.spawn()
|
||||
.map_err(|e| VpnError::Connection(format!("Failed to start OpenVPN: {e}")))?;
|
||||
|
||||
*self.process.lock().await = Some(child);
|
||||
|
||||
// Wait a bit and check if process is still running
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
let mut process_guard = self.process.lock().await;
|
||||
if let Some(ref mut child) = *process_guard {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
// Process exited early
|
||||
let mut error_msg = format!("OpenVPN exited with status: {status}");
|
||||
|
||||
// Try to get stderr output
|
||||
if let Some(stderr) = child.stderr.take() {
|
||||
let reader = BufReader::new(stderr);
|
||||
let lines: Vec<String> = reader.lines().map_while(Result::ok).take(5).collect();
|
||||
if !lines.is_empty() {
|
||||
error_msg.push_str(&format!("\nError: {}", lines.join("\n")));
|
||||
}
|
||||
}
|
||||
|
||||
return Err(VpnError::Connection(error_msg));
|
||||
}
|
||||
Ok(None) => {
|
||||
// Still running, good
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(VpnError::Connection(format!(
|
||||
"Failed to check process status: {e}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Kill the OpenVPN process
|
||||
async fn kill_process(&mut self) -> Result<(), VpnError> {
|
||||
let mut process_guard = self.process.lock().await;
|
||||
|
||||
if let Some(mut child) = process_guard.take() {
|
||||
// Try graceful shutdown first
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use nix::sys::signal::{kill, Signal};
|
||||
use nix::unistd::Pid;
|
||||
|
||||
if let Ok(pid) = child.id().try_into() {
|
||||
let _ = kill(Pid::from_raw(pid), Signal::SIGTERM);
|
||||
// Wait a bit for graceful shutdown
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Force kill if still running
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
// Clean up config file
|
||||
self.config_file = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VpnTunnel for OpenVpnTunnel {
|
||||
async fn connect(&mut self) -> Result<(), VpnError> {
|
||||
if self.connected.load(Ordering::Relaxed) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Start OpenVPN process
|
||||
self.start_process().await?;
|
||||
|
||||
// Wait for connection to be established
|
||||
// Note: In a real implementation, we'd monitor the OpenVPN management interface
|
||||
// For now, we assume success if the process starts and runs for a bit
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
// Check if process is still running
|
||||
let process_guard = self.process.lock().await;
|
||||
if let Some(ref child) = *process_guard {
|
||||
let id = child.id();
|
||||
if id > 0 {
|
||||
self.connected.store(true, Ordering::Release);
|
||||
self.connected_at = Some(Utc::now().timestamp());
|
||||
log::info!("[vpn] OpenVPN tunnel {} connected (PID: {id})", self.vpn_id);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(VpnError::Connection(
|
||||
"Failed to establish OpenVPN connection".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn disconnect(&mut self) -> Result<(), VpnError> {
|
||||
if !self.connected.load(Ordering::Relaxed) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.kill_process().await?;
|
||||
|
||||
self.connected.store(false, Ordering::Release);
|
||||
self.connected_at = None;
|
||||
|
||||
log::info!("[vpn] OpenVPN tunnel {} disconnected", self.vpn_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_connected(&self) -> bool {
|
||||
self.connected.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
fn vpn_id(&self) -> &str {
|
||||
&self.vpn_id
|
||||
}
|
||||
|
||||
fn get_status(&self) -> VpnStatus {
|
||||
VpnStatus {
|
||||
connected: self.is_connected(),
|
||||
vpn_id: self.vpn_id.clone(),
|
||||
connected_at: self.connected_at,
|
||||
bytes_sent: Some(self.bytes_sent.load(Ordering::Relaxed)),
|
||||
bytes_received: Some(self.bytes_received.load(Ordering::Relaxed)),
|
||||
last_handshake: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn bytes_sent(&self) -> u64 {
|
||||
self.bytes_sent.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn bytes_received(&self) -> u64 {
|
||||
self.bytes_received.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for OpenVpnTunnel {
|
||||
fn drop(&mut self) {
|
||||
// Clean up process on drop (synchronously)
|
||||
if let Ok(mut guard) = self.process.try_lock() {
|
||||
if let Some(mut child) = guard.take() {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_config() -> OpenVpnConfig {
|
||||
OpenVpnConfig {
|
||||
raw_config: "client\nremote test.example.com 1194\ndev tun".to_string(),
|
||||
remote_host: "test.example.com".to_string(),
|
||||
remote_port: 1194,
|
||||
protocol: "udp".to_string(),
|
||||
dev_type: "tun".to_string(),
|
||||
has_inline_ca: false,
|
||||
has_inline_cert: false,
|
||||
has_inline_key: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_openvpn_tunnel_creation() {
|
||||
let config = create_test_config();
|
||||
let tunnel = OpenVpnTunnel::new("test-ovpn-1".to_string(), config);
|
||||
|
||||
assert_eq!(tunnel.vpn_id(), "test-ovpn-1");
|
||||
assert!(!tunnel.is_connected());
|
||||
assert_eq!(tunnel.bytes_sent(), 0);
|
||||
assert_eq!(tunnel.bytes_received(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_openvpn_status() {
|
||||
let config = create_test_config();
|
||||
let tunnel = OpenVpnTunnel::new("test-ovpn-2".to_string(), config);
|
||||
|
||||
let status = tunnel.get_status();
|
||||
assert!(!status.connected);
|
||||
assert_eq!(status.vpn_id, "test-ovpn-2");
|
||||
assert!(status.connected_at.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_openvpn_binary_format() {
|
||||
// This test just checks that the function doesn't panic
|
||||
// It may or may not find openvpn depending on the system
|
||||
let result = OpenVpnTunnel::find_openvpn_binary();
|
||||
// Just check that it returns a valid Result
|
||||
match result {
|
||||
Ok(path) => assert!(!path.as_os_str().is_empty()),
|
||||
Err(e) => assert!(e.to_string().contains("not found")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
//! Encrypted storage for VPN configurations.
|
||||
|
||||
use super::config::{VpnConfig, VpnError, VpnType};
|
||||
use aes_gcm::{
|
||||
aead::{Aead, KeyInit},
|
||||
Aes256Gcm, Nonce,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Storage format version for migration support
|
||||
const STORAGE_VERSION: u32 = 1;
|
||||
|
||||
/// Stored VPN configs container
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct VpnStorageData {
|
||||
version: u32,
|
||||
configs: Vec<StoredVpnConfig>,
|
||||
}
|
||||
|
||||
/// Encrypted VPN config as stored on disk
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct StoredVpnConfig {
|
||||
id: String,
|
||||
name: String,
|
||||
vpn_type: VpnType,
|
||||
encrypted_data: String, // Base64 encoded encrypted config
|
||||
nonce: String, // Base64 encoded nonce
|
||||
created_at: i64,
|
||||
last_used: Option<i64>,
|
||||
}
|
||||
|
||||
/// VPN storage manager with encryption
|
||||
pub struct VpnStorage {
|
||||
storage_path: PathBuf,
|
||||
encryption_key: [u8; 32],
|
||||
}
|
||||
|
||||
impl Default for VpnStorage {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl VpnStorage {
|
||||
/// Create a new VPN storage manager
|
||||
pub fn new() -> Self {
|
||||
let storage_path = Self::get_storage_path();
|
||||
let encryption_key = Self::get_or_create_key();
|
||||
|
||||
Self {
|
||||
storage_path,
|
||||
encryption_key,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the storage file path
|
||||
fn get_storage_path() -> PathBuf {
|
||||
let data_dir = directories::ProjectDirs::from("com", "donut", "donutbrowser")
|
||||
.map(|dirs| dirs.data_local_dir().to_path_buf())
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
|
||||
if !data_dir.exists() {
|
||||
let _ = fs::create_dir_all(&data_dir);
|
||||
}
|
||||
|
||||
data_dir.join("vpn_configs.json")
|
||||
}
|
||||
|
||||
/// Get or create the encryption key
|
||||
fn get_or_create_key() -> [u8; 32] {
|
||||
let key_path = directories::ProjectDirs::from("com", "donut", "donutbrowser")
|
||||
.map(|dirs| dirs.data_local_dir().join(".vpn_key"))
|
||||
.unwrap_or_else(|| PathBuf::from(".vpn_key"));
|
||||
|
||||
if key_path.exists() {
|
||||
if let Ok(key_data) = fs::read(&key_path) {
|
||||
if key_data.len() == 32 {
|
||||
let mut key = [0u8; 32];
|
||||
key.copy_from_slice(&key_data);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a new key
|
||||
let key: [u8; 32] = rand::rng().random();
|
||||
let _ = fs::write(&key_path, key);
|
||||
|
||||
// Set restrictive permissions on Unix
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = fs::set_permissions(&key_path, fs::Permissions::from_mode(0o600));
|
||||
}
|
||||
|
||||
key
|
||||
}
|
||||
|
||||
/// Load storage data from disk
|
||||
fn load_storage(&self) -> Result<VpnStorageData, VpnError> {
|
||||
if !self.storage_path.exists() {
|
||||
return Ok(VpnStorageData {
|
||||
version: STORAGE_VERSION,
|
||||
configs: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&self.storage_path)
|
||||
.map_err(|e| VpnError::Storage(format!("Failed to read storage file: {e}")))?;
|
||||
|
||||
serde_json::from_str(&content)
|
||||
.map_err(|e| VpnError::Storage(format!("Failed to parse storage file: {e}")))
|
||||
}
|
||||
|
||||
/// Save storage data to disk
|
||||
fn save_storage(&self, data: &VpnStorageData) -> Result<(), VpnError> {
|
||||
let content = serde_json::to_string_pretty(data)
|
||||
.map_err(|e| VpnError::Storage(format!("Failed to serialize storage: {e}")))?;
|
||||
|
||||
fs::write(&self.storage_path, content)
|
||||
.map_err(|e| VpnError::Storage(format!("Failed to write storage file: {e}")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encrypt config data
|
||||
fn encrypt(&self, data: &str) -> Result<(String, String), VpnError> {
|
||||
let cipher = Aes256Gcm::new_from_slice(&self.encryption_key)
|
||||
.map_err(|e| VpnError::Encryption(format!("Failed to create cipher: {e}")))?;
|
||||
|
||||
let nonce_bytes: [u8; 12] = rand::rng().random();
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
let ciphertext = cipher
|
||||
.encrypt(nonce, data.as_bytes())
|
||||
.map_err(|e| VpnError::Encryption(format!("Encryption failed: {e}")))?;
|
||||
|
||||
Ok((
|
||||
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &ciphertext),
|
||||
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, nonce_bytes),
|
||||
))
|
||||
}
|
||||
|
||||
/// Decrypt config data
|
||||
fn decrypt(&self, encrypted_data: &str, nonce_str: &str) -> Result<String, VpnError> {
|
||||
let cipher = Aes256Gcm::new_from_slice(&self.encryption_key)
|
||||
.map_err(|e| VpnError::Encryption(format!("Failed to create cipher: {e}")))?;
|
||||
|
||||
let ciphertext =
|
||||
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encrypted_data)
|
||||
.map_err(|e| VpnError::Encryption(format!("Failed to decode ciphertext: {e}")))?;
|
||||
|
||||
let nonce_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, nonce_str)
|
||||
.map_err(|e| VpnError::Encryption(format!("Failed to decode nonce: {e}")))?;
|
||||
|
||||
if nonce_bytes.len() != 12 {
|
||||
return Err(VpnError::Encryption("Invalid nonce length".to_string()));
|
||||
}
|
||||
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
let plaintext = cipher
|
||||
.decrypt(nonce, ciphertext.as_ref())
|
||||
.map_err(|e| VpnError::Encryption(format!("Decryption failed: {e}")))?;
|
||||
|
||||
String::from_utf8(plaintext)
|
||||
.map_err(|e| VpnError::Encryption(format!("Failed to decode plaintext: {e}")))
|
||||
}
|
||||
|
||||
/// Save a VPN configuration
|
||||
pub fn save_config(&self, config: &VpnConfig) -> Result<(), VpnError> {
|
||||
let mut storage = self.load_storage()?;
|
||||
|
||||
// Encrypt the config data
|
||||
let (encrypted_data, nonce) = self.encrypt(&config.config_data)?;
|
||||
|
||||
let stored = StoredVpnConfig {
|
||||
id: config.id.clone(),
|
||||
name: config.name.clone(),
|
||||
vpn_type: config.vpn_type,
|
||||
encrypted_data,
|
||||
nonce,
|
||||
created_at: config.created_at,
|
||||
last_used: config.last_used,
|
||||
};
|
||||
|
||||
// Update existing or add new
|
||||
if let Some(pos) = storage.configs.iter().position(|c| c.id == config.id) {
|
||||
storage.configs[pos] = stored;
|
||||
} else {
|
||||
storage.configs.push(stored);
|
||||
}
|
||||
|
||||
self.save_storage(&storage)
|
||||
}
|
||||
|
||||
/// Load a VPN configuration by ID
|
||||
pub fn load_config(&self, id: &str) -> Result<VpnConfig, VpnError> {
|
||||
let storage = self.load_storage()?;
|
||||
|
||||
let stored = storage
|
||||
.configs
|
||||
.iter()
|
||||
.find(|c| c.id == id)
|
||||
.ok_or_else(|| VpnError::NotFound(id.to_string()))?;
|
||||
|
||||
let config_data = self.decrypt(&stored.encrypted_data, &stored.nonce)?;
|
||||
|
||||
Ok(VpnConfig {
|
||||
id: stored.id.clone(),
|
||||
name: stored.name.clone(),
|
||||
vpn_type: stored.vpn_type,
|
||||
config_data,
|
||||
created_at: stored.created_at,
|
||||
last_used: stored.last_used,
|
||||
})
|
||||
}
|
||||
|
||||
/// List all VPN configurations (without decrypted config data)
|
||||
pub fn list_configs(&self) -> Result<Vec<VpnConfig>, VpnError> {
|
||||
let storage = self.load_storage()?;
|
||||
|
||||
Ok(
|
||||
storage
|
||||
.configs
|
||||
.iter()
|
||||
.map(|stored| VpnConfig {
|
||||
id: stored.id.clone(),
|
||||
name: stored.name.clone(),
|
||||
vpn_type: stored.vpn_type,
|
||||
config_data: String::new(), // Don't include config data in list
|
||||
created_at: stored.created_at,
|
||||
last_used: stored.last_used,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Delete a VPN configuration
|
||||
pub fn delete_config(&self, id: &str) -> Result<(), VpnError> {
|
||||
let mut storage = self.load_storage()?;
|
||||
|
||||
let initial_len = storage.configs.len();
|
||||
storage.configs.retain(|c| c.id != id);
|
||||
|
||||
if storage.configs.len() == initial_len {
|
||||
return Err(VpnError::NotFound(id.to_string()));
|
||||
}
|
||||
|
||||
self.save_storage(&storage)
|
||||
}
|
||||
|
||||
/// Update last_used timestamp
|
||||
pub fn update_last_used(&self, id: &str) -> Result<(), VpnError> {
|
||||
let mut storage = self.load_storage()?;
|
||||
|
||||
if let Some(config) = storage.configs.iter_mut().find(|c| c.id == id) {
|
||||
config.last_used = Some(Utc::now().timestamp());
|
||||
self.save_storage(&storage)
|
||||
} else {
|
||||
Err(VpnError::NotFound(id.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Import a VPN config from raw content
|
||||
pub fn import_config(
|
||||
&self,
|
||||
content: &str,
|
||||
filename: &str,
|
||||
name: Option<String>,
|
||||
) -> Result<VpnConfig, VpnError> {
|
||||
let vpn_type = super::detect_vpn_type(content, filename)?;
|
||||
|
||||
// Validate the config by parsing it
|
||||
match vpn_type {
|
||||
VpnType::WireGuard => {
|
||||
super::parse_wireguard_config(content)?;
|
||||
}
|
||||
VpnType::OpenVPN => {
|
||||
super::parse_openvpn_config(content)?;
|
||||
}
|
||||
}
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let display_name = name.unwrap_or_else(|| {
|
||||
// Generate name from filename
|
||||
let base = filename.trim_end_matches(".conf").trim_end_matches(".ovpn");
|
||||
format!("{} ({})", base, vpn_type)
|
||||
});
|
||||
|
||||
let config = VpnConfig {
|
||||
id,
|
||||
name: display_name,
|
||||
vpn_type,
|
||||
config_data: content.to_string(),
|
||||
created_at: Utc::now().timestamp(),
|
||||
last_used: None,
|
||||
};
|
||||
|
||||
self.save_config(&config)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn create_test_storage() -> (VpnStorage, TempDir) {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut storage = VpnStorage::new();
|
||||
storage.storage_path = temp_dir.path().join("test_vpn_configs.json");
|
||||
(storage, temp_dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_roundtrip() {
|
||||
let (storage, _temp) = create_test_storage();
|
||||
let original = "This is a secret VPN configuration";
|
||||
|
||||
let (encrypted, nonce) = storage.encrypt(original).unwrap();
|
||||
let decrypted = storage.decrypt(&encrypted, &nonce).unwrap();
|
||||
|
||||
assert_eq!(original, decrypted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_save_and_load_config() {
|
||||
let (storage, _temp) = create_test_storage();
|
||||
|
||||
let config = VpnConfig {
|
||||
id: "test-id-123".to_string(),
|
||||
name: "Test VPN".to_string(),
|
||||
vpn_type: VpnType::WireGuard,
|
||||
config_data: "[Interface]\nPrivateKey = test\n[Peer]\nPublicKey = peer".to_string(),
|
||||
created_at: 1234567890,
|
||||
last_used: None,
|
||||
};
|
||||
|
||||
storage.save_config(&config).unwrap();
|
||||
let loaded = storage.load_config("test-id-123").unwrap();
|
||||
|
||||
assert_eq!(loaded.id, config.id);
|
||||
assert_eq!(loaded.name, config.name);
|
||||
assert_eq!(loaded.vpn_type, config.vpn_type);
|
||||
assert_eq!(loaded.config_data, config.config_data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_configs() {
|
||||
let (storage, _temp) = create_test_storage();
|
||||
|
||||
let config1 = VpnConfig {
|
||||
id: "id-1".to_string(),
|
||||
name: "VPN 1".to_string(),
|
||||
vpn_type: VpnType::WireGuard,
|
||||
config_data: "secret1".to_string(),
|
||||
created_at: 1000,
|
||||
last_used: None,
|
||||
};
|
||||
|
||||
let config2 = VpnConfig {
|
||||
id: "id-2".to_string(),
|
||||
name: "VPN 2".to_string(),
|
||||
vpn_type: VpnType::OpenVPN,
|
||||
config_data: "secret2".to_string(),
|
||||
created_at: 2000,
|
||||
last_used: Some(3000),
|
||||
};
|
||||
|
||||
storage.save_config(&config1).unwrap();
|
||||
storage.save_config(&config2).unwrap();
|
||||
|
||||
let configs = storage.list_configs().unwrap();
|
||||
assert_eq!(configs.len(), 2);
|
||||
|
||||
// Config data should be empty in listing
|
||||
assert!(configs[0].config_data.is_empty());
|
||||
assert!(configs[1].config_data.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_config() {
|
||||
let (storage, _temp) = create_test_storage();
|
||||
|
||||
let config = VpnConfig {
|
||||
id: "delete-me".to_string(),
|
||||
name: "To Delete".to_string(),
|
||||
vpn_type: VpnType::WireGuard,
|
||||
config_data: "data".to_string(),
|
||||
created_at: 1000,
|
||||
last_used: None,
|
||||
};
|
||||
|
||||
storage.save_config(&config).unwrap();
|
||||
assert!(storage.load_config("delete-me").is_ok());
|
||||
|
||||
storage.delete_config("delete-me").unwrap();
|
||||
assert!(storage.load_config("delete-me").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_nonexistent_config() {
|
||||
let (storage, _temp) = create_test_storage();
|
||||
let result = storage.load_config("nonexistent");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
//! VPN tunnel trait and management.
|
||||
|
||||
use super::config::{VpnError, VpnStatus};
|
||||
use async_trait::async_trait;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Trait for VPN tunnel implementations
|
||||
#[async_trait]
|
||||
pub trait VpnTunnel: Send + Sync {
|
||||
/// Connect the VPN tunnel
|
||||
async fn connect(&mut self) -> Result<(), VpnError>;
|
||||
|
||||
/// Disconnect the VPN tunnel
|
||||
async fn disconnect(&mut self) -> Result<(), VpnError>;
|
||||
|
||||
/// Check if the tunnel is connected
|
||||
fn is_connected(&self) -> bool;
|
||||
|
||||
/// Get the VPN config ID
|
||||
fn vpn_id(&self) -> &str;
|
||||
|
||||
/// Get the current status of the tunnel
|
||||
fn get_status(&self) -> VpnStatus;
|
||||
|
||||
/// Get bytes sent through the tunnel
|
||||
fn bytes_sent(&self) -> u64;
|
||||
|
||||
/// Get bytes received through the tunnel
|
||||
fn bytes_received(&self) -> u64;
|
||||
}
|
||||
|
||||
/// Manager for active VPN tunnels
|
||||
pub struct TunnelManager {
|
||||
active_tunnels: HashMap<String, Box<dyn VpnTunnel>>,
|
||||
}
|
||||
|
||||
impl Default for TunnelManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TunnelManager {
|
||||
/// Create a new tunnel manager
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active_tunnels: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register an active tunnel
|
||||
pub fn register_tunnel(&mut self, vpn_id: String, tunnel: Box<dyn VpnTunnel>) {
|
||||
self.active_tunnels.insert(vpn_id, tunnel);
|
||||
}
|
||||
|
||||
/// Remove a tunnel from management
|
||||
pub fn remove_tunnel(&mut self, vpn_id: &str) -> Option<Box<dyn VpnTunnel>> {
|
||||
self.active_tunnels.remove(vpn_id)
|
||||
}
|
||||
|
||||
/// Get a reference to an active tunnel
|
||||
pub fn get_tunnel(&self, vpn_id: &str) -> Option<&dyn VpnTunnel> {
|
||||
self.active_tunnels.get(vpn_id).map(|t| t.as_ref())
|
||||
}
|
||||
|
||||
/// Get a mutable reference to an active tunnel
|
||||
pub fn get_tunnel_mut(&mut self, vpn_id: &str) -> Option<&mut Box<dyn VpnTunnel>> {
|
||||
self.active_tunnels.get_mut(vpn_id)
|
||||
}
|
||||
|
||||
/// Check if a tunnel is active
|
||||
pub fn is_tunnel_active(&self, vpn_id: &str) -> bool {
|
||||
self
|
||||
.active_tunnels
|
||||
.get(vpn_id)
|
||||
.is_some_and(|t| t.is_connected())
|
||||
}
|
||||
|
||||
/// Get status of all active tunnels
|
||||
pub fn get_all_statuses(&self) -> Vec<VpnStatus> {
|
||||
self
|
||||
.active_tunnels
|
||||
.values()
|
||||
.map(|t| t.get_status())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Disconnect all active tunnels
|
||||
pub async fn disconnect_all(&mut self) -> Vec<Result<(), VpnError>> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for tunnel in self.active_tunnels.values_mut() {
|
||||
results.push(tunnel.disconnect().await);
|
||||
}
|
||||
|
||||
self.active_tunnels.clear();
|
||||
results
|
||||
}
|
||||
|
||||
/// Get the number of active tunnels
|
||||
pub fn active_count(&self) -> usize {
|
||||
self
|
||||
.active_tunnels
|
||||
.values()
|
||||
.filter(|t| t.is_connected())
|
||||
.count()
|
||||
}
|
||||
|
||||
/// List IDs of all active VPN connections
|
||||
pub fn list_active_ids(&self) -> Vec<String> {
|
||||
self
|
||||
.active_tunnels
|
||||
.iter()
|
||||
.filter(|(_, t)| t.is_connected())
|
||||
.map(|(id, _)| id.clone())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct MockTunnel {
|
||||
id: String,
|
||||
connected: bool,
|
||||
bytes_sent: u64,
|
||||
bytes_received: u64,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VpnTunnel for MockTunnel {
|
||||
async fn connect(&mut self) -> Result<(), VpnError> {
|
||||
self.connected = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn disconnect(&mut self) -> Result<(), VpnError> {
|
||||
self.connected = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_connected(&self) -> bool {
|
||||
self.connected
|
||||
}
|
||||
|
||||
fn vpn_id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn get_status(&self) -> VpnStatus {
|
||||
VpnStatus {
|
||||
connected: self.connected,
|
||||
vpn_id: self.id.clone(),
|
||||
connected_at: if self.connected { Some(1000) } else { None },
|
||||
bytes_sent: Some(self.bytes_sent),
|
||||
bytes_received: Some(self.bytes_received),
|
||||
last_handshake: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn bytes_sent(&self) -> u64 {
|
||||
self.bytes_sent
|
||||
}
|
||||
|
||||
fn bytes_received(&self) -> u64 {
|
||||
self.bytes_received
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tunnel_manager_register() {
|
||||
let mut manager = TunnelManager::new();
|
||||
let tunnel = Box::new(MockTunnel {
|
||||
id: "test-1".to_string(),
|
||||
connected: true,
|
||||
bytes_sent: 100,
|
||||
bytes_received: 200,
|
||||
});
|
||||
|
||||
manager.register_tunnel("test-1".to_string(), tunnel);
|
||||
assert!(manager.is_tunnel_active("test-1"));
|
||||
assert!(!manager.is_tunnel_active("test-2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tunnel_manager_remove() {
|
||||
let mut manager = TunnelManager::new();
|
||||
let tunnel = Box::new(MockTunnel {
|
||||
id: "test-1".to_string(),
|
||||
connected: true,
|
||||
bytes_sent: 0,
|
||||
bytes_received: 0,
|
||||
});
|
||||
|
||||
manager.register_tunnel("test-1".to_string(), tunnel);
|
||||
assert!(manager.is_tunnel_active("test-1"));
|
||||
|
||||
let removed = manager.remove_tunnel("test-1");
|
||||
assert!(removed.is_some());
|
||||
assert!(!manager.is_tunnel_active("test-1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tunnel_manager_active_count() {
|
||||
let mut manager = TunnelManager::new();
|
||||
|
||||
let tunnel1 = Box::new(MockTunnel {
|
||||
id: "t1".to_string(),
|
||||
connected: true,
|
||||
bytes_sent: 0,
|
||||
bytes_received: 0,
|
||||
});
|
||||
|
||||
let tunnel2 = Box::new(MockTunnel {
|
||||
id: "t2".to_string(),
|
||||
connected: false,
|
||||
bytes_sent: 0,
|
||||
bytes_received: 0,
|
||||
});
|
||||
|
||||
manager.register_tunnel("t1".to_string(), tunnel1);
|
||||
manager.register_tunnel("t2".to_string(), tunnel2);
|
||||
|
||||
assert_eq!(manager.active_count(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tunnel_manager_disconnect_all() {
|
||||
let mut manager = TunnelManager::new();
|
||||
|
||||
let tunnel1 = Box::new(MockTunnel {
|
||||
id: "t1".to_string(),
|
||||
connected: true,
|
||||
bytes_sent: 0,
|
||||
bytes_received: 0,
|
||||
});
|
||||
|
||||
let tunnel2 = Box::new(MockTunnel {
|
||||
id: "t2".to_string(),
|
||||
connected: true,
|
||||
bytes_sent: 0,
|
||||
bytes_received: 0,
|
||||
});
|
||||
|
||||
manager.register_tunnel("t1".to_string(), tunnel1);
|
||||
manager.register_tunnel("t2".to_string(), tunnel2);
|
||||
|
||||
assert_eq!(manager.active_count(), 2);
|
||||
|
||||
let results = manager.disconnect_all().await;
|
||||
assert_eq!(results.len(), 2);
|
||||
assert!(results.iter().all(|r| r.is_ok()));
|
||||
assert_eq!(manager.active_count(), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
//! WireGuard tunnel implementation using boringtun.
|
||||
|
||||
use super::config::{VpnError, VpnStatus, WireGuardConfig};
|
||||
use super::tunnel::VpnTunnel;
|
||||
use async_trait::async_trait;
|
||||
use boringtun::noise::{Tunn, TunnResult};
|
||||
use boringtun::x25519::{PublicKey, StaticSecret};
|
||||
use chrono::Utc;
|
||||
use std::net::{SocketAddr, ToSocketAddrs, UdpSocket};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
/// WireGuard tunnel implementation
|
||||
pub struct WireGuardTunnel {
|
||||
vpn_id: String,
|
||||
config: WireGuardConfig,
|
||||
tunnel: Option<Arc<Mutex<Box<Tunn>>>>,
|
||||
socket: Option<Arc<UdpSocket>>,
|
||||
connected: AtomicBool,
|
||||
connected_at: Option<i64>,
|
||||
bytes_sent: AtomicU64,
|
||||
bytes_received: AtomicU64,
|
||||
last_handshake: Option<i64>,
|
||||
peer_addr: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
impl WireGuardTunnel {
|
||||
/// Create a new WireGuard tunnel
|
||||
pub fn new(vpn_id: String, config: WireGuardConfig) -> Self {
|
||||
Self {
|
||||
vpn_id,
|
||||
config,
|
||||
tunnel: None,
|
||||
socket: None,
|
||||
connected: AtomicBool::new(false),
|
||||
connected_at: None,
|
||||
bytes_sent: AtomicU64::new(0),
|
||||
bytes_received: AtomicU64::new(0),
|
||||
last_handshake: None,
|
||||
peer_addr: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse base64 key to bytes
|
||||
fn parse_key(key: &str) -> Result<[u8; 32], VpnError> {
|
||||
let decoded = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, key)
|
||||
.map_err(|e| VpnError::InvalidWireGuard(format!("Invalid key encoding: {e}")))?;
|
||||
|
||||
if decoded.len() != 32 {
|
||||
return Err(VpnError::InvalidWireGuard(format!(
|
||||
"Invalid key length: {} (expected 32)",
|
||||
decoded.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut key_bytes = [0u8; 32];
|
||||
key_bytes.copy_from_slice(&decoded);
|
||||
Ok(key_bytes)
|
||||
}
|
||||
|
||||
/// Initialize the WireGuard tunnel
|
||||
fn init_tunnel(&mut self) -> Result<(), VpnError> {
|
||||
// Parse private key
|
||||
let private_key_bytes = Self::parse_key(&self.config.private_key)?;
|
||||
let static_private = StaticSecret::from(private_key_bytes);
|
||||
|
||||
// Parse peer public key
|
||||
let peer_public_bytes = Self::parse_key(&self.config.peer_public_key)?;
|
||||
let peer_public = PublicKey::from(peer_public_bytes);
|
||||
|
||||
// Parse optional preshared key
|
||||
let preshared_key = if let Some(ref psk) = self.config.preshared_key {
|
||||
Some(Self::parse_key(psk)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Create the boringtun tunnel
|
||||
let tunn = Tunn::new(
|
||||
static_private,
|
||||
peer_public,
|
||||
preshared_key,
|
||||
self.config.persistent_keepalive,
|
||||
0, // index
|
||||
None,
|
||||
)
|
||||
.map_err(|e| VpnError::Tunnel(format!("Failed to create tunnel: {e}")))?;
|
||||
|
||||
self.tunnel = Some(Arc::new(Mutex::new(Box::new(tunn))));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve peer endpoint to socket address
|
||||
fn resolve_endpoint(&mut self) -> Result<SocketAddr, VpnError> {
|
||||
let endpoint = &self.config.peer_endpoint;
|
||||
|
||||
// Try to resolve the endpoint
|
||||
let addrs: Vec<SocketAddr> = endpoint
|
||||
.to_socket_addrs()
|
||||
.map_err(|e| VpnError::Connection(format!("Failed to resolve endpoint '{endpoint}': {e}")))?
|
||||
.collect();
|
||||
|
||||
addrs
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| VpnError::Connection(format!("No addresses found for endpoint: {endpoint}")))
|
||||
}
|
||||
|
||||
/// Perform WireGuard handshake
|
||||
async fn handshake(&mut self) -> Result<(), VpnError> {
|
||||
let tunnel = self
|
||||
.tunnel
|
||||
.as_ref()
|
||||
.ok_or_else(|| VpnError::Tunnel("Tunnel not initialized".to_string()))?;
|
||||
|
||||
let socket = self
|
||||
.socket
|
||||
.as_ref()
|
||||
.ok_or_else(|| VpnError::Tunnel("Socket not initialized".to_string()))?;
|
||||
|
||||
let peer_addr = self
|
||||
.peer_addr
|
||||
.ok_or_else(|| VpnError::Tunnel("Peer address not resolved".to_string()))?;
|
||||
|
||||
let mut tunnel_guard = tunnel.lock().await;
|
||||
|
||||
// Generate handshake initiation
|
||||
let mut dst = vec![0u8; 2048];
|
||||
let result = tunnel_guard.format_handshake_initiation(&mut dst, false);
|
||||
|
||||
match result {
|
||||
TunnResult::WriteToNetwork(packet) => {
|
||||
socket
|
||||
.send_to(packet, peer_addr)
|
||||
.map_err(|e| VpnError::Connection(format!("Failed to send handshake: {e}")))?;
|
||||
|
||||
self
|
||||
.bytes_sent
|
||||
.fetch_add(packet.len() as u64, Ordering::Relaxed);
|
||||
}
|
||||
TunnResult::Err(e) => {
|
||||
return Err(VpnError::Tunnel(format!(
|
||||
"Handshake initiation failed: {e:?}"
|
||||
)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Wait for handshake response (with timeout)
|
||||
socket
|
||||
.set_read_timeout(Some(std::time::Duration::from_secs(10)))
|
||||
.map_err(|e| VpnError::Connection(format!("Failed to set timeout: {e}")))?;
|
||||
|
||||
let mut recv_buf = vec![0u8; 2048];
|
||||
|
||||
match socket.recv_from(&mut recv_buf) {
|
||||
Ok((len, _from)) => {
|
||||
self.bytes_received.fetch_add(len as u64, Ordering::Relaxed);
|
||||
|
||||
let result = tunnel_guard.decapsulate(None, &recv_buf[..len], &mut dst);
|
||||
|
||||
match result {
|
||||
TunnResult::WriteToNetwork(response) => {
|
||||
socket
|
||||
.send_to(response, peer_addr)
|
||||
.map_err(|e| VpnError::Connection(format!("Failed to send response: {e}")))?;
|
||||
|
||||
self
|
||||
.bytes_sent
|
||||
.fetch_add(response.len() as u64, Ordering::Relaxed);
|
||||
self.last_handshake = Some(Utc::now().timestamp());
|
||||
}
|
||||
TunnResult::Done => {
|
||||
self.last_handshake = Some(Utc::now().timestamp());
|
||||
}
|
||||
TunnResult::Err(e) => {
|
||||
return Err(VpnError::Tunnel(format!(
|
||||
"Handshake response failed: {e:?}"
|
||||
)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(VpnError::Connection(format!(
|
||||
"Handshake timeout or error: {e}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encrypt and send data through the tunnel
|
||||
pub async fn send(&self, data: &[u8]) -> Result<(), VpnError> {
|
||||
let tunnel = self
|
||||
.tunnel
|
||||
.as_ref()
|
||||
.ok_or_else(|| VpnError::Tunnel("Tunnel not initialized".to_string()))?;
|
||||
|
||||
let socket = self
|
||||
.socket
|
||||
.as_ref()
|
||||
.ok_or_else(|| VpnError::Tunnel("Socket not initialized".to_string()))?;
|
||||
|
||||
let peer_addr = self
|
||||
.peer_addr
|
||||
.ok_or_else(|| VpnError::Tunnel("Peer address not resolved".to_string()))?;
|
||||
|
||||
let mut tunnel_guard = tunnel.lock().await;
|
||||
let mut dst = vec![0u8; data.len() + 256]; // Extra space for WireGuard overhead
|
||||
|
||||
let result = tunnel_guard.encapsulate(data, &mut dst);
|
||||
|
||||
match result {
|
||||
TunnResult::WriteToNetwork(packet) => {
|
||||
socket
|
||||
.send_to(packet, peer_addr)
|
||||
.map_err(|e| VpnError::Connection(format!("Failed to send data: {e}")))?;
|
||||
|
||||
self
|
||||
.bytes_sent
|
||||
.fetch_add(packet.len() as u64, Ordering::Relaxed);
|
||||
}
|
||||
TunnResult::Err(e) => {
|
||||
return Err(VpnError::Tunnel(format!("Encryption failed: {e:?}")));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive and decrypt data from the tunnel
|
||||
pub async fn receive(&self, buf: &mut [u8]) -> Result<usize, VpnError> {
|
||||
let tunnel = self
|
||||
.tunnel
|
||||
.as_ref()
|
||||
.ok_or_else(|| VpnError::Tunnel("Tunnel not initialized".to_string()))?;
|
||||
|
||||
let socket = self
|
||||
.socket
|
||||
.as_ref()
|
||||
.ok_or_else(|| VpnError::Tunnel("Socket not initialized".to_string()))?;
|
||||
|
||||
let mut recv_buf = vec![0u8; 2048];
|
||||
|
||||
let (len, _from) = socket
|
||||
.recv_from(&mut recv_buf)
|
||||
.map_err(|e| VpnError::Connection(format!("Receive failed: {e}")))?;
|
||||
|
||||
self.bytes_received.fetch_add(len as u64, Ordering::Relaxed);
|
||||
|
||||
let mut tunnel_guard = tunnel.lock().await;
|
||||
// decapsulate writes decrypted data directly to buf and returns a slice pointing to it
|
||||
let result = tunnel_guard.decapsulate(None, &recv_buf[..len], buf);
|
||||
|
||||
match result {
|
||||
// Data is already written to buf by decapsulate, just return the length
|
||||
TunnResult::WriteToTunnelV4(decrypted, _) => Ok(decrypted.len()),
|
||||
TunnResult::WriteToTunnelV6(decrypted, _) => Ok(decrypted.len()),
|
||||
TunnResult::Done => Ok(0),
|
||||
TunnResult::Err(e) => Err(VpnError::Tunnel(format!("Decryption failed: {e:?}"))),
|
||||
_ => Ok(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VpnTunnel for WireGuardTunnel {
|
||||
async fn connect(&mut self) -> Result<(), VpnError> {
|
||||
if self.connected.load(Ordering::Relaxed) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Initialize the tunnel
|
||||
self.init_tunnel()?;
|
||||
|
||||
// Resolve endpoint
|
||||
self.peer_addr = Some(self.resolve_endpoint()?);
|
||||
|
||||
// Create UDP socket
|
||||
let socket = UdpSocket::bind("0.0.0.0:0")
|
||||
.map_err(|e| VpnError::Connection(format!("Failed to create socket: {e}")))?;
|
||||
|
||||
socket
|
||||
.set_nonblocking(false)
|
||||
.map_err(|e| VpnError::Connection(format!("Failed to set socket options: {e}")))?;
|
||||
|
||||
self.socket = Some(Arc::new(socket));
|
||||
|
||||
// Perform handshake
|
||||
self.handshake().await?;
|
||||
|
||||
self.connected.store(true, Ordering::Release);
|
||||
self.connected_at = Some(Utc::now().timestamp());
|
||||
|
||||
log::info!("[vpn] WireGuard tunnel {} connected", self.vpn_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn disconnect(&mut self) -> Result<(), VpnError> {
|
||||
if !self.connected.load(Ordering::Relaxed) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.connected.store(false, Ordering::Release);
|
||||
self.tunnel = None;
|
||||
self.socket = None;
|
||||
self.connected_at = None;
|
||||
|
||||
log::info!("[vpn] WireGuard tunnel {} disconnected", self.vpn_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_connected(&self) -> bool {
|
||||
self.connected.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
fn vpn_id(&self) -> &str {
|
||||
&self.vpn_id
|
||||
}
|
||||
|
||||
fn get_status(&self) -> VpnStatus {
|
||||
VpnStatus {
|
||||
connected: self.is_connected(),
|
||||
vpn_id: self.vpn_id.clone(),
|
||||
connected_at: self.connected_at,
|
||||
bytes_sent: Some(self.bytes_sent.load(Ordering::Relaxed)),
|
||||
bytes_received: Some(self.bytes_received.load(Ordering::Relaxed)),
|
||||
last_handshake: self.last_handshake,
|
||||
}
|
||||
}
|
||||
|
||||
fn bytes_sent(&self) -> u64 {
|
||||
self.bytes_sent.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn bytes_received(&self) -> u64 {
|
||||
self.bytes_received.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_config() -> WireGuardConfig {
|
||||
WireGuardConfig {
|
||||
// These are test keys, not real ones
|
||||
private_key: "YEocP0e2o1WT5GlvBvQzVF7EeR6z9aCk+ZdZ5NKEuXA=".to_string(),
|
||||
address: "10.0.0.2/24".to_string(),
|
||||
dns: Some("1.1.1.1".to_string()),
|
||||
mtu: Some(1420),
|
||||
peer_public_key: "aGnF7JlG+U5t0BqB1PVf1yOuELHrWLGGcUJb0eCK9Aw=".to_string(),
|
||||
peer_endpoint: "127.0.0.1:51820".to_string(),
|
||||
allowed_ips: vec!["0.0.0.0/0".to_string()],
|
||||
persistent_keepalive: Some(25),
|
||||
preshared_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wireguard_tunnel_creation() {
|
||||
let config = create_test_config();
|
||||
let tunnel = WireGuardTunnel::new("test-wg-1".to_string(), config);
|
||||
|
||||
assert_eq!(tunnel.vpn_id(), "test-wg-1");
|
||||
assert!(!tunnel.is_connected());
|
||||
assert_eq!(tunnel.bytes_sent(), 0);
|
||||
assert_eq!(tunnel.bytes_received(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_key_valid() {
|
||||
// Valid base64-encoded 32-byte key
|
||||
let key = "YEocP0e2o1WT5GlvBvQzVF7EeR6z9aCk+ZdZ5NKEuXA=";
|
||||
let result = WireGuardTunnel::parse_key(key);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap().len(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_key_invalid_base64() {
|
||||
let key = "not-valid-base64!!!";
|
||||
let result = WireGuardTunnel::parse_key(key);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_key_wrong_length() {
|
||||
// Valid base64 but wrong length
|
||||
let key = "YWJjZA=="; // "abcd" in base64
|
||||
let result = WireGuardTunnel::parse_key(key);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wireguard_status() {
|
||||
let config = create_test_config();
|
||||
let tunnel = WireGuardTunnel::new("test-wg-2".to_string(), config);
|
||||
|
||||
let status = tunnel.get_status();
|
||||
assert!(!status.connected);
|
||||
assert_eq!(status.vpn_id, "test-wg-2");
|
||||
assert!(status.connected_at.is_none());
|
||||
assert_eq!(status.bytes_sent, Some(0));
|
||||
assert_eq!(status.bytes_received, Some(0));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user