summary refs log tree commit diff
path: root/src/devices/wwan
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2019-12-18 18:29:24 +0100
committerMichael Biebl <biebl@debian.org>2019-12-18 18:29:24 +0100
commit28028b26b3371756811e95d894f709f4b1207c00 (patch)
tree6fe7316fd743b51042db47601a8ef8814b3134ac /src/devices/wwan
parente22609983008e1a669196ad64ba3a59ae8c76e0d (diff)
New upstream version 1.22.0 upstream/1.22.0
Diffstat (limited to 'src/devices/wwan')
-rw-r--r--src/devices/wwan/libnm-wwan.ver6
-rw-r--r--src/devices/wwan/meson.build42
-rw-r--r--src/devices/wwan/nm-device-modem.c104
-rw-r--r--src/devices/wwan/nm-device-modem.h18
-rw-r--r--src/devices/wwan/nm-modem-broadband.c193
-rw-r--r--src/devices/wwan/nm-modem-broadband.h18
-rw-r--r--src/devices/wwan/nm-modem-manager.c32
-rw-r--r--src/devices/wwan/nm-modem-manager.h21
-rw-r--r--src/devices/wwan/nm-modem-ofono.c26
-rw-r--r--src/devices/wwan/nm-modem-ofono.h18
-rw-r--r--src/devices/wwan/nm-modem.c92
-rw-r--r--src/devices/wwan/nm-modem.h32
-rw-r--r--src/devices/wwan/nm-service-providers.c459
-rw-r--r--src/devices/wwan/nm-service-providers.h24
-rw-r--r--src/devices/wwan/nm-wwan-factory.c32
-rw-r--r--src/devices/wwan/tests/test-service-providers.c124
-rw-r--r--src/devices/wwan/tests/test-service-providers.xml73
17 files changed, 1018 insertions, 296 deletions
diff --git a/src/devices/wwan/libnm-wwan.ver b/src/devices/wwan/libnm-wwan.ver
index 7ccebcb5..c368a590 100644
--- a/src/devices/wwan/libnm-wwan.ver
+++ b/src/devices/wwan/libnm-wwan.ver
@@ -3,6 +3,7 @@ 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;
@@ -14,15 +15,17 @@ global:
 	nm_modem_get_device_id;
 	nm_modem_get_driver;
 	nm_modem_get_iid;
-	nm_modem_get_path;
 	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;
@@ -32,6 +35,7 @@ global:
 	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/devices/wwan/meson.build b/src/devices/wwan/meson.build
index 482dc205..ed6f8010 100644
--- a/src/devices/wwan/meson.build
+++ b/src/devices/wwan/meson.build
@@ -1,13 +1,15 @@
-sources = files(
+nm_service_providers_source = files('nm-service-providers.c')
+
+sources = nm_service_providers_source + files(
   'nm-modem-broadband.c',
   'nm-modem.c',
   'nm-modem-manager.c',
 )
 
 deps = [
+  daemon_nm_default_dep,
   libsystemd_dep,
   mm_glib_dep,
-  nm_dep,
 ]
 
 if enable_ofono
@@ -20,16 +22,17 @@ libnm_wwan = shared_module(
   'nm-wwan',
   sources: sources,
   dependencies: deps,
-  link_args: [
-    '-Wl,--version-script,@0@'.format(linker_script),
-  ],
+  c_args: daemon_c_flags,
+  link_args: '-Wl,--version-script,@0@'.format(linker_script),
   link_depends: linker_script,
   install: true,
   install_dir: nm_plugindir,
 )
 
+wwan_inc = include_directories('.')
+
 libnm_wwan_dep = declare_dependency(
-  include_directories: include_directories('.'),
+  include_directories: wwan_inc,
   link_with: libnm_wwan,
 )
 
@@ -50,6 +53,7 @@ libnm_device_plugin_wwan = shared_module(
   'nm-device-plugin-wwan',
   sources: sources,
   dependencies: deps,
+  c_args: daemon_c_flags,
   link_with: libnm_wwan,
   link_args: ldflags_linker_script_devices,
   link_depends: linker_script_devices,
@@ -66,11 +70,21 @@ run_target(
   depends: libnm_device_plugin_wwan,
 )
 
-# FIXME: check_so_symbols replacement
-'''
-check-local-devices-wwan: src/devices/wwan/libnm-device-plugin-wwan.la src/devices/wwan/libnm-wwan.la
-  $(srcdir)/tools/check-exports.sh $(builddir)/src/devices/wwan/.libs/libnm-device-plugin-wwan.so "$(srcdir)/linker-script-devices.ver"
-  $(call check_so_symbols,$(builddir)/src/devices/wwan/.libs/libnm-device-plugin-wwan.so)
-  $(srcdir)/tools/check-exports.sh $(builddir)/src/devices/wwan/.libs/libnm-wwan.so "$(srcdir)/src/devices/wwan/libnm-wwan.ver"
-  $(call check_so_symbols,$(builddir)/src/devices/wwan/.libs/libnm-wwan.so)
-'''
+if enable_tests
+  test_unit = 'test-service-providers'
+
+  exe = executable(
+    test_unit,
+    ['tests/' + test_unit + '.c'] + nm_service_providers_source,
+    include_directories: wwan_inc,
+    dependencies: libnetwork_manager_test_dep,
+    c_args: test_c_flags,
+  )
+
+  test(
+    'wwan/' + test_unit,
+    test_script,
+    timeout: default_test_timeout,
+    args: test_args + [exe.full_path()],
+  )
+endif
diff --git a/src/devices/wwan/nm-device-modem.c b/src/devices/wwan/nm-device-modem.c
index 042a6ca4..3e6ccc8f 100644
--- a/src/devices/wwan/nm-device-modem.c
+++ b/src/devices/wwan/nm-device-modem.c
@@ -1,19 +1,5 @@
-/* NetworkManager -- Network link manager
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- *
+// SPDX-License-Identifier: GPL-2.0+
+/*
  * Copyright (C) 2009 - 2019 Red Hat, Inc.
  */
 
@@ -48,10 +34,11 @@ typedef struct {
 	NMModem *modem;
 	NMDeviceModemCapabilities caps;
 	NMDeviceModemCapabilities current_caps;
-	gboolean rf_enabled;
 	char *device_id;
 	char *operator_code;
 	char *apn;
+	bool rf_enabled:1;
+	NMDeviceStageState stage1_state:3;
 } NMDeviceModemPrivate;
 
 struct _NMDeviceModem {
@@ -119,16 +106,17 @@ modem_prepare_result (NMModem *modem,
                       gpointer user_data)
 {
 	NMDeviceModem *self = NM_DEVICE_MODEM (user_data);
+	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE (self);
 	NMDevice *device = NM_DEVICE (self);
-	NMDeviceState state;
 	NMDeviceStateReason reason = i_reason;
 
-	state = nm_device_get_state (device);
-	g_return_if_fail (state == NM_DEVICE_STATE_PREPARE);
+	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)
-		nm_device_activate_schedule_stage2_device_config (device);
-	else {
+	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
@@ -164,7 +152,11 @@ modem_prepare_result (NMModem *modem,
 			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);
 }
 
 static void
@@ -187,16 +179,19 @@ 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);
-	} else {
-		/* Otherwise, on success for modem secrets we need to schedule stage1 again */
-		g_return_if_fail (nm_device_get_state (device) == NM_DEVICE_STATE_NEED_AUTH);
-		nm_device_activate_schedule_stage1_device_prepare (device);
+		return;
 	}
+
+	priv->stage1_state = NM_DEVICE_STAGE_STATE_INIT;
+	nm_device_activate_schedule_stage1_device_prepare (device);
 }
 
 static void
@@ -355,7 +350,7 @@ modem_state_cb (NMModem *modem,
 	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 ((NMDeviceModem *) device);
+	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE (device);
 	NMDeviceState dev_state = nm_device_get_state (device);
 
 	if (new_state <= NM_MODEM_STATE_DISABLING &&
@@ -409,7 +404,7 @@ modem_removed_cb (NMModem *modem, gpointer user_data)
 static gboolean
 owns_iface (NMDevice *device, const char *iface)
 {
-	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE ((NMDeviceModem *) device);
+	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE (device);
 
 	g_return_val_if_fail (priv->modem, FALSE);
 
@@ -447,7 +442,7 @@ get_generic_capabilities (NMDevice *device)
 static const char *
 get_type_description (NMDevice *device)
 {
-	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE ((NMDeviceModem *) device);
+	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE (device);
 
 	if (NM_FLAGS_HAS (priv->current_caps, NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS))
 		return "gsm";
@@ -464,7 +459,7 @@ check_connection_compatible (NMDevice *device, NMConnection *connection, GError
 	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 ((NMDeviceModem *) device)->modem,
+	if (!nm_modem_check_connection_compatible (NM_DEVICE_MODEM_GET_PRIVATE (device)->modem,
 	                                           connection,
 	                                           error ? &local : NULL)) {
 		if (error) {
@@ -530,7 +525,7 @@ complete_connection (NMDevice *device,
                      NMConnection *const*existing_connections,
                      GError **error)
 {
-	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE ((NMDeviceModem *) device);
+	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE (device);
 
 	return nm_modem_complete_connection (priv->modem,
 	                                     nm_device_get_iface (device),
@@ -542,7 +537,10 @@ complete_connection (NMDevice *device,
 static void
 deactivate (NMDevice *device)
 {
-	nm_modem_deactivate (NM_DEVICE_MODEM_GET_PRIVATE ((NMDeviceModem *) device)->modem, device);
+	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE (device);
+
+	nm_modem_deactivate (priv->modem, device);
+	priv->stage1_state = NM_DEVICE_STAGE_STATE_INIT;
 }
 
 /*****************************************************************************/
@@ -583,28 +581,32 @@ deactivate_async (NMDevice *self,
 static NMActStageReturn
 act_stage1_prepare (NMDevice *device, NMDeviceStateReason *out_failure_reason)
 {
-	NMActStageReturn ret;
+	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE (device);
 	NMActRequest *req;
 
-	ret = NM_DEVICE_CLASS (nm_device_modem_parent_class)->act_stage1_prepare (device, out_failure_reason);
-	if (ret != NM_ACT_STAGE_RETURN_SUCCESS)
-		return ret;
-
 	req = nm_device_get_act_request (device);
 	g_return_val_if_fail (req, NM_ACT_STAGE_RETURN_FAILURE);
 
-	return nm_modem_act_stage1_prepare (NM_DEVICE_MODEM_GET_PRIVATE ((NMDeviceModem *) device)->modem, req, out_failure_reason);
+	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)
 {
-	NMActRequest *req;
-
-	req = nm_device_get_act_request (device);
-	g_return_val_if_fail (req, NM_ACT_STAGE_RETURN_FAILURE);
+	nm_modem_act_stage2_config (NM_DEVICE_MODEM_GET_PRIVATE (device)->modem);
 
-	return nm_modem_act_stage2_config (NM_DEVICE_MODEM_GET_PRIVATE ((NMDeviceModem *) device)->modem, req, out_failure_reason);
+	return NM_ACT_STAGE_RETURN_SUCCESS;
 }
 
 static NMActStageReturn
@@ -632,7 +634,7 @@ act_stage3_ip_config_start (NMDevice *device,
 static void
 ip4_config_pre_commit (NMDevice *device, NMIP4Config *config)
 {
-	nm_modem_ip4_pre_commit (NM_DEVICE_MODEM_GET_PRIVATE ((NMDeviceModem *) device)->modem, device, config);
+	nm_modem_ip4_pre_commit (NM_DEVICE_MODEM_GET_PRIVATE (device)->modem, device, config);
 }
 
 static gboolean
@@ -654,7 +656,7 @@ get_ip_iface_identifier (NMDevice *device, NMUtilsIPv6IfaceId *out_iid)
 static gboolean
 get_enabled (NMDevice *device)
 {
-	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE ((NMDeviceModem *) 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);
@@ -710,7 +712,7 @@ set_modem (NMDeviceModem *self, NMModem *modem)
 
 	g_return_if_fail (modem != NULL);
 
-	priv->modem = g_object_ref (modem);
+	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);
@@ -745,7 +747,7 @@ static void
 get_property (GObject *object, guint prop_id,
               GValue *value, GParamSpec *pspec)
 {
-	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE ((NMDeviceModem *) object);
+	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE (object);
 
 	switch (prop_id) {
 	case PROP_MODEM:
@@ -776,7 +778,7 @@ static void
 set_property (GObject *object, guint prop_id,
               const GValue *value, GParamSpec *pspec)
 {
-	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE ((NMDeviceModem *) object);
+	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE (object);
 
 	switch (prop_id) {
 	case PROP_MODEM:
@@ -834,11 +836,11 @@ nm_device_modem_new (NMModem *modem)
 static void
 dispose (GObject *object)
 {
-	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE ((NMDeviceModem *) object);
+	NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE (object);
 
 	if (priv->modem) {
 		g_signal_handlers_disconnect_by_data (priv->modem, NM_DEVICE_MODEM (object));
-		g_clear_object (&priv->modem);
+		nm_clear_pointer (&priv->modem, nm_modem_unclaim);
 	}
 
 	g_clear_pointer (&priv->device_id, g_free);
diff --git a/src/devices/wwan/nm-device-modem.h b/src/devices/wwan/nm-device-modem.h
index 0a557f57..842096e2 100644
--- a/src/devices/wwan/nm-device-modem.h
+++ b/src/devices/wwan/nm-device-modem.h
@@ -1,19 +1,5 @@
-/* NetworkManager -- Network link manager
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- *
+// SPDX-License-Identifier: GPL-2.0+
+/*
  * Copyright (C) 2011 Red Hat, Inc.
  */
 
diff --git a/src/devices/wwan/nm-modem-broadband.c b/src/devices/wwan/nm-modem-broadband.c
index 216fedfe..a1ae8671 100644
--- a/src/devices/wwan/nm-modem-broadband.c
+++ b/src/devices/wwan/nm-modem-broadband.c
@@ -1,25 +1,12 @@
-/* NetworkManager -- Network link manager
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- *
+// SPDX-License-Identifier: GPL-2.0+
+/*
  * Copyright (C) 2012 Aleksander Morgado <aleksander@gnu.org>
  */
 
 #include "nm-default.h"
 
 #include "nm-modem-broadband.h"
+#include "nm-service-providers.h"
 
 #include <arpa/inet.h>
 #include <libmm-glib.h>
@@ -274,7 +261,10 @@ create_cdma_connect_properties (NMConnection *connection)
 }
 
 static MMSimpleConnectProperties *
-create_gsm_connect_properties (NMConnection *connection)
+create_gsm_connect_properties (NMConnection *connection,
+                               const char *apn,
+                               const char *username,
+                               const char *password)
 {
 	NMSettingGsm *setting;
 	NMSettingPpp *s_ppp;
@@ -282,11 +272,14 @@ create_gsm_connect_properties (NMConnection *connection)
 	const char *str;
 
 	setting = nm_connection_get_setting_gsm (connection);
+
 	properties = mm_simple_connect_properties_new ();
 
-	/* Blank APN ("") means the default subscription APN */
-	str = nm_setting_gsm_get_apn (setting);
-	mm_simple_connect_properties_set_apn (properties, str ?: "");
+	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)
@@ -296,14 +289,6 @@ create_gsm_connect_properties (NMConnection *connection)
 	if (str)
 		mm_simple_connect_properties_set_pin (properties, str);
 
-	str = nm_setting_gsm_get_username (setting);
-	if (str)
-		mm_simple_connect_properties_set_user (properties, str);
-
-	str = nm_setting_gsm_get_password (setting);
-	if (str)
-		mm_simple_connect_properties_set_password (properties, str);
-
 	/* Roaming */
 	if (nm_setting_gsm_get_home_only (setting))
 		mm_simple_connect_properties_set_allow_roaming (properties, FALSE);
@@ -356,16 +341,26 @@ connect_ready (MMModemSimple *simple_iface,
                GAsyncResult *res,
                NMModemBroadband *self)
 {
-	ConnectContext *ctx = self->_priv.ctx;
+	ConnectContext *ctx;
 	GError *error = NULL;
 	NMModemIPMethod ip4_method = NM_MODEM_IP_METHOD_UNKNOWN;
 	NMModemIPMethod ip6_method = NM_MODEM_IP_METHOD_UNKNOWN;
+	MMBearer *bearer;
 
-	self->_priv.bearer = mm_modem_simple_connect_finish (simple_iface, res, &error);
+	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) &&
@@ -457,6 +452,95 @@ send_pin_ready (MMSim *sim, GAsyncResult *result, NMModemBroadband *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;
@@ -500,22 +584,11 @@ connect_context_step (NMModemBroadband *self)
 		if (mm_modem_get_state (self->_priv.modem_iface) <= MM_MODEM_STATE_LOCKED)
 			break;
 
-		/* Create core connect properties based on the modem capabilities */
-		g_assert (!ctx->connect_properties);
-
-		if (MODEM_CAPS_3GPP (ctx->caps))
-			ctx->connect_properties = create_gsm_connect_properties (ctx->connection);
-		else if (MODEM_CAPS_3GPP2 (ctx->caps))
-			ctx->connect_properties = create_cdma_connect_properties (ctx->connection);
-		else {
-			_LOGW ("failed to connect '%s': not a mobile broadband modem",
-			       nm_connection_get_id (ctx->connection));
+		if (!try_create_connect_properties (self))
+			break;
 
-			nm_modem_emit_prepare_result (NM_MODEM (self), FALSE, NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED);
-			connect_context_clear (self);
+		if (!self->_priv.ctx)
 			break;
-		}
-		g_assert (ctx->connect_properties);
 
 		/* 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);
@@ -534,6 +607,9 @@ connect_context_step (NMModemBroadband *self)
 	}
 		/* fall through */
 	case CONNECT_STEP_CONNECT:
+		if (!ctx->connect_properties)
+			break;
+
 		if (ctx->ip_types_i < ctx->ip_types->len) {
 			NMModemIPType current;
 
@@ -546,7 +622,7 @@ connect_context_step (NMModemBroadband *self)
 			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_assert_not_reached ();
+				g_return_if_reached ();
 
 			_nm_modem_set_apn (NM_MODEM (self), mm_simple_connect_properties_get_apn (ctx->connect_properties));
 
@@ -556,7 +632,7 @@ connect_context_step (NMModemBroadband *self)
 
 			mm_modem_simple_connect (self->_priv.simple_iface,
 			                         ctx->connect_properties,
-			                         NULL,
+			                         ctx->cancellable,
 			                         (GAsyncReadyCallback) connect_ready,
 			                         self);
 			break;
@@ -586,9 +662,9 @@ connect_context_step (NMModemBroadband *self)
 }
 
 static NMActStageReturn
-act_stage1_prepare (NMModem *_self,
-                    NMConnection *connection,
-                    NMDeviceStateReason *out_failure_reason)
+modem_act_stage1_prepare (NMModem *_self,
+                          NMConnection *connection,
+                          NMDeviceStateReason *out_failure_reason)
 {
 	NMModemBroadband *self = NM_MODEM_BROADBAND (_self);
 
@@ -689,6 +765,9 @@ complete_connection (NMModem *modem,
 		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)) {
@@ -876,8 +955,8 @@ static_stage3_ip4_done (NMModemBroadband *self)
 	guint32 ip4_route_table, ip4_route_metric;
 	NMPlatformIP4Route *r;
 
-	g_assert (self->_priv.ipv4_config);
-	g_assert (self->_priv.bearer);
+	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;
 
@@ -908,7 +987,7 @@ static_stage3_ip4_done (NMModemBroadband *self)
 	}
 
 	data_port = mm_bearer_get_interface (self->_priv.bearer);
-	g_assert (data_port);
+	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));
 
@@ -983,7 +1062,7 @@ stage3_ip6_done (NMModemBroadband *self)
 	const char **dns;
 	guint i;
 
-	g_assert (self->_priv.ipv6_config);
+	g_return_val_if_fail (self->_priv.ipv6_config, FALSE);
 
 	self->_priv.idle_id_ip6 = 0;
 	memset (&address, 0, sizeof (address));
@@ -1015,7 +1094,8 @@ stage3_ip6_done (NMModemBroadband *self)
 	_LOGI ("IPv6 base configuration:");
 
 	data_port = mm_bearer_get_interface (self->_priv.bearer);
-	g_assert (data_port);
+	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));
 
@@ -1383,8 +1463,9 @@ set_property (GObject *object,
 		/* 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_assert (self->_priv.modem_iface != NULL);
+
 		g_signal_connect (self->_priv.modem_iface,
 		                  "state-changed",
 		                  G_CALLBACK (modem_state_changed),
@@ -1512,7 +1593,7 @@ nm_modem_broadband_class_init (NMModemBroadbandClass *klass)
 	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->act_stage1_prepare = act_stage1_prepare;
+	modem_class->modem_act_stage1_prepare = modem_act_stage1_prepare;
 	modem_class->owns_port = owns_port;
 
 	obj_properties[PROP_MODEM] =
diff --git a/src/devices/wwan/nm-modem-broadband.h b/src/devices/wwan/nm-modem-broadband.h
index 9404f0b9..43041e2b 100644
--- a/src/devices/wwan/nm-modem-broadband.h
+++ b/src/devices/wwan/nm-modem-broadband.h
@@ -1,19 +1,5 @@
-/* NetworkManager -- Network link manager
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- *
+// SPDX-License-Identifier: GPL-2.0+
+/*
  * Copyright (C) 2012 - Aleksander Morgado <aleksander@gnu.org>
  */
 
diff --git a/src/devices/wwan/nm-modem-manager.c b/src/devices/wwan/nm-modem-manager.c
index db2c0192..daccf68a 100644
--- a/src/devices/wwan/nm-modem-manager.c
+++ b/src/devices/wwan/nm-modem-manager.c
@@ -1,19 +1,5 @@
-/* NetworkManager -- Network link manager
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- *
+// SPDX-License-Identifier: GPL-2.0+
+/*
  * Copyright (C) 2009 - 2014 Red Hat, Inc.
  * Copyright (C) 2009 Novell, Inc.
  * Copyright (C) 2009 - 2013 Canonical Ltd.
@@ -149,6 +135,20 @@ remove_one_modem (gpointer key, gpointer value, gpointer user_data)
 
 /*****************************************************************************/
 
+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)
 {
diff --git a/src/devices/wwan/nm-modem-manager.h b/src/devices/wwan/nm-modem-manager.h
index 1a26fd9f..97a2126a 100644
--- a/src/devices/wwan/nm-modem-manager.h
+++ b/src/devices/wwan/nm-modem-manager.h
@@ -1,19 +1,5 @@
-/* NetworkManager -- Network link manager
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- *
+// SPDX-License-Identifier: GPL-2.0+
+/*
  * Copyright (C) 2009 - 2014 Red Hat, Inc.
  * Copyright (C) 2009 Novell, Inc.
  * Copyright (C) 2009 Canonical Ltd.
@@ -51,4 +37,7 @@ 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/devices/wwan/nm-modem-ofono.c b/src/devices/wwan/nm-modem-ofono.c
index 31111b62..b68cd7e8 100644
--- a/src/devices/wwan/nm-modem-ofono.c
+++ b/src/devices/wwan/nm-modem-ofono.c
@@ -1,19 +1,5 @@
-/* NetworkManager -- Network link manager
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- *
+// SPDX-License-Identifier: GPL-2.0+
+/*
  * Copyright (C) 2013 - 2016 Canonical Ltd.
  */
 
@@ -1114,9 +1100,9 @@ create_connect_properties (NMConnection *connection)
 }
 
 static NMActStageReturn
-act_stage1_prepare (NMModem *modem,
-                    NMConnection *connection,
-                    NMDeviceStateReason *out_failure_reason)
+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);
@@ -1308,6 +1294,6 @@ nm_modem_ofono_class_init (NMModemOfonoClass *klass)
 	modem_class->deactivate_cleanup = deactivate_cleanup;
 	modem_class->check_connection_compatible_with_modem = check_connection_compatible_with_modem;
 
-	modem_class->act_stage1_prepare = act_stage1_prepare;
+	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/devices/wwan/nm-modem-ofono.h b/src/devices/wwan/nm-modem-ofono.h
index 1dcd79b0..4ff2fbdd 100644
--- a/src/devices/wwan/nm-modem-ofono.h
+++ b/src/devices/wwan/nm-modem-ofono.h
@@ -1,19 +1,5 @@
-/* NetworkManager -- Network link manager
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- *
+// SPDX-License-Identifier: GPL-2.0+
+/*
  * Copyright (C) 2013 - Canonical Ltd.
  */
 
diff --git a/src/devices/wwan/nm-modem.c b/src/devices/wwan/nm-modem.c
index fb2316d5..ad6449b1 100644
--- a/src/devices/wwan/nm-modem.c
+++ b/src/devices/wwan/nm-modem.c
@@ -1,19 +1,5 @@
-/* NetworkManager -- Network link manager
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- *
+// SPDX-License-Identifier: GPL-2.0+
+/*
  * Copyright (C) 2009 - 2014 Red Hat, Inc.
  * Copyright (C) 2009 Novell, Inc.
  */
@@ -110,6 +96,8 @@ typedef struct _NMModemPrivate {
 	/* PPP stats */
 	guint32 in_bytes;
 	guint32 out_bytes;
+
+	bool claimed:1;
 } NMModemPrivate;
 
 G_DEFINE_TYPE (NMModem, nm_modem, G_TYPE_OBJECT)
@@ -187,6 +175,53 @@ nm_modem_state_to_string (NMModemState 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)
 {
@@ -976,9 +1011,9 @@ nm_modem_get_secrets (NMModem *self,
 /*****************************************************************************/
 
 static NMActStageReturn
-act_stage1_prepare (NMModem *modem,
-                    NMConnection *connection,
-                    NMDeviceStateReason *out_failure_reason)
+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;
@@ -995,6 +1030,8 @@ nm_modem_act_stage1_prepare (NMModem *self,
 	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);
@@ -1005,7 +1042,7 @@ nm_modem_act_stage1_prepare (NMModem *self,
 	setting_name = nm_connection_need_secrets (connection, &hints);
 	if (!setting_name) {
 		nm_assert (!hints);
-		return NM_MODEM_GET_CLASS (self)->act_stage1_prepare (self, connection, out_failure_reason);
+		return NM_MODEM_GET_CLASS (self)->modem_act_stage1_prepare (self, connection, out_failure_reason);
 	}
 
 	/* Secrets required... */
@@ -1029,19 +1066,18 @@ nm_modem_act_stage1_prepare (NMModem *self,
 
 /*****************************************************************************/
 
-NMActStageReturn
-nm_modem_act_stage2_config (NMModem *self,
-                            NMActRequest *req,
-                            NMDeviceStateReason *out_failure_reason)
+void
+nm_modem_act_stage2_config (NMModem *self)
 {
-	NMModemPrivate *priv = NM_MODEM_GET_PRIVATE (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;
-
-	return NM_ACT_STAGE_RETURN_SUCCESS;
 }
 
 /*****************************************************************************/
@@ -1800,7 +1836,7 @@ nm_modem_class_init (NMModemClass *klass)
 	object_class->dispose = dispose;
 	object_class->finalize = finalize;
 
-	klass->act_stage1_prepare = act_stage1_prepare;
+	klass->modem_act_stage1_prepare = modem_act_stage1_prepare;
 	klass->stage3_ip6_config_request = stage3_ip6_config_request;
 	klass->deactivate_cleanup = deactivate_cleanup;
 
diff --git a/src/devices/wwan/nm-modem.h b/src/devices/wwan/nm-modem.h
index fccb4fec..f2de990b 100644
--- a/src/devices/wwan/nm-modem.h
+++ b/src/devices/wwan/nm-modem.h
@@ -1,19 +1,5 @@
-/* NetworkManager -- Network link manager
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- *
+// SPDX-License-Identifier: GPL-2.0+
+/*
  * Copyright (C) 2009 - 2011 Red Hat, Inc.
  * Copyright (C) 2009 Novell, Inc.
  */
@@ -136,9 +122,9 @@ typedef struct {
 	                                            NMConnection *const*existing_connections,
 	                                            GError **error);
 
-	NMActStageReturn (*act_stage1_prepare)     (NMModem *modem,
-	                                            NMConnection *connection,
-	                                            NMDeviceStateReason *out_failure_reason);
+	NMActStageReturn (*modem_act_stage1_prepare) (NMModem *modem,
+	                                              NMConnection *connection,
+	                                              NMDeviceStateReason *out_failure_reason);
 
 	NMActStageReturn (*static_stage3_ip4_config_start) (NMModem *self,
 	                                                    NMActRequest *req,
@@ -167,6 +153,10 @@ typedef struct {
 
 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);
@@ -222,9 +212,7 @@ NMActStageReturn nm_modem_act_stage1_prepare (NMModem *modem,
                                               NMActRequest *req,
                                               NMDeviceStateReason *out_failure_reason);
 
-NMActStageReturn nm_modem_act_stage2_config (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,
diff --git a/src/devices/wwan/nm-service-providers.c b/src/devices/wwan/nm-service-providers.c
new file mode 100644
index 00000000..5140f7ed
--- /dev/null
+++ b/src/devices/wwan/nm-service-providers.c
@@ -0,0 +1,459 @@
+// SPDX-License-Identifier: LGPL-2.1+
+/*
+ * Copyright (C) 2009 Novell, Inc.
+ * Author: Tambet Ingo (tambet@gmail.com).
+ * Copyright (C) 2009 - 2019 Red Hat, Inc.
+ * Copyright (C) 2012 Lanedo GmbH
+ */
+
+#include "nm-default.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;
+		g_clear_pointer (&parse_context->apn, g_free);
+		g_clear_pointer (&parse_context->username, g_free);
+		g_clear_pointer (&parse_context->password, g_free);
+		g_clear_pointer (&parse_context->gateway, g_free);
+		g_clear_pointer (&parse_context->auth_method, g_free);
+		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) {
+				g_clear_pointer (&parse_context->auth_method, g_free);
+				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;
+
+	g_clear_pointer (&parse_context->text_buffer, g_free);
+
+	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) {
+		g_clear_pointer (&parse_context->text_buffer, g_free);
+		parse_context->state = PARSER_TOPLEVEL;
+	}
+}
+
+static void
+parser_provider_end (ParseContext *parse_context,
+                     const char *name)
+{
+	if (strcmp (name, "provider") == 0) {
+		g_clear_pointer (&parse_context->text_buffer, g_free);
+		parse_context->state = PARSER_COUNTRY;
+	}
+}
+
+static void
+parser_gsm_end (ParseContext *parse_context,
+                const char *name)
+{
+	if (strcmp (name, "gsm") == 0) {
+		g_clear_pointer (&parse_context->text_buffer, g_free);
+		parse_context->state = PARSER_PROVIDER;
+	}
+}
+
+static void
+parser_gsm_apn_end (ParseContext *parse_context,
+                    const char *name)
+{
+	if (strcmp (name, "username") == 0) {
+		g_clear_pointer (&parse_context->username, g_free);
+		parse_context->username = g_steal_pointer (&parse_context->text_buffer);
+	} else if (strcmp (name, "password") == 0) {
+		g_clear_pointer (&parse_context->password, g_free);
+		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) {
+		g_clear_pointer (&parse_context->gateway, g_free);
+		parse_context->gateway = g_steal_pointer (&parse_context->text_buffer);
+	} else if (strcmp (name, "apn") == 0) {
+		g_clear_pointer (&parse_context->text_buffer, g_free);
+
+		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) {
+		g_clear_pointer (&parse_context->text_buffer, g_free);
+		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/devices/wwan/nm-service-providers.h b/src/devices/wwan/nm-service-providers.h
new file mode 100644
index 00000000..35ad2fc1
--- /dev/null
+++ b/src/devices/wwan/nm-service-providers.h
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: LGPL-2.1+
+/*
+ * 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/devices/wwan/nm-wwan-factory.c b/src/devices/wwan/nm-wwan-factory.c
index a0e5c160..9a85f936 100644
--- a/src/devices/wwan/nm-wwan-factory.c
+++ b/src/devices/wwan/nm-wwan-factory.c
@@ -1,19 +1,5 @@
-/* NetworkManager -- Network link manager
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- *
+// SPDX-License-Identifier: GPL-2.0+
+/*
  * Copyright (C) 2014 Red Hat, Inc.
  */
 
@@ -77,11 +63,10 @@ modem_added_cb (NMModemManager *manager,
                 gpointer user_data)
 {
 	NMWwanFactory *self = NM_WWAN_FACTORY (user_data);
-	NMDevice *device;
+	gs_unref_object NMDevice *device = NULL;
 	const char *driver;
 
-	/* Do nothing if the modem was consumed by some other plugin */
-	if (nm_device_factory_emit_component_added (NM_DEVICE_FACTORY (self), G_OBJECT (modem)))
+	if (nm_modem_is_claimed (modem))
 		return;
 
 	driver = nm_modem_get_driver (modem);
@@ -90,17 +75,16 @@ modem_added_cb (NMModemManager *manager,
 	 * 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_info (LOGD_MB, "ignoring modem '%s' (no associated Bluetooth device)",
-		             nm_modem_get_control_port (modem));
+	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_assert (device);
 	g_signal_emit_by_name (self, NM_DEVICE_FACTORY_DEVICE_ADDED, device);
-	g_object_unref (device);
 }
 
 static NMDevice *
diff --git a/src/devices/wwan/tests/test-service-providers.c b/src/devices/wwan/tests/test-service-providers.c
new file mode 100644
index 00000000..33402cd5
--- /dev/null
+++ b/src/devices/wwan/tests/test-service-providers.c
@@ -0,0 +1,124 @@
+// SPDX-License-Identifier: LGPL-2.1+
+/*
+ * Copyright (C) 2019 Red Hat
+ */
+
+#include "nm-default.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/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/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/devices/wwan/tests/test-service-providers.xml b/src/devices/wwan/tests/test-service-providers.xml
new file mode 100644
index 00000000..f0ca2deb
--- /dev/null
+++ b/src/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>
+