From d95f2d9f016e738eaf0c3415e5ee7d0adf4ea6b7 Mon Sep 17 00:00:00 2001 From: Samuel Lorch Date: Sun, 11 Feb 2024 21:43:10 +0100 Subject: [PATCH] Implement nfTables Rule Generation --- src/apply/mod.rs | 1 + src/apply/nftables.rs | 268 +++++++++++++++++++++++++++ src/config_manager.rs | 1 + src/templates/nftables/nftables.conf | 91 +++++++++ 4 files changed, 361 insertions(+) create mode 100644 src/apply/nftables.rs create mode 100644 src/templates/nftables/nftables.conf diff --git a/src/apply/mod.rs b/src/apply/mod.rs index b048064..627467d 100644 --- a/src/apply/mod.rs +++ b/src/apply/mod.rs @@ -2,6 +2,7 @@ use thiserror::Error; pub mod chrony; pub mod networkd; +pub mod nftables; pub mod unbound; #[derive(Error, Debug)] diff --git a/src/apply/nftables.rs b/src/apply/nftables.rs new file mode 100644 index 0000000..a2e68fd --- /dev/null +++ b/src/apply/nftables.rs @@ -0,0 +1,268 @@ +use super::ApplyError; +use crate::definitions::firewall::{SNATType, Verdict}; +use crate::definitions::object::{Address, AddressType, PortDefinition, Service, ServiceType}; +use crate::{definitions::config::Config, templates}; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use std::process::Command; +use std::{error::Error, io::Write}; +use tera::Context; +use tracing::{error, info}; + +const NFTABLES_CONFIG_PATH: &str = "/etc/nftables/nfsense.conf"; +const NFTABLES_TEMPLATE_PATH: &str = "nftables/nftables.conf"; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Rule { + pub name: String, + pub services: Vec, + pub addresses: String, + pub counter: bool, + pub verdict: Option, + pub destination_nat_action: Option, + pub source_nat_action: Option, +} + +fn convert_addresses_to_strings(addresses: Vec
) -> Vec { + let mut list = vec![]; + for address in addresses { + match address.address_type { + AddressType::Host { address } => list.push(address.to_string()), + AddressType::Range { range } => list.push(range.to_string()), + AddressType::Network { network } => list.push(network.to_string()), + AddressType::Group { .. } => { + //TODO + } + } + } + list +} + +fn convert_list_to_set(list: Vec) -> String { + if list.len() == 0 { + return "".to_string(); + } else if list.len() == 1 { + return list[0].clone(); + } + + let mut res = "{ ".to_string(); + + for (index, element) in list.iter().enumerate() { + res += element; + if index < list.len() - 1 { + res += ", "; + } + } + res += " }"; + res +} + +fn generate_address_matcher( + source_addresses: Vec
, + destination_addresses: Vec
, +) -> Result { + let source_list = convert_addresses_to_strings(source_addresses); + let destination_list = convert_addresses_to_strings(destination_addresses); + let mut res = "".to_string(); + + if source_list.len() > 0 { + res += "ip saddr "; + res += &convert_list_to_set(source_list); + res += " "; + } + + if destination_list.len() > 0 { + res += "ip daddr "; + res += &convert_list_to_set(destination_list); + } + + Ok(res) +} + +fn generate_port_matcher( + protocol: &str, + source: PortDefinition, + destination: PortDefinition, +) -> String { + let source_string = match source { + PortDefinition::Any => "".to_string(), + PortDefinition::Single { port } => { + protocol.to_string() + &" sport ".to_string() + &port.to_string() + } + PortDefinition::Range { + start_port, + end_port, + } => { + protocol.to_string() + + &" sport ".to_string() + + &start_port.to_string() + + " - " + + &end_port.to_string() + } + }; + + let destination_string = match destination { + PortDefinition::Any => "".to_string(), + PortDefinition::Single { port } => { + protocol.to_string() + &" dport ".to_string() + &port.to_string() + } + PortDefinition::Range { + start_port, + end_port, + } => { + protocol.to_string() + + &" dport ".to_string() + + &start_port.to_string() + + " - " + + &end_port.to_string() + } + }; + if source_string.len() != 0 && destination_string.len() != 0 { + source_string + " " + &destination_string + } else { + source_string + &destination_string + } +} + +fn generate_service_matchers(services: Vec) -> Result, ApplyError> { + let mut list = vec![]; + for service in services { + match service.service_type { + ServiceType::TCP { + source, + destination, + } => list.push(generate_port_matcher("tcp", source, destination)), + ServiceType::UDP { + source, + destination, + } => list.push(generate_port_matcher("udp", source, destination)), + ServiceType::ICMP { code } => list.push("icmp codes ".to_string() + &code.to_string()), + ServiceType::Group { .. } => ( + //TODO + ), + } + } + Ok(list) +} + +fn generate_destination_nat_action( + dnat_address: Option
, + dnat_service: Option, +) -> Result { + Ok("".to_string()) +} + +fn generate_source_nat_action( + snat_address: Option
, + snat_service: Option, +) -> Result { + Ok("".to_string()) +} + +pub fn apply_nftables(pending_config: Config, _current_config: Config) -> Result<(), ApplyError> { + let config_data; + let mut context = Context::new(); + + // Forward Rules + let mut forward_rules = vec![]; + for rule in &pending_config.firewall.forward_rules { + forward_rules.push(Rule { + name: rule.name.clone(), + counter: rule.counter, + addresses: generate_address_matcher( + rule.source_addresses(pending_config.clone()), + rule.destination_addresses(pending_config.clone()), + )?, + services: generate_service_matchers(rule.services(pending_config.clone()))?, + verdict: Some(match rule.verdict { + Verdict::Accept => "accept".to_string(), + Verdict::Drop => "drop".to_string(), + Verdict::Continue => "continue".to_string(), + }), + destination_nat_action: None, + source_nat_action: None, + }) + } + context.insert("forward_rules", &forward_rules); + + // Destination Nat Rules + let mut destination_nat_rules = vec![]; + for rule in &pending_config.firewall.destination_nat_rules { + destination_nat_rules.push(Rule { + name: rule.name.clone(), + counter: rule.counter, + addresses: generate_address_matcher( + rule.source_addresses(pending_config.clone()), + rule.destination_addresses(pending_config.clone()), + )?, + services: generate_service_matchers(rule.services(pending_config.clone()))?, + verdict: None, + destination_nat_action: Some(generate_destination_nat_action( + rule.dnat_address(pending_config.clone()), + rule.dnat_service(pending_config.clone()), + )?), + source_nat_action: None, + }) + } + context.insert("destination_nat_rules", &destination_nat_rules); + + // Source Nat Rules + let mut source_nat_rules = vec![]; + for rule in &pending_config.firewall.source_nat_rules { + source_nat_rules.push(Rule { + name: rule.name.clone(), + counter: rule.counter, + addresses: generate_address_matcher( + rule.source_addresses(pending_config.clone()), + rule.destination_addresses(pending_config.clone()), + )?, + services: generate_service_matchers(rule.services(pending_config.clone()))?, + verdict: None, + destination_nat_action: None, + source_nat_action: Some(match rule.snat_type.clone() { + SNATType::Masquerade => "masquerade".to_string(), + SNATType::SNAT { .. } => generate_source_nat_action( + rule.snat_type.address(pending_config.clone()), + rule.snat_type.service(pending_config.clone()), + )?, + }), + }) + } + context.insert("source_nat_rules", &source_nat_rules); + + match templates::TEMPLATES.render(NFTABLES_TEMPLATE_PATH, &context) { + Ok(s) => config_data = s, + Err(e) => { + error!("Error: {}", e); + let mut cause = e.source(); + while let Some(e) = cause { + error!("Reason: {}", e); + cause = e.source(); + } + return Err(ApplyError::TemplateError(e)); + } + } + + info!("Deleting old nftables Config"); + std::fs::remove_file(NFTABLES_CONFIG_PATH)?; + + info!("Writing new nftables Config"); + let mut f = std::fs::File::create(NFTABLES_CONFIG_PATH)?; + f.write_all(config_data.as_bytes())?; + + info!("Restarting nftables"); + match Command::new("systemctl") + .arg("restart") + .arg("nftables") + .output() + { + Ok(out) => { + if out.status.success() { + Ok(()) + } else { + Err(ApplyError::ServiceRestartFailed) + } + } + Err(err) => Err(ApplyError::IOError(err)), + } +} diff --git a/src/config_manager.rs b/src/config_manager.rs index d58f7b0..f746191 100644 --- a/src/config_manager.rs +++ b/src/config_manager.rs @@ -37,6 +37,7 @@ static APPLY_FUNCTIONS: &'static [fn( current_config: Config, ) -> Result<(), super::apply::ApplyError>] = &[ super::apply::networkd::apply_networkd, + super::apply::nftables::apply_nftables, super::apply::chrony::apply_chrony, super::apply::unbound::apply_unbound, ]; diff --git a/src/templates/nftables/nftables.conf b/src/templates/nftables/nftables.conf new file mode 100644 index 0000000..bc17cc6 --- /dev/null +++ b/src/templates/nftables/nftables.conf @@ -0,0 +1,91 @@ +#!/usr/sbin/nft -f + +flush ruleset + +# nfsense nftables inet (ipv4 + ipv6) table +table inet nfsense_inet { + + # Rule Counters for Forward Rules + {% for rule in forward_rules -%} + {% if rule.counter -%} + counter fw_{{ loop.index }} { + comment "{{ rule.name }}" + } + {% endif -%} + {% endfor %} + + # Rule Counters for Destination NAT Rules + {% for rule in destination_nat_rules -%} + {% if rule.counter -%} + counter dnat_{{ loop.index }} { + comment "{{ rule.name }}" + } + {% endif -%} + {% endfor %} + + + # Rule Counters for Source NAT Rules + {% for rule in source_nat_rules -%} + {% if rule.counter -%} + counter snat_{{ loop.index }} { + comment "{{ rule.name }}" + } + {% endif -%} + {% endfor %} + + # Inbound Rules + chain inbound { + type filter hook input priority 0; policy drop; + + # Allow traffic from established and related packets, drop invalid + ct state vmap { established : accept, related : accept, invalid : drop } + + # Allow loopback traffic + iifname lo accept + + # temp Allow Inbound traffic + counter accept comment "temp inbound allow" + } + + # Forward Rules + chain forward { + type filter hook forward priority 0; policy drop; + + # Allow traffic from established and related packets, drop invalid + ct state vmap { established : accept, related : accept, invalid : drop } + + # Generated Forward Rules + {% for rule in forward_rules -%} + {% set index = loop.index -%} + {% for service in rule.services -%} + {{ rule.addresses }} {{ service }} {% if rule.counter %} counter name fw_{{ index }} {% endif %} {{ rule.verdict }} + {% endfor -%} + {% endfor -%} + } + + # Destination NAT Rules + chain prerouting { + type nat hook prerouting priority -100; policy accept; + + # Generated Destination NAT Rules + {% for rule in destination_nat_rules -%} + {% set index = loop.index -%} + {% for service in rule.services -%} + {{ rule.addresses }} {{ service }} {% if rule.counter %} counter name dnat_{{ index }} {% endif %} {{ rule.destination_nat_action }} + {% endfor -%} + {% endfor -%} + } + + # Source NAT Rules + chain postrouting { + type nat hook postrouting priority 100; policy accept; + + # Generated Source NAT Rules + {% for rule in source_nat_rules -%} + {% set index = loop.index -%} + {% for service in rule.services -%} + {{ rule.addresses }} {{ service }} {% if rule.counter %} counter name snat_{{ index }} {% endif %} {{ rule.source_nat_action }} + {% endfor -%} + {% endfor -%} + } +} \ No newline at end of file