Files
nx9-wg/crates/nx9-wg-network/tests/test_native_nftables.rs
T

316 lines
8.7 KiB
Rust

//! 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"));
}