Systemd-Networkd as a Router :: apalrd's adventures
Systemd-Networkd as a Router
2026-08-13
#networking
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<br># This does not depend on anything else, so we can always<br># use it to SSH in via gateway.local<br>[Match]<br>Name=eth1
[Network]<br>IPv6AcceptRA=yes<br>IPv6SendRA=yes<br>MulticastDNS=yes
# RA options<br>[IPv6SendRA]<br>Managed=no<br>OtherInformation=no<br>#Not a router!<br>RouterLifetimeSec=0
# ula prefix (for internet-outage connectivity)<br>[IPv6Prefix]<br>Prefix=fdf9:ff03:0f91:0001::/64<br>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<br># parent for VLAN interfaces<br>[Match]<br>Name=eth0
[Network]<br>DHCP=no<br>IPv6AcceptRA=no<br>#Carry VLANs on this physical interface<br>VLAN=eth0.20<br>VLAN=eth0.21
WAN Interface⌗
This interface actually does the WAN upstream. Some notes in DHCPv6 are provider-specific.<br>/etc/systemd/network/11_wan.network
# Systemd Network unit for WAN interface<br># Replace name with your WAN interface name<br>[Match]<br>Name=eth2
[Network]<br>Description=WAN<br>#DHCP=both means * force ipv6 *<br>#Normally you would set DHCP=ipv4, meaning ipv6 depends on router advertisement M-flag<br>DHCP=both<br>IPv6AcceptRA=yes
[DHCPv4]<br>UseRoutes=yes<br>RouteMetric=100
[IPv6AcceptRA]<br>RouteMetric=100<br>DHCPv6Client=always
[DHCPv6]<br>#Force request even if nothing else indicates it should request PD (should not be needed)<br>ForceDHCPv6PDOtherInformation=yes<br>#Normal values here are /56 and /60<br>PrefixDelegationHint=::/59<br>#This means do not request an address at all<br>#UseAddress=no
LAN Interface⌗
This is a basic LAN interface: /etc/systemd/network/31_lan20.network
# LAN interface on vlan 20<br>[Match]<br>Name=eth0.20
[Network]<br>Address=10.10.20.1/24<br>IPv6SendRA=yes<br>IPv6AcceptRA=no<br>DHCPServer=yes<br>DHCPPrefixDelegation=yes<br>IPMasquerade=ipv6
# DHCP server pool options<br>[DHCPServer]<br>PoolOffset=100<br>PoolSize=100<br>EmitDNS=yes<br>DNS=192.168.0.3
# RA options<br>[IPv6SendRA]<br>Managed=no<br>OtherInformation=no<br>RouterLifetimeSec=1800
# ula prefix (not required)<br>[IPv6Prefix]<br>Prefix=fdf9:ff03:0f91:0001::/64<br>Assign=true
#static prefixes (this can be copied as many times as you want)<br>#[IPv6Prefix]<br>#Prefix=2001:db8:10::/64<br>#Assign=true
#dynamic prefixes<br>[DHCPPrefixDelegation]<br>UplinkInterface=eth2<br>SubnetId=0x2<br>Announce=yes<br>Assign=yes<br>#Token can be used under any Prefix or PrefixDelegation<br>#This means a forced suffix<br>#Not needed, but some people like it<br>Token=::1
VLAN Device⌗
# /etc/systemd/network/31_vlanXX.netdev<br># Network Device for VLAN xx<br>[NetDev]<br>Name=eth0.20<br>Kind=vlan
[VLAN]<br>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<br>define WAN = eth2<br>define LAN20 = eth0.20<br>define LANS = { "eth0.20" }<br>define LAN_RANGES = "10.10.0.0/16"
# IPv4/IPv6 stateful firewall
table inet filter {
# INPUT chain is sessions who terminate at this box<br># this is for services running on this box, not routed through<br># just to be clear<br>chain input {<br>type filter hook input priority filter;<br>policy drop;
# Loopback<br>iifname "lo" accept
# Backup interface<br>iifname "eth1" accept
iifname $WAN accept
# Established/related connections<br>ct state established,related accept
# Invalid packets<br>ct state invalid drop
# ICMP<br>ip protocol icmp accept<br>ip6 nexthdr icmpv6 accept<br>ip6 nexthdr 41 accept
# DHCP client<br># IPv4 DHCP: client -> server<br>iifname $WAN udp sport 68 udp dport 67 accept
# IPv6 DHCP: client -> server<br>iifname $WAN udp sport 546 udp dport 547 accept
# DHCP server for LAN<br>iifname $LANS udp sport 68 udp dport 67 accept<br>iifname $LANS udp sport 546 udp dport 547 accept
# Router administration from LAN (SSH)<br>iifname $LAN20 tcp dport 22 accept
# FORWARD chain is where your normal firewall rules go<br># this is stuff that is ROUTED<br>chain forward {<br>type filter hook forward priority filter;<br>policy drop;
# Established/related connections<br>ct state established,related accept
# Invalid packets<br>ct state invalid drop
# LAN -> WAN<br>iifname $LAN20 oifname $WAN accept
iifname "tun_waw" accept
# IPv4 port forward<br># At the firewall stage, daddr+dport have been NATed<br>iifname $WAN oifname $LAN20 \<br>ip daddr 10.10.20.150 \<br>tcp dport 80 \<br>ct state new accept
# IPv6: allow TCP/443 to the known EUI-64 host<br>iifname $WAN oifname $LAN20 \<br>ip6 daddr & 0:0:0:0:ffff:ffff:ffff:ffff == ::be24:11ff:fe71:a41f \<br>tcp dport 80...