/* -*- 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 #include #include #include #include #include #include #include #include #include "nm-bluez5-dun.h" #include "nm-bt-error.h" #include "nm-logging.h" #include "NetworkManagerUtils.h" 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; }; 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); }