about summary refs log tree commit diff
path: root/src/core/devices/wwan/nm-modem-broadband.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/devices/wwan/nm-modem-broadband.c')
-rw-r--r--src/core/devices/wwan/nm-modem-broadband.c1625
1 files changed, 1625 insertions, 0 deletions
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);
+}