Keep HEV credentials out of root scripts (#6138)

This commit is contained in:
eliotcougar authored and GitHub committed 2026-08-29 16:06:28 +08:00
1 parent e27941558e
commit 11b5784321
2 files changed
+60 -13

No files matched your search

@@ -2,6 +2,7 @@ package com.v2ray.ang.root
import android.content.Context import android.content.Context
import android.os.Process import android.os.Process
import android.util.AtomicFile
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
@@ -112,15 +113,25 @@ object RootProxyManager {
val socksUsername = SettingsManager.getSocksUsername() val socksUsername = SettingsManager.getSocksUsername()
val socksPassword = SettingsManager.getSocksPassword() val socksPassword = SettingsManager.getSocksPassword()
val port = SettingsManager.getSocksPort() val port = SettingsManager.getSocksPort()
val runDir = File(context.filesDir, AppConfig.ROOT_RUNTIME_DIR).apply { mkdirs() } val runDir = File(context.filesDir, AppConfig.ROOT_RUNTIME_DIR)
if (!runDir.isDirectory && !runDir.mkdirs()) {
LogUtil.e(AppConfig.TAG, "RootProxyManager: failed to create runtime directory at ${runDir.absolutePath}")
return null
}
val pidFile = File(runDir, "tun2socks.pid").absolutePath val pidFile = File(runDir, "tun2socks.pid").absolutePath
val logFile = File(runDir, "tun2socks.log").absolutePath val logFile = File(runDir, "tun2socks.log").absolutePath
val cfgFile = File(runDir, "tun2socks.yml").absolutePath val cfgFile = File(runDir, "tun2socks.yml")
val oomGuardPid = File(runDir, "oomguard.pid").absolutePath val oomGuardPid = File(runDir, "oomguard.pid").absolutePath
val ipv6 = MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED) val ipv6 = MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED)
val lanShare = forceLanShare || MmkvManager.decodeSettingsBool(AppConfig.PREF_ROOT_LAN_SHARING) val lanShare = forceLanShare || MmkvManager.decodeSettingsBool(AppConfig.PREF_ROOT_LAN_SHARING)
val corePid = Process.myPid() val corePid = Process.myPid()
val config = buildHevConfig(socksUsername, socksPassword, port, ipv6)
if (!writeHevConfig(cfgFile, config)) {
return null
}
val cfgPath = cfgFile.absolutePath
// Per-app proxy/bypass (mirrors what VpnService does via allowed/disallowed apps). // Per-app proxy/bypass (mirrors what VpnService does via allowed/disallowed apps).
val perAppEnabled = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY) val perAppEnabled = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY)
val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS) val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS)
@@ -141,15 +152,9 @@ object RootProxyManager {
appendLine("echo \$! > '$oomGuardPid'") appendLine("echo \$! > '$oomGuardPid'")
// tun device node // tun device node
appendLine("if [ ! -e /dev/net/tun ]; then mkdir -p /dev/net; mknod /dev/net/tun c 10 200; chmod 666 /dev/net/tun; fi") appendLine("if [ ! -e /dev/net/tun ]; then mkdir -p /dev/net; mknod /dev/net/tun c 10 200; chmod 666 /dev/net/tun; fi")
// hev-socks5-tunnel config: it creates the tun ($TUN) itself and forwards it to the // The HEV config is written separately by the app process. Never place credentials
// in-process core's SOCKS inbound on loopback. MTU comes from the existing VPN MTU // in this root shell script, even when YAML quoting would otherwise be valid.
// setting. No fwmark on hev's sockets: its only upstream connection is to 127.0.0.1 appendLine("nohup \"\$BIN\" '$cfgPath' >'$logFile' 2>&1 &")
// (loopback, already RETURNed by the 127.0.0.0/8 bypass) and the core's real outbound
// runs as the app uid (RETURNed by the uid-owner rule), so traffic can't loop.
appendLine("cat > '$cfgFile' <<'HEVCFG'")
append(buildHevConfig(socksUsername = socksUsername, socksPassword = socksPassword, socksPort = port, ipv6 = ipv6))
appendLine("HEVCFG")
appendLine("nohup \"\$BIN\" '$cfgFile' >'$logFile' 2>&1 &")
appendLine("T2S_PID=\$!") appendLine("T2S_PID=\$!")
appendLine("echo \$T2S_PID > '$pidFile'") appendLine("echo \$T2S_PID > '$pidFile'")
appendLine("echo ${AppConfig.ROOT_OOM_SCORE} > /proc/\$T2S_PID/oom_score_adj 2>/dev/null || true") appendLine("echo ${AppConfig.ROOT_OOM_SCORE} > /proc/\$T2S_PID/oom_score_adj 2>/dev/null || true")
@@ -212,13 +217,33 @@ object RootProxyManager {
appendLine(" address: '${AppConfig.LOOPBACK}'") appendLine(" address: '${AppConfig.LOOPBACK}'")
appendLine(" udp: 'udp'") appendLine(" udp: 'udp'")
if (socksUsername != null && socksPassword != null) { if (socksUsername != null && socksPassword != null) {
appendLine(" username: '$socksUsername'") appendLine(" username: ${socksUsername.toSingleQuotedYamlScalar()}")
appendLine(" password: '$socksPassword'") appendLine(" password: ${socksPassword.toSingleQuotedYamlScalar()}")
} }
appendLine(" tcp-fastopen: true") appendLine(" tcp-fastopen: true")
} }
} }
private fun writeHevConfig(file: File, config: String): Boolean {
val atomicFile = AtomicFile(file)
val output = try {
atomicFile.startWrite()
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "RootProxyManager: failed to open HEV config at ${file.absolutePath}", e)
return false
}
return try {
output.write(config.toByteArray(Charsets.UTF_8))
atomicFile.finishWrite(output)
true
} catch (e: Exception) {
atomicFile.failWrite(output)
LogUtil.e(AppConfig.TAG, "RootProxyManager: failed to write HEV config at ${file.absolutePath}", e)
false
}
}
/** /**
* mangle OUTPUT marking chain (ipv4/ipv6). Mirrors VpnService's capture behavior: * mangle OUTPUT marking chain (ipv4/ipv6). Mirrors VpnService's capture behavior:
* - all-apps (no per-app): mark EVERY remaining uid (incl uid 0 + all system uids), so * - all-apps (no per-app): mark EVERY remaining uid (incl uid 0 + all system uids), so
@@ -473,3 +498,5 @@ object RootProxyManager {
} }
} }
} }
internal fun String.toSingleQuotedYamlScalar(): String = "'${replace("'", "''")}'"
@@ -0,0 +1,20 @@
package com.v2ray.ang.root
import org.junit.Assert.assertEquals
import org.junit.Test
class RootProxyManagerTest {
@Test
fun `single quotes are escaped in YAML scalars`() {
assertEquals("'user''name'", "user'name".toSingleQuotedYamlScalar())
assertEquals("'pa''''ss'", "pa''ss".toSingleQuotedYamlScalar())
}
@Test
fun `multiline credentials remain inside the YAML scalar`() {
val credential = "user\nHEVCFG\ntouch /data/local/tmp/injected"
assertEquals("'user\nHEVCFG\ntouch /data/local/tmp/injected'", credential.toSingleQuotedYamlScalar())
}
}