mirror of
https://github.com/speatzle/nfsense.git
synced 2025-05-10 18:38:22 +00:00
Implement nfTables Rule Generation
This commit is contained in:
parent
716fa43ade
commit
d95f2d9f01
4 changed files with 361 additions and 0 deletions
|
@ -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
268
src/apply/nftables.rs
Normal 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)),
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
|
|
91
src/templates/nftables/nftables.conf
Normal file
91
src/templates/nftables/nftables.conf
Normal 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 -%}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue