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