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