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