Release v1.1.0

This commit is contained in:
thakares committed 2026-09-02 15:19:19 +05:30
1 parent 34227efd2b
commit edc710cbd2
46 files changed
+5324 -190

No files matched your search

+244 -6
View File
@@ -1,5 +1,3 @@
//! WireGuard Interface HTTP handlers.
use crate::error::{ApiError, ApiResult};
use crate::routes::auth::GenericSuccess;
use crate::state::{AppState, SystemEvent};
@@ -7,16 +5,20 @@ 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, WireGuardPrivateKey, WireGuardPublicKey};
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<InterfaceRole>,
pub listen_port: Option<u16>,
pub address_v4: String,
pub address_v6: Option<String>,
@@ -30,6 +32,54 @@ pub struct CreateInterfaceRequest {
pub post_down: Option<String>,
}
#[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<String>,
pub dns: Option<String>,
pub mtu: Option<u16>,
pub listen_port: Option<u16>,
pub peer_count: usize,
pub provider_public_key: String,
pub provider_endpoint: String,
pub provider_allowed_ips: String,
pub persistent_keepalive: Option<u16>,
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<String>,
pub dns: Option<String>,
pub mtu: Option<u16>,
pub listen_port: Option<u16>,
pub provider_public_key: String,
pub provider_endpoint: String,
pub provider_allowed_ips: String,
pub persistent_keepalive: Option<u16>,
pub preshared_key_configured: bool,
pub enabled: bool,
}
#[derive(Debug, Deserialize)]
pub struct UpdateInterfaceRequest {
pub name: Option<String>,
@@ -66,6 +116,30 @@ pub async fn create_interface_handler(
Json(payload): Json<CreateInterfaceRequest>,
) -> ApiResult<Json<Interface>> {
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)?),
@@ -73,8 +147,14 @@ pub async fn create_interface_handler(
};
let listen_port = match payload.listen_port {
Some(p) => validate_listen_port(p)?,
None => 51820,
Some(p) => Some(validate_listen_port(p)?),
None => {
if role == InterfaceRole::Overlay {
Some(51820)
} else {
None
}
}
};
if let Some(m) = payload.mtu {
@@ -93,6 +173,7 @@ pub async fn create_interface_handler(
let iface = Interface {
id: Uuid::new_v4(),
name: payload.name,
role,
private_key: priv_k,
public_key: pub_k,
listen_port,
@@ -119,6 +200,100 @@ pub async fn create_interface_handler(
Ok(Json(iface))
}
/// POST /api/v1/interfaces/upstreams/preview
pub async fn preview_upstream_handler(
Json(payload): Json<UpstreamPreviewRequest>,
) -> ApiResult<Json<UpstreamPreviewResponse>> {
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<AppState>,
Json(payload): Json<UpstreamImportRequest>,
) -> ApiResult<Json<UpstreamImportResponse>> {
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<AppState>,
@@ -160,7 +335,7 @@ pub async fn update_interface_handler(
}
if let Some(port) = payload.listen_port {
validate_listen_port(port)?;
iface.listen_port = port;
iface.listen_port = Some(port);
}
if let Some(ref v4) = payload.address_v4 {
iface.address_v4 = validate_cidr(v4)?;
@@ -216,6 +391,22 @@ pub async fn delete_interface_handler(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> ApiResult<Json<GenericSuccess>> {
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 {
@@ -252,6 +443,18 @@ pub async fn disable_interface_handler(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> ApiResult<Json<GenericSuccess>> {
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 {
@@ -288,3 +491,38 @@ pub async fn interface_status_handler(
active_peer_count: active_count,
}))
}
/// POST /api/v1/interfaces/{id}/restart
pub async fn restart_interface_handler(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> ApiResult<Json<GenericSuccess>> {
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),
}))
}