玩软路由不折腾DNS是没有灵魂的。

———我说的

日常上网的流程

在日常上网时需要先进行DNS解析然后再根据解析结果发起连接,这个基本逻辑并不复杂。

flowchart LR
  a("客户端")
  b("DNS服务器")
  a<-->|"1(域名解析)"|b 
  a-->|2|c("你要访问的网站")
  style b fill:#00CC66

使用代理上网的工作流程

使用代理的意义在于让它替你访问那些你访问不了的网站,但是在引入了代理之后这个逻辑就不再简单了。

flowchart LR
  a("浏览器(使用域名发起连接)")
  dns1(["DNS服务器"])
  c("代理服务器")
  dns2(["离代理服务器更近的DNS"])
  web1("直连的网站")
  web2("需要代理的网站")
  subgraph "代理客户端"
    direction LR
    b1("需要代理吗")-->|2|b2("需要")
    b1-->|2|b3("不需要")
  end
  b2-->c
  a -->|1| b1
  b3-->|"3(域名解析)"|dns1
  b3-->|4|web1
  c<-->|"3(远程域名解析)"|dns2
  c-->|4|web2

  classDef dns fill:#00CC66
  class dns1,dns2 dns
  style b1 fill:#fff

这是一个使用系统代理(或者叫非透明代理,比如HTTP(S)、socks5等协议)访问网站的流程图,其中有些部分被省略了,比如在匹配规则的时候为了检查ip规则可能还需要进行域名解析。

这里面最有趣的地方就是使用域名发起连接,这是远端解析的核心功能。

“用域名发起连接”与“远端解析”

在不使用代理访问互联网时,你发送的数据包目标地址要么是一个IPv4地址要么是IPv6地址,这就像在寄信或者包裹的时候写的收件地址。比如我们寄一份信给肯德基,告诉它想要你想要点餐,虽然平常邮寄东西的时候一定要写明收件地址,但实际上这封信寄到哪一个肯德基都是可以的。

这里使用肯德基进行比喻是有原因的,肯德基是一个连锁品牌,它在很多地方都有店,就像互联网上使用了CDN的网站一样,你在任意一家店能得到的服务都是一样的。既然世界各地的店能提供的服务是一样的那么找一个最近的就行了吧?在之前的DNS文章中我介绍过,利用DNS可以找到离你最近的CDN接入点。

话说回来,如果我只想寄到最近的肯德基那可不可以在收件地址上只写“肯德基”呢?在平常,没写具体的地址显然是没法邮寄的。如果我们有一个人(代理服务器)能帮我们跑腿呢?

有了这个帮我们跑腿的人就好说了,我们直接告诉他把这封信寄到肯德基去,至于是哪的肯德基不重要,让他去找距离他自己近的肯德基就好,近一点能省下很多时间!

那我们没告诉跑腿的这封信要寄到肯德基,我们只写下肯德基的地址会怎样呢?

先从这个地址开始,这个地址一定是我们通过问路(DNS解析)得到的,这是一个离我们最近的地址,近到就在楼下。可是现在我们因为各种原因没法直接去楼下这家肯德基,我们需要通知跑腿把信寄过去收到回信后再寄给我们,他的位置离这里稍远,足足几百公里,从他哪里寄信到楼下要相当长的时间,回信也是,山高路远,如果遇到信被寄丢了就需要等得更久!

说到这里,你应该可以理解“远端解析”的重要性了,“远端解析”的意义是最优化代理访问的体验,而“用域名发起连接”是“远端解析”的前提,带入到上述例子里,“用域名发起连接”是告诉代理服务器你要访问的网站和你的数据,而“远端解析”则是代理服务器在得知你要访问的网站后去找最近的接入点后将你的数据交给他。

远端解析与透明代理

首先,无论是透明代理还是非透明代理一般都会先接入位于局域网中的代理客户端,然后通过代理客户端与代理服务器进行连接。为了便于理解在上一章节的例子中没有提到代理客户端的存在,至少只是理解远端解析时代理客户端的意义还不太重要。

引入代理客户端的主要目的是可以使用一个任意的协议与代理服务器通信(前提是双方都支持这个协议),毕竟给每个你要用的软件都适配一个新的代理协议实在是吃力不讨好,简单的说就是代理客户端负责将不同的协议进行转换,比如各个软件/系统都支持socks5代理协议,而代理客户端将socks5转换为Shadowsocks后就可以让这些不支持Shadowsocks协议的软件不用修改就都能使用Shadowsocks协议代理了。

flowchart LR
  subgraph "局域网"
    direction LR
    c("浏览器")-->|socks5|pc("代理客户端")
  end
pc--->|"各种代理协议"|ps("代理服务器")
ps-->web("网站")

在引入透明代理后,会让上一章节中的例子更复杂。透明代理与非透明代理的重要区别就是你是否知道你有一个代理服务器为你跑腿。

  • 如果你不知道有个人可以为你跑腿,那么你寄信或者是包裹都是像往常一样,先问路获得地址,然后把地址写在包裹上寄出去。
  • 如果你知道有个人可以为你跑腿,那你就可以像上一章节中的例子一样,告诉代理服务器你要访问的网站,然后代理服务器再去找最近的地址……

透明代理的困境

透明代理是指使用了特殊手段接入代理的方法,比如使用TUN模式(虚拟网卡)、Tproxy、redirect、ebpf等方法,这种方法从网络数据入手,劫持流量给代理客户端并且不需要在设备或者软件上配置代理服务器。

在肯德基的例子里我们知道,使用域名发起连接可以改善访问体验,可是透明代理是“透明”的,对于设备/软件来说就像不存在一样的,试想一下:你不知道有人可以为你跑腿,你只能像平常一样去问路获取地址,然后按这个地址寄出信,当有人拿到信时只能得知这封信会寄到某一个地方,但这个地方具体是干什么的却无从得知。

既然使用域名发起连接的访问体验要更好,那能否将平常的流量转换为域名连接呢?在透明代理中最棘手的地方莫过于这里了,单单只是转换流量还好说,但是该如何知道你到底要访问什么网站呢?通过追踪你的DNS解析是一个不错的选择。

现在你明白为什么路由器上那些科学上网插件最后都难逃DNS设置了吧,因为这些插件完成上述转换就要作为一个DNS服务器,如果可以作为你的DNS服务器就可以记录你的DNS请求历史,假设你解析teapotium.com得到了IP104.19.1.1,只要你向104.19.1.1发送数据就代表你想要访问teapotium.com了。在得知目标域名后剩下的就只有一些简单的转换工作了。

形象的解释透明代理的工作流程

我们继续用“肯德基”一些形象的比喻解释:

  • DNS解析 → 问路
  • 需要代理的设备 → 你
  • 访问网站 → 寄信
  • 透明代理网关 → 门卫
  • 门卫的备忘录 → 分流规则
  • 代理服务器 → 跑腿的
  • 目标网站 → 肯德基

今天是周四,你决定利用肯德基疯狂星期四的优惠吃上一顿,所以你准备寄一份信过去,现在先去门卫那里问一下肯德基的地址在哪。

等了一会儿后门卫告诉你最近(可能)的肯德基在地球街101号,门卫一边说着一边记下了地球街101号肯德基

有了地址之后你就可以寄信了,你把地球街101号写在了信封上,然后交给了门卫。

门卫拿到信后发现信的目的地是地球街101号,他看了一下刚刚用的记事本,里面写的地球街101号肯德基,很显然这是一封要寄往肯德基的信。门卫又看看备忘录,这里面有写有去肯德基的信就交给跑腿的,所以他将信封的目的地改为了肯德基,然后把信再包上一层信封后寄给了跑腿的。

跑腿的收到信拆开后得知这是一封要送给肯德基的信,他通过问路得知最近的肯德基在火星街2025号,再将原本目的地肯德基改为了火星街2025号,然后再把信寄出。

在肯德基收到信之后将你点的餐寄给跑腿的最后原路寄回给你。

你知道吗?你直到最后都是以为是地球街101号把你点好的餐寄回的,可实际上是火星街2025号寄回来的,这是透明代理的一个特点!透明代理会将你的信寄给其他地址,然后在回信时以原地址的名义发回给你。

利用远端解析还可以实现跨网络协议访问网站,比如只能访问ipv4的客户端可以通过一个双栈的代理服务器访问一个ipv6的网站

其他获取目标域名的办法

真ip/real-ip/redir-host

在上面的例子里的方式就是真ip,最开始的地球街101号地址是真实存在且有效的,真ip的名字就是由此得来。真ip有个小问题,就是偶尔会弄错你要访问的目标域名,这个解释起来要麻烦点,需要另开一篇文章。

这个问题导致了原版clash直接移除了redir-host模式。

假ip/fake-ip/fakedns

仔细想想,在上面的例子里从一开始就没向地球街101号这个地址寄过什么东西,这个地址仅仅用来获取你的目标域名而已,最后会被转换为域名类型的连接。所以为什么不直接一点,直接告诉你一个假ip,比如:黑洞街1号,不同的域名用不同的假ip,这样还省下了在问路中浪费的时间,随便编一个地址一定要比去问真的快,而且还不会弄错你要访问的域名!

假ip被定义在rfc3089,现在的clash/meta/mihomo,v2ray,xray,sing-box等软件都已经支持,虽然是假ip但大多数情况下不会影响上网。假ip也算是一种DNS污染,它的DNS记录在正常情况下的有效期很短,如果对DNS的ttl处理不当可能导致偶尔无法上网。

假ip除了不会弄错你的目标域名以外还省掉了一次域名解析。在正常的透明代理流程中最多需要3次域名解析,其中上网设备首次连接时解析一次,代理客户端匹配规则时解析一次(可能不用解析),访问网站时解析一次(代理下远端解析或者是直连下代理客户端解析),假ip会省掉上网设备的一次解析,而匹配规则时的解析根据规则列表可能会解析(比如未能命中域名规则在匹配后面的ip规则时就需要进行一次解析),在需要直连的情况下代理客户端可以直接采用刚刚匹配规则时的解析结果进行访问。

真ip其实也可以像假ip这样“省掉”域名解析的,比如:

  1. 使用乐观缓存时从缓存中获取ip,速度和假ip差不多,整体体验上几乎和假ip一致。
  2. 在匹配到ip规则时直接使用真ip匹配规则,完全免去此时的域名解析
  3. 通过直连访问网站时直接使用真ip作为目标,省掉直连时的域名解析

也就是说情况理想时(有缓存,实际ip正确,需要直连)部分连接可以完全省略解析,不过根据代理客户端的代码实现不同,2和3不一定会省掉,应以实际源代码实现为准.

域名嗅探/sniffer

在现实中写的信难免要在内容中写下收件人的姓名,比如Dear Li Hua,只要把信拆开看看这个不就知道这封信要寄给谁了吗?确实如此,在互联网中的大多数流量http/https中会有一些字段用来写目标域名,只要读取这些字段即可,在http流量中这个字段叫host,在https或者其他的tls加密流量中为sni。

尽管有些TLS流量使用了ECH,但它仍然有明文的SNI只是和实际目标不同而已,因为ECH的伪装目标往往还是同CDN下的域名,所以使用透明代理以及远端解析不会影响网站访问,只是代理软件匹配规则时不能向以往那样精细了。

这种方法要比真ip精准,比假ip侵入性更低,但仍然有可能弄错目标。比如Tor为了伪装会将SNI设定为一个正常网站,在使用Tor的时候域名嗅探可能会将Tor的流量发往其他服务器导致无法连接到Tor网络。

参考文献

浅谈在代理环境中的 DNS 解析行为 - Sukka’s Blog
rfc3089 | 一种基于SOCKS的IPv6/IPv4网关机制(英语原文) - ietf
rfc3089 | 一种基于SOCKS的IPv6/IPv4网关机制(中文) - rfc2cn