// SPDX-License-Identifier: GPL-2.0-or-later /* * * BlueZ - Bluetooth protocol stack for Linux * * Copyright (C) 2009-2010 Marcel Holtmann * Copyright (C) 2009-2010 Nokia Corporation * Copyright 2023 NXP * * */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include "lib/bluetooth.h" #include "btio/btio.h" #define DEFAULT_ACCEPT_TIMEOUT 2 static int opt_update_sec = 0; #define DEFAULT_IO_QOS \ { \ .interval = 10000, \ .latency = 10, \ .sdu = 40, \ .phy = 0x02, \ .rtn = 2, \ } struct bt_iso_qos qos = { .ucast = { .cig = BT_ISO_QOS_CIG_UNSET, .cis = BT_ISO_QOS_CIG_UNSET, .sca = 0x07, .packing = 0x00, .framing = 0x00, .in = DEFAULT_IO_QOS, .out = DEFAULT_IO_QOS, }, }; struct io_data { guint ref; GIOChannel *io; int reject; int disconn; int accept; int voice; struct bt_iso_qos *qos; }; static void io_data_unref(struct io_data *data) { data->ref--; if (data->ref) return; if (data->io) g_io_channel_unref(data->io); g_free(data); } static struct io_data *io_data_ref(struct io_data *data) { data->ref++; return data; } static struct io_data *io_data_new(GIOChannel *io, int reject, int disconn, int accept) { struct io_data *data; data = g_new0(struct io_data, 1); data->io = io; data->reject = reject; data->disconn = disconn; data->accept = accept; data->qos = &qos; return io_data_ref(data); } static gboolean io_watch(GIOChannel *io, GIOCondition cond, gpointer user_data) { printf("Disconnected\n"); return FALSE; } static gboolean disconn_timeout(gpointer user_data) { struct io_data *data = user_data; printf("Disconnecting\n"); g_io_channel_shutdown(data->io, TRUE, NULL); return FALSE; } static void update_sec_level(struct io_data *data) { GError *err = NULL; int sec_level; if (!bt_io_get(data->io, &err, BT_IO_OPT_SEC_LEVEL, &sec_level, BT_IO_OPT_INVALID)) { printf("bt_io_get(OPT_SEC_LEVEL): %s\n", err->message); g_clear_error(&err); return; } printf("sec_level=%d\n", sec_level); if (opt_update_sec == sec_level) return; if (!bt_io_set(data->io, &err, BT_IO_OPT_SEC_LEVEL, opt_update_sec, BT_IO_OPT_INVALID)) { printf("bt_io_set(OPT_SEC_LEVEL): %s\n", err->message); g_clear_error(&err); } } static void connect_cb(GIOChannel *io, GError *err, gpointer user_data) { struct io_data *data = user_data; GIOCondition cond; char addr[18]; uint16_t handle, omtu, imtu; uint8_t cls[3], key_size; if (err) { printf("Connecting failed: %s\n", err->message); return; } if (!bt_io_get(io, &err, BT_IO_OPT_DEST, addr, BT_IO_OPT_HANDLE, &handle, BT_IO_OPT_CLASS, cls, BT_IO_OPT_INVALID)) { printf("Unable to get destination address: %s\n", err->message); g_clear_error(&err); strcpy(addr, "(unknown)"); } printf("Successfully connected to %s. handle=%u, class=%02x%02x%02x\n", addr, handle, cls[0], cls[1], cls[2]); if (!bt_io_get(io, &err, BT_IO_OPT_OMTU, &omtu, BT_IO_OPT_IMTU, &imtu, BT_IO_OPT_INVALID)) { printf("Unable to get MTU sizes: %s\n", err->message); g_clear_error(&err); } else printf("imtu=%u, omtu=%u\n", imtu, omtu); if (!bt_io_get(io, &err, BT_IO_OPT_KEY_SIZE, &key_size, BT_IO_OPT_INVALID)) { printf("Unable to get Key size: %s\n", err->message); g_clear_error(&err); } else printf("key_size=%u\n", key_size); if (data->disconn == 0) { g_io_channel_shutdown(io, TRUE, NULL); printf("Disconnected\n"); return; } if (data->io == NULL) data->io = g_io_channel_ref(io); if (data->disconn > 0) { io_data_ref(data); g_timeout_add_seconds_full(G_PRIORITY_DEFAULT, data->disconn, disconn_timeout, data, (GDestroyNotify) io_data_unref); } io_data_ref(data); if (opt_update_sec > 0) update_sec_level(data); cond = G_IO_NVAL | G_IO_HUP | G_IO_ERR; g_io_add_watch_full(io, G_PRIORITY_DEFAULT, cond, io_watch, data, (GDestroyNotify) io_data_unref); } static gboolean confirm_timeout(gpointer user_data) { struct io_data *data = user_data; if (data->reject >= 0) { printf("Rejecting connection\n"); g_io_channel_shutdown(data->io, TRUE, NULL); return FALSE; } printf("Accepting connection\n"); io_data_ref(data); if (opt_update_sec > 0) update_sec_level(data); if (!bt_io_accept(data->io, connect_cb, data, (GDestroyNotify) io_data_unref, NULL)) { printf("bt_io_accept() failed\n"); io_data_unref(data); } return FALSE; } static void confirm_cb(GIOChannel *io, gpointer user_data) { char addr[18]; struct io_data *data = user_data; GError *err = NULL; if (!bt_io_get(io, &err, BT_IO_OPT_DEST, addr, BT_IO_OPT_INVALID)) { printf("bt_io_get(OPT_DEST): %s\n", err->message); g_clear_error(&err); } else printf("Got confirmation request for %s\n", addr); if (data->accept < 0 && data->reject < 0) return; if (data->reject == 0) { printf("Rejecting connection\n"); g_io_channel_shutdown(io, TRUE, NULL); return; } if (data->voice) { if (!bt_io_set(io, &err, BT_IO_OPT_VOICE, data->voice, BT_IO_OPT_INVALID)) { printf("bt_io_set(OPT_VOICE): %s\n", err->message); g_clear_error(&err); } } data->io = g_io_channel_ref(io); io_data_ref(data); if (data->accept == 0) { if (!bt_io_accept(io, connect_cb, data, (GDestroyNotify) io_data_unref, &err)) { printf("bt_io_accept() failed: %s\n", err->message); g_clear_error(&err); io_data_unref(data); return; } } else { int seconds = (data->reject > 0) ? data->reject : data->accept; g_timeout_add_seconds_full(G_PRIORITY_DEFAULT, seconds, confirm_timeout, data, (GDestroyNotify) io_data_unref); } } static void l2cap_connect(const char *src, const char *dst, uint8_t addr_type, uint16_t psm, uint16_t cid, int disconn, int sec, int prio) { struct io_data *data; GError *err = NULL; uint8_t src_type; printf("Connecting to %s L2CAP PSM %u\n", dst, psm); data = io_data_new(NULL, -1, disconn, -1); if (addr_type != BDADDR_BREDR) src_type = BDADDR_LE_PUBLIC; else src_type = BDADDR_BREDR; if (src) data->io = bt_io_connect(connect_cb, data, (GDestroyNotify) io_data_unref, &err, BT_IO_OPT_SOURCE, src, BT_IO_OPT_SOURCE_TYPE, src_type, BT_IO_OPT_DEST, dst, BT_IO_OPT_DEST_TYPE, addr_type, BT_IO_OPT_PSM, psm, BT_IO_OPT_CID, cid, BT_IO_OPT_SEC_LEVEL, sec, BT_IO_OPT_PRIORITY, prio, BT_IO_OPT_INVALID); else data->io = bt_io_connect(connect_cb, data, (GDestroyNotify) io_data_unref, &err, BT_IO_OPT_SOURCE_TYPE, src_type, BT_IO_OPT_DEST, dst, BT_IO_OPT_DEST_TYPE, addr_type, BT_IO_OPT_PSM, psm, BT_IO_OPT_CID, cid, BT_IO_OPT_SEC_LEVEL, sec, BT_IO_OPT_PRIORITY, prio, BT_IO_OPT_INVALID); if (!data->io) { printf("Connecting to %s failed: %s\n", dst, err->message); g_error_free(err); exit(EXIT_FAILURE); } } static void l2cap_listen(const char *src, uint8_t addr_type, uint16_t psm, uint16_t cid, int defer, int reject, int disconn, int accept, int sec, gboolean central) { struct io_data *data; BtIOConnect conn; BtIOConfirm cfm; GIOChannel *l2_srv; GError *err = NULL; if (defer) { conn = NULL; cfm = confirm_cb; } else { conn = connect_cb; cfm = NULL; } if (cid) printf("Listening on L2CAP CID 0x%04x (%u)\n", cid, cid); else printf("Listening on L2CAP PSM 0x%04x (%u)\n", psm, psm); data = io_data_new(NULL, reject, disconn, accept); if (src) l2_srv = bt_io_listen(conn, cfm, data, (GDestroyNotify) io_data_unref, &err, BT_IO_OPT_SOURCE, src, BT_IO_OPT_SOURCE_TYPE, addr_type, BT_IO_OPT_PSM, psm, BT_IO_OPT_CID, cid, BT_IO_OPT_SEC_LEVEL, sec, BT_IO_OPT_CENTRAL, central, BT_IO_OPT_INVALID); else l2_srv = bt_io_listen(conn, cfm, data, (GDestroyNotify) io_data_unref, &err, BT_IO_OPT_SOURCE_TYPE, addr_type, BT_IO_OPT_PSM, psm, BT_IO_OPT_CID, cid, BT_IO_OPT_SEC_LEVEL, sec, BT_IO_OPT_CENTRAL, central, BT_IO_OPT_INVALID); if (!l2_srv) { printf("Listening failed: %s\n", err->message); g_error_free(err); exit(EXIT_FAILURE); } g_io_channel_unref(l2_srv); } static void rfcomm_connect(const char *src, const char *dst, uint8_t ch, int disconn, int sec) { struct io_data *data; GError *err = NULL; printf("Connecting to %s RFCOMM channel %u\n", dst, ch); data = io_data_new(NULL, -1, disconn, -1); if (src) data->io = bt_io_connect(connect_cb, data, (GDestroyNotify) io_data_unref, &err, BT_IO_OPT_SOURCE, src, BT_IO_OPT_DEST, dst, BT_IO_OPT_CHANNEL, ch, BT_IO_OPT_SEC_LEVEL, sec, BT_IO_OPT_INVALID); else data->io = bt_io_connect(connect_cb, data, (GDestroyNotify) io_data_unref, &err, BT_IO_OPT_DEST, dst, BT_IO_OPT_CHANNEL, ch, BT_IO_OPT_SEC_LEVEL, sec, BT_IO_OPT_INVALID); if (!data->io) { printf("Connecting to %s failed: %s\n", dst, err->message); g_error_free(err); exit(EXIT_FAILURE); } } static void rfcomm_listen(const char *src, uint8_t ch, gboolean defer, int reject, int disconn, int accept, int sec, gboolean central) { struct io_data *data; BtIOConnect conn; BtIOConfirm cfm; GIOChannel *rc_srv; GError *err = NULL; if (defer) { conn = NULL; cfm = confirm_cb; } else { conn = connect_cb; cfm = NULL; } data = io_data_new(NULL, reject, disconn, accept); if (src) rc_srv = bt_io_listen(conn, cfm, data, (GDestroyNotify) io_data_unref, &err, BT_IO_OPT_SOURCE, src, BT_IO_OPT_CHANNEL, ch, BT_IO_OPT_SEC_LEVEL, sec, BT_IO_OPT_CENTRAL, central, BT_IO_OPT_INVALID); else rc_srv = bt_io_listen(conn, cfm, data, (GDestroyNotify) io_data_unref, &err, BT_IO_OPT_CHANNEL, ch, BT_IO_OPT_SEC_LEVEL, sec, BT_IO_OPT_CENTRAL, central, BT_IO_OPT_INVALID); if (!rc_srv) { printf("Listening failed: %s\n", err->message); g_error_free(err); exit(EXIT_FAILURE); } bt_io_get(rc_srv, &err, BT_IO_OPT_CHANNEL, &ch, BT_IO_OPT_INVALID); printf("Listening on RFCOMM channel %u\n", ch); g_io_channel_unref(rc_srv); } static void sco_connect(const char *src, const char *dst, int disconn, int voice) { struct io_data *data; GError *err = NULL; printf("Connecting SCO to %s\n", dst); data = io_data_new(NULL, -1, disconn, -1); if (src) data->io = bt_io_connect(connect_cb, data, (GDestroyNotify) io_data_unref, &err, BT_IO_OPT_SOURCE, src, BT_IO_OPT_DEST, dst, BT_IO_OPT_VOICE, voice, BT_IO_OPT_INVALID); else data->io = bt_io_connect(connect_cb, data, (GDestroyNotify) io_data_unref, &err, BT_IO_OPT_DEST, dst, BT_IO_OPT_VOICE, voice, BT_IO_OPT_INVALID); if (!data->io) { printf("Connecting to %s failed: %s\n", dst, err->message); g_error_free(err); exit(EXIT_FAILURE); } } static void sco_listen(const char *src, gboolean defer, int reject, int disconn, int accept, int voice) { struct io_data *data; BtIOConnect conn; BtIOConfirm cfm; GIOChannel *sco_srv; GError *err = NULL; printf("Listening for SCO connections\n"); if (defer) { conn = NULL; cfm = confirm_cb; } else { conn = connect_cb; cfm = NULL; } data = io_data_new(NULL, reject, disconn, accept); data->voice = voice; if (src) sco_srv = bt_io_listen(conn, cfm, data, (GDestroyNotify) io_data_unref, &err, BT_IO_OPT_SOURCE, src, BT_IO_OPT_VOICE, voice, BT_IO_OPT_INVALID); else sco_srv = bt_io_listen(conn, cfm, data, (GDestroyNotify) io_data_unref, &err, BT_IO_OPT_VOICE, voice, BT_IO_OPT_INVALID); if (!sco_srv) { printf("Listening failed: %s\n", err->message); g_error_free(err); exit(EXIT_FAILURE); } g_io_channel_unref(sco_srv); } static void iso_connect(const char *src, const char *dst, int disconn) { struct io_data *data; GError *err = NULL; printf("Connecting ISO to %s\n", dst); data = io_data_new(NULL, -1, disconn, -1); if (src) data->io = bt_io_connect(connect_cb, data, (GDestroyNotify) io_data_unref, &err, BT_IO_OPT_SOURCE, src, BT_IO_OPT_DEST, dst, BT_IO_OPT_MODE, BT_IO_MODE_ISO, BT_IO_OPT_QOS, data->qos, BT_IO_OPT_INVALID); else data->io = bt_io_connect(connect_cb, data, (GDestroyNotify) io_data_unref, &err, BT_IO_OPT_DEST, dst, BT_IO_OPT_MODE, BT_IO_MODE_ISO, BT_IO_OPT_QOS, data->qos, BT_IO_OPT_INVALID); if (!data->io) { printf("Connecting to %s failed: %s\n", dst, err->message); g_error_free(err); exit(EXIT_FAILURE); } } static void iso_listen(const char *src, gboolean defer, int reject, int disconn, int accept) { struct io_data *data; BtIOConnect conn; BtIOConfirm cfm; GIOChannel *iso_srv; GError *err = NULL; printf("Listening for ISO connections\n"); if (defer) { conn = NULL; cfm = confirm_cb; } else { conn = connect_cb; cfm = NULL; } data = io_data_new(NULL, reject, disconn, accept); if (src) iso_srv = bt_io_listen(conn, cfm, data, (GDestroyNotify) io_data_unref, &err, BT_IO_OPT_SOURCE, src, BT_IO_OPT_MODE, BT_IO_MODE_ISO, BT_IO_OPT_INVALID); else iso_srv = bt_io_listen(conn, cfm, data, (GDestroyNotify) io_data_unref, &err, BT_IO_OPT_MODE, BT_IO_MODE_ISO, BT_IO_OPT_INVALID); if (!iso_srv) { printf("Listening failed: %s\n", err->message); g_error_free(err); exit(EXIT_FAILURE); } g_io_channel_unref(iso_srv); } static int opt_channel = -1; static int opt_psm = 0; static gboolean opt_sco = FALSE; static gboolean opt_iso = FALSE; static gboolean opt_defer = FALSE; static gint opt_voice = 0; static char *opt_dev = NULL; static int opt_reject = -1; static int opt_disconn = -1; static int opt_accept = DEFAULT_ACCEPT_TIMEOUT; static int opt_sec = 0; static gboolean opt_central = FALSE; static int opt_priority = 0; static int opt_cid = 0; static guint8 opt_addr_type = 0; static GMainLoop *main_loop; static GOptionEntry options[] = { { "channel", 'c', 0, G_OPTION_ARG_INT, &opt_channel, "RFCOMM channel" }, { "psm", 'p', 0, G_OPTION_ARG_INT, &opt_psm, "L2CAP PSM" }, { "cid", 'j', 0, G_OPTION_ARG_INT, &opt_cid, "L2CAP CID" }, { "addr-type", 't', 0, G_OPTION_ARG_INT, &opt_addr_type, "Address type " "(0 BR/EDR 1 LE Public 2 LE Random" }, { "sco", 's', 0, G_OPTION_ARG_NONE, &opt_sco, "Use SCO" }, { "iso", 'o', 0, G_OPTION_ARG_NONE, &opt_iso, "Use ISO" }, { "defer", 'd', 0, G_OPTION_ARG_NONE, &opt_defer, "Use DEFER_SETUP for incoming connections" }, { "voice", 'V', 0, G_OPTION_ARG_INT, &opt_voice, "Voice setting " "(0x0060 CVSD, 0x0003 Transparent)" }, { "sec-level", 'S', 0, G_OPTION_ARG_INT, &opt_sec, "Security level" }, { "update-sec-level", 'U', 0, G_OPTION_ARG_INT, &opt_update_sec, "Update security level" }, { "dev", 'i', 0, G_OPTION_ARG_STRING, &opt_dev, "Which HCI device to use" }, { "reject", 'r', 0, G_OPTION_ARG_INT, &opt_reject, "Reject connection after N seconds" }, { "disconnect", 'D', 0, G_OPTION_ARG_INT, &opt_disconn, "Disconnect connection after N seconds" }, { "accept", 'a', 0, G_OPTION_ARG_INT, &opt_accept, "Accept connection after N seconds" }, { "central", 'C', 0, G_OPTION_ARG_NONE, &opt_central, "Central role switch (incoming connections)" }, { "master", 'm', 0, G_OPTION_ARG_NONE, &opt_central, "Deprecated. Use central instead." }, { "priority", 'P', 0, G_OPTION_ARG_INT, &opt_priority, "Transmission priority: Setting a priority " "outside the range 0 to 6 requires the" "CAP_NET_ADMIN capability." }, { NULL }, }; static void sig_term(int sig) { g_main_loop_quit(main_loop); } int main(int argc, char *argv[]) { GOptionContext *context; context = g_option_context_new(NULL); g_option_context_add_main_entries(context, options, NULL); if (!g_option_context_parse(context, &argc, &argv, NULL)) exit(EXIT_FAILURE); g_option_context_free(context); printf("accept=%d reject=%d discon=%d defer=%d sec=%d update_sec=%d" " prio=%d voice=0x%04x\n", opt_accept, opt_reject, opt_disconn, opt_defer, opt_sec, opt_update_sec, opt_priority, opt_voice); if (opt_psm || opt_cid) { if (argc > 1) l2cap_connect(opt_dev, argv[1], opt_addr_type, opt_psm, opt_cid, opt_disconn, opt_sec, opt_priority); else l2cap_listen(opt_dev, opt_addr_type, opt_psm, opt_cid, opt_defer, opt_reject, opt_disconn, opt_accept, opt_sec, opt_central); } if (opt_channel != -1) { if (argc > 1) rfcomm_connect(opt_dev, argv[1], opt_channel, opt_disconn, opt_sec); else rfcomm_listen(opt_dev, opt_channel, opt_defer, opt_reject, opt_disconn, opt_accept, opt_sec, opt_central); } if (opt_sco) { if (argc > 1) sco_connect(opt_dev, argv[1], opt_disconn, opt_voice); else sco_listen(opt_dev, opt_defer, opt_reject, opt_disconn, opt_accept, opt_voice); } if (opt_iso) { if (argc > 1) iso_connect(opt_dev, argv[1], opt_disconn); else iso_listen(opt_dev, opt_defer, opt_reject, opt_disconn, opt_accept); } signal(SIGTERM, sig_term); signal(SIGINT, sig_term); main_loop = g_main_loop_new(NULL, FALSE); g_main_loop_run(main_loop); g_main_loop_unref(main_loop); printf("Exiting\n"); exit(EXIT_SUCCESS); }