Skip to content

Commit b9fe94c

Browse files
martinpittkeszybz
authored andcommitted
resolved: don't query domain-limited DNS servers for other domains (systemd#3621)
DNS servers which have route-only domains should only be used for the specified domains. Routing queries about other domains there is a privacy violation, prone to fail (as that DNS server was not meant to be used for other domains), and puts unnecessary load onto that server. Introduce a new helper function dns_server_limited_domains() that checks if the DNS server should only be used for some selected domains, i. e. has some route-only domains without "~.". Use that when determining whether to query it in the scope, and when writing resolv.conf. Extend the test_route_only_dns() case to ensure that the DNS server limited to ~company does not appear in resolv.conf. Add test_route_only_dns_all_domains() to ensure that a server that also has ~. does appear in resolv.conf as global name server. These reproduce systemd#3420. Add a new test_resolved_domain_restricted_dns() test case that verifies that domain-limited DNS servers are only being used for those domains. This reproduces systemd#3421. Clarify what a "routing domain" is in the manpage. Fixes systemd#3420 Fixes systemd#3421
1 parent a86b767 commit b9fe94c

File tree

6 files changed

+152
-3
lines changed

6 files changed

+152
-3
lines changed

man/systemd.network.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -475,8 +475,8 @@
475475

476476
<para>The specified domains are also used for routing of DNS queries: look-ups for host names ending in the
477477
domains specified here are preferably routed to the DNS servers configured for this interface. If a domain
478-
name is prefixed with <literal>~</literal>, the domain name becomes a pure "routing" domain, is used for
479-
DNS query routing purposes only and is not used in the described domain search logic. By specifying a
478+
name is prefixed with <literal>~</literal>, the domain name becomes a pure "routing" domain, the DNS server
479+
is used for the given domain names only and is not used in the described domain search logic. By specifying a
480480
routing domain of <literal>~.</literal> (the tilde indicating definition of a routing domain, the dot
481481
referring to the DNS root domain which is the implied suffix of all valid DNS names) it is possible to
482482
route all DNS traffic preferably to the DNS server specified for this interface. The route domain logic is

src/resolve/resolved-dns-scope.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ int dns_scope_socket_tcp(DnsScope *s, int family, const union in_addr_union *add
407407

408408
DnsScopeMatch dns_scope_good_domain(DnsScope *s, int ifindex, uint64_t flags, const char *domain) {
409409
DnsSearchDomain *d;
410+
DnsServer *dns_server;
410411

411412
assert(s);
412413
assert(domain);
@@ -447,6 +448,13 @@ DnsScopeMatch dns_scope_good_domain(DnsScope *s, int ifindex, uint64_t flags, co
447448
if (dns_name_endswith(domain, d->name) > 0)
448449
return DNS_SCOPE_YES;
449450

451+
/* If the DNS server has route-only domains, don't send other requests
452+
* to it. This would be a privacy violation, will most probably fail
453+
* anyway, and adds unnecessary load. */
454+
dns_server = dns_scope_get_dns_server(s);
455+
if (dns_server && dns_server_limited_domains(dns_server))
456+
return DNS_SCOPE_NO;
457+
450458
switch (s->protocol) {
451459

452460
case DNS_PROTOCOL_DNS:

src/resolve/resolved-dns-server.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,27 @@ void dns_server_warn_downgrade(DnsServer *server) {
576576
server->warned_downgrade = true;
577577
}
578578

579+
bool dns_server_limited_domains(DnsServer *server)
580+
{
581+
DnsSearchDomain *domain;
582+
bool domain_restricted = false;
583+
584+
/* Check if the server has route-only domains without ~., i. e. whether
585+
* it should only be used for particular domains */
586+
if (!server->link)
587+
return false;
588+
589+
LIST_FOREACH(domains, domain, server->link->search_domains)
590+
if (domain->route_only) {
591+
domain_restricted = true;
592+
/* ~. means "any domain", thus it is a global server */
593+
if (streq(DNS_SEARCH_DOMAIN_NAME(domain), "."))
594+
return false;
595+
}
596+
597+
return domain_restricted;
598+
}
599+
579600
static void dns_server_hash_func(const void *p, struct siphash *state) {
580601
const DnsServer *s = p;
581602

src/resolve/resolved-dns-server.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ bool dns_server_dnssec_supported(DnsServer *server);
128128

129129
void dns_server_warn_downgrade(DnsServer *server);
130130

131+
bool dns_server_limited_domains(DnsServer *server);
132+
131133
DnsServer *dns_server_find(DnsServer *first, int family, const union in_addr_union *in_addr, int ifindex);
132134

133135
void dns_server_unlink_all(DnsServer *first);

src/resolve/resolved-resolv-conf.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,16 @@ static void write_resolv_conf_server(DnsServer *s, FILE *f, unsigned *count) {
154154
return;
155155
}
156156

157+
/* Check if the DNS server is limited to particular domains;
158+
* resolv.conf does not have a syntax to express that, so it must not
159+
* appear as a global name server to avoid routing unrelated domains to
160+
* it (which is a privacy violation, will most probably fail anyway,
161+
* and adds unnecessary load) */
162+
if (dns_server_limited_domains(s)) {
163+
log_debug("DNS server %s has route-only domains, not using as global name server", dns_server_string(s));
164+
return;
165+
}
166+
157167
if (*count == MAXNS)
158168
fputs("# Too many DNS servers configured, the following entries may be ignored.\n", f);
159169
(*count)++;

test/networkd-test.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,38 @@ def test_route_only_dns(self):
250250
self.assertNotRegex(contents, 'search.*company')
251251
# our global server should appear
252252
self.assertIn('nameserver 192.168.5.1\n', contents)
253+
# should not have domain-restricted server as global server
254+
self.assertNotIn('nameserver 192.168.42.1\n', contents)
255+
256+
def test_route_only_dns_all_domains(self):
257+
with open('/run/systemd/network/myvpn.netdev', 'w') as f:
258+
f.write('''[NetDev]
259+
Name=dummy0
260+
Kind=dummy
261+
MACAddress=12:34:56:78:9a:bc''')
262+
with open('/run/systemd/network/myvpn.network', 'w') as f:
263+
f.write('''[Match]
264+
Name=dummy0
265+
[Network]
266+
Address=192.168.42.100
267+
DNS=192.168.42.1
268+
Domains= ~company ~.''')
269+
self.addCleanup(os.remove, '/run/systemd/network/myvpn.netdev')
270+
self.addCleanup(os.remove, '/run/systemd/network/myvpn.network')
271+
272+
self.do_test(coldplug=True, ipv6=False,
273+
extra_opts='IPv6AcceptRouterAdvertisements=False')
274+
275+
with open(RESOLV_CONF) as f:
276+
contents = f.read()
277+
278+
# ~company is not a search domain, only a routing domain
279+
self.assertNotRegex(contents, 'search.*company')
280+
281+
# our global server should appear
282+
self.assertIn('nameserver 192.168.5.1\n', contents)
283+
# should have company server as global server due to ~.
284+
self.assertIn('nameserver 192.168.42.1\n', contents)
253285

254286

255287
@unittest.skipUnless(have_dnsmasq, 'dnsmasq not installed')
@@ -260,7 +292,7 @@ def setUp(self):
260292
super().setUp()
261293
self.dnsmasq = None
262294

263-
def create_iface(self, ipv6=False):
295+
def create_iface(self, ipv6=False, dnsmasq_opts=None):
264296
'''Create test interface with DHCP server behind it'''
265297

266298
# add veth pair
@@ -281,6 +313,8 @@ def create_iface(self, ipv6=False):
281313
extra_opts = ['--enable-ra', '--dhcp-range=2600::10,2600::20']
282314
else:
283315
extra_opts = []
316+
if dnsmasq_opts:
317+
extra_opts += dnsmasq_opts
284318
self.dnsmasq = subprocess.Popen(
285319
['dnsmasq', '--keep-in-foreground', '--log-queries',
286320
'--log-facility=' + self.dnsmasq_log, '--conf-file=/dev/null',
@@ -305,6 +339,80 @@ def print_server_log(self):
305339
with open(self.dnsmasq_log) as f:
306340
sys.stdout.write('\n\n---- dnsmasq log ----\n%s\n------\n\n' % f.read())
307341

342+
def test_resolved_domain_restricted_dns(self):
343+
'''resolved: domain-restricted DNS servers'''
344+
345+
# create interface for generic connections; this will map all DNS names
346+
# to 192.168.42.1
347+
self.create_iface(dnsmasq_opts=['--address=/#/192.168.42.1'])
348+
self.writeConfig('/run/systemd/network/general.network', '''\
349+
[Match]
350+
Name=%s
351+
[Network]
352+
DHCP=ipv4
353+
IPv6AcceptRA=False''' % self.iface)
354+
355+
# create second device/dnsmasq for a .company/.lab VPN interface
356+
# static IPs for simplicity
357+
subprocess.check_call(['ip', 'link', 'add', 'name', 'testvpnclient', 'type',
358+
'veth', 'peer', 'name', 'testvpnrouter'])
359+
self.addCleanup(subprocess.call, ['ip', 'link', 'del', 'dev', 'testvpnrouter'])
360+
subprocess.check_call(['ip', 'a', 'flush', 'dev', 'testvpnrouter'])
361+
subprocess.check_call(['ip', 'a', 'add', '10.241.3.1/24', 'dev', 'testvpnrouter'])
362+
subprocess.check_call(['ip', 'link', 'set', 'testvpnrouter', 'up'])
363+
364+
vpn_dnsmasq_log = os.path.join(self.workdir, 'dnsmasq-vpn.log')
365+
vpn_dnsmasq = subprocess.Popen(
366+
['dnsmasq', '--keep-in-foreground', '--log-queries',
367+
'--log-facility=' + vpn_dnsmasq_log, '--conf-file=/dev/null',
368+
'--dhcp-leasefile=/dev/null', '--bind-interfaces',
369+
'--interface=testvpnrouter', '--except-interface=lo',
370+
'--address=/math.lab/10.241.3.3', '--address=/cantina.company/10.241.4.4'])
371+
self.addCleanup(vpn_dnsmasq.wait)
372+
self.addCleanup(vpn_dnsmasq.kill)
373+
374+
self.writeConfig('/run/systemd/network/vpn.network', '''\
375+
[Match]
376+
Name=testvpnclient
377+
[Network]
378+
IPv6AcceptRA=False
379+
Address=10.241.3.2/24
380+
DNS=10.241.3.1
381+
Domains= ~company ~lab''')
382+
383+
subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
384+
subprocess.check_call([self.networkd_wait_online, '--interface', self.iface,
385+
'--interface=testvpnclient', '--timeout=20'])
386+
387+
# ensure we start fresh with every test
388+
subprocess.check_call(['systemctl', 'restart', 'systemd-resolved'])
389+
390+
# test vpnclient specific domains; these should *not* be answered by
391+
# the general DNS
392+
out = subprocess.check_output(['systemd-resolve', 'math.lab'])
393+
self.assertIn(b'math.lab: 10.241.3.3', out)
394+
out = subprocess.check_output(['systemd-resolve', 'kettle.cantina.company'])
395+
self.assertIn(b'kettle.cantina.company: 10.241.4.4', out)
396+
397+
# test general domains
398+
out = subprocess.check_output(['systemd-resolve', 'megasearch.net'])
399+
self.assertIn(b'megasearch.net: 192.168.42.1', out)
400+
401+
with open(self.dnsmasq_log) as f:
402+
general_log = f.read()
403+
with open(vpn_dnsmasq_log) as f:
404+
vpn_log = f.read()
405+
406+
# VPN domains should only be sent to VPN DNS
407+
self.assertRegex(vpn_log, 'query.*math.lab')
408+
self.assertRegex(vpn_log, 'query.*cantina.company')
409+
self.assertNotIn('lab', general_log)
410+
self.assertNotIn('company', general_log)
411+
412+
# general domains should not be sent to the VPN DNS
413+
self.assertRegex(general_log, 'query.*megasearch.net')
414+
self.assertNotIn('megasearch.net', vpn_log)
415+
308416

309417
class NetworkdClientTest(ClientTestBase, unittest.TestCase):
310418
'''Test networkd client against networkd server'''

0 commit comments

Comments
 (0)