]> git.ipfire.org Git - thirdparty/systemd.git/blob - test/networkd-test.py
resolved: fix comments in resolve.conf for search domain overflows (#3422)
[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='IPv6AcceptRouterAdvertisements=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='IPv6AcceptRouterAdvertisements=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
231 @unittest.skipUnless(have_dnsmasq, 'dnsmasq not installed')
232 class DnsmasqClientTest(ClientTestBase, unittest.TestCase):
233 '''Test networkd client against dnsmasq'''
234
235 def setUp(self):
236 super().setUp()
237 self.dnsmasq = None
238
239 def create_iface(self, ipv6=False):
240 '''Create test interface with DHCP server behind it'''
241
242 # add veth pair
243 subprocess.check_call(['ip', 'link', 'add', 'name', self.iface, 'type',
244 'veth', 'peer', 'name', self.if_router])
245
246 # give our router an IP
247 subprocess.check_call(['ip', 'a', 'flush', 'dev', self.if_router])
248 subprocess.check_call(['ip', 'a', 'add', '192.168.5.1/24', 'dev', self.if_router])
249 if ipv6:
250 subprocess.check_call(['ip', 'a', 'add', '2600::1/64', 'dev', self.if_router])
251 subprocess.check_call(['ip', 'link', 'set', self.if_router, 'up'])
252
253 # add DHCP server
254 self.dnsmasq_log = os.path.join(self.workdir, 'dnsmasq.log')
255 lease_file = os.path.join(self.workdir, 'dnsmasq.leases')
256 if ipv6:
257 extra_opts = ['--enable-ra', '--dhcp-range=2600::10,2600::20']
258 else:
259 extra_opts = []
260 self.dnsmasq = subprocess.Popen(
261 ['dnsmasq', '--keep-in-foreground', '--log-queries',
262 '--log-facility=' + self.dnsmasq_log, '--conf-file=/dev/null',
263 '--dhcp-leasefile=' + lease_file, '--bind-interfaces',
264 '--interface=' + self.if_router, '--except-interface=lo',
265 '--dhcp-range=192.168.5.10,192.168.5.200'] + extra_opts)
266
267 def shutdown_iface(self):
268 '''Remove test interface and stop DHCP server'''
269
270 if self.if_router:
271 subprocess.check_call(['ip', 'link', 'del', 'dev', self.if_router])
272 self.if_router = None
273 if self.dnsmasq:
274 self.dnsmasq.kill()
275 self.dnsmasq.wait()
276 self.dnsmasq = None
277
278 def print_server_log(self):
279 '''Print DHCP server log for debugging failures'''
280
281 with open(self.dnsmasq_log) as f:
282 sys.stdout.write('\n\n---- dnsmasq log ----\n%s\n------\n\n' % f.read())
283
284
285 class NetworkdClientTest(ClientTestBase, unittest.TestCase):
286 '''Test networkd client against networkd server'''
287
288 def setUp(self):
289 super().setUp()
290 self.dnsmasq = None
291
292 def create_iface(self, ipv6=False):
293 '''Create test interface with DHCP server behind it'''
294
295 # run "router-side" networkd in own mount namespace to shield it from
296 # "client-side" configuration and networkd
297 (fd, script) = tempfile.mkstemp(prefix='networkd-router.sh')
298 self.addCleanup(os.remove, script)
299 with os.fdopen(fd, 'w+') as f:
300 f.write('''#!/bin/sh -eu
301 mkdir -p /run/systemd/network
302 mkdir -p /run/systemd/netif
303 mount -t tmpfs none /run/systemd/network
304 mount -t tmpfs none /run/systemd/netif
305 [ ! -e /run/dbus ] || mount -t tmpfs none /run/dbus
306 # create router/client veth pair
307 cat << EOF > /run/systemd/network/test.netdev
308 [NetDev]
309 Name=%(ifr)s
310 Kind=veth
311
312 [Peer]
313 Name=%(ifc)s
314 EOF
315
316 cat << EOF > /run/systemd/network/test.network
317 [Match]
318 Name=%(ifr)s
319
320 [Network]
321 Address=192.168.5.1/24
322 %(addr6)s
323 DHCPServer=yes
324
325 [DHCPServer]
326 PoolOffset=10
327 PoolSize=50
328 DNS=192.168.5.1
329 EOF
330
331 # run networkd as in systemd-networkd.service
332 exec $(systemctl cat systemd-networkd.service | sed -n '/^ExecStart=/ { s/^.*=//; p}')
333 ''' % {'ifr': self.if_router, 'ifc': self.iface, 'addr6': ipv6 and 'Address=2600::1/64' or ''})
334
335 os.fchmod(fd, 0o755)
336
337 subprocess.check_call(['systemd-run', '--unit=networkd-test-router.service',
338 '-p', 'InaccessibleDirectories=-/etc/systemd/network',
339 '-p', 'InaccessibleDirectories=-/run/systemd/network',
340 '-p', 'InaccessibleDirectories=-/run/systemd/netif',
341 '--service-type=notify', script])
342
343 # wait until devices got created
344 for timeout in range(50):
345 out = subprocess.check_output(['ip', 'a', 'show', 'dev', self.if_router])
346 if b'state UP' in out and b'scope global' in out:
347 break
348 time.sleep(0.1)
349
350 def shutdown_iface(self):
351 '''Remove test interface and stop DHCP server'''
352
353 if self.if_router:
354 subprocess.check_call(['systemctl', 'stop', 'networkd-test-router.service'])
355 # ensure failed transient unit does not stay around
356 subprocess.call(['systemctl', 'reset-failed', 'networkd-test-router.service'])
357 subprocess.call(['ip', 'link', 'del', 'dev', self.if_router])
358 self.if_router = None
359
360 def print_server_log(self):
361 '''Print DHCP server log for debugging failures'''
362
363 self.show_journal('networkd-test-router.service')
364
365 @unittest.skip('networkd does not have DHCPv6 server support')
366 def test_hotplug_dhcp_ip6(self):
367 pass
368
369 @unittest.skip('networkd does not have DHCPv6 server support')
370 def test_coldplug_dhcp_ip6(self):
371 pass
372
373 def test_search_domains(self):
374
375 # we don't use this interface for this test
376 self.if_router = None
377
378 with open('/run/systemd/network/test.netdev', 'w') as f:
379 f.write('''[NetDev]
380 Name=dummy0
381 Kind=dummy
382 MACAddress=12:34:56:78:9a:bc''')
383 with open('/run/systemd/network/test.network', 'w') as f:
384 f.write('''[Match]
385 Name=dummy0
386 [Network]
387 Address=192.168.42.100
388 DNS=192.168.42.1
389 Domains= one two three four five six seven eight nine ten''')
390 self.addCleanup(os.remove, '/run/systemd/network/test.netdev')
391 self.addCleanup(os.remove, '/run/systemd/network/test.network')
392
393 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
394
395 if os.path.islink('/etc/resolv.conf'):
396 for timeout in range(50):
397 with open('/etc/resolv.conf') as f:
398 contents = f.read()
399 if 'search one\n' in contents:
400 break
401 time.sleep(0.1)
402 self.assertIn('search one two three four five six\n'
403 '# Too many search domains configured, remaining ones ignored.\n',
404 contents)
405
406 def test_search_domains_too_long(self):
407
408 # we don't use this interface for this test
409 self.if_router = None
410
411 name_prefix = 'a' * 60
412
413 with open('/run/systemd/network/test.netdev', 'w') as f:
414 f.write('''[NetDev]
415 Name=dummy0
416 Kind=dummy
417 MACAddress=12:34:56:78:9a:bc''')
418 with open('/run/systemd/network/test.network', 'w') as f:
419 f.write('''[Match]
420 Name=dummy0
421 [Network]
422 Address=192.168.42.100
423 DNS=192.168.42.1
424 Domains=''')
425 for i in range(5):
426 f.write('%s%i ' % (name_prefix, i))
427
428 self.addCleanup(os.remove, '/run/systemd/network/test.netdev')
429 self.addCleanup(os.remove, '/run/systemd/network/test.network')
430
431 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
432
433 if os.path.islink('/etc/resolv.conf'):
434 for timeout in range(50):
435 with open('/etc/resolv.conf') as f:
436 contents = f.read()
437 if 'search one\n' in contents:
438 break
439 time.sleep(0.1)
440 self.assertIn('search %(p)s0 %(p)s1 %(p)s2 %(p)s3\n'
441 '# Total length of all search domains is too long, remaining ones ignored.' % {'p': name_prefix},
442 contents)
443
444
445 if __name__ == '__main__':
446 unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout,
447 verbosity=2))