feat: ping-and-exit for server RTT, remove broken UDP ping
Some checks failed
Mirror to GitHub / mirror (push) Failing after 38s
Build Release Binaries / build-amd64 (push) Failing after 3m38s

- Ping button: pings all servers via native QUIC, saves RTT + fingerprint
  to SharedPreferences, then exits process (System.exit)
- On restart: loads saved ping results (no native .so loading needed)
- Avoids jemalloc crash: native lib only loaded once per process lifetime
- Removed broken UDP probe (QUIC servers don't respond to it)
- SettingsRepository: savePingRtt/loadPingRtt for cached results
- PingResult: added reachable field

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-07 09:31:02 +04:00
parent 00b405aa87
commit eeb85aeac2
4 changed files with 80 additions and 81 deletions

View File

@@ -1,65 +1,35 @@
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.
* Relay ping via native QUIC — requires loading the native .so.
* After ping completes, the process must be restarted (System.exit)
* because jemalloc initialization during .so load corrupts state
* on Android 16 MTE devices.
*
* Flow: ping all servers → save results → exit → app restarts → load results
*/
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,
val serverFingerprint: String = "",
)
/**
* Ping a relay server via UDP. Returns RTT in ms, or unreachable.
* Thread-safe, can be called from coroutine on Dispatchers.IO.
* Ping a relay via the native QUIC stack.
* WARNING: After calling this, the process must be restarted.
*/
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)
val json = com.wzp.engine.WzpEngine.pingRelay(address) ?: return PingResult(0, false)
val obj = org.json.JSONObject(json)
PingResult(
rttMs = obj.getInt("rtt_ms"),
reachable = true,
serverFingerprint = obj.optString("server_fingerprint", ""),
)
} catch (e: Exception) {
Log.w(TAG, "ping $address failed: ${e.message}")
PingResult(0, false)
}
}