about summary refs log tree commit diff
path: root/src/core/devices/bluetooth
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/devices/bluetooth')
-rw-r--r--src/core/devices/bluetooth/meson.build61
-rw-r--r--src/core/devices/bluetooth/nm-bluez-common.h24
-rw-r--r--src/core/devices/bluetooth/nm-bluez-manager.c2896
-rw-r--r--src/core/devices/bluetooth/nm-bluez-manager.h42
-rw-r--r--src/core/devices/bluetooth/nm-bluez5-dun.c857
-rw-r--r--src/core/devices/bluetooth/nm-bluez5-dun.h37
-rw-r--r--src/core/devices/bluetooth/nm-bt-error.c10
-rw-r--r--src/core/devices/bluetooth/nm-bt-error.h19
-rw-r--r--src/core/devices/bluetooth/nm-device-bt.c1412
-rw-r--r--src/core/devices/bluetooth/nm-device-bt.h59
-rw-r--r--src/core/devices/bluetooth/tests/nm-bt-test.c223
11 files changed, 5640 insertions, 0 deletions
diff --git a/src/core/devices/bluetooth/meson.build b/src/core/devices/bluetooth/meson.build
new file mode 100644
index 00000000..d5f26068
--- /dev/null
+++ b/src/core/devices/bluetooth/meson.build
@@ -0,0 +1,61 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+libnm_device_plugin_bluetooth_static = static_library(
+  'nm-device-plugin-bluetooth-static',
+  sources: files(
+    'nm-bluez-manager.c',
+    'nm-bt-error.c',
+    'nm-device-bt.c',
+  ) + (enable_bluez5_dun ? files('nm-bluez5-dun.c') : files()),
+  dependencies: [
+    core_default_dep,
+    libnm_wwan_dep,
+    bluez5_dep,
+  ],
+  c_args: daemon_c_flags,
+)
+
+libnm_device_plugin_bluetooth_static_dep = declare_dependency(
+  link_whole: libnm_device_plugin_bluetooth_static,
+)
+
+libnm_device_plugin_bluetooth = shared_module(
+  'nm-device-plugin-bluetooth',
+  dependencies: [
+    core_plugin_dep,
+    libnm_wwan_dep,
+    bluez5_dep,
+    libnm_device_plugin_bluetooth_static_dep,
+  ],
+  link_args: ldflags_linker_script_devices,
+  link_depends: linker_script_devices,
+  install: true,
+  install_dir: nm_plugindir,
+  install_rpath: nm_plugindir,
+)
+
+core_plugins += libnm_device_plugin_bluetooth
+
+test(
+  'check-local-devices-bluetooth',
+  check_exports,
+  args: [
+    libnm_device_plugin_bluetooth.full_path(),
+    linker_script_devices
+  ],
+)
+
+if enable_tests
+  executable(
+    'nm-bt-test',
+    'tests/nm-bt-test.c',
+    dependencies: [
+      libNetworkManagerTest_dep,
+      core_default_dep,
+      libnm_wwan_dep,
+      bluez5_dep,
+      libnm_device_plugin_bluetooth_static_dep,
+    ],
+    c_args: test_c_flags,
+  )
+endif
diff --git a/src/core/devices/bluetooth/nm-bluez-common.h b/src/core/devices/bluetooth/nm-bluez-common.h
new file mode 100644
index 00000000..868a985e
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-bluez-common.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_BLUEZ_COMMON_H__
+#define __NETWORKMANAGER_BLUEZ_COMMON_H__
+
+#define BLUETOOTH_CONNECT_DUN "dun"
+#define BLUETOOTH_CONNECT_NAP "nap"
+
+#define NM_BLUEZ_SERVICE "org.bluez"
+
+#define NM_BLUEZ_MANAGER_PATH "/"
+
+#define NM_BLUEZ5_ADAPTER_INTERFACE        "org.bluez.Adapter1"
+#define NM_BLUEZ5_DEVICE_INTERFACE         "org.bluez.Device1"
+#define NM_BLUEZ5_NETWORK_INTERFACE        "org.bluez.Network1"
+#define NM_BLUEZ5_NETWORK_SERVER_INTERFACE "org.bluez.NetworkServer1"
+
+#define NM_BLUEZ_MANAGER_BDADDR_ADDED         "bdaddr-added"
+#define NM_BLUEZ_MANAGER_NETWORK_SERVER_ADDED "network-server-added"
+
+#endif /* NM_BLUEZ_COMMON_H */
diff --git a/src/core/devices/bluetooth/nm-bluez-manager.c b/src/core/devices/bluetooth/nm-bluez-manager.c
new file mode 100644
index 00000000..dd998d29
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-bluez-manager.c
@@ -0,0 +1,2896 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 - 2014 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-bluez-manager.h"
+
+#include <signal.h>
+#include <stdlib.h>
+#include <gmodule.h>
+#include <linux/if_ether.h>
+
+#include "nm-glib-aux/nm-dbus-aux.h"
+#include "nm-glib-aux/nm-c-list.h"
+#include "nm-dbus-manager.h"
+#include "devices/nm-device-factory.h"
+#include "devices/nm-device-bridge.h"
+#include "nm-setting-bluetooth.h"
+#include "settings/nm-settings.h"
+#include "nm-bluez-common.h"
+#include "nm-device-bt.h"
+#include "nm-manager.h"
+#include "nm-bluez5-dun.h"
+#include "nm-core-internal.h"
+#include "platform/nm-platform.h"
+#include "nm-std-aux/nm-dbus-compat.h"
+
+/*****************************************************************************/
+
+#if WITH_BLUEZ5_DUN
+    #define _NM_BT_CAPABILITY_SUPPORTED_DUN NM_BT_CAPABILITY_DUN
+#else
+    #define _NM_BT_CAPABILITY_SUPPORTED_DUN NM_BT_CAPABILITY_NONE
+#endif
+#define _NM_BT_CAPABILITY_SUPPORTED (NM_BT_CAPABILITY_NAP | _NM_BT_CAPABILITY_SUPPORTED_DUN)
+
+typedef struct {
+    const char *            bdaddr;
+    CList                   lst_head;
+    NMBluetoothCapabilities bt_type : 8;
+    char                    bdaddr_data[];
+} ConnDataHead;
+
+typedef struct {
+    NMSettingsConnection *sett_conn;
+    ConnDataHead *        cdata_hd;
+    CList                 lst;
+} ConnDataElem;
+
+typedef struct {
+    GCancellable *             ext_cancellable;
+    GCancellable *             int_cancellable;
+    NMBtVTableRegisterCallback callback;
+    gpointer                   callback_user_data;
+    gulong                     ext_cancelled_id;
+} NetworkServerRegisterReqData;
+
+typedef struct {
+    GCancellable *          ext_cancellable;
+    GCancellable *          int_cancellable;
+    NMBluezManagerConnectCb callback;
+    gpointer                callback_user_data;
+    char *                  device_name;
+    gulong                  ext_cancelled_id;
+    guint                   timeout_id;
+    guint                   timeout_wait_connect_id;
+} DeviceConnectReqData;
+
+typedef struct {
+    const char *object_path;
+
+    NMBluezManager *self;
+
+    /* Fields name with "d_" prefix are purely cached values from BlueZ's
+     * ObjectManager D-Bus interface. There is no logic whatsoever about
+     * them.
+     */
+
+    CList process_change_lst;
+
+    struct {
+        char *address;
+    } d_adapter;
+
+    struct {
+        char *address;
+        char *name;
+        char *adapter;
+    } d_device;
+
+    struct {
+        char *interface;
+    } d_network;
+
+    struct {
+        CList                         lst;
+        char *                        adapter_address;
+        NMDevice *                    device_br;
+        NetworkServerRegisterReqData *r_req_data;
+    } x_network_server;
+
+    struct {
+        NMSettingsConnection *panu_connection;
+        NMDeviceBt *          device_bt;
+        DeviceConnectReqData *c_req_data;
+        NMBluez5DunContext *  connect_dun_context;
+        gulong                device_bt_signal_id;
+    } x_device;
+
+    /* indicate whether the D-Bus object has the particular D-Bus interface. */
+    bool d_has_adapter_iface : 1;
+    bool d_has_device_iface : 1;
+    bool d_has_network_iface : 1;
+    bool d_has_network_server_iface : 1;
+
+    /* cached D-Bus properties for Device1 ("d_device*"). */
+    NMBluetoothCapabilities d_device_capabilities : 6;
+    bool                    d_device_connected : 1;
+    bool                    d_device_paired : 1;
+
+    /* cached D-Bus properties for Network1 ("d_network*"). */
+    bool d_network_connected : 1;
+
+    /* cached D-Bus properties for Adapter1 ("d_adapter*"). */
+    bool d_adapter_powered : 1;
+
+    /* properties related to device ("x_device*"). */
+    NMBluetoothCapabilities x_device_connect_bt_type : 6;
+    bool                    x_device_is_usable : 1;
+    bool                    x_device_is_connected : 1;
+
+    bool x_device_panu_connection_allow_create : 1;
+
+    /* flag to remember last time when we checked wether the object
+     * was  a suitable adapter that is usable to a device. */
+    bool was_usable_adapter_for_device_before : 1;
+
+    char _object_path_intern[];
+} BzDBusObj;
+
+typedef struct {
+    NMManager * manager;
+    NMSettings *settings;
+
+    GDBusConnection *dbus_connection;
+
+    NMBtVTableNetworkServer vtable_network_server;
+
+    GCancellable *name_owner_get_cancellable;
+    GCancellable *get_managed_objects_cancellable;
+
+    GHashTable *bzobjs;
+
+    char *name_owner;
+
+    GHashTable *conn_data_heads;
+    GHashTable *conn_data_elems;
+
+    CList network_server_lst_head;
+
+    CList process_change_lst_head;
+
+    guint name_owner_changed_id;
+
+    guint managed_objects_changed_id;
+
+    guint properties_changed_id;
+
+    guint process_change_idle_id;
+
+    bool settings_registered : 1;
+} NMBluezManagerPrivate;
+
+struct _NMBluezManager {
+    NMDeviceFactory       parent;
+    NMBluezManagerPrivate _priv;
+};
+
+struct _NMBluezManagerClass {
+    NMDeviceFactoryClass parent;
+};
+
+G_DEFINE_TYPE(NMBluezManager, nm_bluez_manager, NM_TYPE_DEVICE_FACTORY);
+
+#define NM_BLUEZ_MANAGER_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMBluezManager, NM_IS_BLUEZ_MANAGER)
+
+/*****************************************************************************/
+
+NM_DEVICE_FACTORY_DECLARE_TYPES(NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(
+    NM_LINK_TYPE_BNEP) NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_BLUETOOTH_SETTING_NAME))
+
+G_MODULE_EXPORT NMDeviceFactory *
+                nm_device_factory_create(GError **error)
+{
+    return g_object_new(NM_TYPE_BLUEZ_MANAGER, NULL);
+}
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN      LOGD_BT
+#define _NMLOG(level, ...) __NMLOG_DEFAULT(level, _NMLOG_DOMAIN, "bluez", __VA_ARGS__)
+
+/*****************************************************************************/
+
+static NMBluetoothCapabilities
+convert_uuids_to_capabilities(const char *const *strv)
+{
+    NMBluetoothCapabilities capabilities = NM_BT_CAPABILITY_NONE;
+
+    if (strv) {
+        for (; strv[0]; strv++) {
+            gs_free char *s_part1 = NULL;
+            const char *  str     = strv[0];
+            const char *  s;
+
+            s = strchr(str, '-');
+            if (!s)
+                continue;
+
+            s_part1 = g_strndup(str, s - str);
+            switch (_nm_utils_ascii_str_to_int64(s_part1, 16, 0, G_MAXINT, -1)) {
+            case 0x1103:
+                capabilities |= NM_BT_CAPABILITY_DUN;
+                break;
+            case 0x1116:
+                capabilities |= NM_BT_CAPABILITY_NAP;
+                break;
+            default:
+                break;
+            }
+        }
+    }
+
+    return capabilities;
+}
+
+/*****************************************************************************/
+
+static void     _cleanup_for_name_owner(NMBluezManager *self);
+static void     _connect_disconnect(NMBluezManager *self, BzDBusObj *bzobj, const char *reason);
+static gboolean _bzobjs_network_server_is_usable(const BzDBusObj *bzobj, gboolean require_powered);
+static gboolean _bzobjs_is_dead(const BzDBusObj *bzobj);
+static gboolean _bzobjs_device_is_usable(const BzDBusObj *bzobj,
+                                         BzDBusObj **     out_adapter_bzobj,
+                                         gboolean *       out_create_panu_connection);
+static gboolean _bzobjs_adapter_is_usable_for_device(const BzDBusObj *bzobj);
+static ConnDataHead *
+_conn_track_find_head(NMBluezManager *self, NMBluetoothCapabilities bt_type, const char *bdaddr);
+static void _process_change_idle_schedule(NMBluezManager *self, BzDBusObj *bzobj);
+static void
+_network_server_unregister_bridge(NMBluezManager *self, BzDBusObj *bzobj, const char *reason);
+static gboolean _connect_timeout_wait_connected_cb(gpointer user_data);
+
+/*****************************************************************************/
+
+static void
+_dbus_call_complete_cb_nop(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    /* we don't do anything at all. The only reason to register this
+     * callback is so that GDBusConnection keeps the cancellable alive
+     * long enough until the call completes.
+     *
+     * Note that this cancellable in turn is registered via
+     * nm_shutdown_wait_obj_register_*(), to block shutdown until
+     * we are done. */
+}
+
+/*****************************************************************************/
+
+static void
+_network_server_register_req_data_complete(NetworkServerRegisterReqData *r_req_data, GError *error)
+{
+    nm_clear_g_signal_handler(r_req_data->ext_cancellable, &r_req_data->ext_cancelled_id);
+
+    nm_clear_g_cancellable(&r_req_data->int_cancellable);
+
+    if (r_req_data->callback) {
+        gs_free_error GError *error_cancelled = NULL;
+
+        if (g_cancellable_set_error_if_cancelled(r_req_data->ext_cancellable, &error_cancelled))
+            error = error_cancelled;
+
+        r_req_data->callback(error, r_req_data->callback_user_data);
+    }
+
+    g_object_unref(r_req_data->ext_cancellable);
+    nm_g_slice_free(r_req_data);
+}
+
+static void
+_device_connect_req_data_complete(DeviceConnectReqData *c_req_data,
+                                  NMBluezManager *      self,
+                                  const char *          device_name,
+                                  GError *              error)
+{
+    nm_assert((!!device_name) != (!!error));
+
+    nm_clear_g_signal_handler(c_req_data->ext_cancellable, &c_req_data->ext_cancelled_id);
+
+    nm_clear_g_cancellable(&c_req_data->int_cancellable);
+    nm_clear_g_source(&c_req_data->timeout_id);
+    nm_clear_g_source(&c_req_data->timeout_wait_connect_id);
+
+    if (c_req_data->callback) {
+        gs_free_error GError *error_cancelled = NULL;
+
+        if (g_cancellable_set_error_if_cancelled(c_req_data->ext_cancellable, &error_cancelled)) {
+            error       = error_cancelled;
+            device_name = NULL;
+        }
+
+        c_req_data->callback(self, TRUE, device_name, error, c_req_data->callback_user_data);
+    }
+
+    g_object_unref(c_req_data->ext_cancellable);
+    nm_clear_g_free(&c_req_data->device_name);
+    nm_g_slice_free(c_req_data);
+}
+
+/*****************************************************************************/
+
+static BzDBusObj *
+_bz_dbus_obj_new(NMBluezManager *self, const char *object_path)
+{
+    BzDBusObj *bzobj;
+    gsize      l;
+
+    nm_assert(NM_IS_BLUEZ_MANAGER(self));
+
+    l = strlen(object_path) + 1;
+
+    bzobj  = g_malloc(sizeof(BzDBusObj) + l);
+    *bzobj = (BzDBusObj){
+        .object_path                           = bzobj->_object_path_intern,
+        .self                                  = self,
+        .x_network_server.lst                  = C_LIST_INIT(bzobj->x_network_server.lst),
+        .process_change_lst                    = C_LIST_INIT(bzobj->process_change_lst),
+        .x_device_panu_connection_allow_create = TRUE,
+    };
+    memcpy(bzobj->_object_path_intern, object_path, l);
+
+    return bzobj;
+}
+
+static void
+_bz_dbus_obj_free(BzDBusObj *bzobj)
+{
+    nm_assert(bzobj);
+    nm_assert(NM_IS_BLUEZ_MANAGER(bzobj->self));
+    nm_assert(!bzobj->x_network_server.device_br);
+    nm_assert(!bzobj->x_network_server.r_req_data);
+    nm_assert(!bzobj->x_device.c_req_data);
+
+    c_list_unlink_stale(&bzobj->process_change_lst);
+    c_list_unlink_stale(&bzobj->x_network_server.lst);
+    g_free(bzobj->x_network_server.adapter_address);
+    g_free(bzobj->d_adapter.address);
+    g_free(bzobj->d_network.interface);
+    g_free(bzobj->d_device.address);
+    g_free(bzobj->d_device.name);
+    g_free(bzobj->d_device.adapter);
+    g_free(bzobj);
+}
+
+/*****************************************************************************/
+
+static const char *
+_bzobj_to_string(const BzDBusObj *bzobj, char *buf, gsize len)
+{
+    char *      buf0   = buf;
+    const char *prefix = "";
+    gboolean    device_is_usable;
+    gboolean    create_panu_connection = FALSE;
+    gboolean    network_server_is_usable;
+    char        sbuf_cap[100];
+
+    if (len > 0)
+        buf[0] = '\0';
+
+    if (bzobj->d_has_adapter_iface) {
+        nm_utils_strbuf_append_str(&buf, &len, prefix);
+        prefix = ", ";
+        nm_utils_strbuf_append_str(&buf, &len, "Adapter1 {");
+        if (bzobj->d_adapter.address) {
+            nm_utils_strbuf_append(&buf, &len, " d.address: \"%s\"", bzobj->d_adapter.address);
+            if (bzobj->d_adapter_powered)
+                nm_utils_strbuf_append_str(&buf, &len, ",");
+        }
+        if (bzobj->d_adapter_powered)
+            nm_utils_strbuf_append(&buf, &len, " d.powered: 1");
+        nm_utils_strbuf_append_str(&buf, &len, " }");
+    }
+
+    if (bzobj->d_has_device_iface) {
+        const char *prefix1 = "";
+
+        nm_utils_strbuf_append_str(&buf, &len, prefix);
+        prefix = ", ";
+        nm_utils_strbuf_append_str(&buf, &len, "Device1 {");
+        if (bzobj->d_device.address) {
+            nm_utils_strbuf_append(&buf,
+                                   &len,
+                                   "%s d.address: \"%s\"",
+                                   prefix1,
+                                   bzobj->d_device.address);
+            prefix1 = ",";
+        }
+        if (bzobj->d_device.name) {
+            nm_utils_strbuf_append(&buf, &len, "%s d.name: \"%s\"", prefix1, bzobj->d_device.name);
+            prefix1 = ",";
+        }
+        if (bzobj->d_device.adapter) {
+            nm_utils_strbuf_append(&buf,
+                                   &len,
+                                   "%s d.adapter: \"%s\"",
+                                   prefix1,
+                                   bzobj->d_device.adapter);
+            prefix1 = ",";
+        }
+        if (bzobj->d_device_capabilities != NM_BT_CAPABILITY_NONE) {
+            nm_utils_strbuf_append(&buf,
+                                   &len,
+                                   "%s d.capabilities: \"%s\"",
+                                   prefix1,
+                                   nm_bluetooth_capability_to_string(bzobj->d_device_capabilities,
+                                                                     sbuf_cap,
+                                                                     sizeof(sbuf_cap)));
+            prefix1 = ",";
+        }
+        if (bzobj->d_device_connected) {
+            nm_utils_strbuf_append(&buf, &len, "%s d.connected: 1", prefix1);
+            prefix1 = ",";
+        }
+        if (bzobj->d_device_paired) {
+            nm_utils_strbuf_append(&buf, &len, "%s d.paired: 1", prefix1);
+            prefix1 = ",";
+        }
+        nm_utils_strbuf_append_str(&buf, &len, " }");
+    }
+
+    network_server_is_usable = _bzobjs_network_server_is_usable(bzobj, TRUE);
+
+    if (bzobj->d_has_network_server_iface
+        || network_server_is_usable != (!c_list_is_empty(&bzobj->x_network_server.lst))
+        || !c_list_is_empty(&bzobj->x_network_server.lst)
+        || !nm_streq0(bzobj->d_has_adapter_iface ? bzobj->d_adapter.address : NULL,
+                      bzobj->x_network_server.adapter_address)
+        || bzobj->x_network_server.device_br || bzobj->x_network_server.r_req_data) {
+        nm_utils_strbuf_append_str(&buf, &len, prefix);
+        prefix = ", ";
+
+        nm_utils_strbuf_append(&buf, &len, "NetworkServer1 { ");
+
+        if (!bzobj->d_has_network_server_iface)
+            nm_utils_strbuf_append(&buf, &len, " has-d-iface: 0, ");
+
+        if (network_server_is_usable != (!c_list_is_empty(&bzobj->x_network_server.lst)))
+            nm_utils_strbuf_append(&buf,
+                                   &len,
+                                   "usable: %d, used: %d",
+                                   !!network_server_is_usable,
+                                   !network_server_is_usable);
+        else if (network_server_is_usable)
+            nm_utils_strbuf_append(&buf, &len, "used: 1");
+        else
+            nm_utils_strbuf_append(&buf, &len, "usable: 0");
+
+        if (!nm_streq0(bzobj->d_has_adapter_iface ? bzobj->d_adapter.address : NULL,
+                       bzobj->x_network_server.adapter_address)) {
+            if (bzobj->x_network_server.adapter_address)
+                nm_utils_strbuf_append(&buf,
+                                       &len,
+                                       ", adapter-address: \"%s\"",
+                                       bzobj->x_network_server.adapter_address);
+            else
+                nm_utils_strbuf_append(&buf, &len, ", adapter-address: <NULL>");
+        }
+
+        if (bzobj->x_network_server.device_br)
+            nm_utils_strbuf_append(&buf, &len, ", bridge-device: 1");
+
+        if (bzobj->x_network_server.r_req_data)
+            nm_utils_strbuf_append(&buf, &len, ", register-in-progress: 1");
+
+        nm_utils_strbuf_append_str(&buf, &len, " }");
+    }
+
+    device_is_usable = _bzobjs_device_is_usable(bzobj, NULL, &create_panu_connection);
+
+    if (bzobj->d_has_network_iface || bzobj->d_network.interface || bzobj->d_network_connected
+        || create_panu_connection || bzobj->x_device.panu_connection
+        || device_is_usable != bzobj->x_device_is_usable || bzobj->x_device.device_bt
+        || bzobj->x_device_connect_bt_type != NM_BT_CAPABILITY_NONE
+        || bzobj->x_device.connect_dun_context || bzobj->x_device.c_req_data
+        || bzobj->x_device_is_connected != bzobj->d_network_connected) {
+        nm_utils_strbuf_append_str(&buf, &len, prefix);
+        prefix = ", ";
+        nm_utils_strbuf_append_str(&buf, &len, "Network1 {");
+        if (bzobj->d_network.interface)
+            nm_utils_strbuf_append(&buf,
+                                   &len,
+                                   " d.interface: \"%s\", ",
+                                   bzobj->d_network.interface);
+        if (bzobj->d_network_connected)
+            nm_utils_strbuf_append(&buf, &len, " d.connected: %d, ", !!bzobj->d_network_connected);
+        if (!bzobj->d_has_network_iface)
+            nm_utils_strbuf_append(&buf, &len, " has-d-iface: 0, ");
+        if (device_is_usable != bzobj->x_device_is_usable)
+            nm_utils_strbuf_append(&buf,
+                                   &len,
+                                   " usable: %d, used: %d",
+                                   !!device_is_usable,
+                                   !device_is_usable);
+        else if (device_is_usable)
+            nm_utils_strbuf_append(&buf, &len, " used: 1");
+        else
+            nm_utils_strbuf_append(&buf, &len, " usable: 0");
+
+        if (create_panu_connection)
+            nm_utils_strbuf_append(&buf, &len, ", create-panu-connection: 1");
+
+        if (bzobj->x_device.panu_connection)
+            nm_utils_strbuf_append(&buf, &len, ", has-panu-connection: 1");
+
+        if (bzobj->x_device.device_bt)
+            nm_utils_strbuf_append(&buf, &len, ", has-device: 1");
+
+        if (bzobj->x_device_connect_bt_type != NM_BT_CAPABILITY_NONE
+            || bzobj->x_device.connect_dun_context) {
+            nm_utils_strbuf_append(
+                &buf,
+                &len,
+                ", connect: %s%s",
+                nm_bluetooth_capability_to_string(bzobj->x_device_connect_bt_type,
+                                                  sbuf_cap,
+                                                  sizeof(sbuf_cap)),
+                bzobj->x_device.connect_dun_context ? ",with-dun-context" : "");
+        }
+
+        if (bzobj->x_device.c_req_data)
+            nm_utils_strbuf_append(&buf, &len, ", connecting: 1");
+
+        if (bzobj->x_device_is_connected != bzobj->d_network_connected)
+            nm_utils_strbuf_append(&buf, &len, ", connected: %d", !!bzobj->x_device_is_connected);
+
+        nm_utils_strbuf_append_str(&buf, &len, " }");
+    }
+
+    if (_bzobjs_is_dead(bzobj)) {
+        nm_utils_strbuf_append_str(&buf, &len, prefix);
+        prefix = ", ";
+        nm_utils_strbuf_append_str(&buf, &len, "dead: 1");
+    }
+
+    if (!c_list_is_empty(&bzobj->process_change_lst)) {
+        nm_utils_strbuf_append_str(&buf, &len, prefix);
+        prefix = ", ";
+        nm_utils_strbuf_append(&buf, &len, "change-pending-on-idle: 1");
+    }
+
+    if (_bzobjs_adapter_is_usable_for_device(bzobj)
+        != bzobj->was_usable_adapter_for_device_before) {
+        nm_utils_strbuf_append_str(&buf, &len, prefix);
+        prefix = ", ";
+        nm_utils_strbuf_append(&buf, &len, "change-usable-adapter-for-device: 1");
+    }
+
+    return buf0;
+}
+
+#define _LOG_bzobj(bzobj, context)                           \
+    G_STMT_START                                             \
+    {                                                        \
+        const BzDBusObj *const _bzobj = (bzobj);             \
+        char                   _buf[500];                    \
+                                                             \
+        _LOGT("change %-21s %s : { %s }",                    \
+              (context),                                     \
+              _bzobj->object_path,                           \
+              _bzobj_to_string(_bzobj, _buf, sizeof(_buf))); \
+    }                                                        \
+    G_STMT_END
+
+static gboolean
+_bzobjs_is_dead(const BzDBusObj *bzobj)
+{
+    return !bzobj->d_has_adapter_iface && !bzobj->d_has_device_iface && !bzobj->d_has_network_iface
+           && !bzobj->d_has_network_server_iface && c_list_is_empty(&bzobj->process_change_lst);
+}
+
+static BzDBusObj *
+_bzobjs_get(NMBluezManager *self, const char *object_path)
+{
+    return g_hash_table_lookup(NM_BLUEZ_MANAGER_GET_PRIVATE(self)->bzobjs, &object_path);
+}
+
+static BzDBusObj *
+_bzobjs_add(NMBluezManager *self, const char *object_path)
+{
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    BzDBusObj *            bzobj;
+
+    bzobj = _bz_dbus_obj_new(self, object_path);
+    if (!g_hash_table_add(priv->bzobjs, bzobj))
+        nm_assert_not_reached();
+    return bzobj;
+}
+
+static void
+_bzobjs_del(BzDBusObj *bzobj)
+{
+    nm_assert(bzobj);
+    nm_assert(bzobj == _bzobjs_get(bzobj->self, bzobj->object_path));
+
+    if (!g_hash_table_remove(NM_BLUEZ_MANAGER_GET_PRIVATE(bzobj->self)->bzobjs, bzobj))
+        nm_assert_not_reached();
+}
+
+static void
+_bzobjs_del_if_dead(BzDBusObj *bzobj)
+{
+    if (_bzobjs_is_dead(bzobj))
+        _bzobjs_del(bzobj);
+}
+
+static BzDBusObj *
+_bzobjs_init(NMBluezManager *self, BzDBusObj **inout, const char *object_path)
+{
+    nm_assert(NM_IS_BLUEZ_MANAGER(self));
+    nm_assert(object_path);
+    nm_assert(inout);
+
+    if (!*inout) {
+        *inout = _bzobjs_get(self, object_path);
+        if (!*inout)
+            *inout = _bzobjs_add(self, object_path);
+    }
+
+    nm_assert(nm_streq((*inout)->object_path, object_path));
+    nm_assert(*inout == _bzobjs_get(self, object_path));
+    return *inout;
+}
+
+static gboolean
+_bzobjs_adapter_is_usable_for_device(const BzDBusObj *bzobj)
+{
+    return bzobj->d_has_adapter_iface && bzobj->d_adapter.address && bzobj->d_adapter_powered;
+}
+
+static gboolean
+_bzobjs_device_is_usable(const BzDBusObj *bzobj,
+                         BzDBusObj **     out_adapter_bzobj,
+                         gboolean *       out_create_panu_connection)
+{
+    NMBluezManager *       self;
+    NMBluezManagerPrivate *priv;
+    gboolean               usable_dun = FALSE;
+    gboolean               usable_nap = FALSE;
+    BzDBusObj *            bzobj_adapter;
+    gboolean               create_panu_connection = FALSE;
+
+    if (!bzobj->d_has_device_iface
+        || !NM_FLAGS_ANY((NMBluetoothCapabilities) bzobj->d_device_capabilities,
+                         _NM_BT_CAPABILITY_SUPPORTED)
+        || !bzobj->d_device.name || !bzobj->d_device.address || !bzobj->d_device_paired
+        || !bzobj->d_device.adapter)
+        goto out_unusable;
+
+    self = bzobj->self;
+
+    priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    if (!priv->settings_registered)
+        goto out_unusable;
+
+    bzobj_adapter = _bzobjs_get(self, bzobj->d_device.adapter);
+    if (!bzobj_adapter || !_bzobjs_adapter_is_usable_for_device(bzobj_adapter))
+        goto out_unusable;
+
+#if WITH_BLUEZ5_DUN
+    if (NM_FLAGS_HAS(bzobj->d_device_capabilities, NM_BT_CAPABILITY_DUN)) {
+        if (_conn_track_find_head(self, NM_BT_CAPABILITY_DUN, bzobj->d_device.address))
+            usable_dun = TRUE;
+    }
+#endif
+
+    if (NM_FLAGS_HAS(bzobj->d_device_capabilities, NM_BT_CAPABILITY_NAP)) {
+        if (!bzobj->d_has_network_iface)
+            usable_nap = FALSE;
+        else if (_conn_track_find_head(self, NM_BT_CAPABILITY_NAP, bzobj->d_device.address))
+            usable_nap = TRUE;
+        else if (bzobj->x_device_panu_connection_allow_create) {
+            /* We didn't yet try to create a connection. Presume we are going to create
+             * it when the time comes... */
+            usable_nap             = TRUE;
+            create_panu_connection = TRUE;
+        }
+    }
+
+    if (!usable_dun && !usable_nap) {
+        if (bzobj->x_device.device_bt
+            && nm_device_get_state(NM_DEVICE(bzobj->x_device.device_bt))
+                   > NM_DEVICE_STATE_DISCONNECTED) {
+            /* The device is still activated... the absence of a profile does not
+             * render it unusable (yet). But since there is no more profile, the
+             * device is probably about to disconnect. */
+        } else
+            goto out_unusable;
+    }
+
+    NM_SET_OUT(out_create_panu_connection, create_panu_connection);
+    NM_SET_OUT(out_adapter_bzobj, bzobj_adapter);
+    return TRUE;
+
+out_unusable:
+    NM_SET_OUT(out_create_panu_connection, FALSE);
+    NM_SET_OUT(out_adapter_bzobj, NULL);
+    return FALSE;
+}
+
+static gboolean
+_bzobjs_device_is_connected(const BzDBusObj *bzobj)
+{
+    nm_assert(_bzobjs_device_is_usable(bzobj, NULL, NULL));
+
+    if (!bzobj->d_has_device_iface || !bzobj->d_device_connected)
+        return FALSE;
+
+    if (bzobj->d_has_network_iface && bzobj->d_network_connected)
+        return TRUE;
+    if (bzobj->x_device.connect_dun_context) {
+        /* As long as we have a dun-context, we consider it connected.
+         *
+         * We require NMDeviceBt to try to connect to the modem, and if that fails,
+         * it will disconnect. */
+        return TRUE;
+    }
+    return FALSE;
+}
+
+static gboolean
+_bzobjs_network_server_is_usable(const BzDBusObj *bzobj, gboolean require_powered)
+{
+    return bzobj->d_has_network_server_iface && bzobj->d_has_adapter_iface
+           && bzobj->d_adapter.address && (!require_powered || bzobj->d_adapter_powered);
+}
+
+/*****************************************************************************/
+
+static ConnDataHead *
+_conn_data_head_new(NMBluetoothCapabilities bt_type, const char *bdaddr)
+{
+    ConnDataHead *cdata_hd;
+    gsize         l;
+
+    nm_assert(NM_IN_SET(bt_type, NM_BT_CAPABILITY_DUN, NM_BT_CAPABILITY_NAP));
+    nm_assert(bdaddr);
+
+    l         = strlen(bdaddr) + 1;
+    cdata_hd  = g_malloc(sizeof(ConnDataHead) + l);
+    *cdata_hd = (ConnDataHead){
+        .bdaddr   = cdata_hd->bdaddr_data,
+        .lst_head = C_LIST_INIT(cdata_hd->lst_head),
+        .bt_type  = bt_type,
+    };
+    memcpy(cdata_hd->bdaddr_data, bdaddr, l);
+
+    nm_assert(cdata_hd->bt_type == bt_type);
+
+    return cdata_hd;
+}
+
+static guint
+_conn_data_head_hash(gconstpointer ptr)
+{
+    const ConnDataHead *cdata_hd = ptr;
+    NMHashState         h;
+
+    nm_hash_init(&h, 520317467u);
+    nm_hash_update_val(&h, (NMBluetoothCapabilities) cdata_hd->bt_type);
+    nm_hash_update_str(&h, cdata_hd->bdaddr);
+    return nm_hash_complete(&h);
+}
+
+static gboolean
+_conn_data_head_equal(gconstpointer a, gconstpointer b)
+{
+    const ConnDataHead *cdata_hd_a = a;
+    const ConnDataHead *cdata_hd_b = b;
+
+    return cdata_hd_a->bt_type == cdata_hd_b->bt_type
+           && nm_streq(cdata_hd_a->bdaddr, cdata_hd_b->bdaddr);
+}
+
+static ConnDataHead *
+_conn_track_find_head(NMBluezManager *self, NMBluetoothCapabilities bt_type, const char *bdaddr)
+{
+    ConnDataHead cdata_hd = {
+        .bt_type = bt_type,
+        .bdaddr  = bdaddr,
+    };
+
+    return g_hash_table_lookup(NM_BLUEZ_MANAGER_GET_PRIVATE(self)->conn_data_heads, &cdata_hd);
+}
+
+static ConnDataElem *
+_conn_track_find_elem(NMBluezManager *self, NMSettingsConnection *sett_conn)
+{
+    G_STATIC_ASSERT(G_STRUCT_OFFSET(ConnDataElem, sett_conn) == 0);
+
+    return g_hash_table_lookup(NM_BLUEZ_MANAGER_GET_PRIVATE(self)->conn_data_elems, &sett_conn);
+}
+
+static gboolean
+_conn_track_is_relevant_connection(NMConnection *           connection,
+                                   NMBluetoothCapabilities *out_bt_type,
+                                   const char **            out_bdaddr)
+{
+    NMSettingBluetooth *    s_bt;
+    NMBluetoothCapabilities bt_type;
+    const char *            bdaddr;
+    const char *            b_type;
+
+    s_bt = nm_connection_get_setting_bluetooth(connection);
+    if (!s_bt)
+        return FALSE;
+
+    if (!nm_connection_is_type(connection, NM_SETTING_BLUETOOTH_SETTING_NAME))
+        return FALSE;
+
+    bdaddr = nm_setting_bluetooth_get_bdaddr(s_bt);
+    if (!bdaddr)
+        return FALSE;
+
+    b_type = nm_setting_bluetooth_get_connection_type(s_bt);
+
+    if (nm_streq(b_type, NM_SETTING_BLUETOOTH_TYPE_DUN))
+        bt_type = NM_BT_CAPABILITY_DUN;
+    else if (nm_streq(b_type, NM_SETTING_BLUETOOTH_TYPE_PANU))
+        bt_type = NM_BT_CAPABILITY_NAP;
+    else
+        return FALSE;
+
+    NM_SET_OUT(out_bt_type, bt_type);
+    NM_SET_OUT(out_bdaddr, bdaddr);
+    return TRUE;
+}
+
+static gboolean
+_conn_track_is_relevant_sett_conn(NMSettingsConnection *   sett_conn,
+                                  NMBluetoothCapabilities *out_bt_type,
+                                  const char **            out_bdaddr)
+{
+    NMConnection *connection;
+
+    connection = nm_settings_connection_get_connection(sett_conn);
+    if (!connection)
+        return FALSE;
+
+    return _conn_track_is_relevant_connection(connection, out_bt_type, out_bdaddr);
+}
+
+static gboolean
+_conn_track_is_relevant_for_sett_conn(NMSettingsConnection *  sett_conn,
+                                      NMBluetoothCapabilities bt_type,
+                                      const char *            bdaddr)
+{
+    NMBluetoothCapabilities x_bt_type;
+    const char *            x_bdaddr;
+
+    return bdaddr && _conn_track_is_relevant_sett_conn(sett_conn, &x_bt_type, &x_bdaddr)
+           && x_bt_type == bt_type && nm_streq(x_bdaddr, bdaddr);
+}
+
+static void
+_conn_track_schedule_notify(NMBluezManager *        self,
+                            NMBluetoothCapabilities bt_type,
+                            const char *            bdaddr)
+{
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    GHashTableIter         iter;
+    BzDBusObj *            bzobj;
+
+    g_hash_table_iter_init(&iter, priv->bzobjs);
+    while (g_hash_table_iter_next(&iter, (gpointer *) &bzobj, NULL)) {
+        gboolean device_is_usable;
+
+        device_is_usable = _bzobjs_device_is_usable(bzobj, NULL, NULL);
+        if (bzobj->x_device_is_usable != device_is_usable)
+            _process_change_idle_schedule(self, bzobj);
+    }
+}
+
+static void
+_conn_track_update(NMBluezManager *      self,
+                   NMSettingsConnection *sett_conn,
+                   gboolean              track,
+                   gboolean *            out_changed,
+                   gboolean *            out_changed_usable,
+                   ConnDataElem **       out_conn_data_elem)
+{
+    NMBluezManagerPrivate * priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    ConnDataHead *          cdata_hd;
+    ConnDataElem *          cdata_el;
+    ConnDataElem *          cdata_el_remove = NULL;
+    NMBluetoothCapabilities bt_type;
+    const char *            bdaddr;
+    gboolean                changed        = FALSE;
+    gboolean                changed_usable = FALSE;
+    char                    sbuf_cap[100];
+
+    nm_assert(NM_IS_SETTINGS_CONNECTION(sett_conn));
+
+    cdata_el = _conn_track_find_elem(self, sett_conn);
+
+    if (track)
+        track = _conn_track_is_relevant_sett_conn(sett_conn, &bt_type, &bdaddr);
+
+    if (!track) {
+        cdata_el_remove = g_steal_pointer(&cdata_el);
+        goto out_remove;
+    }
+
+    if (cdata_el) {
+        cdata_hd = cdata_el->cdata_hd;
+        if (cdata_hd->bt_type != bt_type || !nm_streq(cdata_hd->bdaddr, bdaddr))
+            cdata_el_remove = g_steal_pointer(&cdata_el);
+    }
+
+    if (!cdata_el) {
+        _LOGT("connection: track for %s, %s: %s (%s)",
+              nm_bluetooth_capability_to_string(bt_type, sbuf_cap, sizeof(sbuf_cap)),
+              bdaddr,
+              nm_settings_connection_get_uuid(sett_conn),
+              nm_settings_connection_get_id(sett_conn));
+        changed  = TRUE;
+        cdata_hd = _conn_track_find_head(self, bt_type, bdaddr);
+        if (!cdata_hd) {
+            changed_usable = TRUE;
+            cdata_hd       = _conn_data_head_new(bt_type, bdaddr);
+            if (!g_hash_table_add(priv->conn_data_heads, cdata_hd))
+                nm_assert_not_reached();
+            _conn_track_schedule_notify(self, bt_type, bdaddr);
+        }
+        cdata_el            = g_slice_new(ConnDataElem);
+        cdata_el->sett_conn = sett_conn;
+        cdata_el->cdata_hd  = cdata_hd;
+        c_list_link_tail(&cdata_hd->lst_head, &cdata_el->lst);
+        if (!g_hash_table_add(priv->conn_data_elems, cdata_el))
+            nm_assert_not_reached();
+    }
+
+out_remove:
+    if (cdata_el_remove) {
+        GHashTableIter iter;
+        BzDBusObj *    bzobj;
+
+        _LOGT("connection: untrack for %s, %s: %s (%s)",
+              nm_bluetooth_capability_to_string(cdata_el_remove->cdata_hd->bt_type,
+                                                sbuf_cap,
+                                                sizeof(sbuf_cap)),
+              cdata_el_remove->cdata_hd->bdaddr,
+              nm_settings_connection_get_uuid(sett_conn),
+              nm_settings_connection_get_id(sett_conn));
+
+        g_hash_table_iter_init(&iter, priv->bzobjs);
+        while (g_hash_table_iter_next(&iter, (gpointer *) &bzobj, NULL)) {
+            if (bzobj->x_device.panu_connection == sett_conn)
+                bzobj->x_device.panu_connection = NULL;
+        }
+
+        changed  = TRUE;
+        cdata_hd = cdata_el_remove->cdata_hd;
+        c_list_unlink_stale(&cdata_el_remove->lst);
+        if (!g_hash_table_remove(priv->conn_data_elems, cdata_el_remove))
+            nm_assert_not_reached();
+        if (c_list_is_empty(&cdata_hd->lst_head)) {
+            changed_usable = TRUE;
+            _conn_track_schedule_notify(self, cdata_hd->bt_type, cdata_hd->bdaddr);
+            if (!g_hash_table_remove(priv->conn_data_heads, cdata_hd))
+                nm_assert_not_reached();
+        }
+    }
+
+    NM_SET_OUT(out_changed, changed);
+    NM_SET_OUT(out_changed_usable, changed_usable);
+    NM_SET_OUT(out_conn_data_elem, cdata_el);
+}
+
+/*****************************************************************************/
+
+static void
+cp_connection_added(NMSettings *settings, NMSettingsConnection *sett_conn, NMBluezManager *self)
+{
+    _conn_track_update(self, sett_conn, TRUE, NULL, NULL, NULL);
+}
+
+static void
+cp_connection_updated(NMSettings *          settings,
+                      NMSettingsConnection *sett_conn,
+                      guint                 update_reason_u,
+                      NMBluezManager *      self)
+{
+    _conn_track_update(self, sett_conn, TRUE, NULL, NULL, NULL);
+}
+
+static void
+cp_connection_removed(NMSettings *settings, NMSettingsConnection *sett_conn, NMBluezManager *self)
+{
+    _conn_track_update(self, sett_conn, FALSE, NULL, NULL, NULL);
+}
+
+/*****************************************************************************/
+
+static NMBluezManager *
+_network_server_get_bluez_manager(const NMBtVTableNetworkServer *vtable_network_server)
+{
+    NMBluezManager *self;
+
+    self = (NMBluezManager *) (((char *) vtable_network_server)
+                               - G_STRUCT_OFFSET(NMBluezManager, _priv.vtable_network_server));
+
+    g_return_val_if_fail(NM_IS_BLUEZ_MANAGER(self), NULL);
+
+    return self;
+}
+
+static BzDBusObj *
+_network_server_find_has_device(NMBluezManagerPrivate *priv, NMDevice *device)
+{
+    BzDBusObj *bzobj;
+
+    c_list_for_each_entry (bzobj, &priv->network_server_lst_head, x_network_server.lst) {
+        if (bzobj->x_network_server.device_br == device)
+            return bzobj;
+    }
+    return NULL;
+}
+
+static BzDBusObj *
+_network_server_find_available(NMBluezManagerPrivate *priv,
+                               const char *           addr,
+                               NMDevice *             device_accept_busy)
+{
+    BzDBusObj *bzobj;
+
+    c_list_for_each_entry (bzobj, &priv->network_server_lst_head, x_network_server.lst) {
+        if (bzobj->x_network_server.device_br) {
+            if (bzobj->x_network_server.device_br != device_accept_busy)
+                continue;
+        }
+        if (addr && !nm_streq(addr, bzobj->d_adapter.address))
+            continue;
+        nm_assert(!bzobj->x_network_server.r_req_data);
+        return bzobj;
+    }
+    return NULL;
+}
+
+static gboolean
+_network_server_vt_is_available(const NMBtVTableNetworkServer *vtable,
+                                const char *                   addr,
+                                NMDevice *                     device_accept_busy)
+{
+    NMBluezManager *       self = _network_server_get_bluez_manager(vtable);
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    return !!_network_server_find_available(priv, addr, device_accept_busy);
+}
+
+static void
+_network_server_register_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    gs_unref_variant GVariant *ret = NULL;
+    gs_free_error GError *error    = NULL;
+    BzDBusObj *           bzobj;
+
+    ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), res, &error);
+    if (!ret && nm_utils_error_is_cancelled(error))
+        return;
+
+    bzobj = user_data;
+
+    if (!ret) {
+        _LOGT("NAP: [%s]: registering failed: %s", bzobj->object_path, error->message);
+    } else
+        _LOGT("NAP: [%s]: registration successful", bzobj->object_path);
+
+    g_clear_object(&bzobj->x_network_server.r_req_data->int_cancellable);
+    _network_server_register_req_data_complete(g_steal_pointer(&bzobj->x_network_server.r_req_data),
+                                               error);
+}
+
+static void
+_network_server_register_cancelled_cb(GCancellable *cancellable, BzDBusObj *bzobj)
+{
+    _network_server_unregister_bridge(bzobj->self, bzobj, "registration cancelled");
+}
+
+static gboolean
+_network_server_vt_register_bridge(const NMBtVTableNetworkServer *vtable,
+                                   const char *                   addr,
+                                   NMDevice *                     device,
+                                   GCancellable *                 cancellable,
+                                   NMBtVTableRegisterCallback     callback,
+                                   gpointer                       callback_user_data,
+                                   GError **                      error)
+{
+    NMBluezManager *              self = _network_server_get_bluez_manager(vtable);
+    NMBluezManagerPrivate *       priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    NetworkServerRegisterReqData *r_req_data;
+    BzDBusObj *                   bzobj;
+    const char *                  ifname;
+
+    g_return_val_if_fail(NM_IS_DEVICE(device), FALSE);
+    g_return_val_if_fail(G_IS_CANCELLABLE(cancellable), FALSE);
+
+    nm_assert(!g_cancellable_is_cancelled(cancellable));
+    nm_assert(!_network_server_find_has_device(priv, device));
+
+    ifname = nm_device_get_iface(device);
+    g_return_val_if_fail(ifname, FALSE);
+
+    g_return_val_if_fail(ifname, FALSE);
+
+    bzobj = _network_server_find_available(priv, addr, NULL);
+    if (!bzobj) {
+        /* The device checked that a network server is available, before
+         * starting the activation, but for some reason it no longer is.
+         * Indicate that the activation should not proceed. */
+        if (addr) {
+            nm_utils_error_set(error,
+                               NM_UTILS_ERROR_UNKNOWN,
+                               "adapter %s is not available for %s",
+                               addr,
+                               ifname);
+        } else {
+            nm_utils_error_set(error,
+                               NM_UTILS_ERROR_UNKNOWN,
+                               "no adapter available for %s",
+                               ifname);
+        }
+        return FALSE;
+    }
+
+    _LOGD("NAP: [%s]: registering \"%s\" on adapter %s",
+          bzobj->object_path,
+          ifname,
+          bzobj->d_adapter.address);
+
+    r_req_data  = g_slice_new(NetworkServerRegisterReqData);
+    *r_req_data = (NetworkServerRegisterReqData){
+        .int_cancellable    = g_cancellable_new(),
+        .ext_cancellable    = g_object_ref(cancellable),
+        .callback           = callback,
+        .callback_user_data = callback_user_data,
+        .ext_cancelled_id   = g_signal_connect(cancellable,
+                                             "cancelled",
+                                             G_CALLBACK(_network_server_register_cancelled_cb),
+                                             bzobj),
+    };
+
+    bzobj->x_network_server.device_br  = g_object_ref(device);
+    bzobj->x_network_server.r_req_data = r_req_data;
+
+    g_dbus_connection_call(priv->dbus_connection,
+                           priv->name_owner,
+                           bzobj->object_path,
+                           NM_BLUEZ5_NETWORK_SERVER_INTERFACE,
+                           "Register",
+                           g_variant_new("(ss)", BLUETOOTH_CONNECT_NAP, ifname),
+                           NULL,
+                           G_DBUS_CALL_FLAGS_NO_AUTO_START,
+                           -1,
+                           bzobj->x_network_server.r_req_data->int_cancellable,
+                           _network_server_register_cb,
+                           bzobj);
+    return TRUE;
+}
+
+static void
+_network_server_unregister_bridge_complete_on_idle_cb(gpointer user_data, GCancellable *cancellable)
+{
+    gs_free_error GError *        error  = NULL;
+    gs_free char *                reason = NULL;
+    NetworkServerRegisterReqData *r_req_data;
+
+    nm_utils_user_data_unpack(user_data, &r_req_data, &reason);
+
+    nm_utils_error_set(&error,
+                       NM_UTILS_ERROR_UNKNOWN,
+                       "registration was aborted due to %s",
+                       reason);
+    _network_server_register_req_data_complete(r_req_data, error);
+}
+
+static void
+_network_server_unregister_bridge(NMBluezManager *self, BzDBusObj *bzobj, const char *reason)
+{
+    NMBluezManagerPrivate *    priv             = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    _nm_unused gs_unref_object NMDevice *device = NULL;
+    NetworkServerRegisterReqData *       r_req_data;
+
+    nm_assert(NM_IS_DEVICE(bzobj->x_network_server.device_br));
+
+    _LOGD("NAP: [%s]: unregistering \"%s\" (%s)",
+          bzobj->object_path,
+          nm_device_get_iface(bzobj->x_network_server.device_br),
+          reason);
+
+    device = g_steal_pointer(&bzobj->x_network_server.device_br);
+
+    r_req_data = g_steal_pointer(&bzobj->x_network_server.r_req_data);
+
+    if (priv->name_owner) {
+        gs_unref_object GCancellable *cancellable = NULL;
+
+        cancellable = g_cancellable_new();
+
+        nm_shutdown_wait_obj_register_cancellable_full(
+            cancellable,
+            g_strdup_printf("bt-unregister-nap[%s]", bzobj->object_path),
+            TRUE);
+
+        g_dbus_connection_call(priv->dbus_connection,
+                               priv->name_owner,
+                               bzobj->object_path,
+                               NM_BLUEZ5_NETWORK_SERVER_INTERFACE,
+                               "Unregister",
+                               g_variant_new("(s)", BLUETOOTH_CONNECT_NAP),
+                               NULL,
+                               G_DBUS_CALL_FLAGS_NO_AUTO_START,
+                               -1,
+                               cancellable,
+                               _dbus_call_complete_cb_nop,
+                               NULL);
+    }
+
+    if (r_req_data) {
+        nm_clear_g_cancellable(&r_req_data->int_cancellable);
+        nm_utils_invoke_on_idle(r_req_data->ext_cancellable,
+                                _network_server_unregister_bridge_complete_on_idle_cb,
+                                nm_utils_user_data_pack(r_req_data, g_strdup(reason)));
+    }
+
+    _nm_device_bridge_notify_unregister_bt_nap(device, reason);
+}
+
+static gboolean
+_network_server_vt_unregister_bridge(const NMBtVTableNetworkServer *vtable, NMDevice *device)
+{
+    NMBluezManager *       self = _network_server_get_bluez_manager(vtable);
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    BzDBusObj *            bzobj;
+
+    g_return_val_if_fail(NM_IS_DEVICE(device), FALSE);
+
+    bzobj = _network_server_find_has_device(priv, device);
+    if (bzobj)
+        _network_server_unregister_bridge(self, bzobj, "disconnecting");
+
+    return TRUE;
+}
+
+static void
+_network_server_process_change(BzDBusObj *bzobj, gboolean *out_emit_device_availability_changed)
+{
+    NMBluezManager *       self = bzobj->self;
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    gboolean               network_server_is_usable;
+    gboolean               emit_device_availability_changed = FALSE;
+
+    network_server_is_usable = _bzobjs_network_server_is_usable(bzobj, TRUE);
+
+    if (!network_server_is_usable) {
+        if (!c_list_is_empty(&bzobj->x_network_server.lst)) {
+            emit_device_availability_changed = TRUE;
+            c_list_unlink(&bzobj->x_network_server.lst);
+        }
+
+        nm_clear_g_free(&bzobj->x_network_server.adapter_address);
+
+        if (bzobj->x_network_server.device_br) {
+            _network_server_unregister_bridge(self,
+                                              bzobj,
+                                              _bzobjs_network_server_is_usable(bzobj, FALSE)
+                                                  ? "adapter disabled"
+                                                  : "adapter disappeared");
+        }
+
+    } else {
+        if (!nm_streq0(bzobj->x_network_server.adapter_address, bzobj->d_adapter.address)) {
+            emit_device_availability_changed = TRUE;
+            g_free(bzobj->x_network_server.adapter_address);
+            bzobj->x_network_server.adapter_address = g_strdup(bzobj->d_adapter.address);
+        }
+
+        if (c_list_is_empty(&bzobj->x_network_server.lst)) {
+            emit_device_availability_changed = TRUE;
+            c_list_link_tail(&priv->network_server_lst_head, &bzobj->x_network_server.lst);
+        }
+    }
+
+    if (emit_device_availability_changed)
+        NM_SET_OUT(out_emit_device_availability_changed, TRUE);
+}
+
+/*****************************************************************************/
+
+static void
+_conn_create_panu_connection(NMBluezManager *self, BzDBusObj *bzobj)
+{
+    NMBluezManagerPrivate *priv              = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    gs_unref_object NMConnection *connection = NULL;
+    NMSettingsConnection *        added;
+    NMSetting *                   setting;
+    gs_free char *                id = NULL;
+    char                          uuid[37];
+    gs_free_error GError *error = NULL;
+
+    nm_utils_uuid_generate_buf(uuid);
+    id = g_strdup_printf(_("%s Network"), bzobj->d_device.name);
+
+    connection = nm_simple_connection_new();
+
+    setting = nm_setting_connection_new();
+    g_object_set(setting,
+                 NM_SETTING_CONNECTION_ID,
+                 id,
+                 NM_SETTING_CONNECTION_UUID,
+                 uuid,
+                 NM_SETTING_CONNECTION_AUTOCONNECT,
+                 FALSE,
+                 NM_SETTING_CONNECTION_TYPE,
+                 NM_SETTING_BLUETOOTH_SETTING_NAME,
+                 NULL);
+    nm_connection_add_setting(connection, setting);
+
+    setting = nm_setting_bluetooth_new();
+    g_object_set(setting,
+                 NM_SETTING_BLUETOOTH_BDADDR,
+                 bzobj->d_device.address,
+                 NM_SETTING_BLUETOOTH_TYPE,
+                 NM_SETTING_BLUETOOTH_TYPE_PANU,
+                 NULL);
+    nm_connection_add_setting(connection, setting);
+
+    if (!nm_connection_normalize(connection, NULL, NULL, &error)) {
+        _LOGE("connection: couldn't generate a connection for NAP device: %s", error->message);
+        g_return_if_reached();
+    }
+
+    nm_assert(_conn_track_is_relevant_connection(connection, NULL, NULL));
+
+    _LOGT("connection: create in-memory PANU connection %s (%s) for device \"%s\" (%s)",
+          uuid,
+          id,
+          bzobj->d_device.name,
+          bzobj->d_device.address);
+
+    nm_settings_add_connection(priv->settings,
+                               connection,
+                               NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY,
+                               NM_SETTINGS_CONNECTION_ADD_REASON_NONE,
+                               NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED,
+                               &added,
+                               &error);
+    if (!added) {
+        _LOGW("connection: couldn't add new Bluetooth connection for NAP device: '%s' (%s): %s",
+              id,
+              uuid,
+              error->message);
+        return;
+    }
+
+    if (!_conn_track_is_relevant_for_sett_conn(added, NM_BT_CAPABILITY_NAP, bzobj->d_device.address)
+        || !_conn_track_find_elem(self, added) || bzobj->x_device.panu_connection) {
+        _LOGE("connection: something went wrong creating PANU connection %s (%s) for device '%s'",
+              uuid,
+              id,
+              bzobj->d_device.address);
+        g_return_if_reached();
+    }
+
+    bzobj->x_device.panu_connection = added;
+}
+
+/*****************************************************************************/
+
+static void
+_device_state_changed_cb(NMDevice *device,
+                         guint     new_state_u,
+                         guint     old_state_u,
+                         guint     reason_u,
+                         gpointer  user_data)
+{
+    BzDBusObj *bzobj = user_data;
+
+    if (!_bzobjs_device_is_usable(bzobj, NULL, NULL)) {
+        /* the device got unusable? Need to revisit it... */
+        _process_change_idle_schedule(bzobj->self, bzobj);
+    }
+}
+
+static void
+_device_process_change(BzDBusObj *bzobj)
+{
+    NMBluezManager *self                       = bzobj->self;
+    gs_unref_object NMDeviceBt *device_added   = NULL;
+    gs_unref_object NMDeviceBt *device_deleted = NULL;
+    gboolean                    device_is_usable;
+    gboolean                    create_panu_connection = FALSE;
+
+    device_is_usable = _bzobjs_device_is_usable(bzobj, NULL, &create_panu_connection);
+
+    if (create_panu_connection) {
+        bzobj->x_device_panu_connection_allow_create = FALSE;
+        _conn_create_panu_connection(self, bzobj);
+        device_is_usable = _bzobjs_device_is_usable(bzobj, NULL, NULL);
+    } else {
+        if (device_is_usable && bzobj->x_device_panu_connection_allow_create
+            && NM_FLAGS_HAS(bzobj->d_device_capabilities, NM_BT_CAPABILITY_NAP)
+            && _conn_track_find_head(self, NM_BT_CAPABILITY_NAP, bzobj->d_device.address)) {
+            /* We have a useable device and also a panu-connection. We block future attemps
+             * to generate a connection. */
+            bzobj->x_device_panu_connection_allow_create = FALSE;
+        }
+        if (bzobj->x_device.panu_connection) {
+            if (!NM_FLAGS_HAS(nm_settings_connection_get_flags(bzobj->x_device.panu_connection),
+                              NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED)) {
+                /* the connection that we generated earlier still exists, but it's not longer the same
+                 * as it was when we created it. Forget about it, so that we don't delete the profile later... */
+                bzobj->x_device.panu_connection = NULL;
+            } else {
+                if (!device_is_usable
+                    || !_conn_track_is_relevant_for_sett_conn(bzobj->x_device.panu_connection,
+                                                              NM_BT_CAPABILITY_NAP,
+                                                              bzobj->d_device.address)) {
+                    _LOGT("connection: delete in-memory PANU connection %s (%s) as device %s",
+                          nm_settings_connection_get_uuid(bzobj->x_device.panu_connection),
+                          nm_settings_connection_get_id(bzobj->x_device.panu_connection),
+                          !device_is_usable ? "is now unusable" : "no longer matches");
+                    bzobj->x_device_panu_connection_allow_create = TRUE;
+                    nm_settings_connection_delete(g_steal_pointer(&bzobj->x_device.panu_connection),
+                                                  FALSE);
+                }
+            }
+        }
+    }
+
+    bzobj->x_device_is_connected = device_is_usable && _bzobjs_device_is_connected(bzobj);
+
+    bzobj->x_device_is_usable = device_is_usable;
+
+    if (bzobj->x_device.device_bt) {
+        const char *device_to_delete_msg;
+
+        if (!device_is_usable)
+            device_to_delete_msg = "device became unusable";
+        else if (!_nm_device_bt_for_same_device(bzobj->x_device.device_bt,
+                                                bzobj->object_path,
+                                                bzobj->d_device.address,
+                                                NULL,
+                                                bzobj->d_device_capabilities))
+            device_to_delete_msg = "device is no longer compatible";
+        else
+            device_to_delete_msg = NULL;
+
+        if (device_to_delete_msg) {
+            nm_clear_g_signal_handler(bzobj->x_device.device_bt,
+                                      &bzobj->x_device.device_bt_signal_id);
+
+            device_deleted = g_steal_pointer(&bzobj->x_device.device_bt);
+
+            _LOGD("[%s]: drop device because %s", bzobj->object_path, device_to_delete_msg);
+
+            _connect_disconnect(self, bzobj, device_to_delete_msg);
+        }
+    }
+
+    if (device_is_usable) {
+        if (!bzobj->x_device.device_bt) {
+            bzobj->x_device.device_bt = nm_device_bt_new(self,
+                                                         bzobj->object_path,
+                                                         bzobj->d_device.address,
+                                                         bzobj->d_device.name,
+                                                         bzobj->d_device_capabilities);
+            device_added              = g_object_ref(bzobj->x_device.device_bt);
+            bzobj->x_device.device_bt_signal_id =
+                g_signal_connect(device_added,
+                                 NM_DEVICE_STATE_CHANGED,
+                                 G_CALLBACK(_device_state_changed_cb),
+                                 bzobj);
+        } else
+            _nm_device_bt_notify_set_name(bzobj->x_device.device_bt, bzobj->d_device.name);
+
+        _nm_device_bt_notify_set_connected(bzobj->x_device.device_bt, bzobj->x_device_is_connected);
+    }
+
+    if (bzobj->x_device.c_req_data && !bzobj->x_device.c_req_data->int_cancellable
+        && bzobj->x_device_is_connected) {
+        gs_free char *device_name = g_steal_pointer(&bzobj->x_device.c_req_data->device_name);
+
+        _device_connect_req_data_complete(g_steal_pointer(&bzobj->x_device.c_req_data),
+                                          self,
+                                          device_name,
+                                          NULL);
+    }
+
+    if (device_added)
+        g_signal_emit_by_name(self, NM_DEVICE_FACTORY_DEVICE_ADDED, device_added);
+
+    if (device_deleted)
+        _nm_device_bt_notify_removed(device_deleted);
+}
+
+/*****************************************************************************/
+
+static void
+_process_change_idle_all(NMBluezManager *self, gboolean *out_emit_device_availability_changed)
+{
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    BzDBusObj *            bzobj;
+
+    while (
+        (bzobj =
+             c_list_first_entry(&priv->process_change_lst_head, BzDBusObj, process_change_lst))) {
+        c_list_unlink(&bzobj->process_change_lst);
+
+        _LOG_bzobj(bzobj, "before-processing");
+
+        _device_process_change(bzobj);
+
+        _network_server_process_change(bzobj, out_emit_device_availability_changed);
+
+        _LOG_bzobj(bzobj, "after-processing");
+
+        _bzobjs_del_if_dead(bzobj);
+    }
+
+    nm_clear_g_source(&priv->process_change_idle_id);
+}
+
+static gboolean
+_process_change_idle_cb(gpointer user_data)
+{
+    NMBluezManager *       self                             = user_data;
+    NMBluezManagerPrivate *priv                             = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    gboolean               emit_device_availability_changed = FALSE;
+
+    _process_change_idle_all(self, &emit_device_availability_changed);
+
+    if (emit_device_availability_changed)
+        nm_manager_notify_device_availability_maybe_changed(priv->manager);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+_process_change_idle_schedule(NMBluezManager *self, BzDBusObj *bzobj)
+{
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    nm_c_list_move_tail(&priv->process_change_lst_head, &bzobj->process_change_lst);
+    if (priv->process_change_idle_id == 0)
+        priv->process_change_idle_id =
+            g_idle_add_full(G_PRIORITY_DEFAULT_IDLE + 1, _process_change_idle_cb, self, NULL);
+}
+
+static void
+_dbus_process_changes(NMBluezManager *self, BzDBusObj *bzobj, const char *log_reason)
+{
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    gboolean               network_server_is_usable;
+    gboolean               adapter_is_usable_for_device;
+    gboolean               device_is_usable;
+    gboolean               changes                     = FALSE;
+    gboolean               recheck_devices_for_adapter = FALSE;
+
+    nm_assert(bzobj);
+
+    _LOG_bzobj(bzobj, log_reason);
+
+    device_is_usable = _bzobjs_device_is_usable(bzobj, NULL, NULL);
+
+    if (bzobj->x_device_is_usable != device_is_usable)
+        changes = TRUE;
+    else if (bzobj->x_device.device_bt) {
+        if (!device_is_usable)
+            changes = TRUE;
+        else {
+            if (bzobj->x_device_is_connected != _bzobjs_device_is_connected(bzobj)
+                || !_nm_device_bt_for_same_device(bzobj->x_device.device_bt,
+                                                  bzobj->object_path,
+                                                  bzobj->d_device.address,
+                                                  bzobj->d_device.name,
+                                                  bzobj->d_device_capabilities))
+                changes = TRUE;
+        }
+    }
+
+    adapter_is_usable_for_device = _bzobjs_adapter_is_usable_for_device(bzobj);
+    if (adapter_is_usable_for_device != bzobj->was_usable_adapter_for_device_before) {
+        /* this function does not modify bzobj in any other cases except here.
+         * Usually changes are processed delayed, in the idle handler.
+         *
+         * But the bzobj->was_usable_adapter_for_device_before only exists to know whether
+         * we need to re-check device availability. It is correct to set the flag
+         * here, right before we checked. */
+        bzobj->was_usable_adapter_for_device_before = adapter_is_usable_for_device;
+        recheck_devices_for_adapter                 = TRUE;
+        changes                                     = TRUE;
+    }
+
+    if (!changes) {
+        network_server_is_usable = _bzobjs_network_server_is_usable(bzobj, TRUE);
+
+        if (network_server_is_usable != (!c_list_is_empty(&bzobj->x_network_server.lst)))
+            changes = TRUE;
+        else if (bzobj->x_network_server.device_br && !network_server_is_usable)
+            changes = TRUE;
+        else if (!nm_streq0(bzobj->d_has_adapter_iface ? bzobj->d_adapter.address : NULL,
+                            bzobj->x_network_server.adapter_address))
+            changes = TRUE;
+    }
+
+    if (changes)
+        _process_change_idle_schedule(self, bzobj);
+
+    if (recheck_devices_for_adapter) {
+        GHashTableIter iter;
+        BzDBusObj *    bzobj2;
+
+        /* we got a change to the availability of an adapter. We might need to recheck
+         * all devices that use this adapter... */
+        g_hash_table_iter_init(&iter, priv->bzobjs);
+        while (g_hash_table_iter_next(&iter, (gpointer *) &bzobj2, NULL)) {
+            if (bzobj2 == bzobj)
+                continue;
+            if (!nm_streq0(bzobj2->d_device.adapter, bzobj->object_path))
+                continue;
+            if (c_list_is_empty(&bzobj2->process_change_lst))
+                _dbus_process_changes(self, bzobj2, "adapter-changed");
+            else
+                nm_c_list_move_tail(&priv->process_change_lst_head, &bzobj2->process_change_lst);
+        }
+    }
+
+    _bzobjs_del_if_dead(bzobj);
+}
+
+/*****************************************************************************/
+
+#define ALL_RELEVANT_INTERFACE_NAMES          \
+    NM_MAKE_STRV(NM_BLUEZ5_ADAPTER_INTERFACE, \
+                 NM_BLUEZ5_DEVICE_INTERFACE,  \
+                 NM_BLUEZ5_NETWORK_INTERFACE, \
+                 NM_BLUEZ5_NETWORK_SERVER_INTERFACE)
+
+static gboolean
+_dbus_handle_properties_changed(NMBluezManager *   self,
+                                const char *       object_path,
+                                const char *       interface_name,
+                                GVariant *         changed_properties,
+                                const char *const *invalidated_properties,
+                                BzDBusObj **       inout_bzobj)
+{
+    BzDBusObj *  bzobj   = NULL;
+    gboolean     changed = FALSE;
+    const char * property_name;
+    GVariant *   property_value;
+    GVariantIter iter_prop;
+    gsize        i;
+
+    if (!invalidated_properties)
+        invalidated_properties = NM_PTRARRAY_EMPTY(const char *);
+
+    nm_assert(g_variant_is_of_type(changed_properties, G_VARIANT_TYPE("a{sv}")));
+
+    if (inout_bzobj) {
+        bzobj = *inout_bzobj;
+        nm_assert(!bzobj || nm_streq(object_path, bzobj->object_path));
+    }
+
+    if (changed_properties)
+        g_variant_iter_init(&iter_prop, changed_properties);
+
+    if (nm_streq(interface_name, NM_BLUEZ5_ADAPTER_INTERFACE)) {
+        _bzobjs_init(self, &bzobj, object_path);
+        if (!bzobj->d_has_adapter_iface) {
+            changed                    = TRUE;
+            bzobj->d_has_adapter_iface = TRUE;
+        }
+
+        while (changed_properties
+               && g_variant_iter_next(&iter_prop, "{&sv}", &property_name, &property_value)) {
+            _nm_unused gs_unref_variant GVariant *property_value_free = property_value;
+
+            if (nm_streq(property_name, "Address")) {
+                gs_free char *s =
+                    g_variant_is_of_type(property_value, G_VARIANT_TYPE_STRING)
+                        ? nm_utils_hwaddr_canonical(g_variant_get_string(property_value, NULL),
+                                                    ETH_ALEN)
+                        : NULL;
+
+                if (!nm_streq0(bzobj->d_adapter.address, s)) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_adapter.address);
+                    bzobj->d_adapter.address = g_steal_pointer(&s);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Powered")) {
+                bool v = g_variant_is_of_type(property_value, G_VARIANT_TYPE_BOOLEAN)
+                         && g_variant_get_boolean(property_value);
+
+                if (bzobj->d_adapter_powered != v) {
+                    changed                  = TRUE;
+                    bzobj->d_adapter_powered = v;
+                }
+                continue;
+            }
+        }
+
+        for (i = 0; (property_name = invalidated_properties[i]); i++) {
+            if (nm_streq(property_name, "Address")) {
+                if (bzobj->d_adapter.address) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_adapter.address);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Powered")) {
+                if (bzobj->d_adapter_powered) {
+                    changed                  = TRUE;
+                    bzobj->d_adapter_powered = FALSE;
+                }
+                continue;
+            }
+        }
+
+    } else if (nm_streq(interface_name, NM_BLUEZ5_DEVICE_INTERFACE)) {
+        _bzobjs_init(self, &bzobj, object_path);
+        if (!bzobj->d_has_device_iface) {
+            changed                   = TRUE;
+            bzobj->d_has_device_iface = TRUE;
+        }
+
+        while (changed_properties
+               && g_variant_iter_next(&iter_prop, "{&sv}", &property_name, &property_value)) {
+            _nm_unused gs_unref_variant GVariant *property_value_free = property_value;
+
+            if (nm_streq(property_name, "Address")) {
+                gs_free char *s =
+                    g_variant_is_of_type(property_value, G_VARIANT_TYPE_STRING)
+                        ? nm_utils_hwaddr_canonical(g_variant_get_string(property_value, NULL),
+                                                    ETH_ALEN)
+                        : NULL;
+
+                if (!nm_streq0(bzobj->d_device.address, s)) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_device.address);
+                    bzobj->d_device.address = g_steal_pointer(&s);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Name")) {
+                const char *s = g_variant_is_of_type(property_value, G_VARIANT_TYPE_STRING)
+                                    ? g_variant_get_string(property_value, NULL)
+                                    : NULL;
+
+                if (!nm_streq0(bzobj->d_device.name, s)) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_device.name);
+                    bzobj->d_device.name = g_strdup(s);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Adapter")) {
+                const char *s = g_variant_is_of_type(property_value, G_VARIANT_TYPE_OBJECT_PATH)
+                                    ? g_variant_get_string(property_value, NULL)
+                                    : NULL;
+
+                if (!nm_streq0(bzobj->d_device.adapter, s)) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_device.adapter);
+                    bzobj->d_device.adapter = g_strdup(s);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "UUIDs")) {
+                NMBluetoothCapabilities capabilities = NM_BT_CAPABILITY_NONE;
+
+                if (g_variant_is_of_type(property_value, G_VARIANT_TYPE_STRING_ARRAY)) {
+                    gs_free const char **s = g_variant_get_strv(property_value, NULL);
+
+                    capabilities = convert_uuids_to_capabilities(s);
+                }
+                if (bzobj->d_device_capabilities != capabilities) {
+                    changed                      = TRUE;
+                    bzobj->d_device_capabilities = capabilities;
+                    nm_assert(bzobj->d_device_capabilities == capabilities);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Connected")) {
+                bool v = g_variant_is_of_type(property_value, G_VARIANT_TYPE_BOOLEAN)
+                         && g_variant_get_boolean(property_value);
+
+                if (bzobj->d_device_connected != v) {
+                    changed                   = TRUE;
+                    bzobj->d_device_connected = v;
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Paired")) {
+                bool v = g_variant_is_of_type(property_value, G_VARIANT_TYPE_BOOLEAN)
+                         && g_variant_get_boolean(property_value);
+
+                if (bzobj->d_device_paired != v) {
+                    changed                = TRUE;
+                    bzobj->d_device_paired = v;
+                }
+                continue;
+            }
+        }
+
+        for (i = 0; (property_name = invalidated_properties[i]); i++) {
+            if (nm_streq(property_name, "Address")) {
+                if (bzobj->d_device.address) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_device.address);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Name")) {
+                if (bzobj->d_device.name) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_device.name);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Adapter")) {
+                if (bzobj->d_device.adapter) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_device.adapter);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "UUIDs")) {
+                if (bzobj->d_device_capabilities != NM_BT_CAPABILITY_NONE) {
+                    changed                      = TRUE;
+                    bzobj->d_device_capabilities = NM_BT_CAPABILITY_NONE;
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Connected")) {
+                if (bzobj->d_device_connected) {
+                    changed                   = TRUE;
+                    bzobj->d_device_connected = FALSE;
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Paired")) {
+                if (bzobj->d_device_paired) {
+                    changed                = TRUE;
+                    bzobj->d_device_paired = FALSE;
+                }
+                continue;
+            }
+        }
+
+    } else if (nm_streq(interface_name, NM_BLUEZ5_NETWORK_INTERFACE)) {
+        _bzobjs_init(self, &bzobj, object_path);
+        if (!bzobj->d_has_network_iface) {
+            changed                    = TRUE;
+            bzobj->d_has_network_iface = TRUE;
+        }
+
+        while (changed_properties
+               && g_variant_iter_next(&iter_prop, "{&sv}", &property_name, &property_value)) {
+            _nm_unused gs_unref_variant GVariant *property_value_free = property_value;
+
+            if (nm_streq(property_name, "Interface")) {
+                const char *s = g_variant_is_of_type(property_value, G_VARIANT_TYPE_STRING)
+                                    ? g_variant_get_string(property_value, NULL)
+                                    : NULL;
+
+                if (!nm_streq0(bzobj->d_network.interface, s)) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_network.interface);
+                    bzobj->d_network.interface = g_strdup(s);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Connected")) {
+                bool v = g_variant_is_of_type(property_value, G_VARIANT_TYPE_BOOLEAN)
+                         && g_variant_get_boolean(property_value);
+
+                if (bzobj->d_network_connected != v) {
+                    changed                    = TRUE;
+                    bzobj->d_network_connected = v;
+                }
+                continue;
+            }
+        }
+
+        for (i = 0; (property_name = invalidated_properties[i]); i++) {
+            if (nm_streq(property_name, "Interface")) {
+                if (bzobj->d_network.interface) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_network.interface);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Connected")) {
+                if (bzobj->d_network_connected) {
+                    changed                    = TRUE;
+                    bzobj->d_network_connected = FALSE;
+                }
+                continue;
+            }
+        }
+
+    } else if (nm_streq(interface_name, NM_BLUEZ5_NETWORK_SERVER_INTERFACE)) {
+        _bzobjs_init(self, &bzobj, object_path);
+        if (!bzobj->d_has_network_server_iface) {
+            changed                           = TRUE;
+            bzobj->d_has_network_server_iface = TRUE;
+        }
+    }
+
+    nm_assert(!changed || bzobj);
+
+    if (inout_bzobj)
+        *inout_bzobj = bzobj;
+
+    return changed;
+}
+
+static void
+_dbus_handle_interface_added(NMBluezManager *self,
+                             const char *    object_path,
+                             GVariant *      ifaces,
+                             gboolean        initial_get_managed_objects)
+{
+    BzDBusObj *  bzobj   = NULL;
+    gboolean     changed = FALSE;
+    const char * interface_name;
+    GVariant *   changed_properties;
+    GVariantIter iter_ifaces;
+
+    nm_assert(g_variant_is_of_type(ifaces, G_VARIANT_TYPE("a{sa{sv}}")));
+
+    g_variant_iter_init(&iter_ifaces, ifaces);
+    while (g_variant_iter_next(&iter_ifaces, "{&s@a{sv}}", &interface_name, &changed_properties)) {
+        _nm_unused gs_unref_variant GVariant *changed_properties_free = changed_properties;
+
+        if (_dbus_handle_properties_changed(self,
+                                            object_path,
+                                            interface_name,
+                                            changed_properties,
+                                            NULL,
+                                            &bzobj))
+            changed = TRUE;
+    }
+
+    if (changed) {
+        _dbus_process_changes(self,
+                              bzobj,
+                              initial_get_managed_objects ? "dbus-init" : "dbus-iface-added");
+    }
+}
+
+static gboolean
+_dbus_handle_interface_removed(NMBluezManager *   self,
+                               const char *       object_path,
+                               BzDBusObj **       inout_bzobj,
+                               const char *const *removed_interfaces)
+{
+    gboolean   changed = FALSE;
+    BzDBusObj *bzobj;
+    gsize      i;
+
+    if (inout_bzobj && *inout_bzobj) {
+        bzobj = *inout_bzobj;
+        nm_assert(bzobj == _bzobjs_get(self, object_path));
+    } else {
+        bzobj = _bzobjs_get(self, object_path);
+        if (!bzobj)
+            return FALSE;
+        NM_SET_OUT(inout_bzobj, bzobj);
+    }
+
+    for (i = 0; removed_interfaces[i]; i++) {
+        const char *interface_name = removed_interfaces[i];
+
+        if (nm_streq(interface_name, NM_BLUEZ5_ADAPTER_INTERFACE)) {
+            if (bzobj->d_has_adapter_iface) {
+                changed                    = TRUE;
+                bzobj->d_has_adapter_iface = FALSE;
+            }
+            if (bzobj->d_adapter.address) {
+                changed = TRUE;
+                nm_clear_g_free(&bzobj->d_adapter.address);
+            }
+            if (bzobj->d_adapter_powered) {
+                changed                  = TRUE;
+                bzobj->d_adapter_powered = FALSE;
+            }
+        } else if (nm_streq(interface_name, NM_BLUEZ5_DEVICE_INTERFACE)) {
+            if (bzobj->d_has_device_iface) {
+                changed                   = TRUE;
+                bzobj->d_has_device_iface = FALSE;
+            }
+            if (bzobj->d_device.address) {
+                changed = TRUE;
+                nm_clear_g_free(&bzobj->d_device.address);
+            }
+            if (bzobj->d_device.name) {
+                changed = TRUE;
+                nm_clear_g_free(&bzobj->d_device.name);
+            }
+            if (bzobj->d_device.adapter) {
+                changed = TRUE;
+                nm_clear_g_free(&bzobj->d_device.adapter);
+            }
+            if (bzobj->d_device_capabilities != NM_BT_CAPABILITY_NONE) {
+                changed                      = TRUE;
+                bzobj->d_device_capabilities = NM_BT_CAPABILITY_NONE;
+            }
+            if (bzobj->d_device_connected) {
+                changed                   = TRUE;
+                bzobj->d_device_connected = FALSE;
+            }
+            if (bzobj->d_device_paired) {
+                changed                = TRUE;
+                bzobj->d_device_paired = FALSE;
+            }
+        } else if (nm_streq(interface_name, NM_BLUEZ5_NETWORK_INTERFACE)) {
+            if (bzobj->d_has_network_iface) {
+                changed                    = TRUE;
+                bzobj->d_has_network_iface = FALSE;
+            }
+            if (bzobj->d_network.interface) {
+                changed = TRUE;
+                nm_clear_g_free(&bzobj->d_network.interface);
+            }
+            if (bzobj->d_network_connected) {
+                changed                    = TRUE;
+                bzobj->d_network_connected = FALSE;
+            }
+        } else if (nm_streq(interface_name, NM_BLUEZ5_NETWORK_SERVER_INTERFACE)) {
+            if (bzobj->d_has_network_server_iface) {
+                changed                           = TRUE;
+                bzobj->d_has_network_server_iface = FALSE;
+            }
+        }
+    }
+
+    return changed;
+}
+
+static void
+_dbus_managed_objects_changed_cb(GDBusConnection *connection,
+                                 const char *     sender_name,
+                                 const char *     arg_object_path,
+                                 const char *     interface_name,
+                                 const char *     signal_name,
+                                 GVariant *       parameters,
+                                 gpointer         user_data)
+{
+    NMBluezManager *       self  = user_data;
+    NMBluezManagerPrivate *priv  = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    BzDBusObj *            bzobj = NULL;
+    gboolean               changed;
+
+    nm_assert(nm_streq0(interface_name, DBUS_INTERFACE_OBJECT_MANAGER));
+
+    if (priv->get_managed_objects_cancellable) {
+        /* we still wait for the initial GetManagedObjects(). Ignore the event. */
+        return;
+    }
+
+    if (nm_streq(signal_name, "InterfacesAdded")) {
+        gs_unref_variant GVariant *interfaces_and_properties = NULL;
+        const char *               object_path;
+
+        if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(oa{sa{sv}})")))
+            return;
+
+        g_variant_get(parameters, "(&o@a{sa{sv}})", &object_path, &interfaces_and_properties);
+
+        _dbus_handle_interface_added(self, object_path, interfaces_and_properties, FALSE);
+        return;
+    }
+
+    if (nm_streq(signal_name, "InterfacesRemoved")) {
+        gs_free const char **interfaces = NULL;
+        const char *         object_path;
+
+        if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(oas)")))
+            return;
+
+        g_variant_get(parameters, "(&o^a&s)", &object_path, &interfaces);
+
+        changed = _dbus_handle_interface_removed(self, object_path, &bzobj, interfaces);
+        if (changed)
+            _dbus_process_changes(self, bzobj, "dbus-iface-removed");
+        return;
+    }
+}
+
+static void
+_dbus_properties_changed_cb(GDBusConnection *connection,
+                            const char *     sender_name,
+                            const char *     object_path,
+                            const char *     signal_interface_name,
+                            const char *     signal_name,
+                            GVariant *       parameters,
+                            gpointer         user_data)
+{
+    NMBluezManager *       self = user_data;
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    const char *           interface_name;
+    gs_unref_variant GVariant *changed_properties     = NULL;
+    gs_free const char **      invalidated_properties = NULL;
+    BzDBusObj *                bzobj                  = NULL;
+
+    if (priv->get_managed_objects_cancellable) {
+        /* we still wait for the initial GetManagedObjects(). Ignore the event. */
+        return;
+    }
+
+    if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sa{sv}as)")))
+        return;
+
+    g_variant_get(parameters,
+                  "(&s@a{sv}^a&s)",
+                  &interface_name,
+                  &changed_properties,
+                  &invalidated_properties);
+
+    if (_dbus_handle_properties_changed(self,
+                                        object_path,
+                                        interface_name,
+                                        changed_properties,
+                                        invalidated_properties,
+                                        &bzobj))
+        _dbus_process_changes(self, bzobj, "dbus-property-changed");
+}
+
+static void
+_dbus_get_managed_objects_cb(GVariant *result, GError *error, gpointer user_data)
+{
+    NMBluezManager *       self;
+    NMBluezManagerPrivate *priv;
+    GVariantIter           iter;
+    const char *           object_path;
+    GVariant *             ifaces;
+
+    if (!result && nm_utils_error_is_cancelled(error))
+        return;
+
+    self = user_data;
+    priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    g_clear_object(&priv->get_managed_objects_cancellable);
+
+    if (!result) {
+        _LOGT("initial GetManagedObjects() call failed: %s", error->message);
+        _cleanup_for_name_owner(self);
+        return;
+    }
+
+    _LOGT("initial GetManagedObjects call succeeded");
+
+    g_variant_iter_init(&iter, result);
+    while (g_variant_iter_next(&iter, "{&o@a{sa{sv}}}", &object_path, &ifaces)) {
+        _nm_unused gs_unref_variant GVariant *ifaces_free = ifaces;
+
+        _dbus_handle_interface_added(self, object_path, ifaces, TRUE);
+    }
+}
+
+/*****************************************************************************/
+
+static void
+_cleanup_for_name_owner(NMBluezManager *self)
+{
+    NMBluezManagerPrivate *priv                             = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    gboolean               emit_device_availability_changed = FALSE;
+    GHashTableIter         iter;
+    BzDBusObj *            bzobj;
+    gboolean               first = TRUE;
+
+    nm_clear_g_cancellable(&priv->get_managed_objects_cancellable);
+
+    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->managed_objects_changed_id);
+    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->properties_changed_id);
+
+    nm_clear_g_free(&priv->name_owner);
+
+    g_hash_table_iter_init(&iter, priv->bzobjs);
+    while (g_hash_table_iter_next(&iter, (gpointer *) &bzobj, NULL)) {
+        if (first) {
+            first = FALSE;
+            _LOGT("drop all objects form D-Bus cache...");
+        }
+        _dbus_handle_interface_removed(self,
+                                       bzobj->object_path,
+                                       &bzobj,
+                                       ALL_RELEVANT_INTERFACE_NAMES);
+        nm_c_list_move_tail(&priv->process_change_lst_head, &bzobj->process_change_lst);
+    }
+    _process_change_idle_all(self, &emit_device_availability_changed);
+    nm_assert(g_hash_table_size(priv->bzobjs) == 0);
+
+    if (emit_device_availability_changed)
+        nm_manager_notify_device_availability_maybe_changed(priv->manager);
+}
+
+static void
+name_owner_changed(NMBluezManager *self, const char *owner)
+{
+    _nm_unused gs_unref_object NMBluezManager *self_keep_alive = g_object_ref(self);
+    NMBluezManagerPrivate *                    priv            = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    owner = nm_str_not_empty(owner);
+
+    if (!owner)
+        _LOGT("D-Bus name for bluez has no owner");
+    else
+        _LOGT("D-Bus name for bluez has owner %s", owner);
+
+    nm_clear_g_cancellable(&priv->name_owner_get_cancellable);
+
+    if (nm_streq0(priv->name_owner, owner))
+        return;
+
+    _cleanup_for_name_owner(self);
+
+    if (!owner)
+        return;
+
+    priv->name_owner = g_strdup(owner);
+
+    priv->get_managed_objects_cancellable = g_cancellable_new();
+
+    priv->managed_objects_changed_id =
+        nm_dbus_connection_signal_subscribe_object_manager(priv->dbus_connection,
+                                                           priv->name_owner,
+                                                           NM_BLUEZ_MANAGER_PATH,
+                                                           NULL,
+                                                           _dbus_managed_objects_changed_cb,
+                                                           self,
+                                                           NULL);
+
+    priv->properties_changed_id =
+        nm_dbus_connection_signal_subscribe_properties_changed(priv->dbus_connection,
+                                                               priv->name_owner,
+                                                               NULL,
+                                                               NULL,
+                                                               _dbus_properties_changed_cb,
+                                                               self,
+                                                               NULL);
+
+    nm_dbus_connection_call_get_managed_objects(priv->dbus_connection,
+                                                priv->name_owner,
+                                                NM_BLUEZ_MANAGER_PATH,
+                                                G_DBUS_CALL_FLAGS_NO_AUTO_START,
+                                                20000,
+                                                priv->get_managed_objects_cancellable,
+                                                _dbus_get_managed_objects_cb,
+                                                self);
+}
+
+static void
+name_owner_changed_cb(GDBusConnection *connection,
+                      const char *     sender_name,
+                      const char *     object_path,
+                      const char *     interface_name,
+                      const char *     signal_name,
+                      GVariant *       parameters,
+                      gpointer         user_data)
+{
+    NMBluezManager *self = user_data;
+    const char *    new_owner;
+
+    if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)")))
+        return;
+
+    g_variant_get(parameters, "(&s&s&s)", NULL, NULL, &new_owner);
+
+    name_owner_changed(self, new_owner);
+}
+
+static void
+name_owner_get_cb(const char *name_owner, GError *error, gpointer user_data)
+{
+    if (name_owner || !g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        name_owner_changed(user_data, name_owner);
+}
+
+/*****************************************************************************/
+
+static void
+_cleanup_all(NMBluezManager *self)
+{
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    priv->settings_registered = FALSE;
+
+    g_signal_handlers_disconnect_by_func(priv->settings, cp_connection_added, self);
+    g_signal_handlers_disconnect_by_func(priv->settings, cp_connection_updated, self);
+    g_signal_handlers_disconnect_by_func(priv->settings, cp_connection_removed, self);
+
+    g_hash_table_remove_all(priv->conn_data_elems);
+    g_hash_table_remove_all(priv->conn_data_heads);
+
+    _cleanup_for_name_owner(self);
+
+    nm_clear_g_cancellable(&priv->name_owner_get_cancellable);
+
+    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->name_owner_changed_id);
+}
+
+static void
+start(NMDeviceFactory *factory)
+{
+    NMBluezManager *             self;
+    NMBluezManagerPrivate *      priv;
+    NMSettingsConnection *const *sett_conns;
+    guint                        n_sett_conns;
+    guint                        i;
+
+    g_return_if_fail(NM_IS_BLUEZ_MANAGER(factory));
+
+    self = NM_BLUEZ_MANAGER(factory);
+    priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    _cleanup_all(self);
+
+    if (!priv->dbus_connection) {
+        _LOGI("no D-Bus connection available");
+        return;
+    }
+
+    g_signal_connect(priv->settings,
+                     NM_SETTINGS_SIGNAL_CONNECTION_ADDED,
+                     G_CALLBACK(cp_connection_added),
+                     self);
+    g_signal_connect(priv->settings,
+                     NM_SETTINGS_SIGNAL_CONNECTION_UPDATED,
+                     G_CALLBACK(cp_connection_updated),
+                     self);
+    g_signal_connect(priv->settings,
+                     NM_SETTINGS_SIGNAL_CONNECTION_REMOVED,
+                     G_CALLBACK(cp_connection_removed),
+                     self);
+
+    priv->settings_registered = TRUE;
+
+    sett_conns = nm_settings_get_connections(priv->settings, &n_sett_conns);
+    for (i = 0; i < n_sett_conns; i++)
+        _conn_track_update(self, sett_conns[i], TRUE, NULL, NULL, NULL);
+
+    priv->name_owner_changed_id =
+        nm_dbus_connection_signal_subscribe_name_owner_changed(priv->dbus_connection,
+                                                               NM_BLUEZ_SERVICE,
+                                                               name_owner_changed_cb,
+                                                               self,
+                                                               NULL);
+
+    priv->name_owner_get_cancellable = g_cancellable_new();
+
+    nm_dbus_connection_call_get_name_owner(priv->dbus_connection,
+                                           NM_BLUEZ_SERVICE,
+                                           10000,
+                                           priv->name_owner_get_cancellable,
+                                           name_owner_get_cb,
+                                           self);
+}
+
+/*****************************************************************************/
+
+static void
+_connect_returned(NMBluezManager *        self,
+                  BzDBusObj *             bzobj,
+                  NMBluetoothCapabilities bt_type,
+                  const char *            device_name,
+                  NMBluez5DunContext *    dun_context,
+                  GError *                error)
+{
+    char sbuf_cap[100];
+
+    if (error) {
+        nm_assert(!device_name);
+        nm_assert(!dun_context);
+
+        _LOGI("%s [%s]: connect failed: %s",
+              nm_bluetooth_capability_to_string(bzobj->x_device_connect_bt_type,
+                                                sbuf_cap,
+                                                sizeof(sbuf_cap)),
+              bzobj->object_path,
+              error->message);
+
+        _device_connect_req_data_complete(g_steal_pointer(&bzobj->x_device.c_req_data),
+                                          self,
+                                          NULL,
+                                          error);
+        _connect_disconnect(self, bzobj, "cleanup after connect failure");
+        return;
+    }
+
+    nm_assert(bzobj->x_device_connect_bt_type == bt_type);
+    nm_assert(device_name);
+    nm_assert((bt_type == NM_BT_CAPABILITY_DUN) == (!!dun_context));
+    nm_assert(bzobj->x_device.c_req_data);
+
+    g_clear_object(&bzobj->x_device.c_req_data->int_cancellable);
+
+    bzobj->x_device.connect_dun_context = dun_context;
+
+    _LOGD("%s [%s]: connect successful to device %s",
+          nm_bluetooth_capability_to_string(bzobj->x_device_connect_bt_type,
+                                            sbuf_cap,
+                                            sizeof(sbuf_cap)),
+          bzobj->object_path,
+          device_name);
+
+    /* we already have another over-all timer running. But after we connected the device,
+     * we still need to wait for bluez to acknowledge the connected state (via D-Bus, for NAP).
+     * For DUN profiles we likely are already fully connected by now.
+     *
+     * Anyway, schedule another timeout that is possibly shorter than the overall, original
+     * timeout. Now this should go down fast. */
+    bzobj->x_device.c_req_data->timeout_wait_connect_id =
+        g_timeout_add(5000, _connect_timeout_wait_connected_cb, bzobj),
+    bzobj->x_device.c_req_data->device_name = g_strdup(device_name);
+
+    if (_bzobjs_device_is_usable(bzobj, NULL, NULL) && _bzobjs_device_is_connected(bzobj)) {
+        /* We are now connected. Schedule the task that completes the state. */
+        _process_change_idle_schedule(self, bzobj);
+    }
+}
+
+#if WITH_BLUEZ5_DUN
+static void
+_connect_dun_notify_tty_hangup_cb(NMBluez5DunContext *context, gpointer user_data)
+{
+    BzDBusObj *bzobj = user_data;
+
+    _connect_disconnect(bzobj->self, bzobj, "DUN connection hung up");
+}
+
+static void
+_connect_dun_step2_cb(NMBluez5DunContext *context,
+                      const char *        rfcomm_dev,
+                      GError *            error,
+                      gpointer            user_data)
+{
+    BzDBusObj *bzobj;
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    bzobj = user_data;
+
+    if (rfcomm_dev) {
+        /* We want to early notify about the rfcomm path. That is because we might still delay
+         * to signal full activation longer (asynchronously). But the earliest time the callback
+         * is invoked with the rfcomm path, we just created the device synchronously.
+         *
+         * By already notifying the caller about the path early, it avoids a race where ModemManager
+         * would find the modem before the bluetooth code considers the profile fully activated. */
+
+        nm_assert(!error);
+        nm_assert(bzobj->x_device.c_req_data);
+
+        if (!g_cancellable_is_cancelled(bzobj->x_device.c_req_data->ext_cancellable))
+            bzobj->x_device.c_req_data->callback(bzobj->self,
+                                                 FALSE,
+                                                 rfcomm_dev,
+                                                 NULL,
+                                                 bzobj->x_device.c_req_data->callback_user_data);
+
+        if (!context) {
+            /* No context set. This means, we just got notified about the rfcomm path and need to wait
+             * longer, for the next callback. */
+            return;
+        }
+    }
+
+    _connect_returned(bzobj->self, bzobj, NM_BT_CAPABILITY_DUN, rfcomm_dev, context, error);
+}
+
+static void
+_connect_dun_step1_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    gs_unref_variant GVariant *ret = NULL;
+    gs_free_error GError *error    = NULL;
+    BzDBusObj *           bzobj_adapter;
+    BzDBusObj *           bzobj;
+
+    ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), res, &error);
+
+    if (!ret && nm_utils_error_is_cancelled(error))
+        return;
+
+    bzobj = user_data;
+
+    if (error) {
+        _LOGT("DUN: [%s]: bluetooth device connect failed: %s", bzobj->object_path, error->message);
+        /* we actually ignore this error. Let's try, maybe we still can connect via DUN. */
+        g_clear_error(&error);
+    } else
+        _LOGT("DUN: [%s]: bluetooth device connected successfully", bzobj->object_path);
+
+    if (!_bzobjs_device_is_usable(bzobj, &bzobj_adapter, NULL)) {
+        nm_utils_error_set(&error,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           "device %s is not usable for DUN after connect",
+                           bzobj->object_path);
+        _connect_returned(bzobj->self, bzobj, NM_BT_CAPABILITY_DUN, NULL, NULL, error);
+        return;
+    }
+
+    if (!nm_bluez5_dun_connect(bzobj_adapter->d_adapter.address,
+                               bzobj->d_device.address,
+                               bzobj->x_device.c_req_data->int_cancellable,
+                               _connect_dun_step2_cb,
+                               bzobj,
+                               _connect_dun_notify_tty_hangup_cb,
+                               bzobj,
+                               &error)) {
+        _connect_returned(bzobj->self, bzobj, NM_BT_CAPABILITY_DUN, NULL, NULL, error);
+        return;
+    }
+}
+#endif
+
+static void
+_connect_nap_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    gs_unref_variant GVariant *ret                = NULL;
+    const char *               network_iface_name = NULL;
+    gs_free_error GError *error                   = NULL;
+    BzDBusObj *           bzobj;
+
+    ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), res, &error);
+
+    if (!ret && nm_utils_error_is_cancelled(error))
+        return;
+
+    if (ret)
+        g_variant_get(ret, "(&s)", &network_iface_name);
+
+    bzobj = user_data;
+
+    _connect_returned(bzobj->self, bzobj, NM_BT_CAPABILITY_NAP, network_iface_name, NULL, error);
+}
+
+static void
+_connect_cancelled_cb(GCancellable *cancellable, BzDBusObj *bzobj)
+{
+    _connect_disconnect(bzobj->self, bzobj, "connect cancelled");
+}
+
+static gboolean
+_connect_timeout_wait_connected_cb(gpointer user_data)
+{
+    BzDBusObj *bzobj = user_data;
+
+    bzobj->x_device.c_req_data->timeout_wait_connect_id = 0;
+    _connect_disconnect(bzobj->self, bzobj, "timeout waiting for connected");
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+_connect_timeout_cb(gpointer user_data)
+{
+    BzDBusObj *bzobj = user_data;
+
+    bzobj->x_device.c_req_data->timeout_id = 0;
+    _connect_disconnect(bzobj->self, bzobj, "timeout connecting");
+    return G_SOURCE_REMOVE;
+}
+
+static void
+_connect_disconnect(NMBluezManager *self, BzDBusObj *bzobj, const char *reason)
+{
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    DeviceConnectReqData * c_req_data;
+    char                   sbuf_cap[100];
+    gboolean               bt_type;
+
+    if (bzobj->x_device_connect_bt_type == NM_BT_CAPABILITY_NONE) {
+        nm_assert(!bzobj->x_device.c_req_data);
+        return;
+    }
+
+    bt_type = bzobj->x_device_connect_bt_type;
+    nm_assert(NM_IN_SET(bt_type, NM_BT_CAPABILITY_DUN, NM_BT_CAPABILITY_NAP));
+    bzobj->x_device_connect_bt_type = NM_BT_CAPABILITY_NONE;
+
+    c_req_data = g_steal_pointer(&bzobj->x_device.c_req_data);
+
+    _LOGD("%s [%s]: disconnect due to %s",
+          nm_bluetooth_capability_to_string(bt_type, sbuf_cap, sizeof(sbuf_cap)),
+          bzobj->object_path,
+          reason);
+
+    if (c_req_data)
+        nm_clear_g_cancellable(&c_req_data->int_cancellable);
+
+    if (bt_type == NM_BT_CAPABILITY_DUN) {
+        /* For DUN devices, we also called org.bluez.Device1.Connect() (because in order
+         * for nm_bluez5_dun_connect() to succeed, we need to be already connected *why??).
+         *
+         * But upon disconnect we don't call Disconnect() because we don't know whether somebody
+         * else also uses the bluetooth device for other purposes. During disconnect we only
+         * terminate the DUN connection, but don't disconnect entirely. I think that's the
+         * best we can do. */
+#if WITH_BLUEZ5_DUN
+        nm_clear_pointer(&bzobj->x_device.connect_dun_context, nm_bluez5_dun_disconnect);
+#else
+        nm_assert_not_reached();
+#endif
+    } else {
+        if (priv->name_owner) {
+            gs_unref_object GCancellable *cancellable = NULL;
+
+            cancellable = g_cancellable_new();
+
+            nm_shutdown_wait_obj_register_cancellable_full(
+                cancellable,
+                g_strdup_printf("bt-disconnect-nap[%s]", bzobj->object_path),
+                TRUE);
+
+            g_dbus_connection_call(priv->dbus_connection,
+                                   priv->name_owner,
+                                   bzobj->object_path,
+                                   NM_BLUEZ5_NETWORK_INTERFACE,
+                                   "Disconnect",
+                                   g_variant_new("()"),
+                                   NULL,
+                                   G_DBUS_CALL_FLAGS_NO_AUTO_START,
+                                   -1,
+                                   cancellable,
+                                   _dbus_call_complete_cb_nop,
+                                   NULL);
+        }
+    }
+
+    if (c_req_data) {
+        gs_free_error GError *error = NULL;
+
+        nm_utils_error_set(&error, NM_UTILS_ERROR_UNKNOWN, "connect aborted due to %s", reason);
+        _device_connect_req_data_complete(c_req_data, self, NULL, error);
+    }
+}
+
+gboolean
+nm_bluez_manager_connect(NMBluezManager *        self,
+                         const char *            object_path,
+                         NMBluetoothCapabilities connection_bt_type,
+                         int                     timeout_msec,
+                         GCancellable *          cancellable,
+                         NMBluezManagerConnectCb callback,
+                         gpointer                callback_user_data,
+                         GError **               error)
+{
+    gs_unref_object GCancellable *int_cancellable = NULL;
+    DeviceConnectReqData *        c_req_data;
+    NMBluezManagerPrivate *       priv;
+    BzDBusObj *                   bzobj;
+    char                          sbuf_cap[100];
+
+    g_return_val_if_fail(NM_IS_BLUEZ_MANAGER(self), FALSE);
+    g_return_val_if_fail(NM_IN_SET(connection_bt_type, NM_BT_CAPABILITY_DUN, NM_BT_CAPABILITY_NAP),
+                         FALSE);
+    g_return_val_if_fail(callback, FALSE);
+
+    nm_assert(timeout_msec > 0);
+
+    priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    bzobj = _bzobjs_get(self, object_path);
+
+    if (!bzobj) {
+        nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "device %s does not exist", object_path);
+        return FALSE;
+    }
+
+    if (!_bzobjs_device_is_usable(bzobj, NULL, NULL)) {
+        nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "device %s is not usable", object_path);
+        return FALSE;
+    }
+
+    if (!NM_FLAGS_ALL(bzobj->d_device_capabilities, connection_bt_type)) {
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           "device %s has not the required capabilities",
+                           object_path);
+        return FALSE;
+    }
+
+#if !WITH_BLUEZ5_DUN
+    if (connection_bt_type == NM_BT_CAPABILITY_DUN) {
+        nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "DUN is not supported");
+        return FALSE;
+    }
+#endif
+
+    _connect_disconnect(self, bzobj, "new activation");
+
+    _LOGD("%s [%s]: connecting...",
+          nm_bluetooth_capability_to_string(connection_bt_type, sbuf_cap, sizeof(sbuf_cap)),
+          bzobj->object_path);
+
+    int_cancellable = g_cancellable_new();
+
+#if WITH_BLUEZ5_DUN
+    if (connection_bt_type == NM_BT_CAPABILITY_DUN) {
+        g_dbus_connection_call(priv->dbus_connection,
+                               priv->name_owner,
+                               bzobj->object_path,
+                               NM_BLUEZ5_DEVICE_INTERFACE,
+                               "Connect",
+                               NULL,
+                               NULL,
+                               G_DBUS_CALL_FLAGS_NO_AUTO_START,
+                               timeout_msec,
+                               int_cancellable,
+                               _connect_dun_step1_cb,
+                               bzobj);
+    } else
+#endif
+    {
+        nm_assert(connection_bt_type == NM_BT_CAPABILITY_NAP);
+        g_dbus_connection_call(priv->dbus_connection,
+                               priv->name_owner,
+                               bzobj->object_path,
+                               NM_BLUEZ5_NETWORK_INTERFACE,
+                               "Connect",
+                               g_variant_new("(s)", BLUETOOTH_CONNECT_NAP),
+                               G_VARIANT_TYPE("(s)"),
+                               G_DBUS_CALL_FLAGS_NO_AUTO_START,
+                               timeout_msec,
+                               int_cancellable,
+                               _connect_nap_cb,
+                               bzobj);
+    }
+
+    c_req_data  = g_slice_new(DeviceConnectReqData);
+    *c_req_data = (DeviceConnectReqData){
+        .int_cancellable    = g_steal_pointer(&int_cancellable),
+        .ext_cancellable    = g_object_ref(cancellable),
+        .callback           = callback,
+        .callback_user_data = callback_user_data,
+        .ext_cancelled_id =
+            g_signal_connect(cancellable, "cancelled", G_CALLBACK(_connect_cancelled_cb), bzobj),
+        .timeout_id = g_timeout_add(timeout_msec, _connect_timeout_cb, bzobj),
+    };
+
+    bzobj->x_device_connect_bt_type = connection_bt_type;
+    bzobj->x_device.c_req_data      = c_req_data;
+
+    return TRUE;
+}
+
+void
+nm_bluez_manager_disconnect(NMBluezManager *self, const char *object_path)
+{
+    BzDBusObj *bzobj;
+
+    g_return_if_fail(NM_IS_BLUEZ_MANAGER(self));
+    g_return_if_fail(object_path);
+
+    bzobj = _bzobjs_get(self, object_path);
+    if (!bzobj)
+        return;
+
+    _connect_disconnect(self, bzobj, "disconnected by user");
+}
+
+/*****************************************************************************/
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    *out_ignore = TRUE;
+    g_return_val_if_fail(plink->type == NM_LINK_TYPE_BNEP, NULL);
+    return NULL;
+}
+
+static gboolean
+match_connection(NMDeviceFactory *factory, NMConnection *connection)
+{
+    const char *type = nm_connection_get_connection_type(connection);
+
+    nm_assert(nm_streq(type, NM_SETTING_BLUETOOTH_SETTING_NAME));
+
+    if (_nm_connection_get_setting_bluetooth_for_nap(connection))
+        return FALSE; /* handled by the bridge factory */
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+nm_bluez_manager_init(NMBluezManager *self)
+{
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    priv->vtable_network_server = (NMBtVTableNetworkServer){
+        .is_available      = _network_server_vt_is_available,
+        .register_bridge   = _network_server_vt_register_bridge,
+        .unregister_bridge = _network_server_vt_unregister_bridge,
+    };
+
+    c_list_init(&priv->network_server_lst_head);
+    c_list_init(&priv->process_change_lst_head);
+
+    priv->conn_data_heads =
+        g_hash_table_new_full(_conn_data_head_hash, _conn_data_head_equal, g_free, NULL);
+    priv->conn_data_elems = g_hash_table_new_full(nm_pdirect_hash,
+                                                  nm_pdirect_equal,
+                                                  nm_g_slice_free_fcn(ConnDataElem),
+                                                  NULL);
+
+    priv->bzobjs = g_hash_table_new_full(nm_pstr_hash,
+                                         nm_pstr_equal,
+                                         (GDestroyNotify) _bz_dbus_obj_free,
+                                         NULL);
+
+    priv->manager         = g_object_ref(NM_MANAGER_GET);
+    priv->settings        = g_object_ref(NM_SETTINGS_GET);
+    priv->dbus_connection = nm_g_object_ref(NM_MAIN_DBUS_CONNECTION_GET);
+
+    g_atomic_pointer_compare_and_exchange(&nm_bt_vtable_network_server,
+                                          NULL,
+                                          &priv->vtable_network_server);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMBluezManager *       self = NM_BLUEZ_MANAGER(object);
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    /* FIXME(shutdown): we need a nm_device_factory_stop() hook to first unregister all
+     *   BzDBusObj instances and do necessary cleanup actions (like disconnecting devices
+     *   or deleting panu_connection). */
+
+    nm_assert(c_list_is_empty(&priv->network_server_lst_head));
+    nm_assert(c_list_is_empty(&priv->process_change_lst_head));
+    nm_assert(priv->process_change_idle_id == 0);
+
+    g_atomic_pointer_compare_and_exchange(&nm_bt_vtable_network_server,
+                                          &priv->vtable_network_server,
+                                          NULL);
+
+    _cleanup_all(self);
+
+    G_OBJECT_CLASS(nm_bluez_manager_parent_class)->dispose(object);
+
+    g_clear_object(&priv->settings);
+    g_clear_object(&priv->manager);
+    g_clear_object(&priv->dbus_connection);
+
+    nm_clear_pointer(&priv->bzobjs, g_hash_table_destroy);
+}
+
+static void
+nm_bluez_manager_class_init(NMBluezManagerClass *klass)
+{
+    GObjectClass *        object_class  = G_OBJECT_CLASS(klass);
+    NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS(klass);
+
+    object_class->dispose = dispose;
+
+    factory_class->get_supported_types = get_supported_types;
+    factory_class->create_device       = create_device;
+    factory_class->match_connection    = match_connection;
+    factory_class->start               = start;
+}
diff --git a/src/core/devices/bluetooth/nm-bluez-manager.h b/src/core/devices/bluetooth/nm-bluez-manager.h
new file mode 100644
index 00000000..04bfea7d
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-bluez-manager.h
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2009 - 2019 Red Hat, Inc.
+ */
+
+#ifndef __NM_BLUEZ_MANAGER_H__
+#define __NM_BLUEZ_MANAGER_H__
+
+#define NM_TYPE_BLUEZ_MANAGER (nm_bluez_manager_get_type())
+#define NM_BLUEZ_MANAGER(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_BLUEZ_MANAGER, NMBluezManager))
+#define NM_BLUEZ_MANAGER_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_BLUEZ_MANAGER, NMBluezManagerClass))
+#define NM_IS_BLUEZ_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_BLUEZ_MANAGER))
+#define NM_IS_BLUEZ_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_BLUEZ_MANAGER))
+#define NM_BLUEZ_MANAGER_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_BLUEZ_MANAGER, NMBluezManagerClass))
+
+typedef struct _NMBluezManager      NMBluezManager;
+typedef struct _NMBluezManagerClass NMBluezManagerClass;
+
+GType nm_bluez_manager_get_type(void);
+
+typedef void (*NMBluezManagerConnectCb)(
+    NMBluezManager *self,
+    gboolean        is_completed /* or else is early notification with DUN path */,
+    const char *    device_name,
+    GError *        error,
+    gpointer        user_data);
+
+gboolean nm_bluez_manager_connect(NMBluezManager *        self,
+                                  const char *            object_path,
+                                  NMBluetoothCapabilities connection_bt_type,
+                                  int                     timeout_msec,
+                                  GCancellable *          cancellable,
+                                  NMBluezManagerConnectCb callback,
+                                  gpointer                callback_user_data,
+                                  GError **               error);
+
+void nm_bluez_manager_disconnect(NMBluezManager *self, const char *object_path);
+
+#endif /* __NM_BLUEZ_MANAGER_H__ */
diff --git a/src/core/devices/bluetooth/nm-bluez5-dun.c b/src/core/devices/bluetooth/nm-bluez5-dun.c
new file mode 100644
index 00000000..e29884d8
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-bluez5-dun.c
@@ -0,0 +1,857 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include <sys/socket.h>
+#include <bluetooth/sdp.h>
+#include <bluetooth/sdp_lib.h>
+#include <bluetooth/rfcomm.h>
+#include <net/ethernet.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include "nm-bluez5-dun.h"
+#include "nm-bt-error.h"
+#include "NetworkManagerUtils.h"
+
+#define RFCOMM_FMT "/dev/rfcomm%d"
+
+/*****************************************************************************/
+
+typedef struct {
+    GCancellable *       cancellable;
+    NMBluez5DunConnectCb callback;
+    gpointer             callback_user_data;
+
+    sdp_session_t *sdp_session;
+
+    GError *rfcomm_sdp_search_error;
+
+    GSource *source;
+
+    gint64 connect_open_tty_started_at;
+
+    gulong cancelled_id;
+
+    guint8 sdp_session_try_count;
+} ConnectData;
+
+struct _NMBluez5DunContext {
+    const char *dst_str;
+
+    ConnectData *cdat;
+
+    NMBluez5DunNotifyTtyHangupCb notify_tty_hangup_cb;
+    gpointer                     notify_tty_hangup_user_data;
+
+    char *rfcomm_tty_path;
+
+    GSource *rfcomm_tty_poll_source;
+
+    int rfcomm_sock_fd;
+    int rfcomm_tty_fd;
+    int rfcomm_tty_no;
+    int rfcomm_channel;
+
+    bdaddr_t src;
+    bdaddr_t dst;
+
+    char src_str[];
+};
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN      LOGD_BT
+#define _NMLOG_PREFIX_NAME "bluez"
+#define _NMLOG(level, context, ...)                                       \
+    G_STMT_START                                                          \
+    {                                                                     \
+        if (nm_logging_enabled((level), (_NMLOG_DOMAIN))) {               \
+            const NMBluez5DunContext *const _context = (context);         \
+                                                                          \
+            _nm_log((level),                                              \
+                    (_NMLOG_DOMAIN),                                      \
+                    0,                                                    \
+                    NULL,                                                 \
+                    NULL,                                                 \
+                    "%s: DUN[%s] " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),    \
+                    _NMLOG_PREFIX_NAME,                                   \
+                    _context->src_str _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+        }                                                                 \
+    }                                                                     \
+    G_STMT_END
+
+/*****************************************************************************/
+
+static void     _context_invoke_callback_success(NMBluez5DunContext *context);
+static void     _context_invoke_callback_fail_and_free(NMBluez5DunContext *context, GError *error);
+static void     _context_free(NMBluez5DunContext *context);
+static int      _connect_open_tty(NMBluez5DunContext *context);
+static gboolean _connect_sdp_session_start(NMBluez5DunContext *context, GError **error);
+
+/*****************************************************************************/
+
+NM_AUTO_DEFINE_FCN0(NMBluez5DunContext *, _nm_auto_free_context, _context_free);
+#define nm_auto_free_context nm_auto(_nm_auto_free_context)
+
+/*****************************************************************************/
+
+const char *
+nm_bluez5_dun_context_get_adapter(const NMBluez5DunContext *context)
+{
+    return context->src_str;
+}
+
+const char *
+nm_bluez5_dun_context_get_remote(const NMBluez5DunContext *context)
+{
+    return context->dst_str;
+}
+
+const char *
+nm_bluez5_dun_context_get_rfcomm_dev(const NMBluez5DunContext *context)
+{
+    return context->rfcomm_tty_path;
+}
+
+/*****************************************************************************/
+
+static gboolean
+_rfcomm_tty_poll_cb(int fd, GIOCondition condition, gpointer user_data)
+{
+    NMBluez5DunContext *context = user_data;
+
+    _LOGD(context,
+          "receive %s%s%s signal on rfcomm file descriptor",
+          NM_FLAGS_HAS(condition, G_IO_ERR) ? "ERR" : "",
+          NM_FLAGS_ALL(condition, G_IO_HUP | G_IO_ERR) ? "," : "",
+          NM_FLAGS_HAS(condition, G_IO_HUP) ? "HUP" : "");
+
+    nm_clear_g_source_inst(&context->rfcomm_tty_poll_source);
+    context->notify_tty_hangup_cb(context, context->notify_tty_hangup_user_data);
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+_connect_open_tty_retry_cb(gpointer user_data)
+{
+    NMBluez5DunContext *context = user_data;
+    int                 r;
+
+    r = _connect_open_tty(context);
+    if (r >= 0)
+        return G_SOURCE_REMOVE;
+
+    if (nm_utils_get_monotonic_timestamp_nsec()
+        > context->cdat->connect_open_tty_started_at + (30 * 100 * NM_UTILS_NSEC_PER_MSEC)) {
+        gs_free_error GError *error = NULL;
+
+        nm_clear_g_source_inst(&context->cdat->source);
+        g_set_error(&error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "give up waiting to open %s device: %s (%d)",
+                    context->rfcomm_tty_path,
+                    nm_strerror_native(r),
+                    -r);
+        _context_invoke_callback_fail_and_free(context, error);
+        return G_SOURCE_REMOVE;
+    }
+
+    return G_SOURCE_CONTINUE;
+}
+
+static int
+_connect_open_tty(NMBluez5DunContext *context)
+{
+    int fd;
+    int errsv;
+
+    fd = open(context->rfcomm_tty_path, O_RDONLY | O_NOCTTY | O_CLOEXEC);
+    if (fd < 0) {
+        errsv = NM_ERRNO_NATIVE(errno);
+
+        if (!context->cdat->source) {
+            _LOGD(context,
+                  "failed opening tty " RFCOMM_FMT ": %s (%d). Start polling...",
+                  context->rfcomm_tty_no,
+                  nm_strerror_native(errsv),
+                  errsv);
+            context->cdat->connect_open_tty_started_at = nm_utils_get_monotonic_timestamp_nsec();
+            context->cdat->source                      = nm_g_timeout_source_new(100,
+                                                            G_PRIORITY_DEFAULT,
+                                                            _connect_open_tty_retry_cb,
+                                                            context,
+                                                            NULL);
+            g_source_attach(context->cdat->source, NULL);
+        }
+        return -errsv;
+    }
+
+    context->rfcomm_tty_fd = fd;
+
+    context->rfcomm_tty_poll_source = nm_g_unix_fd_source_new(context->rfcomm_tty_fd,
+                                                              G_IO_ERR | G_IO_HUP,
+                                                              G_PRIORITY_DEFAULT,
+                                                              _rfcomm_tty_poll_cb,
+                                                              context,
+                                                              NULL);
+    g_source_attach(context->rfcomm_tty_poll_source, NULL);
+
+    _context_invoke_callback_success(context);
+    return 0;
+}
+
+static void
+_connect_create_rfcomm(NMBluez5DunContext *context)
+{
+    gs_free_error GError *error = NULL;
+    struct rfcomm_dev_req req;
+    int                   devid;
+    int                   errsv;
+    int                   r;
+
+    _LOGD(context, "connected to %s on channel %d", context->dst_str, context->rfcomm_channel);
+
+    /* Create an RFCOMM kernel device for the DUN channel */
+    memset(&req, 0, sizeof(req));
+    req.dev_id  = -1;
+    req.flags   = (1 << RFCOMM_REUSE_DLC) | (1 << RFCOMM_RELEASE_ONHUP);
+    req.channel = context->rfcomm_channel;
+    memcpy(&req.src, &context->src, ETH_ALEN);
+    memcpy(&req.dst, &context->dst, ETH_ALEN);
+    devid = ioctl(context->rfcomm_sock_fd, RFCOMMCREATEDEV, &req);
+    if (devid < 0) {
+        errsv = NM_ERRNO_NATIVE(errno);
+        if (errsv == EBADFD) {
+            /* hm. We use a non-blocking socket to connect. Above getsockopt(SOL_SOCKET,SO_ERROR) indicated
+             * success, but still now we fail with EBADFD. I think that is a bug and we should get the
+             * failure during connect().
+             *
+             * Anyway, craft a less confusing error message than
+             * "failed to create rfcomm device: File descriptor in bad state (77)". */
+            g_set_error(&error,
+                        NM_BT_ERROR,
+                        NM_BT_ERROR_DUN_CONNECT_FAILED,
+                        "unknown failure to connect to DUN device");
+        } else {
+            g_set_error(&error,
+                        NM_BT_ERROR,
+                        NM_BT_ERROR_DUN_CONNECT_FAILED,
+                        "failed to create rfcomm device: %s (%d)",
+                        nm_strerror_native(errsv),
+                        errsv);
+        }
+        _context_invoke_callback_fail_and_free(context, error);
+        return;
+    }
+
+    context->rfcomm_tty_no   = devid;
+    context->rfcomm_tty_path = g_strdup_printf(RFCOMM_FMT, devid);
+
+    r = _connect_open_tty(context);
+    if (r < 0) {
+        /* we created the rfcomm device, but cannot yet open it. That means, we are
+         * not yet fully connected. However, we notify the caller about "what we learned
+         * so far". Note that this happens synchronously.
+         *
+         * The purpose is that once we proceed synchronously, modem-manager races with
+         * the detection of the modem. We want to notify the caller first about the
+         * device name. */
+        context->cdat->callback(NULL,
+                                context->rfcomm_tty_path,
+                                NULL,
+                                context->cdat->callback_user_data);
+    }
+}
+
+static gboolean
+_connect_socket_connect_cb(int fd, GIOCondition condition, gpointer user_data)
+{
+    NMBluez5DunContext *context = user_data;
+    gs_free_error GError *error = NULL;
+    int                   errsv = 0;
+    socklen_t             slen  = sizeof(errsv);
+    int                   r;
+
+    nm_clear_g_source_inst(&context->cdat->source);
+
+    r = getsockopt(context->rfcomm_sock_fd, SOL_SOCKET, SO_ERROR, &errsv, &slen);
+
+    if (r < 0) {
+        errsv = errno;
+        g_set_error(&error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "failed to complete connecting RFCOMM socket: %s (%d)",
+                    nm_strerror_native(errsv),
+                    errsv);
+        _context_invoke_callback_fail_and_free(context, error);
+        return G_SOURCE_REMOVE;
+    }
+
+    if (errsv != 0) {
+        g_set_error(&error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "failed to connect RFCOMM socket: %s (%d)",
+                    nm_strerror_native(errsv),
+                    errsv);
+        _context_invoke_callback_fail_and_free(context, error);
+        return G_SOURCE_REMOVE;
+    }
+
+    _connect_create_rfcomm(context);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+_connect_socket_connect(NMBluez5DunContext *context)
+{
+    gs_free_error GError *error = NULL;
+    struct sockaddr_rc    sa;
+    int                   errsv;
+
+    context->rfcomm_sock_fd =
+        socket(AF_BLUETOOTH, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, BTPROTO_RFCOMM);
+    if (context->rfcomm_sock_fd < 0) {
+        errsv = errno;
+        g_set_error(&error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "failed to create RFCOMM socket: %s (%d)",
+                    nm_strerror_native(errsv),
+                    errsv);
+        _context_invoke_callback_fail_and_free(context, error);
+        return;
+    }
+
+    /* Connect to the remote device */
+    memset(&sa, 0, sizeof(sa));
+    sa.rc_family  = AF_BLUETOOTH;
+    sa.rc_channel = 0;
+    memcpy(&sa.rc_bdaddr, &context->src, ETH_ALEN);
+    if (bind(context->rfcomm_sock_fd, (struct sockaddr *) &sa, sizeof(sa)) != 0) {
+        errsv = errno;
+        g_set_error(&error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "failed to bind socket: %s (%d)",
+                    nm_strerror_native(errsv),
+                    errsv);
+        _context_invoke_callback_fail_and_free(context, error);
+        return;
+    }
+
+    memset(&sa, 0, sizeof(sa));
+    sa.rc_family  = AF_BLUETOOTH;
+    sa.rc_channel = context->rfcomm_channel;
+    memcpy(&sa.rc_bdaddr, &context->dst, ETH_ALEN);
+    if (connect(context->rfcomm_sock_fd, (struct sockaddr *) &sa, sizeof(sa)) != 0) {
+        errsv = errno;
+        if (errsv != EINPROGRESS) {
+            g_set_error(&error,
+                        NM_BT_ERROR,
+                        NM_BT_ERROR_DUN_CONNECT_FAILED,
+                        "failed to connect to remote device: %s (%d)",
+                        nm_strerror_native(errsv),
+                        errsv);
+            _context_invoke_callback_fail_and_free(context, error);
+            return;
+        }
+
+        _LOGD(context,
+              "connecting to %s on channel %d...",
+              context->dst_str,
+              context->rfcomm_channel);
+
+        context->cdat->source = nm_g_unix_fd_source_new(context->rfcomm_sock_fd,
+                                                        G_IO_OUT,
+                                                        G_PRIORITY_DEFAULT,
+                                                        _connect_socket_connect_cb,
+                                                        context,
+                                                        NULL);
+        g_source_attach(context->cdat->source, NULL);
+        return;
+    }
+
+    _connect_create_rfcomm(context);
+}
+
+static void
+_connect_sdp_search_cb(uint8_t type, uint16_t status, uint8_t *rsp, size_t size, void *user_data)
+{
+    NMBluez5DunContext *context = user_data;
+    int                 scanned;
+    int                 seqlen    = 0;
+    int                 bytesleft = size;
+    uint8_t             dataType;
+    int                 channel = -1;
+
+    if (context->cdat->rfcomm_sdp_search_error || context->rfcomm_channel >= 0)
+        return;
+
+    _LOGD(context, "SDP search finished with type=%d status=%d", status, type);
+
+    /* SDP response received */
+    if (status || type != SDP_SVC_SEARCH_ATTR_RSP) {
+        g_set_error(&context->cdat->rfcomm_sdp_search_error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "did not get a Service Discovery response");
+        return;
+    }
+
+    scanned = sdp_extract_seqtype(rsp, bytesleft, &dataType, &seqlen);
+
+    _LOGD(context, "SDP sequence type scanned=%d length=%d", scanned, seqlen);
+
+    scanned = sdp_extract_seqtype(rsp, bytesleft, &dataType, &seqlen);
+    if (!scanned || !seqlen) {
+        /* Short read or unknown sequence type */
+        g_set_error(&context->cdat->rfcomm_sdp_search_error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "improper Service Discovery response");
+        return;
+    }
+
+    rsp += scanned;
+    bytesleft -= scanned;
+    do {
+        sdp_record_t *rec;
+        int           recsize = 0;
+        sdp_list_t *  protos;
+
+        rec = sdp_extract_pdu(rsp, bytesleft, &recsize);
+        if (!rec)
+            break;
+
+        if (!recsize) {
+            sdp_record_free(rec);
+            break;
+        }
+
+        if (sdp_get_access_protos(rec, &protos) == 0) {
+            /* Extract the DUN channel number */
+            channel = sdp_get_proto_port(protos, RFCOMM_UUID);
+            sdp_list_free(protos, NULL);
+
+            _LOGD(context, "SDP channel=%d", channel);
+        }
+        sdp_record_free(rec);
+
+        scanned += recsize;
+        rsp += recsize;
+        bytesleft -= recsize;
+    } while (scanned < (ssize_t) size && bytesleft > 0 && channel < 0);
+
+    if (channel == -1) {
+        g_set_error(&context->cdat->rfcomm_sdp_search_error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "did not receive rfcomm-channel");
+        return;
+    }
+
+    context->rfcomm_channel = channel;
+}
+
+static gboolean
+_connect_sdp_search_io_cb(int fd, GIOCondition condition, gpointer user_data)
+{
+    NMBluez5DunContext *context = user_data;
+    gs_free_error GError *error = NULL;
+    int                   errsv;
+
+    if (condition & (G_IO_ERR | G_IO_HUP | G_IO_NVAL)) {
+        _LOGD(context, "SDP search returned with invalid IO condition 0x%x", (guint) condition);
+        error = g_error_new(NM_BT_ERROR,
+                            NM_BT_ERROR_DUN_CONNECT_FAILED,
+                            "Service Discovery interrupted");
+        nm_clear_g_source_inst(&context->cdat->source);
+        _context_invoke_callback_fail_and_free(context, error);
+        return G_SOURCE_REMOVE;
+    }
+
+    if (sdp_process(context->cdat->sdp_session) == 0) {
+        _LOGD(context, "SDP search still not finished");
+        return G_SOURCE_CONTINUE;
+    }
+
+    nm_clear_g_source_inst(&context->cdat->source);
+
+    if (context->rfcomm_channel < 0 && !context->cdat->rfcomm_sdp_search_error) {
+        errsv = sdp_get_error(context->cdat->sdp_session);
+        _LOGD(context, "SDP search failed: %s (%d)", nm_strerror_native(errsv), errsv);
+        error = g_error_new(NM_BT_ERROR,
+                            NM_BT_ERROR_DUN_CONNECT_FAILED,
+                            "Service Discovery failed with %s (%d)",
+                            nm_strerror_native(errsv),
+                            errsv);
+        _context_invoke_callback_fail_and_free(context, error);
+        return G_SOURCE_REMOVE;
+    }
+
+    if (context->cdat->rfcomm_sdp_search_error) {
+        _LOGD(context,
+              "SDP search failed to complete: %s",
+              context->cdat->rfcomm_sdp_search_error->message);
+        _context_invoke_callback_fail_and_free(context, context->cdat->rfcomm_sdp_search_error);
+        return G_SOURCE_REMOVE;
+    }
+
+    nm_clear_pointer(&context->cdat->sdp_session, sdp_close);
+
+    _connect_socket_connect(context);
+
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+_connect_sdp_session_start_on_idle_cb(gpointer user_data)
+{
+    NMBluez5DunContext *context = user_data;
+    gs_free_error GError *error = NULL;
+
+    nm_clear_g_source_inst(&context->cdat->source);
+
+    _LOGD(context, "retry starting sdp-session...");
+
+    if (!_connect_sdp_session_start(context, &error))
+        _context_invoke_callback_fail_and_free(context, error);
+
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+_connect_sdp_io_cb(int fd, GIOCondition condition, gpointer user_data)
+{
+    NMBluez5DunContext *context = user_data;
+    sdp_list_t *        search;
+    sdp_list_t *        attrs;
+    uuid_t              svclass;
+    uint16_t            attr;
+    int                 errsv;
+    int                 fd_err = 0;
+    int                 r;
+    socklen_t           len     = sizeof(fd_err);
+    gs_free_error GError *error = NULL;
+
+    nm_clear_g_source_inst(&context->cdat->source);
+
+    _LOGD(context, "sdp-session ready to connect with fd=%d", fd);
+
+    if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &fd_err, &len) < 0) {
+        errsv = NM_ERRNO_NATIVE(errno);
+        error = g_error_new(NM_BT_ERROR,
+                            NM_BT_ERROR_DUN_CONNECT_FAILED,
+                            "error for getsockopt on Service Discovery socket: %s (%d)",
+                            nm_strerror_native(errsv),
+                            errsv);
+        goto done;
+    }
+
+    if (fd_err != 0) {
+        errsv = nm_errno_native(fd_err);
+
+        if (NM_IN_SET(errsv, ECONNREFUSED, EHOSTDOWN)
+            && --context->cdat->sdp_session_try_count > 0) {
+            /* *sigh* */
+            _LOGD(context,
+                  "sdp-session failed with %s (%d). Retry in a bit",
+                  nm_strerror_native(errsv),
+                  errsv);
+            nm_clear_g_source_inst(&context->cdat->source);
+            context->cdat->source = nm_g_timeout_source_new(1000,
+                                                            G_PRIORITY_DEFAULT,
+                                                            _connect_sdp_session_start_on_idle_cb,
+                                                            context,
+                                                            NULL);
+            g_source_attach(context->cdat->source, NULL);
+            return G_SOURCE_REMOVE;
+        }
+
+        error = g_error_new(NM_BT_ERROR,
+                            NM_BT_ERROR_DUN_CONNECT_FAILED,
+                            "error on Service Discovery socket: %s (%d)",
+                            nm_strerror_native(errsv),
+                            errsv);
+        goto done;
+    }
+
+    if (sdp_set_notify(context->cdat->sdp_session, _connect_sdp_search_cb, context) < 0) {
+        /* Should not be reached, only can fail if we passed bad sdp_session. */
+        error = g_error_new(NM_BT_ERROR,
+                            NM_BT_ERROR_DUN_CONNECT_FAILED,
+                            "could not set Service Discovery notification");
+        goto done;
+    }
+
+    sdp_uuid16_create(&svclass, DIALUP_NET_SVCLASS_ID);
+    search = sdp_list_append(NULL, &svclass);
+    attr   = SDP_ATTR_PROTO_DESC_LIST;
+    attrs  = sdp_list_append(NULL, &attr);
+
+    r = sdp_service_search_attr_async(context->cdat->sdp_session,
+                                      search,
+                                      SDP_ATTR_REQ_INDIVIDUAL,
+                                      attrs);
+
+    sdp_list_free(attrs, NULL);
+    sdp_list_free(search, NULL);
+
+    if (r < 0) {
+        errsv = nm_errno_native(sdp_get_error(context->cdat->sdp_session));
+        error = g_error_new(NM_BT_ERROR,
+                            NM_BT_ERROR_DUN_CONNECT_FAILED,
+                            "error starting Service Discovery: %s (%d)",
+                            nm_strerror_native(errsv),
+                            errsv);
+        goto done;
+    }
+
+    /* Set callback responsible for update the internal SDP transaction */
+    context->cdat->source = nm_g_unix_fd_source_new(fd,
+                                                    G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
+                                                    G_PRIORITY_DEFAULT,
+                                                    _connect_sdp_search_io_cb,
+                                                    context,
+                                                    NULL);
+    g_source_attach(context->cdat->source, NULL);
+
+done:
+    if (error)
+        _context_invoke_callback_fail_and_free(context, error);
+    return G_SOURCE_REMOVE;
+}
+
+/*****************************************************************************/
+
+static void
+_connect_cancelled_cb(GCancellable *cancellable, NMBluez5DunContext *context)
+{
+    gs_free_error GError *error = NULL;
+
+    if (!g_cancellable_set_error_if_cancelled(cancellable, &error))
+        g_return_if_reached();
+
+    _context_invoke_callback_fail_and_free(context, error);
+}
+
+static gboolean
+_connect_sdp_session_start(NMBluez5DunContext *context, GError **error)
+{
+    nm_assert(context->cdat);
+
+    nm_clear_g_source_inst(&context->cdat->source);
+    nm_clear_pointer(&context->cdat->sdp_session, sdp_close);
+
+    context->cdat->sdp_session = sdp_connect(&context->src, &context->dst, SDP_NON_BLOCKING);
+    if (!context->cdat->sdp_session) {
+        int errsv = nm_errno_native(errno);
+
+        g_set_error(error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "failed to connect to the SDP server: %s (%d)",
+                    nm_strerror_native(errsv),
+                    errsv);
+        return FALSE;
+    }
+
+    context->cdat->source = nm_g_unix_fd_source_new(sdp_get_socket(context->cdat->sdp_session),
+                                                    G_IO_OUT | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
+                                                    G_PRIORITY_DEFAULT,
+                                                    _connect_sdp_io_cb,
+                                                    context,
+                                                    NULL);
+    g_source_attach(context->cdat->source, NULL);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_bluez5_dun_connect(const char *                 adapter,
+                      const char *                 remote,
+                      GCancellable *               cancellable,
+                      NMBluez5DunConnectCb         callback,
+                      gpointer                     callback_user_data,
+                      NMBluez5DunNotifyTtyHangupCb notify_tty_hangup_cb,
+                      gpointer                     notify_tty_hangup_user_data,
+                      GError **                    error)
+{
+    nm_auto_free_context NMBluez5DunContext *context = NULL;
+    ConnectData *                            cdat;
+    gsize                                    src_l;
+    gsize                                    dst_l;
+
+    g_return_val_if_fail(adapter, FALSE);
+    g_return_val_if_fail(remote, FALSE);
+    g_return_val_if_fail(G_IS_CANCELLABLE(cancellable), FALSE);
+    g_return_val_if_fail(callback, FALSE);
+    g_return_val_if_fail(notify_tty_hangup_cb, FALSE);
+    g_return_val_if_fail(!error || !*error, FALSE);
+    nm_assert(!g_cancellable_is_cancelled(cancellable));
+
+    src_l = strlen(adapter) + 1;
+    dst_l = strlen(remote) + 1;
+
+    cdat  = g_slice_new(ConnectData);
+    *cdat = (ConnectData){
+        .callback              = callback,
+        .callback_user_data    = callback_user_data,
+        .cancellable           = g_object_ref(cancellable),
+        .sdp_session_try_count = 5,
+    };
+
+    context  = g_malloc(sizeof(NMBluez5DunContext) + src_l + dst_l);
+    *context = (NMBluez5DunContext){
+        .cdat                        = cdat,
+        .notify_tty_hangup_cb        = notify_tty_hangup_cb,
+        .notify_tty_hangup_user_data = notify_tty_hangup_user_data,
+        .rfcomm_tty_no               = -1,
+        .rfcomm_sock_fd              = -1,
+        .rfcomm_tty_fd               = -1,
+        .rfcomm_channel              = -1,
+    };
+    memcpy(&context->src_str[0], adapter, src_l);
+    context->dst_str = &context->src_str[src_l];
+    memcpy((char *) context->dst_str, remote, dst_l);
+
+    if (str2ba(adapter, &context->src) < 0) {
+        g_set_error(error, NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED, "invalid source");
+        return FALSE;
+    }
+
+    if (str2ba(remote, &context->dst) < 0) {
+        g_set_error(error, NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED, "invalid remote");
+        return FALSE;
+    }
+
+    context->cdat->cancelled_id = g_signal_connect(context->cdat->cancellable,
+                                                   "cancelled",
+                                                   G_CALLBACK(_connect_cancelled_cb),
+                                                   context);
+
+    if (!_connect_sdp_session_start(context, error))
+        return FALSE;
+
+    _LOGD(context, "starting channel number discovery for device %s", context->dst_str);
+
+    g_steal_pointer(&context);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+void
+nm_bluez5_dun_disconnect(NMBluez5DunContext *context)
+{
+    nm_assert(context);
+    nm_assert(!context->cdat);
+
+    _LOGD(context, "disconnecting DUN connection");
+
+    _context_free(context);
+}
+
+/*****************************************************************************/
+
+static void
+_context_cleanup_connect_data(NMBluez5DunContext *context)
+{
+    ConnectData *cdat;
+
+    cdat = g_steal_pointer(&context->cdat);
+    if (!cdat)
+        return;
+
+    nm_clear_g_signal_handler(cdat->cancellable, &cdat->cancelled_id);
+
+    nm_clear_g_source_inst(&cdat->source);
+
+    nm_clear_pointer(&cdat->sdp_session, sdp_close);
+
+    g_clear_object(&cdat->cancellable);
+
+    g_clear_error(&cdat->rfcomm_sdp_search_error);
+
+    nm_g_slice_free(cdat);
+}
+
+static void
+_context_invoke_callback(NMBluez5DunContext *context, GError *error)
+{
+    NMBluez5DunConnectCb callback;
+    gpointer             callback_user_data;
+
+    nm_assert(context);
+    nm_assert(context->cdat);
+    nm_assert(context->cdat->callback);
+    nm_assert(error || context->rfcomm_tty_path);
+
+    if (!error)
+        _LOGD(context, "connected via \"%s\"", context->rfcomm_tty_path);
+    else if (nm_utils_error_is_cancelled(error))
+        _LOGD(context, "cancelled");
+    else
+        _LOGD(context, "failed to connect: %s", error->message);
+
+    callback           = context->cdat->callback;
+    callback_user_data = context->cdat->callback_user_data;
+
+    _context_cleanup_connect_data(context);
+
+    callback(error ? NULL : context,
+             error ? NULL : context->rfcomm_tty_path,
+             error,
+             callback_user_data);
+}
+
+static void
+_context_invoke_callback_success(NMBluez5DunContext *context)
+{
+    nm_assert(context->rfcomm_tty_path);
+    _context_invoke_callback(context, NULL);
+}
+
+static void
+_context_invoke_callback_fail_and_free(NMBluez5DunContext *context, GError *error)
+{
+    nm_assert(error);
+    _context_invoke_callback(context, error);
+    _context_free(context);
+}
+
+static void
+_context_free(NMBluez5DunContext *context)
+{
+    nm_assert(context);
+
+    _context_cleanup_connect_data(context);
+
+    nm_clear_g_source_inst(&context->rfcomm_tty_poll_source);
+
+    if (context->rfcomm_sock_fd >= 0) {
+        if (context->rfcomm_tty_no >= 0) {
+            struct rfcomm_dev_req req;
+
+            memset(&req, 0, sizeof(struct rfcomm_dev_req));
+            req.dev_id             = context->rfcomm_tty_no;
+            context->rfcomm_tty_no = -1;
+            (void) ioctl(context->rfcomm_sock_fd, RFCOMMRELEASEDEV, &req);
+        }
+        nm_close(nm_steal_fd(&context->rfcomm_sock_fd));
+    }
+
+    if (context->rfcomm_tty_fd >= 0)
+        nm_close(nm_steal_fd(&context->rfcomm_tty_fd));
+    nm_clear_g_free(&context->rfcomm_tty_path);
+    g_free(context);
+}
diff --git a/src/core/devices/bluetooth/nm-bluez5-dun.h b/src/core/devices/bluetooth/nm-bluez5-dun.h
new file mode 100644
index 00000000..020d4119
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-bluez5-dun.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#ifndef __NM_BLUEZ5_DUN_H__
+#define __NM_BLUEZ5_DUN_H__
+
+typedef struct _NMBluez5DunContext NMBluez5DunContext;
+
+#if WITH_BLUEZ5_DUN
+
+typedef void (*NMBluez5DunConnectCb)(NMBluez5DunContext *context,
+                                     const char *        rfcomm_dev,
+                                     GError *            error,
+                                     gpointer            user_data);
+
+typedef void (*NMBluez5DunNotifyTtyHangupCb)(NMBluez5DunContext *context, gpointer user_data);
+
+gboolean nm_bluez5_dun_connect(const char *                 adapter,
+                               const char *                 remote,
+                               GCancellable *               cancellable,
+                               NMBluez5DunConnectCb         callback,
+                               gpointer                     callback_user_data,
+                               NMBluez5DunNotifyTtyHangupCb notify_tty_hangup_cb,
+                               gpointer                     notify_tty_hangup_user_data,
+                               GError **                    error);
+
+void nm_bluez5_dun_disconnect(NMBluez5DunContext *context);
+
+const char *nm_bluez5_dun_context_get_adapter(const NMBluez5DunContext *context);
+const char *nm_bluez5_dun_context_get_remote(const NMBluez5DunContext *context);
+const char *nm_bluez5_dun_context_get_rfcomm_dev(const NMBluez5DunContext *context);
+
+#endif /* WITH_BLUEZ5_DUN */
+
+#endif /* __NM_BLUEZ5_DUN_H__ */
diff --git a/src/core/devices/bluetooth/nm-bt-error.c b/src/core/devices/bluetooth/nm-bt-error.c
new file mode 100644
index 00000000..2e49eb7a
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-bt-error.c
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-bt-error.h"
+
+NM_CACHED_QUARK_FCN("nm-bt-error", nm_bt_error_quark);
diff --git a/src/core/devices/bluetooth/nm-bt-error.h b/src/core/devices/bluetooth/nm-bt-error.h
new file mode 100644
index 00000000..432ae5b4
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-bt-error.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#ifndef _NM_BLUEZ5_ERROR_H_
+#define _NM_BLUEZ5_ERROR_H_
+
+typedef enum {
+    NM_BT_ERROR_CONNECTION_NOT_BT = 0,   /*< nick=ConnectionNotBt >*/
+    NM_BT_ERROR_CONNECTION_INVALID,      /*< nick=ConnectionInvalid >*/
+    NM_BT_ERROR_CONNECTION_INCOMPATIBLE, /*< nick=ConnectionIncompatible >*/
+    NM_BT_ERROR_DUN_CONNECT_FAILED,      /*< nick=DunConnectFailed >*/
+} NMBtError;
+
+#define NM_BT_ERROR (nm_bt_error_quark())
+GQuark nm_bt_error_quark(void);
+
+#endif /* _NM_BT_ERROR_H_ */
diff --git a/src/core/devices/bluetooth/nm-device-bt.c b/src/core/devices/bluetooth/nm-device-bt.c
new file mode 100644
index 00000000..c07be2d3
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-device-bt.c
@@ -0,0 +1,1412 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2009 - 2011 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-bt.h"
+
+#include <stdio.h>
+#include <linux/if_ether.h>
+
+#include "nm-core-internal.h"
+#include "nm-bluez-common.h"
+#include "nm-bluez-manager.h"
+#include "devices/nm-device-private.h"
+#include "ppp/nm-ppp-manager.h"
+#include "nm-setting-connection.h"
+#include "nm-setting-bluetooth.h"
+#include "nm-setting-cdma.h"
+#include "nm-setting-gsm.h"
+#include "nm-setting-serial.h"
+#include "nm-setting-ppp.h"
+#include "NetworkManagerUtils.h"
+#include "settings/nm-settings-connection.h"
+#include "nm-utils.h"
+#include "nm-bt-error.h"
+#include "nm-ip4-config.h"
+#include "platform/nm-platform.h"
+
+#include "devices/wwan/nm-modem-manager.h"
+#include "devices/wwan/nm-modem.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceBt
+#include "devices/nm-device-logging.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceBt,
+                             PROP_BT_BDADDR,
+                             PROP_BT_BZ_MGR,
+                             PROP_BT_CAPABILITIES,
+                             PROP_BT_DBUS_PATH,
+                             PROP_BT_NAME, );
+
+enum {
+    PPP_STATS,
+    LAST_SIGNAL,
+};
+
+static guint signals[LAST_SIGNAL] = {0};
+
+typedef struct {
+    NMModemManager *modem_manager;
+
+    NMBluezManager *bz_mgr;
+
+    char *dbus_path;
+
+    char *bdaddr;
+    char *name;
+
+    char *connect_rfcomm_iface;
+
+    GSList *connect_modem_candidates;
+
+    NMModem *modem;
+
+    GCancellable *connect_bz_cancellable;
+
+    gulong connect_watch_link_id;
+
+    guint connect_watch_link_idle_id;
+
+    guint connect_wait_modem_id;
+
+    NMBluetoothCapabilities capabilities : 6;
+
+    NMBluetoothCapabilities connect_bt_type : 6; /* BT type of the current connection */
+
+    NMDeviceStageState stage1_bt_state : 3;
+    NMDeviceStageState stage1_modem_prepare_state : 3;
+
+    bool is_connected : 1;
+
+    bool mm_running : 1;
+
+} NMDeviceBtPrivate;
+
+struct _NMDeviceBt {
+    NMDevice          parent;
+    NMDeviceBtPrivate _priv;
+};
+
+struct _NMDeviceBtClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceBt, nm_device_bt, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_BT_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMDeviceBt, NM_IS_DEVICE_BT, NMDevice)
+
+/*****************************************************************************/
+
+NMBluetoothCapabilities
+nm_device_bt_get_capabilities(NMDeviceBt *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE_BT(self), NM_BT_CAPABILITY_NONE);
+
+    return NM_DEVICE_BT_GET_PRIVATE(self)->capabilities;
+}
+
+static NMBluetoothCapabilities
+get_connection_bt_type(NMConnection *connection)
+{
+    NMSettingBluetooth *s_bt;
+    const char *        bt_type;
+
+    s_bt = nm_connection_get_setting_bluetooth(connection);
+
+    if (s_bt) {
+        bt_type = nm_setting_bluetooth_get_connection_type(s_bt);
+        if (bt_type) {
+            if (nm_streq(bt_type, NM_SETTING_BLUETOOTH_TYPE_DUN))
+                return NM_BT_CAPABILITY_DUN;
+            else if (nm_streq(bt_type, NM_SETTING_BLUETOOTH_TYPE_PANU))
+                return NM_BT_CAPABILITY_NAP;
+        }
+    }
+
+    return NM_BT_CAPABILITY_NONE;
+}
+
+static gboolean
+get_connection_bt_type_check(NMDeviceBt *             self,
+                             NMConnection *           connection,
+                             NMBluetoothCapabilities *out_bt_type,
+                             GError **                error)
+{
+    NMBluetoothCapabilities bt_type;
+
+    bt_type = get_connection_bt_type(connection);
+
+    NM_SET_OUT(out_bt_type, bt_type);
+
+    if (bt_type == NM_BT_CAPABILITY_NONE) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "profile is not a PANU/DUN bluetooth type");
+        return FALSE;
+    }
+
+    if (!NM_FLAGS_ALL(NM_DEVICE_BT_GET_PRIVATE(self)->capabilities, bt_type)) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "device does not support bluetooth type");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *device)
+{
+    return NM_DEVICE_CAP_IS_NON_KERNEL;
+}
+
+static gboolean
+can_auto_connect(NMDevice *device, NMSettingsConnection *sett_conn, char **specific_object)
+{
+    NMDeviceBt *            self = NM_DEVICE_BT(device);
+    NMDeviceBtPrivate *     priv = NM_DEVICE_BT_GET_PRIVATE(self);
+    NMBluetoothCapabilities bt_type;
+
+    nm_assert(!specific_object || !*specific_object);
+
+    if (!NM_DEVICE_CLASS(nm_device_bt_parent_class)->can_auto_connect(device, sett_conn, NULL))
+        return FALSE;
+
+    if (!get_connection_bt_type_check(self,
+                                      nm_settings_connection_get_connection(sett_conn),
+                                      &bt_type,
+                                      NULL))
+        return FALSE;
+
+    /* Can't auto-activate a DUN connection without ModemManager */
+    if (bt_type == NM_BT_CAPABILITY_DUN && priv->mm_running == FALSE)
+        return FALSE;
+
+    return TRUE;
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMDeviceBt *        self = NM_DEVICE_BT(device);
+    NMDeviceBtPrivate * priv = NM_DEVICE_BT_GET_PRIVATE(self);
+    NMSettingBluetooth *s_bt;
+    const char *        bdaddr;
+
+    if (!NM_DEVICE_CLASS(nm_device_bt_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    if (!get_connection_bt_type_check(self, connection, NULL, error))
+        return FALSE;
+
+    s_bt = nm_connection_get_setting_bluetooth(connection);
+
+    bdaddr = nm_setting_bluetooth_get_bdaddr(s_bt);
+    if (!bdaddr) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "profile lacks bdaddr setting");
+        return FALSE;
+    }
+    if (!nm_utils_hwaddr_matches(priv->bdaddr, -1, bdaddr, -1)) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "devices bdaddr setting mismatches");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+check_connection_available(NMDevice *                     device,
+                           NMConnection *                 connection,
+                           NMDeviceCheckConAvailableFlags flags,
+                           const char *                   specific_object,
+                           GError **                      error)
+{
+    NMDeviceBt *            self = NM_DEVICE_BT(device);
+    NMDeviceBtPrivate *     priv = NM_DEVICE_BT_GET_PRIVATE(self);
+    NMBluetoothCapabilities bt_type;
+
+    if (!get_connection_bt_type_check(self, connection, &bt_type, error))
+        return FALSE;
+
+    if (bt_type == NM_BT_CAPABILITY_DUN && !priv->mm_running) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "ModemManager missing for DUN profile");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMDeviceBtPrivate * priv = NM_DEVICE_BT_GET_PRIVATE(device);
+    NMSettingBluetooth *s_bt;
+    const char *        setting_bdaddr;
+    const char *        ctype;
+    gboolean            is_dun = FALSE;
+    gboolean            is_pan = FALSE;
+    NMSettingGsm *      s_gsm;
+    NMSettingCdma *     s_cdma;
+    NMSettingSerial *   s_serial;
+    NMSettingPpp *      s_ppp;
+    const char *        fallback_prefix = NULL, *preferred = NULL;
+
+    s_gsm    = nm_connection_get_setting_gsm(connection);
+    s_cdma   = nm_connection_get_setting_cdma(connection);
+    s_serial = nm_connection_get_setting_serial(connection);
+    s_ppp    = nm_connection_get_setting_ppp(connection);
+
+    s_bt = nm_connection_get_setting_bluetooth(connection);
+    if (!s_bt) {
+        s_bt = (NMSettingBluetooth *) nm_setting_bluetooth_new();
+        nm_connection_add_setting(connection, NM_SETTING(s_bt));
+    }
+
+    ctype = nm_setting_bluetooth_get_connection_type(s_bt);
+    if (ctype) {
+        if (!strcmp(ctype, NM_SETTING_BLUETOOTH_TYPE_DUN))
+            is_dun = TRUE;
+        else if (!strcmp(ctype, NM_SETTING_BLUETOOTH_TYPE_PANU))
+            is_pan = TRUE;
+    } else {
+        if (s_gsm || s_cdma)
+            is_dun = TRUE;
+        else if (priv->capabilities & NM_BT_CAPABILITY_NAP)
+            is_pan = TRUE;
+    }
+
+    if (is_pan) {
+        /* Make sure the device supports PAN */
+        if (!(priv->capabilities & NM_BT_CAPABILITY_NAP)) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                _("PAN requested, but Bluetooth device does not support NAP"));
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_BLUETOOTH_SETTING_NAME,
+                           NM_SETTING_BLUETOOTH_TYPE);
+            return FALSE;
+        }
+
+        /* PAN can't use any DUN-related settings */
+        if (s_gsm || s_cdma || s_serial || s_ppp) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_SETTING,
+                                _("PAN connections cannot specify GSM, CDMA, or serial settings"));
+            g_prefix_error(error,
+                           "%s: ",
+                           s_gsm      ? NM_SETTING_GSM_SETTING_NAME
+                           : s_cdma   ? NM_SETTING_CDMA_SETTING_NAME
+                           : s_serial ? NM_SETTING_SERIAL_SETTING_NAME
+                                      : NM_SETTING_PPP_SETTING_NAME);
+            return FALSE;
+        }
+
+        g_object_set(G_OBJECT(s_bt),
+                     NM_SETTING_BLUETOOTH_TYPE,
+                     NM_SETTING_BLUETOOTH_TYPE_PANU,
+                     NULL);
+
+        fallback_prefix = _("PAN connection");
+    } else if (is_dun) {
+        /* Make sure the device supports PAN */
+        if (!(priv->capabilities & NM_BT_CAPABILITY_DUN)) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                _("DUN requested, but Bluetooth device does not support DUN"));
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_BLUETOOTH_SETTING_NAME,
+                           NM_SETTING_BLUETOOTH_TYPE);
+            return FALSE;
+        }
+
+        /* Need at least a GSM or a CDMA setting */
+        if (!s_gsm && !s_cdma) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_SETTING,
+                                _("DUN connection must include a GSM or CDMA setting"));
+            g_prefix_error(error, "%s: ", NM_SETTING_BLUETOOTH_SETTING_NAME);
+            return FALSE;
+        }
+
+        g_object_set(G_OBJECT(s_bt),
+                     NM_SETTING_BLUETOOTH_TYPE,
+                     NM_SETTING_BLUETOOTH_TYPE_DUN,
+                     NULL);
+
+        if (s_gsm) {
+            fallback_prefix = _("GSM connection");
+        } else {
+            fallback_prefix = _("CDMA connection");
+            if (!nm_setting_cdma_get_number(s_cdma))
+                g_object_set(G_OBJECT(s_cdma), NM_SETTING_CDMA_NUMBER, "#777", NULL);
+        }
+    } else {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("Unknown/unhandled Bluetooth connection type"));
+        g_prefix_error(error,
+                       "%s.%s: ",
+                       NM_SETTING_BLUETOOTH_SETTING_NAME,
+                       NM_SETTING_BLUETOOTH_TYPE);
+        return FALSE;
+    }
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_BLUETOOTH_SETTING_NAME,
+                              existing_connections,
+                              preferred,
+                              fallback_prefix,
+                              NULL,
+                              NULL,
+                              is_dun ? FALSE : TRUE); /* No IPv6 yet for DUN */
+
+    setting_bdaddr = nm_setting_bluetooth_get_bdaddr(s_bt);
+    if (setting_bdaddr) {
+        /* Make sure the setting BT Address (if any) matches the device's */
+        if (!nm_utils_hwaddr_matches(setting_bdaddr, -1, priv->bdaddr, -1)) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                _("connection does not match device"));
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_BLUETOOTH_SETTING_NAME,
+                           NM_SETTING_BLUETOOTH_BDADDR);
+            return FALSE;
+        }
+    } else {
+        /* Lock the connection to this device by default */
+        if (!nm_utils_hwaddr_matches(priv->bdaddr, -1, NULL, ETH_ALEN))
+            g_object_set(G_OBJECT(s_bt), NM_SETTING_BLUETOOTH_BDADDR, priv->bdaddr, NULL);
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+/* IP method PPP */
+
+static void
+ppp_stats(NMModem *modem, guint i_in_bytes, guint i_out_bytes, gpointer user_data)
+{
+    guint32 in_bytes  = i_in_bytes;
+    guint32 out_bytes = i_out_bytes;
+
+    g_signal_emit(NM_DEVICE_BT(user_data),
+                  signals[PPP_STATS],
+                  0,
+                  (guint) in_bytes,
+                  (guint) out_bytes);
+}
+
+static void
+ppp_failed(NMModem *modem, guint i_reason, gpointer user_data)
+{
+    NMDevice *          device = NM_DEVICE(user_data);
+    NMDeviceBt *        self   = NM_DEVICE_BT(user_data);
+    NMDeviceStateReason reason = i_reason;
+
+    switch (nm_device_get_state(device)) {
+    case NM_DEVICE_STATE_PREPARE:
+    case NM_DEVICE_STATE_CONFIG:
+    case NM_DEVICE_STATE_NEED_AUTH:
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, reason);
+        break;
+    case NM_DEVICE_STATE_IP_CONFIG:
+    case NM_DEVICE_STATE_IP_CHECK:
+    case NM_DEVICE_STATE_SECONDARIES:
+    case NM_DEVICE_STATE_ACTIVATED:
+        if (nm_device_activate_ip4_state_in_conf(device))
+            nm_device_activate_schedule_ip_config_timeout(device, AF_INET);
+        else if (nm_device_activate_ip6_state_in_conf(device))
+            nm_device_activate_schedule_ip_config_timeout(device, AF_INET6);
+        else if (nm_device_activate_ip4_state_done(device)) {
+            nm_device_ip_method_failed(device,
+                                       AF_INET,
+                                       NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        } else if (nm_device_activate_ip6_state_done(device)) {
+            nm_device_ip_method_failed(device,
+                                       AF_INET6,
+                                       NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        } else {
+            _LOGW(LOGD_MB,
+                  "PPP failure in unexpected state %u",
+                  (guint) nm_device_get_state(device));
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        }
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+modem_auth_requested(NMModem *modem, gpointer user_data)
+{
+    NMDevice *device = NM_DEVICE(user_data);
+
+    /* Auth requests (PIN, PAP/CHAP passwords, etc) only get handled
+     * during activation.
+     */
+    if (!nm_device_is_activating(device))
+        return;
+
+    nm_device_state_changed(device, NM_DEVICE_STATE_NEED_AUTH, NM_DEVICE_STATE_REASON_NONE);
+}
+
+static void
+modem_auth_result(NMModem *modem, GError *error, gpointer user_data)
+{
+    NMDevice *         device = NM_DEVICE(user_data);
+    NMDeviceBtPrivate *priv   = NM_DEVICE_BT_GET_PRIVATE(device);
+
+    g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_NEED_AUTH);
+
+    if (error) {
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS);
+        return;
+    }
+
+    priv->stage1_modem_prepare_state = NM_DEVICE_STAGE_STATE_INIT;
+    nm_device_activate_schedule_stage1_device_prepare(device, FALSE);
+}
+
+static void
+modem_prepare_result(NMModem *modem, gboolean success, guint i_reason, gpointer user_data)
+{
+    NMDeviceBt *        self   = user_data;
+    NMDeviceBtPrivate * priv   = NM_DEVICE_BT_GET_PRIVATE(self);
+    NMDeviceStateReason reason = i_reason;
+    NMDeviceState       state;
+
+    state = nm_device_get_state(NM_DEVICE(self));
+
+    g_return_if_fail(NM_IN_SET(state, NM_DEVICE_STATE_PREPARE, NM_DEVICE_STATE_NEED_AUTH));
+
+    nm_assert(priv->stage1_modem_prepare_state == NM_DEVICE_STAGE_STATE_PENDING);
+
+    if (!success) {
+        if (nm_device_state_reason_check(reason) == NM_DEVICE_STATE_REASON_SIM_PIN_INCORRECT) {
+            /* If the connect failed because the SIM PIN was wrong don't allow
+             * the device to be auto-activated anymore, which would risk locking
+             * the SIM if the incorrect PIN continues to be used.
+             */
+            nm_device_autoconnect_blocked_set(NM_DEVICE(self),
+                                              NM_DEVICE_AUTOCONNECT_BLOCKED_WRONG_PIN);
+        }
+
+        nm_device_state_changed(NM_DEVICE(self), NM_DEVICE_STATE_FAILED, reason);
+        return;
+    }
+
+    priv->stage1_modem_prepare_state = NM_DEVICE_STAGE_STATE_COMPLETED;
+    nm_device_activate_schedule_stage1_device_prepare(NM_DEVICE(self), FALSE);
+}
+
+static void
+device_state_changed(NMDevice *          device,
+                     NMDeviceState       new_state,
+                     NMDeviceState       old_state,
+                     NMDeviceStateReason reason)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(device);
+
+    if (priv->modem)
+        nm_modem_device_state_changed(priv->modem, new_state, old_state);
+
+    /* Need to recheck available connections whenever MM appears or disappears,
+     * since the device could be both DUN and NAP capable and thus may not
+     * change state (which rechecks available connections) when MM comes and goes.
+     */
+    if (priv->mm_running && NM_FLAGS_HAS(priv->capabilities, NM_BT_CAPABILITY_DUN))
+        nm_device_recheck_available_connections(device);
+}
+
+static void
+modem_ip4_config_result(NMModem *modem, NMIP4Config *config, GError *error, gpointer user_data)
+{
+    NMDeviceBt *self   = NM_DEVICE_BT(user_data);
+    NMDevice *  device = NM_DEVICE(self);
+
+    g_return_if_fail(nm_device_activate_ip4_state_in_conf(device) == TRUE);
+
+    if (error) {
+        _LOGW(LOGD_MB | LOGD_IP4 | LOGD_BT,
+              "retrieving IP4 configuration failed: %s",
+              error->message);
+        nm_device_ip_method_failed(device, AF_INET, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        return;
+    }
+
+    nm_device_activate_schedule_ip_config_result(device, AF_INET, NM_IP_CONFIG_CAST(config));
+}
+
+static void
+ip_ifindex_changed_cb(NMModem *modem, GParamSpec *pspec, gpointer user_data)
+{
+    NMDevice *device = NM_DEVICE(user_data);
+
+    if (!nm_device_is_activating(device))
+        return;
+
+    if (!nm_device_set_ip_ifindex(device, nm_modem_get_ip_ifindex(modem))) {
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+    }
+}
+
+/*****************************************************************************/
+
+static void
+modem_cleanup(NMDeviceBt *self)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    if (priv->modem) {
+        g_signal_handlers_disconnect_matched(priv->modem,
+                                             G_SIGNAL_MATCH_DATA,
+                                             0,
+                                             0,
+                                             NULL,
+                                             NULL,
+                                             self);
+        nm_clear_pointer(&priv->modem, nm_modem_unclaim);
+    }
+}
+
+static void
+modem_state_cb(NMModem *modem, int new_state_i, int old_state_i, gpointer user_data)
+{
+    NMModemState  new_state = new_state_i;
+    NMModemState  old_state = old_state_i;
+    NMDevice *    device    = NM_DEVICE(user_data);
+    NMDeviceState dev_state = nm_device_get_state(device);
+
+    if (new_state <= NM_MODEM_STATE_DISABLING && old_state > NM_MODEM_STATE_DISABLING) {
+        /* Will be called whenever something external to NM disables the
+         * modem directly through ModemManager.
+         */
+        if (nm_device_is_activating(device) || dev_state == NM_DEVICE_STATE_ACTIVATED) {
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_DISCONNECTED,
+                                    NM_DEVICE_STATE_REASON_USER_REQUESTED);
+            return;
+        }
+    }
+
+    if (new_state < NM_MODEM_STATE_CONNECTING && old_state >= NM_MODEM_STATE_CONNECTING
+        && dev_state >= NM_DEVICE_STATE_NEED_AUTH && dev_state <= NM_DEVICE_STATE_ACTIVATED) {
+        /* Fail the device if the modem disconnects unexpectedly while the
+         * device is activating/activated. */
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER);
+        return;
+    }
+}
+
+static void
+modem_removed_cb(NMModem *modem, gpointer user_data)
+{
+    NMDeviceBt *  self = NM_DEVICE_BT(user_data);
+    NMDeviceState state;
+
+    state = nm_device_get_state(NM_DEVICE(self));
+    if (nm_device_is_activating(NM_DEVICE(self)) || state == NM_DEVICE_STATE_ACTIVATED) {
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_BT_FAILED);
+        return;
+    }
+
+    modem_cleanup(self);
+}
+
+static gboolean
+modem_try_claim(NMDeviceBt *self, NMModem *modem)
+{
+    NMDeviceBtPrivate *priv             = NM_DEVICE_BT_GET_PRIVATE(self);
+    gs_free char *     rfcomm_base_name = NULL;
+    NMDeviceState      state;
+
+    if (priv->modem) {
+        if (priv->modem == modem)
+            return TRUE;
+        return FALSE;
+    }
+
+    if (nm_modem_is_claimed(modem))
+        return FALSE;
+
+    if (!priv->connect_rfcomm_iface)
+        return FALSE;
+
+    rfcomm_base_name = g_path_get_basename(priv->connect_rfcomm_iface);
+    if (!nm_streq0(rfcomm_base_name, nm_modem_get_control_port(modem)))
+        return FALSE;
+
+    /* Can only accept the modem in stage1, but since the interface matched
+     * what we were expecting, don't let anything else claim the modem either.
+     */
+    state = nm_device_get_state(NM_DEVICE(self));
+    if (state != NM_DEVICE_STATE_PREPARE) {
+        _LOGD(LOGD_BT | LOGD_MB,
+              "modem found but device not in correct state (%d)",
+              nm_device_get_state(NM_DEVICE(self)));
+        return FALSE;
+    }
+
+    priv->modem                      = nm_modem_claim(modem);
+    priv->stage1_modem_prepare_state = NM_DEVICE_STAGE_STATE_INIT;
+
+    g_signal_connect(modem, NM_MODEM_PPP_STATS, G_CALLBACK(ppp_stats), self);
+    g_signal_connect(modem, NM_MODEM_PPP_FAILED, G_CALLBACK(ppp_failed), self);
+    g_signal_connect(modem, NM_MODEM_PREPARE_RESULT, G_CALLBACK(modem_prepare_result), self);
+    g_signal_connect(modem, NM_MODEM_IP4_CONFIG_RESULT, G_CALLBACK(modem_ip4_config_result), self);
+    g_signal_connect(modem, NM_MODEM_AUTH_REQUESTED, G_CALLBACK(modem_auth_requested), self);
+    g_signal_connect(modem, NM_MODEM_AUTH_RESULT, G_CALLBACK(modem_auth_result), self);
+    g_signal_connect(modem, NM_MODEM_STATE_CHANGED, G_CALLBACK(modem_state_cb), self);
+    g_signal_connect(modem, NM_MODEM_REMOVED, G_CALLBACK(modem_removed_cb), self);
+    g_signal_connect(modem,
+                     "notify::" NM_MODEM_IP_IFINDEX,
+                     G_CALLBACK(ip_ifindex_changed_cb),
+                     self);
+
+    _LOGD(LOGD_BT | LOGD_MB, "modem found");
+
+    return TRUE;
+}
+
+static void
+mm_modem_added_cb(NMModemManager *manager, NMModem *modem, gpointer user_data)
+{
+    NMDeviceBt *       self = user_data;
+    NMDeviceBtPrivate *priv;
+
+    if (!modem_try_claim(user_data, modem))
+        return;
+
+    priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    if (priv->stage1_bt_state == NM_DEVICE_STAGE_STATE_COMPLETED)
+        nm_device_activate_schedule_stage1_device_prepare(NM_DEVICE(self), FALSE);
+}
+
+/*****************************************************************************/
+
+void
+_nm_device_bt_notify_set_connected(NMDeviceBt *self, gboolean connected)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    connected = !!connected;
+    if (priv->is_connected == connected)
+        return;
+
+    priv->is_connected = connected;
+
+    if (connected || priv->stage1_bt_state != NM_DEVICE_STAGE_STATE_COMPLETED
+        || nm_device_get_state(NM_DEVICE(self)) > NM_DEVICE_STATE_ACTIVATED) {
+        _LOGT(LOGD_BT, "set-connected: %d", connected);
+        return;
+    }
+
+    _LOGT(LOGD_BT, "set-connected: %d (disconnecting device...)", connected);
+    nm_device_state_changed(NM_DEVICE(self),
+                            NM_DEVICE_STATE_FAILED,
+                            NM_DEVICE_STATE_REASON_CARRIER);
+}
+
+static gboolean
+connect_watch_link_idle_cb(gpointer user_data)
+{
+    NMDeviceBt *       self = user_data;
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+    int                ifindex;
+
+    priv->connect_watch_link_idle_id = 0;
+
+    if (nm_device_get_state(NM_DEVICE(self)) <= NM_DEVICE_STATE_ACTIVATED) {
+        ifindex = nm_device_get_ip_ifindex(NM_DEVICE(self));
+        if (ifindex > 0
+            && !nm_platform_link_get(nm_device_get_platform(NM_DEVICE(self)), ifindex)) {
+            _LOGT(LOGD_BT, "device disappeared");
+            nm_device_state_changed(NM_DEVICE(self),
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_BT_FAILED);
+        }
+    }
+
+    return G_SOURCE_REMOVE;
+}
+
+static void
+connect_watch_link_cb(NMPlatform *    platform,
+                      int             obj_type_i,
+                      int             ifindex,
+                      NMPlatformLink *info,
+                      int             change_type_i,
+                      NMDevice *      self)
+{
+    const NMPlatformSignalChangeType change_type = change_type_i;
+    NMDeviceBtPrivate *              priv;
+
+    /* bluez doesn't notify us when the connection disconnects.
+     * Neither does NMManager (or NMDevice) tell us when the ip-ifindex goes away.
+     * This is horrible, and should be improved. For now, watch the link ourself... */
+
+    if (NM_IN_SET(change_type, NM_PLATFORM_SIGNAL_CHANGED, NM_PLATFORM_SIGNAL_REMOVED)) {
+        priv = NM_DEVICE_BT_GET_PRIVATE(self);
+        if (priv->connect_watch_link_idle_id == 0)
+            priv->connect_watch_link_idle_id = g_idle_add(connect_watch_link_idle_cb, self);
+    }
+}
+
+static gboolean
+connect_wait_modem_timeout(gpointer user_data)
+{
+    NMDeviceBt *       self = NM_DEVICE_BT(user_data);
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    /* since this timeout is longer than the connect timeout, we must have already
+     * hit the connect-timeout first or being connected. */
+    nm_assert(priv->stage1_bt_state == NM_DEVICE_STAGE_STATE_COMPLETED);
+
+    priv->connect_wait_modem_id = 0;
+    nm_clear_g_cancellable(&priv->connect_bz_cancellable);
+
+    if (priv->modem)
+        _LOGD(LOGD_BT, "timeout connecting modem for DUN connection");
+    else
+        _LOGD(LOGD_BT, "timeout finding modem for DUN connection");
+
+    nm_device_state_changed(NM_DEVICE(self),
+                            NM_DEVICE_STATE_FAILED,
+                            NM_DEVICE_STATE_REASON_MODEM_NOT_FOUND);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+connect_bz_cb(NMBluezManager *bz_mgr,
+              gboolean        is_complete,
+              const char *    device_name,
+              GError *        error,
+              gpointer        user_data)
+{
+    NMDeviceBt *       self;
+    NMDeviceBtPrivate *priv;
+    char               sbuf[100];
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    self = user_data;
+    priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    nm_assert(nm_device_is_activating(NM_DEVICE(self)));
+    nm_assert(NM_IN_SET((NMBluetoothCapabilities) priv->connect_bt_type,
+                        NM_BT_CAPABILITY_DUN,
+                        NM_BT_CAPABILITY_NAP));
+
+    if (!is_complete) {
+        nm_assert(priv->connect_bt_type == NM_BT_CAPABILITY_DUN);
+        nm_assert(device_name);
+        nm_assert(!error);
+
+        if (!nm_streq0(priv->connect_rfcomm_iface, device_name)) {
+            nm_assert(!priv->connect_rfcomm_iface);
+            _LOGD(LOGD_BT,
+                  "DUN is still connecting but got serial port \"%s\" to claim modem",
+                  device_name);
+            g_free(priv->connect_rfcomm_iface);
+            priv->connect_rfcomm_iface = g_strdup(device_name);
+        }
+        return;
+    }
+
+    g_clear_object(&priv->connect_bz_cancellable);
+
+    if (!device_name) {
+        _LOGW(LOGD_BT,
+              "%s connect request failed: %s",
+              nm_bluetooth_capability_to_string(priv->connect_bt_type, sbuf, sizeof(sbuf)),
+              error->message);
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_BT_FAILED);
+        return;
+    }
+
+    _LOGD(LOGD_BT,
+          "%s connect request successful (%s)",
+          nm_bluetooth_capability_to_string(priv->connect_bt_type, sbuf, sizeof(sbuf)),
+          device_name);
+
+    if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN) {
+        if (!nm_streq0(priv->connect_rfcomm_iface, device_name)) {
+            nm_assert_not_reached();
+            g_free(priv->connect_rfcomm_iface);
+            priv->connect_rfcomm_iface = g_strdup(device_name);
+        }
+    } else {
+        nm_assert(priv->connect_bt_type == NM_BT_CAPABILITY_NAP);
+        if (!nm_device_set_ip_iface(NM_DEVICE(self), device_name)) {
+            _LOGW(LOGD_BT, "Error connecting with bluez: cannot find device %s", device_name);
+            nm_device_state_changed(NM_DEVICE(self),
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_BT_FAILED);
+            return;
+        }
+        priv->connect_watch_link_id = g_signal_connect(nm_device_get_platform(NM_DEVICE(self)),
+                                                       NM_PLATFORM_SIGNAL_LINK_CHANGED,
+                                                       G_CALLBACK(connect_watch_link_cb),
+                                                       self);
+    }
+
+    if (!priv->is_connected) {
+        /* we got the callback from NMBluezManager with success. We actually should be
+         * connected and this line shouldn't be reached. */
+        nm_assert_not_reached();
+        _LOGE(LOGD_BT, "bluetooth is unexpectedly not in connected state");
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_BT_FAILED);
+        return;
+    }
+
+    priv->stage1_bt_state = NM_DEVICE_STAGE_STATE_COMPLETED;
+    nm_device_activate_schedule_stage1_device_prepare(NM_DEVICE(self), FALSE);
+}
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceBt *       self     = NM_DEVICE_BT(device);
+    NMDeviceBtPrivate *priv     = NM_DEVICE_BT_GET_PRIVATE(self);
+    gs_free_error GError *error = NULL;
+    NMConnection *        connection;
+
+    connection = nm_device_get_applied_connection(device);
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+
+    priv->connect_bt_type = get_connection_bt_type(connection);
+    if (priv->connect_bt_type == NM_BT_CAPABILITY_NONE) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_BT_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN && !priv->mm_running) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_MODEM_MANAGER_UNAVAILABLE);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    if (priv->stage1_bt_state == NM_DEVICE_STAGE_STATE_PENDING)
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+    else if (priv->stage1_bt_state == NM_DEVICE_STAGE_STATE_INIT) {
+        gs_unref_object GCancellable *cancellable = NULL;
+        char                          sbuf[100];
+
+        _LOGD(LOGD_BT,
+              "connecting to %s bluetooth device",
+              nm_bluetooth_capability_to_string(priv->connect_bt_type, sbuf, sizeof(sbuf)));
+
+        cancellable = g_cancellable_new();
+
+        if (!nm_bluez_manager_connect(priv->bz_mgr,
+                                      priv->dbus_path,
+                                      priv->connect_bt_type,
+                                      30000,
+                                      cancellable,
+                                      connect_bz_cb,
+                                      self,
+                                      &error)) {
+            _LOGD(LOGD_BT, "cannot connect to bluetooth device: %s", error->message);
+            *out_failure_reason = NM_DEVICE_STATE_REASON_BT_FAILED;
+            return NM_ACT_STAGE_RETURN_FAILURE;
+        }
+
+        priv->connect_bz_cancellable = g_steal_pointer(&cancellable);
+        priv->stage1_bt_state        = NM_DEVICE_STAGE_STATE_PENDING;
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+    }
+
+    if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN) {
+        if (!priv->modem) {
+            gs_free NMModem **modems = NULL;
+            guint             i, n;
+
+            if (priv->connect_wait_modem_id == 0)
+                priv->connect_wait_modem_id =
+                    g_timeout_add_seconds(30, connect_wait_modem_timeout, self);
+
+            modems = nm_modem_manager_get_modems(priv->modem_manager, &n);
+            for (i = 0; i < n; i++) {
+                if (modem_try_claim(self, modems[i]))
+                    break;
+            }
+            if (!priv->modem)
+                return NM_ACT_STAGE_RETURN_POSTPONE;
+        }
+
+        if (priv->stage1_modem_prepare_state == NM_DEVICE_STAGE_STATE_PENDING)
+            return NM_ACT_STAGE_RETURN_POSTPONE;
+        if (priv->stage1_modem_prepare_state == NM_DEVICE_STAGE_STATE_INIT) {
+            priv->stage1_modem_prepare_state = NM_DEVICE_STAGE_STATE_PENDING;
+            return nm_modem_act_stage1_prepare(priv->modem,
+                                               nm_device_get_act_request(NM_DEVICE(self)),
+                                               out_failure_reason);
+        }
+    }
+
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static NMActStageReturn
+act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceBt *       self = NM_DEVICE_BT(device);
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN)
+        nm_modem_act_stage2_config(priv->modem);
+
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static NMActStageReturn
+act_stage3_ip_config_start(NMDevice *           device,
+                           int                  addr_family,
+                           gpointer *           out_config,
+                           NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(device);
+
+    nm_assert_addr_family(addr_family);
+
+    if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN) {
+        if (addr_family == AF_INET) {
+            return nm_modem_stage3_ip4_config_start(priv->modem,
+                                                    device,
+                                                    NM_DEVICE_CLASS(nm_device_bt_parent_class),
+                                                    out_failure_reason);
+        } else {
+            return nm_modem_stage3_ip6_config_start(priv->modem, device, out_failure_reason);
+        }
+    }
+
+    return NM_DEVICE_CLASS(nm_device_bt_parent_class)
+        ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason);
+}
+
+static void
+deactivate(NMDevice *device)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(device);
+
+    nm_clear_g_signal_handler(nm_device_get_platform(device), &priv->connect_watch_link_id);
+    nm_clear_g_source(&priv->connect_watch_link_idle_id);
+    priv->stage1_bt_state = NM_DEVICE_STAGE_STATE_INIT;
+    nm_clear_g_source(&priv->connect_wait_modem_id);
+    nm_clear_g_cancellable(&priv->connect_bz_cancellable);
+
+    priv->stage1_bt_state = NM_DEVICE_STAGE_STATE_INIT;
+
+    if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN) {
+        if (priv->modem) {
+            nm_modem_deactivate(priv->modem, device);
+
+            /* Since we're killing the Modem object before it'll get the
+             * state change signal, simulate the state change here.
+             */
+            nm_modem_device_state_changed(priv->modem,
+                                          NM_DEVICE_STATE_DISCONNECTED,
+                                          NM_DEVICE_STATE_ACTIVATED);
+            modem_cleanup(NM_DEVICE_BT(device));
+        }
+    }
+
+    if (priv->connect_bt_type != NM_BT_CAPABILITY_NONE) {
+        priv->connect_bt_type = NM_BT_CAPABILITY_NONE;
+        nm_bluez_manager_disconnect(priv->bz_mgr, priv->dbus_path);
+    }
+
+    nm_clear_g_free(&priv->connect_rfcomm_iface);
+
+    if (NM_DEVICE_CLASS(nm_device_bt_parent_class)->deactivate)
+        NM_DEVICE_CLASS(nm_device_bt_parent_class)->deactivate(device);
+}
+
+void
+_nm_device_bt_notify_removed(NMDeviceBt *self)
+{
+    g_signal_emit_by_name(self, NM_DEVICE_REMOVED);
+}
+
+/*****************************************************************************/
+
+gboolean
+_nm_device_bt_for_same_device(NMDeviceBt *            self,
+                              const char *            dbus_path,
+                              const char *            bdaddr,
+                              const char *            name,
+                              NMBluetoothCapabilities capabilities)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    return nm_streq(priv->dbus_path, dbus_path) && nm_streq(priv->bdaddr, bdaddr)
+           && capabilities == priv->capabilities && (!name || nm_streq(priv->name, name));
+}
+
+void
+_nm_device_bt_notify_set_name(NMDeviceBt *self, const char *name)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    nm_assert(name);
+
+    if (!nm_streq(priv->name, name)) {
+        _LOGT(LOGD_BT, "set-name: %s", name);
+        g_free(priv->name);
+        priv->name = g_strdup(name);
+        _notify(self, PROP_BT_NAME);
+    }
+}
+
+/*****************************************************************************/
+
+static gboolean
+is_available(NMDevice *dev, NMDeviceCheckDevAvailableFlags flags)
+{
+    NMDeviceBt *       self = NM_DEVICE_BT(dev);
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    /* PAN doesn't need ModemManager, so devices that support it are always available */
+    if (priv->capabilities & NM_BT_CAPABILITY_NAP)
+        return TRUE;
+
+    /* DUN requires ModemManager */
+    return priv->mm_running;
+}
+
+static void
+set_mm_running(NMDeviceBt *self)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+    gboolean           running;
+
+    running = (nm_modem_manager_name_owner_get(priv->modem_manager) != NULL);
+
+    if (priv->mm_running != running) {
+        _LOGD(LOGD_BT, "ModemManager now %s", running ? "available" : "unavailable");
+
+        priv->mm_running = running;
+        nm_device_queue_recheck_available(NM_DEVICE(self),
+                                          NM_DEVICE_STATE_REASON_NONE,
+                                          NM_DEVICE_STATE_REASON_MODEM_MANAGER_UNAVAILABLE);
+    }
+}
+
+static void
+mm_name_owner_changed_cb(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+    set_mm_running(user_data);
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_BT_NAME:
+        g_value_set_string(value, priv->name);
+        break;
+    case PROP_BT_CAPABILITIES:
+        g_value_set_uint(value, priv->capabilities);
+        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)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_BT_BZ_MGR:
+        /* construct-only */
+        priv->bz_mgr = g_object_ref(g_value_get_pointer(value));
+        nm_assert(NM_IS_BLUEZ_MANAGER(priv->bz_mgr));
+        break;
+    case PROP_BT_DBUS_PATH:
+        /* construct-only */
+        priv->dbus_path = g_value_dup_string(value);
+        nm_assert(priv->dbus_path);
+        break;
+    case PROP_BT_BDADDR:
+        /* construct-only */
+        priv->bdaddr = g_value_dup_string(value);
+        nm_assert(priv->bdaddr);
+        break;
+    case PROP_BT_NAME:
+        /* construct-only */
+        priv->name = g_value_dup_string(value);
+        nm_assert(priv->name);
+        break;
+    case PROP_BT_CAPABILITIES:
+        /* construct-only */
+        priv->capabilities = g_value_get_uint(value);
+        nm_assert(NM_IN_SET((NMBluetoothCapabilities) priv->capabilities,
+                            NM_BT_CAPABILITY_DUN,
+                            NM_BT_CAPABILITY_NAP,
+                            NM_BT_CAPABILITY_DUN | NM_BT_CAPABILITY_NAP));
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_bt_init(NMDeviceBt *self)
+{}
+
+static void
+constructed(GObject *object)
+{
+    NMDeviceBt *       self = NM_DEVICE_BT(object);
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    G_OBJECT_CLASS(nm_device_bt_parent_class)->constructed(object);
+
+    priv->modem_manager = g_object_ref(nm_modem_manager_get());
+
+    nm_modem_manager_name_owner_ref(priv->modem_manager);
+
+    g_signal_connect(priv->modem_manager,
+                     NM_MODEM_MANAGER_MODEM_ADDED,
+                     G_CALLBACK(mm_modem_added_cb),
+                     self);
+
+    g_signal_connect(priv->modem_manager,
+                     "notify::" NM_MODEM_MANAGER_NAME_OWNER,
+                     G_CALLBACK(mm_name_owner_changed_cb),
+                     self);
+
+    set_mm_running(self);
+}
+
+NMDeviceBt *
+nm_device_bt_new(NMBluezManager *        bz_mgr,
+                 const char *            dbus_path,
+                 const char *            bdaddr,
+                 const char *            name,
+                 NMBluetoothCapabilities capabilities)
+{
+    g_return_val_if_fail(NM_IS_BLUEZ_MANAGER(bz_mgr), NULL);
+    g_return_val_if_fail(dbus_path, NULL);
+    g_return_val_if_fail(bdaddr, NULL);
+    g_return_val_if_fail(name, NULL);
+    g_return_val_if_fail(capabilities != NM_BT_CAPABILITY_NONE, NULL);
+
+    return g_object_new(NM_TYPE_DEVICE_BT,
+                        NM_DEVICE_UDI,
+                        dbus_path,
+                        NM_DEVICE_IFACE,
+                        bdaddr,
+                        NM_DEVICE_DRIVER,
+                        "bluez",
+                        NM_DEVICE_PERM_HW_ADDRESS,
+                        bdaddr,
+                        NM_DEVICE_BT_BDADDR,
+                        bdaddr,
+                        NM_DEVICE_BT_BZ_MGR,
+                        bz_mgr,
+                        NM_DEVICE_BT_CAPABILITIES,
+                        (guint) capabilities,
+                        NM_DEVICE_BT_DBUS_PATH,
+                        dbus_path,
+                        NM_DEVICE_BT_NAME,
+                        name,
+                        NM_DEVICE_TYPE_DESC,
+                        "Bluetooth",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_BT,
+                        NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMDeviceBt *       self = NM_DEVICE_BT(object);
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    nm_clear_g_signal_handler(nm_device_get_platform(NM_DEVICE(self)),
+                              &priv->connect_watch_link_id);
+    nm_clear_g_source(&priv->connect_watch_link_idle_id);
+
+    nm_clear_g_source(&priv->connect_wait_modem_id);
+    nm_clear_g_cancellable(&priv->connect_bz_cancellable);
+
+    if (priv->modem_manager) {
+        g_signal_handlers_disconnect_by_func(priv->modem_manager,
+                                             G_CALLBACK(mm_name_owner_changed_cb),
+                                             self);
+        g_signal_handlers_disconnect_by_func(priv->modem_manager,
+                                             G_CALLBACK(mm_modem_added_cb),
+                                             self);
+        nm_modem_manager_name_owner_unref(priv->modem_manager);
+        g_clear_object(&priv->modem_manager);
+    }
+
+    modem_cleanup(self);
+
+    G_OBJECT_CLASS(nm_device_bt_parent_class)->dispose(object);
+
+    g_clear_object(&priv->bz_mgr);
+}
+
+static void
+finalize(GObject *object)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(object);
+
+    g_free(priv->connect_rfcomm_iface);
+    g_free(priv->dbus_path);
+    g_free(priv->name);
+    g_free(priv->bdaddr);
+
+    G_OBJECT_CLASS(nm_device_bt_parent_class)->finalize(object);
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_bluetooth = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_BLUETOOTH,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress",
+                                                             "s",
+                                                             NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Name", "s", NM_DEVICE_BT_NAME),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("BtCapabilities",
+                                                             "u",
+                                                             NM_DEVICE_BT_CAPABILITIES), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_bt_class_init(NMDeviceBtClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->constructed  = constructed;
+    object_class->get_property = get_property;
+    object_class->set_property = set_property;
+    object_class->dispose      = dispose;
+    object_class->finalize     = finalize;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_bluetooth);
+
+    device_class->connection_type_check_compatible = NM_SETTING_BLUETOOTH_SETTING_NAME;
+
+    device_class->get_generic_capabilities    = get_generic_capabilities;
+    device_class->can_auto_connect            = can_auto_connect;
+    device_class->deactivate                  = deactivate;
+    device_class->act_stage1_prepare          = act_stage1_prepare;
+    device_class->act_stage2_config           = act_stage2_config;
+    device_class->act_stage3_ip_config_start  = act_stage3_ip_config_start;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->check_connection_available  = check_connection_available;
+    device_class->complete_connection         = complete_connection;
+    device_class->is_available                = is_available;
+    device_class->get_configured_mtu          = nm_modem_get_configured_mtu;
+
+    device_class->state_changed = device_state_changed;
+
+    obj_properties[PROP_BT_BZ_MGR] =
+        g_param_spec_pointer(NM_DEVICE_BT_BZ_MGR,
+                             "",
+                             "",
+                             G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_BT_BDADDR] =
+        g_param_spec_string(NM_DEVICE_BT_BDADDR,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_BT_DBUS_PATH] =
+        g_param_spec_string(NM_DEVICE_BT_DBUS_PATH,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_BT_NAME] =
+        g_param_spec_string(NM_DEVICE_BT_NAME,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_BT_CAPABILITIES] =
+        g_param_spec_uint(NM_DEVICE_BT_CAPABILITIES,
+                          "",
+                          "",
+                          NM_BT_CAPABILITY_NONE,
+                          G_MAXUINT,
+                          NM_BT_CAPABILITY_NONE,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+
+    signals[PPP_STATS] = g_signal_new(NM_DEVICE_BT_PPP_STATS,
+                                      G_OBJECT_CLASS_TYPE(object_class),
+                                      G_SIGNAL_RUN_FIRST,
+                                      0,
+                                      NULL,
+                                      NULL,
+                                      NULL,
+                                      G_TYPE_NONE,
+                                      2,
+                                      G_TYPE_UINT /*guint32 in_bytes*/,
+                                      G_TYPE_UINT /*guint32 out_bytes*/);
+}
diff --git a/src/core/devices/bluetooth/nm-device-bt.h b/src/core/devices/bluetooth/nm-device-bt.h
new file mode 100644
index 00000000..c2d3bc18
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-device-bt.h
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2009 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_BT_H__
+#define __NETWORKMANAGER_DEVICE_BT_H__
+
+#include "devices/nm-device.h"
+
+#define NM_TYPE_DEVICE_BT (nm_device_bt_get_type())
+#define NM_DEVICE_BT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_BT, NMDeviceBt))
+#define NM_DEVICE_BT_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_BT, NMDeviceBtClass))
+#define NM_IS_DEVICE_BT(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_BT))
+#define NM_IS_DEVICE_BT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_BT))
+#define NM_DEVICE_BT_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_BT, NMDeviceBtClass))
+
+#define NM_DEVICE_BT_BDADDR       "bt-bdaddr"
+#define NM_DEVICE_BT_BZ_MGR       "bt-bz-mgr"
+#define NM_DEVICE_BT_CAPABILITIES "bt-capabilities"
+#define NM_DEVICE_BT_DBUS_PATH    "bt-dbus-path"
+#define NM_DEVICE_BT_NAME         "bt-name"
+
+#define NM_DEVICE_BT_PPP_STATS "ppp-stats"
+
+typedef struct _NMDeviceBt      NMDeviceBt;
+typedef struct _NMDeviceBtClass NMDeviceBtClass;
+
+GType nm_device_bt_get_type(void);
+
+struct _NMBluezManager;
+
+NMDeviceBt *nm_device_bt_new(struct _NMBluezManager *bz_mgr,
+                             const char *            dbus_path,
+                             const char *            bdaddr,
+                             const char *            name,
+                             NMBluetoothCapabilities capabilities);
+
+gboolean _nm_device_bt_for_same_device(NMDeviceBt *            device,
+                                       const char *            dbus_path,
+                                       const char *            bdaddr,
+                                       const char *            name,
+                                       NMBluetoothCapabilities capabilities);
+
+NMBluetoothCapabilities nm_device_bt_get_capabilities(NMDeviceBt *device);
+
+struct _NMModem;
+
+gboolean nm_device_bt_modem_added(NMDeviceBt *device, struct _NMModem *modem, const char *driver);
+
+void _nm_device_bt_notify_removed(NMDeviceBt *self);
+
+void _nm_device_bt_notify_set_name(NMDeviceBt *self, const char *name);
+
+void _nm_device_bt_notify_set_connected(NMDeviceBt *self, gboolean connected);
+
+#endif /* __NETWORKMANAGER_DEVICE_BT_H__ */
diff --git a/src/core/devices/bluetooth/tests/nm-bt-test.c b/src/core/devices/bluetooth/tests/nm-bt-test.c
new file mode 100644
index 00000000..0fc8aa87
--- /dev/null
+++ b/src/core/devices/bluetooth/tests/nm-bt-test.c
@@ -0,0 +1,223 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "src/core/nm-default-daemon.h"
+
+#include <glib-unix.h>
+
+#include "devices/bluetooth/nm-bluez5-dun.h"
+
+#include "nm-test-utils-core.h"
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN LOGD_BT
+#define _NMLOG(level, ...)                                             \
+    nm_log((level),                                                    \
+           _NMLOG_DOMAIN,                                              \
+           NULL,                                                       \
+           NULL,                                                       \
+           "bt%s%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),            \
+           NM_PRINT_FMT_QUOTED(gl.argv_cmd, "[", gl.argv_cmd, "]", "") \
+               _NM_UTILS_MACRO_REST(__VA_ARGS__))
+
+/*****************************************************************************/
+
+struct {
+    int                argc;
+    const char *const *argv;
+    const char *       argv_cmd;
+    GMainLoop *        loop;
+} gl;
+
+typedef struct _MainCmdInfo {
+    const char *name;
+    int (*main_func)(const struct _MainCmdInfo *main_cmd_info);
+} MainCmdInfo;
+
+/*****************************************************************************/
+
+#if WITH_BLUEZ5_DUN
+
+typedef struct {
+    NMBluez5DunContext *dun_context;
+    GCancellable *      cancellable;
+    guint               timeout_id;
+    guint               sig_term_id;
+    guint               sig_int_id;
+} DunConnectData;
+
+static void
+_dun_connect_cb(NMBluez5DunContext *context,
+                const char *        rfcomm_dev,
+                GError *            error,
+                gpointer            user_data)
+{
+    DunConnectData *dun_connect_data = user_data;
+
+    g_assert(dun_connect_data);
+    g_assert(!dun_connect_data->dun_context);
+    g_assert((!!error) != (!!rfcomm_dev));
+
+    if (rfcomm_dev && !context) {
+        _LOGI("dun-connect notifies path \"%s\". Wait longer...", rfcomm_dev);
+        return;
+    }
+
+    if (rfcomm_dev) {
+        g_assert(context);
+        _LOGI("dun-connect completed with path \"%s\"", rfcomm_dev);
+    } else {
+        g_assert(!context);
+        _LOGI("dun-connect failed with error: %s", error->message);
+    }
+
+    dun_connect_data->dun_context = context;
+
+    g_main_loop_quit(gl.loop);
+}
+
+static void
+_dun_notify_tty_hangup_cb(NMBluez5DunContext *context, gpointer user_data)
+{
+    _LOGI("dun-connect: notified TTY hangup");
+}
+
+static gboolean
+_timeout_cb(gpointer user_data)
+{
+    DunConnectData *dun_connect_data = user_data;
+
+    _LOGI("timeout");
+    dun_connect_data->timeout_id = 0;
+    if (dun_connect_data->cancellable)
+        g_cancellable_cancel(dun_connect_data->cancellable);
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+_sig_xxx_cb(DunConnectData *dun_connect_data, int sigid)
+{
+    _LOGI("signal %s received", sigid == SIGTERM ? "SIGTERM" : "SIGINT");
+    g_main_loop_quit(gl.loop);
+    return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+_sig_term_cb(gpointer user_data)
+{
+    return _sig_xxx_cb(user_data, SIGTERM);
+}
+
+static gboolean
+_sig_int_cb(gpointer user_data)
+{
+    return _sig_xxx_cb(user_data, SIGINT);
+}
+#endif
+
+static int
+do_dun_connect(const MainCmdInfo *main_cmd_info)
+{
+#if WITH_BLUEZ5_DUN
+    gs_unref_object GCancellable *cancellable = NULL;
+    gs_free_error GError *error               = NULL;
+    const char *          adapter;
+    const char *          remote;
+    DunConnectData        dun_connect_data = {};
+
+    if (gl.argc < 4) {
+        _LOGE("missing arguments \"adapter\" and \"remote\"");
+        return -1;
+    }
+
+    adapter = gl.argv[2];
+    remote  = gl.argv[3];
+
+    cancellable                  = g_cancellable_new();
+    dun_connect_data.cancellable = cancellable;
+
+    if (!nm_bluez5_dun_connect(adapter,
+                               remote,
+                               cancellable,
+                               _dun_connect_cb,
+                               &dun_connect_data,
+                               _dun_notify_tty_hangup_cb,
+                               &dun_connect_data,
+                               &error)) {
+        _LOGE("connect failed to start: %s", error->message);
+        return -1;
+    }
+
+    dun_connect_data.timeout_id = g_timeout_add(60000, _timeout_cb, &dun_connect_data);
+
+    g_main_loop_run(gl.loop);
+
+    nm_clear_g_source(&dun_connect_data.timeout_id);
+
+    if (dun_connect_data.dun_context) {
+        dun_connect_data.sig_term_id = g_unix_signal_add(SIGTERM, _sig_term_cb, &dun_connect_data);
+        dun_connect_data.sig_int_id  = g_unix_signal_add(SIGINT, _sig_int_cb, &dun_connect_data);
+
+        g_main_loop_run(gl.loop);
+
+        nm_clear_g_source(&dun_connect_data.sig_term_id);
+        nm_clear_g_source(&dun_connect_data.sig_int_id);
+
+        nm_bluez5_dun_disconnect(g_steal_pointer(&dun_connect_data.dun_context));
+    }
+
+    return 0;
+#else
+    _LOGE("compiled without bluetooth DUN support");
+    return 1;
+#endif
+}
+
+/*****************************************************************************/
+
+NMTST_DEFINE();
+
+int
+main(int argc, char **argv)
+{
+    static const MainCmdInfo main_cmd_infos[] = {
+        {
+            .name      = "dun-connect",
+            .main_func = do_dun_connect,
+        },
+    };
+    int   exit_code = 0;
+    guint i;
+
+    if (!g_getenv("G_MESSAGES_DEBUG"))
+        g_setenv("G_MESSAGES_DEBUG", "all", TRUE);
+
+    nmtst_init_with_logging(&argc, &argv, "DEBUG", "ALL");
+
+    nm_logging_init(NULL, TRUE);
+
+    gl.argv = (const char *const *) argv;
+    gl.argc = argc;
+    gl.loop = g_main_loop_new(NULL, FALSE);
+
+    _LOGI("bluetooth test util start");
+
+    gl.argv_cmd = argc >= 2 ? argv[1] : NULL;
+
+    for (i = 0; i < G_N_ELEMENTS(main_cmd_infos); i++) {
+        if (nm_streq0(main_cmd_infos[i].name, gl.argv_cmd)) {
+            _LOGD("start \"%s\"", gl.argv_cmd);
+            exit_code = main_cmd_infos[i].main_func(&main_cmd_infos[i]);
+            _LOGD("completed with %d", exit_code);
+            break;
+        }
+    }
+    if (gl.argv_cmd && i >= G_N_ELEMENTS(main_cmd_infos)) {
+        nm_log_err(LOGD_BT, "invalid command \"%s\"", gl.argv_cmd);
+        exit_code = -1;
+    }
+
+    nm_clear_pointer(&gl.loop, g_main_loop_unref);
+
+    return exit_code;
+}