DNS 重定向后 dnsmasq 查询为何会绕过 VPN
路由器侧的 REDIRECT 会把 LAN 的 DNS 查询变成一个新的解析器套接字。客户端流上的标记永远不会到达那里,因此要直接设置上游出口。
LAN 客户端能正确连上自己的 VPN,可它的 DNS 查询仍然出现在错误的上行链路上。这并不是通常意义上的泄漏;当路由器不再转发查询而是自己作答时就会出现这种情况。拦截会把目的地址改写成一个本地套接字,随后解析器会用自己的源地址和自己的路由决策开启一段毫不相干的第二段会话。加在客户端流上的标记只描述第一段会话,对第二段一无所知。
标记不会随查询一起走
数据包标记存在于 conntrack 条目和飞行中的数据包上。REDIRECT 规则终结了这次飞行:载荷被交给本地套接字,原来的条目已经完成了它的使命。此后离开路由器的是一个由解析器进程新建的套接字,源地址是路由器自己的地址。
所以,以 LAN 客户端地址为键的策略路由永远匹配不上上游查询。新套接字属于 dnsmasq 而不属于客户端,内核是按照路由器自身流量来评估路由的。源地址规则、接口规则和标记规则看到的都是另一个元组。假定标记会传递下去,是这种配置中最常见的错误,而且在客户端侧的抓包里根本看不出来。
这两段会话确实共享同一个名字和同一个时刻,这也正是快速浏览抓包时它们看起来像同一条流的原因。
转发在哪里变成本地解析
先弄清楚 53 端口是被重定向、被转换到另一台主机,还是原封不动,因为这三种情况表现完全不同。重定向到本地端口会终止转发,而转换到另一台机器则让数据包继续移动,并让客户端的元组始终保持意义。
两个协议都要看。同时支持 UDP 和 TCP 的解析器会在两种协议上都被测到,而只覆盖 UDP 的规则集会给大响应留下一个安静的缺口。路由器自带配置管理器生成的规则同样算数,而修改生成出来的文件通常敌不过下一次重新加载。
sudo nft list ruleset | grep -n -B2 -A4 'dport 53'
sudo iptables -t nat -L -n -v | grep -E ':53|dns'
sudo ss -lunp | grep ':53'
这些输出依次回答三个问题:什么改写了查询、什么接收了它,以及哪个进程拥有监听套接字。一旦知道了归属,上游策略就有了落脚处。
给解析器一套自己的出口策略
当只有部分域名需要走隧道时,按域名的上游是最干净的工具。dnsmasq 接受限定到某个域名的 server 行,并可用可选的接口或源地址把查询从指定设备发出去。正是这种引导让第二段会话跟着 VPN 走。
server=/corp.example/10.30.0.53@tun_work
log-queries
上面的域名、地址和接口都是占位符,请换成你自己的值。域名选择和出口控制仍然是两个独立的决定:路由必须存在,源地址必须被上游解析器允许,防火墙必须放行回包。仅靠接口引导并不是 fail-closed,因为隧道一旦消失,解析器可能落回另一条路径。要把任何公共兜底都挡在必须保持私有的命名空间之外。
把这一行写进路由器支持的配置归属处,而不是写进生成出来的文件,否则下一次重新加载就会把修复抹掉。
共享缓存保不住什么
一个转发器配一个缓存服务所有 LAN 客户端,这会抹掉某些网络所需要的区分。如果两个客户端必须把同一个名字解析成不同结果,共享套接字没有办法还原是谁问的。按客户端区分的策略只能放在独立的解析器实例里,或者放在能保留身份的转发功能里。
缓存下来的应答也不能证明新路径是通的。应答可以从内存里返回,而上游路由依然断着。
53 端口的规则对走 HTTPS 或走 TLS 的 DNS 应用只字未提。那些查询根本到不了重定向,所以 只建立在 53 端口之上的策略只描述了流量的一部分 这一点本身就应当写成文档。
要验证的是第二段会话,而不是第一段
用一个你自己掌控的名字来测,并观察两侧。客户端侧的抓包只能证明查询离开了 LAN;只有路由器侧才能证明它接下来去了哪里。
sudo tcpdump -ni tun_work udp port 53
ip route get 10.30.0.53
dig +short corp.example @10.30.0.53
每次尝试之前都清空缓存,因为温热的应答会掩盖坏掉的上游。用 TCP 再重复一遍,然后在隧道断开的情况下再重复一遍,看解析器在首选路径消失时到底怎么做。最后这个行为才决定这套设计是 fail-closed,还是只是运气好。