diff --git a/man/systemd.network.xml b/man/systemd.network.xml
index b00c2c6795..3b08a292e0 100644
--- a/man/systemd.network.xml
+++ b/man/systemd.network.xml
@@ -6444,6 +6444,117 @@ ServerAddress=192.168.0.1/24
+
+ [ModemManager] Section Options
+
+ This section configures the default setting of the ModemManager integration. See
+ for more information about ModemManager.
+
+ Regardless of the [ModemManager] section settings consider using the following for LTE modems (take into account
+ that LTE modems do not typically support LLDP because LLDP is a Layer 2 protocol for Ethernet networks and an LTE
+ modem connects to a cellular network, not a local Ethernet LAN):
+ [Network]
+LLDP=no
+LinkLocalAddressing=no
+IPv6AcceptRA=no
+
+
+
+ The following options are available in the [ModemManager] section:
+
+
+ SimpleConnectProperties=
+
+ Specifies the white-space separated list of simple connect properties used to connect a modem. See
+ for more
+ information about simple connect. If no properties provided then the connection is not initiated.
+
+
+ =NAME
+ An Access Point Name (APN) is the name of a gateway between a mobile network
+ (GSM, GPRS, 3G, 4G and 5G) and another computer network. Required in 3GPP.
+
+
+
+
+
+ =METHOD
+ Authentication method to use. Takes one of "none", "pap", "chap", "mschap", "mschapv2" or "eap".
+ Optional in 3GPP.
+
+
+
+
+
+ =NAME
+ User name (if any) required by the network. Optional in 3GPP.
+
+
+
+
+
+ =PASSWORD
+ Password (if any) required by the network. Optional in 3GPP.
+
+
+
+
+
+ =TYPE
+ Addressing type. Takes one of "none", "ipv4", "ipv6", "ipv4v6" or "any".
+ Optional in 3GPP and CDMA.
+
+
+
+
+
+ =BOOL
+ A boolean. When true, connection is allowed during roaming. When false,
+ connection is not allowed during roaming. Optional in 3GPP.
+
+
+
+
+
+ =PIN
+ SIM-PIN unlock code.
+
+
+
+
+
+ =ID
+ ETSI MCC-MNC of a network to force registration.
+
+
+
+
+
+
+
+
+ RouteMetric=
+
+ Set the routing metric for routes specified by the mobile network (including the prefix route
+ added for the specified prefix). Takes an unsigned integer in the range 0…4294967295.
+ When unset or set to 0, the kernel's default value will be used.
+
+
+
+
+
+
+ UseGateway=
+
+ When true (the default), the router address will be configured as the default gateway.
+
+
+
+
+
+
+
+
Examples
@@ -6771,6 +6882,29 @@ Xfrm=xfrm0
This allows hardware based ipsec offloading to the eth0 nic.
If offloading is not needed, xfrm interfaces can be assigned to the lo device.
+
+
+
+
+ Connecting to a cellular network with ModemManager
+ # /etc/systemd/network/27-wwan0.network
+[Match]
+Name=wwan0
+
+[Network]
+LLDP=no
+LinkLocalAddressing=no
+IPv6AcceptRA=no
+
+[ModemManager]
+SimpleConnectProperties=apn=internet pin=1111
+RouteMetric=30
+UseGateway=yes
+
+ This connects a cellular modem to a broadband network matched with the network interface wwan0,
+ with APN name internet, SIM card pin unlock code 1111 and sets up a default
+ gateway with route metric of 30.
+
diff --git a/src/network/meson.build b/src/network/meson.build
index d872510b24..4457e83644 100644
--- a/src/network/meson.build
+++ b/src/network/meson.build
@@ -89,6 +89,8 @@ systemd_networkd_extract_sources = files(
'networkd-util.c',
'networkd-wifi.c',
'networkd-wiphy.c',
+ 'networkd-wwan.c',
+ 'networkd-wwan-bus.c',
'tc/cake.c',
'tc/codel.c',
'tc/drr.c',
diff --git a/src/network/networkd-dhcp4.c b/src/network/networkd-dhcp4.c
index 102047a4e4..f274a0c4d9 100644
--- a/src/network/networkd-dhcp4.c
+++ b/src/network/networkd-dhcp4.c
@@ -29,6 +29,7 @@
#include "networkd-route.h"
#include "networkd-setlink.h"
#include "networkd-state-file.h"
+#include "networkd-wwan.h"
#include "parse-util.h"
#include "set.h"
#include "socket-util.h"
@@ -1770,6 +1771,9 @@ int dhcp4_start_full(Link *link, bool set_ipv6_connectivity) {
if (!link->dhcp_client)
return 0;
+ if (link_dhcp_enabled_by_bearer(link, AF_INET) == 0)
+ return 0;
+
if (!link_has_carrier(link))
return 0;
diff --git a/src/network/networkd-dhcp6.c b/src/network/networkd-dhcp6.c
index 5da83d5aeb..26ab7ceb52 100644
--- a/src/network/networkd-dhcp6.c
+++ b/src/network/networkd-dhcp6.c
@@ -24,6 +24,7 @@
#include "networkd-queue.h"
#include "networkd-route.h"
#include "networkd-state-file.h"
+#include "networkd-wwan.h"
#include "set.h"
#include "string-table.h"
#include "string-util.h"
@@ -494,6 +495,9 @@ int dhcp6_start(Link *link) {
if (!link_dhcp6_enabled(link))
return 0;
+ if (link_dhcp_enabled_by_bearer(link, AF_INET6) == 0)
+ return 0;
+
if (!link_has_carrier(link))
return 0;
diff --git a/src/network/networkd-json.c b/src/network/networkd-json.c
index 584f07249d..123594179c 100644
--- a/src/network/networkd-json.c
+++ b/src/network/networkd-json.c
@@ -26,6 +26,7 @@
#include "networkd-route.h"
#include "networkd-route-util.h"
#include "networkd-routing-policy-rule.h"
+#include "networkd-wwan.h"
#include "ordered-set.h"
#include "set.h"
#include "string-util.h"
@@ -541,6 +542,15 @@ static int dns_append_json(Link *link, sd_json_variant **v) {
return r;
}
+ Bearer *b;
+
+ if (link_get_bearer(link, &b) >= 0)
+ FOREACH_ARRAY(dns, b->dns, b->n_dns) {
+ r = dns_append_json_one(link, *dns, NETWORK_CONFIG_SOURCE_MODEM_MANAGER, NULL, &array);
+ if (r < 0)
+ return r;
+ }
+
if (link->dhcp_lease && link_get_use_dns(link, NETWORK_CONFIG_SOURCE_DHCP4)) {
const struct in_addr *dns;
union in_addr_union s;
diff --git a/src/network/networkd-link.c b/src/network/networkd-link.c
index 2a19a6003b..cb79ddb9eb 100644
--- a/src/network/networkd-link.c
+++ b/src/network/networkd-link.c
@@ -63,6 +63,7 @@
#include "networkd-state-file.h"
#include "networkd-sysctl.h"
#include "networkd-wifi.h"
+#include "networkd-wwan-bus.h"
#include "ordered-set.h"
#include "parse-util.h"
#include "set.h"
@@ -529,6 +530,9 @@ void link_check_ready(Link *link) {
if (!link->sr_iov_configured)
return (void) log_link_debug(link, "%s(): SR-IOV is not configured.", __func__);
+ if (!link->bearer_configured)
+ return (void) log_link_debug(link, "%s(): Bearer has not been applied.", __func__);
+
/* IPv6LL is assigned after the link gains its carrier. */
if (!link->network->configure_without_carrier &&
link_ipv6ll_enabled(link) &&
@@ -538,7 +542,7 @@ void link_check_ready(Link *link) {
/* All static addresses must be ready. */
bool has_static_address = false;
SET_FOREACH(a, link->addresses) {
- if (a->source != NETWORK_CONFIG_SOURCE_STATIC)
+ if (!IN_SET(a->source, NETWORK_CONFIG_SOURCE_STATIC, NETWORK_CONFIG_SOURCE_MODEM_MANAGER))
continue;
if (!address_is_ready(a))
return (void) log_link_debug(link, "%s(): static address %s is not ready.", __func__,
@@ -1293,6 +1297,10 @@ static int link_configure(Link *link) {
if (r < 0)
return r;
+ r = link_modem_reconfigure(link);
+ if (r < 0)
+ return r;
+
if (!link_has_carrier(link))
return 0;
diff --git a/src/network/networkd-link.h b/src/network/networkd-link.h
index 845b6f211f..c5b9421bc0 100644
--- a/src/network/networkd-link.h
+++ b/src/network/networkd-link.h
@@ -108,6 +108,7 @@ typedef struct Link {
unsigned set_link_messages;
unsigned set_flags_messages;
unsigned create_stacked_netdev_messages;
+ unsigned bearer_messages;
Set *addresses;
Set *neighbors;
@@ -141,6 +142,7 @@ typedef struct Link {
bool master_set:1;
bool stacked_netdevs_created:1;
bool bridge_vlan_set:1;
+ bool bearer_configured:1;
sd_dhcp_server *dhcp_server;
diff --git a/src/network/networkd-manager.c b/src/network/networkd-manager.c
index e3c794b82b..dbda6311d9 100644
--- a/src/network/networkd-manager.c
+++ b/src/network/networkd-manager.c
@@ -46,6 +46,7 @@
#include "networkd-state-file.h"
#include "networkd-wifi.h"
#include "networkd-wiphy.h"
+#include "networkd-wwan-bus.h"
#include "ordered-set.h"
#include "qdisc.h"
#include "set.h"
@@ -96,6 +97,8 @@ static int on_connected(sd_bus_message *message, void *userdata, sd_bus_error *r
if (m->product_uuid_requested)
(void) manager_request_product_uuid(m);
+ (void) manager_notify_mm_bus_connected(m);
+
return 0;
}
@@ -145,6 +148,8 @@ static int manager_connect_bus(Manager *m) {
if (r < 0)
log_warning_errno(r, "Failed to request match for PrepareForSleep, ignoring: %m");
+ (void) manager_match_mm_signals(m);
+
return 0;
}
@@ -733,6 +738,9 @@ Manager* manager_free(Manager *m) {
set_free(m->rules);
+ sd_bus_slot_unref(m->slot_mm);
+ hashmap_free(m->modems_by_path);
+
sd_netlink_unref(m->rtnl);
sd_netlink_unref(m->genl);
sd_netlink_unref(m->nfnl);
diff --git a/src/network/networkd-manager.h b/src/network/networkd-manager.h
index 4017a92abd..13467d963e 100644
--- a/src/network/networkd-manager.h
+++ b/src/network/networkd-manager.h
@@ -99,6 +99,10 @@ typedef struct Manager {
Hashmap *wiphy_by_index;
Hashmap *wiphy_by_name;
+ /* ModemManager support */
+ sd_bus_slot *slot_mm;
+ Hashmap *modems_by_path;
+
/* For link speed meter */
bool use_speed_meter;
sd_event_source *speed_meter_event_source;
diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf
index 7f3f4c5311..10566b7a4e 100644
--- a/src/network/networkd-network-gperf.gperf
+++ b/src/network/networkd-network-gperf.gperf
@@ -40,6 +40,7 @@ _Pragma("GCC diagnostic ignored \"-Wzero-as-null-pointer-constant\"")
#include "networkd-radv.h"
#include "networkd-route.h"
#include "networkd-routing-policy-rule.h"
+#include "networkd-wwan.h"
#include "qdisc.h"
#include "socket-util.h"
#include "tclass.h"
@@ -492,6 +493,9 @@ CAN.ClassicDataLengthCode, config_parse_can_control_mode,
CAN.Termination, config_parse_can_termination, 0, 0
IPoIB.Mode, config_parse_ipoib_mode, 0, offsetof(Network, ipoib_mode)
IPoIB.IgnoreUserspaceMulticastGroups, config_parse_tristate, 0, offsetof(Network, ipoib_umcast)
+ModemManager.SimpleConnectProperties, config_parse_strv, 0, offsetof(Network, mm_simple_connect_props)
+ModemManager.RouteMetric, config_parse_mm_route_metric, 0, 0
+ModemManager.UseGateway, config_parse_tristate, 0, offsetof(Network, mm_use_gateway)
QDisc.Parent, config_parse_qdisc_parent, _QDISC_KIND_INVALID, 0
QDisc.Handle, config_parse_qdisc_handle, _QDISC_KIND_INVALID, 0
BFIFO.Parent, config_parse_qdisc_parent, QDISC_KIND_BFIFO, 0
diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c
index 45456d677d..8141a45432 100644
--- a/src/network/networkd-network.c
+++ b/src/network/networkd-network.c
@@ -512,6 +512,8 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi
.ipoib_mode = _IP_OVER_INFINIBAND_MODE_INVALID,
.ipoib_umcast = -1,
+
+ .mm_use_gateway = -1,
};
r = config_parse_many_full(
@@ -551,6 +553,7 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi
"LLDP\0"
"TrafficControlQueueingDiscipline\0"
"CAN\0"
+ "ModemManager\0"
"QDisc\0"
"BFIFO\0"
"CAKE\0"
@@ -847,6 +850,9 @@ static Network *network_free(Network *network) {
hashmap_free(network->qdiscs_by_section);
hashmap_free(network->tclasses_by_section);
+ /* ModemManager */
+ strv_free(network->mm_simple_connect_props);
+
return mfree(network);
}
diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h
index b6534c9510..dcd9f68e78 100644
--- a/src/network/networkd-network.h
+++ b/src/network/networkd-network.h
@@ -414,6 +414,12 @@ typedef struct Network {
/* NTP */
char **ntp;
+
+ /* ModemManager support */
+ char **mm_simple_connect_props;
+ int mm_use_gateway;
+ uint32_t mm_route_metric;
+ bool mm_route_metric_set;
} Network;
DECLARE_TRIVIAL_REF_UNREF_FUNC(Network, network);
diff --git a/src/network/networkd-state-file.c b/src/network/networkd-state-file.c
index c57fc8462d..5e485f9895 100644
--- a/src/network/networkd-state-file.c
+++ b/src/network/networkd-state-file.c
@@ -23,6 +23,7 @@
#include "networkd-network.h"
#include "networkd-ntp.h"
#include "networkd-state-file.h"
+#include "networkd-wwan.h"
#include "ordered-set.h"
#include "set.h"
#include "string-util.h"
@@ -109,6 +110,14 @@ static int link_put_dns(Link *link, OrderedSet **s) {
if (r < 0)
return r;
+ Bearer *b;
+
+ if (link_get_bearer(link, &b) >= 0) {
+ r = ordered_set_put_dns_servers(s, link->ifindex, b->dns, b->n_dns);
+ if (r < 0)
+ return r;
+ }
+
if (link->dhcp_lease && link_get_use_dns(link, NETWORK_CONFIG_SOURCE_DHCP4)) {
const struct in_addr *addresses;
@@ -801,6 +810,11 @@ static int link_save(Link *link) {
space = false;
link_save_dns(link, f, link->network->dns, link->network->n_dns, &space);
+ Bearer *b;
+
+ if (link_get_bearer(link, &b) >= 0)
+ link_save_dns(link, f, b->dns, b->n_dns, &space);
+
/* DNR resolvers are not required to provide Do53 service, however resolved doesn't
* know how to handle such a server so for now Do53 service is required, and
* assumed. */
diff --git a/src/network/networkd-util.c b/src/network/networkd-util.c
index 4e1a12d8d3..b818b6121d 100644
--- a/src/network/networkd-util.c
+++ b/src/network/networkd-util.c
@@ -17,14 +17,15 @@
/* This is used in log messages, and never used in parsing settings. So, upper cases are OK. */
static const char * const network_config_source_table[_NETWORK_CONFIG_SOURCE_MAX] = {
- [NETWORK_CONFIG_SOURCE_FOREIGN] = "foreign",
- [NETWORK_CONFIG_SOURCE_STATIC] = "static",
- [NETWORK_CONFIG_SOURCE_IPV4LL] = "IPv4LL",
- [NETWORK_CONFIG_SOURCE_DHCP4] = "DHCPv4",
- [NETWORK_CONFIG_SOURCE_DHCP6] = "DHCPv6",
- [NETWORK_CONFIG_SOURCE_DHCP_PD] = "DHCP-PD",
- [NETWORK_CONFIG_SOURCE_NDISC] = "NDisc",
- [NETWORK_CONFIG_SOURCE_RUNTIME] = "runtime",
+ [NETWORK_CONFIG_SOURCE_FOREIGN] = "foreign",
+ [NETWORK_CONFIG_SOURCE_STATIC] = "static",
+ [NETWORK_CONFIG_SOURCE_IPV4LL] = "IPv4LL",
+ [NETWORK_CONFIG_SOURCE_DHCP4] = "DHCPv4",
+ [NETWORK_CONFIG_SOURCE_DHCP6] = "DHCPv6",
+ [NETWORK_CONFIG_SOURCE_DHCP_PD] = "DHCP-PD",
+ [NETWORK_CONFIG_SOURCE_NDISC] = "NDisc",
+ [NETWORK_CONFIG_SOURCE_RUNTIME] = "runtime",
+ [NETWORK_CONFIG_SOURCE_MODEM_MANAGER] = "ModemManager",
};
DEFINE_STRING_TABLE_LOOKUP(network_config_source, NetworkConfigSource);
diff --git a/src/network/networkd-util.h b/src/network/networkd-util.h
index 16bfb59902..e7da47d7b2 100644
--- a/src/network/networkd-util.h
+++ b/src/network/networkd-util.h
@@ -14,6 +14,7 @@ typedef enum NetworkConfigSource {
NETWORK_CONFIG_SOURCE_DHCP_PD,
NETWORK_CONFIG_SOURCE_NDISC,
NETWORK_CONFIG_SOURCE_RUNTIME, /* through D-Bus method */
+ NETWORK_CONFIG_SOURCE_MODEM_MANAGER,
_NETWORK_CONFIG_SOURCE_MAX,
_NETWORK_CONFIG_SOURCE_INVALID = -EINVAL,
} NetworkConfigSource;
diff --git a/src/network/networkd-wwan-bus.c b/src/network/networkd-wwan-bus.c
new file mode 100644
index 0000000000..e575272696
--- /dev/null
+++ b/src/network/networkd-wwan-bus.c
@@ -0,0 +1,1341 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+/*
+ * 1. ModemManager (MM) integration consists of two big parts: things
+ * we do on the networkd start and what we do during the run-time.
+ *
+ * 2. Initialization phase
+ * 2.1. Wait for networkd to connect to D-Bus
+ * 2.2. Setup D-Bus handlers for the essential signals:
+ * - /org/freedesktop/DBus, org.freedesktop.DBus, NameOwnerChanged - to track MM service availability
+ * - /org/freedesktop/ModemManager1, org.freedesktop.DBus.ObjectManager Interfaces{Added|Removed} -
+ * to track modem plug/unplug
+ * - /org/freedesktop/ModemManager1/Bearer org.freedesktop.DBus.Properties PropertiesChanged
+ * to track bearers
+ * 2.3. Check if MM service is yet available: for that call /org/freedesktop/DBus, org.freedesktop.DBus
+ * ListNames method and see if MM is available. If it is not, then wait for the NameOwnerChanged
+ * signal and see when it is; finish initialization phase.
+ * 2.4. If MM is available - enumerate modems, see p.4.
+ * 2.5. Finish initialization phase.
+ *
+ * 3. Run-time
+ * 3.1. During the run-time we track MM service availability. When it is gone we remove all the modems
+ * and bearers.
+ * 3.2. When MM is connected we do modem enumeration to get in sync with their current state.
+ * 3.3. If a modem was removed we also remove all its bearers.
+ * 3.4. If a modem was added we try to start a simple connect.
+ * 3.5. If connection was interrupted, e.g. modem changed its network connection from connected state
+ * we start an automatic reconnect.
+ *
+ * 4. Modem enumeration
+ * 4.1. Modem enumeration is done by calling GetManagedObjects.
+ * 4.2. By receiving managed objects we try to instantiate all new modems found.
+ * 4.3. For that we inspect all bearers available for that modem and add all new bearers found.
+ * 4.4. We also read modem ports to detect WWAN interface name assigned to this modem, e.g. "wwan0" etc.
+ * N.B. As we only get the interface name known that late and the corresponding .network file was
+ * already used by the networkd to match interfaces etc. it is not possible
+ * to do things like matching APN to .network and so on.
+ *
+ * 5. Simple (re)connect
+ * 5.1. Connection is done by calling org.freedesktop.ModemManager1.Modem.Simple Connect method for
+ * the relevant modem.
+ * 5.2. It is possible that at the time of connect the operation may fail. For that reason and to ensure
+ * we are always connected we employ a periodic timer which will re-try connection hoping it will
+ * be successful this time or when modem has recovered after an error state and so on.
+ * 5.3. networkd will automatically start reconnection if any external entity disconnects modem from
+ * the network.
+ */
+
+#include "af-list.h"
+#include "alloc-util.h"
+#include "bus-error.h"
+#include "bus-internal.h"
+#include "bus-map-properties.h"
+#include "bus-message.h"
+#include "bus-util.h"
+#include "event-util.h"
+#include "hashmap.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-wwan.h"
+#include "networkd-wwan-bus.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "strv.h"
+
+#define RECONNECT_TIMEOUT_USEC (30 * USEC_PER_SEC)
+
+static const char * const modem_state_failed_reason_str[__MM_MODEM_STATE_FAILED_REASON_MAX] = {
+ [MM_MODEM_STATE_FAILED_REASON_NONE] = "No error",
+ [MM_MODEM_STATE_FAILED_REASON_UNKNOWN] = "Unknown error",
+ [MM_MODEM_STATE_FAILED_REASON_SIM_MISSING] = "SIM is required, but missing",
+ [MM_MODEM_STATE_FAILED_REASON_SIM_ERROR] = "SIM is available, but unusable",
+ [MM_MODEM_STATE_FAILED_REASON_UNKNOWN_CAPABILITIES] = "Unknown modem capabilities",
+ [MM_MODEM_STATE_FAILED_REASON_ESIM_WITHOUT_PROFILES] = "eSIM is not initialized",
+};
+
+static int map_name(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata) {
+
+ Bearer *b = ASSERT_PTR(userdata);
+ const char *s;
+ int r;
+
+ assert(m);
+
+ /*
+ * If name is already set - do not wipe it on disconnect, so we can work with link and other code which
+ * relies on the interface name.
+ */
+ r = sd_bus_message_read_basic(m, 's', &s);
+ if (r < 0)
+ return r;
+
+ if (!isempty(b->name))
+ return 0;
+
+ return bearer_set_name(b, s);
+}
+
+static int map_dns(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata) {
+
+ Bearer *b = ASSERT_PTR(userdata);
+ struct in_addr_full *a;
+ const char *s;
+ int r;
+
+ assert(m);
+
+ r = sd_bus_message_read_basic(m, 's', &s);
+ if (r < 0)
+ return r;
+
+ r = in_addr_full_new_from_string(s, &a);
+ if (r < 0)
+ return r;
+
+ if (!GREEDY_REALLOC(b->dns, b->n_dns + 1))
+ return -ENOMEM;
+
+ b->dns[b->n_dns++] = TAKE_PTR(a);
+
+ return 0;
+}
+
+static int map_in_addr(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata,
+ int family) {
+
+ union in_addr_union *addr = ASSERT_PTR(userdata);
+ const char *s;
+ int r;
+
+ assert(m);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+
+ r = sd_bus_message_read_basic(m, 's', &s);
+ if (r < 0)
+ return r;
+
+ return in_addr_from_string(family, s, addr);
+}
+
+static int map_in4(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata) {
+
+ return map_in_addr(bus, member, m, error, userdata, AF_INET);
+}
+
+static int map_in6(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata) {
+
+ return map_in_addr(bus, member, m, error, userdata, AF_INET6);
+}
+
+static int map_prefixlen(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata,
+ int family) {
+
+ unsigned *prefixlen = ASSERT_PTR(userdata);
+ unsigned p;
+ int r;
+
+ assert(m);
+
+ r = sd_bus_message_read_basic(m, 'u', &p);
+ if (r < 0)
+ return r;
+
+ if (p > FAMILY_ADDRESS_SIZE(family) * 8)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Bearer has invalid prefix length %u for %s address, ignoring.",
+ p, af_to_ipv4_ipv6(family));
+
+ *prefixlen = p;
+
+ return 0;
+}
+
+static int map_prefixlen4(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata) {
+
+ return map_prefixlen(bus, member, m, error, userdata, AF_INET);
+}
+
+static int map_prefixlen6(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata) {
+
+ return map_prefixlen(bus, member, m, error, userdata, AF_INET6);
+}
+
+static int map_ip4_config(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata) {
+
+ static const struct bus_properties_map map[] = {
+ { "method", "u", NULL, offsetof(Bearer, ip4_method) },
+ { "address", "s", map_in4, offsetof(Bearer, ip4_address) },
+ { "prefix", "u", map_prefixlen4, offsetof(Bearer, ip4_prefixlen) },
+ { "dns1", "s", map_dns, 0, },
+ { "dns2", "s", map_dns, 0, },
+ { "dns3", "s", map_dns, 0, },
+ { "gateway", "s", map_in4, offsetof(Bearer, ip4_gateway) },
+ { "mtu", "u", NULL, offsetof(Bearer, ip4_mtu) },
+ {}
+ };
+ Bearer *b = ASSERT_PTR(userdata);
+
+ /*
+ * The "Ip4Config" property: if the bearer was configured for IPv4 addressing, upon activation this
+ * property contains the addressing details for assignment to the data interface.
+ * We may have both IPv4 and IPv6 configured.
+ */
+ b->ip_type |= ADDRESS_FAMILY_IPV4;
+
+ return bus_message_map_all_properties(m, map, /* flags= */ 0, error, userdata);
+}
+
+static int map_ip6_config(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata) {
+
+ static const struct bus_properties_map map[] = {
+ { "method", "u", NULL, offsetof(Bearer, ip6_method) },
+ { "address", "s", map_in6, offsetof(Bearer, ip6_address) },
+ { "prefix", "u", map_prefixlen6, offsetof(Bearer, ip6_prefixlen) },
+ { "dns1", "s", map_dns, 0, },
+ { "dns2", "s", map_dns, 0, },
+ { "dns3", "s", map_dns, 0, },
+ { "gateway", "s", map_in6, offsetof(Bearer, ip6_gateway) },
+ { "mtu", "u", NULL, offsetof(Bearer, ip6_mtu) },
+ {}
+ };
+ Bearer *b = ASSERT_PTR(userdata);
+
+ /*
+ * The "Ip6Config" property: if the bearer was configured for IPv6 addressing, upon activation this
+ * property contains the addressing details for assignment to the data interface.
+ * We may have both IPv4 and IPv6 configured.
+ */
+ b->ip_type |= ADDRESS_FAMILY_IPV6;
+
+ return bus_message_map_all_properties(m, map, /* flags= */ 0, error, userdata);
+}
+
+static int map_properties(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata) {
+
+ static const struct bus_properties_map map[] = {
+ { "apn", "s", NULL, offsetof(Bearer, apn) },
+ {}
+ };
+
+ return bus_message_map_all_properties(m, map, BUS_MAP_STRDUP, error, userdata);
+}
+
+static int bus_message_contains_properties(
+ sd_bus_message *m,
+ const struct bus_properties_map *map,
+ sd_bus_error *error) {
+
+ unsigned found_cnt;
+ int r;
+
+ assert(m);
+ assert(map);
+
+ found_cnt = 0;
+
+ r = sd_bus_message_enter_container(m, SD_BUS_TYPE_ARRAY, "{sv}");
+ if (r < 0)
+ return bus_log_parse_error_debug(r);
+
+ while ((r = sd_bus_message_enter_container(m, SD_BUS_TYPE_DICT_ENTRY, "sv")) > 0) {
+ const struct bus_properties_map *prop = NULL;
+ const char *member;
+
+ r = sd_bus_message_read_basic(m, SD_BUS_TYPE_STRING, &member);
+ if (r < 0)
+ return bus_log_parse_error_debug(r);
+
+ for (unsigned i = 0; map[i].member; i++)
+ if (streq(map[i].member, member)) {
+ prop = &map[i];
+ break;
+ }
+
+ r = sd_bus_message_skip(m, "v");
+ if (r < 0)
+ return bus_log_parse_error_debug(r);
+ if (prop)
+ found_cnt++;
+
+ r = sd_bus_message_exit_container(m);
+ if (r < 0)
+ return bus_log_parse_error_debug(r);
+ }
+ if (r < 0)
+ return bus_log_parse_error_debug(r);
+
+ r = sd_bus_message_exit_container(m);
+ if (r < 0)
+ return bus_log_parse_error_debug(r);
+
+ return found_cnt;
+}
+
+static int bearer_get_all_handler(sd_bus_message *message, void *userdata, sd_bus_error *ret_error) {
+ static const struct bus_properties_map map[] = {
+ { "Interface", "s", map_name, 0, },
+ { "Connected", "b", NULL, offsetof(Bearer, connected) },
+ { "Ip4Config", "a{sv}", map_ip4_config, 0, },
+ { "Ip6Config", "a{sv}", map_ip6_config, 0, },
+ { "Properties", "a{sv}", map_properties, 0, },
+ {}
+ };
+
+ Bearer *b = ASSERT_PTR(userdata);
+ const sd_bus_error *e;
+ int r;
+
+ assert(message);
+
+ b->slot_getall = sd_bus_slot_unref(b->slot_getall);
+
+ e = sd_bus_message_get_error(message);
+ if (e) {
+ bool removed = false;
+
+ if (sd_bus_error_has_name(e, SD_BUS_ERROR_UNKNOWN_METHOD))
+ /* The path is already removed? */
+ removed = true;
+
+ r = sd_bus_error_get_errno(e);
+ log_full_errno(removed ? LOG_DEBUG : LOG_WARNING, r,
+ "Could not get properties of bearer \"%s\": %s",
+ b->path, bus_error_message(e, r));
+
+ bearer_drop(b);
+ return 0;
+ }
+
+ /* skip name: string "org.freedesktop.ModemManager1.Bearer" */
+ r = sd_bus_message_skip(message, "s");
+ if (r < 0)
+ return log_warning_errno(r, "Failed while parsing properties of bearer \"%s\": %s",
+ b->path, bus_error_message(ret_error, r));
+
+ r = bus_message_contains_properties(message, map, ret_error);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to check properties of bearer \"%s\": %s",
+ b->path, bus_error_message(ret_error, r));
+
+ /*
+ * We do not want to update link status on properties change which come more or less frequently
+ * and do not involve link state change, e.g. we do not want to bearer_update_link on Rx/Tx counters
+ * change. So, see if this callback was called with the changes we want to track.
+ */
+ if (r == 0)
+ return 0;
+
+ r = sd_bus_message_rewind(message, true);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to rewind properties of bearer \"%s\"", b->path);
+ /* skip name: string "org.freedesktop.ModemManager1.Bearer" */
+ r = sd_bus_message_skip(message, "s");
+ if (r < 0)
+ return log_warning_errno(r, "Failed while parsing properties of bearer \"%s\": %s",
+ b->path, bus_error_message(ret_error, r));
+
+ r = bus_message_map_all_properties(message, map, BUS_MAP_BOOLEAN_AS_BOOL, ret_error, b);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to parse properties of bearer \"%s\": %s",
+ b->path, bus_error_message(ret_error, r));
+
+ if (b->name)
+ log_info("%s: ModemManager announces %s %s is%s connected.",
+ b->name, b->modem->manufacturer, b->modem->model,
+ b->connected ? "" : " not");
+
+ if (b->connected)
+ b->modem->reconnect_state = MODEM_RECONNECT_DONE;
+
+ return bearer_update_link(b);
+}
+
+static int bearer_initialize(Bearer *b) {
+ int r;
+
+ assert(b);
+ assert(b->modem);
+ assert(b->modem->manager);
+ assert(sd_bus_is_ready(b->modem->manager->bus) > 0);
+ assert(b->path);
+
+ b->slot_getall = sd_bus_slot_unref(b->slot_getall);
+
+ r = sd_bus_call_method_async(
+ b->modem->manager->bus,
+ &b->slot_getall,
+ "org.freedesktop.ModemManager1",
+ b->path,
+ "org.freedesktop.DBus.Properties",
+ "GetAll",
+ bearer_get_all_handler,
+ b, "s", "org.freedesktop.ModemManager1.Bearer");
+ if (r < 0)
+ return log_warning_errno(r, "Could not get properties of bearer \"%s\": %m", b->path);
+
+ return 0;
+}
+
+static int bearer_new_and_initialize(Modem *modem, const char *path) {
+ _cleanup_(bearer_freep) Bearer *b = NULL;
+ int r;
+
+ assert(modem);
+ assert(modem->manager);
+ assert(path);
+
+ r = bearer_new(modem, path, &b);
+ if (r < 0) {
+ if (r == -EEXIST)
+ return 0;
+ return log_warning_errno(r, "Failed to allocate new bearer \"%s\": %m", path);
+ }
+
+ r = bearer_initialize(b);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(b);
+ return 0;
+}
+
+static int modem_connect_handler(sd_bus_message *message, void *userdata, sd_bus_error *ret_error) {
+ Modem *modem = ASSERT_PTR(userdata);
+ const sd_bus_error *e;
+ const char *new_bearer;
+ int r;
+
+ assert(message);
+
+ modem->slot_connect = sd_bus_slot_unref(modem->slot_connect);
+
+ e = sd_bus_message_get_error(message);
+ if (e) {
+ r = sd_bus_error_get_errno(e);
+ log_full_errno(LOG_ERR, r,
+ "Could not connect modem %s %s: %s",
+ modem->manufacturer, modem->model,
+ bus_error_message(e, r));
+
+ modem->reconnect_state = MODEM_RECONNECT_WAITING;
+ return 0;
+ }
+
+ sd_bus_message_read(message, "o", &new_bearer);
+ log_debug("ModemManager: %s %s connected, bearer is at %s",
+ modem->manufacturer, modem->model, new_bearer);
+
+ return 0;
+}
+
+static MMBearerIpFamily prop_iptype_lookup(const char *key) {
+ static const struct {
+ MMBearerIpFamily family;
+ const char *str;
+ } table[] = {
+ { MM_BEARER_IP_FAMILY_NONE, "none" },
+ { MM_BEARER_IP_FAMILY_IPV4, "ipv4" },
+ { MM_BEARER_IP_FAMILY_IPV6, "ipv6" },
+ { MM_BEARER_IP_FAMILY_IPV4V6, "ipv4v6" },
+ { MM_BEARER_IP_FAMILY_ANY, "any" },
+ {}
+ };
+
+ assert(key);
+
+ FOREACH_ELEMENT(item, table)
+ if (streq(item->str, key))
+ return item->family;
+
+ log_warning("ModemManager: ignoring unknown ip-type: %s, using any", key);
+ return MM_BEARER_IP_FAMILY_ANY;
+}
+
+static MMBearerAllowedAuth prop_auth_lookup(const char *key) {
+ static const struct {
+ MMBearerAllowedAuth auth;
+ const char *str;
+ } table[] = {
+ { MM_BEARER_ALLOWED_AUTH_NONE, "none" },
+ { MM_BEARER_ALLOWED_AUTH_PAP, "pap" },
+ { MM_BEARER_ALLOWED_AUTH_CHAP, "chap" },
+ { MM_BEARER_ALLOWED_AUTH_MSCHAP, "mschap" },
+ { MM_BEARER_ALLOWED_AUTH_MSCHAPV2, "mschapv2" },
+ { MM_BEARER_ALLOWED_AUTH_EAP, "eap" },
+ {}
+ };
+
+ assert(key);
+
+ FOREACH_ELEMENT(item, table)
+ if (streq(item->str, key))
+ return item->auth;
+
+ log_warning("ModemManager: ignoring unknown allowed-auth: %s, using none", key);
+ return MM_BEARER_ALLOWED_AUTH_NONE;
+}
+
+static const char* prop_type_lookup(const char *key) {
+ static const struct {
+ const char *prop;
+ const char *type;
+ } table[] = {
+ { "apn", "s" },
+ { "allowed-auth", "u" },
+ { "user", "s" },
+ { "password", "s" },
+ { "ip-type", "u" },
+ { "allow-roaming", "b" },
+ { "pin", "s" },
+ { "operator-id", "s" },
+ {}
+ };
+
+ if (!key)
+ return NULL;
+
+ FOREACH_ELEMENT(item, table)
+ if (streq(item->prop, key))
+ return item->type;
+ return NULL;
+}
+
+static int bus_call_method_async_props(
+ sd_bus *bus,
+ sd_bus_slot **slot,
+ const char *destination,
+ const char *path,
+ const char *interface,
+ const char *member,
+ sd_bus_message_handler_t callback,
+ void *userdata,
+ Link *link) {
+
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ int r;
+
+ assert(bus);
+
+ r = sd_bus_message_new_method_call(bus, &m, destination, path, interface, member);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_open_container(m, 'a', "{sv}");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ STRV_FOREACH(prop, link->network->mm_simple_connect_props) {
+ const char *type;
+ _cleanup_free_ char *left = NULL, *right = NULL;
+
+ r = split_pair(*prop, "=", &left, &right);
+ if (r < 0)
+ return log_warning_errno(SYNTHETIC_ERRNO(r),
+ "ModemManager: failed to parse simple connect option: %s, file: %s",
+ *prop, link->network->filename);
+
+ type = prop_type_lookup(left);
+ if (!type)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "ModemManager: unknown simple connect option: %s, file: %s",
+ *prop, link->network->filename);
+
+ if (streq(left, "ip-type")) {
+ MMBearerIpFamily ip_type = prop_iptype_lookup(right);
+
+ r = sd_bus_message_append(m, "{sv}", left, type, (uint32_t)ip_type);
+ } if (streq(left, "allowed-auth")) {
+ MMBearerAllowedAuth auth = prop_auth_lookup(right);
+
+ r = sd_bus_message_append(m, "{sv}", left, type, (uint32_t)auth);
+ } else if (streq(type, "b")) {
+ r = parse_boolean(right);
+ if (r < 0)
+ return -EINVAL;
+ r = sd_bus_message_append(m, "{sv}", left, type, r);
+ } else if (streq(type, "s"))
+ r = sd_bus_message_append(m, "{sv}", left, type, right);
+
+ if (r < 0)
+ return bus_log_create_error(r);
+ }
+
+ r = sd_bus_message_close_container(m);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ return sd_bus_call_async(bus, slot, m, callback, userdata, 0);
+}
+
+static void modem_simple_connect(Modem *modem) {
+ Link *link;
+ int r;
+
+ assert(modem);
+
+ /* Already have simple connect in progress? */
+ if (modem->slot_connect)
+ return;
+
+ if (modem->reconnect_state != MODEM_RECONNECT_SCHEDULED)
+ return;
+
+ /*
+ * If port name is not known yet then wait for the reconnect
+ * timer to trigger reconnection later on.
+ */
+ if (!modem->port_name)
+ return;
+
+ (void) link_get_by_name(modem->manager, modem->port_name, &link);
+ if (!link)
+ return (void) log_debug("ModemManager: cannot find link for %s", modem->port_name);
+
+ /* Check if .network file found at all */
+ if (!link->network)
+ return (void) log_debug("ModemManager: no .network file provideded for %s",
+ modem->port_name);
+
+ /* Check if we are provided with simple connection properties */
+ if (!link->network->mm_simple_connect_props)
+ return (void) log_debug("ModemManager: no simple connect properties provided for %s",
+ modem->port_name);
+
+ log_info("ModemManager: starting simple connect on %s %s interface %s",
+ modem->manufacturer, modem->model, modem->port_name);
+ r = bus_call_method_async_props(
+ modem->manager->bus,
+ &modem->slot_connect,
+ "org.freedesktop.ModemManager1",
+ modem->path,
+ "org.freedesktop.ModemManager1.Modem.Simple",
+ "Connect",
+ modem_connect_handler, modem, link);
+ /*
+ * If we failed to (re)start the connection now then rely on the periodic
+ * timer and wait when it retries the connection attempt.
+ */
+ if (r < 0)
+ log_warning_errno(r, "Could not start modem connection %s %s, will retry: %m",
+ modem->manufacturer, modem->model);
+}
+
+static void modem_simple_disconnect(Modem *modem) {
+ int r;
+
+ assert(modem);
+
+ r = sd_bus_call_method_async(
+ modem->manager->bus,
+ /* ret_slot= */ NULL,
+ "org.freedesktop.ModemManager1",
+ modem->path,
+ "org.freedesktop.ModemManager1.Modem.Simple",
+ "Disconnect",
+ /* callback= */ NULL,
+ /* userdata= */ NULL,
+ "o", "/");
+ if (r < 0)
+ log_warning_errno(r, "Could not disconnect modem %s %s: %m",
+ modem->manufacturer, modem->model);
+}
+
+static int reset_timer(Manager *m, sd_event *e, sd_event_source **s);
+
+static int on_periodic_timer(sd_event_source *s, uint64_t usec, void *userdata) {
+ Manager *manager = ASSERT_PTR(userdata);
+ Modem *modem;
+ sd_event *e;
+ int r;
+
+ assert(s);
+
+ e = sd_event_source_get_event(s);
+
+ HASHMAP_FOREACH(modem, manager->modems_by_path) {
+ /*
+ * We might be rate limiting the reconnection, e.g. if wrong simple connect options are
+ * provided modem manager might try to connect (registered->connecting) and fail soon
+ * (connecting->registered). To rate limit such a case we set MODEM_RECONNECT_WAITING state,
+ * so using this timer we can limit the requests and wait, for example, for network
+ * reconfigure wwanX. Still do not try to reconnect modems in failed state yet.
+ */
+ if (modem->reconnect_state == MODEM_RECONNECT_WAITING) {
+ if (modem->state == MM_MODEM_STATE_LOCKED)
+ /* If SIM is locked do not try to make it worse with applying wrong configuration again. */
+ continue;
+ if (modem->state_fail_reason == MM_MODEM_STATE_FAILED_REASON_NONE)
+ modem->reconnect_state = MODEM_RECONNECT_SCHEDULED;
+ }
+ modem_simple_connect(modem);
+ }
+
+ r = reset_timer(manager, e, &s);
+ if (r < 0)
+ log_warning_errno(r, "ModemManager: Failed to reset periodic timer event source, ignoring: %m");
+
+ return 0;
+}
+
+static int reset_timer(Manager *m, sd_event *e, sd_event_source **s) {
+ return event_reset_time_relative(
+ e,
+ s,
+ CLOCK_MONOTONIC,
+ RECONNECT_TIMEOUT_USEC,
+ /* accuracy= */ 0,
+ on_periodic_timer,
+ m,
+ /* priority= */ 0,
+ "modem-periodic-timer-event-source",
+ /* force_reset= */ false);
+}
+
+static int setup_periodic_timer(Manager *m, sd_event *event) {
+ _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL;
+ int r;
+
+ assert(event);
+ assert(m);
+
+ r = reset_timer(m, event, &s);
+ if (r < 0)
+ return r;
+
+ return sd_event_source_set_floating(s, true);
+}
+
+int link_modem_reconfigure(Link *link) {
+ Modem *modem;
+ int r;
+
+ assert(link);
+
+ if (link_get_modem(link, &modem) >= 0) {
+ modem_simple_disconnect(modem);
+ /* .network has changed: start (re)connect if failed before. */
+ if (modem->reconnect_state == MODEM_RECONNECT_WAITING &&
+ modem->state_fail_reason == MM_MODEM_STATE_FAILED_REASON_NONE) {
+ modem->reconnect_state = MODEM_RECONNECT_SCHEDULED;
+ modem_simple_connect(modem);
+ }
+ }
+
+ r = link_apply_bearer(link);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int modem_on_state_change(
+ Modem *modem,
+ MMModemState old_state,
+ MMModemStateFailedReason old_fail_reason) {
+
+ assert(modem);
+
+ if (IN_SET(modem->state, MM_MODEM_STATE_CONNECTING, MM_MODEM_STATE_CONNECTED))
+ /*
+ * Connection is ok or reconnect is already in progress: either initiataed by us or an
+ * external entity. Make sure we do not try to start reconnection logic and wait for th
+ * modem state change signal and then decide if need be.
+ * We assume that it is not possible to be in the above modem states e.g.
+ * connecting|connected if failed reason is not NONE, e.g. modem is all good.
+ */
+ return 0;
+
+ /* Check if modem is still in failed state. */
+ if (modem->state_fail_reason != MM_MODEM_STATE_FAILED_REASON_NONE) {
+ if (modem->state_fail_reason != old_fail_reason) {
+ log_error("ModemManager: cannot schedule reconnect for %s %s, modem is in failed state: %s",
+ modem->manufacturer, modem->model,
+ modem->state_fail_reason < __MM_MODEM_STATE_FAILED_REASON_MAX ?
+ modem_state_failed_reason_str[modem->state_fail_reason] :
+ "unknown reason");
+
+ /* Do not try to reconnect until modem has recovered. */
+ modem->reconnect_state = MODEM_RECONNECT_WAITING;
+ }
+ return 0;
+ }
+
+ if (modem->reconnect_state == MODEM_RECONNECT_SCHEDULED)
+ /* We are reconnecting now. */
+ return 0;
+
+ /*
+ * Modem is not in failed state and is not connected: try now. It is ok to fail and re-try to
+ * connect with periodic timer later on.
+ */
+ modem->reconnect_state = MODEM_RECONNECT_SCHEDULED;
+ modem_simple_connect(modem);
+
+ return 0;
+}
+
+static int bearer_properties_changed_handler(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ Manager *manager = ASSERT_PTR(userdata);
+ const char *path;
+ Modem *modem;
+ Bearer *b;
+
+ assert(message);
+
+ path = sd_bus_message_get_path(message);
+ if (!path)
+ return 0;
+
+ if (bearer_get_by_path(manager, path, &modem, &b) < 0) {
+ /*
+ * Have new bearer: check if we have the corresponding modem
+ * for it which we might not during initialization.
+ */
+ if (modem)
+ (void) bearer_new_and_initialize(modem, path);
+ return 0;
+ }
+
+ if (b->slot_getall) {
+ /* Not initialized yet. Re-initialize it. */
+ (void) bearer_initialize(b);
+ return 0;
+ }
+
+ (void) bearer_get_all_handler(message, b, error);
+ return 0;
+}
+
+static int modem_map_bearers(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata) {
+
+ Modem *modem = ASSERT_PTR(userdata);
+ _cleanup_strv_free_ char **paths = NULL;
+ int r;
+
+ r = sd_bus_message_read_strv(m, &paths);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ STRV_FOREACH(path, paths) {
+ log_info("ModemManager: bearer found at path %s", *path);
+ (void) bearer_new_and_initialize(modem, *path);
+ }
+
+ return 0;
+}
+
+static int modem_map_ports(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata) {
+
+ Modem *modem = ASSERT_PTR(userdata);
+ const char *port_name;
+ uint32_t port_type;
+ int r;
+
+ r = sd_bus_message_enter_container(m, SD_BUS_TYPE_ARRAY, NULL);
+ if (r < 0)
+ return bus_log_parse_error_debug(r);
+
+ while ((r = sd_bus_message_read(m, "(su)", &port_name, &port_type)) > 0)
+ if (port_type == MM_MODEM_PORT_TYPE_NET) {
+ r = free_and_strdup_warn(&modem->port_name, port_name);
+ if (r < 0)
+ return r;
+ break;
+ }
+
+ r = sd_bus_message_exit_container(m);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ return 0;
+}
+
+static int modem_properties_changed_signal(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *ret_error) {
+
+ static const struct bus_properties_map map[] = {
+ { "Bearers", "a{sv}", modem_map_bearers, 0, },
+ { "State", "i", NULL, offsetof(Modem, state) },
+ { "StateFailedReason", "u", NULL, offsetof(Modem, state_fail_reason) },
+ { "Manufacturer", "s", NULL, offsetof(Modem, manufacturer) },
+ { "Model", "s", NULL, offsetof(Modem, model) },
+ { "Ports", "a{su}", modem_map_ports, 0, },
+ {}
+ };
+ Modem *modem = ASSERT_PTR(userdata);
+ MMModemState old_state;
+ MMModemStateFailedReason old_fail_reason;
+ int r;
+
+ /* skip name: string "org.freedesktop.ModemManager1.Modem" */
+ r = sd_bus_message_skip(message, "s");
+ if (r < 0)
+ return log_warning_errno(r, "Failed while parsing properties of modem %s: %s",
+ modem->path, bus_error_message(ret_error, r));
+
+ r = bus_message_contains_properties(message, map, ret_error);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to check changed properties of modem %s: %s",
+ modem->path, bus_error_message(ret_error, r));
+
+ if (r == 0)
+ return 0;
+
+ r = sd_bus_message_rewind(message, true);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to rewind properties of modem %s", modem->path);
+ old_state = modem->state;
+ old_fail_reason = modem->state_fail_reason;
+
+ /* skip name: string "org.freedesktop.ModemManager1.Bearer" */
+ r = sd_bus_message_skip(message, "s");
+ if (r < 0)
+ return log_warning_errno(r, "Failed while parsing properties of modem %s: %s",
+ modem->path, bus_error_message(ret_error, r));
+
+ r = bus_message_map_all_properties(message, map,
+ BUS_MAP_BOOLEAN_AS_BOOL | BUS_MAP_STRDUP,
+ ret_error, modem);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to parse properties of modem %s: %s",
+ modem->path, bus_error_message(ret_error, r));
+
+ return modem_on_state_change(modem, old_state, old_fail_reason);
+}
+
+static int modem_properties_changed_installed(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *ret_error) {
+
+ Modem *modem = ASSERT_PTR(userdata);
+
+ /*
+ * As soon as the signal handler installed we can start reconnect
+ * so we don't miss any property changed.
+ */
+ return modem_on_state_change(modem, MM_MODEM_STATE_UNKNOWN, MM_MODEM_STATE_FAILED_REASON_UNKNOWN);
+}
+
+static int modem_match_properties_changed(Modem *modem, const char *path) {
+ int r;
+
+ assert(modem);
+ assert(modem->manager);
+ assert(modem->manager->bus);
+
+ r = sd_bus_match_signal_async(modem->manager->bus, &modem->slot_propertieschanged,
+ "org.freedesktop.ModemManager1", path,
+ "org.freedesktop.DBus.Properties", "PropertiesChanged",
+ modem_properties_changed_signal, modem_properties_changed_installed, modem);
+ if (r < 0)
+ return log_error_errno(r, "Failed to request match for PropertiesChanged for modem %s: %m", path);
+
+ return 0;
+}
+
+static int modem_add(Manager *m, const char *path, sd_bus_message *message, sd_bus_error *ret_error) {
+ static const struct bus_properties_map map[] = {
+ { "Bearers", "ao", modem_map_bearers, 0, },
+ { "State", "i", NULL, offsetof(Modem, state) },
+ { "StateFailedReason", "u", NULL, offsetof(Modem, state_fail_reason) },
+ { "Manufacturer", "s", NULL, offsetof(Modem, manufacturer) },
+ { "Model", "s", NULL, offsetof(Modem, model) },
+ { "Ports", "a{su}", modem_map_ports, 0, },
+ {}
+ };
+ Modem *modem;
+ int r;
+
+ r = modem_get_by_path(m, path, &modem);
+ if (r != -ENOENT)
+ return sd_bus_message_skip(message, "a{sv}");
+
+ log_info("ModemManager: modem found at %s\n", path);
+
+ r = modem_new(m, path, &modem);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to initialize modem at %s, ignoring", path);
+
+ r = modem_match_properties_changed(modem, path);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to match on properties changed at %s, ignoring", path);
+
+ r = bus_message_map_all_properties(message, map, BUS_MAP_STRDUP, ret_error, modem);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to map properties at %s, ignoring", path);
+
+ return 0;
+}
+
+static void modem_remove(Manager *m, const char *path) {
+ Modem *modem;
+ int r;
+
+ r = modem_get_by_path(m, path, &modem);
+ if (r < 0)
+ return;
+
+ log_error("ModemManager: %s %s %s removed", modem->manufacturer, modem->model, modem->port_name);
+ modem_free(modem);
+}
+
+static int enumerate_modems_handler(sd_bus_message *message, void *userdata, sd_bus_error *ret_error) {
+ Manager *manager = ASSERT_PTR(userdata);
+ const sd_bus_error *e;
+ const char *modem_path;
+ int r;
+
+ assert(message);
+
+ e = sd_bus_message_get_error(message);
+ if (e) {
+ r = sd_bus_error_get_errno(e);
+ log_warning_errno(r, "Could not get managed objects: %s", bus_error_message(e, r));
+ return 0;
+ }
+
+ r = sd_bus_message_enter_container(message, SD_BUS_TYPE_ARRAY, "{oa{sa{sv}}}");
+ if (r < 0)
+ return bus_log_parse_error_debug(r);
+
+ while ((r = sd_bus_message_enter_container(message, SD_BUS_TYPE_DICT_ENTRY, "oa{sa{sv}}")) > 0) {
+ r = sd_bus_message_read_basic(message, SD_BUS_TYPE_OBJECT_PATH, &modem_path);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_enter_container(message, SD_BUS_TYPE_ARRAY, "{sa{sv}}");
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ while ((r = sd_bus_message_enter_container(message, SD_BUS_TYPE_DICT_ENTRY, "sa{sv}")) > 0) {
+ const char *interface_name = NULL;
+
+ r = sd_bus_message_read_basic(message, 's', &interface_name);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ if (streq("org.freedesktop.ModemManager1.Modem", interface_name)) {
+ r = modem_add(manager, modem_path, message, ret_error);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add modem at %s: %m", modem_path);
+ } else {
+ r = sd_bus_message_skip(message, "a{sv}");
+ if (r < 0)
+ return bus_log_parse_error(r);
+ }
+
+ r = sd_bus_message_exit_container(message);
+ if (r < 0)
+ return bus_log_parse_error(r);
+ }
+
+ r = sd_bus_message_exit_container(message);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_exit_container(message);
+ if (r < 0)
+ return bus_log_parse_error(r);
+ }
+
+ r = sd_bus_message_exit_container(message);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ return 0;
+}
+
+static int enumerate_modems(Manager *manager) {
+ int r;
+
+ log_debug("ModemManager: enumerate modems");
+ /* Enumerate all modems and add new and drop removed. */
+
+ assert(manager);
+ assert(sd_bus_is_ready(manager->bus) > 0);
+
+ r = sd_bus_call_method_async(
+ manager->bus,
+ /* ret_slot= */ NULL,
+ "org.freedesktop.ModemManager1",
+ "/org/freedesktop/ModemManager1",
+ "org.freedesktop.DBus.ObjectManager",
+ "GetManagedObjects",
+ enumerate_modems_handler,
+ manager,
+ /* types= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Could not get managed objects: %m");
+
+ return 0;
+}
+
+static int interface_add_remove_signal(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ Manager *manager = ASSERT_PTR(userdata);
+ int r;
+
+ assert(manager);
+ assert(message);
+
+ manager->slot_mm = sd_bus_slot_unref(manager->slot_mm);
+
+ if (streq(message->member, "InterfacesAdded"))
+ log_info("ModemManager: modem(s) added");
+ else {
+ const char *path;
+
+ r = sd_bus_message_read_basic(message, 'o', &path);
+ if (r < 0)
+ return r;
+
+ modem_remove(manager, path);
+ return 0;
+ }
+
+ return enumerate_modems(manager);
+}
+
+static int name_owner_changed_signal(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ Manager *manager = ASSERT_PTR(userdata);
+ const char *name;
+ const char *new_owner;
+ int r;
+
+ assert(manager);
+ assert(message);
+
+ r = sd_bus_message_read(message, "sss", &name, NULL, &new_owner);
+ if (r < 0) {
+ bus_log_parse_error(r);
+ return 0;
+ }
+
+ if (!streq(name, "org.freedesktop.ModemManager1"))
+ return 0;
+
+ if (!isempty(new_owner))
+ log_info("ModemManager: service is available");
+ else {
+ log_info("ModemManager: service is not available");
+ hashmap_clear(manager->modems_by_path);
+ return 0;
+ }
+ return enumerate_modems(manager);
+}
+
+int manager_match_mm_signals(Manager *manager) {
+ int r;
+
+ assert(manager);
+ assert(manager->bus);
+
+ r = sd_bus_match_signal_async(
+ manager->bus,
+ /* ret= */ NULL,
+ "org.freedesktop.DBus",
+ "/org/freedesktop/DBus",
+ "org.freedesktop.DBus",
+ "NameOwnerChanged",
+ name_owner_changed_signal,
+ /* install_callback= */ NULL,
+ manager);
+ if (r < 0)
+ return log_error_errno(r, "Failed to request signal for NameOwnerChanged");
+
+ r = sd_bus_match_signal_async(
+ manager->bus,
+ /* ret= */ NULL,
+ "org.freedesktop.ModemManager1",
+ "/org/freedesktop/ModemManager1",
+ "org.freedesktop.DBus.ObjectManager",
+ "InterfacesAdded",
+ interface_add_remove_signal,
+ /* install_callback= */ NULL,
+ manager);
+ if (r < 0)
+ return log_error_errno(r, "Failed to request signal for IntefaceAdded");
+
+ r = sd_bus_match_signal_async(
+ manager->bus,
+ /* ret= */ NULL,
+ "org.freedesktop.ModemManager1",
+ "/org/freedesktop/ModemManager1",
+ "org.freedesktop.DBus.ObjectManager",
+ "InterfacesRemoved",
+ interface_add_remove_signal,
+ /* install_callback= */ NULL,
+ manager);
+ if (r < 0)
+ return log_error_errno(r, "Failed to request signal for IntefaceRemoved");
+
+ /* N.B. We need "path_namespace" for bearers, not "path", */
+ r = sd_bus_add_match_async(
+ manager->bus,
+ /* ret_slot= */ NULL,
+ "type='signal',"
+ "sender='org.freedesktop.ModemManager1',"
+ "path_namespace='/org/freedesktop/ModemManager1/Bearer',"
+ "interface='org.freedesktop.DBus.Properties',"
+ "member='PropertiesChanged'",
+ bearer_properties_changed_handler,
+ /* install_callback= */ NULL,
+ manager);
+ if (r < 0)
+ return log_error_errno(r, "Failed to request signal for PropertiesChanged in ModemManager bearers");
+
+ return 0;
+}
+
+static int list_names_handler(sd_bus_message *message, void *userdata, sd_bus_error *ret_error) {
+ Manager *manager = ASSERT_PTR(userdata);
+ _cleanup_strv_free_ char **names = NULL;
+ int r;
+
+ assert(manager);
+ assert(message);
+
+ manager->slot_mm = sd_bus_slot_unref(manager->slot_mm);
+
+ /* Read the list of available services. */
+ r = sd_bus_message_read_strv(message, &names);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ if (!strv_contains(names, "org.freedesktop.ModemManager1"))
+ /* If not found yet then wait for NameOwnerChanged signal. */
+ return 0;
+
+ log_info("ModemManager: service available");
+ return enumerate_modems(manager);
+}
+
+int manager_notify_mm_bus_connected(Manager *m) {
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ int r;
+
+ /*
+ * Called on D-Bus connected.
+ * Check if ModemManager is available. If it is then initialize.
+ * If not then wait for the serivce to be available.
+ */
+ assert(m);
+ assert(sd_bus_is_ready(m->bus) > 0);
+
+ m->slot_mm = sd_bus_slot_unref(m->slot_mm);
+
+ r = sd_bus_call_method_async(
+ m->bus,
+ &m->slot_mm,
+ "org.freedesktop.DBus",
+ "/org/freedesktop/DBus",
+ "org.freedesktop.DBus",
+ "ListNames",
+ list_names_handler,
+ m,
+ /* types= */ NULL);
+ if (r < 0)
+ return log_warning_errno(r, "Could not LsitNames: %m");
+
+ r = sd_event_default(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize sd-event: %m");
+
+ r = setup_periodic_timer(m, event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set up periodic timer: %m");
+
+ return 0;
+}
diff --git a/src/network/networkd-wwan-bus.h b/src/network/networkd-wwan-bus.h
new file mode 100644
index 0000000000..a6697cc219
--- /dev/null
+++ b/src/network/networkd-wwan-bus.h
@@ -0,0 +1,88 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct Manager Manager;
+typedef struct Link Link;
+
+/* From ModemManager-enums.h */
+typedef enum {
+ MM_BEARER_IP_FAMILY_NONE = 0,
+ MM_BEARER_IP_FAMILY_IPV4 = 1 << 0,
+ MM_BEARER_IP_FAMILY_IPV6 = 1 << 1,
+ MM_BEARER_IP_FAMILY_IPV4V6 = 1 << 2,
+ MM_BEARER_IP_FAMILY_ANY = 0xFFFFFFFF,
+} MMBearerIpFamily;
+
+typedef enum {
+ MM_BEARER_TYPE_UNKNOWN = 0,
+ MM_BEARER_TYPE_DEFAULT = 1,
+ MM_BEARER_TYPE_DEFAULT_ATTACH = 2,
+ MM_BEARER_TYPE_DEDICATED = 3,
+} MMBearerType;
+
+typedef enum {
+ MM_MODEM_STATE_FAILED = -1,
+ MM_MODEM_STATE_UNKNOWN = 0,
+ MM_MODEM_STATE_INITIALIZING = 1,
+ MM_MODEM_STATE_LOCKED = 2,
+ MM_MODEM_STATE_DISABLED = 3,
+ MM_MODEM_STATE_DISABLING = 4,
+ MM_MODEM_STATE_ENABLING = 5,
+ MM_MODEM_STATE_ENABLED = 6,
+ MM_MODEM_STATE_SEARCHING = 7,
+ MM_MODEM_STATE_REGISTERED = 8,
+ MM_MODEM_STATE_DISCONNECTING = 9,
+ MM_MODEM_STATE_CONNECTING = 10,
+ MM_MODEM_STATE_CONNECTED = 11,
+} MMModemState;
+
+typedef enum { /*< underscore_name=mm_modem_state_failed_reason >*/
+ MM_MODEM_STATE_FAILED_REASON_NONE = 0,
+ MM_MODEM_STATE_FAILED_REASON_UNKNOWN = 1,
+ MM_MODEM_STATE_FAILED_REASON_SIM_MISSING = 2,
+ MM_MODEM_STATE_FAILED_REASON_SIM_ERROR = 3,
+ MM_MODEM_STATE_FAILED_REASON_UNKNOWN_CAPABILITIES = 4,
+ MM_MODEM_STATE_FAILED_REASON_ESIM_WITHOUT_PROFILES = 5,
+ __MM_MODEM_STATE_FAILED_REASON_MAX = 6,
+} MMModemStateFailedReason;
+
+typedef enum {
+ MM_BEARER_IP_METHOD_UNKNOWN = 0,
+ MM_BEARER_IP_METHOD_PPP = 1,
+ MM_BEARER_IP_METHOD_STATIC = 2,
+ MM_BEARER_IP_METHOD_DHCP = 3,
+} MMBearerIpMethod;
+
+typedef enum { /*< underscore_name=mm_modem_port_type >*/
+ MM_MODEM_PORT_TYPE_UNKNOWN = 1,
+ MM_MODEM_PORT_TYPE_NET = 2,
+ MM_MODEM_PORT_TYPE_AT = 3,
+ MM_MODEM_PORT_TYPE_QCDM = 4,
+ MM_MODEM_PORT_TYPE_GPS = 5,
+ MM_MODEM_PORT_TYPE_QMI = 6,
+ MM_MODEM_PORT_TYPE_MBIM = 7,
+ MM_MODEM_PORT_TYPE_AUDIO = 8,
+ MM_MODEM_PORT_TYPE_IGNORED = 9,
+ MM_MODEM_PORT_TYPE_XMMRPC = 10,
+} MMModemPortType;
+
+typedef enum { /*< underscore_name=mm_bearer_allowed_auth >*/
+ MM_BEARER_ALLOWED_AUTH_UNKNOWN = 0,
+ /* bits 0..4 order match Ericsson device bitmap */
+ MM_BEARER_ALLOWED_AUTH_NONE = 1 << 0,
+ MM_BEARER_ALLOWED_AUTH_PAP = 1 << 1,
+ MM_BEARER_ALLOWED_AUTH_CHAP = 1 << 2,
+ MM_BEARER_ALLOWED_AUTH_MSCHAP = 1 << 3,
+ MM_BEARER_ALLOWED_AUTH_MSCHAPV2 = 1 << 4,
+ MM_BEARER_ALLOWED_AUTH_EAP = 1 << 5,
+} MMBearerAllowedAuth;
+
+typedef enum {
+ MODEM_RECONNECT_DONE, /* No reconnect is required, e.g. connected. */
+ MODEM_RECONNECT_SCHEDULED, /* Reconnect is in progress. */
+ MODEM_RECONNECT_WAITING, /* Waiting for modem to recover. */
+} ModemReconnectState;
+
+int manager_notify_mm_bus_connected(Manager *manager);
+int manager_match_mm_signals(Manager *manager);
+int link_modem_reconfigure(Link *link);
diff --git a/src/network/networkd-wwan.c b/src/network/networkd-wwan.c
new file mode 100644
index 0000000000..64dd37eb6b
--- /dev/null
+++ b/src/network/networkd-wwan.c
@@ -0,0 +1,641 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "bus-util.h"
+#include "hashmap.h"
+#include "networkd-address.h"
+#include "networkd-dhcp4.h"
+#include "networkd-dhcp6.h"
+#include "networkd-link.h"
+#include "networkd-setlink.h"
+#include "networkd-manager.h"
+#include "networkd-ndisc.h"
+#include "networkd-route.h"
+#include "networkd-wwan.h"
+#include "parse-util.h"
+#include "sd-dhcp-client.h"
+#include "sd-dhcp6-client.h"
+#include "sd-ndisc.h"
+#include "set.h"
+#include "string-util.h"
+
+Bearer* bearer_free(Bearer *b) {
+ if (!b)
+ return NULL;
+
+ if (b->modem) {
+ if (b->path)
+ hashmap_remove_value(b->modem->bearers_by_path, b->path, b);
+ if (b->name)
+ hashmap_remove_value(b->modem->bearers_by_name, b->name, b);
+ }
+
+ sd_bus_slot_unref(b->slot_getall);
+
+ free(b->path);
+ free(b->name);
+ free(b->apn);
+
+ in_addr_full_array_free(b->dns, b->n_dns);
+
+ return mfree(b);
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
+ bearer_hash_ops,
+ char,
+ string_hash_func,
+ string_compare_func,
+ Bearer,
+ bearer_free);
+
+int bearer_new(Modem *modem, const char *path, Bearer **ret) {
+ _cleanup_(bearer_freep) Bearer *b = NULL;
+ _cleanup_free_ char *p = NULL;
+ int r;
+
+ assert(modem);
+ assert(path);
+
+ if (hashmap_contains(modem->bearers_by_path, path))
+ return -EEXIST;
+
+ p = strdup(path);
+ if (!p)
+ return -ENOMEM;
+
+ b = new(Bearer, 1);
+ if (!b)
+ return -ENOMEM;
+
+ *b = (Bearer) {
+ .modem = modem,
+ .path = TAKE_PTR(p),
+ };
+
+ r = hashmap_ensure_put(&modem->bearers_by_path, &bearer_hash_ops, b->path, b);
+ if (r < 0)
+ return r;
+
+ if (ret)
+ *ret = b;
+ TAKE_PTR(b);
+ return 0;
+}
+
+int bearer_set_name(Bearer *b, const char *name) {
+ Bearer *old;
+ int r;
+
+ assert(b);
+ assert(b->modem);
+ assert(name);
+
+ if (streq_ptr(b->name, name))
+ return 0;
+
+ if (b->name)
+ hashmap_remove_value(b->modem->bearers_by_name, b->name, b);
+
+ if (isempty(name)) {
+ b->name = mfree(b->name);
+ return 0;
+ }
+
+ r = free_and_strdup(&b->name, name);
+ if (r < 0)
+ return r;
+
+ /*
+ * FIXME: it is possible during reconnect that an interface is already
+ * registered in the hash map: if simple connect options
+ * are changed, e.g. externally modified .network file and then
+ * reloaded with 'networkctl reload'. This may create a new bearer
+ * attached to the same inerface name, e.g. "wwan0". The order in which
+ * we parse the bearer properties is undetermined and it can be that we
+ * need to raplce the old one with the new one now, so only one bearer
+ * with the given interface name exists.
+ */
+ old = hashmap_get(b->modem->bearers_by_name, name);
+ if (old) {
+ hashmap_remove_value(old->modem->bearers_by_name, name, old);
+ old->name = mfree(old->name);
+ }
+
+ return hashmap_ensure_put(&b->modem->bearers_by_name, &bearer_hash_ops, b->name, b);
+}
+
+int bearer_get_by_path(Manager *manager, const char *path, Modem **ret_modem, Bearer **ret_bearer) {
+ Modem *modem;
+ Bearer *b;
+
+ assert(manager);
+ assert(path);
+
+ HASHMAP_FOREACH(modem, manager->modems_by_path) {
+ b = hashmap_get(modem->bearers_by_path, path);
+ if (!b)
+ continue;
+
+ if (ret_bearer)
+ *ret_bearer = b;
+ if (ret_modem)
+ *ret_modem = modem;
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+Modem* modem_free(Modem *modem) {
+ if (!modem)
+ return NULL;
+
+ if (modem->bearers_by_name)
+ hashmap_free(modem->bearers_by_name);
+
+ if (modem->bearers_by_path)
+ hashmap_free(modem->bearers_by_path);
+
+ if (modem->manager)
+ hashmap_remove_value(modem->manager->modems_by_path, modem->path, modem);
+
+ sd_bus_slot_unref(modem->slot_propertieschanged);
+ sd_bus_slot_unref(modem->slot_statechanged);
+ sd_bus_slot_unref(modem->slot_connect);
+
+ free(modem->path);
+ free(modem->manufacturer);
+ free(modem->model);
+ free(modem->port_name);
+
+ return mfree(modem);
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
+ modems_hash_ops,
+ char,
+ string_hash_func,
+ string_compare_func,
+ Modem,
+ modem_free);
+
+int modem_new(Manager *m, const char *path, Modem **ret) {
+ _cleanup_(modem_freep) Modem *modem = NULL;
+ _cleanup_free_ char *p = NULL;
+ int r;
+
+ assert(m);
+ assert(path);
+
+ if (hashmap_contains(m->modems_by_path, path))
+ return -EEXIST;
+
+ p = strdup(path);
+ if (!p)
+ return -ENOMEM;
+
+ modem = new(Modem, 1);
+ if (!modem)
+ return -ENOMEM;
+
+ *modem = (Modem) {
+ .manager = m,
+ .path = TAKE_PTR(p),
+ };
+
+ r = hashmap_ensure_put(&m->modems_by_path, &modems_hash_ops, modem->path, modem);
+ if (r < 0)
+ return r;
+
+ if (ret)
+ *ret = modem;
+
+ TAKE_PTR(modem);
+ return 0;
+}
+
+int modem_get_by_path(Manager *m, const char *path, Modem **ret) {
+ Modem *modem;
+
+ assert(m);
+ assert(path);
+
+ modem = hashmap_get(m->modems_by_path, path);
+ if (!modem)
+ return -ENOENT;
+
+ if (ret)
+ *ret = modem;
+
+ return 0;
+}
+
+int link_get_modem(Link *link, Modem **ret) {
+ Modem *modem;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->ifname);
+
+ HASHMAP_FOREACH(modem, link->manager->modems_by_path)
+ if (modem->port_name && streq(modem->port_name, link->ifname)) {
+ *ret = modem;
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+int link_get_bearer(Link *link, Bearer **ret) {
+ Modem *modem;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->ifname);
+
+ HASHMAP_FOREACH(modem, link->manager->modems_by_path) {
+ Bearer *b;
+
+ b = hashmap_get(modem->bearers_by_name, link->ifname);
+ if (!b)
+ continue;
+
+ if (ret)
+ *ret = b;
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+int link_dhcp_enabled_by_bearer(Link *link, int family) {
+ Bearer *b;
+ int r;
+
+ assert(link);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+
+ r = link_get_bearer(link, &b);
+ if (r < 0)
+ return r;
+
+ if (!b->connected)
+ return false;
+
+ if (!FLAGS_SET(b->ip_type, family == AF_INET ? ADDRESS_FAMILY_IPV4 : ADDRESS_FAMILY_IPV6))
+ return false;
+
+ return (family == AF_INET ? b->ip4_method : b->ip6_method) == MM_BEARER_IP_METHOD_DHCP;
+}
+
+static int bearer_address_handler(
+ sd_netlink *rtnl,
+ sd_netlink_message *m,
+ Request *req,
+ Link *link,
+ Address *address) {
+
+ int r;
+
+ assert(link);
+
+ r = address_configure_handler_internal(m, link, address);
+ if (r <= 0)
+ return r;
+
+ if (link->bearer_messages == 0) {
+ link->bearer_configured = true;
+ link_check_ready(link);
+ }
+
+ return 0;
+}
+
+static int link_request_bearer_address(
+ Link *link,
+ int family,
+ const union in_addr_union *addr,
+ unsigned prefixlen) {
+
+ _cleanup_(address_unrefp) Address *address = NULL;
+ Address *existing;
+ int r;
+
+ assert(link);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(addr);
+
+ if (!in_addr_is_set(family, addr))
+ return 0;
+
+ r = address_new(&address);
+ if (r < 0)
+ return log_oom();
+
+ address->source = NETWORK_CONFIG_SOURCE_MODEM_MANAGER;
+ address->family = family;
+ address->in_addr = *addr;
+ address->prefixlen = prefixlen;
+
+ if (address_get(link, address, &existing) < 0) /* The address is new. */
+ link->bearer_configured = false;
+ else
+ address_unmark(existing);
+
+ r = link_request_address(link, address, &link->bearer_messages,
+ bearer_address_handler, /* ret = */ NULL);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request address provided by bearer: %m");
+
+ return 0;
+}
+
+static int bearer_route_handler(
+ sd_netlink *rtnl,
+ sd_netlink_message *m,
+ Request *req,
+ Link *link,
+ Route *route) {
+
+ int r;
+
+ assert(link);
+
+ r = route_configure_handler_internal(m, req, route);
+ if (r <= 0)
+ return r;
+
+ if (link->bearer_messages == 0) {
+ link->bearer_configured = true;
+ link_check_ready(link);
+ }
+
+ return 0;
+}
+
+static int link_request_bearer_route(
+ Link *link,
+ int family,
+ const union in_addr_union *gw,
+ const union in_addr_union *prefsrc) {
+
+ _cleanup_(route_unrefp) Route *route = NULL;
+ Route *existing;
+ int r;
+
+ assert(link);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(gw);
+ assert(prefsrc);
+
+ if (!in_addr_is_set(family, gw))
+ return 0;
+
+ if (link->network->mm_use_gateway == 0)
+ return 0;
+
+ r = route_new(&route);
+ if (r < 0)
+ return log_oom();
+
+ route->source = NETWORK_CONFIG_SOURCE_MODEM_MANAGER;
+ route->family = family;
+ route->nexthop.family = family;
+ route->nexthop.gw = *gw;
+ if (link->network->mm_route_metric_set) {
+ route->priority = link->network->mm_route_metric;
+ route->priority_set = true;
+ }
+
+ if (prefsrc)
+ route->prefsrc = *prefsrc;
+
+ if (route_get(link->manager, route, &existing) < 0) /* This is a new route. */
+ link->bearer_configured = false;
+ else
+ route_unmark(existing);
+
+ r = link_request_route(link, route, &link->bearer_messages, bearer_route_handler);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request gateway provided by bearer: %m");
+
+ return 0;
+}
+
+static int link_apply_bearer_impl(Link *link, Bearer *b) {
+ Address *address;
+ Route *route;
+ int r, ret = 0;
+
+ assert(link);
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ return 0;
+
+ /* First, mark bearer configs. */
+ SET_FOREACH(address, link->addresses) {
+ if (address->source != NETWORK_CONFIG_SOURCE_MODEM_MANAGER)
+ continue;
+
+ address_mark(address);
+ }
+
+ SET_FOREACH(route, link->manager->routes) {
+ if (route->source != NETWORK_CONFIG_SOURCE_MODEM_MANAGER)
+ continue;
+
+ route_mark(route);
+ }
+
+ if (b && FLAGS_SET(b->ip_type, ADDRESS_FAMILY_IPV4)) {
+ if (b->connected && b->ip4_method == MM_BEARER_IP_METHOD_STATIC) {
+ r = link_request_bearer_address(link, AF_INET, &b->ip4_address, b->ip4_prefixlen);
+ if (r < 0)
+ return r;
+
+ r = link_request_bearer_route(link, AF_INET, &b->ip4_gateway, &b->ip4_address);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->connected && b->ip4_method == MM_BEARER_IP_METHOD_DHCP) {
+ if (!link_dhcp4_enabled(link))
+ log_link_notice(link, "The WWAN connection requested DHCPv4 client, but it is disabled.");
+
+ r = dhcp4_start(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start DHCPv4 client: %m");
+ } else {
+ r = sd_dhcp_client_stop(link->dhcp_client);
+ if (r < 0)
+ ret = log_link_warning_errno(link, r, "Could not stop DHCPv4 client: %m");
+ }
+ }
+
+ if (b && FLAGS_SET(b->ip_type, ADDRESS_FAMILY_IPV6)) {
+ if (b->connected && b->ip6_method == MM_BEARER_IP_METHOD_STATIC) {
+ r = link_request_bearer_address(link, AF_INET6, &b->ip6_address, b->ip6_prefixlen);
+ if (r < 0)
+ return r;
+
+ r = link_request_bearer_route(link, AF_INET6, &b->ip6_gateway, NULL);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->connected && b->ip6_method == MM_BEARER_IP_METHOD_DHCP) {
+ if (!link_ndisc_enabled(link) && !link_dhcp6_enabled(link))
+ log_link_notice(link,
+ "The WWAN connection requested IPv6 dynamic address configuration,"
+ "but both IPv6 Router Discovery and DHCPv6 client are disabled.");
+
+ r = ndisc_start(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start IPv6 Router Discovery: %m");
+
+ r = dhcp6_start(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start DHCPv6 client: %m");
+ } else {
+ r = sd_dhcp6_client_stop(link->dhcp6_client);
+ if (r < 0)
+ ret = log_link_warning_errno(link, r, "Could not stop DHCPv6 client: %m");
+
+ r = sd_ndisc_stop(link->ndisc);
+ if (r < 0)
+ ret = log_link_warning_errno(link, r, "Could not stop IPv6 Router Discovery: %m");
+ }
+ }
+
+ /* Finally, remove all marked configs. */
+ SET_FOREACH(address, link->addresses) {
+ if (address->source != NETWORK_CONFIG_SOURCE_MODEM_MANAGER)
+ continue;
+
+ if (!address_is_marked(address))
+ continue;
+
+ r = address_remove(address, link);
+ if (r < 0)
+ ret = r;
+ }
+
+ SET_FOREACH(route, link->manager->routes) {
+ if (route->source != NETWORK_CONFIG_SOURCE_MODEM_MANAGER)
+ continue;
+
+ if (!route_is_marked(route))
+ continue;
+
+ r = route_remove(route, link->manager);
+ if (ret)
+ ret = r;
+ }
+
+ if (ret < 0)
+ return ret;
+
+ if (link->bearer_messages == 0)
+ link->bearer_configured = true;
+
+ if (!link->bearer_configured)
+ link_set_state(link, LINK_STATE_CONFIGURING);
+
+ link_check_ready(link);
+
+ return 0;
+}
+
+int link_apply_bearer(Link *link) {
+ Bearer *b = NULL;
+ int r;
+
+ assert(link);
+
+ (void) link_get_bearer(link, &b);
+
+ r = link_apply_bearer_impl(link, b);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return r;
+}
+
+int bearer_update_link(Bearer *b) {
+ Link *link;
+ int r;
+
+ assert(b);
+ assert(b->modem);
+ assert(b->modem->manager);
+
+ if (!b->name)
+ return 0;
+
+ if (link_get_by_name(b->modem->manager, b->name, &link) < 0)
+ return 0;
+
+ r = link_apply_bearer_impl(link, b);
+ if (r < 0)
+ link_enter_failed(link);
+
+ /*
+ * Need to bring up the interface after the modem has connected.
+ * This is because ModemManger does the following while connecting:
+ * [1755871777.322239] [modem2] state changed (registered -> connecting)
+ * [1755871777.325012] [modem2/bearer5] launching connection with QMI port (cdc-wdm0) and data port (wwan0) (multiplex none)
+ * [1755871777.327665] [cdc-wdm0/qmi] bringing down data interface 'wwan0'
+ * [1755871777.330108] [modem2/wwan0/net] interface index: 9
+ * [1755871777.335265] [cdc-wdm0/qmi] deleting all links in data interface 'wwan0'
+ */
+
+ r = link_request_to_bring_up_or_down(link, b->connected);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return 0;
+}
+
+void bearer_drop(Bearer *b) {
+ assert(b);
+
+ b->connected = false;
+ b->apn = mfree(b->apn);
+
+ (void) bearer_update_link(b);
+
+ bearer_free(b);
+}
+
+int config_parse_mm_route_metric(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ network->mm_route_metric_set = false;
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &network->mm_route_metric);
+ if (r < 0)
+ return log_syntax_parse_error(unit, filename, line, r, lvalue, rvalue);
+
+ network->mm_route_metric_set = true;
+ return 0;
+}
diff --git a/src/network/networkd-wwan.h b/src/network/networkd-wwan.h
new file mode 100644
index 0000000000..962f76be2e
--- /dev/null
+++ b/src/network/networkd-wwan.h
@@ -0,0 +1,80 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser-forward.h"
+#include "in-addr-util.h"
+#include "network-util.h"
+#include "networkd-wwan-bus.h"
+
+typedef struct Modem Modem;
+
+typedef struct Bearer {
+ Modem *modem;
+
+ sd_bus_slot *slot_getall; /* for GetAll method */
+
+ char *path; /* DBus path e.g /org/freedesktop/ModemManager/Bearer/0 */
+ char *name; /* Interface property, e.g. wwan0 */
+ char *apn; /* "apn" field in Properties */
+ AddressFamily ip_type; /* "ip-type" field in Properties */
+
+ /* Ip4Config or IP6Config property */
+ MMBearerIpMethod ip4_method;
+ MMBearerIpMethod ip6_method;
+ unsigned ip4_prefixlen;
+ unsigned ip6_prefixlen;
+ union in_addr_union ip4_address;
+ union in_addr_union ip6_address;
+ union in_addr_union ip4_gateway;
+ union in_addr_union ip6_gateway;
+ struct in_addr_full **dns;
+ size_t n_dns;
+ uint32_t ip4_mtu;
+ uint32_t ip6_mtu;
+
+ bool connected; /* Connected property */
+} Bearer;
+
+typedef struct Modem {
+ Manager *manager;
+
+ sd_bus_slot *slot_propertieschanged;
+ sd_bus_slot *slot_statechanged;
+ sd_bus_slot *slot_connect;
+
+ char *path; /* DBus path e.g /org/freedesktop/ModemManager/Modem/0 */
+ char *manufacturer; /* The "Manufacturer" property */
+ char *model; /* The "Model" property */
+ char *port_name; /* MM_MODEM_PORT_TYPE_NET of Ports property */
+
+ Hashmap *bearers_by_path;
+ Hashmap *bearers_by_name;
+
+ MMModemState state;
+ MMModemStateFailedReason state_fail_reason;
+ ModemReconnectState reconnect_state;
+} Modem;
+
+int bearer_new(Modem *modem, const char *path, Bearer **ret);
+Bearer* bearer_free(Bearer *b);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Bearer*, bearer_free);
+
+int bearer_set_name(Bearer *b, const char *name);
+
+int bearer_get_by_path(Manager *manager, const char *path, Modem **ret_modem, Bearer **ret_bearer);
+int link_get_bearer(Link *link, Bearer **ret);
+
+int link_dhcp_enabled_by_bearer(Link *link, int family);
+
+int link_apply_bearer(Link *link);
+int bearer_update_link(Bearer *b);
+void bearer_drop(Bearer *b);
+
+int modem_new(Manager *m, const char *path, Modem **ret);
+Modem* modem_free(Modem *modem);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Modem*, modem_free);
+
+int modem_get_by_path(Manager *m, const char *path, Modem **ret);
+int link_get_modem(Link *link, Modem **ret);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_mm_route_metric);
diff --git a/tools/command_ignorelist b/tools/command_ignorelist
index 5859e5def9..548acff8bd 100644
--- a/tools/command_ignorelist
+++ b/tools/command_ignorelist
@@ -573,3 +573,4 @@ loader.conf.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="rebo
loader.conf.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="secure-boot-enroll-action"]/listitem/variablelist/varlistentry[term="reboot"]
loader.conf.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="secure-boot-enroll-action"]/listitem/variablelist/varlistentry[term="shutdown"]
varlinkctl.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="--system"]
+systemd.network.xml ./refsect1[title="[ModemManager] Section Options"]/variablelist/varlistentry[term="SimpleConnectProperties="]