feat: complete nx9-wg v0.8.0 platform
This commit is contained in:
1 parent
c75e5c4e71
commit
c8a9b7cde6
52 files changed
+7751
-725
No files matched your search
@@ -0,0 +1,154 @@
|
||||
//! Kernel-independent unit and conversion tests for Phase 2 Native Linux Network Engine.
|
||||
|
||||
use chrono::Utc;
|
||||
use ipnet::IpNet;
|
||||
use nx9_wg_core::types::network::Route;
|
||||
use nx9_wg_network::error::NetworkError;
|
||||
use nx9_wg_network::forwarding::IpForwardingStatus;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::str::FromStr;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn test_ipv4_route_destination_conversion() {
|
||||
let dest = IpNet::from_str("192.168.10.0/24").expect("valid cidr");
|
||||
assert_eq!(dest.addr(), IpAddr::V4(Ipv4Addr::new(192, 168, 10, 0)));
|
||||
assert_eq!(dest.prefix_len(), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ipv6_route_destination_conversion() {
|
||||
let dest = IpNet::from_str("fd00:abcd::/64").expect("valid ipv6 cidr");
|
||||
assert_eq!(dest.prefix_len(), 64);
|
||||
assert!(dest.addr().is_ipv6());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_gateway_resolution() {
|
||||
let now = Utc::now().naive_utc();
|
||||
let r_with_gw = Route {
|
||||
id: Uuid::new_v4(),
|
||||
network_id: None,
|
||||
interface_id: None,
|
||||
destination: IpNet::from_str("10.100.0.0/16").unwrap(),
|
||||
gateway: Some(IpAddr::from_str("10.0.0.1").unwrap()),
|
||||
interface_name: Some("wg0".to_string()),
|
||||
metric: Some(50),
|
||||
enabled: true,
|
||||
description: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
assert!(r_with_gw.gateway.is_some());
|
||||
assert_eq!(
|
||||
r_with_gw.gateway.unwrap(),
|
||||
IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))
|
||||
);
|
||||
|
||||
let r_no_gw = Route {
|
||||
id: Uuid::new_v4(),
|
||||
network_id: None,
|
||||
interface_id: None,
|
||||
destination: IpNet::from_str("10.200.0.0/16").unwrap(),
|
||||
gateway: None,
|
||||
interface_name: Some("wg0".to_string()),
|
||||
metric: None,
|
||||
enabled: true,
|
||||
description: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
assert!(r_no_gw.gateway.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_route_safety_invariants() {
|
||||
let v4_default = IpNet::from_str("0.0.0.0/0").unwrap();
|
||||
assert!(v4_default.addr().is_unspecified());
|
||||
assert_eq!(v4_default.prefix_len(), 0);
|
||||
|
||||
let v6_default = IpNet::from_str("::/0").unwrap();
|
||||
assert!(v6_default.addr().is_unspecified());
|
||||
assert_eq!(v6_default.prefix_len(), 0);
|
||||
|
||||
let non_default = IpNet::from_str("10.0.0.0/8").unwrap();
|
||||
assert!(!non_default.addr().is_unspecified() || non_default.prefix_len() != 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_forwarding_status_serde() {
|
||||
let status = IpForwardingStatus {
|
||||
ipv4_enabled: true,
|
||||
ipv6_enabled: false,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&status).expect("serialize");
|
||||
assert!(json.contains("\"ipv4_enabled\":true"));
|
||||
assert!(json.contains("\"ipv6_enabled\":false"));
|
||||
|
||||
let deserialized: IpForwardingStatus = serde_json::from_str(&json).expect("deserialize");
|
||||
assert_eq!(status, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_variants_formatting() {
|
||||
let err_iface = NetworkError::InterfaceNotFound("wg-test".to_string());
|
||||
assert_eq!(err_iface.to_string(), "interface 'wg-test' not found");
|
||||
|
||||
let err_addr = NetworkError::AddressNotFound("10.0.0.1/24".to_string());
|
||||
assert_eq!(err_addr.to_string(), "address '10.0.0.1/24' not found");
|
||||
|
||||
let err_route = NetworkError::RouteNotFound("192.168.1.0/24".to_string());
|
||||
assert_eq!(err_route.to_string(), "route '192.168.1.0/24' not found");
|
||||
|
||||
let err_netlink = NetworkError::Netlink("Netlink connection refused".to_string());
|
||||
assert_eq!(
|
||||
err_netlink.to_string(),
|
||||
"netlink error: Netlink connection refused"
|
||||
);
|
||||
|
||||
let err_perm = NetworkError::PermissionDenied("Operation requires CAP_NET_ADMIN".to_string());
|
||||
assert_eq!(
|
||||
err_perm.to_string(),
|
||||
"permission denied: Operation requires CAP_NET_ADMIN"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_equality_and_filtering() {
|
||||
let now = Utc::now().naive_utc();
|
||||
let r1 = Route {
|
||||
id: Uuid::new_v4(),
|
||||
network_id: None,
|
||||
interface_id: None,
|
||||
destination: IpNet::from_str("172.16.0.0/12").unwrap(),
|
||||
gateway: Some(IpAddr::from_str("10.0.0.254").unwrap()),
|
||||
interface_name: Some("wg0".to_string()),
|
||||
metric: Some(20),
|
||||
enabled: true,
|
||||
description: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
let r2 = Route {
|
||||
id: Uuid::new_v4(),
|
||||
network_id: None,
|
||||
interface_id: None,
|
||||
destination: IpNet::from_str("172.16.0.0/12").unwrap(),
|
||||
gateway: Some(IpAddr::from_str("10.0.0.254").unwrap()),
|
||||
interface_name: Some("wg0".to_string()),
|
||||
metric: Some(20),
|
||||
enabled: false,
|
||||
description: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
assert_eq!(r1.destination, r2.destination);
|
||||
assert_eq!(r1.gateway, r2.gateway);
|
||||
assert!(r1.enabled);
|
||||
assert!(!r2.enabled);
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
//! Comprehensive unit tests for native nftables translation, deterministic compilation, and safety invariants.
|
||||
|
||||
use ipnet::IpNet;
|
||||
use nx9_wg_core::types::firewall::{
|
||||
FirewallAction, FirewallDirection, FirewallProtocol, FirewallRule,
|
||||
};
|
||||
use nx9_wg_network::engine::{FirewallDiagnostics, NativeLinuxNftablesEngine};
|
||||
use nx9_wg_network::error::NetworkError;
|
||||
use nx9_wg_network::nftables::NftablesRulesetBuilder;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn make_rule(
|
||||
name: &str,
|
||||
dir: FirewallDirection,
|
||||
action: FirewallAction,
|
||||
proto: FirewallProtocol,
|
||||
src: Option<&str>,
|
||||
dst: Option<&str>,
|
||||
dp: Option<u16>,
|
||||
pr: Option<&str>,
|
||||
priority: i32,
|
||||
enabled: bool,
|
||||
) -> FirewallRule {
|
||||
FirewallRule {
|
||||
id: Uuid::new_v4(),
|
||||
name: name.to_string(),
|
||||
interface_id: None,
|
||||
peer_id: None,
|
||||
direction: dir,
|
||||
action,
|
||||
protocol: proto,
|
||||
source: src.map(|s| s.to_string()),
|
||||
destination: dst.map(|s| s.to_string()),
|
||||
source_port: None,
|
||||
destination_port: dp,
|
||||
port_range: pr.map(|p| p.to_string()),
|
||||
priority,
|
||||
enabled,
|
||||
description: None,
|
||||
created_at: chrono::Utc::now().naive_utc(),
|
||||
updated_at: chrono::Utc::now().naive_utc(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ipv4_rule_translation() {
|
||||
let rules = vec![make_rule(
|
||||
"Allow IPv4 Web",
|
||||
FirewallDirection::In,
|
||||
FirewallAction::Accept,
|
||||
FirewallProtocol::Tcp,
|
||||
Some("192.168.1.0/24"),
|
||||
Some("10.0.0.1"),
|
||||
Some(443),
|
||||
None,
|
||||
10,
|
||||
true,
|
||||
)];
|
||||
|
||||
let ruleset = NftablesRulesetBuilder::build(&rules, false, &[]);
|
||||
assert!(ruleset.contains("table inet nx9_wg"));
|
||||
assert!(ruleset.contains("chain input"));
|
||||
assert!(ruleset.contains("tcp ip saddr 192.168.1.0/24 ip daddr 10.0.0.1 tcp dport 443 accept"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ipv6_rule_translation() {
|
||||
let rules = vec![make_rule(
|
||||
"Allow IPv6 DNS",
|
||||
FirewallDirection::Forward,
|
||||
FirewallAction::Accept,
|
||||
FirewallProtocol::Udp,
|
||||
Some("2001:db8::/64"),
|
||||
Some("2001:db8:ffff::1"),
|
||||
Some(53),
|
||||
None,
|
||||
20,
|
||||
true,
|
||||
)];
|
||||
|
||||
let ruleset = NftablesRulesetBuilder::build(&rules, false, &[]);
|
||||
assert!(ruleset.contains("chain forward"));
|
||||
assert!(
|
||||
ruleset
|
||||
.contains("udp ip6 saddr 2001:db8::/64 ip6 daddr 2001:db8:ffff::1 udp dport 53 accept")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_protocol_groups_and_icmp() {
|
||||
let rules = vec![
|
||||
make_rule(
|
||||
"Allow ICMP Ping",
|
||||
FirewallDirection::In,
|
||||
FirewallAction::Accept,
|
||||
FirewallProtocol::Icmp,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
1,
|
||||
true,
|
||||
),
|
||||
make_rule(
|
||||
"Allow TCP+UDP Services",
|
||||
FirewallDirection::Forward,
|
||||
FirewallAction::Accept,
|
||||
FirewallProtocol::TcpUdp,
|
||||
Some("10.100.0.5"),
|
||||
None,
|
||||
None,
|
||||
Some("53,80,443"),
|
||||
5,
|
||||
true,
|
||||
),
|
||||
];
|
||||
|
||||
let ruleset = NftablesRulesetBuilder::build(&rules, false, &[]);
|
||||
assert!(ruleset.contains("ip protocol icmp accept"));
|
||||
assert!(
|
||||
ruleset.contains(
|
||||
"meta l4proto { tcp, udp } ip saddr 10.100.0.5 th dport { 53, 80, 443 } accept"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_port_ranges_and_single_ports() {
|
||||
let rules = vec![make_rule(
|
||||
"Allow Port Range",
|
||||
FirewallDirection::In,
|
||||
FirewallAction::Accept,
|
||||
FirewallProtocol::Tcp,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some("8000-8100"),
|
||||
15,
|
||||
true,
|
||||
)];
|
||||
|
||||
let ruleset = NftablesRulesetBuilder::build(&rules, false, &[]);
|
||||
assert!(ruleset.contains("tcp tcp dport 8000-8100 accept"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drop_and_reject_actions() {
|
||||
let rules = vec![
|
||||
make_rule(
|
||||
"Block Bad Subnet",
|
||||
FirewallDirection::In,
|
||||
FirewallAction::Drop,
|
||||
FirewallProtocol::Any,
|
||||
Some("198.51.100.0/24"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
50,
|
||||
true,
|
||||
),
|
||||
make_rule(
|
||||
"Reject Telnet",
|
||||
FirewallDirection::Forward,
|
||||
FirewallAction::Reject,
|
||||
FirewallProtocol::Tcp,
|
||||
None,
|
||||
None,
|
||||
Some(23),
|
||||
None,
|
||||
60,
|
||||
true,
|
||||
),
|
||||
];
|
||||
|
||||
let ruleset = NftablesRulesetBuilder::build(&rules, false, &[]);
|
||||
assert!(ruleset.contains("ip saddr 198.51.100.0/24 drop"));
|
||||
assert!(ruleset.contains("tcp tcp dport 23 reject"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nat_masquerade_subnets_scoping() {
|
||||
let v4_subnet: IpNet = "10.100.0.0/24".parse().unwrap();
|
||||
let v6_subnet: IpNet = "fd00:9999::/64".parse().unwrap();
|
||||
|
||||
let ruleset = NftablesRulesetBuilder::build(&[], true, &[v4_subnet, v6_subnet]);
|
||||
assert!(ruleset.contains("chain postrouting"));
|
||||
assert!(ruleset.contains("type nat hook postrouting priority srcnat; policy accept;"));
|
||||
assert!(ruleset.contains("ip saddr 10.100.0.0/24 oifname != \"wg*\" masquerade"));
|
||||
assert!(ruleset.contains("ip6 saddr fd00:9999::/64 oifname != \"wg*\" masquerade"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deterministic_priority_ordering() {
|
||||
let rules = vec![
|
||||
make_rule(
|
||||
"Low Priority",
|
||||
FirewallDirection::In,
|
||||
FirewallAction::Accept,
|
||||
FirewallProtocol::Tcp,
|
||||
None,
|
||||
None,
|
||||
Some(80),
|
||||
None,
|
||||
100,
|
||||
true,
|
||||
),
|
||||
make_rule(
|
||||
"High Priority",
|
||||
FirewallDirection::In,
|
||||
FirewallAction::Drop,
|
||||
FirewallProtocol::Tcp,
|
||||
None,
|
||||
None,
|
||||
Some(80),
|
||||
None,
|
||||
10,
|
||||
true,
|
||||
),
|
||||
make_rule(
|
||||
"Disabled Rule",
|
||||
FirewallDirection::In,
|
||||
FirewallAction::Accept,
|
||||
FirewallProtocol::Tcp,
|
||||
None,
|
||||
None,
|
||||
Some(8080),
|
||||
None,
|
||||
5,
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
let ruleset = NftablesRulesetBuilder::build(&rules, false, &[]);
|
||||
let drop_pos = ruleset.find("tcp tcp dport 80 drop").unwrap();
|
||||
let accept_pos = ruleset.find("tcp tcp dport 80 accept").unwrap();
|
||||
assert!(
|
||||
drop_pos < accept_pos,
|
||||
"Higher priority rule (priority 10) must appear before lower priority rule (priority 100)"
|
||||
);
|
||||
assert!(
|
||||
!ruleset.contains("8080"),
|
||||
"Disabled rule must not appear in generated ruleset"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ownership_validation_rejects_unmanaged_tables() {
|
||||
let engine = NativeLinuxNftablesEngine::new();
|
||||
|
||||
// Rejects global flush
|
||||
let err1 = engine.apply_ruleset("flush ruleset").await.unwrap_err();
|
||||
match err1 {
|
||||
NetworkError::FirewallOwnershipViolation(msg) => {
|
||||
assert!(msg.contains("Refusing to flush global"));
|
||||
}
|
||||
other => panic!("Expected FirewallOwnershipViolation, got: {other:?}"),
|
||||
}
|
||||
|
||||
// Rejects other tables
|
||||
let err2 = engine
|
||||
.apply_ruleset("table ip filter {\n}\n")
|
||||
.await
|
||||
.unwrap_err();
|
||||
match err2 {
|
||||
NetworkError::FirewallOwnershipViolation(msg) => {
|
||||
assert!(msg.contains("Refusing to execute command outside 'table inet nx9_wg'"));
|
||||
}
|
||||
other => panic!("Expected FirewallOwnershipViolation, got: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_variants_and_formatting() {
|
||||
let err_inv = NetworkError::FirewallRuleInvalid("Port out of bounds".to_string());
|
||||
assert_eq!(
|
||||
err_inv.to_string(),
|
||||
"invalid firewall rule: Port out of bounds"
|
||||
);
|
||||
|
||||
let err_own = NetworkError::FirewallOwnershipViolation("Cannot delete eth0".to_string());
|
||||
assert_eq!(
|
||||
err_own.to_string(),
|
||||
"firewall ownership violation: Cannot delete eth0"
|
||||
);
|
||||
|
||||
let err_nat = NetworkError::NatConfigurationInvalid("Wildcard CIDR not permitted".to_string());
|
||||
assert_eq!(
|
||||
err_nat.to_string(),
|
||||
"invalid NAT configuration: Wildcard CIDR not permitted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_firewall_diagnostics_serialization() {
|
||||
let diag = FirewallDiagnostics {
|
||||
table_exists: true,
|
||||
table_name: "nx9_wg".to_string(),
|
||||
family: "inet".to_string(),
|
||||
chain_count: 3,
|
||||
chains: vec![
|
||||
"input".to_string(),
|
||||
"forward".to_string(),
|
||||
"postrouting".to_string(),
|
||||
],
|
||||
rule_count: 5,
|
||||
nat_enabled: true,
|
||||
live_ruleset: Some("table inet nx9_wg { }".to_string()),
|
||||
kernel_status: "active".to_string(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&diag).unwrap();
|
||||
assert!(json.contains("\"table_name\":\"nx9_wg\""));
|
||||
assert!(json.contains("\"nat_enabled\":true"));
|
||||
}
|
||||
Reference in new issue
Block a user