summary refs log tree commit diff
path: root/src/devices/bluetooth/nm-bluez5-dun.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/devices/bluetooth/nm-bluez5-dun.c')
-rw-r--r--src/devices/bluetooth/nm-bluez5-dun.c928
1 files changed, 688 insertions, 240 deletions
diff --git a/src/devices/bluetooth/nm-bluez5-dun.c b/src/devices/bluetooth/nm-bluez5-dun.c
index b9a1fa0a..af463d1a 100644
--- a/src/devices/bluetooth/nm-bluez5-dun.c
+++ b/src/devices/bluetooth/nm-bluez5-dun.c
@@ -1,19 +1,5 @@
-/* NetworkManager -- Network link manager
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- *
+// SPDX-License-Identifier: GPL-2.0+
+/*
  * Copyright (C) 2014 Red Hat, Inc.
  */
 
@@ -32,145 +18,407 @@
 #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;
+
+	gint64 connect_open_tty_started_at;
+
+	gulong cancelled_id;
+
+	guint source_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;
+
+	int rfcomm_sock_fd;
+	int rfcomm_tty_fd;
+	int rfcomm_tty_no;
+	int rfcomm_channel;
+
+	guint rfcomm_tty_poll_id;
+
 	bdaddr_t src;
 	bdaddr_t dst;
-	char *src_str;
-	char *dst_str;
-	int rfcomm_channel;
-	int rfcomm_fd;
-	int rfcomm_tty_fd;
-	int rfcomm_id;
-	NMBluez5DunFunc callback;
-	gpointer user_data;
-	sdp_session_t *sdp_session;
-	guint sdp_watch_id;
+
+	char src_str[];
 };
 
-static void
-dun_connect (NMBluez5DunContext *context)
+/*****************************************************************************/
+
+#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)
 {
-	struct sockaddr_rc sa;
-	int devid, try = 30;
-	char tty[100];
-	const int ttylen = sizeof (tty) - 1;
-	GError *error = NULL;
-	int errsv;
+	return context->src_str;
+}
 
-	struct rfcomm_dev_req req = {
-		.flags = (1 << RFCOMM_REUSE_DLC) | (1 << RFCOMM_RELEASE_ONHUP),
-		.dev_id = -1,
-		.channel = context->rfcomm_channel
-	};
+const char *
+nm_bluez5_dun_context_get_remote (const NMBluez5DunContext *context)
+{
+	return context->dst_str;
+}
 
-	context->rfcomm_fd = socket (AF_BLUETOOTH, SOCK_STREAM | SOCK_CLOEXEC, BTPROTO_RFCOMM);
-	if (context->rfcomm_fd < 0) {
-		errsv = errno;
-		error = g_error_new (NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED,
-		                     "Failed to create RFCOMM socket: (%d) %s",
-		                     errsv, nm_strerror_native (errsv));
-		goto done;
-	}
+const char *
+nm_bluez5_dun_context_get_rfcomm_dev (const NMBluez5DunContext *context)
+{
+	return context->rfcomm_tty_path;
+}
 
-	/* Connect to the remote device */
-	sa.rc_family = AF_BLUETOOTH;
-	sa.rc_channel = 0;
-	memcpy (&sa.rc_bdaddr, &context->src, ETH_ALEN);
-	if (bind (context->rfcomm_fd, (struct sockaddr *) &sa, sizeof(sa))) {
-		errsv = errno;
-		error = g_error_new (NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED,
-		                     "Failed to bind socket: (%d) %s",
-		                     errsv, nm_strerror_native (errsv));
-		goto done;
+/*****************************************************************************/
+
+static gboolean
+_rfcomm_tty_poll_cb (GIOChannel *stream,
+                     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" : "");
+
+	context->rfcomm_tty_poll_id = 0;
+	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_ns () > context->cdat->connect_open_tty_started_at + (30 * 100 * NM_UTILS_NS_PER_MSEC)) {
+		gs_free_error GError *error = NULL;
+
+		context->cdat->source_id = 0;
+		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;
 	}
 
-	sa.rc_channel = context->rfcomm_channel;
-	memcpy (&sa.rc_bdaddr, &context->dst, ETH_ALEN);
-	if (connect (context->rfcomm_fd, (struct sockaddr *) &sa, sizeof (sa)) ) {
-		errsv = errno;
-		error = g_error_new (NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED,
-		                     "Failed to connect to remote device: (%d) %s",
-		                     errsv, nm_strerror_native (errsv));
-		goto done;
+	return G_SOURCE_CONTINUE;
+}
+
+static int
+_connect_open_tty (NMBluez5DunContext *context)
+{
+	nm_auto_unref_io_channel GIOChannel *io_channel = NULL;
+	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_id == 0) {
+			_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_ns ();
+			context->cdat->source_id = g_timeout_add (100,
+			                                          _connect_open_tty_retry_cb,
+			                                          context);
+		}
+		return -errsv;
 	}
 
-	nm_log_dbg (LOGD_BT, "(%s): connected to %s on channel %d",
-	            context->src_str, context->dst_str, context->rfcomm_channel);
+	context->rfcomm_tty_fd = fd;
+
+	io_channel = g_io_channel_unix_new (context->rfcomm_tty_fd);
+	context->rfcomm_tty_poll_id = g_io_add_watch (io_channel,
+	                                              G_IO_ERR | G_IO_HUP,
+	                                              _rfcomm_tty_poll_cb,
+	                                              context);
+
+	_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_fd, RFCOMMCREATEDEV, &req);
+	devid = ioctl (context->rfcomm_sock_fd, RFCOMMCREATEDEV, &req);
 	if (devid < 0) {
-		errsv = errno;
-		error = g_error_new (NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED,
-		                     "Failed to create rfcomm device: (%d) %s",
-		                     errsv, nm_strerror_native (errsv));
-		goto done;
+		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_id = devid;
 
-	snprintf (tty, ttylen, "/dev/rfcomm%d", devid);
-	while ((context->rfcomm_tty_fd = open (tty, O_RDONLY | O_NOCTTY | O_CLOEXEC)) < 0 && try--) {
-		if (try) {
-			g_usleep (100 * 1000);
-			continue;
-		}
+	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 synchrnously, 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);
+	}
+}
 
-		error = g_error_new (NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED,
-		                     "Failed to find rfcomm device: %s",
-		                     tty);
-		break;
+static gboolean
+_connect_socket_connect_cb (GIOChannel *stream,
+                            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;
+
+	context->cdat->source_id = 0;
+
+	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;
 	}
 
-done:
-	context->callback (context, tty, error, context->user_data);
+	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
-sdp_search_cleanup (NMBluez5DunContext *context)
+_connect_socket_connect (NMBluez5DunContext *context)
 {
-	if (context->sdp_session) {
-		sdp_close (context->sdp_session);
-		context->sdp_session = NULL;
+	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) {
+		nm_auto_unref_io_channel GIOChannel *io_channel = NULL;
+
+		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);
+
+		io_channel = g_io_channel_unix_new (context->rfcomm_sock_fd);
+		context->cdat->source_id = g_io_add_watch (io_channel,
+		                                           G_IO_OUT,
+		                                           _connect_socket_connect_cb,
+		                                           context);
+		return;
 	}
 
-	nm_clear_g_source (&context->sdp_watch_id);
+	_connect_create_rfcomm (context);
 }
 
 static void
-sdp_search_completed_cb (uint8_t type, uint16_t status, uint8_t *rsp, size_t size, void *user_data)
+_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, seqlen = 0, bytesleft = size;
+	int scanned;
+	int seqlen = 0;
+	int bytesleft = size;
 	uint8_t dataType;
 	int channel = -1;
 
-	nm_log_dbg (LOGD_BT, "(%s -> %s): SDP search finished with type=%d status=%d",
-	            context->src_str, context->dst_str, status, type);
+	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) {
-		GError *error = g_error_new (NM_BT_ERROR,
-		                             NM_BT_ERROR_DUN_CONNECT_FAILED,
-		                             "Did not get a Service Discovery response");
-		context->callback (context, NULL, error, context->user_data);
-		goto done;
+	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);
 
-	nm_log_dbg (LOGD_BT, "(%s -> %s): SDP sequence type scanned=%d length=%d",
-	            context->src_str, context->dst_str, scanned, seqlen);
+	_LOGD (context, "SDP sequence type scanned=%d length=%d",
+	       scanned, seqlen);
 
 	scanned = sdp_extract_seqtype (rsp, bytesleft, &dataType, &seqlen);
-	if (!scanned || !seqlen) {
+	if (   !scanned
+	    || !seqlen) {
 		/* Short read or unknown sequence type */
-		GError *error = g_error_new (NM_BT_ERROR,
-		                             NM_BT_ERROR_DUN_CONNECT_FAILED,
-		                             "Improper Service Discovery response");
-		context->callback (context, NULL, error, context->user_data);
-		goto done;
+		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;
@@ -194,90 +442,153 @@ sdp_search_completed_cb (uint8_t type, uint16_t status, uint8_t *rsp, size_t siz
 			channel = sdp_get_proto_port (protos, RFCOMM_UUID);
 			sdp_list_free (protos, NULL);
 
-			nm_log_dbg (LOGD_BT, "(%s -> %s): SDP channel=%d",
-			            context->src_str, context->dst_str, channel);
+			_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));
-
-done:
-	if (channel != -1) {
-		context->rfcomm_channel = channel;
-		dun_connect (context);
+	} 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;
 	}
 
-	sdp_search_cleanup (context);
+	context->rfcomm_channel = channel;
 }
 
 static gboolean
-sdp_search_process_cb (GIOChannel *channel, GIOCondition condition, gpointer user_data)
+_connect_sdp_search_io_cb (GIOChannel *io_channel,
+                           GIOCondition condition,
+                           gpointer user_data)
 {
 	NMBluez5DunContext *context = user_data;
-
-	nm_log_dbg (LOGD_BT, "(%s -> %s): SDP search progressed with condition=%d",
-	            context->src_str, context->dst_str, condition);
+	gs_free_error GError *error = NULL;
+	int errsv;
 
 	if (condition & (G_IO_ERR | G_IO_HUP | G_IO_NVAL)) {
-		GError *error = g_error_new (NM_BT_ERROR,
-		                             NM_BT_ERROR_DUN_CONNECT_FAILED,
-		                             "Service Discovery interrupted");
-		context->callback (context, NULL, error, context->user_data);
-		sdp_search_cleanup (context);
-		return FALSE;
+		_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");
+		context->cdat->source_id = 0;
+		_context_invoke_callback_fail_and_free (context, error);
+		return G_SOURCE_REMOVE;
 	}
 
-	if (sdp_process (context->sdp_session) < 0) {
-		nm_log_dbg (LOGD_BT, "(%s -> %s): SDP search finished",
-		            context->src_str, context->dst_str);
+	if (sdp_process (context->cdat->sdp_session) == 0) {
+		_LOGD (context, "SDP search still not finished");
+		return G_SOURCE_CONTINUE;
+	}
 
-		/* Search finished successfully. */
-		return FALSE;
+	context->cdat->source_id = 0;
+
+	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;
 	}
 
-	/* Search progressed successfully. */
-	return TRUE;
+	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
-sdp_connect_watch (GIOChannel *channel, GIOCondition condition, gpointer user_data)
+_connect_sdp_session_start_on_idle_cb (gpointer user_data)
 {
 	NMBluez5DunContext *context = user_data;
-	sdp_list_t *search, *attrs;
+	gs_free_error GError *error = NULL;
+
+	context->cdat->source_id = 0;
+
+	_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 (GIOChannel *io_channel,
+                    GIOCondition condition,
+                    gpointer user_data)
+{
+	NMBluez5DunContext *context = user_data;
+	sdp_list_t *search;
+	sdp_list_t *attrs;
 	uuid_t svclass;
 	uint16_t attr;
-	int fd, fd_err = 0;
-	int err;
+	int fd;
+	int errsv;
+	int fd_err = 0;
+	int r;
 	socklen_t len = sizeof (fd_err);
-	GError *error = NULL;
+	gs_free_error GError *error = NULL;
+
+	context->cdat->source_id = 0;
 
-	context->sdp_watch_id = 0;
+	fd = g_io_channel_unix_get_fd (io_channel);
+
+	_LOGD (context, "sdp-session ready to connect with fd=%d", fd);
 
-	fd = g_io_channel_unix_get_fd (channel);
 	if (getsockopt (fd, SOL_SOCKET, SO_ERROR, &fd_err, &len) < 0) {
-		err = errno;
-		nm_log_dbg (LOGD_BT, "(%s -> %s): getsockopt error=%d",
-		            context->src_str, context->dst_str, err);
-	} else {
-		err = fd_err;
-		nm_log_dbg (LOGD_BT, "(%s -> %s): SO_ERROR error=%d",
-		            context->src_str, context->dst_str, fd_err);
+		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 (err != 0) {
+	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 (&context->cdat->source_id);
+			context->cdat->source_id = g_timeout_add (1000,
+			                                          _connect_sdp_session_start_on_idle_cb,
+			                                          context);
+			return G_SOURCE_REMOVE;
+		}
+
 		error = g_error_new (NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED,
-		                     "Error on Service Discovery socket: (%d) %s",
-		                     err, nm_strerror_native (err));
+		                     "error on Service Discovery socket: %s (%d)",
+		                     nm_strerror_native (errsv), errsv);
 		goto done;
 	}
 
-	if (sdp_set_notify (context->sdp_session, sdp_search_completed_cb, context) < 0) {
+	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 request Service Discovery notification");
+		                     "could not set Service Discovery notification");
 		goto done;
 	}
 
@@ -286,124 +597,261 @@ sdp_connect_watch (GIOChannel *channel, GIOCondition condition, gpointer user_da
 	attr = SDP_ATTR_PROTO_DESC_LIST;
 	attrs = sdp_list_append (NULL, &attr);
 
-	if (!sdp_service_search_attr_async (context->sdp_session, search, SDP_ATTR_REQ_INDIVIDUAL, attrs)) {
-		/* Set callback responsible for update the internal SDP transaction */
-		context->sdp_watch_id = g_io_add_watch (channel,
-		                                        G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
-		                                        sdp_search_process_cb,
-		                                        context);
-	} else {
-		err = sdp_get_error (context->sdp_session);
-		error = g_error_new (NM_BT_ERROR,
-		                     NM_BT_ERROR_DUN_CONNECT_FAILED,
-		                     "Error starting Service Discovery: (%d) %s",
-		                     err, nm_strerror_native (err));
-	}
+	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);
 
-done:
-	if (error) {
-		context->callback (context, NULL, error, context->user_data);
-		sdp_search_cleanup (context);
+	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_id = g_io_add_watch (io_channel,
+	                                           G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
+	                                           _connect_sdp_search_io_cb,
+	                                           context);
+
+done:
+	if (error)
+		_context_invoke_callback_fail_and_free (context, error);
 	return G_SOURCE_REMOVE;
 }
 
-NMBluez5DunContext *
-nm_bluez5_dun_new (const char *adapter,
-                   const char *remote)
+/*****************************************************************************/
 
+static void
+_connect_cancelled_cb (GCancellable *cancellable,
+                       NMBluez5DunContext *context)
 {
-	NMBluez5DunContext *context;
-
-	context = g_slice_new0 (NMBluez5DunContext);
-	str2ba (adapter, &context->src);
-	str2ba (remote, &context->dst);
-	context->src_str = g_strdup (adapter);
-	context->dst_str = g_strdup (remote);
-	context->rfcomm_channel = -1;
-	context->rfcomm_id = -1;
-	context->rfcomm_fd = -1;
-	return 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);
 }
 
-void
-nm_bluez5_dun_connect (NMBluez5DunContext *context,
-                       NMBluez5DunFunc callback,
-                       gpointer user_data)
+static gboolean
+_connect_sdp_session_start (NMBluez5DunContext *context,
+                            GError **error)
 {
-	GIOChannel *channel;
+	nm_auto_unref_io_channel GIOChannel *io_channel = NULL;
 
-	context->callback = callback;
-	context->user_data = user_data;
+	nm_assert (context->cdat);
 
-	if (context->rfcomm_channel != -1) {
-		nm_log_dbg (LOGD_BT, "(%s): channel number on device %s cached: %d",
-		            context->src_str, context->dst_str, context->rfcomm_channel);
-		/* FIXME: don't invoke the callback synchronously. */
-		dun_connect (context);
-		return;
+	nm_clear_g_source (&context->cdat->source_id);
+	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;
 	}
 
-	nm_log_dbg (LOGD_BT, "(%s): starting channel number discovery for device %s",
-	            context->src_str, context->dst_str);
+	io_channel = g_io_channel_unix_new (sdp_get_socket (context->cdat->sdp_session));
+	context->cdat->source_id = g_io_add_watch (io_channel,
+	                                           G_IO_OUT | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
+	                                           _connect_sdp_io_cb,
+	                                           context);
+	return TRUE;
+}
 
-	context->sdp_session = sdp_connect (&context->src, &context->dst, SDP_NON_BLOCKING);
-	if (!context->sdp_session) {
-		GError *error;
-		int err = sdp_get_error (context->sdp_session);
+/*****************************************************************************/
+
+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,
+	};
 
-		error = g_error_new (NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED,
-		                     "Failed to connect to the SDP server: (%d) %s",
-		                      err, nm_strerror_native (err));
-		/* FIXME: don't invoke the callback synchronously. */
-		context->callback (context, NULL, error, context->user_data);
-		return;
+	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;
 	}
 
-	/* FIXME(shutdown): make connect cancellable. */
-	channel = g_io_channel_unix_new (sdp_get_socket (context->sdp_session));
-	context->sdp_watch_id = g_io_add_watch (channel,
-	                                        G_IO_OUT | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
-	                                        sdp_connect_watch,
-	                                        context);
-	g_io_channel_unref (channel);
+	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;
 }
 
-/* Only clean up connection-related stuff to allow reconnect */
+/*****************************************************************************/
+
 void
-nm_bluez5_dun_cleanup (NMBluez5DunContext *context)
+nm_bluez5_dun_disconnect (NMBluez5DunContext *context)
 {
-	g_return_if_fail (context != NULL);
+	nm_assert (context);
+	nm_assert (!context->cdat);
 
-	sdp_search_cleanup (context);
+	_LOGD (context, "disconnecting DUN connection");
 
-	if (context->rfcomm_fd >= 0) {
-		if (context->rfcomm_id >= 0) {
-			struct rfcomm_dev_req req = { 0 };
+	_context_free (context);
+}
 
-			req.dev_id = context->rfcomm_id;
-			(void) ioctl (context->rfcomm_fd, RFCOMMRELEASEDEV, &req);
-			context->rfcomm_id = -1;
-		}
-		nm_close (context->rfcomm_fd);
-		context->rfcomm_fd = -1;
-	}
+/*****************************************************************************/
 
-	nm_close (context->rfcomm_tty_fd);
-	context->rfcomm_tty_fd = -1;
+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 (&cdat->source_id);
+
+	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);
 }
 
-void
-nm_bluez5_dun_free (NMBluez5DunContext *context)
+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, FALSE))
+		_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)
 {
-	g_return_if_fail (context != NULL);
+	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 (&context->rfcomm_tty_poll_id);
+
+	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));
+	}
 
-	nm_bluez5_dun_cleanup (context);
-	g_clear_pointer (&context->src_str, g_free);
-	g_clear_pointer (&context->dst_str, g_free);
-	g_slice_free (NMBluez5DunContext, context);
+	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);
 }