pub mod actions;
use crate::llm::action_helper::call_llm;
use crate::llm::ollama_client::OllamaClient;
use crate::logging::emit::Log;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::state::app_state::AppState;
use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
use actions::{WireguardProtocol, WIREGUARD_PEER_CONNECTED_EVENT};
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tracing::{debug, error, info, trace, warn};
fn short_key(key: &str) -> &str {
match key.char_indices().nth(16) {
Some((idx, _)) => &key[..idx],
None => key,
}
}
use defguard_wireguard_rs::{
host::Peer as WGPeer, key::Key, net::IpAddrMask, InterfaceConfiguration, WGApi,
WireguardInterfaceApi,
};
const MAX_PEERS: usize = 100;
pub struct WireguardServer {
_interface_name: String,
#[cfg(not(target_os = "macos"))]
wgapi: Arc<RwLock<WGApi<defguard_wireguard_rs::Kernel>>>,
#[cfg(target_os = "macos")]
wgapi: Arc<RwLock<WGApi<defguard_wireguard_rs::Userspace>>>,
_private_key: String,
public_key: String,
listen_port: u16,
peers: Arc<RwLock<HashMap<String, ConnectionId>>>,
llm_client: Arc<OllamaClient>,
status_tx: mpsc::UnboundedSender<String>,
}
pub fn build_peer_config(
public_key: &str,
allowed_ips: &[String],
endpoint: Option<SocketAddr>,
) -> Result<WGPeer> {
let peer_key: Key = public_key
.parse()
.with_context(|| format!("Invalid peer public key: {}", short_key(public_key)))?;
if allowed_ips.is_empty() {
return Err(anyhow::anyhow!(
"allowed_ips must not be empty - a peer with no allowed IPs can route nothing"
));
}
let mut allowed_ip_masks: Vec<IpAddrMask> = Vec::with_capacity(allowed_ips.len());
for ip_str in allowed_ips {
let mask = ip_str.parse::<IpAddrMask>().with_context(|| {
format!("Invalid allowed IP (expected CIDR, e.g. 10.20.30.2/32): {ip_str}")
})?;
allowed_ip_masks.push(mask);
}
let mut peer = WGPeer::new(peer_key);
peer.allowed_ips = allowed_ip_masks;
if let Some(ep) = endpoint {
peer.endpoint = Some(ep);
}
Ok(peer)
}
impl WireguardServer {
pub async fn spawn_with_llm_actions(
bind_addr: SocketAddr,
llm_client: Arc<OllamaClient>,
app_state: Arc<AppState>,
server_id: crate::state::ServerId,
status_tx: mpsc::UnboundedSender<String>,
) -> Result<SocketAddr> {
Log::new(Some(&status_tx)).info(format!(
"Starting WireGuard VPN server on {} (full VPN tunnel support)",
bind_addr
));
let private_key = Key::generate();
let public_key = private_key.public_key();
let private_key_str = private_key.to_string();
let public_key_str = public_key.to_string();
Log::new(Some(&status_tx)).info(format!("WireGuard server public key: {}", public_key_str));
let interface_name: String = if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") {
"netget_wg0".into()
} else if cfg!(target_os = "macos") {
"utun10".into()
} else if cfg!(target_os = "windows") {
"netget_wg0".into()
} else {
return Err(anyhow::anyhow!(
"Unsupported operating system for WireGuard"
));
};
Log::new(Some(&status_tx))
.info(format!("Creating WireGuard interface: {}", interface_name));
#[cfg(not(target_os = "macos"))]
let wgapi = WGApi::<defguard_wireguard_rs::Kernel>::new(interface_name.clone())
.context("Failed to create WireGuard API")?;
#[cfg(target_os = "macos")]
let wgapi = WGApi::<defguard_wireguard_rs::Userspace>::new(interface_name.clone())
.context("Failed to create WireGuard API")?;
wgapi
.create_interface()
.context("Failed to create WireGuard interface")?;
Log::new(Some(&status_tx)).info("WireGuard interface created successfully");
let listen_port = bind_addr.port();
let interface_config = InterfaceConfiguration {
name: interface_name.clone(),
prvkey: private_key_str.clone(),
addresses: vec!["10.20.30.1".parse().unwrap()],
port: listen_port as u32,
peers: vec![],
mtu: Some(1420),
};
#[cfg(not(windows))]
wgapi
.configure_interface(&interface_config)
.context("Failed to configure WireGuard interface")?;
#[cfg(windows)]
wgapi
.configure_interface(&interface_config, &[])
.context("Failed to configure WireGuard interface")?;
let log = Log::new(Some(&status_tx));
log.info(format!("Interface listening on UDP port {}", listen_port));
log.info("VPN subnet: 10.20.30.0/24");
let actual_addr = SocketAddr::new(bind_addr.ip(), listen_port);
let wgapi_arc = Arc::new(RwLock::new(wgapi));
let peers = Arc::new(RwLock::new(HashMap::new()));
let server = Arc::new(WireguardServer {
_interface_name: interface_name.clone(),
wgapi: wgapi_arc.clone(),
_private_key: private_key_str,
public_key: public_key_str.clone(),
listen_port,
peers: peers.clone(),
llm_client,
status_tx: status_tx.clone(),
});
app_state
.register_server_handle(server_id, server.clone())
.await;
let server_clone = server.clone();
let status_clone = status_tx.clone();
let app_state_clone = app_state.clone();
let monitor_handle = tokio::spawn(async move {
server_clone
.monitor_peers(app_state_clone, server_id, status_clone)
.await;
});
app_state
.register_server_task(server_id, monitor_handle)
.await;
let log = Log::new(Some(&status_tx));
log.info(format!("WireGuard VPN server ready on {}", actual_addr));
log.info(format!(
"Clients can connect using server public key: {}",
public_key_str
));
Ok(actual_addr)
}
async fn monitor_peers(
&self,
app_state: Arc<AppState>,
server_id: crate::state::ServerId,
status_tx: mpsc::UnboundedSender<String>,
) {
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
loop {
interval.tick().await;
let interface_data = {
let wgapi = self.wgapi.read().await;
match wgapi.read_interface_data() {
Ok(data) => data,
Err(e) => {
Log::new(Some(&status_tx))
.error(format!("Failed to read WireGuard interface data: {}", e));
continue;
}
}
};
trace!(
"WireGuard interface status: {} peers",
interface_data.peers.len()
);
let mut newly_connected = Vec::new();
for (pub_key, peer) in interface_data.peers.iter() {
let peer_key = pub_key.to_string();
let existing = {
let peers = self.peers.read().await;
peers.get(&peer_key).copied()
};
match existing {
None => {
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
{
let mut peers = self.peers.write().await;
peers.insert(peer_key.clone(), connection_id);
}
info!("New WireGuard peer connected: {}", peer_key);
let _ =
status_tx.send(format!("[INFO] New peer: {}", short_key(&peer_key)));
let remote_addr = peer.endpoint;
let now = crate::utils::clock::Instant::now();
let conn_state = ConnectionState {
id: connection_id,
remote_addr: remote_addr
.unwrap_or_else(|| SocketAddr::from(([0, 0, 0, 0], 0))),
local_addr: SocketAddr::new(
std::net::IpAddr::from([10, 20, 30, 1]),
self.listen_port,
),
bytes_sent: peer.tx_bytes,
bytes_received: peer.rx_bytes,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::empty(),
};
app_state
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
newly_connected.push((
peer_key,
connection_id,
remote_addr,
peer.allowed_ips
.iter()
.map(|ip| ip.to_string())
.collect::<Vec<_>>(),
));
}
Some(connection_id) => {
app_state
.update_connection_stats(
server_id,
connection_id,
Some(peer.rx_bytes),
Some(peer.tx_bytes),
None,
None,
)
.await;
}
}
}
for (peer_key, connection_id, endpoint, allowed_ips) in newly_connected {
self.handle_peer_connected(
&app_state,
server_id,
connection_id,
&peer_key,
endpoint,
allowed_ips,
&status_tx,
)
.await;
}
let current_peer_keys: Vec<String> = interface_data
.peers
.iter()
.map(|(pub_key, _peer)| pub_key.to_string())
.collect();
let disconnected_peers: Vec<(String, ConnectionId)> = {
let mut peers = self.peers.write().await;
let gone: Vec<String> = peers
.keys()
.filter(|k| !current_peer_keys.contains(k))
.cloned()
.collect();
gone.into_iter()
.filter_map(|k| peers.remove(&k).map(|id| (k, id)))
.collect()
};
for (peer_key, connection_id) in disconnected_peers {
info!("WireGuard peer disconnected: {}", peer_key);
let _ = status_tx.send(format!(
"[INFO] Peer disconnected: {}",
short_key(&peer_key)
));
app_state
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
}
}
}
#[allow(clippy::too_many_arguments)]
async fn handle_peer_connected(
&self,
app_state: &Arc<AppState>,
server_id: crate::state::ServerId,
connection_id: ConnectionId,
peer_key: &str,
endpoint: Option<SocketAddr>,
allowed_ips: Vec<String>,
status_tx: &mpsc::UnboundedSender<String>,
) {
let event = Event::new(
&WIREGUARD_PEER_CONNECTED_EVENT,
serde_json::json!({
"public_key": peer_key,
"endpoint": endpoint.map(|e| e.to_string()),
"allowed_ips": allowed_ips,
"server_public_key": self.public_key,
"listen_port": self.listen_port,
}),
);
let protocol = WireguardProtocol::new();
let result = match call_llm(
&self.llm_client,
app_state,
server_id,
Some(connection_id),
&event,
&protocol,
)
.await
{
Ok(r) => r,
Err(e) => {
let category = crate::utils::WireFailure::classify(&e);
let summary = format!(
"WireGuard peer {} decision=fail_closed_llm_error category={} \
(no decision produced; peer left exactly as its last explicit \
authorization set it)",
short_key(peer_key),
if category.is_overloaded() {
"overloaded"
} else {
"unavailable"
},
);
Log::new(Some(status_tx)).error(format!("{}: {}", summary, e));
return;
}
};
for message in &result.messages {
Log::new(Some(status_tx)).info(message);
}
let mut decisions: Vec<&'static str> = Vec::new();
for action in &result.raw_actions {
let action_type = action.get("type").and_then(|v| v.as_str()).unwrap_or("");
match action_type {
"authorize_peer" => {
let public_key = action
.get("public_key")
.and_then(|v| v.as_str())
.unwrap_or(peer_key)
.to_string();
let ips: Vec<String> = action
.get("allowed_ips")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_else(|| allowed_ips.clone());
if ips.is_empty() {
decisions.push("model_authorize_invalid");
warn!("authorize_peer for {} has no allowed_ips", public_key);
let _ = status_tx.send(format!(
"[WARN] authorize_peer for {} ignored: no allowed_ips",
short_key(&public_key)
));
continue;
}
let ep = action
.get("endpoint")
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<SocketAddr>().ok())
.or(endpoint);
if let Err(e) = self.add_peer(public_key.clone(), ips, ep).await {
decisions.push("model_authorize_failed");
error!("Failed to authorize WireGuard peer {}: {}", public_key, e);
let _ = status_tx.send(format!("[ERROR] authorize_peer failed: {}", e));
} else {
decisions.push("model_authorize");
}
}
"disconnect_peer" | "reject_peer" => {
let public_key = action
.get("public_key")
.and_then(|v| v.as_str())
.unwrap_or(peer_key)
.to_string();
if let Err(e) = self.remove_peer(public_key.clone(), status_tx).await {
decisions.push("model_reject_failed");
error!("Failed to remove WireGuard peer {}: {}", public_key, e);
let _ = status_tx.send(format!("[ERROR] disconnect_peer failed: {}", e));
} else {
decisions.push("model_reject");
}
}
other => {
debug!("WireGuard: no interface change for action '{}'", other);
}
}
}
let summary = format!(
"WireGuard peer {} decision={}",
short_key(peer_key),
if decisions.is_empty() {
"model_no_action".to_string()
} else {
decisions.join(",")
}
);
Log::new(Some(status_tx)).info(summary);
}
pub async fn add_peer(
&self,
peer_public_key: String,
allowed_ips: Vec<String>,
endpoint: Option<SocketAddr>,
) -> Result<()> {
info!("Adding WireGuard peer: {}", peer_public_key);
let _ = self.status_tx.send(format!(
"[INFO] Adding peer: {}",
short_key(&peer_public_key)
));
let peer_count = {
let peers = self.peers.read().await;
peers.len()
};
if peer_count >= MAX_PEERS {
return Err(anyhow::anyhow!(
"Maximum number of peers ({}) reached",
MAX_PEERS
));
}
let peer = build_peer_config(&peer_public_key, &allowed_ips, endpoint)?;
let wgapi = self.wgapi.write().await;
wgapi
.configure_peer(&peer)
.context("Failed to configure peer")?;
info!("WireGuard peer added successfully: {}", peer_public_key);
let _ = self.status_tx.send(format!(
"→ Peer authorized: {}",
short_key(&peer_public_key)
));
Ok(())
}
pub async fn remove_peer(
&self,
peer_public_key: String,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<()> {
info!("Removing WireGuard peer: {}", peer_public_key);
let _ = status_tx.send(format!(
"[INFO] Removing peer: {}",
short_key(&peer_public_key)
));
let peer_key: Key = peer_public_key.parse().context("Invalid peer public key")?;
let wgapi = self.wgapi.write().await;
wgapi
.remove_peer(&peer_key)
.context("Failed to remove peer")?;
{
let mut peers = self.peers.write().await;
peers.remove(&peer_public_key);
}
info!("WireGuard peer removed: {}", peer_public_key);
let _ = status_tx.send(format!("→ Peer removed: {}", short_key(&peer_public_key)));
Ok(())
}
pub fn get_public_key(&self) -> &str {
&self.public_key
}
pub async fn list_peers(&self) -> Vec<String> {
let peers = self.peers.read().await;
peers.keys().cloned().collect()
}
}
impl Drop for WireguardServer {
fn drop(&mut self) {
info!("WireGuard server shutting down");
}
}