There are a few ways to do this.
systemd-networkd
When using systemd-networkd it is possible to avoid hardcoding or using wrapper scripts to detect the network subnet prefix given by IPv6 Prefix Delegation.
The systemd.network(5) option NFTSet= can get the network prefixes of a connection. The Arch Linux wiki shows how to use Dynamic named sets using systemd-networkd here.
The example given is to create a config file for systemd-networkd like the following.
/etc/systemd/network:
[DHCPv4]
NFTSet=prefix:inet:my_table:eth_ipv4_prefix
NFTSet=ifindex:inet:my_table:eth_ifindex
[DHCPv6]
NFTSet=prefix:inet:my_table:eth_ipv6_prefix
NFTSet=ifindex:inet:my_table:eth_ifindex
[IPv6AcceptRA]
NFTSet=prefix:inet:my_table:eth_ipv6_prefix
NFTSet=ifindex:inet:my_table:eth_ifindex
Then, create the sets in /etc/nftables.conf or in a /etc/nftables.d/NN-some-file.nft drop-in:
table inet my_table {
set eth_ipv4_prefix {
type ipv4_addr
flags interval
comment "Populated by systemd-networkd"
}
set eth_ipv6_prefix {
type ipv6_addr
flags interval
comment "Populated by systemd-networkd"
elements = { fe80::/10 }
}
set eth_ifindex {
type iface_index
comment "Populated by systemd-networkd"
}
chain my_input {
type filter hook input priority filter; policy drop;
iif @eth_ifindex ip6 saddr @eth_ipv6_prefix jump my_input_lan comment "Connections from LAN"
iif @eth_ifindex ip saddr @eth_ipv4_prefix jump my_input_lan comment "Connections from LAN"
}
}
While using SystemD might appeal to you while configuring Servers, or machines already running systemd-networkd, sometimes it's not quite the right fit or systemd-networkd might not be easy to set up. Another common alternative is NetworkManager.
NetworkManager
NetworkManager is usually a better fit for desktop Linux machines, and supports hook scripts.
NetworkManager-dispatcher(8) service can execute scripts for the user in response to network events. It will execute scripts in /{etc,usr/lib}/NetworkManager/dispatcher.d directories (or subdirectories) in alphabetical order in response to network events.
To use this method, create a script in /etc/NetworkManager/dispatcher.d
/etc/NetworkManager/dispatcher.d/01-nftables-cidr-update.sh:
#!/bin/bash
echo "$0: $*"
if [ -z "${DEVICE_IP_IFACE}" ] && [ -n "$1" ]; then
DEVICE_IP_IFACE=$1
fi
case $2 in
dhcp6-change)
if [ -n "${DEVICE_IP_IFACE}" ]; then
mapfile -t IPv6_PREFIXES < <(
ip -json -6 addr show dev "${DEVICE_IP_IFACE}" scope global |
jq -r '.[].addr_info[] |
select(.family == "inet6" and .scope == "global" and .prefixlen < 128) |
if .local then "(.local)/(.prefixlen)" else empty end'
)
echo "Update nftables set with new IPv6 prefix(es):"
for prefix in "${IPv6_PREFIXES[@]}"; do
echo "$prefix"
nft add element ip6 filter my_ipv6_prefixes "{ $prefix }"
done
fi
;;
dhcp4-change)
if [ -n "${DEVICE_IP_IFACE}" ]; then
mapfile -t IPv4_CIDRS < <(
ip -json -4 addr show dev "${DEVICE_IP_IFACE}" scope global |
jq -r '.[].addr_info[] |
select(.family == "inet" and .scope == "global" and .prefixlen < 32) |
if .local then "(.local)/(.prefixlen)" else empty end'
)
echo "Update nftables set with new IPv4 CIDR(s):"
for cidr in "${IPv4_CIDRS[@]}"; do
echo "$cidr"
nft add element ip filter my_ipv4_cidrs "{ $cidr }"
done
fi
;;
esac
debug passed env vars
env
Then, similarly create some NFTables sets to hold the IPv6 prefix(es) and IPv4 network CIDR(s):
/etc/nftables.d/02-input-set-lan-cidrs.nft:
#!/usr/bin/nft -f
# vim:set ts=2 sw=2 et:
Idempotence: destroy if already existing,
but don't fail if it does not exist.
(useful for /etc/nftables.d/ drop-in scripts that usually get re-included when restarting nftables.service)
destroy set ip6 filter my_ipv6_prefixes
Create an IPv6 set to track dynamic delegated prefixes
add set ip6 filter my_ipv6_prefixes {
type ipv6_addr
flags interval
size 65536
auto-merge
comment "Populated by NetworkManager-dispatcher script"
}
again... idempotence
destroy set ip filter my_ipv4_cidrs
Create an IPv4 set to track dynamic DHCP CIDR
add set ip filter my_ipv4_cidrs {
type ipv4_addr
flags interval
size 65536
auto-merge
comment "Populated by NetworkManager-dispatcher script"
}
This script will then automatically add the discovered networks triggered by dhcp4-change and dhcp6-change events from NetworkManager.
NOTE:
- The above
jq .prefixlen check will ignore the /128 IPv6 IP and /32 IPv4 IP assigned to the interface, because these will always be within the netmask / CIDR. Adding them with auto-merge option on the NFTables set will simply merge them into the same CIDR prefix and have no measurable effect.
- The
NetworkManager-dispatcher script on its' own will not take care of removing old CIDRs or IPv6 network prefixes. You might want to detect the old ones at the time of an interface down event, and remove them from the set. For this, you would also need the a similar pre-down script because the old CIDRs and IPv6 prefixes are not saved when the down hook runs. A similar pre-down script to accomplish this might be:
/etc/NetworkManager/dispatcher.d/pre-down.d/01-nftables-dhcp-cidr-remove.sh:
#!/bin/bash
echo "$0: $*"
if [ -z "${DEVICE_IP_IFACE}" ] && [ -n "$1" ]; then
DEVICE_IP_IFACE=$1
fi
case $2 in
pre-down)
if [ -n "${DEVICE_IP_IFACE}" ]; then
mapfile -t IPv6_PREFIXES < <(
ip -json -6 addr show dev "${DEVICE_IP_IFACE}" scope global |
jq -r '.[].addr_info[] |
select(.family == "inet6" and .scope == "global" and .prefixlen < 128) |
if .local then "(.local)/(.prefixlen)" else empty end'
)
echo "Delete OLD IPv6 prefix(es) from nftables set:"
for prefix in "${IPv6_PREFIXES[@]}"; do
echo "$prefix"
nft delete element ip6 filter my_ipv6_prefixes "{ $prefix }"
done
fi
if [ -n "${DEVICE_IP_IFACE}" ]; then
mapfile -t IPv4_CIDRS < <(
ip -json -4 addr show dev "${DEVICE_IP_IFACE}" scope global |
jq -r '.[].addr_info[] |
select(.family == "inet" and .scope == "global" and .prefixlen < 32) |
if .local then "(.local)/(.prefixlen)" else empty end'
)
echo "Delete OLD IPv4 CIDR(s) from nftables set:"
for cidr in "${IPv4_CIDRS[@]}"; do
echo "$cidr"
nft delete element ip filter my_ipv4_cidrs "{ $cidr }"
done
fi
;;
esac
debug passed env vars
env
Now those sets can be used in normal NFTables rules:
table ip filter {
chain input {
type filter hook input priority filter; policy drop;
ip saddr @my_ipv4_cidrs jump my_input_lan comment "Connections from IPv4 LAN CIDR"
}
}
table ip6 filter {
chain input {
type filter hook input priority filter; policy drop;
ip6 saddr @my_ipv6_prefixes jump my_input_lan comment "Connections from IPv6 GUA prefix"
}
}
Hope this helps some folks with integrating NFTables with dynamic IPv6 prefix delegation and IPv4 DHCP LAN networks!