讲一下我自己给自己搭建的网络环境吧,好友很久之前就劝我写了,但那时候工作生活乐趣无穷,没心思写博客,反倒最近烦心事多了,想找点东西换换脑子转移注意力,就跑来写博客了。其实我最早开始玩网络,是因为被学校无端指责我用P2P软件,我去跟他们理论无果,他们拒绝向我提供任何的报告,而我也拒绝向他们认错,于是他们把我家里网“封锁”了,而我在寻求一种穿透他们的方法。经过不断的尝试与探索,我自己的网络结构也演化了好几代,反倒变得比被学校断网之前方便了好多,也引入了IPv6的支持,这样我就可以随时用手机访问自己家里任何设备的任何端口了。

我之所以搭建自己的网络环境,主要是为了提供如下的功能:

  • 把我在世界各地的局域网连接到一起去,让他们互相之间能够访问到,并且为这些网络提供可路由的公网IPv6地址。
  • 突破一些封锁,比如学校的DHNet给我的断网,以及天朝的GFW。
  • 在外面的便携设备,可以随时连接进来,获得可路由的公网IPv6地址。

网络结构的设计

网络采用中心化了的设计,示意图见下面。 一台位于Linode的主机充当了核心的管理节点。这个核心节点维护一个网桥,所有的子网或者设备想要加入,都是通过GRETAP桥接进这个网桥来实现的。如果是单个设备接入的话,那么这台设备直接通过GRETAP接入就好。如果是一个子网想要接入的话,除了建立GRETAP以外,还需要将其跟子网桥接起来。 网络的结构

GRE协议本身无法穿透NAT,而不幸的是,由于IPv4地址的枯竭,现在的运营商很少会给你公网的IPv4地址了。PPTP改版过的GRE协议,是可以透过NAT的,然而笔者费尽周折,都没找到如何在Linux上抛开PPTP单用其GRE。为了解决NAT的问题,有两种解决方案,一种是通过Generic UDP Encapsulation(GUE)来封装GRE包,另一种则是新建一个VPN链接,然后让GRE包直接通过VPN链接跟核心节点相连。

对于新建VPN连接的方案,笔者强烈推荐Wireguard。之所以推荐Wireguard,首先是因为它配置相当简单,而且工具链符合Unix哲学:传统的VPN软件,在连接上VPN的时候,会自动改你的路由表、DNS设置等等,让你的一切连接都走VPN;而对于那些移动设备(办公室的电脑、随身携带的笔记本),我新建VPN只是希望能够提供一个虚拟通道,实现到核心节点的免NAT访问而已,我并不需要修改IPv4流量原来的走向。而且,Wireguard对移动设备非常友好,自带漫游功能,笔记本在一个地方连上Wireguard,然后带走去别的地方,连接上不同的网络,Wireguard也不会中断,而是自动漫游过去。

GRETAP跟网桥都设置好了以后,所有的设备之间就可以在layer2互相访问了,真正要使用的话,再处理好IP地址就好。IPv4的地址跟IPv6的地址的处理方式非常不一样。IPv6的设置异常简单,由于地址空间充足,我直接跟Linode要了个/64的网段,这个网段是我可以自己随意使用的,发往这个前缀下的任何IP地址的包,Linode都会帮我路由到我的那个核心节点去,这样我的核心节点只需要帮忙转发即可。由于layer2已经能互相访问了,我只需要在核心节点设置好radvd向网桥发送Router Advertisement来广播自己的前缀就好,设备收听到了Router Advertisement,就会自动设置好自己的IPv6地址跟路由信息。

IPv4的地址的配置则要复杂很多,由于IPv4地址空间太少,到处都是负责分配私有地址的DHCP服务器,数据包想要去Internet,也得NAT了才能上路。我这里采用半手动的管理的策略,我选择了192.168.x.y这个网段作为整个大网络的网段,对这个网段再进一步划分,想要连接进来的不同的子网,被人为设置了不同的x值,这些子网的路由器的DHCP服务器被设置了/24的前缀长度,也就是路由器只会分配不同的y值。核心节点我就给了它一个192.168.0.1这个地址,而各个子网,都人为选取不同的非零的x值。在每个子网的路由器上,把子网的大小设置为/16,但是DHCP服务器的前缀长度仍然保持/24,这样,子网内的设备想要访问其他子网的时候,包先会被路由到路由器中,然后路由器通过ARP映射表找到相应目标的Mac地址,然后通过layer2直接把包送到目标。如果想要翻墙,可以额外添加针对GeoIP的路由。如果想要整个子网都用核心节点当出口,那么只需要在路由器的路由表中,把核心节点的公网IP的路由人为标记为从WAN的interface出,然后把网关设置为192.168.0.1即可。

核心节点的配置

按照我们设计的网络结构,核心的配置主要需要做这样一些事情:

  • 给一些想要通过Wireguard连接进来的主机建立Wireguard连接
  • 给所有连接进来的设备或者子网设置GRETAP
  • 设置一个网桥,把所有这一大堆GRETAP给桥接到一起

Talk is cheap,我们直接来看code。笔者的所有操作系统都是Archlinux。公网的出口是eth0

先来设置好变量:

SERVER_IP=1.2.3.4  # 保护隐私,此处隐去真实地址
SERVER_IP6=2001:db8::1  # 保护隐私,此处隐去真实地址
SERVER_LOCAL_IP=192.168.0.1
INTERFACE=eth0

网络基础设置的配置

然后就是启用IP Forward、Masquerade以及跟TCPMSS了,这些都是玩隧道跟软路由的常识了:

sysctl -w net.ipv4.conf.all.forwarding=1
sysctl -w net.ipv6.conf.all.forwarding=1
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
iptables -t mangle -A POSTROUTING -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
ip6tables -t mangle -A POSTROUTING -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

然后就是设置好bridge:

ip link add taps_bridge type bridge
ip link set taps_bridge up
ip address add $SERVER_IP6/64 dev taps_bridge
ip address add $SERVER_LOCAL_IP/16 dev taps_bridge

接下来就是配置radvd,先来看配置文件/etc/radvd.conf

interface taps_bridge
{
	AdvSendAdvert on;

	prefix 2001:db8::/64
	{
		AdvOnLink on;
		AdvAutonomous on;
		AdvRouterAddr on;
	};

	RDNSS 2001:4860:4860:0:0:0:0:8888
	{
	};
};

上面的DNS服务器用的是Google Public DNS。写好配置文件以后,就可以开启radvd了:

systemctl start radvd.service

GRE over GUE的配置

接下来设置GUE封装的GRE的连接,每个连接都要来上这么一段:

FOU_PORT=5555
REMOTE_PUBLIC_IP=2.3.4.5  # 保护隐私,此处隐去真实地址
REMOTE_PUBLIC_PORT=5555

modprobe fou
ip fou add port $FOU_PORT gue

ip link add name gretap_over_gue type gretap \
    remote $REMOTE_PUBLIC_IP local $SERVER_IP \
    ttl 225 encap gue encap-sport $FOU_PORT encap-dport $REMOTE_PUBLIC_PORT
ip link set gretap_over_gue up
ip link set gretap_over_gue master taps_bridge

上面的FOU_PORT是GUE接收端用的UDP端口号,关于这方面的更好的介绍,可以参见lwn.net对Foo over UDP的介绍REMOTE_PUBLIC_IPREMOTE_PUBLIC_PORT分别是要连接到核心节点的客户端的公网IP跟NAT转换后的端口。

Wireguard的配置

这里我们使用Wireguard作为我们的VPN解决方案。这里Wireguard的作用,仅仅是给GRETAP提供一个能够穿透NAT的皮而已,所有的上层流量都是封装进GRETAP里面去的,不直接走Wireguard。所以Wireguard的配置上,只需要设置好IP地址就好,不需要复杂的路由表设置。在Wireguard的interface的配置上,我们把核心节点设置成172.16.0.1,其他的设备分别赋予172.16.0.2172.16.0.3、……。

Wireguard的配置教程参见官网. 这里我们的wireguard.conf文件如下:

[Interface]
PrivateKey = aAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAa=  # 此处为了隐私略去真实私钥
#PublicKey = JFcFDUEB0ZUVhUhfYPNT8Ma9FoeMVf2x03CYFyE5jTg=
ListenPort = 23450

# 每个要连接进来的节点都要有一个[Peer]条目

[Peer]
# office desktop
PublicKey = 8TYMD/dmDLgbAp1VSvhemGEe6e8Yr/7GGjRCAce6ZGA=
AllowedIPs = 172.16.0.2/32

[Peer]
# Macbook Pro
PublicKey = fgZdAIh6/KIzpTYZtjtirOKPAujfmCqumPb+F7XGzmM=
AllowedIPs = 172.16.0.3/32

[Peer]
# Yichen
PublicKey = uYQJAsHtvClr2gnnUdMRZyzhTv6N9lY5L9ASZLW+AQM=
AllowedIPs = 172.16.0.4/32

有了配置文件,接下来就是配置Wireguard的interface了:

ip link add dev wg_vpns type wireguard
wg setconf wg_vpns wireguard.conf
ip link set up dev wg_vpns
ip addr add 172.16.0.1/24 dev wg_vpns

GRE in Wireguard的配置

Wireguard已经实现了一个新的子网,直接利用新的子网内的IP地址,建立GRE就行:

下面几行命令,需要在核心节点上执行多次,每次分别设置不同的主机。每台Wireguard连接进来的主机都需要在核心节点上按照下面的方法设置。下面仅以我的办公室的电脑为例:

# gretap for office desktop
ip link add name gretap_office type gretap \
    remote 172.16.0.2 local 172.16.0.1 ttl 225
ip link set gretap_office up
ip link set gretap_office master taps_bridge

至此,核心节点的配置就已经结束了。

客户节点的配置

客户节点的配置要比核心节点简单很多,只需要建立GRETAP即可。取决于不同的情况,可能还需要建立网桥跟Wireguard。

GRE over GUE客户节点的配置

笔者的GRE over GUE主要用在家里,用于把家里的子网跟核心节点桥接起来。笔者用来做这件事情的设备,是一台位于子网中的刷了openwrt的家用路由器。这台路由器关闭了DHCP服务器、Masquerade等家用路由器中标准的功能,IP地址固定在192.168.88.6,然后运行了一个名叫br-lan的网桥,把所有的有线跟无线网桥接起来(家用无线路由器默认都是会有网桥的,不需要人为启用啥)。

首先需要做的是建立GRE:

SERVER_IP=1.2.3.4  # 保护隐私,此处隐去真实地址
LOCAL_IP=192.168.88.6
FOU_PORT=5555

modprobe fou
ip fou add port $FOU_PORT gue
ip link add name gretap_over_gue type gretap \
    remote $SERVER_IP local $LOCAL_IP \
    ttl 225 encap gue encap-sport $FOU_PORT encap-dport $FOU_PORT
ip link set gretap_over_gue up

然后把GRE添加到已有的网桥中即可:

ip link set gretap_over_gue master br-lan

GRE in Wireguard客户节点的配置

首先是配置文件wireguard.conf:

[Interface]
PrivateKey = bBbBbBbBbBbBbBbBbBbBbBbBbBbBbBbBbBbBbBbBbBb=  # 此处为了隐私略去真实私钥
#PublicKey = 8TYMD/dmDLgbAp1VSvhemGEe6e8Yr/7GGjRCAce6ZGA=
ListenPort = 3243

[Peer]
PublicKey = JFcFDUEB0ZUVhUhfYPNT8Ma9FoeMVf2x03CYFyE5jTg=
AllowedIPs = 0.0.0.0/0
EndPoint = 1.2.3.4:23450

然后建立Wireguard连接:

ip link add dev wg_linode type wireguard
wg setconf wg_linode wireguard.conf
ip link set up dev wg_linode
ip addr add 172.16.0.2/24 dev wg_linode

然后就是建立GRETAP:

ip link add name gretap_linode address 12🆎34💿56:ef type gretap \
    remote 172.16.0.1 local 172.16.0.2 ttl 225
ip link set gretap_linode up
ethtool --offload gretap_linode tx off

这里面我人为指定了GRETAP的Mac地址,如果不这么做的话,Linux就会给你随机分配一个,这样就导致每次连接的Mac地址都不一样。而IPv6地址则是根据Mac地址通过EUI-64来自动配置的,我需要一个固定的IPv6地址,这样我就可以给这个地址设置域名,然后从其他地方随时访问这个地址。

另外,我手动关闭了GRETAP的interface的offload,我也不知道这是不是个内核Bug,但是如果不关的话,会出现校验和没人算的尴尬场面,进而导致所有的TCP包都出错。

在Vultr上的尝试

劝我写本文的这位朋友,也在自己的Vultr主机上尝试实现了我的这种网络,来给他的移动设备提供IPv6地址。大的方法基本上跟这里无差,只不过不一样的是,Linode给你/64网段的方法,是在他们的路由表里面,把所有到这个网段的包全都路由到你的主机上去。而Vultr的IPv6,上来就给你的是一个/64的网段,你在Vultr的主机本身也只是监听Router Advertisement来自动配置IPv6地址而已。Vultr那边,不会帮你把到那个地址的所有包全都自动路由到你的主机,而是你得自己去响应Vultr的路由器的IPv6 Neighbor Discovery这种东西,让Vultr知道你的存在,他的路由器才会理你。这种设计,其实更简单了:我们不再需要自己的radvd了,只需要把你的interface跟GRE的那一堆interface桥接到一起就好,Vultr的网络设施,就会自动给你的网络发送Router Advertisement,而你也是有事直接找Vultr,你自己的主机需要做的事情很少。这实际上,相当于Vultr帮你做了一部分工作。具体的配制方法,也很简单,在配置核心节点的那一步,略去radvd的配置,然后设置bridge的时候,把你主机的出口interface也一并加到bridge里面来即可。