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:
@@ -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'),
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user