feat: pure Kotlin UDP ping — periodic every 5s, no JNI crash
Some checks failed
Mirror to GitHub / mirror (push) Failing after 38s
Build Release Binaries / build-amd64 (push) Failing after 3m35s

Replace WzpEngine.pingRelay() (JNI, loads native .so, crashes jemalloc
on Android 16 MTE) with pure Kotlin DatagramSocket UDP probe.

- RelayPinger: sends QUIC Version Negotiation trigger packet, measures
  RTT from response. No native lib, no JNI, zero crash risk.
- Periodic: pings all servers every 5 seconds via coroutine
- Server fingerprint: filled lazily on first real QUIC connection
  (TOFU still works, just delayed)
- Lock status: OFFLINE when ping fails, NEW until first connection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-07 08:57:27 +04:00
parent 97bcc79f9b
commit d09e21965e
3 changed files with 107 additions and 31 deletions

View File

@@ -203,38 +203,46 @@ class CallViewModel : ViewModel(), WzpCallback {
settings?.saveSelectedServer(_selectedServer.value)
}
/** Ping all servers in background, update results. */
fun pingAllServers() {
private var pingJob: Job? = null
/** Start periodic ping every 5 seconds. Safe to call multiple times. */
fun startPeriodicPing() {
if (pingJob?.isActive == true) return
pingJob = viewModelScope.launch {
while (isActive) {
pingAllServersOnce()
delay(5000)
}
}
}
/** Stop periodic ping. */
fun stopPeriodicPing() {
pingJob?.cancel()
pingJob = null
}
/** Ping all servers once (pure Kotlin UDP, no JNI). */
fun pingAllServersOnce() {
viewModelScope.launch {
val results = mutableMapOf<String, PingResult>()
val known = mutableMapOf<String, String>()
_servers.value.forEach { server ->
val pr = withContext(Dispatchers.IO) {
try {
val json = WzpEngine.pingRelay(server.address) ?: return@withContext null
val obj = JSONObject(json)
PingResult(
rttMs = obj.getInt("rtt_ms"),
serverFingerprint = obj.optString("server_fingerprint", ""),
)
} catch (e: Exception) {
Log.w(TAG, "ping ${server.address} failed: ${e.message}")
null
}
}
if (pr != null) {
results[server.address] = pr
// TOFU: save fingerprint on first contact
if (pr.serverFingerprint.isNotEmpty()) {
val saved = settings?.loadServerFingerprint(server.address)
if (saved == null) {
settings?.saveServerFingerprint(server.address, pr.serverFingerprint)
}
known[server.address] = saved ?: pr.serverFingerprint
}
val udpResult = withContext(Dispatchers.IO) {
com.wzp.net.RelayPinger.ping(server.address)
}
results[server.address] = PingResult(
rttMs = udpResult.rttMs,
serverFingerprint = "", // filled lazily on first real connection
)
}
_pingResults.value = results
// Load saved TOFU fingerprints
val known = mutableMapOf<String, String>()
_servers.value.forEach { server ->
settings?.loadServerFingerprint(server.address)?.let {
known[server.address] = it
}
}
_knownFingerprints.value = known
}
}
@@ -242,9 +250,9 @@ class CallViewModel : ViewModel(), WzpCallback {
/** Get lock status for a server. */
fun lockStatus(address: String): LockStatus {
val pr = _pingResults.value[address] ?: return LockStatus.UNKNOWN
val known = _knownFingerprints.value[address]
if (pr.serverFingerprint.isEmpty()) return LockStatus.NEW
if (known == null) return LockStatus.NEW
if (pr.rttMs <= 0 && pr.serverFingerprint.isEmpty()) return LockStatus.OFFLINE
val known = _knownFingerprints.value[address] ?: return LockStatus.NEW
if (pr.serverFingerprint.isEmpty()) return LockStatus.NEW // no fingerprint yet
return if (pr.serverFingerprint == known) LockStatus.VERIFIED else LockStatus.CHANGED
}

View File

@@ -90,8 +90,10 @@ fun InCallScreen(
var showManageRelays by remember { mutableStateOf(false) }
// Don't auto-ping — loading the native .so triggers jemalloc init
// which crashes on Android 16 MTE. Let user click "Ping All" manually.
// Pure Kotlin UDP ping — no native .so loading, safe on Android 16 MTE
LaunchedEffect(Unit) {
viewModel.startPeriodicPing()
}
Surface(
modifier = Modifier.fillMaxSize(),