>
Updated on October 24, 2024
The first thing you need to do is enable routing on the Linux machine that will be used as a firewall [1].
Then for each network interface on the router, set up the following:
References:
Internet | | -------------------- | ISP Router | | 192.168.37.2 | -------------------- | | Firewall | -------------------- | eth0 | | 192.168.37.52 | | | | Firewall | | | | 192.168.200.254 | | USB/eth1 | ------------------- | | LAN | --------------------- | eth0 | | 192.168.200.3 | | | | Workstation | | | | Default Gateway = | | 192.168.200.254 | ---------------------
#!/usr/bin/nft -f
flush ruleset
table inet router {
#------------------- Acronyms and Port #'s---------------------
# NTP - Network Time Protocol - Port 123
# DNS - Domain Name Server - Port 53
# HTTP - Hyper Text Transport Protocol - Port 80
# HTTPS - Hyer Text Transport Protocol Secure - Port 443
# ----------------- Definations and Sets ------------------------
define Internet_Interface = eth0
define LAN_Interface = eth1
set LAN_IP_Addresses {
type ipv4_addr
elements = {192.168.200.3}
}
# --------------------- Pre Rounting --------------------------
chain prerouting {
type nat hook prerouting priority filter; policy accept;
}
# -------------- Input to the Firewall Itself-----------------
chain input {
type filter hook input priority filter; policy accept;
}
# ------------- Output to the Firewall Itself ----------------
chain output {
type filter hook output priority filter; policy accept;
}
#-------------------- Firewall Code --------------------------
#--------------------- LAN_to_Internet -----------------------
chain LAN_to_Internet {
# allow DNS out
udp dport 53 ct state new counter accept
tcp dport 53 ct state new counter accept
# allow HTTP/HTTPS out
tcp dport {80, 443} ct state new counter accept
udp dport {80, 443} ct state new counter accept
# allow NTP out
udp dport 123 ct state new counter accept
# drop everything else
counter drop
}
# -------------------- Forward ----------------------------------
chain forward {
# --- This will allow Debian update and upgrade ---
type filter hook forward priority filter; policy drop;
# Common Code
ct state vmap { established:accept, related:accept, invalid:drop }
ip protocol icmp accept
# LAN_to_Internet
# Since there are only two network interfaces,
# it is only neccessary to test for one of them.
# The line below will only work when Nordvpn is disabled
oifname $Internet_Interface counter jump LAN_to_Internet
# This line will work with Nordvpn enabled
ip saddr @LAN_IP_Addresses counter jump LAN_to_Internet
# Internet_to_LAN
# Only allow return traffic in and through the firewall
oifname $LAN_Interface counter drop
}
# ------------------- Post Routing -----------------------------
chain postrouting {
type nat hook postrouting priority filter; policy accept;
masquerade
}
}
In the chain LAN_to_Interface, the following two statements:
tcp dport {80, 443} ct state new counter accept
udp dport {80, 443} ct state new counter accept
Can be replace by the single statement:
meta l4proto { tcp, udp } th dport 53 accept
Likewise, the following two statements:
tcp dport {80, 443} ct state new counter accept
udp dport {80, 443} ct state new counter accept
Can be replaced by the single statement:
meta l4proto { tcp, udp } th dport {80, 443} ct state new counter accept
References:
This example is from Red Hat Linux Release 8 Chapter 42. Getting started with nftables. More precisely, it is from Chapter 42.7 - Example Protecting a LAN and DMZ using an nftables Script.
The network has the following conditions:
The following are the requirements to the nftable firewall:
Red Hat did not provide a diagram; this is my work.
Internet / \ | | | ------------------------------------------- | INT_DEV = enp1s0 | | IPv4=203.0.113.1 and IPv6=2001:db8a::1 | | | | Linx Router/Firewall | | | | DMZ_DEV LAN-DEV | | ensp8s0 enp7s0 | ------------------------------------------- _______________| |_______________ Public Addresses | | Private Addresses ======================= | =================== ========================== | ========================== || | || || | || || ---------- || || ---------- || || | Switch | || || | Switch | || || ---------- || || ---------- || || ________| |_______ || || _______________| | |_____________ || || | | || || | | | || || ----------------- ---------------- || || -------------- -------------- ----------- || || | Web Server | | Web Server 2 | || || | Admin PC | | Admin PC | | More PC | || || |198.51.100.5 | | 198.51.100.x | || || | 10.0.0.100 | | 10.0.0.200 | | 10.0.0.x | || || |2001:db8:b::/5 | | | || || -------------- -------------- =========== || || ----------------- -------------- || || || || || || || || DMZ Zone 198.51.100.0/24 || || LAN 10.0.0.0/24 || || and 2001:db8:b::/56 || || || ============================================== ========================================================
# Remove all rules
flush ruleset
# Table for both IPv4 and IPv6 rules
table inet nftables_svc {
# Define variables for the interface name
define INET_DEV = enp1s0
define LAN_DEV = enp7s0
define DMZ_DEV = enp8s0
# Set with the IPv4 addresses of admin PCs
set admin_pc_ipv4 {
type ipv4_addr
elements = { 10.0.0.100, 10.0.0.200 }
}
# Chain for incoming trafic. Default policy: drop
chain INPUT {
type filter hook input priority filter
policy drop
# Accept packets in established and related state, drop invalid packets
ct state vmap { established:accept, related:accept, invalid:drop }
# Accept incoming traffic on loopback interface
iifname lo accept
# Allow request from LAN and DMZ to local DNS server
iifname { $LAN_DEV, $DMZ_DEV } meta l4proto { tcp, udp } th dport 53 accept
# Allow admins PCs to access the router using SSH
iifname $LAN_DEV ip saddr @admin_pc_ipv4 tcp dport 22 accept
# Last action: Log blocked packets
# (packets that were not accepted in previous rules in this chain)
log prefix "nft drop IN : "
}
# Chain for outgoing traffic. Default policy: drop
chain OUTPUT {
type filter hook output priority filter
policy drop
# Accept packets in established and related state, drop invalid packets
ct state vmap { established:accept, related:accept, invalid:drop }
# Accept outgoing traffic on loopback interface
oifname lo accept
# Allow local DNS server to recursively resolve queries
oifname $INET_DEV meta l4proto { tcp, udp } th dport 53 accept
# Last action: Log blocked packets
log prefix "nft drop OUT: "
}
# Chain for forwarding traffic. Default policy: drop
chain FORWARD {
type filter hook forward priority filter
policy drop
# Accept packets in established and related state, drop invalid packets
ct state vmap { established:accept, related:accept, invalid:drop }
# IPv4 access from LAN and internet to the HTTPS server in the DMZ
iifname { $LAN_DEV, $INET_DEV } oifname $DMZ_DEV ip daddr 198.51.100.5 tcp dport 443 accept
# IPv6 access from internet to the HTTPS server in the DMZ
iifname $INET_DEV oifname $DMZ_DEV ip6 daddr 2001:db8:b::5 tcp dport 443 accept
# Access from LAN and DMZ to HTTPS servers on the internet
iifname { $LAN_DEV, $DMZ_DEV } oifname $INET_DEV tcp dport 443 accept
# Last action: Log blocked packets
log prefix "nft drop FWD: "
}
# Postrouting chain to handle SNAT
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
# SNAT for IPv4 traffic from LAN to internet
iifname $LAN_DEV oifname $INET_DEV snat ip to 203.0.113.1
}
}
The only line that I thought needed an explanation was:
oifname $INET_DEV meta l4proto { tcp, udp } th dport 53 accept
This single statement will work on both udp and tcp DNS queries. DNS queries general use udp; however, if all of the data will not fit in a single packet then it may use tcp. l4proto refers to level 4 protocol. Level 4 is the transport layer, and tcp and upd are level 4 protocols. The 'th" stands for transport header [1].
The one thing this example did not include was the Network Time Protocol (NAT).
References:
The following is nftables.org's example of a DMZ firewall. Nftables.org is the organization responsible for nftables.
This example assumes a classic perimetral firewall, which is connected to 3 networks: internet, DMZ, and workstation LAN.
You could either put your ruleset all in the same file or split it in different text files for better maintenance.
In this example we will use several files:
Internet / \ | | | ------------------------------------------- | bond0 | | nic_inet | | | | Linx Router/Firewall | | | | nic_dmz nic_lan | | bond1 bond2 | -------------------------------------------- _____________| |____________ Private Addresses | | Private Addresses ======================= | =================== ==================== | ================== || | || || | || || ---------- || || ---------- || || | Swtich | || || | Switch | || || ---------- || || ---------- || || ________| |_______ || || _________| |______ || || | | || || | | || || ----------------- ---------------- || || ---------------- -------------- || || | Web Server 1 | | | || || | WorkStation1 | | | || || | 10.0.1.2 | | Web Server 2 | || || | 10.0.2.2 | | Workstation | || || | fe00:1::2 | | | || || | fe00:2::2 | | | || || ----------------- -------------- || || ---------------- -------------- || || || || || || DMZ Zone 10.0.1.0/24 || || LAN 10.0.2.0/24 || || and fe00:1::/64 || || and fe00:2::/64 || ============================================== ==========================================
flush ruleset
include "./defines.nft"
table inet filter {
chain global {
ct state established,related accept
ct state invalid drop
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
udp dport 53 accept
}
include "./inet-filter-sets.nft"
include "./inet-filter-forward.nft"
include "./inet-filter-local.nft"
}
# interfaces
define nic_inet = bond0
define nic_dmz = bond1
define nic_lan = bond2
# network ranks
define net_ipv4_dmz = 10.0.1.0/24
define net_ipv6_dmz = fe00:1::/64
define net_ipv4_lan = 10.0.2.0/24
define net_ipv6_lan = fe00:2::/64
# some machines
define server1_ipv4 = 10.0.1.2
define server1_ipv6 = fe00:1::2
define workstation1_ipv4 = 10.0.2.2
define workstation1_ipv6 = fe00:2::2
set myset_ipv4 {
type ipv4_addr;
elements = { $server1_ipv4 , $workstation1_ipv4 }
}
set myset_ipv6 {
type ipv6_addr;
elements = { $server1_ipv6 , $workstation1_ipv6 }
}
chain dmz_in {
# your rules for traffic to your dmz servers
ip saddr @myset_ipv4
ip6 saddr @myset_ipv6
}
chain dmz_out {
# your rules for traffic from the dmz to internet
}
chain lan_in {
# your rules for traffic to your LAN nodes
}
chain lan_out {
# your rules for traffic from the LAN to the internet
}
chain forward {
type filter hook forward priority 0; policy drop;
jump global
oifname vmap { $nic_dmz : jump dmz_in , $nic_lan : jump lan_in }
oifname $nic_inet iifname vmap { $nic_dmz : jump dmz_out , $nic_lan : jump lan_out }
}
chain input {
type filter hook input priority 0 ; policy drop;
jump global
# your rules for traffic to the firewall here
}
chain output {
type filter hook output priority 0 ; policy drop;
jump global
# your rules for traffic originated from the firewall itself here
}
As all of these are plain text files, you can even think of maintaining them with some kind of control version
This page was last edited on 1 June 2018, at 13:10.
This is what is wrong with Linux. This organization is responsible for nftables. They do not define the problem - what is the nftable firewall susppose to do? They may have done this to make the code more general. As it is, it is just a skeleton to be completed by the user. In my opinion, this is not a primer or tutorial.
It does not seem logical to me to group IP addresses in DMZ with IP address in LAN - What were they thinking?
Both the servers in the DMZ and the workstation in LAN are in the IP private address space. Consequently, there has to be network addresses translation (NAT) in order to communicate with the Internet. Yet, there is no postrouting code to accomplish NAT in this example.
Monotux.tech took the skeleton firewall provided by nftables.org and used it for his home router: nftables multi network (home) router primer.
Monotux network is more complicated than the classic DMZ firewall by the nftable.org, and he makes the same huge mistake that nftables.org did. That is he does not define the network and what is to be accomplished by the firewall.
However, in his defense, he does flush out a lot more of the details than the nftable.org. He provides the code for Network Address Translation (nat), and code for DNSSEC (Domain Name Server Security) and DoT (DNS over TLS) and MQTT (Message Queue Telemetry Transport).
Monotux does not clearly state the objectives of his firewall. You have to read and understand his code, to try to understand what he was trying to accomplish.
The diagram is my work.
WAN / \ | | | ------------------------------------------- | eth0 | | if_wan | | | | Linx Router/Firewall | | | | if_iot if_clients if_services | | eth1 eth2 eth3 | ------------------------------------------- ______________________| | |____________ Private Addresses | | | Private Addresses ============== | =========== ============================= ========= | ============ || | || || || || | || || ---------- || || || || -------------- || || | Swtich | || || || || | Server | || || ---------- || || || || | DNS | || || ___| |___ || || || || | NTP | || || | | || || Clients || || | MQTT | || || ------- ------- || || || || | D-base | || || | | | | || || || || ------------- || || | IoT | | IoT | || || || || || || | | | | || || || || || || ------- ------- || || || || || || || || net_Clients = || || || || net_iot = || || 198.168.254.0/24 || || net_services = || || 198.168.255.0/24 || || || || 198.168.253.0/24 || ============================ ============================= =======================
SSH to Firewall
# /etc/nftables.conf
flush ruleset
# replace these
define if_wan = eth0
define if_iot = eth1
define if_clients = eth2
define if_services = eth3
define net_iot = 192.168.255.0/24
define net_clients = 192.168.254.0/24
define net_services = 192.168.253.0/24
define host_server = 192.168.253.254
# Covers IPv4 and IPv6
table inet filter {
# A named set
set ports_mqtt {
type inet_service; flags interval;
elements = { 1883,8883 }
}
# Allow DNSSEC, HTTP(s) and DoT out from our firewall
set firewall_out_tcp_accepted {
type inet_service; flags interval;
elements = { 53, 80, 443, 853 }
}
# Allow plain DNS & NTP from our firewall
set firewall_out_udp_accepted {
type inet_service; flags interval;
elements = { 53, 123 }
}
# This is due to one of the quirks with netfilter (same applies
# for iptables), you have to accept established and related
# connections explicitly. Making it a separate chain like this
# will allow us to quickly jump to it.
#
# We also allow ICMP for both v4 and v6.
chain global {
ct state established,related accept
ct state invalid drop
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
}
chain reject_politely {
reject with icmp type port-unreachable
}
# Control what is allowed into the iot network, if any
chain iot_in {}
# ...and what is allowed out
chain iot_out {
# Accept MQTT traffic to our internal MQTT tracker
tcp dport @ports_mqtt ip daddr $host_server ip saddr $net_iot ct state new accept
}
# Control what is allowed into our services network
chain services_in {
# Allow forwarded MQTT traffic from our IOT net to our server
tcp dport @ports_mqtt ip daddr $host_server ip saddr $net_iot ct state new accept
}
# ...and control what is allowed out from our services network
chain services_out {
# Allow NTP out on the internet
udp dport 123 ip saddr $net_services ct state new accept
# Allow HTTP/HTTPS out as well, but use an anonymous set for this
tcp dport { 80, 443 } ip saddr $net_services ct state new accept
}
# Nothing accepted into our clients network
chain clients_in {}
chain clients_out {
# yolo
accept
}
# repeat this for your subnets
# Here's where some interesting things happen. This is where we
# control what is forwarded between subnets, including using the
# chains we defined previously. Our default policy is drop.
chain forward {
type filter hook forward priority 0; policy drop;
# First accept established & related traffic, by jumping to
# our global chain
jump global
# Verdict maps! This saves me _several lines_ of rules!!11
# This could have been written line for line as well, I guess.
# Map the output interface to a chain. So if traffic has been
# forwarded to this interface this is what we allow in, if
# that makes sense?
oifname vmap { $if_services : jump services_in,
$if_iot : jump iot_in,
$if_clients : jump clients_in }
# If the output interface is our external, what is allowed out
# from each subnet?
oifname $if_wan iifname vmap { $if_services : jump services_out,
$if_iot : jump iot_out,
$if_clients : jump clients_out }
}
# Control what is allowed on our firewall
chain incoming {
type filter hook input priority 0; policy drop;
jump global
iif lo accept
# Allow SSH but rate limit on our external interface
iifname $if_wan tcp dport 22 ct state new flow table ssh-ftable { ip saddr limit rate 2/minute } accept
# Allow SSH from our clients
iifname $if_clients tcp dport 22 ct state new accept
# Rejections should be nice
jump reject_politely
}
# Control what is allowed out from our firewall itself.
chain outgoing {
type filter hook output priority 100; policy drop;
jump global
# What should be allowed out from your firewall itself? If
# anything is acceptable, change the policy or just write:
# accept
# Otherwise, specify what is allowed, some examples below
udp dport @firewall_out_udp_accepted ct state new accept
tcp dport @firewall_out_tcp_accepted ct state new accept
jump reject_politely
}
}
# Finally, NAT!
table ip firewall {
chain prerouting {
type nat hook prerouting priority 0;
# Port forward tcp 80/443 to our internal webserver
iifname $if_wan tcp dport { http, https } dnat to "192.168.0.100" comment "DNAT to webserver"
}
#### POSTROUTING
chain postrouting {
type nat hook postrouting priority 100;
# Here you can specify which nets that are allowed to do NAT. For
# my own network I'm not allowing my IoT or management networks to
# reach the internet.
ip saddr $net_clients oifname $if_wan masquerade
}
}
Sometimes I want to refer to multiple interfaces or subnets in several rules, so I use more sets. The documentation is great but here are two more examples:
set if_all_clients {
type ifname; flags constant;
elements = { $if_office, $if_wifi }
}
set net_clients {
type ipv4_addr; flags interval;
elements = { $net_wifi, $net_office }
}
Concatenations: Now this is really pushing what is necessary, but I liked this feature and it has saved me several lines so… :-)
# https://wiki.nftables.org/wiki-nftables/index.php/Concatenations
set services_in_tcp_ip_port {
type ipv4_addr . ipv4_addr . inet_service;
elements = {
$host_app . $host_db . 5432,
$host_app . $host_tsdb . 8086,
$net_iot . $host_mqtt . 1883
}
}
chain services_in {
ip saddr . ip daddr . tcp dport @services_in_tcp_ip_port counter ct state new accept
}
So now we need one line in our services_in chain to allow our app server to talk to our database server and time series database servers, and let out IoT network talk to our MQTT server. There's something nice with this, and apparently it's also pretty fast?
Conclusions: I started writing this months ago before realizing that would end up just poorly rewriting the official nftables wiki. I do hope that the content above might come in handy for someone else, tho.
References: