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