From 38d78d1ee9f967aecda14a1c80cd664a66b976c7 Mon Sep 17 00:00:00 2001 From: Zbigniew Jędrzejewski-Szmek Date: Wed, 14 Sep 2016 06:52:40 -0400 Subject: networkd-test: add a helper function to always clean up temporary config files --- test/networkd-test.py | 54 +++++++++++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 30 deletions(-) (limited to 'test/networkd-test.py') diff --git a/test/networkd-test.py b/test/networkd-test.py index bfa1bf3580..57f4c04295 100755 --- a/test/networkd-test.py +++ b/test/networkd-test.py @@ -54,7 +54,6 @@ class ClientTestBase: self.workdir_obj = tempfile.TemporaryDirectory() self.workdir = self.workdir_obj.name self.config = '/run/systemd/network/test_eth42.network' - os.makedirs(os.path.dirname(self.config), exist_ok=True) # avoid "Failed to open /dev/tty" errors in containers os.environ['SYSTEMD_LOG_TARGET'] = 'journal' @@ -77,10 +76,14 @@ class ClientTestBase: def tearDown(self): self.shutdown_iface() - if os.path.exists(self.config): - os.unlink(self.config) subprocess.call(['systemctl', 'stop', 'systemd-networkd']) + def writeConfig(self, fname, contents): + os.makedirs(os.path.dirname(fname), exist_ok=True) + with open(fname, 'w') as f: + f.write(contents) + self.addCleanup(os.remove, fname) + def show_journal(self, unit): '''Show journal of given unit since start of the test''' @@ -107,8 +110,8 @@ class ClientTestBase: def do_test(self, coldplug=True, ipv6=False, extra_opts='', online_timeout=10, dhcp_mode='yes'): subprocess.check_call(['systemctl', 'start', 'systemd-resolved']) - with open(self.config, 'w') as f: - f.write('''[Match] + self.writeConfig(self.config, '''\ +[Match] Name=%s [Network] DHCP=%s @@ -225,20 +228,18 @@ DHCP=%s self.do_test(coldplug=False, ipv6=True) def test_route_only_dns(self): - with open('/run/systemd/network/myvpn.netdev', 'w') as f: - f.write('''[NetDev] + self.writeConfig('/run/systemd/network/myvpn.netdev', '''\ +[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] + self.writeConfig('/run/systemd/network/myvpn.network', '''\ +[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') @@ -320,7 +321,8 @@ class NetworkdClientTest(ClientTestBase, unittest.TestCase): (fd, script) = tempfile.mkstemp(prefix='networkd-router.sh') self.addCleanup(os.remove, script) with os.fdopen(fd, 'w+') as f: - f.write('''#!/bin/sh -eu + f.write('''\ +#!/bin/sh -eu mkdir -p /run/systemd/network mkdir -p /run/systemd/netif mount -t tmpfs none /run/systemd/network @@ -398,20 +400,18 @@ exec $(systemctl cat systemd-networkd.service | sed -n '/^ExecStart=/ { s/^.*=// # we don't use this interface for this test self.if_router = None - with open('/run/systemd/network/test.netdev', 'w') as f: - f.write('''[NetDev] + self.writeConfig('/run/systemd/network/test.netdev', '''\ +[NetDev] Name=dummy0 Kind=dummy MACAddress=12:34:56:78:9a:bc''') - with open('/run/systemd/network/test.network', 'w') as f: - f.write('''[Match] + self.writeConfig('/run/systemd/network/test.network', '''\ +[Match] Name=dummy0 [Network] Address=192.168.42.100 DNS=192.168.42.1 Domains= one two three four five six seven eight nine ten''') - self.addCleanup(os.remove, '/run/systemd/network/test.netdev') - self.addCleanup(os.remove, '/run/systemd/network/test.network') subprocess.check_call(['systemctl', 'start', 'systemd-networkd']) @@ -432,23 +432,18 @@ Domains= one two three four five six seven eight nine ten''') name_prefix = 'a' * 60 - with open('/run/systemd/network/test.netdev', 'w') as f: - f.write('''[NetDev] + self.writeConfig('/run/systemd/network/test.netdev', '''\ +[NetDev] Name=dummy0 Kind=dummy MACAddress=12:34:56:78:9a:bc''') - with open('/run/systemd/network/test.network', 'w') as f: - f.write('''[Match] + self.writeConfig('/run/systemd/network/test.network', '''\ +[Match] Name=dummy0 [Network] Address=192.168.42.100 DNS=192.168.42.1 -Domains=''') - for i in range(5): - f.write('%s%i ' % (name_prefix, i)) - - self.addCleanup(os.remove, '/run/systemd/network/test.netdev') - self.addCleanup(os.remove, '/run/systemd/network/test.network') +Domains={p}0 {p}1 {p}2 {p}3 {p}4'''.format(p=name_prefix)) subprocess.check_call(['systemctl', 'start', 'systemd-networkd']) @@ -458,10 +453,9 @@ Domains=''') if ' one' in contents: break time.sleep(0.1) - self.assertRegex(contents, 'search .*%(p)s0 %(p)s1 %(p)s2' % {'p': name_prefix}) + self.assertRegex(contents, 'search .*{p}0 {p}1 {p}2'.format(p=name_prefix)) self.assertIn('# Total length of all search domains is too long, remaining ones ignored.', contents) - if __name__ == '__main__': unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) -- cgit v1.2.3-54-g00ecf From 047a0dacde06740050858d72c385cc07a4091feb Mon Sep 17 00:00:00 2001 From: Jean-Sébastien Bour Date: Sat, 9 Jul 2016 16:55:26 +0200 Subject: networkd: support drop-in directories for .network files Fixes #3655. [zj: Fix the tests.] --- src/network/networkd-network.c | 18 ++++++++++++++++-- test/networkd-test.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) (limited to 'test/networkd-test.py') diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c index 49faba5b12..0b36f13be8 100644 --- a/src/network/networkd-network.c +++ b/src/network/networkd-network.c @@ -40,6 +40,10 @@ static int network_load_one(Manager *manager, const char *filename) { _cleanup_network_free_ Network *network = NULL; _cleanup_fclose_ FILE *file = NULL; char *d; + const char *dropin_dirname; + _cleanup_strv_free_ char **dropin_dirs = NULL; + _cleanup_free_ char *dropin_dirs_nulstr = NULL; + size_t dropin_dirs_nulstr_size; Route *route; Address *address; int r; @@ -137,7 +141,17 @@ static int network_load_one(Manager *manager, const char *filename) { network->arp = -1; network->ipv6_accept_ra_use_dns = true; - r = config_parse(NULL, filename, file, + dropin_dirname = strjoina("/", network->name, ".network.d"); + + r = strv_extend_strv_concat(&dropin_dirs, (char**) network_dirs, dropin_dirname); + if (r < 0) + return r; + + r = strv_make_nulstr(dropin_dirs, &dropin_dirs_nulstr, &dropin_dirs_nulstr_size); + if (r < 0) + return r; + + r = config_parse_many(filename, dropin_dirs_nulstr, "Match\0" "Link\0" "Network\0" @@ -151,7 +165,7 @@ static int network_load_one(Manager *manager, const char *filename) { "BridgeFDB\0" "BridgeVLAN\0", config_item_perf_lookup, network_network_gperf_lookup, - false, false, true, network); + false, network); if (r < 0) return r; diff --git a/test/networkd-test.py b/test/networkd-test.py index 57f4c04295..baa1dc2a47 100755 --- a/test/networkd-test.py +++ b/test/networkd-test.py @@ -456,6 +456,36 @@ Domains={p}0 {p}1 {p}2 {p}3 {p}4'''.format(p=name_prefix)) self.assertRegex(contents, 'search .*{p}0 {p}1 {p}2'.format(p=name_prefix)) self.assertIn('# Total length of all search domains is too long, remaining ones ignored.', contents) + def test_dropin(self): + # we don't use this interface for this test + self.if_router = None + + self.writeConfig('/run/systemd/network/test.netdev', '''\ +[NetDev] +Name=dummy0 +Kind=dummy +MACAddress=12:34:56:78:9a:bc''') + self.writeConfig('/run/systemd/network/test.network', '''\ +[Match] +Name=dummy0 +[Network] +Address=192.168.42.100 +DNS=192.168.42.1''') + self.writeConfig('/run/systemd/network/test.network.d/dns.conf', '''\ +[Network] +DNS=127.0.0.1''') + + subprocess.check_call(['systemctl', 'start', 'systemd-networkd']) + + for timeout in range(50): + with open(RESOLV_CONF) as f: + contents = f.read() + if ' 127.0.0.1' in contents: + break + time.sleep(0.1) + self.assertIn('nameserver 192.168.42.1\n', contents) + self.assertIn('nameserver 127.0.0.1\n', contents) + if __name__ == '__main__': unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) -- cgit v1.2.3-54-g00ecf From b9fe94cad99968a58e169592d999306fd059eb14 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Fri, 30 Sep 2016 09:30:08 +0200 Subject: resolved: don't query domain-limited DNS servers for other domains (#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 #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 | 4 +- src/resolve/resolved-dns-scope.c | 8 +++ src/resolve/resolved-dns-server.c | 21 +++++++ src/resolve/resolved-dns-server.h | 2 + src/resolve/resolved-resolv-conf.c | 10 ++++ test/networkd-test.py | 110 ++++++++++++++++++++++++++++++++++++- 6 files changed, 152 insertions(+), 3 deletions(-) (limited to 'test/networkd-test.py') diff --git a/man/systemd.network.xml b/man/systemd.network.xml index 9c1b10fc5c..08dd157e31 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -475,8 +475,8 @@ 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 ~, 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 ~, 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 ~. (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 diff --git a/src/resolve/resolved-dns-scope.c b/src/resolve/resolved-dns-scope.c index ed0c6aa105..03811ac8e7 100644 --- a/src/resolve/resolved-dns-scope.c +++ b/src/resolve/resolved-dns-scope.c @@ -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: diff --git a/src/resolve/resolved-dns-server.c b/src/resolve/resolved-dns-server.c index 9b7b471600..97cc8c0e09 100644 --- a/src/resolve/resolved-dns-server.c +++ b/src/resolve/resolved-dns-server.c @@ -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; diff --git a/src/resolve/resolved-dns-server.h b/src/resolve/resolved-dns-server.h index c1732faffd..83e288a202 100644 --- a/src/resolve/resolved-dns-server.h +++ b/src/resolve/resolved-dns-server.h @@ -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); diff --git a/src/resolve/resolved-resolv-conf.c b/src/resolve/resolved-resolv-conf.c index 31b25ca50f..801014caf5 100644 --- a/src/resolve/resolved-resolv-conf.c +++ b/src/resolve/resolved-resolv-conf.c @@ -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)++; diff --git a/test/networkd-test.py b/test/networkd-test.py index baa1dc2a47..3091722fc1 100755 --- a/test/networkd-test.py +++ b/test/networkd-test.py @@ -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''' -- cgit v1.2.3-54-g00ecf