How Source-Based Routing Works in BitChat's BLE Mesh
BitChat's BLE mesh implements source-based routing in protocol version 2 by embedding explicit relay paths into packet headers, allowing unicast forwarding through intermediate nodes while falling back to broadcast flooding when routes fail.
BitChat is a permissionless Bluetooth Low Energy (BLE) mesh network where every device acts as a potential relay for peers. Starting with protocol version 2, the network introduces source-based routing—a mechanism that replaces the default broadcast-flooding approach with efficient unicast paths for directed traffic. This article examines the packet structure, routing logic, and failure handling implemented in the permissionlesstech/bitchat repository.
Packet Structure and the HAS_ROUTE Flag
Source-based routing in BitChat begins with the version 2 packet format defined in docs/SOURCE_ROUTING.md. The protocol introduces a HAS_ROUTE flag in the 16-byte header that signals the presence of a source-route field.
Version 2 Header Layout
The packet structure follows this exact byte layout:
- Header – 16 bytes (Version 2 identifier, 4-byte payload length,
HAS_ROUTEflag) - Fixed Fields –
SenderID(8 bytes),RecipientID(8 bytes) - Source Route – Variable length: 1-byte
Countfollowed byCount × 8bytes of intermediate peer IDs - Payload – Length indicated by the 4-byte length field
- Signature – Ed25519 signature covering the entire packet (header, IDs, route, and payload)
The route list excludes the sender and final recipient, containing only the intermediate relay IDs that form the path.
Topology Discovery and Confirmed Edges
Before routing can occur, nodes must discover the mesh topology. Each device advertises its direct neighbors via a TLV (type 0x04) appended to the IdentityAnnouncement packet.
These neighbor lists feed into the MeshTopologyTracker class, which constructs a mesh graph where edges are marked confirmed only when both peers announce each other. This bidirectional verification ensures that routes only traverse links capable of two-way communication, stored in bitchat/MeshTopologyTracker.swift.
The Routing Decision Engine in BLEService.swift
When a packet arrives that is not addressed to the local peer, the BLEService.swift file (specifically around line 330-350 in the handleIncomingPacket method) executes the routing logic:
Parsing Incoming Routes
- Version and Flag Check – If
Version ≥ 2andHAS_ROUTEis set, the service treats the packet as source-routed. - Self-Location – The service extracts the route list and searches for the local
PeerIDwithin the array.
Determining the Next Hop
The next hop selection follows precise indexing rules:
- If the local ID is found at index i, the next hop is the peer ID at position i + 1.
- If the local ID is the last entry in the route list, the next hop becomes the final
RecipientID.
The code uses meshTopology to resolve these identifiers before calling BLEService.sendMessage(to:) to unicast the packet.
Fallback to Broadcast Flooding
If the next hop cannot be reached (e.g., the link no longer exists), the system falls back to BLEService.broadcastPacket, ensuring delivery via the legacy flood mechanism while preserving backward compatibility.
Route Origination Policy (iOS Implementation)
The iOS client does not attach routes to every packet. The BLESourceRouteOriginationPolicy enforces six strict conditions before originating a source route:
- Local Authorship –
SenderIDmust equal the local device ID (relays never re-sign packets). - Directed Traffic –
RecipientIDmust not be a broadcast address. - Adequate TTL – TTL must be greater than 1, ensuring sufficient hop budget.
- Non-Direct Recipient – The recipient must not be directly connected.
- Path Constraints – The confirmed-edge path must be ≤ 4 hops, and every intermediate hop must have observed version 2 traffic.
- No Recent Failures – The
BLESourceRouteFailureCachemust not contain a recent failure entry for this recipient.
If any condition fails, the packet transmits via the broadcast flood path, maintaining compatibility with version 1 peers.
Handling Large Payloads: Fragmentation
Large file transfers require packet fragmentation. The BLEFragmentHandler.swift implementation ensures that all fragments inherit the parent packet's version 2 header and the exact same route list.
This inheritance guarantees that every fragment travels the identical unicast path, preventing individual fragments from falling back to flooding while others follow the route, which could cause reassembly failures or security issues.
Security and Failure Recovery
Signature Coverage
The route list is protected by the same Ed25519 signature that covers the entire packet. Any modification to the route—such as a malicious relay inserting or removing hops—invalidates the signature, causing the destination to drop the packet.
Failure Caching and Backoff
When a routed packet fails to receive acknowledgment from the next hop within 10 seconds, BLESourceRouteFailureCache records the incident. Subsequent routing attempts to that recipient are suppressed for 60 seconds, after which the system retries the route. This backoff mechanism prevents persistent attempts over broken links while allowing eventual recovery when topology changes.
Summary
- BitChat's BLE mesh uses protocol version 2 to enable source-based routing via a
HAS_ROUTEflag and explicit relay lists in packet headers. - Topology tracking in
MeshTopologyTracker.swiftmaintains a confirmed-edge graph from bidirectional neighbor advertisements. - Routing decisions in
BLEService.swiftparse the route list to determine the next hop, falling back to broadcast flooding when the next hop is unreachable. - Origination policy requires six conditions including path length ≤ 4 hops and no recent failures, enforced by
BLESourceRouteOriginationPolicy. - Fragmentation preserves routing consistency by copying the parent route list to all fragments via
BLEFragmentHandler.swift. - Security relies on Ed25519 signatures covering the entire packet including the route, with failure recovery managed by
BLESourceRouteFailureCacheusing 10-second timeouts and 60-second backoffs.
Frequently Asked Questions
How does BitChat prevent routing loops with source-based routing?
BitChat prevents routing loops by design: the source route is an explicit ordered list of intermediate nodes. Each relay searches for its own ID in the list and forwards only to the specific next hop at index i + 1. Since packets never deviate from this predetermined path and relays do not modify the route list (which would invalidate the Ed25519 signature), loops cannot form within the specified route structure.
What happens if an intermediate node in a source route becomes unavailable?
If BLEService.swift cannot reach the calculated next hop (stored in meshTopology), the system immediately falls back to BLEService.broadcastPacket. This broadcast flooding guarantees message delivery across the mesh at the cost of increased bandwidth, ensuring reliability even when topology changes invalidate precomputed routes.
Can legacy version 1 nodes participate in source-routed packets?
Legacy version 1 nodes cannot parse source-routed packets because they lack support for the HAS_ROUTE flag and variable-length route field. However, BitChat maintains backward compatibility: if any node in the potential path has not observed version 2 traffic, or if the origination policy detects version 1 peers in the route, the sender transmits via the legacy broadcast flood path instead of attaching a source route.
How does BitChat handle routing failures and broken paths?
When a routed packet fails to receive acknowledgment within 10 seconds, BLESourceRouteFailureCache marks the recipient as unreachable for 60 seconds. During this suppression window, the iOS client avoids originating new routes to that destination, defaulting instead to broadcast transmission. After the backoff period expires, the system clears the cache entry and attempts to establish a fresh route through the updated mesh topology.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →