BitChat Source Routing Mechanism: How Explicit Hop Paths Replace Flooding in Protocol v2
BitChat implements source-based routing through protocol version 2, which embeds an explicit ordered list of intermediate relay peers directly into packets, enabling efficient unicast forwarding instead of default flood-and-gossip behavior.
The BitChat peer-to-peer messaging protocol supports two forwarding modes: legacy flooding and directed unicast via source routing. This article examines the source routing mechanism as implemented in the permissionlesstech/bitchat repository, covering packet structure, topology discovery, route computation, and relay forwarding logic.
Protocol Versioning and Routing Flags
BitChat distinguishes packet capabilities through version numbering. Version 1 uses a 2-byte payload length field with no routing capabilities. Version 2 expands this to 4 bytes and introduces the HAS_ROUTE (0x08) flag, which is only valid when version ≥ 2 according to [docs/SOURCE_ROUTING.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/SOURCE_ROUTING.md#L11-L17).
The version field determines how peers parse subsequent bytes. A v2 parser checks the flag bitmap before attempting to read the variable-length source route field.
Packet Layout and Source Route Field
A v2 packet with source routing activated follows this byte layout:
Fixed Header (16 B) | SenderID (8 B) | RecipientID (8 B) |
Source Route (variable) | Payload | Signature
The Source Route field consists of two components:
- 1-byte hop count specifying how many intermediate relays follow
- N × 8-byte peer IDs for each intermediate hop
As documented in [docs/SOURCE_ROUTING.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/SOURCE_ROUTING.md#L68-L71), the route lists only intermediate hops—the sender and final recipient are intentionally omitted. This reduces packet size and prevents route manipulation attacks that might target endpoint impersonation.
Topology Discovery via Neighbor Gossip
Before computing routes, nodes must build a mesh graph. BitChat accomplishes this through a Neighbor List TLV (0x04) embedded in IdentityAnnouncement packets. Each node advertises its directly-connected peers using this TLV structure defined in [docs/SOURCE_ROUTING.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/SOURCE_ROUTING.md#L82-L94).
The mesh-graph service constructs bidirectional edges with strict confirmation requirements: an edge is only valid when both peers list each other. Unconfirmed edges are ignored for route calculation to prevent routing through stale or one-sided connections. This symmetric validation appears in [docs/SOURCE_ROUTING.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/SOURCE_ROUTING.md#L98-L104).
Route Computation and Origination Policy
When the local device initiates a directed packet, BLESourceRouteOriginationPolicy in [bitchat/Services/BLE/BLESourceRouteOriginationPolicy.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLESourceRouteOriginationPolicy.swift#L4-L39) applies six sequential gate checks:
- Local authorship – the packet must originate from this peer
- Single-peer destination –
RecipientIDtargets exactly one peer - TTL > 1 – link-local packets like
REQUEST_SYNCnever carry routes - Indirect recipient – the destination must not be directly connected
- Policy permission –
shouldAttemptRoutereturns true - Route availability –
computeRouteclosure returns a non-empty path
If any gate fails, the packet falls back to the legacy flood/direct-write path. This conservative approach preserves reliability while opportunistically optimizing when conditions permit.
Swift Implementation Example
// Route origination with policy gates
let packet = BitchatPacket(
version: 2,
senderID: localPeerIDData,
recipientID: recipientIDData,
payload: messageData,
flags: [.hasRoute]
)
// Policy evaluation through BLESourceRouteOriginationPolicy
let routedPacket = BLESourceRouteOriginationPolicy.route(
for: packet,
to: recipientID,
localPeerIDData: localPeerIDData,
isRecipientConnected: { BLEService.shared.isConnected($0) },
shouldAttemptRoute: BLEService.shared.shouldAttemptRoute,
computeRoute: { target in
MeshTopologyTracker.shared.computeRoute(to: target)?
.map { $0.rawData }
}
)
// Transmission via BLEService
BLEService.shared.send(routedPacket)
Forwarding Logic for Relay Nodes
When a relay receives a packet not addressed to itself, BLERouteForwardingPolicy in [bitchat/Services/BLE/BLERouteForwardingPolicy.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLERouteForwardingPolicy.swift) determines handling through priority-ordered conditions:
| Condition | Action | Source Lines |
|---|---|---|
REQUEST_SYNC packet type |
Suppress flood relay | L38-L44 |
| Packet addressed to local peer | Suppress flood relay | L46-L48 |
| No route present in packet | Allow normal flood relay | L50-L52 |
| TTL ≤ 1 | Suppress flood relay (loop prevention) | L54-L56 |
| Valid route present | Forward to next hop (or destination if final hop) | L62-L73 |
Critical reliability feature: if the calculated next hop is unreachable, the policy falls back to flood-relay to guarantee delivery rather than silently dropping the packet.
Relay Forwarding Implementation
func handleRelay(_ packet: BitchatPacket, from incomingPeer: PeerID) {
let forwardPlan = BLERouteForwardingPolicy.plan(
for: packet,
localPeerID: localPeerID,
localRoutingData: localPeerID.rawData,
routingPeer: { data in MeshTopologyTracker.shared.peer(for: data) },
isPeerConnected: { BLEService.shared.isConnected($0) }
)
switch forwardPlan {
case .allowFloodRelay:
flood(packet, excludedPeer: incomingPeer)
case .suppressFloodRelay:
// Process locally or drop per packet type rules
processLocally(packet)
case .forward(let forwardPacket, let nextHopPeer):
guard let nextHop = nextHopPeer else {
// Unreachable next hop: fallback to flood
flood(packet, excludedPeer: incomingPeer)
return
}
BLEService.shared.send(to: nextHop, packet: forwardPacket)
}
}
Security and Integrity
The existing Ed25519 signature covers the entire serialized packet including the source route field. Any modification to hop count, peer IDs, or order invalidates the signature, causing receiving peers to drop the packet before processing. This design prevents:
- Route manipulation attacks that might redirect traffic
- Hop count inflation for denial-of-service amplification
- Path truncation attacks
As noted in [docs/SOURCE_ROUTING.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/SOURCE_ROUTING.md#L18-L25), the signature boundary encompasses all routing metadata, not just the payload.
Fragmentation Handling
Large packets exceeding MTU are fragmented, but all fragments inherit the v2 version and identical source route. This ensures fragmented streams continue unicast forwarding rather than reverting to flooding mid-transmission. Per [docs/SOURCE_ROUTING.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/SOURCE_ROUTING.md#L107-L113), fragment reassembly occurs at the destination using the same route path as the original packet.
Key Implementation Files
Summary
- Source routing in BitChat operates exclusively through protocol v2 packets with the
HAS_ROUTEflag set - Topology discovery uses symmetric neighbor gossip via TLV
0x04in identity announcements - Route origination requires six policy gates to pass, falling back to flooding on any failure
- Relay forwarding follows explicit hop lists with TTL-based loop prevention and unreachable-hop fallback
- Security is maintained through end-to-end Ed25519 signatures covering all routing metadata
- Fragmentation preserves source routes across all packet segments
Frequently Asked Questions
What triggers source routing instead of flooding in BitChat?
Source routing activates when BLESourceRouteOriginationPolicy confirms six conditions: local packet authorship, single-peer destination, TTL greater than 1, indirect recipient connection, policy permission via shouldAttemptRoute, and successful route computation. If any condition fails, the packet uses legacy flooding. This conservative design prioritizes delivery reliability over optimization.
How does BitChat prevent routing loops with source routes?
The BLERouteForwardingPolicy suppresses flood relay when TTL ≤ 1, preventing packets from circulating indefinitely. Additionally, each relay decrements TTL and forwards only to the explicit next hop. The source route itself is fixed at origination—relays cannot modify the path, eliminating dynamic loop formation through protocol design.
Can an attacker modify the source route to redirect traffic?
No. The Ed25519 signature covers the entire packet including the 1-byte hop count and all 8-byte peer IDs in the route. Any bit-flipping in the route invalidates the signature, causing immediate packet rejection by the next receiver. The sender and recipient are omitted from the route field specifically to reduce attack surface against endpoint impersonation.
What happens if an intermediate relay in the source route goes offline?
The BLERouteForwardingPolicy detects unreachable next hops through the isPeerConnected closure evaluation. When the designated next hop is unavailable, the policy automatically falls back to flood-relay behavior, broadcasting the packet to all connected peers except the incoming relay. This guarantees eventual delivery at the cost of temporarily increased bandwidth usage.
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 →