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 β€” value 0 (SDP Only) Β· link key type: None Β· encryption: Not required
  • BT_SECURITY_LOW β€” value 1 (Low) Β· link key type: Unauthenticated Β· encryption: Not required
  • BT_SECURITY_MEDIUM β€” value 2 (Medium - default) Β· link key type: Unauthenticated Β· encryption: Desired
  • BT_SECURITY_HIGH β€” value 3 (High) Β· link key type: Authenticated Β· encryption: Required
  • BT_SECURITY_FIPS (since Linux 3.15) β€” value 4 (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 required
  • 1 β€” 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 β€” value 0x00 (default) Β· Do not flush data
  • BT_FLUSHABLE_ON β€” value 0x01 Β· 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 mode
  • BT_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 only
  • BT_CHANNEL_POLICY_BREDR_PREFERRED β€” 1 β€” BR/EDR Preferred
  • BT_CHANNEL_POLICY_BREDR_PREFERRED β€” 2 β€” AMP Preferred
πŸ™ˆ Not me pointing out that BT_CHANNEL_POLICY_BREDR_PREFERRED appears twice in the source table, once with value 1 and once with value 2. 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 1SLOT
  • BT_PHY_BR_1M_3SLOT β€” BIT 1 β€” BR 1Mbps 3SLOT
  • BT_PHY_BR_1M_5SLOT β€” BIT 2 β€” BR 1Mbps 5SLOT
  • BT_PHY_BR_2M_1SLOT β€” BIT 3 β€” EDR 2Mbps 1SLOT
  • BT_PHY_BR_2M_3SLOT β€” BIT 4 β€” EDR 2Mbps 3SLOT
  • BT_PHY_BR_2M_5SLOT β€” BIT 5 β€” EDR 2Mbps 5SLOT
  • BT_PHY_BR_3M_1SLOT β€” BIT 6 β€” EDR 3Mbps 1SLOT
  • BT_PHY_BR_3M_3SLOT β€” BIT 7 β€” EDR 3Mbps 3SLOT
  • BT_PHY_BR_3M_5SLOT β€” BIT 8 β€” EDR 3Mbps 5SLOT
  • BT_PHY_LE_1M_TX β€” BIT 9 β€” LE 1Mbps TX
  • BT_PHY_LE_1M_RX β€” BIT 10 β€” LE 1Mbps RX
  • BT_PHY_LE_2M_TX β€” BIT 11 β€” LE 2Mbps TX
  • BT_PHY_LE_2M_RX β€” BIT 12 β€” LE 2Mbps RX
  • BT_PHY_LE_CODED_TX β€” BIT 13 β€” LE Coded TX
  • BT_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: Any
  • BT_MODE_ERTM β€” 0x01 β€” Enhanced Retransmission mode β€” link: BR/EDR
  • BT_MODE_STREAM β€” 0x02 β€” Stream mode β€” link: BR/EDR
  • BT_MODE_LE_FLOWCTL β€” 0x03 β€” Credit based flow control mode β€” link: LE
  • BT_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_info tells you which flavour it is (SCM_TSTAMP_SND or SCM_TSTAMP_COMPLETION, iykyk), and serr->ee_data hands 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, point ifr_data at your struct ethtool_ts_info, set cmd.cmd = ETHTOOL_GET_TS_INFO, and the answer lands in cmd.so_timestamping. Iconic behaviour honestly πŸŽ€

🌈 RESOURCES

http://www.bluez.org

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 πŸ’…


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