From 1b7018c9ad4db58bc080cebc6562fe9c6f66a165 Mon Sep 17 00:00:00 2001 From: containerscrew Date: Wed, 4 Dec 2024 23:20:45 +0100 Subject: [PATCH] Change config format & some tests --- docs/todo.md | 1 + nflux.toml | 33 ++++---- nflux/src/config.rs | 94 +++++++++++------------ nflux/src/lib.rs | 3 +- nflux/src/main.rs | 21 ++++-- nflux/tests/config_tests.rs | 146 +++++++++++++----------------------- 6 files changed, 126 insertions(+), 172 deletions(-) diff --git a/docs/todo.md b/docs/todo.md index 22267b1..3da2760 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -8,3 +8,4 @@ - [2] Prometheus metrics for XDP program. - [3] Implement in how much time a log_connection_event will be sended to Perf Ring Buffer. - [3] Allow the user to change the config in the runtime. +- [4] Implement default values in nflux.toml (config.rs) diff --git a/nflux.toml b/nflux.toml index c308281..8af3a9d 100644 --- a/nflux.toml +++ b/nflux.toml @@ -1,10 +1,11 @@ -[firewall] -# TODO: Add support for multiple interfaces +[nflux] +# Global configuration for nflux interface_names = ["wlp2s0", "eth0"] + +[logging] log_level = "info" # trace, debug, info, warn, or error. Defaults to info if not set log_type = "text" # text or json. Defaults to text if not set -# TODO -# default_action = "deny" # global default action if no specific rule matches +# log_file = "/var/log/firewall.log" [ip_rules] # Fine-tuned rules for IP-based filtering @@ -13,19 +14,15 @@ log_type = "text" # text or json. Defaults to text if not set # "192.168.0.170/24" = { priority = 2, action = "deny", ports = [22], protocol = "tcp", log = false, description = "Deny SSH from entire subnet" } # "2001:0db8:85a3:0000:0000:8a2e:0370:7334" = { action = "deny", ports = [80], protocol = "tcp" } -[icmp_rules] -# Rules for ICMP traffic -"192.168.0.1/24" = { action = "deny", protocol = "icmp" } -"192.168.0.88/24" = { action = "allow", protocol = "icmp" } -"192.168.0.22/24" = { action = "deny", protocol = "icmp" } +# [icmp_rules] +# # Rules for ICMP traffic +# "192.168.0.1/24" = { action = "deny", protocol = "icmp" } +# "192.168.0.88/24" = { action = "allow", protocol = "icmp" } +# "192.168.0.22/24" = { action = "deny", protocol = "icmp" } + +# [mac_rules] +# # Rules for MAC address filtering +# "00:0a:95:9d:68:16" = { action = "allow" } +# "00:0a:95:9d:68:17" = { action = "deny" } -[mac_rules] -# Rules for MAC address filtering -"00:0a:95:9d:68:16" = { action = "allow" } -"00:0a:95:9d:68:17" = { action = "deny" } -[logging] -log_denied_packets = true -log_allowed_packets = false -log_format = "json" -log_file = "/var/log/firewall.log" diff --git a/nflux/src/config.rs b/nflux/src/config.rs index 0aba033..ea22801 100644 --- a/nflux/src/config.rs +++ b/nflux/src/config.rs @@ -1,26 +1,39 @@ use anyhow::{Context, Result}; -use serde::{Deserialize, Deserializer}; +use serde::Deserialize; use std::collections::HashMap; use std::env; use std::fs; /// Enum for `action` -#[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] // Allow "deny" or "allow" in config +#[derive(Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] pub enum Action { Deny, Allow, } /// Enum for `protocol` -#[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] // Allow "tcp" or "udp" in config +#[derive(Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] pub enum Protocol { Tcp, Udp, Icmp, } +// General firewall configuration +#[derive(Debug, Deserialize)] +pub struct NfluxConfig { + pub interface_names: Vec, +} + +// Logging config +#[derive(Debug, Deserialize)] +pub struct LoggingConfig { + pub log_level: String, + pub log_type: String, +} + /// Generic rule for both IPv4 and IPv6 #[derive(Debug, Deserialize)] #[allow(dead_code)] @@ -33,61 +46,42 @@ pub struct Rules { pub description: String, } -/// Configuration for ICMP rules -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub struct IcmpRules { - pub action: Action, // Allow or Deny - pub protocol: String, // Always "icmp" -} - -/// Configuration for MAC-based rules -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub struct MacRule { - pub action: Action, // Allow or Deny -} - -/// General firewall configuration -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub struct Firewall { - pub interface_names: Vec, // List of interfaces - pub log_level: String, // Log level - pub log_type: String, // Log type -} - -/// Logging configuration -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub struct LoggingConfig { - pub log_denied_packets: bool, - pub log_allowed_packets: bool, - pub log_format: String, - pub log_file: String, -} - -/// Top-level configuration structure +// Top-level configuration structure #[derive(Debug, Deserialize)] #[allow(dead_code)] -pub struct FirewallConfig { - pub firewall: Firewall, - pub ip_rules: HashMap, - pub icmp_rules: HashMap, - pub mac_rules: HashMap, +pub struct Nflux { + pub nflux: NfluxConfig, pub logging: LoggingConfig, + pub ip_rules: HashMap, } -impl FirewallConfig { - /// Load the configuration from a file, defaulting to `/etc/nflux/nflux.toml` if not specified - pub fn load() -> Result { +impl Nflux { + // Load the configuration from a file and return the `Nflux` struct + pub fn load_config() -> Result { let config_file = env::var("NFLUX_CONFIG_FILE_PATH") .unwrap_or_else(|_| "/etc/nflux/nflux.toml".to_string()); let config_content = fs::read_to_string(&config_file) .with_context(|| format!("Failed to read configuration file: {}", config_file))?; - toml::from_str(&config_content) - .with_context(|| format!("Failed to parse configuration file: {}", config_file)) + let config: Self = toml::from_str(&config_content) + .with_context(|| format!("Failed to parse configuration file: {}", config_file))?; + + config.validate()?; + + Ok(config) + } + + // A separate validation function to ensure correctness + pub fn validate(&self) -> Result<()> { + for (ip, rule) in &self.ip_rules { + if rule.priority == 0 { + anyhow::bail!("Priority must be greater than 0"); + } + if !rule.ports.iter().all(|&port| (1..=65535).contains(&port)) { + anyhow::bail!("Invalid port number in rule for IP: {}", ip); + } + } + Ok(()) } } diff --git a/nflux/src/lib.rs b/nflux/src/lib.rs index 82c1749..dc87fcc 100644 --- a/nflux/src/lib.rs +++ b/nflux/src/lib.rs @@ -4,8 +4,9 @@ mod logger; mod utils; // Dependencies -pub use config::{FirewallConfig, IcmpRules, Protocol, Rules}; +pub use config::{NfluxConfig, Rules, Nflux, Action, Protocol}; pub use core::set_mem_limit; +pub use utils::{is_root_user, wait_for_shutdown}; /// RXH version. pub const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/nflux/src/main.rs b/nflux/src/main.rs index 263abe6..9a18efe 100644 --- a/nflux/src/main.rs +++ b/nflux/src/main.rs @@ -3,7 +3,7 @@ mod core; mod logger; mod utils; -use crate::utils::{is_root_user, wait_for_shutdown}; + use anyhow::Context; use aya::maps::lpm_trie::Key; use aya::maps::perf::{AsyncPerfEventArrayBuffer, PerfBufferError}; @@ -12,12 +12,13 @@ use aya::programs::{Xdp, XdpFlags}; use aya::util::online_cpus; use aya::{include_bytes_aligned, Ebpf}; use bytes::BytesMut; -use config::{Action, FirewallConfig, Protocol, Rules}; +use config::{Action, Nflux, Protocol, Rules}; use logger::setup_logger; -use nflux::set_mem_limit; use nflux_common::{convert_protocol, ConnectionEvent, IpRule, LpmKeyIpv4, LpmKeyIpv6}; +use utils::{is_root_user, wait_for_shutdown}; +use core::set_mem_limit; use std::collections::HashMap; -use std::net::{Ipv4Addr, Ipv6Addr}; +use std::net::Ipv4Addr; use std::ptr; use tokio::task; use tracing::{error, info, warn}; @@ -25,10 +26,10 @@ use tracing::{error, info, warn}; #[tokio::main] async fn main() -> anyhow::Result<()> { // Load configuration file - let config = FirewallConfig::load().context("Failed to load firewall configuration")?; + let config = Nflux::load_config().context("Failed to load nflux configuration")?; // Enable logging - setup_logger(&config.firewall.log_level, &config.firewall.log_type); + setup_logger(&config.logging.log_level, &config.logging.log_type); // Ensure the program is run as root if !is_root_user() { @@ -50,7 +51,7 @@ async fn main() -> anyhow::Result<()> { let program: &mut Xdp = bpf.program_mut("nflux").unwrap().try_into()?; program.load()?; program - .attach(&config.firewall.interface_names[0], XdpFlags::default()) + .attach(&config.nflux.interface_names[0], XdpFlags::default()) .context( "Failed to attach XDP program. Ensure the interface is physical and not virtual.", )?; @@ -59,7 +60,7 @@ async fn main() -> anyhow::Result<()> { info!("nflux started successfully!"); info!( "XDP program attached to interface: {:?}", - config.firewall.interface_names[0] + config.nflux.interface_names[0] ); // Start processing events from the eBPF program @@ -170,6 +171,10 @@ fn prepare_ip_rule(rule: &Rules) -> anyhow::Result { Protocol::Tcp => 6, Protocol::Udp => 17, Protocol::Icmp => 1, + _ => { + warn!("Unsupported protocol: {:?}", rule.protocol); + return Err(anyhow::anyhow!("Unsupported protocol")); + } }, priority: rule.priority, }) diff --git a/nflux/tests/config_tests.rs b/nflux/tests/config_tests.rs index 9153c19..afbc5eb 100644 --- a/nflux/tests/config_tests.rs +++ b/nflux/tests/config_tests.rs @@ -1,121 +1,77 @@ -use nflux::Config; use std::fs; -use tempfile::tempdir; +use nflux::{Action, Nflux, Protocol}; +use tempfile::TempDir; -#[test] -fn test_load_valid_config() { - let config_content = r#" - [log] - log_level = "info" - log_type = "json" - - [nflux] - interface_name = "wlp2s0" - - [firewall] - allowed_ports = [22, 80] - allowed_ipv4 = [] - allow_icmp = false - "#; - - let temp_dir = tempdir().unwrap(); +fn setup_temp_config(content: &str) -> TempDir { + let temp_dir = tempfile::tempdir().unwrap(); let config_path = temp_dir.path().join("nflux.toml"); - fs::write(&config_path, config_content).unwrap(); + fs::write(&config_path, content).unwrap(); std::env::set_var("NFLUX_CONFIG_FILE_PATH", config_path.to_str().unwrap()); - let config = Config::load(); - assert_eq!(config.log.log_level, "info"); - assert_eq!(config.log.log_type, "json"); - assert_eq!(config.firewall.allowed_ports, vec![22, 80]); + temp_dir } -#[test] -fn test_load_missing_config_file() { - std::env::set_var("NFLUX_CONFIG_FILE_PATH", "/nonexistent/path/nflux.toml"); - - let result = std::panic::catch_unwind(|| Config::load()); - assert!( - result.is_err(), - "Expected panic when config file is missing" - ); -} #[test] -fn test_load_invalid_config_format() { +fn test_load_valid_config() { let config_content = r#" - [log - log_level = "info" + [nflux] + interface_names = ["eth0", "wlan0"] + + [logging] + log_level = "debug" log_type = "json" - "#; // Missing closing bracket and invalid TOML format - let temp_dir = tempdir().unwrap(); - let config_path = temp_dir.path().join("nflux.toml"); - fs::write(&config_path, config_content).unwrap(); + [ip_rules] + "192.168.0.1" = { priority = 1, action = "allow", ports = [22], protocol = "tcp", log = true, description = "SSH rule" } + "#; - std::env::set_var("NFLUX_CONFIG_FILE_PATH", config_path.to_str().unwrap()); + let _temp_dir = setup_temp_config(config_content); + + let config = Nflux::load_config().unwrap(); + + // Assertions + assert_eq!(config.nflux.interface_names, vec!["eth0", "wlan0"]); + assert_eq!(config.logging.log_level, "debug"); + assert_eq!(config.logging.log_type, "json"); - let result = std::panic::catch_unwind(|| Config::load()); - assert!( - result.is_err(), - "Expected panic when config file is invalid" - ); + let rule = config.ip_rules.get("192.168.0.1").unwrap(); + assert_eq!(rule.priority, 1); + assert_eq!(rule.action, Action::Allow); + assert_eq!(rule.ports, vec![22]); + assert_eq!(rule.protocol, Protocol::Tcp); + assert_eq!(rule.log, true); + assert_eq!(rule.description, "SSH rule"); } + // #[test] -// fn test_load_config_default_value_fallback() { -// // No environment variable set, expect the default path to be used. -// std::env::remove_var("NFLUX_CONFIG_FILE_PATH"); +// fn test_load_missing_config_file() { +// std::env::set_var("NFLUX_CONFIG_FILE_PATH", "/nonexistent/path/nflux.toml"); + +// let result = Nflux::load_config(); -// let result = std::panic::catch_unwind(|| Config::load()); -// assert!(result.is_err(), "Expected panic when default config file is missing"); +// // Assert that loading fails +// assert!(result.is_err()); +// assert!(result +// .unwrap_err() +// .to_string() +// .contains("Failed to read configuration file")); // } // #[test] -// fn test_load_partial_config() { -// let config_content = r#" -// [log] -// log_level = "warn" - -// [firewall] -// allowed_ports = [443] -// "#; // Missing some fields (e.g., log_type) +// fn test_load_invalid_config_format() { +// let invalid_config_content = "invalid: [toml"; -// let temp_dir = tempdir().unwrap(); -// let config_path = temp_dir.path().join("nflux.toml"); -// fs::write(&config_path, config_content).unwrap(); +// setup_temp_config(invalid_config_content); -// std::env::set_var("NFLUX_CONFIG_FILE_PATH", config_path.to_str().unwrap()); +// let result = Nflux::load_config(); -// let config = Config::load(); -// assert_eq!(config.log.log_level, "warn"); -// assert_eq!(config.log.log_type, "text"); // Should fallback to the default value -// assert_eq!(config.firewall.allowed_ports, vec![443]); -// } - -// #[test] -// fn test_load_config_with_multiple_allowed_ips() { -// let config_content = r#" -// [log] -// log_level = "debug" -// log_type = "json" - -// [firewall] -// allowed_ports = [22, 443] -// allowed_ipv4 = ["192.168.0.1", "10.0.0.1"] -// allow_icmp = true -// "#; - -// let temp_dir = tempdir().unwrap(); -// let config_path = temp_dir.path().join("nflux.toml"); -// fs::write(&config_path, config_content).unwrap(); - -// std::env::set_var("NFLUX_CONFIG_FILE_PATH", config_path.to_str().unwrap()); - -// let config = Config::load(); -// assert_eq!(config.log.log_level, "debug"); -// assert_eq!(config.log.log_type, "json"); -// assert_eq!(config.firewall.allowed_ports, vec![22, 443]); -// assert_eq!(config.firewall.allowed_ipv4, vec!["192.168.0.1", "10.0.0.1"]); -// assert!(config.firewall.allow_icmp); +// // Assert that loading fails due to parse error +// assert!(result.is_err()); +// assert!(result +// .unwrap_err() +// .to_string() +// .contains("Failed to parse configuration file")); // }