at master 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 control-enable = mkDefault false; 251 control-interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1")); 252 server-key-file = mkDefault "${cfg.stateDir}/unbound_server.key"; 253 server-cert-file = mkDefault "${cfg.stateDir}/unbound_server.pem"; 254 control-key-file = mkDefault "${cfg.stateDir}/unbound_control.key"; 255 control-cert-file = mkDefault "${cfg.stateDir}/unbound_control.pem"; 256 } 257 // optionalAttrs (cfg.localControlSocketPath != null) { 258 control-enable = true; 259 control-interface = cfg.localControlSocketPath; 260 }; 261 }; 262 263 environment.systemPackages = [ cfg.package ]; 264 265 users.users = mkIf (cfg.user == "unbound") { 266 unbound = { 267 description = "unbound daemon user"; 268 isSystemUser = true; 269 group = cfg.group; 270 }; 271 }; 272 273 users.groups = mkIf (cfg.group == "unbound") { 274 unbound = { }; 275 }; 276 277 networking = mkIf cfg.resolveLocalQueries { 278 resolvconf = { 279 useLocalResolver = mkDefault true; 280 }; 281 }; 282 283 environment.etc."unbound/unbound.conf".source = confFile; 284 285 systemd.services.unbound = { 286 description = "Unbound recursive Domain Name Server"; 287 after = [ "network.target" ]; 288 before = [ "nss-lookup.target" ]; 289 wantedBy = [ 290 "multi-user.target" 291 "nss-lookup.target" 292 ]; 293 294 path = mkIf cfg.settings.remote-control.control-enable [ pkgs.openssl ]; 295 296 preStart = '' 297 ${optionalString cfg.enableRootTrustAnchor '' 298 ${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!" 299 ''} 300 ${optionalString cfg.settings.remote-control.control-enable '' 301 ${cfg.package}/bin/unbound-control-setup -d ${cfg.stateDir} 302 ''} 303 ''; 304 305 restartTriggers = [ 306 confFile 307 ]; 308 309 serviceConfig = { 310 ExecStart = "${cfg.package}/bin/unbound -p -d -c /etc/unbound/unbound.conf"; 311 ExecReload = "+/run/current-system/sw/bin/kill -HUP $MAINPID"; 312 313 NotifyAccess = "main"; 314 Type = "notify"; 315 316 AmbientCapabilities = [ 317 "CAP_NET_BIND_SERVICE" 318 "CAP_NET_RAW" # needed if ip-transparent is set to true 319 ]; 320 CapabilityBoundingSet = [ 321 "CAP_NET_BIND_SERVICE" 322 "CAP_NET_RAW" 323 ]; 324 325 User = cfg.user; 326 Group = cfg.group; 327 328 MemoryDenyWriteExecute = true; 329 NoNewPrivileges = true; 330 PrivateDevices = true; 331 PrivateTmp = true; 332 ProtectHome = true; 333 ProtectControlGroups = true; 334 ProtectKernelModules = true; 335 ProtectSystem = "strict"; 336 ProtectClock = true; 337 ProtectHostname = true; 338 ProtectProc = "invisible"; 339 ProcSubset = "pid"; 340 ProtectKernelLogs = true; 341 ProtectKernelTunables = true; 342 RuntimeDirectory = "unbound"; 343 ConfigurationDirectory = "unbound"; 344 StateDirectory = "unbound"; 345 RestrictAddressFamilies = [ 346 "AF_INET" 347 "AF_INET6" 348 "AF_NETLINK" 349 "AF_UNIX" 350 ]; 351 RestrictRealtime = true; 352 SystemCallArchitectures = "native"; 353 SystemCallFilter = [ "@system-service" ]; 354 RestrictNamespaces = true; 355 LockPersonality = true; 356 RestrictSUIDSGID = true; 357 358 ReadWritePaths = [ cfg.stateDir ]; 359 360 Restart = "on-failure"; 361 RestartSec = "5s"; 362 }; 363 }; 364 }; 365 366 imports = [ 367 (mkRenamedOptionModule 368 [ "services" "unbound" "interfaces" ] 369 [ "services" "unbound" "settings" "server" "interface" ] 370 ) 371 (mkChangedOptionModule 372 [ "services" "unbound" "allowedAccess" ] 373 [ "services" "unbound" "settings" "server" "access-control" ] 374 ( 375 config: 376 map (value: "${value} allow") (getAttrFromPath [ "services" "unbound" "allowedAccess" ] config) 377 ) 378 ) 379 (mkRemovedOptionModule [ "services" "unbound" "forwardAddresses" ] '' 380 Add a new setting: 381 services.unbound.settings.forward-zone = [{ 382 name = "."; 383 forward-addr = [ # Your current services.unbound.forwardAddresses ]; 384 }]; 385 If any of those addresses are local addresses (127.0.0.1 or ::1), you must 386 also set services.unbound.settings.server.do-not-query-localhost to false. 387 '') 388 (mkRemovedOptionModule [ "services" "unbound" "extraConfig" ] '' 389 You can use services.unbound.settings to add any configuration you want. 390 '') 391 ]; 392}