目录

四元组,路由与防火墙 —— 一个数据包的西游记

很多朋友在折腾防火墙或者科学上网时,经常遇到一些底层网络问题不知如何深挖,或自己的需求过于古怪没人提过解决方案。究其原因是对 TCP/IP 或 UDP/IP 认知还处于黑盒子的阶段。

接下来我们来一个实际例子,跟着一个小数据包出门溜一圈,帮助你建立初步的概念,之后折腾时不至于完全瞎蒙。这个例子里,你装了一个使用 TUN 模式的梯子 sing-box

写快递单时,最重要的东西是寄件人地址和收件人地址。四元组基本上就是这个功能,再多一个寄/发件人邮箱号码的概念。想象一个大楼里住着 65535 户人,光有收件地址是不够精准定位到一个人的。

我下面所说的「四元组」,指的其实都是带上协议编号的「五元组」。

你的电脑内

包的诞生

你在 Chrome 里敲入 https://twitter.com 并按了回车。浏览器发现这是 https 协议,于是开始构造这个连接的第一个 TCP 数据包。

  • DST IP

    数据包的目的 IP ,也就是收件人地址。浏览器查到 twitter.com 在内部 DNS 缓存里有记录为 104.244.42.65

    我们先跳过本应有的 DNS 环节。DNS 只是把本例的 TCP 改成 UDP ,443 改成 53 而已。流程上没有特殊之处。

  • DST PORT

    数据包的目的端口号。也就是收件人快递箱编号。这个例子里,用户没在地址里指定 :端口号 ,所以浏览器填上了 HTTPS 协议默认的端口号 443

    下面两个字段浏览器没填就提交给系统的网络 API 了,由系统帮它补上。

  • SRC IP

    数据包的来源 IP ,也就是寄件人地址。系统根据当前网络的默认设置,帮忙填上了这一栏(本例中的具体内容下详),也就是系统默认网关所在网卡的当前 IP 。

  • SRC Port

    数据包的来源端口,也就是寄件人的快递箱编号。系统从当前空闲的端口分配里随机挑了一个 43235

    同时操作系统把这个端口号返回给浏览器,浏览器开始监听这个端口后续的数据包传入。向系统申请监听成功后,系统会把它标记为「正在使用」。未来这个「连接」的后续数据包都会发生在这个 43235 端口上。

    1
    2
    3
    4
    5
    6
    
    # 如何查看现有的监听呢?
    $ sudo ss -tulp
    # -t: TCP
    # -u: UDP
    # -l: 只显示正在 LISTEN 的
    # -p: 输出监听它的进程的信息 (不用 sudo 显示不完整)
    

现在数据包头长这样,这就是四元组了:

Address (IP) Port (TCP)
SRC (下详) 43235
DST 104.244.42.65 443

我们暂不关心数据包的内容。

为什么 SRC 很重要?有 DST 不就能发包了?

是的你能发,理论上让对方能收到的信息已经齐备。但

  • 我们讨论的是 TCP 连接,对方得回信的。如果你没填寄件地址,收件人该向哪儿回信呢?
  • 网络服务提供商(ISP)很可能会把 SRC 错误(没填或填了内网地址之类的)的包给丢弃掉,因为这个数据包大概率是配置错误了,转发它是浪费硬件资源。一个不指望回信的 TCP 包?常理来说不太可能(

先去哪儿呢

现在数据包在内核里了。内核首先根据当前的系统路由表决定它要被发往哪个网络设备。

以使用 ip 命令的 Linux 为例,在启动 sing-box 的 TUN inbound 之前,路由规则(路由表的表)长这样:

1
2
3
4
5
6
7
8
$ ip rule
0:	from all lookup local
5210:	from all fwmark 0x80000/0xff0000 lookup main
5230:	from all fwmark 0x80000/0xff0000 lookup default
5250:	from all fwmark 0x80000/0xff0000 unreachable
5270:	from all lookup 52
32766:	from all lookup main
32767:	from all lookup default

启动 sing-box 之后则变为这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ ip rule
0:	from all lookup local
5210:	from all fwmark 0x80000/0xff0000 lookup main
5230:	from all fwmark 0x80000/0xff0000 lookup default
5250:	from all fwmark 0x80000/0xff0000 unreachable
5270:	from all lookup 52
9000:	from all to 172.19.0.0/30 lookup 2022
9001:	from all lookup 2022 suppress_prefixlength 0
9002:	not from all dport 53 lookup main suppress_prefixlength 0
9002:	from all ipproto icmp goto 9010
9003:	not from all iif lo lookup 2022
9003:	from 0.0.0.0 iif lo lookup 2022
9003:	from 172.19.0.0/30 iif lo lookup 2022
9010:	from all nop
32766:	from all lookup main
32767:	from all lookup default

很容易猜出:

  • 编号越小优先级越高
  • fromto 指的是如何通过包里的四元组来做判断的。
  • 多出来的 90xxlookup 2022 应该就是 sing-box 的逻辑了。那 2022 究竟是啥呢?
1
2
$ ip route show table 2022
default dev tun0

就一句话,全导到 tun0 里了。果真如此么?我们可以做个测试,让系统告诉我们连接到某个 DST IP 时会怎么走:

1
2
3
$ ip route get 104.244.42.65
104.244.42.65 dev tun0 table 2022 src 172.19.0.0 uid 1000
  cache

啊哈,果然如此。顺便还告诉我们是第 2022 号路由表起作用了,以及系统会帮我们自动填写的 SRC IP 是什么 (172.19.0.0)。

其它系统一定有对标的工具,一定有。你现在知道这东西叫什么了,就知道怎么搜索了。

上面这个探针例子不精确。若想更精确地检测路由规则,你需要提供更多四元组信息:

1
$ ip route get to 104.244.42.65 ipproto tcp dport 443

想想你心中那个数据包的四元组是什么内容,你就知道这里条件该怎么写了。能用的条件都在 ip route get help 里。

现在数据包里的四元组长这样,并准备发给 tun0 设备了:

Address (IP) Port (TCP)
SRC 172.19.0.0 43235
DST 104.244.42.65 443

梯子介入了

tun0 是被你的 sing-box 梯子创建的虚拟设备。sing-box 进程接到这个数据包后,也分析了它的四元组,根据你的 JSON 配置里写的规则,决定了它该走哪个 outbound 被转发出去。

举个例子,根据你写的规则,这个包被转发到某个基于 TCP 的梯子 1.2.3.4:5555 上了,那么 sing-box 会构建一个全新的 TCP 包,把老 TCP 包整个加密,成为新数据包的内容。然后将新包转发到正确的网卡上,不失一般性地举例为 enp3s0 (IP 为 10.0.0.15 ) 。SRC 的 IP 和 Port 同样由系统告诉我们。下面是新包的四元组例子:

Address (IP) Port (TCP)
SRC 10.0.0.15 36823
DST 1.2.3.4 5555

sing-box 让系统不要查路由表直接把这个数据包转送给 enp3s0 。现在这个包到达 enp3s0 网卡的硬件缓冲区了,网卡把它发给了网关 10.0.0.1 ,也就是你家里的路由器。

为什么 sing-box 知道是 enp3s0 网卡?

  • 你可以在 JSON 配置里手动指定出口 .route.default_interface
  • 或者,你可以让 sing-box 查找系统路由表的默认出站规则对应的网口 .route.auto_detect_interface

为什么系统知道网关是 10.0.0.1

这是路由器 DHCP 给你的信息的一部分。可以在 ip route 里看到详情。 default via 开头的便是。

IPv6 虽然是全球唯一的 IP ,但你的数据包依然需要一个物理出口才能流出你家。 ip -6 route 可以看 IPv6 的数据包网关在哪里。

路由器收到了

你家的路由器收到了这个包,同样解析了里面的四元组,发现 SRC IP 和 DST IP 都不是它自己 10.0.0.1 ,于是走 FORWARD 那套防火墙规则。规则跑完后发现这个包得被转发给 PPPoE 网卡里。临走前,路由器又顺手改了一下你的包头,并且又随机映射了一个新端口。

假设你家有公网 IP 114.5.6.7 。现在包长这样:

Address (IP) Port (TCP)
SRC 114.5.6.7 32899
DST 1.2.3.4 5555

谁允许路由器动我包的!

上面这个修改法叫 NAT 网络地址转换,更精确地说是 masquerade 包伪装。试想如果路由器没有改 SRC,那对面 DST是没法「回信」的:SRC IP 是个内网地址 10.0.0.15 。见上「为什么 SRC 很重要?有 DST 不就能发包了?」。

当然,路由器会记下这个转换关系:下次如果我收到 DST PORT 为 32899 的,记住是我们内网里 10.0.0.15:36283 那位要的数据包,转发给他就好。

这时你应该懂了,路由器通过不同的 port 来区分这个数据包应该被转发到内网中的哪台机器,原因是从内网里发出的包都被路由器 masquerade 过。

NAT 有两种,对源地址的修改 srcnat / SNAT 和对目的地址的修改 dstnat / DNAT 。上面这个例子是 srcnat ,我们只修改了四元组里的 SRC 部分。

dstnat 最典型的例子是「公网开端口」:你家 NAS 10.0.0.25 里跑着一个 BT 下载,正在监听 TCP 端口号 37000 。你家路由器有公网 IP 如上,并配置好了将公网端口 41234 DNAT 给内网的 10.0.0.25:37000 。请在脑内试着模拟一下,一个数据包从 INTERNET 流入 NAS 的过程中,每一环节的四元组是什么。

「防火墙」?

防火墙这名字有点误导,我觉得「包过滤和转发规则」更合适。你应该听说过「三表五链」这个说法,结合上面四元组的知识,我简单地介绍一下。

三表:

  • filter :判断一个包能不能放行的规则
  • nat :要不要修改,以及如何修改一个包的 四元组 的规则
    • 注意 nat 只关心四元组
  • mangle :要不要修改,以及如何修改一个包的 内容 的规则

五链:

  • PREROUTING :所有下面这些规则的前置,数据包刚进入内核的一环
  • INPUTDST IP 为本机时生效
  • OUTPUTSRC IP 为本机时生效
  • FORWARDSRC IPDST IP 都不是本机时生效
    • 说明这个数据包流过本机但不停留,本机作为一个转手者存在。这种设备通常叫路由器。这也是为什么路由器里 FORWARD 的规则最多。
  • POSTROUTING :所有上面这些规则的后置,进入网卡之前的一环

我没公网 IP 啊

那就是多几个环节的 NAT。想象出了你家之后依然是一个 100.x.x.x 的「大局域网」,还要再做几个环节的 NAT 才能和公网 IP 通信。

互联网内

这个包现在到 ISP 的小区路由里了,ISP 根据你的四元组,不停地把包往下一个路由器传递。你的包经过了一跳又一跳,漂洋过海,绕了半个地球和一个大洋,终于到达了你的梯子服务器 1.2.3.4

怎么跳的?

traceroute (及其衍生工具 mtrnexttrace )能尽力帮你还原每一跳的细节。

运营商怎么定义往哪儿跳的呢?这就是 BGP 的范畴了,三篇博客说不完。

梯子内

你的服务器 1.2.3.4 跑了梯子的服务端,正在监听 TCP 5555 端口。一个闲来无事的下午,它突然收到了一个数据包:

Address (IP) Port (TCP)
SRC 114.5.6.7 32899
DST 1.2.3.4 5555

它用梯子协议解密了包内容,还原出了真正的「原版」数据包:

Address (IP) Port (TCP)
SRC 172.19.0.0 43235
DST 104.244.42.65 443

梯子服务端用这个数据包请求了 104.244.42.65:443 (当然顺手 srcnat 了一下,要不然收不到回信了),拿到了回复:

Address (IP) Port (TCP)
SRC 104.244.42.65 443
DST 1.2.3.4 无所谓

以及一个新的返回包的内容。

好吧,服务端任务完成了,是时候回复给梯子客户端了。服务端把这个返回包也用梯子协议加密了后,把一开始收到的包的四元组反转了一下,作为新包的头:

Address (IP) Port (TCP)
SRC 1.2.3.4 5555
DST 114.5.6.7 32899

再把它扔进茫茫的互联网里。

后日谈

剩下的就是上面流程的反转了。因为每一步 NAT 都有记录,该答复的对象都是会收到答复的。

最后的最后,浏览器收到了客户端解密后的包:

Address (IP) Port (TCP)
SRC 104.244.42.65 443
DST 172.19.0.0 43235

看上去那么地美好,就像那啥不存在一样。

补充

其它暂时想不到了。留言催更我。

这回可以说说 OSI 七层模型了吧

确实,此时可以介绍下 OSI 的主要几层了:

  • 1 物理层:双绞线、RJ45、光纤之类的
  • 2 数据链路层:MAC 地址是这一层的概念
  • 3 网络层:IP、ICMP(你最熟悉的 ping 就是)、IGMP(运营商的 IPTV 用的就是这个协议发送直播 URL 的)
  • 4 传输层:最常见的是 TCP 和 UDP。
  • 7 应用层:HTTP、SSH、IMAP、SMTP、NTP……

TCP/IP 的意思是,建立在 IP 网络层之上的 TCP 协议。

注意 TCP 和 UDP 是相同地位的不同种东西,所以 TCP 端口号和 UDP 端口号是不一样的(虽然他们都是 65535 个)。上面表格里的 Port 我特地标注了是 TCP 的。

今后上淘宝看到「二层交换机」、「三层交换机」、「七层交换机」,你就知道他们的能力范围了。

上层应用使用什么下层技术不一定有绑定关系,比如 HTTP 用的 TCP,而 http3 (quic) 是 UDP 。

Fake IP 是什么

非常麻烦的一环是如何拿到 twitter.com 的正确 IP 地址(以使得 sing-box 里基于 IP 的规则能生效)。

然后社区突然想到,其实应用(比如浏览器)只是要一个 IP 而已,至于这个 IP 最后真的发到哪个服务器,完全可以让服务端来判断。

加密后的数据包里已经有明确清晰的四元组了,并且和里面「真正」的包的四元组完全无关。拉上去再看看,想想对不对。

服务端那里倒是有信息判断原版请求的:对HTTP,明文的,看header的 Host: 就行;对 HTTPS,SNI 握手环节也是有明文的域名的。其它协议暂不考虑了。

客户端只需要收结果就好了,而服务端要考虑的就有很多了。

此时 FakeIP 就诞生了:随便生成一个 IP ,make 应用 happy 。

你应该能看懂一丢丢 wireshark 了

点开一个包的详情,你就能看到

  • 第一层:从哪个网络设备收到
  • 第二层:收发该包的网卡 MAC 地址
  • 第三层:包的源 IP 和目标 IP
  • 第四层:TCP 的源端口号和目标端口号

接下来 filter 一个包你应该也心中有数了,只要想象一下你要抓的包的四元组长什么样,然后让 ChatGPT 告诉你就行。