From e4cb8a89b7174f5cb5f15e20b4b256fefd23b576 Mon Sep 17 00:00:00 2001 From: hawkff <109485367+hawkff@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:26:10 -0400 Subject: [PATCH 1/3] fix(urltest): await external plugin loopback readiness instead of fixed 500ms URL-testing external-plugin profiles (naive/mieru/masterdnsvpn) raced the sidecar: sing-box dialed the plugin's loopback SOCKS port before it had bound, surfacing a flaky 'connection refused'. The live VPN path already polls readiness via awaitExternalProcessesReady(); the URL test still used a fixed delay(500). Reuse awaitExternalProcessesReady(strict = true) in TestInstance: poll each plugin loopback until it accepts (bounded by the connection-test timeout; MasterDnsVPN gets its longer window), and on timeout report a clear 'sidecar listener not ready' error instead of a misleading connection failure. Verified on-device: naive/mieru now show real latency (189/194ms) where they previously flaked; a genuinely slow/unbound sidecar yields the clear not-ready error. --- .../io/nekohasekai/sagernet/bg/proto/BoxInstance.kt | 11 +++++++++-- .../io/nekohasekai/sagernet/bg/proto/TestInstance.kt | 9 ++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt index 4d04eb42b..aef9a759c 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt @@ -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 @@ -300,8 +305,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)") diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt index bc6907137..79938d12b 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt @@ -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 @@ -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) { From 195fff10838b8ca43faf72ea72abdff1bae62cd7 Mon Sep 17 00:00:00 2001 From: hawkff <109485367+hawkff@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:38:45 -0400 Subject: [PATCH 2/3] review: cap strict (URL-test) readiness window so total stays bounded Per Greptile: in strict mode the non-MasterDnsVPN readiness window reused the full connectionTestTimeout, so a slow-binding sidecar could roughly double the perceived URL-test duration (readiness wait + the test itself). Cap the strict readiness wait at min(2s, timeout); a healthy sidecar binds well under a second. MasterDnsVPN keeps its required 60s window. --- .../java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt index aef9a759c..ceec0eb65 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt @@ -278,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()) } From 15a57d45c257dc59f03b59d945f49337f5fca536 Mon Sep 17 00:00:00 2001 From: hawkff <109485367+hawkff@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:44:47 -0400 Subject: [PATCH 3/3] ci: trigger build on latest commit