]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
network: add ModemManager support
authorOleksandr Andrushchenko <andr2000@gmail.com>
Wed, 3 Sep 2025 07:20:24 +0000 (10:20 +0300)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Mon, 16 Feb 2026 04:22:21 +0000 (13:22 +0900)
[Match]
Name=wwan*

[Network]
LLDP=no
LinkLocalAddressing=no
IPv6AcceptRA=no

[ModemManager]
SimpleConnectPropertie]s=apn=internet ip-type=ipv4 allow-roaming=no pin=1111 operator-id=25503
RouteMetric=200
UseGateway=yes

Co-authored-by: Yu Watanabe <watanabe.yu+github@gmail.com>
20 files changed:
man/systemd.network.xml
src/network/meson.build
src/network/networkd-dhcp4.c
src/network/networkd-dhcp6.c
src/network/networkd-json.c
src/network/networkd-link.c
src/network/networkd-link.h
src/network/networkd-manager.c
src/network/networkd-manager.h
src/network/networkd-network-gperf.gperf
src/network/networkd-network.c
src/network/networkd-network.h
src/network/networkd-state-file.c
src/network/networkd-util.c
src/network/networkd-util.h
src/network/networkd-wwan-bus.c [new file with mode: 0644]
src/network/networkd-wwan-bus.h [new file with mode: 0644]
src/network/networkd-wwan.c [new file with mode: 0644]
src/network/networkd-wwan.h [new file with mode: 0644]
tools/command_ignorelist

index b00c2c67957029d6a30dd071a0355ea49b555abe..3b08a292e0df0833bb1436458db0f23ed30f5216 100644 (file)
@@ -6444,6 +6444,117 @@ ServerAddress=192.168.0.1/24</programlisting>
     </variablelist>
   </refsect1>
 
+  <refsect1>
+    <title>[ModemManager] Section Options</title>
+
+    <para>This section configures the default setting of the ModemManager integration. See
+    <ulink url="https://modemmanager.org/docs/modemmanager/" /> for more information about ModemManager.</para>
+
+    <para>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):
+      <programlisting>[Network]
+LLDP=no
+LinkLocalAddressing=no
+IPv6AcceptRA=no
+      </programlisting>
+    </para>
+
+    <para>The following options are available in the [ModemManager] section:</para>
+    <variablelist class='network-directives'>
+      <varlistentry>
+        <term><varname>SimpleConnectProperties=</varname></term>
+        <listitem>
+          <para>Specifies the white-space separated list of simple connect properties used to connect a modem. See
+          <ulink url="https://www.freedesktop.org/software/ModemManager/man/latest/mmcli.1.html" /> for more
+          information about simple connect. If no properties provided then the connection is not initiated.</para>
+
+          <varlistentry>
+            <term><option>apn</option>=<replaceable>NAME</replaceable></term>
+            <listitem><para>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.</para>
+
+            <xi:include href="version-info.xml" xpointer="v260"/></listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><option>allowed-auth</option>=<replaceable>METHOD</replaceable></term>
+            <listitem><para>Authentication method to use. Takes one of "none", "pap", "chap", "mschap", "mschapv2" or "eap".
+            Optional in 3GPP.</para>
+
+            <xi:include href="version-info.xml" xpointer="v260"/></listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><option>user</option>=<replaceable>NAME</replaceable></term>
+            <listitem><para>User name (if any) required by the network. Optional in 3GPP.</para>
+
+            <xi:include href="version-info.xml" xpointer="v260"/></listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><option>password</option>=<replaceable>PASSWORD</replaceable></term>
+            <listitem><para>Password (if any) required by the network. Optional in 3GPP.</para>
+
+            <xi:include href="version-info.xml" xpointer="v260"/></listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><option>ip-type</option>=<replaceable>TYPE</replaceable></term>
+            <listitem><para>Addressing type. Takes one of "none", "ipv4", "ipv6", "ipv4v6" or "any".
+            Optional in 3GPP and CDMA.</para>
+
+            <xi:include href="version-info.xml" xpointer="v260"/></listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><option>allow-roaming</option>=<replaceable>BOOL</replaceable></term>
+            <listitem><para>A boolean. When true, connection is allowed during roaming. When false,
+            connection is not allowed during roaming. Optional in 3GPP.</para>
+
+            <xi:include href="version-info.xml" xpointer="v260"/></listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><option>pin</option>=<replaceable>PIN</replaceable></term>
+            <listitem><para>SIM-PIN unlock code.</para>
+
+            <xi:include href="version-info.xml" xpointer="v260"/></listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><option>operator-id</option>=<replaceable>ID</replaceable></term>
+            <listitem><para>ETSI MCC-MNC of a network to force registration.</para>
+
+            <xi:include href="version-info.xml" xpointer="v260"/></listitem>
+          </varlistentry>
+
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><varname>RouteMetric=</varname></term>
+        <listitem>
+          <para>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.</para>
+
+          <xi:include href="version-info.xml" xpointer="v260"/>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><varname>UseGateway=</varname></term>
+        <listitem>
+          <para>When true (the default), the router address will be configured as the default gateway.</para>
+
+          <xi:include href="version-info.xml" xpointer="v260"/>
+        </listitem>
+      </varlistentry>
+
+    </variablelist>
+  </refsect1>
+
   <refsect1>
     <title>Examples</title>
     <example>
@@ -6771,6 +6882,29 @@ Xfrm=xfrm0</programlisting>
       This allows hardware based ipsec offloading to the <literal>eth0</literal> nic.
       If offloading is not needed, xfrm interfaces can be assigned to the <literal>lo</literal> device.
       </para>
+
+    </example>
+
+    <example>
+      <title>Connecting to a cellular network with ModemManager</title>
+      <programlisting># /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</programlisting>
+
+      <para>This connects a cellular modem to a broadband network matched with the network interface <literal>wwan0</literal>,
+      with APN name <literal>internet</literal>, SIM card pin unlock code <literal>1111</literal> and sets up a default
+      gateway with route metric of 30.
+      </para>
     </example>
   </refsect1>
 
index d872510b24e2a58417e12d37ab7a08fe5662ac2f..4457e836444b004e7ef297063f30d29a056a0f85 100644 (file)
@@ -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',
index 102047a4e4303ea21e374fef3c3247279feda739..f274a0c4d94a096214741badf6e8d678b0397d5a 100644 (file)
@@ -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;
 
index 5da83d5aeb3c53b6aff9fd2b5acd1f533ae6a6ef..26ab7ceb52944715b336827495430d4e5f64672a 100644 (file)
@@ -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;
 
index 584f07249d4b2771ec7911bb47cb106cb39441e8..123594179cbff69b3f3a8e25e413cf98fd5a51da 100644 (file)
@@ -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;
index 2a19a6003b9da7f36dfc277c3aa019ede241d0e2..cb79ddb9eb74a942a592b68c486be78eda8e863b 100644 (file)
@@ -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;
 
index 845b6f211f9cb54df69bb6c6042c69fa6127d2a0..c5b9421bc0b2b396b5c1adbb6496943555bb9efa 100644 (file)
@@ -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;
 
index e3c794b82b8981973133e60c2463056a6a487451..dbda6311d9a9b807f00d24b3065e3680ad74fd18 100644 (file)
@@ -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);
index 4017a92abd965293912e2000d45c937ccfb36ea4..13467d963e58ab18d16d2b06838c15459ce83129 100644 (file)
@@ -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;
index 7f3f4c531134f26e15422cf75102a9ea171131e8..10566b7a4ed8541c34d4f48461c72546ae645e4f 100644 (file)
@@ -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
index 45456d677d2d8c7de46e898c7b025d52b1fa9bd4..8141a45432e45ff9570cec27b7569e72982b1773 100644 (file)
@@ -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);
 }
 
index b6534c9510af795d86dea9a98946041d79ee6b68..dcd9f68e781971d4ca35f77b54c901f1e917133f 100644 (file)
@@ -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);
index c57fc8462d6cf39d9b28b1a58198e2d849d0cadf..5e485f9895ff2e7446ebb38c3f8cb4d9090e979d 100644 (file)
@@ -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. */
index 4e1a12d8d3bc97b6c8bb6debb2a29f4d38c9254f..b818b6121de7cf93e0982bd05d6ddc55e6d63281 100644 (file)
 
 /* 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);
index 16bfb599023ab509e0d74e13951c4aff8142f4e7..e7da47d7b22041d0d3739e1fbf42944c87f45380 100644 (file)
@@ -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 (file)
index 0000000..e575272
--- /dev/null
@@ -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 (file)
index 0000000..a6697cc
--- /dev/null
@@ -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 (file)
index 0000000..64dd37e
--- /dev/null
@@ -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:
+         * <msg> [1755871777.322239] [modem2] state changed (registered -> connecting)
+         * <dbg> [1755871777.325012] [modem2/bearer5] launching connection with QMI port (cdc-wdm0) and data port (wwan0) (multiplex none)
+         * <dbg> [1755871777.327665] [cdc-wdm0/qmi] bringing down data interface 'wwan0'
+         * <dbg> [1755871777.330108] [modem2/wwan0/net] interface index: 9
+         * <dbg> [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 (file)
index 0000000..962f76b
--- /dev/null
@@ -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);
index 5859e5def946b0b4c1e8d55aec577e178392e8e9..548acff8bd03416727084429a66822a612b35b06 100644 (file)
@@ -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="]