Skip to content

Commit 7561ec7

Browse files
committed
refactor: unify local proxy handling across run modes
1 parent c2a4e53 commit 7561ec7

38 files changed

Lines changed: 346 additions & 257 deletions

app/src/main/kotlin/app/AppState.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ data class AppState(
9292

9393
val transparentProxyPort: String = DefaultTproxyPort.toString(),
9494
val enableRootBootScript: Boolean = false,
95-
val enableSocks5Proxy: Boolean = false,
9695
val socks5ProxyPort: String = DefaultTun2SocksProxyPort.toString(),
9796
val enableHttpProxy: Boolean = false,
9897
val httpProxyPort: String = DefaultRootHttpProxyPort.toString(),

app/src/main/kotlin/app/effects/RootBootScriptSynchronizer.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import app.AppState
99
import app.modes.RunModeTproxy
1010
import app.modes.RunModeTun2Socks
1111
import data.AndroidAppStateStore
12+
import engine.proxy.withResolvedDynamicLocalProxyPort
1213
import features.logs.AndroidAppLogger
1314
import features.proxy.server.model.ProxyServer
1415
import features.routing.model.RouteRule
@@ -29,7 +30,13 @@ internal fun RootBootScriptSynchronizer(
2930
.distinctUntilChanged { previous, next -> previous.signature == next.signature }
3031
.conflate()
3132
.collect { refresh ->
32-
val state = refresh.appState
33+
val state = refresh.appState.withResolvedDynamicLocalProxyPort()
34+
if (state != refresh.appState) {
35+
stateStore.update { currentState ->
36+
if (currentState == refresh.appState) state else currentState
37+
}
38+
return@collect
39+
}
3340
if (!state.enableRootBootScript || (state.runMode != RunModeTproxy && state.runMode != RunModeTun2Socks)) {
3441
return@collect
3542
}
@@ -87,8 +94,12 @@ private data class RootBootScriptSignature(
8794
val directDnsDomains: List<String>,
8895
val enableDirectDnsForProxyServerDomains: Boolean,
8996
val dnsHosts: List<String>,
97+
val localProxyPort: String,
98+
val enableDynamicLocalProxyPort: Boolean,
99+
val localProxyListenAllInterfaces: Boolean,
100+
val localProxyUsername: String,
101+
val localProxyPassword: String,
90102
val transparentProxyPort: String,
91-
val enableSocks5Proxy: Boolean,
92103
val socks5ProxyPort: String,
93104
val enableHttpProxy: Boolean,
94105
val httpProxyPort: String,
@@ -143,8 +154,12 @@ private fun AppState.toRootBootScriptRefresh(): RootBootScriptRefresh {
143154
directDnsDomains = directDnsDomains,
144155
enableDirectDnsForProxyServerDomains = enableDirectDnsForProxyServerDomains,
145156
dnsHosts = dnsHosts,
157+
localProxyPort = localProxyPort,
158+
enableDynamicLocalProxyPort = enableDynamicLocalProxyPort,
159+
localProxyListenAllInterfaces = localProxyListenAllInterfaces,
160+
localProxyUsername = localProxyUsername,
161+
localProxyPassword = localProxyPassword,
146162
transparentProxyPort = transparentProxyPort,
147-
enableSocks5Proxy = enableSocks5Proxy,
148163
socks5ProxyPort = socks5ProxyPort,
149164
enableHttpProxy = enableHttpProxy,
150165
httpProxyPort = httpProxyPort,

app/src/main/kotlin/data/AppSettingsPreferences.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ internal class AppSettingsPreferences(
133133
KeyEnableRootBootScript,
134134
defaults.enableRootBootScript,
135135
),
136-
enableSocks5Proxy = preferences.getBoolean(KeyEnableSocks5Proxy, defaults.enableSocks5Proxy),
137136
socks5ProxyPort = preferences.getString(
138137
KeySocks5ProxyPort,
139138
defaults.socks5ProxyPort,
@@ -203,7 +202,6 @@ internal class AppSettingsPreferences(
203202
.putStringList(KeyDnsHosts, state.dnsHosts)
204203
.putString(KeyTransparentProxyPort, state.transparentProxyPort)
205204
.putBoolean(KeyEnableRootBootScript, state.enableRootBootScript)
206-
.putBoolean(KeyEnableSocks5Proxy, state.enableSocks5Proxy)
207205
.putString(KeySocks5ProxyPort, state.socks5ProxyPort)
208206
.putBoolean(KeyEnableHttpProxy, state.enableHttpProxy)
209207
.putString(KeyHttpProxyPort, state.httpProxyPort)
@@ -291,7 +289,6 @@ private const val KeyEnableDirectDnsForProxyServerDomains = "enable_direct_dns_f
291289
private const val KeyDnsHosts = "dns_hosts"
292290
private const val KeyTransparentProxyPort = "transparent_proxy_port"
293291
private const val KeyEnableRootBootScript = "enable_root_boot_script"
294-
private const val KeyEnableSocks5Proxy = "enable_socks5_proxy"
295292
private const val KeySocks5ProxyPort = "socks5_proxy_port"
296293
private const val KeyEnableHttpProxy = "enable_http_proxy"
297294
private const val KeyHttpProxyPort = "http_proxy_port"

app/src/main/kotlin/engine/proxy/AndroidProxyEngine.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,18 @@ class AndroidProxyEngine(
7474
}
7575

7676
private suspend fun startUnlocked(request: ProxyEngineStartRequest): ProxyEngineStatus = withContext(Dispatchers.Default) {
77-
val nextEngine = when (request.appState.runMode) {
77+
val resolvedRequest = request.copy(appState = request.appState.withResolvedDynamicLocalProxyPort())
78+
val nextEngine = when (resolvedRequest.appState.runMode) {
7879
RunModeTproxy -> tproxyEngine
7980
RunModeTun2Socks -> tun2SocksEngine
8081
else -> vpnXrayEngine
8182
}
82-
val currentEngine = activeEngine ?: findEngineToStop(request.appState.runMode)
83+
val currentEngine = activeEngine ?: findEngineToStop(resolvedRequest.appState.runMode)
8384
if (currentEngine != null && currentEngine !== nextEngine) {
8485
currentEngine.stop()
8586
}
8687
activeEngine = nextEngine
87-
nextEngine.start(request)
88+
nextEngine.start(resolvedRequest).copy(appState = resolvedRequest.appState)
8889
}
8990

9091
private suspend fun stopUnlocked(preferredRunMode: Int? = null): ProxyEngineStatus = withContext(Dispatchers.Default) {
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// Copyright 2026, AsteriskNG contributors
2+
// SPDX-License-Identifier: GPL-3.0
3+
4+
package engine.proxy
5+
6+
import app.AppState
7+
import app.effectiveFakeDnsEnabled
8+
import app.modes.RunModeTun2Socks
9+
import app.modes.RunModeTproxy
10+
import engine.network.NetworkDefaults
11+
import engine.network.toPortOrNull
12+
import engine.tproxy.DefaultTproxyPort
13+
import engine.tun2socks.DefaultTun2SocksProxyPort
14+
import engine.vpn.VpnDefaults
15+
import engine.xray.XrayProtocols
16+
import engine.xray.toJsonStringArray
17+
import engine.xray.xraySniffingDestOverrides
18+
import java.net.InetAddress
19+
import java.net.ServerSocket
20+
import java.util.concurrent.atomic.AtomicReference
21+
import kotlinx.serialization.json.JsonObject
22+
import kotlinx.serialization.json.buildJsonArray
23+
import kotlinx.serialization.json.buildJsonObject
24+
import kotlinx.serialization.json.put
25+
26+
internal const val LocalProxyLoopbackAddress = NetworkDefaults.IPV4_LOOPBACK_ADDRESS
27+
private const val LocalProxyAllInterfacesAddress = NetworkDefaults.IPV4_ANY_ADDRESS
28+
29+
internal data class LocalProxyOptions(
30+
val listenAddress: String,
31+
val port: Int,
32+
val username: String,
33+
val password: String,
34+
)
35+
36+
internal object LocalProxyRuntime {
37+
private val currentOptions = AtomicReference<LocalProxyOptions?>()
38+
39+
fun update(options: LocalProxyOptions) {
40+
currentOptions.set(options)
41+
}
42+
43+
fun clear() {
44+
currentOptions.set(null)
45+
}
46+
47+
fun current(): LocalProxyOptions? {
48+
return currentOptions.get()
49+
}
50+
}
51+
52+
internal fun AppState.withResolvedDynamicLocalProxyPort(): AppState {
53+
if (!enableDynamicLocalProxyPort) return this
54+
55+
val configuredPort = localProxyPort.toPortOrNull()
56+
val listenAddress = localProxyListenAddress()
57+
val excludedPorts = localProxyExcludedPorts()
58+
val currentOptions = LocalProxyRuntime.current()
59+
val canKeepConfiguredPort = configuredPort != null &&
60+
configuredPort !in excludedPorts &&
61+
(
62+
isPortAvailable(listenAddress, configuredPort) ||
63+
currentOptions?.matches(listenAddress, configuredPort) == true
64+
)
65+
val resolvedPort = when {
66+
canKeepConfiguredPort -> configuredPort
67+
else -> availablePort(listenAddress, excludedPorts) ?: configuredPort ?: VpnDefaults.LOCAL_PROXY_PORT
68+
}
69+
val resolvedPortText = resolvedPort.toString()
70+
return if (localProxyPort == resolvedPortText) this else copy(localProxyPort = resolvedPortText)
71+
}
72+
73+
internal fun AppState.toLocalProxyOptions(): LocalProxyOptions {
74+
return LocalProxyOptions(
75+
listenAddress = localProxyListenAddress(),
76+
port = localProxyPort.toPortOrNull() ?: VpnDefaults.LOCAL_PROXY_PORT,
77+
username = localProxyUsername.trim(),
78+
password = localProxyPassword,
79+
)
80+
}
81+
82+
internal fun buildLocalSocksInbound(
83+
appState: AppState,
84+
tag: String,
85+
options: LocalProxyOptions,
86+
): JsonObject {
87+
return buildJsonObject {
88+
put("tag", tag)
89+
put("listen", options.listenAddress)
90+
put("port", options.port)
91+
put("protocol", XrayProtocols.SOCKS)
92+
put("settings", options.toSocksInboundSettings())
93+
put(
94+
"sniffing",
95+
buildJsonObject {
96+
put("enabled", appState.enableSniffing)
97+
put("destOverride", xraySniffingDestOverrides(appState.effectiveFakeDnsEnabled).toJsonStringArray())
98+
put("routeOnly", appState.enableSniffingRouteOnly)
99+
},
100+
)
101+
}
102+
}
103+
104+
private fun AppState.localProxyListenAddress(): String {
105+
return if (localProxyListenAllInterfaces) {
106+
LocalProxyAllInterfacesAddress
107+
} else {
108+
LocalProxyLoopbackAddress
109+
}
110+
}
111+
112+
private fun AppState.localProxyExcludedPorts(): Set<Int> {
113+
return buildSet {
114+
if (runMode == RunModeTproxy) {
115+
add(transparentProxyPort.toPortOrNull() ?: DefaultTproxyPort)
116+
}
117+
if (runMode == RunModeTun2Socks) {
118+
add(socks5ProxyPort.toPortOrNull() ?: DefaultTun2SocksProxyPort)
119+
}
120+
if (enableHttpProxy) {
121+
httpProxyPort.toPortOrNull()?.let(::add)
122+
}
123+
}
124+
}
125+
126+
private fun LocalProxyOptions.matches(listenAddress: String, port: Int): Boolean {
127+
return this.port == port &&
128+
(
129+
this.listenAddress == listenAddress ||
130+
this.listenAddress == LocalProxyAllInterfacesAddress &&
131+
listenAddress == LocalProxyLoopbackAddress
132+
)
133+
}
134+
135+
private fun LocalProxyOptions.toSocksInboundSettings(): JsonObject {
136+
return buildJsonObject {
137+
put("auth", if (username.isBlank()) "noauth" else "password")
138+
put("udp", true)
139+
put("userLevel", 0)
140+
if (listenAddress == LocalProxyLoopbackAddress) {
141+
put("ip", LocalProxyLoopbackAddress)
142+
}
143+
if (username.isNotBlank()) {
144+
put(
145+
"users",
146+
buildJsonArray {
147+
add(
148+
buildJsonObject {
149+
put("user", username)
150+
put("pass", password)
151+
},
152+
)
153+
},
154+
)
155+
}
156+
}
157+
}
158+
159+
internal fun availablePort(
160+
listenAddress: String,
161+
excludedPorts: Set<Int> = emptySet(),
162+
): Int? {
163+
return runCatching {
164+
repeat(10) {
165+
ServerSocket(0, 0, InetAddress.getByName(listenAddress)).use { socket ->
166+
if (socket.localPort !in excludedPorts) {
167+
return@runCatching socket.localPort
168+
}
169+
}
170+
}
171+
null
172+
}.getOrNull()
173+
}
174+
175+
private fun isPortAvailable(
176+
listenAddress: String,
177+
port: Int,
178+
): Boolean {
179+
return runCatching {
180+
ServerSocket(port, 0, InetAddress.getByName(listenAddress)).use { }
181+
}.isSuccess
182+
}

app/src/main/kotlin/engine/proxy/ProxyEngineModels.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ data class ProxyEngineStartRequest(
1414
data class ProxyEngineStatus(
1515
val running: Boolean,
1616
val runMode: Int? = null,
17+
val appState: AppState? = null,
1718
)

app/src/main/kotlin/engine/root/RootConfigSupport.kt

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -97,45 +97,19 @@ private fun AppState.toRootStartConfig(
9797
}
9898

9999
internal fun AppState.buildRootSharedProxyInbounds(
100-
socksInboundTag: String,
101100
httpInboundTag: String,
102-
includeSocks5Proxy: Boolean = true,
103101
): List<JsonObject> {
104102
return buildList {
105-
socks5ProxyPort.toPortOrNull()
106-
?.takeIf { includeSocks5Proxy && enableSocks5Proxy }
107-
?.let { port -> add(buildRootSocksProxyInbound(socksInboundTag, port)) }
108103
httpProxyPort.toPortOrNull()
109104
?.takeIf { enableHttpProxy }
110105
?.let { port -> add(buildRootHttpProxyInbound(httpInboundTag, port)) }
111106
}
112107
}
113108

114-
internal fun AppState.rootSocks5ProxyPortValue(): Int {
109+
internal fun AppState.tun2SocksInternalProxyPortValue(): Int {
115110
return socks5ProxyPort.toPortOrNull() ?: DefaultTun2SocksProxyPort
116111
}
117112

118-
private fun buildRootSocksProxyInbound(
119-
tag: String,
120-
port: Int,
121-
): JsonObject {
122-
return buildJsonObject {
123-
put("tag", tag)
124-
put("listen", RootSharedProxyListenAddress)
125-
put("port", port)
126-
put("protocol", XrayProtocols.SOCKS)
127-
put(
128-
"settings",
129-
buildJsonObject {
130-
put("auth", "noauth")
131-
put("udp", true)
132-
put("ip", RootSharedProxyListenAddress)
133-
put("userLevel", 0)
134-
},
135-
)
136-
}
137-
}
138-
139113
private fun buildRootHttpProxyInbound(
140114
tag: String,
141115
port: Int,

app/src/main/kotlin/engine/root/RootIptablesConfig.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package engine.root
55

66
import android.content.Context
7+
import android.os.Process
78
import app.AppState
89
import app.modes.ProxyAppListModeGlobal
910
import utils.toTrimmedNonEmptyDistinctList
@@ -20,6 +21,7 @@ internal data class RootIptablesConfig(
2021
val proxyPrivateIpv6Cidrs: List<String> = emptyList(),
2122
val bypassPrivateIpv4Cidrs: List<String> = emptyList(),
2223
val bypassPrivateIpv6Cidrs: List<String> = emptyList(),
24+
val forcedBypassUids: List<Int> = emptyList(),
2325
val proxyAppListMode: Int = ProxyAppListModeGlobal,
2426
val proxyApplicationUids: List<Int> = emptyList(),
2527
)
@@ -50,6 +52,7 @@ internal fun RootIptablesConfig.withAppSettings(
5052
proxyPrivateIpv6Cidrs = proxyPrivateCidrs.ipv6Cidrs(),
5153
bypassPrivateIpv4Cidrs = bypassPrivateCidrs.ipv4Cidrs(),
5254
bypassPrivateIpv6Cidrs = bypassPrivateCidrs.ipv6Cidrs(),
55+
forcedBypassUids = listOf(Process.myUid()),
5356
proxyAppListMode = appListMode,
5457
proxyApplicationUids = if (appListMode == ProxyAppListModeGlobal) {
5558
emptyList()

app/src/main/kotlin/engine/root/RootModeEngine.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package engine.root
55

66
import android.content.Context
7+
import engine.proxy.LocalProxyRuntime
78
import engine.proxy.ProxyEngineStartRequest
89
import engine.proxy.ProxyEngineStatus
910
import engine.proxy.mode.AndroidModeProxyEngine
@@ -42,6 +43,7 @@ internal class RootModeEngine<Config : RootModeStartConfig>(
4243
logFileTailers = config.root.coreLogPaths.startCoreLogTailers(config.root.enableAccessLog)
4344
runCatching {
4445
runner.start(config)
46+
LocalProxyRuntime.update(config.localProxyOptions)
4547
if (rootContext.appState.enableRootBootScript) {
4648
runner.installBootScript(config)
4749
} else {
@@ -50,6 +52,7 @@ internal class RootModeEngine<Config : RootModeStartConfig>(
5052
}.onFailure { error ->
5153
runCatching { runner.stop(config.root.runtimeLayout) }
5254
.onFailure { stopError -> AndroidAppLogger.warn(logTag, "Failed to clean up $modeName after startup failure", stopError) }
55+
LocalProxyRuntime.clear()
5356
logFileTailers.forEach { tailer -> tailer.stop() }
5457
logFileTailers = emptyList()
5558
AndroidAppLogger.error(logTag, "Failed to start $modeName mode", error)
@@ -69,6 +72,7 @@ internal class RootModeEngine<Config : RootModeStartConfig>(
6972
}.onFailure { error ->
7073
AndroidAppLogger.warn(logTag, "Failed to stop $modeName mode", error)
7174
}
75+
LocalProxyRuntime.clear()
7276
return status()
7377
}
7478

0 commit comments

Comments
 (0)