Systemd-Networkd as a Router
Today I’m taking a look at my MONO Gateway - but with my own software twist. You may know I am not a fan of the OpenWRT project for end users (mostly due to their very poor and inconsistent documentation), and usually recommend OPNsense, but this time I want to build something DECLARATIVE! So I’m of course going back to just plain Debian.
I’m using systemd-networkd for this, so all of the configs are in systemd’s ini-style. I rather like this syntax.
Networkd Configs⌗
Here are my example configs
Backup Interface⌗
/etc/systemd/network/06_backup.network
# 'backup' network interface
# This does not depend on anything else, so we can always
# use it to SSH in via gateway.local
[Match]
Name=eth1
[Network]
IPv6AcceptRA=yes
IPv6SendRA=yes
MulticastDNS=yes
# RA options
[IPv6SendRA]
Managed=no
OtherInformation=no
#Not a router!
RouterLifetimeSec=0
# ula prefix (for internet-outage connectivity)
[IPv6Prefix]
Prefix=fdf9:ff03:0f91:0001::/64
Assign=true
Vlan Parent Interface⌗
This interface just has VLAN slaves. /etc/systemd/network/10_eth0_vlans.network:
# Systemd Network unit for interface which is just a
# parent for VLAN interfaces
[Match]
Name=eth0
[Network]
DHCP=no
IPv6AcceptRA=no
#Carry VLANs on this physical interface
VLAN=eth0.20
VLAN=eth0.21
WAN Interface⌗
This interface actually does the WAN upstream. Some notes in DHCPv6 are provider-specific.
/etc/systemd/network/11_wan.network
# Systemd Network unit for WAN interface
# Replace name with your WAN interface name
[Match]
Name=eth2
[Network]
Description=WAN
#DHCP=both means * force ipv6 *
#Normally you would set DHCP=ipv4, meaning ipv6 depends on router advertisement M-flag
DHCP=both
IPv6AcceptRA=yes
[DHCPv4]
UseRoutes=yes
RouteMetric=100
[IPv6AcceptRA]
RouteMetric=100
DHCPv6Client=always
[DHCPv6]
#Force request even if nothing else indicates it should request PD (should not be needed)
ForceDHCPv6PDOtherInformation=yes
#Normal values here are /56 and /60
PrefixDelegationHint=::/59
#This means do not request an address at all
#UseAddress=no
LAN Interface⌗
This is a basic LAN interface: /etc/systemd/network/31_lan20.network
# LAN interface on vlan 20
[Match]
Name=eth0.20
[Network]
Address=10.10.20.1/24
IPv6SendRA=yes
IPv6AcceptRA=no
DHCPServer=yes
DHCPPrefixDelegation=yes
IPMasquerade=ipv6
# DHCP server pool options
[DHCPServer]
PoolOffset=100
PoolSize=100
EmitDNS=yes
DNS=192.168.0.3
# RA options
[IPv6SendRA]
Managed=no
OtherInformation=no
RouterLifetimeSec=1800
# ula prefix (not required)
[IPv6Prefix]
Prefix=fdf9:ff03:0f91:0001::/64
Assign=true
#static prefixes (this can be copied as many times as you want)
#[IPv6Prefix]
#Prefix=2001:db8:10::/64
#Assign=true
#dynamic prefixes
[DHCPPrefixDelegation]
UplinkInterface=eth2
SubnetId=0x2
Announce=yes
Assign=yes
#Token can be used under any Prefix or PrefixDelegation
#This means a forced suffix
#Not needed, but some people like it
Token=::1
VLAN Device⌗
# /etc/systemd/network/31_vlanXX.netdev
# Network Device for VLAN xx
[NetDev]
Name=eth0.20
Kind=vlan
[VLAN]
Id=20
Nftables.conf⌗
This goes in /etc/nftables.conf - you may need a systemd unit to initialize it
#!/usr/sbin/nft -f
flush ruleset
#
# Local Variables
#
define WAN = eth2
define LAN20 = eth0.20
define LANS = { "eth0.20" }
define LAN_RANGES = "10.10.0.0/16"
#
# IPv4/IPv6 stateful firewall
#
table inet filter {
# INPUT chain is sessions who terminate at this box
# this is for services running on this box, not routed through
# just to be clear
chain input {
type filter hook input priority filter;
policy drop;
# Loopback
iifname "lo" accept
# Backup interface
iifname "eth1" accept
iifname $WAN accept
# Established/related connections
ct state established,related accept
# Invalid packets
ct state invalid drop
#
# ICMP
#
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
ip6 nexthdr 41 accept
#
# DHCP client
#
# IPv4 DHCP: client -> server
iifname $WAN udp sport 68 udp dport 67 accept
# IPv6 DHCP: client -> server
iifname $WAN udp sport 546 udp dport 547 accept
#
# DHCP server for LAN
#
iifname $LANS udp sport 68 udp dport 67 accept
iifname $LANS udp sport 546 udp dport 547 accept
#
# Router administration from LAN (SSH)
#
iifname $LAN20 tcp dport 22 accept
}
# FORWARD chain is where your normal firewall rules go
# this is stuff that is ROUTED
chain forward {
type filter hook forward priority filter;
policy drop;
# Established/related connections
ct state established,related accept
# Invalid packets
ct state invalid drop
#
# LAN -> WAN
#
iifname $LAN20 oifname $WAN accept
iifname "tun_waw" accept
#
# IPv4 port forward
# At the firewall stage, daddr+dport have been NATed
#
iifname $WAN oifname $LAN20 \
ip daddr 10.10.20.150 \
tcp dport 80 \
ct state new accept
#
# IPv6: allow TCP/443 to the known EUI-64 host
#
iifname $WAN oifname $LAN20 \
ip6 daddr & 0:0:0:0:ffff:ffff:ffff:ffff == ::be24:11ff:fe71:a41f \
tcp dport 80 \
ct state new accept
}
# OUTPUT chain is sessions who are initiated by the box
chain output {
type filter hook output priority filter;
policy accept;
}
}
#
# IPv4 NAT
#
table ip nat {
# IPv4 Port Forwards go in PREROUTING
chain prerouting {
type nat hook prerouting priority dstnat;
policy accept;
# $WAN TCP/8080 -> LAN 10.10.20.150:80
iifname $WAN \
tcp dport 8080 \
dnat to 10.10.20.150:80
}
# MASQUERADE goes in POSTROUTING
chain postrouting {
type nat hook postrouting priority srcnat;
policy accept;
# IPv4 LAN Internet access
oifname $WAN \
masquerade
}
}