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
This commit is contained in:
Luca Boccassi
2026-03-28 14:40:11 +00:00
parent e0ab84e21b
commit abe3d570f8
6 changed files with 595 additions and 0 deletions

View File

@@ -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'),
},

View File

@@ -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 <getopt.h>
#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);

View File

@@ -23,6 +23,7 @@ foreach testcase : [
'NetworkdIPv6PrefixTests',
'NetworkdMTUTests',
'NetworkdSysctlTest',
'NetworkdWWANTests',
]
integration_tests += [
integration_test_template + {

View File

@@ -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

View File

@@ -0,0 +1,13 @@
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<!-- Allow root to own and interact with the mock ModemManager service -->
<policy user="root">
<allow own="org.freedesktop.ModemManager1"/>
<allow send_destination="org.freedesktop.ModemManager1"/>
</policy>
<policy context="default">
<allow send_destination="org.freedesktop.ModemManager1"/>
</policy>
</busconfig>

View File

@@ -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: