diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 0b9bd6d976f6..f706b63b5e3e 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1165,6 +1165,7 @@ ./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/firewalld/default.nix b/nixos/modules/services/networking/firewalld/default.nix new file mode 100644 index 000000000000..e0e8bf4cb47c --- /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 000000000000..c8977dd346e7 --- /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 000000000000..92db5b251d10 --- /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 000000000000..d47e56aac17d --- /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 000000000000..574c45304dd8 --- /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; + }; +}