]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
resolved: don't query domain-limited DNS servers for other domains (#3621)
authorMartin Pitt <martin.pitt@ubuntu.com>
Fri, 30 Sep 2016 07:30:08 +0000 (09:30 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Fri, 30 Sep 2016 07:30:08 +0000 (09:30 +0200)
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 #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 #3421.

Clarify what a "routing domain" is in the manpage.

Fixes #3420
Fixes #3421

man/systemd.network.xml
src/resolve/resolved-dns-scope.c
src/resolve/resolved-dns-server.c
src/resolve/resolved-dns-server.h
src/resolve/resolved-resolv-conf.c
test/networkd-test.py

index 9c1b10fc5c742a27569c0eebb81381651da5eb84..08dd157e31ea4247124d73214cc08d2f588746a5 100644 (file)
 
             <para>The specified domains are also used for routing of DNS queries: look-ups for host names ending in the
             domains specified here are preferably routed to the DNS servers configured for this interface. If a domain
-            name is prefixed with <literal>~</literal>, the domain name becomes a pure "routing" domain, is used for
-            DNS query routing purposes only and is not used in the described domain search logic. By specifying a
+            name is prefixed with <literal>~</literal>, the domain name becomes a pure "routing" domain, the DNS server
+            is used for the given domain names only and is not used in the described domain search logic. By specifying a
             routing domain of <literal>~.</literal> (the tilde indicating definition of a routing domain, the dot
             referring to the DNS root domain which is the implied suffix of all valid DNS names) it is possible to
             route all DNS traffic preferably to the DNS server specified for this interface. The route domain logic is
index ed0c6aa10589a8e6e452f9fb105d3b01d4f1b2c9..03811ac8e74ce7565dcbbf14848a6e7c24bf4bb5 100644 (file)
@@ -407,6 +407,7 @@ int dns_scope_socket_tcp(DnsScope *s, int family, const union in_addr_union *add
 
 DnsScopeMatch dns_scope_good_domain(DnsScope *s, int ifindex, uint64_t flags, const char *domain) {
         DnsSearchDomain *d;
+        DnsServer *dns_server;
 
         assert(s);
         assert(domain);
@@ -447,6 +448,13 @@ DnsScopeMatch dns_scope_good_domain(DnsScope *s, int ifindex, uint64_t flags, co
                 if (dns_name_endswith(domain, d->name) > 0)
                         return DNS_SCOPE_YES;
 
+        /* If the DNS server has route-only domains, don't send other requests
+         * to it. This would be a privacy violation, will most probably fail
+         * anyway, and adds unnecessary load. */
+        dns_server = dns_scope_get_dns_server(s);
+        if (dns_server && dns_server_limited_domains(dns_server))
+                return DNS_SCOPE_NO;
+
         switch (s->protocol) {
 
         case DNS_PROTOCOL_DNS:
index 9b7b471600cf2915f7717a3cb23c1eb616396777..97cc8c0e09ee80b810fcbe69cd9b6d635ded734e 100644 (file)
@@ -576,6 +576,27 @@ void dns_server_warn_downgrade(DnsServer *server) {
         server->warned_downgrade = true;
 }
 
+bool dns_server_limited_domains(DnsServer *server)
+{
+        DnsSearchDomain *domain;
+        bool domain_restricted = false;
+
+        /* Check if the server has route-only domains without ~., i. e. whether
+         * it should only be used for particular domains */
+        if (!server->link)
+                return false;
+
+        LIST_FOREACH(domains, domain, server->link->search_domains)
+                if (domain->route_only) {
+                        domain_restricted = true;
+                        /* ~. means "any domain", thus it is a global server */
+                        if (streq(DNS_SEARCH_DOMAIN_NAME(domain), "."))
+                                return false;
+                }
+
+        return domain_restricted;
+}
+
 static void dns_server_hash_func(const void *p, struct siphash *state) {
         const DnsServer *s = p;
 
index c1732faffdaf4697c47b76f69f7a36ec7a9993eb..83e288a202322e0dbd29f605828e45596e583fcd 100644 (file)
@@ -128,6 +128,8 @@ bool dns_server_dnssec_supported(DnsServer *server);
 
 void dns_server_warn_downgrade(DnsServer *server);
 
+bool dns_server_limited_domains(DnsServer *server);
+
 DnsServer *dns_server_find(DnsServer *first, int family, const union in_addr_union *in_addr, int ifindex);
 
 void dns_server_unlink_all(DnsServer *first);
index 31b25ca50f2e4fb64ef1fa0aa9c6d4435af2b2d4..801014caf563866595e6b2ba61dfb9dfe3dad140 100644 (file)
@@ -154,6 +154,16 @@ static void write_resolv_conf_server(DnsServer *s, FILE *f, unsigned *count) {
                 return;
         }
 
+        /* Check if the DNS server is limited to particular domains;
+         * resolv.conf does not have a syntax to express that, so it must not
+         * appear as a global name server to avoid routing unrelated domains to
+         * it (which is a privacy violation, will most probably fail anyway,
+         * and adds unnecessary load) */
+        if (dns_server_limited_domains(s)) {
+                log_debug("DNS server %s has route-only domains, not using as global name server", dns_server_string(s));
+                return;
+        }
+
         if (*count == MAXNS)
                 fputs("# Too many DNS servers configured, the following entries may be ignored.\n", f);
         (*count)++;
index baa1dc2a4741a1f19dc799c22646c2da0a75e3a4..3091722fc1faea345bfc13abffefe5705922a2fa 100755 (executable)
@@ -250,6 +250,38 @@ Domains= ~company''')
             self.assertNotRegex(contents, 'search.*company')
             # our global server should appear
             self.assertIn('nameserver 192.168.5.1\n', contents)
+            # should not have domain-restricted server as global server
+            self.assertNotIn('nameserver 192.168.42.1\n', contents)
+
+    def test_route_only_dns_all_domains(self):
+        with open('/run/systemd/network/myvpn.netdev', 'w') as f:
+            f.write('''[NetDev]
+Name=dummy0
+Kind=dummy
+MACAddress=12:34:56:78:9a:bc''')
+        with open('/run/systemd/network/myvpn.network', 'w') as f:
+            f.write('''[Match]
+Name=dummy0
+[Network]
+Address=192.168.42.100
+DNS=192.168.42.1
+Domains= ~company ~.''')
+        self.addCleanup(os.remove, '/run/systemd/network/myvpn.netdev')
+        self.addCleanup(os.remove, '/run/systemd/network/myvpn.network')
+
+        self.do_test(coldplug=True, ipv6=False,
+                     extra_opts='IPv6AcceptRouterAdvertisements=False')
+
+        with open(RESOLV_CONF) as f:
+            contents = f.read()
+
+        # ~company is not a search domain, only a routing domain
+        self.assertNotRegex(contents, 'search.*company')
+
+        # our global server should appear
+        self.assertIn('nameserver 192.168.5.1\n', contents)
+        # should have company server as global server due to ~.
+        self.assertIn('nameserver 192.168.42.1\n', contents)
 
 
 @unittest.skipUnless(have_dnsmasq, 'dnsmasq not installed')
@@ -260,7 +292,7 @@ class DnsmasqClientTest(ClientTestBase, unittest.TestCase):
         super().setUp()
         self.dnsmasq = None
 
-    def create_iface(self, ipv6=False):
+    def create_iface(self, ipv6=False, dnsmasq_opts=None):
         '''Create test interface with DHCP server behind it'''
 
         # add veth pair
@@ -281,6 +313,8 @@ class DnsmasqClientTest(ClientTestBase, unittest.TestCase):
             extra_opts = ['--enable-ra', '--dhcp-range=2600::10,2600::20']
         else:
             extra_opts = []
+        if dnsmasq_opts:
+            extra_opts += dnsmasq_opts
         self.dnsmasq = subprocess.Popen(
             ['dnsmasq', '--keep-in-foreground', '--log-queries',
              '--log-facility=' + self.dnsmasq_log, '--conf-file=/dev/null',
@@ -305,6 +339,80 @@ class DnsmasqClientTest(ClientTestBase, unittest.TestCase):
         with open(self.dnsmasq_log) as f:
             sys.stdout.write('\n\n---- dnsmasq log ----\n%s\n------\n\n' % f.read())
 
+    def test_resolved_domain_restricted_dns(self):
+        '''resolved: domain-restricted DNS servers'''
+
+        # create interface for generic connections; this will map all DNS names
+        # to 192.168.42.1
+        self.create_iface(dnsmasq_opts=['--address=/#/192.168.42.1'])
+        self.writeConfig('/run/systemd/network/general.network', '''\
+[Match]
+Name=%s
+[Network]
+DHCP=ipv4
+IPv6AcceptRA=False''' % self.iface)
+
+        # create second device/dnsmasq for a .company/.lab VPN interface
+        # static IPs for simplicity
+        subprocess.check_call(['ip', 'link', 'add', 'name', 'testvpnclient', 'type',
+                               'veth', 'peer', 'name', 'testvpnrouter'])
+        self.addCleanup(subprocess.call, ['ip', 'link', 'del', 'dev', 'testvpnrouter'])
+        subprocess.check_call(['ip', 'a', 'flush', 'dev', 'testvpnrouter'])
+        subprocess.check_call(['ip', 'a', 'add', '10.241.3.1/24', 'dev', 'testvpnrouter'])
+        subprocess.check_call(['ip', 'link', 'set', 'testvpnrouter', 'up'])
+
+        vpn_dnsmasq_log = os.path.join(self.workdir, 'dnsmasq-vpn.log')
+        vpn_dnsmasq = subprocess.Popen(
+            ['dnsmasq', '--keep-in-foreground', '--log-queries',
+             '--log-facility=' + vpn_dnsmasq_log, '--conf-file=/dev/null',
+             '--dhcp-leasefile=/dev/null', '--bind-interfaces',
+             '--interface=testvpnrouter', '--except-interface=lo',
+             '--address=/math.lab/10.241.3.3', '--address=/cantina.company/10.241.4.4'])
+        self.addCleanup(vpn_dnsmasq.wait)
+        self.addCleanup(vpn_dnsmasq.kill)
+
+        self.writeConfig('/run/systemd/network/vpn.network', '''\
+[Match]
+Name=testvpnclient
+[Network]
+IPv6AcceptRA=False
+Address=10.241.3.2/24
+DNS=10.241.3.1
+Domains= ~company ~lab''')
+
+        subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
+        subprocess.check_call([self.networkd_wait_online, '--interface', self.iface,
+                               '--interface=testvpnclient', '--timeout=20'])
+
+        # ensure we start fresh with every test
+        subprocess.check_call(['systemctl', 'restart', 'systemd-resolved'])
+
+        # test vpnclient specific domains; these should *not* be answered by
+        # the general DNS
+        out = subprocess.check_output(['systemd-resolve', 'math.lab'])
+        self.assertIn(b'math.lab: 10.241.3.3', out)
+        out = subprocess.check_output(['systemd-resolve', 'kettle.cantina.company'])
+        self.assertIn(b'kettle.cantina.company: 10.241.4.4', out)
+
+        # test general domains
+        out = subprocess.check_output(['systemd-resolve', 'megasearch.net'])
+        self.assertIn(b'megasearch.net: 192.168.42.1', out)
+
+        with open(self.dnsmasq_log) as f:
+            general_log = f.read()
+        with open(vpn_dnsmasq_log) as f:
+            vpn_log = f.read()
+
+        # VPN domains should only be sent to VPN DNS
+        self.assertRegex(vpn_log, 'query.*math.lab')
+        self.assertRegex(vpn_log, 'query.*cantina.company')
+        self.assertNotIn('lab', general_log)
+        self.assertNotIn('company', general_log)
+
+        # general domains should not be sent to the VPN DNS
+        self.assertRegex(general_log, 'query.*megasearch.net')
+        self.assertNotIn('megasearch.net', vpn_log)
+
 
 class NetworkdClientTest(ClientTestBase, unittest.TestCase):
     '''Test networkd client against networkd server'''