Implement nfTables Rule Generation

This commit is contained in:
Samuel Lorch 2024-02-11 21:43:10 +01:00
parent 716fa43ade
commit d95f2d9f01
4 changed files with 361 additions and 0 deletions

View file

@ -2,6 +2,7 @@ use thiserror::Error;
pub mod chrony;
pub mod networkd;
pub mod nftables;
pub mod unbound;
#[derive(Error, Debug)]

268
src/apply/nftables.rs Normal file
View file

@ -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<String>,
pub addresses: String,
pub counter: bool,
pub verdict: Option<String>,
pub destination_nat_action: Option<String>,
pub source_nat_action: Option<String>,
}
fn convert_addresses_to_strings(addresses: Vec<Address>) -> Vec<String> {
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>) -> 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<Address>,
destination_addresses: Vec<Address>,
) -> Result<String, ApplyError> {
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<Service>) -> Result<Vec<String>, 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<Address>,
dnat_service: Option<Service>,
) -> Result<String, ApplyError> {
Ok("".to_string())
}
fn generate_source_nat_action(
snat_address: Option<Address>,
snat_service: Option<Service>,
) -> Result<String, ApplyError> {
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)),
}
}

View file

@ -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,
];

View file

@ -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 -%}
}
}