at 23.11-pre 11 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4let 5 cfg = config.services.unbound; 6 7 yesOrNo = v: if v then "yes" else "no"; 8 9 toOption = indent: n: v: "${indent}${toString n}: ${v}"; 10 11 toConf = indent: n: v: 12 if builtins.isFloat v then (toOption indent n (builtins.toJSON v)) 13 else if isInt v then (toOption indent n (toString v)) 14 else if isBool v then (toOption indent n (yesOrNo v)) 15 else if isString v then (toOption indent n v) 16 else if isList v then (concatMapStringsSep "\n" (toConf indent n) v) 17 else if isAttrs v then (concatStringsSep "\n" ( 18 ["${indent}${n}:"] ++ ( 19 mapAttrsToList (toConf "${indent} ") v 20 ) 21 )) 22 else throw (traceSeq v "services.unbound.settings: unexpected type"); 23 24 confNoServer = concatStringsSep "\n" ((mapAttrsToList (toConf "") (builtins.removeAttrs cfg.settings [ "server" ])) ++ [""]); 25 confServer = concatStringsSep "\n" (mapAttrsToList (toConf " ") (builtins.removeAttrs cfg.settings.server [ "define-tag" ])); 26 27 confFile = pkgs.writeText "unbound.conf" '' 28 server: 29 ${optionalString (cfg.settings.server.define-tag != "") (toOption " " "define-tag" cfg.settings.server.define-tag)} 30 ${confServer} 31 ${confNoServer} 32 ''; 33 34 rootTrustAnchorFile = "${cfg.stateDir}/root.key"; 35 36in { 37 38 ###### interface 39 40 options = { 41 services.unbound = { 42 43 enable = mkEnableOption (lib.mdDoc "Unbound domain name server"); 44 45 package = mkOption { 46 type = types.package; 47 default = pkgs.unbound-with-systemd; 48 defaultText = literalExpression "pkgs.unbound-with-systemd"; 49 description = lib.mdDoc "The unbound package to use"; 50 }; 51 52 user = mkOption { 53 type = types.str; 54 default = "unbound"; 55 description = lib.mdDoc "User account under which unbound runs."; 56 }; 57 58 group = mkOption { 59 type = types.str; 60 default = "unbound"; 61 description = lib.mdDoc "Group under which unbound runs."; 62 }; 63 64 stateDir = mkOption { 65 type = types.path; 66 default = "/var/lib/unbound"; 67 description = lib.mdDoc "Directory holding all state for unbound to run."; 68 }; 69 70 resolveLocalQueries = mkOption { 71 type = types.bool; 72 default = true; 73 description = lib.mdDoc '' 74 Whether unbound should resolve local queries (i.e. add 127.0.0.1 to 75 /etc/resolv.conf). 76 ''; 77 }; 78 79 enableRootTrustAnchor = mkOption { 80 default = true; 81 type = types.bool; 82 description = lib.mdDoc "Use and update root trust anchor for DNSSEC validation."; 83 }; 84 85 localControlSocketPath = mkOption { 86 default = null; 87 # FIXME: What is the proper type here so users can specify strings, 88 # paths and null? 89 # My guess would be `types.nullOr (types.either types.str types.path)` 90 # but I haven't verified yet. 91 type = types.nullOr types.str; 92 example = "/run/unbound/unbound.ctl"; 93 description = lib.mdDoc '' 94 When not set to `null` this option defines the path 95 at which the unbound remote control socket should be created at. The 96 socket will be owned by the unbound user (`unbound`) 97 and group will be `nogroup`. 98 99 Users that should be permitted to access the socket must be in the 100 `config.services.unbound.group` group. 101 102 If this option is `null` remote control will not be 103 enabled. Unbounds default values apply. 104 ''; 105 }; 106 107 settings = mkOption { 108 default = {}; 109 type = with types; submodule { 110 111 freeformType = let 112 validSettingsPrimitiveTypes = oneOf [ int str bool float ]; 113 validSettingsTypes = oneOf [ validSettingsPrimitiveTypes (listOf validSettingsPrimitiveTypes) ]; 114 settingsType = oneOf [ str (attrsOf validSettingsTypes) ]; 115 in attrsOf (oneOf [ settingsType (listOf settingsType) ]) 116 // { description = '' 117 unbound.conf configuration type. The format consist of an attribute 118 set of settings. Each settings can be either one value, a list of 119 values or an attribute set. The allowed values are integers, 120 strings, booleans or floats. 121 ''; 122 }; 123 124 options = { 125 remote-control.control-enable = mkOption { 126 type = bool; 127 default = false; 128 internal = true; 129 }; 130 }; 131 }; 132 example = literalExpression '' 133 { 134 server = { 135 interface = [ "127.0.0.1" ]; 136 }; 137 forward-zone = [ 138 { 139 name = "."; 140 forward-addr = "1.1.1.1@853#cloudflare-dns.com"; 141 } 142 { 143 name = "example.org."; 144 forward-addr = [ 145 "1.1.1.1@853#cloudflare-dns.com" 146 "1.0.0.1@853#cloudflare-dns.com" 147 ]; 148 } 149 ]; 150 remote-control.control-enable = true; 151 }; 152 ''; 153 description = lib.mdDoc '' 154 Declarative Unbound configuration 155 See the {manpage}`unbound.conf(5)` manpage for a list of 156 available options. 157 ''; 158 }; 159 }; 160 }; 161 162 ###### implementation 163 164 config = mkIf cfg.enable { 165 166 services.unbound.settings = { 167 server = { 168 directory = mkDefault cfg.stateDir; 169 username = cfg.user; 170 chroot = ''""''; 171 pidfile = ''""''; 172 # when running under systemd there is no need to daemonize 173 do-daemonize = false; 174 interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1")); 175 access-control = mkDefault ([ "127.0.0.0/8 allow" ] ++ (optional config.networking.enableIPv6 "::1/128 allow")); 176 auto-trust-anchor-file = mkIf cfg.enableRootTrustAnchor rootTrustAnchorFile; 177 tls-cert-bundle = mkDefault "/etc/ssl/certs/ca-certificates.crt"; 178 # prevent race conditions on system startup when interfaces are not yet 179 # configured 180 ip-freebind = mkDefault true; 181 define-tag = mkDefault ""; 182 }; 183 remote-control = { 184 control-enable = mkDefault false; 185 control-interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1")); 186 server-key-file = mkDefault "${cfg.stateDir}/unbound_server.key"; 187 server-cert-file = mkDefault "${cfg.stateDir}/unbound_server.pem"; 188 control-key-file = mkDefault "${cfg.stateDir}/unbound_control.key"; 189 control-cert-file = mkDefault "${cfg.stateDir}/unbound_control.pem"; 190 } // optionalAttrs (cfg.localControlSocketPath != null) { 191 control-enable = true; 192 control-interface = cfg.localControlSocketPath; 193 }; 194 }; 195 196 environment.systemPackages = [ cfg.package ]; 197 198 users.users = mkIf (cfg.user == "unbound") { 199 unbound = { 200 description = "unbound daemon user"; 201 isSystemUser = true; 202 group = cfg.group; 203 }; 204 }; 205 206 users.groups = mkIf (cfg.group == "unbound") { 207 unbound = {}; 208 }; 209 210 networking = mkIf cfg.resolveLocalQueries { 211 resolvconf = { 212 useLocalResolver = mkDefault true; 213 }; 214 215 networkmanager.dns = "unbound"; 216 }; 217 218 environment.etc."unbound/unbound.conf".source = confFile; 219 220 systemd.services.unbound = { 221 description = "Unbound recursive Domain Name Server"; 222 after = [ "network.target" ]; 223 before = [ "nss-lookup.target" ]; 224 wantedBy = [ "multi-user.target" "nss-lookup.target" ]; 225 226 path = mkIf cfg.settings.remote-control.control-enable [ pkgs.openssl ]; 227 228 preStart = '' 229 ${optionalString cfg.enableRootTrustAnchor '' 230 ${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!" 231 ''} 232 ${optionalString cfg.settings.remote-control.control-enable '' 233 ${cfg.package}/bin/unbound-control-setup -d ${cfg.stateDir} 234 ''} 235 ''; 236 237 restartTriggers = [ 238 confFile 239 ]; 240 241 serviceConfig = { 242 ExecStart = "${cfg.package}/bin/unbound -p -d -c /etc/unbound/unbound.conf"; 243 ExecReload = "+/run/current-system/sw/bin/kill -HUP $MAINPID"; 244 245 NotifyAccess = "main"; 246 Type = "notify"; 247 248 # FIXME: Which of these do we actually need, can we drop the chroot flag? 249 AmbientCapabilities = [ 250 "CAP_NET_BIND_SERVICE" 251 "CAP_NET_RAW" 252 "CAP_SETGID" 253 "CAP_SETUID" 254 "CAP_SYS_CHROOT" 255 "CAP_SYS_RESOURCE" 256 ]; 257 258 User = cfg.user; 259 Group = cfg.group; 260 261 MemoryDenyWriteExecute = true; 262 NoNewPrivileges = true; 263 PrivateDevices = true; 264 PrivateTmp = true; 265 ProtectHome = true; 266 ProtectControlGroups = true; 267 ProtectKernelModules = true; 268 ProtectSystem = "strict"; 269 RuntimeDirectory = "unbound"; 270 ConfigurationDirectory = "unbound"; 271 StateDirectory = "unbound"; 272 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" "AF_UNIX" ]; 273 RestrictRealtime = true; 274 SystemCallArchitectures = "native"; 275 SystemCallFilter = [ 276 "~@clock" 277 "@cpu-emulation" 278 "@debug" 279 "@keyring" 280 "@module" 281 "mount" 282 "@obsolete" 283 "@resources" 284 ]; 285 RestrictNamespaces = true; 286 LockPersonality = true; 287 RestrictSUIDSGID = true; 288 289 ReadWritePaths = [ cfg.stateDir ]; 290 291 Restart = "on-failure"; 292 RestartSec = "5s"; 293 }; 294 }; 295 }; 296 297 imports = [ 298 (mkRenamedOptionModule [ "services" "unbound" "interfaces" ] [ "services" "unbound" "settings" "server" "interface" ]) 299 (mkChangedOptionModule [ "services" "unbound" "allowedAccess" ] [ "services" "unbound" "settings" "server" "access-control" ] ( 300 config: map (value: "${value} allow") (getAttrFromPath [ "services" "unbound" "allowedAccess" ] config) 301 )) 302 (mkRemovedOptionModule [ "services" "unbound" "forwardAddresses" ] '' 303 Add a new setting: 304 services.unbound.settings.forward-zone = [{ 305 name = "."; 306 forward-addr = [ # Your current services.unbound.forwardAddresses ]; 307 }]; 308 If any of those addresses are local addresses (127.0.0.1 or ::1), you must 309 also set services.unbound.settings.server.do-not-query-localhost to false. 310 '') 311 (mkRemovedOptionModule [ "services" "unbound" "extraConfig" ] '' 312 You can use services.unbound.settings to add any configuration you want. 313 '') 314 ]; 315}