Finalize nx9-wg production release
This commit is contained in:
1 parent
4dfe42fe68
commit
d704c1e131
30 files changed
+2502
-370
No files matched your search
@@ -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))
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user