代理Proxy和VPN检测

149 阅读2分钟

在移动应用安全场景中,攻击者常借助 HTTP 代理(如 Burp、Charles)   或 VPN 隧道(如 Reqable、Clh、V2y)   实现抓包、重放、篡改流量。
因此,检测设备当前是否处于代理或 VPN 环境,是 APP 进行流量完整性防护的关键步骤。

代理(Proxy)

代理服务器充当中间人角色,在用户与目标网站之间中转请求与响应。
它可以隐藏真实 IP、绕过访问限制或加速网络请求。
但其安全性有限,数据可能未加密或被服务商窃取。

VPN(Virtual Private Network)

VPN 通过加密隧道将设备的所有网络流量封装传输至远程服务器,实现更强的隐私与安全保护。
VPN 会接管设备的网络路由,所有数据在离开设备前即被加密,可有效防止流量嗅探与中间人攻击。

/**
 * 代理检测
 * 可通过系统属性读取是否存在手动代理配置
 * (如OkHttp默认配置)会遵循系统代理设置。
 */
fun detectProxyBySystemProperties(): Triple<Boolean, String?, Int?> {
    val proxyHost = System.getProperty("http.proxyHost")
    val proxyPort = System.getProperty("http.proxyPort")

    val httpsProxyHost = System.getProperty("https.proxyHost")
    val httpsProxyPort = System.getProperty("https.proxyPort")

    val host = proxyHost ?: httpsProxyHost
    val port = proxyPort ?: httpsProxyPort

    return if (!host.isNullOrBlank() && !port.isNullOrBlank()) {
        val portInt = port.toIntOrNull()
        val isEnabled = portInt != null
        Triple(isEnabled, host, portInt)
    } else {
        Triple(false, null, null)
    }
}

/**
 * 代理检测
 * 更精确的方式是遍历当前所有网络接口,读取LinkProperties的代理信息:
 * 典型特征:Wi‑Fi 手动代理通常能在此命中;蜂窝网络较少使用 HTTP 代理。
 */
fun detectLinkProxiesStrict(
    connectionManager: ConnectivityManager,
    networks: List<Network>
): List<String> {
    val proxyList = mutableListOf<String>()
    for (network in networks) {
        val linkProperties = connectionManager.getLinkProperties(network)
        linkProperties?.let {
            val proxyInfo = it.httpProxy
            proxyInfo?.let { proxy ->
                val host = proxy.host
                val port = proxy.port
                proxyList.add("$host:$port")
            }
        }
    }
    return proxyList.distinct()
}
/**
 * VPN检测
 * Android 系统会在 VPN 活动时为网络标记TRANSPORT_VPN, 系统级检测,准确率高。
 * 某些第三方 VPN 应用可能未正确上报传输能力。
 */
private fun isVpnActiveByCapabilities(context: Context): Boolean {
    val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val networks = cm.allNetworks?.toList().orEmpty()

    if (networks.isNotEmpty()) {
        for (nw in networks) {
            val caps = cm.getNetworkCapabilities(nw) ?: continue
            if (caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) return true
        }
        return false
    } else {
        val caps = cm.getNetworkCapabilities(cm.activeNetwork)
        return caps?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true
    }
}

/**
 * VPN检测
 * 通过枚举网卡接口名(tun*、ppp*)判断是否存在虚拟隧道。
 * 不依赖系统信号,可发现部分伪装型 VPN。
 * 可能被自定义内核或 Root 工具隐藏。
 */
private fun isVpnActiveByInterfaces(): Pair<Boolean, List<String>> {
    val suspicious = mutableListOf<String>()
    try {
        val en = NetworkInterface.getNetworkInterfaces()
        while (en.hasMoreElements()) {
            val ni = en.nextElement()
            val name = ni?.name ?: continue
            val lower = name.lowercase(Locale.getDefault())
            if (lower.startsWith("tun") || lower.startsWith("ppp")) suspicious.add(name)
        }
    } catch (_: Throwable) {
    }
    return Pair(suspicious.isNotEmpty(), suspicious)
}