Mapsets
Mapsets can be one of:
- Sets (i.e.
{ 10.11.1.1, 10.11.22.33 }
) - Maps (i.e.
{ 80 : 192.168.1.100 }
) - Verdict Maps (i.e.
{ 192.168.1.1 : drop }
or{ 192.168.1.1 . 80 : drop }
)
One reason mapsets can be useful, is they allow modifying the firewall while it is running
with nft
commands, without needing to write the full rules out.
I’ll now show some examples, to keep config more readable, consider everything prefixed with networking.nftables.gen
.
Sharing mapsets between rules
Allows only 3 specific internal hosts to SSH or HTTP/HTTPS.
# match against saddr
tables.filter.mapsets.internal_hosts = {
lhsType = "saddr";
elements = [
{ l = "10.11.1.1"; }
{ l = "10.11.22.33"; }
{ l = "10.44.55.66"; }
];
};
# Allow HTTP/HTTPS inbound only to specified mapset
tables.filter.input.rules.reverse-proxy = {
tcpDport = [80 443];
mapset = "internal_hosts";
log = true;
};
# Allow HTTP/HTTPS inbound only to specified mapset
tables.filter.input.rules.internal-ssh = {
tcpDport = [22];
mapset = "internal_hosts";
counter = true;
};
We can then modify the mapset at runtime without changing the NixOS configuration:
# for a Set
nft add element "inet filter internal_hosts { 192.168.1.1 }"
nft add element "inet filter wireguard_inbound_udp { 19999 : accept }"
Examples
Wireguard UDP Inbound
This example shows using a verdict map to allow multiple inbound udp for an external interface
tables.filter.mapsets.wireguard_inbound_udp = {
verdict = "verdict";
lhsType = "udp dport";
elements = [
{ l = 51820; v = "accept"; }
{ l = 51821; v = "jump log-and-accept"; }
];
};
tables.filter.log-and-accept.rules.default = {
log = true;
counter = true;
verdict = "accept";
};
tables.filter.input.rules.wg-in = {
mapset = "wireguard_inbound_udp";
comment = "handle inbound wireguard udp";
};
Selective NAT with forwarding
Sometimes when performing NAT for internal networks / bridges you want to align the forwarding table and NAT postrouting chains.
Defining this as a mapset can both reduce duplication, and allow configuration while the system is running.
let
genMapset = verdict: {
verdict = "verdict";
lhsType = "iifname";
rhsType = "oifname";
elements = [
{ l = "dmz"; r = "vpn-egress"; v = verdict; }
{ l = "libvirtbr0"; r = "enp1s0"; v = verdict; }
];
};
in
tables.filter = {
# generate forward and snat allow maps for snat forwarding
mapsets = {
egress_allow_map = genMapset "accept";
egress_snat_map = genMapset "jump masquerade_random";
};
masquerade_random.rules.all = {
comment = "masquerade all";
verdict = "masquerade random";
counter = true;
};
# allow forwarding for specific interfaces mapset
forward.rules.egress_allow_map = {
comment = "allow forwarding from internal -> egress";
mapset = "egress_allow_map";
};
# egress + masquerade specific interfaces from mapset
egress-snat.__type.hook = "postrouting";
egress-snat.rules.map = {
mapset = "egress_snat_map";
comment = "NAT from lan -> egress";
};
};
This generates the following config in nftables.
table inet filter {
map egress_allow_map {
type ifname . ifname : verdict
elements = {
dmz . vpn-egress : accept,
libvirtbr0 . enp1s0 : accept
}
}
map egress_snat_map {
type ifname . ifname : verdict
elements = {
dmz . vpn-egress : jump masquerade_random,
libvirtbr0 . enp1s0 : jump masquerade_random
}
}
chain forward {
type filter hook forward priority filter; policy drop;
counter iifname . oifname vmap @egress_allow_map accept comment "allow forwarding from internal -> egress"
}
chain egress-snat {
type nat hook postrouting priority srcnat;
iifname . oifname vmap @egress_snat_map comment "NAT from lan -> egress"
}
}