{ config, lib, pkgs, ... }: with lib; let cfg = config.services.unbound; stateDir = "/var/lib/unbound"; access = concatMapStringsSep "\n " (x: "access-control: ${x} allow") cfg.allowedAccess; interfaces = concatMapStringsSep "\n " (x: "interface: ${x}") cfg.interfaces; isLocalAddress = x: substring 0 3 x == "::1" || substring 0 9 x == "127.0.0.1"; forward = optionalString (any isLocalAddress cfg.forwardAddresses) '' do-not-query-localhost: no '' + optionalString (cfg.forwardAddresses != []) '' forward-zone: name: . '' + concatMapStringsSep "\n" (x: " forward-addr: ${x}") cfg.forwardAddresses; rootTrustAnchorFile = "${stateDir}/root.key"; trustAnchor = optionalString cfg.enableRootTrustAnchor "auto-trust-anchor-file: ${rootTrustAnchorFile}"; confFile = pkgs.writeText "unbound.conf" '' server: ip-freebind: yes directory: "${stateDir}" username: unbound chroot: "" pidfile: "" # when running under systemd there is no need to daemonize do-daemonize: no ${interfaces} ${access} ${trustAnchor} ${lib.optionalString (cfg.localControlSocketPath != null) '' remote-control: control-enable: yes control-interface: ${cfg.localControlSocketPath} ''} ${cfg.extraConfig} ${forward} ''; in { ###### interface options = { services.unbound = { enable = mkEnableOption "Unbound domain name server"; package = mkOption { type = types.package; default = pkgs.unbound-with-systemd; defaultText = "pkgs.unbound-with-systemd"; description = "The unbound package to use"; }; allowedAccess = mkOption { default = [ "127.0.0.0/24" ]; type = types.listOf types.str; description = "What networks are allowed to use unbound as a resolver."; }; interfaces = mkOption { default = [ "127.0.0.1" ] ++ optional config.networking.enableIPv6 "::1"; type = types.listOf types.str; description = '' What addresses the server should listen on. This supports the interface syntax documented in unbound.conf8. ''; }; forwardAddresses = mkOption { default = []; type = types.listOf types.str; description = "What servers to forward queries to."; }; enableRootTrustAnchor = mkOption { default = true; type = types.bool; description = "Use and update root trust anchor for DNSSEC validation."; }; localControlSocketPath = mkOption { default = null; # FIXME: What is the proper type here so users can specify strings, # paths and null? # My guess would be `types.nullOr (types.either types.str types.path)` # but I haven't verified yet. type = types.nullOr types.str; example = "/run/unbound/unbound.ctl"; description = '' When not set to null this option defines the path at which the unbound remote control socket should be created at. The socket will be owned by the unbound user (unbound) and group will be nogroup. Users that should be permitted to access the socket must be in the unbound group. If this option is null remote control will not be configured at all. Unbounds default values apply. ''; }; extraConfig = mkOption { default = ""; type = types.lines; description = '' Extra unbound config. See unbound.conf8 . ''; }; }; }; ###### implementation config = mkIf cfg.enable { environment.systemPackages = [ cfg.package ]; users.users.unbound = { description = "unbound daemon user"; isSystemUser = true; group = lib.mkIf (cfg.localControlSocketPath != null) (lib.mkDefault "unbound"); }; # We need a group so that we can give users access to the configured # control socket. Unbound allows access to the socket only to the unbound # user and the primary group. users.groups = lib.mkIf (cfg.localControlSocketPath != null) { unbound = {}; }; networking.resolvconf.useLocalResolver = mkDefault true; environment.etc."unbound/unbound.conf".source = confFile; systemd.services.unbound = { description = "Unbound recursive Domain Name Server"; after = [ "network.target" ]; before = [ "nss-lookup.target" ]; wantedBy = [ "multi-user.target" "nss-lookup.target" ]; preStart = lib.mkIf cfg.enableRootTrustAnchor '' ${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!" ''; restartTriggers = [ confFile ]; serviceConfig = { ExecStart = "${cfg.package}/bin/unbound -p -d -c /etc/unbound/unbound.conf"; ExecReload = "+/run/current-system/sw/bin/kill -HUP $MAINPID"; NotifyAccess = "main"; Type = "notify"; # FIXME: Which of these do we actualy need, can we drop the chroot flag? AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_NET_RAW" "CAP_SETGID" "CAP_SETUID" "CAP_SYS_CHROOT" "CAP_SYS_RESOURCE" ]; User = "unbound"; Group = lib.mkIf (cfg.localControlSocketPath != null) (lib.mkDefault "unbound"); MemoryDenyWriteExecute = true; NoNewPrivileges = true; PrivateDevices = true; PrivateTmp = true; ProtectHome = true; ProtectControlGroups = true; ProtectKernelModules = true; ProtectSystem = "strict"; RuntimeDirectory = "unbound"; ConfigurationDirectory = "unbound"; StateDirectory = "unbound"; RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" "AF_UNIX" ]; RestrictRealtime = true; SystemCallArchitectures = "native"; SystemCallFilter = [ "~@clock" "@cpu-emulation" "@debug" "@keyring" "@module" "mount" "@obsolete" "@resources" ]; RestrictNamespaces = true; LockPersonality = true; RestrictSUIDSGID = true; }; }; # If networkmanager is enabled, ask it to interface with unbound. networking.networkmanager.dns = "unbound"; }; }