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
@@ -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";
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user