diff --git a/app/src/main/java/chat/vojo/proxy/core/route/SiteRouter.kt b/app/src/main/java/chat/vojo/proxy/core/route/SiteRouter.kt index c745e45..8a08b8f 100644 --- a/app/src/main/java/chat/vojo/proxy/core/route/SiteRouter.kt +++ b/app/src/main/java/chat/vojo/proxy/core/route/SiteRouter.kt @@ -101,6 +101,11 @@ enum class RouteDecision { PROXY, DIRECT } * - [RoutingMode.PROXY_ALL]: everything proxied; matched sites go DIRECT. * - [RoutingMode.DIRECT_ALL]: everything direct; matched sites go through the PROXY. * + * With **no rules at all** the split is inert and everything is proxied (a full tunnel) — + * this deliberately overrides DIRECT_ALL, whose literal "everything direct" would otherwise + * leave a connected proxy carrying nothing while the user still sees a green "proxy confirmed". + * The split only takes effect once there is at least one site to match against. + * * Only domain destinations can match — IP-literal / IPv6 / non-DNS flows have no domain * to test, so they always take the global default (the documented coverage gap). */ @@ -109,6 +114,8 @@ class SiteRouter( private val matcher: SiteMatcher, ) { fun decide(host: String, isDomain: Boolean): RouteDecision { + // No rules ⇒ full tunnel, never the degenerate all-direct of an empty DIRECT_ALL. + if (matcher.isEmpty) return RouteDecision.PROXY val matched = isDomain && matcher.matches(host) return when (mode) { RoutingMode.PROXY_ALL -> if (matched) RouteDecision.DIRECT else RouteDecision.PROXY diff --git a/app/src/test/java/chat/vojo/proxy/core/route/SiteRoutingTest.kt b/app/src/test/java/chat/vojo/proxy/core/route/SiteRoutingTest.kt index 0a8275d..7041771 100644 --- a/app/src/test/java/chat/vojo/proxy/core/route/SiteRoutingTest.kt +++ b/app/src/test/java/chat/vojo/proxy/core/route/SiteRoutingTest.kt @@ -107,6 +107,18 @@ class SiteRoutingTest { assertEquals(RouteDecision.DIRECT, r.decide("93.184.216.34", isDomain = false)) } + @Test fun directAll_emptyList_fallsBackToFullTunnel() { + // The footgun guard: DIRECT_ALL with no rules must NOT send everything direct (a + // connected proxy carrying nothing). With no rules the split is inert = full tunnel. + val r = SiteRouter(RoutingMode.DIRECT_ALL, SiteMatcher.EMPTY) + assertFalse(r.hasRules) + assertEquals(RouteDecision.PROXY, r.decide("anything.com", isDomain = true)) + assertEquals(RouteDecision.PROXY, r.decide("93.184.216.34", isDomain = false)) + // Same via build() with no entries. + val b = SiteRouter.build(RoutingMode.DIRECT_ALL, emptySet(), GeositeSource.EMPTY) + assertEquals(RouteDecision.PROXY, b.decide("anything.com", isDomain = true)) + } + // --- SiteRouter.build (entries -> rules) --- @Test fun build_mixesPlainAndGeositeEntries() {