mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:45:18 +00:00
Add source-based routing support (#862)
* Add source-based routing support * include neighbors in ANNOUNCE --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
@@ -21,8 +21,9 @@ struct BitchatPacket: Codable {
|
|||||||
let payload: Data
|
let payload: Data
|
||||||
var signature: Data?
|
var signature: Data?
|
||||||
var ttl: UInt8
|
var ttl: UInt8
|
||||||
|
var route: [Data]?
|
||||||
|
|
||||||
init(type: UInt8, senderID: Data, recipientID: Data?, timestamp: UInt64, payload: Data, signature: Data?, ttl: UInt8, version: UInt8 = 1) {
|
init(type: UInt8, senderID: Data, recipientID: Data?, timestamp: UInt64, payload: Data, signature: Data?, ttl: UInt8, version: UInt8 = 1, route: [Data]? = nil) {
|
||||||
self.version = version
|
self.version = version
|
||||||
self.type = type
|
self.type = type
|
||||||
self.senderID = senderID
|
self.senderID = senderID
|
||||||
@@ -31,6 +32,7 @@ struct BitchatPacket: Codable {
|
|||||||
self.payload = payload
|
self.payload = payload
|
||||||
self.signature = signature
|
self.signature = signature
|
||||||
self.ttl = ttl
|
self.ttl = ttl
|
||||||
|
self.route = route
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convenience initializer for new binary format
|
// Convenience initializer for new binary format
|
||||||
@@ -53,6 +55,7 @@ struct BitchatPacket: Codable {
|
|||||||
self.payload = payload
|
self.payload = payload
|
||||||
self.signature = nil
|
self.signature = nil
|
||||||
self.ttl = ttl
|
self.ttl = ttl
|
||||||
|
self.route = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var data: Data? {
|
var data: Data? {
|
||||||
@@ -81,7 +84,8 @@ struct BitchatPacket: Codable {
|
|||||||
payload: payload,
|
payload: payload,
|
||||||
signature: nil, // Remove signature for signing
|
signature: nil, // Remove signature for signing
|
||||||
ttl: 0, // Use fixed TTL=0 for signing to ensure relay compatibility
|
ttl: 0, // Use fixed TTL=0 for signing to ensure relay compatibility
|
||||||
version: version
|
version: version,
|
||||||
|
route: route
|
||||||
)
|
)
|
||||||
return BinaryProtocol.encode(unsignedPacket)
|
return BinaryProtocol.encode(unsignedPacket)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,20 @@ extension PeerID {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PeerID {
|
||||||
|
var routingData: Data? {
|
||||||
|
if let direct = Data(hexString: id), direct.count == 8 { return direct }
|
||||||
|
if let bareData = Data(hexString: bare), bareData.count == 8 { return bareData }
|
||||||
|
let short = toShort()
|
||||||
|
return Data(hexString: short.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(routingData: Data) {
|
||||||
|
guard routingData.count == 8 else { return nil }
|
||||||
|
self.init(hexData: routingData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Validation
|
// MARK: - Validation
|
||||||
|
|
||||||
extension PeerID {
|
extension PeerID {
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ struct BinaryProtocol {
|
|||||||
static let hasRecipient: UInt8 = 0x01
|
static let hasRecipient: UInt8 = 0x01
|
||||||
static let hasSignature: UInt8 = 0x02
|
static let hasSignature: UInt8 = 0x02
|
||||||
static let isCompressed: UInt8 = 0x04
|
static let isCompressed: UInt8 = 0x04
|
||||||
|
static let hasRoute: UInt8 = 0x08
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode BitchatPacket to binary format
|
// Encode BitchatPacket to binary format
|
||||||
@@ -160,8 +161,21 @@ struct BinaryProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let lengthFieldBytes = lengthFieldSize(for: version)
|
let lengthFieldBytes = lengthFieldSize(for: version)
|
||||||
|
let originalRoute = packet.route ?? []
|
||||||
|
if originalRoute.contains(where: { $0.isEmpty }) { return nil }
|
||||||
|
let sanitizedRoute: [Data] = originalRoute.map { hop in
|
||||||
|
if hop.count == senderIDSize { return hop }
|
||||||
|
if hop.count > senderIDSize { return Data(hop.prefix(senderIDSize)) }
|
||||||
|
var padded = hop
|
||||||
|
padded.append(Data(repeating: 0, count: senderIDSize - hop.count))
|
||||||
|
return padded
|
||||||
|
}
|
||||||
|
guard sanitizedRoute.count <= 255 else { return nil }
|
||||||
|
|
||||||
|
let hasRoute = !sanitizedRoute.isEmpty
|
||||||
|
let routeLength = hasRoute ? 1 + sanitizedRoute.count * senderIDSize : 0
|
||||||
let originalSizeFieldBytes = isCompressed ? lengthFieldBytes : 0
|
let originalSizeFieldBytes = isCompressed ? lengthFieldBytes : 0
|
||||||
let payloadDataSize = payload.count + originalSizeFieldBytes
|
let payloadDataSize = routeLength + payload.count + originalSizeFieldBytes
|
||||||
|
|
||||||
if version == 1 && payloadDataSize > Int(UInt16.max) { return nil }
|
if version == 1 && payloadDataSize > Int(UInt16.max) { return nil }
|
||||||
if version == 2 && payloadDataSize > Int(UInt32.max) { return nil }
|
if version == 2 && payloadDataSize > Int(UInt32.max) { return nil }
|
||||||
@@ -185,6 +199,7 @@ struct BinaryProtocol {
|
|||||||
if packet.recipientID != nil { flags |= Flags.hasRecipient }
|
if packet.recipientID != nil { flags |= Flags.hasRecipient }
|
||||||
if packet.signature != nil { flags |= Flags.hasSignature }
|
if packet.signature != nil { flags |= Flags.hasSignature }
|
||||||
if isCompressed { flags |= Flags.isCompressed }
|
if isCompressed { flags |= Flags.isCompressed }
|
||||||
|
if hasRoute { flags |= Flags.hasRoute }
|
||||||
data.append(flags)
|
data.append(flags)
|
||||||
|
|
||||||
if version == 2 {
|
if version == 2 {
|
||||||
@@ -212,6 +227,13 @@ struct BinaryProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasRoute {
|
||||||
|
data.append(UInt8(sanitizedRoute.count))
|
||||||
|
for hop in sanitizedRoute {
|
||||||
|
data.append(hop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if isCompressed, let originalSize = originalPayloadSize {
|
if isCompressed, let originalSize = originalPayloadSize {
|
||||||
if version == 2 {
|
if version == 2 {
|
||||||
let value = UInt32(originalSize)
|
let value = UInt32(originalSize)
|
||||||
@@ -321,9 +343,27 @@ struct BinaryProtocol {
|
|||||||
if recipientID == nil { return nil }
|
if recipientID == nil { return nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var route: [Data]? = nil
|
||||||
|
var remainingPayloadBytes = payloadLength
|
||||||
|
|
||||||
|
if (flags & Flags.hasRoute) != 0 {
|
||||||
|
guard remainingPayloadBytes >= 1, let routeCount = read8() else { return nil }
|
||||||
|
remainingPayloadBytes -= 1
|
||||||
|
if routeCount > 0 {
|
||||||
|
var hops: [Data] = []
|
||||||
|
for _ in 0..<Int(routeCount) {
|
||||||
|
guard remainingPayloadBytes >= senderIDSize,
|
||||||
|
let hop = readData(senderIDSize) else { return nil }
|
||||||
|
remainingPayloadBytes -= senderIDSize
|
||||||
|
hops.append(hop)
|
||||||
|
}
|
||||||
|
route = hops
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let payload: Data
|
let payload: Data
|
||||||
if isCompressed {
|
if isCompressed {
|
||||||
guard payloadLength >= lengthFieldBytes else { return nil }
|
guard remainingPayloadBytes >= lengthFieldBytes else { return nil }
|
||||||
let originalSize: Int
|
let originalSize: Int
|
||||||
if version == 2 {
|
if version == 2 {
|
||||||
guard let rawSize = read32() else { return nil }
|
guard let rawSize = read32() else { return nil }
|
||||||
@@ -332,16 +372,12 @@ struct BinaryProtocol {
|
|||||||
guard let rawSize = read16() else { return nil }
|
guard let rawSize = read16() else { return nil }
|
||||||
originalSize = Int(rawSize)
|
originalSize = Int(rawSize)
|
||||||
}
|
}
|
||||||
// Guard to keep decompression bounded to sane BLE payload limits
|
remainingPayloadBytes -= lengthFieldBytes
|
||||||
// Use maxFramedFileBytes to account for TLV overhead in file transfer payloads
|
|
||||||
guard originalSize >= 0 && originalSize <= FileTransferLimits.maxFramedFileBytes else { return nil }
|
guard originalSize >= 0 && originalSize <= FileTransferLimits.maxFramedFileBytes else { return nil }
|
||||||
let compressedSize = payloadLength - lengthFieldBytes
|
let compressedSize = remainingPayloadBytes
|
||||||
guard compressedSize >= 0, let compressed = readData(compressedSize) else { return nil }
|
guard compressedSize > 0, let compressed = readData(compressedSize) else { return nil }
|
||||||
|
remainingPayloadBytes = 0
|
||||||
|
|
||||||
// Validate compression ratio to prevent zip bomb attacks
|
|
||||||
// Primary protection: originalSize capped at 1MB (line 336)
|
|
||||||
// Defense-in-depth: reject extreme ratios (prevents DoS via memory allocation)
|
|
||||||
guard compressedSize > 0 else { return nil }
|
|
||||||
let compressionRatio = Double(originalSize) / Double(compressedSize)
|
let compressionRatio = Double(originalSize) / Double(compressedSize)
|
||||||
guard compressionRatio <= 50_000.0 else {
|
guard compressionRatio <= 50_000.0 else {
|
||||||
SecureLogger.warning("🚫 Suspicious compression ratio: \(String(format: "%.0f", compressionRatio)):1", category: .security)
|
SecureLogger.warning("🚫 Suspicious compression ratio: \(String(format: "%.0f", compressionRatio)):1", category: .security)
|
||||||
@@ -352,7 +388,9 @@ struct BinaryProtocol {
|
|||||||
decompressed.count == originalSize else { return nil }
|
decompressed.count == originalSize else { return nil }
|
||||||
payload = decompressed
|
payload = decompressed
|
||||||
} else {
|
} else {
|
||||||
guard let rawPayload = readData(payloadLength) else { return nil }
|
guard remainingPayloadBytes >= 0,
|
||||||
|
let rawPayload = readData(remainingPayloadBytes) else { return nil }
|
||||||
|
remainingPayloadBytes = 0
|
||||||
payload = rawPayload
|
payload = rawPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +410,8 @@ struct BinaryProtocol {
|
|||||||
payload: payload,
|
payload: payload,
|
||||||
signature: signature,
|
signature: signature,
|
||||||
ttl: ttl,
|
ttl: ttl,
|
||||||
version: version
|
version: version,
|
||||||
|
route: route
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ struct AnnouncementPacket {
|
|||||||
let nickname: String
|
let nickname: String
|
||||||
let noisePublicKey: Data // Noise static public key (Curve25519.KeyAgreement)
|
let noisePublicKey: Data // Noise static public key (Curve25519.KeyAgreement)
|
||||||
let signingPublicKey: Data // Ed25519 public key for signing
|
let signingPublicKey: Data // Ed25519 public key for signing
|
||||||
|
let directNeighbors: [Data]? // 8-byte peer IDs
|
||||||
|
|
||||||
private enum TLVType: UInt8 {
|
private enum TLVType: UInt8 {
|
||||||
case nickname = 0x01
|
case nickname = 0x01
|
||||||
case noisePublicKey = 0x02
|
case noisePublicKey = 0x02
|
||||||
case signingPublicKey = 0x03
|
case signingPublicKey = 0x03
|
||||||
|
case directNeighbors = 0x04
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode() -> Data? {
|
func encode() -> Data? {
|
||||||
@@ -35,6 +37,16 @@ struct AnnouncementPacket {
|
|||||||
data.append(TLVType.signingPublicKey.rawValue)
|
data.append(TLVType.signingPublicKey.rawValue)
|
||||||
data.append(UInt8(signingPublicKey.count))
|
data.append(UInt8(signingPublicKey.count))
|
||||||
data.append(signingPublicKey)
|
data.append(signingPublicKey)
|
||||||
|
|
||||||
|
// TLV for direct neighbors (optional)
|
||||||
|
if let neighbors = directNeighbors, !neighbors.isEmpty {
|
||||||
|
let neighborsData = neighbors.prefix(10).reduce(Data()) { $0 + $1 }
|
||||||
|
if !neighborsData.isEmpty && neighborsData.count % 8 == 0 {
|
||||||
|
data.append(TLVType.directNeighbors.rawValue)
|
||||||
|
data.append(UInt8(neighborsData.count))
|
||||||
|
data.append(neighborsData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
@@ -44,6 +56,7 @@ struct AnnouncementPacket {
|
|||||||
var nickname: String?
|
var nickname: String?
|
||||||
var noisePublicKey: Data?
|
var noisePublicKey: Data?
|
||||||
var signingPublicKey: Data?
|
var signingPublicKey: Data?
|
||||||
|
var directNeighbors: [Data]?
|
||||||
|
|
||||||
while offset + 2 <= data.count {
|
while offset + 2 <= data.count {
|
||||||
let typeRaw = data[offset]
|
let typeRaw = data[offset]
|
||||||
@@ -63,6 +76,17 @@ struct AnnouncementPacket {
|
|||||||
noisePublicKey = Data(value)
|
noisePublicKey = Data(value)
|
||||||
case .signingPublicKey:
|
case .signingPublicKey:
|
||||||
signingPublicKey = Data(value)
|
signingPublicKey = Data(value)
|
||||||
|
case .directNeighbors:
|
||||||
|
if length > 0 && length % 8 == 0 {
|
||||||
|
var neighbors = [Data]()
|
||||||
|
let count = length / 8
|
||||||
|
for i in 0..<count {
|
||||||
|
let start = value.startIndex + i * 8
|
||||||
|
let end = start + 8
|
||||||
|
neighbors.append(Data(value[start..<end]))
|
||||||
|
}
|
||||||
|
directNeighbors = neighbors
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unknown TLV; skip (tolerant decoder for forward compatibility)
|
// Unknown TLV; skip (tolerant decoder for forward compatibility)
|
||||||
@@ -74,7 +98,8 @@ struct AnnouncementPacket {
|
|||||||
return AnnouncementPacket(
|
return AnnouncementPacket(
|
||||||
nickname: nickname,
|
nickname: nickname,
|
||||||
noisePublicKey: noisePublicKey,
|
noisePublicKey: noisePublicKey,
|
||||||
signingPublicKey: signingPublicKey
|
signingPublicKey: signingPublicKey,
|
||||||
|
directNeighbors: directNeighbors
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ final class BLEService: NSObject {
|
|||||||
formatter.dateFormat = "yyyyMMdd_HHmmss"
|
formatter.dateFormat = "yyyyMMdd_HHmmss"
|
||||||
return formatter
|
return formatter
|
||||||
}()
|
}()
|
||||||
|
private let meshTopology = MeshTopologyTracker()
|
||||||
|
|
||||||
// 5. Fragment Reassembly (necessary for messages > MTU)
|
// 5. Fragment Reassembly (necessary for messages > MTU)
|
||||||
private struct FragmentKey: Hashable { let sender: UInt64; let id: UInt64 }
|
private struct FragmentKey: Hashable { let sender: UInt64; let id: UInt64 }
|
||||||
@@ -569,6 +570,7 @@ final class BLEService: NSObject {
|
|||||||
peerToPeripheralUUID.removeAll()
|
peerToPeripheralUUID.removeAll()
|
||||||
subscribedCentrals.removeAll()
|
subscribedCentrals.removeAll()
|
||||||
centralToPeerID.removeAll()
|
centralToPeerID.removeAll()
|
||||||
|
meshTopology.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Connectivity and peers
|
// MARK: Connectivity and peers
|
||||||
@@ -726,6 +728,8 @@ final class BLEService: NSObject {
|
|||||||
version: 2
|
version: 2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.applyRouteIfAvailable(&packet, to: peerID)
|
||||||
|
|
||||||
if let signed = self.noiseService.signPacket(packet) {
|
if let signed = self.noiseService.signPacket(packet) {
|
||||||
packet = signed
|
packet = signed
|
||||||
}
|
}
|
||||||
@@ -745,7 +749,7 @@ final class BLEService: NSObject {
|
|||||||
SecureLogger.debug("📤 Sending READ receipt for message \(receipt.originalMessageID) to \(peerID)", category: .session)
|
SecureLogger.debug("📤 Sending READ receipt for message \(receipt.originalMessageID) to \(peerID)", category: .session)
|
||||||
do {
|
do {
|
||||||
let encrypted = try noiseService.encrypt(payload, for: peerID)
|
let encrypted = try noiseService.encrypt(payload, for: peerID)
|
||||||
let packet = BitchatPacket(
|
var packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: myPeerIDData,
|
||||||
recipientID: Data(hexString: peerID.id),
|
recipientID: Data(hexString: peerID.id),
|
||||||
@@ -754,6 +758,7 @@ final class BLEService: NSObject {
|
|||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: messageTTL
|
||||||
)
|
)
|
||||||
|
applyRouteIfAvailable(&packet, to: peerID)
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.error("Failed to send read receipt: \(error)")
|
SecureLogger.error("Failed to send read receipt: \(error)")
|
||||||
@@ -1226,7 +1231,7 @@ final class BLEService: NSObject {
|
|||||||
if noiseService.hasEstablishedSession(with: peerID) {
|
if noiseService.hasEstablishedSession(with: peerID) {
|
||||||
do {
|
do {
|
||||||
let encrypted = try noiseService.encrypt(payload, for: peerID)
|
let encrypted = try noiseService.encrypt(payload, for: peerID)
|
||||||
let packet = BitchatPacket(
|
var packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: myPeerIDData,
|
||||||
recipientID: Data(hexString: peerID.id),
|
recipientID: Data(hexString: peerID.id),
|
||||||
@@ -1235,6 +1240,7 @@ final class BLEService: NSObject {
|
|||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: messageTTL
|
||||||
)
|
)
|
||||||
|
applyRouteIfAvailable(&packet, to: peerID)
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.error("Failed to send delivery ACK: \(error)")
|
SecureLogger.error("Failed to send delivery ACK: \(error)")
|
||||||
@@ -1407,10 +1413,15 @@ final class BLEService: NSObject {
|
|||||||
let noisePub = noiseService.getStaticPublicKeyData() // For noise handshakes and peer identification
|
let noisePub = noiseService.getStaticPublicKeyData() // For noise handshakes and peer identification
|
||||||
let signingPub = noiseService.getSigningPublicKeyData() // For signature verification
|
let signingPub = noiseService.getSigningPublicKeyData() // For signature verification
|
||||||
|
|
||||||
|
let connectedPeerIDs: [Data] = collectionsQueue.sync {
|
||||||
|
peers.values.filter { $0.isConnected }.compactMap { $0.peerID.routingData }
|
||||||
|
}
|
||||||
|
|
||||||
let announcement = AnnouncementPacket(
|
let announcement = AnnouncementPacket(
|
||||||
nickname: myNickname,
|
nickname: myNickname,
|
||||||
noisePublicKey: noisePub,
|
noisePublicKey: noisePub,
|
||||||
signingPublicKey: signingPub
|
signingPublicKey: signingPub,
|
||||||
|
directNeighbors: connectedPeerIDs
|
||||||
)
|
)
|
||||||
|
|
||||||
guard let payload = announcement.encode() else {
|
guard let payload = announcement.encode() else {
|
||||||
@@ -1739,6 +1750,7 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
|
|||||||
peers[peerID] = info
|
peers[peerID] = info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
clearDirectLink(with: peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart scanning with allow duplicates for faster rediscovery
|
// Restart scanning with allow duplicates for faster rediscovery
|
||||||
@@ -2026,6 +2038,7 @@ extension BLEService: CBPeripheralDelegate {
|
|||||||
peripherals[peripheralUUID] = state
|
peripherals[peripheralUUID] = state
|
||||||
}
|
}
|
||||||
peerToPeripheralUUID[senderID] = peripheralUUID
|
peerToPeripheralUUID[senderID] = peripheralUUID
|
||||||
|
registerDirectLink(with: senderID)
|
||||||
}
|
}
|
||||||
|
|
||||||
let msgID = makeMessageID(for: packet)
|
let msgID = makeMessageID(for: packet)
|
||||||
@@ -2192,6 +2205,7 @@ extension BLEService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
// Clean up mappings
|
// Clean up mappings
|
||||||
centralToPeerID.removeValue(forKey: centralUUID)
|
centralToPeerID.removeValue(forKey: centralUUID)
|
||||||
|
clearDirectLink(with: peerID)
|
||||||
|
|
||||||
// Update UI immediately
|
// Update UI immediately
|
||||||
notifyUI { [weak self] in
|
notifyUI { [weak self] in
|
||||||
@@ -2308,7 +2322,10 @@ extension BLEService: CBPeripheralManagerDelegate {
|
|||||||
subscribedCentrals.append(sorted[0].central)
|
subscribedCentrals.append(sorted[0].central)
|
||||||
}
|
}
|
||||||
if packet.type == MessageType.announce.rawValue {
|
if packet.type == MessageType.announce.rawValue {
|
||||||
if packet.ttl == messageTTL { centralToPeerID[centralUUID] = senderID }
|
if packet.ttl == messageTTL {
|
||||||
|
centralToPeerID[centralUUID] = senderID
|
||||||
|
registerDirectLink(with: senderID)
|
||||||
|
}
|
||||||
// Record ingress link for last-hop suppression then process
|
// Record ingress link for last-hop suppression then process
|
||||||
let msgID = makeMessageID(for: packet)
|
let msgID = makeMessageID(for: packet)
|
||||||
collectionsQueue.async(flags: .barrier) { [weak self] in
|
collectionsQueue.async(flags: .barrier) { [weak self] in
|
||||||
@@ -2418,6 +2435,63 @@ extension BLEService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func routingData(for peerID: PeerID) -> Data? {
|
||||||
|
peerID.toShort().routingData
|
||||||
|
}
|
||||||
|
|
||||||
|
private func registerDirectLink(with peerID: PeerID) {
|
||||||
|
meshTopology.recordDirectLink(between: myPeerIDData, and: routingData(for: peerID))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clearDirectLink(with peerID: PeerID) {
|
||||||
|
meshTopology.removeDirectLink(between: myPeerIDData, and: routingData(for: peerID))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func registerRoute(_ route: [Data]?) {
|
||||||
|
guard let hops = route, !hops.isEmpty else { return }
|
||||||
|
meshTopology.recordRoute(hops)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func computeRoute(to peerID: PeerID) -> [Data]? {
|
||||||
|
meshTopology.computeRoute(from: myPeerIDData, to: routingData(for: peerID))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyRouteIfAvailable(_ packet: inout BitchatPacket, to recipient: PeerID) {
|
||||||
|
guard let route = computeRoute(to: recipient), route.count >= 2 else { return }
|
||||||
|
packet.route = route
|
||||||
|
meshTopology.recordRoute(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func routingPeer(from data: Data) -> PeerID? {
|
||||||
|
PeerID(routingData: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func forwardAlongRouteIfNeeded(_ packet: BitchatPacket) -> Bool {
|
||||||
|
guard let route = packet.route, !route.isEmpty else { return false }
|
||||||
|
let myRoutingData = routingData(for: myPeerID) ?? (myPeerIDData.isEmpty ? nil : myPeerIDData)
|
||||||
|
guard let selfData = myRoutingData,
|
||||||
|
let index = route.firstIndex(of: selfData) else { return false }
|
||||||
|
|
||||||
|
// No further hops: respect explicit route termination
|
||||||
|
if index == route.count - 1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
guard packet.ttl > 1 else { return true }
|
||||||
|
|
||||||
|
let nextHopData = route[index + 1]
|
||||||
|
guard let nextPeer = routingPeer(from: nextHopData),
|
||||||
|
isPeerConnected(nextPeer) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
registerDirectLink(with: nextPeer)
|
||||||
|
var relayPacket = packet
|
||||||
|
relayPacket.ttl = packet.ttl - 1
|
||||||
|
sendPacketDirected(relayPacket, to: nextPeer)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/// Safely fetch the current direct-link state for a peer using the BLE queue.
|
/// Safely fetch the current direct-link state for a peer using the BLE queue.
|
||||||
private func linkState(for peerID: PeerID) -> (hasPeripheral: Bool, hasCentral: Bool) {
|
private func linkState(for peerID: PeerID) -> (hasPeripheral: Bool, hasCentral: Bool) {
|
||||||
let computeState = { () -> (Bool, Bool) in
|
let computeState = { () -> (Bool, Bool) in
|
||||||
@@ -2451,6 +2525,7 @@ extension BLEService {
|
|||||||
let fingerprint = noiseService.getIdentityFingerprint()
|
let fingerprint = noiseService.getIdentityFingerprint()
|
||||||
myPeerID = PeerID(str: fingerprint.prefix(16))
|
myPeerID = PeerID(str: fingerprint.prefix(16))
|
||||||
myPeerIDData = Data(hexString: myPeerID.id) ?? Data()
|
myPeerIDData = Data(hexString: myPeerID.id) ?? Data()
|
||||||
|
meshTopology.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func restartGossipManager() {
|
private func restartGossipManager() {
|
||||||
@@ -2469,7 +2544,7 @@ extension BLEService {
|
|||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
let encrypted = try noiseService.encrypt(typedPayload, for: peerID)
|
let encrypted = try noiseService.encrypt(typedPayload, for: peerID)
|
||||||
let packet = BitchatPacket(
|
var packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: myPeerIDData,
|
||||||
recipientID: Data(hexString: peerID.id),
|
recipientID: Data(hexString: peerID.id),
|
||||||
@@ -2478,6 +2553,7 @@ extension BLEService {
|
|||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: messageTTL
|
||||||
)
|
)
|
||||||
|
applyRouteIfAvailable(&packet, to: peerID)
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.error("Failed to send verification payload: \(error)")
|
SecureLogger.error("Failed to send verification payload: \(error)")
|
||||||
@@ -2696,7 +2772,7 @@ extension BLEService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let packet = BitchatPacket(
|
var packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: myPeerIDData,
|
||||||
recipientID: recipientData,
|
recipientID: recipientData,
|
||||||
@@ -2705,6 +2781,7 @@ extension BLEService {
|
|||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: messageTTL
|
||||||
)
|
)
|
||||||
|
applyRouteIfAvailable(&packet, to: recipientID)
|
||||||
|
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
|
|
||||||
@@ -2744,7 +2821,7 @@ extension BLEService {
|
|||||||
let handshakeData = try noiseService.initiateHandshake(with: peerID)
|
let handshakeData = try noiseService.initiateHandshake(with: peerID)
|
||||||
|
|
||||||
// Send handshake init
|
// Send handshake init
|
||||||
let packet = BitchatPacket(
|
var packet = BitchatPacket(
|
||||||
type: MessageType.noiseHandshake.rawValue,
|
type: MessageType.noiseHandshake.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: myPeerIDData,
|
||||||
recipientID: Data(hexString: peerID.id),
|
recipientID: Data(hexString: peerID.id),
|
||||||
@@ -2753,6 +2830,7 @@ extension BLEService {
|
|||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: messageTTL
|
||||||
)
|
)
|
||||||
|
applyRouteIfAvailable(&packet, to: peerID)
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.error("Failed to initiate handshake: \(error)")
|
SecureLogger.error("Failed to initiate handshake: \(error)")
|
||||||
@@ -2786,7 +2864,7 @@ extension BLEService {
|
|||||||
|
|
||||||
let encrypted = try noiseService.encrypt(messagePayload, for: peerID)
|
let encrypted = try noiseService.encrypt(messagePayload, for: peerID)
|
||||||
|
|
||||||
let packet = BitchatPacket(
|
var packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: myPeerIDData,
|
||||||
recipientID: Data(hexString: peerID.id),
|
recipientID: Data(hexString: peerID.id),
|
||||||
@@ -2795,6 +2873,7 @@ extension BLEService {
|
|||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: messageTTL
|
||||||
)
|
)
|
||||||
|
applyRouteIfAvailable(&packet, to: peerID)
|
||||||
|
|
||||||
// We're already on messageQueue from the callback
|
// We're already on messageQueue from the callback
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
@@ -2946,7 +3025,8 @@ extension BLEService {
|
|||||||
timestamp: packet.timestamp,
|
timestamp: packet.timestamp,
|
||||||
payload: payload,
|
payload: payload,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: packet.ttl
|
ttl: packet.ttl,
|
||||||
|
route: packet.route
|
||||||
)
|
)
|
||||||
|
|
||||||
let workItem = DispatchWorkItem { [weak self] in
|
let workItem = DispatchWorkItem { [weak self] in
|
||||||
@@ -3161,6 +3241,11 @@ extension BLEService {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerRoute(packet.route)
|
||||||
|
if peerID != myPeerID && packet.ttl == messageTTL {
|
||||||
|
registerDirectLink(with: peerID)
|
||||||
|
}
|
||||||
|
|
||||||
// Deduplication (thread-safe)
|
// Deduplication (thread-safe)
|
||||||
let senderID = PeerID(hexData: packet.senderID)
|
let senderID = PeerID(hexData: packet.senderID)
|
||||||
@@ -3245,6 +3330,10 @@ extension BLEService {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if forwardAlongRouteIfNeeded(packet) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Relay if TTL > 1 and we're not the original sender
|
// Relay if TTL > 1 and we're not the original sender
|
||||||
// Relay decision and scheduling (extracted via RelayController)
|
// Relay decision and scheduling (extracted via RelayController)
|
||||||
do {
|
do {
|
||||||
@@ -3591,7 +3680,7 @@ extension BLEService {
|
|||||||
do {
|
do {
|
||||||
if let response = try noiseService.processHandshakeMessage(from: peerID, message: packet.payload) {
|
if let response = try noiseService.processHandshakeMessage(from: peerID, message: packet.payload) {
|
||||||
// Send response
|
// Send response
|
||||||
let responsePacket = BitchatPacket(
|
var responsePacket = BitchatPacket(
|
||||||
type: MessageType.noiseHandshake.rawValue,
|
type: MessageType.noiseHandshake.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: myPeerIDData,
|
||||||
recipientID: Data(hexString: peerID.id),
|
recipientID: Data(hexString: peerID.id),
|
||||||
@@ -3600,6 +3689,7 @@ extension BLEService {
|
|||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: messageTTL
|
||||||
)
|
)
|
||||||
|
applyRouteIfAvailable(&responsePacket, to: peerID)
|
||||||
// We're on messageQueue from delegate callback
|
// We're on messageQueue from delegate callback
|
||||||
broadcastPacket(responsePacket)
|
broadcastPacket(responsePacket)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Tracks observed mesh topology and computes hop-by-hop routes.
|
||||||
|
final class MeshTopologyTracker {
|
||||||
|
private typealias RoutingID = Data
|
||||||
|
|
||||||
|
private let queue = DispatchQueue(label: "mesh.topology", attributes: .concurrent)
|
||||||
|
private let hopSize = 8
|
||||||
|
private var adjacency: [RoutingID: Set<RoutingID>] = [:]
|
||||||
|
|
||||||
|
func reset() {
|
||||||
|
queue.sync(flags: .barrier) {
|
||||||
|
self.adjacency.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordDirectLink(between a: Data?, and b: Data?) {
|
||||||
|
guard let left = sanitize(a), let right = sanitize(b), left != right else { return }
|
||||||
|
queue.sync(flags: .barrier) {
|
||||||
|
var setA = self.adjacency[left] ?? []
|
||||||
|
setA.insert(right)
|
||||||
|
self.adjacency[left] = setA
|
||||||
|
|
||||||
|
var setB = self.adjacency[right] ?? []
|
||||||
|
setB.insert(left)
|
||||||
|
self.adjacency[right] = setB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeDirectLink(between a: Data?, and b: Data?) {
|
||||||
|
guard let left = sanitize(a), let right = sanitize(b), left != right else { return }
|
||||||
|
queue.sync(flags: .barrier) {
|
||||||
|
if var setA = self.adjacency[left] {
|
||||||
|
setA.remove(right)
|
||||||
|
self.adjacency[left] = setA.isEmpty ? nil : setA
|
||||||
|
}
|
||||||
|
if var setB = self.adjacency[right] {
|
||||||
|
setB.remove(left)
|
||||||
|
self.adjacency[right] = setB.isEmpty ? nil : setB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removePeer(_ data: Data?) {
|
||||||
|
guard let peer = sanitize(data) else { return }
|
||||||
|
queue.sync(flags: .barrier) {
|
||||||
|
guard let neighbors = self.adjacency.removeValue(forKey: peer) else { return }
|
||||||
|
for neighbor in neighbors {
|
||||||
|
if var set = self.adjacency[neighbor] {
|
||||||
|
set.remove(peer)
|
||||||
|
self.adjacency[neighbor] = set.isEmpty ? nil : set
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordRoute(_ hops: [Data]) {
|
||||||
|
let sanitized = hops.compactMap { sanitize($0) }
|
||||||
|
guard sanitized.count >= 2 else { return }
|
||||||
|
queue.sync(flags: .barrier) {
|
||||||
|
for idx in 0..<(sanitized.count - 1) {
|
||||||
|
let left = sanitized[idx]
|
||||||
|
let right = sanitized[idx + 1]
|
||||||
|
guard left != right else { continue }
|
||||||
|
|
||||||
|
var setA = self.adjacency[left] ?? []
|
||||||
|
setA.insert(right)
|
||||||
|
self.adjacency[left] = setA
|
||||||
|
|
||||||
|
var setB = self.adjacency[right] ?? []
|
||||||
|
setB.insert(left)
|
||||||
|
self.adjacency[right] = setB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeRoute(from start: Data?, to goal: Data?, maxHops: Int = 255) -> [Data]? {
|
||||||
|
guard let source = sanitize(start), let target = sanitize(goal) else { return nil }
|
||||||
|
if source == target { return [source] }
|
||||||
|
|
||||||
|
let graph = queue.sync { adjacency }
|
||||||
|
guard graph[source] != nil, graph[target] != nil else { return nil }
|
||||||
|
|
||||||
|
var visited: Set<RoutingID> = [source]
|
||||||
|
var queuePaths: [[RoutingID]] = [[source]]
|
||||||
|
var index = 0
|
||||||
|
|
||||||
|
while index < queuePaths.count {
|
||||||
|
let path = queuePaths[index]
|
||||||
|
index += 1
|
||||||
|
guard path.count <= maxHops else { continue }
|
||||||
|
guard let last = path.last, let neighbors = graph[last] else { continue }
|
||||||
|
|
||||||
|
for neighbor in neighbors {
|
||||||
|
if visited.contains(neighbor) { continue }
|
||||||
|
var nextPath = path
|
||||||
|
nextPath.append(neighbor)
|
||||||
|
if neighbor == target { return nextPath }
|
||||||
|
if nextPath.count <= maxHops {
|
||||||
|
queuePaths.append(nextPath)
|
||||||
|
}
|
||||||
|
visited.insert(neighbor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private func sanitize(_ data: Data?) -> Data? {
|
||||||
|
guard var value = data, !value.isEmpty else { return nil }
|
||||||
|
if value.count > hopSize {
|
||||||
|
value = Data(value.prefix(hopSize))
|
||||||
|
} else if value.count < hopSize {
|
||||||
|
value.append(Data(repeating: 0, count: hopSize - value.count))
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,86 @@ struct BinaryProtocolTests {
|
|||||||
#expect(decodedPacket.signature != nil)
|
#expect(decodedPacket.signature != nil)
|
||||||
#expect(decodedPacket.signature == TestConstants.testSignature)
|
#expect(decodedPacket.signature == TestConstants.testSignature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func packetWithRouteRoundTrip() throws {
|
||||||
|
let route: [Data] = [
|
||||||
|
try #require(Data(hexString: "0102030405060708")),
|
||||||
|
try #require(Data(hexString: "1112131415161718")),
|
||||||
|
try #require(Data(hexString: "2122232425262728"))
|
||||||
|
]
|
||||||
|
|
||||||
|
var packet = BitchatPacket(
|
||||||
|
type: 0x01,
|
||||||
|
senderID: route[0],
|
||||||
|
recipientID: route.last,
|
||||||
|
timestamp: 1_720_000_000_000,
|
||||||
|
payload: Data("route-test".utf8),
|
||||||
|
signature: nil,
|
||||||
|
ttl: 6
|
||||||
|
)
|
||||||
|
packet.route = route
|
||||||
|
|
||||||
|
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode packet with route")
|
||||||
|
let flagsByte = encoded[BinaryProtocol.Offsets.flags]
|
||||||
|
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) != 0)
|
||||||
|
|
||||||
|
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode packet with route")
|
||||||
|
let decodedRoute = try #require(decoded.route)
|
||||||
|
#expect(decodedRoute.count == route.count)
|
||||||
|
for (expected, actual) in zip(route, decodedRoute) {
|
||||||
|
#expect(actual == expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func packetWithRoutePadsShortHop() throws {
|
||||||
|
let sender = try #require(Data(hexString: "0011223344556677"))
|
||||||
|
let destination = try #require(Data(hexString: "8899aabbccddeeff"))
|
||||||
|
let shortHop = Data([0xAA, 0xBB, 0xCC])
|
||||||
|
|
||||||
|
var packet = BitchatPacket(
|
||||||
|
type: 0x02,
|
||||||
|
senderID: sender,
|
||||||
|
recipientID: destination,
|
||||||
|
timestamp: 1_730_000_000_000,
|
||||||
|
payload: Data("pad-test".utf8),
|
||||||
|
signature: nil,
|
||||||
|
ttl: 5
|
||||||
|
)
|
||||||
|
packet.route = [shortHop, destination]
|
||||||
|
|
||||||
|
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode packet with short hop route")
|
||||||
|
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode packet with short hop route")
|
||||||
|
let decodedRoute = try #require(decoded.route)
|
||||||
|
let firstHop = try #require(decodedRoute.first)
|
||||||
|
#expect(firstHop.count == BinaryProtocol.senderIDSize)
|
||||||
|
#expect(firstHop.prefix(shortHop.count) == shortHop)
|
||||||
|
let paddingBytes = firstHop.suffix(firstHop.count - shortHop.count)
|
||||||
|
#expect(paddingBytes.allSatisfy { $0 == 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func packetWithRouteAndCompressedPayload() throws {
|
||||||
|
let route: [Data] = [
|
||||||
|
try #require(Data(hexString: "0101010101010101")),
|
||||||
|
try #require(Data(hexString: "0202020202020202"))
|
||||||
|
]
|
||||||
|
let repeatedString = String(repeating: "compress-me", count: 150)
|
||||||
|
var packet = BitchatPacket(
|
||||||
|
type: 0x03,
|
||||||
|
senderID: route[0],
|
||||||
|
recipientID: route.last,
|
||||||
|
timestamp: 1_740_000_000_000,
|
||||||
|
payload: Data(repeatedString.utf8),
|
||||||
|
signature: nil,
|
||||||
|
ttl: 7
|
||||||
|
)
|
||||||
|
packet.route = route
|
||||||
|
|
||||||
|
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode packet with route and compression")
|
||||||
|
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode packet with route and compression")
|
||||||
|
#expect(decoded.payload == Data(repeatedString.utf8))
|
||||||
|
let decodedRoute = try #require(decoded.route)
|
||||||
|
#expect(decodedRoute == route)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Compression Tests
|
// MARK: - Compression Tests
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
//
|
||||||
|
// MeshTopologyTrackerTests.swift
|
||||||
|
// bitchatTests
|
||||||
|
//
|
||||||
|
// This is free and unencumbered software released into the public domain.
|
||||||
|
// For more information, see <https://unlicense.org>
|
||||||
|
//
|
||||||
|
|
||||||
|
import Testing
|
||||||
|
import Foundation
|
||||||
|
@testable import bitchat
|
||||||
|
|
||||||
|
struct MeshTopologyTrackerTests {
|
||||||
|
private func hex(_ value: String) throws -> Data {
|
||||||
|
try #require(Data(hexString: value))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func directLinkProducesRoute() throws {
|
||||||
|
let tracker = MeshTopologyTracker()
|
||||||
|
let a = try hex("0102030405060708")
|
||||||
|
let b = try hex("1112131415161718")
|
||||||
|
|
||||||
|
tracker.recordDirectLink(between: a, and: b)
|
||||||
|
let route = try #require(tracker.computeRoute(from: a, to: b))
|
||||||
|
#expect(route == [a, b])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func multiHopRouteComputation() throws {
|
||||||
|
let tracker = MeshTopologyTracker()
|
||||||
|
let a = try hex("0001020304050607")
|
||||||
|
let b = try hex("1011121314151617")
|
||||||
|
let c = try hex("2021222324252627")
|
||||||
|
let d = try hex("3031323334353637")
|
||||||
|
|
||||||
|
tracker.recordDirectLink(between: a, and: b)
|
||||||
|
tracker.recordDirectLink(between: b, and: c)
|
||||||
|
tracker.recordDirectLink(between: c, and: d)
|
||||||
|
|
||||||
|
let route = try #require(tracker.computeRoute(from: a, to: d))
|
||||||
|
#expect(route == [a, b, c, d])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func recordRouteAddsEdges() throws {
|
||||||
|
let tracker = MeshTopologyTracker()
|
||||||
|
var a = Data([0xAA, 0xBB, 0xCC])
|
||||||
|
let b = try hex("4445464748494A4B")
|
||||||
|
let c = try hex("5455565758595A5B")
|
||||||
|
|
||||||
|
tracker.recordRoute([a, b, c])
|
||||||
|
|
||||||
|
a.append(Data(repeating: 0, count: BinaryProtocol.senderIDSize - a.count))
|
||||||
|
let route = try #require(tracker.computeRoute(from: a, to: c))
|
||||||
|
#expect(route.first == a)
|
||||||
|
#expect(route.last == c)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func removingDirectLinkBreaksRoute() throws {
|
||||||
|
let tracker = MeshTopologyTracker()
|
||||||
|
let a = try hex("0101010101010101")
|
||||||
|
let b = try hex("0202020202020202")
|
||||||
|
let c = try hex("0303030303030303")
|
||||||
|
|
||||||
|
tracker.recordDirectLink(between: a, and: b)
|
||||||
|
tracker.recordDirectLink(between: b, and: c)
|
||||||
|
let initialRoute = try #require(tracker.computeRoute(from: a, to: c))
|
||||||
|
#expect(initialRoute == [a, b, c])
|
||||||
|
|
||||||
|
tracker.removeDirectLink(between: b, and: c)
|
||||||
|
#expect(tracker.computeRoute(from: a, to: c) == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func removingPeerClearsEdges() throws {
|
||||||
|
let tracker = MeshTopologyTracker()
|
||||||
|
let a = try hex("0F0E0D0C0B0A0908")
|
||||||
|
let b = try hex("0A0B0C0D0E0F0001")
|
||||||
|
let c = try hex("0011223344556677")
|
||||||
|
|
||||||
|
tracker.recordRoute([a, b, c])
|
||||||
|
let initialRoute = try #require(tracker.computeRoute(from: a, to: c))
|
||||||
|
#expect(initialRoute == [a, b, c])
|
||||||
|
|
||||||
|
tracker.removePeer(b)
|
||||||
|
#expect(tracker.computeRoute(from: a, to: c) == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -217,7 +217,27 @@ struct PeerIDTests {
|
|||||||
let short = peerID.toShort()
|
let short = peerID.toShort()
|
||||||
#expect(short == peerID)
|
#expect(short == peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func routingData_fromShortID() throws {
|
||||||
|
let peerID = PeerID(str: hex16)
|
||||||
|
let routing = try #require(peerID.routingData)
|
||||||
|
#expect(routing.count == 8)
|
||||||
|
#expect(routing == Data(hexString: hex16))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func routingData_fromNoiseKey() throws {
|
||||||
|
let peerID = PeerID(str: hex64)
|
||||||
|
let routing = try #require(peerID.routingData)
|
||||||
|
let expectedShort = peerID.toShort()
|
||||||
|
#expect(routing == Data(hexString: expectedShort.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func routingPeerRoundTrip() throws {
|
||||||
|
let raw = try #require(Data(hexString: hex16))
|
||||||
|
let peerID = try #require(PeerID(routingData: raw))
|
||||||
|
#expect(peerID.routingData == raw)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Codable
|
// MARK: - Codable
|
||||||
|
|
||||||
@Test func codable_emptyPrefix() throws {
|
@Test func codable_emptyPrefix() throws {
|
||||||
|
|||||||
Reference in New Issue
Block a user