本地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判断规则来处理,但目前所有流量都被拦截,我怀疑可能是下面几个点出了问题:
allowBypass这个静态变量是不是真的需要标记为@volatile?多线程环境下可能存在可见性问题- 没贴出来的Builder路由配置是不是有问题,导致流量没正确转发或者响应没回传?
- DNS响应的伪造逻辑是不是有漏洞,导致即使白名单内的域名也没正确解析?
- 之前WiFi能用蜂窝不行,是不是蜂窝网络下的DNS服务器配置或者路由规则和WiFi有差异,我的代码没适配这种情况?
有没有大佬能帮我排查下问题所在,或者给点优化的方向?
内容来源于stack exchange
相关产品推荐
相关产品推荐

