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