release: NX9-WG v1.0.0
This commit is contained in:
1 parent
c8a9b7cde6
commit
4dfe42fe68
42 files changed
+4689
-336
No files matched your search
@@ -215,9 +215,82 @@ pub struct Peer {
|
||||
pub updated_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
impl Peer {
|
||||
/// Returns the effective server-side WireGuard AllowedIPs string for this peer.
|
||||
///
|
||||
/// # Semantics:
|
||||
/// - If `server_allowed_ips` is explicitly configured and non-empty, it is used after
|
||||
/// validating CIDRs. For `RoadWarrior` peers, default full-tunnel routes (`0.0.0.0/0`, `::/0`)
|
||||
/// are strictly forbidden as server-side AllowedIPs.
|
||||
/// - For `RoadWarrior` peers: derives exclusively from the peer's assigned tunnel addresses
|
||||
/// (`address_v4/32` and `address_v6/128`).
|
||||
/// - For non-`RoadWarrior` peers (e.g. `SiteGateway`, `Server`, `Relay`): uses assigned tunnel
|
||||
/// addresses or explicit subnets from `allowed_ips` (excluding `0.0.0.0/0` and `::/0`).
|
||||
///
|
||||
/// # Invariants:
|
||||
/// - NEVER returns `0.0.0.0/0` or `::/0` as server-side AllowedIPs for a RoadWarrior peer.
|
||||
/// - NEVER falls back to client full-tunnel routing policy.
|
||||
pub fn server_wireguard_allowed_ips(&self) -> String {
|
||||
// 1. Explicit server_allowed_ips override
|
||||
if let Some(ref s_allowed) = self.server_allowed_ips {
|
||||
let trimmed = s_allowed.trim();
|
||||
if !trimmed.is_empty() {
|
||||
let mut valid_cidrs = Vec::new();
|
||||
for item in trimmed.split(',') {
|
||||
let item_trim = item.trim();
|
||||
if let Ok(net) = item_trim.parse::<IpNet>() {
|
||||
// For RoadWarrior, reject 0.0.0.0/0 or ::/0
|
||||
if self.peer_type == PeerType::RoadWarrior && net.prefix_len() == 0 {
|
||||
continue;
|
||||
}
|
||||
valid_cidrs.push(net.to_string());
|
||||
}
|
||||
}
|
||||
if !valid_cidrs.is_empty() {
|
||||
return valid_cidrs.join(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Default for RoadWarrior (and default for any peer with assigned addresses):
|
||||
// Derive exclusively from assigned tunnel addresses (/32 and /128)
|
||||
let mut addrs = Vec::new();
|
||||
if let Some(ref v4) = self.address_v4 {
|
||||
addrs.push(format!("{}/32", v4.addr()));
|
||||
}
|
||||
if let Some(ref v6) = self.address_v6 {
|
||||
addrs.push(format!("{}/128", v6.addr()));
|
||||
}
|
||||
|
||||
if !addrs.is_empty() {
|
||||
return addrs.join(", ");
|
||||
}
|
||||
|
||||
// 3. For non-RoadWarrior peers without assigned tunnel addresses (e.g. site gateway routing subnets),
|
||||
// inspect allowed_ips, strictly excluding default 0.0.0.0/0 and ::/0
|
||||
if self.peer_type != PeerType::RoadWarrior {
|
||||
let mut valid_cidrs = Vec::new();
|
||||
for item in self.allowed_ips.split(',') {
|
||||
let item_trim = item.trim();
|
||||
if let Ok(net) = item_trim.parse::<IpNet>()
|
||||
&& net.prefix_len() > 0
|
||||
{
|
||||
valid_cidrs.push(net.to_string());
|
||||
}
|
||||
}
|
||||
if !valid_cidrs.is_empty() {
|
||||
return valid_cidrs.join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::Utc;
|
||||
|
||||
#[test]
|
||||
fn test_peer_type_roundtrip() {
|
||||
@@ -233,4 +306,102 @@ mod tests {
|
||||
let pk = WireGuardPrivateKey::new("secret".to_string());
|
||||
assert_eq!(format!("{:?}", pk), "[REDACTED]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_road_warrior_server_allowed_ips_derives_from_assigned_address() {
|
||||
let now = Utc::now().naive_utc();
|
||||
let peer = Peer {
|
||||
id: Uuid::new_v4(),
|
||||
interface_id: Uuid::new_v4(),
|
||||
name: "Mobile".to_string(),
|
||||
peer_type: PeerType::RoadWarrior,
|
||||
state: PeerState::Active,
|
||||
public_key: WireGuardPublicKey::new("pubkey123".to_string()),
|
||||
private_key: None,
|
||||
preshared_key: None,
|
||||
endpoint: None,
|
||||
allowed_ips: "0.0.0.0/0, ::/0".to_string(), // Client full tunnel routing policy
|
||||
server_allowed_ips: None,
|
||||
address_v4: Some("10.100.0.9/24".parse().unwrap()),
|
||||
address_v6: Some("fd00::9/64".parse().unwrap()),
|
||||
dns: None,
|
||||
mtu: None,
|
||||
persistent_keepalive: Some(25),
|
||||
profile: PeerProfile::FullTunnel,
|
||||
expires_at: None,
|
||||
last_handshake_at: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
// Server-side AllowedIPs MUST be 10.100.0.9/32, fd00::9/128 (never 0.0.0.0/0)
|
||||
assert_eq!(
|
||||
peer.server_wireguard_allowed_ips(),
|
||||
"10.100.0.9/32, fd00::9/128"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_road_warrior_explicit_server_allowed_ips_override() {
|
||||
let now = Utc::now().naive_utc();
|
||||
let peer = Peer {
|
||||
id: Uuid::new_v4(),
|
||||
interface_id: Uuid::new_v4(),
|
||||
name: "Mobile".to_string(),
|
||||
peer_type: PeerType::RoadWarrior,
|
||||
state: PeerState::Active,
|
||||
public_key: WireGuardPublicKey::new("pubkey123".to_string()),
|
||||
private_key: None,
|
||||
preshared_key: None,
|
||||
endpoint: None,
|
||||
allowed_ips: "0.0.0.0/0, ::/0".to_string(),
|
||||
server_allowed_ips: Some("10.100.0.9/32, 192.168.50.0/24".to_string()),
|
||||
address_v4: Some("10.100.0.9/32".parse().unwrap()),
|
||||
address_v6: None,
|
||||
dns: None,
|
||||
mtu: None,
|
||||
persistent_keepalive: Some(25),
|
||||
profile: PeerProfile::FullTunnel,
|
||||
expires_at: None,
|
||||
last_handshake_at: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
peer.server_wireguard_allowed_ips(),
|
||||
"10.100.0.9/32, 192.168.50.0/24"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_road_warrior_forbids_default_route_as_server_allowed_ips() {
|
||||
let now = Utc::now().naive_utc();
|
||||
let peer = Peer {
|
||||
id: Uuid::new_v4(),
|
||||
interface_id: Uuid::new_v4(),
|
||||
name: "Mobile".to_string(),
|
||||
peer_type: PeerType::RoadWarrior,
|
||||
state: PeerState::Active,
|
||||
public_key: WireGuardPublicKey::new("pubkey123".to_string()),
|
||||
private_key: None,
|
||||
preshared_key: None,
|
||||
endpoint: None,
|
||||
allowed_ips: "0.0.0.0/0, ::/0".to_string(),
|
||||
server_allowed_ips: Some("0.0.0.0/0".to_string()), // Attempt to set full tunnel as server allowed ips
|
||||
address_v4: Some("10.100.0.9/32".parse().unwrap()),
|
||||
address_v6: None,
|
||||
dns: None,
|
||||
mtu: None,
|
||||
persistent_keepalive: Some(25),
|
||||
profile: PeerProfile::FullTunnel,
|
||||
expires_at: None,
|
||||
last_handshake_at: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
// Rejects 0.0.0.0/0 and falls back to assigned address 10.100.0.9/32
|
||||
assert_eq!(peer.server_wireguard_allowed_ips(), "10.100.0.9/32");
|
||||
}
|
||||
}
|
||||
@@ -146,6 +146,27 @@ pub fn validate_ip_in_network(ip: IpAddr, net: IpNet) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate a comma-separated list of CIDR subnets (e.g. "10.100.0.9/32, 192.168.1.0/24").
|
||||
pub fn validate_allowed_ips_cidr_list(list: &str) -> Result<Vec<IpNet>> {
|
||||
let mut nets = Vec::new();
|
||||
for item in list.split(',') {
|
||||
let trimmed = item.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let net = validate_cidr(trimmed)?;
|
||||
if !nets.contains(&net) {
|
||||
nets.push(net);
|
||||
}
|
||||
}
|
||||
if nets.is_empty() {
|
||||
return Err(Nx9Error::Validation(
|
||||
"AllowedIPs list cannot be empty".into(),
|
||||
));
|
||||
}
|
||||
Ok(nets)
|
||||
}
|
||||
|
||||
/// Validate port.
|
||||
pub fn validate_port(port: u16) -> Result<u16> {
|
||||
if port == 0 {
|
||||
|
||||
Reference in new issue
Block a user