]> git.ipfire.org Git - thirdparty/systemd.git/blob - test/networkd-test.py
resolved: add test for route-only domain filtering (#3609)
[thirdparty/systemd.git] / test / networkd-test.py
1 #!/usr/bin/env python3
2 #
3 # networkd integration test
4 # This uses temporary configuration in /run and temporary veth devices, and
5 # does not write anything on disk or change any system configuration;
6 # but it assumes (and checks at the beginning) that networkd is not currently
7 # running.
8 #
9 # This can be run on a normal installation, in QEMU, nspawn (with
10 # --private-network), LXD (with "--config raw.lxc=lxc.aa_profile=unconfined"),
11 # or LXC system containers. You need at least the "ip" tool from the iproute
12 # package; it is recommended to install dnsmasq too to get full test coverage.
13 #
14 # ATTENTION: This uses the *installed* networkd, not the one from the built
15 # source tree.
16 #
17 # (C) 2015 Canonical Ltd.
18 # Author: Martin Pitt <martin.pitt@ubuntu.com>
19 #
20 # systemd is free software; you can redistribute it and/or modify it
21 # under the terms of the GNU Lesser General Public License as published by
22 # the Free Software Foundation; either version 2.1 of the License, or
23 # (at your option) any later version.
24
25 # systemd is distributed in the hope that it will be useful, but
26 # WITHOUT ANY WARRANTY; without even the implied warranty of
27 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
28 # Lesser General Public License for more details.
29 #
30 # You should have received a copy of the GNU Lesser General Public License
31 # along with systemd; If not, see <http://www.gnu.org/licenses/>.
32
33 import os
34 import sys
35 import time
36 import unittest
37 import tempfile
38 import subprocess
39 import shutil
40
41 networkd_active = subprocess.call(['systemctl', 'is-active', '--quiet',
42 'systemd-networkd']) == 0
43 have_dnsmasq = shutil.which('dnsmasq')
44
45
46 @unittest.skipIf(networkd_active,
47 'networkd is already active')
48 class ClientTestBase:
49 def setUp(self):
50 self.iface = 'test_eth42'
51 self.if_router = 'router_eth42'
52 self.workdir_obj = tempfile.TemporaryDirectory()
53 self.workdir = self.workdir_obj.name
54 self.config = '/run/systemd/network/test_eth42.network'
55 os.makedirs(os.path.dirname(self.config), exist_ok=True)
56
57 # avoid "Failed to open /dev/tty" errors in containers
58 os.environ['SYSTEMD_LOG_TARGET'] = 'journal'
59
60 # determine path to systemd-networkd-wait-online
61 for p in ['/usr/lib/systemd/systemd-networkd-wait-online',
62 '/lib/systemd/systemd-networkd-wait-online']:
63 if os.path.exists(p):
64 self.networkd_wait_online = p
65 break
66 else:
67 self.fail('systemd-networkd-wait-online not found')
68
69 # get current journal cursor
70 out = subprocess.check_output(['journalctl', '-b', '--quiet',
71 '--no-pager', '-n0', '--show-cursor'],
72 universal_newlines=True)
73 self.assertTrue(out.startswith('-- cursor:'))
74 self.journal_cursor = out.split()[-1]
75
76 def tearDown(self):
77 self.shutdown_iface()
78 if os.path.exists(self.config):
79 os.unlink(self.config)
80 subprocess.call(['systemctl', 'stop', 'systemd-networkd'])
81
82 def show_journal(self, unit):
83 '''Show journal of given unit since start of the test'''
84
85 print('---- %s ----' % unit)
86 sys.stdout.flush()
87 subprocess.call(['journalctl', '-b', '--no-pager', '--quiet',
88 '--cursor', self.journal_cursor, '-u', unit])
89
90 def create_iface(self, ipv6=False):
91 '''Create test interface with DHCP server behind it'''
92
93 raise NotImplementedError('must be implemented by a subclass')
94
95 def shutdown_iface(self):
96 '''Remove test interface and stop DHCP server'''
97
98 raise NotImplementedError('must be implemented by a subclass')
99
100 def print_server_log(self):
101 '''Print DHCP server log for debugging failures'''
102
103 raise NotImplementedError('must be implemented by a subclass')
104
105 def do_test(self, coldplug=True, ipv6=False, extra_opts='',
106 online_timeout=10, dhcp_mode='yes'):
107 with open(self.config, 'w') as f:
108 f.write('''[Match]
109 Name=%s
110 [Network]
111 DHCP=%s
112 %s''' % (self.iface, dhcp_mode, extra_opts))
113
114 if coldplug:
115 # create interface first, then start networkd
116 self.create_iface(ipv6=ipv6)
117 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
118 else:
119 # start networkd first, then create interface
120 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
121 self.create_iface(ipv6=ipv6)
122
123 try:
124 subprocess.check_call([self.networkd_wait_online, '--interface',
125 self.iface, '--timeout=%i' % online_timeout])
126
127 if ipv6:
128 # check iface state and IP 6 address; FIXME: we need to wait a bit
129 # longer, as the iface is "configured" already with IPv4 *or*
130 # IPv6, but we want to wait for both
131 for timeout in range(10):
132 out = subprocess.check_output(['ip', 'a', 'show', 'dev', self.iface])
133 if b'state UP' in out and b'inet6 2600' in out and b'inet 192.168' in out:
134 break
135 time.sleep(1)
136 else:
137 self.fail('timed out waiting for IPv6 configuration')
138
139 self.assertRegex(out, b'inet6 2600::.* scope global .*dynamic')
140 self.assertRegex(out, b'inet6 fe80::.* scope link')
141 else:
142 # should have link-local address on IPv6 only
143 out = subprocess.check_output(['ip', '-6', 'a', 'show', 'dev', self.iface])
144 self.assertRegex(out, b'inet6 fe80::.* scope link')
145 self.assertNotIn(b'scope global', out)
146
147 # should have IPv4 address
148 out = subprocess.check_output(['ip', '-4', 'a', 'show', 'dev', self.iface])
149 self.assertIn(b'state UP', out)
150 self.assertRegex(out, b'inet 192.168.5.\d+/.* scope global dynamic')
151
152 # check networkctl state
153 out = subprocess.check_output(['networkctl'])
154 self.assertRegex(out, ('%s\s+ether\s+routable\s+unmanaged' % self.if_router).encode())
155 self.assertRegex(out, ('%s\s+ether\s+routable\s+configured' % self.iface).encode())
156
157 out = subprocess.check_output(['networkctl', 'status', self.iface])
158 self.assertRegex(out, b'Type:\s+ether')
159 self.assertRegex(out, b'State:\s+routable.*configured')
160 self.assertRegex(out, b'Address:\s+192.168.5.\d+')
161 if ipv6:
162 self.assertRegex(out, b'2600::')
163 else:
164 self.assertNotIn(b'2600::', out)
165 self.assertRegex(out, b'fe80::')
166 self.assertRegex(out, b'Gateway:\s+192.168.5.1')
167 self.assertRegex(out, b'DNS:\s+192.168.5.1')
168 except (AssertionError, subprocess.CalledProcessError):
169 # show networkd status, journal, and DHCP server log on failure
170 with open(self.config) as f:
171 print('\n---- %s ----\n%s' % (self.config, f.read()))
172 print('---- interface status ----')
173 sys.stdout.flush()
174 subprocess.call(['ip', 'a', 'show', 'dev', self.iface])
175 print('---- networkctl status %s ----' % self.iface)
176 sys.stdout.flush()
177 subprocess.call(['networkctl', 'status', self.iface])
178 self.show_journal('systemd-networkd.service')
179 self.print_server_log()
180 raise
181
182 # verify resolv.conf if it gets dynamically managed
183 if os.path.islink('/etc/resolv.conf'):
184 for timeout in range(50):
185 with open('/etc/resolv.conf') as f:
186 contents = f.read()
187 if 'nameserver 192.168.5.1\n' in contents:
188 break
189 # resolv.conf can have at most three nameservers; if we already
190 # have three different ones, that's also okay
191 if contents.count('nameserver ') >= 3:
192 break
193 time.sleep(0.1)
194 else:
195 self.fail('nameserver 192.168.5.1 not found in /etc/resolv.conf')
196
197 if not coldplug:
198 # check post-down.d hook
199 self.shutdown_iface()
200
201 def test_coldplug_dhcp_yes_ip4(self):
202 # we have a 12s timeout on RA, so we need to wait longer
203 self.do_test(coldplug=True, ipv6=False, online_timeout=15)
204
205 def test_coldplug_dhcp_yes_ip4_no_ra(self):
206 # with disabling RA explicitly things should be fast
207 self.do_test(coldplug=True, ipv6=False,
208 extra_opts='IPv6AcceptRA=False')
209
210 def test_coldplug_dhcp_ip4_only(self):
211 # we have a 12s timeout on RA, so we need to wait longer
212 self.do_test(coldplug=True, ipv6=False, dhcp_mode='ipv4',
213 online_timeout=15)
214
215 def test_coldplug_dhcp_ip4_only_no_ra(self):
216 # with disabling RA explicitly things should be fast
217 self.do_test(coldplug=True, ipv6=False, dhcp_mode='ipv4',
218 extra_opts='IPv6AcceptRA=False')
219
220 def test_coldplug_dhcp_ip6(self):
221 self.do_test(coldplug=True, ipv6=True)
222
223 def test_hotplug_dhcp_ip4(self):
224 # With IPv4 only we have a 12s timeout on RA, so we need to wait longer
225 self.do_test(coldplug=False, ipv6=False, online_timeout=15)
226
227 def test_hotplug_dhcp_ip6(self):
228 self.do_test(coldplug=False, ipv6=True)
229
230 def test_route_only_dns(self):
231 with open('/run/systemd/network/myvpn.netdev', 'w') as f:
232 f.write('''[NetDev]
233 Name=dummy0
234 Kind=dummy
235 MACAddress=12:34:56:78:9a:bc''')
236 with open('/run/systemd/network/myvpn.network', 'w') as f:
237 f.write('''[Match]
238 Name=dummy0
239 [Network]
240 Address=192.168.42.100
241 DNS=192.168.42.1
242 Domains= ~company''')
243 self.addCleanup(os.remove, '/run/systemd/network/myvpn.netdev')
244 self.addCleanup(os.remove, '/run/systemd/network/myvpn.network')
245
246 self.do_test(coldplug=True, ipv6=False,
247 extra_opts='IPv6AcceptRouterAdvertisements=False')
248
249 if os.path.islink('/etc/resolv.conf'):
250 with open('/etc/resolv.conf') as f:
251 contents = f.read()
252
253 # ~company is not a search domain, only a routing domain
254 self.assertNotRegex(contents, 'search.*company')
255
256 # our global server should appear, unless we already have three
257 # (different) servers
258 if contents.count('nameserver ') < 3:
259 self.assertIn('nameserver 192.168.5.1\n', contents)
260
261
262 @unittest.skipUnless(have_dnsmasq, 'dnsmasq not installed')
263 class DnsmasqClientTest(ClientTestBase, unittest.TestCase):
264 '''Test networkd client against dnsmasq'''
265
266 def setUp(self):
267 super().setUp()
268 self.dnsmasq = None
269
270 def create_iface(self, ipv6=False):
271 '''Create test interface with DHCP server behind it'''
272
273 # add veth pair
274 subprocess.check_call(['ip', 'link', 'add', 'name', self.iface, 'type',
275 'veth', 'peer', 'name', self.if_router])
276
277 # give our router an IP
278 subprocess.check_call(['ip', 'a', 'flush', 'dev', self.if_router])
279 subprocess.check_call(['ip', 'a', 'add', '192.168.5.1/24', 'dev', self.if_router])
280 if ipv6:
281 subprocess.check_call(['ip', 'a', 'add', '2600::1/64', 'dev', self.if_router])
282 subprocess.check_call(['ip', 'link', 'set', self.if_router, 'up'])
283
284 # add DHCP server
285 self.dnsmasq_log = os.path.join(self.workdir, 'dnsmasq.log')
286 lease_file = os.path.join(self.workdir, 'dnsmasq.leases')
287 if ipv6:
288 extra_opts = ['--enable-ra', '--dhcp-range=2600::10,2600::20']
289 else:
290 extra_opts = []
291 self.dnsmasq = subprocess.Popen(
292 ['dnsmasq', '--keep-in-foreground', '--log-queries',
293 '--log-facility=' + self.dnsmasq_log, '--conf-file=/dev/null',
294 '--dhcp-leasefile=' + lease_file, '--bind-interfaces',
295 '--interface=' + self.if_router, '--except-interface=lo',
296 '--dhcp-range=192.168.5.10,192.168.5.200'] + extra_opts)
297
298 def shutdown_iface(self):
299 '''Remove test interface and stop DHCP server'''
300
301 if self.if_router:
302 subprocess.check_call(['ip', 'link', 'del', 'dev', self.if_router])
303 self.if_router = None
304 if self.dnsmasq:
305 self.dnsmasq.kill()
306 self.dnsmasq.wait()
307 self.dnsmasq = None
308
309 def print_server_log(self):
310 '''Print DHCP server log for debugging failures'''
311
312 with open(self.dnsmasq_log) as f:
313 sys.stdout.write('\n\n---- dnsmasq log ----\n%s\n------\n\n' % f.read())
314
315
316 class NetworkdClientTest(ClientTestBase, unittest.TestCase):
317 '''Test networkd client against networkd server'''
318
319 def setUp(self):
320 super().setUp()
321 self.dnsmasq = None
322
323 def create_iface(self, ipv6=False):
324 '''Create test interface with DHCP server behind it'''
325
326 # run "router-side" networkd in own mount namespace to shield it from
327 # "client-side" configuration and networkd
328 (fd, script) = tempfile.mkstemp(prefix='networkd-router.sh')
329 self.addCleanup(os.remove, script)
330 with os.fdopen(fd, 'w+') as f:
331 f.write('''#!/bin/sh -eu
332 mkdir -p /run/systemd/network
333 mkdir -p /run/systemd/netif
334 mount -t tmpfs none /run/systemd/network
335 mount -t tmpfs none /run/systemd/netif
336 [ ! -e /run/dbus ] || mount -t tmpfs none /run/dbus
337 # create router/client veth pair
338 cat << EOF > /run/systemd/network/test.netdev
339 [NetDev]
340 Name=%(ifr)s
341 Kind=veth
342
343 [Peer]
344 Name=%(ifc)s
345 EOF
346
347 cat << EOF > /run/systemd/network/test.network
348 [Match]
349 Name=%(ifr)s
350
351 [Network]
352 Address=192.168.5.1/24
353 %(addr6)s
354 DHCPServer=yes
355
356 [DHCPServer]
357 PoolOffset=10
358 PoolSize=50
359 DNS=192.168.5.1
360 EOF
361
362 # run networkd as in systemd-networkd.service
363 exec $(systemctl cat systemd-networkd.service | sed -n '/^ExecStart=/ { s/^.*=//; p}')
364 ''' % {'ifr': self.if_router, 'ifc': self.iface, 'addr6': ipv6 and 'Address=2600::1/64' or ''})
365
366 os.fchmod(fd, 0o755)
367
368 subprocess.check_call(['systemd-run', '--unit=networkd-test-router.service',
369 '-p', 'InaccessibleDirectories=-/etc/systemd/network',
370 '-p', 'InaccessibleDirectories=-/run/systemd/network',
371 '-p', 'InaccessibleDirectories=-/run/systemd/netif',
372 '--service-type=notify', script])
373
374 # wait until devices got created
375 for timeout in range(50):
376 out = subprocess.check_output(['ip', 'a', 'show', 'dev', self.if_router])
377 if b'state UP' in out and b'scope global' in out:
378 break
379 time.sleep(0.1)
380
381 def shutdown_iface(self):
382 '''Remove test interface and stop DHCP server'''
383
384 if self.if_router:
385 subprocess.check_call(['systemctl', 'stop', 'networkd-test-router.service'])
386 # ensure failed transient unit does not stay around
387 subprocess.call(['systemctl', 'reset-failed', 'networkd-test-router.service'])
388 subprocess.call(['ip', 'link', 'del', 'dev', self.if_router])
389 self.if_router = None
390
391 def print_server_log(self):
392 '''Print DHCP server log for debugging failures'''
393
394 self.show_journal('networkd-test-router.service')
395
396 @unittest.skip('networkd does not have DHCPv6 server support')
397 def test_hotplug_dhcp_ip6(self):
398 pass
399
400 @unittest.skip('networkd does not have DHCPv6 server support')
401 def test_coldplug_dhcp_ip6(self):
402 pass
403
404 def test_search_domains(self):
405
406 # we don't use this interface for this test
407 self.if_router = None
408
409 with open('/run/systemd/network/test.netdev', 'w') as f:
410 f.write('''[NetDev]
411 Name=dummy0
412 Kind=dummy
413 MACAddress=12:34:56:78:9a:bc''')
414 with open('/run/systemd/network/test.network', 'w') as f:
415 f.write('''[Match]
416 Name=dummy0
417 [Network]
418 Address=192.168.42.100
419 DNS=192.168.42.1
420 Domains= one two three four five six seven eight nine ten''')
421 self.addCleanup(os.remove, '/run/systemd/network/test.netdev')
422 self.addCleanup(os.remove, '/run/systemd/network/test.network')
423
424 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
425
426 if os.path.islink('/etc/resolv.conf'):
427 for timeout in range(50):
428 with open('/etc/resolv.conf') as f:
429 contents = f.read()
430 if 'search one\n' in contents:
431 break
432 time.sleep(0.1)
433 self.assertIn('search one two three four five six\n'
434 '# Too many search domains configured, remaining ones ignored.\n',
435 contents)
436
437 def test_search_domains_too_long(self):
438
439 # we don't use this interface for this test
440 self.if_router = None
441
442 name_prefix = 'a' * 60
443
444 with open('/run/systemd/network/test.netdev', 'w') as f:
445 f.write('''[NetDev]
446 Name=dummy0
447 Kind=dummy
448 MACAddress=12:34:56:78:9a:bc''')
449 with open('/run/systemd/network/test.network', 'w') as f:
450 f.write('''[Match]
451 Name=dummy0
452 [Network]
453 Address=192.168.42.100
454 DNS=192.168.42.1
455 Domains=''')
456 for i in range(5):
457 f.write('%s%i ' % (name_prefix, i))
458
459 self.addCleanup(os.remove, '/run/systemd/network/test.netdev')
460 self.addCleanup(os.remove, '/run/systemd/network/test.network')
461
462 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
463
464 if os.path.islink('/etc/resolv.conf'):
465 for timeout in range(50):
466 with open('/etc/resolv.conf') as f:
467 contents = f.read()
468 if 'search one\n' in contents:
469 break
470 time.sleep(0.1)
471 self.assertIn('search %(p)s0 %(p)s1 %(p)s2 %(p)s3\n'
472 '# Total length of all search domains is too long, remaining ones ignored.' % {'p': name_prefix},
473 contents)
474
475
476 if __name__ == '__main__':
477 unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout,
478 verbosity=2))