From 0b36abcdf2e18eb497f59087991f06116d37787b Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Jul 2026 01:50:40 -0400 Subject: [PATCH 01/11] Add BNEP C interop structures --- .../BluetoothLinux/Internal/CInterop.swift | 115 +++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/Sources/BluetoothLinux/Internal/CInterop.swift b/Sources/BluetoothLinux/Internal/CInterop.swift index bddf54c..c2b5fc6 100644 --- a/Sources/BluetoothLinux/Internal/CInterop.swift +++ b/Sources/BluetoothLinux/Internal/CInterop.swift @@ -275,7 +275,120 @@ public extension CInterop { } public extension CInterop { - + + /// `bnep_connadd_req` + struct BNEPConnectionAddRequest { + + /// int sock; + public var socket: CInt + + /// uint32_t flags; + public var flags: UInt32 + + /// uint16_t role; + public var role: UInt16 + + /// char device[16]; + public var device: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) + + public init( + socket: CInt, + flags: UInt32, + role: UInt16, + device: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + ) { + self.socket = socket + self.flags = flags + self.role = role + self.device = device + } + } +} + +public extension CInterop { + + /// `bnep_conndel_req` + struct BNEPConnectionDeleteRequest: Equatable, Hashable { + + /// uint32_t flags; + public var flags: UInt32 + + /// uint8_t dst[6]; + /// + /// The remote address in network (Ethernet) byte order. + public var destination: BluetoothAddress + + public init( + flags: UInt32, + destination: BluetoothAddress + ) { + self.flags = flags + self.destination = destination + } + } +} + +public extension CInterop { + + /// `bnep_conninfo` + struct BNEPConnectionInformation { + + /// uint32_t flags; + public var flags: UInt32 + + /// uint16_t role; + public var role: UInt16 + + /// uint16_t state; + public var state: UInt16 + + /// uint8_t dst[6]; + /// + /// The remote address in network (Ethernet) byte order. + public var destination: BluetoothAddress + + /// char device[16]; + public var device: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) + + public init( + flags: UInt32, + role: UInt16, + state: UInt16, + destination: BluetoothAddress, + device: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + ) { + self.flags = flags + self.role = role + self.state = state + self.destination = destination + self.device = device + } + } +} + +public extension CInterop { + + /// `bnep_connlist_req` + struct BNEPConnectionListRequest { + + /// uint32_t cnum; + public var count: UInt32 + + /// struct bnep_conninfo *ci; + public var connections: UnsafeMutablePointer? + + public init( + count: UInt32, + connections: UnsafeMutablePointer? = nil + ) { + self.count = count + self.connections = connections + } + } +} + +public extension CInterop { + /// `sco_conninfo` SCO Connection Information struct SCOConnectionInfo { From 43a2991aa116ca2cb3eb0d754bf4c129a49abeeb Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Jul 2026 01:50:40 -0400 Subject: [PATCH 02/11] Add BNEP role --- Sources/BluetoothLinux/BNEP/BNEPRole.swift | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 Sources/BluetoothLinux/BNEP/BNEPRole.swift diff --git a/Sources/BluetoothLinux/BNEP/BNEPRole.swift b/Sources/BluetoothLinux/BNEP/BNEPRole.swift new file mode 100644 index 0000000..92a3444 --- /dev/null +++ b/Sources/BluetoothLinux/BNEP/BNEPRole.swift @@ -0,0 +1,37 @@ +// +// BNEPRole.swift +// BluetoothLinux +// + +/// BNEP service role. +/// +/// The role of a device in a Bluetooth network encapsulation session, +/// identified by its service class UUID. +public enum BNEPRole: UInt16, CaseIterable, Sendable { + + /// Personal Area Network User + case personalAreaNetworkUser = 0x1115 + + /// Network Access Point + case networkAccessPoint = 0x1116 + + /// Group Ad-hoc Network + case groupNetwork = 0x1117 +} + +// MARK: - CustomStringConvertible + +extension BNEPRole: CustomStringConvertible, CustomDebugStringConvertible { + + public var description: String { + switch self { + case .personalAreaNetworkUser: return "PANU" + case .networkAccessPoint: return "NAP" + case .groupNetwork: return "GN" + } + } + + public var debugDescription: String { + description + } +} From f2154f2609fb9367db83c42685b85134a98a68f9 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Jul 2026 01:50:40 -0400 Subject: [PATCH 03/11] Add BNEP connection flags --- .../BNEP/BNEPConnectionFlag.swift | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 Sources/BluetoothLinux/BNEP/BNEPConnectionFlag.swift diff --git a/Sources/BluetoothLinux/BNEP/BNEPConnectionFlag.swift b/Sources/BluetoothLinux/BNEP/BNEPConnectionFlag.swift new file mode 100644 index 0000000..da907cb --- /dev/null +++ b/Sources/BluetoothLinux/BNEP/BNEPConnectionFlag.swift @@ -0,0 +1,24 @@ +// +// BNEPConnectionFlag.swift +// BluetoothLinux +// + +/// BNEP connection flags. +@frozen +public struct BNEPConnectionFlag: OptionSet, Equatable, Hashable, Sendable { + + public let rawValue: UInt32 + + public init(rawValue: UInt32) { + self.rawValue = rawValue + } +} + +public extension BNEPConnectionFlag { + + /// The kernel should send the setup connection response. + /// + /// When set, the setup connection request has already been received + /// and the kernel replies to it once the session is created. + static var setupResponse: BNEPConnectionFlag { BNEPConnectionFlag(rawValue: 1 << 0) } +} From ca69ec46e245730ecdba1e3249c9b77678197cd7 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Jul 2026 01:50:40 -0400 Subject: [PATCH 04/11] Add BNEP connection state --- .../BNEP/BNEPConnectionState.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 Sources/BluetoothLinux/BNEP/BNEPConnectionState.swift diff --git a/Sources/BluetoothLinux/BNEP/BNEPConnectionState.swift b/Sources/BluetoothLinux/BNEP/BNEPConnectionState.swift new file mode 100644 index 0000000..e84978c --- /dev/null +++ b/Sources/BluetoothLinux/BNEP/BNEPConnectionState.swift @@ -0,0 +1,19 @@ +// +// BNEPConnectionState.swift +// BluetoothLinux +// + +/// BNEP connection state. +public enum BNEPConnectionState: UInt16, CaseIterable, Sendable { + + case unknown = 0x00 + case connected = 0x01 + case open = 0x02 + case bound = 0x03 + case listening = 0x04 + case connecting = 0x05 + case connecting2 = 0x06 + case config = 0x07 + case disconnecting = 0x08 + case closed = 0x09 +} From 817339768ed009d5e635554834ad023fcc7dadf4 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Jul 2026 01:50:40 -0400 Subject: [PATCH 05/11] Add BNEP connection information --- .../BluetoothLinux/BNEP/BNEPConnection.swift | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 Sources/BluetoothLinux/BNEP/BNEPConnection.swift diff --git a/Sources/BluetoothLinux/BNEP/BNEPConnection.swift b/Sources/BluetoothLinux/BNEP/BNEPConnection.swift new file mode 100644 index 0000000..9b9fad9 --- /dev/null +++ b/Sources/BluetoothLinux/BNEP/BNEPConnection.swift @@ -0,0 +1,57 @@ +// +// BNEPConnection.swift +// BluetoothLinux +// + +import Foundation +import Bluetooth +import SystemPackage +import Socket + +/// BNEP connection information. +public struct BNEPConnection: Equatable, Hashable, Sendable { + + /// Connection flags. + public let flags: BNEPConnectionFlag + + /// Service role of the local device. + public let role: BNEPRole? + + /// Connection state. + public let state: BNEPConnectionState + + /// Address of the remote device. + public let destination: BluetoothAddress + + /// Name of the network interface bridging the connection (e.g. `bnep0`). + public let device: String +} + +internal extension BNEPConnection { + + init(_ bytes: CInterop.BNEPConnectionInformation) { + self.flags = BNEPConnectionFlag(rawValue: bytes.flags) + self.role = BNEPRole(rawValue: bytes.role) + self.state = BNEPConnectionState(rawValue: bytes.state) ?? .unknown + self.destination = bytes.destination + self.device = withUnsafeBytes(of: bytes.device) { buffer in + String(decoding: buffer.prefix(while: { $0 != 0 }), as: UTF8.self) + } + } +} + +internal extension String { + + /// Encode as a fixed-size null-terminated C string tuple of 16 bytes. + @usableFromInline + var deviceNameBytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) { + var bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + let utf8 = Array(self.utf8.prefix(15)) + withUnsafeMutableBytes(of: &bytes) { buffer in + for (index, byte) in utf8.enumerated() { + buffer[index] = byte + } + } + return bytes + } +} From b957aa105aa216f95683649ddd14da5a78433acd Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Jul 2026 01:50:40 -0400 Subject: [PATCH 06/11] Add BNEP add connection ioctl --- .../BNEP/IOCTL/BNEPAddConnection.swift | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 Sources/BluetoothLinux/BNEP/IOCTL/BNEPAddConnection.swift diff --git a/Sources/BluetoothLinux/BNEP/IOCTL/BNEPAddConnection.swift b/Sources/BluetoothLinux/BNEP/IOCTL/BNEPAddConnection.swift new file mode 100644 index 0000000..c7a31ef --- /dev/null +++ b/Sources/BluetoothLinux/BNEP/IOCTL/BNEPAddConnection.swift @@ -0,0 +1,97 @@ +// +// BNEPAddConnection.swift +// BluetoothLinux +// + +import Bluetooth +import SystemPackage +import Socket + +public extension BNEPIO { + + /// BNEP Add Connection + /// + /// Bridges a connected L2CAP socket into a kernel network interface. + /// The kernel writes the name of the created interface back into the request. + struct AddConnection: IOControlValue { + + @_alwaysEmitIntoClient + public static var id: BNEPIO { .addConnection } + + @usableFromInline + internal private(set) var bytes: CInterop.BNEPConnectionAddRequest + + @usableFromInline + internal init(_ bytes: CInterop.BNEPConnectionAddRequest) { + self.bytes = bytes + } + + public init( + socket: SocketDescriptor, + flags: BNEPConnectionFlag = [], + role: BNEPRole, + device: String = "" + ) { + self.init(CInterop.BNEPConnectionAddRequest( + socket: socket.rawValue, + flags: flags.rawValue, + role: role.rawValue, + device: device.deviceNameBytes) + ) + } + + @_alwaysEmitIntoClient + public mutating func withUnsafeMutablePointer(_ body: (UnsafeMutableRawPointer) throws -> (Result)) rethrows -> Result { + try Swift.withUnsafeMutableBytes(of: &bytes) { buffer in + try body(buffer.baseAddress!) + } + } + } +} + +public extension BNEPIO.AddConnection { + + @_alwaysEmitIntoClient + var socket: SocketDescriptor { + return .init(rawValue: bytes.socket) + } + + @_alwaysEmitIntoClient + var flags: BNEPConnectionFlag { + return .init(rawValue: bytes.flags) + } + + @_alwaysEmitIntoClient + var role: BNEPRole? { + return .init(rawValue: bytes.role) + } + + /// Name of the network interface bridging the connection (e.g. `bnep0`). + var device: String { + withUnsafeBytes(of: bytes.device) { buffer in + String(decoding: buffer.prefix(while: { $0 != 0 }), as: UTF8.self) + } + } +} + +// MARK: - File Descriptor + +internal extension SocketDescriptor { + + @usableFromInline + func bnepAddConnection( + socket: SocketDescriptor, + flags: BNEPConnectionFlag = [], + role: BNEPRole, + device: String = "" + ) throws -> String { + var request = BNEPIO.AddConnection( + socket: socket, + flags: flags, + role: role, + device: device + ) + try inputOutput(&request) + return request.device + } +} From 434ba01e36b46a548adbde4717fea7e3d8c27276 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Jul 2026 01:50:40 -0400 Subject: [PATCH 07/11] Add BNEP remove connection ioctl --- .../BNEP/IOCTL/BNEPRemoveConnection.swift | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 Sources/BluetoothLinux/BNEP/IOCTL/BNEPRemoveConnection.swift diff --git a/Sources/BluetoothLinux/BNEP/IOCTL/BNEPRemoveConnection.swift b/Sources/BluetoothLinux/BNEP/IOCTL/BNEPRemoveConnection.swift new file mode 100644 index 0000000..c79b00e --- /dev/null +++ b/Sources/BluetoothLinux/BNEP/IOCTL/BNEPRemoveConnection.swift @@ -0,0 +1,76 @@ +// +// BNEPRemoveConnection.swift +// BluetoothLinux +// + +import Bluetooth +import SystemPackage +import Socket + +public extension BNEPIO { + + /// BNEP Remove Connection + /// + /// Destroys the kernel session (and network interface) for the specified remote device. + struct RemoveConnection: Equatable, Hashable, IOControlValue { + + @_alwaysEmitIntoClient + public static var id: BNEPIO { .removeConnection } + + @usableFromInline + internal private(set) var bytes: CInterop.BNEPConnectionDeleteRequest + + @usableFromInline + internal init(_ bytes: CInterop.BNEPConnectionDeleteRequest) { + self.bytes = bytes + } + + @_alwaysEmitIntoClient + public init( + destination: BluetoothAddress, + flags: BNEPConnectionFlag = [] + ) { + self.init(CInterop.BNEPConnectionDeleteRequest( + flags: flags.rawValue, + destination: destination) + ) + } + + @_alwaysEmitIntoClient + public mutating func withUnsafeMutablePointer(_ body: (UnsafeMutableRawPointer) throws -> (Result)) rethrows -> Result { + try Swift.withUnsafeMutableBytes(of: &bytes) { buffer in + try body(buffer.baseAddress!) + } + } + } +} + +public extension BNEPIO.RemoveConnection { + + @_alwaysEmitIntoClient + var destination: BluetoothAddress { + return bytes.destination + } + + @_alwaysEmitIntoClient + var flags: BNEPConnectionFlag { + return .init(rawValue: bytes.flags) + } +} + +// MARK: - File Descriptor + +internal extension SocketDescriptor { + + @usableFromInline + func bnepRemoveConnection( + destination: BluetoothAddress, + flags: BNEPConnectionFlag = [] + ) throws { + var request = BNEPIO.RemoveConnection( + destination: destination, + flags: flags + ) + try inputOutput(&request) + } +} From 64947c22f411d2e2590221fcf9a5b77a4c36486a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Jul 2026 01:50:40 -0400 Subject: [PATCH 08/11] Add BNEP connection list ioctl --- .../BNEP/IOCTL/BNEPGetConnectionList.swift | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 Sources/BluetoothLinux/BNEP/IOCTL/BNEPGetConnectionList.swift diff --git a/Sources/BluetoothLinux/BNEP/IOCTL/BNEPGetConnectionList.swift b/Sources/BluetoothLinux/BNEP/IOCTL/BNEPGetConnectionList.swift new file mode 100644 index 0000000..380b7ba --- /dev/null +++ b/Sources/BluetoothLinux/BNEP/IOCTL/BNEPGetConnectionList.swift @@ -0,0 +1,79 @@ +// +// BNEPGetConnectionList.swift +// BluetoothLinux +// + +import Bluetooth +import SystemPackage +import Socket + +public extension BNEPIO { + + /// BNEP Get Connection List + struct GetConnectionList: IOControlValue { + + @_alwaysEmitIntoClient + public static var id: BNEPIO { .getConnectionList } + + @_alwaysEmitIntoClient + public static var maxLimit: Int { 256 } + + public var limit: Int + + public private(set) var response: [BNEPConnection] + + public init(limit: Int = Self.maxLimit) { + precondition(limit > 0, "Must request at least one connection") + precondition(limit <= Self.maxLimit, "Only \(Self.maxLimit) maximum connections is allowed") + self.limit = limit + self.response = [] + } + + public mutating func withUnsafeMutablePointer(_ body: (UnsafeMutableRawPointer) throws -> (Result)) rethrows -> Result { + + // Unlike other list requests, `struct bnep_connlist_req` embeds a + // pointer to a caller-allocated `bnep_conninfo` array rather than a + // flexible array member. + let limit = self.limit + var connections = [CInterop.BNEPConnectionInformation]( + repeating: CInterop.BNEPConnectionInformation( + flags: 0, + role: 0, + state: 0, + destination: .zero + ), + count: limit + ) + var count: UInt32 = 0 + let result: Result = try connections.withUnsafeMutableBufferPointer { buffer in + var request = CInterop.BNEPConnectionListRequest( + count: UInt32(limit), + connections: buffer.baseAddress + ) + let result = try Swift.withUnsafeMutableBytes(of: &request) { requestBuffer in + try body(requestBuffer.baseAddress!) + } + count = request.count + return result + } + self.response = connections + .prefix(Int(min(count, UInt32(limit)))) + .map { BNEPConnection($0) } + return result + } + } +} + +// MARK: - File Descriptor + +internal extension SocketDescriptor { + + @usableFromInline + func bnepConnectionList( + limit: Int = BNEPIO.GetConnectionList.maxLimit + ) throws -> [BNEPConnection] { + var request = BNEPIO.GetConnectionList(limit: limit) + try inputOutput(&request) + return request.response + } +} From c8de996244add7155feeeb172c56ff719b79a6ea Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Jul 2026 01:50:40 -0400 Subject: [PATCH 09/11] Add BNEP supported features ioctl --- .../BNEP/IOCTL/BNEPGetSupportedFeatures.swift | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 Sources/BluetoothLinux/BNEP/IOCTL/BNEPGetSupportedFeatures.swift diff --git a/Sources/BluetoothLinux/BNEP/IOCTL/BNEPGetSupportedFeatures.swift b/Sources/BluetoothLinux/BNEP/IOCTL/BNEPGetSupportedFeatures.swift new file mode 100644 index 0000000..ec2f909 --- /dev/null +++ b/Sources/BluetoothLinux/BNEP/IOCTL/BNEPGetSupportedFeatures.swift @@ -0,0 +1,60 @@ +// +// BNEPGetSupportedFeatures.swift +// BluetoothLinux +// + +import Bluetooth +import SystemPackage +import Socket + +public extension BNEPIO { + + /// BNEP Get Supported Features + struct GetSupportedFeatures: Equatable, Hashable, IOControlValue { + + @_alwaysEmitIntoClient + public static var id: BNEPIO { .getSupportedFeatures } + + @usableFromInline + internal private(set) var bytes: UInt32 + + public init() { + self.bytes = 0 + } + + @_alwaysEmitIntoClient + public mutating func withUnsafeMutablePointer(_ body: (UnsafeMutableRawPointer) throws -> (Result)) rethrows -> Result { + try Swift.withUnsafeMutableBytes(of: &bytes) { buffer in + try body(buffer.baseAddress!) + } + } + } +} + +public extension BNEPIO.GetSupportedFeatures { + + /// Feature bitmask reported by the kernel. + @_alwaysEmitIntoClient + var features: UInt32 { + return bytes + } + + /// Whether the kernel can send the setup connection response + /// (``BNEPConnectionFlag/setupResponse``). + @_alwaysEmitIntoClient + var isSetupResponseSupported: Bool { + return bytes & (1 << 0) != 0 + } +} + +// MARK: - File Descriptor + +internal extension SocketDescriptor { + + @usableFromInline + func bnepSupportedFeatures() throws -> BNEPIO.GetSupportedFeatures { + var request = BNEPIO.GetSupportedFeatures() + try inputOutput(&request) + return request + } +} From 282556db10baa7b121839f4408aea4da7559d928 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Jul 2026 01:50:40 -0400 Subject: [PATCH 10/11] Add BNEP control socket --- Sources/BluetoothLinux/BNEP/BNEP.swift | 84 +++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/Sources/BluetoothLinux/BNEP/BNEP.swift b/Sources/BluetoothLinux/BNEP/BNEP.swift index 83b021e..99e023b 100644 --- a/Sources/BluetoothLinux/BNEP/BNEP.swift +++ b/Sources/BluetoothLinux/BNEP/BNEP.swift @@ -1,8 +1,86 @@ // -// File.swift -// +// BNEP.swift +// BluetoothLinux // -// Created by Alsey Coleman Miller on 16/10/21. +// Bluetooth Network Encapsulation Protocol (PAN) control socket. // import Foundation +import Bluetooth +import SystemPackage +import Socket + +/// BNEP control socket. +/// +/// Manages kernel Bluetooth network encapsulation sessions, which bridge +/// connected L2CAP sockets (PSM 15) into virtual Ethernet interfaces +/// (e.g. `bnep0`) for Personal Area Networking. +public struct BNEPSocket: Sendable { + + // MARK: - Properties + + @usableFromInline + internal let fileDescriptor: SocketDescriptor + + // MARK: - Initialization + + /// Open a BNEP control socket. + public init() throws(Errno) { + self.fileDescriptor = try .bluetooth(.bnep, flags: [.closeOnExec]) + } + + // MARK: - Methods + + /// Close the control socket. + /// + /// Established sessions are not affected. + public func close() { + try? fileDescriptor.close() + } + + /// Bridge a connected L2CAP socket into a kernel network interface. + /// + /// The L2CAP socket must be connected to the remote device on the BNEP PSM (15) + /// and the setup connection request/response exchange must already have taken place + /// (or pass ``BNEPConnectionFlag/setupResponse`` to let the kernel reply to a + /// received setup request). + /// + /// - Returns: The name of the created network interface (e.g. `bnep0`). + @discardableResult + public func addConnection( + _ socket: L2CAPSocket, + role: BNEPRole, + flags: BNEPConnectionFlag = [], + device: String = "" + ) throws -> String { + try fileDescriptor.bnepAddConnection( + socket: socket.fileDescriptor, + flags: flags, + role: role, + device: device + ) + } + + /// Destroy the session (and network interface) for the specified remote device. + public func removeConnection( + destination: BluetoothAddress, + flags: BNEPConnectionFlag = [] + ) throws { + try fileDescriptor.bnepRemoveConnection( + destination: destination, + flags: flags + ) + } + + /// List the active sessions. + public func connections( + limit: Int = BNEPIO.GetConnectionList.maxLimit + ) throws -> [BNEPConnection] { + try fileDescriptor.bnepConnectionList(limit: limit) + } + + /// Read the feature bitmask supported by the kernel. + public func supportedFeatures() throws -> BNEPIO.GetSupportedFeatures { + try fileDescriptor.bnepSupportedFeatures() + } +} From a091b951ca6f102218b7523331bd4c4bf119c566 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Jul 2026 01:50:40 -0400 Subject: [PATCH 11/11] Add BNEP tests --- Tests/BluetoothLinuxTests/BNEPTests.swift | 163 ++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 Tests/BluetoothLinuxTests/BNEPTests.swift diff --git a/Tests/BluetoothLinuxTests/BNEPTests.swift b/Tests/BluetoothLinuxTests/BNEPTests.swift new file mode 100644 index 0000000..9a020eb --- /dev/null +++ b/Tests/BluetoothLinuxTests/BNEPTests.swift @@ -0,0 +1,163 @@ +// +// BNEPTests.swift +// BluetoothLinuxTests +// + +#if ENABLE_MOCKING +import Foundation +import XCTest +import Bluetooth +import SystemPackage +import Socket +@testable import BluetoothLinux +#if canImport(Glibc) +import Glibc +#elseif canImport(Darwin) +import Darwin +#endif + +final class BNEPTests: XCTestCase { + + /// A fake file descriptor; mocked syscalls never reach the kernel. + private let fileDescriptor = SocketDescriptor(rawValue: 3) + + func testIORawValues() { + // read-direction requests are sign-extended, matching the C `ioctl` request argument + XCTAssertEqual(BNEPIO.addConnection.rawValue, 0x400442C8) + XCTAssertEqual(BNEPIO.removeConnection.rawValue, 0x400442C9) + XCTAssertEqual(BNEPIO.getConnectionList.rawValue, UInt(bitPattern: Int(Int32(bitPattern: 0x800442D2)))) + XCTAssertEqual(BNEPIO.getConnectionInfo.rawValue, UInt(bitPattern: Int(Int32(bitPattern: 0x800442D3)))) + XCTAssertEqual(BNEPIO.getSupportedFeatures.rawValue, UInt(bitPattern: Int(Int32(bitPattern: 0x800442D4)))) + for value in BNEPIO.allCases { + XCTAssertEqual(BNEPIO(rawValue: value.rawValue), value) + } + } + + func testRequestLayout() { + // must match the kernel's C struct layouts + XCTAssertEqual(MemoryLayout.size, 26) + XCTAssertEqual(MemoryLayout.stride, 28) + XCTAssertEqual(MemoryLayout.size, 10) + XCTAssertEqual(MemoryLayout.size, 30) + XCTAssertEqual(MemoryLayout.stride, 32) + XCTAssertEqual(MemoryLayout.size, 16) + } + + func testDeviceName() { + let bytes = "bnep0".deviceNameBytes + XCTAssertEqual(bytes.0, UInt8(ascii: "b")) + XCTAssertEqual(bytes.4, UInt8(ascii: "0")) + XCTAssertEqual(bytes.5, 0) + // truncated to 15 bytes plus null terminator + let truncated = "a-very-long-interface-name".deviceNameBytes + XCTAssertEqual(truncated.15, 0) + let information = CInterop.BNEPConnectionInformation( + flags: 0, + role: BNEPRole.networkAccessPoint.rawValue, + state: BNEPConnectionState.connected.rawValue, + destination: BluetoothAddress(bytes: (1, 2, 3, 4, 5, 6)), + device: bytes + ) + let connection = BNEPConnection(information) + XCTAssertEqual(connection.device, "bnep0") + XCTAssertEqual(connection.role, .networkAccessPoint) + XCTAssertEqual(connection.state, .connected) + XCTAssertEqual(connection.destination, BluetoothAddress(bytes: (1, 2, 3, 4, 5, 6))) + } + + func testAddConnectionFakedKernelReply() throws { + try MockingDriver.withMockingEnabled { driver in + driver.ioctlHandler = { fd, request, pointer in + XCTAssertEqual(request, BNEPIO.addConnection.rawValue) + guard let pointer else { + XCTFail("Expected a request buffer for BNEPCONNADD") + errno = EINVAL + return -1 + } + let request = pointer.assumingMemoryBound(to: CInterop.BNEPConnectionAddRequest.self) + XCTAssertEqual(request.pointee.socket, 7) + XCTAssertEqual(request.pointee.role, BNEPRole.personalAreaNetworkUser.rawValue) + // kernel writes back the created interface name + request.pointee.device = "bnep0".deviceNameBytes + return 0 + } + let device = try fileDescriptor.bnepAddConnection( + socket: SocketDescriptor(rawValue: 7), + role: .personalAreaNetworkUser + ) + XCTAssertEqual(device, "bnep0") + } + } + + func testRemoveConnectionIsTraced() throws { + try MockingDriver.withMockingEnabled { driver in + try fileDescriptor.bnepRemoveConnection( + destination: BluetoothAddress(bytes: (1, 2, 3, 4, 5, 6)) + ) + XCTAssertEqual( + driver.trace.dequeue(), + Trace.Entry(name: "ioctl", [ + fileDescriptor.rawValue, + BNEPIO.removeConnection.rawValue + ]) + ) + XCTAssertTrue(driver.trace.isEmpty) + } + } + + func testConnectionListFakedKernelReply() throws { + try MockingDriver.withMockingEnabled { driver in + let expected = CInterop.BNEPConnectionInformation( + flags: BNEPConnectionFlag.setupResponse.rawValue, + role: BNEPRole.networkAccessPoint.rawValue, + state: BNEPConnectionState.connected.rawValue, + destination: BluetoothAddress(bytes: (6, 5, 4, 3, 2, 1)), + device: "bnep0".deviceNameBytes + ) + driver.ioctlHandler = { fd, request, pointer in + XCTAssertEqual(request, BNEPIO.getConnectionList.rawValue) + guard let pointer else { + XCTFail("Expected a request buffer for BNEPGETCONNLIST") + errno = EINVAL + return -1 + } + let request = pointer.assumingMemoryBound(to: CInterop.BNEPConnectionListRequest.self) + guard let connections = request.pointee.connections else { + XCTFail("Expected a connection buffer") + errno = EINVAL + return -1 + } + XCTAssertGreaterThanOrEqual(request.pointee.count, 1) + connections[0] = expected + request.pointee.count = 1 + return 0 + } + let connections = try fileDescriptor.bnepConnectionList(limit: 8) + XCTAssertEqual(connections.count, 1) + XCTAssertEqual(connections[0].device, "bnep0") + XCTAssertEqual(connections[0].role, .networkAccessPoint) + XCTAssertEqual(connections[0].state, .connected) + XCTAssertEqual(connections[0].flags, [.setupResponse]) + XCTAssertEqual(connections[0].destination, BluetoothAddress(bytes: (6, 5, 4, 3, 2, 1))) + } + } + + func testSupportedFeaturesFakedKernelReply() throws { + try MockingDriver.withMockingEnabled { driver in + driver.ioctlHandler = { fd, request, pointer in + XCTAssertEqual(request, BNEPIO.getSupportedFeatures.rawValue) + guard let pointer else { + XCTFail("Expected a request buffer for BNEPGETSUPPFEAT") + errno = EINVAL + return -1 + } + pointer.assumingMemoryBound(to: UInt32.self).pointee = 1 + return 0 + } + let features = try fileDescriptor.bnepSupportedFeatures() + XCTAssertEqual(features.features, 1) + XCTAssertTrue(features.isSetupResponseSupported) + } + } +} +#endif