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

+24
View File
@@ -29,3 +29,27 @@ impl std::fmt::Debug for Setting {
.finish()
}
}
/// Persistent WireGuard server endpoint configuration.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServerEndpointSettings {
pub host: String,
pub port: u16,
pub enabled: bool,
}
impl Default for ServerEndpointSettings {
fn default() -> Self {
Self {
host: String::new(),
port: 51820,
enabled: true,
}
}
}
pub const SETTING_SERVER_HOST: &str = "wireguard.server_host";
pub const SETTING_SERVER_PORT: &str = "wireguard.server_port";
pub const SETTING_SERVER_ENDPOINT_ENABLED: &str = "wireguard.server_endpoint_enabled";
pub const LEGACY_SETTING_SERVER_ENDPOINT: &str = "server_endpoint";
pub const LEGACY_SETTING_PUBLIC_ENDPOINT: &str = "public_endpoint";
+177
View File
@@ -269,6 +269,120 @@ pub fn validate_client_mtu(mtu: u16) -> Result<u16> {
Ok(mtu)
}
/// Validate server host or IP for WireGuard server endpoint settings.
///
/// Rules:
/// - Trim surrounding whitespace.
/// - Reject empty host.
/// - Accept valid DNS hostname.
/// - Accept valid IPv4 address.
/// - Accept valid IPv6 address (e.g. 2001:db8::10 or [2001:db8::10]).
/// - Reject embedded port syntax (e.g. example.com:51820, 192.168.1.1:51820, [::1]:51820)
/// with an explicit error indicating that port belongs in the separate port field.
pub fn validate_server_host(host: &str) -> Result<String> {
let trimmed = host.trim();
if trimmed.is_empty() {
return Err(Nx9Error::Validation(
"server host / IP cannot be empty".into(),
));
}
// Check if bracketed IPv6 (e.g. [2001:db8::10] or [2001:db8::10]:51820)
if trimmed.starts_with('[') {
if let Some(closing) = trimmed.find(']') {
let inside = &trimmed[1..closing];
if closing + 1 < trimmed.len() {
// Contains characters after bracket, likely a port
return Err(Nx9Error::Validation(
"server host must not include a port; specify the port in the Client Endpoint Port field".into(),
));
}
if inside.parse::<std::net::Ipv6Addr>().is_ok() {
return Ok(inside.to_string());
}
}
return Err(Nx9Error::Validation(format!(
"invalid IPv6 server host '{trimmed}'"
)));
}
// Check if direct unbracketed IPv6
if let Ok(ipv6) = trimmed.parse::<std::net::Ipv6Addr>() {
return Ok(ipv6.to_string());
}
// If it contains a colon and was not parsed as IPv6 above, it has an embedded port or is invalid
if trimmed.contains(':') {
return Err(Nx9Error::Validation(
"server host must not include a port; specify the port in the Client Endpoint Port field".into(),
));
}
// Check if IPv4
if let Ok(ipv4) = trimmed.parse::<std::net::Ipv4Addr>() {
return Ok(ipv4.to_string());
}
// Validate DNS hostname (RFC 1123 / RFC 952)
if trimmed.len() > 253 {
return Err(Nx9Error::Validation(
"server hostname exceeds maximum length of 253 characters".into(),
));
}
for label in trimmed.split('.') {
if label.is_empty() {
return Err(Nx9Error::Validation(format!(
"invalid hostname '{trimmed}': empty label"
)));
}
if label.len() > 63 {
return Err(Nx9Error::Validation(format!(
"invalid hostname '{trimmed}': label '{label}' exceeds 63 characters"
)));
}
if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
return Err(Nx9Error::Validation(format!(
"invalid hostname '{trimmed}': contains invalid characters"
)));
}
if label.starts_with('-') || label.ends_with('-') {
return Err(Nx9Error::Validation(format!(
"invalid hostname '{trimmed}': label '{label}' cannot start or end with hyphen"
)));
}
}
Ok(trimmed.to_string())
}
/// Validate WireGuard client endpoint port (range 1..=65535).
pub fn validate_server_port(port: u16) -> Result<u16> {
if port == 0 {
return Err(Nx9Error::Validation(
"server endpoint port must be between 1 and 65535".into(),
));
}
Ok(port)
}
/// Format host and port into a standard WireGuard Endpoint string.
///
/// Formats IPv6 as `[host]:port` and hostname/IPv4 as `host:port`.
pub fn format_endpoint(host: &str, port: u16) -> String {
let trimmed = host.trim();
let unbracketed = trimmed
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.unwrap_or(trimmed);
if unbracketed.parse::<std::net::Ipv6Addr>().is_ok() || unbracketed.contains(':') {
format!("[{}]:{}", unbracketed, port)
} else {
format!("{}:{}", unbracketed, port)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -355,4 +469,67 @@ mod tests {
assert!(validate_client_mtu(9001).is_err());
assert!(validate_client_mtu(65535).is_err());
}
#[test]
fn test_validate_server_host_valid() {
assert_eq!(
validate_server_host("vpn.thakares.com").unwrap(),
"vpn.thakares.com"
);
assert_eq!(
validate_server_host(" 203.0.113.10 ").unwrap(),
"203.0.113.10"
);
assert_eq!(
validate_server_host("2001:db8::10").unwrap(),
"2001:db8::10"
);
assert_eq!(
validate_server_host("[2001:db8::10]").unwrap(),
"2001:db8::10"
);
assert_eq!(
validate_server_host("vpn-node-01.internal").unwrap(),
"vpn-node-01.internal"
);
assert_eq!(validate_server_host("localhost").unwrap(), "localhost");
}
#[test]
fn test_validate_server_host_invalid() {
assert!(validate_server_host("").is_err());
assert!(validate_server_host(" ").is_err());
// Embedded ports rejected
assert!(validate_server_host("vpn.thakares.com:51820").is_err());
assert!(validate_server_host("203.0.113.10:51820").is_err());
assert!(validate_server_host("[2001:db8::10]:51820").is_err());
// Invalid hostname characters
assert!(validate_server_host("vpn$host.com").is_err());
assert!(validate_server_host("-invalid.com").is_err());
}
#[test]
fn test_validate_server_port() {
assert_eq!(validate_server_port(1).unwrap(), 1);
assert_eq!(validate_server_port(51820).unwrap(), 51820);
assert_eq!(validate_server_port(65535).unwrap(), 65535);
assert!(validate_server_port(0).is_err());
}
#[test]
fn test_format_endpoint() {
assert_eq!(
format_endpoint("vpn.thakares.com", 51820),
"vpn.thakares.com:51820"
);
assert_eq!(format_endpoint("203.0.113.10", 51820), "203.0.113.10:51820");
assert_eq!(
format_endpoint("2001:db8::10", 51820),
"[2001:db8::10]:51820"
);
assert_eq!(
format_endpoint("[2001:db8::10]", 51820),
"[2001:db8::10]:51820"
);
}
}