L2CAP(7)
π L2CAP(7) β the l2cap protocol, explained by your favorite Bluetooth bestie
okay so before we even start: this is a page from Linux System Administration, section 7, and it's all about l2cap. grab a snack, we're getting into sockets π«Ά
β¨ NAME β¨
l2cap β the L2CAP protocol. that's it, that's the name. iconic of it honestly π
β¨ SYNOPSIS β¨
the vibes, in code form:
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/l2cap.h>
l2cap_socket = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);
β¨ DESCRIPTION β¨
so L2CAP is the protocol that gives higher-level protocols an interface to send and receive data over a Bluetooth connection π. it sits on top of the Bluetooth Host Controller Interface (HCI) and hands out a whole set of channels that higher-level protocols can use to transmit data. the way that stack is layered?? obsessed.
and the services it provides to those higher-level protocols? literally so much β¨:
- segmentation and reassembly of large data packets π¦
- flow control, so the receiver doesn't get absolutely overloaded π
- support for multiple channels per connection, which means concurrent data transmission using different protocols β main character energy, fr fr
π¦ SOCKET ADDRESS π¦
here's the struct, bestie, memorise her:
struct sockaddr_l2 {
sa_family_t l2_family;
unsigned short l2_psm;
bdaddr_t l2_bdaddr;
unsigned short l2_cid;
uint8_t l2_bdaddr_type;
};
and here's an example of her in the wild:
struct sockaddr_l2 addr;
memset(&addr, 0, sizeof(addr));
addr.l2_family = AF_BLUETOOTH;
bacpy(&addr.l2_bdaddr, bdaddr);
if (cid)
addr.l2_cid = htobs(cid);
else
addr.l2_psm = htobs(psm);
addr.l2_bdaddr_type = bdaddr_type;
like, zero the whole thing first, set the family to AF_BLUETOOTH, copy the address in with bacpy, and then it's cid or psm β pick your fighter π
πΈ SOCKET OPTIONS (SOL_BLUETOOTH) πΈ
okay so every socket option listed below? you set it with setsockopt(2) and you read it back with getsockopt(2) β and the socket level has to be set to SOL_BLUETOOTH. don't forget that part, i'm begging π
β BT_SECURITY (since Linux 2.6.30)
this one sets the channel security level. possible values, and yes i'm giving you the full rundown because every single one matters:
BT_SECURITY_SDPβ value0(SDP Only) Β· link key type: None Β· encryption: Not requiredBT_SECURITY_LOWβ value1(Low) Β· link key type: Unauthenticated Β· encryption: Not requiredBT_SECURITY_MEDIUMβ value2(Medium - default) Β· link key type: Unauthenticated Β· encryption: DesiredBT_SECURITY_HIGHβ value3(High) Β· link key type: Authenticated Β· encryption: RequiredBT_SECURITY_FIPS(since Linux 3.15) β value4(Secure Only) Β· link key type: Authenticated (P-256 based Secure Simple Pairing and Secure Authentication) Β· encryption: Required
note that BT_SECURITY_MEDIUM is the default β she's the one you get if you don't say otherwise π
example, because i KNOW you want one:
int level = BT_SECURITY_HIGH;
int err = setsockopt(l2cap_socket, SOL_BLUETOOTH, BT_SECURITY, &level,
sizeof(level));
if (err == -1) {
perror("setsockopt");
return 1;
}
β BT_DEFER_SETUP (since Linux 2.6.30)
this is the channel defer connection setup option. what it does: it controls whether the connection procedure needs to be authorized by userspace before responding β which is what allows authorization at the profile level π₯. possible values:
0β Disable (default) Β· Authorization: Not required1β Enable Β· Authorization: Required
example time, and this one is a whole journey so buckle up:
int defer_setup = 1;
int err = setsockopt(l2cap_socket, SOL_BLUETOOTH, BT_DEFER_SETUP,
&defer_setup, sizeof(defer_setup));
if (err == -1) {
perror("setsockopt");
return err;
}
err = listen(l2cap_socket, 5);
if (err) {
perror("listen");
return err;
}
struct sockaddr_l2 remote_addr = {0};
socklen_t addr_len = sizeof(remote_addr);
int new_socket = accept(l2cap_socket, (struct sockaddr*)&remote_addr,
&addr_len);
if (new_socket < 0) {
perror("accept");
return new_socket;
}
/* To complete the connection setup of new_socket read 1 byte */
char c;
struct pollfd pfd;
memset(&pfd, 0, sizeof(pfd));
pfd.fd = new_socket;
pfd.events = POLLOUT;
err = poll(&pfd, 1, 0);
if (err) {
perror("poll");
return err;
}
if (!(pfd.revents & POLLOUT)) {
err = read(sk, &c, 1);
if (err < 0) {
perror("read");
return err;
}
}
the key little detail: to complete the connection setup of new_socket you read 1 byte. that's the moment, that's the ritual π―οΈ
β BT_FLUSHABLE (since Linux 2.6.39)
the channel flushable flag. it controls whether the channel data can be flushed or not, and the possible values are:
BT_FLUSHABLE_OFFβ value0x00(default) Β· Do not flush dataBT_FLUSHABLE_ONβ value0x01Β· Flush data
so by default nothing gets flushed. if you want the data flushed, you gotta say so π
more of this page is coming, bestie β don't go anywhere π
π BT_POWER (since Linux 3.1)
Okay so this one is the channel power policy β it's literally just the switch for "is this channel gonna force its way out of sniff mode or not?" That's the whole vibe check. Possible values, bestie:
BT_POWER_FORCE_ACTIVE_OFFβ0x00β don't force exit of sniff modeBT_POWER_FORCE_ACTIVE_ONβ0x01(default) β force exit of sniff mode π
So the default is yes, we're forcing it, no cap.
π¦ BT_CHANNEL_POLICY (since Linux 3.10)
High-speed (AMP) channel policy. Possible values:
BT_CHANNEL_POLICY_BREDR_ONLYβ0(default) β BR/EDR onlyBT_CHANNEL_POLICY_BREDR_PREFERREDβ1β BR/EDR PreferredBT_CHANNEL_POLICY_BREDR_PREFERREDβ2β AMP Preferred
π Not me pointing out thatBT_CHANNEL_POLICY_BREDR_PREFERREDappears twice in the source table, once with value1and once with value2. We're just reproducing what's there, bestie β copy it exactly, don't improvise π
β BT_PHY (since Linux 5.10)
Channel supported PHY(s), i.e. the whole menu of physical-layer options. Possible values:
BT_PHY_BR_1M_1SLOTβBIT 0β BR 1Mbps 1SLOTBT_PHY_BR_1M_3SLOTβBIT 1β BR 1Mbps 3SLOTBT_PHY_BR_1M_5SLOTβBIT 2β BR 1Mbps 5SLOTBT_PHY_BR_2M_1SLOTβBIT 3β EDR 2Mbps 1SLOTBT_PHY_BR_2M_3SLOTβBIT 4β EDR 2Mbps 3SLOTBT_PHY_BR_2M_5SLOTβBIT 5β EDR 2Mbps 5SLOTBT_PHY_BR_3M_1SLOTβBIT 6β EDR 3Mbps 1SLOTBT_PHY_BR_3M_3SLOTβBIT 7β EDR 3Mbps 3SLOTBT_PHY_BR_3M_5SLOTβBIT 8β EDR 3Mbps 5SLOTBT_PHY_LE_1M_TXβBIT 9β LE 1Mbps TXBT_PHY_LE_1M_RXβBIT 10β LE 1Mbps RXBT_PHY_LE_2M_TXβBIT 11β LE 2Mbps TXBT_PHY_LE_2M_RXβBIT 12β LE 2Mbps RXBT_PHY_LE_CODED_TXβBIT 13β LE Coded TXBT_PHY_LE_CODED_RXβBIT 14β LE Coded RX
Fifteen of them. Every single bit accounted for. She's thorough π
π BT_MODE (since Linux 5.10)
Channel Mode, possible values β and yes, this one comes with a which-link-does-it-work-on column, so pay attention:
BT_MODE_BASICβ0x00(default) β Basic mode β link: AnyBT_MODE_ERTMβ0x01β Enhanced Retransmission mode β link: BR/EDRBT_MODE_STREAMβ0x02β Stream mode β link: BR/EDRBT_MODE_LE_FLOWCTLβ0x03β Credit based flow control mode β link: LEBT_MODE_EXT_FLOWCTLβ0x04β Extended Credit based flow control mode β link: Any
Periodt. Basic is the default, and Any link types get BT_MODE_BASIC and BT_MODE_EXT_FLOWCTL π«Ά
π SOCKET OPTIONS (SOL_SOCKET) π
Real talk: the SOL_SOCKET level socket options that modify generic socket features (SO_SNDBUF, SO_RCVBUF, etc.) have their usual meaning β see socket(7) for that β¨
What we care about here are the SOL_SOCKET level L2CAP socket options that have Bluetooth-specific handling in the kernel. Those are listed below, bestie π
π°οΈ SO_TIMESTAMPING, SO_TIMESTAMP, SO_TIMESTAMPNS
See https://docs.kernel.org/networking/timestamping.html β that's the lore drop, go read it π
For L2CAP sockets, software RX timestamps are supported. Software TX timestamps (SOF_TIMESTAMPING_TX_SOFTWARE, SOF_TIMESTAMPING_TX_COMPLETION) are supported since Linux 6.15. She got an upgrade π
The software RX timestamp is the time when the kernel received the packet from the controller driver. Exactly what it sounds like π―
The SCM_TSTAMP_SND timestamp is emitted when the packet is sent to the controller driver. The SCM_TSTAMP_COMPLETION timestamp is emitted when the controller reports the packet completed. Other TX timestamp types are not supported β so don't come looking, they're not there, fr fr π
You can use SIOCETHTOOL to query supported flags.
The timestamps are in CLOCK_REALTIME time. Iconic, reliable, the classic π°οΈβ¨
Example (Enable RX timestamping):
int flags = SOF_TIMESTAMPING_SOFTWARE |
SOF_TIMESTAMPING_RX_SOFTWARE;
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags));
Example (Read packet and its RX timestamp):
...and we're back, bestie, mid-example, no big deal π
this next chunk is literally the same recvmsg moment but with MSG_ERRQUEUE energy β you're pulling the timestamp straight out of the error queue, and it's giving "I know exactly when this packet existed" β¨
char data_buf[256];
union {
char buf[CMSG_SPACE(sizeof(struct scm_timestamping))];
struct cmsghdr align;
} control;
struct iovec data = {
.iov_base = data_buf,
.iov_len = sizeof(data_buf),
};
struct msghdr msg = {
.msg_iov = &data,
.msg_iovlen = 1,
.msg_control = control.buf,
.msg_controllen = sizeof(control.buf),
};
struct scm_timestamping tss;
res = recvmsg(fd, &msg, MSG_ERRQUEUE | MSG_DONTWAIT);
if (res < 0)
goto error;
for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_TIMESTAMPING)
memcpy(&tss, CMSG_DATA(cmsg), sizeof(tss));
}
tstamp_clock_realtime = tss.ts[0];
Example (Enable TX timestamping):
int flags = SOF_TIMESTAMPING_SOFTWARE |
SOF_TIMESTAMPING_TX_SOFTWARE |
SOF_TIMESTAMPING_TX_COMPLETION |
SOF_TIMESTAMPING_OPT_ID;
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags));
Example (Read TX timestamps):
union {
char buf[2 * CMSG_SPACE(sizeof(struct scm_timestamping))];
struct cmsghdr align;
} control;
struct iovec data = {
.iov_base = NULL,
.iov_len = 0
};
struct msghdr msg = {
.msg_iov = &data,
.msg_iovlen = 1,
.msg_control = control.buf,
.msg_controllen = sizeof(control.buf),
};
struct cmsghdr *cmsg;
struct scm_timestamping tss;
struct sock_extended_err serr;
int res;
res = recvmsg(fd, &msg, MSG_ERRQUEUE | MSG_DONTWAIT);
if (res < 0)
goto error;
for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_TIMESTAMPING)
memcpy(&tss, CMSG_DATA(cmsg), sizeof(tss));
else if (cmsg->cmsg_level == SOL_BLUETOOTH && cmsg->cmsg_type == BT_SCM_ERROR)
memcpy(&serr, CMSG_DATA(cmsg), sizeof(serr));
}
tstamp_clock_realtime = tss.ts[0];
tstamp_type = serr->ee_info; /* SCM_TSTAMP_SND or SCM_TSTAMP_COMPLETION */
tstamp_seqnum = serr->ee_data;
ngl that last block is the moment, okay?tss.ts[0]gives you the realtime stamp,serr->ee_infotells you which flavour it is (SCM_TSTAMP_SNDorSCM_TSTAMP_COMPLETION, iykyk), andserr->ee_datahands you the seqnum. You're basically the main character of timing now π«Ά
π§ IOCTLS
okay so these are the ioctls that are specific to L2CAP sockets β not the whole zoo, just the ones with L2CAP operations. Slay.
π¦ SIOCETHTOOL (since Linux 6.16-rc1)
This one only supports the command ETHTOOL_GET_TS_INFO, which you can use to query which SOF_TIMESTAMPING_* flags are supported π and the SOF_TIMESTAMPING_OPT_* flags are always available as applicable β periodt.
Example:
#include <linux/ethtool.h>
#include <linux/sockios.h>
#include <net/if.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
...
struct ifreq ifr = {};
struct ethtool_ts_info cmd = {};
int sk;
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "hci0");
ifr.ifr_data = (void *)&cmd;
cmd.cmd = ETHTOOL_GET_TS_INFO;
sk = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);
if (sk < 0)
goto error;
if (ioctl(sk, SIOCETHTOOL, &ifr))
goto error;
sof_available = cmd.so_timestamping;
like, you literally open the socket, pointifr_dataat yourstruct ethtool_ts_info, setcmd.cmd = ETHTOOL_GET_TS_INFO, and the answer lands incmd.so_timestamping. Iconic behaviour honestly π
π RESOURCES
that's the spot, bestie β go touch grass on the BlueZ site πΈ
π REPORTING BUGS
linux-bluetooth@vger.kernel.org
found a bug? tell the mailing list, not me, I'm just the messenger π be nice about it though, they're doing their best π«Ά
π SEE ALSO
the obligatory "go read these too" corner, and honestly you should π
π COPYRIGHT
Free use of this software is granted under the terms of the GNU Lesser General Public Licenses (LGPL).
so like, yes you may, the license says so, no cap β¨
okay that's the whole page bestie, we did it, we ate, no crumbs left π this was BlueZ, dated May 2024, and the page is L2CAP(7) β go forth and slay your Bluetooth sockets ππ
π¦ where this page comes from
- centos-stream 9: bluez-libs-devel 5.87+2.git8750129efca8-1.el9 Β· GPLv2+
- centos-stream 9: bluez-libs-devel 5.87+2.git8750129efca8-1.el9 Β· GPLv2+
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms