at 16.09-beta 17 kB view raw
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-input is the main chain for input packet processing. 8 9 - nixos-fw-log-refuse and nixos-fw-refuse are called for 10 refused packets. (The former jumps to the latter after logging 11 the packet.) If you want additional logging, or want to accept 12 certain packets anyway, you can insert rules at the start of 13 these chain. 14 15 - nixos-fw-accept is called for accepted packets. If you want 16 additional logging, or want to reject certain packets anyway, you 17 can insert rules at the start of this chain. 18 19*/ 20 21{ config, lib, pkgs, ... }: 22 23with lib; 24 25let 26 27 cfg = config.networking.firewall; 28 29 helpers = 30 '' 31 # Helper command to manipulate both the IPv4 and IPv6 tables. 32 ip46tables() { 33 iptables -w "$@" 34 ${optionalString config.networking.enableIPv6 '' 35 ip6tables -w "$@" 36 ''} 37 } 38 ''; 39 40 writeShScript = name: text: let dir = pkgs.writeScriptBin name '' 41 #! ${pkgs.stdenv.shell} -e 42 ${text} 43 ''; in "${dir}/bin/${name}"; 44 45 startScript = writeShScript "firewall-start" '' 46 ${helpers} 47 48 # Flush the old firewall rules. !!! Ideally, updating the 49 # firewall would be atomic. Apparently that's possible 50 # with iptables-restore. 51 ip46tables -D INPUT -j nixos-fw 2> /dev/null || true 52 for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse FW_REFUSE; do 53 ip46tables -F "$chain" 2> /dev/null || true 54 ip46tables -X "$chain" 2> /dev/null || true 55 done 56 57 58 # The "nixos-fw-accept" chain just accepts packets. 59 ip46tables -N nixos-fw-accept 60 ip46tables -A nixos-fw-accept -j ACCEPT 61 62 63 # The "nixos-fw-refuse" chain rejects or drops packets. 64 ip46tables -N nixos-fw-refuse 65 66 ${if cfg.rejectPackets then '' 67 # Send a reset for existing TCP connections that we've 68 # somehow forgotten about. Send ICMP "port unreachable" 69 # for everything else. 70 ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset 71 ip46tables -A nixos-fw-refuse -j REJECT 72 '' else '' 73 ip46tables -A nixos-fw-refuse -j DROP 74 ''} 75 76 77 # The "nixos-fw-log-refuse" chain performs logging, then 78 # jumps to the "nixos-fw-refuse" chain. 79 ip46tables -N nixos-fw-log-refuse 80 81 ${optionalString cfg.logRefusedConnections '' 82 ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "rejected connection: " 83 ''} 84 ${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) '' 85 ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \ 86 -j LOG --log-level info --log-prefix "rejected broadcast: " 87 ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \ 88 -j LOG --log-level info --log-prefix "rejected multicast: " 89 ''} 90 ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse 91 ${optionalString cfg.logRefusedPackets '' 92 ip46tables -A nixos-fw-log-refuse \ 93 -j LOG --log-level info --log-prefix "rejected packet: " 94 ''} 95 ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse 96 97 98 # The "nixos-fw" chain does the actual work. 99 ip46tables -N nixos-fw 100 101 # Perform a reverse-path test to refuse spoofers 102 # For now, we just drop, as the raw table doesn't have a log-refuse yet 103 ${optionalString (kernelHasRPFilter && cfg.checkReversePath) '' 104 # Clean up rpfilter rules 105 ip46tables -t raw -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true 106 ip46tables -t raw -F nixos-fw-rpfilter 2> /dev/null || true 107 ip46tables -t raw -N nixos-fw-rpfilter 2> /dev/null || true 108 109 ip46tables -t raw -A nixos-fw-rpfilter -m rpfilter -j RETURN 110 111 # Allows this host to act as a DHCPv4 server 112 iptables -t raw -A nixos-fw-rpfilter -s 0.0.0.0 -d 255.255.255.255 -p udp --sport 68 --dport 67 -j RETURN 113 114 ${optionalString cfg.logReversePathDrops '' 115 ip46tables -t raw -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: " 116 ''} 117 ip46tables -t raw -A nixos-fw-rpfilter -j DROP 118 119 ip46tables -t raw -A PREROUTING -j nixos-fw-rpfilter 120 ''} 121 122 # Accept all traffic on the trusted interfaces. 123 ${flip concatMapStrings cfg.trustedInterfaces (iface: '' 124 ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept 125 '')} 126 127 # Accept packets from established or related connections. 128 ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept 129 130 # Accept connections to the allowed TCP ports. 131 ${concatMapStrings (port: 132 '' 133 ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept 134 '' 135 ) cfg.allowedTCPPorts 136 } 137 138 # Accept connections to the allowed TCP port ranges. 139 ${concatMapStrings (rangeAttr: 140 let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in 141 '' 142 ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept 143 '' 144 ) cfg.allowedTCPPortRanges 145 } 146 147 # Accept packets on the allowed UDP ports. 148 ${concatMapStrings (port: 149 '' 150 ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept 151 '' 152 ) cfg.allowedUDPPorts 153 } 154 155 # Accept packets on the allowed UDP port ranges. 156 ${concatMapStrings (rangeAttr: 157 let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in 158 '' 159 ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept 160 '' 161 ) cfg.allowedUDPPortRanges 162 } 163 164 # Accept IPv4 multicast. Not a big security risk since 165 # probably nobody is listening anyway. 166 #iptables -A nixos-fw -d 224.0.0.0/4 -j nixos-fw-accept 167 168 # Optionally respond to ICMPv4 pings. 169 ${optionalString cfg.allowPing '' 170 iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null) 171 "-m limit ${cfg.pingLimit} " 172 }-j nixos-fw-accept 173 ''} 174 175 # Accept all ICMPv6 messages except redirects and node 176 # information queries (type 139). See RFC 4890, section 177 # 4.4. 178 ${optionalString config.networking.enableIPv6 '' 179 ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP 180 ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP 181 ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept 182 ''} 183 184 ${cfg.extraCommands} 185 186 # Reject/drop everything else. 187 ip46tables -A nixos-fw -j nixos-fw-log-refuse 188 189 190 # Enable the firewall. 191 ip46tables -A INPUT -j nixos-fw 192 ''; 193 194 stopScript = writeShScript "firewall-stop" '' 195 ${helpers} 196 197 # Clean up in case reload fails 198 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 199 200 # Clean up after added ruleset 201 ip46tables -D INPUT -j nixos-fw 2>/dev/null || true 202 203 ${optionalString (kernelHasRPFilter && cfg.checkReversePath) '' 204 ip46tables -t raw -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true 205 ''} 206 207 ${cfg.extraStopCommands} 208 ''; 209 210 reloadScript = writeShScript "firewall-reload" '' 211 ${helpers} 212 213 # Create a unique drop rule 214 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 215 ip46tables -F nixos-drop 2>/dev/null || true 216 ip46tables -X nixos-drop 2>/dev/null || true 217 ip46tables -N nixos-drop 218 ip46tables -A nixos-drop -j DROP 219 220 # Don't allow traffic to leak out until the script has completed 221 ip46tables -A INPUT -j nixos-drop 222 if ${startScript}; then 223 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 224 else 225 echo "Failed to reload firewall... Stopping" 226 ${stopScript} 227 exit 1 228 fi 229 ''; 230 231 kernelPackages = config.boot.kernelPackages; 232 233 kernelHasRPFilter = kernelPackages.kernel.features.netfilterRPFilter or false; 234 kernelCanDisableHelpers = kernelPackages.kernel.features.canDisableNetfilterConntrackHelpers or false; 235 236in 237 238{ 239 240 ###### interface 241 242 options = { 243 244 networking.firewall.enable = mkOption { 245 type = types.bool; 246 default = true; 247 description = 248 '' 249 Whether to enable the firewall. This is a simple stateful 250 firewall that blocks connection attempts to unauthorised TCP 251 or UDP ports on this machine. It does not affect packet 252 forwarding. 253 ''; 254 }; 255 256 networking.firewall.logRefusedConnections = mkOption { 257 type = types.bool; 258 default = true; 259 description = 260 '' 261 Whether to log rejected or dropped incoming connections. 262 ''; 263 }; 264 265 networking.firewall.logRefusedPackets = mkOption { 266 type = types.bool; 267 default = false; 268 description = 269 '' 270 Whether to log all rejected or dropped incoming packets. 271 This tends to give a lot of log messages, so it's mostly 272 useful for debugging. 273 ''; 274 }; 275 276 networking.firewall.logRefusedUnicastsOnly = mkOption { 277 type = types.bool; 278 default = true; 279 description = 280 '' 281 If <option>networking.firewall.logRefusedPackets</option> 282 and this option are enabled, then only log packets 283 specifically directed at this machine, i.e., not broadcasts 284 or multicasts. 285 ''; 286 }; 287 288 networking.firewall.rejectPackets = mkOption { 289 type = types.bool; 290 default = false; 291 description = 292 '' 293 If set, forbidden packets are rejected rather than dropped 294 (ignored). This means that an ICMP "port unreachable" error 295 message is sent back to the client. Rejecting packets makes 296 port scanning somewhat easier. 297 ''; 298 }; 299 300 networking.firewall.trustedInterfaces = mkOption { 301 type = types.listOf types.str; 302 description = 303 '' 304 Traffic coming in from these interfaces will be accepted 305 unconditionally. 306 ''; 307 }; 308 309 networking.firewall.allowedTCPPorts = mkOption { 310 default = []; 311 example = [ 22 80 ]; 312 type = types.listOf types.int; 313 description = 314 '' 315 List of TCP ports on which incoming connections are 316 accepted. 317 ''; 318 }; 319 320 networking.firewall.allowedTCPPortRanges = mkOption { 321 default = []; 322 example = [ { from = 8999; to = 9003; } ]; 323 type = types.listOf (types.attrsOf types.int); 324 description = 325 '' 326 A range of TCP ports on which incoming connections are 327 accepted. 328 ''; 329 }; 330 331 networking.firewall.allowedUDPPorts = mkOption { 332 default = []; 333 example = [ 53 ]; 334 type = types.listOf types.int; 335 description = 336 '' 337 List of open UDP ports. 338 ''; 339 }; 340 341 networking.firewall.allowedUDPPortRanges = mkOption { 342 default = []; 343 example = [ { from = 60000; to = 61000; } ]; 344 type = types.listOf (types.attrsOf types.int); 345 description = 346 '' 347 Range of open UDP ports. 348 ''; 349 }; 350 351 networking.firewall.allowPing = mkOption { 352 default = true; 353 type = types.bool; 354 description = 355 '' 356 Whether to respond to incoming ICMPv4 echo requests 357 ("pings"). ICMPv6 pings are always allowed because the 358 larger address space of IPv6 makes network scanning much 359 less effective. 360 ''; 361 }; 362 363 networking.firewall.pingLimit = mkOption { 364 default = null; 365 type = types.nullOr (types.separatedString " "); 366 description = 367 '' 368 If pings are allowed, this allows setting rate limits 369 on them. If non-null, this option should be in the form 370 of flags like "--limit 1/minute --limit-burst 5" 371 ''; 372 }; 373 374 networking.firewall.checkReversePath = mkOption { 375 default = kernelHasRPFilter; 376 type = types.bool; 377 description = 378 '' 379 Performs a reverse path filter test on a packet. 380 If a reply to the packet would not be sent via the same interface 381 that the packet arrived on, it is refused. 382 383 If using asymmetric routing or other complicated routing, 384 disable this setting and setup your own counter-measures. 385 386 (needs kernel 3.3+) 387 ''; 388 }; 389 390 networking.firewall.logReversePathDrops = mkOption { 391 default = false; 392 type = types.bool; 393 description = 394 '' 395 Logs dropped packets failing the reverse path filter test if 396 the option networking.firewall.checkReversePath is enabled. 397 ''; 398 }; 399 400 networking.firewall.connectionTrackingModules = mkOption { 401 default = [ "ftp" ]; 402 example = [ "ftp" "irc" "sane" "sip" "tftp" "amanda" "h323" "netbios_sn" "pptp" "snmp" ]; 403 type = types.listOf types.str; 404 description = 405 '' 406 List of connection-tracking helpers that are auto-loaded. 407 The complete list of possible values is given in the example. 408 409 As helpers can pose as a security risk, it is advised to 410 set this to an empty list and disable the setting 411 networking.firewall.autoLoadConntrackHelpers 412 413 Loading of helpers is recommended to be done through the new 414 CT target. More info: 415 https://home.regit.org/netfilter-en/secure-use-of-helpers/ 416 ''; 417 }; 418 419 networking.firewall.autoLoadConntrackHelpers = mkOption { 420 default = true; 421 type = types.bool; 422 description = 423 '' 424 Whether to auto-load connection-tracking helpers. 425 See the description at networking.firewall.connectionTrackingModules 426 427 (needs kernel 3.5+) 428 ''; 429 }; 430 431 networking.firewall.extraCommands = mkOption { 432 type = types.lines; 433 default = ""; 434 example = "iptables -A INPUT -p icmp -j ACCEPT"; 435 description = 436 '' 437 Additional shell commands executed as part of the firewall 438 initialisation script. These are executed just before the 439 final "reject" firewall rule is added, so they can be used 440 to allow packets that would otherwise be refused. 441 ''; 442 }; 443 444 networking.firewall.extraPackages = mkOption { 445 type = types.listOf types.package; 446 default = [ ]; 447 example = literalExample "[ pkgs.ipset ]"; 448 description = 449 '' 450 Additional packages to be included in the environment of the system 451 as well as the path of networking.firewall.extraCommands. 452 ''; 453 }; 454 455 networking.firewall.extraStopCommands = mkOption { 456 type = types.lines; 457 default = ""; 458 example = "iptables -P INPUT ACCEPT"; 459 description = 460 '' 461 Additional shell commands executed as part of the firewall 462 shutdown script. These are executed just after the removal 463 of the nixos input rule, or if the service enters a failed state. 464 ''; 465 }; 466 467 }; 468 469 470 ###### implementation 471 472 # FIXME: Maybe if `enable' is false, the firewall should still be 473 # built but not started by default? 474 config = mkIf cfg.enable { 475 476 networking.firewall.trustedInterfaces = [ "lo" ]; 477 478 environment.systemPackages = [ pkgs.iptables ] ++ cfg.extraPackages; 479 480 boot.kernelModules = map (x: "nf_conntrack_${x}") cfg.connectionTrackingModules; 481 boot.extraModprobeConfig = optionalString (!cfg.autoLoadConntrackHelpers) '' 482 options nf_conntrack nf_conntrack_helper=0 483 ''; 484 485 assertions = [ { assertion = ! cfg.checkReversePath || kernelHasRPFilter; 486 message = "This kernel does not support rpfilter"; } 487 { assertion = cfg.autoLoadConntrackHelpers || kernelCanDisableHelpers; 488 message = "This kernel does not support disabling conntrack helpers"; } 489 ]; 490 491 systemd.services.firewall = { 492 description = "Firewall"; 493 wantedBy = [ "network-pre.target" ]; 494 before = [ "network-pre.target" ]; 495 after = [ "systemd-modules-load.service" ]; 496 497 path = [ pkgs.iptables ] ++ cfg.extraPackages; 498 499 # FIXME: this module may also try to load kernel modules, but 500 # containers don't have CAP_SYS_MODULE. So the host system had 501 # better have all necessary modules already loaded. 502 unitConfig.ConditionCapability = "CAP_NET_ADMIN"; 503 504 reloadIfChanged = true; 505 506 serviceConfig = { 507 Type = "oneshot"; 508 RemainAfterExit = true; 509 ExecStart = "@${startScript} firewall-start"; 510 ExecReload = "@${reloadScript} firewall-reload"; 511 ExecStop = "@${stopScript} firewall-stop"; 512 }; 513 }; 514 515 }; 516 517}