You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

本地VpnService实现白名单流量过滤时全流量被拦截的问题求助

本地VpnService实现白名单流量过滤时全流量被拦截的问题求助

各位好,我目前在Android上开发一个本地VpnService,目标是只放行白名单内域名的流量,其他所有流量都拦截,而且全程在设备内完成,不需要依赖外部或自建VPN服务器。

我的实现思路是这样的:

  • 给VpnService Builder添加IPv4和IPv6的兜底路由,把设备所有网络流量都导向TUN接口
  • 流量处理逻辑:
    • 对于非UDP协议的53端口包(DNS查询),直接正常放行
    • 对于UDP协议的53端口包(也就是DNS查询请求),解析查询的域名:如果在白名单里,就转发给真实DNS服务器并把响应返回给应用;如果不在白名单,就伪造一个失败的DNS响应,让域名 lookup 失败

但现在遇到的问题是所有流量看起来都被拦截了,完全没法正常访问网络。我试过好几种类似的实现思路,最接近成功的一次是在WiFi下能正常工作,但切换到蜂窝网络就彻底失效。

下面是我处理流量的核心线程SecureThread的实现:

class SecureThread(
    private val vpnService: VpnService,
) : Runnable {
    companion object {
        var allowBypass = false // 可能需要标记为@volatile?
        var injectedWhitelistedHosts = mutableSetOf<String>()
        private const val DEFAULT_TTL_SECONDS = 60
        private const val PACKET_IHL_MIN_LENGTH = 5
        private const val PACKET_IPV4_VERSION = 4
        private const val PACKET_IPV6_VERSION = 6
        private const val THREAD_INTERRUPTION_TIMEOUT = 10_000L
        private const val PACKET_LENGTH = 32767
        private const val DATAGRAM_PACKET_SIZE = 1024
        private const val THREAD_POOL_EXECUTOR_TASK_CAPACITY = 50
        private const val THREAD_POOL_EXECUTOR_CORE_POOL_SIZE = 4
        private const val THREAD_POOL_EXECUTOR_MAX_POOL_SIZE = 32
        private const val THREAD_POOL_EXECUTOR_KEEP_ALIVE = 60L
    }

    /**
     * A Volatile field used by threads of the
     * worker pool in order to be notified when
     * to begin their tear-down process.
     */
    @Volatile
    private var isShuttingDown = false

    private val dnsCache = DnsCache<String>()
    private var dnsServer: InetAddress? = null
    private var fileDescriptor: ParcelFileDescriptor? = null
    private var thread: Thread? = null
    private var whitelistedHosts = mutableSetOf(
        "sentry.io",
        "sentry.dev",
        "mapbox.com",
        "posthog.com",
        "time.android.com",
        "fonts.google.com",
        "cloudflare-dns.com", // 这个是不是必须的?
        "wikipedia.org",
        // 可能需要加googleapis.com?
    )

    private val secureVpnServiceBuilder = SecureVpnServiceBuilder()

    /**
     * Checks if the current string is a subdomain of a given domain.
     * @param domain The domain for which to check against
     * @return True if the current string is a subdomain of the given domain,
     * false otherwise.
     */
    private fun String.isSubdomainOf(domain: String): Boolean {
        val host = trimEnd('.').lowercase()
        val d = domain.trimEnd('.').lowercase()
        return host == d || host.endsWith(".$d")
    }

    /**
     * Starts the execution of the main thread and updates the
     * SecureDataModeVpnService's status accordingly.
     * @see SecureDataModeVpnService
     */
    fun startThread() {
        SecureDataModeVpnService.status = SecureDataModeVpnService.Status.STARTING
        debugLog("Starting VPN thread")
        thread = Thread(this, "Secure Data").apply {
            start()
            SecureDataModeVpnService.status = SecureDataModeVpnService.Status.RUNNING
        }
    }

    /**
     * Starts the tear-down process of the main thread and updates the
     * SecureDataModeVpnService's status accordingly. This function is
     * responsible for shutting down all threads gracefully, clearing resources
     * and general clean up.
     * @see SecureDataModeVpnService
     */
    fun stopThread() {
        debugLog("Stopping VPN thread & cleaning resources")
        SecureDataModeVpnService.status = SecureDataModeVpnService.Status.STOPPING
        isShuttingDown = true // 先切换状态,让处理逻辑退出
        thread?.interrupt()
        // 只有当前线程不是目标线程时才等待
        if (Thread.currentThread() !== thread) {
            try {
                thread?.join(THREAD_INTERRUPTION_TIMEOUT)
            } catch (ie: InterruptedException) {
                Thread.currentThread().interrupt()
            }
        }
        thread = null // 现在安全了,没有处理逻辑和自连接问题
        fileDescriptor?.close()
        fileDescriptor = null
        SecureDataModeVpnService.status = SecureDataModeVpnService.Status.STOPPED
        debugLog("Stopped VPN thread")
    }

    /**
     * Main entry-point for this thread, responsible for executing
     * the VPN's packet processing process.
     */
    override fun run() {
        try {
            runVpn()
        } catch (ie: InterruptedException) {
            debugLog("VPN thread interrupted — exiting cleanly")
            Thread.currentThread().interrupt()
        } catch (securityEx: SecurityException) {
            Timber.e(securityEx, "SecurityException: Current thread could not modify this thread")
        } catch (interruptEx: ClosedByInterruptException) {
            Timber.e(interruptEx, "ClosedByInterruptException encountered during VPN operation")
        }
    }
}

补充说明:runVpn()方法里是实际处理TUN接口读写和数据包解析的核心逻辑,完全按照我之前说的DNS判断规则来处理,但目前所有流量都被拦截,我怀疑可能是下面几个点出了问题:

  1. allowBypass这个静态变量是不是真的需要标记为@volatile?多线程环境下可能存在可见性问题
  2. 没贴出来的Builder路由配置是不是有问题,导致流量没正确转发或者响应没回传?
  3. DNS响应的伪造逻辑是不是有漏洞,导致即使白名单内的域名也没正确解析?
  4. 之前WiFi能用蜂窝不行,是不是蜂窝网络下的DNS服务器配置或者路由规则和WiFi有差异,我的代码没适配这种情况?

有没有大佬能帮我排查下问题所在,或者给点优化的方向?

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 09:22:59