nixos/{firewall, nat}: add a nftables based implementation

Rvfg a43c7b2a 2379de68

+7
nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
···
</listitem>
<listitem>
<para>
+
The <literal>firewall</literal> and <literal>nat</literal>
+
module now has a nftables based implementation. Enable
+
<literal>networking.nftables</literal> to use it.
+
</para>
+
</listitem>
+
<listitem>
+
<para>
The <literal>services.fwupd</literal> module now allows
arbitrary daemon settings to be configured in a structured
manner
+2
nixos/doc/manual/release-notes/rl-2305.section.md
···
- Resilio sync secret keys can now be provided using a secrets file at runtime, preventing these secrets from ending up in the Nix store.
+
- The `firewall` and `nat` module now has a nftables based implementation. Enable `networking.nftables` to use it.
+
- The `services.fwupd` module now allows arbitrary daemon settings to be configured in a structured manner ([`services.fwupd.daemonSettings`](#opt-services.fwupd.daemonSettings)).
- The `unifi-poller` package and corresponding NixOS module have been renamed to `unpoller` to match upstream.
+4
nixos/modules/module-list.nix
···
./services/networking/firefox-syncserver.nix
./services/networking/fireqos.nix
./services/networking/firewall.nix
+
./services/networking/firewall-iptables.nix
+
./services/networking/firewall-nftables.nix
./services/networking/flannel.nix
./services/networking/freenet.nix
./services/networking/freeradius.nix
···
./services/networking/namecoind.nix
./services/networking/nar-serve.nix
./services/networking/nat.nix
+
./services/networking/nat-iptables.nix
+
./services/networking/nat-nftables.nix
./services/networking/nats.nix
./services/networking/nbd.nix
./services/networking/ncdns.nix
+6 -1
nixos/modules/services/audio/roon-bridge.nix
···
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPortRanges = [{ from = 9100; to = 9200; }];
allowedUDPPorts = [ 9003 ];
-
extraCommands = ''
+
extraCommands = optionalString (!config.networking.nftables.enable) ''
iptables -A INPUT -s 224.0.0.0/4 -j ACCEPT
iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT
iptables -A INPUT -s 240.0.0.0/5 -j ACCEPT
iptables -A INPUT -m pkttype --pkt-type multicast -j ACCEPT
iptables -A INPUT -m pkttype --pkt-type broadcast -j ACCEPT
+
'';
+
extraInputRules = optionalString config.networking.nftables.enable ''
+
ip saddr { 224.0.0.0/4, 240.0.0.0/5 } accept
+
ip daddr 224.0.0.0/4 accept
+
pkttype { multicast, broadcast } accept
'';
};
+6 -1
nixos/modules/services/audio/roon-server.nix
···
{ from = 30000; to = 30010; }
];
allowedUDPPorts = [ 9003 ];
-
extraCommands = ''
+
extraCommands = optionalString (!config.networking.nftables.enable) ''
## IGMP / Broadcast ##
iptables -A INPUT -s 224.0.0.0/4 -j ACCEPT
iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT
iptables -A INPUT -s 240.0.0.0/5 -j ACCEPT
iptables -A INPUT -m pkttype --pkt-type multicast -j ACCEPT
iptables -A INPUT -m pkttype --pkt-type broadcast -j ACCEPT
+
'';
+
extraInputRules = optionalString config.networking.nftables.enable ''
+
ip saddr { 224.0.0.0/4, 240.0.0.0/5 } accept
+
ip daddr 224.0.0.0/4 accept
+
pkttype { multicast, broadcast } accept
'';
};
+334
nixos/modules/services/networking/firewall-iptables.nix
···
+
/* This module enables a simple firewall.
+
+
The firewall can be customised in arbitrary ways by setting
+
‘networking.firewall.extraCommands’. For modularity, the firewall
+
uses several chains:
+
+
- ‘nixos-fw’ is the main chain for input packet processing.
+
+
- ‘nixos-fw-accept’ is called for accepted packets. If you want
+
additional logging, or want to reject certain packets anyway, you
+
can insert rules at the start of this chain.
+
+
- ‘nixos-fw-log-refuse’ and ‘nixos-fw-refuse’ are called for
+
refused packets. (The former jumps to the latter after logging
+
the packet.) If you want additional logging, or want to accept
+
certain packets anyway, you can insert rules at the start of
+
this chain.
+
+
- ‘nixos-fw-rpfilter’ is used as the main chain in the mangle table,
+
called from the built-in ‘PREROUTING’ chain. If the kernel
+
supports it and `cfg.checkReversePath` is set this chain will
+
perform a reverse path filter test.
+
+
- ‘nixos-drop’ is used while reloading the firewall in order to drop
+
all traffic. Since reloading isn't implemented in an atomic way
+
this'll prevent any traffic from leaking through while reloading
+
the firewall. However, if the reloading fails, the ‘firewall-stop’
+
script will be called which in return will effectively disable the
+
complete firewall (in the default configuration).
+
+
*/
+
+
{ config, lib, pkgs, ... }:
+
+
with lib;
+
+
let
+
+
cfg = config.networking.firewall;
+
+
inherit (config.boot.kernelPackages) kernel;
+
+
kernelHasRPFilter = ((kernel.config.isEnabled or (x: false)) "IP_NF_MATCH_RPFILTER") || (kernel.features.netfilterRPFilter or false);
+
+
helpers = import ./helpers.nix { inherit config lib; };
+
+
writeShScript = name: text:
+
let
+
dir = pkgs.writeScriptBin name ''
+
#! ${pkgs.runtimeShell} -e
+
${text}
+
'';
+
in
+
"${dir}/bin/${name}";
+
+
startScript = writeShScript "firewall-start" ''
+
${helpers}
+
+
# Flush the old firewall rules. !!! Ideally, updating the
+
# firewall would be atomic. Apparently that's possible
+
# with iptables-restore.
+
ip46tables -D INPUT -j nixos-fw 2> /dev/null || true
+
for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse; do
+
ip46tables -F "$chain" 2> /dev/null || true
+
ip46tables -X "$chain" 2> /dev/null || true
+
done
+
+
+
# The "nixos-fw-accept" chain just accepts packets.
+
ip46tables -N nixos-fw-accept
+
ip46tables -A nixos-fw-accept -j ACCEPT
+
+
+
# The "nixos-fw-refuse" chain rejects or drops packets.
+
ip46tables -N nixos-fw-refuse
+
+
${if cfg.rejectPackets then ''
+
# Send a reset for existing TCP connections that we've
+
# somehow forgotten about. Send ICMP "port unreachable"
+
# for everything else.
+
ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset
+
ip46tables -A nixos-fw-refuse -j REJECT
+
'' else ''
+
ip46tables -A nixos-fw-refuse -j DROP
+
''}
+
+
+
# The "nixos-fw-log-refuse" chain performs logging, then
+
# jumps to the "nixos-fw-refuse" chain.
+
ip46tables -N nixos-fw-log-refuse
+
+
${optionalString cfg.logRefusedConnections ''
+
ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "refused connection: "
+
''}
+
${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) ''
+
ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \
+
-j LOG --log-level info --log-prefix "refused broadcast: "
+
ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \
+
-j LOG --log-level info --log-prefix "refused multicast: "
+
''}
+
ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse
+
${optionalString cfg.logRefusedPackets ''
+
ip46tables -A nixos-fw-log-refuse \
+
-j LOG --log-level info --log-prefix "refused packet: "
+
''}
+
ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse
+
+
+
# The "nixos-fw" chain does the actual work.
+
ip46tables -N nixos-fw
+
+
# Clean up rpfilter rules
+
ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true
+
ip46tables -t mangle -F nixos-fw-rpfilter 2> /dev/null || true
+
ip46tables -t mangle -X nixos-fw-rpfilter 2> /dev/null || true
+
+
${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
+
# Perform a reverse-path test to refuse spoofers
+
# For now, we just drop, as the mangle table doesn't have a log-refuse yet
+
ip46tables -t mangle -N nixos-fw-rpfilter 2> /dev/null || true
+
ip46tables -t mangle -A nixos-fw-rpfilter -m rpfilter --validmark ${optionalString (cfg.checkReversePath == "loose") "--loose"} -j RETURN
+
+
# Allows this host to act as a DHCP4 client without first having to use APIPA
+
iptables -t mangle -A nixos-fw-rpfilter -p udp --sport 67 --dport 68 -j RETURN
+
+
# Allows this host to act as a DHCPv4 server
+
iptables -t mangle -A nixos-fw-rpfilter -s 0.0.0.0 -d 255.255.255.255 -p udp --sport 68 --dport 67 -j RETURN
+
+
${optionalString cfg.logReversePathDrops ''
+
ip46tables -t mangle -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: "
+
''}
+
ip46tables -t mangle -A nixos-fw-rpfilter -j DROP
+
+
ip46tables -t mangle -A PREROUTING -j nixos-fw-rpfilter
+
''}
+
+
# Accept all traffic on the trusted interfaces.
+
${flip concatMapStrings cfg.trustedInterfaces (iface: ''
+
ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept
+
'')}
+
+
# Accept packets from established or related connections.
+
ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept
+
+
# Accept connections to the allowed TCP ports.
+
${concatStrings (mapAttrsToList (iface: cfg:
+
concatMapStrings (port:
+
''
+
ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
+
''
+
) cfg.allowedTCPPorts
+
) cfg.allInterfaces)}
+
+
# Accept connections to the allowed TCP port ranges.
+
${concatStrings (mapAttrsToList (iface: cfg:
+
concatMapStrings (rangeAttr:
+
let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in
+
''
+
ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
+
''
+
) cfg.allowedTCPPortRanges
+
) cfg.allInterfaces)}
+
+
# Accept packets on the allowed UDP ports.
+
${concatStrings (mapAttrsToList (iface: cfg:
+
concatMapStrings (port:
+
''
+
ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
+
''
+
) cfg.allowedUDPPorts
+
) cfg.allInterfaces)}
+
+
# Accept packets on the allowed UDP port ranges.
+
${concatStrings (mapAttrsToList (iface: cfg:
+
concatMapStrings (rangeAttr:
+
let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in
+
''
+
ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
+
''
+
) cfg.allowedUDPPortRanges
+
) cfg.allInterfaces)}
+
+
# Optionally respond to ICMPv4 pings.
+
${optionalString cfg.allowPing ''
+
iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null)
+
"-m limit ${cfg.pingLimit} "
+
}-j nixos-fw-accept
+
''}
+
+
${optionalString config.networking.enableIPv6 ''
+
# Accept all ICMPv6 messages except redirects and node
+
# information queries (type 139). See RFC 4890, section
+
# 4.4.
+
ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP
+
ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP
+
ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept
+
+
# Allow this host to act as a DHCPv6 client
+
ip6tables -A nixos-fw -d fe80::/64 -p udp --dport 546 -j nixos-fw-accept
+
''}
+
+
${cfg.extraCommands}
+
+
# Reject/drop everything else.
+
ip46tables -A nixos-fw -j nixos-fw-log-refuse
+
+
+
# Enable the firewall.
+
ip46tables -A INPUT -j nixos-fw
+
'';
+
+
stopScript = writeShScript "firewall-stop" ''
+
${helpers}
+
+
# Clean up in case reload fails
+
ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
+
+
# Clean up after added ruleset
+
ip46tables -D INPUT -j nixos-fw 2>/dev/null || true
+
+
${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
+
ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true
+
''}
+
+
${cfg.extraStopCommands}
+
'';
+
+
reloadScript = writeShScript "firewall-reload" ''
+
${helpers}
+
+
# Create a unique drop rule
+
ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
+
ip46tables -F nixos-drop 2>/dev/null || true
+
ip46tables -X nixos-drop 2>/dev/null || true
+
ip46tables -N nixos-drop
+
ip46tables -A nixos-drop -j DROP
+
+
# Don't allow traffic to leak out until the script has completed
+
ip46tables -A INPUT -j nixos-drop
+
+
${cfg.extraStopCommands}
+
+
if ${startScript}; then
+
ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
+
else
+
echo "Failed to reload firewall... Stopping"
+
${stopScript}
+
exit 1
+
fi
+
'';
+
+
in
+
+
{
+
+
options = {
+
+
networking.firewall = {
+
extraCommands = mkOption {
+
type = types.lines;
+
default = "";
+
example = "iptables -A INPUT -p icmp -j ACCEPT";
+
description = lib.mdDoc ''
+
Additional shell commands executed as part of the firewall
+
initialisation script. These are executed just before the
+
final "reject" firewall rule is added, so they can be used
+
to allow packets that would otherwise be refused.
+
+
This option only works with the iptables based firewall.
+
'';
+
};
+
+
extraStopCommands = mkOption {
+
type = types.lines;
+
default = "";
+
example = "iptables -P INPUT ACCEPT";
+
description = lib.mdDoc ''
+
Additional shell commands executed as part of the firewall
+
shutdown script. These are executed just after the removal
+
of the NixOS input rule, or if the service enters a failed
+
state.
+
+
This option only works with the iptables based firewall.
+
'';
+
};
+
};
+
+
};
+
+
# FIXME: Maybe if `enable' is false, the firewall should still be
+
# built but not started by default?
+
config = mkIf (cfg.enable && config.networking.nftables.enable == false) {
+
+
assertions = [
+
# This is approximately "checkReversePath -> kernelHasRPFilter",
+
# but the checkReversePath option can include non-boolean
+
# values.
+
{
+
assertion = cfg.checkReversePath == false || kernelHasRPFilter;
+
message = "This kernel does not support rpfilter";
+
}
+
];
+
+
networking.firewall.checkReversePath = mkIf (!kernelHasRPFilter) (mkDefault false);
+
+
systemd.services.firewall = {
+
description = "Firewall";
+
wantedBy = [ "sysinit.target" ];
+
wants = [ "network-pre.target" ];
+
before = [ "network-pre.target" ];
+
after = [ "systemd-modules-load.service" ];
+
+
path = [ cfg.package ] ++ cfg.extraPackages;
+
+
# FIXME: this module may also try to load kernel modules, but
+
# containers don't have CAP_SYS_MODULE. So the host system had
+
# better have all necessary modules already loaded.
+
unitConfig.ConditionCapability = "CAP_NET_ADMIN";
+
unitConfig.DefaultDependencies = false;
+
+
reloadIfChanged = true;
+
+
serviceConfig = {
+
Type = "oneshot";
+
RemainAfterExit = true;
+
ExecStart = "@${startScript} firewall-start";
+
ExecReload = "@${reloadScript} firewall-reload";
+
ExecStop = "@${stopScript} firewall-stop";
+
};
+
};
+
+
};
+
+
}
+167
nixos/modules/services/networking/firewall-nftables.nix
···
+
{ config, lib, pkgs, ... }:
+
+
with lib;
+
+
let
+
+
cfg = config.networking.firewall;
+
+
ifaceSet = concatStringsSep ", " (
+
map (x: ''"${x}"'') cfg.trustedInterfaces
+
);
+
+
portsToNftSet = ports: portRanges: concatStringsSep ", " (
+
map (x: toString x) ports
+
++ map (x: "${toString x.from}-${toString x.to}") portRanges
+
);
+
+
in
+
+
{
+
+
options = {
+
+
networking.firewall = {
+
extraInputRules = mkOption {
+
type = types.lines;
+
default = "";
+
example = "ip6 saddr { fc00::/7, fe80::/10 } tcp dport 24800 accept";
+
description = lib.mdDoc ''
+
Additional nftables rules to be appended to the input-allow
+
chain.
+
+
This option only works with the nftables based firewall.
+
'';
+
};
+
+
extraForwardRules = mkOption {
+
type = types.lines;
+
default = "";
+
example = "iifname wg0 accept";
+
description = lib.mdDoc ''
+
Additional nftables rules to be appended to the forward-allow
+
chain.
+
+
This option only works with the nftables based firewall.
+
'';
+
};
+
};
+
+
};
+
+
config = mkIf (cfg.enable && config.networking.nftables.enable) {
+
+
assertions = [
+
{
+
assertion = cfg.extraCommands == "";
+
message = "extraCommands is incompatible with the nftables based firewall: ${cfg.extraCommands}";
+
}
+
{
+
assertion = cfg.extraStopCommands == "";
+
message = "extraStopCommands is incompatible with the nftables based firewall: ${cfg.extraStopCommands}";
+
}
+
{
+
assertion = cfg.pingLimit == null || !(hasPrefix "--" cfg.pingLimit);
+
message = "nftables syntax like \"2/second\" should be used in networking.firewall.pingLimit";
+
}
+
{
+
assertion = config.networking.nftables.rulesetFile == null;
+
message = "networking.nftables.rulesetFile conflicts with the firewall";
+
}
+
];
+
+
networking.nftables.ruleset = ''
+
+
table inet nixos-fw {
+
+
${optionalString (cfg.checkReversePath != false) ''
+
chain rpfilter {
+
type filter hook prerouting priority mangle + 10; policy drop;
+
+
meta nfproto ipv4 udp sport . udp dport { 67 . 68, 68 . 67 } accept comment "DHCPv4 client/server"
+
fib saddr . mark ${optionalString (cfg.checkReversePath != "loose") ". iif"} oif exists accept
+
+
${optionalString cfg.logReversePathDrops ''
+
log level info prefix "rpfilter drop: "
+
''}
+
+
}
+
''}
+
+
chain input {
+
type filter hook input priority filter; policy drop;
+
+
${optionalString (ifaceSet != "") ''iifname { ${ifaceSet} } accept comment "trusted interfaces"''}
+
+
# Some ICMPv6 types like NDP is untracked
+
ct state vmap { invalid : drop, established : accept, related : accept, * : jump input-allow } comment "*: new and untracked"
+
+
${optionalString cfg.logRefusedConnections ''
+
tcp flags syn / fin,syn,rst,ack log level info prefix "refused connection: "
+
''}
+
${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) ''
+
pkttype broadcast log level info prefix "refused broadcast: "
+
pkttype multicast log level info prefix "refused multicast: "
+
''}
+
${optionalString cfg.logRefusedPackets ''
+
pkttype host log level info prefix "refused packet: "
+
''}
+
+
${optionalString cfg.rejectPackets ''
+
meta l4proto tcp reject with tcp reset
+
reject
+
''}
+
+
}
+
+
chain input-allow {
+
+
${concatStrings (mapAttrsToList (iface: cfg:
+
let
+
ifaceExpr = optionalString (iface != "default") "iifname ${iface}";
+
tcpSet = portsToNftSet cfg.allowedTCPPorts cfg.allowedTCPPortRanges;
+
udpSet = portsToNftSet cfg.allowedUDPPorts cfg.allowedUDPPortRanges;
+
in
+
''
+
${optionalString (tcpSet != "") "${ifaceExpr} tcp dport { ${tcpSet} } accept"}
+
${optionalString (udpSet != "") "${ifaceExpr} udp dport { ${udpSet} } accept"}
+
''
+
) cfg.allInterfaces)}
+
+
${optionalString cfg.allowPing ''
+
icmp type echo-request ${optionalString (cfg.pingLimit != null) "limit rate ${cfg.pingLimit}"} accept comment "allow ping"
+
''}
+
+
icmpv6 type != { nd-redirect, 139 } accept comment "Accept all ICMPv6 messages except redirects and node information queries (type 139). See RFC 4890, section 4.4."
+
ip6 daddr fe80::/64 udp dport 546 accept comment "DHCPv6 client"
+
+
${cfg.extraInputRules}
+
+
}
+
+
${optionalString cfg.filterForward ''
+
chain forward {
+
type filter hook forward priority filter; policy drop;
+
+
ct state vmap { invalid : drop, established : accept, related : accept, * : jump forward-allow } comment "*: new and untracked"
+
+
}
+
+
chain forward-allow {
+
+
icmpv6 type != { router-renumbering, 139 } accept comment "Accept all ICMPv6 messages except renumbering and node information queries (type 139). See RFC 4890, section 4.3."
+
+
ct status dnat accept comment "allow port forward"
+
+
${cfg.extraForwardRules}
+
+
}
+
''}
+
+
}
+
+
'';
+
+
};
+
+
}
+141 -439
nixos/modules/services/networking/firewall.nix
···
-
/* This module enables a simple firewall.
-
-
The firewall can be customised in arbitrary ways by setting
-
‘networking.firewall.extraCommands’. For modularity, the firewall
-
uses several chains:
-
-
- ‘nixos-fw’ is the main chain for input packet processing.
-
-
- ‘nixos-fw-accept’ is called for accepted packets. If you want
-
additional logging, or want to reject certain packets anyway, you
-
can insert rules at the start of this chain.
-
-
- ‘nixos-fw-log-refuse’ and ‘nixos-fw-refuse’ are called for
-
refused packets. (The former jumps to the latter after logging
-
the packet.) If you want additional logging, or want to accept
-
certain packets anyway, you can insert rules at the start of
-
this chain.
-
-
- ‘nixos-fw-rpfilter’ is used as the main chain in the mangle table,
-
called from the built-in ‘PREROUTING’ chain. If the kernel
-
supports it and `cfg.checkReversePath` is set this chain will
-
perform a reverse path filter test.
-
-
- ‘nixos-drop’ is used while reloading the firewall in order to drop
-
all traffic. Since reloading isn't implemented in an atomic way
-
this'll prevent any traffic from leaking through while reloading
-
the firewall. However, if the reloading fails, the ‘firewall-stop’
-
script will be called which in return will effectively disable the
-
complete firewall (in the default configuration).
-
-
*/
-
{ config, lib, pkgs, ... }:
with lib;
···
cfg = config.networking.firewall;
-
inherit (config.boot.kernelPackages) kernel;
-
-
kernelHasRPFilter = ((kernel.config.isEnabled or (x: false)) "IP_NF_MATCH_RPFILTER") || (kernel.features.netfilterRPFilter or false);
-
-
helpers = import ./helpers.nix { inherit config lib; };
-
-
writeShScript = name: text: let dir = pkgs.writeScriptBin name ''
-
#! ${pkgs.runtimeShell} -e
-
${text}
-
''; in "${dir}/bin/${name}";
-
-
defaultInterface = { default = mapAttrs (name: value: cfg.${name}) commonOptions; };
-
allInterfaces = defaultInterface // cfg.interfaces;
-
-
startScript = writeShScript "firewall-start" ''
-
${helpers}
-
-
# Flush the old firewall rules. !!! Ideally, updating the
-
# firewall would be atomic. Apparently that's possible
-
# with iptables-restore.
-
ip46tables -D INPUT -j nixos-fw 2> /dev/null || true
-
for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse; do
-
ip46tables -F "$chain" 2> /dev/null || true
-
ip46tables -X "$chain" 2> /dev/null || true
-
done
-
-
-
# The "nixos-fw-accept" chain just accepts packets.
-
ip46tables -N nixos-fw-accept
-
ip46tables -A nixos-fw-accept -j ACCEPT
-
-
-
# The "nixos-fw-refuse" chain rejects or drops packets.
-
ip46tables -N nixos-fw-refuse
-
-
${if cfg.rejectPackets then ''
-
# Send a reset for existing TCP connections that we've
-
# somehow forgotten about. Send ICMP "port unreachable"
-
# for everything else.
-
ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset
-
ip46tables -A nixos-fw-refuse -j REJECT
-
'' else ''
-
ip46tables -A nixos-fw-refuse -j DROP
-
''}
-
-
-
# The "nixos-fw-log-refuse" chain performs logging, then
-
# jumps to the "nixos-fw-refuse" chain.
-
ip46tables -N nixos-fw-log-refuse
-
-
${optionalString cfg.logRefusedConnections ''
-
ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "refused connection: "
-
''}
-
${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) ''
-
ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \
-
-j LOG --log-level info --log-prefix "refused broadcast: "
-
ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \
-
-j LOG --log-level info --log-prefix "refused multicast: "
-
''}
-
ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse
-
${optionalString cfg.logRefusedPackets ''
-
ip46tables -A nixos-fw-log-refuse \
-
-j LOG --log-level info --log-prefix "refused packet: "
-
''}
-
ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse
-
-
-
# The "nixos-fw" chain does the actual work.
-
ip46tables -N nixos-fw
-
-
# Clean up rpfilter rules
-
ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true
-
ip46tables -t mangle -F nixos-fw-rpfilter 2> /dev/null || true
-
ip46tables -t mangle -X nixos-fw-rpfilter 2> /dev/null || true
-
-
${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
-
# Perform a reverse-path test to refuse spoofers
-
# For now, we just drop, as the mangle table doesn't have a log-refuse yet
-
ip46tables -t mangle -N nixos-fw-rpfilter 2> /dev/null || true
-
ip46tables -t mangle -A nixos-fw-rpfilter -m rpfilter --validmark ${optionalString (cfg.checkReversePath == "loose") "--loose"} -j RETURN
-
-
# Allows this host to act as a DHCP4 client without first having to use APIPA
-
iptables -t mangle -A nixos-fw-rpfilter -p udp --sport 67 --dport 68 -j RETURN
-
-
# Allows this host to act as a DHCPv4 server
-
iptables -t mangle -A nixos-fw-rpfilter -s 0.0.0.0 -d 255.255.255.255 -p udp --sport 68 --dport 67 -j RETURN
-
-
${optionalString cfg.logReversePathDrops ''
-
ip46tables -t mangle -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: "
-
''}
-
ip46tables -t mangle -A nixos-fw-rpfilter -j DROP
-
-
ip46tables -t mangle -A PREROUTING -j nixos-fw-rpfilter
-
''}
-
-
# Accept all traffic on the trusted interfaces.
-
${flip concatMapStrings cfg.trustedInterfaces (iface: ''
-
ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept
-
'')}
-
-
# Accept packets from established or related connections.
-
ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept
-
-
# Accept connections to the allowed TCP ports.
-
${concatStrings (mapAttrsToList (iface: cfg:
-
concatMapStrings (port:
-
''
-
ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
-
''
-
) cfg.allowedTCPPorts
-
) allInterfaces)}
-
-
# Accept connections to the allowed TCP port ranges.
-
${concatStrings (mapAttrsToList (iface: cfg:
-
concatMapStrings (rangeAttr:
-
let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in
-
''
-
ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
-
''
-
) cfg.allowedTCPPortRanges
-
) allInterfaces)}
-
-
# Accept packets on the allowed UDP ports.
-
${concatStrings (mapAttrsToList (iface: cfg:
-
concatMapStrings (port:
-
''
-
ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
-
''
-
) cfg.allowedUDPPorts
-
) allInterfaces)}
-
-
# Accept packets on the allowed UDP port ranges.
-
${concatStrings (mapAttrsToList (iface: cfg:
-
concatMapStrings (rangeAttr:
-
let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in
-
''
-
ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
-
''
-
) cfg.allowedUDPPortRanges
-
) allInterfaces)}
-
-
# Optionally respond to ICMPv4 pings.
-
${optionalString cfg.allowPing ''
-
iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null)
-
"-m limit ${cfg.pingLimit} "
-
}-j nixos-fw-accept
-
''}
-
-
${optionalString config.networking.enableIPv6 ''
-
# Accept all ICMPv6 messages except redirects and node
-
# information queries (type 139). See RFC 4890, section
-
# 4.4.
-
ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP
-
ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP
-
ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept
-
-
# Allow this host to act as a DHCPv6 client
-
ip6tables -A nixos-fw -d fe80::/64 -p udp --dport 546 -j nixos-fw-accept
-
''}
-
-
${cfg.extraCommands}
-
-
# Reject/drop everything else.
-
ip46tables -A nixos-fw -j nixos-fw-log-refuse
-
-
-
# Enable the firewall.
-
ip46tables -A INPUT -j nixos-fw
-
'';
-
-
stopScript = writeShScript "firewall-stop" ''
-
${helpers}
-
-
# Clean up in case reload fails
-
ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
-
-
# Clean up after added ruleset
-
ip46tables -D INPUT -j nixos-fw 2>/dev/null || true
-
-
${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
-
ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true
-
''}
-
-
${cfg.extraStopCommands}
-
'';
-
-
reloadScript = writeShScript "firewall-reload" ''
-
${helpers}
-
-
# Create a unique drop rule
-
ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
-
ip46tables -F nixos-drop 2>/dev/null || true
-
ip46tables -X nixos-drop 2>/dev/null || true
-
ip46tables -N nixos-drop
-
ip46tables -A nixos-drop -j DROP
-
-
# Don't allow traffic to leak out until the script has completed
-
ip46tables -A INPUT -j nixos-drop
-
-
${cfg.extraStopCommands}
-
-
if ${startScript}; then
-
ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
-
else
-
echo "Failed to reload firewall... Stopping"
-
${stopScript}
-
exit 1
-
fi
-
'';
-
canonicalizePortList =
ports: lib.unique (builtins.sort builtins.lessThan ports);
···
default = [ ];
apply = canonicalizePortList;
example = [ 22 80 ];
-
description =
-
lib.mdDoc ''
-
List of TCP ports on which incoming connections are
-
accepted.
-
'';
+
description = lib.mdDoc ''
+
List of TCP ports on which incoming connections are
+
accepted.
+
'';
};
allowedTCPPortRanges = mkOption {
type = types.listOf (types.attrsOf types.port);
default = [ ];
-
example = [ { from = 8999; to = 9003; } ];
-
description =
-
lib.mdDoc ''
-
A range of TCP ports on which incoming connections are
-
accepted.
-
'';
+
example = [{ from = 8999; to = 9003; }];
+
description = lib.mdDoc ''
+
A range of TCP ports on which incoming connections are
+
accepted.
+
'';
};
allowedUDPPorts = mkOption {
···
default = [ ];
apply = canonicalizePortList;
example = [ 53 ];
-
description =
-
lib.mdDoc ''
-
List of open UDP ports.
-
'';
+
description = lib.mdDoc ''
+
List of open UDP ports.
+
'';
};
allowedUDPPortRanges = mkOption {
type = types.listOf (types.attrsOf types.port);
default = [ ];
-
example = [ { from = 60000; to = 61000; } ];
-
description =
-
lib.mdDoc ''
-
Range of open UDP ports.
-
'';
+
example = [{ from = 60000; to = 61000; }];
+
description = lib.mdDoc ''
+
Range of open UDP ports.
+
'';
};
};
···
{
-
###### interface
-
options = {
networking.firewall = {
enable = mkOption {
type = types.bool;
default = true;
-
description =
-
lib.mdDoc ''
-
Whether to enable the firewall. This is a simple stateful
-
firewall that blocks connection attempts to unauthorised TCP
-
or UDP ports on this machine. It does not affect packet
-
forwarding.
-
'';
+
description = lib.mdDoc ''
+
Whether to enable the firewall. This is a simple stateful
+
firewall that blocks connection attempts to unauthorised TCP
+
or UDP ports on this machine.
+
'';
};
package = mkOption {
type = types.package;
-
default = pkgs.iptables;
-
defaultText = literalExpression "pkgs.iptables";
+
default = if config.networking.nftables.enable then pkgs.nftables else pkgs.iptables;
+
defaultText = literalExpression ''if config.networking.nftables.enable then "pkgs.nftables" else "pkgs.iptables"'';
example = literalExpression "pkgs.iptables-legacy";
-
description =
-
lib.mdDoc ''
-
The iptables package to use for running the firewall service.
-
'';
+
description = lib.mdDoc ''
+
The package to use for running the firewall service.
+
'';
};
logRefusedConnections = mkOption {
type = types.bool;
default = true;
-
description =
-
lib.mdDoc ''
-
Whether to log rejected or dropped incoming connections.
-
Note: The logs are found in the kernel logs, i.e. dmesg
-
or journalctl -k.
-
'';
+
description = lib.mdDoc ''
+
Whether to log rejected or dropped incoming connections.
+
Note: The logs are found in the kernel logs, i.e. dmesg
+
or journalctl -k.
+
'';
};
logRefusedPackets = mkOption {
type = types.bool;
default = false;
-
description =
-
lib.mdDoc ''
-
Whether to log all rejected or dropped incoming packets.
-
This tends to give a lot of log messages, so it's mostly
-
useful for debugging.
-
Note: The logs are found in the kernel logs, i.e. dmesg
-
or journalctl -k.
-
'';
+
description = lib.mdDoc ''
+
Whether to log all rejected or dropped incoming packets.
+
This tends to give a lot of log messages, so it's mostly
+
useful for debugging.
+
Note: The logs are found in the kernel logs, i.e. dmesg
+
or journalctl -k.
+
'';
};
logRefusedUnicastsOnly = mkOption {
type = types.bool;
default = true;
-
description =
-
lib.mdDoc ''
-
If {option}`networking.firewall.logRefusedPackets`
-
and this option are enabled, then only log packets
-
specifically directed at this machine, i.e., not broadcasts
-
or multicasts.
-
'';
+
description = lib.mdDoc ''
+
If {option}`networking.firewall.logRefusedPackets`
+
and this option are enabled, then only log packets
+
specifically directed at this machine, i.e., not broadcasts
+
or multicasts.
+
'';
};
rejectPackets = mkOption {
type = types.bool;
default = false;
-
description =
-
lib.mdDoc ''
-
If set, refused packets are rejected rather than dropped
-
(ignored). This means that an ICMP "port unreachable" error
-
message is sent back to the client (or a TCP RST packet in
-
case of an existing connection). Rejecting packets makes
-
port scanning somewhat easier.
-
'';
+
description = lib.mdDoc ''
+
If set, refused packets are rejected rather than dropped
+
(ignored). This means that an ICMP "port unreachable" error
+
message is sent back to the client (or a TCP RST packet in
+
case of an existing connection). Rejecting packets makes
+
port scanning somewhat easier.
+
'';
};
trustedInterfaces = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "enp0s2" ];
-
description =
-
lib.mdDoc ''
-
Traffic coming in from these interfaces will be accepted
-
unconditionally. Traffic from the loopback (lo) interface
-
will always be accepted.
-
'';
+
description = lib.mdDoc ''
+
Traffic coming in from these interfaces will be accepted
+
unconditionally. Traffic from the loopback (lo) interface
+
will always be accepted.
+
'';
};
allowPing = mkOption {
type = types.bool;
default = true;
-
description =
-
lib.mdDoc ''
-
Whether to respond to incoming ICMPv4 echo requests
-
("pings"). ICMPv6 pings are always allowed because the
-
larger address space of IPv6 makes network scanning much
-
less effective.
-
'';
+
description = lib.mdDoc ''
+
Whether to respond to incoming ICMPv4 echo requests
+
("pings"). ICMPv6 pings are always allowed because the
+
larger address space of IPv6 makes network scanning much
+
less effective.
+
'';
};
pingLimit = mkOption {
type = types.nullOr (types.separatedString " ");
default = null;
example = "--limit 1/minute --limit-burst 5";
-
description =
-
lib.mdDoc ''
-
If pings are allowed, this allows setting rate limits
-
on them. If non-null, this option should be in the form of
-
flags like "--limit 1/minute --limit-burst 5"
-
'';
+
description = lib.mdDoc ''
+
If pings are allowed, this allows setting rate limits on them.
+
+
For the iptables based firewall, it should be set like
+
"--limit 1/minute --limit-burst 5".
+
+
For the nftables based firewall, it should be set like
+
"2/second" or "1/minute burst 5 packets".
+
'';
};
checkReversePath = mkOption {
-
type = types.either types.bool (types.enum ["strict" "loose"]);
-
default = kernelHasRPFilter;
-
defaultText = literalMD "`true` if supported by the chosen kernel";
+
type = types.either types.bool (types.enum [ "strict" "loose" ]);
+
default = true;
+
defaultText = literalMD "`true` except if the iptables based firewall is in use and the kernel lacks rpfilter support";
example = "loose";
-
description =
-
lib.mdDoc ''
-
Performs a reverse path filter test on a packet. If a reply
-
to the packet would not be sent via the same interface that
-
the packet arrived on, it is refused.
+
description = lib.mdDoc ''
+
Performs a reverse path filter test on a packet. If a reply
+
to the packet would not be sent via the same interface that
+
the packet arrived on, it is refused.
-
If using asymmetric routing or other complicated routing, set
-
this option to loose mode or disable it and setup your own
-
counter-measures.
+
If using asymmetric routing or other complicated routing, set
+
this option to loose mode or disable it and setup your own
+
counter-measures.
-
This option can be either true (or "strict"), "loose" (only
-
drop the packet if the source address is not reachable via any
-
interface) or false. Defaults to the value of
-
kernelHasRPFilter.
-
'';
+
This option can be either true (or "strict"), "loose" (only
+
drop the packet if the source address is not reachable via any
+
interface) or false.
+
'';
};
logReversePathDrops = mkOption {
type = types.bool;
default = false;
-
description =
-
lib.mdDoc ''
-
Logs dropped packets failing the reverse path filter test if
-
the option networking.firewall.checkReversePath is enabled.
-
'';
+
description = lib.mdDoc ''
+
Logs dropped packets failing the reverse path filter test if
+
the option networking.firewall.checkReversePath is enabled.
+
'';
+
};
+
+
filterForward = mkOption {
+
type = types.bool;
+
default = false;
+
description = lib.mdDoc ''
+
Enable filtering in IP forwarding.
+
+
This option only works with the nftables based firewall.
+
'';
};
connectionTrackingModules = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "ftp" "irc" "sane" "sip" "tftp" "amanda" "h323" "netbios_sn" "pptp" "snmp" ];
-
description =
-
lib.mdDoc ''
-
List of connection-tracking helpers that are auto-loaded.
-
The complete list of possible values is given in the example.
+
description = lib.mdDoc ''
+
List of connection-tracking helpers that are auto-loaded.
+
The complete list of possible values is given in the example.
-
As helpers can pose as a security risk, it is advised to
-
set this to an empty list and disable the setting
-
networking.firewall.autoLoadConntrackHelpers unless you
-
know what you are doing. Connection tracking is disabled
-
by default.
+
As helpers can pose as a security risk, it is advised to
+
set this to an empty list and disable the setting
+
networking.firewall.autoLoadConntrackHelpers unless you
+
know what you are doing. Connection tracking is disabled
+
by default.
-
Loading of helpers is recommended to be done through the
-
CT target. More info:
-
https://home.regit.org/netfilter-en/secure-use-of-helpers/
-
'';
+
Loading of helpers is recommended to be done through the
+
CT target. More info:
+
https://home.regit.org/netfilter-en/secure-use-of-helpers/
+
'';
};
autoLoadConntrackHelpers = mkOption {
type = types.bool;
default = false;
-
description =
-
lib.mdDoc ''
-
Whether to auto-load connection-tracking helpers.
-
See the description at networking.firewall.connectionTrackingModules
-
-
(needs kernel 3.5+)
-
'';
-
};
+
description = lib.mdDoc ''
+
Whether to auto-load connection-tracking helpers.
+
See the description at networking.firewall.connectionTrackingModules
-
extraCommands = mkOption {
-
type = types.lines;
-
default = "";
-
example = "iptables -A INPUT -p icmp -j ACCEPT";
-
description =
-
lib.mdDoc ''
-
Additional shell commands executed as part of the firewall
-
initialisation script. These are executed just before the
-
final "reject" firewall rule is added, so they can be used
-
to allow packets that would otherwise be refused.
-
'';
+
(needs kernel 3.5+)
+
'';
};
extraPackages = mkOption {
type = types.listOf types.package;
default = [ ];
example = literalExpression "[ pkgs.ipset ]";
-
description =
-
lib.mdDoc ''
-
Additional packages to be included in the environment of the system
-
as well as the path of networking.firewall.extraCommands.
-
'';
+
description = lib.mdDoc ''
+
Additional packages to be included in the environment of the system
+
as well as the path of networking.firewall.extraCommands.
+
'';
};
-
extraStopCommands = mkOption {
-
type = types.lines;
-
default = "";
-
example = "iptables -P INPUT ACCEPT";
-
description =
-
lib.mdDoc ''
-
Additional shell commands executed as part of the firewall
-
shutdown script. These are executed just after the removal
-
of the NixOS input rule, or if the service enters a failed
-
state.
-
'';
+
interfaces = mkOption {
+
default = { };
+
type = with types; attrsOf (submodule [{ options = commonOptions; }]);
+
description = lib.mdDoc ''
+
Interface-specific open ports.
+
'';
};
-
interfaces = mkOption {
-
default = { };
-
type = with types; attrsOf (submodule [ { options = commonOptions; } ]);
-
description =
-
lib.mdDoc ''
-
Interface-specific open ports.
-
'';
+
allInterfaces = mkOption {
+
internal = true;
+
visible = false;
+
default = { default = mapAttrs (name: value: cfg.${name}) commonOptions; } // cfg.interfaces;
+
type = with types; attrsOf (submodule [{ options = commonOptions; }]);
+
description = lib.mdDoc ''
+
All open ports.
+
'';
};
} // commonOptions;
};
-
###### implementation
-
-
# FIXME: Maybe if `enable' is false, the firewall should still be
-
# built but not started by default?
config = mkIf cfg.enable {
+
assertions = [
+
{
+
assertion = cfg.filterForward -> config.networking.nftables.enable;
+
message = "filterForward only works with the nftables based firewall";
+
}
+
];
+
networking.firewall.trustedInterfaces = [ "lo" ];
environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages;
···
boot.extraModprobeConfig = optionalString cfg.autoLoadConntrackHelpers ''
options nf_conntrack nf_conntrack_helper=1
'';
-
-
assertions = [
-
# This is approximately "checkReversePath -> kernelHasRPFilter",
-
# but the checkReversePath option can include non-boolean
-
# values.
-
{ assertion = cfg.checkReversePath == false || kernelHasRPFilter;
-
message = "This kernel does not support rpfilter"; }
-
];
-
-
systemd.services.firewall = {
-
description = "Firewall";
-
wantedBy = [ "sysinit.target" ];
-
wants = [ "network-pre.target" ];
-
before = [ "network-pre.target" ];
-
after = [ "systemd-modules-load.service" ];
-
-
path = [ cfg.package ] ++ cfg.extraPackages;
-
-
# FIXME: this module may also try to load kernel modules, but
-
# containers don't have CAP_SYS_MODULE. So the host system had
-
# better have all necessary modules already loaded.
-
unitConfig.ConditionCapability = "CAP_NET_ADMIN";
-
unitConfig.DefaultDependencies = false;
-
-
reloadIfChanged = true;
-
-
serviceConfig = {
-
Type = "oneshot";
-
RemainAfterExit = true;
-
ExecStart = "@${startScript} firewall-start";
-
ExecReload = "@${reloadScript} firewall-reload";
-
ExecStop = "@${stopScript} firewall-stop";
-
};
-
};
};
+191
nixos/modules/services/networking/nat-iptables.nix
···
+
# This module enables Network Address Translation (NAT).
+
# XXX: todo: support multiple upstream links
+
# see http://yesican.chsoft.biz/lartc/MultihomedLinuxNetworking.html
+
+
{ config, lib, pkgs, ... }:
+
+
with lib;
+
+
let
+
cfg = config.networking.nat;
+
+
mkDest = externalIP:
+
if externalIP == null
+
then "-j MASQUERADE"
+
else "-j SNAT --to-source ${externalIP}";
+
dest = mkDest cfg.externalIP;
+
destIPv6 = mkDest cfg.externalIPv6;
+
+
# Whether given IP (plus optional port) is an IPv6.
+
isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2;
+
+
helpers = import ./helpers.nix { inherit config lib; };
+
+
flushNat = ''
+
${helpers}
+
ip46tables -w -t nat -D PREROUTING -j nixos-nat-pre 2>/dev/null|| true
+
ip46tables -w -t nat -F nixos-nat-pre 2>/dev/null || true
+
ip46tables -w -t nat -X nixos-nat-pre 2>/dev/null || true
+
ip46tables -w -t nat -D POSTROUTING -j nixos-nat-post 2>/dev/null || true
+
ip46tables -w -t nat -F nixos-nat-post 2>/dev/null || true
+
ip46tables -w -t nat -X nixos-nat-post 2>/dev/null || true
+
ip46tables -w -t nat -D OUTPUT -j nixos-nat-out 2>/dev/null || true
+
ip46tables -w -t nat -F nixos-nat-out 2>/dev/null || true
+
ip46tables -w -t nat -X nixos-nat-out 2>/dev/null || true
+
+
${cfg.extraStopCommands}
+
'';
+
+
mkSetupNat = { iptables, dest, internalIPs, forwardPorts }: ''
+
# We can't match on incoming interface in POSTROUTING, so
+
# mark packets coming from the internal interfaces.
+
${concatMapStrings (iface: ''
+
${iptables} -w -t nat -A nixos-nat-pre \
+
-i '${iface}' -j MARK --set-mark 1
+
'') cfg.internalInterfaces}
+
+
# NAT the marked packets.
+
${optionalString (cfg.internalInterfaces != []) ''
+
${iptables} -w -t nat -A nixos-nat-post -m mark --mark 1 \
+
${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
+
''}
+
+
# NAT packets coming from the internal IPs.
+
${concatMapStrings (range: ''
+
${iptables} -w -t nat -A nixos-nat-post \
+
-s '${range}' ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
+
'') internalIPs}
+
+
# NAT from external ports to internal ports.
+
${concatMapStrings (fwd: ''
+
${iptables} -w -t nat -A nixos-nat-pre \
+
-i ${toString cfg.externalInterface} -p ${fwd.proto} \
+
--dport ${builtins.toString fwd.sourcePort} \
+
-j DNAT --to-destination ${fwd.destination}
+
+
${concatMapStrings (loopbackip:
+
let
+
matchIP = if isIPv6 fwd.destination then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)";
+
m = builtins.match "${matchIP}:([0-9-]+)" fwd.destination;
+
destinationIP = if m == null then throw "bad ip:ports `${fwd.destination}'" else elemAt m 0;
+
destinationPorts = if m == null then throw "bad ip:ports `${fwd.destination}'" else builtins.replaceStrings ["-"] [":"] (elemAt m 1);
+
in ''
+
# Allow connections to ${loopbackip}:${toString fwd.sourcePort} from the host itself
+
${iptables} -w -t nat -A nixos-nat-out \
+
-d ${loopbackip} -p ${fwd.proto} \
+
--dport ${builtins.toString fwd.sourcePort} \
+
-j DNAT --to-destination ${fwd.destination}
+
+
# Allow connections to ${loopbackip}:${toString fwd.sourcePort} from other hosts behind NAT
+
${iptables} -w -t nat -A nixos-nat-pre \
+
-d ${loopbackip} -p ${fwd.proto} \
+
--dport ${builtins.toString fwd.sourcePort} \
+
-j DNAT --to-destination ${fwd.destination}
+
+
${iptables} -w -t nat -A nixos-nat-post \
+
-d ${destinationIP} -p ${fwd.proto} \
+
--dport ${destinationPorts} \
+
-j SNAT --to-source ${loopbackip}
+
'') fwd.loopbackIPs}
+
'') forwardPorts}
+
'';
+
+
setupNat = ''
+
${helpers}
+
# Create subchains where we store rules
+
ip46tables -w -t nat -N nixos-nat-pre
+
ip46tables -w -t nat -N nixos-nat-post
+
ip46tables -w -t nat -N nixos-nat-out
+
+
${mkSetupNat {
+
iptables = "iptables";
+
inherit dest;
+
inherit (cfg) internalIPs;
+
forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts;
+
}}
+
+
${optionalString cfg.enableIPv6 (mkSetupNat {
+
iptables = "ip6tables";
+
dest = destIPv6;
+
internalIPs = cfg.internalIPv6s;
+
forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
+
})}
+
+
${optionalString (cfg.dmzHost != null) ''
+
iptables -w -t nat -A nixos-nat-pre \
+
-i ${toString cfg.externalInterface} -j DNAT \
+
--to-destination ${cfg.dmzHost}
+
''}
+
+
${cfg.extraCommands}
+
+
# Append our chains to the nat tables
+
ip46tables -w -t nat -A PREROUTING -j nixos-nat-pre
+
ip46tables -w -t nat -A POSTROUTING -j nixos-nat-post
+
ip46tables -w -t nat -A OUTPUT -j nixos-nat-out
+
'';
+
+
in
+
+
{
+
+
options = {
+
+
networking.nat.extraCommands = mkOption {
+
type = types.lines;
+
default = "";
+
example = "iptables -A INPUT -p icmp -j ACCEPT";
+
description = lib.mdDoc ''
+
Additional shell commands executed as part of the nat
+
initialisation script.
+
+
This option is incompatible with the nftables based nat module.
+
'';
+
};
+
+
networking.nat.extraStopCommands = mkOption {
+
type = types.lines;
+
default = "";
+
example = "iptables -D INPUT -p icmp -j ACCEPT || true";
+
description = lib.mdDoc ''
+
Additional shell commands executed as part of the nat
+
teardown script.
+
+
This option is incompatible with the nftables based nat module.
+
'';
+
};
+
+
};
+
+
+
config = mkIf (!config.networking.nftables.enable)
+
(mkMerge [
+
({ networking.firewall.extraCommands = mkBefore flushNat; })
+
(mkIf config.networking.nat.enable {
+
+
networking.firewall = mkIf config.networking.firewall.enable {
+
extraCommands = setupNat;
+
extraStopCommands = flushNat;
+
};
+
+
systemd.services = mkIf (!config.networking.firewall.enable) {
+
nat = {
+
description = "Network Address Translation";
+
wantedBy = [ "network.target" ];
+
after = [ "network-pre.target" "systemd-modules-load.service" ];
+
path = [ config.networking.firewall.package ];
+
unitConfig.ConditionCapability = "CAP_NET_ADMIN";
+
+
serviceConfig = {
+
Type = "oneshot";
+
RemainAfterExit = true;
+
};
+
+
script = flushNat + setupNat;
+
+
postStop = flushNat;
+
};
+
};
+
})
+
]);
+
}
+184
nixos/modules/services/networking/nat-nftables.nix
···
+
{ config, lib, pkgs, ... }:
+
+
with lib;
+
+
let
+
cfg = config.networking.nat;
+
+
mkDest = externalIP:
+
if externalIP == null
+
then "masquerade"
+
else "snat ${externalIP}";
+
dest = mkDest cfg.externalIP;
+
destIPv6 = mkDest cfg.externalIPv6;
+
+
toNftSet = list: concatStringsSep ", " list;
+
toNftRange = ports: replaceStrings [ ":" ] [ "-" ] (toString ports);
+
+
ifaceSet = toNftSet (map (x: ''"${x}"'') cfg.internalInterfaces);
+
ipSet = toNftSet cfg.internalIPs;
+
ipv6Set = toNftSet cfg.internalIPv6s;
+
oifExpr = optionalString (cfg.externalInterface != null) ''oifname "${cfg.externalInterface}"'';
+
+
# Whether given IP (plus optional port) is an IPv6.
+
isIPv6 = ip: length (lib.splitString ":" ip) > 2;
+
+
splitIPPorts = IPPorts:
+
let
+
matchIP = if isIPv6 IPPorts then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)";
+
m = builtins.match "${matchIP}:([0-9-]+)" IPPorts;
+
in
+
{
+
IP = if m == null then throw "bad ip:ports `${IPPorts}'" else elemAt m 0;
+
ports = if m == null then throw "bad ip:ports `${IPPorts}'" else elemAt m 1;
+
};
+
+
mkTable = { ipVer, dest, ipSet, forwardPorts, dmzHost }:
+
let
+
# nftables does not support both port and port range as values in a dnat map.
+
# e.g. "dnat th dport map { 80 : 10.0.0.1 . 80, 443 : 10.0.0.2 . 900-1000 }"
+
# So we split them.
+
fwdPorts = filter (x: length (splitString "-" x.destination) == 1) forwardPorts;
+
fwdPortsRange = filter (x: length (splitString "-" x.destination) > 1) forwardPorts;
+
+
# nftables maps for port forward
+
# l4proto . dport : addr . port
+
toFwdMap = forwardPorts: toNftSet (map
+
(fwd:
+
with (splitIPPorts fwd.destination);
+
"${fwd.proto} . ${toNftRange fwd.sourcePort} : ${IP} . ${ports}"
+
)
+
forwardPorts);
+
fwdMap = toFwdMap fwdPorts;
+
fwdRangeMap = toFwdMap fwdPortsRange;
+
+
# nftables maps for port forward loopback dnat
+
# daddr . l4proto . dport : addr . port
+
toFwdLoopDnatMap = forwardPorts: toNftSet (concatMap
+
(fwd: map
+
(loopbackip:
+
with (splitIPPorts fwd.destination);
+
"${loopbackip} . ${fwd.proto} . ${toNftRange fwd.sourcePort} : ${IP} . ${ports}"
+
)
+
fwd.loopbackIPs)
+
forwardPorts);
+
fwdLoopDnatMap = toFwdLoopDnatMap fwdPorts;
+
fwdLoopDnatRangeMap = toFwdLoopDnatMap fwdPortsRange;
+
+
# nftables set for port forward loopback snat
+
# daddr . l4proto . dport
+
fwdLoopSnatSet = toNftSet (map
+
(fwd:
+
with (splitIPPorts fwd.destination);
+
"${IP} . ${fwd.proto} . ${ports}"
+
)
+
forwardPorts);
+
in
+
''
+
chain pre {
+
type nat hook prerouting priority dstnat;
+
+
${optionalString (fwdMap != "") ''
+
iifname "${cfg.externalInterface}" dnat meta l4proto . th dport map { ${fwdMap} } comment "port forward"
+
''}
+
${optionalString (fwdRangeMap != "") ''
+
iifname "${cfg.externalInterface}" dnat meta l4proto . th dport map { ${fwdRangeMap} } comment "port forward"
+
''}
+
+
${optionalString (fwdLoopDnatMap != "") ''
+
dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatMap} } comment "port forward loopback from other hosts behind NAT"
+
''}
+
${optionalString (fwdLoopDnatRangeMap != "") ''
+
dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatRangeMap} } comment "port forward loopback from other hosts behind NAT"
+
''}
+
+
${optionalString (dmzHost != null) ''
+
iifname "${cfg.externalInterface}" dnat ${dmzHost} comment "dmz"
+
''}
+
}
+
+
chain post {
+
type nat hook postrouting priority srcnat;
+
+
${optionalString (ifaceSet != "") ''
+
iifname { ${ifaceSet} } ${oifExpr} ${dest} comment "from internal interfaces"
+
''}
+
${optionalString (ipSet != "") ''
+
${ipVer} saddr { ${ipSet} } ${oifExpr} ${dest} comment "from internal IPs"
+
''}
+
+
${optionalString (fwdLoopSnatSet != "") ''
+
iifname != "${cfg.externalInterface}" ${ipVer} daddr . meta l4proto . th dport { ${fwdLoopSnatSet} } masquerade comment "port forward loopback snat"
+
''}
+
}
+
+
chain out {
+
type nat hook output priority mangle;
+
+
${optionalString (fwdLoopDnatMap != "") ''
+
dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatMap} } comment "port forward loopback from the host itself"
+
''}
+
${optionalString (fwdLoopDnatRangeMap != "") ''
+
dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatRangeMap} } comment "port forward loopback from the host itself"
+
''}
+
}
+
'';
+
+
in
+
+
{
+
+
config = mkIf (config.networking.nftables.enable && cfg.enable) {
+
+
assertions = [
+
{
+
assertion = cfg.extraCommands == "";
+
message = "extraCommands is incompatible with the nftables based nat module: ${cfg.extraCommands}";
+
}
+
{
+
assertion = cfg.extraStopCommands == "";
+
message = "extraStopCommands is incompatible with the nftables based nat module: ${cfg.extraStopCommands}";
+
}
+
{
+
assertion = config.networking.nftables.rulesetFile == null;
+
message = "networking.nftables.rulesetFile conflicts with the nat module";
+
}
+
];
+
+
networking.nftables.ruleset = ''
+
table ip nixos-nat {
+
${mkTable {
+
ipVer = "ip";
+
inherit dest ipSet;
+
forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts;
+
inherit (cfg) dmzHost;
+
}}
+
}
+
+
${optionalString cfg.enableIPv6 ''
+
table ip6 nixos-nat {
+
${mkTable {
+
ipVer = "ip6";
+
dest = destIPv6;
+
ipSet = ipv6Set;
+
forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
+
dmzHost = null;
+
}}
+
}
+
''}
+
'';
+
+
networking.firewall.extraForwardRules = optionalString config.networking.firewall.filterForward ''
+
${optionalString (ifaceSet != "") ''
+
iifname { ${ifaceSet} } ${oifExpr} accept comment "from internal interfaces"
+
''}
+
${optionalString (ipSet != "") ''
+
ip saddr { ${ipSet} } ${oifExpr} accept comment "from internal IPs"
+
''}
+
${optionalString (ipv6Set != "") ''
+
ip6 saddr { ${ipv6Set} } ${oifExpr} accept comment "from internal IPv6s"
+
''}
+
'';
+
+
};
+
}
+85 -256
nixos/modules/services/networking/nat.nix
···
with lib;
let
-
cfg = config.networking.nat;
-
mkDest = externalIP: if externalIP == null
-
then "-j MASQUERADE"
-
else "-j SNAT --to-source ${externalIP}";
-
dest = mkDest cfg.externalIP;
-
destIPv6 = mkDest cfg.externalIPv6;
-
-
# Whether given IP (plus optional port) is an IPv6.
-
isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2;
-
-
helpers = import ./helpers.nix { inherit config lib; };
-
-
flushNat = ''
-
${helpers}
-
ip46tables -w -t nat -D PREROUTING -j nixos-nat-pre 2>/dev/null|| true
-
ip46tables -w -t nat -F nixos-nat-pre 2>/dev/null || true
-
ip46tables -w -t nat -X nixos-nat-pre 2>/dev/null || true
-
ip46tables -w -t nat -D POSTROUTING -j nixos-nat-post 2>/dev/null || true
-
ip46tables -w -t nat -F nixos-nat-post 2>/dev/null || true
-
ip46tables -w -t nat -X nixos-nat-post 2>/dev/null || true
-
ip46tables -w -t nat -D OUTPUT -j nixos-nat-out 2>/dev/null || true
-
ip46tables -w -t nat -F nixos-nat-out 2>/dev/null || true
-
ip46tables -w -t nat -X nixos-nat-out 2>/dev/null || true
-
-
${cfg.extraStopCommands}
-
'';
-
-
mkSetupNat = { iptables, dest, internalIPs, forwardPorts }: ''
-
# We can't match on incoming interface in POSTROUTING, so
-
# mark packets coming from the internal interfaces.
-
${concatMapStrings (iface: ''
-
${iptables} -w -t nat -A nixos-nat-pre \
-
-i '${iface}' -j MARK --set-mark 1
-
'') cfg.internalInterfaces}
-
-
# NAT the marked packets.
-
${optionalString (cfg.internalInterfaces != []) ''
-
${iptables} -w -t nat -A nixos-nat-post -m mark --mark 1 \
-
${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
-
''}
-
-
# NAT packets coming from the internal IPs.
-
${concatMapStrings (range: ''
-
${iptables} -w -t nat -A nixos-nat-post \
-
-s '${range}' ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
-
'') internalIPs}
-
-
# NAT from external ports to internal ports.
-
${concatMapStrings (fwd: ''
-
${iptables} -w -t nat -A nixos-nat-pre \
-
-i ${toString cfg.externalInterface} -p ${fwd.proto} \
-
--dport ${builtins.toString fwd.sourcePort} \
-
-j DNAT --to-destination ${fwd.destination}
-
-
${concatMapStrings (loopbackip:
-
let
-
matchIP = if isIPv6 fwd.destination then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)";
-
m = builtins.match "${matchIP}:([0-9-]+)" fwd.destination;
-
destinationIP = if m == null then throw "bad ip:ports `${fwd.destination}'" else elemAt m 0;
-
destinationPorts = if m == null then throw "bad ip:ports `${fwd.destination}'" else builtins.replaceStrings ["-"] [":"] (elemAt m 1);
-
in ''
-
# Allow connections to ${loopbackip}:${toString fwd.sourcePort} from the host itself
-
${iptables} -w -t nat -A nixos-nat-out \
-
-d ${loopbackip} -p ${fwd.proto} \
-
--dport ${builtins.toString fwd.sourcePort} \
-
-j DNAT --to-destination ${fwd.destination}
-
-
# Allow connections to ${loopbackip}:${toString fwd.sourcePort} from other hosts behind NAT
-
${iptables} -w -t nat -A nixos-nat-pre \
-
-d ${loopbackip} -p ${fwd.proto} \
-
--dport ${builtins.toString fwd.sourcePort} \
-
-j DNAT --to-destination ${fwd.destination}
-
-
${iptables} -w -t nat -A nixos-nat-post \
-
-d ${destinationIP} -p ${fwd.proto} \
-
--dport ${destinationPorts} \
-
-j SNAT --to-source ${loopbackip}
-
'') fwd.loopbackIPs}
-
'') forwardPorts}
-
'';
-
-
setupNat = ''
-
${helpers}
-
# Create subchains where we store rules
-
ip46tables -w -t nat -N nixos-nat-pre
-
ip46tables -w -t nat -N nixos-nat-post
-
ip46tables -w -t nat -N nixos-nat-out
-
-
${mkSetupNat {
-
iptables = "iptables";
-
inherit dest;
-
inherit (cfg) internalIPs;
-
forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts;
-
}}
-
-
${optionalString cfg.enableIPv6 (mkSetupNat {
-
iptables = "ip6tables";
-
dest = destIPv6;
-
internalIPs = cfg.internalIPv6s;
-
forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
-
})}
-
-
${optionalString (cfg.dmzHost != null) ''
-
iptables -w -t nat -A nixos-nat-pre \
-
-i ${toString cfg.externalInterface} -j DNAT \
-
--to-destination ${cfg.dmzHost}
-
''}
-
-
${cfg.extraCommands}
-
-
# Append our chains to the nat tables
-
ip46tables -w -t nat -A PREROUTING -j nixos-nat-pre
-
ip46tables -w -t nat -A POSTROUTING -j nixos-nat-post
-
ip46tables -w -t nat -A OUTPUT -j nixos-nat-out
-
'';
+
cfg = config.networking.nat;
in
{
-
###### interface
-
options = {
networking.nat.enable = mkOption {
type = types.bool;
default = false;
-
description =
-
lib.mdDoc ''
-
Whether to enable Network Address Translation (NAT).
-
'';
+
description = lib.mdDoc ''
+
Whether to enable Network Address Translation (NAT).
+
'';
};
networking.nat.enableIPv6 = mkOption {
type = types.bool;
default = false;
-
description =
-
lib.mdDoc ''
-
Whether to enable IPv6 NAT.
-
'';
+
description = lib.mdDoc ''
+
Whether to enable IPv6 NAT.
+
'';
};
networking.nat.internalInterfaces = mkOption {
type = types.listOf types.str;
-
default = [];
+
default = [ ];
example = [ "eth0" ];
-
description =
-
lib.mdDoc ''
-
The interfaces for which to perform NAT. Packets coming from
-
these interface and destined for the external interface will
-
be rewritten.
-
'';
+
description = lib.mdDoc ''
+
The interfaces for which to perform NAT. Packets coming from
+
these interface and destined for the external interface will
+
be rewritten.
+
'';
};
networking.nat.internalIPs = mkOption {
type = types.listOf types.str;
-
default = [];
+
default = [ ];
example = [ "192.168.1.0/24" ];
-
description =
-
lib.mdDoc ''
-
The IP address ranges for which to perform NAT. Packets
-
coming from these addresses (on any interface) and destined
-
for the external interface will be rewritten.
-
'';
+
description = lib.mdDoc ''
+
The IP address ranges for which to perform NAT. Packets
+
coming from these addresses (on any interface) and destined
+
for the external interface will be rewritten.
+
'';
};
networking.nat.internalIPv6s = mkOption {
type = types.listOf types.str;
-
default = [];
+
default = [ ];
example = [ "fc00::/64" ];
-
description =
-
lib.mdDoc ''
-
The IPv6 address ranges for which to perform NAT. Packets
-
coming from these addresses (on any interface) and destined
-
for the external interface will be rewritten.
-
'';
+
description = lib.mdDoc ''
+
The IPv6 address ranges for which to perform NAT. Packets
+
coming from these addresses (on any interface) and destined
+
for the external interface will be rewritten.
+
'';
};
networking.nat.externalInterface = mkOption {
type = types.nullOr types.str;
default = null;
example = "eth1";
-
description =
-
lib.mdDoc ''
-
The name of the external network interface.
-
'';
+
description = lib.mdDoc ''
+
The name of the external network interface.
+
'';
};
networking.nat.externalIP = mkOption {
type = types.nullOr types.str;
default = null;
example = "203.0.113.123";
-
description =
-
lib.mdDoc ''
-
The public IP address to which packets from the local
-
network are to be rewritten. If this is left empty, the
-
IP address associated with the external interface will be
-
used.
-
'';
+
description = lib.mdDoc ''
+
The public IP address to which packets from the local
+
network are to be rewritten. If this is left empty, the
+
IP address associated with the external interface will be
+
used.
+
'';
};
networking.nat.externalIPv6 = mkOption {
type = types.nullOr types.str;
default = null;
example = "2001:dc0:2001:11::175";
-
description =
-
lib.mdDoc ''
-
The public IPv6 address to which packets from the local
-
network are to be rewritten. If this is left empty, the
-
IP address associated with the external interface will be
-
used.
-
'';
+
description = lib.mdDoc ''
+
The public IPv6 address to which packets from the local
+
network are to be rewritten. If this is left empty, the
+
IP address associated with the external interface will be
+
used.
+
'';
};
networking.nat.forwardPorts = mkOption {
···
loopbackIPs = mkOption {
type = types.listOf types.str;
-
default = [];
+
default = [ ];
example = literalExpression ''[ "55.1.2.3" ]'';
description = lib.mdDoc "Public IPs for NAT reflection; for connections to `loopbackip:sourcePort' from the host itself and from other hosts behind NAT";
};
};
});
-
default = [];
+
default = [ ];
example = [
{ sourcePort = 8080; destination = "10.0.0.1:80"; proto = "tcp"; }
{ sourcePort = 8080; destination = "[fc00::2]:80"; proto = "tcp"; }
];
-
description =
-
lib.mdDoc ''
-
List of forwarded ports from the external interface to
-
internal destinations by using DNAT. Destination can be
-
IPv6 if IPv6 NAT is enabled.
-
'';
+
description = lib.mdDoc ''
+
List of forwarded ports from the external interface to
+
internal destinations by using DNAT. Destination can be
+
IPv6 if IPv6 NAT is enabled.
+
'';
};
networking.nat.dmzHost = mkOption {
type = types.nullOr types.str;
default = null;
example = "10.0.0.1";
-
description =
-
lib.mdDoc ''
-
The local IP address to which all traffic that does not match any
-
forwarding rule is forwarded.
-
'';
-
};
-
-
networking.nat.extraCommands = mkOption {
-
type = types.lines;
-
default = "";
-
example = "iptables -A INPUT -p icmp -j ACCEPT";
-
description =
-
lib.mdDoc ''
-
Additional shell commands executed as part of the nat
-
initialisation script.
-
'';
-
};
-
-
networking.nat.extraStopCommands = mkOption {
-
type = types.lines;
-
default = "";
-
example = "iptables -D INPUT -p icmp -j ACCEPT || true";
-
description =
-
lib.mdDoc ''
-
Additional shell commands executed as part of the nat
-
teardown script.
-
'';
+
description = lib.mdDoc ''
+
The local IP address to which all traffic that does not match any
+
forwarding rule is forwarded.
+
'';
};
};
-
###### implementation
-
-
config = mkMerge [
-
{ networking.firewall.extraCommands = mkBefore flushNat; }
-
(mkIf config.networking.nat.enable {
-
-
assertions = [
-
{ assertion = cfg.enableIPv6 -> config.networking.enableIPv6;
-
message = "networking.nat.enableIPv6 requires networking.enableIPv6";
-
}
-
{ assertion = (cfg.dmzHost != null) -> (cfg.externalInterface != null);
-
message = "networking.nat.dmzHost requires networking.nat.externalInterface";
-
}
-
{ assertion = (cfg.forwardPorts != []) -> (cfg.externalInterface != null);
-
message = "networking.nat.forwardPorts requires networking.nat.externalInterface";
-
}
-
];
+
config = mkIf config.networking.nat.enable {
-
# Use the same iptables package as in config.networking.firewall.
-
# When the firewall is enabled, this should be deduplicated without any
-
# error.
-
environment.systemPackages = [ config.networking.firewall.package ];
+
assertions = [
+
{
+
assertion = cfg.enableIPv6 -> config.networking.enableIPv6;
+
message = "networking.nat.enableIPv6 requires networking.enableIPv6";
+
}
+
{
+
assertion = (cfg.dmzHost != null) -> (cfg.externalInterface != null);
+
message = "networking.nat.dmzHost requires networking.nat.externalInterface";
+
}
+
{
+
assertion = (cfg.forwardPorts != [ ]) -> (cfg.externalInterface != null);
+
message = "networking.nat.forwardPorts requires networking.nat.externalInterface";
+
}
+
];
-
boot = {
-
kernelModules = [ "nf_nat_ftp" ];
-
kernel.sysctl = {
-
"net.ipv4.conf.all.forwarding" = mkOverride 99 true;
-
"net.ipv4.conf.default.forwarding" = mkOverride 99 true;
-
} // optionalAttrs cfg.enableIPv6 {
-
# Do not prevent IPv6 autoconfiguration.
-
# See <http://strugglers.net/~andy/blog/2011/09/04/linux-ipv6-router-advertisements-and-forwarding/>.
-
"net.ipv6.conf.all.accept_ra" = mkOverride 99 2;
-
"net.ipv6.conf.default.accept_ra" = mkOverride 99 2;
+
# Use the same iptables package as in config.networking.firewall.
+
# When the firewall is enabled, this should be deduplicated without any
+
# error.
+
environment.systemPackages = [ config.networking.firewall.package ];
-
# Forward IPv6 packets.
-
"net.ipv6.conf.all.forwarding" = mkOverride 99 true;
-
"net.ipv6.conf.default.forwarding" = mkOverride 99 true;
-
};
-
};
+
boot = {
+
kernelModules = [ "nf_nat_ftp" ];
+
kernel.sysctl = {
+
"net.ipv4.conf.all.forwarding" = mkOverride 99 true;
+
"net.ipv4.conf.default.forwarding" = mkOverride 99 true;
+
} // optionalAttrs cfg.enableIPv6 {
+
# Do not prevent IPv6 autoconfiguration.
+
# See <http://strugglers.net/~andy/blog/2011/09/04/linux-ipv6-router-advertisements-and-forwarding/>.
+
"net.ipv6.conf.all.accept_ra" = mkOverride 99 2;
+
"net.ipv6.conf.default.accept_ra" = mkOverride 99 2;
-
networking.firewall = mkIf config.networking.firewall.enable {
-
extraCommands = setupNat;
-
extraStopCommands = flushNat;
+
# Forward IPv6 packets.
+
"net.ipv6.conf.all.forwarding" = mkOverride 99 true;
+
"net.ipv6.conf.default.forwarding" = mkOverride 99 true;
};
+
};
-
systemd.services = mkIf (!config.networking.firewall.enable) { nat = {
-
description = "Network Address Translation";
-
wantedBy = [ "network.target" ];
-
after = [ "network-pre.target" "systemd-modules-load.service" ];
-
path = [ config.networking.firewall.package ];
-
unitConfig.ConditionCapability = "CAP_NET_ADMIN";
-
-
serviceConfig = {
-
Type = "oneshot";
-
RemainAfterExit = true;
-
};
-
-
script = flushNat + setupNat;
-
-
postStop = flushNat;
-
}; };
-
})
-
];
+
};
}
+10 -16
nixos/modules/services/networking/nftables.nix
···
default = false;
description =
lib.mdDoc ''
-
Whether to enable nftables. nftables is a Linux-based packet
-
filtering framework intended to replace frameworks like iptables.
-
-
This conflicts with the standard networking firewall, so make sure to
-
disable it before using nftables.
+
Whether to enable nftables and use nftables based firewall if enabled.
+
nftables is a Linux-based packet filtering framework intended to
+
replace frameworks like iptables.
Note that if you have Docker enabled you will not be able to use
nftables without intervention. Docker uses iptables internally to
···
lib.mdDoc ''
The ruleset to be used with nftables. Should be in a format that
can be loaded using "/bin/nft -f". The ruleset is updated atomically.
+
This option conflicts with rulesetFile.
'';
};
networking.nftables.rulesetFile = mkOption {
-
type = types.path;
-
default = pkgs.writeTextFile {
-
name = "nftables-rules";
-
text = cfg.ruleset;
-
};
-
defaultText = literalMD ''a file with the contents of {option}`networking.nftables.ruleset`'';
+
type = types.nullOr types.path;
+
default = null;
description =
lib.mdDoc ''
The ruleset file to be used with nftables. Should be in a format that
can be loaded using "nft -f". The ruleset is updated atomically.
+
This option conflicts with ruleset and nftables based firewall.
'';
};
};
···
###### implementation
config = mkIf cfg.enable {
-
assertions = [{
-
assertion = config.networking.firewall.enable == false;
-
message = "You can not use nftables and iptables at the same time. networking.firewall.enable must be set to false.";
-
}];
boot.blacklistedKernelModules = [ "ip_tables" ];
environment.systemPackages = [ pkgs.nftables ];
networking.networkmanager.firewallBackend = mkDefault "nftables";
···
rulesScript = pkgs.writeScript "nftables-rules" ''
#! ${pkgs.nftables}/bin/nft -f
flush ruleset
-
include "${cfg.rulesetFile}"
+
${if cfg.rulesetFile != null then ''
+
include "${cfg.rulesetFile}"
+
'' else cfg.ruleset}
'';
in {
Type = "oneshot";
+5 -1
nixos/tests/all-tests.nix
···
firefox-esr = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr; }; # used in `tested` job
firefox-esr-102 = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr-102; };
firejail = handleTest ./firejail.nix {};
-
firewall = handleTest ./firewall.nix {};
+
firewall = handleTest ./firewall.nix { nftables = false; };
+
firewall-nftables = handleTest ./firewall.nix { nftables = true; };
fish = handleTest ./fish.nix {};
flannel = handleTestOn ["x86_64-linux"] ./flannel.nix {};
fluentd = handleTest ./fluentd.nix {};
···
nat.firewall = handleTest ./nat.nix { withFirewall = true; };
nat.firewall-conntrack = handleTest ./nat.nix { withFirewall = true; withConntrackHelpers = true; };
nat.standalone = handleTest ./nat.nix { withFirewall = false; };
+
nat.nftables.firewall = handleTest ./nat.nix { withFirewall = true; nftables = true; };
+
nat.nftables.firewall-conntrack = handleTest ./nat.nix { withFirewall = true; withConntrackHelpers = true; nftables = true; };
+
nat.nftables.standalone = handleTest ./nat.nix { withFirewall = false; nftables = true; };
nats = handleTest ./nats.nix {};
navidrome = handleTest ./navidrome.nix {};
nbd = handleTest ./nbd.nix {};
+8 -5
nixos/tests/firewall.nix
···
# Test the firewall module.
-
import ./make-test-python.nix ( { pkgs, ... } : {
-
name = "firewall";
+
import ./make-test-python.nix ( { pkgs, nftables, ... } : {
+
name = "firewall" + pkgs.lib.optionalString nftables "-nftables";
meta = with pkgs.lib.maintainers; {
maintainers = [ eelco ];
};
···
{ ... }:
{ networking.firewall.enable = true;
networking.firewall.logRefusedPackets = true;
+
networking.nftables.enable = nftables;
services.httpd.enable = true;
services.httpd.adminAddr = "foo@example.org";
};
···
{ ... }:
{ networking.firewall.enable = true;
networking.firewall.rejectPackets = true;
+
networking.nftables.enable = nftables;
};
attacker =
···
testScript = { nodes, ... }: let
newSystem = nodes.walled2.config.system.build.toplevel;
+
unit = if nftables then "nftables" else "firewall";
in ''
start_all()
-
walled.wait_for_unit("firewall")
+
walled.wait_for_unit("${unit}")
walled.wait_for_unit("httpd")
attacker.wait_for_unit("network.target")
···
walled.succeed("ping -c 1 attacker >&2")
# If we stop the firewall, then connections should succeed.
-
walled.stop_job("firewall")
+
walled.stop_job("${unit}")
attacker.succeed("curl -v http://walled/ >&2")
# Check whether activation of a new configuration reloads the firewall.
walled.succeed(
-
"${newSystem}/bin/switch-to-configuration test 2>&1 | grep -qF firewall.service"
+
"${newSystem}/bin/switch-to-configuration test 2>&1 | grep -qF ${unit}.service"
)
'';
})
+8 -4
nixos/tests/nat.nix
···
# client on the inside network, a server on the outside network, and a
# router connected to both that performs Network Address Translation
# for the client.
-
import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ? false, ... }:
+
import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ? false, nftables ? false, ... }:
let
-
unit = if withFirewall then "firewall" else "nat";
+
unit = if nftables then "nftables" else (if withFirewall then "firewall" else "nat");
routerBase =
lib.mkMerge [
{ virtualisation.vlans = [ 2 1 ];
networking.firewall.enable = withFirewall;
+
networking.firewall.filterForward = nftables;
+
networking.nftables.enable = nftables;
networking.nat.internalIPs = [ "192.168.1.0/24" ];
networking.nat.externalInterface = "eth1";
}
···
];
in
{
-
name = "nat" + (if withFirewall then "WithFirewall" else "Standalone")
+
name = "nat" + (lib.optionalString nftables "Nftables")
+
+ (if withFirewall then "WithFirewall" else "Standalone")
+ (lib.optionalString withConntrackHelpers "withConntrackHelpers");
meta = with pkgs.lib.maintainers; {
maintainers = [ eelco rob ];
···
{ virtualisation.vlans = [ 1 ];
networking.defaultGateway =
(pkgs.lib.head nodes.router.config.networking.interfaces.eth2.ipv4.addresses).address;
+
networking.nftables.enable = nftables;
}
(lib.optionalAttrs withConntrackHelpers {
networking.firewall.connectionTrackingModules = [ "ftp" ];
···
# FIXME: this should not be necessary, but nat.service is not started because
# network.target is not triggered
# (https://github.com/NixOS/nixpkgs/issues/16230#issuecomment-226408359)
-
${lib.optionalString (!withFirewall) ''
+
${lib.optionalString (!withFirewall && !nftables) ''
router.succeed("systemctl start nat.service")
''}
client.succeed("curl --fail http://server/ >&2")