diff options
Diffstat (limited to 'src/devices/bluetooth/nm-bluez5-dun.c')
-rw-r--r-- | src/devices/bluetooth/nm-bluez5-dun.c | 409 |
1 files changed, 409 insertions, 0 deletions
diff --git a/src/devices/bluetooth/nm-bluez5-dun.c b/src/devices/bluetooth/nm-bluez5-dun.c new file mode 100644 index 0000000000..dcd3d73b9d --- /dev/null +++ b/src/devices/bluetooth/nm-bluez5-dun.c @@ -0,0 +1,409 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* 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. + * + * Copyright (C) 2014 Red Hat, Inc. + */ + +#include <config.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 <errno.h> +#include <fcntl.h> + +#include "nm-bluez5-dun.h" +#include "nm-bt-error.h" +#include "nm-logging.h" +#include "NetworkManagerUtils.h" + +typedef struct _NMBluez5DunContext { + 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; +} NMBluez5DunContext; + +static void +dun_connect (NMBluez5DunContext *context) +{ + struct sockaddr_rc sa; + int devid, try = 30; + char tty[100]; + const int ttylen = sizeof (tty) - 1; + GError *error = NULL; + + struct rfcomm_dev_req req = { + .flags = (1 << RFCOMM_REUSE_DLC) | (1 << RFCOMM_RELEASE_ONHUP), + .dev_id = -1, + .channel = context->rfcomm_channel + }; + + context->rfcomm_fd = socket (AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM); + if (context->rfcomm_fd < 0) { + int errsv = errno; + error = g_error_new (NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED, + "Failed to create RFCOMM socket: (%d) %s", + errsv, strerror (errsv)); + goto done; + } + + /* 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))) { + int errsv = errno; + error = g_error_new (NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED, + "Failed to bind socket: (%d) %s", + errsv, strerror (errsv)); + goto done; + } + + sa.rc_channel = context->rfcomm_channel; + memcpy (&sa.rc_bdaddr, &context->dst, ETH_ALEN); + if (connect (context->rfcomm_fd, (struct sockaddr *) &sa, sizeof (sa)) ) { + int errsv = errno; + error = g_error_new (NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED, + "Failed to connect to remote device: (%d) %s", + errsv, strerror (errsv)); + goto done; + } + + nm_log_dbg (LOGD_BT, "(%s): connected to %s on channel %d", + context->src_str, context->dst_str, context->rfcomm_channel); + + /* Create an RFCOMM kernel device for the DUN channel */ + memcpy (&req.src, &context->src, ETH_ALEN); + memcpy (&req.dst, &context->dst, ETH_ALEN); + devid = ioctl (context->rfcomm_fd, RFCOMMCREATEDEV, &req); + if (devid < 0) { + int errsv = errno; + error = g_error_new (NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED, + "Failed to create rfcomm device: (%d) %s", + errsv, strerror (errsv)); + goto done; + } + context->rfcomm_id = devid; + + snprintf (tty, ttylen, "/dev/rfcomm%d", devid); + while ((context->rfcomm_tty_fd = open (tty, O_RDONLY | O_NOCTTY)) < 0 && try--) { + if (try) { + g_usleep (100 * 1000); + continue; + } + + error = g_error_new (NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED, + "Failed to find rfcomm device: %s", + tty); + break; + } + +done: + context->callback (context, tty, error, context->user_data); +} + +static void +sdp_search_cleanup (NMBluez5DunContext *context) +{ + if (context->sdp_session) { + sdp_close (context->sdp_session); + context->sdp_session = NULL; + } + + if (context->sdp_watch_id) { + g_source_remove (context->sdp_watch_id); + context->sdp_watch_id = 0; + } +} + +static void +sdp_search_completed_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; + 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); + + /* 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; + } + + 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); + + scanned = sdp_extract_seqtype (rsp, bytesleft, &dataType, &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; + } + + 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); + + nm_log_dbg (LOGD_BT, "(%s -> %s): SDP channel=%d", + context->src_str, context->dst_str, 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); + } + + sdp_search_cleanup (context); +} + +static gboolean +sdp_search_process_cb (GIOChannel *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); + + 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; + } + + if (sdp_process (context->sdp_session) < 0) { + nm_log_dbg (LOGD_BT, "(%s -> %s): SDP search finished", + context->src_str, context->dst_str); + + /* Search finished successfully. */ + return FALSE; + } + + /* Search progressed successfully. */ + return TRUE; +} + +static gboolean +sdp_connect_watch (GIOChannel *channel, GIOCondition condition, gpointer user_data) +{ + NMBluez5DunContext *context = user_data; + sdp_list_t *search, *attrs; + uuid_t svclass; + uint16_t attr; + int fd, err, fd_err = 0; + socklen_t len = sizeof (fd_err); + GError *error = NULL; + + context->sdp_watch_id = 0; + + fd = g_io_channel_unix_get_fd (channel); + if (getsockopt (fd, SOL_SOCKET, SO_ERROR, &fd_err, &len) < 0) { + nm_log_dbg (LOGD_BT, "(%s -> %s): getsockopt error=%d", + context->src_str, context->dst_str, errno); + err = errno; + } else { + nm_log_dbg (LOGD_BT, "(%s -> %s): SO_ERROR error=%d", + context->src_str, context->dst_str, fd_err); + err = fd_err; + } + + if (err != 0) { + error = g_error_new (NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED, + "Error on Service Discovery socket: (%d) %s", + err, strerror (err)); + goto done; + } + + if (sdp_set_notify (context->sdp_session, sdp_search_completed_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"); + 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); + + 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, strerror (err)); + } + + 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); + } + + return G_SOURCE_REMOVE; +} + +NMBluez5DunContext * +nm_bluez5_dun_new (const char *adapter, + const char *remote) + +{ + 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; +} + +void +nm_bluez5_dun_connect (NMBluez5DunContext *context, + NMBluez5DunFunc callback, + gpointer user_data) +{ + GIOChannel *channel; + + context->callback = callback; + context->user_data = user_data; + + 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); + dun_connect (context); + return; + } + + nm_log_dbg (LOGD_BT, "(%s): starting channel number discovery for device %s", + context->src_str, context->dst_str); + + 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); + + error = g_error_new (NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED, + "Failed to connect to the SDP server: (%d) %s", + err, strerror (err)); + context->callback (context, NULL, error, context->user_data); + return; + } + + 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); +} + +/* Only clean up connection-related stuff to allow reconnect */ +void +nm_bluez5_dun_cleanup (NMBluez5DunContext *context) +{ + g_return_if_fail (context != NULL); + + sdp_search_cleanup (context); + + if (context->rfcomm_fd >= 0) { + if (context->rfcomm_id >= 0) { + struct rfcomm_dev_req req = { 0 }; + + req.dev_id = context->rfcomm_id; + ioctl (context->rfcomm_fd, RFCOMMRELEASEDEV, &req); + context->rfcomm_id = -1; + } + close (context->rfcomm_fd); + context->rfcomm_fd = -1; + } + + close (context->rfcomm_tty_fd); + context->rfcomm_tty_fd = -1; +} + +void +nm_bluez5_dun_free (NMBluez5DunContext *context) +{ + g_return_if_fail (context != NULL); + + 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); +} |