]> git.ipfire.org Git - thirdparty/systemd.git/blame - test/networkd-test.py
Revert "timesyncd: enable DynamicUser="
[thirdparty/systemd.git] / test / networkd-test.py
CommitLineData
4ddb85b1 1#!/usr/bin/env python3
35df7443 2# SPDX-License-Identifier: LGPL-2.1+
4ddb85b1
MP
3#
4# networkd integration test
5# This uses temporary configuration in /run and temporary veth devices, and
6# does not write anything on disk or change any system configuration;
7# but it assumes (and checks at the beginning) that networkd is not currently
8# running.
daad34df
MP
9#
10# This can be run on a normal installation, in QEMU, nspawn (with
11# --private-network), LXD (with "--config raw.lxc=lxc.aa_profile=unconfined"),
12# or LXC system containers. You need at least the "ip" tool from the iproute
13# package; it is recommended to install dnsmasq too to get full test coverage.
14#
4ddb85b1
MP
15# ATTENTION: This uses the *installed* networkd, not the one from the built
16# source tree.
17#
810adae9 18# © 2015 Canonical Ltd.
4ddb85b1 19# Author: Martin Pitt <martin.pitt@ubuntu.com>
4ddb85b1 20
ec89276c 21import errno
4ddb85b1 22import os
278391c2
BOT
23import shutil
24import socket
25import subprocess
4ddb85b1 26import sys
278391c2 27import tempfile
4ddb85b1
MP
28import time
29import unittest
4ddb85b1 30
ec89276c
DM
31HAVE_DNSMASQ = shutil.which('dnsmasq') is not None
32
33NETWORK_UNITDIR = '/run/systemd/network'
34
35NETWORKD_WAIT_ONLINE = shutil.which('systemd-networkd-wait-online',
36 path='/usr/lib/systemd:/lib/systemd')
4ddb85b1 37
30b42a9a
MP
38RESOLV_CONF = '/run/systemd/resolve/resolv.conf'
39
c4a0a2d5
MP
40tmpmounts = []
41running_units = []
42stopped_units = []
43
4ddb85b1 44
ec89276c 45def setUpModule():
c4a0a2d5
MP
46 global tmpmounts
47
ec89276c
DM
48 """Initialize the environment, and perform sanity checks on it."""
49 if NETWORKD_WAIT_ONLINE is None:
50 raise OSError(errno.ENOENT, 'systemd-networkd-wait-online not found')
51
c4a0a2d5
MP
52 # Do not run any tests if the system is using networkd already and it's not virtualized
53 if (subprocess.call(['systemctl', 'is-active', '--quiet', 'systemd-networkd.service']) == 0 and
54 subprocess.call(['systemd-detect-virt', '--quiet']) != 0):
55 raise unittest.SkipTest('not virtualized and networkd is already active')
56 # Ensure we don't mess with an existing networkd config
57 for u in ['systemd-networkd.socket', 'systemd-networkd', 'systemd-resolved']:
58 if subprocess.call(['systemctl', 'is-active', '--quiet', u]) == 0:
59 subprocess.call(['systemctl', 'stop', u])
60 running_units.append(u)
61 else:
62 stopped_units.append(u)
63 for d in ['/etc/systemd/network', '/run/systemd/network',
64 '/run/systemd/netif', '/run/systemd/resolve']:
65 if os.path.isdir(d):
66 subprocess.check_call(["mount", "-t", "tmpfs", "none", d])
67 tmpmounts.append(d)
68 if os.path.isdir('/run/systemd/resolve'):
69 os.chmod('/run/systemd/resolve', 0o755)
ec89276c
DM
70
71 # Avoid "Failed to open /dev/tty" errors in containers.
72 os.environ['SYSTEMD_LOG_TARGET'] = 'journal'
73
74 # Ensure the unit directory exists so tests can dump files into it.
75 os.makedirs(NETWORK_UNITDIR, exist_ok=True)
76
c44c1b8a
MP
77 # create static systemd-network user for networkd-test-router.service (it
78 # needs to do some stuff as root and can't start as user; but networkd
79 # still insists on the user)
80 subprocess.check_call(['adduser', '--system', '--no-create-home', 'systemd-network'])
81
ec89276c 82
c4a0a2d5
MP
83def tearDownModule():
84 global tmpmounts
85 for d in tmpmounts:
86 subprocess.check_call(["umount", d])
87 for u in stopped_units:
88 subprocess.call(["systemctl", "stop", u])
89 for u in running_units:
90 subprocess.call(["systemctl", "restart", u])
91
92
ec89276c
DM
93class NetworkdTestingUtilities:
94 """Provide a set of utility functions to facilitate networkd tests.
95
96 This class must be inherited along with unittest.TestCase to define
97 some required methods.
98 """
99
618b196e
DM
100 def add_veth_pair(self, veth, peer, veth_options=(), peer_options=()):
101 """Add a veth interface pair, and queue them to be removed."""
102 subprocess.check_call(['ip', 'link', 'add', 'name', veth] +
103 list(veth_options) +
104 ['type', 'veth', 'peer', 'name', peer] +
105 list(peer_options))
106 self.addCleanup(subprocess.call, ['ip', 'link', 'del', 'dev', peer])
107
ec89276c
DM
108 def write_network(self, unit_name, contents):
109 """Write a network unit file, and queue it to be removed."""
110 unit_path = os.path.join(NETWORK_UNITDIR, unit_name)
111
112 with open(unit_path, 'w') as unit:
113 unit.write(contents)
114 self.addCleanup(os.remove, unit_path)
115
116 def write_network_dropin(self, unit_name, dropin_name, contents):
117 """Write a network unit drop-in, and queue it to be removed."""
278391c2
BOT
118 dropin_dir = os.path.join(NETWORK_UNITDIR, "{}.d".format(unit_name))
119 dropin_path = os.path.join(dropin_dir, "{}.conf".format(dropin_name))
ec89276c
DM
120
121 os.makedirs(dropin_dir, exist_ok=True)
b56be296 122 self.addCleanup(os.rmdir, dropin_dir)
ec89276c
DM
123 with open(dropin_path, 'w') as dropin:
124 dropin.write(contents)
125 self.addCleanup(os.remove, dropin_path)
126
b56be296
DJL
127 def read_attr(self, link, attribute):
128 """Read a link attributed from the sysfs."""
129 # Note we we don't want to check if interface `link' is managed, we
130 # want to evaluate link variable and pass the value of the link to
131 # assert_link_states e.g. eth0=managed.
132 self.assert_link_states(**{link:'managed'})
133 with open(os.path.join('/sys/class/net', link, attribute)) as f:
134 return f.readline().strip()
135
a09dc546
DM
136 def assert_link_states(self, **kwargs):
137 """Match networkctl link states to the given ones.
138
139 Each keyword argument should be the name of a network interface
140 with its expected value of the "SETUP" column in output from
141 networkctl. The interfaces have five seconds to come online
142 before the check is performed. Every specified interface must
143 be present in the output, and any other interfaces found in the
144 output are ignored.
145
146 A special interface state "managed" is supported, which matches
147 any value in the "SETUP" column other than "unmanaged".
148 """
149 if not kwargs:
150 return
151 interfaces = set(kwargs)
152
153 # Wait for the requested interfaces, but don't fail for them.
154 subprocess.call([NETWORKD_WAIT_ONLINE, '--timeout=5'] +
278391c2 155 ['--interface={}'.format(iface) for iface in kwargs])
a09dc546
DM
156
157 # Validate each link state found in the networkctl output.
158 out = subprocess.check_output(['networkctl', '--no-legend']).rstrip()
159 for line in out.decode('utf-8').split('\n'):
160 fields = line.split()
161 if len(fields) >= 5 and fields[1] in kwargs:
162 iface = fields[1]
163 expected = kwargs[iface]
164 actual = fields[-1]
165 if (actual != expected and
166 not (expected == 'managed' and actual != 'unmanaged')):
278391c2 167 self.fail("Link {} expects state {}, found {}".format(iface, expected, actual))
a09dc546
DM
168 interfaces.remove(iface)
169
170 # Ensure that all requested interfaces have been covered.
171 if interfaces:
278391c2 172 self.fail("Missing links in status output: {}".format(interfaces))
a09dc546 173
ec89276c 174
b56be296
DJL
175class BridgeTest(NetworkdTestingUtilities, unittest.TestCase):
176 """Provide common methods for testing networkd against servers."""
177
178 def setUp(self):
179 self.write_network('port1.netdev', '''\
180[NetDev]
181Name=port1
182Kind=dummy
183MACAddress=12:34:56:78:9a:bc''')
184 self.write_network('port2.netdev', '''\
185[NetDev]
186Name=port2
187Kind=dummy
188MACAddress=12:34:56:78:9a:bd''')
189 self.write_network('mybridge.netdev', '''\
190[NetDev]
191Name=mybridge
192Kind=bridge''')
193 self.write_network('port1.network', '''\
194[Match]
195Name=port1
196[Network]
197Bridge=mybridge''')
198 self.write_network('port2.network', '''\
199[Match]
200Name=port2
201[Network]
202Bridge=mybridge''')
203 self.write_network('mybridge.network', '''\
204[Match]
205Name=mybridge
206[Network]
207DNS=192.168.250.1
208Address=192.168.250.33/24
209Gateway=192.168.250.1''')
207f5f4d 210 subprocess.call(['systemctl', 'reset-failed', 'systemd-networkd', 'systemd-resolved'])
b56be296
DJL
211 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
212
213 def tearDown(self):
214 subprocess.check_call(['systemctl', 'stop', 'systemd-networkd'])
215 subprocess.check_call(['ip', 'link', 'del', 'mybridge'])
216 subprocess.check_call(['ip', 'link', 'del', 'port1'])
217 subprocess.check_call(['ip', 'link', 'del', 'port2'])
218
219 def test_bridge_init(self):
220 self.assert_link_states(
221 port1='managed',
222 port2='managed',
223 mybridge='managed')
224
225 def test_bridge_port_priority(self):
226 self.assertEqual(self.read_attr('port1', 'brport/priority'), '32')
227 self.write_network_dropin('port1.network', 'priority', '''\
228[Bridge]
229Priority=28
230''')
231 subprocess.check_call(['systemctl', 'restart', 'systemd-networkd'])
232 self.assertEqual(self.read_attr('port1', 'brport/priority'), '28')
233
234 def test_bridge_port_priority_set_zero(self):
235 """It should be possible to set the bridge port priority to 0"""
236 self.assertEqual(self.read_attr('port2', 'brport/priority'), '32')
237 self.write_network_dropin('port2.network', 'priority', '''\
238[Bridge]
239Priority=0
240''')
241 subprocess.check_call(['systemctl', 'restart', 'systemd-networkd'])
242 self.assertEqual(self.read_attr('port2', 'brport/priority'), '0')
243
4319c181
SS
244 def test_bridge_port_property(self):
245 """Test the "[Bridge]" section keys"""
246 self.assertEqual(self.read_attr('port2', 'brport/priority'), '32')
247 self.write_network_dropin('port2.network', 'property', '''\
248[Bridge]
249UnicastFlood=true
250HairPin=true
251UseBPDU=true
252FastLeave=true
253AllowPortToBeRoot=true
254Cost=555
255Priority=23
256''')
257 subprocess.check_call(['systemctl', 'restart', 'systemd-networkd'])
258
259 self.assertEqual(self.read_attr('port2', 'brport/priority'), '23')
260 self.assertEqual(self.read_attr('port2', 'brport/hairpin_mode'), '1')
261 self.assertEqual(self.read_attr('port2', 'brport/path_cost'), '555')
262 self.assertEqual(self.read_attr('port2', 'brport/multicast_fast_leave'), '1')
263 self.assertEqual(self.read_attr('port2', 'brport/unicast_flood'), '1')
264 self.assertEqual(self.read_attr('port2', 'brport/bpdu_guard'), '1')
265 self.assertEqual(self.read_attr('port2', 'brport/root_block'), '1')
266
ec89276c
DM
267class ClientTestBase(NetworkdTestingUtilities):
268 """Provide common methods for testing networkd against servers."""
269
fd0cec03
MP
270 @classmethod
271 def setUpClass(klass):
272 klass.orig_log_level = subprocess.check_output(
273 ['systemctl', 'show', '--value', '--property', 'LogLevel'],
274 universal_newlines=True).strip()
275 subprocess.check_call(['systemd-analyze', 'set-log-level', 'debug'])
276
277 @classmethod
278 def tearDownClass(klass):
279 subprocess.check_call(['systemd-analyze', 'set-log-level', klass.orig_log_level])
280
4ddb85b1
MP
281 def setUp(self):
282 self.iface = 'test_eth42'
283 self.if_router = 'router_eth42'
284 self.workdir_obj = tempfile.TemporaryDirectory()
285 self.workdir = self.workdir_obj.name
ec89276c 286 self.config = 'test_eth42.network'
4ddb85b1
MP
287
288 # get current journal cursor
fd0cec03 289 subprocess.check_output(['journalctl', '--sync'])
4ddb85b1
MP
290 out = subprocess.check_output(['journalctl', '-b', '--quiet',
291 '--no-pager', '-n0', '--show-cursor'],
292 universal_newlines=True)
293 self.assertTrue(out.startswith('-- cursor:'))
294 self.journal_cursor = out.split()[-1]
295
207f5f4d 296 subprocess.call(['systemctl', 'reset-failed', 'systemd-networkd', 'systemd-resolved'])
c44c1b8a 297
4ddb85b1
MP
298 def tearDown(self):
299 self.shutdown_iface()
4ddb85b1 300 subprocess.call(['systemctl', 'stop', 'systemd-networkd'])
9e0c296a
MP
301 subprocess.call(['ip', 'link', 'del', 'dummy0'],
302 stderr=subprocess.DEVNULL)
4ddb85b1
MP
303
304 def show_journal(self, unit):
305 '''Show journal of given unit since start of the test'''
306
278391c2 307 print('---- {} ----'.format(unit))
fd0cec03 308 subprocess.check_output(['journalctl', '--sync'])
4ddb85b1
MP
309 sys.stdout.flush()
310 subprocess.call(['journalctl', '-b', '--no-pager', '--quiet',
311 '--cursor', self.journal_cursor, '-u', unit])
312
313 def create_iface(self, ipv6=False):
314 '''Create test interface with DHCP server behind it'''
315
316 raise NotImplementedError('must be implemented by a subclass')
317
318 def shutdown_iface(self):
319 '''Remove test interface and stop DHCP server'''
320
321 raise NotImplementedError('must be implemented by a subclass')
322
323 def print_server_log(self):
324 '''Print DHCP server log for debugging failures'''
325
326 raise NotImplementedError('must be implemented by a subclass')
327
328 def do_test(self, coldplug=True, ipv6=False, extra_opts='',
329 online_timeout=10, dhcp_mode='yes'):
d26fdaa2
MP
330 try:
331 subprocess.check_call(['systemctl', 'start', 'systemd-resolved'])
332 except subprocess.CalledProcessError:
333 self.show_journal('systemd-resolved.service')
334 raise
ec89276c 335 self.write_network(self.config, '''\
38d78d1e 336[Match]
278391c2 337Name={}
4ddb85b1 338[Network]
278391c2
BOT
339DHCP={}
340{}'''.format(self.iface, dhcp_mode, extra_opts))
4ddb85b1
MP
341
342 if coldplug:
343 # create interface first, then start networkd
344 self.create_iface(ipv6=ipv6)
345 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
e8c0de91 346 elif coldplug is not None:
4ddb85b1
MP
347 # start networkd first, then create interface
348 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
349 self.create_iface(ipv6=ipv6)
e8c0de91
MP
350 else:
351 # "None" means test sets up interface by itself
352 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
4ddb85b1
MP
353
354 try:
ec89276c 355 subprocess.check_call([NETWORKD_WAIT_ONLINE, '--interface',
4ddb85b1
MP
356 self.iface, '--timeout=%i' % online_timeout])
357
358 if ipv6:
359 # check iface state and IP 6 address; FIXME: we need to wait a bit
360 # longer, as the iface is "configured" already with IPv4 *or*
361 # IPv6, but we want to wait for both
00d5eaaf 362 for _ in range(10):
4ddb85b1
MP
363 out = subprocess.check_output(['ip', 'a', 'show', 'dev', self.iface])
364 if b'state UP' in out and b'inet6 2600' in out and b'inet 192.168' in out:
365 break
366 time.sleep(1)
367 else:
368 self.fail('timed out waiting for IPv6 configuration')
369
370 self.assertRegex(out, b'inet6 2600::.* scope global .*dynamic')
371 self.assertRegex(out, b'inet6 fe80::.* scope link')
372 else:
373 # should have link-local address on IPv6 only
374 out = subprocess.check_output(['ip', '-6', 'a', 'show', 'dev', self.iface])
cda39975 375 self.assertRegex(out, br'inet6 fe80::.* scope link')
4ddb85b1
MP
376 self.assertNotIn(b'scope global', out)
377
378 # should have IPv4 address
379 out = subprocess.check_output(['ip', '-4', 'a', 'show', 'dev', self.iface])
380 self.assertIn(b'state UP', out)
cda39975 381 self.assertRegex(out, br'inet 192.168.5.\d+/.* scope global dynamic')
4ddb85b1
MP
382
383 # check networkctl state
384 out = subprocess.check_output(['networkctl'])
278391c2
BOT
385 self.assertRegex(out, (r'{}\s+ether\s+[a-z-]+\s+unmanaged'.format(self.if_router)).encode())
386 self.assertRegex(out, (r'{}\s+ether\s+routable\s+configured'.format(self.iface)).encode())
4ddb85b1
MP
387
388 out = subprocess.check_output(['networkctl', 'status', self.iface])
cda39975
ZJS
389 self.assertRegex(out, br'Type:\s+ether')
390 self.assertRegex(out, br'State:\s+routable.*configured')
391 self.assertRegex(out, br'Address:\s+192.168.5.\d+')
4ddb85b1 392 if ipv6:
cda39975 393 self.assertRegex(out, br'2600::')
4ddb85b1 394 else:
cda39975
ZJS
395 self.assertNotIn(br'2600::', out)
396 self.assertRegex(out, br'fe80::')
397 self.assertRegex(out, br'Gateway:\s+192.168.5.1')
398 self.assertRegex(out, br'DNS:\s+192.168.5.1')
4ddb85b1
MP
399 except (AssertionError, subprocess.CalledProcessError):
400 # show networkd status, journal, and DHCP server log on failure
ec89276c 401 with open(os.path.join(NETWORK_UNITDIR, self.config)) as f:
278391c2 402 print('\n---- {} ----\n{}'.format(self.config, f.read()))
4ddb85b1
MP
403 print('---- interface status ----')
404 sys.stdout.flush()
405 subprocess.call(['ip', 'a', 'show', 'dev', self.iface])
278391c2 406 print('---- networkctl status {} ----'.format(self.iface))
4ddb85b1
MP
407 sys.stdout.flush()
408 subprocess.call(['networkctl', 'status', self.iface])
409 self.show_journal('systemd-networkd.service')
410 self.print_server_log()
411 raise
412
30b42a9a
MP
413 for timeout in range(50):
414 with open(RESOLV_CONF) as f:
415 contents = f.read()
416 if 'nameserver 192.168.5.1\n' in contents:
417 break
418 time.sleep(0.1)
419 else:
420 self.fail('nameserver 192.168.5.1 not found in ' + RESOLV_CONF)
4ddb85b1 421
e8c0de91 422 if coldplug is False:
4ddb85b1
MP
423 # check post-down.d hook
424 self.shutdown_iface()
425
426 def test_coldplug_dhcp_yes_ip4(self):
427 # we have a 12s timeout on RA, so we need to wait longer
428 self.do_test(coldplug=True, ipv6=False, online_timeout=15)
429
430 def test_coldplug_dhcp_yes_ip4_no_ra(self):
431 # with disabling RA explicitly things should be fast
432 self.do_test(coldplug=True, ipv6=False,
f921f573 433 extra_opts='IPv6AcceptRA=False')
4ddb85b1
MP
434
435 def test_coldplug_dhcp_ip4_only(self):
436 # we have a 12s timeout on RA, so we need to wait longer
437 self.do_test(coldplug=True, ipv6=False, dhcp_mode='ipv4',
438 online_timeout=15)
439
440 def test_coldplug_dhcp_ip4_only_no_ra(self):
441 # with disabling RA explicitly things should be fast
442 self.do_test(coldplug=True, ipv6=False, dhcp_mode='ipv4',
f921f573 443 extra_opts='IPv6AcceptRA=False')
4ddb85b1
MP
444
445 def test_coldplug_dhcp_ip6(self):
446 self.do_test(coldplug=True, ipv6=True)
447
448 def test_hotplug_dhcp_ip4(self):
449 # With IPv4 only we have a 12s timeout on RA, so we need to wait longer
450 self.do_test(coldplug=False, ipv6=False, online_timeout=15)
451
452 def test_hotplug_dhcp_ip6(self):
453 self.do_test(coldplug=False, ipv6=True)
454
94363cbb 455 def test_route_only_dns(self):
ec89276c 456 self.write_network('myvpn.netdev', '''\
38d78d1e 457[NetDev]
94363cbb
MP
458Name=dummy0
459Kind=dummy
460MACAddress=12:34:56:78:9a:bc''')
ec89276c 461 self.write_network('myvpn.network', '''\
38d78d1e 462[Match]
94363cbb
MP
463Name=dummy0
464[Network]
465Address=192.168.42.100
466DNS=192.168.42.1
467Domains= ~company''')
94363cbb
MP
468
469 self.do_test(coldplug=True, ipv6=False,
470 extra_opts='IPv6AcceptRouterAdvertisements=False')
471
30b42a9a
MP
472 with open(RESOLV_CONF) as f:
473 contents = f.read()
94363cbb
MP
474 # ~company is not a search domain, only a routing domain
475 self.assertNotRegex(contents, 'search.*company')
30b42a9a
MP
476 # our global server should appear
477 self.assertIn('nameserver 192.168.5.1\n', contents)
b9fe94ca
MP
478 # should not have domain-restricted server as global server
479 self.assertNotIn('nameserver 192.168.42.1\n', contents)
480
481 def test_route_only_dns_all_domains(self):
ec89276c 482 self.write_network('myvpn.netdev', '''[NetDev]
b9fe94ca
MP
483Name=dummy0
484Kind=dummy
485MACAddress=12:34:56:78:9a:bc''')
ec89276c 486 self.write_network('myvpn.network', '''[Match]
b9fe94ca
MP
487Name=dummy0
488[Network]
489Address=192.168.42.100
490DNS=192.168.42.1
491Domains= ~company ~.''')
b9fe94ca
MP
492
493 self.do_test(coldplug=True, ipv6=False,
494 extra_opts='IPv6AcceptRouterAdvertisements=False')
495
496 with open(RESOLV_CONF) as f:
497 contents = f.read()
498
499 # ~company is not a search domain, only a routing domain
500 self.assertNotRegex(contents, 'search.*company')
501
502 # our global server should appear
503 self.assertIn('nameserver 192.168.5.1\n', contents)
504 # should have company server as global server due to ~.
505 self.assertIn('nameserver 192.168.42.1\n', contents)
94363cbb 506
4ddb85b1 507
ec89276c 508@unittest.skipUnless(HAVE_DNSMASQ, 'dnsmasq not installed')
4ddb85b1
MP
509class DnsmasqClientTest(ClientTestBase, unittest.TestCase):
510 '''Test networkd client against dnsmasq'''
511
512 def setUp(self):
513 super().setUp()
514 self.dnsmasq = None
e8c0de91 515 self.iface_mac = 'de:ad:be:ef:47:11'
4ddb85b1 516
b9fe94ca 517 def create_iface(self, ipv6=False, dnsmasq_opts=None):
4ddb85b1
MP
518 '''Create test interface with DHCP server behind it'''
519
520 # add veth pair
e8c0de91
MP
521 subprocess.check_call(['ip', 'link', 'add', 'name', self.iface,
522 'address', self.iface_mac,
523 'type', 'veth', 'peer', 'name', self.if_router])
4ddb85b1
MP
524
525 # give our router an IP
526 subprocess.check_call(['ip', 'a', 'flush', 'dev', self.if_router])
527 subprocess.check_call(['ip', 'a', 'add', '192.168.5.1/24', 'dev', self.if_router])
528 if ipv6:
529 subprocess.check_call(['ip', 'a', 'add', '2600::1/64', 'dev', self.if_router])
530 subprocess.check_call(['ip', 'link', 'set', self.if_router, 'up'])
531
532 # add DHCP server
533 self.dnsmasq_log = os.path.join(self.workdir, 'dnsmasq.log')
534 lease_file = os.path.join(self.workdir, 'dnsmasq.leases')
535 if ipv6:
536 extra_opts = ['--enable-ra', '--dhcp-range=2600::10,2600::20']
537 else:
538 extra_opts = []
b9fe94ca
MP
539 if dnsmasq_opts:
540 extra_opts += dnsmasq_opts
4ddb85b1
MP
541 self.dnsmasq = subprocess.Popen(
542 ['dnsmasq', '--keep-in-foreground', '--log-queries',
543 '--log-facility=' + self.dnsmasq_log, '--conf-file=/dev/null',
544 '--dhcp-leasefile=' + lease_file, '--bind-interfaces',
545 '--interface=' + self.if_router, '--except-interface=lo',
546 '--dhcp-range=192.168.5.10,192.168.5.200'] + extra_opts)
547
548 def shutdown_iface(self):
549 '''Remove test interface and stop DHCP server'''
550
551 if self.if_router:
552 subprocess.check_call(['ip', 'link', 'del', 'dev', self.if_router])
553 self.if_router = None
554 if self.dnsmasq:
555 self.dnsmasq.kill()
556 self.dnsmasq.wait()
557 self.dnsmasq = None
558
559 def print_server_log(self):
560 '''Print DHCP server log for debugging failures'''
561
562 with open(self.dnsmasq_log) as f:
278391c2 563 sys.stdout.write('\n\n---- dnsmasq log ----\n{}\n------\n\n'.format(f.read()))
4ddb85b1 564
b9fe94ca
MP
565 def test_resolved_domain_restricted_dns(self):
566 '''resolved: domain-restricted DNS servers'''
567
568 # create interface for generic connections; this will map all DNS names
569 # to 192.168.42.1
570 self.create_iface(dnsmasq_opts=['--address=/#/192.168.42.1'])
ec89276c 571 self.write_network('general.network', '''\
b9fe94ca 572[Match]
278391c2 573Name={}
b9fe94ca
MP
574[Network]
575DHCP=ipv4
278391c2 576IPv6AcceptRA=False'''.format(self.iface))
b9fe94ca
MP
577
578 # create second device/dnsmasq for a .company/.lab VPN interface
579 # static IPs for simplicity
618b196e 580 self.add_veth_pair('testvpnclient', 'testvpnrouter')
b9fe94ca
MP
581 subprocess.check_call(['ip', 'a', 'flush', 'dev', 'testvpnrouter'])
582 subprocess.check_call(['ip', 'a', 'add', '10.241.3.1/24', 'dev', 'testvpnrouter'])
583 subprocess.check_call(['ip', 'link', 'set', 'testvpnrouter', 'up'])
584
585 vpn_dnsmasq_log = os.path.join(self.workdir, 'dnsmasq-vpn.log')
586 vpn_dnsmasq = subprocess.Popen(
587 ['dnsmasq', '--keep-in-foreground', '--log-queries',
588 '--log-facility=' + vpn_dnsmasq_log, '--conf-file=/dev/null',
589 '--dhcp-leasefile=/dev/null', '--bind-interfaces',
590 '--interface=testvpnrouter', '--except-interface=lo',
591 '--address=/math.lab/10.241.3.3', '--address=/cantina.company/10.241.4.4'])
592 self.addCleanup(vpn_dnsmasq.wait)
593 self.addCleanup(vpn_dnsmasq.kill)
594
ec89276c 595 self.write_network('vpn.network', '''\
b9fe94ca
MP
596[Match]
597Name=testvpnclient
598[Network]
599IPv6AcceptRA=False
600Address=10.241.3.2/24
601DNS=10.241.3.1
602Domains= ~company ~lab''')
603
604 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
ec89276c 605 subprocess.check_call([NETWORKD_WAIT_ONLINE, '--interface', self.iface,
b9fe94ca
MP
606 '--interface=testvpnclient', '--timeout=20'])
607
608 # ensure we start fresh with every test
609 subprocess.check_call(['systemctl', 'restart', 'systemd-resolved'])
610
611 # test vpnclient specific domains; these should *not* be answered by
612 # the general DNS
613 out = subprocess.check_output(['systemd-resolve', 'math.lab'])
614 self.assertIn(b'math.lab: 10.241.3.3', out)
615 out = subprocess.check_output(['systemd-resolve', 'kettle.cantina.company'])
616 self.assertIn(b'kettle.cantina.company: 10.241.4.4', out)
617
618 # test general domains
619 out = subprocess.check_output(['systemd-resolve', 'megasearch.net'])
620 self.assertIn(b'megasearch.net: 192.168.42.1', out)
621
622 with open(self.dnsmasq_log) as f:
623 general_log = f.read()
624 with open(vpn_dnsmasq_log) as f:
625 vpn_log = f.read()
626
627 # VPN domains should only be sent to VPN DNS
628 self.assertRegex(vpn_log, 'query.*math.lab')
629 self.assertRegex(vpn_log, 'query.*cantina.company')
27e2e323
MP
630 self.assertNotIn('.lab', general_log)
631 self.assertNotIn('.company', general_log)
b9fe94ca
MP
632
633 # general domains should not be sent to the VPN DNS
634 self.assertRegex(general_log, 'query.*megasearch.net')
635 self.assertNotIn('megasearch.net', vpn_log)
636
4050e04b
MP
637 def test_resolved_etc_hosts(self):
638 '''resolved queries to /etc/hosts'''
639
640 # FIXME: -t MX query fails with enabled DNSSEC (even when using
641 # the known negative trust anchor .internal instead of .example)
642 conf = '/run/systemd/resolved.conf.d/test-disable-dnssec.conf'
643 os.makedirs(os.path.dirname(conf), exist_ok=True)
644 with open(conf, 'w') as f:
645 f.write('[Resolve]\nDNSSEC=no')
646 self.addCleanup(os.remove, conf)
647
648 # create /etc/hosts bind mount which resolves my.example for IPv4
649 hosts = os.path.join(self.workdir, 'hosts')
650 with open(hosts, 'w') as f:
651 f.write('172.16.99.99 my.example\n')
652 subprocess.check_call(['mount', '--bind', hosts, '/etc/hosts'])
653 self.addCleanup(subprocess.call, ['umount', '/etc/hosts'])
654 subprocess.check_call(['systemctl', 'stop', 'systemd-resolved.service'])
655
656 # note: different IPv4 address here, so that it's easy to tell apart
657 # what resolved the query
658 self.create_iface(dnsmasq_opts=['--host-record=my.example,172.16.99.1,2600::99:99',
659 '--host-record=other.example,172.16.0.42,2600::42',
660 '--mx-host=example,mail.example'],
661 ipv6=True)
662 self.do_test(coldplug=None, ipv6=True)
663
664 try:
665 # family specific queries
666 out = subprocess.check_output(['systemd-resolve', '-4', 'my.example'])
667 self.assertIn(b'my.example: 172.16.99.99', out)
668 # we don't expect an IPv6 answer; if /etc/hosts has any IP address,
669 # it's considered a sufficient source
670 self.assertNotEqual(subprocess.call(['systemd-resolve', '-6', 'my.example']), 0)
671 # "any family" query; IPv4 should come from /etc/hosts
672 out = subprocess.check_output(['systemd-resolve', 'my.example'])
673 self.assertIn(b'my.example: 172.16.99.99', out)
674 # IP → name lookup; again, takes the /etc/hosts one
675 out = subprocess.check_output(['systemd-resolve', '172.16.99.99'])
676 self.assertIn(b'172.16.99.99: my.example', out)
677
678 # non-address RRs should fall back to DNS
679 out = subprocess.check_output(['systemd-resolve', '--type=MX', 'example'])
680 self.assertIn(b'example IN MX 1 mail.example', out)
681
682 # other domains query DNS
683 out = subprocess.check_output(['systemd-resolve', 'other.example'])
684 self.assertIn(b'172.16.0.42', out)
685 out = subprocess.check_output(['systemd-resolve', '172.16.0.42'])
686 self.assertIn(b'172.16.0.42: other.example', out)
687 except (AssertionError, subprocess.CalledProcessError):
688 self.show_journal('systemd-resolved.service')
689 self.print_server_log()
690 raise
691
e8c0de91
MP
692 def test_transient_hostname(self):
693 '''networkd sets transient hostname from DHCP'''
694
89748b0a
MP
695 orig_hostname = socket.gethostname()
696 self.addCleanup(socket.sethostname, orig_hostname)
697 # temporarily move /etc/hostname away; restart hostnamed to pick it up
698 if os.path.exists('/etc/hostname'):
699 subprocess.check_call(['mount', '--bind', '/dev/null', '/etc/hostname'])
700 self.addCleanup(subprocess.call, ['umount', '/etc/hostname'])
701 subprocess.check_call(['systemctl', 'stop', 'systemd-hostnamed.service'])
702
278391c2 703 self.create_iface(dnsmasq_opts=['--dhcp-host={},192.168.5.210,testgreen'.format(self.iface_mac)])
e8c0de91
MP
704 self.do_test(coldplug=None, extra_opts='IPv6AcceptRA=False', dhcp_mode='ipv4')
705
fd0cec03
MP
706 try:
707 # should have received the fixed IP above
708 out = subprocess.check_output(['ip', '-4', 'a', 'show', 'dev', self.iface])
709 self.assertRegex(out, b'inet 192.168.5.210/24 .* scope global dynamic')
2926b130
MP
710 # should have set transient hostname in hostnamed; this is
711 # sometimes a bit lagging (issue #4753), so retry a few times
712 for retry in range(1, 6):
713 out = subprocess.check_output(['hostnamectl'])
714 if b'testgreen' in out:
715 break
716 time.sleep(5)
717 sys.stdout.write('[retry %i] ' % retry)
718 sys.stdout.flush()
719 else:
278391c2 720 self.fail('Transient hostname not found in hostnamectl:\n{}'.format(out.decode()))
fd0cec03
MP
721 # and also applied to the system
722 self.assertEqual(socket.gethostname(), 'testgreen')
723 except AssertionError:
724 self.show_journal('systemd-networkd.service')
725 self.show_journal('systemd-hostnamed.service')
726 self.print_server_log()
727 raise
89748b0a
MP
728
729 def test_transient_hostname_with_static(self):
730 '''transient hostname is not applied if static hostname exists'''
731
732 orig_hostname = socket.gethostname()
733 self.addCleanup(socket.sethostname, orig_hostname)
734 if not os.path.exists('/etc/hostname'):
735 self.writeConfig('/etc/hostname', orig_hostname)
736 subprocess.check_call(['systemctl', 'stop', 'systemd-hostnamed.service'])
737
278391c2 738 self.create_iface(dnsmasq_opts=['--dhcp-host={},192.168.5.210,testgreen'.format(self.iface_mac)])
89748b0a
MP
739 self.do_test(coldplug=None, extra_opts='IPv6AcceptRA=False', dhcp_mode='ipv4')
740
fd0cec03
MP
741 try:
742 # should have received the fixed IP above
743 out = subprocess.check_output(['ip', '-4', 'a', 'show', 'dev', self.iface])
744 self.assertRegex(out, b'inet 192.168.5.210/24 .* scope global dynamic')
745 # static hostname wins over transient one, thus *not* applied
746 self.assertEqual(socket.gethostname(), orig_hostname)
747 except AssertionError:
748 self.show_journal('systemd-networkd.service')
749 self.show_journal('systemd-hostnamed.service')
750 self.print_server_log()
751 raise
e8c0de91 752
4ddb85b1
MP
753
754class NetworkdClientTest(ClientTestBase, unittest.TestCase):
755 '''Test networkd client against networkd server'''
756
757 def setUp(self):
758 super().setUp()
759 self.dnsmasq = None
760
2c99aba7 761 def create_iface(self, ipv6=False, dhcpserver_opts=None):
4ddb85b1
MP
762 '''Create test interface with DHCP server behind it'''
763
764 # run "router-side" networkd in own mount namespace to shield it from
765 # "client-side" configuration and networkd
766 (fd, script) = tempfile.mkstemp(prefix='networkd-router.sh')
767 self.addCleanup(os.remove, script)
768 with os.fdopen(fd, 'w+') as f:
38d78d1e 769 f.write('''\
7629744a 770#!/bin/sh
771set -eu
4ddb85b1
MP
772mkdir -p /run/systemd/network
773mkdir -p /run/systemd/netif
774mount -t tmpfs none /run/systemd/network
775mount -t tmpfs none /run/systemd/netif
776[ ! -e /run/dbus ] || mount -t tmpfs none /run/dbus
777# create router/client veth pair
778cat << EOF > /run/systemd/network/test.netdev
779[NetDev]
780Name=%(ifr)s
781Kind=veth
782
783[Peer]
784Name=%(ifc)s
785EOF
786
787cat << EOF > /run/systemd/network/test.network
788[Match]
789Name=%(ifr)s
790
791[Network]
792Address=192.168.5.1/24
793%(addr6)s
794DHCPServer=yes
795
796[DHCPServer]
797PoolOffset=10
798PoolSize=50
799DNS=192.168.5.1
2c99aba7 800%(dhopts)s
4ddb85b1
MP
801EOF
802
803# run networkd as in systemd-networkd.service
5ed0dcf4 804exec $(systemctl cat systemd-networkd.service | sed -n '/^ExecStart=/ { s/^.*=//; s/^[@+-]//; s/^!*//; p}')
2c99aba7
MP
805''' % {'ifr': self.if_router, 'ifc': self.iface, 'addr6': ipv6 and 'Address=2600::1/64' or '',
806 'dhopts': dhcpserver_opts or ''})
4ddb85b1
MP
807
808 os.fchmod(fd, 0o755)
809
810 subprocess.check_call(['systemd-run', '--unit=networkd-test-router.service',
811 '-p', 'InaccessibleDirectories=-/etc/systemd/network',
812 '-p', 'InaccessibleDirectories=-/run/systemd/network',
813 '-p', 'InaccessibleDirectories=-/run/systemd/netif',
814 '--service-type=notify', script])
815
816 # wait until devices got created
00d5eaaf 817 for _ in range(50):
4ddb85b1
MP
818 out = subprocess.check_output(['ip', 'a', 'show', 'dev', self.if_router])
819 if b'state UP' in out and b'scope global' in out:
820 break
821 time.sleep(0.1)
822
823 def shutdown_iface(self):
824 '''Remove test interface and stop DHCP server'''
825
826 if self.if_router:
827 subprocess.check_call(['systemctl', 'stop', 'networkd-test-router.service'])
828 # ensure failed transient unit does not stay around
829 subprocess.call(['systemctl', 'reset-failed', 'networkd-test-router.service'])
830 subprocess.call(['ip', 'link', 'del', 'dev', self.if_router])
831 self.if_router = None
832
833 def print_server_log(self):
834 '''Print DHCP server log for debugging failures'''
835
836 self.show_journal('networkd-test-router.service')
837
838 @unittest.skip('networkd does not have DHCPv6 server support')
839 def test_hotplug_dhcp_ip6(self):
840 pass
841
842 @unittest.skip('networkd does not have DHCPv6 server support')
843 def test_coldplug_dhcp_ip6(self):
844 pass
845
d2bc1251
MP
846 def test_search_domains(self):
847
848 # we don't use this interface for this test
849 self.if_router = None
850
ec89276c 851 self.write_network('test.netdev', '''\
38d78d1e 852[NetDev]
d2bc1251
MP
853Name=dummy0
854Kind=dummy
855MACAddress=12:34:56:78:9a:bc''')
ec89276c 856 self.write_network('test.network', '''\
38d78d1e 857[Match]
d2bc1251
MP
858Name=dummy0
859[Network]
860Address=192.168.42.100
861DNS=192.168.42.1
862Domains= one two three four five six seven eight nine ten''')
d2bc1251
MP
863
864 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
865
30b42a9a
MP
866 for timeout in range(50):
867 with open(RESOLV_CONF) as f:
868 contents = f.read()
869 if ' one' in contents:
870 break
871 time.sleep(0.1)
872 self.assertRegex(contents, 'search .*one two three four')
873 self.assertNotIn('seven\n', contents)
874 self.assertIn('# Too many search domains configured, remaining ones ignored.\n', contents)
d2bc1251
MP
875
876 def test_search_domains_too_long(self):
877
878 # we don't use this interface for this test
879 self.if_router = None
880
881 name_prefix = 'a' * 60
882
ec89276c 883 self.write_network('test.netdev', '''\
38d78d1e 884[NetDev]
d2bc1251
MP
885Name=dummy0
886Kind=dummy
887MACAddress=12:34:56:78:9a:bc''')
ec89276c 888 self.write_network('test.network', '''\
38d78d1e 889[Match]
d2bc1251
MP
890Name=dummy0
891[Network]
892Address=192.168.42.100
893DNS=192.168.42.1
38d78d1e 894Domains={p}0 {p}1 {p}2 {p}3 {p}4'''.format(p=name_prefix))
d2bc1251
MP
895
896 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
897
30b42a9a
MP
898 for timeout in range(50):
899 with open(RESOLV_CONF) as f:
900 contents = f.read()
901 if ' one' in contents:
902 break
903 time.sleep(0.1)
38d78d1e 904 self.assertRegex(contents, 'search .*{p}0 {p}1 {p}2'.format(p=name_prefix))
30b42a9a 905 self.assertIn('# Total length of all search domains is too long, remaining ones ignored.', contents)
d2bc1251 906
047a0dac
JSB
907 def test_dropin(self):
908 # we don't use this interface for this test
909 self.if_router = None
910
ec89276c 911 self.write_network('test.netdev', '''\
047a0dac
JSB
912[NetDev]
913Name=dummy0
914Kind=dummy
915MACAddress=12:34:56:78:9a:bc''')
ec89276c 916 self.write_network('test.network', '''\
047a0dac
JSB
917[Match]
918Name=dummy0
919[Network]
920Address=192.168.42.100
921DNS=192.168.42.1''')
ec89276c 922 self.write_network_dropin('test.network', 'dns', '''\
047a0dac
JSB
923[Network]
924DNS=127.0.0.1''')
925
926 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
927
928 for timeout in range(50):
929 with open(RESOLV_CONF) as f:
930 contents = f.read()
931 if ' 127.0.0.1' in contents:
932 break
933 time.sleep(0.1)
934 self.assertIn('nameserver 192.168.42.1\n', contents)
935 self.assertIn('nameserver 127.0.0.1\n', contents)
936
2c99aba7
MP
937 def test_dhcp_timezone(self):
938 '''networkd sets time zone from DHCP'''
939
940 def get_tz():
941 out = subprocess.check_output(['busctl', 'get-property', 'org.freedesktop.timedate1',
942 '/org/freedesktop/timedate1', 'org.freedesktop.timedate1', 'Timezone'])
943 assert out.startswith(b's "')
944 out = out.strip()
945 assert out.endswith(b'"')
946 return out[3:-1].decode()
947
948 orig_timezone = get_tz()
949 self.addCleanup(subprocess.call, ['timedatectl', 'set-timezone', orig_timezone])
950
951 self.create_iface(dhcpserver_opts='EmitTimezone=yes\nTimezone=Pacific/Honolulu')
952 self.do_test(coldplug=None, extra_opts='IPv6AcceptRA=false\n[DHCP]\nUseTimezone=true', dhcp_mode='ipv4')
953
954 # should have applied the received timezone
955 try:
956 self.assertEqual(get_tz(), 'Pacific/Honolulu')
957 except AssertionError:
958 self.show_journal('systemd-networkd.service')
959 self.show_journal('systemd-hostnamed.service')
960 raise
961
962
618b196e
DM
963class MatchClientTest(unittest.TestCase, NetworkdTestingUtilities):
964 """Test [Match] sections in .network files.
965
966 Be aware that matching the test host's interfaces will wipe their
967 configuration, so as a precaution, all network files should have a
968 restrictive [Match] section to only ever interfere with the
969 temporary veth interfaces created here.
970 """
971
972 def tearDown(self):
973 """Stop networkd."""
974 subprocess.call(['systemctl', 'stop', 'systemd-networkd'])
975
976 def test_basic_matching(self):
977 """Verify the Name= line works throughout this class."""
978 self.add_veth_pair('test_if1', 'fake_if2')
979 self.write_network('test.network', "[Match]\nName=test_*\n[Network]")
980 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
981 self.assert_link_states(test_if1='managed', fake_if2='unmanaged')
982
983 def test_inverted_matching(self):
984 """Verify that a '!'-prefixed value inverts the match."""
985 # Use a MAC address as the interfaces' common matching attribute
986 # to avoid depending on udev, to support testing in containers.
987 mac = '00:01:02:03:98:99'
988 self.add_veth_pair('test_veth', 'test_peer',
989 ['addr', mac], ['addr', mac])
990 self.write_network('no-veth.network', """\
991[Match]
278391c2 992MACAddress={}
618b196e 993Name=!nonexistent *peer*
278391c2 994[Network]""".format(mac))
618b196e
DM
995 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
996 self.assert_link_states(test_veth='managed', test_peer='unmanaged')
997
998
a09dc546
DM
999class UnmanagedClientTest(unittest.TestCase, NetworkdTestingUtilities):
1000 """Test if networkd manages the correct interfaces."""
1001
1002 def setUp(self):
1003 """Write .network files to match the named veth devices."""
1004 # Define the veth+peer pairs to be created.
1005 # Their pairing doesn't actually matter, only their names do.
1006 self.veths = {
1007 'm1def': 'm0unm',
1008 'm1man': 'm1unm',
1009 }
1010
1011 # Define the contents of .network files to be read in order.
1012 self.configs = (
1013 "[Match]\nName=m1def\n",
1014 "[Match]\nName=m1unm\n[Link]\nUnmanaged=yes\n",
1015 "[Match]\nName=m1*\n[Link]\nUnmanaged=no\n",
1016 )
1017
1018 # Write out the .network files to be cleaned up automatically.
1019 for i, config in enumerate(self.configs):
1020 self.write_network("%02d-test.network" % i, config)
1021
1022 def tearDown(self):
1023 """Stop networkd."""
1024 subprocess.call(['systemctl', 'stop', 'systemd-networkd'])
1025
1026 def create_iface(self):
1027 """Create temporary veth pairs for interface matching."""
1028 for veth, peer in self.veths.items():
618b196e 1029 self.add_veth_pair(veth, peer)
a09dc546
DM
1030
1031 def test_unmanaged_setting(self):
1032 """Verify link states with Unmanaged= settings, hot-plug."""
1033 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
1034 self.create_iface()
1035 self.assert_link_states(m1def='managed',
1036 m1man='managed',
1037 m1unm='unmanaged',
1038 m0unm='unmanaged')
1039
1040 def test_unmanaged_setting_coldplug(self):
1041 """Verify link states with Unmanaged= settings, cold-plug."""
1042 self.create_iface()
1043 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
1044 self.assert_link_states(m1def='managed',
1045 m1man='managed',
1046 m1unm='unmanaged',
1047 m0unm='unmanaged')
1048
1049 def test_catchall_config(self):
1050 """Verify link states with a catch-all config, hot-plug."""
1051 # Don't actually catch ALL interfaces. It messes up the host.
1052 self.write_network('all.network', "[Match]\nName=m[01]???\n")
1053 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
1054 self.create_iface()
1055 self.assert_link_states(m1def='managed',
1056 m1man='managed',
1057 m1unm='unmanaged',
1058 m0unm='managed')
1059
1060 def test_catchall_config_coldplug(self):
1061 """Verify link states with a catch-all config, cold-plug."""
1062 # Don't actually catch ALL interfaces. It messes up the host.
1063 self.write_network('all.network', "[Match]\nName=m[01]???\n")
1064 self.create_iface()
1065 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
1066 self.assert_link_states(m1def='managed',
1067 m1man='managed',
1068 m1unm='unmanaged',
1069 m0unm='managed')
1070
1071
4ddb85b1
MP
1072if __name__ == '__main__':
1073 unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout,
1074 verbosity=2))