release: NX9-WG v1.0.0

This commit is contained in:
thakares committed 2026-08-18 17:32:56 +05:30
1 parent c8a9b7cde6
commit 4dfe42fe68
42 files changed
+4689 -336

No files matched your search

@@ -19,7 +19,7 @@ use nx9_wg_core::types::network::Route;
use nx9_wg_core::types::wireguard::{Interface, Peer, PeerProfile, PeerState, PeerType};
use nx9_wg_core::validation::validate_cidr;
use nx9_wg_db::Store;
use nx9_wg_network::SimulatedNetworkEngine;
use nx9_wg_network::{NetworkEngine, SimulatedNetworkEngine};
use nx9_wireguard::{SimulatedWireGuardEngine, WireGuardEngine};
use std::sync::Arc;
use tempfile::{TempDir, tempdir};
@@ -399,3 +399,187 @@ async fn test_reconciliation_status_lifecycle_and_multi_cycle_idempotency() {
assert!(!plan.has_drift, "Cycle {cycle} plan must show zero drift");
}
}
#[tokio::test]
async fn test_reconciliation_report_schema_and_json_contract() {
use nx9_wg_api::reconciliation::{ReconciliationReport, ReconciliationStatus};
let report = ReconciliationReport {
success: true,
status: ReconciliationStatus::Converged,
executed_actions: 3,
failed_actions: 0,
details: vec![
"Synchronized interface 'wg0' with 5 peers".to_string(),
"Synchronized 1 routing entries".to_string(),
"Synchronized 0 firewall rules into table inet nx9_wg (NAT: true)".to_string(),
],
};
let json_val = serde_json::to_value(&report).unwrap();
assert_eq!(json_val["success"], true);
assert_eq!(json_val["status"], "converged");
assert_eq!(json_val["executed_actions"], 3);
assert_eq!(json_val["failed_actions"], 0);
assert!(json_val["details"].is_array());
assert_eq!(json_val["details"].as_array().unwrap().len(), 3);
}
#[tokio::test]
async fn test_reconciliation_nftables_canonical_drift_and_kernel_handle_tolerance() {
let (_dir, store, _state, _wg_engine, net_engine, reconciler) = setup_test_env().await;
// Add firewall rule in SQLite
let fw = FirewallRule {
id: Uuid::new_v4(),
name: "allow-https".to_string(),
interface_id: None,
peer_id: None,
direction: FirewallDirection::In,
source: None,
destination: None,
protocol: FirewallProtocol::Tcp,
source_port: None,
destination_port: Some(443),
port_range: None,
action: FirewallAction::Accept,
priority: 50,
enabled: true,
description: None,
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
};
store.create_firewall_rule(&fw).await.unwrap();
// 1. Initial Plan should detect drift
let plan = reconciler.plan().await.unwrap();
assert!(plan.has_drift);
assert_eq!(plan.firewall_changes, 1);
// 2. Apply should converge
let report = reconciler.apply().await.unwrap();
assert!(report.success);
assert_eq!(
report.status,
nx9_wg_api::reconciliation::ReconciliationStatus::Converged
);
assert_eq!(report.failed_actions, 0);
// 3. Post-apply verify: exactly 0 drift
let plan_after = reconciler.plan().await.unwrap();
assert!(!plan_after.has_drift);
assert_eq!(plan_after.firewall_changes, 0);
// 4. Simulate kernel returning ruleset with handles and tabs
let simulated_kernel_output_with_handles = r#"table inet nx9_wg {
chain input {
type filter hook input priority filter; policy accept;
ct state established,related accept # handle 46
iifname "lo" accept # handle 1
tcp dport 443 accept # handle 10
}
chain forward {
type filter hook forward priority filter; policy accept;
ct state established,related accept # handle 4
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
}
}
"#;
// Set simulated ruleset to text containing kernel handles
net_engine
.sync_firewall(std::slice::from_ref(&fw), false, &[])
.await
.unwrap();
// Directly test drift function against simulated kernel handles
let expected = nx9_wg_network::NftablesRulesetBuilder::build(&[fw], false, &[]);
assert!(
!nx9_wg_network::has_nftables_drift(&expected, simulated_kernel_output_with_handles),
"Ruleset with handles must not trigger false drift"
);
}
#[tokio::test]
async fn test_interface_address_and_mtu_drift_lifecycle() {
let (_dir, store, _state, wg_engine, _net_engine, reconciler) = setup_test_env().await;
let (priv_key, pub_key) = generate_keypair();
let iface_id = Uuid::new_v4();
let iface = Interface {
id: iface_id,
name: "wg0".to_string(),
private_key: priv_key,
public_key: pub_key.clone(),
listen_port: 51820,
address_v4: validate_cidr("10.100.0.1/24").unwrap(),
address_v6: Some(validate_cidr("fd00::1/64").unwrap()),
mtu: Some(1420),
dns: None,
enabled: true,
pre_up: None,
post_up: None,
pre_down: None,
post_down: None,
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
};
store.create_interface(&iface).await.unwrap();
// 1. Initially, interface does not exist in wg_engine -> plan reports create_interface drift
let plan = reconciler.plan().await.unwrap();
assert!(plan.has_drift);
assert_eq!(plan.interface_changes, 1);
assert_eq!(plan.actions[0].action_type, "create_interface");
// 2. Apply initial sync -> interface is created and synchronized
let report = reconciler.apply().await.unwrap();
assert!(report.success);
assert_eq!(
report.status,
nx9_wg_api::reconciliation::ReconciliationStatus::Converged
);
// 3. Post-apply plan must have 0 drift
let plan_after = reconciler.plan().await.unwrap();
assert!(!plan_after.has_drift);
assert_eq!(plan_after.interface_changes, 0);
// 4. Manually strip IPv4 address from live interface to simulate kernel address drop
let mut stats = wg_engine.get_interface_stats("wg0").await.unwrap().unwrap();
stats.addresses = vec!["fd00::1/64".to_string()]; // IPv4 missing
// Sync altered stats
wg_engine
.sync_interface(
&Interface {
address_v4: validate_cidr("10.99.99.99/24").unwrap(), // different
..iface.clone()
},
&[],
)
.await
.unwrap();
// 5. Plan MUST detect the missing/mismatched IPv4 address as drift
let plan_drift = reconciler.plan().await.unwrap();
assert!(plan_drift.has_drift);
assert_eq!(plan_drift.interface_changes, 1);
assert_eq!(plan_drift.actions[0].action_type, "update_interface");
assert!(plan_drift.actions[0].description.contains("IPv4 address"));
// 6. Apply reconciliation -> restores correct addresses
let report2 = reconciler.apply().await.unwrap();
assert!(report2.success);
assert_eq!(
report2.status,
nx9_wg_api::reconciliation::ReconciliationStatus::Converged
);
// 7. Final plan reports 0 drift
let final_plan = reconciler.plan().await.unwrap();
assert!(!final_plan.has_drift);
assert_eq!(final_plan.interface_changes, 0);
}