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

@@ -0,0 +1,66 @@
package com.wzp.net
import android.util.Log
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetSocketAddress
/**
* Pure Kotlin UDP ping — no JNI, no native lib loading.
* Sends a minimal packet to the relay and measures response time.
* QUIC servers reply with Version Negotiation to unknown packets.
*/
object RelayPinger {
private const val TAG = "RelayPinger"
private const val TIMEOUT_MS = 2000
// Minimal QUIC-like Initial packet (just enough to provoke a response)
// First byte 0xC0 = long header, version 0x00000000 = version negotiation trigger
private val PROBE = byteArrayOf(
0xC0.toByte(), // long header form
0x00, 0x00, 0x00, 0x00, // version 0 → triggers Version Negotiation
0x08, // DCID length = 8
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // fake DCID
0x00, // SCID length = 0
0x00, 0x00, // token length = 0 (for Initial)
0x00, 0x04, // payload length = 4
0x00, 0x00, 0x00, 0x00, // dummy payload
)
data class PingResult(
val rttMs: Int,
val reachable: Boolean,
)
/**
* Ping a relay server via UDP. Returns RTT in ms, or unreachable.
* Thread-safe, can be called from coroutine on Dispatchers.IO.
*/
fun ping(address: String): PingResult {
return try {
val parts = address.split(":")
if (parts.size != 2) return PingResult(0, false)
val host = parts[0]
val port = parts[1].toIntOrNull() ?: return PingResult(0, false)
val socket = DatagramSocket()
socket.soTimeout = TIMEOUT_MS
val dest = InetSocketAddress(host, port)
val sendPacket = DatagramPacket(PROBE, PROBE.size, dest)
val recvBuf = ByteArray(1200)
val recvPacket = DatagramPacket(recvBuf, recvBuf.size)
val start = System.nanoTime()
socket.send(sendPacket)
socket.receive(recvPacket) // blocks until response or timeout
val rttMs = ((System.nanoTime() - start) / 1_000_000).toInt()
socket.close()
PingResult(rttMs, true)
} catch (e: Exception) {
Log.w(TAG, "ping $address failed: ${e.message}")
PingResult(0, false)
}
}
}