source-based routing wip

This commit is contained in:
callebtc
2025-09-05 16:46:34 +02:00
parent 5769e70ea4
commit a5928987fe
7 changed files with 229 additions and 8 deletions
@@ -247,6 +247,16 @@ class BluetoothConnectionManager(
serverManager.getCharacteristic()
)
}
fun sendToPeer(peerID: String, routed: RoutedPacket): Boolean {
if (!isActive) return false
return packetBroadcaster.sendToPeer(
peerID,
routed,
serverManager.getGattServer(),
serverManager.getCharacteristic()
)
}
// Expose role controls for debug UI
@@ -401,6 +401,10 @@ class BluetoothMeshService(private val context: Context) {
override fun relayPacket(routed: RoutedPacket) {
connectionManager.broadcastPacket(routed)
}
override fun sendToPeer(peerID: String, routed: RoutedPacket): Boolean {
return connectionManager.sendToPeer(peerID, routed)
}
}
// BluetoothConnectionManager delegates
@@ -987,21 +991,36 @@ class BluetoothMeshService(private val context: Context) {
*/
private fun signPacketBeforeBroadcast(packet: BitchatPacket): BitchatPacket {
return try {
// Optionally compute and attach a source route for addressed packets
val withRoute = try {
val rec = packet.recipientID
if (rec != null && !rec.contentEquals(SpecialRecipients.BROADCAST)) {
val dest = rec.joinToString("") { b -> "%02x".format(b) }
val path = com.bitchat.android.services.meshgraph.RoutePlanner.shortestPath(myPeerID, dest)
if (path != null && path.size >= 3) {
// Exclude first (sender) and last (recipient); only intermediates
val intermediates = path.subList(1, path.size - 1)
val hopsBytes = intermediates.map { hexStringToByteArray(it) }
packet.copy(route = hopsBytes)
} else packet.copy(route = null)
} else packet
} catch (_: Exception) { packet }
// Get the canonical packet data for signing (without signature)
val packetDataForSigning = packet.toBinaryDataForSigning()
val packetDataForSigning = withRoute.toBinaryDataForSigning()
if (packetDataForSigning == null) {
Log.w(TAG, "Failed to encode packet type ${packet.type} for signing, sending unsigned")
return packet
return withRoute
}
// Sign the packet data using our signing key
val signature = encryptionService.signData(packetDataForSigning)
if (signature != null) {
Log.d(TAG, "✅ Signed packet type ${packet.type} (signature ${signature.size} bytes)")
packet.copy(signature = signature)
withRoute.copy(signature = signature)
} else {
Log.w(TAG, "Failed to sign packet type ${packet.type}, sending unsigned")
packet
withRoute
}
} catch (e: Exception) {
Log.w(TAG, "Error signing packet type ${packet.type}: ${e.message}, sending unsigned")
@@ -159,6 +159,46 @@ class BluetoothPacketBroadcaster(
}
}
}
/**
* Targeted send to a specific peer (by peerID) if directly connected.
* Returns true if sent to at least one matching connection.
*/
fun sendToPeer(
targetPeerID: String,
routed: RoutedPacket,
gattServer: BluetoothGattServer?,
characteristic: BluetoothGattCharacteristic?
): Boolean {
val packet = routed.packet
val data = packet.toBinaryData() ?: return false
val typeName = MessageType.fromValue(packet.type)?.name ?: packet.type.toString()
val senderPeerID = routed.peerID ?: packet.senderID.toHexString()
val incomingAddr = routed.relayAddress
val incomingPeer = incomingAddr?.let { connectionTracker.addressPeerMap[it] }
val senderNick = senderPeerID.let { pid -> nicknameResolver?.invoke(pid) }
// Try server-side connections first
val targetDevice = connectionTracker.getSubscribedDevices()
.firstOrNull { connectionTracker.addressPeerMap[it.address] == targetPeerID }
if (targetDevice != null) {
if (notifyDevice(targetDevice, data, gattServer, characteristic)) {
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, targetPeerID, targetDevice.address, packet.ttl)
return true
}
}
// Try client-side connections next
val targetConn = connectionTracker.getConnectedDevices().values
.firstOrNull { connectionTracker.addressPeerMap[it.device.address] == targetPeerID }
if (targetConn != null) {
if (writeToDeviceConn(targetConn, data)) {
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, targetPeerID, targetConn.device.address, packet.ttl)
return true
}
}
return false
}
/**
* Internal broadcast implementation - runs in serialized actor context
@@ -112,6 +112,9 @@ class PacketProcessor(private val myPeerID: String) {
override fun broadcastPacket(routed: RoutedPacket) {
delegate?.relayPacket(routed)
}
override fun sendToPeer(peerID: String, routed: RoutedPacket): Boolean {
return delegate?.sendToPeer(peerID, routed) ?: false
}
}
}
@@ -310,4 +313,5 @@ interface PacketProcessorDelegate {
fun sendAnnouncementToPeer(peerID: String)
fun sendCachedMessages(peerID: String)
fun relayPacket(routed: RoutedPacket)
fun sendToPeer(peerID: String, routed: RoutedPacket): Boolean
}
@@ -65,9 +65,35 @@ class PacketRelayManager(private val myPeerID: String) {
val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte())
Log.d(TAG, "Decremented TTL from ${'$'}{packet.ttl} to ${'$'}{relayPacket.ttl}")
// Source-based routing: if route is set and includes us, try targeted next-hop forwarding
val route = relayPacket.route
if (!route.isNullOrEmpty()) {
val myIdBytes = hexStringToPeerBytes(myPeerID)
val index = route.indexOfFirst { it.contentEquals(myIdBytes) }
if (index >= 0) {
val nextHopIdHex: String? = run {
val nextIndex = index + 1
if (nextIndex < route.size) {
route[nextIndex].toHexString()
} else {
// We are the last intermediate; try final recipient as next hop
relayPacket.recipientID?.toHexString()
}
}
if (nextHopIdHex != null) {
val success = try { delegate?.sendToPeer(nextHopIdHex, RoutedPacket(relayPacket, peerID, routed.relayAddress)) } catch (_: Exception) { false } ?: false
if (success) {
Log.i(TAG, "📦 Source-route relay: ${myPeerID.take(8)} -> ${nextHopIdHex.take(8)} (type ${'$'}{packet.type}, TTL ${'$'}{relayPacket.ttl})")
return
} else {
Log.w(TAG, "Source-route next hop ${nextHopIdHex.take(8)} not directly connected; falling back to broadcast")
}
}
}
}
// Apply relay logic based on packet type and debug switch
val shouldRelay = isRelayEnabled() && shouldRelayPacket(relayPacket, peerID)
if (shouldRelay) {
relayPacket(RoutedPacket(relayPacket, peerID, routed.relayAddress))
} else {
@@ -206,4 +232,17 @@ interface PacketRelayManagerDelegate {
// Packet operations
fun broadcastPacket(routed: RoutedPacket)
fun sendToPeer(peerID: String, routed: RoutedPacket): Boolean
}
private fun hexStringToPeerBytes(hex: String): ByteArray {
val result = ByteArray(8)
var idx = 0
var out = 0
while (idx + 1 < hex.length && out < 8) {
val b = hex.substring(idx, idx + 2).toIntOrNull(16)?.toByte() ?: 0
result[out++] = b
idx += 2
}
return result
}
@@ -57,7 +57,8 @@ data class BitchatPacket(
val timestamp: ULong,
val payload: ByteArray,
var signature: ByteArray? = null, // Changed from val to var for packet signing
var ttl: UByte
var ttl: UByte,
var route: List<ByteArray>? = null // Optional source route: ordered list of peerIDs (8 bytes each), including sender and final recipient
) : Parcelable {
constructor(
@@ -95,7 +96,8 @@ data class BitchatPacket(
timestamp = timestamp,
payload = payload,
signature = null, // Remove signature for signing
ttl = 0u // Use fixed TTL=0 for signing to ensure relay compatibility
ttl = 0u, // Use fixed TTL=0 for signing to ensure relay compatibility
route = route
)
return BinaryProtocol.encode(unsignedPacket)
}
@@ -147,6 +149,11 @@ data class BitchatPacket(
if (!signature.contentEquals(other.signature)) return false
} else if (other.signature != null) return false
if (ttl != other.ttl) return false
if (route != null || other.route != null) {
val a = route?.map { it.toList() } ?: emptyList()
val b = other.route?.map { it.toList() } ?: emptyList()
if (a != b) return false
}
return true
}
@@ -160,6 +167,7 @@ data class BitchatPacket(
result = 31 * result + payload.contentHashCode()
result = 31 * result + (signature?.contentHashCode() ?: 0)
result = 31 * result + ttl.hashCode()
result = 31 * result + (route?.fold(1) { acc, bytes -> 31 * acc + bytes.contentHashCode() } ?: 0)
return result
}
}
@@ -177,6 +185,7 @@ object BinaryProtocol {
const val HAS_RECIPIENT: UByte = 0x01u
const val HAS_SIGNATURE: UByte = 0x02u
const val IS_COMPRESSED: UByte = 0x04u
const val HAS_ROUTE: UByte = 0x08u
}
fun encode(packet: BitchatPacket): ByteArray? {
@@ -215,6 +224,9 @@ object BinaryProtocol {
if (isCompressed) {
flags = flags or Flags.IS_COMPRESSED
}
if (!packet.route.isNullOrEmpty()) {
flags = flags or Flags.HAS_ROUTE
}
buffer.put(flags.toByte())
// Payload length (2 bytes, big-endian) - includes original size if compressed
@@ -236,6 +248,14 @@ object BinaryProtocol {
buffer.put(ByteArray(RECIPIENT_ID_SIZE - recipientBytes.size))
}
}
// Route (optional): 1 byte count + N*8 bytes
packet.route?.let { routeList ->
val cleaned = routeList.map { bytes -> bytes.take(SENDER_ID_SIZE).toByteArray().let { if (it.size < SENDER_ID_SIZE) it + ByteArray(SENDER_ID_SIZE - it.size) else it } }
val count = cleaned.size.coerceAtMost(255)
buffer.put(count.toByte())
cleaned.take(count).forEach { hop -> buffer.put(hop) }
}
// Payload (with original size prepended if compressed)
if (isCompressed) {
@@ -302,6 +322,7 @@ object BinaryProtocol {
val hasRecipient = (flags and Flags.HAS_RECIPIENT) != 0u.toUByte()
val hasSignature = (flags and Flags.HAS_SIGNATURE) != 0u.toUByte()
val isCompressed = (flags and Flags.IS_COMPRESSED) != 0u.toUByte()
val hasRoute = (flags and Flags.HAS_ROUTE) != 0u.toUByte()
// Payload length
val payloadLength = buffer.getShort().toUShort()
@@ -309,6 +330,15 @@ object BinaryProtocol {
// Calculate expected total size
var expectedSize = HEADER_SIZE + SENDER_ID_SIZE + payloadLength.toInt()
if (hasRecipient) expectedSize += RECIPIENT_ID_SIZE
var routeCount = 0
if (hasRoute) {
// Peek count (1 byte) without consuming buffer for now
val mark = buffer.position()
if (raw.size >= mark + 1) {
routeCount = raw[mark].toUByte().toInt()
}
expectedSize += 1 + (routeCount * SENDER_ID_SIZE)
}
if (hasSignature) expectedSize += SIGNATURE_SIZE
if (raw.size < expectedSize) return null
@@ -324,6 +354,18 @@ object BinaryProtocol {
recipientBytes
} else null
// Route (optional)
val route: List<ByteArray>? = if (hasRoute) {
val count = buffer.get().toUByte().toInt()
val hops = mutableListOf<ByteArray>()
repeat(count) {
val hop = ByteArray(SENDER_ID_SIZE)
buffer.get(hop)
hops.add(hop)
}
hops
} else null
// Payload
val payload = if (isCompressed) {
// First 2 bytes are original size
@@ -357,7 +399,8 @@ object BinaryProtocol {
timestamp = timestamp,
payload = payload,
signature = signature,
ttl = ttl
ttl = ttl,
route = route
)
} catch (e: Exception) {
@@ -0,0 +1,66 @@
package com.bitchat.android.services.meshgraph
import android.util.Log
import java.util.PriorityQueue
/**
* Computes shortest paths on the current mesh graph snapshot using Dijkstra.
* Assumes unit edge weights.
*/
object RoutePlanner {
private const val TAG = "RoutePlanner"
/**
* Return full path [src, ..., dst] if reachable, else null.
*/
fun shortestPath(src: String, dst: String): List<String>? {
if (src == dst) return listOf(src)
val snapshot = MeshGraphService.getInstance().graphState.value
val neighbors = mutableMapOf<String, MutableSet<String>>()
snapshot.edges.forEach { e ->
neighbors.getOrPut(e.a) { mutableSetOf() }.add(e.b)
neighbors.getOrPut(e.b) { mutableSetOf() }.add(e.a)
}
// Ensure nodes known even if isolated
snapshot.nodes.forEach { n -> neighbors.putIfAbsent(n.peerID, mutableSetOf()) }
if (!neighbors.containsKey(src) || !neighbors.containsKey(dst)) return null
val dist = mutableMapOf<String, Int>()
val prev = mutableMapOf<String, String?>()
val pq = PriorityQueue<Pair<String, Int>>(compareBy { it.second })
neighbors.keys.forEach { v ->
dist[v] = if (v == src) 0 else Int.MAX_VALUE
prev[v] = null
}
pq.add(src to 0)
while (pq.isNotEmpty()) {
val (u, d) = pq.poll()
if (d > (dist[u] ?: Int.MAX_VALUE)) continue
if (u == dst) break
neighbors[u]?.forEach { v ->
val alt = d + 1
if (alt < (dist[v] ?: Int.MAX_VALUE)) {
dist[v] = alt
prev[v] = u
pq.add(v to alt)
}
}
}
if ((dist[dst] ?: Int.MAX_VALUE) == Int.MAX_VALUE) return null
val path = mutableListOf<String>()
var cur: String? = dst
while (cur != null) {
path.add(cur)
cur = prev[cur]
}
path.reverse()
Log.d(TAG, "Computed path $path")
return path
}
}