使用 GitHub Actions 科学上网

2022-08-04 ⏳5.9分钟(2.4千字) 🕸️

本文介绍一种科学上网的思路,基于 GitHub Actions 服务,可以流畅观看 YouTube 4k 视频,不限流量,完全免费!

GitHub Actions 是微软为开发者提供的免费构建工具。每当有任务需要运行时,GitHub 会在在 Azure 云创建虚拟机(2C4G)。这台虚拟机可以访问公网,但它没有公网IP,所以无法从外界直接访问。如果能找到一种从外界访问的方法,我们就可以把 GitHub Actions 虚拟机当成普通的VPS来用,从而实现科学上网。这个办法就是NAT打洞🕳️,也叫NAT穿透。

假设整个网络拓扑如下:

                     /- 1.1.1.1               /- 10.0.0.1
      Client <---> C-NAT <------> S-NAT <---> Server
10.1.0.1 -/              2.2.2.2 -/

服务器(Server)就是前面说的 GitHub Actions 虚拟机。Azure 为所有虚拟机提供地址转换功能,我们称之为 S-NAT。我们自己的设备为客户端(Client)。一般家用设备或者办公设备都会通过路由器上网,同样也提供地址转换功能,我们称之为 C-NAT。

以上是最典型的架构。如果你的设备有公网IP,则会简化 NAT 打洞的难度。我们先说典型场景。因为 NAT 打洞通常只面向 UDP 通信,所以下文所说的报文如非说明都是 UDP 数据。

S-NAT 的作用是帮助 Server 访问外网。具体来说就是修改所有来自 Server 的 IP 报文,把它的源地址改成 S-NAT 的公网IP,并把源端口改为按照某个特定策略分配的数字。举个例子。

假设 Server 想给 3.3.3.3 的 80 端口发送数据,则从 Server 端看到的数据流是

10.0.0.1:11111 -> 3.3.3.3:80

这里的 11111 是 Server 的本机端口,一般是随机选取,也可以手工指定。取值不重要,重要的是数据通信需要四元组<源地址,源端口,目标地址,目标端口>

显然,源地址为私有地址,无法在公网上路由。Server 发出的报文被转发给 S-NAT。S-NAT 收到后发现是发给外网的数据,于是把报文的源地址改成了 2.2.2.2。然后根据既定策略选择了一个端口,比如 22222 作为报文的源端口。于是数据流变成:

10.0.0.1:11111 -> 2.2.2.2:22222 -> 3.3.3.3:80

3.3.3.3 收到的数据看起来就是从 2.2.2.2:22222 发出来的。它并不知道数据是由 Server 发出的。于是 3.3.3.3 开始回复数据:

3.3.3.3:80 -> 2.2.2.2:22222

S-NAT 收到数据后将目标地址改成 10.0.0.1,并将目标端口改成 11111,然后再发给 Server。

可是,当 S-NAT 收到来自 3.3.3.3 的报文之后,它是怎么确定数据要转发给 Server 呢?S-NAT 后面可能有很多设备。为此,S-NAT 会维护一个列表,临时保存内网设备跟本地端口的映射关系。因为设备容量有限,NAT 设备会定时清理长时间不活跃的映射关系。

以上面场景为例,映射关系如下:

2.2.2.2:22222 -> 10.0.0.1:11111

S-NAT 每完成一次地址和端口转换,就记录一条反向的关联映射。这样当收到外网报文时,只要根据目标地址和目标端口就能查到对应的内网设备。因为来自 2.2.2.2:22222 数据目标一定是 10.0.0.1:11111,所以 S-NAT 就会转发给 10.0.0.1:11111。

到这里,我们就理解了 NAT 的工作原理。如果可以让 S-NAT 为 Server 添加一条映射关系,那么就有可能实现从公网访问 Server。这是 NAT 打洞的核心原理。

为什么是有可能呢?因为 NAT 设备地址转换的时候有多种端口选择策略,有的能打洞,有的不能。就 Azure 云的 NAT 设备来说,它有如下特性:

  1. Random port
  2. Independent Mapping
  3. Port Dependent Filter

Random port 的意思是在地址转换的时候会随机选择端口。也就是说我们没办法在发数据之前确定 NAT 使用的端口。

Independent Mapping 的意思是映射规则跟目标地址和目标端口无关,只源地址和源端口有关。也就是说,如果 Server 分别向 3.3.3.3:80 和 4.4.4.4:443 发送数据,使用的的本地端口都是 11111:

10.0.0.1:11111 -> 3.3.3.3:80
10.0.0.1:11111 -> 4.4.4.4:443

那么 S-NAT 只会保存一条反向映射关系:

2.2.2.2:22222 -> 10.0.0.1:11111

不论从什么地方发来的数据,只要目标是 2.2.2.2:22222,都会被转发给 10.0.0.1:1111。这就让 NAT 穿透成为可能。

假如 Server 想让外界访问自己的 11111 端口,它需要找一台有网IP的设备当作协调服务器。Server 给协调服务器的某端口发一条消息,协调服务器收到后就会得到 S-NAT 的公网 IP 以及 S-NAT 为 Server 的 11111 端口分配的映射端口(比如 22222)。协议服务器需要把这些信息再返回给 Server。到现在,就完成了 NAT 打洞的第一步。Server 可以把收到的 IP 和端口发给别人。收到的设备只要给 2.2.2.2:22222 发包,这些包就会转发给 Server 的 11111 端口。

这里说的协议服务器在现实中叫作 STUN 服务器。其最主要的作用就是帮助 NAT 后面的设备发现自己对应的公网 IP 和端口。

那么 NAT 打洞是不是就完成了呢?非也。因为我们还没说 Azure 地址转换的第二个特性 Port Dependent Filter。这是一条防火墙规则。简单来说就是需要内网设备先发数据,然后才允许外网设备回复数据。再举个例子。

4.4.4.4:33333 -> 2.2.2.2:22222 # 丢弃

2.2.2.2:22222 -> 4.4.4.4:33333
4.4.4.4:33333 -> 2.2.2.2:22222 # 接收

如果 S-NAT 直接收到来自 4.4.4.4:33333 的报文,它会认为这是非法数据,直接丢弃。如果 Server 先通过 2.2.2.2:2222 给 4.4.4.4:33333 发送一条数据,那么 S-NAT 就会记录该事件,并允许在一定时间内接收来自 4.4.4.4:33333 的数据。该记录通常只会保存三十秒。

所以,为了让来自 4.4.4.4:33333 的报文顺利通过 NAT 设备转发给 10.0.0.1:11111,我们需要让 Server 先发送一条数据给 4.4.4.4:33333。因为 S-NAT 的访问记录很快就会过期,所以需要 Sever 周期性发送数据。这可以使用 nping 实现:

sudo nping --udp \
           --ttl 4 \
	   --count 20 \
	   --delay 28s \
           --no-capture \
	   --source-port 443 \
           --dest-port 33333 4.4.4.4

这里比较有意思的是--ttl 4参数,把数据的 TTL 设置为四跳。数据每经过一个路由器后 TTL 都会减一。这样数据离开 S-NAT 后很快就会因为 TTL 达到零而被丢弃。这样数据就不会真正发送到目标设备。

还有一个需要注意的重点是,如果 Client 想要连接 Server,它必须事先确定自己的IP和端口。因为只有这样,Sever 才好设置 S-NAT 为来自 Client 数据放行。

但是因为公网 IP 比较稀缺,Client 一般也是通过 NAT 访问互联网。所以 C-NAT 也会做跟 S-NAT 一样的工作。但是家用的 C-NAT 跟公司商用的 C-NAT 有很大的区别。

家用 NAT 设备的端口映射规则比较简单,一般是跟报文的源端口保持一致。也就是说来自 10.1.0.1:1024 的数据一般也会被 C-NAT 映射成 1.1.1.1:1024。这样我们只需要确定 C-NAT 的公网 IP 就可以了。如果设备不用这样的规则,那我们还可以使用 UPnP 手工指定端口映射。

以上是说家用设备有公网 IP 的情况。如果没有,那家用跟公司的商用设备就没什么区别了。

现在进行技术总结:

整个过程被 ValdikSS 封装成一个开源项目。大家可以自由食用。食用办法如下:

到这里我们便得到了一台 VPS,配置为2C4G,网速不慢。最简单的科学上网办法是使用 ssh 转发:

ssh -D 1337 -q -C -N root@192.168.166.1

这样就会在 127.0.0.1:1337 启动一个 sock5 代理。修改浏览器代理配置后就可以畅游世界了👏。

这么好的东西有没有缺点呢?当然有。

但无论如何,做为一种应急梯子🪜还是绰绰有余的🎉另外 ValdikSS 的脑洞是真的大!