feat: daemon support, general improvement, and preparation for Windows release

This commit is contained in:
zhom
2026-02-01 20:55:09 +04:00
parent e9f4edd120
commit 4a59459eb2
58 changed files with 9763 additions and 296 deletions
+489
View File
@@ -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"));
}
}
+31
View File
@@ -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()));
+343
View File
@@ -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")),
}
}
}
+415
View File
@@ -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());
}
}
+256
View File
@@ -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);
}
}
+413
View File
@@ -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));
}
}