arblant
railway-clean-ip-scanner-android / app / src / main / java / com / arblant / railwayipscanner / VlessScanner.kt
VlessScanner.kt219 lines · 8.6 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
package com.arblant.railwayipscanner

import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.net.InetSocketAddress
import java.net.Socket
import java.security.cert.X509Certificate
import java.util.UUID
import javax.net.ssl.SNIHostName
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocket
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager

/**
 * 100% Real VLESS End-to-End WebSocket + Google 204 Protocol Scanner.
 * Completely eliminates false positives on Iranian mobile networks (MCI / Irancell / Wi-Fi).
 */
object VlessScanner {
    private const val DEFAULT_TIMEOUT_MS = 2400

    private val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
        override fun getAcceptedIssuers(): Array<X509Certificate>? = null
        override fun checkClientTrusted(certs: Array<X509Certificate>, authType: String) {}
        override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) {}
    })

    private val sslContext: SSLContext by lazy {
        SSLContext.getInstance("TLS").apply {
            init(null, trustAllCerts, java.security.SecureRandom())
        }
    }

    fun testIpRealDelay(
        ip: String,
        domain: String,
        uuidString: String,
        path: String = "/",
        port: Int = 443,
        doubleCheck: Boolean = true
    ): ScanResult {
        val firstAttempt = runSingleProbe(ip, domain, uuidString, path, port)
        if (!firstAttempt.isSuccess) {
            return firstAttempt
        }

        if (doubleCheck) {
            try { Thread.sleep(60) } catch (_: Exception) {}
            val secondAttempt = runSingleProbe(ip, domain, uuidString, path, port)
            if (!secondAttempt.isSuccess) {
                return ScanResult(ip, false, 0, "Unstable connection (packet drop on 2nd probe)")
            }
            try { Thread.sleep(60) } catch (_: Exception) {}
            val thirdAttempt = runSingleProbe(ip, domain, uuidString, path, port)
            if (!thirdAttempt.isSuccess) {
                return ScanResult(ip, false, 0, "Unstable connection (packet drop on 3rd probe)")
            }
            val avgDelay = (firstAttempt.delayMs + secondAttempt.delayMs + thirdAttempt.delayMs) / 3
            return ScanResult(ip, true, avgDelay)
        }

        return firstAttempt
    }

    private fun runSingleProbe(
        ip: String,
        domain: String,
        uuidString: String,
        path: String,
        port: Int
    ): ScanResult {
        val t0 = System.currentTimeMillis()
        var rawSocket: Socket? = null
        var sslSocket: SSLSocket? = null
        try {
            // 1. Direct TCP Connect
            rawSocket = Socket()
            rawSocket.connect(InetSocketAddress(ip, port), DEFAULT_TIMEOUT_MS)
            rawSocket.soTimeout = DEFAULT_TIMEOUT_MS

            // 2. TLS Handshake with Domain SNI & ALPN http/1.1
            val factory = sslContext.socketFactory
            sslSocket = factory.createSocket(rawSocket, domain, port, true) as SSLSocket
            sslSocket.soTimeout = DEFAULT_TIMEOUT_MS

            val sslParams = sslSocket.sslParameters
            sslParams.serverNames = listOf(SNIHostName(domain))
            try {
                sslParams.applicationProtocols = arrayOf("http/1.1")
            } catch (_: Exception) {}
            sslSocket.sslParameters = sslParams
            sslSocket.startHandshake()

            val out: OutputStream = sslSocket.outputStream
            val inp: InputStream = sslSocket.inputStream

            val cleanPath = if (path.startsWith("/")) path else "/$path"

            // 3. Send WebSocket Upgrade Request
            val crlf = "\r\n"
            val wsReq = "GET " + cleanPath + " HTTP/1.1" + crlf +
                    "Host: " + domain + crlf +
                    "Upgrade: websocket" + crlf +
                    "Connection: Upgrade" + crlf +
                    "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" + crlf +
                    "Sec-WebSocket-Version: 13" + crlf +
                    "User-Agent: Mozilla/5.0 (Android; SpiderPanel)" + crlf + crlf
            out.write(wsReq.toByteArray(Charsets.UTF_8))
            out.flush()

            // Read Upgrade Response completely until \r\n\r\n
            val headerBytes = readUntilDoubleCRLF(inp)
            if (headerBytes == null) {
                return ScanResult(ip, false, 0, "No WS upgrade response")
            }
            val headerStr = String(headerBytes, Charsets.UTF_8)
            val firstLine = headerStr.lines().firstOrNull() ?: ""
            if (!firstLine.contains("101")) {
                return ScanResult(ip, false, 0, "WS Upgrade failed: $firstLine")
            }

            // 4. Binary VLESS Protocol Header with UUID Auth
            val uuidObj = UUID.fromString(uuidString)
            val uuidBytes = ByteArray(16).apply {
                val msb = uuidObj.mostSignificantBits
                val lsb = uuidObj.leastSignificantBits
                for (i in 0..7) this[i] = ((msb ushr (8 * (7 - i))) and 0xFF).toByte()
                for (i in 8..15) this[i] = ((lsb ushr (8 * (15 - i))) and 0xFF).toByte()
            }

            val targetHost = "www.google.com".toByteArray(Charsets.UTF_8)
            val vlessHeader = ByteArray(1 + 16 + 1 + 1 + 2 + 1 + 1 + targetHost.size).apply {
                var pos = 0
                this[pos++] = 0.toByte() // Version 0
                System.arraycopy(uuidBytes, 0, this, pos, 16); pos += 16
                this[pos++] = 0.toByte() // Proto addon length
                this[pos++] = 1.toByte() // Command: 1 = TCP
                this[pos++] = 0.toByte(); this[pos++] = 80.toByte() // Port 80
                this[pos++] = 2.toByte() // Address Type: Domain
                this[pos++] = targetHost.size.toByte()
                System.arraycopy(targetHost, 0, this, pos, targetHost.size)
            }

            // HTTP 204 Probe Payload
            val httpPayload = ("GET /generate_204 HTTP/1.1" + crlf +
                    "Host: www.google.com" + crlf +
                    "User-Agent: v2rayN" + crlf +
                    "Connection: close" + crlf + crlf).toByteArray(Charsets.UTF_8)

            val fullData = vlessHeader + httpPayload

            // WebSocket Client Masking
            val mask = byteArrayOf(0x12, 0x34, 0x56, 0x78)
            val maskedData = ByteArray(fullData.size)
            for (i in fullData.indices) {
                maskedData[i] = (fullData[i].toInt() xor mask[i % 4].toInt()).toByte()
            }

            val frameHeader: ByteArray = if (fullData.size < 126) {
                byteArrayOf(0x82.toByte(), (0x80 or fullData.size).toByte()) + mask
            } else {
                byteArrayOf(
                    0x82.toByte(),
                    (0x80 or 126).toByte(),
                    ((fullData.size ushr 8) and 0xFF).toByte(),
                    (fullData.size and 0xFF).toByte()
                ) + mask
            }

            out.write(frameHeader + maskedData)
            out.flush()

            // 5. Read End-to-End Google Response
            val respBuf = ByteArray(1024)
            val readLen = inp.read(respBuf)
            val delayMs = System.currentTimeMillis() - t0

            if (readLen > 0) {
                val respStr = String(respBuf, 0, readLen, Charsets.ISO_8859_1)
                if (respStr.contains("HTTP/1.1 204") || respStr.contains("204 No Content")) {
                    return ScanResult(ip, true, delayMs)
                }
            }
            return ScanResult(ip, false, delayMs, "No valid Google 204 response")

        } catch (e: Exception) {
            return ScanResult(ip, false, 0, e.message ?: "Connection error")
        } finally {
            try { sslSocket?.close() } catch (_: Exception) {}
            try { rawSocket?.close() } catch (_: Exception) {}
        }
    }

    private fun readUntilDoubleCRLF(inp: InputStream): ByteArray? {
        val buffer = ByteArrayOutputStream()
        val matchPattern = byteArrayOf(13, 10, 13, 10)
        var matched = 0
        val maxBytes = 4096
        var totalRead = 0

        while (totalRead < maxBytes) {
            val b = inp.read()
            if (b == -1) break
            totalRead++
            buffer.write(b)

            if (b.toByte() == matchPattern[matched]) {
                matched++
                if (matched == matchPattern.size) {
                    return buffer.toByteArray()
                }
            } else {
                matched = if (b.toByte() == matchPattern[0]) 1 else 0
            }
        }
        return if (buffer.size() > 0) buffer.toByteArray() else null
    }
}