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