fix: include selected networks in dataplane NAT

This commit is contained in:
thakares committed 2026-09-01 18:21:58 +05:30
1 parent 5599e1b5c8
commit 34227efd2b
10 files changed
+545 -54

No files matched your search

@@ -5,10 +5,12 @@ use axum::body::Body;
use axum::http::{Request, StatusCode};
use chrono::Utc;
use ipnet::IpNet;
use nx9_wg_api::collect_managed_wg_subnets;
use nx9_wg_api::reconciliation::ReconciliationEngine;
use nx9_wg_api::routes::build_api_router;
use nx9_wg_api::state::AppState;
use nx9_wg_core::crypto::generate_keypair;
use nx9_wg_core::types::network::Network;
use nx9_wg_core::types::wireguard::{Interface, Peer, PeerProfile, PeerState, PeerType};
use nx9_wg_db::Store;
use nx9_wg_network::{NetworkEngine, SimulatedNetworkEngine};
@@ -344,6 +346,149 @@ async fn test_forwarding_and_nat_reconciliation_invariants() {
assert_eq!(plan.interface_changes, 0);
}
#[tokio::test]
async fn test_selected_network_dataplane_nat_and_routes() {
let (state, iface, _peer, _session_id) = setup_test_context().await;
let now = Utc::now().naive_utc();
let network = Network {
id: Uuid::new_v4(),
name: "mobile-clients".to_string(),
cidr: IpNet::from_str("10.100.2.0/24").unwrap(),
enabled: true,
description: None,
created_at: now,
updated_at: now,
};
state.store.create_network(&network).await.unwrap();
let (peer_priv, peer_pub) = generate_keypair();
let selected_peer = Peer {
id: Uuid::new_v4(),
interface_id: iface.id,
name: "test-mobile".to_string(),
peer_type: PeerType::RoadWarrior,
state: PeerState::Active,
public_key: peer_pub,
private_key: Some(peer_priv),
preshared_key: None,
endpoint: None,
allowed_ips: "0.0.0.0/0, ::/0".to_string(),
server_allowed_ips: None,
address_v4: Some(IpNet::from_str("10.100.2.1/32").unwrap()),
address_v6: None,
dns: Some("1.1.1.1, 1.0.0.1".to_string()),
mtu: Some(1280),
persistent_keepalive: Some(25),
profile: PeerProfile::FullTunnel,
expires_at: None,
last_handshake_at: None,
created_at: now,
updated_at: now,
};
state.store.create_peer(&selected_peer).await.unwrap();
assert_eq!(
selected_peer.server_wireguard_allowed_ips(),
"10.100.2.1/32",
"server-side AllowedIPs must remain the assigned selected-Network address"
);
assert_eq!(selected_peer.allowed_ips, "0.0.0.0/0, ::/0");
let subnets = collect_managed_wg_subnets(&state.store).await.unwrap();
assert!(
subnets
.iter()
.any(|s| s.trunc().to_string() == "10.100.0.0/24"),
"Interface CIDR must remain in managed NAT subnets"
);
assert!(
subnets
.iter()
.any(|s| s.trunc().to_string() == "10.100.2.0/24"),
"selected Network CIDR must participate in managed NAT subnets"
);
let wg_engine = Arc::new(SimulatedWireGuardEngine::new());
let net_engine = Arc::new(SimulatedNetworkEngine::new());
let reconciler =
ReconciliationEngine::new(state.clone(), wg_engine.clone(), net_engine.clone());
let report = reconciler.apply().await.unwrap();
assert!(report.success);
let persisted_iface = state.store.get_interface(iface.id).await.unwrap().unwrap();
assert_eq!(persisted_iface.address_v4.to_string(), "10.100.0.1/24");
assert_eq!(persisted_iface.name, "wg0");
let stored_routes = state.store.list_routes().await.unwrap();
assert!(
!stored_routes
.iter()
.any(|r| r.destination.trunc().to_string() == "10.100.2.0/24"),
"peer-allocation Network CIDR must not be persisted as a static route"
);
let ruleset = net_engine.get_active_nftables_ruleset().await.unwrap();
assert!(
ruleset.contains("ip saddr 10.100.0.0/24 oifname != \"wg*\" masquerade"),
"Interface-CIDR peers must keep existing NAT: {ruleset}"
);
assert!(
ruleset.contains("ip saddr 10.100.2.0/24 oifname != \"wg*\" masquerade"),
"selected Network CIDR must be masqueraded for full-tunnel Internet: {ruleset}"
);
let live_stats = wg_engine.get_interface_stats("wg0").await.unwrap().unwrap();
assert!(
live_stats
.peers
.iter()
.any(|p| p.allowed_ips.iter().any(|a| a == "10.100.2.1/32")),
"kernel peer AllowedIPs must include the selected-Network assignment"
);
let (fallback_priv, fallback_pub) = generate_keypair();
let fallback_peer = Peer {
id: Uuid::new_v4(),
interface_id: iface.id,
name: "fallback-null-network".to_string(),
peer_type: PeerType::RoadWarrior,
state: PeerState::Active,
public_key: fallback_pub,
private_key: Some(fallback_priv),
preshared_key: None,
endpoint: None,
allowed_ips: "0.0.0.0/0, ::/0".to_string(),
server_allowed_ips: None,
address_v4: Some(IpNet::from_str("10.100.0.2/32").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,
};
state.store.create_peer(&fallback_peer).await.unwrap();
assert_eq!(
fallback_peer.server_wireguard_allowed_ips(),
"10.100.0.2/32"
);
let report = reconciler.apply().await.unwrap();
assert!(report.success);
let ruleset = net_engine.get_active_nftables_ruleset().await.unwrap();
assert!(ruleset.contains("ip saddr 10.100.0.0/24 oifname != \"wg*\" masquerade"));
assert!(ruleset.contains("ip saddr 10.100.2.0/24 oifname != \"wg*\" masquerade"));
let plan = reconciler.plan().await.unwrap();
assert!(!plan.has_drift);
assert_eq!(plan.firewall_changes, 0);
assert_eq!(plan.route_changes, 0);
}
#[tokio::test]
async fn test_interface_editing_persistence_and_key_preservation() {
let (state, iface, _peer, session_id) = setup_test_context().await;