diff --git a/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
index 657b5c6f26d8..7a4ebc389c28 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
@@ -303,6 +303,13 @@
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
diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md
index 27bd64e514f1..761e11831c7d 100644
--- a/nixos/doc/manual/release-notes/rl-2305.section.md
+++ b/nixos/doc/manual/release-notes/rl-2305.section.md
@@ -86,6 +86,8 @@ In addition to numerous new and upgraded packages, this release has the followin
- 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.
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index b099dcc71c40..7c25b0156a2f 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -821,6 +821,8 @@
./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
@@ -891,6 +893,8 @@
./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
diff --git a/nixos/modules/services/audio/roon-bridge.nix b/nixos/modules/services/audio/roon-bridge.nix
index db84ba286221..e9335091ba9a 100644
--- a/nixos/modules/services/audio/roon-bridge.nix
+++ b/nixos/modules/services/audio/roon-bridge.nix
@@ -53,13 +53,18 @@ in {
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
+ '';
};
diff --git a/nixos/modules/services/audio/roon-server.nix b/nixos/modules/services/audio/roon-server.nix
index 74cae909f5db..fbe74f63b9da 100644
--- a/nixos/modules/services/audio/roon-server.nix
+++ b/nixos/modules/services/audio/roon-server.nix
@@ -58,7 +58,7 @@ in {
{ 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
@@ -66,6 +66,11 @@ in {
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
+ '';
};
diff --git a/nixos/modules/services/networking/firewall-iptables.nix b/nixos/modules/services/networking/firewall-iptables.nix
new file mode 100644
index 000000000000..63e952194d67
--- /dev/null
+++ b/nixos/modules/services/networking/firewall-iptables.nix
@@ -0,0 +1,334 @@
+/* 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";
+ };
+ };
+
+ };
+
+}
diff --git a/nixos/modules/services/networking/firewall-nftables.nix b/nixos/modules/services/networking/firewall-nftables.nix
new file mode 100644
index 000000000000..0ed3c228075d
--- /dev/null
+++ b/nixos/modules/services/networking/firewall-nftables.nix
@@ -0,0 +1,167 @@
+{ 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}
+
+ }
+ ''}
+
+ }
+
+ '';
+
+ };
+
+}
diff --git a/nixos/modules/services/networking/firewall.nix b/nixos/modules/services/networking/firewall.nix
index 27119dcc57c5..4e332d489e4d 100644
--- a/nixos/modules/services/networking/firewall.nix
+++ b/nixos/modules/services/networking/firewall.nix
@@ -1,35 +1,3 @@
-/* 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;
@@ -38,216 +6,6 @@ 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}";
-
- 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);
@@ -257,22 +15,20 @@ let
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 {
@@ -280,20 +36,18 @@ let
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.
+ '';
};
};
@@ -301,240 +55,222 @@ in
{
- ###### 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
+ description = lib.mdDoc ''
+ Whether to auto-load connection-tracking helpers.
+ See the description at networking.firewall.connectionTrackingModules
- (needs kernel 3.5+)
- '';
- };
-
- 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.
- '';
- };
-
- 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.
- '';
+ description = lib.mdDoc ''
+ Additional packages to be included in the environment of the system
+ as well as the path of networking.firewall.extraCommands.
+ '';
};
interfaces = mkOption {
default = { };
- type = with types; attrsOf (submodule [ { options = commonOptions; } ]);
- description =
- lib.mdDoc ''
- Interface-specific open ports.
- '';
+ 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;
@@ -545,40 +281,6 @@ in
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";
- };
- };
-
};
}
diff --git a/nixos/modules/services/networking/nat-iptables.nix b/nixos/modules/services/networking/nat-iptables.nix
new file mode 100644
index 000000000000..d1bed401feeb
--- /dev/null
+++ b/nixos/modules/services/networking/nat-iptables.nix
@@ -0,0 +1,191 @@
+# 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;
+ };
+ };
+ })
+ ]);
+}
diff --git a/nixos/modules/services/networking/nat-nftables.nix b/nixos/modules/services/networking/nat-nftables.nix
new file mode 100644
index 000000000000..483910a16658
--- /dev/null
+++ b/nixos/modules/services/networking/nat-nftables.nix
@@ -0,0 +1,184 @@
+{ 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"
+ ''}
+ '';
+
+ };
+}
diff --git a/nixos/modules/services/networking/nat.nix b/nixos/modules/services/networking/nat.nix
index 0b70ae47ccf5..a6f403b46f87 100644
--- a/nixos/modules/services/networking/nat.nix
+++ b/nixos/modules/services/networking/nat.nix
@@ -7,219 +7,95 @@
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
{
- ###### 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 {
@@ -246,122 +122,75 @@ in
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 = mkIf config.networking.nat.enable {
- 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";
+ }
+ ];
- 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";
- }
- ];
+ # 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 ];
- # 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 ];
+ 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 .
+ "net.ipv6.conf.all.accept_ra" = mkOverride 99 2;
+ "net.ipv6.conf.default.accept_ra" = mkOverride 99 2;
- 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 .
- "net.ipv6.conf.all.accept_ra" = mkOverride 99 2;
- "net.ipv6.conf.default.accept_ra" = mkOverride 99 2;
-
- # Forward IPv6 packets.
- "net.ipv6.conf.all.forwarding" = mkOverride 99 true;
- "net.ipv6.conf.default.forwarding" = mkOverride 99 true;
- };
+ # Forward IPv6 packets.
+ "net.ipv6.conf.all.forwarding" = mkOverride 99 true;
+ "net.ipv6.conf.default.forwarding" = mkOverride 99 true;
};
+ };
- 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;
- }; };
- })
- ];
+ };
}
diff --git a/nixos/modules/services/networking/nftables.nix b/nixos/modules/services/networking/nftables.nix
index 8166a8e7110b..bd13e8c9929a 100644
--- a/nixos/modules/services/networking/nftables.nix
+++ b/nixos/modules/services/networking/nftables.nix
@@ -12,11 +12,9 @@ in
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
@@ -79,19 +77,17 @@ in
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.
'';
};
};
@@ -99,10 +95,6 @@ in
###### 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";
@@ -116,7 +108,9 @@ in
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";
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 30bcfcf6111a..446de6898ce0 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -211,7 +211,8 @@ in {
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 {};
@@ -412,6 +413,9 @@ in {
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 {};
diff --git a/nixos/tests/firewall.nix b/nixos/tests/firewall.nix
index 5c434c1cb6d6..dd7551f143a5 100644
--- a/nixos/tests/firewall.nix
+++ b/nixos/tests/firewall.nix
@@ -1,7 +1,7 @@
# 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 ];
};
@@ -11,6 +11,7 @@ import ./make-test-python.nix ( { pkgs, ... } : {
{ ... }:
{ networking.firewall.enable = true;
networking.firewall.logRefusedPackets = true;
+ networking.nftables.enable = nftables;
services.httpd.enable = true;
services.httpd.adminAddr = "foo@example.org";
};
@@ -23,6 +24,7 @@ import ./make-test-python.nix ( { pkgs, ... } : {
{ ... }:
{ networking.firewall.enable = true;
networking.firewall.rejectPackets = true;
+ networking.nftables.enable = nftables;
};
attacker =
@@ -35,10 +37,11 @@ import ./make-test-python.nix ( { pkgs, ... } : {
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")
@@ -54,12 +57,12 @@ import ./make-test-python.nix ( { pkgs, ... } : {
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"
)
'';
})
diff --git a/nixos/tests/nat.nix b/nixos/tests/nat.nix
index 545eb46f2bf5..912a04deae8b 100644
--- a/nixos/tests/nat.nix
+++ b/nixos/tests/nat.nix
@@ -3,14 +3,16 @@
# 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";
}
@@ -21,7 +23,8 @@ import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ?
];
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 ];
@@ -34,6 +37,7 @@ import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ?
{ 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" ];
@@ -111,7 +115,7 @@ import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ?
# 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")