diff --git a/nixos/doc/manual/release-notes/rl-2511.section.md b/nixos/doc/manual/release-notes/rl-2511.section.md index e8d8392a45e35..142679661d68f 100644 --- a/nixos/doc/manual/release-notes/rl-2511.section.md +++ b/nixos/doc/manual/release-notes/rl-2511.section.md @@ -32,6 +32,10 @@ Refer to the [GNOME release notes](https://release.gnome.org/49/) for more details. +- FirewallD support has been added. It can be configured both as a standalone service (through `services.firewalld`), and as a backend to the existing `networking.firewall` options. + +- `networking.firewall` now has a `backend` option for choosing which backend to use. + ## New Modules {#sec-release-25.11-new-modules} @@ -53,6 +57,8 @@ - [umami](https://github.com/umami-software/umami), a simple, fast, privacy-focused alternative to Google Analytics. Available with [services.umami](#opt-services.umami.enable). +- [FirewallD](https://firewalld.org/), a firewall daemon with D-Bus interface providing a dynamic firewall. Available as [services.firewalld](#opt-services.firewalld.enable) and a [networking.firewall.backend](#opt-networking.firewall.backend). + - [FileBrowser](https://filebrowser.org/), a web application for managing and sharing files. Available as [services.filebrowser](#opt-services.filebrowser.enable). - Options under [networking.getaddrinfo](#opt-networking.getaddrinfo.enable) are now allowed to declaratively configure address selection and sorting behavior of `getaddrinfo` in dual-stack networks. diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 0b9bd6d976f64..d0a4f0b7db128 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1162,9 +1162,11 @@ ./services/networking/ferm.nix ./services/networking/firefox-syncserver.nix ./services/networking/fireqos.nix + ./services/networking/firewall-firewalld.nix ./services/networking/firewall-iptables.nix ./services/networking/firewall-nftables.nix ./services/networking/firewall.nix + ./services/networking/firewalld ./services/networking/firezone/gateway.nix ./services/networking/firezone/gui-client.nix ./services/networking/firezone/headless-client.nix diff --git a/nixos/modules/services/networking/firewall-firewalld.nix b/nixos/modules/services/networking/firewall-firewalld.nix new file mode 100644 index 0000000000000..2db34eb1cad1d --- /dev/null +++ b/nixos/modules/services/networking/firewall-firewalld.nix @@ -0,0 +1,61 @@ +{ config, lib, ... }: + +let + cfg = config.networking.firewall; +in +{ + config = lib.mkIf (cfg.enable && cfg.backend == "firewalld") { + assertions = [ + { + assertion = cfg.interfaces == { }; + message = '' + Per interface configurations is not supported with the firewalld based firewall. + Create zones with `services.firewalld.zones` instead. + ''; + } + ]; + + boot.kernel.sysctl."net.ipv4.conf.all.rp_filter" = + if cfg.checkReversePath == false then + 0 + else if cfg.checkReversePath == "loose" then + 1 + else + 2; + + services.firewalld = { + settings = { + DefaultZone = lib.mkDefault "nixos-fw-default"; + LogDenied = + if cfg.logRefusedConnections then + (if cfg.logRefusedUnicastsOnly then "unicast" else "all") + else + "off"; + IPv6_rpfilter = + if cfg.checkReversePath == false then + "no" + else + let + mode = if cfg.checkReversePath == true then "strict" else cfg.checkReversePath; + suffix = if cfg.filterForward then "" else "-forward"; + in + "${mode}${suffix}"; + }; + zones = { + nixos-fw-default = { + target = if cfg.rejectPackets then "%%REJECT%%" else "DROP"; + icmpBlockInversion = true; + icmpBlocks = lib.mkIf cfg.allowPing [ "echo-request" ]; + ports = + let + f = protocol: port: { inherit protocol port; }; + tcpPorts = map (f "tcp") (cfg.allowedTCPPorts ++ cfg.allowedTCPPortRanges); + udpPorts = map (f "udp") (cfg.allowedUDPPorts ++ cfg.allowedUDPPortRanges); + in + tcpPorts ++ udpPorts; + }; + trusted.interfaces = cfg.trustedInterfaces; + }; + }; + }; +} diff --git a/nixos/modules/services/networking/firewall-iptables.nix b/nixos/modules/services/networking/firewall-iptables.nix index 53d7d3434daee..36b4cfc6f9b7f 100644 --- a/nixos/modules/services/networking/firewall-iptables.nix +++ b/nixos/modules/services/networking/firewall-iptables.nix @@ -285,9 +285,7 @@ let in { - options = { - networking.firewall = { extraCommands = lib.mkOption { type = lib.types.lines; @@ -317,13 +315,11 @@ in ''; }; }; - }; # FIXME: Maybe if `enable' is false, the firewall should still be # built but not started by default? - config = lib.mkIf (cfg.enable && config.networking.nftables.enable == false) { - + config = lib.mkIf (cfg.enable && cfg.backend == "iptables") { assertions = [ # This is approximately "checkReversePath -> kernelHasRPFilter", # but the checkReversePath option can include non-boolean @@ -336,6 +332,8 @@ in networking.firewall.checkReversePath = lib.mkIf (!kernelHasRPFilter) (lib.mkDefault false); + environment.systemPackages = [ pkgs.nixos-firewall-tool ]; + systemd.services.firewall = { description = "Firewall"; wantedBy = [ "sysinit.target" ]; @@ -365,7 +363,5 @@ in ExecStop = "@${stopScript} firewall-stop"; }; }; - }; - } diff --git a/nixos/modules/services/networking/firewall-nftables.nix b/nixos/modules/services/networking/firewall-nftables.nix index d9695c0e4d272..a22fc29d35ffb 100644 --- a/nixos/modules/services/networking/firewall-nftables.nix +++ b/nixos/modules/services/networking/firewall-nftables.nix @@ -19,9 +19,7 @@ let in { - options = { - networking.firewall = { extraInputRules = lib.mkOption { type = lib.types.lines; @@ -59,11 +57,9 @@ in ''; }; }; - }; - config = lib.mkIf (cfg.enable && config.networking.nftables.enable) { - + config = lib.mkIf (cfg.enable && cfg.backend == "nftables") { assertions = [ { assertion = cfg.extraCommands == ""; @@ -83,6 +79,8 @@ in } ]; + environment.systemPackages = [ pkgs.nixos-firewall-tool ]; + networking.nftables.tables."nixos-fw".family = "inet"; networking.nftables.tables."nixos-fw".content = '' set temp-ports { @@ -203,7 +201,5 @@ in } ''} ''; - }; - } diff --git a/nixos/modules/services/networking/firewall.nix b/nixos/modules/services/networking/firewall.nix index e733e9e6b87b5..95308c2a9cfce 100644 --- a/nixos/modules/services/networking/firewall.nix +++ b/nixos/modules/services/networking/firewall.nix @@ -68,9 +68,7 @@ let in { - options = { - networking.firewall = { enable = lib.mkOption { type = lib.types.bool; @@ -82,6 +80,32 @@ in ''; }; + backend = lib.mkOption { + type = lib.types.enum [ + "iptables" + "nftables" + "firewalld" + ]; + default = + if config.services.firewalld.enable then + "firewalld" + else if config.networking.nftables.enable then + "nftables" + else + "iptables"; + defaultText = lib.literalExpression '' + if config.services.firewalld.enable then + "firewalld" + else if config.networking.nftables.enable then + "nftables" + else + "iptables" + ''; + description = '' + Underlying implementation for the firewall service. + ''; + }; + package = lib.mkOption { type = lib.types.package; default = if config.networking.nftables.enable then pkgs.nftables else pkgs.iptables; @@ -292,11 +316,9 @@ in }; } // commonOptions; - }; config = lib.mkIf cfg.enable { - assertions = [ { assertion = cfg.filterForward -> config.networking.nftables.enable; @@ -311,11 +333,7 @@ in networking.firewall.trustedInterfaces = [ "lo" ]; - environment.systemPackages = [ - cfg.package - pkgs.nixos-firewall-tool - ] - ++ cfg.extraPackages; + environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages; boot.kernelModules = (lib.optional cfg.autoLoadConntrackHelpers "nf_conntrack") @@ -323,7 +341,5 @@ in boot.extraModprobeConfig = lib.optionalString cfg.autoLoadConntrackHelpers '' options nf_conntrack nf_conntrack_helper=1 ''; - }; - } diff --git a/nixos/modules/services/networking/firewalld/default.nix b/nixos/modules/services/networking/firewalld/default.nix new file mode 100644 index 0000000000000..e0e8bf4cb47c1 --- /dev/null +++ b/nixos/modules/services/networking/firewalld/default.nix @@ -0,0 +1,66 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.firewalld; + paths = pkgs.buildEnv { + name = "firewalld-paths"; + paths = cfg.packages; + pathsToLink = [ "/lib/firewalld" ]; + }; +in +{ + imports = [ + ./service.nix + ./settings.nix + ./zone.nix + ]; + + options.services.firewalld = { + enable = lib.mkEnableOption "FirewallD"; + package = lib.mkPackageOption pkgs "firewalld" { }; + packages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + description = '' + Packages providing firewalld zones and other files. + Files found in `/lib/firewalld` will be included. + ''; + }; + extraArgs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "--debug" ]; + description = "Extra arguments to pass to FirewallD."; + }; + }; + + config = lib.mkIf cfg.enable { + environment.systemPackages = [ cfg.package ]; + services.dbus.packages = [ cfg.package ]; + services.firewalld.packages = [ cfg.package ]; + + services.logrotate.settings."/var/log/firewalld" = { + copytruncate = true; + minsize = "1M"; + }; + + environment.etc."sysconfig/firewalld".text = '' + FIREWALLD_ARGS=${lib.concatStringsSep " " cfg.extraArgs} + ''; + + systemd.packages = [ cfg.package ]; + systemd.services.firewalld = { + aliases = [ "dbus-org.fedoraproject.FirewallD1.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig.ExecReload = "${lib.getExe' pkgs.coreutils "kill"} -HUP $MAINPID"; + environment.NIX_FIREWALLD_CONFIG_PATH = "${paths}/lib/firewalld"; + }; + }; + + meta.maintainers = with lib.maintainers; [ prince213 ]; +} diff --git a/nixos/modules/services/networking/firewalld/lib.nix b/nixos/modules/services/networking/firewalld/lib.nix new file mode 100644 index 0000000000000..c8977dd346e7f --- /dev/null +++ b/nixos/modules/services/networking/firewalld/lib.nix @@ -0,0 +1,55 @@ +{ lib }: + +let + inherit (lib) mkOption; + inherit (lib.types) + either + enum + nullOr + port + submodule + ; + mkPortOption = + { + optional ? false, + }: + mkOption { + type = + let + type = either port (submodule { + options = { + from = mkOption { type = port; }; + to = mkOption { type = port; }; + }; + }); + in + if optional then (nullOr type) else type; + description = ""; + apply = + value: if builtins.isAttrs value then "${toString value.from}-${toString value.to}" else value; + }; + protocolOption = mkOption { + type = enum [ + "tcp" + "udp" + "sctp" + "dccp" + ]; + description = ""; + }; +in +{ + inherit mkPortOption; + inherit protocolOption; + + toXmlAttrs = lib.mapAttrs' (name: lib.nameValuePair ("@" + name)); + mkXmlAttr = name: value: { "@${name}" = value; }; + filterNullAttrs = lib.filterAttrsRecursive (_: value: value != null); + + portProtocolOptions = { + options = { + port = mkPortOption { }; + protocol = protocolOption; + }; + }; +} diff --git a/nixos/modules/services/networking/firewalld/service.nix b/nixos/modules/services/networking/firewalld/service.nix new file mode 100644 index 0000000000000..92db5b251d100 --- /dev/null +++ b/nixos/modules/services/networking/firewalld/service.nix @@ -0,0 +1,121 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.firewalld; + format = pkgs.formats.xml { }; + lib' = import ./lib.nix { inherit lib; }; + inherit (lib') + filterNullAttrs + mkXmlAttr + portProtocolOptions + toXmlAttrs + ; + inherit (lib) mkOption; + inherit (lib.types) + attrsOf + listOf + nonEmptyStr + nullOr + strMatching + submodule + ; +in +{ + options.services.firewalld.services = mkOption { + description = '' + firewalld service configuration files. See {manpage}`firewalld.service(5)`. + ''; + default = { }; + type = attrsOf (submodule { + options = { + version = mkOption { + type = nullOr nonEmptyStr; + description = "Version of the service."; + default = null; + }; + short = mkOption { + type = nullOr nonEmptyStr; + description = "Short description for the service."; + default = null; + }; + description = mkOption { + type = nullOr nonEmptyStr; + description = "Description for the service."; + default = null; + }; + ports = mkOption { + type = listOf (submodule portProtocolOptions); + description = "Ports of the service."; + default = [ ]; + }; + protocols = mkOption { + type = listOf nonEmptyStr; + description = "Protocols for the service."; + default = [ ]; + }; + sourcePorts = mkOption { + type = listOf (submodule portProtocolOptions); + description = "Source ports for the service."; + default = [ ]; + }; + destination = mkOption { + type = submodule { + options = { + ipv4 = mkOption { + type = nullOr (strMatching "([0-9]{1,3}\\.){3}[0-9]{1,3}(/[0-9]{1,2})?"); + description = "IPv4 destination."; + default = null; + }; + ipv6 = mkOption { + type = nullOr (strMatching "[0-9A-Fa-f:]{3,39}(/[0-9]{1,3})?"); + description = "IPv6 destination."; + default = null; + }; + }; + }; + description = "Destinations for the service."; + default = { }; + }; + includes = mkOption { + type = listOf nonEmptyStr; + description = "Services to include for the service."; + default = [ ]; + }; + helpers = mkOption { + type = listOf nonEmptyStr; + description = "Helpers for the service."; + default = [ ]; + }; + }; + }); + }; + + config = lib.mkIf cfg.enable { + environment.etc = lib.mapAttrs' ( + name: value: + lib.nameValuePair "firewalld/services/${name}.xml" { + source = format.generate "firewalld-service-${name}.xml" { + service = filterNullAttrs ( + lib.mergeAttrsList [ + (toXmlAttrs { inherit (value) version; }) + { + inherit (value) short description; + port = builtins.map toXmlAttrs value.ports; + protocol = builtins.map (mkXmlAttr "value") value.protocols; + source-port = builtins.map toXmlAttrs value.sourcePorts; + destination = toXmlAttrs value.destination; + include = builtins.map (mkXmlAttr "service") value.includes; + helper = builtins.map (mkXmlAttr "name") value.helpers; + } + ] + ); + }; + } + ) cfg.services; + }; +} diff --git a/nixos/modules/services/networking/firewalld/settings.nix b/nixos/modules/services/networking/firewalld/settings.nix new file mode 100644 index 0000000000000..d47e56aac17da --- /dev/null +++ b/nixos/modules/services/networking/firewalld/settings.nix @@ -0,0 +1,198 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.firewalld; + format = pkgs.formats.keyValue { }; + inherit (lib) mkOption; + inherit (lib.types) + bool + commas + either + enum + nonEmptyStr + separatedString + submodule + ; +in +{ + options.services.firewalld.settings = mkOption { + description = '' + FirewallD config file. + See {manpage}`firewalld.conf(5)`. + ''; + default = { }; + type = submodule { + freeformType = format.type; + options = { + DefaultZone = mkOption { + type = nonEmptyStr; + description = "Default zone for connections."; + default = "public"; + }; + CleanupOnExit = mkOption { + type = bool; + description = "Whether to clean up firewall rules when firewalld stops."; + default = true; + }; + CleanupModulesOnExit = mkOption { + type = bool; + description = "Whether to unload all firewall-related kernel modules when firewalld stops."; + default = false; + }; + IPv6_rpfilter = mkOption { + type = enum [ + "strict" + "loose" + "strict-forward" + "loose-forward" + "no" + ]; + description = '' + Performs reverse path filtering (RPF) on IPv6 packets as per RFC 3704. + + Possible values: + + `"strict"` + : Performs "strict" filtering as per RFC 3704. + This check verifies that the in ingress interface is the same interface that would be used to send a packet reply to the source. + That is, `ingress == egress`. + + `"loose"` + : Performs "loose" filtering as per RFC 3704. + This check only verifies that there is a route back to the source through any interface; even if it's not the same one on which the packet arrived. + + `"strict-forward"` + : This is almost identical to "strict", but does not perform RPF for packets targeted to the host (INPUT). + + `"loose-forward"` + : This is almost identical to "loose", but does not perform RPF for packets targeted to the host (INPUT). + + `"no"` + : RPF is completely disabled. + + The rp_filter for IPv4 is controlled using sysctl. + ''; + default = "strict"; + }; + IndividualCalls = mkOption { + type = bool; + description = '' + Whether to use individual -restore calls to apply changes to the firewall. + The use of individual calls increases the time that is needed to apply changes and to start the daemon, but is good for debugging as error messages are more specific. + ''; + default = false; + }; + LogDenied = mkOption { + type = enum [ + "all" + "unicast" + "broadcast" + "multicast" + "off" + ]; + description = '' + Add logging rules right before reject and drop rules in the INPUT, FORWARD and OUTPUT chains for the default rules and also final reject and drop rules in zones for the configured link-layer packet type. + ''; + default = "off"; + }; + FirewallBackend = mkOption { + type = enum [ + "nftables" + "iptables" + ]; + description = '' + The firewall backend implementation. + This applies to all firewalld primitives. + The only exception is direct and passthrough rules which always use the traditional iptables, ip6tables, and ebtables backends. + + ::: {.caution} + The iptables backend is deprecated. + It will be removed in a future release. + ::: + ''; + default = "nftables"; + }; + FlushAllOnReload = mkOption { + type = bool; + description = "Whether to flush all runtime rules on a reload."; + default = true; + }; + ReloadPolicy = mkOption { + type = + let + policy = enum [ + "DROP" + "REJECT" + "ACCEPT" + ]; + in + either policy commas; + description = "The policy during reload."; + default = "INPUT:DROP,FORWARD:DROP,OUTPUT:DROP"; + }; + RFC3964_IPv4 = mkOption { + type = bool; + description = '' + Whether to filter IPv6 traffic with 6to4 destination addresses that correspond to IPv4 addresses that should not be routed over the public internet. + ''; + default = true; + }; + StrictForwardPorts = mkOption { + type = bool; + description = '' + If enabled, the generated destination NAT (DNAT) rules will NOT accept traffic that was DNAT'd by other entities, e.g. docker. + Firewalld will be strict and not allow published container ports until they're explicitly allowed via firewalld. + If set to `false`, then docker (and podman) integrates seamlessly with firewalld. + Published container ports are implicitly allowed. + ''; + default = false; + }; + NftablesFlowtable = mkOption { + type = separatedString " "; + description = '' + This may improve forwarded traffic throughput by enabling nftables flowtable. + It is a software fastpath and avoids calling nftables rule evaluation for data packets. + Its value is a space separate list of interfaces. + ''; + default = "off"; + }; + NftablesCounters = mkOption { + type = bool; + description = "Whether to add a counter to every nftables rule."; + default = false; + }; + NftablesTableOwner = mkOption { + type = bool; + description = '' + If enabled, the generated nftables rule set will be owned exclusively by firewalld. + This prevents other entities from mistakenly (or maliciously) modifying firewalld's rule set. + If you intend to modify firewalld's rules, set this to `false`. + ''; + default = true; + }; + }; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.settings.FirewallBackend == "nftables" -> config.networking.nftables.enable; + message = '' + FirewallD uses nftables as the firewall backend (by default), but nftables support isn't enabled. + Please read the description of networking.nftables.enable for possible problems. + If using nftables is not desired, set services.firewalld.settings.FirewallBackend to "iptables", but be aware that FirewallD has deprecated support for it, and will override firewall rule set by other services, if any. + ''; + } + ]; + + environment.etc."firewalld/firewalld.conf" = { + source = format.generate "firewalld.conf" cfg.settings; + }; + }; +} diff --git a/nixos/modules/services/networking/firewalld/zone.nix b/nixos/modules/services/networking/firewalld/zone.nix new file mode 100644 index 0000000000000..574c45304dd8c --- /dev/null +++ b/nixos/modules/services/networking/firewalld/zone.nix @@ -0,0 +1,281 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.firewalld; + format = pkgs.formats.xml { }; + lib' = import ./lib.nix { inherit lib; }; + inherit (lib') + filterNullAttrs + mkPortOption + mkXmlAttr + portProtocolOptions + protocolOption + toXmlAttrs + ; + inherit (lib) mkOption; + inherit (lib.types) + attrTag + attrsOf + bool + enum + ints + listOf + nonEmptyStr + nullOr + strMatching + submodule + ; +in +{ + options.services.firewalld.zones = mkOption { + description = '' + firewalld zone configuration files. + See {manpage}`firewalld.zone(5)`. + ''; + default = { }; + example = { + public = { + forward = true; + services = [ + "ssh" + "dhcpv6-client" + ]; + }; + external = { + forward = true; + services = [ + "ssh" + ]; + masquerade = true; + }; + dmz = { + forward = true; + services = [ + "ssh" + ]; + }; + work = { + forward = true; + services = [ + "ssh" + "dhcpv6-client" + ]; + }; + home = { + forward = true; + services = [ + "ssh" + "mdns" + "samba-client" + "dhcpv6-client" + ]; + }; + internal = { + forward = true; + services = [ + "ssh" + "mdns" + "samba-client" + "dhcpv6-client" + ]; + }; + }; + type = attrsOf (submodule { + options = { + version = mkOption { + type = nullOr nonEmptyStr; + description = "Version of the zone."; + default = null; + }; + target = mkOption { + type = enum [ + "ACCEPT" + "%%REJECT%%" + "DROP" + ]; + description = "Action for packets that doesn't match any rules."; + default = "%%REJECT%%"; + }; + ingressPriority = mkOption { + type = nullOr ints.s16; + description = '' + Priority for inbound traffic. + Lower values have higher priority. + ''; + default = null; + }; + egressPriority = mkOption { + type = nullOr ints.s16; + description = '' + Priority for outbound traffic. + Lower values have higher priority. + ''; + default = null; + }; + interfaces = mkOption { + type = listOf nonEmptyStr; + description = "Interfaces to bind."; + default = [ ]; + }; + sources = mkOption { + type = listOf (attrTag { + address = mkOption { + type = nonEmptyStr; + description = '' + An IP address or a network IP address with a mask for IPv4 or IPv6. + For IPv4, the mask can be a network mask or a plain number. + For IPv6 the mask is a plain number. + The use of host names is not supported. + ''; + }; + mac = mkOption { + type = strMatching "([[:xdigit:]]{2}:){5}[[:xdigit:]]{2}"; + description = "A MAC address."; + }; + ipset = mkOption { + type = nonEmptyStr; + description = "An ipset."; + }; + }); + description = "Source addresses, address ranges, MAC addresses or ipsets to bind."; + default = [ ]; + }; + icmpBlockInversion = mkOption { + type = bool; + description = '' + Whether to invert the icmp block handling. + Only enabled ICMP types are accepted and all others are rejected in the zone. + ''; + default = false; + }; + forward = mkOption { + type = bool; + description = '' + Whether to enable intra-zone forwarding. + When enabled, packets will be forwarded between interfaces or sources within a zone, even if the zone's target is not set to ACCEPT. + ''; + default = false; + }; + short = mkOption { + type = nullOr nonEmptyStr; + description = "Short description for the zone."; + default = null; + }; + description = mkOption { + type = nullOr nonEmptyStr; + description = "Description for the zone."; + default = null; + }; + services = mkOption { + type = listOf nonEmptyStr; + description = "Services to allow in the zone."; + default = [ ]; + }; + ports = mkOption { + type = listOf (submodule portProtocolOptions); + description = "Ports to allow in the zone."; + default = [ ]; + }; + protocols = mkOption { + type = listOf nonEmptyStr; + description = "Protocols to allow in the zone."; + default = [ ]; + }; + icmpBlocks = mkOption { + type = listOf nonEmptyStr; + description = "ICMP types to block in the zone."; + default = [ ]; + }; + masquerade = mkOption { + type = bool; + description = "Whether to enable masquerading in the zone."; + default = false; + }; + forwardPorts = mkOption { + type = listOf (submodule { + options = { + port = mkPortOption { }; + protocol = protocolOption; + to-port = (mkPortOption { optional = true; }) // { + default = null; + }; + to-addr = mkOption { + type = nullOr nonEmptyStr; + description = "Destination IP address."; + default = null; + }; + }; + }); + description = "Ports to forward in the zone."; + default = [ ]; + }; + sourcePorts = mkOption { + type = listOf (submodule portProtocolOptions); + description = "Source ports to allow in the zone."; + default = [ ]; + }; + rules = mkOption { + type = listOf (format.type); + description = "Rich rules for the zone."; + default = [ ]; + }; + }; + }); + }; + + config = lib.mkIf cfg.enable { + services.firewalld.zones = { + drop = { + target = "DROP"; + forward = true; + }; + block = { + forward = true; + }; + trusted = { + target = "ACCEPT"; + forward = true; + }; + }; + + environment.etc = lib.mapAttrs' ( + name: value: + lib.nameValuePair "firewalld/zones/${name}.xml" { + source = format.generate "firewalld-zone-${name}.xml" { + zone = + let + mkXmlAttrList = name: builtins.map (mkXmlAttr name); + mkXmlTag = value: if value then "" else null; + in + filterNullAttrs ( + lib.mergeAttrsList [ + (toXmlAttrs { inherit (value) version target; }) + (mkXmlAttr "ingress-priority" value.ingressPriority) + (mkXmlAttr "egress-priority" value.egressPriority) + { + interface = mkXmlAttrList "name" value.interfaces; + source = builtins.map toXmlAttrs value.sources; + icmp-block-inversion = mkXmlTag value.icmpBlockInversion; + forward = mkXmlTag value.forward; + inherit (value) short description; + service = mkXmlAttrList "name" value.services; + port = builtins.map toXmlAttrs value.ports; + protocol = mkXmlAttrList "value" value.protocols; + icmp-block = mkXmlAttrList "name" value.icmpBlocks; + masquerade = mkXmlTag value.masquerade; + forward-port = builtins.map toXmlAttrs (builtins.map filterNullAttrs value.forwardPorts); + source-port = builtins.map toXmlAttrs value.sourcePorts; + rule = value.rules; + } + ] + ); + }; + } + ) cfg.zones; + }; +} diff --git a/nixos/modules/services/networking/netbird.nix b/nixos/modules/services/networking/netbird.nix index ba2c1be37c522..c19cf7d88edbb 100644 --- a/nixos/modules/services/networking/netbird.nix +++ b/nixos/modules/services/networking/netbird.nix @@ -507,19 +507,35 @@ in ) "loose"; # Ports opened on a specific - interfaces = listToAttrs ( - toClientList (client: { - name = client.interface; - value.allowedUDPPorts = optionals client.openInternalFirewall [ - # note: those should be opened up by NetBird itself, but it needs additional - # NixOS -specific debugging and tweaking before it works - 5353 # <0.59.0 DNS forwarder port, kept for compatibility with those clients - 22054 # >=0.59.0 DNS forwarder port - ]; - }) + interfaces = lib.mkIf (config.networking.firewall.backend != "firewalld") ( + listToAttrs ( + toClientList (client: { + name = client.interface; + value.allowedUDPPorts = optionals client.openInternalFirewall [ + # note: those should be opened up by NetBird itself, but it needs additional + # NixOS -specific debugging and tweaking before it works + 5353 # <0.59.0 DNS forwarder port, kept for compatibility with those clients + 22054 # >=0.59.0 DNS forwarder port + ]; + }) + ) ); }; + services.firewalld.zones.netbird = { + interfaces = lib.pipe cfg.clients [ + (lib.filterAttrs (_: client: client.openFirewall)) + lib.attrValues + (map (client: client.interface)) + ]; + ports = [ + { + protocol = "udp"; + port = 5353; + } + ]; + }; + systemd.network.networks = mkIf config.networking.useNetworkd ( toClientAttrs ( client: diff --git a/nixos/modules/services/video/wivrn.nix b/nixos/modules/services/video/wivrn.nix index 9c7250fb7d08c..5508a51137c3d 100644 --- a/nixos/modules/services/video/wivrn.nix +++ b/nixos/modules/services/video/wivrn.nix @@ -231,6 +231,8 @@ in allowedUDPPorts = [ 9757 ]; }; + services.firewalld.packages = [ cfg.package ]; + environment = { systemPackages = [ cfg.package diff --git a/nixos/modules/services/x11/desktop-managers/kodi.nix b/nixos/modules/services/x11/desktop-managers/kodi.nix index 0dd2f8ffb829e..01252e47b37e7 100644 --- a/nixos/modules/services/x11/desktop-managers/kodi.nix +++ b/nixos/modules/services/x11/desktop-managers/kodi.nix @@ -38,5 +38,7 @@ in ]; environment.systemPackages = [ cfg.package ]; + + services.firewalld.packages = [ cfg.package ]; }; } diff --git a/nixos/modules/virtualisation/libvirtd.nix b/nixos/modules/virtualisation/libvirtd.nix index b4a5e1cebc49c..e7b02ee88b00a 100644 --- a/nixos/modules/virtualisation/libvirtd.nix +++ b/nixos/modules/virtualisation/libvirtd.nix @@ -447,6 +447,8 @@ in Include ${cfg.package}/etc/ssh/ssh_config.d/30-libvirt-ssh-proxy.conf ''; + services.firewalld.packages = [ cfg.package ]; + systemd.packages = [ cfg.package ]; systemd.services.libvirtd-config = { diff --git a/nixos/modules/virtualisation/podman/default.nix b/nixos/modules/virtualisation/podman/default.nix index 8145c099eead2..5f01142b4a132 100644 --- a/nixos/modules/virtualisation/podman/default.nix +++ b/nixos/modules/virtualisation/podman/default.nix @@ -249,7 +249,9 @@ in }; # containers cannot reach aardvark-dns otherwise - networking.firewall.interfaces.${network_interface}.allowedUDPPorts = lib.mkIf dns_enabled [ 53 ]; + networking.firewall = lib.mkIf (config.networking.firewall.backend != "firewalld") { + interfaces.${network_interface}.allowedUDPPorts = lib.mkIf dns_enabled [ 53 ]; + }; virtualisation.containers = { enable = true; # Enable common /etc/containers configuration diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 0b4b244b263b0..40f6d0afbf655 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -569,12 +569,17 @@ in firejail = runTest ./firejail.nix; firewall = runTest { imports = [ ./firewall.nix ]; - _module.args.nftables = false; + _module.args.backend = "iptables"; + }; + firewall-firewalld = runTest { + imports = [ ./firewall.nix ]; + _module.args.backend = "firewalld"; }; firewall-nftables = runTest { imports = [ ./firewall.nix ]; - _module.args.nftables = true; + _module.args.backend = "nftables"; }; + firewalld = runTest ./firewalld.nix; firezone = runTest ./firezone/firezone.nix; fish = runTest ./fish.nix; flannel = runTestOn [ "x86_64-linux" ] ./flannel.nix; diff --git a/nixos/tests/firewall.nix b/nixos/tests/firewall.nix index 019508adb0b28..31db36ef8dc18 100644 --- a/nixos/tests/firewall.nix +++ b/nixos/tests/firewall.nix @@ -1,10 +1,11 @@ # Test the firewall module. -{ lib, nftables, ... }: +{ lib, backend, ... }: { - name = "firewall" + lib.optionalString nftables "-nftables"; + name = "firewall-${backend}"; meta = with lib.maintainers; { maintainers = [ + prince213 rvfg garyguo ]; @@ -12,10 +13,11 @@ nodes = { walled = - { ... }: + { lib, ... }: { networking.firewall = { enable = true; + inherit backend; logRefusedPackets = true; # Syntax smoke test, not actually verified otherwise allowedTCPPorts = [ @@ -37,26 +39,29 @@ to = 8010; } ]; - interfaces.eth0 = { - allowedTCPPorts = [ 10003 ]; - allowedTCPPortRanges = [ - { - from = 10000; - to = 10005; - } - ]; - }; - interfaces.eth3 = { - allowedUDPPorts = [ 10003 ]; - allowedUDPPortRanges = [ - { - from = 10000; - to = 10005; - } - ]; + interfaces = lib.mkIf (backend != "firewalld") { + eth0 = { + allowedTCPPorts = [ 10003 ]; + allowedTCPPortRanges = [ + { + from = 10000; + to = 10005; + } + ]; + }; + eth3 = { + allowedUDPPorts = [ 10003 ]; + allowedUDPPortRanges = [ + { + from = 10000; + to = 10005; + } + ]; + }; }; }; - networking.nftables.enable = nftables; + services.firewalld.enable = backend == "firewalld"; + networking.nftables.enable = backend != "iptables"; services.httpd.enable = true; services.httpd.adminAddr = "foo@example.org"; @@ -77,7 +82,13 @@ testScript = { nodes, ... }: let - unit = if nftables then "nftables" else "firewall"; + unit = if backend == "iptables" then "firewall" else backend; + openPort = + if backend == "firewalld" then + "firewall-cmd --add-port=80/tcp" + else + "nixos-firewall-tool open tcp 80"; + reset = if backend == "firewalld" then "firewall-cmd --reload" else "nixos-firewall-tool reset"; in '' start_all() @@ -98,11 +109,11 @@ walled.succeed("ping -c 1 attacker >&2") # Open tcp port 80 at runtime - walled.succeed("nixos-firewall-tool open tcp 80") + walled.succeed("${openPort}") attacker.succeed("curl -v http://walled/ >&2") # Reset the firewall - walled.succeed("nixos-firewall-tool reset") + walled.succeed("${reset}") attacker.fail("curl --fail --connect-timeout 2 http://walled/ >&2") # If we stop the firewall, then connections should succeed. diff --git a/nixos/tests/firewalld.nix b/nixos/tests/firewalld.nix new file mode 100644 index 0000000000000..a191fe8063547 --- /dev/null +++ b/nixos/tests/firewalld.nix @@ -0,0 +1,52 @@ +{ lib, pkgs, ... }: +{ + name = "firewalld"; + meta.maintainers = with pkgs.lib.maintainers; [ + prince213 + ]; + + nodes = { + walled = { + networking.nftables.enable = true; + services.firewalld.enable = true; + services.httpd.enable = true; + services.httpd.adminAddr = "foo@example.org"; + }; + + open = { + networking.nftables.enable = true; + services.firewalld = { + enable = true; + settings.DefaultZone = "trusted"; + }; + services.httpd.enable = true; + services.httpd.adminAddr = "foo@example.org"; + }; + }; + + testScript = '' + start_all() + + walled.wait_for_unit("firewalld") + walled.wait_for_unit("httpd") + + open.wait_for_unit("network.target") + + with subtest("walled local httpd works"): + walled.succeed("curl -v http://localhost/ >&2") + + with subtest("incoming connections are blocked"): + open.fail("curl --fail --connect-timeout 2 http://walled/ >&2") + + with subtest("outgoing connections are allowed"): + walled.succeed("curl -v http://open/ >&2") + + with subtest("runtime configuration can be changed"): + walled.succeed("firewall-cmd --add-service=http") + open.succeed("curl -v http://walled/ >&2") + + with subtest("runtime configuration are not permanent"): + walled.succeed("firewall-cmd --complete-reload") + open.fail("curl --fail --connect-timeout 2 http://walled/ >&2") + ''; +} diff --git a/pkgs/by-name/fi/firewalld/package.nix b/pkgs/by-name/fi/firewalld/package.nix index fbab1684c4e86..9b0abeb7cbfd6 100644 --- a/pkgs/by-name/fi/firewalld/package.nix +++ b/pkgs/by-name/fi/firewalld/package.nix @@ -26,6 +26,7 @@ sysctl, wrapGAppsNoGuiHook, withGui ? false, + nixosTests, }: let @@ -153,6 +154,11 @@ stdenv.mkDerivation rec { wrapPythonProgramsIn "$out/bin" "$out ${pythonPath}" ''; + passthru.tests = { + firewalld = nixosTests.firewalld; + firewall-firewalld = nixosTests.firewall-firewalld; + }; + meta = { description = "Firewall daemon with D-Bus interface"; homepage = "https://firewalld.org";