// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 2014 Red Hat, Inc.
 */

#include "nm-default.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 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);
	}
}

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);
}
