1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9let
10 cfg = config.services.resolved;
11
12 dnsmasqResolve = config.services.dnsmasq.enable && config.services.dnsmasq.resolveLocalQueries;
13
14 resolvedConf = ''
15 [Resolve]
16 ${optionalString (
17 config.networking.nameservers != [ ]
18 ) "DNS=${concatStringsSep " " config.networking.nameservers}"}
19 ${optionalString (cfg.fallbackDns != null) "FallbackDNS=${concatStringsSep " " cfg.fallbackDns}"}
20 ${optionalString (cfg.domains != [ ]) "Domains=${concatStringsSep " " cfg.domains}"}
21 LLMNR=${cfg.llmnr}
22 DNSSEC=${cfg.dnssec}
23 DNSOverTLS=${cfg.dnsovertls}
24 ${config.services.resolved.extraConfig}
25 '';
26
27in
28{
29
30 options = {
31
32 services.resolved.enable = mkOption {
33 default = false;
34 type = types.bool;
35 description = ''
36 Whether to enable the systemd DNS resolver daemon, `systemd-resolved`.
37
38 Search for `services.resolved` to see all options.
39 '';
40 };
41
42 services.resolved.fallbackDns = mkOption {
43 default = null;
44 example = [
45 "8.8.8.8"
46 "2001:4860:4860::8844"
47 ];
48 type = types.nullOr (types.listOf types.str);
49 description = ''
50 A list of IPv4 and IPv6 addresses to use as the fallback DNS servers.
51 If this option is null, a compiled-in list of DNS servers is used instead.
52 Setting this option to an empty list will override the built-in list to an empty list, disabling fallback.
53 '';
54 };
55
56 services.resolved.domains = mkOption {
57 default = config.networking.search;
58 defaultText = literalExpression "config.networking.search";
59 example = [ "example.com" ];
60 type = types.listOf types.str;
61 description = ''
62 A list of domains. These domains are used as search suffixes
63 when resolving single-label host names (domain names which
64 contain no dot), in order to qualify them into fully-qualified
65 domain names (FQDNs).
66
67 For compatibility reasons, if this setting is not specified,
68 the search domains listed in
69 {file}`/etc/resolv.conf` are used instead, if
70 that file exists and any domains are configured in it.
71 '';
72 };
73
74 services.resolved.llmnr = mkOption {
75 default = "true";
76 example = "false";
77 type = types.enum [
78 "true"
79 "resolve"
80 "false"
81 ];
82 description = ''
83 Controls Link-Local Multicast Name Resolution support
84 (RFC 4795) on the local host.
85
86 If set to
87 - `"true"`: Enables full LLMNR responder and resolver support.
88 - `"false"`: Disables both.
89 - `"resolve"`: Only resolution support is enabled, but responding is disabled.
90 '';
91 };
92
93 services.resolved.dnssec = mkOption {
94 default = "false";
95 example = "true";
96 type = types.enum [
97 "true"
98 "allow-downgrade"
99 "false"
100 ];
101 description = ''
102 If set to
103 - `"true"`:
104 all DNS lookups are DNSSEC-validated locally (excluding
105 LLMNR and Multicast DNS). Note that this mode requires a
106 DNS server that supports DNSSEC. If the DNS server does
107 not properly support DNSSEC all validations will fail.
108 - `"allow-downgrade"`:
109 DNSSEC validation is attempted, but if the server does not
110 support DNSSEC properly, DNSSEC mode is automatically
111 disabled. Note that this mode makes DNSSEC validation
112 vulnerable to "downgrade" attacks, where an attacker might
113 be able to trigger a downgrade to non-DNSSEC mode by
114 synthesizing a DNS response that suggests DNSSEC was not
115 supported.
116 - `"false"`: DNS lookups are not DNSSEC validated.
117
118 At the time of September 2023, systemd upstream advise
119 to disable DNSSEC by default as the current code
120 is not robust enough to deal with "in the wild" non-compliant
121 servers, which will usually give you a broken bad experience
122 in addition of insecure.
123 '';
124 };
125
126 services.resolved.dnsovertls = mkOption {
127 default = "false";
128 example = "true";
129 type = types.enum [
130 "true"
131 "opportunistic"
132 "false"
133 ];
134 description = ''
135 If set to
136 - `"true"`:
137 all DNS lookups will be encrypted. This requires
138 that the DNS server supports DNS-over-TLS and
139 has a valid certificate. If the hostname was specified
140 via the `address#hostname` format in {option}`services.resolved.domains`
141 then the specified hostname is used to validate its certificate.
142 - `"opportunistic"`:
143 all DNS lookups will attempt to be encrypted, but will fallback
144 to unecrypted requests if the server does not support DNS-over-TLS.
145 Note that this mode does allow for a malicious party to conduct a
146 downgrade attack by immitating the DNS server and pretending to not
147 support encryption.
148 - `"false"`:
149 all DNS lookups are done unencrypted.
150 '';
151 };
152
153 services.resolved.extraConfig = mkOption {
154 default = "";
155 type = types.lines;
156 description = ''
157 Extra config to append to resolved.conf.
158 '';
159 };
160
161 boot.initrd.services.resolved.enable = mkOption {
162 default = config.boot.initrd.systemd.network.enable;
163 defaultText = "config.boot.initrd.systemd.network.enable";
164 description = ''
165 Whether to enable resolved for stage 1 networking.
166 Uses the toplevel 'services.resolved' options for 'resolved.conf'
167 '';
168 };
169
170 };
171
172 config = mkMerge [
173 (mkIf cfg.enable {
174
175 assertions = [
176 {
177 assertion = !config.networking.useHostResolvConf;
178 message = "Using host resolv.conf is not supported with systemd-resolved";
179 }
180 ];
181
182 users.users.systemd-resolve.group = "systemd-resolve";
183
184 # add resolve to nss hosts database if enabled and nscd enabled
185 # system.nssModules is configured in nixos/modules/system/boot/systemd.nix
186 # added with order 501 to allow modules to go before with mkBefore
187 system.nssDatabases.hosts = (mkOrder 501 [ "resolve [!UNAVAIL=return]" ]);
188
189 systemd.additionalUpstreamSystemUnits = [
190 "systemd-resolved.service"
191 ];
192
193 systemd.services.systemd-resolved = {
194 wantedBy = [ "sysinit.target" ];
195 aliases = [ "dbus-org.freedesktop.resolve1.service" ];
196 reloadTriggers = [ config.environment.etc."systemd/resolved.conf".source ];
197 stopIfChanged = false;
198 };
199
200 environment.etc =
201 {
202 "systemd/resolved.conf".text = resolvedConf;
203
204 # symlink the dynamic stub resolver of resolv.conf as recommended by upstream:
205 # https://www.freedesktop.org/software/systemd/man/systemd-resolved.html#/etc/resolv.conf
206 "resolv.conf".source = "/run/systemd/resolve/stub-resolv.conf";
207 }
208 // optionalAttrs dnsmasqResolve {
209 "dnsmasq-resolv.conf".source = "/run/systemd/resolve/resolv.conf";
210 };
211
212 # If networkmanager is enabled, ask it to interface with resolved.
213 networking.networkmanager.dns = "systemd-resolved";
214
215 networking.resolvconf.package = pkgs.systemd;
216
217 })
218
219 (mkIf config.boot.initrd.services.resolved.enable {
220
221 assertions = [
222 {
223 assertion = config.boot.initrd.systemd.enable;
224 message = "'boot.initrd.services.resolved.enable' can only be enabled with systemd stage 1.";
225 }
226 ];
227
228 boot.initrd.systemd = {
229 contents = {
230 "/etc/systemd/resolved.conf".text = resolvedConf;
231 };
232
233 tmpfiles.settings.systemd-resolved-stub."/etc/resolv.conf".L.argument =
234 "/run/systemd/resolve/stub-resolv.conf";
235
236 additionalUpstreamUnits = [ "systemd-resolved.service" ];
237 users.systemd-resolve = { };
238 groups.systemd-resolve = { };
239 storePaths = [ "${config.boot.initrd.systemd.package}/lib/systemd/systemd-resolved" ];
240 services.systemd-resolved = {
241 wantedBy = [ "sysinit.target" ];
242 aliases = [ "dbus-org.freedesktop.resolve1.service" ];
243 };
244 };
245
246 })
247 ];
248
249}