Here is my final ruleset for my router. As far as I can tell, it correctly supports port forwarding and accepts specified ports to the server. WAN and LAN Interfaces are specified in their respective sets. Support for a captive portal exists, but hasn’t been thoroughly tested.
The port triggering section (which is based on Hazel’s Zone Port Triggering with Linux nftables) works correctly, as well. However, it only directs the port specified to the IP address that made the initial request. The port is held open to the WAN for a period of 10 minutes from last access from the IP address. Additional ports can be added in the rules per TCP/UDP port, but such is not handled by this ruleset.
Additionally, DoT (DNS-over-TLC) and DoQ (DNS-over-QUIC) are blocked from the local networks, but not from the router itself.
#############################################################################
# Define rules that affect both IPv4 and IPv6:
#############################################################################
#flush table inet firewall
table inet firewall {
################################################[ MAPS & SETS ]#################################################
# List of WAN interfaces:
set DEV_WAN { type ifname; elements = { wan } }
# List of interfaces denied access to WAN interfaces:
set DEV_WAN_DENY { type ifname; }
# List of LAN interfaces:
set DEV_LAN { type ifname; elements = { br0, mt7615_24g, mt7615_5g } }
# List of interfaces denied access to WAN interfaces from LAN:
set DEV_LAN_DENY { type ifname; }
############################[ Captive Portal Info ]#############################
# List of interfaces that have a Captive Portal on them:
set DEV_PORTAL { type ifname; }
# List of MAC addresses that passed Captive Portal:
set PORTAL_PASS { type ether_addr; }
##########################[ Accept TCP/UDP Port Sets ]##########################
set ACCEPT_PORT_TCP { type inet_service; flags interval; }
set ACCEPT_PORT_UDP { type inet_service; flags interval; }
############################[ Port Triggering Sets ]############################
set TRIGGER_LIST_TCP { type inet_service; flags interval; }
set TRIGGER_LIST_UDP { type inet_service; flags interval; }
###########################[ Port Forwarding Stuff ]############################
map FORWARD_PORT_TCP { type inet_service : ipv4_addr . inet_service; }
map FORWARD_PORT_UDP { type inet_service : ipv4_addr . inet_service; }
map FORWARD_RANGE_TCP { type inet_service : ipv4_addr; flags interval; }
map FORWARD_RANGE_UDP { type inet_service : ipv4_addr; flags interval; }
############################################[ DO NOT EDIT THESE ]#############################################
set PRIVATE_SUBNETS {
type ipv4_addr; flags interval;
elements = { 0.0.0.0/8, 10.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.2.0/24, 192.168.0.0/16, 224.0.0.0/5, 240.0.0.0/5 }
}
set INSIDE_NETWORK {
type ipv4_addr; flags interval;
elements = { 192.168.2.0/24, 192.168.21.0/24, 192.168.22.0/24 }
}
set TRIGGER_OPEN_TCP { type inet_service; flags timeout; }
map TRIGGER_PORT_TCP { type inet_service : ipv4_addr; }
set TRIGGER_OPEN_UDP { type inet_service; flags timeout; }
map TRIGGER_PORT_UDP { type inet_service : ipv4_addr; }
##############################################[ Filter: INPUT ]###############################################
chain INPUT {
type filter hook input priority filter + 100; policy drop;
# Allow traffic from established and related packets, drop invalid
ct state vmap { established : accept, related : accept, invalid : drop }
# Redirect incoming port 67 to port 68:
meta l4proto udp udp sport 67 udp dport 68 accept
# Accept anything that has been DNAT'ed:
ct status dnat accept
# Accept all connections from "lo" interface:
iifname lo accept
# Jump to "INPUT_WAN" chain for our WAN interfaces:
iifname @DEV_WAN jump INPUT_WAN
# Drop any remaining packets from our WAN interfaces:
iifname @DEV_WAN drop
# Jump to "INPUT_LAN" chain for our LAN interfaces:
iifname @DEV_LAN jump INPUT_LAN
# Allow traffic from loopback and LAN interfaces:
iifname @DEV_LAN accept
}
#############################################################################
chain INPUT_WAN {
# Allow multicast packets inbound from the Internet:
# NOTE: Commented out by nftables-script.sh if option "allow_multicast" is "N".
# pkttype multicast accept
# Control port 113 (IDENT) access from the Internet:
# NOTE: Commented out by nftables-script.sh if option "drop_ident" is "N".
# tcp dport 113 accept
# Accept any ports listed in "ACCEPT_PORT_TCP" and "ACCEPT_PORT_UDP" sets:
tcp dport @ACCEPT_PORT_TCP accept
udp dport @ACCEPT_PORT_UDP accept
}
#############################################################################
chain INPUT_LAN {
}
#############################################[ Filter: FORWARD ]##############################################
chain FORWARD {
type filter hook forward priority filter + 100; policy drop;
# Allow traffic from established and related packets, drop invalid:
ct state vmap { established : accept, related : accept, invalid : drop }
# Accept anything that has been DNAT'ed:
ct status dnat accept
# All portal interfaces must jump to "FORWARD_PORTAL" chain now:
iifname @DEV_PORTAL jump FORWARD_PORTAL
# LAN to WAN communication must jump to "FORWARD_WAN" chain now:
iifname @DEV_LAN oifname @DEV_WAN jump FORWARD_WAN
# Reject communication from WAN-restricted interfaces to WAN interfaces:
iifname @DEV_WAN_DENY oifname @DEV_WAN reject
# Forward connections from the LAN interfaces to WAN interfaces:
iifname @DEV_LAN oifname @DEV_WAN accept
# LAN to LAN communication must jump to "FORWARD_LAN" chain now:
iifname @DEV_LAN oifname @DEV_LAN jump FORWARD_LAN
# Reject communication from LAN-restricted interfaces to LAN interfaces:
iifname @DEV_LAN_DENY oifname @DEV_LAN reject
# Forward connections from the LAN interfaces to LAN interfaces:
iifname @DEV_LAN oifname @DEV_LAN accept
}
#############################################################################
chain FORWARD_PORTAL {
# Accept any packet that has the "Pass" mark:
# NOTE: "0x50617373" is "Pass" converted to hexadecimal! :p ==> CMD: "printf Pass | xxd -p" <==
mark 0x50617373 return
# Accept any packets directed to the Captive Portal WebUI:
ip daddr 192.168.2.1 tcp dport 80 accept
# Accept all DNS and DHCP communication:
meta l4proto {tcp, udp} @th,16,16 { 53, 67 } accept
# Reject all other communication:
reject
}
#############################################################################
chain FORWARD_WAN {
# Reject all DoT (DNS-over-TLS) packets from LAN interfaces:
# NOTE: Commented out by nftables-script.sh if option "allow_dot" is "N".
meta l4proto {tcp, udp} @th,16,16 853 reject
# Reject all DoQ (DNS-over-QUIC) packets from LAN interfaces:
# NOTE: Commented out by nftables-script.sh if option "allow_doq" is "N".
meta l4proto {tcp, udp} @th,16,16 8853 reject
}
#############################################################################
chain FORWARD_LAN {
}
##############################################[ Filter: OUTPUT ]##############################################
chain OUTPUT {
type filter hook output priority filter + 100; policy accept;
# User "vpn" must jump to the "OUTPUT_VPN" chain now:
meta skuid "vpn" jump OUTPUT_VPN
# All WAN interfaces must jump to the "OUTPUT_WAN" chain now:
oifname @DEV_WAN jump OUTPUT_WAN
# All LAN interfaces must jump to the "OUTPUT_LAN" chain now:
oifname @DEV_LAN jump OUTPUT_LAN
}
#############################################################################
chain OUTPUT_VPN {
# Default rule is to block from accessing anything but the "lo" interface.
# This rule will be replaced by the OpenVPN configuration script when connecting.
oifname != "lo" drop
}
#############################################################################
chain OUTPUT_WAN {
# Reject multicast packets outbound to the Internet.
# NOTE: Commented out by nftables-script.sh if option "allow_multicast" is "Y".
pkttype multicast reject
}
#############################################################################
chain OUTPUT_LAN {
}
##############################################[ Mangle: OUTPUT ]##############################################
chain MANGLE_OUTPUT {
type route hook output priority mangle + 100; policy accept;
# All packets must jump to "OUTPUT_VPN" chain now:
jump MANGLE_OUTPUT_VPN
}
#############################################################################
chain MANGLE_OUTPUT_VPN {
}
############################################[ Mangle: PREROUTING ]############################################
chain MANGLE_PREROUTING {
type filter hook prerouting priority mangle; policy accept;
# Jump to DDOS protection chain now:
jump MANGLE_PREROUTING_DDOS
# Mark any packets from MAC addresses that passed the Captive Portal with "Pass" flag.
# NOTE: "0x50617373" is "Pass" converted to hexadecimal! :p ==> CMD: "printf Pass | xxd -p" <==
iifname @DEV_PORTAL ether saddr @PORTAL_PASS counter meta mark set 0x50617373
}
#############################################################################
chain MANGLE_PREROUTING_DDOS {
# Drop packets that use bogus TCP flags:
tcp flags & (fin|syn) == fin|syn counter drop comment "Bogus TCP flags"
tcp flags & (syn|rst) == syn|rst counter drop comment "Bogus TCP flags"
tcp flags & (fin|rst) == fin|rst counter drop comment "Bogus TCP flags"
tcp flags & (fin|ack) == fin counter drop comment "Bogus TCP flags"
tcp flags & (ack|urg) == urg counter drop comment "Bogus TCP flags"
tcp flags & (psh|ack) == psh counter drop comment "Bogus TCP flags"
# Drop Null packets:
tcp flags & (fin|syn|rst|psh|ack|urg) == 0x0 counter drop comment "Null TCP flags"
# Drop new packets that don't use the SYN flag:
tcp flags & (fin|syn|rst|ack) != syn ct state new counter drop comment "New Packets without SYN flag"
# Drop XMAS packets:
tcp flags & (fin|syn|rst|psh|ack|urg) == fin|syn|rst|psh|ack|urg counter drop comment "XMAS Packets"
# Block fragmented packets:
ip frag-off & 0x1fff != 0 counter drop comment "Fragmented packets"
# Block Packets from spoofing as the "lo" interface:
iifname != "lo" ip saddr 127.0.0.0/8 counter drop comment "Spoofed IP address"
# Block Packets From Private Subnets from WAN interfaces. WAN interfaces should insert an
# "return" on their IP address/range into the "prerouting_private" chain in order to
# keep double-nat configurations working as expected:
iifname @DEV_WAN ip saddr @PRIVATE_SUBNETS jump MANGLE_PREROUTING_PRIVATE
}
#############################################################################
chain MANGLE_PREROUTING_PRIVATE {
# WAN interfaces should insert an "return" on their IP address/range into the
# "MANGLE_PREROUTING_PRIVATE" chain in order to keep double-nat configurations working as expected:
counter drop comment "Private IP subnet from WAN"
}
#############################################[ NAT: PREROUTING ]##############################################
chain NAT_PREROUTING {
type nat hook prerouting priority dstnat + 100; policy accept;
# All Captive Portal interfaces must jump to "PREROUTING_PORTAL" chain now:
iifname @DEV_PORTAL jump NAT_PREROUTING_PORTAL
# WAN interfaces must jump to "prerouting_wan" chain now:
iifname @DEV_WAN jump NAT_PREROUTING_WAN
# LAN interfaces must jump to "prerouting_lan" chain now:
iifname @DEV_LAN jump NAT_PREROUTING_LAN
}
#############################################################################
chain NAT_PREROUTING_PORTAL {
# Accept packets directed to the Captive Portal WebUI:
ip daddr 192.168.2.1 accept
# Accept any packet that has the "Pass" mark:
# NOTE: "0x50617373" is "Pass" converted to hexadecimal! :p ==> CMD: "printf Pass | xxd -p" <==
mark 0x50617373 accept
# Redirect any HTTP requests to the Captive Portal WebUI:
tcp dport 80 dnat ip to 192.168.2.1
}
#############################################################################
chain NAT_PREROUTING_WAN {
# Remove outbound port from "trigger_port" set if timeout has expired:
tcp dport != @TRIGGER_OPEN_TCP delete @TRIGGER_PORT_TCP { tcp dport : 0.0.0.0 }
udp dport != @TRIGGER_OPEN_UDP delete @TRIGGER_PORT_UDP { udp dport : 0.0.0.0 }
# Forward each port in "forward_port" map to it's respective IP address/port combo:
dnat ip addr . port to tcp dport map @FORWARD_PORT_TCP
dnat ip addr . port to udp dport map @FORWARD_PORT_UDP
# Forward range of ports in "trigger_port" map to their respective IP addresses:
dnat ip to tcp dport map @FORWARD_RANGE_TCP
dnat ip to udp dport map @FORWARD_RANGE_UDP
# Forward each port in "trigger_port" map to it's respective IP address --ONLY-- if the timeout hasn't expired:
tcp dport @TRIGGER_OPEN_TCP dnat ip to tcp dport map @TRIGGER_PORT_TCP
udp dport @TRIGGER_OPEN_UDP dnat ip to udp dport map @TRIGGER_PORT_UDP
}
#############################################################################
chain NAT_PREROUTING_LAN {
}
#############################################[ NAT: POSTROUTING ]#############################################
chain NAT_POSTROUTING {
type nat hook postrouting priority srcnat + 100; policy accept;
# All LAN interfaces must jump to the "NAT_POSTROUTING_LAN" chain:
oifname @DEV_LAN jump NAT_POSTROUTING_LAN
# All WAN interfaces must jump to the "NAT_POSTROUTING_WAN" chain:
oifname @DEV_WAN jump NAT_POSTROUTING_WAN
}
#############################################################################
chain NAT_POSTROUTING_LAN {
}
#############################################################################
chain NAT_POSTROUTING_WAN {
# If source IP address is from inside the network AND the destination port is in
# the port triggering list, jump to the appropriate chain:
ip saddr @INSIDE_NETWORK tcp dport @TRIGGER_LIST_TCP jump NAT_POSTROUTING_TRIGGER
ip saddr @INSIDE_NETWORK udp dport @TRIGGER_LIST_UDP jump NAT_POSTROUTING_TRIGGER
# Masquerade everything going out on our WAN interfaces:
masquerade
}
#############################################################################
chain NAT_POSTROUTING_TRIGGER {
# Remove outbound port from "trigger_port" set if timeout has expired:
tcp dport != @TRIGGER_OPEN_TCP delete @TRIGGER_PORT_TCP { tcp dport : 0.0.0.0 }
udp dport != @TRIGGER_OPEN_UDP delete @TRIGGER_PORT_UDP { udp dport : 0.0.0.0 }
# Link the outbound port to the source IP address:
add @TRIGGER_PORT_TCP { tcp dport : ip saddr }
add @TRIGGER_PORT_UDP { udp dport : ip saddr }
# Update the timeout for the protocol/port combination:
update @TRIGGER_OPEN_TCP { tcp dport timeout 10m }
update @TRIGGER_OPEN_UDP { udp dport timeout 10m }
}
}