From fbd32585c848d0087fcf4c31247902550cc2b5e8 Mon Sep 17 00:00:00 2001 From: Marcus Ramberg Date: Sun, 16 Nov 2025 21:31:47 +0100 Subject: [PATCH] nixos/netfoil: init --- nixos/modules/module-list.nix | 1 + nixos/modules/services/networking/netfoil.nix | 263 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/netfoil.nix | 28 ++ 4 files changed, 293 insertions(+) create mode 100644 nixos/modules/services/networking/netfoil.nix create mode 100644 nixos/tests/netfoil.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 774140f86d92..99dcc808ff83 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1306,6 +1306,7 @@ ./services/networking/netbird.nix ./services/networking/netbird/server.nix ./services/networking/netclient.nix + ./services/networking/netfoil.nix ./services/networking/networkd-dispatcher.nix ./services/networking/networkmanager.nix ./services/networking/newt.nix diff --git a/nixos/modules/services/networking/netfoil.nix b/nixos/modules/services/networking/netfoil.nix new file mode 100644 index 000000000000..df5c5a5e1d6e --- /dev/null +++ b/nixos/modules/services/networking/netfoil.nix @@ -0,0 +1,263 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.netfoil; +in +{ + options = { + services.netfoil = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Enable Netfoil, a minimal, filtering, DNS proxy"; + }; + listen = { + port = lib.mkOption { + type = lib.types.int; + default = 53; + description = "Port on which Netfoil listens for incoming connections"; + }; + ipAddress = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "IP address on which Netfoil listens for incoming connections"; + }; + }; + logAllowed = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Log allowed DNS queries"; + }; + doHUrl = lib.mkOption { + type = lib.types.str; + default = "https://security.cloudflare-dns.com/dns-query"; + description = "The DoH URL to use for upstream DNS queries"; + }; + doHIPs = lib.mkOption { + type = lib.types.str; + default = "1.1.1.2,1.0.0.2"; + description = "The DoH IPs to use for upstream DNS queries"; + }; + logDenied = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Log denied DNS queries"; + }; + config = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = "Additional configuration options for Netfoil"; + }; + rules = { + allow = { + exact = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "List of exact domain names to allow"; + }; + ipv4 = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "List of ipv4 CIDR ranges to allow"; + }; + ipv6 = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "List of ipv6 CIDR ranges to allow"; + }; + tld = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "List of TLDs to allow"; + }; + }; + deny = { + exact = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "List of exact domain names to deny"; + }; + ipv4 = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "List of ipv4 CIDR ranges to deny"; + }; + ipv6 = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "List of ipv6 CIDR ranges to deny"; + }; + tld = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "List of TLDs to deny"; + }; + }; + known = { + knownTlds = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + ".com" + ".net" + ".org" + ".edu" + ".gov" + ".mil" + ".int" + ]; + description = "List of known TLDs"; + }; + }; + pin = { + a = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "List of A records to pin "; + }; + responseDomain = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "List of domains to pin "; + }; + }; + }; + }; + }; + + config = lib.mkIf cfg.enable ( + let + configFile = lib.concatStringsSep "\n" ( + [ + "LogAllowed=${lib.boolToString cfg.logAllowed}" + "LogDenied=${lib.boolToString cfg.logDenied}" + "DoHURL=${cfg.doHUrl}" + "DoHIPs=${cfg.doHIPs}" + ] + ++ (map (key: "${key} = \"${cfg.config.${key}}\"") (lib.attrNames cfg.config)) + ++ lib.optional ((lib.length cfg.rules.pin.responseDomain) != 0) "PinResponseDomain=true" + ); + configDir = pkgs.buildEnv { + name = "netfoil-config"; + paths = [ + (pkgs.writeTextDir "config" configFile) + (pkgs.writeTextDir "allow.exact" (lib.concatStringsSep "\n" cfg.rules.allow.exact)) + (pkgs.writeTextDir "allow.ipv4" (lib.concatStringsSep "\n" cfg.rules.allow.ipv4)) + (pkgs.writeTextDir "allow.ipv6" (lib.concatStringsSep "\n" cfg.rules.allow.ipv6)) + (pkgs.writeTextDir "allow.suffix" (lib.concatStringsSep "\n" cfg.rules.allow.tld)) + (pkgs.writeTextDir "allow.tld" (lib.concatStringsSep "\n" cfg.rules.allow.tld)) + (pkgs.writeTextDir "deny.exact" (lib.concatStringsSep "\n" cfg.rules.deny.exact)) + (pkgs.writeTextDir "deny.ipv4" (lib.concatStringsSep "\n" cfg.rules.deny.ipv4)) + (pkgs.writeTextDir "deny.ipv6" (lib.concatStringsSep "\n" cfg.rules.deny.ipv6)) + (pkgs.writeTextDir "deny.suffix" (lib.concatStringsSep "\n" cfg.rules.deny.tld)) + (pkgs.writeTextDir "deny.tld" (lib.concatStringsSep "\n" cfg.rules.deny.tld)) + (pkgs.writeTextDir "known.tld" (lib.concatStringsSep "\n" cfg.rules.known.knownTlds)) + (pkgs.writeTextDir "pin.a" (lib.concatStringsSep "\n" cfg.rules.pin.a)) + (pkgs.writeTextDir "pin.response-domain" (lib.concatStringsSep "\n" cfg.rules.pin.responseDomain)) + ]; + }; + in + { + systemd = { + services.netfoil = { + enable = true; + description = "Netfoil DNS proxy"; + after = [ "network.target" ]; + requires = [ "netfoil.socket" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + ExecStart = "${pkgs.netfoil}/bin/netfoil --config-directory ${configDir}"; + Restart = "always"; + RestartSec = "5"; + DynamicUser = true; + BindReadOnlyPaths = [ + "${pkgs.netfoil}" + "${configDir}" + "/etc/ssl" + builtins.storeDir + ]; + Slice = "netfoil.slice"; + AmbientCapabilities = ""; + CapabilityBoundingSet = [ ]; + SystenCallArchitectures = "native"; + SystemCallFilter = [ + "@basic-io" + "@file-system" + "@network-io" + "@signal" + "@process" + "@io-event" + "@system-service" + "@resources" + ]; + RuntimeDirectory = "netfoil"; + RuntimeDirectoryMode = "0755"; + RootDirectory = "/run/netfoil"; + RestrictAddressFamilies = "AF_INET AF_INET6"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + + # This might set AllowDevices=char-rtc r + ProtectClock = true; + + ProtectKernelModules = true; + ProtectKernelLogs = true; + + LockPersonality = true; + MemoryDenyWriteExecute = true; + + RemoveIPC = true; + UMask = "0077"; + + # IPC namespace + PrivateIPC = true; + + # UTS namespace + ProtectHostname = true; + + # Changes mounts (custom is more strict) + # https://github.com/systemd/systemd/blob/main/src/core/namespace.c + # + ProtectControlGroups = true; + ProtectHome = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + ProtectSystem = "strict"; + PrivateTmp = true; + + # + # seccomp _sysctl (custom filter does not allow it anyway) + # /proc and /sys mounts (custom is more strict) + ProtectKernelTunables = true; + # + # seccomp @raw-io (custom filter does not allow it anyway) + PrivateDevices = true; + DevicePolicy = "closed"; + + SocketBindDeny = "any"; + + CPUQuota = "50%"; + MemoryMax = "100M"; + TasksMax = "100"; + }; + }; + slices.netfoil = { + description = "Slice for Netfoil DNS proxy"; + }; + sockets.netfoil = { + description = "Netfoil DNS proxy socket"; + wantedBy = [ "sockets.target" ]; + socketConfig = { + ListenDatagram = "${cfg.listen.ipAddress}:${toString cfg.listen.port}"; + Service = "netfoil.service"; + }; + }; + }; + } + ); +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 767da3a67b16..074487b2e75e 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1064,6 +1064,7 @@ in netbox_4_4 = handleTest ./web-apps/netbox/default.nix { netbox = pkgs.netbox_4_4; }; netbox_4_5 = handleTest ./web-apps/netbox/default.nix { netbox = pkgs.netbox_4_5; }; netdata = runTest ./netdata.nix; + netfoil = runTest ./netfoil.nix; networking.networkd = handleTest ./networking/networkd-and-scripted.nix { networkd = true; }; networking.networkmanager = handleTest ./networking/networkmanager.nix { }; networking.scripted = handleTest ./networking/networkd-and-scripted.nix { networkd = false; }; diff --git a/nixos/tests/netfoil.nix b/nixos/tests/netfoil.nix new file mode 100644 index 000000000000..8697de95bdec --- /dev/null +++ b/nixos/tests/netfoil.nix @@ -0,0 +1,28 @@ +{ lib, ... }: +{ + name = "netfoil"; + meta.maintainers = with lib.maintainers; [ + marcusramberg + sgo + ]; + + nodes = { + one = + { config, ... }: + { + services.netfoil = { + enable = true; + listen.port = 6353; + }; + }; + }; + interactive.sshBackdoor.enable = true; + + testScript = '' + start_all() + + with subtest("ensure netfoil starts and listens on 6353"): + one.wait_for_unit("netfoil.service") + one.wait_for_open_port(6353) + ''; +}