fix: include selected networks in dataplane NAT
This commit is contained in:
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;
|
||||
|
||||
Reference in new issue
Block a user