use crate::error::{ApiError, ApiResult}; use crate::routes::auth::GenericSuccess; use crate::state::{AppState, SystemEvent}; use axum::Json; use axum::extract::{Path, State}; use chrono::Utc; use nx9_wg_core::crypto::generate_keypair; use nx9_wg_core::types::wireguard::{ Interface, InterfaceRole, WireGuardPrivateKey, WireGuardPublicKey, }; use nx9_wg_core::validation::{ validate_cidr, validate_interface_name, validate_listen_port, validate_mtu, }; use nx9_wireguard::UpstreamConfigParser; use serde::{Deserialize, Serialize}; use uuid::Uuid; #[derive(Debug, Deserialize)] pub struct CreateInterfaceRequest { pub name: String, pub role: Option, pub listen_port: Option, pub address_v4: String, pub address_v6: Option, pub mtu: Option, pub dns: Option, pub private_key: Option, pub public_key: Option, pub pre_up: Option, pub post_up: Option, pub pre_down: Option, pub post_down: Option, } #[derive(Debug, Deserialize)] pub struct UpstreamPreviewRequest { pub name: String, pub config: String, } #[derive(Debug, Serialize)] pub struct UpstreamPreviewResponse { pub name: String, pub role: String, pub address_v4: String, pub address_v6: Option, pub dns: Option, pub mtu: Option, pub listen_port: Option, pub peer_count: usize, pub provider_public_key: String, pub provider_endpoint: String, pub provider_allowed_ips: String, pub persistent_keepalive: Option, pub preshared_key_configured: bool, } #[derive(Debug, Deserialize)] pub struct UpstreamImportRequest { pub name: String, pub config: String, } #[derive(Debug, Serialize)] pub struct UpstreamImportResponse { pub interface_id: Uuid, pub peer_id: Uuid, pub name: String, pub role: String, pub address_v4: String, pub address_v6: Option, pub dns: Option, pub mtu: Option, pub listen_port: Option, pub provider_public_key: String, pub provider_endpoint: String, pub provider_allowed_ips: String, pub persistent_keepalive: Option, pub preshared_key_configured: bool, pub enabled: bool, } #[derive(Debug, Deserialize)] pub struct UpdateInterfaceRequest { pub name: Option, pub listen_port: Option, pub address_v4: Option, pub address_v6: Option, pub mtu: Option, pub dns: Option, pub enabled: Option, pub pre_up: Option, pub post_up: Option, pub pre_down: Option, pub post_down: Option, } #[derive(Debug, Serialize)] pub struct InterfaceStatusResponse { pub interface: Interface, pub peer_count: usize, pub active_peer_count: usize, } /// GET /api/v1/interfaces pub async fn list_interfaces_handler( State(state): State, ) -> ApiResult>> { let list = state.store.list_interfaces().await?; Ok(Json(list)) } /// POST /api/v1/interfaces pub async fn create_interface_handler( State(state): State, Json(payload): Json, ) -> ApiResult> { validate_interface_name(&payload.name)?; let role = payload.role.unwrap_or(if payload.name == "wg0" { InterfaceRole::Overlay } else { InterfaceRole::Upstream }); if role == InterfaceRole::Overlay { let existing = state.store.list_interfaces().await?; if existing.iter().any(|i| i.role == InterfaceRole::Overlay) { return Err(ApiError::Conflict( "Only one Overlay interface ('wg0') is permitted".to_string(), )); } if payload.name != "wg0" { return Err(ApiError::Validation( "The primary overlay interface must be named 'wg0'".to_string(), )); } } else if payload.name == "wg0" { return Err(ApiError::Validation( "An Upstream interface cannot use the reserved name 'wg0'".to_string(), )); } let address_v4 = validate_cidr(&payload.address_v4)?; let address_v6 = match payload.address_v6.as_deref() { Some(s) if !s.trim().is_empty() => Some(validate_cidr(s)?), _ => None, }; let listen_port = match payload.listen_port { Some(p) => Some(validate_listen_port(p)?), None => { if role == InterfaceRole::Overlay { Some(51820) } else { None } } }; if let Some(m) = payload.mtu { validate_mtu(m)?; } let (priv_k, pub_k) = match (payload.private_key, payload.public_key) { (Some(priv_s), Some(pub_s)) => ( WireGuardPrivateKey::new(priv_s), WireGuardPublicKey::new(pub_s), ), _ => generate_keypair(), }; let now = Utc::now().naive_utc(); let iface = Interface { id: Uuid::new_v4(), name: payload.name, role, private_key: priv_k, public_key: pub_k, listen_port, address_v4, address_v6, mtu: payload.mtu, dns: payload.dns, enabled: true, pre_up: payload.pre_up, post_up: payload.post_up, pre_down: payload.pre_down, post_down: payload.post_down, created_at: now, updated_at: now, }; state.store.create_interface(&iface).await?; state.broadcast(SystemEvent::InterfaceChanged { id: iface.id.to_string(), action: "created".to_string(), }); Ok(Json(iface)) } /// POST /api/v1/interfaces/upstreams/preview pub async fn preview_upstream_handler( Json(payload): Json, ) -> ApiResult> { let parsed = UpstreamConfigParser::parse(&payload.config, &payload.name) .map_err(|e| ApiError::Validation(e.to_string()))?; Ok(Json(UpstreamPreviewResponse { name: parsed.interface_name, role: "upstream".to_string(), address_v4: parsed.address_v4.to_string(), address_v6: parsed.address_v6.map(|ip| ip.to_string()), dns: parsed.dns, mtu: parsed.mtu, listen_port: parsed.listen_port, peer_count: 1, provider_public_key: parsed.peer.public_key.as_str().to_string(), provider_endpoint: parsed.peer.endpoint, provider_allowed_ips: parsed.peer.allowed_ips, persistent_keepalive: parsed.peer.persistent_keepalive, preshared_key_configured: parsed.peer.preshared_key.is_some(), })) } /// POST /api/v1/interfaces/upstreams/import pub async fn import_upstream_handler( State(state): State, Json(payload): Json, ) -> ApiResult> { let parsed = UpstreamConfigParser::parse(&payload.config, &payload.name) .map_err(|e| ApiError::Validation(e.to_string()))?; // Check for interface name collision if state .store .get_interface_by_name(&parsed.interface_name) .await? .is_some() { return Err(ApiError::Conflict(format!( "An interface named '{}' already exists", parsed.interface_name ))); } let interface_id = Uuid::new_v4(); let peer_id = Uuid::new_v4(); let psk_configured = parsed.peer.preshared_key.is_some(); let (iface, peer) = parsed.into_desired_state(interface_id, peer_id); // Persist desired state transactionally state.store.create_interface(&iface).await?; if let Err(e) = state.store.create_peer(&peer).await { let _ = state.store.delete_interface(iface.id).await; return Err(ApiError::from(e)); } // Synchronize to kernel / runtime state if let Err(e) = state .wg_engine .sync_interface(&iface, &[peer.clone()]) .await { tracing::error!( interface = %iface.name, error = %e, "Kernel sync failed after upstream import" ); } state.broadcast(SystemEvent::InterfaceChanged { id: iface.id.to_string(), action: "imported".to_string(), }); Ok(Json(UpstreamImportResponse { interface_id: iface.id, peer_id: peer.id, name: iface.name, role: iface.role.to_string(), address_v4: iface.address_v4.to_string(), address_v6: iface.address_v6.map(|ip| ip.to_string()), dns: iface.dns, mtu: iface.mtu, listen_port: iface.listen_port, provider_public_key: peer.public_key.as_str().to_string(), provider_endpoint: peer.endpoint.unwrap_or_default(), provider_allowed_ips: peer.allowed_ips, persistent_keepalive: peer.persistent_keepalive, preshared_key_configured: psk_configured, enabled: iface.enabled, })) } /// GET /api/v1/interfaces/{id} pub async fn get_interface_handler( State(state): State, Path(id): Path, ) -> ApiResult> { let iface = state .store .get_interface(id) .await? .ok_or_else(|| ApiError::NotFound(format!("Interface '{id}' not found")))?; Ok(Json(iface)) } /// PUT /api/v1/interfaces/{id} pub async fn update_interface_handler( State(state): State, Path(id): Path, Json(payload): Json, ) -> ApiResult> { let mut iface = state .store .get_interface(id) .await? .ok_or_else(|| ApiError::NotFound(format!("Interface '{id}' not found")))?; if let Some(ref name) = payload.name { let trimmed = name.trim(); validate_interface_name(trimmed)?; if iface.name != trimmed { if let Ok(Some(existing)) = state.store.get_interface_by_name(trimmed).await && existing.id != iface.id { return Err(ApiError::Validation(format!( "Interface with name '{trimmed}' already exists" ))); } iface.name = trimmed.to_string(); } } if let Some(port) = payload.listen_port { validate_listen_port(port)?; iface.listen_port = Some(port); } if let Some(ref v4) = payload.address_v4 { iface.address_v4 = validate_cidr(v4)?; } if let Some(ref v6) = payload.address_v6 { if v6.trim().is_empty() { iface.address_v6 = None; } else { iface.address_v6 = Some(validate_cidr(v6.trim())?); } } if let Some(m) = payload.mtu { validate_mtu(m)?; iface.mtu = Some(m); } if let Some(ref dns) = payload.dns { if dns.trim().is_empty() { iface.dns = None; } else { iface.dns = Some(dns.trim().to_string()); } } if let Some(en) = payload.enabled { iface.enabled = en; } if payload.pre_up.is_some() { iface.pre_up = payload.pre_up; } if payload.post_up.is_some() { iface.post_up = payload.post_up; } if payload.pre_down.is_some() { iface.pre_down = payload.pre_down; } if payload.post_down.is_some() { iface.post_down = payload.post_down; } iface.updated_at = Utc::now().naive_utc(); state.store.update_interface(&iface).await?; state.broadcast(SystemEvent::InterfaceChanged { id: iface.id.to_string(), action: "updated".to_string(), }); Ok(Json(iface)) } /// DELETE /api/v1/interfaces/{id} pub async fn delete_interface_handler( State(state): State, Path(id): Path, ) -> ApiResult> { let iface = state .store .get_interface(id) .await? .ok_or_else(|| ApiError::NotFound(format!("Interface '{id}' not found")))?; if iface.name == "wg0" { return Err(ApiError::Forbidden( "The primary overlay interface 'wg0' cannot be deleted".to_string(), )); } // 1. Attempt kernel deletion let _ = state.wg_engine.delete_interface(&iface.name).await; // 2. Delete from DB state.store.delete_interface(id).await?; state.broadcast(SystemEvent::InterfaceChanged { id: id.to_string(), action: "deleted".to_string(), }); Ok(Json(GenericSuccess { success: true, message: format!("Interface '{id}' and all associated peers deleted"), })) } /// POST /api/v1/interfaces/{id}/enable pub async fn enable_interface_handler( State(state): State, Path(id): Path, ) -> ApiResult> { state.store.set_interface_enabled(id, true).await?; state.broadcast(SystemEvent::InterfaceChanged { id: id.to_string(), action: "enabled".to_string(), }); Ok(Json(GenericSuccess { success: true, message: format!("Interface '{id}' enabled"), })) } /// POST /api/v1/interfaces/{id}/disable pub async fn disable_interface_handler( State(state): State, Path(id): Path, ) -> ApiResult> { let iface = state .store .get_interface(id) .await? .ok_or_else(|| ApiError::NotFound(format!("Interface '{id}' not found")))?; if iface.name == "wg0" { return Err(ApiError::Forbidden( "The primary overlay interface 'wg0' cannot be disabled".to_string(), )); } state.store.set_interface_enabled(id, false).await?; state.broadcast(SystemEvent::InterfaceChanged { id: id.to_string(), action: "disabled".to_string(), }); Ok(Json(GenericSuccess { success: true, message: format!("Interface '{id}' disabled"), })) } /// GET /api/v1/interfaces/{id}/status pub async fn interface_status_handler( State(state): State, Path(id): Path, ) -> ApiResult> { let iface = state .store .get_interface(id) .await? .ok_or_else(|| ApiError::NotFound(format!("Interface '{id}' not found")))?; let peers = state.store.list_peers_for_interface(id).await?; let active_count = peers .iter() .filter(|p| p.state == nx9_wg_core::types::wireguard::PeerState::Active) .count(); Ok(Json(InterfaceStatusResponse { interface: iface, peer_count: peers.len(), active_peer_count: active_count, })) } /// POST /api/v1/interfaces/{id}/restart pub async fn restart_interface_handler( State(state): State, Path(id): Path, ) -> ApiResult> { let iface = state .store .get_interface(id) .await? .ok_or_else(|| ApiError::NotFound(format!("Interface '{id}' not found")))?; // 1. Tear down the kernel WireGuard interface let _ = state.wg_engine.delete_interface(&iface.name).await; // 2. Re-sync from desired state (recreate link, addresses, peers, routes) let peers = state.store.list_peers_for_interface(iface.id).await?; state .wg_engine .sync_interface(&iface, &peers) .await .map_err(|e| { ApiError::Internal(format!("Failed to restart interface '{}': {e}", iface.name)) })?; state.broadcast(SystemEvent::InterfaceChanged { id: iface.id.to_string(), action: "restarted".to_string(), }); Ok(Json(GenericSuccess { success: true, message: format!("Interface '{}' restarted successfully", iface.name), })) }