// SPDX-License-Identifier: GPL-2.0+ /* * Copyright (C) 2014 Red Hat, Inc. */ #include "nm-default.h" #include #include #include #include #include #include #include #include #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); }