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