From: Luca Boccassi Date: Sat, 28 Mar 2026 14:40:11 +0000 (+0000) Subject: test: add a mock ModemManager for basic coverage of sd-networkd's integration X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=abe3d570f8006fca5138b2d5cfb4e8b530be02e5;p=thirdparty%2Fsystemd.git test: add a mock ModemManager for basic coverage of sd-networkd's integration Just the minimal setup and test case required to cover https://github.com/systemd/systemd/issues/41389 for now, can be expanded in the future Boring boilerplate is bot-made, don't @ me Co-developed-by: Claude Opus 4.6 noreply@anthropic.com --- diff --git a/src/network/meson.build b/src/network/meson.build index 85b57669e46..00361a0017e 100644 --- a/src/network/meson.build +++ b/src/network/meson.build @@ -258,6 +258,12 @@ executables += [ network_test_template + { 'sources' : files('test-networkd-util.c'), }, + test_template + { + 'sources' : files('test-modem-manager-mock.c'), + 'conditions' : ['ENABLE_NETWORKD'], + 'link_with' : [libshared], + 'type' : 'manual', + }, network_fuzz_template + { 'sources' : files('fuzz-netdev-parser.c'), }, diff --git a/src/network/test-modem-manager-mock.c b/src/network/test-modem-manager-mock.c new file mode 100644 index 00000000000..60f0dfa8d4e --- /dev/null +++ b/src/network/test-modem-manager-mock.c @@ -0,0 +1,486 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* + * Minimal mock of ModemManager's D-Bus interface for testing systemd-networkd + * wwan/bearer support. + * + * Claims the org.freedesktop.ModemManager1 bus name and responds to: + * - GetManagedObjects on /org/freedesktop/ModemManager1 + * - GetAll on /org/freedesktop/ModemManager1/Bearer/0 + * - Simple.Connect on /org/freedesktop/ModemManager1/Modem/0 + */ + +#include + +#include "sd-bus.h" +#include "sd-daemon.h" +#include "sd-event.h" + +#include "alloc-util.h" +#include "build.h" +#include "log.h" +#include "main-func.h" +#include "parse-util.h" +#include "string-util.h" + +static char *arg_ifname = NULL; +static char *arg_ipv4_address = NULL; +static char *arg_ipv4_gateway = NULL; +static uint32_t arg_ipv4_prefix = 24; +static char *arg_ipv6_address = NULL; +static char *arg_ipv6_gateway = NULL; +static uint32_t arg_ipv6_prefix = 64; + +STATIC_DESTRUCTOR_REGISTER(arg_ifname, freep); +STATIC_DESTRUCTOR_REGISTER(arg_ipv4_address, freep); +STATIC_DESTRUCTOR_REGISTER(arg_ipv4_gateway, freep); +STATIC_DESTRUCTOR_REGISTER(arg_ipv6_address, freep); +STATIC_DESTRUCTOR_REGISTER(arg_ipv6_gateway, freep); + +/* ModemManager enum values */ +#define MM_BEARER_IP_METHOD_STATIC 2 +#define MM_MODEM_PORT_TYPE_NET 2 +#define MM_MODEM_STATE_CONNECTED 11 + +static int append_bearer_properties(sd_bus_message *reply) { + int r; + + /* a{sv} of bearer properties */ + r = sd_bus_message_open_container(reply, 'a', "{sv}"); + if (r < 0) + return r; + + /* Interface */ + r = sd_bus_message_append(reply, "{sv}", "Interface", "s", arg_ifname); + if (r < 0) + return r; + + /* Connected */ + r = sd_bus_message_append(reply, "{sv}", "Connected", "b", true); + if (r < 0) + return r; + + /* Ip4Config: a{sv} */ + if (arg_ipv4_address) { + r = sd_bus_message_open_container(reply, 'e', "sv"); + if (r < 0) + return r; + r = sd_bus_message_append_basic(reply, 's', "Ip4Config"); + if (r < 0) + return r; + r = sd_bus_message_open_container(reply, 'v', "a{sv}"); + if (r < 0) + return r; + r = sd_bus_message_open_container(reply, 'a', "{sv}"); + if (r < 0) + return r; + r = sd_bus_message_append(reply, "{sv}", "method", "u", (uint32_t) MM_BEARER_IP_METHOD_STATIC); + if (r < 0) + return r; + r = sd_bus_message_append(reply, "{sv}", "address", "s", arg_ipv4_address); + if (r < 0) + return r; + r = sd_bus_message_append(reply, "{sv}", "prefix", "u", arg_ipv4_prefix); + if (r < 0) + return r; + r = sd_bus_message_append(reply, "{sv}", "gateway", "s", arg_ipv4_gateway); + if (r < 0) + return r; + r = sd_bus_message_append(reply, "{sv}", "mtu", "u", (uint32_t) 1500); + if (r < 0) + return r; + r = sd_bus_message_close_container(reply); /* a{sv} */ + if (r < 0) + return r; + r = sd_bus_message_close_container(reply); /* v */ + if (r < 0) + return r; + r = sd_bus_message_close_container(reply); /* e */ + if (r < 0) + return r; + } + + /* Ip6Config: a{sv} */ + if (arg_ipv6_address) { + r = sd_bus_message_open_container(reply, 'e', "sv"); + if (r < 0) + return r; + r = sd_bus_message_append_basic(reply, 's', "Ip6Config"); + if (r < 0) + return r; + r = sd_bus_message_open_container(reply, 'v', "a{sv}"); + if (r < 0) + return r; + r = sd_bus_message_open_container(reply, 'a', "{sv}"); + if (r < 0) + return r; + r = sd_bus_message_append(reply, "{sv}", "method", "u", (uint32_t) MM_BEARER_IP_METHOD_STATIC); + if (r < 0) + return r; + r = sd_bus_message_append(reply, "{sv}", "address", "s", arg_ipv6_address); + if (r < 0) + return r; + r = sd_bus_message_append(reply, "{sv}", "prefix", "u", arg_ipv6_prefix); + if (r < 0) + return r; + r = sd_bus_message_append(reply, "{sv}", "gateway", "s", arg_ipv6_gateway); + if (r < 0) + return r; + r = sd_bus_message_append(reply, "{sv}", "mtu", "u", (uint32_t) 1500); + if (r < 0) + return r; + r = sd_bus_message_close_container(reply); /* a{sv} */ + if (r < 0) + return r; + r = sd_bus_message_close_container(reply); /* v */ + if (r < 0) + return r; + r = sd_bus_message_close_container(reply); /* e */ + if (r < 0) + return r; + } + + /* Properties: a{sv} with apn */ + r = sd_bus_message_open_container(reply, 'e', "sv"); + if (r < 0) + return r; + r = sd_bus_message_append_basic(reply, 's', "Properties"); + if (r < 0) + return r; + r = sd_bus_message_open_container(reply, 'v', "a{sv}"); + if (r < 0) + return r; + r = sd_bus_message_open_container(reply, 'a', "{sv}"); + if (r < 0) + return r; + r = sd_bus_message_append(reply, "{sv}", "apn", "s", "internet.test"); + if (r < 0) + return r; + r = sd_bus_message_close_container(reply); /* a{sv} */ + if (r < 0) + return r; + r = sd_bus_message_close_container(reply); /* v */ + if (r < 0) + return r; + r = sd_bus_message_close_container(reply); /* e */ + if (r < 0) + return r; + + r = sd_bus_message_close_container(reply); /* outer a{sv} */ + if (r < 0) + return r; + + return 0; +} + +static int handle_get_managed_objects(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + int r; + + r = sd_bus_message_new_method_return(msg, &reply); + if (r < 0) + return r; + + /* a{oa{sa{sv}}} */ + r = sd_bus_message_open_container(reply, 'a', "{oa{sa{sv}}}"); + if (r < 0) + return r; + + /* Modem object */ + r = sd_bus_message_open_container(reply, 'e', "oa{sa{sv}}"); + if (r < 0) + return r; + r = sd_bus_message_append_basic(reply, 'o', "/org/freedesktop/ModemManager1/Modem/0"); + if (r < 0) + return r; + + /* Array of interfaces */ + r = sd_bus_message_open_container(reply, 'a', "{sa{sv}}"); + if (r < 0) + return r; + + /* org.freedesktop.ModemManager1.Modem interface */ + r = sd_bus_message_open_container(reply, 'e', "sa{sv}"); + if (r < 0) + return r; + r = sd_bus_message_append_basic(reply, 's', "org.freedesktop.ModemManager1.Modem"); + if (r < 0) + return r; + + /* Modem properties: a{sv} */ + r = sd_bus_message_open_container(reply, 'a', "{sv}"); + if (r < 0) + return r; + + /* Bearers: ao */ + r = sd_bus_message_append(reply, "{sv}", "Bearers", "ao", 1, "/org/freedesktop/ModemManager1/Bearer/0"); + if (r < 0) + return r; + + /* State: i (CONNECTED) */ + r = sd_bus_message_append(reply, "{sv}", "State", "i", (int32_t) MM_MODEM_STATE_CONNECTED); + if (r < 0) + return r; + + /* StateFailedReason: u (NONE) */ + r = sd_bus_message_append(reply, "{sv}", "StateFailedReason", "u", (uint32_t) 0); + if (r < 0) + return r; + + /* Manufacturer */ + r = sd_bus_message_append(reply, "{sv}", "Manufacturer", "s", "MockModem"); + if (r < 0) + return r; + + /* Model */ + r = sd_bus_message_append(reply, "{sv}", "Model", "s", "Virtual"); + if (r < 0) + return r; + + /* Ports: a(su) — array of structs with port name and type */ + r = sd_bus_message_open_container(reply, 'e', "sv"); + if (r < 0) + return r; + r = sd_bus_message_append_basic(reply, 's', "Ports"); + if (r < 0) + return r; + r = sd_bus_message_open_container(reply, 'v', "a(su)"); + if (r < 0) + return r; + r = sd_bus_message_open_container(reply, 'a', "(su)"); + if (r < 0) + return r; + r = sd_bus_message_append(reply, "(su)", arg_ifname, (uint32_t) MM_MODEM_PORT_TYPE_NET); + if (r < 0) + return r; + r = sd_bus_message_close_container(reply); /* a(su) */ + if (r < 0) + return r; + r = sd_bus_message_close_container(reply); /* v */ + if (r < 0) + return r; + r = sd_bus_message_close_container(reply); /* e */ + if (r < 0) + return r; + + r = sd_bus_message_close_container(reply); /* modem properties a{sv} */ + if (r < 0) + return r; + r = sd_bus_message_close_container(reply); /* e sa{sv} */ + if (r < 0) + return r; + r = sd_bus_message_close_container(reply); /* a{sa{sv}} */ + if (r < 0) + return r; + r = sd_bus_message_close_container(reply); /* e oa{sa{sv}} */ + if (r < 0) + return r; + r = sd_bus_message_close_container(reply); /* a{oa{sa{sv}}} */ + if (r < 0) + return r; + + r = sd_bus_send(NULL, reply, NULL); + if (r < 0) + return r; + + return 1; /* handled */ +} + +static int handle_get_all(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + int r; + + r = sd_bus_message_new_method_return(msg, &reply); + if (r < 0) + return r; + + /* bearer_get_all_handler() in networkd expects a leading interface name string + * before the a{sv} properties dict (it calls sd_bus_message_skip(message, "s")). */ + r = sd_bus_message_append_basic(reply, 's', "org.freedesktop.ModemManager1.Bearer"); + if (r < 0) + return r; + + r = append_bearer_properties(reply); + if (r < 0) + return r; + + r = sd_bus_send(NULL, reply, NULL); + if (r < 0) + return r; + + return 1; /* handled */ +} + +static int handle_simple_connect(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + int r; + + /* Return the bearer path */ + r = sd_bus_message_new_method_return(msg, &reply); + if (r < 0) + return r; + + r = sd_bus_message_append(reply, "o", "/org/freedesktop/ModemManager1/Bearer/0"); + if (r < 0) + return r; + + r = sd_bus_send(NULL, reply, NULL); + if (r < 0) + return r; + + return 1; /* handled */ +} + +static int filter_handler(sd_bus_message *m, void *userdata, sd_bus_error *error) { + const char *path, *interface, *member; + uint8_t type; + + if (sd_bus_message_get_type(m, &type) < 0 || type != SD_BUS_MESSAGE_METHOD_CALL) + return 0; + + path = sd_bus_message_get_path(m); + interface = sd_bus_message_get_interface(m); + member = sd_bus_message_get_member(m); + + if (!path || !interface || !member) + return 0; + + if (streq(path, "/org/freedesktop/ModemManager1") && + streq(interface, "org.freedesktop.DBus.ObjectManager") && + streq(member, "GetManagedObjects")) + return handle_get_managed_objects(m, userdata, error); + + if (startswith(path, "/org/freedesktop/ModemManager1/Bearer/") && + streq(interface, "org.freedesktop.DBus.Properties") && + streq(member, "GetAll")) + return handle_get_all(m, userdata, error); + + if (startswith(path, "/org/freedesktop/ModemManager1/Modem/") && + streq(interface, "org.freedesktop.ModemManager1.Modem.Simple") && + streq(member, "Connect")) + return handle_simple_connect(m, userdata, error); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_IFNAME = 0x100, + ARG_IPV4_ADDRESS, + ARG_IPV4_GATEWAY, + ARG_IPV4_PREFIX, + ARG_IPV6_ADDRESS, + ARG_IPV6_GATEWAY, + ARG_IPV6_PREFIX, + }; + + static const struct option options[] = { + { "ifname", required_argument, NULL, ARG_IFNAME }, + { "ipv4-address", required_argument, NULL, ARG_IPV4_ADDRESS }, + { "ipv4-gateway", required_argument, NULL, ARG_IPV4_GATEWAY }, + { "ipv4-prefix", required_argument, NULL, ARG_IPV4_PREFIX }, + { "ipv6-address", required_argument, NULL, ARG_IPV6_ADDRESS }, + { "ipv6-gateway", required_argument, NULL, ARG_IPV6_GATEWAY }, + { "ipv6-prefix", required_argument, NULL, ARG_IPV6_PREFIX }, + { "version", no_argument, NULL, 'v' }, + { "help", no_argument, NULL, 'h' }, + {} + }; + + int c, r; + + while ((c = getopt_long(argc, argv, "vh", options, NULL)) >= 0) + switch (c) { + case ARG_IFNAME: + if (free_and_strdup(&arg_ifname, optarg) < 0) + return log_oom(); + break; + case ARG_IPV4_ADDRESS: + if (free_and_strdup(&arg_ipv4_address, optarg) < 0) + return log_oom(); + break; + case ARG_IPV4_GATEWAY: + if (free_and_strdup(&arg_ipv4_gateway, optarg) < 0) + return log_oom(); + break; + case ARG_IPV4_PREFIX: + r = safe_atou32(optarg, &arg_ipv4_prefix); + if (r < 0) + return log_error_errno(r, "Failed to parse IPv4 prefix length: %m"); + break; + case ARG_IPV6_ADDRESS: + if (free_and_strdup(&arg_ipv6_address, optarg) < 0) + return log_oom(); + break; + case ARG_IPV6_GATEWAY: + if (free_and_strdup(&arg_ipv6_gateway, optarg) < 0) + return log_oom(); + break; + case ARG_IPV6_PREFIX: + r = safe_atou32(optarg, &arg_ipv6_prefix); + if (r < 0) + return log_error_errno(r, "Failed to parse IPv6 prefix length: %m"); + break; + case 'v': + return version(); + case 'h': + printf("Usage: %s [OPTIONS...]\n\n" + "Mock ModemManager D-Bus service for testing.\n\n" + " --ifname=NAME Interface name\n" + " --ipv4-address=ADDR IPv4 address\n" + " --ipv4-gateway=ADDR IPv4 gateway\n" + " --ipv4-prefix=LEN IPv4 prefix length\n" + " --ipv6-address=ADDR IPv6 address\n" + " --ipv6-gateway=ADDR IPv6 gateway\n" + " --ipv6-prefix=LEN IPv6 prefix length\n" + " -h, --help Show this help\n" + " -v, --version Show version\n", + program_invocation_short_name); + return 0; + default: + return -EINVAL; + } + + if (!arg_ifname) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--ifname is required"); + + return 1; /* work to do */ +} + +static int run(int argc, char *argv[]) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + r = sd_event_new(&event); + if (r < 0) + return log_error_errno(r, "Failed to create event loop: %m"); + + r = sd_bus_open_system(&bus); + if (r < 0) + return log_error_errno(r, "Failed to connect to system bus: %m"); + + r = sd_bus_add_filter(bus, NULL, filter_handler, NULL); + if (r < 0) + return log_error_errno(r, "Failed to add filter: %m"); + + r = sd_bus_request_name(bus, "org.freedesktop.ModemManager1", 0); + if (r < 0) + return log_error_errno(r, "Failed to acquire bus name: %m"); + + r = sd_bus_attach_event(bus, event, 0); + if (r < 0) + return log_error_errno(r, "Failed to attach bus to event loop: %m"); + + (void) sd_notify(0, "READY=1"); + + return sd_event_loop(event); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/test/integration-tests/TEST-85-NETWORK/meson.build b/test/integration-tests/TEST-85-NETWORK/meson.build index 9e8534cff02..f708f05067b 100644 --- a/test/integration-tests/TEST-85-NETWORK/meson.build +++ b/test/integration-tests/TEST-85-NETWORK/meson.build @@ -23,6 +23,7 @@ foreach testcase : [ 'NetworkdIPv6PrefixTests', 'NetworkdMTUTests', 'NetworkdSysctlTest', + 'NetworkdWWANTests', ] integration_tests += [ integration_test_template + { diff --git a/test/test-network/conf/25-wwan-ipv4v6.network b/test/test-network/conf/25-wwan-ipv4v6.network new file mode 100644 index 00000000000..bf0a857716d --- /dev/null +++ b/test/test-network/conf/25-wwan-ipv4v6.network @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=dummy98 + +[Network] +LLDP=no +LinkLocalAddressing=no +IPv6AcceptRA=no + +[MobileNetwork] +APN=internet.test +IPFamily=both diff --git a/test/test-network/conf/mock-modem-manager.conf b/test/test-network/conf/mock-modem-manager.conf new file mode 100644 index 00000000000..0a762d7a727 --- /dev/null +++ b/test/test-network/conf/mock-modem-manager.conf @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index bab725bd239..03404e6cbeb 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -73,6 +73,7 @@ resolvectl_bin = shutil.which('resolvectl', path=which_paths) timedatectl_bin = shutil.which('timedatectl', path=which_paths) udevadm_bin = shutil.which('udevadm', path=which_paths) test_ndisc_send = None +test_modem_manager_mock = None build_dir = None source_dir = None @@ -973,6 +974,28 @@ def start_radvd(*additional_options, config_file): def stop_radvd(): stop_by_pid_file(radvd_pid_file) +def start_modem_manager_mock(*additional_options): + dbus_policy_src = os.path.join(networkd_ci_temp_dir, 'mock-modem-manager.conf') + cp(dbus_policy_src, '/etc/dbus-1/system.d/mock-modem-manager.conf') + check_output('systemctl reload dbus.service') + + command = ' '.join([test_modem_manager_mock] + list(additional_options)) + with open('/run/systemd/system/test-modem-manager-mock.service', mode='w', encoding='utf-8') as f: + f.write('[Unit]\n' + 'Description=Mock ModemManager for networkd testing\n' + '[Service]\n' + 'Type=notify\n' + 'BusName=org.freedesktop.ModemManager1\n' + f'ExecStart={command}\n') + check_output('systemctl daemon-reload') + check_output('systemctl start test-modem-manager-mock.service') + +def stop_modem_manager_mock(): + call('systemctl stop test-modem-manager-mock.service') + rm_f('/run/systemd/system/test-modem-manager-mock.service') + call('systemctl daemon-reload') + rm_f('/etc/dbus-1/system.d/mock-modem-manager.conf') + def radvd_check_config(config_file): if not shutil.which('radvd'): print('radvd is not installed, assuming the config check failed') @@ -1099,6 +1122,7 @@ def tear_down_common(): stop_dnsmasq() stop_isc_dhcpd() stop_radvd() + stop_modem_manager_mock() # 2. remove modules call_quiet('rmmod netdevsim') @@ -9567,6 +9591,54 @@ class NetworkdSysctlTest(unittest.TestCase, Utilities): self.assertNotIn("changed sysctl '/proc/sys/net/ipv6/conf/dummy98/max_addresses'", log) self.assertNotIn("Sysctl monitor BPF returned error", log) +class NetworkdWWANTests(unittest.TestCase, Utilities): + + def setUp(self): + setup_common() + + def tearDown(self): + tear_down_common() + + def test_wwan_ipv4v6_static(self): + """Test WWAN bearer with both IPv4 and IPv6 static configuration. + + Regression test for https://github.com/systemd/systemd/issues/41389 + """ + if not os.path.exists(test_modem_manager_mock): + self.skipTest(f'{test_modem_manager_mock} does not exist.') + + copy_network_unit('12-dummy.netdev', '25-wwan-ipv4v6.network') + try: + start_modem_manager_mock( + '--ifname', 'dummy98', + '--ipv4-address', '100.120.244.160', + '--ipv4-gateway', '100.120.244.161', + '--ipv4-prefix', '26', + '--ipv6-address', '2001:db8::1', + '--ipv6-gateway', '2001:db8::2', + '--ipv6-prefix', '64', + ) + except (subprocess.CalledProcessError, PermissionError, OSError) as e: + self.skipTest(f'Failed to start mock ModemManager: {e}') + start_networkd() + self.wait_online('dummy98:routable') + + output = check_output('ip -4 address show dev dummy98') + print(output) + self.assertIn('100.120.244.160/26', output) + + output = check_output('ip -6 address show dev dummy98') + print(output) + self.assertIn('2001:db8::1/64', output) + + output = check_output('ip -4 route show dev dummy98') + print(output) + self.assertIn('default via 100.120.244.161', output) + + output = check_output('ip -6 route show dev dummy98') + print(output) + self.assertIn('default via 2001:db8::2', output) + if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--build-dir', help='Path to build dir', dest='build_dir') @@ -9626,6 +9698,11 @@ if __name__ == '__main__': else: test_ndisc_send = '/usr/lib/tests/test-ndisc-send' + if build_dir: + test_modem_manager_mock = os.path.normpath(os.path.join(build_dir, 'test-modem-manager-mock')) + else: + test_modem_manager_mock = '/usr/lib/systemd/tests/unit-tests/manual/test-modem-manager-mock' + if asan_options: env.update({'ASAN_OPTIONS': asan_options}) if lsan_options: