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")