Finalize nx9-wg production release

This commit is contained in:
thakares committed 2026-08-18 22:27:36 +05:30
1 parent 4dfe42fe68
commit d704c1e131
30 files changed
+2502 -370

No files matched your search

+256 -1
View File
@@ -3,7 +3,11 @@
use crate::error::{DbError, Result};
use crate::models::{format_datetime, parse_datetime};
use chrono::Utc;
use nx9_wg_core::types::settings::Setting;
use nx9_wg_core::types::settings::{
LEGACY_SETTING_PUBLIC_ENDPOINT, LEGACY_SETTING_SERVER_ENDPOINT,
SETTING_SERVER_ENDPOINT_ENABLED, SETTING_SERVER_HOST, SETTING_SERVER_PORT,
ServerEndpointSettings, Setting,
};
use sqlx::{Row, SqlitePool};
/// Retrieve a setting by its key.
@@ -99,3 +103,254 @@ pub async fn list_settings(pool: &SqlitePool) -> Result<Vec<Setting>> {
Ok(list)
}
/// Retrieve structured server endpoint settings from the database with legacy fallback.
pub async fn get_server_endpoint_settings(pool: &SqlitePool) -> Result<ServerEndpointSettings> {
let host_opt = get_setting_value(pool, SETTING_SERVER_HOST).await?;
let port_opt = get_setting_value(pool, SETTING_SERVER_PORT).await?;
let enabled_opt = get_setting_value(pool, SETTING_SERVER_ENDPOINT_ENABLED).await?;
let enabled = enabled_opt
.as_deref()
.map(|v| {
let t = v.trim();
t.parse::<bool>().unwrap_or_else(|_| t == "1")
})
.unwrap_or(true);
let port = port_opt
.as_deref()
.and_then(|v| v.trim().parse::<u16>().ok())
.filter(|&p| p > 0)
.unwrap_or(51820);
if let Some(host) = host_opt.filter(|h| !h.trim().is_empty()) {
return Ok(ServerEndpointSettings {
host: host.trim().to_string(),
port,
enabled,
});
}
// Legacy fallback: inspect server_endpoint
if let Some(legacy) = get_setting_value(pool, LEGACY_SETTING_SERVER_ENDPOINT).await? {
let trimmed = legacy.trim();
if !trimmed.is_empty() {
let (legacy_host, legacy_port) = split_host_port(trimmed, port);
return Ok(ServerEndpointSettings {
host: legacy_host,
port: legacy_port,
enabled,
});
}
}
// Legacy fallback: inspect public_endpoint
if let Some(legacy) = get_setting_value(pool, LEGACY_SETTING_PUBLIC_ENDPOINT).await? {
let trimmed = legacy.trim();
if !trimmed.is_empty() {
let (legacy_host, legacy_port) = split_host_port(trimmed, port);
return Ok(ServerEndpointSettings {
host: legacy_host,
port: legacy_port,
enabled,
});
}
}
Ok(ServerEndpointSettings {
host: String::new(),
port,
enabled,
})
}
/// Persist structured server endpoint settings.
pub async fn set_server_endpoint_settings(
pool: &SqlitePool,
settings: &ServerEndpointSettings,
) -> Result<()> {
let host_trimmed = settings.host.trim();
if settings.enabled && !host_trimmed.is_empty() {
nx9_wg_core::validation::validate_server_host(host_trimmed)?;
nx9_wg_core::validation::validate_server_port(settings.port)?;
}
set_setting(pool, SETTING_SERVER_HOST, host_trimmed, false).await?;
set_setting(pool, SETTING_SERVER_PORT, &settings.port.to_string(), false).await?;
set_setting(
pool,
SETTING_SERVER_ENDPOINT_ENABLED,
&settings.enabled.to_string(),
false,
)
.await?;
// Synchronize legacy server_endpoint setting for backwards compatibility
if settings.enabled && !host_trimmed.is_empty() {
let formatted = nx9_wg_core::validation::format_endpoint(host_trimmed, settings.port);
set_setting(pool, LEGACY_SETTING_SERVER_ENDPOINT, &formatted, false).await?;
} else {
delete_setting(pool, LEGACY_SETTING_SERVER_ENDPOINT).await?;
delete_setting(pool, LEGACY_SETTING_PUBLIC_ENDPOINT).await?;
}
Ok(())
}
/// Authoritative server endpoint resolver.
///
/// Precedence:
/// 1. Explicit endpoint override (if non-empty)
/// 2. Persistent `wireguard.server_*` settings (when enabled and host non-empty)
/// 3. Legacy `server_endpoint` setting (if non-empty)
/// 4. Legacy `public_endpoint` setting (if non-empty)
/// 5. Actionable error explaining how to configure server endpoint or provide `--endpoint`.
pub async fn resolve_server_endpoint(
pool: &SqlitePool,
explicit_override: Option<&str>,
) -> Result<String> {
// 1. Explicit endpoint override
if let Some(ep) = explicit_override {
let trimmed = ep.trim();
if !trimmed.is_empty() {
return parse_and_normalize_endpoint(trimmed);
}
}
// Check enabled toggle
let enabled_opt = get_setting_value(pool, SETTING_SERVER_ENDPOINT_ENABLED).await?;
let enabled = enabled_opt
.as_deref()
.map(|v| {
let t = v.trim();
t.parse::<bool>().unwrap_or_else(|_| t == "1")
})
.unwrap_or(true);
if !enabled {
return Err(DbError::Validation(
"No reachable WireGuard server endpoint is configured. Configure WireGuard Server Endpoint in Settings or provide --endpoint.".to_string(),
));
}
// 2. Persistent wireguard.server_* settings
let host_opt = get_setting_value(pool, SETTING_SERVER_HOST).await?;
let port_opt = get_setting_value(pool, SETTING_SERVER_PORT).await?;
let port = port_opt
.as_deref()
.and_then(|v| v.trim().parse::<u16>().ok())
.filter(|&p| p > 0)
.unwrap_or(51820);
if let Some(host) = host_opt.filter(|h| !h.trim().is_empty()) {
let validated_host = nx9_wg_core::validation::validate_server_host(&host)?;
let validated_port = nx9_wg_core::validation::validate_server_port(port)?;
return Ok(nx9_wg_core::validation::format_endpoint(
&validated_host,
validated_port,
));
}
// 3. Legacy server_endpoint fallback
if let Some(legacy) = get_setting_value(pool, LEGACY_SETTING_SERVER_ENDPOINT).await? {
let trimmed = legacy.trim();
if !trimmed.is_empty() {
return parse_and_normalize_endpoint(trimmed);
}
}
// 4. Legacy public_endpoint fallback
if let Some(legacy) = get_setting_value(pool, LEGACY_SETTING_PUBLIC_ENDPOINT).await? {
let trimmed = legacy.trim();
if !trimmed.is_empty() {
return parse_and_normalize_endpoint(trimmed);
}
}
// 5. Actionable error
Err(DbError::Validation(
"No reachable WireGuard server endpoint is configured. Configure WireGuard Server Endpoint in Settings or provide --endpoint.".to_string(),
))
}
fn split_host_port(s: &str, default_port: u16) -> (String, u16) {
let trimmed = s.trim();
if trimmed.starts_with('[')
&& let Some(closing) = trimmed.find(']')
{
let host_part = &trimmed[1..closing];
let rest = &trimmed[closing + 1..];
if let Some(port_str) = rest.strip_prefix(':')
&& let Ok(port) = port_str.parse::<u16>()
&& port > 0
{
return (host_part.to_string(), port);
}
return (host_part.to_string(), default_port);
}
if let Ok(ipv6) = trimmed.parse::<std::net::Ipv6Addr>() {
return (ipv6.to_string(), default_port);
}
if let Some(last_colon) = trimmed.rfind(':') {
let host_part = &trimmed[..last_colon];
let port_part = &trimmed[last_colon + 1..];
if let Ok(port) = port_part.parse::<u16>()
&& port > 0
{
return (host_part.to_string(), port);
}
}
(trimmed.to_string(), default_port)
}
fn parse_and_normalize_endpoint(ep: &str) -> Result<String> {
let trimmed = ep.trim();
if trimmed.is_empty() {
return Err(DbError::Validation("endpoint cannot be empty".into()));
}
if trimmed.starts_with('[')
&& let Some(closing) = trimmed.find(']')
{
let host_part = &trimmed[1..closing];
let ipv6 = host_part.parse::<std::net::Ipv6Addr>().map_err(|e| {
DbError::Validation(format!("invalid IPv6 in endpoint '{trimmed}': {e}"))
})?;
let rest = &trimmed[closing + 1..];
let port = if let Some(port_str) = rest.strip_prefix(':') {
port_str
.parse::<u16>()
.map_err(|_| DbError::Validation(format!("invalid port in endpoint '{trimmed}'")))?
} else if rest.is_empty() {
51820
} else {
return Err(DbError::Validation(format!(
"invalid endpoint format '{trimmed}'"
)));
};
if port == 0 {
return Err(DbError::Validation("port must be non-zero".into()));
}
return Ok(format!("[{}]:{}", ipv6, port));
}
if let Ok(ipv6) = trimmed.parse::<std::net::Ipv6Addr>() {
return Ok(format!("[{}]:51820", ipv6));
}
if let Some(last_colon) = trimmed.rfind(':') {
let host_part = &trimmed[..last_colon];
let port_part = &trimmed[last_colon + 1..];
if let Ok(port) = port_part.parse::<u16>() {
if port == 0 {
return Err(DbError::Validation("port must be non-zero".into()));
}
let host = nx9_wg_core::validation::validate_server_host(host_part)?;
return Ok(nx9_wg_core::validation::format_endpoint(&host, port));
}
}
let host = nx9_wg_core::validation::validate_server_host(trimmed)?;
Ok(nx9_wg_core::validation::format_endpoint(&host, 51820))
}
+17
View File
@@ -524,6 +524,23 @@ impl Store {
crate::settings::list_settings(&self.pool).await
}
pub async fn get_server_endpoint_settings(
&self,
) -> Result<nx9_wg_core::types::settings::ServerEndpointSettings> {
crate::settings::get_server_endpoint_settings(&self.pool).await
}
pub async fn set_server_endpoint_settings(
&self,
settings: &nx9_wg_core::types::settings::ServerEndpointSettings,
) -> Result<()> {
crate::settings::set_server_endpoint_settings(&self.pool, settings).await
}
pub async fn resolve_server_endpoint(&self, explicit_override: Option<&str>) -> Result<String> {
crate::settings::resolve_server_endpoint(&self.pool, explicit_override).await
}
// Audit
pub async fn create_audit_event(
&self,