Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,13 @@ abstract class BoxInstance(
* is sufficient. MasterDnsVPN is the exception: it only starts listening after DNS
* MTU probing and session setup, which can take tens of seconds (with retries) on
* lossy or restricted links, so it gets a longer readiness window.
*
* @param strict when true (URL test), a sidecar that never binds is a hard failure with
* a clear message, instead of the live-service behavior of logging and continuing
* (the live path keeps a long-lived connection that sing-box retries; a one-shot URL
* test has no such luxury and would otherwise surface a flaky "connection refused").
*/
suspend fun awaitExternalProcessesReady() {
suspend fun awaitExternalProcessesReady(strict: Boolean = false) {
if (!::processes.isInitialized || processes.processCount == 0) return
val ports = config.externalIndex.flatMap { it.chain.keys }.distinct()
if (ports.isEmpty()) return
Expand All @@ -273,6 +278,11 @@ abstract class BoxInstance(
}
val readinessTimeoutMs = if (hasMasterDnsVpn) {
maxOf(60_000L, DataStore.connectionTestTimeout.toLong())
} else if (strict) {
// URL test: a healthy sidecar binds well under a second. Cap the readiness
// wait so a slow/unbound sidecar can't make the total perceived test time
// roughly double the configured timeout (readiness wait + the url test itself).
minOf(2_000L, maxOf(1_000L, DataStore.connectionTestTimeout.toLong()))
} else {
maxOf(1_000L, DataStore.connectionTestTimeout.toLong())
}
Expand Down Expand Up @@ -300,8 +310,10 @@ abstract class BoxInstance(
// otherwise), so a timeout there is fatal. Other sidecars (Mieru/Naïve/
// TrojanGo/Hysteria) were historically fire-and-forget: the first sing-box
// dial retries, so a slow bind shouldn't hard-fail VPN start — log and continue.
// For a URL test (strict), there is no retry window, so a listener that never
// binds is reported as a clear error instead of a flaky "connection refused".
val message = "sidecar listener not ready on port(s): ${pending.joinToString()}"
if (hasMasterDnsVpn) {
if (hasMasterDnsVpn || strict) {
throw IOException(message)
} else {
Logs.w("$message; continuing (sing-box will retry the connection)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
import io.nekohasekai.sagernet.ktx.tryResume
import io.nekohasekai.sagernet.ktx.tryResumeWithException
import kotlinx.coroutines.delay
import libcore.Libcore
import moe.matsuri.nb4a.net.LocalResolverImpl
import kotlin.coroutines.suspendCoroutine
Expand All @@ -28,8 +27,12 @@ class TestInstance(profile: ProxyEntity, val link: String, private val timeout:
init()
launch()
if (processes.processCount > 0) {
// wait for plugin start
delay(500)
// Wait until the external plugin sidecar(s) have actually bound
// their loopback SOCKS port before testing, instead of a fixed
// 500ms guess that often raced the sidecar (flaky "connection
// refused"). strict = true turns a never-bound listener into a
// clear error rather than a misleading connection failure.
awaitExternalProcessesReady(strict = true)
}
c.tryResume(Libcore.urlTest(box, link, timeout))
} catch (e: Exception) {
Expand Down
Loading