250 lines
7.8 KiB
Rust
250 lines
7.8 KiB
Rust
//! WireGuard Interface repository operations.
|
|
|
|
use crate::error::{DbError, Result};
|
|
use crate::models::{format_datetime, parse_datetime};
|
|
use chrono::Utc;
|
|
use ipnet::IpNet;
|
|
use nx9_wg_core::types::wireguard::{
|
|
Interface, InterfaceRole, WireGuardPrivateKey, WireGuardPublicKey,
|
|
};
|
|
use sqlx::{Row, SqlitePool};
|
|
use std::str::FromStr;
|
|
use uuid::Uuid;
|
|
|
|
/// Helper to convert a database row into an `Interface` domain struct.
|
|
fn row_to_interface(r: &sqlx::sqlite::SqliteRow) -> Result<Interface> {
|
|
let id_str: String = r.try_get("id")?;
|
|
let name: String = r.try_get("name")?;
|
|
let role_str: Option<String> = r.try_get("role").ok();
|
|
let private_key_str: String = r.try_get("private_key")?;
|
|
let public_key_str: String = r.try_get("public_key")?;
|
|
let listen_port_i64: Option<i64> = r.try_get("listen_port")?;
|
|
let ipv4_cidr_str: String = r.try_get("ipv4_cidr")?;
|
|
let ipv6_cidr_str: Option<String> = r.try_get("ipv6_cidr")?;
|
|
let mtu_i64: Option<i64> = r.try_get("mtu")?;
|
|
let dns: Option<String> = r.try_get("dns")?;
|
|
let enabled_i64: i64 = r.try_get("enabled")?;
|
|
let pre_up: Option<String> = r.try_get("pre_up")?;
|
|
let post_up: Option<String> = r.try_get("post_up")?;
|
|
let pre_down: Option<String> = r.try_get("pre_down")?;
|
|
let post_down: Option<String> = r.try_get("post_down")?;
|
|
let created_at_str: String = r.try_get("created_at")?;
|
|
let updated_at_str: String = r.try_get("updated_at")?;
|
|
|
|
let id = Uuid::parse_str(&id_str)
|
|
.map_err(|e| DbError::Validation(format!("invalid interface UUID '{id_str}': {e}")))?;
|
|
|
|
let role = match role_str.as_deref() {
|
|
Some("upstream") => InterfaceRole::Upstream,
|
|
_ => InterfaceRole::Overlay,
|
|
};
|
|
|
|
let address_v4 = IpNet::from_str(&ipv4_cidr_str)
|
|
.map_err(|e| DbError::Validation(format!("invalid ipv4_cidr '{ipv4_cidr_str}': {e}")))?;
|
|
|
|
let address_v6 = match ipv6_cidr_str {
|
|
Some(s) => Some(
|
|
IpNet::from_str(&s)
|
|
.map_err(|e| DbError::Validation(format!("invalid ipv6_cidr '{s}': {e}")))?,
|
|
),
|
|
None => None,
|
|
};
|
|
|
|
Ok(Interface {
|
|
id,
|
|
name,
|
|
role,
|
|
private_key: WireGuardPrivateKey::new(private_key_str),
|
|
public_key: WireGuardPublicKey::new(public_key_str),
|
|
listen_port: listen_port_i64.map(|p| p as u16),
|
|
address_v4,
|
|
address_v6,
|
|
mtu: mtu_i64.map(|m| m as u16),
|
|
dns,
|
|
enabled: enabled_i64 != 0,
|
|
pre_up,
|
|
post_up,
|
|
pre_down,
|
|
post_down,
|
|
created_at: parse_datetime(&created_at_str)?,
|
|
updated_at: parse_datetime(&updated_at_str)?,
|
|
})
|
|
}
|
|
|
|
/// Create a new WireGuard interface desired configuration record.
|
|
pub async fn create_interface(pool: &SqlitePool, iface: &Interface) -> Result<()> {
|
|
let id_str = iface.id.to_string();
|
|
let ipv4_str = iface.address_v4.to_string();
|
|
let ipv6_str = iface.address_v6.as_ref().map(|ip| ip.to_string());
|
|
let created_at_str = format_datetime(&iface.created_at);
|
|
let updated_at_str = format_datetime(&iface.updated_at);
|
|
|
|
sqlx::query(
|
|
r#"
|
|
INSERT INTO interfaces (
|
|
id, name, role, private_key, public_key, listen_port, ipv4_cidr, ipv6_cidr,
|
|
mtu, dns, enabled, pre_up, post_up, pre_down, post_down, created_at, updated_at
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
"#,
|
|
)
|
|
.bind(&id_str)
|
|
.bind(&iface.name)
|
|
.bind(iface.role.as_str())
|
|
.bind(iface.private_key.as_str())
|
|
.bind(iface.public_key.as_str())
|
|
.bind(iface.listen_port.map(|p| p as i64))
|
|
.bind(&ipv4_str)
|
|
.bind(ipv6_str)
|
|
.bind(iface.mtu.map(|m| m as i64))
|
|
.bind(&iface.dns)
|
|
.bind(if iface.enabled { 1 } else { 0 })
|
|
.bind(&iface.pre_up)
|
|
.bind(&iface.post_up)
|
|
.bind(&iface.pre_down)
|
|
.bind(&iface.post_down)
|
|
.bind(&created_at_str)
|
|
.bind(&updated_at_str)
|
|
.execute(pool)
|
|
.await
|
|
.map_err(|e| match &e {
|
|
sqlx::Error::Database(dbe) if dbe.is_unique_violation() => DbError::Conflict(format!(
|
|
"Interface with name '{}' already exists",
|
|
iface.name
|
|
)),
|
|
_ => DbError::Sqlx(e),
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Retrieve an interface by its UUID.
|
|
pub async fn get_interface(pool: &SqlitePool, id: Uuid) -> Result<Option<Interface>> {
|
|
let id_str = id.to_string();
|
|
let row = sqlx::query("SELECT * FROM interfaces WHERE id = ?")
|
|
.bind(&id_str)
|
|
.fetch_optional(pool)
|
|
.await
|
|
.map_err(DbError::Sqlx)?;
|
|
|
|
match row {
|
|
Some(r) => Ok(Some(row_to_interface(&r)?)),
|
|
None => Ok(None),
|
|
}
|
|
}
|
|
|
|
/// Retrieve an interface by its name.
|
|
pub async fn get_interface_by_name(pool: &SqlitePool, name: &str) -> Result<Option<Interface>> {
|
|
let row = sqlx::query("SELECT * FROM interfaces WHERE name = ?")
|
|
.bind(name)
|
|
.fetch_optional(pool)
|
|
.await
|
|
.map_err(DbError::Sqlx)?;
|
|
|
|
match row {
|
|
Some(r) => Ok(Some(row_to_interface(&r)?)),
|
|
None => Ok(None),
|
|
}
|
|
}
|
|
|
|
/// List all interfaces.
|
|
pub async fn list_interfaces(pool: &SqlitePool) -> Result<Vec<Interface>> {
|
|
let rows = sqlx::query("SELECT * FROM interfaces ORDER BY name ASC")
|
|
.fetch_all(pool)
|
|
.await
|
|
.map_err(DbError::Sqlx)?;
|
|
|
|
let mut list = Vec::with_capacity(rows.len());
|
|
for r in rows {
|
|
list.push(row_to_interface(&r)?);
|
|
}
|
|
Ok(list)
|
|
}
|
|
|
|
/// Update an interface record.
|
|
pub async fn update_interface(pool: &SqlitePool, iface: &Interface) -> Result<()> {
|
|
let id_str = iface.id.to_string();
|
|
let ipv4_str = iface.address_v4.to_string();
|
|
let ipv6_str = iface.address_v6.as_ref().map(|ip| ip.to_string());
|
|
let now = Utc::now().naive_utc();
|
|
let now_str = format_datetime(&now);
|
|
|
|
let result = sqlx::query(
|
|
r#"
|
|
UPDATE interfaces
|
|
SET name = ?, role = ?, private_key = ?, public_key = ?, listen_port = ?,
|
|
ipv4_cidr = ?, ipv6_cidr = ?, mtu = ?, dns = ?, enabled = ?,
|
|
pre_up = ?, post_up = ?, pre_down = ?, post_down = ?, updated_at = ?
|
|
WHERE id = ?
|
|
"#,
|
|
)
|
|
.bind(&iface.name)
|
|
.bind(iface.role.as_str())
|
|
.bind(iface.private_key.as_str())
|
|
.bind(iface.public_key.as_str())
|
|
.bind(iface.listen_port.map(|p| p as i64))
|
|
.bind(&ipv4_str)
|
|
.bind(ipv6_str)
|
|
.bind(iface.mtu.map(|m| m as i64))
|
|
.bind(&iface.dns)
|
|
.bind(if iface.enabled { 1 } else { 0 })
|
|
.bind(&iface.pre_up)
|
|
.bind(&iface.post_up)
|
|
.bind(&iface.pre_down)
|
|
.bind(&iface.post_down)
|
|
.bind(&now_str)
|
|
.bind(&id_str)
|
|
.execute(pool)
|
|
.await
|
|
.map_err(DbError::Sqlx)?;
|
|
|
|
if result.rows_affected() == 0 {
|
|
return Err(DbError::NotFound(format!("Interface '{id_str}' not found")));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Delete an interface by UUID. Peers are deleted automatically via ON DELETE CASCADE.
|
|
pub async fn delete_interface(pool: &SqlitePool, id: Uuid) -> Result<()> {
|
|
let id_str = id.to_string();
|
|
let result = sqlx::query("DELETE FROM interfaces WHERE id = ?")
|
|
.bind(&id_str)
|
|
.execute(pool)
|
|
.await
|
|
.map_err(DbError::Sqlx)?;
|
|
|
|
if result.rows_affected() == 0 {
|
|
return Err(DbError::NotFound(format!("Interface '{id_str}' not found")));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Enable or disable an interface.
|
|
pub async fn set_interface_enabled(pool: &SqlitePool, id: Uuid, enabled: bool) -> Result<()> {
|
|
let id_str = id.to_string();
|
|
let now = Utc::now().naive_utc();
|
|
let now_str = format_datetime(&now);
|
|
|
|
let result = sqlx::query(
|
|
r#"
|
|
UPDATE interfaces
|
|
SET enabled = ?, updated_at = ?
|
|
WHERE id = ?
|
|
"#,
|
|
)
|
|
.bind(if enabled { 1 } else { 0 })
|
|
.bind(&now_str)
|
|
.bind(&id_str)
|
|
.execute(pool)
|
|
.await
|
|
.map_err(DbError::Sqlx)?;
|
|
|
|
if result.rows_affected() == 0 {
|
|
return Err(DbError::NotFound(format!("Interface '{id_str}' not found")));
|
|
}
|
|
|
|
Ok(())
|
|
}
|