]>
Commit | Line | Data |
---|---|---|
db9ecf05 | 1 | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
f8dc7e34 LP |
2 | |
3 | #include <resolv.h> | |
ca78ad1d ZJS |
4 | #include <sys/stat.h> |
5 | #include <sys/types.h> | |
6 | #include <unistd.h> | |
f8dc7e34 LP |
7 | |
8 | #include "alloc-util.h" | |
9 | #include "dns-domain.h" | |
10 | #include "fd-util.h" | |
f8dc7e34 | 11 | #include "fileio.h" |
ca8b81d9 | 12 | #include "fs-util.h" |
0690160e | 13 | #include "label-util.h" |
f8dc7e34 | 14 | #include "ordered-set.h" |
b8953115 | 15 | #include "path-util.h" |
f8dc7e34 | 16 | #include "resolved-conf.h" |
59c0fd0e | 17 | #include "resolved-dns-server.h" |
f8dc7e34 | 18 | #include "resolved-resolv-conf.h" |
61c12865 | 19 | #include "stat-util.h" |
4261ab65 | 20 | #include "string-table.h" |
f8dc7e34 LP |
21 | #include "string-util.h" |
22 | #include "strv.h" | |
e4de7287 | 23 | #include "tmpfile-util-label.h" |
f8dc7e34 | 24 | |
f43580f1 | 25 | int manager_check_resolv_conf(const Manager *m) { |
ca8b81d9 | 26 | struct stat st, own; |
f43580f1 YW |
27 | |
28 | assert(m); | |
29 | ||
30 | /* This warns only when our stub listener is disabled and /etc/resolv.conf is a symlink to | |
ca8b81d9 | 31 | * PRIVATE_STATIC_RESOLV_CONF. */ |
f43580f1 YW |
32 | |
33 | if (m->dns_stub_listener_mode != DNS_STUB_LISTENER_NO) | |
34 | return 0; | |
35 | ||
ca8b81d9 | 36 | if (stat("/etc/resolv.conf", &st) < 0) { |
f43580f1 YW |
37 | if (errno == ENOENT) |
38 | return 0; | |
39 | ||
40 | return log_warning_errno(errno, "Failed to stat /etc/resolv.conf: %m"); | |
41 | } | |
42 | ||
ca8b81d9 ZJS |
43 | /* Is it symlinked to our own uplink file? */ |
44 | if (stat(PRIVATE_STATIC_RESOLV_CONF, &own) >= 0 && | |
a9dac7a6 | 45 | stat_inode_same(&st, &own)) |
ca8b81d9 ZJS |
46 | return log_warning_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), |
47 | "DNSStubListener= is disabled, but /etc/resolv.conf is a symlink to " | |
48 | PRIVATE_STATIC_RESOLV_CONF " which expects DNSStubListener= to be enabled."); | |
f43580f1 YW |
49 | |
50 | return 0; | |
51 | } | |
52 | ||
043d3928 | 53 | static bool file_is_our_own(const struct stat *st) { |
043d3928 LP |
54 | assert(st); |
55 | ||
b6de578d LP |
56 | FOREACH_STRING(path, |
57 | PRIVATE_UPLINK_RESOLV_CONF, | |
58 | PRIVATE_STUB_RESOLV_CONF, | |
59 | PRIVATE_STATIC_RESOLV_CONF) { | |
60 | ||
61 | struct stat own; | |
62 | ||
63 | /* Is it symlinked to our own uplink file? */ | |
64 | if (stat(path, &own) >= 0 && | |
a9dac7a6 | 65 | stat_inode_same(st, &own)) |
b6de578d LP |
66 | return true; |
67 | } | |
043d3928 LP |
68 | |
69 | return false; | |
70 | } | |
71 | ||
f8dc7e34 LP |
72 | int manager_read_resolv_conf(Manager *m) { |
73 | _cleanup_fclose_ FILE *f = NULL; | |
043d3928 | 74 | struct stat st; |
b351c300 | 75 | unsigned n = 0; |
f8dc7e34 LP |
76 | int r; |
77 | ||
78 | assert(m); | |
79 | ||
80 | /* Reads the system /etc/resolv.conf, if it exists and is not | |
81 | * symlinked to our own resolv.conf instance */ | |
82 | ||
83 | if (!m->read_resolv_conf) | |
84 | return 0; | |
85 | ||
86 | r = stat("/etc/resolv.conf", &st); | |
87 | if (r < 0) { | |
88 | if (errno == ENOENT) | |
bf7fabd6 LP |
89 | return 0; |
90 | ||
91 | r = log_warning_errno(errno, "Failed to stat /etc/resolv.conf: %m"); | |
f8dc7e34 LP |
92 | goto clear; |
93 | } | |
94 | ||
95 | /* Have we already seen the file? */ | |
61c12865 | 96 | if (stat_inode_unmodified(&st, &m->resolv_conf_stat)) |
f8dc7e34 LP |
97 | return 0; |
98 | ||
043d3928 | 99 | if (file_is_our_own(&st)) |
bf7fabd6 | 100 | return 0; |
f8dc7e34 LP |
101 | |
102 | f = fopen("/etc/resolv.conf", "re"); | |
103 | if (!f) { | |
104 | if (errno == ENOENT) | |
bf7fabd6 LP |
105 | return 0; |
106 | ||
107 | r = log_warning_errno(errno, "Failed to open /etc/resolv.conf: %m"); | |
f8dc7e34 LP |
108 | goto clear; |
109 | } | |
110 | ||
111 | if (fstat(fileno(f), &st) < 0) { | |
112 | r = log_error_errno(errno, "Failed to stat open file: %m"); | |
113 | goto clear; | |
114 | } | |
115 | ||
043d3928 LP |
116 | if (file_is_our_own(&st)) |
117 | return 0; | |
118 | ||
4b95f179 | 119 | dns_server_mark_all(m->dns_servers); |
a51c1048 | 120 | dns_search_domain_mark_all(m->search_domains); |
f8dc7e34 | 121 | |
e1b9fc23 LP |
122 | for (;;) { |
123 | _cleanup_free_ char *line = NULL; | |
f8dc7e34 | 124 | const char *a; |
f8dc7e34 | 125 | |
0ff6ff2b | 126 | r = read_stripped_line(f, LONG_LINE_MAX, &line); |
e1b9fc23 LP |
127 | if (r < 0) { |
128 | log_error_errno(r, "Failed to read /etc/resolv.conf: %m"); | |
129 | goto clear; | |
130 | } | |
131 | if (r == 0) | |
132 | break; | |
133 | ||
b351c300 LP |
134 | n++; |
135 | ||
0ff6ff2b | 136 | if (IN_SET(*line, '#', ';', 0)) |
f8dc7e34 LP |
137 | continue; |
138 | ||
0ff6ff2b | 139 | a = first_word(line, "nameserver"); |
f8dc7e34 | 140 | if (a) { |
281df579 | 141 | r = manager_parse_dns_server_string_and_warn(m, DNS_SERVER_SYSTEM, a); |
f8dc7e34 LP |
142 | if (r < 0) |
143 | log_warning_errno(r, "Failed to parse DNS server address '%s', ignoring.", a); | |
144 | ||
145 | continue; | |
146 | } | |
a51c1048 | 147 | |
0ff6ff2b | 148 | a = first_word(line, "domain"); |
a51c1048 | 149 | if (!a) /* We treat "domain" lines, and "search" lines as equivalent, and add both to our list. */ |
0ff6ff2b | 150 | a = first_word(line, "search"); |
a51c1048 LP |
151 | if (a) { |
152 | r = manager_parse_search_domains_and_warn(m, a); | |
153 | if (r < 0) | |
154 | log_warning_errno(r, "Failed to parse search domain string '%s', ignoring.", a); | |
02c20535 LP |
155 | |
156 | continue; | |
a51c1048 | 157 | } |
b351c300 | 158 | |
0ff6ff2b | 159 | log_syntax(NULL, LOG_DEBUG, "/etc/resolv.conf", n, 0, "Ignoring resolv.conf line: %s", line); |
f8dc7e34 LP |
160 | } |
161 | ||
61c12865 | 162 | m->resolv_conf_stat = st; |
bf7fabd6 | 163 | |
a51c1048 LP |
164 | /* Flush out all servers and search domains that are still |
165 | * marked. Those are then ones that didn't appear in the new | |
166 | * /etc/resolv.conf */ | |
4b95f179 | 167 | dns_server_unlink_marked(m->dns_servers); |
a51c1048 | 168 | dns_search_domain_unlink_marked(m->search_domains); |
f8dc7e34 LP |
169 | |
170 | /* Whenever /etc/resolv.conf changes, start using the first | |
171 | * DNS server of it. This is useful to deal with broken | |
172 | * network managing implementations (like NetworkManager), | |
173 | * that when connecting to a VPN place both the VPN DNS | |
174 | * servers and the local ones in /etc/resolv.conf. Without | |
175 | * resetting the DNS server to use back to the first entry we | |
176 | * will continue to use the local one thus being unable to | |
177 | * resolve VPN domains. */ | |
178 | manager_set_dns_server(m, m->dns_servers); | |
179 | ||
452b4e32 LP |
180 | /* Unconditionally flush the cache when /etc/resolv.conf is |
181 | * modified, even if the data it contained was completely | |
182 | * identical to the previous version we used. We do this | |
183 | * because altering /etc/resolv.conf is typically done when | |
184 | * the network configuration changes, and that should be | |
185 | * enough to flush the global unicast DNS cache. */ | |
186 | if (m->unicast_scope) | |
187 | dns_cache_flush(&m->unicast_scope->cache); | |
188 | ||
59c0fd0e LP |
189 | /* If /etc/resolv.conf changed, make sure to forget everything we learned about the DNS servers. After all we |
190 | * might now talk to a very different DNS server that just happens to have the same IP address as an old one | |
191 | * (think 192.168.1.1). */ | |
192 | dns_server_reset_features_all(m->dns_servers); | |
193 | ||
f8dc7e34 LP |
194 | return 0; |
195 | ||
196 | clear: | |
4b95f179 | 197 | dns_server_unlink_all(m->dns_servers); |
a51c1048 | 198 | dns_search_domain_unlink_all(m->search_domains); |
f8dc7e34 LP |
199 | return r; |
200 | } | |
201 | ||
202 | static void write_resolv_conf_server(DnsServer *s, FILE *f, unsigned *count) { | |
f76fa088 LP |
203 | DnsScope *scope; |
204 | ||
f8dc7e34 LP |
205 | assert(s); |
206 | assert(f); | |
207 | assert(count); | |
208 | ||
2817157b | 209 | if (!dns_server_string(s)) { |
be4bf266 | 210 | log_warning("Out of memory, or invalid DNS address. Ignoring server."); |
f8dc7e34 LP |
211 | return; |
212 | } | |
213 | ||
a50dadf2 LP |
214 | /* resolv.conf simply doesn't support any other ports than 53, hence there's nothing much we can |
215 | * do — we have to suppress these entries */ | |
216 | if (dns_server_port(s) != 53) { | |
217 | log_debug("DNS server %s with non-standard UDP port number, suppressing from generated resolv.conf.", dns_server_string(s)); | |
218 | return; | |
219 | } | |
220 | ||
ca5394d2 LP |
221 | /* Check if the scope this DNS server belongs to is suitable as 'default' route for lookups; resolv.conf does |
222 | * not have a syntax to express that, so it must not appear as a global name server to avoid routing unrelated | |
223 | * domains to it (which is a privacy violation, will most probably fail anyway, and adds unnecessary load) */ | |
f76fa088 | 224 | scope = dns_server_scope(s); |
ca5394d2 | 225 | if (scope && !dns_scope_is_default_route(scope)) { |
f76fa088 | 226 | log_debug("Scope of DNS server %s has only route-only domains, not using as global name server", dns_server_string(s)); |
b9fe94ca MP |
227 | return; |
228 | } | |
229 | ||
f8dc7e34 | 230 | if (*count == MAXNS) |
0d536673 | 231 | fputs("# Too many DNS servers configured, the following entries may be ignored.\n", f); |
313cefa1 | 232 | (*count)++; |
f8dc7e34 | 233 | |
2817157b | 234 | fprintf(f, "nameserver %s\n", dns_server_string(s)); |
f8dc7e34 LP |
235 | } |
236 | ||
237 | static void write_resolv_conf_search( | |
d2bc1251 MP |
238 | OrderedSet *domains, |
239 | FILE *f) { | |
d2bc1251 | 240 | char *domain; |
f8dc7e34 | 241 | |
d2bc1251 | 242 | assert(domains); |
f8dc7e34 | 243 | assert(f); |
f8dc7e34 | 244 | |
0d536673 | 245 | fputs("search", f); |
f8dc7e34 | 246 | |
90e74a66 | 247 | ORDERED_SET_FOREACH(domain, domains) { |
0d536673 LP |
248 | fputc(' ', f); |
249 | fputs(domain, f); | |
f8dc7e34 LP |
250 | } |
251 | ||
0d536673 | 252 | fputs("\n", f); |
f8dc7e34 LP |
253 | } |
254 | ||
e6b2d948 | 255 | static int write_uplink_resolv_conf_contents(FILE *f, OrderedSet *dns, OrderedSet *domains) { |
f8dc7e34 | 256 | |
2b767e92 ZJS |
257 | fputs("# This is "PRIVATE_UPLINK_RESOLV_CONF" managed by man:systemd-resolved(8).\n" |
258 | "# Do not edit.\n" | |
259 | "#\n" | |
260 | "# This file might be symlinked as /etc/resolv.conf. If you're looking at\n" | |
261 | "# /etc/resolv.conf and seeing this text, you have followed the symlink.\n" | |
0d536673 LP |
262 | "#\n" |
263 | "# This is a dynamic resolv.conf file for connecting local clients directly to\n" | |
264 | "# all known uplink DNS servers. This file lists all configured search domains.\n" | |
265 | "#\n" | |
ce416f42 LP |
266 | "# Third party programs should typically not access this file directly, but only\n" |
267 | "# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a\n" | |
268 | "# different way, replace this symlink by a static file or a different symlink.\n" | |
0d536673 LP |
269 | "#\n" |
270 | "# See man:systemd-resolved.service(8) for details about the supported modes of\n" | |
271 | "# operation for /etc/resolv.conf.\n" | |
272 | "\n", f); | |
f8dc7e34 LP |
273 | |
274 | if (ordered_set_isempty(dns)) | |
0d536673 | 275 | fputs("# No DNS servers known.\n", f); |
f8dc7e34 LP |
276 | else { |
277 | unsigned count = 0; | |
278 | DnsServer *s; | |
279 | ||
90e74a66 | 280 | ORDERED_SET_FOREACH(s, dns) |
f8dc7e34 LP |
281 | write_resolv_conf_server(s, f, &count); |
282 | } | |
283 | ||
b3ffa2b5 | 284 | if (ordered_set_isempty(domains)) |
31619e2f ZJS |
285 | fputs("search .\n", f); /* Make sure that if the local hostname is chosen as fqdn this does not |
286 | * imply a search domain */ | |
b3ffa2b5 | 287 | else |
d2bc1251 | 288 | write_resolv_conf_search(domains, f); |
f8dc7e34 LP |
289 | |
290 | return fflush_and_check(f); | |
291 | } | |
292 | ||
e6b2d948 | 293 | static int write_stub_resolv_conf_contents(FILE *f, OrderedSet *dns, OrderedSet *domains) { |
2b767e92 ZJS |
294 | fputs("# This is "PRIVATE_STUB_RESOLV_CONF" managed by man:systemd-resolved(8).\n" |
295 | "# Do not edit.\n" | |
296 | "#\n" | |
297 | "# This file might be symlinked as /etc/resolv.conf. If you're looking at\n" | |
298 | "# /etc/resolv.conf and seeing this text, you have followed the symlink.\n" | |
ce416f42 LP |
299 | "#\n" |
300 | "# This is a dynamic resolv.conf file for connecting local clients to the\n" | |
301 | "# internal DNS stub resolver of systemd-resolved. This file lists all\n" | |
302 | "# configured search domains.\n" | |
303 | "#\n" | |
304 | "# Run \"resolvectl status\" to see details about the uplink DNS servers\n" | |
305 | "# currently in use.\n" | |
306 | "#\n" | |
307 | "# Third party programs should typically not access this file directly, but only\n" | |
308 | "# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a\n" | |
309 | "# different way, replace this symlink by a static file or a different symlink.\n" | |
310 | "#\n" | |
311 | "# See man:systemd-resolved.service(8) for details about the supported modes of\n" | |
312 | "# operation for /etc/resolv.conf.\n" | |
313 | "\n" | |
314 | "nameserver 127.0.0.53\n" | |
a742f982 | 315 | "options edns0 trust-ad\n", f); |
e6b2d948 | 316 | |
b3ffa2b5 | 317 | if (ordered_set_isempty(domains)) |
31619e2f ZJS |
318 | fputs("search .\n", f); /* Make sure that if the local hostname is chosen as fqdn this does not |
319 | * imply a search domain */ | |
b3ffa2b5 | 320 | else |
e6b2d948 DJL |
321 | write_resolv_conf_search(domains, f); |
322 | ||
323 | return fflush_and_check(f); | |
324 | } | |
325 | ||
f8dc7e34 | 326 | int manager_write_resolv_conf(Manager *m) { |
9176a57c | 327 | _cleanup_ordered_set_free_ OrderedSet *dns = NULL, *domains = NULL; |
e2ef1e9a | 328 | _cleanup_(unlink_and_freep) char *temp_path_uplink = NULL, *temp_path_stub = NULL; |
0d536673 | 329 | _cleanup_fclose_ FILE *f_uplink = NULL, *f_stub = NULL; |
f8dc7e34 LP |
330 | int r; |
331 | ||
332 | assert(m); | |
333 | ||
334 | /* Read the system /etc/resolv.conf first */ | |
7207052d | 335 | (void) manager_read_resolv_conf(m); |
f8dc7e34 LP |
336 | |
337 | /* Add the full list to a set, to filter out duplicates */ | |
9176a57c LP |
338 | r = manager_compile_dns_servers(m, &dns); |
339 | if (r < 0) | |
e2ef1e9a | 340 | return log_warning_errno(r, "Failed to compile list of DNS servers, ignoring: %m"); |
f8dc7e34 | 341 | |
6f7da49d | 342 | r = manager_compile_search_domains(m, &domains, false); |
9176a57c | 343 | if (r < 0) |
e2ef1e9a | 344 | return log_warning_errno(r, "Failed to compile list of search domains, ignoring: %m"); |
f8dc7e34 | 345 | |
e6b2d948 | 346 | r = fopen_temporary_label(PRIVATE_UPLINK_RESOLV_CONF, PRIVATE_UPLINK_RESOLV_CONF, &f_uplink, &temp_path_uplink); |
f8dc7e34 | 347 | if (r < 0) |
e2ef1e9a | 348 | return log_warning_errno(r, "Failed to open new %s for writing, ignoring: %m", PRIVATE_UPLINK_RESOLV_CONF); |
0d536673 | 349 | |
0d536673 LP |
350 | (void) fchmod(fileno(f_uplink), 0644); |
351 | ||
e6b2d948 | 352 | r = write_uplink_resolv_conf_contents(f_uplink, dns, domains); |
e2ef1e9a LP |
353 | if (r < 0) |
354 | return log_warning_errno(r, "Failed to write new %s, ignoring: %m", PRIVATE_UPLINK_RESOLV_CONF); | |
f8dc7e34 | 355 | |
ca8b81d9 ZJS |
356 | if (m->dns_stub_listener_mode != DNS_STUB_LISTENER_NO) { |
357 | r = fopen_temporary_label(PRIVATE_STUB_RESOLV_CONF, PRIVATE_STUB_RESOLV_CONF, &f_stub, &temp_path_stub); | |
e2ef1e9a LP |
358 | if (r < 0) |
359 | return log_warning_errno(r, "Failed to open new %s for writing, ignoring: %m", PRIVATE_STUB_RESOLV_CONF); | |
f8dc7e34 | 360 | |
ca8b81d9 | 361 | (void) fchmod(fileno(f_stub), 0644); |
965228a8 | 362 | |
ca8b81d9 | 363 | r = write_stub_resolv_conf_contents(f_stub, dns, domains); |
e2ef1e9a LP |
364 | if (r < 0) |
365 | return log_warning_errno(r, "Failed to write new %s, ignoring: %m", PRIVATE_STUB_RESOLV_CONF); | |
ca8b81d9 | 366 | |
10195179 | 367 | r = conservative_rename(temp_path_stub, PRIVATE_STUB_RESOLV_CONF); |
f3e1f00d | 368 | if (r < 0) |
e2ef1e9a | 369 | log_warning_errno(r, "Failed to move new %s into place, ignoring: %m", PRIVATE_STUB_RESOLV_CONF); |
ca8b81d9 | 370 | |
e2ef1e9a | 371 | temp_path_stub = mfree(temp_path_stub); /* free the string explicitly, so that we don't unlink anymore */ |
ca8b81d9 | 372 | } else { |
b8953115 SL |
373 | _cleanup_free_ char *fname = NULL; |
374 | r = path_extract_filename(PRIVATE_UPLINK_RESOLV_CONF, &fname); | |
375 | if (r < 0) | |
376 | return log_warning_errno(r, "Failed to extract filename from path '" PRIVATE_UPLINK_RESOLV_CONF "', ignoring: %m"); | |
377 | ||
378 | r = symlink_atomic_label(fname, PRIVATE_STUB_RESOLV_CONF); | |
ca8b81d9 | 379 | if (r < 0) |
e2ef1e9a | 380 | log_warning_errno(r, "Failed to symlink %s, ignoring: %m", PRIVATE_STUB_RESOLV_CONF); |
e6b2d948 DJL |
381 | } |
382 | ||
10195179 | 383 | r = conservative_rename(temp_path_uplink, PRIVATE_UPLINK_RESOLV_CONF); |
f3e1f00d | 384 | if (r < 0) |
e2ef1e9a | 385 | log_warning_errno(r, "Failed to move new %s into place: %m", PRIVATE_UPLINK_RESOLV_CONF); |
7207052d | 386 | |
e2ef1e9a | 387 | temp_path_uplink = mfree(temp_path_uplink); /* free the string explicitly, so that we don't unlink anymore */ |
f8dc7e34 LP |
388 | return r; |
389 | } | |
4261ab65 LP |
390 | |
391 | int resolv_conf_mode(void) { | |
392 | static const char * const table[_RESOLV_CONF_MODE_MAX] = { | |
393 | [RESOLV_CONF_UPLINK] = PRIVATE_UPLINK_RESOLV_CONF, | |
394 | [RESOLV_CONF_STUB] = PRIVATE_STUB_RESOLV_CONF, | |
395 | [RESOLV_CONF_STATIC] = PRIVATE_STATIC_RESOLV_CONF, | |
396 | }; | |
397 | ||
398 | struct stat system_st; | |
399 | ||
400 | if (stat("/etc/resolv.conf", &system_st) < 0) { | |
401 | if (errno == ENOENT) | |
402 | return RESOLV_CONF_MISSING; | |
403 | ||
404 | return -errno; | |
405 | } | |
406 | ||
407 | for (ResolvConfMode m = 0; m < _RESOLV_CONF_MODE_MAX; m++) { | |
408 | struct stat our_st; | |
409 | ||
410 | if (!table[m]) | |
411 | continue; | |
412 | ||
413 | if (stat(table[m], &our_st) < 0) { | |
414 | if (errno != ENOENT) | |
415 | log_debug_errno(errno, "Failed to stat() %s, ignoring: %m", table[m]); | |
416 | ||
417 | continue; | |
418 | } | |
419 | ||
a9dac7a6 | 420 | if (stat_inode_same(&system_st, &our_st)) |
4261ab65 LP |
421 | return m; |
422 | } | |
423 | ||
424 | return RESOLV_CONF_FOREIGN; | |
425 | } | |
426 | ||
427 | static const char* const resolv_conf_mode_table[_RESOLV_CONF_MODE_MAX] = { | |
428 | [RESOLV_CONF_UPLINK] = "uplink", | |
429 | [RESOLV_CONF_STUB] = "stub", | |
430 | [RESOLV_CONF_STATIC] = "static", | |
431 | [RESOLV_CONF_MISSING] = "missing", | |
432 | [RESOLV_CONF_FOREIGN] = "foreign", | |
433 | }; | |
434 | DEFINE_STRING_TABLE_LOOKUP(resolv_conf_mode, ResolvConfMode); |