mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-15 06:56:30 +00:00
fix: complete direct-link routing rollback
This commit is contained in:
parent
6dff0844bb
commit
62dd3ca90e
@ -0,0 +1,26 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
|
||||
/**
|
||||
* Describes transport reachability learned from an already-validated ANNOUNCE.
|
||||
*
|
||||
* This is deliberately only a routing observation. Noise authenticates the peer independently and
|
||||
* must not be restarted merely to associate the current transport link with that peer.
|
||||
*/
|
||||
internal object DirectLinkAnnouncementPolicy {
|
||||
data class Observation(
|
||||
val peerID: String,
|
||||
val relayAddress: String,
|
||||
val ingressLinkID: String
|
||||
)
|
||||
|
||||
fun observationFor(routed: RoutedPacket, maxTtl: UByte): Observation? {
|
||||
if (routed.packet.ttl != maxTtl) return null
|
||||
return Observation(
|
||||
peerID = routed.peerID ?: return null,
|
||||
relayAddress = routed.relayAddress ?: return null,
|
||||
ingressLinkID = routed.ingressLinkID ?: return null
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package com.bitchat.android.wifiaware
|
||||
|
||||
/** Resolves a packet to the exact still-active ingress link that delivered it. */
|
||||
internal object IngressLinkPolicy {
|
||||
data class Link<T : Any>(
|
||||
val relayAddress: String,
|
||||
val transport: T
|
||||
)
|
||||
|
||||
fun <T : Any> resolve(
|
||||
ingressLinkID: String?,
|
||||
relayAddress: String?,
|
||||
links: Map<String, Link<T>>,
|
||||
currentTransportForRelay: (String) -> T?
|
||||
): Link<T>? {
|
||||
val linkID = ingressLinkID ?: return null
|
||||
val relayAddress = relayAddress ?: return null
|
||||
val link = links[linkID] ?: return null
|
||||
if (link.relayAddress != relayAddress) return null
|
||||
return link.takeIf { currentTransportForRelay(relayAddress) === it.transport }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.whenever
|
||||
|
||||
class BluetoothConnectionTrackerLinkObservationTest {
|
||||
private val scope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
|
||||
private val tracker = BluetoothConnectionTracker(scope, mock())
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stale connection callbacks cannot mutate or remove replacement link`() {
|
||||
val address = "AA:BB:CC:DD:EE:FF"
|
||||
val device = mock<BluetoothDevice>()
|
||||
whenever(device.address).thenReturn(address)
|
||||
|
||||
tracker.addDeviceConnection(
|
||||
address,
|
||||
BluetoothConnectionTracker.DeviceConnection(device = device, linkID = "link-a")
|
||||
)
|
||||
tracker.addDeviceConnection(
|
||||
address,
|
||||
BluetoothConnectionTracker.DeviceConnection(device = device, linkID = "link-b")
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
tracker.updateDeviceConnectionIfCurrent(address, "link-a") {
|
||||
it.copy(rssi = -10)
|
||||
}
|
||||
)
|
||||
assertFalse(tracker.cleanupDeviceConnectionIfCurrent(address, "link-a"))
|
||||
assertEquals("link-b", tracker.getCurrentLinkID(address))
|
||||
|
||||
assertTrue(tracker.observePeerIfCurrent(address, "link-b", "0011223344556677"))
|
||||
assertEquals("0011223344556677", tracker.addressPeerMap[address])
|
||||
assertSame(device, tracker.getDeviceConnection(address)?.device)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `one peer can remain directly observed over multiple current links`() {
|
||||
val firstAddress = "AA:BB:CC:DD:EE:01"
|
||||
val secondAddress = "AA:BB:CC:DD:EE:02"
|
||||
val firstDevice = mock<BluetoothDevice>()
|
||||
val secondDevice = mock<BluetoothDevice>()
|
||||
whenever(firstDevice.address).thenReturn(firstAddress)
|
||||
whenever(secondDevice.address).thenReturn(secondAddress)
|
||||
|
||||
tracker.addDeviceConnection(
|
||||
firstAddress,
|
||||
BluetoothConnectionTracker.DeviceConnection(device = firstDevice, linkID = "link-a")
|
||||
)
|
||||
tracker.addDeviceConnection(
|
||||
secondAddress,
|
||||
BluetoothConnectionTracker.DeviceConnection(device = secondDevice, linkID = "link-b")
|
||||
)
|
||||
|
||||
assertTrue(tracker.observePeerIfCurrent(firstAddress, "link-a", PEER_ID))
|
||||
assertTrue(tracker.observePeerIfCurrent(secondAddress, "link-b", PEER_ID))
|
||||
assertTrue(tracker.observePeerIfCurrent(secondAddress, "link-b", PEER_ID))
|
||||
assertEquals(2, tracker.addressPeerMap.values.count { it == PEER_ID })
|
||||
|
||||
assertTrue(tracker.cleanupDeviceConnectionIfCurrent(firstAddress, "link-a"))
|
||||
assertEquals(PEER_ID, tracker.addressPeerMap[secondAddress])
|
||||
assertTrue(tracker.addressPeerMap.containsValue(PEER_ID))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val PEER_ID = "0011223344556677"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class DirectLinkAnnouncementPolicyTest {
|
||||
@Test
|
||||
fun `accepted max ttl announce is a direct routing observation`() {
|
||||
val routed = announce(ttl = MAX_TTL)
|
||||
|
||||
assertEquals(
|
||||
DirectLinkAnnouncementPolicy.Observation(PEER_ID, RELAY_ADDRESS, LINK_ID),
|
||||
DirectLinkAnnouncementPolicy.observationFor(routed, MAX_TTL)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `relayed announce is not a direct routing observation`() {
|
||||
assertNull(
|
||||
DirectLinkAnnouncementPolicy.observationFor(
|
||||
announce(ttl = (MAX_TTL - 1u).toUByte()),
|
||||
MAX_TTL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `repeated announce remains the same observation without transport authentication state`() {
|
||||
val routed = announce(ttl = MAX_TTL)
|
||||
|
||||
val first = DirectLinkAnnouncementPolicy.observationFor(routed, MAX_TTL)
|
||||
val second = DirectLinkAnnouncementPolicy.observationFor(routed, MAX_TTL)
|
||||
|
||||
assertEquals(first, second)
|
||||
}
|
||||
|
||||
private fun announce(ttl: UByte) = RoutedPacket(
|
||||
packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
senderID = PEER_ID.hexToBytes(),
|
||||
timestamp = 1u,
|
||||
payload = byteArrayOf(1),
|
||||
ttl = ttl
|
||||
),
|
||||
peerID = PEER_ID,
|
||||
relayAddress = RELAY_ADDRESS,
|
||||
ingressLinkID = LINK_ID
|
||||
)
|
||||
|
||||
private fun String.hexToBytes(): ByteArray =
|
||||
chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
|
||||
private companion object {
|
||||
const val PEER_ID = "0011223344556677"
|
||||
const val RELAY_ADDRESS = "transport-neighbor"
|
||||
const val LINK_ID = "current-link"
|
||||
val MAX_TTL: UByte = 7u
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package com.bitchat.android.wifiaware
|
||||
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Test
|
||||
|
||||
class IngressLinkPolicyTest {
|
||||
@Test
|
||||
fun `observation resolves only the exact ingress link`() {
|
||||
val attackerSocket = Any()
|
||||
val victimSocket = Any()
|
||||
val links = mapOf(
|
||||
"attacker-link" to IngressLinkPolicy.Link("provisional-attacker", attackerSocket),
|
||||
"victim-link" to IngressLinkPolicy.Link("provisional-victim", victimSocket)
|
||||
)
|
||||
val current = mapOf(
|
||||
"provisional-attacker" to attackerSocket,
|
||||
"provisional-victim" to victimSocket
|
||||
)
|
||||
|
||||
val resolved = IngressLinkPolicy.resolve(
|
||||
ingressLinkID = "victim-link",
|
||||
relayAddress = "provisional-victim",
|
||||
links = links,
|
||||
currentTransportForRelay = current::get
|
||||
)
|
||||
|
||||
assertSame(victimSocket, resolved?.transport)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stale replaced or mismatched ingress links cannot be observed`() {
|
||||
val completedSocket = Any()
|
||||
val replacementSocket = Any()
|
||||
val links = mapOf(
|
||||
"completed-link" to IngressLinkPolicy.Link("provisional", completedSocket)
|
||||
)
|
||||
|
||||
assertNull(
|
||||
IngressLinkPolicy.resolve(
|
||||
ingressLinkID = "missing-link",
|
||||
relayAddress = "provisional",
|
||||
links = links,
|
||||
currentTransportForRelay = { completedSocket }
|
||||
)
|
||||
)
|
||||
assertNull(
|
||||
IngressLinkPolicy.resolve(
|
||||
ingressLinkID = "completed-link",
|
||||
relayAddress = "different-provisional",
|
||||
links = links,
|
||||
currentTransportForRelay = { completedSocket }
|
||||
)
|
||||
)
|
||||
assertNull(
|
||||
IngressLinkPolicy.resolve(
|
||||
ingressLinkID = "completed-link",
|
||||
relayAddress = "provisional",
|
||||
links = links,
|
||||
currentTransportForRelay = { replacementSocket }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user