at 25.11-pre 12 kB view raw
1/* 2 This module enables a simple firewall. 3 4 The firewall can be customised in arbitrary ways by setting 5 networking.firewall.extraCommands. For modularity, the firewall 6 uses several chains: 7 8 - nixos-fw is the main chain for input packet processing. 9 10 - nixos-fw-accept is called for accepted packets. If you want 11 additional logging, or want to reject certain packets anyway, you 12 can insert rules at the start of this chain. 13 14 - nixos-fw-log-refuse and nixos-fw-refuse are called for 15 refused packets. (The former jumps to the latter after logging 16 the packet.) If you want additional logging, or want to accept 17 certain packets anyway, you can insert rules at the start of 18 this chain. 19 20 - nixos-fw-rpfilter is used as the main chain in the mangle table, 21 called from the built-in PREROUTING chain. If the kernel 22 supports it and `cfg.checkReversePath` is set this chain will 23 perform a reverse path filter test. 24 25 - nixos-drop is used while reloading the firewall in order to drop 26 all traffic. Since reloading isn't implemented in an atomic way 27 this'll prevent any traffic from leaking through while reloading 28 the firewall. However, if the reloading fails, the firewall-stop 29 script will be called which in return will effectively disable the 30 complete firewall (in the default configuration). 31*/ 32{ 33 config, 34 lib, 35 pkgs, 36 ... 37}: 38let 39 40 cfg = config.networking.firewall; 41 42 inherit (config.boot.kernelPackages) kernel; 43 44 kernelHasRPFilter = 45 ((kernel.config.isEnabled or (x: false)) "IP_NF_MATCH_RPFILTER") 46 || (kernel.features.netfilterRPFilter or false); 47 48 helpers = import ./helpers.nix { inherit config lib; }; 49 50 writeShScript = 51 name: text: 52 let 53 dir = pkgs.writeScriptBin name '' 54 #! ${pkgs.runtimeShell} -e 55 ${text} 56 ''; 57 in 58 "${dir}/bin/${name}"; 59 60 startScript = writeShScript "firewall-start" '' 61 ${helpers} 62 63 # Flush the old firewall rules. !!! Ideally, updating the 64 # firewall would be atomic. Apparently that's possible 65 # with iptables-restore. 66 ip46tables -D INPUT -j nixos-fw 2> /dev/null || true 67 for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse; do 68 ip46tables -F "$chain" 2> /dev/null || true 69 ip46tables -X "$chain" 2> /dev/null || true 70 done 71 72 73 # The "nixos-fw-accept" chain just accepts packets. 74 ip46tables -N nixos-fw-accept 75 ip46tables -A nixos-fw-accept -j ACCEPT 76 77 78 # The "nixos-fw-refuse" chain rejects or drops packets. 79 ip46tables -N nixos-fw-refuse 80 81 ${ 82 if cfg.rejectPackets then 83 '' 84 # Send a reset for existing TCP connections that we've 85 # somehow forgotten about. Send ICMP "port unreachable" 86 # for everything else. 87 ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset 88 ip46tables -A nixos-fw-refuse -j REJECT 89 '' 90 else 91 '' 92 ip46tables -A nixos-fw-refuse -j DROP 93 '' 94 } 95 96 97 # The "nixos-fw-log-refuse" chain performs logging, then 98 # jumps to the "nixos-fw-refuse" chain. 99 ip46tables -N nixos-fw-log-refuse 100 101 ${lib.optionalString cfg.logRefusedConnections '' 102 ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "refused connection: " 103 ''} 104 ${lib.optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) '' 105 ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \ 106 -j LOG --log-level info --log-prefix "refused broadcast: " 107 ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \ 108 -j LOG --log-level info --log-prefix "refused multicast: " 109 ''} 110 ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse 111 ${lib.optionalString cfg.logRefusedPackets '' 112 ip46tables -A nixos-fw-log-refuse \ 113 -j LOG --log-level info --log-prefix "refused packet: " 114 ''} 115 ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse 116 117 118 # The "nixos-fw" chain does the actual work. 119 ip46tables -N nixos-fw 120 121 # Clean up rpfilter rules 122 ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true 123 ip46tables -t mangle -F nixos-fw-rpfilter 2> /dev/null || true 124 ip46tables -t mangle -X nixos-fw-rpfilter 2> /dev/null || true 125 126 ${lib.optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) '' 127 # Perform a reverse-path test to refuse spoofers 128 # For now, we just drop, as the mangle table doesn't have a log-refuse yet 129 ip46tables -t mangle -N nixos-fw-rpfilter 2> /dev/null || true 130 ip46tables -t mangle -A nixos-fw-rpfilter -m rpfilter --validmark ${ 131 lib.optionalString (cfg.checkReversePath == "loose") "--loose" 132 } -j RETURN 133 134 # Allows this host to act as a DHCP4 client without first having to use APIPA 135 iptables -t mangle -A nixos-fw-rpfilter -p udp --sport 67 --dport 68 -j RETURN 136 137 # Allows this host to act as a DHCPv4 server 138 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 139 140 ${lib.optionalString cfg.logReversePathDrops '' 141 ip46tables -t mangle -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: " 142 ''} 143 ip46tables -t mangle -A nixos-fw-rpfilter -j DROP 144 145 ip46tables -t mangle -A PREROUTING -j nixos-fw-rpfilter 146 ''} 147 148 # Accept all traffic on the trusted interfaces. 149 ${lib.flip lib.concatMapStrings cfg.trustedInterfaces (iface: '' 150 ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept 151 '')} 152 153 # Accept packets from established or related connections. 154 ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept 155 156 # Accept connections to the allowed TCP ports. 157 ${lib.concatStrings ( 158 lib.mapAttrsToList ( 159 iface: cfg: 160 lib.concatMapStrings (port: '' 161 ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept ${ 162 lib.optionalString (iface != "default") "-i ${iface}" 163 } 164 '') cfg.allowedTCPPorts 165 ) cfg.allInterfaces 166 )} 167 168 # Accept connections to the allowed TCP port ranges. 169 ${lib.concatStrings ( 170 lib.mapAttrsToList ( 171 iface: cfg: 172 lib.concatMapStrings ( 173 rangeAttr: 174 let 175 range = toString rangeAttr.from + ":" + toString rangeAttr.to; 176 in 177 '' 178 ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept ${ 179 lib.optionalString (iface != "default") "-i ${iface}" 180 } 181 '' 182 ) cfg.allowedTCPPortRanges 183 ) cfg.allInterfaces 184 )} 185 186 # Accept packets on the allowed UDP ports. 187 ${lib.concatStrings ( 188 lib.mapAttrsToList ( 189 iface: cfg: 190 lib.concatMapStrings (port: '' 191 ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept ${ 192 lib.optionalString (iface != "default") "-i ${iface}" 193 } 194 '') cfg.allowedUDPPorts 195 ) cfg.allInterfaces 196 )} 197 198 # Accept packets on the allowed UDP port ranges. 199 ${lib.concatStrings ( 200 lib.mapAttrsToList ( 201 iface: cfg: 202 lib.concatMapStrings ( 203 rangeAttr: 204 let 205 range = toString rangeAttr.from + ":" + toString rangeAttr.to; 206 in 207 '' 208 ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept ${ 209 lib.optionalString (iface != "default") "-i ${iface}" 210 } 211 '' 212 ) cfg.allowedUDPPortRanges 213 ) cfg.allInterfaces 214 )} 215 216 # Optionally respond to ICMPv4 pings. 217 ${lib.optionalString cfg.allowPing '' 218 iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${ 219 lib.optionalString (cfg.pingLimit != null) "-m limit ${cfg.pingLimit} " 220 }-j nixos-fw-accept 221 ''} 222 223 ${lib.optionalString config.networking.enableIPv6 '' 224 # Accept all ICMPv6 messages except redirects and node 225 # information queries (type 139). See RFC 4890, section 226 # 4.4. 227 ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP 228 ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP 229 ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept 230 231 # Allow this host to act as a DHCPv6 client 232 ip6tables -A nixos-fw -d fe80::/64 -p udp --dport 546 -j nixos-fw-accept 233 ''} 234 235 ${cfg.extraCommands} 236 237 # Reject/drop everything else. 238 ip46tables -A nixos-fw -j nixos-fw-log-refuse 239 240 241 # Enable the firewall. 242 ip46tables -A INPUT -j nixos-fw 243 ''; 244 245 stopScript = writeShScript "firewall-stop" '' 246 ${helpers} 247 248 # Clean up in case reload fails 249 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 250 251 # Clean up after added ruleset 252 ip46tables -D INPUT -j nixos-fw 2>/dev/null || true 253 254 ${lib.optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) '' 255 ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true 256 ''} 257 258 ${cfg.extraStopCommands} 259 ''; 260 261 reloadScript = writeShScript "firewall-reload" '' 262 ${helpers} 263 264 # Create a unique drop rule 265 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 266 ip46tables -F nixos-drop 2>/dev/null || true 267 ip46tables -X nixos-drop 2>/dev/null || true 268 ip46tables -N nixos-drop 269 ip46tables -A nixos-drop -j DROP 270 271 # Don't allow traffic to leak out until the script has completed 272 ip46tables -A INPUT -j nixos-drop 273 274 ${cfg.extraStopCommands} 275 276 if ${startScript}; then 277 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 278 else 279 echo "Failed to reload firewall... Stopping" 280 ${stopScript} 281 exit 1 282 fi 283 ''; 284 285in 286 287{ 288 289 options = { 290 291 networking.firewall = { 292 extraCommands = lib.mkOption { 293 type = lib.types.lines; 294 default = ""; 295 example = "iptables -A INPUT -p icmp -j ACCEPT"; 296 description = '' 297 Additional shell commands executed as part of the firewall 298 initialisation script. These are executed just before the 299 final "reject" firewall rule is added, so they can be used 300 to allow packets that would otherwise be refused. 301 302 This option only works with the iptables based firewall. 303 ''; 304 }; 305 306 extraStopCommands = lib.mkOption { 307 type = lib.types.lines; 308 default = ""; 309 example = "iptables -P INPUT ACCEPT"; 310 description = '' 311 Additional shell commands executed as part of the firewall 312 shutdown script. These are executed just after the removal 313 of the NixOS input rule, or if the service enters a failed 314 state. 315 316 This option only works with the iptables based firewall. 317 ''; 318 }; 319 }; 320 321 }; 322 323 # FIXME: Maybe if `enable' is false, the firewall should still be 324 # built but not started by default? 325 config = lib.mkIf (cfg.enable && config.networking.nftables.enable == false) { 326 327 assertions = [ 328 # This is approximately "checkReversePath -> kernelHasRPFilter", 329 # but the checkReversePath option can include non-boolean 330 # values. 331 { 332 assertion = cfg.checkReversePath == false || kernelHasRPFilter; 333 message = "This kernel does not support rpfilter"; 334 } 335 ]; 336 337 networking.firewall.checkReversePath = lib.mkIf (!kernelHasRPFilter) (lib.mkDefault false); 338 339 systemd.services.firewall = { 340 description = "Firewall"; 341 wantedBy = [ "sysinit.target" ]; 342 wants = [ "network-pre.target" ]; 343 after = [ "systemd-modules-load.service" ]; 344 before = [ 345 "network-pre.target" 346 "shutdown.target" 347 ]; 348 conflicts = [ "shutdown.target" ]; 349 350 path = [ cfg.package ] ++ cfg.extraPackages; 351 352 # FIXME: this module may also try to load kernel modules, but 353 # containers don't have CAP_SYS_MODULE. So the host system had 354 # better have all necessary modules already loaded. 355 unitConfig.ConditionCapability = "CAP_NET_ADMIN"; 356 unitConfig.DefaultDependencies = false; 357 358 reloadIfChanged = true; 359 360 serviceConfig = { 361 Type = "oneshot"; 362 RemainAfterExit = true; 363 ExecStart = "@${startScript} firewall-start"; 364 ExecReload = "@${reloadScript} firewall-reload"; 365 ExecStop = "@${stopScript} firewall-stop"; 366 }; 367 }; 368 369 }; 370 371}