feat(net): split routing via hev mapped-DNS + protected direct dial
Enable fake-DNS when sites set (force IPv4); LocalSocks5Server routes proxy-vs-direct by domain, dialing direct on the underlying internet network.
This commit is contained in:
parent
e01b0c2a87
commit
fadb0870e8
3 changed files with 166 additions and 9 deletions
|
|
@ -1,13 +1,18 @@
|
||||||
package chat.vojo.proxy.core.socks
|
package chat.vojo.proxy.core.socks
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import chat.vojo.proxy.core.net.ATYP_DOMAIN
|
||||||
import chat.vojo.proxy.core.net.parseSocksAddress
|
import chat.vojo.proxy.core.net.parseSocksAddress
|
||||||
import chat.vojo.proxy.core.net.readExact
|
import chat.vojo.proxy.core.net.readExact
|
||||||
import chat.vojo.proxy.core.net.readFully
|
import chat.vojo.proxy.core.net.readFully
|
||||||
import chat.vojo.proxy.core.net.readSocksAddress
|
import chat.vojo.proxy.core.net.readSocksAddress
|
||||||
import chat.vojo.proxy.core.net.u16be
|
import chat.vojo.proxy.core.net.u16be
|
||||||
|
import chat.vojo.proxy.core.route.RouteDecision
|
||||||
|
import chat.vojo.proxy.core.route.SiteRouter
|
||||||
|
import chat.vojo.proxy.core.upstream.DirectDialer
|
||||||
import chat.vojo.proxy.core.upstream.UdpAssociation
|
import chat.vojo.proxy.core.upstream.UdpAssociation
|
||||||
import chat.vojo.proxy.core.upstream.Upstream
|
import chat.vojo.proxy.core.upstream.Upstream
|
||||||
|
import chat.vojo.proxy.core.upstream.UpstreamConn
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -58,9 +63,15 @@ private const val MAX_INFLIGHT_DNS_TCP = 8
|
||||||
*
|
*
|
||||||
* [onUpstreamError] receives the actual failure so the caller can classify it
|
* [onUpstreamError] receives the actual failure so the caller can classify it
|
||||||
* (auth vs TLS vs timeout vs wrong Shadowsocks key) instead of a generic "down".
|
* (auth vs TLS vs timeout vs wrong Shadowsocks key) instead of a generic "down".
|
||||||
|
*
|
||||||
|
* [router] decides proxy-vs-direct per destination (the "Сайты" split). When it says
|
||||||
|
* DIRECT, [directDialer] dials the real host on the underlying network instead of the
|
||||||
|
* proxy; both paths then share the same half-close relay below.
|
||||||
*/
|
*/
|
||||||
class LocalSocks5Server(
|
class LocalSocks5Server(
|
||||||
private val upstream: Upstream,
|
private val upstream: Upstream,
|
||||||
|
private val router: SiteRouter,
|
||||||
|
private val directDialer: DirectDialer,
|
||||||
private val onUpstreamError: ((Throwable) -> Unit)? = null,
|
private val onUpstreamError: ((Throwable) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
private val serverSocket = ServerSocket().apply {
|
private val serverSocket = ServerSocket().apply {
|
||||||
|
|
@ -186,11 +197,21 @@ class LocalSocks5Server(
|
||||||
cout: OutputStream,
|
cout: OutputStream,
|
||||||
dest: chat.vojo.proxy.core.net.Destination,
|
dest: chat.vojo.proxy.core.net.Destination,
|
||||||
) {
|
) {
|
||||||
|
// Domain split: only domain destinations can be matched (mapped-DNS restores the
|
||||||
|
// name into dest.host); IP-literal/IPv6 flows take the router's global default.
|
||||||
|
val decision = router.decide(dest.host, dest.atyp == ATYP_DOMAIN)
|
||||||
val conn = try {
|
val conn = try {
|
||||||
upstream.connectTcp(dest)
|
if (decision == RouteDecision.DIRECT) {
|
||||||
|
val s = directDialer.connectDirect(dest.host, dest.port)
|
||||||
|
UpstreamConn(s, s.getInputStream(), s.getOutputStream())
|
||||||
|
} else {
|
||||||
|
upstream.connectTcp(dest)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "upstream connect failed for $dest: ${e.message}")
|
Log.w(TAG, "$decision connect failed for $dest: ${e.message}")
|
||||||
onUpstreamError?.invoke(e)
|
// Only proxy failures feed the upstream-error classifier; a failed direct dial
|
||||||
|
// is a real-network problem, not a misconfigured proxy.
|
||||||
|
if (decision == RouteDecision.PROXY) onUpstreamError?.invoke(e)
|
||||||
runCatching { writeReply(cout, rep = 0x05) } // connection refused
|
runCatching { writeReply(cout, rep = 0x05) } // connection refused
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -259,6 +280,9 @@ class LocalSocks5Server(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUdpAssociate(client: Socket, cin: InputStream, cout: OutputStream) {
|
private fun handleUdpAssociate(client: Socket, cin: InputStream, cout: OutputStream) {
|
||||||
|
// NOTE: UDP is not domain-routed — unlike handleConnect it never consults `router`,
|
||||||
|
// so UDP (incl. QUIC/HTTP-3) always goes through the proxy regardless of routing mode.
|
||||||
|
// Direct UDP would need a protected per-destination relay; see docs/architecture.md.
|
||||||
val relay = DatagramSocket(InetSocketAddress("127.0.0.1", 0))
|
val relay = DatagramSocket(InetSocketAddress("127.0.0.1", 0))
|
||||||
track(relay)
|
track(relay)
|
||||||
val session = UdpSession(relay)
|
val session = UdpSession(relay)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package chat.vojo.proxy.core.upstream
|
||||||
|
|
||||||
|
import java.net.Socket
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a VPN-protected TCP socket straight to a destination, bypassing the proxy.
|
||||||
|
*
|
||||||
|
* The "direct" half of domain split-routing: for sites the [SiteRouter][chat.vojo.proxy.core.route.SiteRouter]
|
||||||
|
* sends direct, the local SOCKS5 bridge dials the real host through this instead of the
|
||||||
|
* upstream. Resolution and egress MUST happen on the underlying (non-VPN) network, or a
|
||||||
|
* direct flow loops back into the tun / fake DNS (see the protect() invariant in
|
||||||
|
* docs/conventions.md). Implemented by ProxyVpnService.
|
||||||
|
*/
|
||||||
|
fun interface DirectDialer {
|
||||||
|
fun connectDirect(host: String, port: Int): Socket
|
||||||
|
}
|
||||||
|
|
@ -21,14 +21,19 @@ import chat.vojo.proxy.R
|
||||||
import chat.vojo.proxy.core.AppSettings
|
import chat.vojo.proxy.core.AppSettings
|
||||||
import chat.vojo.proxy.core.ProxyConfig
|
import chat.vojo.proxy.core.ProxyConfig
|
||||||
import chat.vojo.proxy.core.ProxyType
|
import chat.vojo.proxy.core.ProxyType
|
||||||
|
import chat.vojo.proxy.core.RoutingMode
|
||||||
import chat.vojo.proxy.core.Tun2Socks
|
import chat.vojo.proxy.core.Tun2Socks
|
||||||
import chat.vojo.proxy.core.net.Destination
|
import chat.vojo.proxy.core.net.Destination
|
||||||
import chat.vojo.proxy.core.net.readExact
|
import chat.vojo.proxy.core.net.readExact
|
||||||
import chat.vojo.proxy.core.formatBytes
|
import chat.vojo.proxy.core.formatBytes
|
||||||
import chat.vojo.proxy.core.formatUptime
|
import chat.vojo.proxy.core.formatUptime
|
||||||
|
import chat.vojo.proxy.core.route.SiteRouter
|
||||||
import chat.vojo.proxy.core.socks.LocalSocks5Server
|
import chat.vojo.proxy.core.socks.LocalSocks5Server
|
||||||
|
import chat.vojo.proxy.core.upstream.CONNECT_TIMEOUT_MS
|
||||||
|
import chat.vojo.proxy.core.upstream.DirectDialer
|
||||||
import chat.vojo.proxy.core.upstream.SocketProtector
|
import chat.vojo.proxy.core.upstream.SocketProtector
|
||||||
import chat.vojo.proxy.core.upstream.Upstream
|
import chat.vojo.proxy.core.upstream.Upstream
|
||||||
|
import chat.vojo.proxy.data.AssetGeositeSource
|
||||||
import chat.vojo.proxy.data.ConfigStore
|
import chat.vojo.proxy.data.ConfigStore
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -40,10 +45,13 @@ import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.net.DatagramSocket
|
import java.net.DatagramSocket
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.InetSocketAddress
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
|
import java.net.UnknownHostException
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
class ProxyVpnService : VpnService(), SocketProtector {
|
class ProxyVpnService : VpnService(), SocketProtector, DirectDialer {
|
||||||
|
|
||||||
// Every lifecycle transition (start/stop/restart/engine-exit) runs on this
|
// Every lifecycle transition (start/stop/restart/engine-exit) runs on this
|
||||||
// single thread: no start/stop races, no blocking joins on the main thread.
|
// single thread: no start/stop races, no blocking joins on the main thread.
|
||||||
|
|
@ -146,14 +154,28 @@ class ProxyVpnService : VpnService(), SocketProtector {
|
||||||
updateNotification(getString(R.string.notif_connecting))
|
updateNotification(getString(R.string.notif_connecting))
|
||||||
|
|
||||||
val upstream = Upstream.create(config, this)
|
val upstream = Upstream.create(config, this)
|
||||||
val socks = LocalSocks5Server(upstream, ::onUpstreamUnreachable).also { it.start() }
|
val router = SiteRouter.build(
|
||||||
|
state.settings.routingMode,
|
||||||
|
state.settings.routedSites,
|
||||||
|
AssetGeositeSource(this),
|
||||||
|
)
|
||||||
|
val socks = LocalSocks5Server(upstream, router, this, ::onUpstreamUnreachable).also { it.start() }
|
||||||
server = socks
|
server = socks
|
||||||
|
|
||||||
val pfd = establish(config, state.settings, socks.port)
|
val pfd = establish(config, state.settings, socks.port)
|
||||||
vpnInterface = pfd
|
vpnInterface = pfd
|
||||||
VpnState.event("Интерфейс поднят", "$TUN_IPV4 · mtu ${state.settings.mtu}")
|
VpnState.event("Интерфейс поднят", "$TUN_IPV4 · mtu ${state.settings.mtu}")
|
||||||
VpnState.event("SOCKS5-мост", "127.0.0.1:${socks.port}")
|
VpnState.event("SOCKS5-мост", "127.0.0.1:${socks.port}")
|
||||||
VpnState.event("DNS через туннель", dnsServerFor(state.settings))
|
if (domainRoutingActive(state.settings)) {
|
||||||
|
val modeLabel = when (state.settings.routingMode) {
|
||||||
|
RoutingMode.PROXY_ALL -> "через прокси, кроме списка"
|
||||||
|
RoutingMode.DIRECT_ALL -> "напрямую, кроме списка"
|
||||||
|
}
|
||||||
|
VpnState.event("Маршрутизация по доменам", "${state.settings.routedSites.size} правил · $modeLabel")
|
||||||
|
VpnState.event("Перехват DNS (fake-IP)", "$MAPDNS_ADDR · только IPv4")
|
||||||
|
} else {
|
||||||
|
VpnState.event("DNS через туннель", dnsServerFor(state.settings))
|
||||||
|
}
|
||||||
|
|
||||||
val hevConfig = buildEngineConfig(socks, state.settings)
|
val hevConfig = buildEngineConfig(socks, state.settings)
|
||||||
val fd = pfd.fd
|
val fd = pfd.fd
|
||||||
|
|
@ -330,12 +352,16 @@ class ProxyVpnService : VpnService(), SocketProtector {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun establish(config: ProxyConfig, settings: AppSettings, socksPort: Int): ParcelFileDescriptor {
|
private fun establish(config: ProxyConfig, settings: AppSettings, socksPort: Int): ParcelFileDescriptor {
|
||||||
|
// Domain routing needs the OS resolver to hit hev's fake-DNS so domains survive
|
||||||
|
// into the SOCKS5 CONNECT. Point DNS at the synthetic resolver; otherwise keep the
|
||||||
|
// tunnelled real resolver.
|
||||||
|
val sites = domainRoutingActive(settings)
|
||||||
val builder = Builder()
|
val builder = Builder()
|
||||||
.setSession("vojo · ${config.name}")
|
.setSession("vojo · ${config.name}")
|
||||||
.setMtu(settings.mtu)
|
.setMtu(settings.mtu)
|
||||||
.addAddress(TUN_IPV4, 32)
|
.addAddress(TUN_IPV4, 32)
|
||||||
.addRoute("0.0.0.0", 0)
|
.addRoute("0.0.0.0", 0)
|
||||||
.addDnsServer(dnsServerFor(settings))
|
.addDnsServer(if (sites) MAPDNS_ADDR else dnsServerFor(settings))
|
||||||
.setBlocking(false)
|
.setBlocking(false)
|
||||||
|
|
||||||
// IPv6 is only pulled into the tun when the user enables it AND the engine is
|
// IPv6 is only pulled into the tun when the user enables it AND the engine is
|
||||||
|
|
@ -345,7 +371,11 @@ class ProxyVpnService : VpnService(), SocketProtector {
|
||||||
// rejects it (rep=5), which breaks Happy-Eyeballs apps that then prefer v6.
|
// rejects it (rep=5), which breaks Happy-Eyeballs apps that then prefer v6.
|
||||||
// With v6 absent here, Android adds its own `::/0 unreachable` route, so native
|
// With v6 absent here, Android adds its own `::/0 unreachable` route, so native
|
||||||
// v6 is blackholed (no leak) and apps fall back to v4 through the proxy.
|
// v6 is blackholed (no leak) and apps fall back to v4 through the proxy.
|
||||||
if (settings.ipv6) {
|
//
|
||||||
|
// While domain routing is active we force IPv4-only regardless of settings.ipv6:
|
||||||
|
// hev mapped-DNS is IPv4-only, so a dual-stack app reaching a routed site over v6
|
||||||
|
// would bypass the split. Suppressing v6 funnels everything through the fake-DNS.
|
||||||
|
if (settings.ipv6 && !sites) {
|
||||||
builder.addAddress(TUN_IPV6, 128).addRoute("::", 0)
|
builder.addAddress(TUN_IPV6, 128).addRoute("::", 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -386,11 +416,20 @@ class ProxyVpnService : VpnService(), SocketProtector {
|
||||||
return if (!settings.ipv6 && ':' in dns) "1.1.1.1" else dns
|
return if (!settings.ipv6 && ':' in dns) "1.1.1.1" else dns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain ("Сайты") routing is on whenever there are site entries — that's when we need
|
||||||
|
* hev to restore domains via fake-DNS so the bridge can split proxy-vs-direct. Empty
|
||||||
|
* list = legacy behaviour (no mapped-DNS, no forced-IPv4), even under DIRECT_ALL where
|
||||||
|
* every IP-literal flow simply takes the direct default.
|
||||||
|
*/
|
||||||
|
private fun domainRoutingActive(settings: AppSettings): Boolean = settings.routedSites.isNotEmpty()
|
||||||
|
|
||||||
private fun buildEngineConfig(socks: LocalSocks5Server, settings: AppSettings): String = buildString {
|
private fun buildEngineConfig(socks: LocalSocks5Server, settings: AppSettings): String = buildString {
|
||||||
|
val sites = domainRoutingActive(settings)
|
||||||
append("tunnel:\n")
|
append("tunnel:\n")
|
||||||
append(" mtu: ${settings.mtu}\n")
|
append(" mtu: ${settings.mtu}\n")
|
||||||
append(" ipv4: $TUN_IPV4\n")
|
append(" ipv4: $TUN_IPV4\n")
|
||||||
if (settings.ipv6) append(" ipv6: '$TUN_IPV6'\n")
|
if (settings.ipv6 && !sites) append(" ipv6: '$TUN_IPV6'\n")
|
||||||
append("socks5:\n")
|
append("socks5:\n")
|
||||||
append(" address: 127.0.0.1\n")
|
append(" address: 127.0.0.1\n")
|
||||||
append(" port: ${socks.port}\n")
|
append(" port: ${socks.port}\n")
|
||||||
|
|
@ -402,6 +441,18 @@ class ProxyVpnService : VpnService(), SocketProtector {
|
||||||
append("misc:\n")
|
append("misc:\n")
|
||||||
append(" connect-timeout: 8000\n")
|
append(" connect-timeout: 8000\n")
|
||||||
append(" log-level: warn\n")
|
append(" log-level: warn\n")
|
||||||
|
// Fake-IP / mapped-DNS: hev intercepts DNS to MAPDNS_ADDR:MAPDNS_PORT, answers with
|
||||||
|
// a synthetic IP from FAKE_NET/FAKE_MASK, and reverse-maps it to the original domain
|
||||||
|
// on the SOCKS5 CONNECT — so LocalSocks5Server can route by domain. Only emitted when
|
||||||
|
// sites are configured; absent = the engine leaves DNS untouched (cache-size 0 = off).
|
||||||
|
if (sites) {
|
||||||
|
append("mapdns:\n")
|
||||||
|
append(" address: $MAPDNS_ADDR\n")
|
||||||
|
append(" port: $MAPDNS_PORT\n")
|
||||||
|
append(" network: $FAKE_NET\n")
|
||||||
|
append(" netmask: $FAKE_MASK\n")
|
||||||
|
append(" cache-size: $MAPDNS_CACHE_SIZE\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startStatsLoop() {
|
private fun startStatsLoop() {
|
||||||
|
|
@ -474,6 +525,56 @@ class ProxyVpnService : VpnService(), SocketProtector {
|
||||||
override fun protect(socket: Socket): Boolean = super<VpnService>.protect(socket)
|
override fun protect(socket: Socket): Boolean = super<VpnService>.protect(socket)
|
||||||
override fun protect(socket: DatagramSocket): Boolean = super<VpnService>.protect(socket)
|
override fun protect(socket: DatagramSocket): Boolean = super<VpnService>.protect(socket)
|
||||||
|
|
||||||
|
// --- DirectDialer ---
|
||||||
|
/**
|
||||||
|
* Dials [host]:[port] directly, bypassing the proxy, for sites routed DIRECT. Both the
|
||||||
|
* DNS resolution and the socket egress are pinned to the underlying (non-VPN) network so
|
||||||
|
* a direct flow can never loop back into the tun or the fake DNS. protect() is applied as
|
||||||
|
* belt-and-suspenders; its false return is benign (see the SocketProtector contract).
|
||||||
|
*/
|
||||||
|
override fun connectDirect(host: String, port: Int): Socket {
|
||||||
|
// The underlying (non-VPN) network is REQUIRED: resolving/dialing on the process
|
||||||
|
// default would hit the VPN's fake-DNS (198.18.0.2), which our excluded sockets can't
|
||||||
|
// even reach (it only lives inside the tun) — so a missing network must fail loudly,
|
||||||
|
// never fall back to the default resolver.
|
||||||
|
val net = underlyingNetwork() ?: throw UnknownHostException("no underlying network for direct dial")
|
||||||
|
// Resolve on that network's real resolver and bind the socket to it, so the direct
|
||||||
|
// flow egresses the physical interface and never loops back into the tun.
|
||||||
|
val target = net.getAllByName(host).firstOrNull() ?: throw UnknownHostException(host)
|
||||||
|
val socket = Socket()
|
||||||
|
protect(socket)
|
||||||
|
runCatching { net.bindSocket(socket) }
|
||||||
|
socket.tcpNoDelay = true
|
||||||
|
socket.connect(InetSocketAddress(target, port), CONNECT_TIMEOUT_MS)
|
||||||
|
return socket
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The real (non-VPN) transport to dial direct flows over. While we're connected,
|
||||||
|
* cm.activeNetwork is the VPN itself, but the default-network callback reports the network
|
||||||
|
* BENEATH our VPN, so [lastNetwork] is the right egress — validated here as still non-VPN,
|
||||||
|
* with an allNetworks scan as a fallback.
|
||||||
|
*/
|
||||||
|
private fun underlyingNetwork(): Network? {
|
||||||
|
val cm = getSystemService(ConnectivityManager::class.java) ?: return null
|
||||||
|
// Must carry general internet — NOT just any non-VPN transport. A device may expose an
|
||||||
|
// IMS/MMTEL cellular network (NOT_VPN, TRANSPORT_CELLULAR, but NO INTERNET capability)
|
||||||
|
// that can't resolve hostnames; picking it broke every direct dial (device-confirmed).
|
||||||
|
fun isUsable(n: Network): Boolean {
|
||||||
|
val caps = cm.getNetworkCapabilities(n) ?: return false
|
||||||
|
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) &&
|
||||||
|
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||||
|
(caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
|
||||||
|
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
|
||||||
|
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET))
|
||||||
|
}
|
||||||
|
fun validated(n: Network) =
|
||||||
|
cm.getNetworkCapabilities(n)?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) == true
|
||||||
|
lastNetwork?.let { if (isUsable(it) && validated(it)) return it }
|
||||||
|
val usable = cm.allNetworks.filter { isUsable(it) }
|
||||||
|
return usable.firstOrNull { validated(it) } ?: usable.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
private fun fail(message: String) { // lifecycle thread
|
private fun fail(message: String) { // lifecycle thread
|
||||||
VpnState.event("Ошибка", message, VpnState.TunnelEvent.Tone.ERROR)
|
VpnState.event("Ошибка", message, VpnState.TunnelEvent.Tone.ERROR)
|
||||||
shutdown(errorMessage = message)
|
shutdown(errorMessage = message)
|
||||||
|
|
@ -685,6 +786,22 @@ class ProxyVpnService : VpnService(), SocketProtector {
|
||||||
const val TUN_IPV4 = "198.18.0.1"
|
const val TUN_IPV4 = "198.18.0.1"
|
||||||
private const val TUN_IPV6 = "fc00::1"
|
private const val TUN_IPV6 = "fc00::1"
|
||||||
|
|
||||||
|
// hev mapped-DNS (fake-IP) parameters, used only while domain routing is active.
|
||||||
|
// Synthetic resolver the OS DNS is pointed at; hev intercepts UDP to this addr:port.
|
||||||
|
private const val MAPDNS_ADDR = "198.18.0.2"
|
||||||
|
private const val MAPDNS_PORT = 53
|
||||||
|
// Fake-IP pool. 198.19.0.0/16 sits in the RFC 2544 benchmarking range (non-routable),
|
||||||
|
// clears the 198.18.0.1/32 tun address and the synthetic resolver, and is NOT in the
|
||||||
|
// CGNAT/private set isUnroutableRelay() rewrites — these fake IPs become domains before
|
||||||
|
// ever reaching the upstream, so that relay path is never involved. /16 = 65534 slots.
|
||||||
|
private const val FAKE_NET = "198.19.0.0"
|
||||||
|
private const val FAKE_MASK = "255.255.0.0"
|
||||||
|
// Size the LRU to the full /16 pool (max allowed is ~mask = 65535). A smaller cache would
|
||||||
|
// evict still-referenced fake IPs for broad categories (geolocation-!cn alone is ~24.7k
|
||||||
|
// domains), and an evicted IP reverse-maps to nothing → the raw 198.19.x.x literal leaks
|
||||||
|
// to the bridge and dead-ends. Filling the whole pool makes eviction effectively impossible.
|
||||||
|
private const val MAPDNS_CACHE_SIZE = 65534
|
||||||
|
|
||||||
fun start(context: Context) {
|
fun start(context: Context) {
|
||||||
val intent = Intent(context, ProxyVpnService::class.java).setAction(ACTION_START)
|
val intent = Intent(context, ProxyVpnService::class.java).setAction(ACTION_START)
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue