From: Frantisek Sumsal Date: Mon, 17 Jul 2023 08:12:39 +0000 (+0200) Subject: test-network: check for captive portals received via NDISC X-Git-Tag: v254-rc3~33^2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=refs%2Fpull%2F28424%2Fhead;p=thirdparty%2Fsystemd.git test-network: check for captive portals received via NDISC This requires fairly recent radvd that supports sending RAs with captive portals [0]. Also, this should hopefully provide coverage for issues like: - https://github.com/systemd/systemd/issues/28229 - https://github.com/systemd/systemd/issues/28231 - https://github.com/systemd/systemd/issues/28277 [0] https://github.com/radvd-project/radvd/pull/141 --- diff --git a/test/test-network/conf/25-veth-bridge-captive.network b/test/test-network/conf/25-veth-bridge-captive.network new file mode 100644 index 00000000000..34906184d3c --- /dev/null +++ b/test/test-network/conf/25-veth-bridge-captive.network @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=client-p +Name=router-captivep + +[Network] +Bridge=bridge99 +IPv6AcceptRA=no +IPv6SendRA=yes diff --git a/test/test-network/conf/25-veth-client-captive.network b/test/test-network/conf/25-veth-client-captive.network new file mode 100644 index 00000000000..985366c1a6d --- /dev/null +++ b/test/test-network/conf/25-veth-client-captive.network @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=client + +[Network] +IPv6AcceptRA=yes + +[IPv6AcceptRA] +UseDNS=no +UseDomains=no +UseCaptivePortal=yes diff --git a/test/test-network/conf/25-veth-router-captive.netdev b/test/test-network/conf/25-veth-router-captive.netdev new file mode 100644 index 00000000000..a9368cd4c52 --- /dev/null +++ b/test/test-network/conf/25-veth-router-captive.netdev @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[NetDev] +Name=router-captive +Kind=veth +MACAddress=12:34:56:78:9a:99 + +[Peer] +Name=router-captivep +MACAddress=12:34:56:78:9b:99 diff --git a/test/test-network/conf/25-veth-router-captive.network b/test/test-network/conf/25-veth-router-captive.network new file mode 100644 index 00000000000..21259a3e1d8 --- /dev/null +++ b/test/test-network/conf/25-veth-router-captive.network @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=router-captive + +[Network] +IPv6AcceptRA=no +IPv6SendRA=no diff --git a/test/test-network/conf/radvd/captive-portal.conf b/test/test-network/conf/radvd/captive-portal.conf new file mode 100644 index 00000000000..723a0dfc558 --- /dev/null +++ b/test/test-network/conf/radvd/captive-portal.conf @@ -0,0 +1,11 @@ +interface router-captive +{ + AdvSendAdvert on; + AdvCaptivePortalAPI "http://systemd.io"; + + prefix 2002:da8:1:99::/64 + { + AdvOnLink on; + AdvAutonomous on; + }; +}; diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index fca7408a0db..7f3d262ac95 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -35,6 +35,8 @@ dnsmasq_lease_file = '/run/networkd-ci/test-dnsmasq.lease' isc_dhcpd_pid_file = '/run/networkd-ci/test-isc-dhcpd.pid' isc_dhcpd_lease_file = '/run/networkd-ci/test-isc-dhcpd.lease' +radvd_pid_file = '/run/networkd-ci/test-radvd.pid' + systemd_lib_paths = ['/usr/lib/systemd', '/lib/systemd'] which_paths = ':'.join(systemd_lib_paths + os.getenv('PATH', os.defpath).lstrip(':').split(':')) @@ -537,6 +539,23 @@ def read_ipv6_sysctl_attr(link, attribute): def read_ipv4_sysctl_attr(link, attribute): return read_ip_sysctl_attr(link, attribute, 'ipv4') +def stop_by_pid_file(pid_file): + if not os.path.exists(pid_file): + return + with open(pid_file, 'r', encoding='utf-8') as f: + pid = f.read().rstrip(' \t\r\n\0') + os.kill(int(pid), signal.SIGTERM) + for _ in range(25): + try: + os.kill(int(pid), 0) + print(f"PID {pid} is still alive, waiting...") + time.sleep(.2) + except OSError as e: + if e.errno == errno.ESRCH: + break + print(f"Unexpected exception when waiting for {pid} to die: {e.errno}") + rm_f(pid_file) + def start_dnsmasq(*additional_options, interface='veth-peer', lease_time='2m', ipv4_range='192.168.5.10,192.168.5.200', ipv4_router='192.168.5.1', ipv6_range='2600::10,2600::20'): command = ( 'dnsmasq', @@ -558,23 +577,6 @@ def start_dnsmasq(*additional_options, interface='veth-peer', lease_time='2m', i ) + additional_options check_output(*command) -def stop_by_pid_file(pid_file): - if not os.path.exists(pid_file): - return - with open(pid_file, 'r', encoding='utf-8') as f: - pid = f.read().rstrip(' \t\r\n\0') - os.kill(int(pid), signal.SIGTERM) - for _ in range(25): - try: - os.kill(int(pid), 0) - print(f"PID {pid} is still alive, waiting...") - time.sleep(.2) - except OSError as e: - if e.errno == errno.ESRCH: - break - print(f"Unexpected exception when waiting for {pid} to die: {e.errno}") - os.remove(pid_file) - def stop_dnsmasq(): stop_by_pid_file(dnsmasq_pid_file) rm_f(dnsmasq_lease_file) @@ -594,6 +596,29 @@ def stop_isc_dhcpd(): stop_by_pid_file(isc_dhcpd_pid_file) rm_f(isc_dhcpd_lease_file) +def start_radvd(*additional_options, config_file): + config_file_path = os.path.join(networkd_ci_temp_dir, 'radvd', config_file) + command = ( + 'radvd', + f'--pidfile={radvd_pid_file}', + f'--config={config_file_path}', + '--logmethod=stderr', + ) + additional_options + check_output(*command) + +def stop_radvd(): + stop_by_pid_file(radvd_pid_file) + +def radvd_check_config(config_file): + if not shutil.which('radvd'): + print('radvd is not installed, assuming the config check failed') + return False + + # Note: can't use networkd_ci_temp_dir here, as this command may run before that dir is + # set up (one instance is @unittest.skipX()) + config_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'conf/radvd', config_file) + return call(f'radvd --config={config_file_path} --configtest') == 0 + def networkd_invocation_id(): return check_output('systemctl show --value -p InvocationID systemd-networkd.service') @@ -637,9 +662,10 @@ def setup_common(): print() def tear_down_common(): - # 1. stop DHCP servers + # 1. stop DHCP/RA servers stop_dnsmasq() stop_isc_dhcpd() + stop_radvd() # 2. remove modules call_quiet('rmmod netdevsim') @@ -4553,6 +4579,64 @@ class NetworkdRATests(unittest.TestCase, Utilities): print(output) self.assertIn('pref low', output) + @unittest.skipUnless(radvd_check_config('captive-portal.conf'), "Installed radvd doesn't support captive portals") + def test_captive_portal(self): + copy_network_unit('25-veth-client.netdev', + '25-veth-router-captive.netdev', + '26-bridge.netdev', + '25-veth-client-captive.network', + '25-veth-router-captive.network', + '25-veth-bridge-captive.network', + '25-bridge99.network') + start_networkd() + self.wait_online(['bridge99:routable', 'client-p:enslaved', + 'router-captive:degraded', 'router-captivep:enslaved']) + + start_radvd(config_file='captive-portal.conf') + networkctl_reconfigure('client') + self.wait_online(['client:routable']) + + self.wait_address('client', '2002:da8:1:99:1034:56ff:fe78:9a00/64', ipv='-6', timeout_sec=10) + output = check_output(*networkctl_cmd, 'status', 'client', env=env) + print(output) + self.assertIn('Captive Portal: http://systemd.io', output) + + @unittest.skipUnless(radvd_check_config('captive-portal.conf'), "Installed radvd doesn't support captive portals") + def test_invalid_captive_portal(self): + def radvd_write_config(captive_portal_uri): + with open(os.path.join(networkd_ci_temp_dir, 'radvd/bogus-captive-portal.conf'), mode='w', encoding='utf-8') as f: + f.write(f'interface router-captive {{ AdvSendAdvert on; AdvCaptivePortalAPI "{captive_portal_uri}"; prefix 2002:da8:1:99::/64 {{ AdvOnLink on; AdvAutonomous on; }}; }};') + + captive_portal_uris = [ + "42ěščěškd ěšč ě s", + " ", + "🤔", + ] + + copy_network_unit('25-veth-client.netdev', + '25-veth-router-captive.netdev', + '26-bridge.netdev', + '25-veth-client-captive.network', + '25-veth-router-captive.network', + '25-veth-bridge-captive.network', + '25-bridge99.network') + start_networkd() + self.wait_online(['bridge99:routable', 'client-p:enslaved', + 'router-captive:degraded', 'router-captivep:enslaved']) + + for uri in captive_portal_uris: + print(f"Captive portal: {uri}") + radvd_write_config(uri) + stop_radvd() + start_radvd(config_file='bogus-captive-portal.conf') + networkctl_reconfigure('client') + self.wait_online(['client:routable']) + + self.wait_address('client', '2002:da8:1:99:1034:56ff:fe78:9a00/64', ipv='-6', timeout_sec=10) + output = check_output(*networkctl_cmd, 'status', 'client', env=env) + print(output) + self.assertNotIn('Captive Portal:', output) + class NetworkdDHCPServerTests(unittest.TestCase, Utilities): def setUp(self):