Building a home router

2024 edition

WARNING: technical content ahead! There’s also a tonne of config files, which make this page look longer than it really is, but hopefully they’ll help other people who want to do similar work.

Back in 2017 I described how to build a home router based on CentOS 7.

C7 is now out of date, so I figured it was time to rebuild it, this time using Rocky Linux 9. That should give me another 8 years of security patches!

My requirements were mostly the same, but I wasn’t going to use WiFi on the machine; I’d already replaced that with Access Points. I also wasn’t so much with IPv6 any more; this is mostly because the Hurricane Electric tunnelbroker IP addresses were on a lot of “low reputation” lists, and I was getting annoyed with that. I still use IPv6 but only specific machines, with a static configuration, use it.

I figured it was also time to get a new machine. There are a LOT of cheap hand-sized computers, these days, including some dual port ones. I went for a CTone Mini PC; 8GB RAM, 256Gb SSD, dual 1Gbit NICs. Sounded perfect. Unfortunately it wasn’t. It uses RTL chipsets, which appear to be less performant than the existing machine with Intel NICs; it worked but speed test results were consistently 100Mbit/s slower. So I got a new SSD for my old machine and built fresh on that. If I failed to get it to work then I could just put the old SSD back in. The new slow machine isn’t wasted though; it’s now a backup just in case my primary ever fails.

Basic networking

Requirements

At home I have 3 VLANS; 10 (LAN), 11 (guestnet), 12 (IoT). Although I no longer need to (no wireless) I’m going to keep using bridges for everything and I’m going to keep using the old name.

So, to summarize, the result would look something like

Use Bridge Interface
Internet br-wan enp2s0
LAN br-lan enp3s0.10
Guest br-guest enp3s0.11
IoT br-iot enp3s0.12

For simplicity, I’m going to assign enp2s0 to $WAN, and enp3s0 to $LAN.

WAN

First we need to make sure there’s no existing connections defined from the OS install. This can be done with the nmcli con show command. If you see anything like Wired connection 1 then you should delete them with nmcli con del ....

You will lose network access doing this, so make sure you’re on the console!

Bridges are created with nmcli connection add type bridge ... and interfaces are associated with it with nmcli connection add type bridge-slave ... commands.

So the WAN interface would be created with

nmcli device set $WAN autoconnect yes

nmcli connection add type bridge con-name br-wan ifname br-wan bridge.stp no ipv4.method auto ipv4.dns "10.0.0.1 10.0.0.5" ipv4.dns-search "spuddy.org" ipv6.method disabled
nmcli connection add type bridge-slave con-name $WAN ifname $WAN master br-wan

In this I am hard-coding my internal DNS servers (this machine and my backup DNS server) and DNS search path.

Woohoo! At this point the machine should(!) have internet access.

Internal VLANs

Similarly we can define the 3 VLANs and bridges. br-lan will have a static IPv6 address, but the rest just have IPv4.

nmcli device set $LAN autoconnect no

nmcli connection add type bridge con-name br-lan ifname br-lan bridge.stp no ip4 10.0.0.1/24 ipv6.method manual ipv6.address 2001:470:1f07:dc4::1/64
nmcli connection add type vlan con-name $LAN.10 ifname $LAN.10 vlan.parent $LAN vlan.id 10 slave-type bridge master br-lan
nmcli device set $LAN.10 autoconnect yes

nmcli connection add type bridge con-name br-guest ifname br-guest bridge.stp no ip4 10.100.100.1/24 ipv6.method disabled
nmcli connection add type vlan con-name $LAN.11 ifname $LAN.11 vlan.parent $LAN vlan.id 11 slave-type bridge master br-guest
nmcli device set $LAN.11 autoconnect yes

nmcli connection add type bridge con-name br-iot ifname br-iot bridge.stp no ip4 10.100.200.1/24 ipv6.method disabled
nmcli connection add type vlan con-name $LAN.12 ifname $LAN.12 vlan.parent $LAN vlan.id 12 slave-type bridge master br-iot
nmcli device set $LAN.12 autoconnect yes

We can now see how this looks:

# nmcli dev
DEVICE     TYPE      STATE                   CONNECTION
br-wan     bridge    connected               br-wan
br-lan     bridge    connected               br-lan
br-guest   bridge    connected               br-guest
br-iot     bridge    connected               br-iot
enp2s0     ethernet  connected               enp2s0
enp3s0.10  vlan      connected               enp3s0.10
enp3s0.11  vlan      connected               enp3s0.11
enp3s0.12  vlan      connected               enp3s0.12
lo         loopback  connected (externally)  lo
enp3s0     ethernet  disconnected            --
enp1s0     ethernet  unavailable             --

# nmcli con
NAME              UUID                                  TYPE       DEVICE
br-wan            0102b304-effc-4437-a70a-aa3360d4d3e0  bridge     br-wan
br-lan            acf41256-e68b-40a8-a85a-944eb95035a6  bridge     br-lan
br-guest          a3c962bb-e605-46aa-8d43-7cb9db4ebad1  bridge     br-guest
br-iot            f7715bbd-1a7c-41fb-bfb0-5ddc2900048a  bridge     br-iot
enp2s0            e966376e-39e5-4789-aaaa-5a2c0154323d  ethernet   enp2s0
enp3s0.10         bfd85eca-989c-499c-89d2-cb4f57d0cde9  vlan       enp3s0.10
enp3s0.11         a63e60ce-e808-499e-bfb1-aedb5ee83541  vlan       enp3s0.11
enp3s0.12         2d775836-ab3b-4ccb-a120-c32e2ec0efc7  vlan       enp3s0.12
lo                f63cce44-1aed-467f-bd27-96348e58c8f2  loopback   lo

If we install the old bridge-utils package (from EPEL) we can also see very simply the bridge configurations

% brctl show
bridge name     bridge id               STP enabled     interfaces
br-guest        8000.000db9439cce       no              enp3s0.11
br-iot          8000.000db9439cce       no              enp3s0.12
br-lan          8000.000db9439cce       no              enp3s0.10
br-wan          8000.000db9439ccd       no              enp2s0

And IP addresses can be seen with ip -4 a. So far so good!

Config files.

NetworkManager is not meant to be manipulated via config files (I much prefer the old /etc/sysconfig/network-scripts) but there are files we can look at, in /etc/NetworkManager/system-connections. e.g.

% ls /etc/NetworkManager/system-connections/
br-guest.nmconnection  br-wan.nmconnection     enp3s0.11.nmconnection
br-iot.nmconnection    enp2s0.nmconnection     enp3s0.12.nmconnection
br-lan.nmconnection    enp3s0.10.nmconnection

IPv6

Hurricane Electric IPv6 tunnels are very easy to set up. You need three values; the tunnelbroker endpoint IPv4 address, and the two IPv6 address associated with each end. These will probably be ….::2 for your end, and ….::1 for their end.

nmcli connect add type ip-tunnel ifname he-sit mode sit remote $ipv4address ipv4.method disabled ipv6.method manual ipv6.address $connection::2/64 ipv6.gateway $connection::1

This will show up under nmcli con and nmcli dev

% nmcli dev | grep he-sit
he-sit     iptunnel  connected               ip-tunnel-he-sit

% nmcli con | grep he-sit
ip-tunnel-he-sit  6840bf43-8275-4ccc-88a1-6ff16caabea2  ip-tunnel  he-sit

The tunnel should come up (if your IPv4 address is correct on the HE end) and we can see this in ip -6 a output


24: he-sit@NONE: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1480 state UNKNOWN qlen 1000
    inet6 _connection_::2/64 scope global noprefixroute
       valid_lft forever preferred_lft forever

If all is going well, ping6 google.com should work!

PING google.com(lga25s41-in-x0e.1e100.net (2607:f8b0:4006:80f::200e)) 56 data bytes
64 bytes from lga25s41-in-x0e.1e100.net (2607:f8b0:4006:80f::200e): icmp_seq=1 ttl=119 time=9.21 ms
...

Now HE uses your IPv4 address to validate the tunnel is allowed to be established. If you’re on a dynamic IP then a simple script that calls their API would work.

PASS=YOUR_PASSWORD
USERID=YOUR_USERNAME
GTUNID=YOUR_TUNNEL_ID
HOST=https://$USERID:$PASS@ipv4.tunnelbroker.net

newip=$1

# Check to see what the end point currently is
x=$(wget -q --no-check-certificate -O - $HOST/tunnelInfo.php?tid=$GTUNID | sed -n 's/^.*<clientv4>\(.*\)<.*$/\1/p')

if [ "x$x" == "x$newip" ]
then
  echo Tunnel IP is already correct
  exit
fi

wget -q --no-check-certificate -O - "$HOST/nic/update?hostname=$GTUNID&myip=$newip"

Firewalls and routing

That’s the low level plumbing done. Now to make it look like a router

Forwarding

Routers need to forward packets… This is done by setting sysctl options. They can be made to persist by putting them into a config file; e.g.

% cat /etc/sysctl.d/forward.conf
net.ipv4.ip_forward = 1
net.ipv4.conf.all.forwarding = 1
net.ipv6.conf.all.forwarding = 1

At this point the 3 VLANs can see each other. Unrestricted and there’s no internet access, but we’re getting there!

firewalld

RL9 has two ways that can manipulate network table rules; the first is firewalld, and the second is to use nftables directly.

I prefer nftables, so we need to disable firewalld and enable `nftables

# systemctl disable --now firewalld
# systemctl mask firewalld
# systemctl enable --now nftables

nftables

I’m going to look at this similarly to how iptables did it. We have standard rules (INPUT/OUTPUT/FORWARD) we have to look at them from the perspective of the router. INPUT rules are those that impact packets that are targeted at the router; OUTPUT rules are those that originate from the router; FORWARD rules are those that pass through the router. This gets important for NAT where the packet targets the router’s external IP address. Similarly for NAT rules we have PREROUTING and POSTROUTING.

nftables has a bit more “fluff” needed to create the necessary structures, so that’s where we start

# Clear out all the rules
flush ruleset

# Create empty structures
add table ip filter
add table ip nat
add table ip6 filter

I’m also going to define some structures that we’ll use later.

add map nat fport { type inet_service : interval ipv4_addr . inet_service ; flag
s interval; }
add set ip nat reflect { type inet_service; }
add set ip nat this_host { type ipv4_addr; }

add set ip6 filter my_hosts { type ipv6_addr; }

IPv4

We can now start building the IPv4 rules. Let’s create the 3 standard chains and 2 more chains that will be used for logging of packets

add chain ip filter INPUT { type filter hook input priority 0; policy accept; }
add chain ip filter OUTPUT { type filter hook output priority 0; policy accept; }
add chain ip filter FORWARD { type filter hook forward priority 0; policy accept; }

# Log rules for input and forwarding
add chain ip filter LOGGER
add chain ip filter LOGGERF

The logging rules are pretty simple. We limit how many can be sent, to avoid DoSing ourselves with log messages!

# Logging rule for INPUT drops
add rule ip filter LOGGER limit rate 2/minute burst 5 packets counter log prefix "IPTables-Dropped: "
add rule ip filter LOGGER counter drop

# Logging rule for FORWARD drops
add rule ip filter LOGGERF limit rate 2/minute burst 5 packets counter log prefix "IPTables-Dropped-Forward: "
add rule ip filter LOGGERF counter drop

Now we can handle INPUT rules (remember, this is traffic meant for this machine, and it may come from any of the VLANs or internet). We want to allow all traffic from the LAN; guestnet and IoT have limited access. Everything else should be blocked. Remember, we’re only talking about traffic targeting the router; we’ll get to port forwarding later.

# Allow traffic for established connections, blocking invalid header format
add rule ip filter INPUT ct state related,established counter accept
add rule ip filter INPUT ct state invalid counter drop

# All localhost traffic allowed
add rule ip filter INPUT iifname "lo" counter accept

# The LAN can see this machine
add rule ip filter INPUT iifname "br-lan" counter accept

# Allow the world to ping this machine (amongst other things)
add rule ip filter INPUT ip protocol icmp counter accept

# Let guestnet and IoT see DNS servers on this machine (tcp+udp)
add rule ip filter INPUT iifname "br-guest" tcp dport 53 counter accept
add rule ip filter INPUT iifname "br-guest" udp dport 53 counter accept
add rule ip filter INPUT iifname "br-guest" tcp dport 853 counter accept
add rule ip filter INPUT iifname "br-guest" udp dport 853 counter accept

# IoT may also need NTP access
add rule ip filter INPUT iifname "br-iot" tcp dport 53 counter accept
add rule ip filter INPUT iifname "br-iot" udp dport 53 counter accept
add rule ip filter INPUT iifname "br-iot" udp dport 123 counter accept

# Log block traffic; this could be noisy, so only enable it if you
# want lots of logs!
## add rule ip filter INPUT counter jump LOGGER

# Block all other incoming traffic
add rule ip filter INPUT counter drop

OUTPUT rules are very simple. We allow everything, because this is just for traffic that originated on this machine.

add rule ip filter OUTPUT ct state related,established counter accept
add rule ip filter OUTPUT ct state invalid counter drop

Now we need to allow traffic from our internal networks to see the internet. I’m also going to add a special rule; if my mobile is on guestnet then it can see the whole of the WAN.

There are also some other special rules (eg allow guests to see my printer) that we need to handle

# Allow established connections to work, blocking invalid header format
add rule ip filter FORWARD ct state related,established counter accept
add rule ip filter FORWARD ct state invalid counter drop

# Allow the LAN to see the world
add rule ip filter FORWARD iifname "br-lan" counter accept

# Allow return traffic for NAT'd connections to work
add rule ip filter FORWARD iifname "br-wan" ct status dnat counter accept

# Allow my mobile devices to have access to everything
add rule ip filter FORWARD iifname "br-guest" ip saddr 10.100.100.8 counter accept

# Allow guestnet to see the printer
add rule ip filter FORWARD iifname "br-guest" ip daddr 10.0.0.10 counter accept

# Allow guestnet devices to see my MQTT server
add rule ip filter FORWARD iifname "br-guest" oifname "br-lan" tcp dport 1883 counter accept

# Allow guestnet to see the internet
add rule ip filter FORWARD iifname "br-guest" oifname "br-wan" counter accept

# Allow IoT devices to see my MQTT server
add rule ip filter FORWARD iifname "br-iot" oifname "br-lan" tcp dport 1883 counter accept

# Allow ESP8266 devices on IoT to be network updates
# (The process causes the ESP8266 to call back to a webserver on the host)
add rule ip filter FORWARD iifname "br-iot" oifname "br-lan" tcp dport 8266 counter accept

# Allow IoT devices to see the internet
add rule ip filter FORWARD iifname "br-iot" oifname "br-wan" counter accept

# Allow marked traffic (ie reflected traffic) to reach the destination
add rule ip filter FORWARD meta mark 100 counter accept

# Log and block all other forwarded traffic
add rule ip filter FORWARD counter jump LOGGERF
add rule ip filter FORWARD counter drop

NAT

OK! We have connectivity on the internal network, now we need to set up the NAT rules to allow the internal network to SNAT (source NAT) to reach the internet, and to allow the internet to DNAT (destination NAT) to allow it to reach specific internal services.

There’s also an odd edge-case. If you’re on the guest network and try to reach an internal service via the external IP address (e.g. a web server) then it all breaks. This is because the incoming traffic doesn’t arrive via the br-wan interface and so isn’t NAT’d. In iptables days we used a set of “reflection” rules (see the earlier blog entry about this). With nftables we can be smarter. We can set up some rules that use sets and maps to hold the information.

So to define incoming port connections and those we want reflected:

# Port forwarding for incoming traffic

add element nat fport { 80 : 10.0.0.100 . 80 }   # http
add element nat fport { 443 : 10.0.0.100 . 443 } # https
add element nat fport { 22 : 10.0.0.110 . 22 }   # ssh
add element nat fport { 6881-6999 : 10.0.0.120 . 6881-6999 } # RL9 torrents

# These ports should be reflected.  Basically the ports from fport that
# people might want to access
add element nat reflect { 80, 443, 22 }

We’re now in a position to define a set of rules

add chain ip nat INPUT { type nat hook input priority 100; policy accept; }
add chain ip nat OUTPUT { type nat hook output priority -100; policy accept; }
add chain ip nat PREROUTING { type nat hook prerouting priority -100; policy accept; }
add chain ip nat POSTROUTING { type nat hook postrouting priority 100; policy accept; }

# Log specific incoming traffic
add rule ip nat PREROUTING iifname "br-wan" tcp dport 22 limit rate 2/minute burst 5 packets counter log prefix "IPTables-New-SSH: "

# Tag internal reflection traffic
add rule ip nat PREROUTING ip saddr 10.0.0.0/8 ip daddr @this_host tcp dport @reflect mark set 100

# DNAT incoming traffic from the internet (or internal send to br-wan address)
add rule ip nat PREROUTING ip daddr @this_host ip protocol tcp dnat ip  addr . port to tcp dport map @fport

# Do NAT on egress traffic to the internet
add rule ip nat POSTROUTING oifname "br-wan" counter masquerade

# Also NAT marked traffic from internal to external IP
add rule ip nat POSTROUTING meta mark 100 counter masquerade

@this_host

You might have noticed the @this_host rules. This refers to the set we defined earlier… but we haven’t populated it, yet!

There are two rules that work by have a PREROUTING rule detect traffic designed for reflection, and this sets a mark on it; the POSTROUTING rule detects this mark and ensures the traffic is masqueraded (SNAT’d).

The other rule using @this_host is the general incoming forwarding rule, which uses the fport map we filled out earlier. This catches both internet and VLAN initiated traffic that’s trying to hit this machine and ensures they’re both forwarded (DNAT’d) to the right target.

However, this set needs to hold the external IP address of the machine. We can set it in two places.

dhcp hooks

Whenever the br-wan interface gets assigned an address from DHCP we can arrange for a script to be called. This can simply populate the external IP address (passed as a variable) into the set:

% cat /etc/NetworkManager/dispatcher.d/nft_host
#!/bin/sh

if [ "$1" == "br-wan" -a -n "$DHCP4_IP_ADDRESS" ]
then
  nft "flush set nat this_host;add element nat this_host { $DHCP4_IP_ADDRESS }"
  echo `date`: Put $DHCP4_IP_ADDRESS into set
fi

cron job

Just in case the hook doesn’t fire properly, or if you reload the rules (and so have an empty set) we can also run a cron job every 5 minutes that can check the br-wan IP address, and if it’s not in the set we add it. I call this script reflect.

#!/bin/sh

export PATH=/sbin:$PATH

export DHCP4_IP_ADDRESS=$(ip -4 address show dev br-wan | sed -n 's/ *inet \([0-9.][0-9.]*\)\/.*/\1/p')

# Check to see if this is in the NFT ruleset
if [ -z "$(nft list set ip nat this_host | grep "{ $DHCP4_IP_ADDRESS }")" ]
then
  /etc/NetworkManager/dispatcher.d/nft_host br-wan dhcp4-change
fi

IPv6

Phew, that was a lot of work. IPv6 is a lot simpler; we allow any machine on the LAN to see the internet on IPv6 and we only allow a limited subset of external machines (machines I own) to be able to reach in from the internet.

We can specify the trusted machines by putting them in the my_hosts set

# My IPv6 hosts

add element ip6 filter my_hosts { \
  XXXX:XXXX:XXXX:XXXX:1, \
  YYYY:YYYY::YYYY:YYYY:YYYY:YYYY, \
  ZZZZ:ZZZ:ZZ:ZZ:ZZZZ:ZZZZ:ZZZZ:0 \
}

And now the IPv6 rules are pretty simple:

add chain ip6 filter INPUT { type filter hook input priority 0; policy accept; }
add chain ip6 filter OUTPUT { type filter hook output priority 0; policy accept;
 }
add chain ip6 filter FORWARD { type filter hook forward priority 0; policy accept; }

add rule ip6 filter INPUT ct state related,established counter accept
add rule ip6 filter INPUT ct state invalid counter drop
add rule ip6 filter INPUT iifname "lo" counter accept
add rule ip6 filter INPUT iifname "br-lan" counter accept

add rule ip6 filter INPUT meta l4proto ipv6-icmp counter accept

add rule ip6 filter INPUT ip6 saddr @my_hosts counter accept

add rule ip6 filter INPUT counter drop

add rule ip6 filter OUTPUT ct state related,established counter accept
add rule ip6 filter OUTPUT ct state invalid counter drop

add rule ip6 filter FORWARD ct state related,established counter accept
add rule ip6 filter FORWARD ct state invalid counter drop
add rule ip6 filter FORWARD iifname "br-lan" counter accept

add rule ip6 filter FORWARD meta l4proto ipv6-icmp icmpv6 type destination-unreachable counter accept
add rule ip6 filter FORWARD meta l4proto ipv6-icmp icmpv6 type packet-too-big counter accept
add rule ip6 filter FORWARD meta l4proto ipv6-icmp icmpv6 type time-exceeded counter accept
add rule ip6 filter FORWARD meta l4proto ipv6-icmp icmpv6 type parameter-problem counter accept

add rule ip6 filter FORWARD ip6 saddr @my_hosts counter accept

add rule ip6 filter FORWARD counter drop

Get this to load on reboot

What I do is have a complete list of all my rules in a file; I’ll include it below. We can then load this with the single command nft -f <filename>

Earlier we ran systemctl enable --now nftables. This picks up /etc/sysconfig/nftables.conf so we can now save the new rules

nft list ruleset > /etc/sysconfig/nftables.conf

Of course after reloading the rules, @this_host is empty so we also need to run the reflect command (or wait for the cron job).

We can do all of this in one line

nft -f nft.rules ; nft list ruleset >| /etc/sysconfig/nftables.conf ; reflect

The final nft ruleset

# Load this rules with
#   nft -f <this file>
# Save the rules so they apply on reboot with
#   nft list ruleset > /etc/sysconfig/nftables.conf
#
# In one command
#
#   nft -f nft ; nft list ruleset >| /etc/sysconfig/nftables.conf ; reflect
#
# Remember to configure nftables to start at boot time, and to disable
# firewalld
#
# systemctl disable --now firewalld
# systemctl mask firewalld
# systemctl enable --now nftables

###############################################################################

# Clear out all the rules
flush ruleset

# Create empty structures
add table ip filter
add table ip nat
add table ip6 filter

add map nat fport { type inet_service : interval ipv4_addr . inet_service ; flags interval; }
add set ip nat reflect { type inet_service; }
add set ip nat this_host { type ipv4_addr; }

add set ip6 filter my_hosts { type ipv6_addr; }

###############################################################################

# This should be the only configuration needed

# Port forwarding for incoming traffic

add element nat fport { 80 : 10.0.0.100 . 80 }   # http
add element nat fport { 443 : 10.0.0.100 . 443 } # https
add element nat fport { 22 : 10.0.0.110 . 22 }   # ssh
add element nat fport { 6881-6999 : 10.0.0.120 . 6881-6999 } # RL9 torrents

# These ports should be reflected.  Basically the ports from fport that
# people might want to access
add element nat reflect { 80, 443, 22 }

# My IPv6 hosts

add element ip6 filter my_hosts { \
  XXXX:XXXX:XXXX:XXXX:1, \
  YYYY:YYYY::YYYY:YYYY:YYYY:YYYY, \
  ZZZZ:ZZZ:ZZ:ZZ:ZZZZ:ZZZZ:ZZZZ:0 \
}

###############################################################################

# This ruleset is split into two parts; ip and ip6

###############################################################################
#
# IPv4
#
###############################################################################

# Basic rules:
#   LAN can see everything
#   Guestnet and IoT network can see a few internal resources (eg DNS)
#     and the internet
#   Outside world can see a few exposed services


# Define the 3 standard filters:
#     INPUT: Traffic targeting this machine
#    OUTPUT: Traffic originating from this machine
#   FORWARD: Traffic goin through this machine

add chain ip filter INPUT { type filter hook input priority 0; policy accept; }
add chain ip filter OUTPUT { type filter hook output priority 0; policy accept; }
add chain ip filter FORWARD { type filter hook forward priority 0; policy accept; }

# Log rules for input and forwarding
add chain ip filter LOGGER
add chain ip filter LOGGERF

######
###### Logging rules
######

# Logging rule for INPUT drops
add rule ip filter LOGGER limit rate 2/minute burst 5 packets counter log prefix "IPTables-Dropped: "
add rule ip filter LOGGER counter drop

# Logging rule for FORWARD drops
add rule ip filter LOGGERF limit rate 2/minute burst 5 packets counter log prefix "IPTables-Dropped-Forward: "
add rule ip filter LOGGERF counter drop

######
###### INPUT rules
######

# Allow traffic for established connections, blocking invalid header format
add rule ip filter INPUT ct state related,established counter accept
add rule ip filter INPUT ct state invalid counter drop

# All localhost traffic allowed
add rule ip filter INPUT iifname "lo" counter accept

# The LAN can see this machine
add rule ip filter INPUT iifname "br-lan" counter accept

# Allow the world to ping this machine (amongst other things)
add rule ip filter INPUT ip protocol icmp counter accept

# Let guestnet and IoT see DNS servers on this machine (tcp+udp)
add rule ip filter INPUT iifname "br-guest" tcp dport 53 counter accept
add rule ip filter INPUT iifname "br-guest" udp dport 53 counter accept
add rule ip filter INPUT iifname "br-guest" tcp dport 853 counter accept
add rule ip filter INPUT iifname "br-guest" udp dport 853 counter accept

add rule ip filter INPUT iifname "br-iot" tcp dport 53 counter accept
add rule ip filter INPUT iifname "br-iot" udp dport 53 counter accept
add rule ip filter INPUT iifname "br-iot" udp dport 123 counter accept

# Log block traffic; this could be noisy, so only enable it if you
# want lots of logs!
## add rule ip filter INPUT counter jump LOGGER

# Block all other incoming traffic
add rule ip filter INPUT counter drop

######
###### OUTPUT rules
######

# Output is easy; we let this machine talk to anything
add rule ip filter OUTPUT ct state related,established counter accept
add rule ip filter OUTPUT ct state invalid counter drop

######
###### FORWARD rules
######

# Allow established connections to work, blocking invalid header format
add rule ip filter FORWARD ct state related,established counter accept
add rule ip filter FORWARD ct state invalid counter drop

# Allow the LAN to see the world
add rule ip filter FORWARD iifname "br-lan" counter accept

# Allow the VPN to see everything
add rule ip filter FORWARD iifname "tun0" counter accept

# Allow return traffic for NAT'd connections to work
add rule ip filter FORWARD iifname "br-wan" ct status dnat counter accept

# Allow guestnet to see external DNS servers
add rule ip filter FORWARD iifname "br-guest" tcp dport 53 counter accept
add rule ip filter FORWARD iifname "br-guest" udp dport 53 counter accept
add rule ip filter FORWARD iifname "br-guest" tcp dport 853 counter accept
add rule ip filter FORWARD iifname "br-guest" udp dport 853 counter accept

add rule ip filter FORWARD iifname "br-iot" tcp dport 53 counter accept
add rule ip filter FORWARD iifname "br-iot" udp dport 53 counter accept

# Allow my mobile devices to have access to everything
add rule ip filter FORWARD iifname "br-guest" ip saddr 10.100.100.8 counter accept

# Allow guestnet to see the printer
add rule ip filter FORWARD iifname "br-guest" ip daddr 10.0.0.10 counter accept

# Allow guestnet devices to see my MQTT server
add rule ip filter FORWARD iifname "br-guest" oifname "br-lan" tcp dport 1883 counter accept

# Allow guestnet to see the internet
add rule ip filter FORWARD iifname "br-guest" oifname "br-wan" counter accept

# Allow IoT devices to see my MQTT server
add rule ip filter FORWARD iifname "br-iot" oifname "br-lan" tcp dport 1883 counter accept

# Allow ESP8266 devices on IoT to be network updates
# (The process causes the ESP8266 to call back to a webserver on the host)
add rule ip filter FORWARD iifname "br-iot" oifname "br-lan" tcp dport 8266 counter accept

# Allow IoT devices to see the internet
add rule ip filter FORWARD iifname "br-iot" oifname "br-wan" counter accept

# Allow marked traffic (ie reflected traffic) to reach the destination
add rule ip filter FORWARD meta mark 100 counter accept

# Log and block all other forwarded traffic
add rule ip filter FORWARD counter jump LOGGERF
add rule ip filter FORWARD counter drop

######
###### NAT rules
######

add chain ip nat INPUT { type nat hook input priority 100; policy accept; }
add chain ip nat OUTPUT { type nat hook output priority -100; policy accept; }
add chain ip nat PREROUTING { type nat hook prerouting priority -100; policy accept; }
add chain ip nat POSTROUTING { type nat hook postrouting priority 100; policy accept; }

# Log specific incoming traffic
add rule ip nat PREROUTING iifname "br-wan" tcp dport 22 limit rate 2/minute burst 5 packets counter log prefix "IPTables-New-SSH: "

# Tag internal reflection traffic
add rule ip nat PREROUTING ip saddr 10.0.0.0/8 ip daddr @this_host tcp dport @reflect mark set 100

# DNAT incoming traffic from the internet (or internal send to br-wan address)
add rule ip nat PREROUTING ip daddr @this_host ip protocol tcp dnat ip  addr . port to tcp dport map @fport

# Do NAT on egress traffic to the internet
add rule ip nat POSTROUTING oifname "br-wan" counter masquerade

# Also NAT marked traffic from internal to external IP
add rule ip nat POSTROUTING meta mark 100 counter masquerade

###############################################################################
#
# IPv6
#
###############################################################################

# IPv6 is simpler; there's no NAT involved.  We allow LAN to see the
# world and only allow trusted external machines to see in to LAN

add chain ip6 filter INPUT { type filter hook input priority 0; policy accept; }
add chain ip6 filter OUTPUT { type filter hook output priority 0; policy accept; }
add chain ip6 filter FORWARD { type filter hook forward priority 0; policy accept; }

add rule ip6 filter INPUT ct state related,established counter accept
add rule ip6 filter INPUT ct state invalid counter drop
add rule ip6 filter INPUT iifname "lo" counter accept
add rule ip6 filter INPUT iifname "br-lan" counter accept

add rule ip6 filter INPUT meta l4proto ipv6-icmp counter accept

add rule ip6 filter INPUT ip6 saddr @my_hosts counter accept

add rule ip6 filter INPUT counter drop

add rule ip6 filter OUTPUT ct state related,established counter accept
add rule ip6 filter OUTPUT ct state invalid counter drop

add rule ip6 filter FORWARD ct state related,established counter accept
add rule ip6 filter FORWARD ct state invalid counter drop
add rule ip6 filter FORWARD iifname "br-lan" counter accept

add rule ip6 filter FORWARD meta l4proto ipv6-icmp icmpv6 type destination-unreachable counter accept
add rule ip6 filter FORWARD meta l4proto ipv6-icmp icmpv6 type packet-too-big counter accept
add rule ip6 filter FORWARD meta l4proto ipv6-icmp icmpv6 type time-exceeded counter accept
add rule ip6 filter FORWARD meta l4proto ipv6-icmp icmpv6 type parameter-problem counter accept

add rule ip6 filter FORWARD ip6 saddr @my_hosts counter accept

add rule ip6 filter FORWARD counter drop