lol

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

Rvfg a43c7b2a 2379de68

+1158 -723
+7
nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
··· 305 305 </listitem> 306 306 <listitem> 307 307 <para> 308 + The <literal>firewall</literal> and <literal>nat</literal> 309 + module now has a nftables based implementation. Enable 310 + <literal>networking.nftables</literal> to use it. 311 + </para> 312 + </listitem> 313 + <listitem> 314 + <para> 308 315 The <literal>services.fwupd</literal> module now allows 309 316 arbitrary daemon settings to be configured in a structured 310 317 manner
+2
nixos/doc/manual/release-notes/rl-2305.section.md
··· 86 86 87 87 - Resilio sync secret keys can now be provided using a secrets file at runtime, preventing these secrets from ending up in the Nix store. 88 88 89 + - The `firewall` and `nat` module now has a nftables based implementation. Enable `networking.nftables` to use it. 90 + 89 91 - The `services.fwupd` module now allows arbitrary daemon settings to be configured in a structured manner ([`services.fwupd.daemonSettings`](#opt-services.fwupd.daemonSettings)). 90 92 91 93 - The `unifi-poller` package and corresponding NixOS module have been renamed to `unpoller` to match upstream.
+4
nixos/modules/module-list.nix
··· 821 821 ./services/networking/firefox-syncserver.nix 822 822 ./services/networking/fireqos.nix 823 823 ./services/networking/firewall.nix 824 + ./services/networking/firewall-iptables.nix 825 + ./services/networking/firewall-nftables.nix 824 826 ./services/networking/flannel.nix 825 827 ./services/networking/freenet.nix 826 828 ./services/networking/freeradius.nix ··· 891 893 ./services/networking/namecoind.nix 892 894 ./services/networking/nar-serve.nix 893 895 ./services/networking/nat.nix 896 + ./services/networking/nat-iptables.nix 897 + ./services/networking/nat-nftables.nix 894 898 ./services/networking/nats.nix 895 899 ./services/networking/nbd.nix 896 900 ./services/networking/ncdns.nix
+6 -1
nixos/modules/services/audio/roon-bridge.nix
··· 53 53 networking.firewall = mkIf cfg.openFirewall { 54 54 allowedTCPPortRanges = [{ from = 9100; to = 9200; }]; 55 55 allowedUDPPorts = [ 9003 ]; 56 - extraCommands = '' 56 + extraCommands = optionalString (!config.networking.nftables.enable) '' 57 57 iptables -A INPUT -s 224.0.0.0/4 -j ACCEPT 58 58 iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT 59 59 iptables -A INPUT -s 240.0.0.0/5 -j ACCEPT 60 60 iptables -A INPUT -m pkttype --pkt-type multicast -j ACCEPT 61 61 iptables -A INPUT -m pkttype --pkt-type broadcast -j ACCEPT 62 + ''; 63 + extraInputRules = optionalString config.networking.nftables.enable '' 64 + ip saddr { 224.0.0.0/4, 240.0.0.0/5 } accept 65 + ip daddr 224.0.0.0/4 accept 66 + pkttype { multicast, broadcast } accept 62 67 ''; 63 68 }; 64 69
+6 -1
nixos/modules/services/audio/roon-server.nix
··· 58 58 { from = 30000; to = 30010; } 59 59 ]; 60 60 allowedUDPPorts = [ 9003 ]; 61 - extraCommands = '' 61 + extraCommands = optionalString (!config.networking.nftables.enable) '' 62 62 ## IGMP / Broadcast ## 63 63 iptables -A INPUT -s 224.0.0.0/4 -j ACCEPT 64 64 iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT 65 65 iptables -A INPUT -s 240.0.0.0/5 -j ACCEPT 66 66 iptables -A INPUT -m pkttype --pkt-type multicast -j ACCEPT 67 67 iptables -A INPUT -m pkttype --pkt-type broadcast -j ACCEPT 68 + ''; 69 + extraInputRules = optionalString config.networking.nftables.enable '' 70 + ip saddr { 224.0.0.0/4, 240.0.0.0/5 } accept 71 + ip daddr 224.0.0.0/4 accept 72 + pkttype { multicast, broadcast } accept 68 73 ''; 69 74 }; 70 75
+334
nixos/modules/services/networking/firewall-iptables.nix
··· 1 + /* This module enables a simple firewall. 2 + 3 + The firewall can be customised in arbitrary ways by setting 4 + ‘networking.firewall.extraCommands’. For modularity, the firewall 5 + uses several chains: 6 + 7 + - ‘nixos-fw’ is the main chain for input packet processing. 8 + 9 + - ‘nixos-fw-accept’ is called for accepted packets. If you want 10 + additional logging, or want to reject certain packets anyway, you 11 + can insert rules at the start of this chain. 12 + 13 + - ‘nixos-fw-log-refuse’ and ‘nixos-fw-refuse’ are called for 14 + refused packets. (The former jumps to the latter after logging 15 + the packet.) If you want additional logging, or want to accept 16 + certain packets anyway, you can insert rules at the start of 17 + this chain. 18 + 19 + - ‘nixos-fw-rpfilter’ is used as the main chain in the mangle table, 20 + called from the built-in ‘PREROUTING’ chain. If the kernel 21 + supports it and `cfg.checkReversePath` is set this chain will 22 + perform a reverse path filter test. 23 + 24 + - ‘nixos-drop’ is used while reloading the firewall in order to drop 25 + all traffic. Since reloading isn't implemented in an atomic way 26 + this'll prevent any traffic from leaking through while reloading 27 + the firewall. However, if the reloading fails, the ‘firewall-stop’ 28 + script will be called which in return will effectively disable the 29 + complete firewall (in the default configuration). 30 + 31 + */ 32 + 33 + { config, lib, pkgs, ... }: 34 + 35 + with lib; 36 + 37 + let 38 + 39 + cfg = config.networking.firewall; 40 + 41 + inherit (config.boot.kernelPackages) kernel; 42 + 43 + kernelHasRPFilter = ((kernel.config.isEnabled or (x: false)) "IP_NF_MATCH_RPFILTER") || (kernel.features.netfilterRPFilter or false); 44 + 45 + helpers = import ./helpers.nix { inherit config lib; }; 46 + 47 + writeShScript = name: text: 48 + let 49 + dir = pkgs.writeScriptBin name '' 50 + #! ${pkgs.runtimeShell} -e 51 + ${text} 52 + ''; 53 + in 54 + "${dir}/bin/${name}"; 55 + 56 + startScript = writeShScript "firewall-start" '' 57 + ${helpers} 58 + 59 + # Flush the old firewall rules. !!! Ideally, updating the 60 + # firewall would be atomic. Apparently that's possible 61 + # with iptables-restore. 62 + ip46tables -D INPUT -j nixos-fw 2> /dev/null || true 63 + for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse; do 64 + ip46tables -F "$chain" 2> /dev/null || true 65 + ip46tables -X "$chain" 2> /dev/null || true 66 + done 67 + 68 + 69 + # The "nixos-fw-accept" chain just accepts packets. 70 + ip46tables -N nixos-fw-accept 71 + ip46tables -A nixos-fw-accept -j ACCEPT 72 + 73 + 74 + # The "nixos-fw-refuse" chain rejects or drops packets. 75 + ip46tables -N nixos-fw-refuse 76 + 77 + ${if cfg.rejectPackets then '' 78 + # Send a reset for existing TCP connections that we've 79 + # somehow forgotten about. Send ICMP "port unreachable" 80 + # for everything else. 81 + ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset 82 + ip46tables -A nixos-fw-refuse -j REJECT 83 + '' else '' 84 + ip46tables -A nixos-fw-refuse -j DROP 85 + ''} 86 + 87 + 88 + # The "nixos-fw-log-refuse" chain performs logging, then 89 + # jumps to the "nixos-fw-refuse" chain. 90 + ip46tables -N nixos-fw-log-refuse 91 + 92 + ${optionalString cfg.logRefusedConnections '' 93 + ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "refused connection: " 94 + ''} 95 + ${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) '' 96 + ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \ 97 + -j LOG --log-level info --log-prefix "refused broadcast: " 98 + ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \ 99 + -j LOG --log-level info --log-prefix "refused multicast: " 100 + ''} 101 + ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse 102 + ${optionalString cfg.logRefusedPackets '' 103 + ip46tables -A nixos-fw-log-refuse \ 104 + -j LOG --log-level info --log-prefix "refused packet: " 105 + ''} 106 + ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse 107 + 108 + 109 + # The "nixos-fw" chain does the actual work. 110 + ip46tables -N nixos-fw 111 + 112 + # Clean up rpfilter rules 113 + ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true 114 + ip46tables -t mangle -F nixos-fw-rpfilter 2> /dev/null || true 115 + ip46tables -t mangle -X nixos-fw-rpfilter 2> /dev/null || true 116 + 117 + ${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) '' 118 + # Perform a reverse-path test to refuse spoofers 119 + # For now, we just drop, as the mangle table doesn't have a log-refuse yet 120 + ip46tables -t mangle -N nixos-fw-rpfilter 2> /dev/null || true 121 + ip46tables -t mangle -A nixos-fw-rpfilter -m rpfilter --validmark ${optionalString (cfg.checkReversePath == "loose") "--loose"} -j RETURN 122 + 123 + # Allows this host to act as a DHCP4 client without first having to use APIPA 124 + iptables -t mangle -A nixos-fw-rpfilter -p udp --sport 67 --dport 68 -j RETURN 125 + 126 + # Allows this host to act as a DHCPv4 server 127 + 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 128 + 129 + ${optionalString cfg.logReversePathDrops '' 130 + ip46tables -t mangle -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: " 131 + ''} 132 + ip46tables -t mangle -A nixos-fw-rpfilter -j DROP 133 + 134 + ip46tables -t mangle -A PREROUTING -j nixos-fw-rpfilter 135 + ''} 136 + 137 + # Accept all traffic on the trusted interfaces. 138 + ${flip concatMapStrings cfg.trustedInterfaces (iface: '' 139 + ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept 140 + '')} 141 + 142 + # Accept packets from established or related connections. 143 + ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept 144 + 145 + # Accept connections to the allowed TCP ports. 146 + ${concatStrings (mapAttrsToList (iface: cfg: 147 + concatMapStrings (port: 148 + '' 149 + ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"} 150 + '' 151 + ) cfg.allowedTCPPorts 152 + ) cfg.allInterfaces)} 153 + 154 + # Accept connections to the allowed TCP port ranges. 155 + ${concatStrings (mapAttrsToList (iface: cfg: 156 + concatMapStrings (rangeAttr: 157 + let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in 158 + '' 159 + ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"} 160 + '' 161 + ) cfg.allowedTCPPortRanges 162 + ) cfg.allInterfaces)} 163 + 164 + # Accept packets on the allowed UDP ports. 165 + ${concatStrings (mapAttrsToList (iface: cfg: 166 + concatMapStrings (port: 167 + '' 168 + ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"} 169 + '' 170 + ) cfg.allowedUDPPorts 171 + ) cfg.allInterfaces)} 172 + 173 + # Accept packets on the allowed UDP port ranges. 174 + ${concatStrings (mapAttrsToList (iface: cfg: 175 + concatMapStrings (rangeAttr: 176 + let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in 177 + '' 178 + ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"} 179 + '' 180 + ) cfg.allowedUDPPortRanges 181 + ) cfg.allInterfaces)} 182 + 183 + # Optionally respond to ICMPv4 pings. 184 + ${optionalString cfg.allowPing '' 185 + iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null) 186 + "-m limit ${cfg.pingLimit} " 187 + }-j nixos-fw-accept 188 + ''} 189 + 190 + ${optionalString config.networking.enableIPv6 '' 191 + # Accept all ICMPv6 messages except redirects and node 192 + # information queries (type 139). See RFC 4890, section 193 + # 4.4. 194 + ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP 195 + ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP 196 + ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept 197 + 198 + # Allow this host to act as a DHCPv6 client 199 + ip6tables -A nixos-fw -d fe80::/64 -p udp --dport 546 -j nixos-fw-accept 200 + ''} 201 + 202 + ${cfg.extraCommands} 203 + 204 + # Reject/drop everything else. 205 + ip46tables -A nixos-fw -j nixos-fw-log-refuse 206 + 207 + 208 + # Enable the firewall. 209 + ip46tables -A INPUT -j nixos-fw 210 + ''; 211 + 212 + stopScript = writeShScript "firewall-stop" '' 213 + ${helpers} 214 + 215 + # Clean up in case reload fails 216 + ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 217 + 218 + # Clean up after added ruleset 219 + ip46tables -D INPUT -j nixos-fw 2>/dev/null || true 220 + 221 + ${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) '' 222 + ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true 223 + ''} 224 + 225 + ${cfg.extraStopCommands} 226 + ''; 227 + 228 + reloadScript = writeShScript "firewall-reload" '' 229 + ${helpers} 230 + 231 + # Create a unique drop rule 232 + ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 233 + ip46tables -F nixos-drop 2>/dev/null || true 234 + ip46tables -X nixos-drop 2>/dev/null || true 235 + ip46tables -N nixos-drop 236 + ip46tables -A nixos-drop -j DROP 237 + 238 + # Don't allow traffic to leak out until the script has completed 239 + ip46tables -A INPUT -j nixos-drop 240 + 241 + ${cfg.extraStopCommands} 242 + 243 + if ${startScript}; then 244 + ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 245 + else 246 + echo "Failed to reload firewall... Stopping" 247 + ${stopScript} 248 + exit 1 249 + fi 250 + ''; 251 + 252 + in 253 + 254 + { 255 + 256 + options = { 257 + 258 + networking.firewall = { 259 + extraCommands = mkOption { 260 + type = types.lines; 261 + default = ""; 262 + example = "iptables -A INPUT -p icmp -j ACCEPT"; 263 + description = lib.mdDoc '' 264 + Additional shell commands executed as part of the firewall 265 + initialisation script. These are executed just before the 266 + final "reject" firewall rule is added, so they can be used 267 + to allow packets that would otherwise be refused. 268 + 269 + This option only works with the iptables based firewall. 270 + ''; 271 + }; 272 + 273 + extraStopCommands = mkOption { 274 + type = types.lines; 275 + default = ""; 276 + example = "iptables -P INPUT ACCEPT"; 277 + description = lib.mdDoc '' 278 + Additional shell commands executed as part of the firewall 279 + shutdown script. These are executed just after the removal 280 + of the NixOS input rule, or if the service enters a failed 281 + state. 282 + 283 + This option only works with the iptables based firewall. 284 + ''; 285 + }; 286 + }; 287 + 288 + }; 289 + 290 + # FIXME: Maybe if `enable' is false, the firewall should still be 291 + # built but not started by default? 292 + config = mkIf (cfg.enable && config.networking.nftables.enable == false) { 293 + 294 + assertions = [ 295 + # This is approximately "checkReversePath -> kernelHasRPFilter", 296 + # but the checkReversePath option can include non-boolean 297 + # values. 298 + { 299 + assertion = cfg.checkReversePath == false || kernelHasRPFilter; 300 + message = "This kernel does not support rpfilter"; 301 + } 302 + ]; 303 + 304 + networking.firewall.checkReversePath = mkIf (!kernelHasRPFilter) (mkDefault false); 305 + 306 + systemd.services.firewall = { 307 + description = "Firewall"; 308 + wantedBy = [ "sysinit.target" ]; 309 + wants = [ "network-pre.target" ]; 310 + before = [ "network-pre.target" ]; 311 + after = [ "systemd-modules-load.service" ]; 312 + 313 + path = [ cfg.package ] ++ cfg.extraPackages; 314 + 315 + # FIXME: this module may also try to load kernel modules, but 316 + # containers don't have CAP_SYS_MODULE. So the host system had 317 + # better have all necessary modules already loaded. 318 + unitConfig.ConditionCapability = "CAP_NET_ADMIN"; 319 + unitConfig.DefaultDependencies = false; 320 + 321 + reloadIfChanged = true; 322 + 323 + serviceConfig = { 324 + Type = "oneshot"; 325 + RemainAfterExit = true; 326 + ExecStart = "@${startScript} firewall-start"; 327 + ExecReload = "@${reloadScript} firewall-reload"; 328 + ExecStop = "@${stopScript} firewall-stop"; 329 + }; 330 + }; 331 + 332 + }; 333 + 334 + }
+167
nixos/modules/services/networking/firewall-nftables.nix
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + with lib; 4 + 5 + let 6 + 7 + cfg = config.networking.firewall; 8 + 9 + ifaceSet = concatStringsSep ", " ( 10 + map (x: ''"${x}"'') cfg.trustedInterfaces 11 + ); 12 + 13 + portsToNftSet = ports: portRanges: concatStringsSep ", " ( 14 + map (x: toString x) ports 15 + ++ map (x: "${toString x.from}-${toString x.to}") portRanges 16 + ); 17 + 18 + in 19 + 20 + { 21 + 22 + options = { 23 + 24 + networking.firewall = { 25 + extraInputRules = mkOption { 26 + type = types.lines; 27 + default = ""; 28 + example = "ip6 saddr { fc00::/7, fe80::/10 } tcp dport 24800 accept"; 29 + description = lib.mdDoc '' 30 + Additional nftables rules to be appended to the input-allow 31 + chain. 32 + 33 + This option only works with the nftables based firewall. 34 + ''; 35 + }; 36 + 37 + extraForwardRules = mkOption { 38 + type = types.lines; 39 + default = ""; 40 + example = "iifname wg0 accept"; 41 + description = lib.mdDoc '' 42 + Additional nftables rules to be appended to the forward-allow 43 + chain. 44 + 45 + This option only works with the nftables based firewall. 46 + ''; 47 + }; 48 + }; 49 + 50 + }; 51 + 52 + config = mkIf (cfg.enable && config.networking.nftables.enable) { 53 + 54 + assertions = [ 55 + { 56 + assertion = cfg.extraCommands == ""; 57 + message = "extraCommands is incompatible with the nftables based firewall: ${cfg.extraCommands}"; 58 + } 59 + { 60 + assertion = cfg.extraStopCommands == ""; 61 + message = "extraStopCommands is incompatible with the nftables based firewall: ${cfg.extraStopCommands}"; 62 + } 63 + { 64 + assertion = cfg.pingLimit == null || !(hasPrefix "--" cfg.pingLimit); 65 + message = "nftables syntax like \"2/second\" should be used in networking.firewall.pingLimit"; 66 + } 67 + { 68 + assertion = config.networking.nftables.rulesetFile == null; 69 + message = "networking.nftables.rulesetFile conflicts with the firewall"; 70 + } 71 + ]; 72 + 73 + networking.nftables.ruleset = '' 74 + 75 + table inet nixos-fw { 76 + 77 + ${optionalString (cfg.checkReversePath != false) '' 78 + chain rpfilter { 79 + type filter hook prerouting priority mangle + 10; policy drop; 80 + 81 + meta nfproto ipv4 udp sport . udp dport { 67 . 68, 68 . 67 } accept comment "DHCPv4 client/server" 82 + fib saddr . mark ${optionalString (cfg.checkReversePath != "loose") ". iif"} oif exists accept 83 + 84 + ${optionalString cfg.logReversePathDrops '' 85 + log level info prefix "rpfilter drop: " 86 + ''} 87 + 88 + } 89 + ''} 90 + 91 + chain input { 92 + type filter hook input priority filter; policy drop; 93 + 94 + ${optionalString (ifaceSet != "") ''iifname { ${ifaceSet} } accept comment "trusted interfaces"''} 95 + 96 + # Some ICMPv6 types like NDP is untracked 97 + ct state vmap { invalid : drop, established : accept, related : accept, * : jump input-allow } comment "*: new and untracked" 98 + 99 + ${optionalString cfg.logRefusedConnections '' 100 + tcp flags syn / fin,syn,rst,ack log level info prefix "refused connection: " 101 + ''} 102 + ${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) '' 103 + pkttype broadcast log level info prefix "refused broadcast: " 104 + pkttype multicast log level info prefix "refused multicast: " 105 + ''} 106 + ${optionalString cfg.logRefusedPackets '' 107 + pkttype host log level info prefix "refused packet: " 108 + ''} 109 + 110 + ${optionalString cfg.rejectPackets '' 111 + meta l4proto tcp reject with tcp reset 112 + reject 113 + ''} 114 + 115 + } 116 + 117 + chain input-allow { 118 + 119 + ${concatStrings (mapAttrsToList (iface: cfg: 120 + let 121 + ifaceExpr = optionalString (iface != "default") "iifname ${iface}"; 122 + tcpSet = portsToNftSet cfg.allowedTCPPorts cfg.allowedTCPPortRanges; 123 + udpSet = portsToNftSet cfg.allowedUDPPorts cfg.allowedUDPPortRanges; 124 + in 125 + '' 126 + ${optionalString (tcpSet != "") "${ifaceExpr} tcp dport { ${tcpSet} } accept"} 127 + ${optionalString (udpSet != "") "${ifaceExpr} udp dport { ${udpSet} } accept"} 128 + '' 129 + ) cfg.allInterfaces)} 130 + 131 + ${optionalString cfg.allowPing '' 132 + icmp type echo-request ${optionalString (cfg.pingLimit != null) "limit rate ${cfg.pingLimit}"} accept comment "allow ping" 133 + ''} 134 + 135 + 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." 136 + ip6 daddr fe80::/64 udp dport 546 accept comment "DHCPv6 client" 137 + 138 + ${cfg.extraInputRules} 139 + 140 + } 141 + 142 + ${optionalString cfg.filterForward '' 143 + chain forward { 144 + type filter hook forward priority filter; policy drop; 145 + 146 + ct state vmap { invalid : drop, established : accept, related : accept, * : jump forward-allow } comment "*: new and untracked" 147 + 148 + } 149 + 150 + chain forward-allow { 151 + 152 + 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." 153 + 154 + ct status dnat accept comment "allow port forward" 155 + 156 + ${cfg.extraForwardRules} 157 + 158 + } 159 + ''} 160 + 161 + } 162 + 163 + ''; 164 + 165 + }; 166 + 167 + }
+141 -439
nixos/modules/services/networking/firewall.nix
··· 1 - /* This module enables a simple firewall. 2 - 3 - The firewall can be customised in arbitrary ways by setting 4 - ‘networking.firewall.extraCommands’. For modularity, the firewall 5 - uses several chains: 6 - 7 - - ‘nixos-fw’ is the main chain for input packet processing. 8 - 9 - - ‘nixos-fw-accept’ is called for accepted packets. If you want 10 - additional logging, or want to reject certain packets anyway, you 11 - can insert rules at the start of this chain. 12 - 13 - - ‘nixos-fw-log-refuse’ and ‘nixos-fw-refuse’ are called for 14 - refused packets. (The former jumps to the latter after logging 15 - the packet.) If you want additional logging, or want to accept 16 - certain packets anyway, you can insert rules at the start of 17 - this chain. 18 - 19 - - ‘nixos-fw-rpfilter’ is used as the main chain in the mangle table, 20 - called from the built-in ‘PREROUTING’ chain. If the kernel 21 - supports it and `cfg.checkReversePath` is set this chain will 22 - perform a reverse path filter test. 23 - 24 - - ‘nixos-drop’ is used while reloading the firewall in order to drop 25 - all traffic. Since reloading isn't implemented in an atomic way 26 - this'll prevent any traffic from leaking through while reloading 27 - the firewall. However, if the reloading fails, the ‘firewall-stop’ 28 - script will be called which in return will effectively disable the 29 - complete firewall (in the default configuration). 30 - 31 - */ 32 - 33 1 { config, lib, pkgs, ... }: 34 2 35 3 with lib; ··· 38 6 39 7 cfg = config.networking.firewall; 40 8 41 - inherit (config.boot.kernelPackages) kernel; 42 - 43 - kernelHasRPFilter = ((kernel.config.isEnabled or (x: false)) "IP_NF_MATCH_RPFILTER") || (kernel.features.netfilterRPFilter or false); 44 - 45 - helpers = import ./helpers.nix { inherit config lib; }; 46 - 47 - writeShScript = name: text: let dir = pkgs.writeScriptBin name '' 48 - #! ${pkgs.runtimeShell} -e 49 - ${text} 50 - ''; in "${dir}/bin/${name}"; 51 - 52 - defaultInterface = { default = mapAttrs (name: value: cfg.${name}) commonOptions; }; 53 - allInterfaces = defaultInterface // cfg.interfaces; 54 - 55 - startScript = writeShScript "firewall-start" '' 56 - ${helpers} 57 - 58 - # Flush the old firewall rules. !!! Ideally, updating the 59 - # firewall would be atomic. Apparently that's possible 60 - # with iptables-restore. 61 - ip46tables -D INPUT -j nixos-fw 2> /dev/null || true 62 - for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse; do 63 - ip46tables -F "$chain" 2> /dev/null || true 64 - ip46tables -X "$chain" 2> /dev/null || true 65 - done 66 - 67 - 68 - # The "nixos-fw-accept" chain just accepts packets. 69 - ip46tables -N nixos-fw-accept 70 - ip46tables -A nixos-fw-accept -j ACCEPT 71 - 72 - 73 - # The "nixos-fw-refuse" chain rejects or drops packets. 74 - ip46tables -N nixos-fw-refuse 75 - 76 - ${if cfg.rejectPackets then '' 77 - # Send a reset for existing TCP connections that we've 78 - # somehow forgotten about. Send ICMP "port unreachable" 79 - # for everything else. 80 - ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset 81 - ip46tables -A nixos-fw-refuse -j REJECT 82 - '' else '' 83 - ip46tables -A nixos-fw-refuse -j DROP 84 - ''} 85 - 86 - 87 - # The "nixos-fw-log-refuse" chain performs logging, then 88 - # jumps to the "nixos-fw-refuse" chain. 89 - ip46tables -N nixos-fw-log-refuse 90 - 91 - ${optionalString cfg.logRefusedConnections '' 92 - ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "refused connection: " 93 - ''} 94 - ${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) '' 95 - ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \ 96 - -j LOG --log-level info --log-prefix "refused broadcast: " 97 - ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \ 98 - -j LOG --log-level info --log-prefix "refused multicast: " 99 - ''} 100 - ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse 101 - ${optionalString cfg.logRefusedPackets '' 102 - ip46tables -A nixos-fw-log-refuse \ 103 - -j LOG --log-level info --log-prefix "refused packet: " 104 - ''} 105 - ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse 106 - 107 - 108 - # The "nixos-fw" chain does the actual work. 109 - ip46tables -N nixos-fw 110 - 111 - # Clean up rpfilter rules 112 - ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true 113 - ip46tables -t mangle -F nixos-fw-rpfilter 2> /dev/null || true 114 - ip46tables -t mangle -X nixos-fw-rpfilter 2> /dev/null || true 115 - 116 - ${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) '' 117 - # Perform a reverse-path test to refuse spoofers 118 - # For now, we just drop, as the mangle table doesn't have a log-refuse yet 119 - ip46tables -t mangle -N nixos-fw-rpfilter 2> /dev/null || true 120 - ip46tables -t mangle -A nixos-fw-rpfilter -m rpfilter --validmark ${optionalString (cfg.checkReversePath == "loose") "--loose"} -j RETURN 121 - 122 - # Allows this host to act as a DHCP4 client without first having to use APIPA 123 - iptables -t mangle -A nixos-fw-rpfilter -p udp --sport 67 --dport 68 -j RETURN 124 - 125 - # Allows this host to act as a DHCPv4 server 126 - 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 127 - 128 - ${optionalString cfg.logReversePathDrops '' 129 - ip46tables -t mangle -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: " 130 - ''} 131 - ip46tables -t mangle -A nixos-fw-rpfilter -j DROP 132 - 133 - ip46tables -t mangle -A PREROUTING -j nixos-fw-rpfilter 134 - ''} 135 - 136 - # Accept all traffic on the trusted interfaces. 137 - ${flip concatMapStrings cfg.trustedInterfaces (iface: '' 138 - ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept 139 - '')} 140 - 141 - # Accept packets from established or related connections. 142 - ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept 143 - 144 - # Accept connections to the allowed TCP ports. 145 - ${concatStrings (mapAttrsToList (iface: cfg: 146 - concatMapStrings (port: 147 - '' 148 - ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"} 149 - '' 150 - ) cfg.allowedTCPPorts 151 - ) allInterfaces)} 152 - 153 - # Accept connections to the allowed TCP port ranges. 154 - ${concatStrings (mapAttrsToList (iface: cfg: 155 - concatMapStrings (rangeAttr: 156 - let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in 157 - '' 158 - ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"} 159 - '' 160 - ) cfg.allowedTCPPortRanges 161 - ) allInterfaces)} 162 - 163 - # Accept packets on the allowed UDP ports. 164 - ${concatStrings (mapAttrsToList (iface: cfg: 165 - concatMapStrings (port: 166 - '' 167 - ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"} 168 - '' 169 - ) cfg.allowedUDPPorts 170 - ) allInterfaces)} 171 - 172 - # Accept packets on the allowed UDP port ranges. 173 - ${concatStrings (mapAttrsToList (iface: cfg: 174 - concatMapStrings (rangeAttr: 175 - let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in 176 - '' 177 - ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"} 178 - '' 179 - ) cfg.allowedUDPPortRanges 180 - ) allInterfaces)} 181 - 182 - # Optionally respond to ICMPv4 pings. 183 - ${optionalString cfg.allowPing '' 184 - iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null) 185 - "-m limit ${cfg.pingLimit} " 186 - }-j nixos-fw-accept 187 - ''} 188 - 189 - ${optionalString config.networking.enableIPv6 '' 190 - # Accept all ICMPv6 messages except redirects and node 191 - # information queries (type 139). See RFC 4890, section 192 - # 4.4. 193 - ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP 194 - ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP 195 - ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept 196 - 197 - # Allow this host to act as a DHCPv6 client 198 - ip6tables -A nixos-fw -d fe80::/64 -p udp --dport 546 -j nixos-fw-accept 199 - ''} 200 - 201 - ${cfg.extraCommands} 202 - 203 - # Reject/drop everything else. 204 - ip46tables -A nixos-fw -j nixos-fw-log-refuse 205 - 206 - 207 - # Enable the firewall. 208 - ip46tables -A INPUT -j nixos-fw 209 - ''; 210 - 211 - stopScript = writeShScript "firewall-stop" '' 212 - ${helpers} 213 - 214 - # Clean up in case reload fails 215 - ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 216 - 217 - # Clean up after added ruleset 218 - ip46tables -D INPUT -j nixos-fw 2>/dev/null || true 219 - 220 - ${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) '' 221 - ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true 222 - ''} 223 - 224 - ${cfg.extraStopCommands} 225 - ''; 226 - 227 - reloadScript = writeShScript "firewall-reload" '' 228 - ${helpers} 229 - 230 - # Create a unique drop rule 231 - ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 232 - ip46tables -F nixos-drop 2>/dev/null || true 233 - ip46tables -X nixos-drop 2>/dev/null || true 234 - ip46tables -N nixos-drop 235 - ip46tables -A nixos-drop -j DROP 236 - 237 - # Don't allow traffic to leak out until the script has completed 238 - ip46tables -A INPUT -j nixos-drop 239 - 240 - ${cfg.extraStopCommands} 241 - 242 - if ${startScript}; then 243 - ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 244 - else 245 - echo "Failed to reload firewall... Stopping" 246 - ${stopScript} 247 - exit 1 248 - fi 249 - ''; 250 - 251 9 canonicalizePortList = 252 10 ports: lib.unique (builtins.sort builtins.lessThan ports); 253 11 ··· 257 15 default = [ ]; 258 16 apply = canonicalizePortList; 259 17 example = [ 22 80 ]; 260 - description = 261 - lib.mdDoc '' 262 - List of TCP ports on which incoming connections are 263 - accepted. 264 - ''; 18 + description = lib.mdDoc '' 19 + List of TCP ports on which incoming connections are 20 + accepted. 21 + ''; 265 22 }; 266 23 267 24 allowedTCPPortRanges = mkOption { 268 25 type = types.listOf (types.attrsOf types.port); 269 26 default = [ ]; 270 - example = [ { from = 8999; to = 9003; } ]; 271 - description = 272 - lib.mdDoc '' 273 - A range of TCP ports on which incoming connections are 274 - accepted. 275 - ''; 27 + example = [{ from = 8999; to = 9003; }]; 28 + description = lib.mdDoc '' 29 + A range of TCP ports on which incoming connections are 30 + accepted. 31 + ''; 276 32 }; 277 33 278 34 allowedUDPPorts = mkOption { ··· 280 36 default = [ ]; 281 37 apply = canonicalizePortList; 282 38 example = [ 53 ]; 283 - description = 284 - lib.mdDoc '' 285 - List of open UDP ports. 286 - ''; 39 + description = lib.mdDoc '' 40 + List of open UDP ports. 41 + ''; 287 42 }; 288 43 289 44 allowedUDPPortRanges = mkOption { 290 45 type = types.listOf (types.attrsOf types.port); 291 46 default = [ ]; 292 - example = [ { from = 60000; to = 61000; } ]; 293 - description = 294 - lib.mdDoc '' 295 - Range of open UDP ports. 296 - ''; 47 + example = [{ from = 60000; to = 61000; }]; 48 + description = lib.mdDoc '' 49 + Range of open UDP ports. 50 + ''; 297 51 }; 298 52 }; 299 53 ··· 301 55 302 56 { 303 57 304 - ###### interface 305 - 306 58 options = { 307 59 308 60 networking.firewall = { 309 61 enable = mkOption { 310 62 type = types.bool; 311 63 default = true; 312 - description = 313 - lib.mdDoc '' 314 - Whether to enable the firewall. This is a simple stateful 315 - firewall that blocks connection attempts to unauthorised TCP 316 - or UDP ports on this machine. It does not affect packet 317 - forwarding. 318 - ''; 64 + description = lib.mdDoc '' 65 + Whether to enable the firewall. This is a simple stateful 66 + firewall that blocks connection attempts to unauthorised TCP 67 + or UDP ports on this machine. 68 + ''; 319 69 }; 320 70 321 71 package = mkOption { 322 72 type = types.package; 323 - default = pkgs.iptables; 324 - defaultText = literalExpression "pkgs.iptables"; 73 + default = if config.networking.nftables.enable then pkgs.nftables else pkgs.iptables; 74 + defaultText = literalExpression ''if config.networking.nftables.enable then "pkgs.nftables" else "pkgs.iptables"''; 325 75 example = literalExpression "pkgs.iptables-legacy"; 326 - description = 327 - lib.mdDoc '' 328 - The iptables package to use for running the firewall service. 329 - ''; 76 + description = lib.mdDoc '' 77 + The package to use for running the firewall service. 78 + ''; 330 79 }; 331 80 332 81 logRefusedConnections = mkOption { 333 82 type = types.bool; 334 83 default = true; 335 - description = 336 - lib.mdDoc '' 337 - Whether to log rejected or dropped incoming connections. 338 - Note: The logs are found in the kernel logs, i.e. dmesg 339 - or journalctl -k. 340 - ''; 84 + description = lib.mdDoc '' 85 + Whether to log rejected or dropped incoming connections. 86 + Note: The logs are found in the kernel logs, i.e. dmesg 87 + or journalctl -k. 88 + ''; 341 89 }; 342 90 343 91 logRefusedPackets = mkOption { 344 92 type = types.bool; 345 93 default = false; 346 - description = 347 - lib.mdDoc '' 348 - Whether to log all rejected or dropped incoming packets. 349 - This tends to give a lot of log messages, so it's mostly 350 - useful for debugging. 351 - Note: The logs are found in the kernel logs, i.e. dmesg 352 - or journalctl -k. 353 - ''; 94 + description = lib.mdDoc '' 95 + Whether to log all rejected or dropped incoming packets. 96 + This tends to give a lot of log messages, so it's mostly 97 + useful for debugging. 98 + Note: The logs are found in the kernel logs, i.e. dmesg 99 + or journalctl -k. 100 + ''; 354 101 }; 355 102 356 103 logRefusedUnicastsOnly = mkOption { 357 104 type = types.bool; 358 105 default = true; 359 - description = 360 - lib.mdDoc '' 361 - If {option}`networking.firewall.logRefusedPackets` 362 - and this option are enabled, then only log packets 363 - specifically directed at this machine, i.e., not broadcasts 364 - or multicasts. 365 - ''; 106 + description = lib.mdDoc '' 107 + If {option}`networking.firewall.logRefusedPackets` 108 + and this option are enabled, then only log packets 109 + specifically directed at this machine, i.e., not broadcasts 110 + or multicasts. 111 + ''; 366 112 }; 367 113 368 114 rejectPackets = mkOption { 369 115 type = types.bool; 370 116 default = false; 371 - description = 372 - lib.mdDoc '' 373 - If set, refused packets are rejected rather than dropped 374 - (ignored). This means that an ICMP "port unreachable" error 375 - message is sent back to the client (or a TCP RST packet in 376 - case of an existing connection). Rejecting packets makes 377 - port scanning somewhat easier. 378 - ''; 117 + description = lib.mdDoc '' 118 + If set, refused packets are rejected rather than dropped 119 + (ignored). This means that an ICMP "port unreachable" error 120 + message is sent back to the client (or a TCP RST packet in 121 + case of an existing connection). Rejecting packets makes 122 + port scanning somewhat easier. 123 + ''; 379 124 }; 380 125 381 126 trustedInterfaces = mkOption { 382 127 type = types.listOf types.str; 383 128 default = [ ]; 384 129 example = [ "enp0s2" ]; 385 - description = 386 - lib.mdDoc '' 387 - Traffic coming in from these interfaces will be accepted 388 - unconditionally. Traffic from the loopback (lo) interface 389 - will always be accepted. 390 - ''; 130 + description = lib.mdDoc '' 131 + Traffic coming in from these interfaces will be accepted 132 + unconditionally. Traffic from the loopback (lo) interface 133 + will always be accepted. 134 + ''; 391 135 }; 392 136 393 137 allowPing = mkOption { 394 138 type = types.bool; 395 139 default = true; 396 - description = 397 - lib.mdDoc '' 398 - Whether to respond to incoming ICMPv4 echo requests 399 - ("pings"). ICMPv6 pings are always allowed because the 400 - larger address space of IPv6 makes network scanning much 401 - less effective. 402 - ''; 140 + description = lib.mdDoc '' 141 + Whether to respond to incoming ICMPv4 echo requests 142 + ("pings"). ICMPv6 pings are always allowed because the 143 + larger address space of IPv6 makes network scanning much 144 + less effective. 145 + ''; 403 146 }; 404 147 405 148 pingLimit = mkOption { 406 149 type = types.nullOr (types.separatedString " "); 407 150 default = null; 408 151 example = "--limit 1/minute --limit-burst 5"; 409 - description = 410 - lib.mdDoc '' 411 - If pings are allowed, this allows setting rate limits 412 - on them. If non-null, this option should be in the form of 413 - flags like "--limit 1/minute --limit-burst 5" 414 - ''; 152 + description = lib.mdDoc '' 153 + If pings are allowed, this allows setting rate limits on them. 154 + 155 + For the iptables based firewall, it should be set like 156 + "--limit 1/minute --limit-burst 5". 157 + 158 + For the nftables based firewall, it should be set like 159 + "2/second" or "1/minute burst 5 packets". 160 + ''; 415 161 }; 416 162 417 163 checkReversePath = mkOption { 418 - type = types.either types.bool (types.enum ["strict" "loose"]); 419 - default = kernelHasRPFilter; 420 - defaultText = literalMD "`true` if supported by the chosen kernel"; 164 + type = types.either types.bool (types.enum [ "strict" "loose" ]); 165 + default = true; 166 + defaultText = literalMD "`true` except if the iptables based firewall is in use and the kernel lacks rpfilter support"; 421 167 example = "loose"; 422 - description = 423 - lib.mdDoc '' 424 - Performs a reverse path filter test on a packet. If a reply 425 - to the packet would not be sent via the same interface that 426 - the packet arrived on, it is refused. 168 + description = lib.mdDoc '' 169 + Performs a reverse path filter test on a packet. If a reply 170 + to the packet would not be sent via the same interface that 171 + the packet arrived on, it is refused. 427 172 428 - If using asymmetric routing or other complicated routing, set 429 - this option to loose mode or disable it and setup your own 430 - counter-measures. 173 + If using asymmetric routing or other complicated routing, set 174 + this option to loose mode or disable it and setup your own 175 + counter-measures. 431 176 432 - This option can be either true (or "strict"), "loose" (only 433 - drop the packet if the source address is not reachable via any 434 - interface) or false. Defaults to the value of 435 - kernelHasRPFilter. 436 - ''; 177 + This option can be either true (or "strict"), "loose" (only 178 + drop the packet if the source address is not reachable via any 179 + interface) or false. 180 + ''; 437 181 }; 438 182 439 183 logReversePathDrops = mkOption { 440 184 type = types.bool; 441 185 default = false; 442 - description = 443 - lib.mdDoc '' 444 - Logs dropped packets failing the reverse path filter test if 445 - the option networking.firewall.checkReversePath is enabled. 446 - ''; 186 + description = lib.mdDoc '' 187 + Logs dropped packets failing the reverse path filter test if 188 + the option networking.firewall.checkReversePath is enabled. 189 + ''; 190 + }; 191 + 192 + filterForward = mkOption { 193 + type = types.bool; 194 + default = false; 195 + description = lib.mdDoc '' 196 + Enable filtering in IP forwarding. 197 + 198 + This option only works with the nftables based firewall. 199 + ''; 447 200 }; 448 201 449 202 connectionTrackingModules = mkOption { 450 203 type = types.listOf types.str; 451 204 default = [ ]; 452 205 example = [ "ftp" "irc" "sane" "sip" "tftp" "amanda" "h323" "netbios_sn" "pptp" "snmp" ]; 453 - description = 454 - lib.mdDoc '' 455 - List of connection-tracking helpers that are auto-loaded. 456 - The complete list of possible values is given in the example. 206 + description = lib.mdDoc '' 207 + List of connection-tracking helpers that are auto-loaded. 208 + The complete list of possible values is given in the example. 457 209 458 - As helpers can pose as a security risk, it is advised to 459 - set this to an empty list and disable the setting 460 - networking.firewall.autoLoadConntrackHelpers unless you 461 - know what you are doing. Connection tracking is disabled 462 - by default. 210 + As helpers can pose as a security risk, it is advised to 211 + set this to an empty list and disable the setting 212 + networking.firewall.autoLoadConntrackHelpers unless you 213 + know what you are doing. Connection tracking is disabled 214 + by default. 463 215 464 - Loading of helpers is recommended to be done through the 465 - CT target. More info: 466 - https://home.regit.org/netfilter-en/secure-use-of-helpers/ 467 - ''; 216 + Loading of helpers is recommended to be done through the 217 + CT target. More info: 218 + https://home.regit.org/netfilter-en/secure-use-of-helpers/ 219 + ''; 468 220 }; 469 221 470 222 autoLoadConntrackHelpers = mkOption { 471 223 type = types.bool; 472 224 default = false; 473 - description = 474 - lib.mdDoc '' 475 - Whether to auto-load connection-tracking helpers. 476 - See the description at networking.firewall.connectionTrackingModules 477 - 478 - (needs kernel 3.5+) 479 - ''; 480 - }; 225 + description = lib.mdDoc '' 226 + Whether to auto-load connection-tracking helpers. 227 + See the description at networking.firewall.connectionTrackingModules 481 228 482 - extraCommands = mkOption { 483 - type = types.lines; 484 - default = ""; 485 - example = "iptables -A INPUT -p icmp -j ACCEPT"; 486 - description = 487 - lib.mdDoc '' 488 - Additional shell commands executed as part of the firewall 489 - initialisation script. These are executed just before the 490 - final "reject" firewall rule is added, so they can be used 491 - to allow packets that would otherwise be refused. 492 - ''; 229 + (needs kernel 3.5+) 230 + ''; 493 231 }; 494 232 495 233 extraPackages = mkOption { 496 234 type = types.listOf types.package; 497 235 default = [ ]; 498 236 example = literalExpression "[ pkgs.ipset ]"; 499 - description = 500 - lib.mdDoc '' 501 - Additional packages to be included in the environment of the system 502 - as well as the path of networking.firewall.extraCommands. 503 - ''; 237 + description = lib.mdDoc '' 238 + Additional packages to be included in the environment of the system 239 + as well as the path of networking.firewall.extraCommands. 240 + ''; 504 241 }; 505 242 506 - extraStopCommands = mkOption { 507 - type = types.lines; 508 - default = ""; 509 - example = "iptables -P INPUT ACCEPT"; 510 - description = 511 - lib.mdDoc '' 512 - Additional shell commands executed as part of the firewall 513 - shutdown script. These are executed just after the removal 514 - of the NixOS input rule, or if the service enters a failed 515 - state. 516 - ''; 243 + interfaces = mkOption { 244 + default = { }; 245 + type = with types; attrsOf (submodule [{ options = commonOptions; }]); 246 + description = lib.mdDoc '' 247 + Interface-specific open ports. 248 + ''; 517 249 }; 518 250 519 - interfaces = mkOption { 520 - default = { }; 521 - type = with types; attrsOf (submodule [ { options = commonOptions; } ]); 522 - description = 523 - lib.mdDoc '' 524 - Interface-specific open ports. 525 - ''; 251 + allInterfaces = mkOption { 252 + internal = true; 253 + visible = false; 254 + default = { default = mapAttrs (name: value: cfg.${name}) commonOptions; } // cfg.interfaces; 255 + type = with types; attrsOf (submodule [{ options = commonOptions; }]); 256 + description = lib.mdDoc '' 257 + All open ports. 258 + ''; 526 259 }; 527 260 } // commonOptions; 528 261 529 262 }; 530 263 531 264 532 - ###### implementation 533 - 534 - # FIXME: Maybe if `enable' is false, the firewall should still be 535 - # built but not started by default? 536 265 config = mkIf cfg.enable { 537 266 267 + assertions = [ 268 + { 269 + assertion = cfg.filterForward -> config.networking.nftables.enable; 270 + message = "filterForward only works with the nftables based firewall"; 271 + } 272 + ]; 273 + 538 274 networking.firewall.trustedInterfaces = [ "lo" ]; 539 275 540 276 environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages; ··· 544 280 boot.extraModprobeConfig = optionalString cfg.autoLoadConntrackHelpers '' 545 281 options nf_conntrack nf_conntrack_helper=1 546 282 ''; 547 - 548 - assertions = [ 549 - # This is approximately "checkReversePath -> kernelHasRPFilter", 550 - # but the checkReversePath option can include non-boolean 551 - # values. 552 - { assertion = cfg.checkReversePath == false || kernelHasRPFilter; 553 - message = "This kernel does not support rpfilter"; } 554 - ]; 555 - 556 - systemd.services.firewall = { 557 - description = "Firewall"; 558 - wantedBy = [ "sysinit.target" ]; 559 - wants = [ "network-pre.target" ]; 560 - before = [ "network-pre.target" ]; 561 - after = [ "systemd-modules-load.service" ]; 562 - 563 - path = [ cfg.package ] ++ cfg.extraPackages; 564 - 565 - # FIXME: this module may also try to load kernel modules, but 566 - # containers don't have CAP_SYS_MODULE. So the host system had 567 - # better have all necessary modules already loaded. 568 - unitConfig.ConditionCapability = "CAP_NET_ADMIN"; 569 - unitConfig.DefaultDependencies = false; 570 - 571 - reloadIfChanged = true; 572 - 573 - serviceConfig = { 574 - Type = "oneshot"; 575 - RemainAfterExit = true; 576 - ExecStart = "@${startScript} firewall-start"; 577 - ExecReload = "@${reloadScript} firewall-reload"; 578 - ExecStop = "@${stopScript} firewall-stop"; 579 - }; 580 - }; 581 283 582 284 }; 583 285
+191
nixos/modules/services/networking/nat-iptables.nix
··· 1 + # This module enables Network Address Translation (NAT). 2 + # XXX: todo: support multiple upstream links 3 + # see http://yesican.chsoft.biz/lartc/MultihomedLinuxNetworking.html 4 + 5 + { config, lib, pkgs, ... }: 6 + 7 + with lib; 8 + 9 + let 10 + cfg = config.networking.nat; 11 + 12 + mkDest = externalIP: 13 + if externalIP == null 14 + then "-j MASQUERADE" 15 + else "-j SNAT --to-source ${externalIP}"; 16 + dest = mkDest cfg.externalIP; 17 + destIPv6 = mkDest cfg.externalIPv6; 18 + 19 + # Whether given IP (plus optional port) is an IPv6. 20 + isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2; 21 + 22 + helpers = import ./helpers.nix { inherit config lib; }; 23 + 24 + flushNat = '' 25 + ${helpers} 26 + ip46tables -w -t nat -D PREROUTING -j nixos-nat-pre 2>/dev/null|| true 27 + ip46tables -w -t nat -F nixos-nat-pre 2>/dev/null || true 28 + ip46tables -w -t nat -X nixos-nat-pre 2>/dev/null || true 29 + ip46tables -w -t nat -D POSTROUTING -j nixos-nat-post 2>/dev/null || true 30 + ip46tables -w -t nat -F nixos-nat-post 2>/dev/null || true 31 + ip46tables -w -t nat -X nixos-nat-post 2>/dev/null || true 32 + ip46tables -w -t nat -D OUTPUT -j nixos-nat-out 2>/dev/null || true 33 + ip46tables -w -t nat -F nixos-nat-out 2>/dev/null || true 34 + ip46tables -w -t nat -X nixos-nat-out 2>/dev/null || true 35 + 36 + ${cfg.extraStopCommands} 37 + ''; 38 + 39 + mkSetupNat = { iptables, dest, internalIPs, forwardPorts }: '' 40 + # We can't match on incoming interface in POSTROUTING, so 41 + # mark packets coming from the internal interfaces. 42 + ${concatMapStrings (iface: '' 43 + ${iptables} -w -t nat -A nixos-nat-pre \ 44 + -i '${iface}' -j MARK --set-mark 1 45 + '') cfg.internalInterfaces} 46 + 47 + # NAT the marked packets. 48 + ${optionalString (cfg.internalInterfaces != []) '' 49 + ${iptables} -w -t nat -A nixos-nat-post -m mark --mark 1 \ 50 + ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest} 51 + ''} 52 + 53 + # NAT packets coming from the internal IPs. 54 + ${concatMapStrings (range: '' 55 + ${iptables} -w -t nat -A nixos-nat-post \ 56 + -s '${range}' ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest} 57 + '') internalIPs} 58 + 59 + # NAT from external ports to internal ports. 60 + ${concatMapStrings (fwd: '' 61 + ${iptables} -w -t nat -A nixos-nat-pre \ 62 + -i ${toString cfg.externalInterface} -p ${fwd.proto} \ 63 + --dport ${builtins.toString fwd.sourcePort} \ 64 + -j DNAT --to-destination ${fwd.destination} 65 + 66 + ${concatMapStrings (loopbackip: 67 + let 68 + matchIP = if isIPv6 fwd.destination then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)"; 69 + m = builtins.match "${matchIP}:([0-9-]+)" fwd.destination; 70 + destinationIP = if m == null then throw "bad ip:ports `${fwd.destination}'" else elemAt m 0; 71 + destinationPorts = if m == null then throw "bad ip:ports `${fwd.destination}'" else builtins.replaceStrings ["-"] [":"] (elemAt m 1); 72 + in '' 73 + # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from the host itself 74 + ${iptables} -w -t nat -A nixos-nat-out \ 75 + -d ${loopbackip} -p ${fwd.proto} \ 76 + --dport ${builtins.toString fwd.sourcePort} \ 77 + -j DNAT --to-destination ${fwd.destination} 78 + 79 + # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from other hosts behind NAT 80 + ${iptables} -w -t nat -A nixos-nat-pre \ 81 + -d ${loopbackip} -p ${fwd.proto} \ 82 + --dport ${builtins.toString fwd.sourcePort} \ 83 + -j DNAT --to-destination ${fwd.destination} 84 + 85 + ${iptables} -w -t nat -A nixos-nat-post \ 86 + -d ${destinationIP} -p ${fwd.proto} \ 87 + --dport ${destinationPorts} \ 88 + -j SNAT --to-source ${loopbackip} 89 + '') fwd.loopbackIPs} 90 + '') forwardPorts} 91 + ''; 92 + 93 + setupNat = '' 94 + ${helpers} 95 + # Create subchains where we store rules 96 + ip46tables -w -t nat -N nixos-nat-pre 97 + ip46tables -w -t nat -N nixos-nat-post 98 + ip46tables -w -t nat -N nixos-nat-out 99 + 100 + ${mkSetupNat { 101 + iptables = "iptables"; 102 + inherit dest; 103 + inherit (cfg) internalIPs; 104 + forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts; 105 + }} 106 + 107 + ${optionalString cfg.enableIPv6 (mkSetupNat { 108 + iptables = "ip6tables"; 109 + dest = destIPv6; 110 + internalIPs = cfg.internalIPv6s; 111 + forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts; 112 + })} 113 + 114 + ${optionalString (cfg.dmzHost != null) '' 115 + iptables -w -t nat -A nixos-nat-pre \ 116 + -i ${toString cfg.externalInterface} -j DNAT \ 117 + --to-destination ${cfg.dmzHost} 118 + ''} 119 + 120 + ${cfg.extraCommands} 121 + 122 + # Append our chains to the nat tables 123 + ip46tables -w -t nat -A PREROUTING -j nixos-nat-pre 124 + ip46tables -w -t nat -A POSTROUTING -j nixos-nat-post 125 + ip46tables -w -t nat -A OUTPUT -j nixos-nat-out 126 + ''; 127 + 128 + in 129 + 130 + { 131 + 132 + options = { 133 + 134 + networking.nat.extraCommands = mkOption { 135 + type = types.lines; 136 + default = ""; 137 + example = "iptables -A INPUT -p icmp -j ACCEPT"; 138 + description = lib.mdDoc '' 139 + Additional shell commands executed as part of the nat 140 + initialisation script. 141 + 142 + This option is incompatible with the nftables based nat module. 143 + ''; 144 + }; 145 + 146 + networking.nat.extraStopCommands = mkOption { 147 + type = types.lines; 148 + default = ""; 149 + example = "iptables -D INPUT -p icmp -j ACCEPT || true"; 150 + description = lib.mdDoc '' 151 + Additional shell commands executed as part of the nat 152 + teardown script. 153 + 154 + This option is incompatible with the nftables based nat module. 155 + ''; 156 + }; 157 + 158 + }; 159 + 160 + 161 + config = mkIf (!config.networking.nftables.enable) 162 + (mkMerge [ 163 + ({ networking.firewall.extraCommands = mkBefore flushNat; }) 164 + (mkIf config.networking.nat.enable { 165 + 166 + networking.firewall = mkIf config.networking.firewall.enable { 167 + extraCommands = setupNat; 168 + extraStopCommands = flushNat; 169 + }; 170 + 171 + systemd.services = mkIf (!config.networking.firewall.enable) { 172 + nat = { 173 + description = "Network Address Translation"; 174 + wantedBy = [ "network.target" ]; 175 + after = [ "network-pre.target" "systemd-modules-load.service" ]; 176 + path = [ config.networking.firewall.package ]; 177 + unitConfig.ConditionCapability = "CAP_NET_ADMIN"; 178 + 179 + serviceConfig = { 180 + Type = "oneshot"; 181 + RemainAfterExit = true; 182 + }; 183 + 184 + script = flushNat + setupNat; 185 + 186 + postStop = flushNat; 187 + }; 188 + }; 189 + }) 190 + ]); 191 + }
+184
nixos/modules/services/networking/nat-nftables.nix
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + with lib; 4 + 5 + let 6 + cfg = config.networking.nat; 7 + 8 + mkDest = externalIP: 9 + if externalIP == null 10 + then "masquerade" 11 + else "snat ${externalIP}"; 12 + dest = mkDest cfg.externalIP; 13 + destIPv6 = mkDest cfg.externalIPv6; 14 + 15 + toNftSet = list: concatStringsSep ", " list; 16 + toNftRange = ports: replaceStrings [ ":" ] [ "-" ] (toString ports); 17 + 18 + ifaceSet = toNftSet (map (x: ''"${x}"'') cfg.internalInterfaces); 19 + ipSet = toNftSet cfg.internalIPs; 20 + ipv6Set = toNftSet cfg.internalIPv6s; 21 + oifExpr = optionalString (cfg.externalInterface != null) ''oifname "${cfg.externalInterface}"''; 22 + 23 + # Whether given IP (plus optional port) is an IPv6. 24 + isIPv6 = ip: length (lib.splitString ":" ip) > 2; 25 + 26 + splitIPPorts = IPPorts: 27 + let 28 + matchIP = if isIPv6 IPPorts then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)"; 29 + m = builtins.match "${matchIP}:([0-9-]+)" IPPorts; 30 + in 31 + { 32 + IP = if m == null then throw "bad ip:ports `${IPPorts}'" else elemAt m 0; 33 + ports = if m == null then throw "bad ip:ports `${IPPorts}'" else elemAt m 1; 34 + }; 35 + 36 + mkTable = { ipVer, dest, ipSet, forwardPorts, dmzHost }: 37 + let 38 + # nftables does not support both port and port range as values in a dnat map. 39 + # e.g. "dnat th dport map { 80 : 10.0.0.1 . 80, 443 : 10.0.0.2 . 900-1000 }" 40 + # So we split them. 41 + fwdPorts = filter (x: length (splitString "-" x.destination) == 1) forwardPorts; 42 + fwdPortsRange = filter (x: length (splitString "-" x.destination) > 1) forwardPorts; 43 + 44 + # nftables maps for port forward 45 + # l4proto . dport : addr . port 46 + toFwdMap = forwardPorts: toNftSet (map 47 + (fwd: 48 + with (splitIPPorts fwd.destination); 49 + "${fwd.proto} . ${toNftRange fwd.sourcePort} : ${IP} . ${ports}" 50 + ) 51 + forwardPorts); 52 + fwdMap = toFwdMap fwdPorts; 53 + fwdRangeMap = toFwdMap fwdPortsRange; 54 + 55 + # nftables maps for port forward loopback dnat 56 + # daddr . l4proto . dport : addr . port 57 + toFwdLoopDnatMap = forwardPorts: toNftSet (concatMap 58 + (fwd: map 59 + (loopbackip: 60 + with (splitIPPorts fwd.destination); 61 + "${loopbackip} . ${fwd.proto} . ${toNftRange fwd.sourcePort} : ${IP} . ${ports}" 62 + ) 63 + fwd.loopbackIPs) 64 + forwardPorts); 65 + fwdLoopDnatMap = toFwdLoopDnatMap fwdPorts; 66 + fwdLoopDnatRangeMap = toFwdLoopDnatMap fwdPortsRange; 67 + 68 + # nftables set for port forward loopback snat 69 + # daddr . l4proto . dport 70 + fwdLoopSnatSet = toNftSet (map 71 + (fwd: 72 + with (splitIPPorts fwd.destination); 73 + "${IP} . ${fwd.proto} . ${ports}" 74 + ) 75 + forwardPorts); 76 + in 77 + '' 78 + chain pre { 79 + type nat hook prerouting priority dstnat; 80 + 81 + ${optionalString (fwdMap != "") '' 82 + iifname "${cfg.externalInterface}" dnat meta l4proto . th dport map { ${fwdMap} } comment "port forward" 83 + ''} 84 + ${optionalString (fwdRangeMap != "") '' 85 + iifname "${cfg.externalInterface}" dnat meta l4proto . th dport map { ${fwdRangeMap} } comment "port forward" 86 + ''} 87 + 88 + ${optionalString (fwdLoopDnatMap != "") '' 89 + dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatMap} } comment "port forward loopback from other hosts behind NAT" 90 + ''} 91 + ${optionalString (fwdLoopDnatRangeMap != "") '' 92 + dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatRangeMap} } comment "port forward loopback from other hosts behind NAT" 93 + ''} 94 + 95 + ${optionalString (dmzHost != null) '' 96 + iifname "${cfg.externalInterface}" dnat ${dmzHost} comment "dmz" 97 + ''} 98 + } 99 + 100 + chain post { 101 + type nat hook postrouting priority srcnat; 102 + 103 + ${optionalString (ifaceSet != "") '' 104 + iifname { ${ifaceSet} } ${oifExpr} ${dest} comment "from internal interfaces" 105 + ''} 106 + ${optionalString (ipSet != "") '' 107 + ${ipVer} saddr { ${ipSet} } ${oifExpr} ${dest} comment "from internal IPs" 108 + ''} 109 + 110 + ${optionalString (fwdLoopSnatSet != "") '' 111 + iifname != "${cfg.externalInterface}" ${ipVer} daddr . meta l4proto . th dport { ${fwdLoopSnatSet} } masquerade comment "port forward loopback snat" 112 + ''} 113 + } 114 + 115 + chain out { 116 + type nat hook output priority mangle; 117 + 118 + ${optionalString (fwdLoopDnatMap != "") '' 119 + dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatMap} } comment "port forward loopback from the host itself" 120 + ''} 121 + ${optionalString (fwdLoopDnatRangeMap != "") '' 122 + dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatRangeMap} } comment "port forward loopback from the host itself" 123 + ''} 124 + } 125 + ''; 126 + 127 + in 128 + 129 + { 130 + 131 + config = mkIf (config.networking.nftables.enable && cfg.enable) { 132 + 133 + assertions = [ 134 + { 135 + assertion = cfg.extraCommands == ""; 136 + message = "extraCommands is incompatible with the nftables based nat module: ${cfg.extraCommands}"; 137 + } 138 + { 139 + assertion = cfg.extraStopCommands == ""; 140 + message = "extraStopCommands is incompatible with the nftables based nat module: ${cfg.extraStopCommands}"; 141 + } 142 + { 143 + assertion = config.networking.nftables.rulesetFile == null; 144 + message = "networking.nftables.rulesetFile conflicts with the nat module"; 145 + } 146 + ]; 147 + 148 + networking.nftables.ruleset = '' 149 + table ip nixos-nat { 150 + ${mkTable { 151 + ipVer = "ip"; 152 + inherit dest ipSet; 153 + forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts; 154 + inherit (cfg) dmzHost; 155 + }} 156 + } 157 + 158 + ${optionalString cfg.enableIPv6 '' 159 + table ip6 nixos-nat { 160 + ${mkTable { 161 + ipVer = "ip6"; 162 + dest = destIPv6; 163 + ipSet = ipv6Set; 164 + forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts; 165 + dmzHost = null; 166 + }} 167 + } 168 + ''} 169 + ''; 170 + 171 + networking.firewall.extraForwardRules = optionalString config.networking.firewall.filterForward '' 172 + ${optionalString (ifaceSet != "") '' 173 + iifname { ${ifaceSet} } ${oifExpr} accept comment "from internal interfaces" 174 + ''} 175 + ${optionalString (ipSet != "") '' 176 + ip saddr { ${ipSet} } ${oifExpr} accept comment "from internal IPs" 177 + ''} 178 + ${optionalString (ipv6Set != "") '' 179 + ip6 saddr { ${ipv6Set} } ${oifExpr} accept comment "from internal IPv6s" 180 + ''} 181 + ''; 182 + 183 + }; 184 + }
+85 -256
nixos/modules/services/networking/nat.nix
··· 7 7 with lib; 8 8 9 9 let 10 - cfg = config.networking.nat; 11 10 12 - mkDest = externalIP: if externalIP == null 13 - then "-j MASQUERADE" 14 - else "-j SNAT --to-source ${externalIP}"; 15 - dest = mkDest cfg.externalIP; 16 - destIPv6 = mkDest cfg.externalIPv6; 17 - 18 - # Whether given IP (plus optional port) is an IPv6. 19 - isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2; 20 - 21 - helpers = import ./helpers.nix { inherit config lib; }; 22 - 23 - flushNat = '' 24 - ${helpers} 25 - ip46tables -w -t nat -D PREROUTING -j nixos-nat-pre 2>/dev/null|| true 26 - ip46tables -w -t nat -F nixos-nat-pre 2>/dev/null || true 27 - ip46tables -w -t nat -X nixos-nat-pre 2>/dev/null || true 28 - ip46tables -w -t nat -D POSTROUTING -j nixos-nat-post 2>/dev/null || true 29 - ip46tables -w -t nat -F nixos-nat-post 2>/dev/null || true 30 - ip46tables -w -t nat -X nixos-nat-post 2>/dev/null || true 31 - ip46tables -w -t nat -D OUTPUT -j nixos-nat-out 2>/dev/null || true 32 - ip46tables -w -t nat -F nixos-nat-out 2>/dev/null || true 33 - ip46tables -w -t nat -X nixos-nat-out 2>/dev/null || true 34 - 35 - ${cfg.extraStopCommands} 36 - ''; 37 - 38 - mkSetupNat = { iptables, dest, internalIPs, forwardPorts }: '' 39 - # We can't match on incoming interface in POSTROUTING, so 40 - # mark packets coming from the internal interfaces. 41 - ${concatMapStrings (iface: '' 42 - ${iptables} -w -t nat -A nixos-nat-pre \ 43 - -i '${iface}' -j MARK --set-mark 1 44 - '') cfg.internalInterfaces} 45 - 46 - # NAT the marked packets. 47 - ${optionalString (cfg.internalInterfaces != []) '' 48 - ${iptables} -w -t nat -A nixos-nat-post -m mark --mark 1 \ 49 - ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest} 50 - ''} 51 - 52 - # NAT packets coming from the internal IPs. 53 - ${concatMapStrings (range: '' 54 - ${iptables} -w -t nat -A nixos-nat-post \ 55 - -s '${range}' ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest} 56 - '') internalIPs} 57 - 58 - # NAT from external ports to internal ports. 59 - ${concatMapStrings (fwd: '' 60 - ${iptables} -w -t nat -A nixos-nat-pre \ 61 - -i ${toString cfg.externalInterface} -p ${fwd.proto} \ 62 - --dport ${builtins.toString fwd.sourcePort} \ 63 - -j DNAT --to-destination ${fwd.destination} 64 - 65 - ${concatMapStrings (loopbackip: 66 - let 67 - matchIP = if isIPv6 fwd.destination then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)"; 68 - m = builtins.match "${matchIP}:([0-9-]+)" fwd.destination; 69 - destinationIP = if m == null then throw "bad ip:ports `${fwd.destination}'" else elemAt m 0; 70 - destinationPorts = if m == null then throw "bad ip:ports `${fwd.destination}'" else builtins.replaceStrings ["-"] [":"] (elemAt m 1); 71 - in '' 72 - # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from the host itself 73 - ${iptables} -w -t nat -A nixos-nat-out \ 74 - -d ${loopbackip} -p ${fwd.proto} \ 75 - --dport ${builtins.toString fwd.sourcePort} \ 76 - -j DNAT --to-destination ${fwd.destination} 77 - 78 - # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from other hosts behind NAT 79 - ${iptables} -w -t nat -A nixos-nat-pre \ 80 - -d ${loopbackip} -p ${fwd.proto} \ 81 - --dport ${builtins.toString fwd.sourcePort} \ 82 - -j DNAT --to-destination ${fwd.destination} 83 - 84 - ${iptables} -w -t nat -A nixos-nat-post \ 85 - -d ${destinationIP} -p ${fwd.proto} \ 86 - --dport ${destinationPorts} \ 87 - -j SNAT --to-source ${loopbackip} 88 - '') fwd.loopbackIPs} 89 - '') forwardPorts} 90 - ''; 91 - 92 - setupNat = '' 93 - ${helpers} 94 - # Create subchains where we store rules 95 - ip46tables -w -t nat -N nixos-nat-pre 96 - ip46tables -w -t nat -N nixos-nat-post 97 - ip46tables -w -t nat -N nixos-nat-out 98 - 99 - ${mkSetupNat { 100 - iptables = "iptables"; 101 - inherit dest; 102 - inherit (cfg) internalIPs; 103 - forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts; 104 - }} 105 - 106 - ${optionalString cfg.enableIPv6 (mkSetupNat { 107 - iptables = "ip6tables"; 108 - dest = destIPv6; 109 - internalIPs = cfg.internalIPv6s; 110 - forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts; 111 - })} 112 - 113 - ${optionalString (cfg.dmzHost != null) '' 114 - iptables -w -t nat -A nixos-nat-pre \ 115 - -i ${toString cfg.externalInterface} -j DNAT \ 116 - --to-destination ${cfg.dmzHost} 117 - ''} 118 - 119 - ${cfg.extraCommands} 120 - 121 - # Append our chains to the nat tables 122 - ip46tables -w -t nat -A PREROUTING -j nixos-nat-pre 123 - ip46tables -w -t nat -A POSTROUTING -j nixos-nat-post 124 - ip46tables -w -t nat -A OUTPUT -j nixos-nat-out 125 - ''; 11 + cfg = config.networking.nat; 126 12 127 13 in 128 14 129 15 { 130 16 131 - ###### interface 132 - 133 17 options = { 134 18 135 19 networking.nat.enable = mkOption { 136 20 type = types.bool; 137 21 default = false; 138 - description = 139 - lib.mdDoc '' 140 - Whether to enable Network Address Translation (NAT). 141 - ''; 22 + description = lib.mdDoc '' 23 + Whether to enable Network Address Translation (NAT). 24 + ''; 142 25 }; 143 26 144 27 networking.nat.enableIPv6 = mkOption { 145 28 type = types.bool; 146 29 default = false; 147 - description = 148 - lib.mdDoc '' 149 - Whether to enable IPv6 NAT. 150 - ''; 30 + description = lib.mdDoc '' 31 + Whether to enable IPv6 NAT. 32 + ''; 151 33 }; 152 34 153 35 networking.nat.internalInterfaces = mkOption { 154 36 type = types.listOf types.str; 155 - default = []; 37 + default = [ ]; 156 38 example = [ "eth0" ]; 157 - description = 158 - lib.mdDoc '' 159 - The interfaces for which to perform NAT. Packets coming from 160 - these interface and destined for the external interface will 161 - be rewritten. 162 - ''; 39 + description = lib.mdDoc '' 40 + The interfaces for which to perform NAT. Packets coming from 41 + these interface and destined for the external interface will 42 + be rewritten. 43 + ''; 163 44 }; 164 45 165 46 networking.nat.internalIPs = mkOption { 166 47 type = types.listOf types.str; 167 - default = []; 48 + default = [ ]; 168 49 example = [ "192.168.1.0/24" ]; 169 - description = 170 - lib.mdDoc '' 171 - The IP address ranges for which to perform NAT. Packets 172 - coming from these addresses (on any interface) and destined 173 - for the external interface will be rewritten. 174 - ''; 50 + description = lib.mdDoc '' 51 + The IP address ranges for which to perform NAT. Packets 52 + coming from these addresses (on any interface) and destined 53 + for the external interface will be rewritten. 54 + ''; 175 55 }; 176 56 177 57 networking.nat.internalIPv6s = mkOption { 178 58 type = types.listOf types.str; 179 - default = []; 59 + default = [ ]; 180 60 example = [ "fc00::/64" ]; 181 - description = 182 - lib.mdDoc '' 183 - The IPv6 address ranges for which to perform NAT. Packets 184 - coming from these addresses (on any interface) and destined 185 - for the external interface will be rewritten. 186 - ''; 61 + description = lib.mdDoc '' 62 + The IPv6 address ranges for which to perform NAT. Packets 63 + coming from these addresses (on any interface) and destined 64 + for the external interface will be rewritten. 65 + ''; 187 66 }; 188 67 189 68 networking.nat.externalInterface = mkOption { 190 69 type = types.nullOr types.str; 191 70 default = null; 192 71 example = "eth1"; 193 - description = 194 - lib.mdDoc '' 195 - The name of the external network interface. 196 - ''; 72 + description = lib.mdDoc '' 73 + The name of the external network interface. 74 + ''; 197 75 }; 198 76 199 77 networking.nat.externalIP = mkOption { 200 78 type = types.nullOr types.str; 201 79 default = null; 202 80 example = "203.0.113.123"; 203 - description = 204 - lib.mdDoc '' 205 - The public IP address to which packets from the local 206 - network are to be rewritten. If this is left empty, the 207 - IP address associated with the external interface will be 208 - used. 209 - ''; 81 + description = lib.mdDoc '' 82 + The public IP address to which packets from the local 83 + network are to be rewritten. If this is left empty, the 84 + IP address associated with the external interface will be 85 + used. 86 + ''; 210 87 }; 211 88 212 89 networking.nat.externalIPv6 = mkOption { 213 90 type = types.nullOr types.str; 214 91 default = null; 215 92 example = "2001:dc0:2001:11::175"; 216 - description = 217 - lib.mdDoc '' 218 - The public IPv6 address to which packets from the local 219 - network are to be rewritten. If this is left empty, the 220 - IP address associated with the external interface will be 221 - used. 222 - ''; 93 + description = lib.mdDoc '' 94 + The public IPv6 address to which packets from the local 95 + network are to be rewritten. If this is left empty, the 96 + IP address associated with the external interface will be 97 + used. 98 + ''; 223 99 }; 224 100 225 101 networking.nat.forwardPorts = mkOption { ··· 246 122 247 123 loopbackIPs = mkOption { 248 124 type = types.listOf types.str; 249 - default = []; 125 + default = [ ]; 250 126 example = literalExpression ''[ "55.1.2.3" ]''; 251 127 description = lib.mdDoc "Public IPs for NAT reflection; for connections to `loopbackip:sourcePort' from the host itself and from other hosts behind NAT"; 252 128 }; 253 129 }; 254 130 }); 255 - default = []; 131 + default = [ ]; 256 132 example = [ 257 133 { sourcePort = 8080; destination = "10.0.0.1:80"; proto = "tcp"; } 258 134 { sourcePort = 8080; destination = "[fc00::2]:80"; proto = "tcp"; } 259 135 ]; 260 - description = 261 - lib.mdDoc '' 262 - List of forwarded ports from the external interface to 263 - internal destinations by using DNAT. Destination can be 264 - IPv6 if IPv6 NAT is enabled. 265 - ''; 136 + description = lib.mdDoc '' 137 + List of forwarded ports from the external interface to 138 + internal destinations by using DNAT. Destination can be 139 + IPv6 if IPv6 NAT is enabled. 140 + ''; 266 141 }; 267 142 268 143 networking.nat.dmzHost = mkOption { 269 144 type = types.nullOr types.str; 270 145 default = null; 271 146 example = "10.0.0.1"; 272 - description = 273 - lib.mdDoc '' 274 - The local IP address to which all traffic that does not match any 275 - forwarding rule is forwarded. 276 - ''; 277 - }; 278 - 279 - networking.nat.extraCommands = mkOption { 280 - type = types.lines; 281 - default = ""; 282 - example = "iptables -A INPUT -p icmp -j ACCEPT"; 283 - description = 284 - lib.mdDoc '' 285 - Additional shell commands executed as part of the nat 286 - initialisation script. 287 - ''; 288 - }; 289 - 290 - networking.nat.extraStopCommands = mkOption { 291 - type = types.lines; 292 - default = ""; 293 - example = "iptables -D INPUT -p icmp -j ACCEPT || true"; 294 - description = 295 - lib.mdDoc '' 296 - Additional shell commands executed as part of the nat 297 - teardown script. 298 - ''; 147 + description = lib.mdDoc '' 148 + The local IP address to which all traffic that does not match any 149 + forwarding rule is forwarded. 150 + ''; 299 151 }; 300 152 301 153 }; 302 154 303 155 304 - ###### implementation 305 - 306 - config = mkMerge [ 307 - { networking.firewall.extraCommands = mkBefore flushNat; } 308 - (mkIf config.networking.nat.enable { 309 - 310 - assertions = [ 311 - { assertion = cfg.enableIPv6 -> config.networking.enableIPv6; 312 - message = "networking.nat.enableIPv6 requires networking.enableIPv6"; 313 - } 314 - { assertion = (cfg.dmzHost != null) -> (cfg.externalInterface != null); 315 - message = "networking.nat.dmzHost requires networking.nat.externalInterface"; 316 - } 317 - { assertion = (cfg.forwardPorts != []) -> (cfg.externalInterface != null); 318 - message = "networking.nat.forwardPorts requires networking.nat.externalInterface"; 319 - } 320 - ]; 156 + config = mkIf config.networking.nat.enable { 321 157 322 - # Use the same iptables package as in config.networking.firewall. 323 - # When the firewall is enabled, this should be deduplicated without any 324 - # error. 325 - environment.systemPackages = [ config.networking.firewall.package ]; 158 + assertions = [ 159 + { 160 + assertion = cfg.enableIPv6 -> config.networking.enableIPv6; 161 + message = "networking.nat.enableIPv6 requires networking.enableIPv6"; 162 + } 163 + { 164 + assertion = (cfg.dmzHost != null) -> (cfg.externalInterface != null); 165 + message = "networking.nat.dmzHost requires networking.nat.externalInterface"; 166 + } 167 + { 168 + assertion = (cfg.forwardPorts != [ ]) -> (cfg.externalInterface != null); 169 + message = "networking.nat.forwardPorts requires networking.nat.externalInterface"; 170 + } 171 + ]; 326 172 327 - boot = { 328 - kernelModules = [ "nf_nat_ftp" ]; 329 - kernel.sysctl = { 330 - "net.ipv4.conf.all.forwarding" = mkOverride 99 true; 331 - "net.ipv4.conf.default.forwarding" = mkOverride 99 true; 332 - } // optionalAttrs cfg.enableIPv6 { 333 - # Do not prevent IPv6 autoconfiguration. 334 - # See <http://strugglers.net/~andy/blog/2011/09/04/linux-ipv6-router-advertisements-and-forwarding/>. 335 - "net.ipv6.conf.all.accept_ra" = mkOverride 99 2; 336 - "net.ipv6.conf.default.accept_ra" = mkOverride 99 2; 173 + # Use the same iptables package as in config.networking.firewall. 174 + # When the firewall is enabled, this should be deduplicated without any 175 + # error. 176 + environment.systemPackages = [ config.networking.firewall.package ]; 337 177 338 - # Forward IPv6 packets. 339 - "net.ipv6.conf.all.forwarding" = mkOverride 99 true; 340 - "net.ipv6.conf.default.forwarding" = mkOverride 99 true; 341 - }; 342 - }; 178 + boot = { 179 + kernelModules = [ "nf_nat_ftp" ]; 180 + kernel.sysctl = { 181 + "net.ipv4.conf.all.forwarding" = mkOverride 99 true; 182 + "net.ipv4.conf.default.forwarding" = mkOverride 99 true; 183 + } // optionalAttrs cfg.enableIPv6 { 184 + # Do not prevent IPv6 autoconfiguration. 185 + # See <http://strugglers.net/~andy/blog/2011/09/04/linux-ipv6-router-advertisements-and-forwarding/>. 186 + "net.ipv6.conf.all.accept_ra" = mkOverride 99 2; 187 + "net.ipv6.conf.default.accept_ra" = mkOverride 99 2; 343 188 344 - networking.firewall = mkIf config.networking.firewall.enable { 345 - extraCommands = setupNat; 346 - extraStopCommands = flushNat; 189 + # Forward IPv6 packets. 190 + "net.ipv6.conf.all.forwarding" = mkOverride 99 true; 191 + "net.ipv6.conf.default.forwarding" = mkOverride 99 true; 347 192 }; 193 + }; 348 194 349 - systemd.services = mkIf (!config.networking.firewall.enable) { nat = { 350 - description = "Network Address Translation"; 351 - wantedBy = [ "network.target" ]; 352 - after = [ "network-pre.target" "systemd-modules-load.service" ]; 353 - path = [ config.networking.firewall.package ]; 354 - unitConfig.ConditionCapability = "CAP_NET_ADMIN"; 355 - 356 - serviceConfig = { 357 - Type = "oneshot"; 358 - RemainAfterExit = true; 359 - }; 360 - 361 - script = flushNat + setupNat; 362 - 363 - postStop = flushNat; 364 - }; }; 365 - }) 366 - ]; 195 + }; 367 196 }
+10 -16
nixos/modules/services/networking/nftables.nix
··· 12 12 default = false; 13 13 description = 14 14 lib.mdDoc '' 15 - Whether to enable nftables. nftables is a Linux-based packet 16 - filtering framework intended to replace frameworks like iptables. 17 - 18 - This conflicts with the standard networking firewall, so make sure to 19 - disable it before using nftables. 15 + Whether to enable nftables and use nftables based firewall if enabled. 16 + nftables is a Linux-based packet filtering framework intended to 17 + replace frameworks like iptables. 20 18 21 19 Note that if you have Docker enabled you will not be able to use 22 20 nftables without intervention. Docker uses iptables internally to ··· 79 77 lib.mdDoc '' 80 78 The ruleset to be used with nftables. Should be in a format that 81 79 can be loaded using "/bin/nft -f". The ruleset is updated atomically. 80 + This option conflicts with rulesetFile. 82 81 ''; 83 82 }; 84 83 networking.nftables.rulesetFile = mkOption { 85 - type = types.path; 86 - default = pkgs.writeTextFile { 87 - name = "nftables-rules"; 88 - text = cfg.ruleset; 89 - }; 90 - defaultText = literalMD ''a file with the contents of {option}`networking.nftables.ruleset`''; 84 + type = types.nullOr types.path; 85 + default = null; 91 86 description = 92 87 lib.mdDoc '' 93 88 The ruleset file to be used with nftables. Should be in a format that 94 89 can be loaded using "nft -f". The ruleset is updated atomically. 90 + This option conflicts with ruleset and nftables based firewall. 95 91 ''; 96 92 }; 97 93 }; ··· 99 95 ###### implementation 100 96 101 97 config = mkIf cfg.enable { 102 - assertions = [{ 103 - assertion = config.networking.firewall.enable == false; 104 - message = "You can not use nftables and iptables at the same time. networking.firewall.enable must be set to false."; 105 - }]; 106 98 boot.blacklistedKernelModules = [ "ip_tables" ]; 107 99 environment.systemPackages = [ pkgs.nftables ]; 108 100 networking.networkmanager.firewallBackend = mkDefault "nftables"; ··· 116 108 rulesScript = pkgs.writeScript "nftables-rules" '' 117 109 #! ${pkgs.nftables}/bin/nft -f 118 110 flush ruleset 119 - include "${cfg.rulesetFile}" 111 + ${if cfg.rulesetFile != null then '' 112 + include "${cfg.rulesetFile}" 113 + '' else cfg.ruleset} 120 114 ''; 121 115 in { 122 116 Type = "oneshot";
+5 -1
nixos/tests/all-tests.nix
··· 211 211 firefox-esr = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr; }; # used in `tested` job 212 212 firefox-esr-102 = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr-102; }; 213 213 firejail = handleTest ./firejail.nix {}; 214 - firewall = handleTest ./firewall.nix {}; 214 + firewall = handleTest ./firewall.nix { nftables = false; }; 215 + firewall-nftables = handleTest ./firewall.nix { nftables = true; }; 215 216 fish = handleTest ./fish.nix {}; 216 217 flannel = handleTestOn ["x86_64-linux"] ./flannel.nix {}; 217 218 fluentd = handleTest ./fluentd.nix {}; ··· 412 413 nat.firewall = handleTest ./nat.nix { withFirewall = true; }; 413 414 nat.firewall-conntrack = handleTest ./nat.nix { withFirewall = true; withConntrackHelpers = true; }; 414 415 nat.standalone = handleTest ./nat.nix { withFirewall = false; }; 416 + nat.nftables.firewall = handleTest ./nat.nix { withFirewall = true; nftables = true; }; 417 + nat.nftables.firewall-conntrack = handleTest ./nat.nix { withFirewall = true; withConntrackHelpers = true; nftables = true; }; 418 + nat.nftables.standalone = handleTest ./nat.nix { withFirewall = false; nftables = true; }; 415 419 nats = handleTest ./nats.nix {}; 416 420 navidrome = handleTest ./navidrome.nix {}; 417 421 nbd = handleTest ./nbd.nix {};
+8 -5
nixos/tests/firewall.nix
··· 1 1 # Test the firewall module. 2 2 3 - import ./make-test-python.nix ( { pkgs, ... } : { 4 - name = "firewall"; 3 + import ./make-test-python.nix ( { pkgs, nftables, ... } : { 4 + name = "firewall" + pkgs.lib.optionalString nftables "-nftables"; 5 5 meta = with pkgs.lib.maintainers; { 6 6 maintainers = [ eelco ]; 7 7 }; ··· 11 11 { ... }: 12 12 { networking.firewall.enable = true; 13 13 networking.firewall.logRefusedPackets = true; 14 + networking.nftables.enable = nftables; 14 15 services.httpd.enable = true; 15 16 services.httpd.adminAddr = "foo@example.org"; 16 17 }; ··· 23 24 { ... }: 24 25 { networking.firewall.enable = true; 25 26 networking.firewall.rejectPackets = true; 27 + networking.nftables.enable = nftables; 26 28 }; 27 29 28 30 attacker = ··· 35 37 36 38 testScript = { nodes, ... }: let 37 39 newSystem = nodes.walled2.config.system.build.toplevel; 40 + unit = if nftables then "nftables" else "firewall"; 38 41 in '' 39 42 start_all() 40 43 41 - walled.wait_for_unit("firewall") 44 + walled.wait_for_unit("${unit}") 42 45 walled.wait_for_unit("httpd") 43 46 attacker.wait_for_unit("network.target") 44 47 ··· 54 57 walled.succeed("ping -c 1 attacker >&2") 55 58 56 59 # If we stop the firewall, then connections should succeed. 57 - walled.stop_job("firewall") 60 + walled.stop_job("${unit}") 58 61 attacker.succeed("curl -v http://walled/ >&2") 59 62 60 63 # Check whether activation of a new configuration reloads the firewall. 61 64 walled.succeed( 62 - "${newSystem}/bin/switch-to-configuration test 2>&1 | grep -qF firewall.service" 65 + "${newSystem}/bin/switch-to-configuration test 2>&1 | grep -qF ${unit}.service" 63 66 ) 64 67 ''; 65 68 })
+8 -4
nixos/tests/nat.nix
··· 3 3 # client on the inside network, a server on the outside network, and a 4 4 # router connected to both that performs Network Address Translation 5 5 # for the client. 6 - import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ? false, ... }: 6 + import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ? false, nftables ? false, ... }: 7 7 let 8 - unit = if withFirewall then "firewall" else "nat"; 8 + unit = if nftables then "nftables" else (if withFirewall then "firewall" else "nat"); 9 9 10 10 routerBase = 11 11 lib.mkMerge [ 12 12 { virtualisation.vlans = [ 2 1 ]; 13 13 networking.firewall.enable = withFirewall; 14 + networking.firewall.filterForward = nftables; 15 + networking.nftables.enable = nftables; 14 16 networking.nat.internalIPs = [ "192.168.1.0/24" ]; 15 17 networking.nat.externalInterface = "eth1"; 16 18 } ··· 21 23 ]; 22 24 in 23 25 { 24 - name = "nat" + (if withFirewall then "WithFirewall" else "Standalone") 26 + name = "nat" + (lib.optionalString nftables "Nftables") 27 + + (if withFirewall then "WithFirewall" else "Standalone") 25 28 + (lib.optionalString withConntrackHelpers "withConntrackHelpers"); 26 29 meta = with pkgs.lib.maintainers; { 27 30 maintainers = [ eelco rob ]; ··· 34 37 { virtualisation.vlans = [ 1 ]; 35 38 networking.defaultGateway = 36 39 (pkgs.lib.head nodes.router.config.networking.interfaces.eth2.ipv4.addresses).address; 40 + networking.nftables.enable = nftables; 37 41 } 38 42 (lib.optionalAttrs withConntrackHelpers { 39 43 networking.firewall.connectionTrackingModules = [ "ftp" ]; ··· 111 115 # FIXME: this should not be necessary, but nat.service is not started because 112 116 # network.target is not triggered 113 117 # (https://github.com/NixOS/nixpkgs/issues/16230#issuecomment-226408359) 114 - ${lib.optionalString (!withFirewall) '' 118 + ${lib.optionalString (!withFirewall && !nftables) '' 115 119 router.succeed("systemctl start nat.service") 116 120 ''} 117 121 client.succeed("curl --fail http://server/ >&2")