summary refs log tree commit diff
path: root/src/core/devices/wwan
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2021-02-11 18:11:46 +0100
committerMichael Biebl <biebl@debian.org>2021-02-11 18:11:46 +0100
commit80ec1decc49c72efec2a8b87c06245c92c0ab807 (patch)
treee3b229aa94e8dcf0590f2317664176e7b8f7607b /src/core/devices/wwan
parent65f86e8f56267192d42f2b629fc6b0c99fb9cd0c (diff)
New upstream version 1.29.90 upstream/1.29.90
Diffstat (limited to 'src/core/devices/wwan')
-rw-r--r--src/core/devices/wwan/libnm-wwan.ver41
-rw-r--r--src/core/devices/wwan/meson.build88
-rw-r--r--src/core/devices/wwan/nm-device-modem.c957
-rw-r--r--src/core/devices/wwan/nm-device-modem.h36
-rw-r--r--src/core/devices/wwan/nm-modem-broadband.c1625
-rw-r--r--src/core/devices/wwan/nm-modem-broadband.h29
-rw-r--r--src/core/devices/wwan/nm-modem-manager.c876
-rw-r--r--src/core/devices/wwan/nm-modem-manager.h45
-rw-r--r--src/core/devices/wwan/nm-modem-ofono.c1239
-rw-r--r--src/core/devices/wwan/nm-modem-ofono.h35
-rw-r--r--src/core/devices/wwan/nm-modem.c2037
-rw-r--r--src/core/devices/wwan/nm-modem.h269
-rw-r--r--src/core/devices/wwan/nm-service-providers.c455
-rw-r--r--src/core/devices/wwan/nm-service-providers.h24
-rw-r--r--src/core/devices/wwan/nm-wwan-factory.c146
-rw-r--r--src/core/devices/wwan/tests/test-service-providers.c130
-rw-r--r--src/core/devices/wwan/tests/test-service-providers.xml73
17 files changed, 8105 insertions, 0 deletions
diff --git a/src/core/devices/wwan/libnm-wwan.ver b/src/core/devices/wwan/libnm-wwan.ver
new file mode 100644
index 00000000..c368a590
--- /dev/null
+++ b/src/core/devices/wwan/libnm-wwan.ver
@@ -0,0 +1,41 @@
+{
+global:
+	nm_modem_act_stage1_prepare;
+	nm_modem_act_stage2_config;
+	nm_modem_check_connection_compatible;
+	nm_modem_claim;
+	nm_modem_complete_connection;
+	nm_modem_deactivate;
+	nm_modem_deactivate_async;
+	nm_modem_device_state_changed;
+	nm_modem_get_apn;
+	nm_modem_get_capabilities;
+	nm_modem_get_configured_mtu;
+	nm_modem_get_control_port;
+	nm_modem_get_device_id;
+	nm_modem_get_driver;
+	nm_modem_get_iid;
+	nm_modem_get_ip_ifindex;
+	nm_modem_get_operator_code;
+	nm_modem_get_path;
+	nm_modem_get_secrets;
+	nm_modem_get_state;
+	nm_modem_get_type;
+	nm_modem_get_uid;
+	nm_modem_ip4_pre_commit;
+	nm_modem_is_claimed;
+	nm_modem_manager_get;
+	nm_modem_manager_get_modems;
+	nm_modem_manager_get_type;
+	nm_modem_manager_name_owner_get;
+	nm_modem_manager_name_owner_ref;
+	nm_modem_manager_name_owner_unref;
+	nm_modem_owns_port;
+	nm_modem_set_mm_enabled;
+	nm_modem_stage3_ip4_config_start;
+	nm_modem_stage3_ip6_config_start;
+	nm_modem_state_to_string;
+	nm_modem_unclaim;
+local:
+	*;
+};
diff --git a/src/core/devices/wwan/meson.build b/src/core/devices/wwan/meson.build
new file mode 100644
index 00000000..87af0429
--- /dev/null
+++ b/src/core/devices/wwan/meson.build
@@ -0,0 +1,88 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+wwan_inc = include_directories('.')
+
+linker_script = join_paths(meson.current_source_dir(), 'libnm-wwan.ver')
+
+libnm_wwan = shared_module(
+  'nm-wwan',
+  sources: files(
+  'nm-service-providers.c',
+  'nm-modem-broadband.c',
+  'nm-modem.c',
+  'nm-modem-manager.c',
+  ) + (enable_ofono ? files('nm-modem-ofono.c') : files()),
+  dependencies: [
+    core_plugin_dep,
+    libsystemd_dep,
+    mm_glib_dep,
+  ],
+  c_args: daemon_c_flags,
+  link_args: '-Wl,--version-script,@0@'.format(linker_script),
+  link_depends: linker_script,
+  install: true,
+  install_dir: nm_plugindir,
+)
+
+libnm_wwan_dep = declare_dependency(
+  include_directories: wwan_inc,
+  link_with: libnm_wwan,
+)
+
+core_plugins += libnm_wwan
+
+test(
+  'check-wwan',
+  check_exports,
+  args: [
+    libnm_wwan.full_path(),
+    linker_script,
+  ],
+)
+
+libnm_device_plugin_wwan = shared_module(
+  'nm-device-plugin-wwan',
+  sources: files(
+    'nm-device-modem.c',
+    'nm-wwan-factory.c',
+  ),
+  dependencies: [
+    core_plugin_dep,
+    libsystemd_dep,
+    mm_glib_dep,
+  ],
+  c_args: daemon_c_flags,
+  link_with: libnm_wwan,
+  link_args: ldflags_linker_script_devices,
+  link_depends: linker_script_devices,
+  install: true,
+  install_dir: nm_plugindir,
+  install_rpath: nm_plugindir,
+)
+
+core_plugins += libnm_device_plugin_wwan
+
+run_target(
+  'check-local-devices-wwan',
+  command: [check_exports, libnm_device_plugin_wwan.full_path(), linker_script_devices],
+  depends: libnm_device_plugin_wwan,
+)
+
+if enable_tests
+  exe = executable(
+    'test-service-providers',
+    files(
+      'tests/test-service-providers.c',
+      'nm-service-providers.c',
+    ),
+    include_directories: wwan_inc,
+    dependencies: libNetworkManagerTest_dep,
+    c_args: test_c_flags,
+  )
+  test(
+    'wwan/test-service-providers',
+    test_script,
+    timeout: default_test_timeout,
+    args: test_args + [exe.full_path()],
+  )
+endif
diff --git a/src/core/devices/wwan/nm-device-modem.c b/src/core/devices/wwan/nm-device-modem.c
new file mode 100644
index 00000000..3ea89d2c
--- /dev/null
+++ b/src/core/devices/wwan/nm-device-modem.c
@@ -0,0 +1,957 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2009 - 2019 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-modem.h"
+
+#include "nm-modem.h"
+#include "nm-ip4-config.h"
+#include "devices/nm-device-private.h"
+#include "nm-rfkill-manager.h"
+#include "settings/nm-settings-connection.h"
+#include "nm-modem-broadband.h"
+#include "NetworkManagerUtils.h"
+#include "nm-core-internal.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceModem
+#include "devices/nm-device-logging.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceModem,
+                             PROP_MODEM,
+                             PROP_CAPABILITIES,
+                             PROP_CURRENT_CAPABILITIES,
+                             PROP_DEVICE_ID,
+                             PROP_OPERATOR_CODE,
+                             PROP_APN, );
+
+typedef struct {
+    NMModem *                 modem;
+    NMDeviceModemCapabilities caps;
+    NMDeviceModemCapabilities current_caps;
+    char *                    device_id;
+    char *                    operator_code;
+    char *                    apn;
+    bool                      rf_enabled : 1;
+    NMDeviceStageState        stage1_state : 3;
+} NMDeviceModemPrivate;
+
+struct _NMDeviceModem {
+    NMDevice             parent;
+    NMDeviceModemPrivate _priv;
+};
+
+struct _NMDeviceModemClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceModem, nm_device_modem, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_MODEM_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceModem, NM_IS_DEVICE_MODEM, NMDevice)
+
+/*****************************************************************************/
+
+static void
+ppp_failed(NMModem *modem, guint i_reason, gpointer user_data)
+{
+    NMDevice *          device = NM_DEVICE(user_data);
+    NMDeviceModem *     self   = NM_DEVICE_MODEM(user_data);
+    NMDeviceStateReason reason = i_reason;
+
+    switch (nm_device_get_state(device)) {
+    case NM_DEVICE_STATE_PREPARE:
+    case NM_DEVICE_STATE_CONFIG:
+    case NM_DEVICE_STATE_NEED_AUTH:
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, reason);
+        break;
+    case NM_DEVICE_STATE_IP_CONFIG:
+    case NM_DEVICE_STATE_IP_CHECK:
+    case NM_DEVICE_STATE_SECONDARIES:
+    case NM_DEVICE_STATE_ACTIVATED:
+        if (nm_device_activate_ip4_state_in_conf(device))
+            nm_device_activate_schedule_ip_config_timeout(device, AF_INET);
+        else if (nm_device_activate_ip6_state_in_conf(device))
+            nm_device_activate_schedule_ip_config_timeout(device, AF_INET6);
+        else if (nm_device_activate_ip4_state_done(device)) {
+            nm_device_ip_method_failed(device,
+                                       AF_INET,
+                                       NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        } else if (nm_device_activate_ip6_state_done(device)) {
+            nm_device_ip_method_failed(device,
+                                       AF_INET6,
+                                       NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        } else {
+            _LOGW(LOGD_MB,
+                  "PPP failure in unexpected state %u",
+                  (guint) nm_device_get_state(device));
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        }
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+modem_prepare_result(NMModem *modem, gboolean success, guint i_reason, gpointer user_data)
+{
+    NMDeviceModem *       self   = NM_DEVICE_MODEM(user_data);
+    NMDeviceModemPrivate *priv   = NM_DEVICE_MODEM_GET_PRIVATE(self);
+    NMDevice *            device = NM_DEVICE(self);
+    NMDeviceStateReason   reason = i_reason;
+
+    if (nm_device_get_state(device) != NM_DEVICE_STATE_PREPARE
+        || priv->stage1_state != NM_DEVICE_STAGE_STATE_PENDING) {
+        nm_assert_not_reached();
+        success = FALSE;
+    }
+
+    if (!success) {
+        /* There are several reasons to block autoconnection at device level:
+         *
+         *  - Wrong SIM-PIN: The device won't autoconnect because it doesn't make sense
+         *    to retry the connection with the same PIN. This error also makes autoconnection
+         *    blocked at settings level, so not even a modem unplug and replug will allow
+         *    autoconnection again. It is somewhat redundant to block autoconnection at
+         *    both device and setting level really.
+         *
+         *  - SIM wrong or not inserted: If the modem is reporting a SIM not inserted error,
+         *    we can block autoconnection at device level, so that if the same device is
+         *    unplugged and replugged with a SIM (or if a SIM hotplug event happens in MM,
+         *    recreating the device completely), we can try the autoconnection again.
+         *
+         *  - Modem initialization failed: For some reason unknown to NM, the modem wasn't
+         *    initialized correctly, which leads to an unusable device. A device unplug and
+         *    replug may solve the issue, so make it a device-level autoconnection blocking
+         *    reason.
+         */
+        switch (nm_device_state_reason_check(reason)) {
+        case NM_DEVICE_STATE_REASON_GSM_SIM_PIN_REQUIRED:
+        case NM_DEVICE_STATE_REASON_GSM_SIM_PUK_REQUIRED:
+        case NM_DEVICE_STATE_REASON_SIM_PIN_INCORRECT:
+            nm_device_autoconnect_blocked_set(device, NM_DEVICE_AUTOCONNECT_BLOCKED_WRONG_PIN);
+            break;
+        case NM_DEVICE_STATE_REASON_GSM_SIM_NOT_INSERTED:
+        case NM_DEVICE_STATE_REASON_GSM_SIM_WRONG:
+            nm_device_autoconnect_blocked_set(device, NM_DEVICE_AUTOCONNECT_BLOCKED_SIM_MISSING);
+            break;
+        case NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED:
+            nm_device_autoconnect_blocked_set(device, NM_DEVICE_AUTOCONNECT_BLOCKED_INIT_FAILED);
+            break;
+        default:
+            break;
+        }
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, reason);
+        return;
+    }
+
+    priv->stage1_state = NM_DEVICE_STAGE_STATE_COMPLETED;
+    nm_device_activate_schedule_stage1_device_prepare(device, FALSE);
+}
+
+static void
+modem_auth_requested(NMModem *modem, gpointer user_data)
+{
+    NMDevice *device = NM_DEVICE(user_data);
+
+    /* Auth requests (PIN, PAP/CHAP passwords, etc) only get handled
+     * during activation.
+     */
+    if (!nm_device_is_activating(device))
+        return;
+
+    nm_device_state_changed(device, NM_DEVICE_STATE_NEED_AUTH, NM_DEVICE_STATE_REASON_NONE);
+}
+
+static void
+modem_auth_result(NMModem *modem, GError *error, gpointer user_data)
+{
+    NMDevice *            device = NM_DEVICE(user_data);
+    NMDeviceModemPrivate *priv   = NM_DEVICE_MODEM_GET_PRIVATE(device);
+
+    g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_NEED_AUTH);
+
+    if (error) {
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS);
+        return;
+    }
+
+    priv->stage1_state = NM_DEVICE_STAGE_STATE_INIT;
+    nm_device_activate_schedule_stage1_device_prepare(device, FALSE);
+}
+
+static void
+modem_ip4_config_result(NMModem *modem, NMIP4Config *config, GError *error, gpointer user_data)
+{
+    NMDeviceModem *self   = NM_DEVICE_MODEM(user_data);
+    NMDevice *     device = NM_DEVICE(self);
+
+    g_return_if_fail(nm_device_activate_ip4_state_in_conf(device) == TRUE);
+
+    if (error) {
+        _LOGW(LOGD_MB | LOGD_IP4, "retrieving IPv4 configuration failed: %s", error->message);
+        nm_device_ip_method_failed(device, AF_INET, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+    } else {
+        nm_device_set_dev2_ip_config(device, AF_INET, NM_IP_CONFIG_CAST(config));
+        nm_device_activate_schedule_ip_config_result(device, AF_INET, NULL);
+    }
+}
+
+static void
+modem_ip6_config_result(NMModem *    modem,
+                        NMIP6Config *config,
+                        gboolean     do_slaac,
+                        GError *     error,
+                        gpointer     user_data)
+{
+    NMDeviceModem *     self   = NM_DEVICE_MODEM(user_data);
+    NMDevice *          device = NM_DEVICE(self);
+    NMActStageReturn    ret;
+    NMDeviceStateReason failure_reason      = NM_DEVICE_STATE_REASON_NONE;
+    gs_unref_object NMIP6Config *ignored    = NULL;
+    gboolean                     got_config = !!config;
+
+    g_return_if_fail(nm_device_activate_ip6_state_in_conf(device) == TRUE);
+
+    if (error) {
+        _LOGW(LOGD_MB | LOGD_IP6, "retrieving IPv6 configuration failed: %s", error->message);
+        nm_device_ip_method_failed(device, AF_INET6, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        return;
+    }
+
+    /* Re-enable IPv6 on the interface */
+    nm_device_sysctl_ip_conf_set(device, AF_INET6, "disable_ipv6", "0");
+
+    if (config)
+        nm_device_set_dev2_ip_config(device, AF_INET6, NM_IP_CONFIG_CAST(config));
+
+    if (do_slaac == FALSE) {
+        if (got_config)
+            nm_device_activate_schedule_ip_config_result(device, AF_INET6, NULL);
+        else {
+            _LOGW(LOGD_MB | LOGD_IP6,
+                  "retrieving IPv6 configuration failed: SLAAC not requested and no addresses");
+            nm_device_ip_method_failed(device,
+                                       AF_INET6,
+                                       NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        }
+        return;
+    }
+
+    /* Start SLAAC now that we have a link-local address from the modem */
+    ret =
+        NM_DEVICE_CLASS(nm_device_modem_parent_class)
+            ->act_stage3_ip_config_start(device, AF_INET6, (gpointer *) &ignored, &failure_reason);
+
+    nm_assert(ignored == NULL);
+
+    switch (ret) {
+    case NM_ACT_STAGE_RETURN_FAILURE:
+        nm_device_ip_method_failed(device, AF_INET6, failure_reason);
+        break;
+    case NM_ACT_STAGE_RETURN_IP_FAIL:
+        /* all done */
+        nm_device_activate_schedule_ip_config_result(device, AF_INET6, NULL);
+        break;
+    case NM_ACT_STAGE_RETURN_POSTPONE:
+        /* let SLAAC run */
+        break;
+    default:
+        /* Should never get here since we've assured that the IPv6 method
+         * will either be "auto" or "ignored" when starting IPv6 configuration.
+         */
+        nm_assert_not_reached();
+    }
+}
+
+static void
+ip_ifindex_changed_cb(NMModem *modem, GParamSpec *pspec, gpointer user_data)
+{
+    NMDevice *device = NM_DEVICE(user_data);
+
+    if (!nm_device_is_activating(device))
+        return;
+
+    if (!nm_device_set_ip_ifindex(device, nm_modem_get_ip_ifindex(modem))) {
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        return;
+    }
+
+    /* Disable IPv6 immediately on the interface since NM handles IPv6
+     * internally, and leaving it enabled could allow the kernel's IPv6
+     * RA handling code to run before NM is ready.
+     */
+    nm_device_sysctl_ip_conf_set(device, AF_INET6, "disable_ipv6", "1");
+}
+
+static void
+operator_code_changed_cb(NMModem *modem, GParamSpec *pspec, gpointer user_data)
+{
+    NMDeviceModem *       self          = NM_DEVICE_MODEM(user_data);
+    NMDeviceModemPrivate *priv          = NM_DEVICE_MODEM_GET_PRIVATE(self);
+    const char *          operator_code = nm_modem_get_operator_code(modem);
+
+    if (g_strcmp0(priv->operator_code, operator_code) != 0) {
+        g_free(priv->operator_code);
+        priv->operator_code = g_strdup(operator_code);
+        _notify(self, PROP_OPERATOR_CODE);
+    }
+}
+
+static void
+apn_changed_cb(NMModem *modem, GParamSpec *pspec, gpointer user_data)
+{
+    NMDeviceModem *       self = NM_DEVICE_MODEM(user_data);
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self);
+    const char *          apn  = nm_modem_get_apn(modem);
+
+    if (g_strcmp0(priv->apn, apn) != 0) {
+        g_free(priv->apn);
+        priv->apn = g_strdup(apn);
+        _notify(self, PROP_APN);
+    }
+}
+
+static void
+ids_changed_cb(NMModem *modem, GParamSpec *pspec, gpointer user_data)
+{
+    nm_device_recheck_available_connections(NM_DEVICE(user_data));
+}
+
+static void
+modem_state_cb(NMModem *modem, int new_state_i, int old_state_i, gpointer user_data)
+{
+    NMModemState          new_state = new_state_i;
+    NMModemState          old_state = old_state_i;
+    NMDevice *            device    = NM_DEVICE(user_data);
+    NMDeviceModemPrivate *priv      = NM_DEVICE_MODEM_GET_PRIVATE(device);
+    NMDeviceState         dev_state = nm_device_get_state(device);
+
+    if (new_state <= NM_MODEM_STATE_DISABLING && old_state > NM_MODEM_STATE_DISABLING
+        && priv->rf_enabled) {
+        /* Called when the ModemManager modem enabled state is changed externally
+         * to NetworkManager (eg something using MM's D-Bus API directly).
+         */
+        if (nm_device_is_activating(device) || dev_state == NM_DEVICE_STATE_ACTIVATED) {
+            /* user-initiated action, hence DISCONNECTED not FAILED */
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_DISCONNECTED,
+                                    NM_DEVICE_STATE_REASON_USER_REQUESTED);
+            return;
+        }
+    }
+
+    if (new_state < NM_MODEM_STATE_CONNECTING && old_state >= NM_MODEM_STATE_CONNECTING
+        && dev_state >= NM_DEVICE_STATE_NEED_AUTH && dev_state <= NM_DEVICE_STATE_ACTIVATED) {
+        /* Fail the device if the modem disconnects unexpectedly while the
+         * device is activating/activated. */
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER);
+        return;
+    }
+
+    if (new_state > NM_MODEM_STATE_LOCKED && old_state == NM_MODEM_STATE_LOCKED) {
+        /* If the modem is now unlocked, enable/disable it according to the
+         * device's enabled/disabled state.
+         */
+        nm_modem_set_mm_enabled(priv->modem, priv->rf_enabled);
+
+        if (dev_state == NM_DEVICE_STATE_NEED_AUTH) {
+            /* The modem was unlocked externally to NetworkManager,
+             * deactivate so the default connection can be
+             * automatically activated again */
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_DEACTIVATING,
+                                    NM_DEVICE_STATE_REASON_MODEM_AVAILABLE);
+        }
+
+        /* Now allow connections without a PIN to be available */
+        nm_device_recheck_available_connections(device);
+    }
+
+    nm_device_queue_recheck_available(device,
+                                      NM_DEVICE_STATE_REASON_MODEM_AVAILABLE,
+                                      NM_DEVICE_STATE_REASON_MODEM_FAILED);
+}
+
+static void
+modem_removed_cb(NMModem *modem, gpointer user_data)
+{
+    g_signal_emit_by_name(NM_DEVICE(user_data), NM_DEVICE_REMOVED);
+}
+
+/*****************************************************************************/
+
+static gboolean
+owns_iface(NMDevice *device, const char *iface)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device);
+
+    g_return_val_if_fail(priv->modem, FALSE);
+
+    return nm_modem_owns_port(priv->modem, iface);
+}
+
+/*****************************************************************************/
+
+static void
+device_state_changed(NMDevice *          device,
+                     NMDeviceState       new_state,
+                     NMDeviceState       old_state,
+                     NMDeviceStateReason reason)
+{
+    NMDeviceModem *       self = NM_DEVICE_MODEM(device);
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->modem);
+
+    if (new_state == NM_DEVICE_STATE_UNAVAILABLE && old_state < NM_DEVICE_STATE_UNAVAILABLE) {
+        /* Log initial modem state */
+        _LOGI(LOGD_MB,
+              "modem state '%s'",
+              nm_modem_state_to_string(nm_modem_get_state(priv->modem)));
+    }
+    nm_modem_device_state_changed(priv->modem, new_state, old_state);
+}
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *device)
+{
+    return NM_DEVICE_CAP_IS_NON_KERNEL;
+}
+
+static const char *
+get_type_description(NMDevice *device)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device);
+
+    if (NM_FLAGS_HAS(priv->current_caps, NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS))
+        return "gsm";
+    if (NM_FLAGS_HAS(priv->current_caps, NM_DEVICE_MODEM_CAPABILITY_CDMA_EVDO))
+        return "cdma";
+    return NM_DEVICE_CLASS(nm_device_modem_parent_class)->get_type_description(device);
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    GError *local = NULL;
+
+    if (!NM_DEVICE_CLASS(nm_device_modem_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    if (!nm_modem_check_connection_compatible(NM_DEVICE_MODEM_GET_PRIVATE(device)->modem,
+                                              connection,
+                                              error ? &local : NULL)) {
+        if (error) {
+            g_set_error(error,
+                        NM_UTILS_ERROR,
+                        g_error_matches(local,
+                                        NM_UTILS_ERROR,
+                                        NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE)
+                            ? NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE
+                            : NM_UTILS_ERROR_UNKNOWN,
+                        "modem is incompatible with connection: %s",
+                        local->message);
+            g_error_free(local);
+        }
+        return FALSE;
+    }
+    return TRUE;
+}
+
+static gboolean
+check_connection_available(NMDevice *                     device,
+                           NMConnection *                 connection,
+                           NMDeviceCheckConAvailableFlags flags,
+                           const char *                   specific_object,
+                           GError **                      error)
+{
+    NMDeviceModem *       self = NM_DEVICE_MODEM(device);
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self);
+    NMModemState          state;
+
+    if (!priv->rf_enabled) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "RFKILL for modem enabled");
+        return FALSE;
+    }
+
+    if (!priv->modem) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "modem not available");
+        return FALSE;
+    }
+
+    state = nm_modem_get_state(priv->modem);
+    if (state <= NM_MODEM_STATE_INITIALIZING) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "modem not initialized");
+        return FALSE;
+    }
+
+    if (state == NM_MODEM_STATE_LOCKED) {
+        if (!nm_connection_get_setting_gsm(connection)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "modem is locked without pin available");
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device);
+
+    return nm_modem_complete_connection(priv->modem,
+                                        nm_device_get_iface(device),
+                                        connection,
+                                        existing_connections,
+                                        error);
+}
+
+static void
+deactivate(NMDevice *device)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device);
+
+    nm_modem_deactivate(priv->modem, device);
+    priv->stage1_state = NM_DEVICE_STAGE_STATE_INIT;
+}
+
+/*****************************************************************************/
+
+static void
+modem_deactivate_async_cb(NMModem *modem, GError *error, gpointer user_data)
+{
+    gs_unref_object NMDevice * self = NULL;
+    NMDeviceDeactivateCallback callback;
+    gpointer                   callback_user_data;
+
+    nm_utils_user_data_unpack(user_data, &self, &callback, &callback_user_data);
+    callback(self, error, callback_user_data);
+}
+
+static void
+deactivate_async(NMDevice *                 self,
+                 GCancellable *             cancellable,
+                 NMDeviceDeactivateCallback callback,
+                 gpointer                   user_data)
+{
+    nm_assert(G_IS_CANCELLABLE(cancellable));
+    nm_assert(callback);
+
+    nm_modem_deactivate_async(NM_DEVICE_MODEM_GET_PRIVATE(self)->modem,
+                              self,
+                              cancellable,
+                              modem_deactivate_async_cb,
+                              nm_utils_user_data_pack(g_object_ref(self), callback, user_data));
+}
+
+/*****************************************************************************/
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device);
+    NMActRequest *        req;
+
+    req = nm_device_get_act_request(device);
+    g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE);
+
+    if (priv->stage1_state == NM_DEVICE_STAGE_STATE_INIT) {
+        priv->stage1_state = NM_DEVICE_STAGE_STATE_PENDING;
+        return nm_modem_act_stage1_prepare(NM_DEVICE_MODEM_GET_PRIVATE(device)->modem,
+                                           req,
+                                           out_failure_reason);
+    }
+
+    if (priv->stage1_state == NM_DEVICE_STAGE_STATE_PENDING)
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+
+    nm_assert(priv->stage1_state == NM_DEVICE_STAGE_STATE_COMPLETED);
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static NMActStageReturn
+act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    nm_modem_act_stage2_config(NM_DEVICE_MODEM_GET_PRIVATE(device)->modem);
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static NMActStageReturn
+act_stage3_ip_config_start(NMDevice *           device,
+                           int                  addr_family,
+                           gpointer *           out_config,
+                           NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device);
+
+    nm_assert_addr_family(addr_family);
+
+    if (addr_family == AF_INET) {
+        return nm_modem_stage3_ip4_config_start(priv->modem,
+                                                device,
+                                                NM_DEVICE_CLASS(nm_device_modem_parent_class),
+                                                out_failure_reason);
+    } else {
+        return nm_modem_stage3_ip6_config_start(priv->modem, device, out_failure_reason);
+    }
+}
+
+static void
+ip4_config_pre_commit(NMDevice *device, NMIP4Config *config)
+{
+    nm_modem_ip4_pre_commit(NM_DEVICE_MODEM_GET_PRIVATE(device)->modem, device, config);
+}
+
+static gboolean
+get_ip_iface_identifier(NMDevice *device, NMUtilsIPv6IfaceId *out_iid)
+{
+    NMDeviceModem *       self = NM_DEVICE_MODEM(device);
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self);
+    gboolean              success;
+
+    g_return_val_if_fail(priv->modem, FALSE);
+    success = nm_modem_get_iid(priv->modem, out_iid);
+    if (!success)
+        success =
+            NM_DEVICE_CLASS(nm_device_modem_parent_class)->get_ip_iface_identifier(device, out_iid);
+    return success;
+}
+
+/*****************************************************************************/
+
+static gboolean
+get_enabled(NMDevice *device)
+{
+    NMDeviceModemPrivate *priv        = NM_DEVICE_MODEM_GET_PRIVATE(device);
+    NMModemState          modem_state = nm_modem_get_state(priv->modem);
+
+    return priv->rf_enabled && (modem_state >= NM_MODEM_STATE_LOCKED);
+}
+
+static void
+set_enabled(NMDevice *device, gboolean enabled)
+{
+    NMDeviceModem *       self = NM_DEVICE_MODEM(device);
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self);
+
+    /* Called only by the Manager in response to rfkill switch changes or
+     * global user WWAN enable/disable preference changes.
+     */
+    priv->rf_enabled = enabled;
+
+    if (priv->modem) {
+        /* Sync the ModemManager modem enabled/disabled with rfkill/user preference */
+        nm_modem_set_mm_enabled(priv->modem, enabled);
+    }
+
+    if (enabled == FALSE) {
+        nm_device_state_changed(device, NM_DEVICE_STATE_UNAVAILABLE, NM_DEVICE_STATE_REASON_NONE);
+    }
+}
+
+static gboolean
+is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
+{
+    NMDeviceModem *       self = NM_DEVICE_MODEM(device);
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self);
+    NMModemState          modem_state;
+
+    if (!priv->rf_enabled)
+        return FALSE;
+
+    g_assert(priv->modem);
+    modem_state = nm_modem_get_state(priv->modem);
+    if (modem_state <= NM_MODEM_STATE_INITIALIZING)
+        return FALSE;
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+set_modem(NMDeviceModem *self, NMModem *modem)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self);
+
+    g_return_if_fail(modem != NULL);
+
+    priv->modem = nm_modem_claim(modem);
+
+    g_signal_connect(modem, NM_MODEM_PPP_FAILED, G_CALLBACK(ppp_failed), self);
+    g_signal_connect(modem, NM_MODEM_PREPARE_RESULT, G_CALLBACK(modem_prepare_result), self);
+    g_signal_connect(modem, NM_MODEM_IP4_CONFIG_RESULT, G_CALLBACK(modem_ip4_config_result), self);
+    g_signal_connect(modem, NM_MODEM_IP6_CONFIG_RESULT, G_CALLBACK(modem_ip6_config_result), self);
+    g_signal_connect(modem, NM_MODEM_AUTH_REQUESTED, G_CALLBACK(modem_auth_requested), self);
+    g_signal_connect(modem, NM_MODEM_AUTH_RESULT, G_CALLBACK(modem_auth_result), self);
+    g_signal_connect(modem, NM_MODEM_STATE_CHANGED, G_CALLBACK(modem_state_cb), self);
+    g_signal_connect(modem, NM_MODEM_REMOVED, G_CALLBACK(modem_removed_cb), self);
+
+    g_signal_connect(modem,
+                     "notify::" NM_MODEM_IP_IFINDEX,
+                     G_CALLBACK(ip_ifindex_changed_cb),
+                     self);
+    g_signal_connect(modem, "notify::" NM_MODEM_DEVICE_ID, G_CALLBACK(ids_changed_cb), self);
+    g_signal_connect(modem, "notify::" NM_MODEM_SIM_ID, G_CALLBACK(ids_changed_cb), self);
+    g_signal_connect(modem, "notify::" NM_MODEM_SIM_OPERATOR_ID, G_CALLBACK(ids_changed_cb), self);
+    g_signal_connect(modem,
+                     "notify::" NM_MODEM_OPERATOR_CODE,
+                     G_CALLBACK(operator_code_changed_cb),
+                     self);
+    g_signal_connect(modem, "notify::" NM_MODEM_APN, G_CALLBACK(apn_changed_cb), self);
+}
+
+static guint32
+get_dhcp_timeout_for_device(NMDevice *device, int addr_family)
+{
+    /* DHCP is always done by the modem firmware, not by the network, and
+     * by the time we get around to DHCP the firmware should already know
+     * the IP addressing details.  So the DHCP timeout can be much shorter.
+     */
+    return 15;
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_MODEM:
+        g_value_set_object(value, priv->modem);
+        break;
+    case PROP_CAPABILITIES:
+        g_value_set_uint(value, priv->caps);
+        break;
+    case PROP_CURRENT_CAPABILITIES:
+        g_value_set_uint(value, priv->current_caps);
+        break;
+    case PROP_DEVICE_ID:
+        g_value_set_string(value, priv->device_id);
+        break;
+    case PROP_OPERATOR_CODE:
+        g_value_set_string(value, priv->operator_code);
+        break;
+    case PROP_APN:
+        g_value_set_string(value, priv->apn);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+static void
+set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_MODEM:
+        /* construct-only */
+        set_modem(NM_DEVICE_MODEM(object), g_value_get_object(value));
+        break;
+    case PROP_CAPABILITIES:
+        priv->caps = g_value_get_uint(value);
+        break;
+    case PROP_CURRENT_CAPABILITIES:
+        priv->current_caps = g_value_get_uint(value);
+        break;
+    case PROP_DEVICE_ID:
+        /* construct-only */
+        priv->device_id = g_value_dup_string(value);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_modem_init(NMDeviceModem *self)
+{}
+
+NMDevice *
+nm_device_modem_new(NMModem *modem)
+{
+    NMDeviceModemCapabilities caps         = NM_DEVICE_MODEM_CAPABILITY_NONE;
+    NMDeviceModemCapabilities current_caps = NM_DEVICE_MODEM_CAPABILITY_NONE;
+
+    g_return_val_if_fail(NM_IS_MODEM(modem), NULL);
+
+    /* Load capabilities */
+    nm_modem_get_capabilities(modem, &caps, &current_caps);
+
+    return g_object_new(NM_TYPE_DEVICE_MODEM,
+                        NM_DEVICE_UDI,
+                        nm_modem_get_path(modem),
+                        NM_DEVICE_IFACE,
+                        nm_modem_get_uid(modem),
+                        NM_DEVICE_DRIVER,
+                        nm_modem_get_driver(modem),
+                        NM_DEVICE_TYPE_DESC,
+                        "Broadband",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_MODEM,
+                        NM_DEVICE_RFKILL_TYPE,
+                        RFKILL_TYPE_WWAN,
+                        NM_DEVICE_MODEM_MODEM,
+                        modem,
+                        NM_DEVICE_MODEM_CAPABILITIES,
+                        caps,
+                        NM_DEVICE_MODEM_CURRENT_CAPABILITIES,
+                        current_caps,
+                        NM_DEVICE_MODEM_DEVICE_ID,
+                        nm_modem_get_device_id(modem),
+                        NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(object);
+
+    if (priv->modem) {
+        g_signal_handlers_disconnect_by_data(priv->modem, NM_DEVICE_MODEM(object));
+        nm_clear_pointer(&priv->modem, nm_modem_unclaim);
+    }
+
+    nm_clear_g_free(&priv->device_id);
+    nm_clear_g_free(&priv->operator_code);
+    nm_clear_g_free(&priv->apn);
+
+    G_OBJECT_CLASS(nm_device_modem_parent_class)->dispose(object);
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_modem = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_MODEM,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("ModemCapabilities",
+                                                             "u",
+                                                             NM_DEVICE_MODEM_CAPABILITIES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("CurrentCapabilities",
+                                                             "u",
+                                                             NM_DEVICE_MODEM_CURRENT_CAPABILITIES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("DeviceId",
+                                                           "s",
+                                                           NM_DEVICE_MODEM_DEVICE_ID),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("OperatorCode",
+                                                           "s",
+                                                           NM_DEVICE_MODEM_OPERATOR_CODE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Apn", "s", NM_DEVICE_MODEM_APN), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_modem_class_init(NMDeviceModemClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->dispose      = dispose;
+    object_class->get_property = get_property;
+    object_class->set_property = set_property;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_modem);
+
+    device_class->get_generic_capabilities    = get_generic_capabilities;
+    device_class->get_type_description        = get_type_description;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->check_connection_available  = check_connection_available;
+    device_class->complete_connection         = complete_connection;
+    device_class->deactivate_async            = deactivate_async;
+    device_class->deactivate                  = deactivate;
+    device_class->act_stage1_prepare          = act_stage1_prepare;
+    device_class->act_stage2_config           = act_stage2_config;
+    device_class->act_stage3_ip_config_start  = act_stage3_ip_config_start;
+    device_class->ip4_config_pre_commit       = ip4_config_pre_commit;
+    device_class->get_enabled                 = get_enabled;
+    device_class->set_enabled                 = set_enabled;
+    device_class->owns_iface                  = owns_iface;
+    device_class->is_available                = is_available;
+    device_class->get_ip_iface_identifier     = get_ip_iface_identifier;
+    device_class->get_configured_mtu          = nm_modem_get_configured_mtu;
+    device_class->get_dhcp_timeout_for_device = get_dhcp_timeout_for_device;
+
+    device_class->state_changed = device_state_changed;
+
+    obj_properties[PROP_MODEM] =
+        g_param_spec_object(NM_DEVICE_MODEM_MODEM,
+                            "",
+                            "",
+                            NM_TYPE_MODEM,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_CAPABILITIES] =
+        g_param_spec_uint(NM_DEVICE_MODEM_CAPABILITIES,
+                          "",
+                          "",
+                          0,
+                          G_MAXUINT32,
+                          NM_DEVICE_MODEM_CAPABILITY_NONE,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_CURRENT_CAPABILITIES] =
+        g_param_spec_uint(NM_DEVICE_MODEM_CURRENT_CAPABILITIES,
+                          "",
+                          "",
+                          0,
+                          G_MAXUINT32,
+                          NM_DEVICE_MODEM_CAPABILITY_NONE,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_DEVICE_ID] =
+        g_param_spec_string(NM_DEVICE_MODEM_DEVICE_ID,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_OPERATOR_CODE] =
+        g_param_spec_string(NM_DEVICE_MODEM_OPERATOR_CODE,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_APN] = g_param_spec_string(NM_DEVICE_MODEM_APN,
+                                                   "",
+                                                   "",
+                                                   NULL,
+                                                   G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
diff --git a/src/core/devices/wwan/nm-device-modem.h b/src/core/devices/wwan/nm-device-modem.h
new file mode 100644
index 00000000..f171d76f
--- /dev/null
+++ b/src/core/devices/wwan/nm-device-modem.h
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2011 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_MODEM_H__
+#define __NETWORKMANAGER_DEVICE_MODEM_H__
+
+#include "devices/nm-device.h"
+#include "nm-modem.h"
+
+#define NM_TYPE_DEVICE_MODEM (nm_device_modem_get_type())
+#define NM_DEVICE_MODEM(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_MODEM, NMDeviceModem))
+#define NM_DEVICE_MODEM_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_MODEM, NMDeviceModemClass))
+#define NM_IS_DEVICE_MODEM(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_MODEM))
+#define NM_IS_DEVICE_MODEM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_MODEM))
+#define NM_DEVICE_MODEM_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_MODEM, NMDeviceModemClass))
+
+#define NM_DEVICE_MODEM_MODEM                "modem"
+#define NM_DEVICE_MODEM_CAPABILITIES         "modem-capabilities"
+#define NM_DEVICE_MODEM_CURRENT_CAPABILITIES "current-capabilities"
+#define NM_DEVICE_MODEM_DEVICE_ID            "device-id"
+#define NM_DEVICE_MODEM_OPERATOR_CODE        "operator-code"
+#define NM_DEVICE_MODEM_APN                  "apn"
+
+typedef struct _NMDeviceModem      NMDeviceModem;
+typedef struct _NMDeviceModemClass NMDeviceModemClass;
+
+GType nm_device_modem_get_type(void);
+
+NMDevice *nm_device_modem_new(NMModem *modem);
+
+#endif /* __NETWORKMANAGER_DEVICE_MODEM_H__ */
diff --git a/src/core/devices/wwan/nm-modem-broadband.c b/src/core/devices/wwan/nm-modem-broadband.c
new file mode 100644
index 00000000..ca028804
--- /dev/null
+++ b/src/core/devices/wwan/nm-modem-broadband.c
@@ -0,0 +1,1625 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2012 Aleksander Morgado <aleksander@gnu.org>
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-modem-broadband.h"
+#include "nm-service-providers.h"
+
+#include <arpa/inet.h>
+#include <libmm-glib.h>
+
+#include "nm-core-internal.h"
+#include "NetworkManagerUtils.h"
+#include "devices/nm-device-private.h"
+#include "platform/nm-platform.h"
+#include "nm-ip4-config.h"
+#include "nm-ip6-config.h"
+
+#define NM_MODEM_BROADBAND_MODEM "modem"
+
+static gboolean
+MODEM_CAPS_3GPP(MMModemCapability caps)
+{
+    G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+    /* MM_MODEM_CAPABILITY_LTE_ADVANCED is marked as deprecated since ModemManager 1.14.0.
+     *
+     * The flag probably was never used, it certainly isn't used since 1.14.0.
+     *
+     * Still, just to be sure, there is no harm in checking it here. Suppress the
+     * warning, it should have no bad effect.
+     */
+    return NM_FLAGS_ANY(caps,
+                        (MM_MODEM_CAPABILITY_GSM_UMTS | MM_MODEM_CAPABILITY_LTE
+                         | MM_MODEM_CAPABILITY_LTE_ADVANCED));
+    G_GNUC_END_IGNORE_DEPRECATIONS
+}
+
+#define MODEM_CAPS_3GPP2(caps) (caps & (MM_MODEM_CAPABILITY_CDMA_EVDO))
+
+/* Maximum time to keep the DBus call waiting for a connection result.
+ * This value is greater than the default timeout in ModemManager (180s since
+ * 1.16), so that whenever possible the timeout happens first there instead of
+ * in NetworkManager. */
+#define MODEM_CONNECT_TIMEOUT_SECS 200
+
+/*****************************************************************************/
+
+typedef enum {
+    CONNECT_STEP_FIRST,
+    CONNECT_STEP_WAIT_FOR_SIM,
+    CONNECT_STEP_UNLOCK,
+    CONNECT_STEP_WAIT_FOR_READY,
+    CONNECT_STEP_CONNECT,
+    CONNECT_STEP_LAST,
+} ConnectStep;
+
+typedef struct {
+    NMModemBroadband *self;
+    ConnectStep       step;
+
+    MMModemCapability          caps;
+    NMConnection *             connection;
+    GCancellable *             cancellable;
+    MMSimpleConnectProperties *connect_properties;
+    GArray *                   ip_types;
+    guint                      ip_types_i;
+    guint                      ip_type_tries;
+    GError *                   first_error;
+} ConnectContext;
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_MODEM, );
+
+typedef struct {
+    /* The modem object from dbus */
+    MMObject *modem_object;
+    /* Per-interface objects */
+    MMModem *      modem_iface;
+    MMModem3gpp *  modem_3gpp_iface;
+    MMModemSimple *simple_iface;
+    MMSim *        sim_iface;
+
+    /* Connection setup */
+    ConnectContext *ctx;
+
+    MMBearer *        bearer;
+    MMBearerIpConfig *ipv4_config;
+    MMBearerIpConfig *ipv6_config;
+
+    guint idle_id_ip4;
+    guint idle_id_ip6;
+
+    guint32 pin_tries;
+} NMModemBroadbandPrivate;
+
+struct _NMModemBroadband {
+    NMModem                 parent;
+    NMModemBroadbandPrivate _priv;
+};
+
+struct _NMModemBroadbandClass {
+    NMModemClass parent;
+};
+
+G_DEFINE_TYPE(NMModemBroadband, nm_modem_broadband, NM_TYPE_MODEM)
+
+#define NM_MODEM_BROADBAND_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMModemBroadband, NM_IS_MODEM_BROADBAND)
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN      LOGD_MB
+#define _NMLOG_PREFIX_NAME "modem-broadband"
+#define _NMLOG(level, ...)                                                       \
+    G_STMT_START                                                                 \
+    {                                                                            \
+        const NMLogLevel _level = (level);                                       \
+                                                                                 \
+        if (nm_logging_enabled(_level, (_NMLOG_DOMAIN))) {                       \
+            NMModemBroadband *const __self = (self);                             \
+            char                    __prefix_name[128];                          \
+            const char *            __uid;                                       \
+                                                                                 \
+            _nm_log(_level,                                                      \
+                    (_NMLOG_DOMAIN),                                             \
+                    0,                                                           \
+                    NULL,                                                        \
+                    ((__self && __self->_priv.ctx)                               \
+                         ? nm_connection_get_uuid(__self->_priv.ctx->connection) \
+                         : NULL),                                                \
+                    "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                 \
+                    _NMLOG_PREFIX_NAME,                                          \
+                    (__self ? ({                                                 \
+                        ((__uid = nm_modem_get_uid((NMModem *) __self))          \
+                             ? nm_sprintf_buf(__prefix_name, "[%s]", __uid)      \
+                             : "(null)");                                        \
+                    })                                                           \
+                            : "") _NM_UTILS_MACRO_REST(__VA_ARGS__));            \
+        }                                                                        \
+    }                                                                            \
+    G_STMT_END
+
+/*****************************************************************************/
+
+static NMDeviceStateReason
+translate_mm_error(NMModemBroadband *self, GError *error)
+{
+    NMDeviceStateReason reason;
+
+    g_return_val_if_fail(error != NULL, NM_DEVICE_STATE_REASON_UNKNOWN);
+
+    if (g_error_matches(error, MM_CONNECTION_ERROR, MM_CONNECTION_ERROR_NO_CARRIER))
+        reason = NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER;
+    else if (g_error_matches(error, MM_CONNECTION_ERROR, MM_CONNECTION_ERROR_NO_DIALTONE))
+        reason = NM_DEVICE_STATE_REASON_MODEM_NO_DIAL_TONE;
+    else if (g_error_matches(error, MM_CONNECTION_ERROR, MM_CONNECTION_ERROR_BUSY))
+        reason = NM_DEVICE_STATE_REASON_MODEM_BUSY;
+    else if (g_error_matches(error, MM_CONNECTION_ERROR, MM_CONNECTION_ERROR_NO_ANSWER))
+        reason = NM_DEVICE_STATE_REASON_MODEM_DIAL_TIMEOUT;
+    else if (g_error_matches(error,
+                             MM_MOBILE_EQUIPMENT_ERROR,
+                             MM_MOBILE_EQUIPMENT_ERROR_NETWORK_NOT_ALLOWED))
+        reason = NM_DEVICE_STATE_REASON_GSM_REGISTRATION_DENIED;
+    else if (g_error_matches(error,
+                             MM_MOBILE_EQUIPMENT_ERROR,
+                             MM_MOBILE_EQUIPMENT_ERROR_NETWORK_TIMEOUT))
+        reason = NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT;
+    else if (g_error_matches(error,
+                             MM_MOBILE_EQUIPMENT_ERROR,
+                             MM_MOBILE_EQUIPMENT_ERROR_NO_NETWORK))
+        reason = NM_DEVICE_STATE_REASON_GSM_REGISTRATION_NOT_SEARCHING;
+    else if (g_error_matches(error,
+                             MM_MOBILE_EQUIPMENT_ERROR,
+                             MM_MOBILE_EQUIPMENT_ERROR_SIM_NOT_INSERTED))
+        reason = NM_DEVICE_STATE_REASON_GSM_SIM_NOT_INSERTED;
+    else if (g_error_matches(error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_PIN))
+        reason = NM_DEVICE_STATE_REASON_GSM_SIM_PIN_REQUIRED;
+    else if (g_error_matches(error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_PUK))
+        reason = NM_DEVICE_STATE_REASON_GSM_SIM_PUK_REQUIRED;
+    else if (g_error_matches(error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_WRONG))
+        reason = NM_DEVICE_STATE_REASON_GSM_SIM_WRONG;
+    else if (g_error_matches(error,
+                             MM_MOBILE_EQUIPMENT_ERROR,
+                             MM_MOBILE_EQUIPMENT_ERROR_INCORRECT_PASSWORD))
+        reason = NM_DEVICE_STATE_REASON_SIM_PIN_INCORRECT;
+    else {
+        /* unable to map the ModemManager error to a NM_DEVICE_STATE_REASON */
+        _LOGD("unmapped error detected: '%s'", error->message);
+        reason = NM_DEVICE_STATE_REASON_UNKNOWN;
+    }
+
+    return reason;
+}
+
+/*****************************************************************************/
+
+static void
+get_capabilities(NMModem *                  _self,
+                 NMDeviceModemCapabilities *modem_caps,
+                 NMDeviceModemCapabilities *current_caps)
+{
+    NMModemBroadband * self          = NM_MODEM_BROADBAND(_self);
+    MMModemCapability  all_supported = MM_MODEM_CAPABILITY_NONE;
+    MMModemCapability *supported;
+    guint              n_supported;
+
+    /* For now, we don't care about the capability combinations, just merge all
+     * combinations in a single mask */
+    if (mm_modem_get_supported_capabilities(self->_priv.modem_iface, &supported, &n_supported)) {
+        guint i;
+
+        for (i = 0; i < n_supported; i++)
+            all_supported |= supported[i];
+
+        g_free(supported);
+    }
+
+    *modem_caps = (NMDeviceModemCapabilities) all_supported;
+    *current_caps =
+        (NMDeviceModemCapabilities) mm_modem_get_current_capabilities(self->_priv.modem_iface);
+}
+
+static gboolean
+owns_port(NMModem *_self, const char *iface)
+{
+    NMModemBroadband *     self    = NM_MODEM_BROADBAND(_self);
+    const MMModemPortInfo *ports   = NULL;
+    guint                  n_ports = 0, i;
+
+    mm_modem_peek_ports(self->_priv.modem_iface, &ports, &n_ports);
+    for (i = 0; i < n_ports; i++) {
+        if (nm_streq0(iface, ports[i].name))
+            return TRUE;
+    }
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+static void
+ask_for_pin(NMModemBroadband *self)
+{
+    guint32 tries;
+
+    tries = self->_priv.pin_tries++;
+    nm_modem_get_secrets(NM_MODEM(self),
+                         NM_SETTING_GSM_SETTING_NAME,
+                         tries ? TRUE : FALSE,
+                         NM_SETTING_GSM_PIN);
+}
+
+static NMModemIPMethod
+get_bearer_ip_method(MMBearerIpConfig *config)
+{
+    MMBearerIpMethod mm_method;
+
+    mm_method = mm_bearer_ip_config_get_method(config);
+    if (mm_method == MM_BEARER_IP_METHOD_PPP)
+        return NM_MODEM_IP_METHOD_PPP;
+    else if (mm_method == MM_BEARER_IP_METHOD_STATIC)
+        return NM_MODEM_IP_METHOD_STATIC;
+    else if (mm_method == MM_BEARER_IP_METHOD_DHCP)
+        return NM_MODEM_IP_METHOD_AUTO;
+    return NM_MODEM_IP_METHOD_UNKNOWN;
+}
+
+static MMSimpleConnectProperties *
+create_cdma_connect_properties(NMConnection *connection)
+{
+    MMSimpleConnectProperties *properties;
+
+    properties = mm_simple_connect_properties_new();
+
+#if !MM_CHECK_VERSION(1, 9, 1)
+    {
+        NMSettingCdma *setting;
+        const char *   str;
+
+        setting = nm_connection_get_setting_cdma(connection);
+        str     = nm_setting_cdma_get_number(setting);
+        if (str)
+            mm_simple_connect_properties_set_number(properties, str);
+    }
+#endif
+
+    return properties;
+}
+
+static MMSimpleConnectProperties *
+create_gsm_connect_properties(NMConnection *connection,
+                              const char *  apn,
+                              const char *  username,
+                              const char *  password)
+{
+    NMSettingGsm *             setting;
+    NMSettingPpp *             s_ppp;
+    MMSimpleConnectProperties *properties;
+    const char *               str;
+
+    setting = nm_connection_get_setting_gsm(connection);
+
+    properties = mm_simple_connect_properties_new();
+
+    mm_simple_connect_properties_set_apn(properties, apn ?: "");
+    if (username)
+        mm_simple_connect_properties_set_user(properties, username);
+    if (password)
+        mm_simple_connect_properties_set_password(properties, password);
+
+    str = nm_setting_gsm_get_network_id(setting);
+    if (str)
+        mm_simple_connect_properties_set_operator_id(properties, str);
+
+    str = nm_setting_gsm_get_pin(setting);
+    if (str)
+        mm_simple_connect_properties_set_pin(properties, str);
+
+    /* Roaming */
+    if (nm_setting_gsm_get_home_only(setting))
+        mm_simple_connect_properties_set_allow_roaming(properties, FALSE);
+
+    /* For IpMethod == STATIC or DHCP */
+    s_ppp = nm_connection_get_setting_ppp(connection);
+    if (s_ppp) {
+        MMBearerAllowedAuth allowed_auth = MM_BEARER_ALLOWED_AUTH_UNKNOWN;
+
+        if (nm_setting_ppp_get_noauth(s_ppp))
+            allowed_auth = MM_BEARER_ALLOWED_AUTH_NONE;
+        if (!nm_setting_ppp_get_refuse_pap(s_ppp))
+            allowed_auth |= MM_BEARER_ALLOWED_AUTH_PAP;
+        if (!nm_setting_ppp_get_refuse_chap(s_ppp))
+            allowed_auth |= MM_BEARER_ALLOWED_AUTH_CHAP;
+        if (!nm_setting_ppp_get_refuse_mschap(s_ppp))
+            allowed_auth |= MM_BEARER_ALLOWED_AUTH_MSCHAP;
+        if (!nm_setting_ppp_get_refuse_mschapv2(s_ppp))
+            allowed_auth |= MM_BEARER_ALLOWED_AUTH_MSCHAPV2;
+        if (!nm_setting_ppp_get_refuse_eap(s_ppp))
+            allowed_auth |= MM_BEARER_ALLOWED_AUTH_EAP;
+
+        mm_simple_connect_properties_set_allowed_auth(properties, allowed_auth);
+    }
+
+    return properties;
+}
+
+static void
+connect_context_clear(NMModemBroadband *self)
+{
+    if (self->_priv.ctx) {
+        ConnectContext *ctx = self->_priv.ctx;
+
+        g_clear_error(&ctx->first_error);
+        nm_clear_pointer(&ctx->ip_types, g_array_unref);
+        nm_clear_g_cancellable(&ctx->cancellable);
+        g_clear_object(&ctx->connection);
+        g_clear_object(&ctx->connect_properties);
+        g_clear_object(&ctx->self);
+        g_slice_free(ConnectContext, ctx);
+        self->_priv.ctx = NULL;
+    }
+}
+
+static void connect_context_step(NMModemBroadband *self);
+
+static void
+connect_ready(MMModemSimple *simple_iface, GAsyncResult *res, NMModemBroadband *self)
+{
+    ConnectContext *ctx;
+    GError *        error      = NULL;
+    NMModemIPMethod ip4_method = NM_MODEM_IP_METHOD_UNKNOWN;
+    NMModemIPMethod ip6_method = NM_MODEM_IP_METHOD_UNKNOWN;
+    MMBearer *      bearer;
+
+    bearer = mm_modem_simple_connect_finish(simple_iface, res, &error);
+
+    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+        g_error_free(error);
+        return;
+    }
+
+    ctx = self->_priv.ctx;
+
+    if (!ctx)
+        return;
+
+    self->_priv.bearer = bearer;
+
+    if (!self->_priv.bearer) {
+        if (g_error_matches(error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_PIN)
+            || (g_error_matches(error, MM_CORE_ERROR, MM_CORE_ERROR_UNAUTHORIZED)
+                && mm_modem_get_unlock_required(self->_priv.modem_iface)
+                       == MM_MODEM_LOCK_SIM_PIN)) {
+            g_error_free(error);
+
+            /* Request PIN */
+            ask_for_pin(self);
+            connect_context_clear(self);
+            return;
+        }
+
+        /* Save the error, if it's the first one */
+        if (!ctx->first_error) {
+            /* Strip remote error info before saving it */
+            if (g_dbus_error_is_remote_error(error))
+                g_dbus_error_strip_remote_error(error);
+            ctx->first_error = error;
+        } else
+            g_clear_error(&error);
+
+        if (ctx->ip_type_tries == 0 && g_error_matches(error, MM_CORE_ERROR, MM_CORE_ERROR_RETRY)) {
+            /* Try one more time */
+            ctx->ip_type_tries++;
+        } else {
+            /* If the modem/provider lies and the IP type we tried isn't supported,
+             * retry with the next one, if any.
+             */
+            ctx->ip_types_i++;
+            ctx->ip_type_tries = 0;
+        }
+        connect_context_step(self);
+        return;
+    }
+
+    /* Grab IP configurations */
+    self->_priv.ipv4_config = mm_bearer_get_ipv4_config(self->_priv.bearer);
+    if (self->_priv.ipv4_config)
+        ip4_method = get_bearer_ip_method(self->_priv.ipv4_config);
+
+    self->_priv.ipv6_config = mm_bearer_get_ipv6_config(self->_priv.bearer);
+    if (self->_priv.ipv6_config)
+        ip6_method = get_bearer_ip_method(self->_priv.ipv6_config);
+
+    if (!nm_modem_set_data_port(NM_MODEM(self),
+                                NM_PLATFORM_GET,
+                                mm_bearer_get_interface(self->_priv.bearer),
+                                ip4_method,
+                                ip6_method,
+                                mm_bearer_get_ip_timeout(self->_priv.bearer),
+                                &error)) {
+        _LOGW("failed to connect modem: %s", error->message);
+        g_error_free(error);
+        nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+        connect_context_clear(self);
+        return;
+    }
+
+    ctx->step++;
+    connect_context_step(self);
+}
+
+static void
+send_pin_ready(MMSim *sim, GAsyncResult *result, NMModemBroadband *self)
+{
+    gs_free_error GError *error = NULL;
+
+    mm_sim_send_pin_finish(sim, result, &error);
+
+    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    if (!self->_priv.ctx || self->_priv.ctx->step != CONNECT_STEP_UNLOCK)
+        g_return_if_reached();
+
+    if (error) {
+        if (g_error_matches(error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_PIN)
+            || (g_error_matches(error, MM_CORE_ERROR, MM_CORE_ERROR_UNAUTHORIZED)
+                && mm_modem_get_unlock_required(self->_priv.modem_iface) == MM_MODEM_LOCK_SIM_PIN))
+            ask_for_pin(self);
+        else
+            nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, translate_mm_error(self, error));
+        return;
+    }
+
+    self->_priv.ctx->step++;
+    connect_context_step(self);
+}
+
+static void
+find_gsm_apn_cb(const char *  apn,
+                const char *  username,
+                const char *  password,
+                const char *  gateway,
+                const char *  auth_method,
+                const GSList *dns,
+                GError *      error,
+                gpointer      user_data)
+{
+    NMModemBroadband *       self = user_data;
+    NMModemBroadbandPrivate *priv = NM_MODEM_BROADBAND_GET_PRIVATE(self);
+    ConnectContext *         ctx  = priv->ctx;
+
+    if (error) {
+        _LOGW("failed to connect '%s': APN not found: %s",
+              nm_connection_get_id(ctx->connection),
+              error->message);
+
+        nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_GSM_APN_FAILED);
+        connect_context_clear(self);
+        return;
+    }
+
+    /* Blank APN ("") means the default subscription APN */
+    ctx->connect_properties =
+        create_gsm_connect_properties(ctx->connection, apn, username, password);
+    g_return_if_fail(ctx->connect_properties);
+    connect_context_step(self);
+}
+
+static gboolean
+try_create_connect_properties(NMModemBroadband *self)
+{
+    NMModemBroadbandPrivate *priv = NM_MODEM_BROADBAND_GET_PRIVATE(self);
+    ConnectContext *         ctx  = priv->ctx;
+
+    if (MODEM_CAPS_3GPP(ctx->caps)) {
+        NMSettingGsm *s_gsm = nm_connection_get_setting_gsm(ctx->connection);
+
+        if (!s_gsm || nm_setting_gsm_get_auto_config(s_gsm)) {
+            gs_unref_object MMModem3gpp *modem_3gpp = NULL;
+            const char *                 network_id = NULL;
+
+            s_gsm = nm_connection_get_setting_gsm(ctx->connection);
+            if (s_gsm)
+                network_id = nm_setting_gsm_get_network_id(s_gsm);
+            if (!network_id) {
+                if (mm_modem_get_state(self->_priv.modem_iface) < MM_MODEM_STATE_REGISTERED)
+                    return FALSE;
+                modem_3gpp = mm_object_get_modem_3gpp(priv->modem_object);
+                network_id = mm_modem_3gpp_get_operator_code(modem_3gpp);
+            }
+            if (!network_id) {
+                _LOGW("failed to connect '%s': unable to determine the network id",
+                      nm_connection_get_id(ctx->connection));
+                goto out;
+            }
+
+            nm_service_providers_find_gsm_apn(MOBILE_BROADBAND_PROVIDER_INFO_DATABASE,
+                                              network_id,
+                                              ctx->cancellable,
+                                              find_gsm_apn_cb,
+                                              self);
+        } else {
+            ctx->connect_properties =
+                create_gsm_connect_properties(ctx->connection,
+                                              nm_setting_gsm_get_apn(s_gsm),
+                                              nm_setting_gsm_get_username(s_gsm),
+                                              nm_setting_gsm_get_password(s_gsm));
+            g_return_val_if_fail(ctx->connect_properties, TRUE);
+        }
+
+        return TRUE;
+    } else if (MODEM_CAPS_3GPP2(ctx->caps)) {
+        ctx->connect_properties = create_cdma_connect_properties(ctx->connection);
+        g_return_val_if_fail(ctx->connect_properties, FALSE);
+        return TRUE;
+    } else {
+        _LOGW("failed to connect '%s': not a mobile broadband modem",
+              nm_connection_get_id(ctx->connection));
+    }
+
+out:
+    nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED);
+    connect_context_clear(self);
+    return TRUE;
+}
+
+static void
+connect_context_step(NMModemBroadband *self)
+{
+    ConnectContext *ctx = self->_priv.ctx;
+
+    switch (ctx->step) {
+    case CONNECT_STEP_FIRST:
+        ctx->step++;
+        /* fall-through */
+
+    case CONNECT_STEP_WAIT_FOR_SIM:
+        if (MODEM_CAPS_3GPP(ctx->caps) && !self->_priv.sim_iface) {
+            /* Have to wait for the SIM to show up */
+            break;
+        }
+        ctx->step++;
+        /* fall-through */
+
+    case CONNECT_STEP_UNLOCK:
+        if (MODEM_CAPS_3GPP(ctx->caps)
+            && mm_modem_get_unlock_required(self->_priv.modem_iface) == MM_MODEM_LOCK_SIM_PIN) {
+            NMSettingGsm *s_gsm = nm_connection_get_setting_gsm(ctx->connection);
+            const char *  pin   = nm_setting_gsm_get_pin(s_gsm);
+
+            /* If we have a PIN already, send it.  If we don't, get it. */
+            if (pin) {
+                mm_sim_send_pin(self->_priv.sim_iface,
+                                pin,
+                                ctx->cancellable,
+                                (GAsyncReadyCallback) send_pin_ready,
+                                self);
+            } else {
+                ask_for_pin(self);
+            }
+            break;
+        }
+        ctx->step++;
+        /* fall-through */
+    case CONNECT_STEP_WAIT_FOR_READY:
+    {
+        GError *error = NULL;
+
+        if (mm_modem_get_state(self->_priv.modem_iface) <= MM_MODEM_STATE_LOCKED)
+            break;
+
+        if (!try_create_connect_properties(self))
+            break;
+
+        if (!self->_priv.ctx)
+            break;
+
+        /* Build up list of IP types that we need to use in the retries */
+        ctx->ip_types = nm_modem_get_connection_ip_type(NM_MODEM(self), ctx->connection, &error);
+        if (!ctx->ip_types) {
+            _LOGW("failed to connect '%s': %s",
+                  nm_connection_get_id(ctx->connection),
+                  error->message);
+            g_clear_error(&error);
+
+            nm_modem_emit_prepare_result(NM_MODEM(self),
+                                         FALSE,
+                                         NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED);
+            connect_context_clear(self);
+            break;
+        }
+
+        ctx->step++;
+    }
+        /* fall-through */
+    case CONNECT_STEP_CONNECT:
+        if (!ctx->connect_properties)
+            break;
+
+        if (ctx->ip_types_i < ctx->ip_types->len) {
+            NMModemIPType current;
+
+            current = g_array_index(ctx->ip_types, NMModemIPType, ctx->ip_types_i);
+
+            if (current == NM_MODEM_IP_TYPE_IPV4)
+                mm_simple_connect_properties_set_ip_type(ctx->connect_properties,
+                                                         MM_BEARER_IP_FAMILY_IPV4);
+            else if (current == NM_MODEM_IP_TYPE_IPV6)
+                mm_simple_connect_properties_set_ip_type(ctx->connect_properties,
+                                                         MM_BEARER_IP_FAMILY_IPV6);
+            else if (current == NM_MODEM_IP_TYPE_IPV4V6)
+                mm_simple_connect_properties_set_ip_type(ctx->connect_properties,
+                                                         MM_BEARER_IP_FAMILY_IPV4V6);
+            else
+                g_return_if_reached();
+
+            _nm_modem_set_apn(NM_MODEM(self),
+                              mm_simple_connect_properties_get_apn(ctx->connect_properties));
+
+            _LOGD("launching connection with ip type '%s' (try %d)",
+                  nm_modem_ip_type_to_string(current),
+                  ctx->ip_type_tries + 1);
+
+            mm_modem_simple_connect(self->_priv.simple_iface,
+                                    ctx->connect_properties,
+                                    ctx->cancellable,
+                                    (GAsyncReadyCallback) connect_ready,
+                                    self);
+            break;
+        }
+
+        ctx->step++;
+        /* fall-through */
+
+    case CONNECT_STEP_LAST:
+        if (self->_priv.ipv4_config || self->_priv.ipv6_config)
+            nm_modem_emit_prepare_result(NM_MODEM(self), TRUE, NM_DEVICE_STATE_REASON_NONE);
+        else {
+            /* If we have a saved error from a previous attempt, use it */
+            if (!ctx->first_error)
+                ctx->first_error = g_error_new_literal(NM_DEVICE_ERROR,
+                                                       NM_DEVICE_ERROR_INVALID_CONNECTION,
+                                                       "invalid bearer IP configuration");
+
+            _LOGW("failed to connect modem: %s", ctx->first_error->message);
+            nm_modem_emit_prepare_result(NM_MODEM(self),
+                                         FALSE,
+                                         translate_mm_error(self, ctx->first_error));
+        }
+
+        connect_context_clear(self);
+        break;
+    }
+}
+
+static NMActStageReturn
+modem_act_stage1_prepare(NMModem *            _self,
+                         NMConnection *       connection,
+                         NMDeviceStateReason *out_failure_reason)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(_self);
+
+    /* Make sure we can get the Simple interface from the modem */
+    if (!self->_priv.simple_iface) {
+        self->_priv.simple_iface = mm_object_get_modem_simple(self->_priv.modem_object);
+        if (!self->_priv.simple_iface) {
+            _LOGW("cannot access the Simple mobile broadband modem interface");
+            NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED);
+            return NM_ACT_STAGE_RETURN_FAILURE;
+        }
+    }
+
+    connect_context_clear(self);
+
+    /* Allocate new context for this connect stage attempt */
+    self->_priv.ctx              = g_slice_new0(ConnectContext);
+    self->_priv.ctx->caps        = mm_modem_get_current_capabilities(self->_priv.modem_iface);
+    self->_priv.ctx->cancellable = g_cancellable_new();
+    self->_priv.ctx->connection  = g_object_ref(connection);
+
+    g_dbus_proxy_set_default_timeout(G_DBUS_PROXY(self->_priv.simple_iface),
+                                     MODEM_CONNECT_TIMEOUT_SECS * 1000);
+    connect_context_step(self);
+
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+check_connection_compatible_with_modem(NMModem *_self, NMConnection *connection, GError **error)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(_self);
+    MMModemCapability modem_caps;
+
+    modem_caps = mm_modem_get_current_capabilities(self->_priv.modem_iface);
+
+    if (MODEM_CAPS_3GPP(modem_caps)) {
+        if (!_nm_connection_check_main_setting(connection, NM_SETTING_GSM_SETTING_NAME, error))
+            return FALSE;
+
+        return TRUE;
+    }
+
+    if (MODEM_CAPS_3GPP2(modem_caps)) {
+        if (!_nm_connection_check_main_setting(connection, NM_SETTING_CDMA_SETTING_NAME, error))
+            return FALSE;
+
+        return TRUE;
+    }
+
+    if (!_nm_connection_check_main_setting(connection, NM_SETTING_GSM_SETTING_NAME, NULL)
+        && !_nm_connection_check_main_setting(connection, NM_SETTING_CDMA_SETTING_NAME, NULL)) {
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                           "connection type %s is not supported by modem",
+                           nm_connection_get_connection_type(connection));
+        return FALSE;
+    }
+
+    nm_utils_error_set(error,
+                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                       "modem lacks capabilities for %s profile",
+                       nm_connection_get_connection_type(connection));
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+complete_connection(NMModem *            modem,
+                    const char *         iface,
+                    NMConnection *       connection,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(modem);
+    MMModemCapability modem_caps;
+    NMSettingPpp *    s_ppp;
+
+    modem_caps = mm_modem_get_current_capabilities(self->_priv.modem_iface);
+
+    /* PPP settings common to 3GPP and 3GPP2 */
+    s_ppp = nm_connection_get_setting_ppp(connection);
+    if (!s_ppp) {
+        s_ppp = (NMSettingPpp *) nm_setting_ppp_new();
+        g_object_set(G_OBJECT(s_ppp),
+                     NM_SETTING_PPP_LCP_ECHO_FAILURE,
+                     5,
+                     NM_SETTING_PPP_LCP_ECHO_INTERVAL,
+                     30,
+                     NULL);
+        nm_connection_add_setting(connection, NM_SETTING(s_ppp));
+    }
+
+    if (MODEM_CAPS_3GPP(modem_caps)) {
+        NMSettingGsm *s_gsm;
+
+        s_gsm = nm_connection_get_setting_gsm(connection);
+        if (!s_gsm) {
+            s_gsm = (NMSettingGsm *) nm_setting_gsm_new();
+            nm_connection_add_setting(connection, NM_SETTING(s_gsm));
+            g_object_set(G_OBJECT(s_gsm), NM_SETTING_GSM_AUTO_CONFIG, TRUE, NULL);
+        }
+
+        if (!nm_setting_gsm_get_device_id(s_gsm)) {
+            g_object_set(G_OBJECT(s_gsm),
+                         NM_SETTING_GSM_DEVICE_ID,
+                         nm_modem_get_device_id(modem),
+                         NULL);
+        }
+
+        nm_utils_complete_generic(NM_PLATFORM_GET,
+                                  connection,
+                                  NM_SETTING_GSM_SETTING_NAME,
+                                  existing_connections,
+                                  NULL,
+                                  _("GSM connection"),
+                                  NULL,
+                                  NULL,
+                                  FALSE); /* No IPv6 yet by default */
+
+        return TRUE;
+    }
+
+    if (MODEM_CAPS_3GPP2(modem_caps)) {
+        NMSettingCdma *s_cdma;
+
+        s_cdma = nm_connection_get_setting_cdma(connection);
+        if (!s_cdma) {
+            s_cdma = (NMSettingCdma *) nm_setting_cdma_new();
+            nm_connection_add_setting(connection, NM_SETTING(s_cdma));
+        }
+
+        if (!nm_setting_cdma_get_number(s_cdma))
+            g_object_set(G_OBJECT(s_cdma), NM_SETTING_CDMA_NUMBER, "#777", NULL);
+
+        nm_utils_complete_generic(NM_PLATFORM_GET,
+                                  connection,
+                                  NM_SETTING_CDMA_SETTING_NAME,
+                                  existing_connections,
+                                  NULL,
+                                  _("CDMA connection"),
+                                  NULL,
+                                  iface,
+                                  FALSE); /* No IPv6 yet by default */
+
+        return TRUE;
+    }
+
+    g_set_error(error,
+                NM_DEVICE_ERROR,
+                NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
+                "Device is not a mobile broadband modem");
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+get_user_pass(NMModem *modem, NMConnection *connection, const char **user, const char **pass)
+{
+    NMSettingGsm * s_gsm;
+    NMSettingCdma *s_cdma;
+
+    s_gsm  = nm_connection_get_setting_gsm(connection);
+    s_cdma = nm_connection_get_setting_cdma(connection);
+    if (!s_gsm && !s_cdma)
+        return FALSE;
+
+    if (user) {
+        if (s_gsm)
+            *user = nm_setting_gsm_get_username(s_gsm);
+        else if (s_cdma)
+            *user = nm_setting_cdma_get_username(s_cdma);
+    }
+    if (pass) {
+        if (s_gsm)
+            *pass = nm_setting_gsm_get_password(s_gsm);
+        else if (s_cdma)
+            *pass = nm_setting_cdma_get_password(s_cdma);
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+/* Query/Update enabled state */
+
+static void
+set_power_state_low_ready(MMModem *modem, GAsyncResult *result, NMModemBroadband *self)
+{
+    GError *error = NULL;
+
+    if (!mm_modem_set_power_state_finish(modem, result, &error)) {
+        /* Log but ignore errors; not all modems support low power state */
+        _LOGD("failed to set modem low power state: %s", NM_G_ERROR_MSG(error));
+        g_clear_error(&error);
+    }
+
+    /* Balance refcount */
+    g_object_unref(self);
+}
+
+static void
+modem_disable_ready(MMModem *modem_iface, GAsyncResult *res, NMModemBroadband *self)
+{
+    GError *error = NULL;
+
+    if (mm_modem_disable_finish(modem_iface, res, &error)) {
+        /* Once disabled, move to low-power mode */
+        mm_modem_set_power_state(modem_iface,
+                                 MM_MODEM_POWER_STATE_LOW,
+                                 NULL,
+                                 (GAsyncReadyCallback) set_power_state_low_ready,
+                                 g_object_ref(self));
+    } else {
+        _LOGW("failed to disable modem: %s", NM_G_ERROR_MSG(error));
+        nm_modem_set_prev_state(NM_MODEM(self), "disable failed");
+        g_clear_error(&error);
+    }
+
+    /* Balance refcount */
+    g_object_unref(self);
+}
+
+static void
+modem_enable_ready(MMModem *modem_iface, GAsyncResult *res, NMModemBroadband *self)
+{
+    GError *error = NULL;
+
+    if (!mm_modem_enable_finish(modem_iface, res, &error)) {
+        _LOGW("failed to enable modem: %s", NM_G_ERROR_MSG(error));
+        nm_modem_set_prev_state(NM_MODEM(self), "enable failed");
+        g_clear_error(&error);
+    }
+
+    /* Balance refcount */
+    g_object_unref(self);
+}
+
+static void
+set_mm_enabled(NMModem *_self, gboolean enabled)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(_self);
+
+    if (enabled) {
+        mm_modem_enable(self->_priv.modem_iface,
+                        NULL, /* cancellable */
+                        (GAsyncReadyCallback) modem_enable_ready,
+                        g_object_ref(self));
+    } else {
+        mm_modem_disable(self->_priv.modem_iface,
+                         NULL, /* cancellable */
+                         (GAsyncReadyCallback) modem_disable_ready,
+                         g_object_ref(self));
+    }
+}
+
+/*****************************************************************************/
+/* IPv4 method static */
+
+static gboolean
+static_stage3_ip4_done(NMModemBroadband *self)
+{
+    GError *        error               = NULL;
+    gs_unref_object NMIP4Config *config = NULL;
+    const char *                 data_port;
+    const char *                 address_string;
+    const char *                 gw_string;
+    guint32                      address_network;
+    guint32                      gw = 0;
+    NMPlatformIP4Address         address;
+    const char **                dns;
+    guint                        i;
+    guint32                      ip4_route_table, ip4_route_metric;
+    NMPlatformIP4Route *         r;
+    guint32                      mtu_n;
+
+    g_return_val_if_fail(self->_priv.ipv4_config, FALSE);
+    g_return_val_if_fail(self->_priv.bearer, FALSE);
+
+    self->_priv.idle_id_ip4 = 0;
+
+    _LOGI("IPv4 static configuration:");
+
+    /* Fully fail if invalid IP address retrieved */
+    address_string = mm_bearer_ip_config_get_address(self->_priv.ipv4_config);
+    if (!address_string
+        || !nm_utils_parse_inaddr_bin(AF_INET, address_string, NULL, &address_network)) {
+        error =
+            g_error_new(NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_INVALID_CONNECTION,
+                        "(%s) retrieving IP4 configuration failed: invalid address given %s%s%s",
+                        nm_modem_get_uid(NM_MODEM(self)),
+                        NM_PRINT_FMT_QUOTE_STRING(address_string));
+        goto out;
+    }
+
+    /* Missing gateway not a hard failure */
+    gw_string = mm_bearer_ip_config_get_gateway(self->_priv.ipv4_config);
+    if (gw_string && !nm_utils_parse_inaddr_bin(AF_INET, gw_string, NULL, &gw)) {
+        error =
+            g_error_new(NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_INVALID_CONNECTION,
+                        "(%s) retrieving IP4 configuration failed: invalid gateway address \"%s\"",
+                        nm_modem_get_uid(NM_MODEM(self)),
+                        gw_string);
+        goto out;
+    }
+
+    data_port = mm_bearer_get_interface(self->_priv.bearer);
+    g_return_val_if_fail(data_port, FALSE);
+    config = nm_ip4_config_new(nm_platform_get_multi_idx(NM_PLATFORM_GET),
+                               nm_platform_link_get_ifindex(NM_PLATFORM_GET, data_port));
+
+    memset(&address, 0, sizeof(address));
+    address.address      = address_network;
+    address.peer_address = address_network;
+    address.plen         = mm_bearer_ip_config_get_prefix(self->_priv.ipv4_config);
+    address.addr_source  = NM_IP_CONFIG_SOURCE_WWAN;
+    if (address.plen <= 32)
+        nm_ip4_config_add_address(config, &address);
+
+    _LOGI("  address %s/%d", address_string, address.plen);
+
+    nm_modem_get_route_parameters(NM_MODEM(self), &ip4_route_table, &ip4_route_metric, NULL, NULL);
+    r = &(NMPlatformIP4Route){
+        .rt_source     = NM_IP_CONFIG_SOURCE_WWAN,
+        .gateway       = gw,
+        .table_coerced = nm_platform_route_table_coerce(ip4_route_table),
+        .metric        = ip4_route_metric,
+    };
+    nm_ip4_config_add_route(config, r, NULL);
+    _LOGI("  gateway %s", gw_string);
+
+    /* DNS servers */
+    dns = mm_bearer_ip_config_get_dns(self->_priv.ipv4_config);
+    for (i = 0; dns && dns[i]; i++) {
+        if (nm_utils_parse_inaddr_bin(AF_INET, dns[i], NULL, &address_network)
+            && address_network > 0) {
+            nm_ip4_config_add_nameserver(config, address_network);
+            _LOGI("  DNS %s", dns[i]);
+        }
+    }
+
+#if MM_CHECK_VERSION(1, 4, 0)
+    mtu_n = mm_bearer_ip_config_get_mtu(self->_priv.ipv4_config);
+    if (mtu_n) {
+        nm_ip4_config_set_mtu(config, mtu_n, NM_IP_CONFIG_SOURCE_WWAN);
+        _LOGI("  MTU %u", mtu_n);
+    }
+#endif
+
+out:
+    g_signal_emit_by_name(self, NM_MODEM_IP4_CONFIG_RESULT, config, error);
+    g_clear_error(&error);
+    return FALSE;
+}
+
+static NMActStageReturn
+static_stage3_ip4_config_start(NMModem *            modem,
+                               NMActRequest *       req,
+                               NMDeviceStateReason *out_failure_reason)
+{
+    NMModemBroadband *       self = NM_MODEM_BROADBAND(modem);
+    NMModemBroadbandPrivate *priv = NM_MODEM_BROADBAND_GET_PRIVATE(self);
+
+    /* We schedule it in an idle just to follow the same logic as in the
+     * generic modem implementation. */
+    nm_clear_g_source(&priv->idle_id_ip4);
+    priv->idle_id_ip4 = g_idle_add((GSourceFunc) static_stage3_ip4_done, self);
+
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+/*****************************************************************************/
+/* IPv6 method static */
+
+static gboolean
+stage3_ip6_done(NMModemBroadband *self)
+{
+    GError *             error  = NULL;
+    NMIP6Config *        config = NULL;
+    const char *         data_port;
+    const char *         address_string;
+    NMPlatformIP6Address address;
+    NMModemIPMethod      ip_method;
+    const char **        dns;
+    guint                i;
+
+    g_return_val_if_fail(self->_priv.ipv6_config, FALSE);
+
+    self->_priv.idle_id_ip6 = 0;
+    memset(&address, 0, sizeof(address));
+
+    ip_method = get_bearer_ip_method(self->_priv.ipv6_config);
+
+    address_string = mm_bearer_ip_config_get_address(self->_priv.ipv6_config);
+    if (!address_string) {
+        /* DHCP/SLAAC is allowed to skip addresses; other methods require it */
+        if (ip_method != NM_MODEM_IP_METHOD_AUTO) {
+            error = g_error_new(NM_DEVICE_ERROR,
+                                NM_DEVICE_ERROR_INVALID_CONNECTION,
+                                "(%s) retrieving IPv6 configuration failed: no address given",
+                                nm_modem_get_uid(NM_MODEM(self)));
+        }
+        goto out;
+    }
+
+    /* Fail if invalid IP address retrieved */
+    if (!inet_pton(AF_INET6, address_string, (void *) &(address.address))) {
+        error = g_error_new(NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "(%s) retrieving IPv6 configuration failed: invalid address given '%s'",
+                            nm_modem_get_uid(NM_MODEM(self)),
+                            address_string);
+        goto out;
+    }
+
+    _LOGI("IPv6 base configuration:");
+
+    data_port = mm_bearer_get_interface(self->_priv.bearer);
+    g_return_val_if_fail(data_port, FALSE);
+
+    config = nm_ip6_config_new(nm_platform_get_multi_idx(NM_PLATFORM_GET),
+                               nm_platform_link_get_ifindex(NM_PLATFORM_GET, data_port));
+
+    address.plen = mm_bearer_ip_config_get_prefix(self->_priv.ipv6_config);
+    if (address.plen <= 128)
+        nm_ip6_config_add_address(config, &address);
+
+    _LOGI("  address %s/%d", address_string, address.plen);
+
+    address_string = mm_bearer_ip_config_get_gateway(self->_priv.ipv6_config);
+    if (address_string) {
+        guint32 ip6_route_table, ip6_route_metric;
+
+        if (inet_pton(AF_INET6, address_string, &address.address) != 1) {
+            error =
+                g_error_new(NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "(%s) retrieving IPv6 configuration failed: invalid gateway given '%s'",
+                            nm_modem_get_uid(NM_MODEM(self)),
+                            address_string);
+            goto out;
+        }
+
+        nm_modem_get_route_parameters(NM_MODEM(self),
+                                      NULL,
+                                      NULL,
+                                      &ip6_route_table,
+                                      &ip6_route_metric);
+        {
+            const NMPlatformIP6Route r = {
+                .rt_source     = NM_IP_CONFIG_SOURCE_WWAN,
+                .gateway       = address.address,
+                .table_coerced = nm_platform_route_table_coerce(ip6_route_table),
+                .metric        = ip6_route_metric,
+            };
+
+            _LOGI("  gateway %s", address_string);
+            nm_ip6_config_add_route(config, &r, NULL);
+        }
+    } else if (ip_method == NM_MODEM_IP_METHOD_STATIC) {
+        /* Gateway required for the 'static' method */
+        error = g_error_new(NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "(%s) retrieving IPv6 configuration failed: missing gateway",
+                            nm_modem_get_uid(NM_MODEM(self)));
+        goto out;
+    }
+
+    /* DNS servers */
+    dns = mm_bearer_ip_config_get_dns(self->_priv.ipv6_config);
+    for (i = 0; dns && dns[i]; i++) {
+        struct in6_addr addr;
+
+        if (inet_pton(AF_INET6, dns[i], &addr)) {
+            nm_ip6_config_add_nameserver(config, &addr);
+            _LOGI("  DNS %s", dns[i]);
+        }
+    }
+
+out:
+    nm_modem_emit_ip6_config_result(NM_MODEM(self), config, error);
+    g_clear_object(&config);
+    g_clear_error(&error);
+    return FALSE;
+}
+
+static NMActStageReturn
+stage3_ip6_config_request(NMModem *modem, NMDeviceStateReason *out_failure_reason)
+{
+    NMModemBroadband *       self = NM_MODEM_BROADBAND(modem);
+    NMModemBroadbandPrivate *priv = NM_MODEM_BROADBAND_GET_PRIVATE(self);
+
+    /* We schedule it in an idle just to follow the same logic as in the
+     * generic modem implementation. */
+    nm_clear_g_source(&priv->idle_id_ip6);
+    priv->idle_id_ip6 = g_idle_add((GSourceFunc) stage3_ip6_done, self);
+
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+/*****************************************************************************/
+/* Disconnect */
+
+typedef struct {
+    NMModemBroadband *         self;
+    _NMModemDisconnectCallback callback;
+    gpointer                   callback_user_data;
+    GCancellable *             cancellable;
+    gboolean                   warn;
+} DisconnectContext;
+
+static void
+disconnect_context_complete(DisconnectContext *ctx, GError *error)
+{
+    if (ctx->callback)
+        ctx->callback(NM_MODEM(ctx->self), error, ctx->callback_user_data);
+    nm_g_object_unref(ctx->cancellable);
+    g_object_unref(ctx->self);
+    g_slice_free(DisconnectContext, ctx);
+}
+
+static void
+disconnect_context_complete_on_idle(gpointer user_data, GCancellable *cancellable)
+{
+    DisconnectContext *ctx                = user_data;
+    gs_free_error GError *cancelled_error = NULL;
+
+    g_cancellable_set_error_if_cancelled(cancellable, &cancelled_error);
+    disconnect_context_complete(ctx, cancelled_error);
+}
+
+static void
+simple_disconnect_ready(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    MMModemSimple *    modem_iface = MM_MODEM_SIMPLE(source_object);
+    DisconnectContext *ctx         = user_data;
+    gs_free_error GError *error    = NULL;
+
+    if (!mm_modem_simple_disconnect_finish(modem_iface, res, &error)) {
+        if (ctx->warn && !g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN)) {
+            NMModemBroadband *self = ctx->self;
+
+            _LOGW("failed to disconnect modem: %s", error->message);
+        }
+    }
+
+    disconnect_context_complete(ctx, error);
+}
+
+static void
+disconnect(NMModem *                  modem,
+           gboolean                   warn,
+           GCancellable *             cancellable,
+           _NMModemDisconnectCallback callback,
+           gpointer                   user_data)
+{
+    NMModemBroadband * self = NM_MODEM_BROADBAND(modem);
+    DisconnectContext *ctx;
+
+    connect_context_clear(self);
+    _nm_modem_set_apn(NM_MODEM(self), NULL);
+
+    ctx                     = g_slice_new0(DisconnectContext);
+    ctx->self               = g_object_ref(self);
+    ctx->cancellable        = nm_g_object_ref(cancellable);
+    ctx->callback           = callback;
+    ctx->callback_user_data = user_data;
+
+    /* Don't bother warning on FAILED since the modem is already gone */
+    ctx->warn = warn;
+
+    /* Already cancelled or no simple-iface? We are done. */
+    if (!ctx->self->_priv.simple_iface || g_cancellable_is_cancelled(cancellable)) {
+        nm_utils_invoke_on_idle(cancellable, disconnect_context_complete_on_idle, ctx);
+        return;
+    }
+
+    _LOGD("notifying ModemManager about the modem disconnection");
+    mm_modem_simple_disconnect(self->_priv.simple_iface,
+                               NULL, /* bearer path; if NULL given ALL get disconnected */
+                               cancellable,
+                               simple_disconnect_ready,
+                               ctx);
+}
+
+/*****************************************************************************/
+
+static void
+deactivate_cleanup(NMModem *modem, NMDevice *device, gboolean stop_ppp_manager)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(modem);
+
+    /* TODO: cancel SimpleConnect() if any */
+
+    /* Cleanup IPv4 addresses and routes */
+    g_clear_object(&self->_priv.ipv4_config);
+    g_clear_object(&self->_priv.ipv6_config);
+    g_clear_object(&self->_priv.bearer);
+
+    self->_priv.pin_tries = 0;
+
+    NM_MODEM_CLASS(nm_modem_broadband_parent_class)
+        ->deactivate_cleanup(modem, device, stop_ppp_manager);
+}
+
+/*****************************************************************************/
+
+#define MAP_STATE(name)         \
+    case MM_MODEM_STATE_##name: \
+        return NM_MODEM_STATE_##name;
+
+static NMModemState
+mm_state_to_nm(MMModemState mm_state)
+{
+    switch (mm_state) {
+        MAP_STATE(UNKNOWN)
+        MAP_STATE(FAILED)
+        MAP_STATE(INITIALIZING)
+        MAP_STATE(LOCKED)
+        MAP_STATE(DISABLED)
+        MAP_STATE(DISABLING)
+        MAP_STATE(ENABLING)
+        MAP_STATE(ENABLED)
+        MAP_STATE(SEARCHING)
+        MAP_STATE(REGISTERED)
+        MAP_STATE(DISCONNECTING)
+        MAP_STATE(CONNECTING)
+        MAP_STATE(CONNECTED)
+    }
+    return NM_MODEM_STATE_UNKNOWN;
+}
+
+static void
+modem_state_changed(MMModem *                modem,
+                    MMModemState             old_state,
+                    MMModemState             new_state,
+                    MMModemStateChangeReason reason,
+                    NMModemBroadband *       self)
+{
+    /* After the SIM is unlocked MM1 will move the device to INITIALIZING which
+     * is an unavailable state.  That makes state handling confusing here, so
+     * suppress this state change and let the modem move from LOCKED to DISABLED.
+     */
+    if (new_state == MM_MODEM_STATE_INITIALIZING && old_state == MM_MODEM_STATE_LOCKED)
+        return;
+
+    nm_modem_set_state(NM_MODEM(self),
+                       mm_state_to_nm(new_state),
+                       mm_modem_state_change_reason_get_string(reason));
+
+    if (self->_priv.ctx && self->_priv.ctx->step == CONNECT_STEP_WAIT_FOR_READY)
+        connect_context_step(self);
+}
+
+/*****************************************************************************/
+
+static NMModemIPType
+mm_ip_family_to_nm(MMBearerIpFamily family)
+{
+    NMModemIPType nm_type = NM_MODEM_IP_TYPE_UNKNOWN;
+
+    if (family & MM_BEARER_IP_FAMILY_IPV4)
+        nm_type |= NM_MODEM_IP_TYPE_IPV4;
+    if (family & MM_BEARER_IP_FAMILY_IPV6)
+        nm_type |= NM_MODEM_IP_TYPE_IPV6;
+    if (family & MM_BEARER_IP_FAMILY_IPV4V6)
+        nm_type |= MM_BEARER_IP_FAMILY_IPV4V6;
+
+    return nm_type;
+}
+
+static void
+get_sim_ready(MMModem *modem, GAsyncResult *res, NMModemBroadband *self)
+{
+    GError *error = NULL;
+    MMSim * new_sim;
+
+    new_sim = mm_modem_get_sim_finish(modem, res, &error);
+    if (new_sim != self->_priv.sim_iface) {
+        g_clear_object(&self->_priv.sim_iface);
+        self->_priv.sim_iface = new_sim;
+    } else
+        g_clear_object(&new_sim);
+
+    if (self->_priv.sim_iface) {
+        g_object_set(G_OBJECT(self),
+                     NM_MODEM_SIM_ID,
+                     mm_sim_get_identifier(self->_priv.sim_iface),
+                     NM_MODEM_SIM_OPERATOR_ID,
+                     mm_sim_get_operator_identifier(self->_priv.sim_iface),
+                     NULL);
+
+        /* If we're waiting for the SIM during a connect, proceed with the connect */
+        if (self->_priv.ctx && self->_priv.ctx->step == CONNECT_STEP_WAIT_FOR_SIM)
+            connect_context_step(self);
+    } else {
+        _NMLOG(g_error_matches(error, MM_CORE_ERROR, MM_CORE_ERROR_NOT_FOUND) ? LOGL_INFO
+                                                                              : LOGL_WARN,
+               "failed to retrieve SIM object: %s",
+               NM_G_ERROR_MSG(error));
+    }
+    g_clear_error(&error);
+    g_object_unref(self);
+}
+
+static void
+sim_changed(MMModem *modem, GParamSpec *pspec, gpointer user_data)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(user_data);
+
+    g_return_if_fail(modem == self->_priv.modem_iface);
+
+    if (mm_modem_get_sim_path(self->_priv.modem_iface)) {
+        mm_modem_get_sim(self->_priv.modem_iface,
+                         NULL, /* cancellable */
+                         (GAsyncReadyCallback) get_sim_ready,
+                         g_object_ref(self));
+    } else
+        g_object_set(G_OBJECT(self), NM_MODEM_SIM_ID, NULL, NM_MODEM_SIM_OPERATOR_ID, NULL, NULL);
+}
+
+static void
+supported_ip_families_changed(MMModem *modem, GParamSpec *pspec, gpointer user_data)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(user_data);
+
+    g_return_if_fail(modem == self->_priv.modem_iface);
+
+    g_object_set(G_OBJECT(self),
+                 NM_MODEM_IP_TYPES,
+                 mm_ip_family_to_nm(mm_modem_get_supported_ip_families(modem)),
+                 NULL);
+}
+
+static void
+operator_code_changed(MMModem3gpp *modem_3gpp, GParamSpec *pspec, gpointer user_data)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(user_data);
+
+    g_return_if_fail(modem_3gpp == self->_priv.modem_3gpp_iface);
+    _nm_modem_set_operator_code(NM_MODEM(self), mm_modem_3gpp_get_operator_code(modem_3gpp));
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(object);
+
+    switch (prop_id) {
+    case PROP_MODEM:
+        g_value_set_object(value, self->_priv.modem_object);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+static void
+set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(object);
+
+    switch (prop_id) {
+    case PROP_MODEM:
+        /* construct-only */
+        self->_priv.modem_object = g_value_dup_object(value);
+        self->_priv.modem_iface  = mm_object_get_modem(self->_priv.modem_object);
+        g_return_if_fail(self->_priv.modem_iface);
+        self->_priv.modem_3gpp_iface = mm_object_get_modem_3gpp(self->_priv.modem_object);
+
+        g_signal_connect(self->_priv.modem_iface,
+                         "state-changed",
+                         G_CALLBACK(modem_state_changed),
+                         self);
+        g_signal_connect(self->_priv.modem_iface, "notify::sim", G_CALLBACK(sim_changed), self);
+        sim_changed(self->_priv.modem_iface, NULL, self);
+        g_signal_connect(self->_priv.modem_iface,
+                         "notify::supported-ip-families",
+                         G_CALLBACK(supported_ip_families_changed),
+                         self);
+
+        if (self->_priv.modem_3gpp_iface) {
+            g_signal_connect(self->_priv.modem_3gpp_iface,
+                             "notify::operator-code",
+                             G_CALLBACK(operator_code_changed),
+                             self);
+        }
+
+        /* Note: don't grab the Simple iface here; the Modem interface is the
+         * only one assumed to be always valid and available */
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_modem_broadband_init(NMModemBroadband *self)
+{}
+
+NMModem *
+nm_modem_broadband_new(GObject *object, GError **error)
+{
+    MMObject *         modem_object;
+    MMModem *          modem_iface;
+    MMModem3gpp *      modem_3gpp_iface;
+    const char *const *drivers;
+    const char *       operator_code = NULL;
+    gs_free char *     driver        = NULL;
+
+    g_return_val_if_fail(MM_IS_OBJECT(object), NULL);
+    modem_object = MM_OBJECT(object);
+
+    /* Ensure we have the 'Modem' interface and the primary port at least */
+    modem_iface = mm_object_peek_modem(modem_object);
+    g_return_val_if_fail(modem_iface, NULL);
+    g_return_val_if_fail(mm_modem_get_primary_port(modem_iface), NULL);
+
+    /* Build a single string with all drivers listed */
+    drivers = mm_modem_get_drivers(modem_iface);
+    if (drivers)
+        driver = g_strjoinv(", ", (char **) drivers);
+
+    modem_3gpp_iface = mm_object_peek_modem_3gpp(modem_object);
+    if (modem_3gpp_iface)
+        operator_code = mm_modem_3gpp_get_operator_code(modem_3gpp_iface);
+
+    return g_object_new(NM_TYPE_MODEM_BROADBAND,
+                        NM_MODEM_PATH,
+                        mm_object_get_path(modem_object),
+                        NM_MODEM_UID,
+                        mm_modem_get_primary_port(modem_iface),
+                        NM_MODEM_CONTROL_PORT,
+                        mm_modem_get_primary_port(modem_iface),
+                        NM_MODEM_IP_TYPES,
+                        mm_ip_family_to_nm(mm_modem_get_supported_ip_families(modem_iface)),
+                        NM_MODEM_STATE,
+                        (int) mm_state_to_nm(mm_modem_get_state(modem_iface)),
+                        NM_MODEM_DEVICE_ID,
+                        mm_modem_get_device_identifier(modem_iface),
+                        NM_MODEM_BROADBAND_MODEM,
+                        modem_object,
+                        NM_MODEM_DRIVER,
+                        driver,
+                        NM_MODEM_OPERATOR_CODE,
+                        operator_code,
+                        NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMModemBroadband *       self = NM_MODEM_BROADBAND(object);
+    NMModemBroadbandPrivate *priv = NM_MODEM_BROADBAND_GET_PRIVATE(self);
+
+    nm_clear_g_source(&priv->idle_id_ip4);
+    nm_clear_g_source(&priv->idle_id_ip6);
+
+    connect_context_clear(self);
+    g_clear_object(&self->_priv.ipv4_config);
+    g_clear_object(&self->_priv.ipv6_config);
+    g_clear_object(&self->_priv.bearer);
+
+    if (self->_priv.modem_iface) {
+        g_signal_handlers_disconnect_by_data(self->_priv.modem_iface, self);
+        g_clear_object(&self->_priv.modem_iface);
+    }
+
+    if (self->_priv.modem_3gpp_iface) {
+        g_signal_handlers_disconnect_by_data(self->_priv.modem_3gpp_iface, self);
+        g_clear_object(&self->_priv.modem_3gpp_iface);
+    }
+
+    g_clear_object(&self->_priv.simple_iface);
+    g_clear_object(&self->_priv.sim_iface);
+    g_clear_object(&self->_priv.modem_object);
+
+    G_OBJECT_CLASS(nm_modem_broadband_parent_class)->dispose(object);
+}
+
+static void
+nm_modem_broadband_class_init(NMModemBroadbandClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+    NMModemClass *modem_class  = NM_MODEM_CLASS(klass);
+
+    object_class->dispose      = dispose;
+    object_class->get_property = get_property;
+    object_class->set_property = set_property;
+
+    modem_class->get_capabilities                       = get_capabilities;
+    modem_class->static_stage3_ip4_config_start         = static_stage3_ip4_config_start;
+    modem_class->stage3_ip6_config_request              = stage3_ip6_config_request;
+    modem_class->disconnect                             = disconnect;
+    modem_class->deactivate_cleanup                     = deactivate_cleanup;
+    modem_class->set_mm_enabled                         = set_mm_enabled;
+    modem_class->get_user_pass                          = get_user_pass;
+    modem_class->check_connection_compatible_with_modem = check_connection_compatible_with_modem;
+    modem_class->complete_connection                    = complete_connection;
+    modem_class->modem_act_stage1_prepare               = modem_act_stage1_prepare;
+    modem_class->owns_port                              = owns_port;
+
+    obj_properties[PROP_MODEM] =
+        g_param_spec_object(NM_MODEM_BROADBAND_MODEM,
+                            "",
+                            "",
+                            MM_GDBUS_TYPE_OBJECT,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
diff --git a/src/core/devices/wwan/nm-modem-broadband.h b/src/core/devices/wwan/nm-modem-broadband.h
new file mode 100644
index 00000000..627fe25a
--- /dev/null
+++ b/src/core/devices/wwan/nm-modem-broadband.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2012 - Aleksander Morgado <aleksander@gnu.org>
+ */
+
+#ifndef __NETWORKMANAGER_MODEM_BROADBAND_H__
+#define __NETWORKMANAGER_MODEM_BROADBAND_H__
+
+#include "nm-modem.h"
+
+#define NM_TYPE_MODEM_BROADBAND (nm_modem_broadband_get_type())
+#define NM_MODEM_BROADBAND(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM_BROADBAND, NMModemBroadband))
+#define NM_MODEM_BROADBAND_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MODEM_BROADBAND, NMModemBroadbandClass))
+#define NM_IS_MODEM_BROADBAND(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MODEM_BROADBAND))
+#define NM_IS_MODEM_BROADBAND_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_MODEM_BROADBAND))
+#define NM_MODEM_BROADBAND_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_MODEM_BROADBAND, NMModemBroadbandClass))
+
+typedef struct _NMModemBroadband      NMModemBroadband;
+typedef struct _NMModemBroadbandClass NMModemBroadbandClass;
+
+GType nm_modem_broadband_get_type(void);
+
+NMModem *nm_modem_broadband_new(GObject *object, GError **error);
+
+#endif /* __NETWORKMANAGER_MODEM_BROADBAND_H__ */
diff --git a/src/core/devices/wwan/nm-modem-manager.c b/src/core/devices/wwan/nm-modem-manager.c
new file mode 100644
index 00000000..598c6898
--- /dev/null
+++ b/src/core/devices/wwan/nm-modem-manager.c
@@ -0,0 +1,876 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2009 - 2014 Red Hat, Inc.
+ * Copyright (C) 2009 Novell, Inc.
+ * Copyright (C) 2009 - 2013 Canonical Ltd.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-modem-manager.h"
+
+#include <libmm-glib.h>
+
+#if HAVE_LIBSYSTEMD
+    #include <systemd/sd-daemon.h>
+#else
+    #define sd_booted() FALSE
+#endif
+
+#include "nm-std-aux/nm-dbus-compat.h"
+#include "nm-modem.h"
+#include "nm-modem-broadband.h"
+
+#if WITH_OFONO
+    #include "nm-modem-ofono.h"
+#endif
+
+#define MODEM_POKE_INTERVAL 120
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMModemManager, PROP_NAME_OWNER, );
+
+enum {
+    MODEM_ADDED,
+    LAST_SIGNAL,
+};
+
+static guint signals[LAST_SIGNAL] = {0};
+
+typedef struct {
+    GDBusConnection *dbus_connection;
+
+    /* used during g_bus_get() and later during mm_manager_new(). */
+    GCancellable *main_cancellable;
+
+    struct {
+        MMManager *   manager;
+        GCancellable *poke_cancellable;
+        gulong        handle_name_owner_changed_id;
+        gulong        handle_object_added_id;
+        gulong        handle_object_removed_id;
+        guint         relaunch_id;
+
+        /* this only has one use: that the <info> logging line about
+         * ModemManager available distinguishes between first-time
+         * and later name-owner-changed. */
+        enum {
+            LOG_AVAILABLE_NOT_INITIALIZED = 0,
+            LOG_AVAILABLE_YES,
+            LOG_AVAILABLE_NO,
+        } log_available : 3;
+
+        GDBusProxy *  proxy;
+        GCancellable *proxy_cancellable;
+        guint         proxy_ref_count;
+        char *        proxy_name_owner;
+    } modm;
+
+#if WITH_OFONO
+    struct {
+        GDBusProxy *  proxy;
+        GCancellable *cancellable;
+    } ofono;
+#endif
+
+    GHashTable *modems;
+} NMModemManagerPrivate;
+
+struct _NMModemManager {
+    GObject               parent;
+    NMModemManagerPrivate _priv;
+};
+
+struct _NMModemManagerClass {
+    GObjectClass parent;
+};
+
+G_DEFINE_TYPE(NMModemManager, nm_modem_manager, G_TYPE_OBJECT)
+
+#define NM_MODEM_MANAGER_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMModemManager, NM_IS_MODEM_MANAGER)
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN      LOGD_MB
+#define _NMLOG(level, ...) __NMLOG_DEFAULT(level, _NMLOG_DOMAIN, "modem-manager", __VA_ARGS__)
+
+/*****************************************************************************/
+
+NM_DEFINE_SINGLETON_GETTER(NMModemManager, nm_modem_manager_get, NM_TYPE_MODEM_MANAGER);
+
+/*****************************************************************************/
+
+static void modm_schedule_manager_relaunch(NMModemManager *self, guint n_seconds);
+static void modm_ensure_manager(NMModemManager *self);
+
+/*****************************************************************************/
+
+static void
+handle_new_modem(NMModemManager *self, NMModem *modem)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    const char *           path;
+
+    path = nm_modem_get_path(modem);
+    if (g_hash_table_lookup(priv->modems, path)) {
+        g_warn_if_reached();
+        return;
+    }
+
+    /* Track the new modem */
+    g_hash_table_insert(priv->modems, g_strdup(path), modem);
+    g_signal_emit(self, signals[MODEM_ADDED], 0, modem);
+}
+
+static gboolean
+remove_one_modem(gpointer key, gpointer value, gpointer user_data)
+{
+    nm_modem_emit_removed(NM_MODEM(value));
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+NMModem **
+nm_modem_manager_get_modems(NMModemManager *self, guint *out_len)
+{
+    g_return_val_if_fail(NM_IS_MODEM_MANAGER(self), NULL);
+
+    return (NMModem **) nm_utils_hash_values_to_array(NM_MODEM_MANAGER_GET_PRIVATE(self)->modems,
+                                                      NULL,
+                                                      NULL,
+                                                      out_len);
+}
+
+/*****************************************************************************/
+
+static void
+modm_clear_manager(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    if (!priv->modm.manager)
+        return;
+    nm_clear_g_signal_handler(priv->modm.manager, &priv->modm.handle_name_owner_changed_id);
+    nm_clear_g_signal_handler(priv->modm.manager, &priv->modm.handle_object_added_id);
+    nm_clear_g_signal_handler(priv->modm.manager, &priv->modm.handle_object_removed_id);
+    g_clear_object(&priv->modm.manager);
+}
+
+static void
+modm_handle_object_added(MMManager *modem_manager, MMObject *modem_object, NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    const char *           path;
+    MMModem *              modem_iface;
+    NMModem *              modem;
+    GError *               error = NULL;
+
+    /* Ensure we don't have the same modem already */
+    path = mm_object_get_path(modem_object);
+    if (g_hash_table_lookup(priv->modems, path)) {
+        _LOGW("modem with path %s already exists, ignoring", path);
+        return;
+    }
+
+    /* Ensure we have the 'Modem' interface at least */
+    modem_iface = mm_object_peek_modem(modem_object);
+    if (!modem_iface) {
+        _LOGW("modem with path %s doesn't have the Modem interface, ignoring", path);
+        return;
+    }
+
+    /* Ensure we have a primary port reported */
+    if (!mm_modem_get_primary_port(modem_iface)) {
+        _LOGW("modem with path %s has unknown primary port, ignoring", path);
+        return;
+    }
+
+    /* Create a new modem object */
+    modem = nm_modem_broadband_new(G_OBJECT(modem_object), &error);
+    if (modem)
+        handle_new_modem(self, modem);
+    else
+        _LOGW("failed to create modem: %s", error->message);
+    g_clear_error(&error);
+}
+
+static void
+modm_handle_object_removed(MMManager *manager, MMObject *modem_object, NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    NMModem *              modem;
+    const char *           path;
+
+    path  = mm_object_get_path(modem_object);
+    modem = (NMModem *) g_hash_table_lookup(priv->modems, path);
+    if (!modem)
+        return;
+
+    nm_modem_emit_removed(modem);
+    g_hash_table_remove(priv->modems, path);
+}
+
+static void
+modm_manager_available(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    GList *                modems, *l;
+
+    if (priv->modm.log_available != LOG_AVAILABLE_YES) {
+        _LOGI("ModemManager %savailable", priv->modm.log_available ? "now " : "");
+        priv->modm.log_available = LOG_AVAILABLE_YES;
+    }
+
+    /* Update initial modems list */
+    modems = g_dbus_object_manager_get_objects(G_DBUS_OBJECT_MANAGER(priv->modm.manager));
+    for (l = modems; l; l = g_list_next(l))
+        modm_handle_object_added(priv->modm.manager, MM_OBJECT(l->data), self);
+    g_list_free_full(modems, (GDestroyNotify) g_object_unref);
+}
+
+static void
+modm_handle_name_owner_changed(MMManager *modem_manager, GParamSpec *pspec, NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    char *                 name_owner;
+
+    /* Quit poking, if any */
+    nm_clear_g_source(&priv->modm.relaunch_id);
+
+    name_owner =
+        g_dbus_object_manager_client_get_name_owner(G_DBUS_OBJECT_MANAGER_CLIENT(modem_manager));
+    if (!name_owner) {
+        if (priv->modm.log_available != LOG_AVAILABLE_NO) {
+            _LOGI("ModemManager %savailable", priv->modm.log_available ? "no longer " : "not ");
+            priv->modm.log_available = LOG_AVAILABLE_NO;
+        }
+
+        /* If not managed by systemd, schedule relaunch */
+        if (!sd_booted())
+            modm_schedule_manager_relaunch(self, 0);
+
+        return;
+    }
+
+    /* Available! */
+    g_free(name_owner);
+
+    /* Hack alert: GDBusObjectManagerClient won't signal neither 'object-added'
+     * nor 'object-removed' if it was created while there was no ModemManager in
+     * the bus. This hack avoids this issue until we get a GIO with the fix
+     * included... */
+    modm_clear_manager(self);
+    modm_ensure_manager(self);
+
+    /* Whenever GDBusObjectManagerClient is fixed, we can just do the following:
+     * modm_manager_available (self);
+     */
+}
+
+static void
+modm_manager_poke_cb(GObject *connection, GAsyncResult *res, gpointer user_data)
+{
+    NMModemManager *       self;
+    NMModemManagerPrivate *priv;
+    gs_free_error GError *error       = NULL;
+    gs_unref_variant GVariant *result = NULL;
+
+    result = g_dbus_connection_call_finish(G_DBUS_CONNECTION(connection), res, &error);
+
+    if (!result && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = user_data;
+    priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    g_clear_object(&priv->modm.poke_cancellable);
+
+    if (error) {
+        _LOGW("error poking ModemManager: %s", error->message);
+
+        /* Don't reschedule poke is MM service doesn't exist. */
+        if (!g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN)
+            && !g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SPAWN_SERVICE_NOT_FOUND)) {
+            /* Setup timeout to relaunch */
+            modm_schedule_manager_relaunch(self, MODEM_POKE_INTERVAL);
+        }
+    }
+}
+
+static void
+modm_manager_poke(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    nm_clear_g_cancellable(&priv->modm.poke_cancellable);
+    priv->modm.poke_cancellable = g_cancellable_new();
+
+    /* If there is no current owner right away, ensure we poke to get one */
+    g_dbus_connection_call(priv->dbus_connection,
+                           NM_MODEM_MANAGER_MM_DBUS_SERVICE,
+                           NM_MODEM_MANAGER_MM_DBUS_PATH,
+                           DBUS_INTERFACE_PEER,
+                           "Ping",
+                           NULL,
+                           NULL,
+                           G_DBUS_CALL_FLAGS_NONE,
+                           -1,
+                           priv->modm.poke_cancellable,
+                           modm_manager_poke_cb,
+                           self);
+}
+
+static void
+modm_manager_check_name_owner(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv       = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    gs_free char *         name_owner = NULL;
+
+    name_owner = g_dbus_object_manager_client_get_name_owner(
+        G_DBUS_OBJECT_MANAGER_CLIENT(priv->modm.manager));
+    if (name_owner) {
+        modm_manager_available(self);
+        return;
+    }
+
+    /* If the lifecycle is not managed by systemd, poke */
+    if (!sd_booted())
+        modm_manager_poke(self);
+}
+
+static void
+modm_manager_new_cb(GObject *source, GAsyncResult *res, gpointer user_data)
+{
+    NMModemManager *       self;
+    NMModemManagerPrivate *priv;
+    gs_free_error GError *error = NULL;
+    MMManager *           modem_manager;
+
+    modem_manager = mm_manager_new_finish(res, &error);
+    if (!modem_manager && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = user_data;
+    priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    nm_assert(!priv->modm.manager);
+
+    g_clear_object(&priv->main_cancellable);
+
+    if (!modem_manager) {
+        /* We're not really supposed to get any error here. If we do get one,
+         * though, just re-schedule the MMManager creation after some time.
+         * During this period, name-owner changes won't be followed. */
+        _LOGW("error creating ModemManager client: %s", error->message);
+        /* Setup timeout to relaunch */
+        modm_schedule_manager_relaunch(self, MODEM_POKE_INTERVAL);
+        return;
+    }
+
+    priv->modm.manager = modem_manager;
+
+    /* Setup signals in the GDBusObjectManagerClient */
+    priv->modm.handle_name_owner_changed_id =
+        g_signal_connect(priv->modm.manager,
+                         "notify::name-owner",
+                         G_CALLBACK(modm_handle_name_owner_changed),
+                         self);
+    priv->modm.handle_object_added_id   = g_signal_connect(priv->modm.manager,
+                                                         "object-added",
+                                                         G_CALLBACK(modm_handle_object_added),
+                                                         self);
+    priv->modm.handle_object_removed_id = g_signal_connect(priv->modm.manager,
+                                                           "object-removed",
+                                                           G_CALLBACK(modm_handle_object_removed),
+                                                           self);
+
+    modm_manager_check_name_owner(self);
+}
+
+static void
+modm_ensure_manager(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    g_assert(priv->dbus_connection);
+
+    /* Create the GDBusObjectManagerClient. We do not request to autostart, as
+     * we don't really want the MMManager creation to fail. We can always poke
+     * later on if we want to request the autostart */
+    if (!priv->modm.manager) {
+        if (!priv->main_cancellable)
+            priv->main_cancellable = g_cancellable_new();
+        mm_manager_new(priv->dbus_connection,
+                       G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START,
+                       priv->main_cancellable,
+                       modm_manager_new_cb,
+                       self);
+        return;
+    }
+
+    /* If already available, recheck name owner! */
+    modm_manager_check_name_owner(self);
+}
+
+static gboolean
+modm_schedule_manager_relaunch_cb(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    priv->modm.relaunch_id = 0;
+    modm_ensure_manager(self);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+modm_schedule_manager_relaunch(NMModemManager *self, guint n_seconds)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    /* No need to pass an extra reference to self; timeout/idle will be
+     * cancelled if the object gets disposed. */
+    if (n_seconds)
+        priv->modm.relaunch_id =
+            g_timeout_add_seconds(n_seconds, (GSourceFunc) modm_schedule_manager_relaunch_cb, self);
+    else
+        priv->modm.relaunch_id = g_idle_add((GSourceFunc) modm_schedule_manager_relaunch_cb, self);
+}
+
+/*****************************************************************************/
+
+static void
+modm_proxy_name_owner_reset(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    char *                 name = NULL;
+
+    if (priv->modm.proxy)
+        name = g_dbus_proxy_get_name_owner(priv->modm.proxy);
+
+    if (nm_streq0(priv->modm.proxy_name_owner, name)) {
+        g_free(name);
+        return;
+    }
+    g_free(priv->modm.proxy_name_owner);
+    priv->modm.proxy_name_owner = name;
+
+    _notify(self, PROP_NAME_OWNER);
+}
+
+static void
+modm_proxy_name_owner_changed_cb(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+    modm_proxy_name_owner_reset(user_data);
+}
+
+static void
+modm_proxy_new_cb(GObject *source_object, GAsyncResult *result, gpointer user_data)
+{
+    NMModemManager *       self;
+    NMModemManagerPrivate *priv;
+    GDBusProxy *           proxy;
+    gs_free_error GError *error = NULL;
+
+    proxy = g_dbus_proxy_new_for_bus_finish(result, &error);
+    if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = user_data;
+    priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    g_clear_object(&priv->modm.proxy_cancellable);
+
+    if (!proxy) {
+        _LOGW("could not obtain D-Bus proxy for ModemManager: %s", error->message);
+        return;
+    }
+
+    priv->modm.proxy = proxy;
+    g_signal_connect(priv->modm.proxy,
+                     "notify::g-name-owner",
+                     G_CALLBACK(modm_proxy_name_owner_changed_cb),
+                     self);
+
+    modm_proxy_name_owner_reset(self);
+}
+
+void
+nm_modem_manager_name_owner_ref(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv;
+
+    g_return_if_fail(NM_IS_MODEM_MANAGER(self));
+
+    priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    if (priv->modm.proxy_ref_count++ > 0) {
+        /* only try once to create the proxy. If proxy creation
+         * for the first "ref" failed, it's unclear what to do.
+         * The proxy is hosed. */
+        return;
+    }
+
+    nm_assert(!priv->modm.proxy && !priv->modm.proxy_cancellable);
+
+    priv->modm.proxy_cancellable = g_cancellable_new();
+
+    g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM,
+                             G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES
+                                 | G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS
+                                 | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+                             NULL,
+                             NM_MODEM_MANAGER_MM_DBUS_SERVICE,
+                             NM_MODEM_MANAGER_MM_DBUS_PATH,
+                             NM_MODEM_MANAGER_MM_DBUS_INTERFACE,
+                             priv->modm.proxy_cancellable,
+                             modm_proxy_new_cb,
+                             self);
+}
+
+void
+nm_modem_manager_name_owner_unref(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv;
+
+    g_return_if_fail(NM_IS_MODEM_MANAGER(self));
+
+    priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->modm.proxy_ref_count > 0);
+
+    if (--priv->modm.proxy_ref_count > 0)
+        return;
+
+    nm_clear_g_cancellable(&priv->modm.proxy_cancellable);
+    g_clear_object(&priv->modm.proxy);
+
+    modm_proxy_name_owner_reset(self);
+}
+
+const char *
+nm_modem_manager_name_owner_get(NMModemManager *self)
+{
+    g_return_val_if_fail(NM_IS_MODEM_MANAGER(self), NULL);
+    nm_assert(NM_MODEM_MANAGER_GET_PRIVATE(self)->modm.proxy_ref_count > 0);
+
+    return NM_MODEM_MANAGER_GET_PRIVATE(self)->modm.proxy_name_owner;
+}
+
+/*****************************************************************************/
+
+#if WITH_OFONO
+
+static void
+ofono_create_modem(NMModemManager *self, const char *path)
+{
+    NMModemManagerPrivate *priv  = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    NMModem *              modem = NULL;
+
+    /* Ensure duplicate modems aren't created.  Because we're not using the
+     * ObjectManager interface there's a race during oFono startup where we
+     * receive ModemAdded signals before GetModems() returns, so some of the
+     * modems returned from GetModems() may already have been created.
+     */
+    if (!g_hash_table_lookup(priv->modems, path)) {
+        modem = nm_modem_ofono_new(path);
+        if (modem)
+            handle_new_modem(self, modem);
+        else
+            _LOGW("Failed to create oFono modem for %s", path);
+    }
+}
+
+static void
+ofono_signal_cb(GDBusProxy *proxy,
+                char *      sender_name,
+                char *      signal_name,
+                GVariant *  parameters,
+                gpointer    user_data)
+{
+    NMModemManager *       self = NM_MODEM_MANAGER(user_data);
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    char *                 object_path;
+    NMModem *              modem;
+
+    if (g_strcmp0(signal_name, "ModemAdded") == 0) {
+        g_variant_get(parameters, "(oa{sv})", &object_path, NULL);
+        _LOGI("oFono modem appeared: %s", object_path);
+
+        ofono_create_modem(NM_MODEM_MANAGER(user_data), object_path);
+        g_free(object_path);
+    } else if (g_strcmp0(signal_name, "ModemRemoved") == 0) {
+        g_variant_get(parameters, "(o)", &object_path);
+        _LOGI("oFono modem removed: %s", object_path);
+
+        modem = (NMModem *) g_hash_table_lookup(priv->modems, object_path);
+        if (modem) {
+            nm_modem_emit_removed(modem);
+            g_hash_table_remove(priv->modems, object_path);
+        } else {
+            _LOGW("could not remove modem %s, not found in table", object_path);
+        }
+        g_free(object_path);
+    }
+}
+
+static void
+ofono_enumerate_devices_done(GObject *proxy, GAsyncResult *res, gpointer user_data)
+{
+    NMModemManager *       self;
+    NMModemManagerPrivate *priv;
+    gs_free_error GError *error = NULL;
+    GVariant *            results;
+    GVariantIter *        iter;
+    const char *          path;
+
+    results = g_dbus_proxy_call_finish(G_DBUS_PROXY(proxy), res, &error);
+    if (!results && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_MANAGER(user_data);
+    priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    g_clear_object(&priv->ofono.cancellable);
+
+    if (!results) {
+        _LOGW("failed to enumerate oFono devices: %s", error->message);
+        return;
+    }
+
+    g_variant_get(results, "(a(oa{sv}))", &iter);
+    while (g_variant_iter_loop(iter, "(&oa{sv})", &path, NULL))
+        ofono_create_modem(self, path);
+    g_variant_iter_free(iter);
+    g_variant_unref(results);
+}
+
+static void
+ofono_check_name_owner(NMModemManager *self, gboolean first_invocation)
+{
+    NMModemManagerPrivate *priv       = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    gs_free char *         name_owner = NULL;
+
+    name_owner = g_dbus_proxy_get_name_owner(G_DBUS_PROXY(priv->ofono.proxy));
+    if (name_owner) {
+        _LOGI("oFono is %savailable", first_invocation ? "" : "now ");
+
+        nm_clear_g_cancellable(&priv->ofono.cancellable);
+        priv->ofono.cancellable = g_cancellable_new();
+
+        g_dbus_proxy_call(priv->ofono.proxy,
+                          "GetModems",
+                          NULL,
+                          G_DBUS_CALL_FLAGS_NONE,
+                          -1,
+                          priv->ofono.cancellable,
+                          ofono_enumerate_devices_done,
+                          self);
+    } else {
+        GHashTableIter iter;
+        NMModem *      modem;
+
+        _LOGI("oFono is %savailable", first_invocation ? "not " : "no longer ");
+
+        /* Remove any oFono modems that might be left around */
+        g_hash_table_iter_init(&iter, priv->modems);
+        while (g_hash_table_iter_next(&iter, NULL, (gpointer) &modem)) {
+            if (NM_IS_MODEM_OFONO(modem)) {
+                nm_modem_emit_removed(modem);
+                g_hash_table_iter_remove(&iter);
+            }
+        }
+    }
+}
+
+static void
+ofono_name_owner_changed(GDBusProxy *ofono_proxy, GParamSpec *pspec, NMModemManager *self)
+{
+    ofono_check_name_owner(self, FALSE);
+}
+
+static void
+ofono_proxy_new_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    NMModemManager *       self;
+    NMModemManagerPrivate *priv;
+    gs_free_error GError *error = NULL;
+    GDBusProxy *          proxy;
+
+    proxy = g_dbus_proxy_new_finish(res, &error);
+    if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_MANAGER(user_data);
+    priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    g_clear_object(&priv->ofono.cancellable);
+
+    if (!proxy) {
+        _LOGW("error getting oFono bus proxy: %s", error->message);
+        return;
+    }
+
+    priv->ofono.proxy = proxy;
+
+    g_signal_connect(priv->ofono.proxy,
+                     "notify::g-name-owner",
+                     G_CALLBACK(ofono_name_owner_changed),
+                     self);
+
+    g_signal_connect(priv->ofono.proxy, "g-signal", G_CALLBACK(ofono_signal_cb), self);
+
+    ofono_check_name_owner(self, TRUE);
+}
+
+static void
+ofono_init_proxy(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    nm_assert(priv->dbus_connection);
+    nm_assert(!priv->ofono.cancellable);
+
+    priv->ofono.cancellable = g_cancellable_new();
+
+    g_dbus_proxy_new(priv->dbus_connection,
+                     G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+                     NULL,
+                     OFONO_DBUS_SERVICE,
+                     OFONO_DBUS_PATH,
+                     OFONO_DBUS_INTERFACE,
+                     priv->ofono.cancellable,
+                     ofono_proxy_new_cb,
+                     self);
+}
+#endif
+
+/*****************************************************************************/
+
+static void
+bus_get_ready(GObject *source, GAsyncResult *res, gpointer user_data)
+{
+    NMModemManager *       self;
+    NMModemManagerPrivate *priv;
+    gs_free_error GError *error = NULL;
+    GDBusConnection *     connection;
+
+    connection = g_bus_get_finish(res, &error);
+    if (!connection && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_MANAGER(user_data);
+    priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    if (!connection) {
+        _LOGW("error getting bus connection: %s", error->message);
+        return;
+    }
+
+    priv->dbus_connection = connection;
+
+    modm_ensure_manager(self);
+#if WITH_OFONO
+    ofono_init_proxy(self);
+#endif
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMModemManager *       self = NM_MODEM_MANAGER(object);
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    switch (prop_id) {
+    case PROP_NAME_OWNER:
+        g_value_set_string(value, priv->modm.proxy_name_owner);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_modem_manager_init(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    priv->modems = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_object_unref);
+
+    priv->main_cancellable = g_cancellable_new();
+
+    g_bus_get(G_BUS_TYPE_SYSTEM, priv->main_cancellable, bus_get_ready, self);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMModemManager *       self = NM_MODEM_MANAGER(object);
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    nm_clear_g_cancellable(&priv->main_cancellable);
+    nm_clear_g_cancellable(&priv->modm.poke_cancellable);
+
+    nm_clear_g_source(&priv->modm.relaunch_id);
+
+    nm_clear_g_cancellable(&priv->modm.proxy_cancellable);
+    g_clear_object(&priv->modm.proxy);
+    nm_clear_g_free(&priv->modm.proxy_name_owner);
+
+    modm_clear_manager(self);
+
+#if WITH_OFONO
+    if (priv->ofono.proxy) {
+        g_signal_handlers_disconnect_by_func(priv->ofono.proxy, ofono_name_owner_changed, self);
+        g_signal_handlers_disconnect_by_func(priv->ofono.proxy, ofono_signal_cb, self);
+        g_clear_object(&priv->ofono.proxy);
+    }
+    nm_clear_g_cancellable(&priv->ofono.cancellable);
+#endif
+
+    g_clear_object(&priv->dbus_connection);
+
+    if (priv->modems) {
+        g_hash_table_foreach_remove(priv->modems, remove_one_modem, object);
+        g_hash_table_destroy(priv->modems);
+        priv->modems = NULL;
+    }
+
+    G_OBJECT_CLASS(nm_modem_manager_parent_class)->dispose(object);
+}
+
+static void
+nm_modem_manager_class_init(NMModemManagerClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    object_class->dispose      = dispose;
+    object_class->get_property = get_property;
+
+    obj_properties[PROP_NAME_OWNER] =
+        g_param_spec_string(NM_MODEM_MANAGER_NAME_OWNER,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+
+    signals[MODEM_ADDED] = g_signal_new(NM_MODEM_MANAGER_MODEM_ADDED,
+                                        G_OBJECT_CLASS_TYPE(object_class),
+                                        G_SIGNAL_RUN_FIRST,
+                                        0,
+                                        NULL,
+                                        NULL,
+                                        NULL,
+                                        G_TYPE_NONE,
+                                        1,
+                                        NM_TYPE_MODEM);
+}
diff --git a/src/core/devices/wwan/nm-modem-manager.h b/src/core/devices/wwan/nm-modem-manager.h
new file mode 100644
index 00000000..e89a032b
--- /dev/null
+++ b/src/core/devices/wwan/nm-modem-manager.h
@@ -0,0 +1,45 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2009 - 2014 Red Hat, Inc.
+ * Copyright (C) 2009 Novell, Inc.
+ * Copyright (C) 2009 Canonical Ltd.
+ */
+
+#ifndef __NETWORKMANAGER_MODEM_MANAGER_H__
+#define __NETWORKMANAGER_MODEM_MANAGER_H__
+
+#include "nm-modem.h"
+
+#define NM_TYPE_MODEM_MANAGER (nm_modem_manager_get_type())
+#define NM_MODEM_MANAGER(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM_MANAGER, NMModemManager))
+#define NM_MODEM_MANAGER_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MODEM_MANAGER, NMModemManagerClass))
+#define NM_IS_MODEM_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MODEM_MANAGER))
+#define NM_IS_MODEM_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_MODEM_MANAGER))
+#define NM_MODEM_MANAGER_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_MODEM_MANAGER, NMModemManagerClass))
+
+#define NM_MODEM_MANAGER_MODEM_ADDED "modem-added"
+
+#define NM_MODEM_MANAGER_NAME_OWNER "name-owner"
+
+#define NM_MODEM_MANAGER_MM_DBUS_SERVICE   "org.freedesktop.ModemManager1"
+#define NM_MODEM_MANAGER_MM_DBUS_PATH      "/org/freedesktop/ModemManager1"
+#define NM_MODEM_MANAGER_MM_DBUS_INTERFACE "org.freedesktop.ModemManager1"
+
+typedef struct _NMModemManager      NMModemManager;
+typedef struct _NMModemManagerClass NMModemManagerClass;
+
+GType nm_modem_manager_get_type(void);
+
+NMModemManager *nm_modem_manager_get(void);
+
+void nm_modem_manager_name_owner_ref(NMModemManager *self);
+void nm_modem_manager_name_owner_unref(NMModemManager *self);
+
+const char *nm_modem_manager_name_owner_get(NMModemManager *self);
+
+NMModem **nm_modem_manager_get_modems(NMModemManager *self, guint *out_len);
+
+#endif /* __NETWORKMANAGER_MODEM_MANAGER_H__ */
diff --git a/src/core/devices/wwan/nm-modem-ofono.c b/src/core/devices/wwan/nm-modem-ofono.c
new file mode 100644
index 00000000..21734cee
--- /dev/null
+++ b/src/core/devices/wwan/nm-modem-ofono.c
@@ -0,0 +1,1239 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 - 2016 Canonical Ltd.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-modem-ofono.h"
+
+#include "nm-core-internal.h"
+#include "devices/nm-device-private.h"
+#include "nm-modem.h"
+#include "platform/nm-platform.h"
+#include "nm-ip4-config.h"
+
+#define VARIANT_IS_OF_TYPE_BOOLEAN(v) \
+    ((v) != NULL && (g_variant_is_of_type((v), G_VARIANT_TYPE_BOOLEAN)))
+#define VARIANT_IS_OF_TYPE_STRING(v) \
+    ((v) != NULL && (g_variant_is_of_type((v), G_VARIANT_TYPE_STRING)))
+#define VARIANT_IS_OF_TYPE_OBJECT_PATH(v) \
+    ((v) != NULL && (g_variant_is_of_type((v), G_VARIANT_TYPE_OBJECT_PATH)))
+#define VARIANT_IS_OF_TYPE_STRING_ARRAY(v) \
+    ((v) != NULL && (g_variant_is_of_type((v), G_VARIANT_TYPE_STRING_ARRAY)))
+#define VARIANT_IS_OF_TYPE_DICTIONARY(v) \
+    ((v) != NULL && (g_variant_is_of_type((v), G_VARIANT_TYPE_DICTIONARY)))
+
+/*****************************************************************************/
+
+typedef struct {
+    GHashTable *connect_properties;
+
+    GDBusProxy *modem_proxy;
+    GDBusProxy *connman_proxy;
+    GDBusProxy *context_proxy;
+    GDBusProxy *sim_proxy;
+
+    GCancellable *modem_proxy_cancellable;
+    GCancellable *connman_proxy_cancellable;
+    GCancellable *context_proxy_cancellable;
+    GCancellable *sim_proxy_cancellable;
+
+    GError *property_error;
+
+    char *context_path;
+    char *imsi;
+
+    gboolean modem_online;
+    gboolean gprs_attached;
+
+    NMIP4Config *ip4_config;
+} NMModemOfonoPrivate;
+
+struct _NMModemOfono {
+    NMModem             parent;
+    NMModemOfonoPrivate _priv;
+};
+
+struct _NMModemOfonoClass {
+    NMModemClass parent;
+};
+
+G_DEFINE_TYPE(NMModemOfono, nm_modem_ofono, NM_TYPE_MODEM)
+
+#define NM_MODEM_OFONO_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMModemOfono, NM_IS_MODEM_OFONO)
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN      LOGD_MB
+#define _NMLOG_PREFIX_NAME "modem-ofono"
+#define _NMLOG(level, ...)                                                  \
+    G_STMT_START                                                            \
+    {                                                                       \
+        const NMLogLevel _level = (level);                                  \
+                                                                            \
+        if (nm_logging_enabled(_level, (_NMLOG_DOMAIN))) {                  \
+            NMModemOfono *const __self = (self);                            \
+            char                __prefix_name[128];                         \
+            const char *        __uid;                                      \
+                                                                            \
+            _nm_log(_level,                                                 \
+                    (_NMLOG_DOMAIN),                                        \
+                    0,                                                      \
+                    NULL,                                                   \
+                    NULL,                                                   \
+                    "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),            \
+                    _NMLOG_PREFIX_NAME,                                     \
+                    (__self ? ({                                            \
+                        ((__uid = nm_modem_get_uid((NMModem *) __self))     \
+                             ? nm_sprintf_buf(__prefix_name, "[%s]", __uid) \
+                             : "(null)");                                   \
+                    })                                                      \
+                            : "") _NM_UTILS_MACRO_REST(__VA_ARGS__));       \
+        }                                                                   \
+    }                                                                       \
+    G_STMT_END
+
+/*****************************************************************************/
+
+static void
+get_capabilities(NMModem *                  _self,
+                 NMDeviceModemCapabilities *modem_caps,
+                 NMDeviceModemCapabilities *current_caps)
+{
+    /* FIXME: auto-detect capabilities to allow LTE */
+    *modem_caps   = NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS;
+    *current_caps = NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS;
+}
+
+static void
+update_modem_state(NMModemOfono *self)
+{
+    NMModemOfonoPrivate *priv      = NM_MODEM_OFONO_GET_PRIVATE(self);
+    NMModemState         state     = nm_modem_get_state(NM_MODEM(self));
+    NMModemState         new_state = NM_MODEM_STATE_DISABLED;
+    const char *         reason    = NULL;
+
+    _LOGI("'Attached': %s 'Online': %s 'IMSI': %s",
+          priv->gprs_attached ? "true" : "false",
+          priv->modem_online ? "true" : "false",
+          priv->imsi);
+
+    if (priv->modem_online == FALSE) {
+        reason = "modem 'Online=false'";
+    } else if (priv->imsi == NULL && state != NM_MODEM_STATE_ENABLING) {
+        reason = "modem not ready";
+    } else if (priv->gprs_attached == FALSE) {
+        new_state = NM_MODEM_STATE_SEARCHING;
+        reason    = "modem searching";
+    } else {
+        new_state = NM_MODEM_STATE_REGISTERED;
+        reason    = "modem ready";
+    }
+
+    if (state != new_state)
+        nm_modem_set_state(NM_MODEM(self), new_state, reason);
+}
+
+/* Disconnect */
+typedef struct {
+    NMModemOfono *             self;
+    _NMModemDisconnectCallback callback;
+    gpointer                   callback_user_data;
+    GCancellable *             cancellable;
+    gboolean                   warn;
+} DisconnectContext;
+
+static void
+disconnect_context_complete(DisconnectContext *ctx, GError *error)
+{
+    if (ctx->callback)
+        ctx->callback(NM_MODEM(ctx->self), error, ctx->callback_user_data);
+    nm_g_object_unref(ctx->cancellable);
+    g_object_unref(ctx->self);
+    g_slice_free(DisconnectContext, ctx);
+}
+
+static void
+disconnect_context_complete_on_idle(gpointer user_data, GCancellable *cancellable)
+{
+    DisconnectContext *ctx      = user_data;
+    gs_free_error GError *error = NULL;
+
+    if (!g_cancellable_set_error_if_cancelled(cancellable, &error)) {
+        g_set_error_literal(&error,
+                            NM_UTILS_ERROR,
+                            NM_UTILS_ERROR_UNKNOWN,
+                            ("modem is currently not connected"));
+    }
+    disconnect_context_complete(ctx, error);
+}
+
+static void
+disconnect_done(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    DisconnectContext *ctx       = user_data;
+    NMModemOfono *     self      = ctx->self;
+    gs_free_error GError *error  = NULL;
+    gs_unref_variant GVariant *v = NULL;
+
+    v = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), result, &error);
+    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+        disconnect_context_complete(ctx, error);
+        return;
+    }
+
+    if (error && ctx->warn)
+        _LOGW("failed to disconnect modem: %s", error->message);
+
+    _LOGD("modem disconnected");
+
+    update_modem_state(self);
+    disconnect_context_complete(ctx, error);
+}
+
+static void
+disconnect(NMModem *                  modem,
+           gboolean                   warn,
+           GCancellable *             cancellable,
+           _NMModemDisconnectCallback callback,
+           gpointer                   user_data)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(modem);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    DisconnectContext *  ctx;
+    NMModemState         state = nm_modem_get_state(NM_MODEM(self));
+
+    _LOGD("warn: %s modem_state: %s", warn ? "TRUE" : "FALSE", nm_modem_state_to_string(state));
+
+    ctx                     = g_slice_new0(DisconnectContext);
+    ctx->self               = g_object_ref(self);
+    ctx->cancellable        = nm_g_object_ref(cancellable);
+    ctx->warn               = warn;
+    ctx->callback           = callback;
+    ctx->callback_user_data = user_data;
+
+    if (state != NM_MODEM_STATE_CONNECTED || g_cancellable_is_cancelled(cancellable)) {
+        nm_utils_invoke_on_idle(cancellable, disconnect_context_complete_on_idle, ctx);
+        return;
+    }
+
+    nm_modem_set_state(NM_MODEM(self),
+                       NM_MODEM_STATE_DISCONNECTING,
+                       nm_modem_state_to_string(NM_MODEM_STATE_DISCONNECTING));
+
+    g_dbus_proxy_call(priv->context_proxy,
+                      "SetProperty",
+                      g_variant_new("(sv)", "Active", g_variant_new("b", warn)),
+                      G_DBUS_CALL_FLAGS_NONE,
+                      20000,
+                      ctx->cancellable,
+                      disconnect_done,
+                      ctx);
+}
+
+static void
+deactivate_cleanup(NMModem *modem, NMDevice *device, gboolean stop_ppp_manager)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(modem);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    /* TODO: cancel SimpleConnect() if any */
+
+    g_clear_object(&priv->ip4_config);
+
+    NM_MODEM_CLASS(nm_modem_ofono_parent_class)
+        ->deactivate_cleanup(modem, device, stop_ppp_manager);
+}
+
+static gboolean
+check_connection_compatible_with_modem(NMModem *modem, NMConnection *connection, GError **error)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(modem);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    const char *         id;
+
+    if (!_nm_connection_check_main_setting(connection, NM_SETTING_GSM_SETTING_NAME, NULL)) {
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                           "connection type %s is not supported by ofono modem",
+                           nm_connection_get_connection_type(connection));
+        return FALSE;
+    }
+
+    if (!priv->imsi) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "modem has no IMSI");
+        return FALSE;
+    }
+
+    id = nm_connection_get_id(connection);
+
+    if (!strstr(id, "/context")) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "the connection ID has no context");
+        return FALSE;
+    }
+
+    if (!strstr(id, priv->imsi)) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "the connection ID does not contain the IMSI");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+handle_sim_property(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(user_data);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    if (g_strcmp0(property, "SubscriberIdentity") == 0 && VARIANT_IS_OF_TYPE_STRING(v)) {
+        gsize       length;
+        const char *value_str = g_variant_get_string(v, &length);
+
+        _LOGD("SubscriberIdentify found");
+
+        /* Check for empty DBus string value */
+        if (length && g_strcmp0(value_str, "(null)") != 0
+            && g_strcmp0(value_str, priv->imsi) != 0) {
+            if (priv->imsi != NULL) {
+                _LOGW("SimManager:'SubscriberIdentity' changed: %s", priv->imsi);
+                g_free(priv->imsi);
+            }
+
+            priv->imsi = g_strdup(value_str);
+            update_modem_state(self);
+        }
+    }
+}
+
+static void
+sim_property_changed(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
+{
+    GVariant *v_child = g_variant_get_child_value(v, 0);
+
+    handle_sim_property(proxy, property, v_child, user_data);
+    g_variant_unref(v_child);
+}
+
+static void
+sim_get_properties_done(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono *       self;
+    NMModemOfonoPrivate *priv;
+    gs_free_error GError *error             = NULL;
+    gs_unref_variant GVariant *v_properties = NULL;
+    gs_unref_variant GVariant *v_dict       = NULL;
+    GVariant *                 v;
+    GVariantIter               i;
+    const char *               property;
+
+    v_properties =
+        _nm_dbus_proxy_call_finish(G_DBUS_PROXY(source), result, G_VARIANT_TYPE("(a{sv})"), &error);
+    if (!v_properties && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_OFONO(user_data);
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    g_clear_object(&priv->sim_proxy_cancellable);
+
+    if (!v_properties) {
+        g_dbus_error_strip_remote_error(error);
+        _LOGW("error getting sim properties: %s", error->message);
+        return;
+    }
+
+    _LOGD("sim v_properties is type: %s", g_variant_get_type_string(v_properties));
+
+    v_dict = g_variant_get_child_value(v_properties, 0);
+    if (!v_dict) {
+        _LOGW("error getting sim properties: no v_dict");
+        return;
+    }
+
+    _LOGD("sim v_dict is type: %s", g_variant_get_type_string(v_dict));
+
+    /*
+     * TODO:
+     * 1) optimize by looking up properties ( Online, Interfaces ), instead
+     *    of iterating
+     *
+     * 2) reduce code duplication between all of the get_properties_done
+     *    functions in this class.
+     */
+
+    g_variant_iter_init(&i, v_dict);
+    while (g_variant_iter_next(&i, "{&sv}", &property, &v)) {
+        handle_sim_property(NULL, property, v, self);
+        g_variant_unref(v);
+    }
+}
+
+static void
+_sim_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono *       self;
+    NMModemOfonoPrivate *priv;
+    gs_free_error GError *error = NULL;
+    GDBusProxy *          proxy;
+
+    proxy = g_dbus_proxy_new_for_bus_finish(result, &error);
+    if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = user_data;
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    if (!proxy) {
+        _LOGW("failed to create SimManager proxy: %s", error->message);
+        g_clear_object(&priv->sim_proxy_cancellable);
+        return;
+    }
+
+    priv->sim_proxy = proxy;
+
+    /* Watch for custom ofono PropertyChanged signals */
+    _nm_dbus_signal_connect(priv->sim_proxy,
+                            "PropertyChanged",
+                            G_VARIANT_TYPE("(sv)"),
+                            G_CALLBACK(sim_property_changed),
+                            self);
+
+    g_dbus_proxy_call(priv->sim_proxy,
+                      "GetProperties",
+                      NULL,
+                      G_DBUS_CALL_FLAGS_NONE,
+                      20000,
+                      priv->sim_proxy_cancellable,
+                      sim_get_properties_done,
+                      self);
+}
+
+static void
+handle_sim_iface(NMModemOfono *self, gboolean found)
+{
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    _LOGD("SimManager interface %sfound", found ? "" : "not ");
+
+    if (!found && (priv->sim_proxy || priv->sim_proxy_cancellable)) {
+        _LOGI("SimManager interface disappeared");
+        nm_clear_g_cancellable(&priv->sim_proxy_cancellable);
+        if (priv->sim_proxy) {
+            g_signal_handlers_disconnect_by_data(priv->sim_proxy, self);
+            g_clear_object(&priv->sim_proxy);
+        }
+        nm_clear_g_free(&priv->imsi);
+        update_modem_state(self);
+    } else if (found && (!priv->sim_proxy && !priv->sim_proxy_cancellable)) {
+        _LOGI("found new SimManager interface");
+
+        priv->sim_proxy_cancellable = g_cancellable_new();
+
+        g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM,
+                                 G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES
+                                     | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+                                 NULL, /* GDBusInterfaceInfo */
+                                 OFONO_DBUS_SERVICE,
+                                 nm_modem_get_path(NM_MODEM(self)),
+                                 OFONO_DBUS_INTERFACE_SIM_MANAGER,
+                                 priv->sim_proxy_cancellable, /* GCancellable */
+                                 _sim_proxy_new_cb,
+                                 self);
+    }
+}
+
+static void
+handle_connman_property(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(user_data);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    if (g_strcmp0(property, "Attached") == 0 && VARIANT_IS_OF_TYPE_BOOLEAN(v)) {
+        gboolean attached     = g_variant_get_boolean(v);
+        gboolean old_attached = priv->gprs_attached;
+
+        _LOGD("Attached: %s", attached ? "True" : "False");
+
+        if (priv->gprs_attached != attached) {
+            priv->gprs_attached = attached;
+
+            _LOGI("Attached %s -> %s",
+                  old_attached ? "true" : "false",
+                  attached ? "true" : "false");
+
+            update_modem_state(self);
+        }
+    }
+}
+
+static void
+connman_property_changed(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
+{
+    GVariant *v_child = g_variant_get_child_value(v, 0);
+
+    handle_connman_property(proxy, property, v_child, user_data);
+    g_variant_unref(v_child);
+}
+
+static void
+connman_get_properties_done(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono *       self;
+    NMModemOfonoPrivate *priv;
+    gs_free_error GError *error             = NULL;
+    gs_unref_variant GVariant *v_properties = NULL;
+    gs_unref_variant GVariant *v_dict       = NULL;
+    GVariant *                 v;
+    GVariantIter               i;
+    const char *               property;
+
+    v_properties =
+        _nm_dbus_proxy_call_finish(G_DBUS_PROXY(source), result, G_VARIANT_TYPE("(a{sv})"), &error);
+    if (!v_properties && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_OFONO(user_data);
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    g_clear_object(&priv->connman_proxy_cancellable);
+
+    if (!v_properties) {
+        g_dbus_error_strip_remote_error(error);
+        _LOGW("error getting connman properties: %s", error->message);
+        return;
+    }
+
+    v_dict = g_variant_get_child_value(v_properties, 0);
+
+    /*
+     * TODO:
+     * 1) optimize by looking up properties ( Online, Interfaces ), instead
+     *    of iterating
+     *
+     * 2) reduce code duplication between all of the get_properties_done
+     *    functions in this class.
+     */
+
+    g_variant_iter_init(&i, v_dict);
+    while (g_variant_iter_next(&i, "{&sv}", &property, &v)) {
+        handle_connman_property(NULL, property, v, self);
+        g_variant_unref(v);
+    }
+}
+
+static void
+_connman_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono *       self;
+    NMModemOfonoPrivate *priv;
+    gs_free_error GError *error = NULL;
+    GDBusProxy *          proxy;
+
+    proxy = g_dbus_proxy_new_for_bus_finish(result, &error);
+    if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = user_data;
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    if (!proxy) {
+        _LOGW("failed to create ConnectionManager proxy: %s", error->message);
+        g_clear_object(&priv->connman_proxy_cancellable);
+        return;
+    }
+
+    priv->connman_proxy = proxy;
+
+    _nm_dbus_signal_connect(priv->connman_proxy,
+                            "PropertyChanged",
+                            G_VARIANT_TYPE("(sv)"),
+                            G_CALLBACK(connman_property_changed),
+                            self);
+
+    g_dbus_proxy_call(priv->connman_proxy,
+                      "GetProperties",
+                      NULL,
+                      G_DBUS_CALL_FLAGS_NONE,
+                      20000,
+                      priv->connman_proxy_cancellable,
+                      connman_get_properties_done,
+                      self);
+}
+
+static void
+handle_connman_iface(NMModemOfono *self, gboolean found)
+{
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    _LOGD("ConnectionManager interface %sfound", found ? "" : "not ");
+
+    if (!found && (priv->connman_proxy || priv->connman_proxy_cancellable)) {
+        _LOGI("ConnectionManager interface disappeared");
+        nm_clear_g_cancellable(&priv->connman_proxy_cancellable);
+        if (priv->connman_proxy) {
+            g_signal_handlers_disconnect_by_data(priv->connman_proxy, self);
+            g_clear_object(&priv->connman_proxy);
+        }
+
+        /* The connection manager proxy disappeared, we should
+         * consider the modem disabled.
+         */
+        priv->gprs_attached = FALSE;
+
+        update_modem_state(self);
+    } else if (found && (!priv->connman_proxy && !priv->connman_proxy_cancellable)) {
+        _LOGI("found new ConnectionManager interface");
+
+        priv->connman_proxy_cancellable = g_cancellable_new();
+
+        g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM,
+                                 G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES
+                                     | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+                                 NULL, /* GDBusInterfaceInfo */
+                                 OFONO_DBUS_SERVICE,
+                                 nm_modem_get_path(NM_MODEM(self)),
+                                 OFONO_DBUS_INTERFACE_CONNECTION_MANAGER,
+                                 priv->connman_proxy_cancellable,
+                                 _connman_proxy_new_cb,
+                                 self);
+    }
+}
+
+static void
+handle_modem_property(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(user_data);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    if ((g_strcmp0(property, "Online") == 0) && VARIANT_IS_OF_TYPE_BOOLEAN(v)) {
+        gboolean online = g_variant_get_boolean(v);
+
+        _LOGD("Online: %s", online ? "True" : "False");
+
+        if (online != priv->modem_online) {
+            priv->modem_online = online;
+            _LOGI("modem is now %s", online ? "Online" : "Offline");
+            update_modem_state(self);
+        }
+
+    } else if ((g_strcmp0(property, "Interfaces") == 0) && VARIANT_IS_OF_TYPE_STRING_ARRAY(v)) {
+        const char **array, **iter;
+        gboolean     found_connman = FALSE;
+        gboolean     found_sim     = FALSE;
+
+        _LOGD("Interfaces found");
+
+        array = g_variant_get_strv(v, NULL);
+        if (array) {
+            for (iter = array; *iter; iter++) {
+                if (g_strcmp0(OFONO_DBUS_INTERFACE_SIM_MANAGER, *iter) == 0)
+                    found_sim = TRUE;
+                else if (g_strcmp0(OFONO_DBUS_INTERFACE_CONNECTION_MANAGER, *iter) == 0)
+                    found_connman = TRUE;
+            }
+            g_free(array);
+        }
+
+        handle_sim_iface(self, found_sim);
+        handle_connman_iface(self, found_connman);
+    }
+}
+
+static void
+modem_property_changed(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
+{
+    GVariant *v_child = g_variant_get_child_value(v, 0);
+
+    handle_modem_property(proxy, property, v_child, user_data);
+    g_variant_unref(v_child);
+}
+
+static void
+modem_get_properties_done(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono *       self;
+    NMModemOfonoPrivate *priv;
+    gs_free_error GError *error             = NULL;
+    gs_unref_variant GVariant *v_properties = NULL;
+    gs_unref_variant GVariant *v_dict       = NULL;
+    GVariant *                 v;
+    GVariantIter               i;
+    const char *               property;
+
+    v_properties =
+        _nm_dbus_proxy_call_finish(G_DBUS_PROXY(source), result, G_VARIANT_TYPE("(a{sv})"), &error);
+    if (!v_properties && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_OFONO(user_data);
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    g_clear_object(&priv->modem_proxy_cancellable);
+
+    if (!v_properties) {
+        g_dbus_error_strip_remote_error(error);
+        _LOGW("error getting modem properties: %s", error->message);
+        return;
+    }
+
+    v_dict = g_variant_get_child_value(v_properties, 0);
+    if (!v_dict) {
+        _LOGW("error getting modem properties: no v_dict");
+        return;
+    }
+
+    /*
+     * TODO:
+     * 1) optimize by looking up properties ( Online, Interfaces ), instead
+     *    of iterating
+     *
+     * 2) reduce code duplication between all of the get_properties_done
+     *    functions in this class.
+     */
+
+    g_variant_iter_init(&i, v_dict);
+    while (g_variant_iter_next(&i, "{&sv}", &property, &v)) {
+        handle_modem_property(NULL, property, v, self);
+        g_variant_unref(v);
+    }
+}
+
+static void
+stage1_prepare_done(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono *       self;
+    NMModemOfonoPrivate *priv;
+    gs_free_error GError *error  = NULL;
+    gs_unref_variant GVariant *v = NULL;
+
+    v = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), result, &error);
+    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_OFONO(user_data);
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    g_clear_object(&priv->context_proxy_cancellable);
+
+    nm_clear_pointer(&priv->connect_properties, g_hash_table_destroy);
+
+    if (error) {
+        _LOGW("connection failed: %s", error->message);
+
+        nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_MODEM_BUSY);
+        /*
+         * FIXME: add code to check for InProgress so that the
+         * connection doesn't continue to try and activate,
+         * leading to the connection being disabled, and a 5m
+         * timeout...
+         */
+    }
+}
+
+static void
+context_property_changed(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(user_data);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    NMPlatformIP4Address addr;
+    gboolean             ret          = FALSE;
+    gs_unref_variant GVariant *v_dict = NULL;
+    const char *               interface;
+    const char *               s;
+    const char **              array, **iter;
+    guint32                    address_network, gateway_network;
+    guint32                    ip4_route_table, ip4_route_metric;
+    int                        ifindex;
+    GError *                   error = NULL;
+
+    _LOGD("PropertyChanged: %s", property);
+
+    /*
+     * TODO: might be a good idea and re-factor this to mimic bluez-device,
+     * ie. have this function just check the key, and call a sub-func to
+     * handle the action.
+     */
+
+    if (g_strcmp0(property, "Settings") != 0)
+        return;
+
+    v_dict = g_variant_get_child_value(v, 0);
+    if (!v_dict) {
+        _LOGW("error getting IPv4 Settings: no v_dict");
+        goto out;
+    }
+
+    _LOGI("IPv4 static Settings:");
+
+    if (!g_variant_lookup(v_dict, "Interface", "&s", &interface)) {
+        _LOGW("Settings 'Interface' missing");
+        goto out;
+    }
+
+    _LOGD("Interface: %s", interface);
+    if (!nm_modem_set_data_port(NM_MODEM(self),
+                                NM_PLATFORM_GET,
+                                interface,
+                                NM_MODEM_IP_METHOD_STATIC,
+                                NM_MODEM_IP_METHOD_UNKNOWN,
+                                0,
+                                &error)) {
+        _LOGW("failed to connect to modem: %s", error->message);
+        g_clear_error(&error);
+        goto out;
+    }
+
+    ifindex = nm_modem_get_ip_ifindex(NM_MODEM(self));
+    nm_assert(ifindex > 0);
+
+    /* TODO: verify handling of ip4_config; check other places it's used... */
+    g_clear_object(&priv->ip4_config);
+
+    priv->ip4_config = nm_ip4_config_new(nm_platform_get_multi_idx(NM_PLATFORM_GET), ifindex);
+
+    if (!g_variant_lookup(v_dict, "Address", "&s", &s)) {
+        _LOGW("Settings 'Address' missing");
+        goto out;
+    }
+    if (!s || !nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &address_network)) {
+        _LOGW("can't convert 'Address' %s to addr", s ?: "");
+        goto out;
+    }
+    memset(&addr, 0, sizeof(addr));
+    addr.ifindex     = ifindex;
+    addr.address     = address_network;
+    addr.addr_source = NM_IP_CONFIG_SOURCE_WWAN;
+
+    if (!g_variant_lookup(v_dict, "Netmask", "&s", &s)) {
+        _LOGW("Settings 'Netmask' missing");
+        goto out;
+    }
+    if (!s || !nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &address_network)) {
+        _LOGW("invalid 'Netmask': %s", s ?: "");
+        goto out;
+    }
+    addr.plen = nm_utils_ip4_netmask_to_prefix(address_network);
+
+    _LOGI("Address: %s", nm_platform_ip4_address_to_string(&addr, NULL, 0));
+    nm_ip4_config_add_address(priv->ip4_config, &addr);
+
+    if (!g_variant_lookup(v_dict, "Gateway", "&s", &s) || !s) {
+        _LOGW("Settings 'Gateway' missing");
+        goto out;
+    }
+    if (!nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &gateway_network)) {
+        _LOGW("invalid 'Gateway': %s", s);
+        goto out;
+    }
+    nm_modem_get_route_parameters(NM_MODEM(self), &ip4_route_table, &ip4_route_metric, NULL, NULL);
+    {
+        const NMPlatformIP4Route r = {
+            .rt_source     = NM_IP_CONFIG_SOURCE_WWAN,
+            .gateway       = gateway_network,
+            .table_coerced = nm_platform_route_table_coerce(ip4_route_table),
+            .metric        = ip4_route_metric,
+        };
+
+        _LOGI("Gateway: %s", s);
+        nm_ip4_config_add_route(priv->ip4_config, &r, NULL);
+    }
+
+    if (!g_variant_lookup(v_dict, "DomainNameServers", "^a&s", &array)) {
+        _LOGW("Settings 'DomainNameServers' missing");
+        goto out;
+    }
+    if (array) {
+        for (iter = array; *iter; iter++) {
+            if (nm_utils_parse_inaddr_bin(AF_INET, *iter, NULL, &address_network)
+                && address_network) {
+                _LOGI("DNS: %s", *iter);
+                nm_ip4_config_add_nameserver(priv->ip4_config, address_network);
+            } else {
+                _LOGW("invalid NameServer: %s", *iter);
+            }
+        }
+
+        if (iter == array) {
+            _LOGW("Settings: 'DomainNameServers': none specified");
+            g_free(array);
+            goto out;
+        }
+        g_free(array);
+    }
+
+    if (g_variant_lookup(v_dict, "MessageProxy", "&s", &s)) {
+        _LOGI("MessageProxy: %s", s);
+        if (s && nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &address_network)) {
+            nm_modem_get_route_parameters(NM_MODEM(self),
+                                          &ip4_route_table,
+                                          &ip4_route_metric,
+                                          NULL,
+                                          NULL);
+
+            {
+                const NMPlatformIP4Route mms_route = {
+                    .network       = address_network,
+                    .plen          = 32,
+                    .gateway       = gateway_network,
+                    .table_coerced = nm_platform_route_table_coerce(ip4_route_table),
+                    .metric        = ip4_route_metric,
+                };
+
+                nm_ip4_config_add_route(priv->ip4_config, &mms_route, NULL);
+            }
+        } else {
+            _LOGW("invalid MessageProxy: %s", s);
+        }
+    }
+
+    ret = TRUE;
+
+out:
+    if (nm_modem_get_state(NM_MODEM(self)) != NM_MODEM_STATE_CONNECTED) {
+        _LOGI("emitting PREPARE_RESULT: %s", ret ? "TRUE" : "FALSE");
+        nm_modem_emit_prepare_result(NM_MODEM(self),
+                                     ret,
+                                     ret ? NM_DEVICE_STATE_REASON_NONE
+                                         : NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+    } else {
+        _LOGW("MODEM_PPP_FAILED");
+        nm_modem_emit_ppp_failed(NM_MODEM(self), NM_DEVICE_STATE_REASON_PPP_FAILED);
+    }
+}
+
+static NMActStageReturn
+static_stage3_ip4_config_start(NMModem *            modem,
+                               NMActRequest *       req,
+                               NMDeviceStateReason *out_failure_reason)
+{
+    NMModemOfono *       self  = NM_MODEM_OFONO(modem);
+    NMModemOfonoPrivate *priv  = NM_MODEM_OFONO_GET_PRIVATE(self);
+    GError *             error = NULL;
+
+    if (!priv->ip4_config) {
+        _LOGD("IP4 config not ready(?)");
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    _LOGD("IP4 config is done; setting modem_state -> CONNECTED");
+    g_signal_emit_by_name(self, NM_MODEM_IP4_CONFIG_RESULT, priv->ip4_config, error);
+
+    /* Signal listener takes ownership of the IP4Config */
+    priv->ip4_config = NULL;
+
+    nm_modem_set_state(NM_MODEM(self),
+                       NM_MODEM_STATE_CONNECTED,
+                       nm_modem_state_to_string(NM_MODEM_STATE_CONNECTED));
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+static void
+context_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono *       self;
+    NMModemOfonoPrivate *priv;
+    gs_free_error GError *error = NULL;
+    GDBusProxy *          proxy;
+
+    proxy = g_dbus_proxy_new_for_bus_finish(result, &error);
+    if (!proxy || g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_OFONO(user_data);
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    if (!proxy) {
+        _LOGE("failed to create ofono ConnectionContext DBus proxy: %s", error->message);
+        g_clear_object(&priv->context_proxy_cancellable);
+        nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_MODEM_BUSY);
+        return;
+    }
+
+    priv->context_proxy = proxy;
+
+    if (!priv->gprs_attached) {
+        g_clear_object(&priv->context_proxy_cancellable);
+        nm_modem_emit_prepare_result(NM_MODEM(self),
+                                     FALSE,
+                                     NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER);
+        return;
+    }
+
+    /* We have an old copy of the settings from a previous activation,
+     * clear it so that we can gate getting the IP config from oFono
+     * on whether or not we have already received them
+     */
+    g_clear_object(&priv->ip4_config);
+
+    _nm_dbus_signal_connect(priv->context_proxy,
+                            "PropertyChanged",
+                            G_VARIANT_TYPE("(sv)"),
+                            G_CALLBACK(context_property_changed),
+                            self);
+
+    g_dbus_proxy_call(priv->context_proxy,
+                      "SetProperty",
+                      g_variant_new("(sv)", "Active", g_variant_new("b", TRUE)),
+                      G_DBUS_CALL_FLAGS_NONE,
+                      20000,
+                      priv->context_proxy_cancellable,
+                      stage1_prepare_done,
+                      self);
+}
+
+static void
+do_context_activate(NMModemOfono *self)
+{
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    g_return_if_fail(NM_IS_MODEM_OFONO(self));
+
+    nm_clear_g_cancellable(&priv->context_proxy_cancellable);
+    g_clear_object(&priv->context_proxy);
+
+    priv->context_proxy_cancellable = g_cancellable_new();
+
+    g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM,
+                             G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+                             NULL,
+                             OFONO_DBUS_SERVICE,
+                             priv->context_path,
+                             OFONO_DBUS_INTERFACE_CONNECTION_CONTEXT,
+                             priv->context_proxy_cancellable,
+                             context_proxy_new_cb,
+                             self);
+}
+
+static GHashTable *
+create_connect_properties(NMConnection *connection)
+{
+    NMSettingGsm *setting;
+    GHashTable *  properties;
+    const char *  str;
+
+    setting    = nm_connection_get_setting_gsm(connection);
+    properties = g_hash_table_new(nm_str_hash, g_str_equal);
+
+    str = nm_setting_gsm_get_apn(setting);
+    if (str)
+        g_hash_table_insert(properties, "AccessPointName", g_strdup(str));
+
+    str = nm_setting_gsm_get_username(setting);
+    if (str)
+        g_hash_table_insert(properties, "Username", g_strdup(str));
+
+    str = nm_setting_gsm_get_password(setting);
+    if (str)
+        g_hash_table_insert(properties, "Password", g_strdup(str));
+
+    return properties;
+}
+
+static NMActStageReturn
+modem_act_stage1_prepare(NMModem *            modem,
+                         NMConnection *       connection,
+                         NMDeviceStateReason *out_failure_reason)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(modem);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    const char *         context_id;
+    char **              id = NULL;
+
+    context_id = nm_connection_get_id(connection);
+    id         = g_strsplit(context_id, "/", 0);
+    g_return_val_if_fail(id[2], NM_ACT_STAGE_RETURN_FAILURE);
+
+    _LOGD("trying %s %s", id[1], id[2]);
+
+    g_free(priv->context_path);
+    priv->context_path = g_strdup_printf("%s/%s", nm_modem_get_path(modem), id[2]);
+    g_strfreev(id);
+
+    if (!priv->context_path) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_GSM_APN_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    if (priv->connect_properties)
+        g_hash_table_destroy(priv->connect_properties);
+
+    priv->connect_properties = create_connect_properties(connection);
+
+    _LOGI("activating context %s", priv->context_path);
+
+    if (nm_modem_get_state(modem) == NM_MODEM_STATE_REGISTERED) {
+        do_context_activate(self);
+    } else {
+        _LOGW("could not activate context: modem is not registered.");
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+static void
+modem_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono *       self;
+    NMModemOfonoPrivate *priv;
+    gs_free_error GError *error = NULL;
+    GDBusProxy *          proxy;
+
+    proxy = g_dbus_proxy_new_for_bus_finish(result, &error);
+    if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_OFONO(user_data);
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    if (!proxy) {
+        _LOGE("failed to create ofono modem DBus proxy: %s", error->message);
+        g_clear_object(&priv->modem_proxy_cancellable);
+        return;
+    }
+
+    priv->modem_proxy = proxy;
+
+    _nm_dbus_signal_connect(priv->modem_proxy,
+                            "PropertyChanged",
+                            G_VARIANT_TYPE("(sv)"),
+                            G_CALLBACK(modem_property_changed),
+                            self);
+
+    g_dbus_proxy_call(priv->modem_proxy,
+                      "GetProperties",
+                      NULL,
+                      G_DBUS_CALL_FLAGS_NONE,
+                      20000,
+                      priv->modem_proxy_cancellable,
+                      modem_get_properties_done,
+                      self);
+}
+
+/*****************************************************************************/
+
+static void
+nm_modem_ofono_init(NMModemOfono *self)
+{}
+
+static void
+constructed(GObject *object)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(object);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    priv->modem_proxy_cancellable = g_cancellable_new();
+
+    g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM,
+                             G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+                             NULL,
+                             OFONO_DBUS_SERVICE,
+                             nm_modem_get_path(NM_MODEM(self)),
+                             OFONO_DBUS_INTERFACE_MODEM,
+                             priv->modem_proxy_cancellable,
+                             modem_proxy_new_cb,
+                             self);
+
+    G_OBJECT_CLASS(nm_modem_ofono_parent_class)->constructed(object);
+}
+
+NMModem *
+nm_modem_ofono_new(const char *path)
+{
+    gs_free char *basename = NULL;
+
+    g_return_val_if_fail(path != NULL, NULL);
+
+    nm_log_info(LOGD_MB, "ofono: creating new Ofono modem path %s", path);
+
+    /* Use short modem name (not its object path) as the NM device name (which
+     * comes from NM_MODEM_UID)and the device ID.
+     */
+    basename = g_path_get_basename(path);
+
+    return g_object_new(NM_TYPE_MODEM_OFONO,
+                        NM_MODEM_PATH,
+                        path,
+                        NM_MODEM_UID,
+                        basename,
+                        NM_MODEM_DEVICE_ID,
+                        basename,
+                        NM_MODEM_CONTROL_PORT,
+                        "ofono", /* mandatory */
+                        NM_MODEM_DRIVER,
+                        "ofono",
+                        NM_MODEM_STATE,
+                        (int) NM_MODEM_STATE_INITIALIZING,
+                        NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(object);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    nm_clear_g_cancellable(&priv->modem_proxy_cancellable);
+    nm_clear_g_cancellable(&priv->connman_proxy_cancellable);
+    nm_clear_g_cancellable(&priv->context_proxy_cancellable);
+    nm_clear_g_cancellable(&priv->sim_proxy_cancellable);
+
+    if (priv->connect_properties) {
+        g_hash_table_destroy(priv->connect_properties);
+        priv->connect_properties = NULL;
+    }
+
+    g_clear_object(&priv->ip4_config);
+
+    if (priv->modem_proxy) {
+        g_signal_handlers_disconnect_by_data(priv->modem_proxy, self);
+        g_clear_object(&priv->modem_proxy);
+    }
+
+    if (priv->connman_proxy) {
+        g_signal_handlers_disconnect_by_data(priv->connman_proxy, self);
+        g_clear_object(&priv->connman_proxy);
+    }
+
+    if (priv->context_proxy) {
+        g_signal_handlers_disconnect_by_data(priv->context_proxy, self);
+        g_clear_object(&priv->context_proxy);
+    }
+
+    if (priv->sim_proxy) {
+        g_signal_handlers_disconnect_by_data(priv->sim_proxy, self);
+        g_clear_object(&priv->sim_proxy);
+    }
+
+    g_free(priv->imsi);
+    priv->imsi = NULL;
+
+    G_OBJECT_CLASS(nm_modem_ofono_parent_class)->dispose(object);
+}
+
+static void
+nm_modem_ofono_class_init(NMModemOfonoClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+    NMModemClass *modem_class  = NM_MODEM_CLASS(klass);
+
+    object_class->constructed = constructed;
+    object_class->dispose     = dispose;
+
+    modem_class->get_capabilities                       = get_capabilities;
+    modem_class->disconnect                             = disconnect;
+    modem_class->deactivate_cleanup                     = deactivate_cleanup;
+    modem_class->check_connection_compatible_with_modem = check_connection_compatible_with_modem;
+
+    modem_class->modem_act_stage1_prepare       = modem_act_stage1_prepare;
+    modem_class->static_stage3_ip4_config_start = static_stage3_ip4_config_start;
+}
diff --git a/src/core/devices/wwan/nm-modem-ofono.h b/src/core/devices/wwan/nm-modem-ofono.h
new file mode 100644
index 00000000..260e3954
--- /dev/null
+++ b/src/core/devices/wwan/nm-modem-ofono.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 - Canonical Ltd.
+ */
+
+#ifndef NM_MODEM_OFONO_H
+#define NM_MODEM_OFONO_H
+
+#include "nm-modem.h"
+
+#define NM_TYPE_MODEM_OFONO    (nm_modem_ofono_get_type())
+#define NM_MODEM_OFONO(obj)    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM_OFONO, NMModemOfono))
+#define NM_IS_MODEM_OFONO(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MODEM_OFONO))
+#define NM_MODEM_OFONO_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MODEM_OFONO, NMModemOfonoClass))
+#define NM_IS_MODEM_OFONO_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_MODEM_OFONO))
+#define NM_MODEM_OFONO_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_MODEM_OFONO, NMModemOfonoClass))
+
+#define OFONO_DBUS_SERVICE                      "org.ofono"
+#define OFONO_DBUS_PATH                         "/"
+#define OFONO_DBUS_INTERFACE                    "org.ofono.Manager"
+#define OFONO_DBUS_INTERFACE_MODEM              "org.ofono.Modem"
+#define OFONO_DBUS_INTERFACE_CONNECTION_MANAGER "org.ofono.ConnectionManager"
+#define OFONO_DBUS_INTERFACE_CONNECTION_CONTEXT "org.ofono.ConnectionContext"
+#define OFONO_DBUS_INTERFACE_SIM_MANAGER        "org.ofono.SimManager"
+
+typedef struct _NMModemOfono      NMModemOfono;
+typedef struct _NMModemOfonoClass NMModemOfonoClass;
+
+GType nm_modem_ofono_get_type(void);
+
+NMModem *nm_modem_ofono_new(const char *path);
+
+#endif /* NM_MODEM_OFONO_H */
diff --git a/src/core/devices/wwan/nm-modem.c b/src/core/devices/wwan/nm-modem.c
new file mode 100644
index 00000000..0d334fa4
--- /dev/null
+++ b/src/core/devices/wwan/nm-modem.c
@@ -0,0 +1,2037 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2009 - 2014 Red Hat, Inc.
+ * Copyright (C) 2009 Novell, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-modem.h"
+
+#include <fcntl.h>
+#include <termios.h>
+#include <linux/rtnetlink.h>
+
+#include "nm-core-internal.h"
+#include "platform/nm-platform.h"
+#include "nm-setting-connection.h"
+#include "NetworkManagerUtils.h"
+#include "devices/nm-device-private.h"
+#include "nm-netns.h"
+#include "nm-act-request.h"
+#include "nm-ip4-config.h"
+#include "nm-ip6-config.h"
+#include "ppp/nm-ppp-manager-call.h"
+#include "ppp/nm-ppp-status.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMModem,
+                             PROP_CONTROL_PORT,
+                             PROP_IP_IFINDEX,
+                             PROP_PATH,
+                             PROP_UID,
+                             PROP_DRIVER,
+                             PROP_STATE,
+                             PROP_DEVICE_ID,
+                             PROP_SIM_ID,
+                             PROP_IP_TYPES,
+                             PROP_SIM_OPERATOR_ID,
+                             PROP_OPERATOR_CODE,
+                             PROP_APN, );
+
+enum {
+    PPP_STATS,
+    PPP_FAILED,
+    PREPARE_RESULT,
+    IP4_CONFIG_RESULT,
+    IP6_CONFIG_RESULT,
+    AUTH_REQUESTED,
+    AUTH_RESULT,
+    REMOVED,
+    STATE_CHANGED,
+    LAST_SIGNAL,
+};
+
+static guint signals[LAST_SIGNAL] = {0};
+
+typedef struct _NMModemPrivate {
+    char *uid;
+    char *path;
+    char *driver;
+    char *control_port;
+    char *data_port;
+
+    /* TODO: ip_iface is solely used for nm_modem_owns_port().
+     * We should rework the code that it's not necessary */
+    char *ip_iface;
+
+    int                ip_ifindex;
+    NMModemIPMethod    ip4_method;
+    NMModemIPMethod    ip6_method;
+    NMUtilsIPv6IfaceId iid;
+    NMModemState       state;
+    NMModemState       prev_state; /* revert to this state if enable/disable fails */
+    char *             device_id;
+    char *             sim_id;
+    NMModemIPType      ip_types;
+    char *             sim_operator_id;
+    char *             operator_code;
+    char *             apn;
+
+    NMPPPManager *ppp_manager;
+
+    NMActRequest *                act_request;
+    guint32                       secrets_tries;
+    NMActRequestGetSecretsCallId *secrets_id;
+
+    guint mm_ip_timeout;
+
+    guint32 ip4_route_table;
+    guint32 ip4_route_metric;
+    guint32 ip6_route_table;
+    guint32 ip6_route_metric;
+
+    /* PPP stats */
+    guint32 in_bytes;
+    guint32 out_bytes;
+
+    bool claimed : 1;
+} NMModemPrivate;
+
+G_DEFINE_TYPE(NMModem, nm_modem, G_TYPE_OBJECT)
+
+#define NM_MODEM_GET_PRIVATE(self) _NM_GET_PRIVATE_PTR(self, NMModem, NM_IS_MODEM)
+
+/*****************************************************************************/
+
+#define _NMLOG_PREFIX_BUFLEN 64
+#define _NMLOG_PREFIX_NAME   "modem"
+#define _NMLOG_DOMAIN        LOGD_MB
+
+static const char *
+_nmlog_prefix(char *prefix, NMModem *self)
+{
+    const char *uuid;
+    int         c;
+
+    if (!self)
+        return "";
+
+    uuid = nm_modem_get_uid(self);
+
+    if (uuid) {
+        char pp[_NMLOG_PREFIX_BUFLEN - 5];
+
+        c = g_snprintf(prefix, _NMLOG_PREFIX_BUFLEN, "[%s]", nm_strquote(pp, sizeof(pp), uuid));
+    } else
+        c = g_snprintf(prefix, _NMLOG_PREFIX_BUFLEN, "[%p]", self);
+    nm_assert(c < _NMLOG_PREFIX_BUFLEN);
+
+    return prefix;
+}
+
+#define _NMLOG(level, ...)                                                        \
+    G_STMT_START                                                                  \
+    {                                                                             \
+        char _prefix[_NMLOG_PREFIX_BUFLEN];                                       \
+                                                                                  \
+        nm_log((level),                                                           \
+               _NMLOG_DOMAIN,                                                     \
+               NULL,                                                              \
+               NULL,                                                              \
+               "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                       \
+               _NMLOG_PREFIX_NAME,                                                \
+               _nmlog_prefix(_prefix, (self)) _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+    }                                                                             \
+    G_STMT_END
+
+/*****************************************************************************/
+
+static void _set_ip_ifindex(NMModem *self, int ifindex, const char *ifname);
+
+/*****************************************************************************/
+/* State/enabled/connected */
+
+static const char *state_table[] = {
+    [NM_MODEM_STATE_UNKNOWN]       = "unknown",
+    [NM_MODEM_STATE_FAILED]        = "failed",
+    [NM_MODEM_STATE_INITIALIZING]  = "initializing",
+    [NM_MODEM_STATE_LOCKED]        = "locked",
+    [NM_MODEM_STATE_DISABLED]      = "disabled",
+    [NM_MODEM_STATE_DISABLING]     = "disabling",
+    [NM_MODEM_STATE_ENABLING]      = "enabling",
+    [NM_MODEM_STATE_ENABLED]       = "enabled",
+    [NM_MODEM_STATE_SEARCHING]     = "searching",
+    [NM_MODEM_STATE_REGISTERED]    = "registered",
+    [NM_MODEM_STATE_DISCONNECTING] = "disconnecting",
+    [NM_MODEM_STATE_CONNECTING]    = "connecting",
+    [NM_MODEM_STATE_CONNECTED]     = "connected",
+};
+
+const char *
+nm_modem_state_to_string(NMModemState state)
+{
+    if ((gsize) state < G_N_ELEMENTS(state_table))
+        return state_table[state];
+    return NULL;
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_modem_is_claimed(NMModem *self)
+{
+    g_return_val_if_fail(NM_IS_MODEM(self), FALSE);
+
+    return NM_MODEM_GET_PRIVATE(self)->claimed;
+}
+
+NMModem *
+nm_modem_claim(NMModem *self)
+{
+    NMModemPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_MODEM(self), NULL);
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+
+    g_return_val_if_fail(!priv->claimed, NULL);
+
+    priv->claimed = TRUE;
+    return g_object_ref(self);
+}
+
+void
+nm_modem_unclaim(NMModem *self)
+{
+    NMModemPrivate *priv;
+
+    g_return_if_fail(NM_IS_MODEM(self));
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->claimed);
+
+    /* we don't actually unclaim the instance. This instance should not be re-used
+     * by another owner, that is because we only claim modems as we receive them.
+     * There is no mechanism that somebody else would later re-use them again.
+     *
+     * // priv->claimed = FALSE; */
+
+    g_object_unref(self);
+}
+
+/*****************************************************************************/
+
+NMModemState
+nm_modem_get_state(NMModem *self)
+{
+    return NM_MODEM_GET_PRIVATE(self)->state;
+}
+
+void
+nm_modem_set_state(NMModem *self, NMModemState new_state, const char *reason)
+{
+    NMModemPrivate *priv      = NM_MODEM_GET_PRIVATE(self);
+    NMModemState    old_state = priv->state;
+
+    priv->prev_state = NM_MODEM_STATE_UNKNOWN;
+
+    if (new_state != old_state) {
+        _LOGI("modem state changed, '%s' --> '%s' (reason: %s)",
+              nm_modem_state_to_string(old_state),
+              nm_modem_state_to_string(new_state),
+              reason ?: "none");
+
+        priv->state = new_state;
+        _notify(self, PROP_STATE);
+        g_signal_emit(self, signals[STATE_CHANGED], 0, (int) new_state, (int) old_state);
+    }
+}
+
+void
+nm_modem_set_prev_state(NMModem *self, const char *reason)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    /* Reset modem to previous state if the state hasn't already changed */
+    if (priv->prev_state != NM_MODEM_STATE_UNKNOWN)
+        nm_modem_set_state(self, priv->prev_state, reason);
+}
+
+void
+nm_modem_set_mm_enabled(NMModem *self, gboolean enabled)
+{
+    NMModemPrivate *priv       = NM_MODEM_GET_PRIVATE(self);
+    NMModemState    prev_state = priv->state;
+
+    if (enabled && priv->state >= NM_MODEM_STATE_ENABLING) {
+        _LOGD("cannot enable modem: already enabled");
+        return;
+    }
+    if (!enabled && priv->state <= NM_MODEM_STATE_DISABLING) {
+        _LOGD("cannot disable modem: already disabled");
+        return;
+    }
+
+    if (priv->state <= NM_MODEM_STATE_INITIALIZING) {
+        _LOGD("cannot enable/disable modem: initializing or failed");
+        return;
+    } else if (priv->state == NM_MODEM_STATE_LOCKED) {
+        /* Don't try to enable if the modem is locked since that will fail */
+        _LOGW("cannot enable/disable modem: locked");
+
+        /* Try to unlock the modem if it's being enabled */
+        if (enabled)
+            g_signal_emit(self, signals[AUTH_REQUESTED], 0);
+        return;
+    }
+
+    /* Not all modem classes support set_mm_enabled */
+    if (NM_MODEM_GET_CLASS(self)->set_mm_enabled)
+        NM_MODEM_GET_CLASS(self)->set_mm_enabled(self, enabled);
+
+    /* Pre-empt the state change signal */
+    nm_modem_set_state(self,
+                       enabled ? NM_MODEM_STATE_ENABLING : NM_MODEM_STATE_DISABLING,
+                       "user preference");
+    priv->prev_state = prev_state;
+}
+
+void
+nm_modem_emit_removed(NMModem *self)
+{
+    g_signal_emit(self, signals[REMOVED], 0);
+}
+
+void
+nm_modem_emit_prepare_result(NMModem *self, gboolean success, NMDeviceStateReason reason)
+{
+    nm_assert(NM_IS_MODEM(self));
+
+    g_signal_emit(self, signals[PREPARE_RESULT], 0, success, (guint) reason);
+}
+
+void
+nm_modem_emit_ppp_failed(NMModem *self, NMDeviceStateReason reason)
+{
+    nm_assert(NM_IS_MODEM(self));
+
+    g_signal_emit(self, signals[PPP_FAILED], 0, (guint) reason);
+}
+
+NMModemIPType
+nm_modem_get_supported_ip_types(NMModem *self)
+{
+    return NM_MODEM_GET_PRIVATE(self)->ip_types;
+}
+
+const char *
+nm_modem_ip_type_to_string(NMModemIPType ip_type)
+{
+    switch (ip_type) {
+    case NM_MODEM_IP_TYPE_IPV4:
+        return "ipv4";
+    case NM_MODEM_IP_TYPE_IPV6:
+        return "ipv6";
+    case NM_MODEM_IP_TYPE_IPV4V6:
+        return "ipv4v6";
+    default:
+        g_return_val_if_reached("unknown");
+    }
+}
+
+static GArray *
+build_single_ip_type_array(NMModemIPType type)
+{
+    return g_array_append_val(g_array_sized_new(FALSE, FALSE, sizeof(NMModemIPType), 1), type);
+}
+
+/**
+ * nm_modem_get_connection_ip_type:
+ * @self: the #NMModem
+ * @connection: the #NMConnection to determine IP type to use
+ *
+ * Given a modem and a connection, determine which #NMModemIPTypes to use
+ * when connecting.
+ *
+ * Returns: an array of #NMModemIpType values, in the order in which they
+ * should be tried.
+ */
+GArray *
+nm_modem_get_connection_ip_type(NMModem *self, NMConnection *connection, GError **error)
+{
+    NMModemPrivate *   priv = NM_MODEM_GET_PRIVATE(self);
+    NMSettingIPConfig *s_ip4, *s_ip6;
+    const char *       method;
+    gboolean           ip4 = TRUE, ip6 = TRUE;
+    gboolean           ip4_may_fail = TRUE, ip6_may_fail = TRUE;
+
+    s_ip4 = nm_connection_get_setting_ip4_config(connection);
+    if (s_ip4) {
+        method = nm_setting_ip_config_get_method(s_ip4);
+        if (g_strcmp0(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED) == 0)
+            ip4 = FALSE;
+        ip4_may_fail = nm_setting_ip_config_get_may_fail(s_ip4);
+    }
+
+    s_ip6 = nm_connection_get_setting_ip6_config(connection);
+    if (s_ip6) {
+        method = nm_setting_ip_config_get_method(s_ip6);
+        if (NM_IN_STRSET(method,
+                         NM_SETTING_IP6_CONFIG_METHOD_IGNORE,
+                         NM_SETTING_IP6_CONFIG_METHOD_DISABLED))
+            ip6 = FALSE;
+        ip6_may_fail = nm_setting_ip_config_get_may_fail(s_ip6);
+    }
+
+    if (ip4 && !ip6) {
+        if (!(priv->ip_types & NM_MODEM_IP_TYPE_IPV4)) {
+            g_set_error_literal(error,
+                                NM_DEVICE_ERROR,
+                                NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
+                                "Connection requested IPv4 but IPv4 is "
+                                "unsupported by the modem.");
+            return NULL;
+        }
+        return build_single_ip_type_array(NM_MODEM_IP_TYPE_IPV4);
+    }
+
+    if (ip6 && !ip4) {
+        if (!(priv->ip_types & NM_MODEM_IP_TYPE_IPV6)) {
+            g_set_error_literal(error,
+                                NM_DEVICE_ERROR,
+                                NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
+                                "Connection requested IPv6 but IPv6 is "
+                                "unsupported by the modem.");
+            return NULL;
+        }
+        return build_single_ip_type_array(NM_MODEM_IP_TYPE_IPV6);
+    }
+
+    if (ip4 && ip6) {
+        NMModemIPType type;
+        GArray *      out;
+
+        out = g_array_sized_new(FALSE, FALSE, sizeof(NMModemIPType), 3);
+
+        /* Modem supports dual-stack? */
+        if (priv->ip_types & NM_MODEM_IP_TYPE_IPV4V6) {
+            type = NM_MODEM_IP_TYPE_IPV4V6;
+            g_array_append_val(out, type);
+        }
+
+        /* If IPv6 may-fail=false, we should NOT try IPv4 as fallback */
+        if ((priv->ip_types & NM_MODEM_IP_TYPE_IPV4) && ip6_may_fail) {
+            type = NM_MODEM_IP_TYPE_IPV4;
+            g_array_append_val(out, type);
+        }
+
+        /* If IPv4 may-fail=false, we should NOT try IPv6 as fallback */
+        if ((priv->ip_types & NM_MODEM_IP_TYPE_IPV6) && ip4_may_fail) {
+            type = NM_MODEM_IP_TYPE_IPV6;
+            g_array_append_val(out, type);
+        }
+
+        if (out->len > 0)
+            return out;
+
+        /* Error... */
+        g_array_unref(out);
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
+                            "Connection requested both IPv4 and IPv6 "
+                            "but dual-stack addressing is unsupported "
+                            "by the modem.");
+        return NULL;
+    }
+
+    g_set_error_literal(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
+                        "Connection specified no IP configuration!");
+    return NULL;
+}
+
+const char *
+nm_modem_get_device_id(NMModem *self)
+{
+    return NM_MODEM_GET_PRIVATE(self)->device_id;
+}
+
+const char *
+nm_modem_get_sim_id(NMModem *self)
+{
+    return NM_MODEM_GET_PRIVATE(self)->sim_id;
+}
+
+const char *
+nm_modem_get_sim_operator_id(NMModem *self)
+{
+    return NM_MODEM_GET_PRIVATE(self)->sim_operator_id;
+}
+
+const char *
+nm_modem_get_operator_code(NMModem *self)
+{
+    return NM_MODEM_GET_PRIVATE(self)->operator_code;
+}
+
+const char *
+nm_modem_get_apn(NMModem *self)
+{
+    return NM_MODEM_GET_PRIVATE(self)->apn;
+}
+
+/*****************************************************************************/
+/* IP method PPP */
+
+static void
+ppp_state_changed(NMPPPManager *ppp_manager, NMPPPStatus status, gpointer user_data)
+{
+    switch (status) {
+    case NM_PPP_STATUS_DISCONNECT:
+        nm_modem_emit_ppp_failed(user_data, NM_DEVICE_STATE_REASON_PPP_DISCONNECT);
+        break;
+    case NM_PPP_STATUS_DEAD:
+        nm_modem_emit_ppp_failed(user_data, NM_DEVICE_STATE_REASON_PPP_FAILED);
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+ppp_ifindex_set(NMPPPManager *ppp_manager, int ifindex, const char *iface, gpointer user_data)
+{
+    NMModem *self = NM_MODEM(user_data);
+
+    nm_assert(ifindex >= 0);
+    nm_assert(NM_MODEM_GET_PRIVATE(self)->ppp_manager == ppp_manager);
+
+    if (ifindex <= 0 && iface) {
+        /* this might happen, if the ifname was already deleted
+         * and we failed to resolve ifindex.
+         *
+         * Forget about the name. */
+        iface = NULL;
+    }
+    _set_ip_ifindex(self, ifindex, iface);
+}
+
+static void
+ppp_ip4_config(NMPPPManager *ppp_manager, NMIP4Config *config, gpointer user_data)
+{
+    NMModem *self = NM_MODEM(user_data);
+    guint32  i, num;
+    guint32  bad_dns1       = htonl(0x0A0B0C0D);
+    guint32  good_dns1      = htonl(0x04020201); /* GTE nameserver */
+    guint32  bad_dns2       = htonl(0x0A0B0C0E);
+    guint32  good_dns2      = htonl(0x04020202); /* GTE nameserver */
+    gboolean dns_workaround = FALSE;
+
+    /* Work around a PPP bug (#1732) which causes many mobile broadband
+     * providers to return 10.11.12.13 and 10.11.12.14 for the DNS servers.
+     * Apparently fixed in ppp-2.4.5 but we've had some reports that this is
+     * not the case.
+     *
+     * http://git.ozlabs.org/?p=ppp.git;a=commitdiff_plain;h=2e09ef6886bbf00bc5a9a641110f801e372ffde6
+     * http://git.ozlabs.org/?p=ppp.git;a=commitdiff_plain;h=f8191bf07df374f119a07910a79217c7618f113e
+     */
+
+    num = nm_ip4_config_get_num_nameservers(config);
+    if (num == 2) {
+        gboolean found1 = FALSE, found2 = FALSE;
+
+        for (i = 0; i < num; i++) {
+            guint32 ns = nm_ip4_config_get_nameserver(config, i);
+
+            if (ns == bad_dns1)
+                found1 = TRUE;
+            else if (ns == bad_dns2)
+                found2 = TRUE;
+        }
+
+        /* Be somewhat conservative about substitutions; the "bad" nameservers
+         * could actually be valid in some cases, so only substitute if ppp
+         * returns *only* the two bad nameservers.
+         */
+        dns_workaround = (found1 && found2);
+    }
+
+    if (!num || dns_workaround) {
+        _LOGW("compensating for invalid PPP-provided nameservers");
+        nm_ip4_config_reset_nameservers(config);
+        nm_ip4_config_add_nameserver(config, good_dns1);
+        nm_ip4_config_add_nameserver(config, good_dns2);
+    }
+
+    g_signal_emit(self, signals[IP4_CONFIG_RESULT], 0, config, NULL);
+}
+
+static void
+ppp_ip6_config(NMPPPManager *            ppp_manager,
+               const NMUtilsIPv6IfaceId *iid,
+               NMIP6Config *             config,
+               gpointer                  user_data)
+{
+    NMModem *self = NM_MODEM(user_data);
+
+    NM_MODEM_GET_PRIVATE(self)->iid = *iid;
+
+    nm_modem_emit_ip6_config_result(self, config, NULL);
+}
+
+static void
+ppp_stats(NMPPPManager *ppp_manager, guint i_in_bytes, guint i_out_bytes, gpointer user_data)
+{
+    NMModem *       self      = NM_MODEM(user_data);
+    NMModemPrivate *priv      = NM_MODEM_GET_PRIVATE(self);
+    guint32         in_bytes  = i_in_bytes;
+    guint32         out_bytes = i_out_bytes;
+
+    if (priv->in_bytes != in_bytes || priv->out_bytes != out_bytes) {
+        priv->in_bytes  = in_bytes;
+        priv->out_bytes = out_bytes;
+        g_signal_emit(self, signals[PPP_STATS], 0, (guint) in_bytes, (guint) out_bytes);
+    }
+}
+
+static gboolean
+port_speed_is_zero(const char *port)
+{
+    struct termios    options;
+    nm_auto_close int fd   = -1;
+    gs_free char *    path = NULL;
+
+    nm_assert(port);
+
+    if (port[0] != '/') {
+        if (!port[0] || strchr(port, '/') || NM_IN_STRSET(port, ".", ".."))
+            return FALSE;
+        path = g_build_path("/sys/class/tty", port, NULL);
+        port = path;
+    }
+
+    fd = open(port, O_RDWR | O_NONBLOCK | O_NOCTTY | O_CLOEXEC);
+    if (fd < 0)
+        return FALSE;
+
+    memset(&options, 0, sizeof(struct termios));
+    if (tcgetattr(fd, &options) != 0)
+        return FALSE;
+
+    return cfgetospeed(&options) == B0;
+}
+
+static NMActStageReturn
+ppp_stage3_ip_config_start(NMModem *            self,
+                           NMActRequest *       req,
+                           NMDeviceStateReason *out_failure_reason)
+{
+    NMModemPrivate *priv          = NM_MODEM_GET_PRIVATE(self);
+    const char *    ppp_name      = NULL;
+    GError *        error         = NULL;
+    guint           ip_timeout    = 30;
+    guint           baud_override = 0;
+
+    g_return_val_if_fail(NM_IS_MODEM(self), NM_ACT_STAGE_RETURN_FAILURE);
+    g_return_val_if_fail(NM_IS_ACT_REQUEST(req), NM_ACT_STAGE_RETURN_FAILURE);
+
+    /* If we're already running PPP don't restart it; for example, if both
+     * IPv4 and IPv6 are requested, IPv4 gets started first, but we use the
+     * same pppd for both v4 and v6.
+     */
+    if (priv->ppp_manager)
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+
+    if (NM_MODEM_GET_CLASS(self)->get_user_pass) {
+        NMConnection *connection = nm_act_request_get_applied_connection(req);
+
+        g_assert(connection);
+        if (!NM_MODEM_GET_CLASS(self)->get_user_pass(self, connection, &ppp_name, NULL))
+            return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    if (!priv->data_port) {
+        _LOGE("error starting PPP (no data port)");
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PPP_START_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    /* Check if ModemManager requested a specific IP timeout to be used. If 0 reported,
+     * use the default one (30s) */
+    if (priv->mm_ip_timeout > 0) {
+        _LOGI("using modem-specified IP timeout: %u seconds", priv->mm_ip_timeout);
+        ip_timeout = priv->mm_ip_timeout;
+    }
+
+    /* Some tty drivers and modems ignore port speed, but pppd requires the
+     * port speed to be > 0 or it exits. If the port speed is 0 pass an
+     * explicit speed to pppd to prevent the exit.
+     * https://bugzilla.redhat.com/show_bug.cgi?id=1281731
+     */
+    if (port_speed_is_zero(priv->data_port))
+        baud_override = 57600;
+
+    priv->ppp_manager = nm_ppp_manager_create(priv->data_port, &error);
+
+    if (priv->ppp_manager) {
+        nm_ppp_manager_set_route_parameters(priv->ppp_manager,
+                                            priv->ip4_route_table,
+                                            priv->ip4_route_metric,
+                                            priv->ip6_route_table,
+                                            priv->ip6_route_metric);
+    }
+
+    if (!priv->ppp_manager
+        || !nm_ppp_manager_start(priv->ppp_manager,
+                                 req,
+                                 ppp_name,
+                                 ip_timeout,
+                                 baud_override,
+                                 &error)) {
+        _LOGE("error starting PPP: %s", error->message);
+        g_error_free(error);
+        g_clear_object(&priv->ppp_manager);
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PPP_START_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_STATE_CHANGED,
+                     G_CALLBACK(ppp_state_changed),
+                     self);
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_IFINDEX_SET,
+                     G_CALLBACK(ppp_ifindex_set),
+                     self);
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_IP4_CONFIG,
+                     G_CALLBACK(ppp_ip4_config),
+                     self);
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_IP6_CONFIG,
+                     G_CALLBACK(ppp_ip6_config),
+                     self);
+    g_signal_connect(priv->ppp_manager, NM_PPP_MANAGER_SIGNAL_STATS, G_CALLBACK(ppp_stats), self);
+
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+/*****************************************************************************/
+
+NMActStageReturn
+nm_modem_stage3_ip4_config_start(NMModem *            self,
+                                 NMDevice *           device,
+                                 NMDeviceClass *      device_class,
+                                 NMDeviceStateReason *out_failure_reason)
+{
+    NMModemPrivate * priv;
+    NMActRequest *   req;
+    NMConnection *   connection;
+    const char *     method;
+    NMActStageReturn ret;
+
+    _LOGD("ip4_config_start");
+
+    g_return_val_if_fail(NM_IS_MODEM(self), NM_ACT_STAGE_RETURN_FAILURE);
+    g_return_val_if_fail(NM_IS_DEVICE(device), NM_ACT_STAGE_RETURN_FAILURE);
+    g_return_val_if_fail(NM_IS_DEVICE_CLASS(device_class), NM_ACT_STAGE_RETURN_FAILURE);
+
+    req = nm_device_get_act_request(device);
+    g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE);
+
+    connection = nm_act_request_get_applied_connection(req);
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+
+    nm_modem_set_route_parameters_from_device(self, device);
+
+    method = nm_utils_get_ip_config_method(connection, AF_INET);
+
+    /* Only Disabled and Auto methods make sense for WWAN */
+    if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED))
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+
+    if (!nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)) {
+        _LOGE("unhandled WWAN IPv4 method '%s'; will fail", method);
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_IP_METHOD_UNSUPPORTED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+    switch (priv->ip4_method) {
+    case NM_MODEM_IP_METHOD_PPP:
+        ret = ppp_stage3_ip_config_start(self, req, out_failure_reason);
+        break;
+    case NM_MODEM_IP_METHOD_STATIC:
+        _LOGD("MODEM_IP_METHOD_STATIC");
+        ret =
+            NM_MODEM_GET_CLASS(self)->static_stage3_ip4_config_start(self, req, out_failure_reason);
+        break;
+    case NM_MODEM_IP_METHOD_AUTO:
+        _LOGD("MODEM_IP_METHOD_AUTO");
+        ret = device_class->act_stage3_ip_config_start(device, AF_INET, NULL, out_failure_reason);
+        break;
+    default:
+        _LOGI("IPv4 configuration disabled");
+        ret = NM_ACT_STAGE_RETURN_IP_FAIL;
+        break;
+    }
+
+    return ret;
+}
+
+void
+nm_modem_ip4_pre_commit(NMModem *modem, NMDevice *device, NMIP4Config *config)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(modem);
+
+    /* If the modem has an ethernet-type data interface (ie, not PPP and thus
+     * not point-to-point) and IP config has a /32 prefix, then we assume that
+     * ARP will be pointless and we turn it off.
+     */
+    if (priv->ip4_method == NM_MODEM_IP_METHOD_STATIC
+        || priv->ip4_method == NM_MODEM_IP_METHOD_AUTO) {
+        const NMPlatformIP4Address *address = nm_ip4_config_get_first_address(config);
+
+        g_assert(address);
+        if (address->plen == 32)
+            nm_platform_link_set_noarp(nm_device_get_platform(device),
+                                       nm_device_get_ip_ifindex(device));
+    }
+}
+
+/*****************************************************************************/
+
+void
+nm_modem_emit_ip6_config_result(NMModem *self, NMIP6Config *config, GError *error)
+{
+    NMModemPrivate *            priv = NM_MODEM_GET_PRIVATE(self);
+    NMDedupMultiIter            ipconf_iter;
+    const NMPlatformIP6Address *addr;
+    gboolean                    do_slaac = TRUE;
+
+    if (error) {
+        g_signal_emit(self, signals[IP6_CONFIG_RESULT], 0, NULL, FALSE, error);
+        return;
+    }
+
+    if (config) {
+        /* If the IPv6 configuration only included a Link-Local address, then
+         * we have to run SLAAC to get the full IPv6 configuration.
+         */
+        nm_ip_config_iter_ip6_address_for_each (&ipconf_iter, config, &addr) {
+            if (IN6_IS_ADDR_LINKLOCAL(&addr->address)) {
+                if (!priv->iid.id)
+                    priv->iid.id = ((guint64 *) (&addr->address.s6_addr))[1];
+            } else
+                do_slaac = FALSE;
+        }
+    }
+    g_assert(config || do_slaac);
+
+    g_signal_emit(self, signals[IP6_CONFIG_RESULT], 0, config, do_slaac, NULL);
+}
+
+static NMActStageReturn
+stage3_ip6_config_request(NMModem *self, NMDeviceStateReason *out_failure_reason)
+{
+    NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+    return NM_ACT_STAGE_RETURN_FAILURE;
+}
+
+NMActStageReturn
+nm_modem_stage3_ip6_config_start(NMModem *            self,
+                                 NMDevice *           device,
+                                 NMDeviceStateReason *out_failure_reason)
+{
+    NMModemPrivate * priv;
+    NMActRequest *   req;
+    NMActStageReturn ret;
+    NMConnection *   connection;
+    const char *     method;
+
+    g_return_val_if_fail(NM_IS_MODEM(self), NM_ACT_STAGE_RETURN_FAILURE);
+
+    req = nm_device_get_act_request(device);
+    g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE);
+
+    connection = nm_act_request_get_applied_connection(req);
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+
+    nm_modem_set_route_parameters_from_device(self, device);
+
+    method = nm_utils_get_ip_config_method(connection, AF_INET6);
+
+    /* Only Ignore, Disabled and Auto methods make sense for WWAN */
+    if (NM_IN_STRSET(method,
+                     NM_SETTING_IP6_CONFIG_METHOD_IGNORE,
+                     NM_SETTING_IP6_CONFIG_METHOD_DISABLED))
+        return NM_ACT_STAGE_RETURN_IP_DONE;
+
+    if (!nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_AUTO)) {
+        _LOGW("unhandled WWAN IPv6 method '%s'; will fail", method);
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+    switch (priv->ip6_method) {
+    case NM_MODEM_IP_METHOD_PPP:
+        ret = ppp_stage3_ip_config_start(self, req, out_failure_reason);
+        break;
+    case NM_MODEM_IP_METHOD_STATIC:
+    case NM_MODEM_IP_METHOD_AUTO:
+        /* Both static and DHCP/Auto retrieve a base IP config from the modem
+         * which in the static case is the full config, and the DHCP/Auto case
+         * is just the IPv6LL address to use for SLAAC.
+         */
+        ret = NM_MODEM_GET_CLASS(self)->stage3_ip6_config_request(self, out_failure_reason);
+        break;
+    default:
+        _LOGI("IPv6 configuration disabled");
+        ret = NM_ACT_STAGE_RETURN_IP_FAIL;
+        break;
+    }
+
+    return ret;
+}
+
+guint32
+nm_modem_get_configured_mtu(NMDevice *self, NMDeviceMtuSource *out_source, gboolean *out_force)
+{
+    NMConnection *connection;
+    NMSetting *   setting;
+    gint64        mtu_default;
+    guint         mtu = 0;
+    const char *  property_name;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert(out_source);
+
+    connection = nm_device_get_applied_connection(self);
+    if (!connection)
+        g_return_val_if_reached(0);
+
+    setting = (NMSetting *) nm_connection_get_setting_gsm(connection);
+    if (!setting)
+        setting = (NMSetting *) nm_connection_get_setting_cdma(connection);
+
+    if (setting) {
+        g_object_get(setting, "mtu", &mtu, NULL);
+        if (mtu) {
+            *out_source = NM_DEVICE_MTU_SOURCE_CONNECTION;
+            return mtu;
+        }
+
+        property_name = NM_IS_SETTING_GSM(setting) ? "gsm.mtu" : "cdma.mtu";
+        mtu_default =
+            nm_device_get_configured_mtu_from_connection_default(self, property_name, G_MAXUINT32);
+        if (mtu_default >= 0) {
+            *out_source = NM_DEVICE_MTU_SOURCE_CONNECTION;
+            return (guint32) mtu_default;
+        }
+    }
+
+    *out_source = NM_DEVICE_MTU_SOURCE_NONE;
+    return 0;
+}
+
+/*****************************************************************************/
+
+static void
+cancel_get_secrets(NMModem *self)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    if (priv->secrets_id)
+        nm_act_request_cancel_secrets(priv->act_request, priv->secrets_id);
+}
+
+static void
+modem_secrets_cb(NMActRequest *                req,
+                 NMActRequestGetSecretsCallId *call_id,
+                 NMSettingsConnection *        connection,
+                 GError *                      error,
+                 gpointer                      user_data)
+{
+    NMModem *       self = NM_MODEM(user_data);
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    g_return_if_fail(call_id == priv->secrets_id);
+
+    priv->secrets_id = NULL;
+
+    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)
+        || g_error_matches(error, NM_AGENT_MANAGER_ERROR, NM_AGENT_MANAGER_ERROR_NO_SECRETS))
+        return;
+
+    if (error)
+        _LOGW("modem-secrets: %s", error->message);
+
+    g_signal_emit(self, signals[AUTH_RESULT], 0, error);
+}
+
+void
+nm_modem_get_secrets(NMModem *   self,
+                     const char *setting_name,
+                     gboolean    request_new,
+                     const char *hint)
+{
+    NMModemPrivate *             priv  = NM_MODEM_GET_PRIVATE(self);
+    NMSecretAgentGetSecretsFlags flags = NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION;
+
+    cancel_get_secrets(self);
+
+    if (request_new)
+        flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW;
+    priv->secrets_id = nm_act_request_get_secrets(priv->act_request,
+                                                  FALSE,
+                                                  setting_name,
+                                                  flags,
+                                                  NM_MAKE_STRV(hint),
+                                                  modem_secrets_cb,
+                                                  self);
+    g_return_if_fail(priv->secrets_id);
+    g_signal_emit(self, signals[AUTH_REQUESTED], 0);
+}
+
+/*****************************************************************************/
+
+static NMActStageReturn
+modem_act_stage1_prepare(NMModem *            modem,
+                         NMConnection *       connection,
+                         NMDeviceStateReason *out_failure_reason)
+{
+    NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_UNKNOWN);
+    return NM_ACT_STAGE_RETURN_FAILURE;
+}
+
+NMActStageReturn
+nm_modem_act_stage1_prepare(NMModem *            self,
+                            NMActRequest *       req,
+                            NMDeviceStateReason *out_failure_reason)
+{
+    NMModemPrivate *  priv                    = NM_MODEM_GET_PRIVATE(self);
+    gs_unref_ptrarray GPtrArray *hints        = NULL;
+    const char *                 setting_name = NULL;
+    NMSecretAgentGetSecretsFlags flags        = NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION;
+    NMConnection *               connection;
+
+    g_return_val_if_fail(NM_IS_ACT_REQUEST(req), NM_ACT_STAGE_RETURN_FAILURE);
+
+    if (priv->act_request)
+        g_object_unref(priv->act_request);
+    priv->act_request = g_object_ref(req);
+
+    connection = nm_act_request_get_applied_connection(req);
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+
+    setting_name = nm_connection_need_secrets(connection, &hints);
+    if (!setting_name) {
+        nm_assert(!hints);
+        return NM_MODEM_GET_CLASS(self)->modem_act_stage1_prepare(self,
+                                                                  connection,
+                                                                  out_failure_reason);
+    }
+
+    /* Secrets required... */
+    if (priv->secrets_tries++)
+        flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW;
+
+    if (hints)
+        g_ptr_array_add(hints, NULL);
+
+    priv->secrets_id = nm_act_request_get_secrets(req,
+                                                  FALSE,
+                                                  setting_name,
+                                                  flags,
+                                                  hints ? (const char *const *) hints->pdata : NULL,
+                                                  modem_secrets_cb,
+                                                  self);
+    g_return_val_if_fail(priv->secrets_id, NM_ACT_STAGE_RETURN_FAILURE);
+    g_signal_emit(self, signals[AUTH_REQUESTED], 0);
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+/*****************************************************************************/
+
+void
+nm_modem_act_stage2_config(NMModem *self)
+{
+    NMModemPrivate *priv;
+
+    g_return_if_fail(NM_IS_MODEM(self));
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+    /* Clear secrets tries counter since secrets were successfully used
+     * already if we get here.
+     */
+    priv->secrets_tries = 0;
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_modem_check_connection_compatible(NMModem *self, NMConnection *connection, GError **error)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    if (nm_streq0(nm_connection_get_connection_type(connection), NM_SETTING_GSM_SETTING_NAME)) {
+        NMSettingGsm *s_gsm;
+        const char *  str;
+
+        s_gsm = _nm_connection_check_main_setting(connection, NM_SETTING_GSM_SETTING_NAME, error);
+        if (!s_gsm)
+            return FALSE;
+
+        str = nm_setting_gsm_get_device_id(s_gsm);
+        if (str) {
+            if (!priv->device_id) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "GSM profile has device-id, device does not");
+                return FALSE;
+            }
+            if (!nm_streq(str, priv->device_id)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "device has differing device-id than GSM profile");
+                return FALSE;
+            }
+        }
+
+        /* SIM properties may not be available before the SIM is unlocked, so
+         * to ensure that autoconnect works, the connection's SIM properties
+         * are only compared if present on the device.
+         */
+
+        if (priv->sim_id && (str = nm_setting_gsm_get_sim_id(s_gsm))) {
+            if (!nm_streq(str, priv->sim_id)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "device has differing sim-id than GSM profile");
+                return FALSE;
+            }
+        }
+
+        if (priv->sim_operator_id && (str = nm_setting_gsm_get_sim_operator_id(s_gsm))) {
+            if (!nm_streq(str, priv->sim_operator_id)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "device has differing sim-operator-id than GSM profile");
+                return FALSE;
+            }
+        }
+    }
+
+    return NM_MODEM_GET_CLASS(self)->check_connection_compatible_with_modem(self,
+                                                                            connection,
+                                                                            error);
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_modem_complete_connection(NMModem *            self,
+                             const char *         iface,
+                             NMConnection *       connection,
+                             NMConnection *const *existing_connections,
+                             GError **            error)
+{
+    NMModemClass *klass;
+
+    klass = NM_MODEM_GET_CLASS(self);
+    if (!klass->complete_connection) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_INVALID_CONNECTION,
+                    "Modem class %s had no complete_connection method",
+                    G_OBJECT_TYPE_NAME(self));
+        return FALSE;
+    }
+
+    return klass->complete_connection(self, iface, connection, existing_connections, error);
+}
+
+/*****************************************************************************/
+
+static void
+deactivate_cleanup(NMModem *self, NMDevice *device, gboolean stop_ppp_manager)
+{
+    NMModemPrivate *priv;
+    int             ifindex;
+
+    g_return_if_fail(NM_IS_MODEM(self));
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+
+    priv->secrets_tries = 0;
+
+    if (priv->act_request) {
+        cancel_get_secrets(self);
+        g_object_unref(priv->act_request);
+        priv->act_request = NULL;
+    }
+
+    priv->in_bytes = priv->out_bytes = 0;
+
+    if (priv->ppp_manager) {
+        g_signal_handlers_disconnect_by_data(priv->ppp_manager, self);
+        if (stop_ppp_manager)
+            nm_ppp_manager_stop(priv->ppp_manager, NULL, NULL, NULL);
+        g_clear_object(&priv->ppp_manager);
+    }
+
+    if (device) {
+        g_return_if_fail(NM_IS_DEVICE(device));
+
+        if (priv->ip4_method == NM_MODEM_IP_METHOD_STATIC
+            || priv->ip4_method == NM_MODEM_IP_METHOD_AUTO
+            || priv->ip6_method == NM_MODEM_IP_METHOD_STATIC
+            || priv->ip6_method == NM_MODEM_IP_METHOD_AUTO) {
+            ifindex = nm_device_get_ip_ifindex(device);
+            if (ifindex > 0) {
+                NMPlatform *platform = nm_device_get_platform(device);
+
+                nm_platform_ip_route_flush(platform, AF_UNSPEC, ifindex);
+                nm_platform_ip_address_flush(platform, AF_UNSPEC, ifindex);
+                nm_platform_link_set_down(platform, ifindex);
+            }
+        }
+    }
+
+    nm_clear_g_free(&priv->data_port);
+    priv->mm_ip_timeout = 0;
+    priv->ip4_method    = NM_MODEM_IP_METHOD_UNKNOWN;
+    priv->ip6_method    = NM_MODEM_IP_METHOD_UNKNOWN;
+    _set_ip_ifindex(self, -1, NULL);
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    NMModem *                 self;
+    NMDevice *                device;
+    GCancellable *            cancellable;
+    NMModemDeactivateCallback callback;
+    gpointer                  callback_user_data;
+} DeactivateContext;
+
+static void
+deactivate_context_complete(DeactivateContext *ctx, GError *error)
+{
+    NMModem *self = ctx->self;
+
+    _LOGD("modem deactivation finished %s%s%s",
+          NM_PRINT_FMT_QUOTED(error, "with failure: ", error->message, "", "successfully"));
+
+    if (ctx->callback)
+        ctx->callback(ctx->self, error, ctx->callback_user_data);
+    nm_g_object_unref(ctx->cancellable);
+    g_object_unref(ctx->device);
+    g_object_unref(ctx->self);
+    g_slice_free(DeactivateContext, ctx);
+}
+
+static void
+_deactivate_call_disconnect_cb(NMModem *self, GError *error, gpointer user_data)
+{
+    deactivate_context_complete(user_data, error);
+}
+
+static void
+_deactivate_call_disconnect(DeactivateContext *ctx)
+{
+    NM_MODEM_GET_CLASS(ctx->self)->disconnect(ctx->self,
+                                              FALSE,
+                                              ctx->cancellable,
+                                              _deactivate_call_disconnect_cb,
+                                              ctx);
+}
+
+static void
+_deactivate_ppp_manager_stop_cb(NMPPPManager *          ppp_manager,
+                                NMPPPManagerStopHandle *handle,
+                                gboolean                was_cancelled,
+                                gpointer                user_data)
+{
+    DeactivateContext *ctx = user_data;
+
+    g_object_unref(ppp_manager);
+
+    if (was_cancelled) {
+        gs_free_error GError *error = NULL;
+
+        if (!g_cancellable_set_error_if_cancelled(ctx->cancellable, &error))
+            nm_assert_not_reached();
+        deactivate_context_complete(ctx, error);
+        return;
+    }
+
+    nm_assert(!g_cancellable_is_cancelled(ctx->cancellable));
+    _deactivate_call_disconnect(ctx);
+}
+
+void
+nm_modem_deactivate_async(NMModem *                 self,
+                          NMDevice *                device,
+                          GCancellable *            cancellable,
+                          NMModemDeactivateCallback callback,
+                          gpointer                  user_data)
+{
+    NMModemPrivate *   priv = NM_MODEM_GET_PRIVATE(self);
+    DeactivateContext *ctx;
+    NMPPPManager *     ppp_manager;
+
+    g_return_if_fail(NM_IS_MODEM(self));
+    g_return_if_fail(NM_IS_DEVICE(device));
+    g_return_if_fail(G_IS_CANCELLABLE(cancellable));
+
+    ctx                     = g_slice_new(DeactivateContext);
+    ctx->self               = g_object_ref(self);
+    ctx->device             = g_object_ref(device);
+    ctx->cancellable        = g_object_ref(cancellable);
+    ctx->callback           = callback;
+    ctx->callback_user_data = user_data;
+
+    ppp_manager = nm_g_object_ref(priv->ppp_manager);
+
+    NM_MODEM_GET_CLASS(self)->deactivate_cleanup(self, ctx->device, FALSE);
+
+    if (ppp_manager) {
+        /* If we have a PPP manager, stop it.
+         *
+         * Pass on the reference in @ppp_manager. */
+        nm_ppp_manager_stop(ppp_manager, ctx->cancellable, _deactivate_ppp_manager_stop_cb, ctx);
+        return;
+    }
+
+    _deactivate_call_disconnect(ctx);
+}
+
+/*****************************************************************************/
+
+void
+nm_modem_deactivate(NMModem *self, NMDevice *device)
+{
+    /* First cleanup */
+    NM_MODEM_GET_CLASS(self)->deactivate_cleanup(self, device, TRUE);
+    /* Then disconnect without waiting */
+    NM_MODEM_GET_CLASS(self)->disconnect(self, FALSE, NULL, NULL, NULL);
+}
+
+/*****************************************************************************/
+
+void
+nm_modem_device_state_changed(NMModem *self, NMDeviceState new_state, NMDeviceState old_state)
+{
+    gboolean        was_connected = FALSE, warn = TRUE;
+    NMModemPrivate *priv;
+
+    g_return_if_fail(NM_IS_MODEM(self));
+
+    if (old_state >= NM_DEVICE_STATE_PREPARE && old_state <= NM_DEVICE_STATE_DEACTIVATING)
+        was_connected = TRUE;
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+
+    /* Make sure we don't leave the serial device open */
+    switch (new_state) {
+    case NM_DEVICE_STATE_UNMANAGED:
+    case NM_DEVICE_STATE_UNAVAILABLE:
+    case NM_DEVICE_STATE_FAILED:
+    case NM_DEVICE_STATE_DISCONNECTED:
+        if (priv->act_request) {
+            cancel_get_secrets(self);
+            g_object_unref(priv->act_request);
+            priv->act_request = NULL;
+        }
+
+        if (was_connected) {
+            /* Don't bother warning on FAILED since the modem is already gone */
+            if (new_state == NM_DEVICE_STATE_FAILED || new_state == NM_DEVICE_STATE_DISCONNECTED)
+                warn = FALSE;
+            /* First cleanup */
+            NM_MODEM_GET_CLASS(self)->deactivate_cleanup(self, NULL, TRUE);
+            NM_MODEM_GET_CLASS(self)->disconnect(self, warn, NULL, NULL, NULL);
+        }
+        break;
+    default:
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+const char *
+nm_modem_get_uid(NMModem *self)
+{
+    g_return_val_if_fail(NM_IS_MODEM(self), NULL);
+
+    return NM_MODEM_GET_PRIVATE(self)->uid;
+}
+
+const char *
+nm_modem_get_path(NMModem *self)
+{
+    g_return_val_if_fail(NM_IS_MODEM(self), NULL);
+
+    return NM_MODEM_GET_PRIVATE(self)->path;
+}
+
+const char *
+nm_modem_get_driver(NMModem *self)
+{
+    g_return_val_if_fail(NM_IS_MODEM(self), NULL);
+
+    return NM_MODEM_GET_PRIVATE(self)->driver;
+}
+
+const char *
+nm_modem_get_control_port(NMModem *self)
+{
+    g_return_val_if_fail(NM_IS_MODEM(self), NULL);
+
+    return NM_MODEM_GET_PRIVATE(self)->control_port;
+}
+
+int
+nm_modem_get_ip_ifindex(NMModem *self)
+{
+    NMModemPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_MODEM(self), 0);
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+
+    /* internally we track an unset ip_ifindex as -1.
+     * For the caller of nm_modem_get_ip_ifindex(), this
+     * shall be zero too. */
+    return priv->ip_ifindex != -1 ? priv->ip_ifindex : 0;
+}
+
+static void
+_set_ip_ifindex(NMModem *self, int ifindex, const char *ifname)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    nm_assert(ifindex >= -1);
+    nm_assert((ifindex > 0) == !!ifname);
+
+    if (!nm_streq0(priv->ip_iface, ifname)) {
+        g_free(priv->ip_iface);
+        priv->ip_iface = g_strdup(ifname);
+    }
+
+    if (priv->ip_ifindex != ifindex) {
+        priv->ip_ifindex = ifindex;
+        _notify(self, PROP_IP_IFINDEX);
+    }
+}
+
+gboolean
+nm_modem_set_data_port(NMModem *       self,
+                       NMPlatform *    platform,
+                       const char *    data_port,
+                       NMModemIPMethod ip4_method,
+                       NMModemIPMethod ip6_method,
+                       guint           timeout,
+                       GError **       error)
+{
+    NMModemPrivate *priv;
+    gboolean        is_ppp;
+    int             ifindex = -1;
+
+    g_return_val_if_fail(NM_IS_MODEM(self), FALSE);
+    g_return_val_if_fail(NM_IS_PLATFORM(platform), FALSE);
+    g_return_val_if_fail(!error || !*error, FALSE);
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+
+    if (priv->ppp_manager || priv->data_port || priv->ip_ifindex != -1) {
+        g_set_error_literal(error,
+                            NM_UTILS_ERROR,
+                            NM_UTILS_ERROR_UNKNOWN,
+                            "cannot set data port in activated state");
+        /* this really shouldn't happen. Assert. */
+        g_return_val_if_reached(FALSE);
+    }
+
+    if (!data_port) {
+        g_set_error_literal(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "missing data port");
+        return FALSE;
+    }
+
+    is_ppp = (ip4_method == NM_MODEM_IP_METHOD_PPP) || (ip6_method == NM_MODEM_IP_METHOD_PPP);
+    if (is_ppp) {
+        if (!NM_IN_SET(ip4_method, NM_MODEM_IP_METHOD_UNKNOWN, NM_MODEM_IP_METHOD_PPP)
+            || !NM_IN_SET(ip6_method, NM_MODEM_IP_METHOD_UNKNOWN, NM_MODEM_IP_METHOD_PPP)) {
+            g_set_error_literal(error,
+                                NM_UTILS_ERROR,
+                                NM_UTILS_ERROR_UNKNOWN,
+                                "conflicting ip methods");
+            return FALSE;
+        }
+    } else if (!NM_IN_SET(ip4_method,
+                          NM_MODEM_IP_METHOD_UNKNOWN,
+                          NM_MODEM_IP_METHOD_STATIC,
+                          NM_MODEM_IP_METHOD_AUTO)
+               || !NM_IN_SET(ip6_method,
+                             NM_MODEM_IP_METHOD_UNKNOWN,
+                             NM_MODEM_IP_METHOD_STATIC,
+                             NM_MODEM_IP_METHOD_AUTO)
+               || (ip4_method == NM_MODEM_IP_METHOD_UNKNOWN
+                   && ip6_method == NM_MODEM_IP_METHOD_UNKNOWN)) {
+        g_set_error_literal(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "invalid ip methods");
+        return FALSE;
+    }
+
+    if (!is_ppp) {
+        ifindex = nm_platform_if_nametoindex(platform, data_port);
+        if (ifindex <= 0) {
+            g_set_error(error,
+                        NM_UTILS_ERROR,
+                        NM_UTILS_ERROR_UNKNOWN,
+                        "cannot find network interface %s",
+                        data_port);
+            return FALSE;
+        }
+        if (!nm_platform_process_events_ensure_link(platform, ifindex, data_port)) {
+            g_set_error(error,
+                        NM_UTILS_ERROR,
+                        NM_UTILS_ERROR_UNKNOWN,
+                        "cannot find network interface %s in platform cache",
+                        data_port);
+            return FALSE;
+        }
+    }
+
+    priv->mm_ip_timeout = timeout;
+    priv->ip4_method    = ip4_method;
+    priv->ip6_method    = ip6_method;
+    if (is_ppp) {
+        priv->data_port = g_strdup(data_port);
+        _set_ip_ifindex(self, -1, NULL);
+    } else {
+        priv->data_port = NULL;
+        _set_ip_ifindex(self, ifindex, data_port);
+    }
+    return TRUE;
+}
+
+gboolean
+nm_modem_owns_port(NMModem *self, const char *iface)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    g_return_val_if_fail(iface != NULL, FALSE);
+
+    if (NM_MODEM_GET_CLASS(self)->owns_port)
+        return NM_MODEM_GET_CLASS(self)->owns_port(self, iface);
+
+    return NM_IN_STRSET(iface, priv->ip_iface, priv->data_port, priv->control_port);
+}
+
+gboolean
+nm_modem_get_iid(NMModem *self, NMUtilsIPv6IfaceId *out_iid)
+{
+    g_return_val_if_fail(NM_IS_MODEM(self), FALSE);
+
+    *out_iid = NM_MODEM_GET_PRIVATE(self)->iid;
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+void
+nm_modem_get_route_parameters(NMModem *self,
+                              guint32 *out_ip4_route_table,
+                              guint32 *out_ip4_route_metric,
+                              guint32 *out_ip6_route_table,
+                              guint32 *out_ip6_route_metric)
+{
+    NMModemPrivate *priv;
+
+    g_return_if_fail(NM_IS_MODEM(self));
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+    NM_SET_OUT(out_ip4_route_table, priv->ip4_route_table);
+    NM_SET_OUT(out_ip4_route_metric, priv->ip4_route_metric);
+    NM_SET_OUT(out_ip6_route_table, priv->ip6_route_table);
+    NM_SET_OUT(out_ip6_route_metric, priv->ip6_route_metric);
+}
+
+void
+nm_modem_set_route_parameters(NMModem *self,
+                              guint32  ip4_route_table,
+                              guint32  ip4_route_metric,
+                              guint32  ip6_route_table,
+                              guint32  ip6_route_metric)
+{
+    NMModemPrivate *priv;
+
+    g_return_if_fail(NM_IS_MODEM(self));
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+    if (priv->ip4_route_table != ip4_route_table || priv->ip4_route_metric != ip4_route_metric
+        || priv->ip6_route_table != ip6_route_table || priv->ip6_route_metric != ip6_route_metric) {
+        priv->ip4_route_table  = ip4_route_table;
+        priv->ip4_route_metric = ip4_route_metric;
+        priv->ip6_route_table  = ip6_route_table;
+        priv->ip6_route_metric = ip6_route_metric;
+
+        _LOGT("route-parameters: table-v4: %u, metric-v4: %u, table-v6: %u, metric-v6: %u",
+              priv->ip4_route_table,
+              priv->ip4_route_metric,
+              priv->ip6_route_table,
+              priv->ip6_route_metric);
+    }
+
+    if (priv->ppp_manager) {
+        nm_ppp_manager_set_route_parameters(priv->ppp_manager,
+                                            priv->ip4_route_table,
+                                            priv->ip4_route_metric,
+                                            priv->ip6_route_table,
+                                            priv->ip6_route_metric);
+    }
+}
+
+void
+nm_modem_set_route_parameters_from_device(NMModem *self, NMDevice *device)
+{
+    g_return_if_fail(NM_IS_DEVICE(device));
+
+    nm_modem_set_route_parameters(self,
+                                  nm_device_get_route_table(device, AF_INET),
+                                  nm_device_get_route_metric(device, AF_INET),
+                                  nm_device_get_route_table(device, AF_INET6),
+                                  nm_device_get_route_metric(device, AF_INET6));
+}
+
+/*****************************************************************************/
+
+void
+nm_modem_get_capabilities(NMModem *                  self,
+                          NMDeviceModemCapabilities *modem_caps,
+                          NMDeviceModemCapabilities *current_caps)
+{
+    g_return_if_fail(NM_IS_MODEM(self));
+
+    NM_MODEM_GET_CLASS(self)->get_capabilities(self, modem_caps, current_caps);
+}
+
+/*****************************************************************************/
+
+void
+_nm_modem_set_operator_code(NMModem *self, const char *operator_code)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    if (g_strcmp0(priv->operator_code, operator_code) != 0) {
+        g_free(priv->operator_code);
+        priv->operator_code = g_strdup(operator_code);
+        _notify(self, PROP_OPERATOR_CODE);
+    }
+}
+
+void
+_nm_modem_set_apn(NMModem *self, const char *apn)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    if (g_strcmp0(priv->apn, apn) != 0) {
+        g_free(priv->apn);
+        priv->apn = g_strdup(apn);
+        _notify(self, PROP_APN);
+    }
+}
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMModem *       self = NM_MODEM(object);
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    switch (prop_id) {
+    case PROP_PATH:
+        g_value_set_string(value, priv->path);
+        break;
+    case PROP_DRIVER:
+        g_value_set_string(value, priv->driver);
+        break;
+    case PROP_CONTROL_PORT:
+        g_value_set_string(value, priv->control_port);
+        break;
+    case PROP_IP_IFINDEX:
+        g_value_set_int(value, nm_modem_get_ip_ifindex(self));
+        break;
+    case PROP_UID:
+        g_value_set_string(value, priv->uid);
+        break;
+    case PROP_STATE:
+        g_value_set_int(value, priv->state);
+        break;
+    case PROP_DEVICE_ID:
+        g_value_set_string(value, priv->device_id);
+        break;
+    case PROP_SIM_ID:
+        g_value_set_string(value, priv->sim_id);
+        break;
+    case PROP_IP_TYPES:
+        g_value_set_uint(value, priv->ip_types);
+        break;
+    case PROP_SIM_OPERATOR_ID:
+        g_value_set_string(value, priv->sim_operator_id);
+        break;
+    case PROP_OPERATOR_CODE:
+        g_value_set_string(value, priv->operator_code);
+        break;
+    case PROP_APN:
+        g_value_set_string(value, priv->apn);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+static void
+set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(object);
+    const char *    s;
+
+    switch (prop_id) {
+    case PROP_PATH:
+        /* construct-only */
+        priv->path = g_value_dup_string(value);
+        g_return_if_fail(priv->path);
+        break;
+    case PROP_DRIVER:
+        /* construct-only */
+        priv->driver = g_value_dup_string(value);
+        break;
+    case PROP_CONTROL_PORT:
+        /* construct-only */
+        priv->control_port = g_value_dup_string(value);
+        break;
+    case PROP_UID:
+        /* construct-only */
+        priv->uid = g_value_dup_string(value);
+        break;
+    case PROP_STATE:
+        /* construct-only */
+        priv->state = g_value_get_int(value);
+        break;
+    case PROP_DEVICE_ID:
+        /* construct-only */
+        priv->device_id = g_value_dup_string(value);
+        break;
+    case PROP_SIM_ID:
+        g_free(priv->sim_id);
+        priv->sim_id = g_value_dup_string(value);
+        break;
+    case PROP_IP_TYPES:
+        priv->ip_types = g_value_get_uint(value);
+        break;
+    case PROP_SIM_OPERATOR_ID:
+        nm_clear_g_free(&priv->sim_operator_id);
+        s = g_value_get_string(value);
+        if (s && s[0])
+            priv->sim_operator_id = g_strdup(s);
+        break;
+    case PROP_OPERATOR_CODE:
+        /* construct-only */
+        priv->operator_code = g_value_dup_string(value);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_modem_init(NMModem *self)
+{
+    NMModemPrivate *priv;
+
+    self->_priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_MODEM, NMModemPrivate);
+    priv        = self->_priv;
+
+    priv->ip_ifindex       = -1;
+    priv->ip4_route_table  = RT_TABLE_MAIN;
+    priv->ip4_route_metric = 700;
+    priv->ip6_route_table  = RT_TABLE_MAIN;
+    priv->ip6_route_metric = 700;
+}
+
+static void
+constructed(GObject *object)
+{
+    NMModemPrivate *priv;
+
+    G_OBJECT_CLASS(nm_modem_parent_class)->constructed(object);
+
+    priv = NM_MODEM_GET_PRIVATE(NM_MODEM(object));
+
+    g_return_if_fail(priv->control_port);
+}
+
+/*****************************************************************************/
+
+static void
+dispose(GObject *object)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(object);
+
+    g_clear_object(&priv->act_request);
+
+    G_OBJECT_CLASS(nm_modem_parent_class)->dispose(object);
+}
+
+static void
+finalize(GObject *object)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(object);
+
+    g_free(priv->uid);
+    g_free(priv->path);
+    g_free(priv->driver);
+    g_free(priv->control_port);
+    g_free(priv->data_port);
+    g_free(priv->ip_iface);
+    g_free(priv->device_id);
+    g_free(priv->sim_id);
+    g_free(priv->sim_operator_id);
+    g_free(priv->operator_code);
+    g_free(priv->apn);
+
+    G_OBJECT_CLASS(nm_modem_parent_class)->finalize(object);
+}
+
+static void
+nm_modem_class_init(NMModemClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    g_type_class_add_private(object_class, sizeof(NMModemPrivate));
+
+    object_class->constructed  = constructed;
+    object_class->set_property = set_property;
+    object_class->get_property = get_property;
+    object_class->dispose      = dispose;
+    object_class->finalize     = finalize;
+
+    klass->modem_act_stage1_prepare  = modem_act_stage1_prepare;
+    klass->stage3_ip6_config_request = stage3_ip6_config_request;
+    klass->deactivate_cleanup        = deactivate_cleanup;
+
+    obj_properties[PROP_UID] =
+        g_param_spec_string(NM_MODEM_UID,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_PATH] =
+        g_param_spec_string(NM_MODEM_PATH,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_DRIVER] =
+        g_param_spec_string(NM_MODEM_DRIVER,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_CONTROL_PORT] =
+        g_param_spec_string(NM_MODEM_CONTROL_PORT,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_IP_IFINDEX] = g_param_spec_int(NM_MODEM_IP_IFINDEX,
+                                                       "",
+                                                       "",
+                                                       0,
+                                                       G_MAXINT,
+                                                       0,
+                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_STATE] =
+        g_param_spec_int(NM_MODEM_STATE,
+                         "",
+                         "",
+                         NM_MODEM_STATE_UNKNOWN,
+                         _NM_MODEM_STATE_LAST,
+                         NM_MODEM_STATE_UNKNOWN,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_DEVICE_ID] =
+        g_param_spec_string(NM_MODEM_DEVICE_ID,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_SIM_ID] =
+        g_param_spec_string(NM_MODEM_SIM_ID,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_IP_TYPES] =
+        g_param_spec_uint(NM_MODEM_IP_TYPES,
+                          "IP Types",
+                          "Supported IP types",
+                          0,
+                          G_MAXUINT32,
+                          NM_MODEM_IP_TYPE_IPV4,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_SIM_OPERATOR_ID] =
+        g_param_spec_string(NM_MODEM_SIM_OPERATOR_ID,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_OPERATOR_CODE] =
+        g_param_spec_string(NM_MODEM_OPERATOR_CODE,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_APN] =
+        g_param_spec_string(NM_MODEM_APN, "", "", NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+
+    signals[PPP_STATS] = g_signal_new(NM_MODEM_PPP_STATS,
+                                      G_OBJECT_CLASS_TYPE(object_class),
+                                      G_SIGNAL_RUN_FIRST,
+                                      0,
+                                      NULL,
+                                      NULL,
+                                      NULL,
+                                      G_TYPE_NONE,
+                                      2,
+                                      G_TYPE_UINT /*guint32 in_bytes*/,
+                                      G_TYPE_UINT /*guint32 out_bytes*/);
+
+    signals[PPP_FAILED] = g_signal_new(NM_MODEM_PPP_FAILED,
+                                       G_OBJECT_CLASS_TYPE(object_class),
+                                       G_SIGNAL_RUN_FIRST,
+                                       0,
+                                       NULL,
+                                       NULL,
+                                       NULL,
+                                       G_TYPE_NONE,
+                                       1,
+                                       G_TYPE_UINT);
+
+    signals[IP4_CONFIG_RESULT] = g_signal_new(NM_MODEM_IP4_CONFIG_RESULT,
+                                              G_OBJECT_CLASS_TYPE(object_class),
+                                              G_SIGNAL_RUN_FIRST,
+                                              0,
+                                              NULL,
+                                              NULL,
+                                              NULL,
+                                              G_TYPE_NONE,
+                                              2,
+                                              G_TYPE_OBJECT,
+                                              G_TYPE_POINTER);
+
+    /**
+     * NMModem::ip6-config-result:
+     * @modem: the #NMModem  on which the signal is emitted
+     * @config: the #NMIP6Config to apply to the modem's data port
+     * @do_slaac: %TRUE if IPv6 SLAAC should be started
+     * @error: a #GError if any error occurred during IP configuration
+     *
+     * This signal is emitted when IPv6 configuration has completed or failed.
+     * If @error is set the configuration failed.  If @config is set, then
+     * the details should be applied to the data port before any further
+     * configuration (like SLAAC) is done.  @do_slaac indicates whether SLAAC
+     * should be started after applying @config to the data port.
+     */
+    signals[IP6_CONFIG_RESULT] = g_signal_new(NM_MODEM_IP6_CONFIG_RESULT,
+                                              G_OBJECT_CLASS_TYPE(object_class),
+                                              G_SIGNAL_RUN_FIRST,
+                                              0,
+                                              NULL,
+                                              NULL,
+                                              NULL,
+                                              G_TYPE_NONE,
+                                              3,
+                                              G_TYPE_OBJECT,
+                                              G_TYPE_BOOLEAN,
+                                              G_TYPE_POINTER);
+
+    signals[PREPARE_RESULT] = g_signal_new(NM_MODEM_PREPARE_RESULT,
+                                           G_OBJECT_CLASS_TYPE(object_class),
+                                           G_SIGNAL_RUN_FIRST,
+                                           0,
+                                           NULL,
+                                           NULL,
+                                           NULL,
+                                           G_TYPE_NONE,
+                                           2,
+                                           G_TYPE_BOOLEAN,
+                                           G_TYPE_UINT);
+
+    signals[AUTH_REQUESTED] = g_signal_new(NM_MODEM_AUTH_REQUESTED,
+                                           G_OBJECT_CLASS_TYPE(object_class),
+                                           G_SIGNAL_RUN_FIRST,
+                                           0,
+                                           NULL,
+                                           NULL,
+                                           NULL,
+                                           G_TYPE_NONE,
+                                           0);
+
+    signals[AUTH_RESULT] = g_signal_new(NM_MODEM_AUTH_RESULT,
+                                        G_OBJECT_CLASS_TYPE(object_class),
+                                        G_SIGNAL_RUN_FIRST,
+                                        0,
+                                        NULL,
+                                        NULL,
+                                        NULL,
+                                        G_TYPE_NONE,
+                                        1,
+                                        G_TYPE_POINTER);
+
+    signals[REMOVED] = g_signal_new(NM_MODEM_REMOVED,
+                                    G_OBJECT_CLASS_TYPE(object_class),
+                                    G_SIGNAL_RUN_FIRST,
+                                    0,
+                                    NULL,
+                                    NULL,
+                                    NULL,
+                                    G_TYPE_NONE,
+                                    0);
+
+    signals[STATE_CHANGED] = g_signal_new(NM_MODEM_STATE_CHANGED,
+                                          G_OBJECT_CLASS_TYPE(object_class),
+                                          G_SIGNAL_RUN_FIRST,
+                                          0,
+                                          NULL,
+                                          NULL,
+                                          NULL,
+                                          G_TYPE_NONE,
+                                          2,
+                                          G_TYPE_INT,
+                                          G_TYPE_INT);
+}
diff --git a/src/core/devices/wwan/nm-modem.h b/src/core/devices/wwan/nm-modem.h
new file mode 100644
index 00000000..87162cfc
--- /dev/null
+++ b/src/core/devices/wwan/nm-modem.h
@@ -0,0 +1,269 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2009 - 2011 Red Hat, Inc.
+ * Copyright (C) 2009 Novell, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_MODEM_H__
+#define __NETWORKMANAGER_MODEM_H__
+
+#include "ppp/nm-ppp-manager.h"
+#include "devices/nm-device.h"
+
+#define NM_TYPE_MODEM            (nm_modem_get_type())
+#define NM_MODEM(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM, NMModem))
+#define NM_MODEM_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MODEM, NMModemClass))
+#define NM_IS_MODEM(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MODEM))
+#define NM_IS_MODEM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_MODEM))
+#define NM_MODEM_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_MODEM, NMModemClass))
+
+/* Properties */
+#define NM_MODEM_UID             "uid"
+#define NM_MODEM_PATH            "path"
+#define NM_MODEM_DRIVER          "driver"
+#define NM_MODEM_CONTROL_PORT    "control-port"
+#define NM_MODEM_IP_IFINDEX      "ip-ifindex"
+#define NM_MODEM_STATE           "state"
+#define NM_MODEM_DEVICE_ID       "device-id"
+#define NM_MODEM_SIM_ID          "sim-id"
+#define NM_MODEM_IP_TYPES        "ip-types" /* Supported IP types */
+#define NM_MODEM_SIM_OPERATOR_ID "sim-operator-id"
+#define NM_MODEM_OPERATOR_CODE   "operator-code"
+#define NM_MODEM_APN             "apn"
+
+/* Signals */
+#define NM_MODEM_PPP_STATS         "ppp-stats"
+#define NM_MODEM_PPP_FAILED        "ppp-failed"
+#define NM_MODEM_PREPARE_RESULT    "prepare-result"
+#define NM_MODEM_IP4_CONFIG_RESULT "ip4-config-result"
+#define NM_MODEM_IP6_CONFIG_RESULT "ip6-config-result"
+#define NM_MODEM_AUTH_REQUESTED    "auth-requested"
+#define NM_MODEM_AUTH_RESULT       "auth-result"
+#define NM_MODEM_REMOVED           "removed"
+#define NM_MODEM_STATE_CHANGED     "state-changed"
+
+typedef enum {
+    NM_MODEM_IP_METHOD_UNKNOWN = 0,
+    NM_MODEM_IP_METHOD_PPP,
+    NM_MODEM_IP_METHOD_STATIC,
+    NM_MODEM_IP_METHOD_AUTO, /* DHCP and/or SLAAC */
+} NMModemIPMethod;
+
+/**
+ * NMModemIPType:
+ * @NM_MODEM_IP_TYPE_UNKNOWN: unknown or no IP support
+ * @NM_MODEM_IP_TYPE_IPV4: IPv4-only bearers are supported
+ * @NM_MODEM_IP_TYPE_IPV6: IPv6-only bearers are supported
+ * @NM_MODEM_IP_TYPE_IPV4V6: dual-stack IPv4 + IPv6 bearers are supported
+ *
+ * Indicates what IP protocols the modem supports for an IP bearer.  Any
+ * combination of flags is possible.  For example, (%NM_MODEM_IP_TYPE_IPV4 |
+ * %NM_MODEM_IP_TYPE_IPV6) indicates that the modem supports IPv4 and IPv6
+ * but not simultaneously on the same bearer.
+ */
+typedef enum {
+    NM_MODEM_IP_TYPE_UNKNOWN = 0x0,
+    NM_MODEM_IP_TYPE_IPV4    = 0x1,
+    NM_MODEM_IP_TYPE_IPV6    = 0x2,
+    NM_MODEM_IP_TYPE_IPV4V6  = 0x4
+} NMModemIPType;
+
+typedef enum { /*< underscore_name=nm_modem_state >*/
+               NM_MODEM_STATE_UNKNOWN       = 0,
+               NM_MODEM_STATE_FAILED        = 1,
+               NM_MODEM_STATE_INITIALIZING  = 2,
+               NM_MODEM_STATE_LOCKED        = 3,
+               NM_MODEM_STATE_DISABLED      = 4,
+               NM_MODEM_STATE_DISABLING     = 5,
+               NM_MODEM_STATE_ENABLING      = 6,
+               NM_MODEM_STATE_ENABLED       = 7,
+               NM_MODEM_STATE_SEARCHING     = 8,
+               NM_MODEM_STATE_REGISTERED    = 9,
+               NM_MODEM_STATE_DISCONNECTING = 10,
+               NM_MODEM_STATE_CONNECTING    = 11,
+               NM_MODEM_STATE_CONNECTED     = 12,
+
+               _NM_MODEM_STATE_LAST0,
+               _NM_MODEM_STATE_LAST = _NM_MODEM_STATE_LAST0 - 1,
+} NMModemState;
+
+struct _NMModemPrivate;
+
+struct _NMModem {
+    GObject                 parent;
+    struct _NMModemPrivate *_priv;
+};
+
+typedef struct _NMModem NMModem;
+
+typedef void (*_NMModemDisconnectCallback)(NMModem *modem, GError *error, gpointer user_data);
+
+typedef struct {
+    GObjectClass parent;
+
+    void (*get_capabilities)(NMModem *                  self,
+                             NMDeviceModemCapabilities *modem_caps,
+                             NMDeviceModemCapabilities *current_caps);
+
+    gboolean (*get_user_pass)(NMModem *     modem,
+                              NMConnection *connection,
+                              const char ** user,
+                              const char ** pass);
+
+    gboolean (*check_connection_compatible_with_modem)(NMModem *     modem,
+                                                       NMConnection *connection,
+                                                       GError **     error);
+
+    gboolean (*complete_connection)(NMModem *            modem,
+                                    const char *         iface,
+                                    NMConnection *       connection,
+                                    NMConnection *const *existing_connections,
+                                    GError **            error);
+
+    NMActStageReturn (*modem_act_stage1_prepare)(NMModem *            modem,
+                                                 NMConnection *       connection,
+                                                 NMDeviceStateReason *out_failure_reason);
+
+    NMActStageReturn (*static_stage3_ip4_config_start)(NMModem *            self,
+                                                       NMActRequest *       req,
+                                                       NMDeviceStateReason *out_failure_reason);
+
+    /* Request the IP6 config; when the config returns the modem
+     * subclass should emit the ip6_config_result signal.
+     */
+    NMActStageReturn (*stage3_ip6_config_request)(NMModem *            self,
+                                                  NMDeviceStateReason *out_failure_reason);
+
+    void (*set_mm_enabled)(NMModem *self, gboolean enabled);
+
+    void (*disconnect)(NMModem *                  self,
+                       gboolean                   warn,
+                       GCancellable *             cancellable,
+                       _NMModemDisconnectCallback callback,
+                       gpointer                   user_data);
+
+    void (*deactivate_cleanup)(NMModem *self, NMDevice *device, gboolean stop_ppp_manager);
+
+    gboolean (*owns_port)(NMModem *self, const char *iface);
+} NMModemClass;
+
+GType nm_modem_get_type(void);
+
+gboolean nm_modem_is_claimed(NMModem *modem);
+NMModem *nm_modem_claim(NMModem *modem);
+void     nm_modem_unclaim(NMModem *modem);
+
+const char *nm_modem_get_path(NMModem *modem);
+const char *nm_modem_get_uid(NMModem *modem);
+const char *nm_modem_get_control_port(NMModem *modem);
+int         nm_modem_get_ip_ifindex(NMModem *modem);
+const char *nm_modem_get_driver(NMModem *modem);
+const char *nm_modem_get_device_id(NMModem *modem);
+const char *nm_modem_get_sim_id(NMModem *modem);
+const char *nm_modem_get_sim_operator_id(NMModem *modem);
+gboolean    nm_modem_get_iid(NMModem *modem, NMUtilsIPv6IfaceId *out_iid);
+const char *nm_modem_get_operator_code(NMModem *modem);
+const char *nm_modem_get_apn(NMModem *modem);
+
+gboolean nm_modem_set_data_port(NMModem *       self,
+                                NMPlatform *    platform,
+                                const char *    data_port,
+                                NMModemIPMethod ip4_method,
+                                NMModemIPMethod ip6_method,
+                                guint           timeout,
+                                GError **       error);
+
+gboolean nm_modem_owns_port(NMModem *modem, const char *iface);
+
+void nm_modem_get_capabilities(NMModem *                  self,
+                               NMDeviceModemCapabilities *modem_caps,
+                               NMDeviceModemCapabilities *current_caps);
+
+gboolean
+nm_modem_check_connection_compatible(NMModem *self, NMConnection *connection, GError **error);
+
+gboolean nm_modem_complete_connection(NMModem *            self,
+                                      const char *         iface,
+                                      NMConnection *       connection,
+                                      NMConnection *const *existing_connections,
+                                      GError **            error);
+
+void nm_modem_get_route_parameters(NMModem *self,
+                                   guint32 *out_ip4_route_table,
+                                   guint32 *out_ip4_route_metric,
+                                   guint32 *out_ip6_route_table,
+                                   guint32 *out_ip6_route_metric);
+
+void nm_modem_set_route_parameters(NMModem *self,
+                                   guint32  ip4_route_table,
+                                   guint32  ip4_route_metric,
+                                   guint32  ip6_route_table,
+                                   guint32  ip6_route_metric);
+
+void nm_modem_set_route_parameters_from_device(NMModem *modem, NMDevice *device);
+
+NMActStageReturn nm_modem_act_stage1_prepare(NMModem *            modem,
+                                             NMActRequest *       req,
+                                             NMDeviceStateReason *out_failure_reason);
+
+void nm_modem_act_stage2_config(NMModem *modem);
+
+NMActStageReturn nm_modem_stage3_ip4_config_start(NMModem *            modem,
+                                                  NMDevice *           device,
+                                                  NMDeviceClass *      device_class,
+                                                  NMDeviceStateReason *out_failure_reason);
+
+NMActStageReturn nm_modem_stage3_ip6_config_start(NMModem *            modem,
+                                                  NMDevice *           device,
+                                                  NMDeviceStateReason *out_failure_reason);
+
+void nm_modem_ip4_pre_commit(NMModem *modem, NMDevice *device, NMIP4Config *config);
+
+void nm_modem_get_secrets(NMModem *   modem,
+                          const char *setting_name,
+                          gboolean    request_new,
+                          const char *hint);
+
+void nm_modem_deactivate(NMModem *modem, NMDevice *device);
+
+typedef void (*NMModemDeactivateCallback)(NMModem *self, GError *error, gpointer user_data);
+
+void nm_modem_deactivate_async(NMModem *                 self,
+                               NMDevice *                device,
+                               GCancellable *            cancellable,
+                               NMModemDeactivateCallback callback,
+                               gpointer                  user_data);
+
+void
+nm_modem_device_state_changed(NMModem *modem, NMDeviceState new_state, NMDeviceState old_state);
+
+void nm_modem_set_mm_enabled(NMModem *self, gboolean enabled);
+
+NMModemState nm_modem_get_state(NMModem *self);
+void         nm_modem_set_state(NMModem *self, NMModemState new_state, const char *reason);
+void         nm_modem_set_prev_state(NMModem *self, const char *reason);
+const char * nm_modem_state_to_string(NMModemState state);
+
+NMModemIPType nm_modem_get_supported_ip_types(NMModem *self);
+
+/* For the modem-manager only */
+void nm_modem_emit_removed(NMModem *self);
+
+void nm_modem_emit_prepare_result(NMModem *self, gboolean success, NMDeviceStateReason reason);
+
+void nm_modem_emit_ppp_failed(NMModem *self, NMDeviceStateReason reason);
+
+GArray *nm_modem_get_connection_ip_type(NMModem *self, NMConnection *connection, GError **error);
+
+/* For subclasses */
+void nm_modem_emit_ip6_config_result(NMModem *self, NMIP6Config *config, GError *error);
+
+const char *nm_modem_ip_type_to_string(NMModemIPType ip_type);
+
+guint32
+nm_modem_get_configured_mtu(NMDevice *self, NMDeviceMtuSource *out_source, gboolean *out_force);
+
+void _nm_modem_set_operator_code(NMModem *self, const char *operator_code);
+void _nm_modem_set_apn(NMModem *self, const char *apn);
+
+#endif /* __NETWORKMANAGER_MODEM_H__ */
diff --git a/src/core/devices/wwan/nm-service-providers.c b/src/core/devices/wwan/nm-service-providers.c
new file mode 100644
index 00000000..89add6c5
--- /dev/null
+++ b/src/core/devices/wwan/nm-service-providers.c
@@ -0,0 +1,455 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2009 Novell, Inc.
+ * Author: Tambet Ingo (tambet@gmail.com).
+ * Copyright (C) 2009 - 2019 Red Hat, Inc.
+ * Copyright (C) 2012 Lanedo GmbH
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-service-providers.h"
+
+typedef enum {
+    PARSER_TOPLEVEL = 0,
+    PARSER_COUNTRY,
+    PARSER_PROVIDER,
+    PARSER_METHOD_GSM,
+    PARSER_METHOD_GSM_APN,
+    PARSER_METHOD_CDMA,
+    PARSER_DONE,
+    PARSER_ERROR
+} ParseContextState;
+
+typedef struct {
+    char *                           mccmnc;
+    NMServiceProvidersGsmApnCallback callback;
+    gpointer                         user_data;
+    GCancellable *                   cancellable;
+    GMarkupParseContext *            ctx;
+    char                             buffer[4096];
+
+    char *            text_buffer;
+    ParseContextState state;
+
+    gboolean mccmnc_matched;
+    gboolean found_internet_apn;
+    char *   apn;
+    char *   username;
+    char *   password;
+    char *   gateway;
+    char *   auth_method;
+    GSList * dns;
+} ParseContext;
+
+/*****************************************************************************/
+
+static void
+parser_toplevel_start(ParseContext *parse_context,
+                      const char *  name,
+                      const char ** attribute_names,
+                      const char ** attribute_values)
+{
+    int i;
+
+    if (strcmp(name, "serviceproviders") == 0) {
+        for (i = 0; attribute_names && attribute_names[i]; i++) {
+            if (strcmp(attribute_names[i], "format") == 0) {
+                if (strcmp(attribute_values[i], "2.0")) {
+                    g_warning("%s: mobile broadband provider database format '%s'"
+                              " not supported.",
+                              __func__,
+                              attribute_values[i]);
+                    parse_context->state = PARSER_ERROR;
+                    break;
+                }
+            }
+        }
+    } else if (strcmp(name, "country") == 0) {
+        parse_context->state = PARSER_COUNTRY;
+    }
+}
+
+static void
+parser_country_start(ParseContext *parse_context,
+                     const char *  name,
+                     const char ** attribute_names,
+                     const char ** attribute_values)
+{
+    if (strcmp(name, "provider") == 0)
+        parse_context->state = PARSER_PROVIDER;
+}
+
+static void
+parser_provider_start(ParseContext *parse_context,
+                      const char *  name,
+                      const char ** attribute_names,
+                      const char ** attribute_values)
+{
+    parse_context->mccmnc_matched = FALSE;
+    if (strcmp(name, "gsm") == 0)
+        parse_context->state = PARSER_METHOD_GSM;
+    else if (strcmp(name, "cdma") == 0)
+        parse_context->state = PARSER_METHOD_CDMA;
+}
+
+static void
+parser_gsm_start(ParseContext *parse_context,
+                 const char *  name,
+                 const char ** attribute_names,
+                 const char ** attribute_values)
+{
+    int i;
+
+    if (strcmp(name, "network-id") == 0) {
+        const char *mcc = NULL, *mnc = NULL;
+
+        for (i = 0; attribute_names && attribute_names[i]; i++) {
+            if (strcmp(attribute_names[i], "mcc") == 0)
+                mcc = attribute_values[i];
+            else if (strcmp(attribute_names[i], "mnc") == 0)
+                mnc = attribute_values[i];
+            if (mcc && strlen(mcc) && mnc && strlen(mnc)) {
+                char *mccmnc = g_strdup_printf("%s%s", mcc, mnc);
+
+                if (strcmp(mccmnc, parse_context->mccmnc) == 0)
+                    parse_context->mccmnc_matched = TRUE;
+                g_free(mccmnc);
+                break;
+            }
+        }
+    } else if (strcmp(name, "apn") == 0) {
+        parse_context->found_internet_apn = FALSE;
+        nm_clear_g_free(&parse_context->apn);
+        nm_clear_g_free(&parse_context->username);
+        nm_clear_g_free(&parse_context->password);
+        nm_clear_g_free(&parse_context->gateway);
+        nm_clear_g_free(&parse_context->auth_method);
+        g_slist_free_full(parse_context->dns, g_free);
+        parse_context->dns = NULL;
+
+        for (i = 0; attribute_names && attribute_names[i]; i++) {
+            if (strcmp(attribute_names[i], "value") == 0) {
+                parse_context->state = PARSER_METHOD_GSM_APN;
+                parse_context->apn   = g_strstrip(g_strdup(attribute_values[i]));
+                break;
+            }
+        }
+    }
+}
+
+static void
+parser_gsm_apn_start(ParseContext *parse_context,
+                     const char *  name,
+                     const char ** attribute_names,
+                     const char ** attribute_values)
+{
+    int i;
+
+    if (strcmp(name, "usage") == 0) {
+        for (i = 0; attribute_names && attribute_names[i]; i++) {
+            if ((strcmp(attribute_names[i], "type") == 0)
+                && (strcmp(attribute_values[i], "internet") == 0)) {
+                parse_context->found_internet_apn = TRUE;
+                break;
+            }
+        }
+    } else if (strcmp(name, "authentication") == 0) {
+        for (i = 0; attribute_names && attribute_names[i]; i++) {
+            if (strcmp(attribute_names[i], "method") == 0) {
+                nm_clear_g_free(&parse_context->auth_method);
+                parse_context->auth_method = g_strstrip(g_strdup(attribute_values[i]));
+                break;
+            }
+        }
+    }
+}
+
+static void
+parser_start_element(GMarkupParseContext *context,
+                     const char *         element_name,
+                     const char **        attribute_names,
+                     const char **        attribute_values,
+                     gpointer             user_data,
+                     GError **            error)
+{
+    ParseContext *parse_context = user_data;
+
+    nm_clear_g_free(&parse_context->text_buffer);
+
+    switch (parse_context->state) {
+    case PARSER_TOPLEVEL:
+        parser_toplevel_start(parse_context, element_name, attribute_names, attribute_values);
+        break;
+    case PARSER_COUNTRY:
+        parser_country_start(parse_context, element_name, attribute_names, attribute_values);
+        break;
+    case PARSER_PROVIDER:
+        parser_provider_start(parse_context, element_name, attribute_names, attribute_values);
+        break;
+    case PARSER_METHOD_GSM:
+        parser_gsm_start(parse_context, element_name, attribute_names, attribute_values);
+        break;
+    case PARSER_METHOD_GSM_APN:
+        parser_gsm_apn_start(parse_context, element_name, attribute_names, attribute_values);
+        break;
+    case PARSER_METHOD_CDMA:
+        break;
+    case PARSER_ERROR:
+        break;
+    case PARSER_DONE:
+        break;
+    }
+}
+
+static void
+parser_country_end(ParseContext *parse_context, const char *name)
+{
+    if (strcmp(name, "country") == 0) {
+        nm_clear_g_free(&parse_context->text_buffer);
+        parse_context->state = PARSER_TOPLEVEL;
+    }
+}
+
+static void
+parser_provider_end(ParseContext *parse_context, const char *name)
+{
+    if (strcmp(name, "provider") == 0) {
+        nm_clear_g_free(&parse_context->text_buffer);
+        parse_context->state = PARSER_COUNTRY;
+    }
+}
+
+static void
+parser_gsm_end(ParseContext *parse_context, const char *name)
+{
+    if (strcmp(name, "gsm") == 0) {
+        nm_clear_g_free(&parse_context->text_buffer);
+        parse_context->state = PARSER_PROVIDER;
+    }
+}
+
+static void
+parser_gsm_apn_end(ParseContext *parse_context, const char *name)
+{
+    if (strcmp(name, "username") == 0) {
+        nm_clear_g_free(&parse_context->username);
+        parse_context->username = g_steal_pointer(&parse_context->text_buffer);
+    } else if (strcmp(name, "password") == 0) {
+        nm_clear_g_free(&parse_context->password);
+        parse_context->password = g_steal_pointer(&parse_context->text_buffer);
+    } else if (strcmp(name, "dns") == 0) {
+        parse_context->dns =
+            g_slist_prepend(parse_context->dns, g_steal_pointer(&parse_context->text_buffer));
+    } else if (strcmp(name, "gateway") == 0) {
+        nm_clear_g_free(&parse_context->gateway);
+        parse_context->gateway = g_steal_pointer(&parse_context->text_buffer);
+    } else if (strcmp(name, "apn") == 0) {
+        nm_clear_g_free(&parse_context->text_buffer);
+
+        if (parse_context->mccmnc_matched && parse_context->found_internet_apn)
+            parse_context->state = PARSER_DONE;
+        else
+            parse_context->state = PARSER_METHOD_GSM;
+    }
+}
+
+static void
+parser_cdma_end(ParseContext *parse_context, const char *name)
+{
+    if (strcmp(name, "cdma") == 0) {
+        nm_clear_g_free(&parse_context->text_buffer);
+        parse_context->state = PARSER_PROVIDER;
+    }
+}
+
+static void
+parser_end_element(GMarkupParseContext *context,
+                   const char *         element_name,
+                   gpointer             user_data,
+                   GError **            error)
+{
+    ParseContext *parse_context = user_data;
+
+    switch (parse_context->state) {
+    case PARSER_TOPLEVEL:
+        break;
+    case PARSER_COUNTRY:
+        parser_country_end(parse_context, element_name);
+        break;
+    case PARSER_PROVIDER:
+        parser_provider_end(parse_context, element_name);
+        break;
+    case PARSER_METHOD_GSM:
+        parser_gsm_end(parse_context, element_name);
+        break;
+    case PARSER_METHOD_GSM_APN:
+        parser_gsm_apn_end(parse_context, element_name);
+        break;
+    case PARSER_METHOD_CDMA:
+        parser_cdma_end(parse_context, element_name);
+        break;
+    case PARSER_ERROR:
+        break;
+    case PARSER_DONE:
+        break;
+    }
+}
+
+static void
+parser_text(GMarkupParseContext *context,
+            const char *         text,
+            gsize                text_len,
+            gpointer             user_data,
+            GError **            error)
+{
+    ParseContext *parse_context = user_data;
+
+    g_free(parse_context->text_buffer);
+    parse_context->text_buffer = g_strdup(text);
+}
+
+static const GMarkupParser parser = {
+    .start_element = parser_start_element,
+    .end_element   = parser_end_element,
+    .text          = parser_text,
+    .passthrough   = NULL,
+    .error         = NULL,
+};
+
+/*****************************************************************************/
+
+static void
+finish_parse_context(ParseContext *parse_context, GError *error)
+{
+    if (parse_context->callback) {
+        if (error) {
+            parse_context
+                ->callback(NULL, NULL, NULL, NULL, NULL, NULL, error, parse_context->user_data);
+        } else {
+            parse_context->callback(parse_context->apn,
+                                    parse_context->username,
+                                    parse_context->password,
+                                    parse_context->gateway,
+                                    parse_context->auth_method,
+                                    parse_context->dns,
+                                    error,
+                                    parse_context->user_data);
+        }
+    }
+
+    g_free(parse_context->mccmnc);
+    g_markup_parse_context_free(parse_context->ctx);
+
+    g_free(parse_context->text_buffer);
+    g_free(parse_context->apn);
+    g_free(parse_context->username);
+    g_free(parse_context->password);
+    g_free(parse_context->gateway);
+    g_free(parse_context->auth_method);
+    g_slist_free_full(parse_context->dns, g_free);
+
+    g_slice_free(ParseContext, parse_context);
+}
+
+static void read_next_chunk(GInputStream *stream, ParseContext *parse_context);
+
+static void
+stream_read_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    GInputStream *stream        = G_INPUT_STREAM(source_object);
+    ParseContext *parse_context = user_data;
+    gssize        len;
+    GError *      error = NULL;
+
+    len = g_input_stream_read_finish(stream, res, &error);
+    if (len == -1) {
+        g_prefix_error(&error, "Error reading service provider database: ");
+        finish_parse_context(parse_context, error);
+        g_clear_error(&error);
+        return;
+    }
+
+    if (len == 0) {
+        g_set_error(&error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    "Operator ID '%s' not found in service provider database",
+                    parse_context->mccmnc);
+        finish_parse_context(parse_context, error);
+        g_clear_error(&error);
+        return;
+    }
+
+    if (!g_markup_parse_context_parse(parse_context->ctx, parse_context->buffer, len, &error)) {
+        g_prefix_error(&error, "Error parsing service provider database: ");
+        finish_parse_context(parse_context, error);
+        g_clear_error(&error);
+        return;
+    }
+
+    if (parse_context->state == PARSER_DONE) {
+        finish_parse_context(parse_context, NULL);
+        return;
+    }
+
+    read_next_chunk(stream, parse_context);
+}
+
+static void
+read_next_chunk(GInputStream *stream, ParseContext *parse_context)
+{
+    g_input_stream_read_async(stream,
+                              parse_context->buffer,
+                              sizeof(parse_context->buffer),
+                              G_PRIORITY_DEFAULT,
+                              parse_context->cancellable,
+                              stream_read_cb,
+                              parse_context);
+}
+
+static void
+file_read_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    GFile *           file          = G_FILE(source_object);
+    ParseContext *    parse_context = user_data;
+    GFileInputStream *stream;
+    gs_free_error GError *error = NULL;
+
+    stream = g_file_read_finish(file, res, &error);
+    if (!stream) {
+        g_prefix_error(&error, "Error opening service provider database: ");
+        finish_parse_context(parse_context, error);
+        return;
+    }
+
+    read_next_chunk(G_INPUT_STREAM(stream), parse_context);
+
+    g_object_unref(stream);
+}
+
+/*****************************************************************************/
+
+void
+nm_service_providers_find_gsm_apn(const char *                     service_providers,
+                                  const char *                     mccmnc,
+                                  GCancellable *                   cancellable,
+                                  NMServiceProvidersGsmApnCallback callback,
+                                  gpointer                         user_data)
+{
+    GFile *       file;
+    ParseContext *parse_context;
+
+    parse_context              = g_slice_new0(ParseContext);
+    parse_context->mccmnc      = g_strdup(mccmnc);
+    parse_context->cancellable = cancellable;
+    parse_context->callback    = callback;
+    parse_context->user_data   = user_data;
+    parse_context->ctx         = g_markup_parse_context_new(&parser, 0, parse_context, NULL);
+
+    file = g_file_new_for_path(service_providers);
+
+    g_file_read_async(file, G_PRIORITY_DEFAULT, cancellable, file_read_cb, parse_context);
+
+    g_object_unref(file);
+}
diff --git a/src/core/devices/wwan/nm-service-providers.h b/src/core/devices/wwan/nm-service-providers.h
new file mode 100644
index 00000000..959f660a
--- /dev/null
+++ b/src/core/devices/wwan/nm-service-providers.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_SERVICE_PROVIDERS_H__
+#define __NETWORKMANAGER_SERVICE_PROVIDERS_H__
+
+typedef void (*NMServiceProvidersGsmApnCallback)(const char *  apn,
+                                                 const char *  username,
+                                                 const char *  password,
+                                                 const char *  gateway,
+                                                 const char *  auth_method,
+                                                 const GSList *dns,
+                                                 GError *      error,
+                                                 gpointer      user_data);
+
+void nm_service_providers_find_gsm_apn(const char *                     service_providers,
+                                       const char *                     mccmnc,
+                                       GCancellable *                   cancellable,
+                                       NMServiceProvidersGsmApnCallback callback,
+                                       gpointer                         user_data);
+
+#endif /* __NETWORKMANAGER_SERVICE_PROVIDERS_H__ */
diff --git a/src/core/devices/wwan/nm-wwan-factory.c b/src/core/devices/wwan/nm-wwan-factory.c
new file mode 100644
index 00000000..5d2ce2b3
--- /dev/null
+++ b/src/core/devices/wwan/nm-wwan-factory.c
@@ -0,0 +1,146 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include <gmodule.h>
+
+#include "devices/nm-device-factory.h"
+#include "nm-setting-gsm.h"
+#include "nm-setting-cdma.h"
+#include "nm-modem-manager.h"
+#include "nm-device-modem.h"
+#include "platform/nm-platform.h"
+
+/*****************************************************************************/
+
+#define NM_TYPE_WWAN_FACTORY (nm_wwan_factory_get_type())
+#define NM_WWAN_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WWAN_FACTORY, NMWwanFactory))
+#define NM_WWAN_FACTORY_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WWAN_FACTORY, NMWwanFactoryClass))
+#define NM_IS_WWAN_FACTORY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WWAN_FACTORY))
+#define NM_IS_WWAN_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_WWAN_FACTORY))
+#define NM_WWAN_FACTORY_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_WWAN_FACTORY, NMWwanFactoryClass))
+
+typedef struct {
+    NMModemManager *mm;
+} NMWwanFactoryPrivate;
+
+typedef struct {
+    NMDeviceFactory      parent;
+    NMWwanFactoryPrivate _priv;
+} NMWwanFactory;
+
+typedef struct {
+    NMDeviceFactoryClass parent;
+} NMWwanFactoryClass;
+
+static GType nm_wwan_factory_get_type(void);
+
+G_DEFINE_TYPE(NMWwanFactory, nm_wwan_factory, NM_TYPE_DEVICE_FACTORY)
+
+#define NM_WWAN_FACTORY_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMWwanFactory, NM_IS_WWAN_FACTORY)
+
+/*****************************************************************************/
+
+NM_DEVICE_FACTORY_DECLARE_TYPES(NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(
+    NM_LINK_TYPE_WWAN_NET) NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_GSM_SETTING_NAME,
+                                                                   NM_SETTING_CDMA_SETTING_NAME))
+
+G_MODULE_EXPORT NMDeviceFactory *
+                nm_device_factory_create(GError **error)
+{
+    return g_object_new(NM_TYPE_WWAN_FACTORY, NULL);
+}
+
+/*****************************************************************************/
+
+static void
+modem_added_cb(NMModemManager *manager, NMModem *modem, gpointer user_data)
+{
+    NMWwanFactory * self             = NM_WWAN_FACTORY(user_data);
+    gs_unref_object NMDevice *device = NULL;
+    const char *              driver;
+
+    if (nm_modem_is_claimed(modem))
+        return;
+
+    driver = nm_modem_get_driver(modem);
+
+    /* If it was a Bluetooth modem and no bluetooth device claimed it, ignore
+     * it.  The rfcomm port (and thus the modem) gets created automatically
+     * by the Bluetooth code during the connection process.
+     */
+    if (driver && strstr(driver, "bluetooth")) {
+        nm_log_dbg(
+            LOGD_MB,
+            "WWAN factory ignores bluetooth modem '%s' which should be handled by bluetooth plugin",
+            nm_modem_get_control_port(modem));
+        return;
+    }
+
+    /* Make the new modem device */
+    device = nm_device_modem_new(modem);
+    g_signal_emit_by_name(self, NM_DEVICE_FACTORY_DEVICE_ADDED, device);
+}
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    g_return_val_if_fail(plink, NULL);
+    g_return_val_if_fail(plink->type == NM_LINK_TYPE_WWAN_NET, NULL);
+    *out_ignore = TRUE;
+    return NULL;
+}
+
+static void
+start(NMDeviceFactory *factory)
+{
+    NMWwanFactory *       self = NM_WWAN_FACTORY(factory);
+    NMWwanFactoryPrivate *priv = NM_WWAN_FACTORY_GET_PRIVATE(self);
+
+    priv->mm = g_object_ref(nm_modem_manager_get());
+
+    g_signal_connect(priv->mm, NM_MODEM_MANAGER_MODEM_ADDED, G_CALLBACK(modem_added_cb), self);
+}
+
+/*****************************************************************************/
+
+static void
+nm_wwan_factory_init(NMWwanFactory *self)
+{}
+
+static void
+dispose(GObject *object)
+{
+    NMWwanFactory *       self = NM_WWAN_FACTORY(object);
+    NMWwanFactoryPrivate *priv = NM_WWAN_FACTORY_GET_PRIVATE(self);
+
+    if (priv->mm)
+        g_signal_handlers_disconnect_by_func(priv->mm, modem_added_cb, self);
+    g_clear_object(&priv->mm);
+
+    /* Chain up to the parent class */
+    G_OBJECT_CLASS(nm_wwan_factory_parent_class)->dispose(object);
+}
+
+static void
+nm_wwan_factory_class_init(NMWwanFactoryClass *klass)
+{
+    GObjectClass *        object_class  = G_OBJECT_CLASS(klass);
+    NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS(klass);
+
+    object_class->dispose = dispose;
+
+    factory_class->get_supported_types = get_supported_types;
+    factory_class->create_device       = create_device;
+    factory_class->start               = start;
+}
diff --git a/src/core/devices/wwan/tests/test-service-providers.c b/src/core/devices/wwan/tests/test-service-providers.c
new file mode 100644
index 00000000..f95cccf8
--- /dev/null
+++ b/src/core/devices/wwan/tests/test-service-providers.c
@@ -0,0 +1,130 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019 Red Hat
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-service-providers.h"
+
+#include "nm-test-utils-core.h"
+
+static void
+test_positive_cb(const char *  apn,
+                 const char *  username,
+                 const char *  password,
+                 const char *  gateway,
+                 const char *  auth_method,
+                 const GSList *dns,
+                 GError *      error,
+                 gpointer      user_data)
+{
+    GMainLoop *loop = user_data;
+
+    g_main_loop_quit(loop);
+    g_assert_no_error(error);
+    g_assert_cmpstr(apn, ==, "gprs.example.com");
+    g_assert_cmpstr(username, ==, "praise");
+    g_assert_cmpstr(password, ==, "santa");
+    g_assert_cmpstr(gateway, ==, "192.0.2.3");
+    g_assert_cmpstr(auth_method, ==, "pap");
+
+    g_assert_nonnull(dns);
+    g_assert_cmpstr(dns->data, ==, "192.0.2.2");
+    dns = dns->next;
+    g_assert_nonnull(dns);
+    g_assert_cmpstr(dns->data, ==, "192.0.2.1");
+    g_assert_null(dns->next);
+}
+
+static void
+test_positive(void)
+{
+    GMainLoop *loop = g_main_loop_new(NULL, FALSE);
+
+    nm_service_providers_find_gsm_apn(NM_BUILD_SRCDIR
+                                      "/src/core/devices/wwan/tests/test-service-providers.xml",
+                                      "13337",
+                                      NULL,
+                                      test_positive_cb,
+                                      loop);
+    g_main_loop_run(loop);
+    g_main_loop_unref(loop);
+}
+
+/*****************************************************************************/
+
+static void
+test_negative_cb(const char *  apn,
+                 const char *  username,
+                 const char *  password,
+                 const char *  gateway,
+                 const char *  auth_method,
+                 const GSList *dns,
+                 GError *      error,
+                 gpointer      user_data)
+{
+    GMainLoop *loop = user_data;
+
+    g_main_loop_quit(loop);
+    g_assert_error(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN);
+}
+
+static void
+test_negative(void)
+{
+    GMainLoop *loop = g_main_loop_new(NULL, FALSE);
+
+    nm_service_providers_find_gsm_apn(NM_BUILD_SRCDIR
+                                      "/src/core/devices/wwan/tests/test-service-providers.xml",
+                                      "78130",
+                                      NULL,
+                                      test_negative_cb,
+                                      loop);
+    g_main_loop_run(loop);
+    g_main_loop_unref(loop);
+}
+
+/*****************************************************************************/
+
+static void
+test_nonexistent_cb(const char *  apn,
+                    const char *  username,
+                    const char *  password,
+                    const char *  gateway,
+                    const char *  auth_method,
+                    const GSList *dns,
+                    GError *      error,
+                    gpointer      user_data)
+{
+    GMainLoop *loop = user_data;
+
+    g_main_loop_quit(loop);
+    g_assert_error(error, G_IO_ERROR, G_IO_ERROR_AGAIN);
+}
+
+static void
+test_nonexistent(void)
+{
+    GMainLoop *loop = g_main_loop_new(NULL, FALSE);
+
+    nm_service_providers_find_gsm_apn("nonexistent.xml", "13337", NULL, test_nonexistent_cb, loop);
+    g_main_loop_run(loop);
+    g_main_loop_unref(loop);
+}
+
+/*****************************************************************************/
+
+NMTST_DEFINE();
+
+int
+main(int argc, char **argv)
+{
+    nmtst_init_assert_logging(&argc, &argv, "INFO", "DEFAULT");
+
+    g_test_add_func("/service-providers/positive", test_positive);
+    g_test_add_func("/service-providers/negative", test_negative);
+    g_test_add_func("/service-providers/nonexistent", test_nonexistent);
+
+    return g_test_run();
+}
diff --git a/src/core/devices/wwan/tests/test-service-providers.xml b/src/core/devices/wwan/tests/test-service-providers.xml
new file mode 100644
index 00000000..f0ca2deb
--- /dev/null
+++ b/src/core/devices/wwan/tests/test-service-providers.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding='utf-8'?>
+<!DOCTYPE serviceproviders SYSTEM "serviceproviders.2.dtd">
+
+<serviceproviders format="2.0">
+
+<country code="feh">
+	<provider>
+		<name>Sophia</name>
+		<gsm>
+			<network-id mcc="666" mnc="999"/>
+			<apn value="access.example.com">
+				<plan type="postpaid"/>
+				<usage type="internet"/>
+				<name>APN</name>
+				<dns>192.0.2.1</dns>
+				<dns>192.0.2.2</dns>
+			</apn>
+		</gsm>
+	</provider>
+</country>
+
+<country code="meh">
+	<provider>
+		<name>Demiurge</name>
+		<gsm>
+			<network-id mcc="133" mnc="37"/>
+			<network-id mcc="133" mnc="666"/>
+			<apn value="mms">
+				<usage type="mms"/>
+				<name>Unsolicited Nudes MMS</name>
+				<username>mms</username>
+				<password>mms</password>
+				<mmsc>http://mms.example.com/</mmsc>
+				<mmsproxy>192.0.2.1:8080</mmsproxy>
+			</apn>
+			<apn value="gprs.example.com">
+				<plan type="postpaid"/>
+				<usage type="internet"/>
+				<name>GPRS</name>
+				<username>praise</username>
+				<password>santa</password>
+				<dns>192.0.2.1</dns>
+				<dns>192.0.2.2</dns>
+				<gateway>192.0.2.3</gateway>
+				<authentication method="pap"/>
+			</apn>
+			<apn value="second.example.com">
+				<plan type="postpaid"/>
+				<usage type="internet"/>
+				<name>Second</name>
+				<username>worship</username>
+				<password>doom</password>
+			</apn>
+		</gsm>
+	</provider>
+
+	<provider>
+		<name>Personal</name>
+		<gsm>
+			<network-id mcc="666" mnc="999"/>
+			<apn value="access.example.com">
+				<plan type="postpaid"/>
+				<usage type="internet"/>
+				<name>APN</name>
+				<dns>192.0.2.1</dns>
+				<dns>192.0.2.2</dns>
+			</apn>
+		</gsm>
+	</provider>
+</country>
+
+</serviceproviders>
+