为IPsec VPN设置在中国大陆的中转节点

要点:
– 以下这种思路是不对的:服务器A是出口服务器,然后服务器B是中转服务器,服务器B用VPN客户端的配置去连接服务器A,然后服务器B也配置VPN服务端连接,服务器B就能当中转节点用了。
– 注意主服务器和中转节点的NAT和防火墙的设置,用来中转的服务器的VPN配置不能加NAT,NAT的操作统一在出口服务器进行。
– VPN的用途是在不安全的互联网上建立一条加密的通道,使得远程工作的员工能安全地访问公司内网的资源不被窃听。VPN不是用来翻墙的,拿VPN用来翻墙,等同于每天开着半挂车通勤上班,生怕别人不知道你要装逼。
– 给懒人复制粘贴准备的配置文件在文末。

为什么要折腾VPN

我又来折腾因特网安全协议了,视无数比IPsec VPN好的爬墙方式为无物。

从2017年到2024年,托openSUSE贡献者MargueriteSu的福,我用IPsec VPN翻墙翻了六年,竟然几乎从来没有被封死过。也许这就是GreatFire.org所说的 “依附的自由” ,因为IPsec服务在跨国企业内网互联等用途上应用的如此广泛,即使这个协议的特征明显的不能再明显,我也依然能用到现在。至于能用到什么时候,可能得等到CCP彻底和外资撕破脸皮或者外贸创汇这条路完全堵死或者国内大乱。

不管怎么说,2024年了,我的梯子还活着,甚至还用它来玩游戏。这篇文章的诞生也和玩游戏不无关系。本人长期沉迷MapleStory,也就是国际服冒险岛。这段时间找了个国服的老朋友帮忙代打,但是他连我用来玩游戏的加速器(梯子)延迟不美丽。在找不到三大运营商通吃的国外服务器的情况下,我想到了这个歪招:找个国内的延迟不错的服务器,搭上VPN服务端,之后透过某种方式把流量全都加密转发到国外的服务器上。这个服务器在国内的延迟有保证,连接国外的服务器也是直连,这样延迟就通过走指定路径的VPN隧道变相降低了。也就是所谓的中转节点:连上国内的VPN服务,实际上打开网页查询当前IP是在国外用来爬墙的IP。

有那么多先进的翻墙协议为什么我却视而不见呢?原因是:先进的翻墙软件在本地的通联方式,一般都是本机启动一个应用层的http或者socks5代理服务器,网游运营商们都在想尽办法锁区,你指望他支持通过本机代理连接游戏服务器,无异于劝老虎下半辈子吃素。只有在网络层上转发数据的VPN,才能帮我欺骗网游客户端,让它认为我的的IP在服务区域内,我才能成功地玩上MapleStory这个垃圾游戏。哦所以为啥我不去找能把socks5服务器包装成虚拟网卡的东西?免费的SSTap停更了;Proxifier我曾经买过,新版本下了试用版好像也能用,但是要求我再掏一次钱,还没有升级优惠,这就非常的不厚道。所以我最后的选择还是操作系统内建的VPN客户端。

“错误”的中转服务器的搭建思路

给VPN做中转节点,有一个想当然的思路实际上是行不通的。假设服务器A是翻墙用的VPS,服务器B用来中转,然后都是提供的IPsec VPN服务;现在服务器B要变成服务器A的傀儡,于是你在服务器B上用NetworkManager提供的IPsec VPN配置模板,把服务器B作为VPN客户端连接上服务器A,这样就实现了“中转”。当你照这个思路配置完了,然后服务器B去连接A,你会发现服务器B确实是连上了服务器A,但是和服务器B建立的SSH连接也断了,你也连不上服务器B提供的VPN服务。你不得不登上云服务器网站,透过服务商提供的VNC连接把VPN关掉。

为什么这个路子行不通?先考虑VPN客户端和服务器之间连接的场景。

VPN客户端可以看作是在内核上设立的一个检查站,这个检查站会检查所有的入站和出站的IP包,本机IP或者对端IP满足了“检查站”设置的规则,这个IP包就会被分流到IPsec隧道。这个规则是通过swanctl.conf里的local_tsremote_ts来设置的。假设有个美帝的公司内网的网段是172.16.0.0/16且提供了IPsec VPN服务,中国出差的员工Carol用strongSwan作为IPsec VPN客户端连接到公司内网,Carol把remote_ts的值设置成172.16.0.0/16用这个配置连上了公司的VPN,此时Carol电脑上的VPN客户端就会分流所有目标IP落在172.16.0.0/16的出站IP包和源IP落在172.16.0.0/16的入站IP包,把这些IP包都引导到IPsec隧道这条路上(把整个IP包加密封装成ESP包再加个新的IP头发给VPN服务器),Carol便可以远程到自己工位上的电脑继续未完成的工作,至于不满足这个规则的其它IP包则保持原来的样子不变。

再来说翻墙的用途,Carol想摸鱼看YouTube,因此Carol想到了白嫖公司的VPN。她可以把remote_ts的值设成0.0.0.0/0,这个时候在VPN客户端的指引下Carol电脑所有的IP包都必须走IPsec隧道,直连互联网的路出入两个方向都被VPN客户端设置的规则封死了。此时有人ping Carol的电脑,由于VPN客户端设置了只有通过IPsec隧道传入传出的IP包才能放行,ping过来的ICMP包不符合这个规则,所以内核必须把这些包丢掉。把Carol的电脑换成用于中转的服务器B,道理是一样的,VPN连接建立以后,连过来的TCP包ISAKMP包都不符合VPN客户端半路插进去的规则,全都要被丢掉,服务器B对外的通信自然也都会中断。

打个政治不正确的比喻,这里(基于策略的)的VPN,就像通往圣城麦加的公路上的路牌。信仰真主的请直行进入IPsec隧道,不信的请靠右行驶进入匝道,通往互联网方向。

在墙外进行的组网实验

我恶补了读大学时学的网络安全课本,又简单看了strongSwan的测试用例。课本明确说了在互联网上安全地互联多个区域的内网就是IPsec的应用场景之一。strongSwan提供的net-net测试用例,讲的也是这个场景下如何配置VPN。

在真正用国内国外两个服务器实现里通外国之前,我用了墙外自己买的两只小鸡(vps)试着实现这个中转的配置。假设两个小鸡的名字分别叫moon和sun,和net-net测试用例保持一致,且moon和sun都已经预先配置好了IPsec VPN服务。现在预期达到的目标是:moon的VPN服务和之前一样正常提供;计算机连接sun也和以前一样,但是连接互联网的流量会通过两台服务器建立的IPsec隧道中转到moon上,统一由moon来处理。这时连接sun的计算机,访问ipip.net查自己的IP应该是moon的IP而不是sun的。我把科班知识都还给了老师了,这个简单的需求,竟然花掉了我两周多的时间。因此这个配置过程需要记录下来作为备忘,不然以后又要踩坑了。然后写这篇烂文又耗掉了快一个月。

strongSwan的测试用例net2net-psk

我有点懒,不想为签发pki证书输那一大坨openssl命令,所以VPN网关之间互联使用的认证方式是预共享密钥。

按照测试用例设定的场景,网关服务器moon和sun背后的电脑需要能互相访问对方网关背后的电脑。把umlswitch0想象成互联网,moon的IP是192.168.0.1,sun的IP是192.168.0.2。成功建立点对点连接后,alice ping bob的IP地址,应该能收到bob的回复;bob ping alice也是一样。这种情况就表示测试通过。

按照测试用例的指示,网关moon和sun的swanctl.conf文件都需要加上这样的一条VPN连接配置。以网关moon为例:

    gw-gw {
        local_addrs  = 192.168.0.1 # moon 的IP,也可以填域名。网关sun的配置和remote_addrs对调
        remote_addrs = 192.168.0.2 # sun 的IP,网关sun的配置和local_addrs对调

        local {
            auth = psk
            id = moon.strongswan.org # 用于验证的预共享密钥ID,实际上可以和remote的那个ID共用一个
        }
        remote {
            auth = psk
            id = sun.strongswan.org # 用于验证的预共享密钥ID
        }
        children {
            net-net {
                local_ts  = 10.0.0.0/24 # moon给VPN客户端的虚拟IP地址,sun的配置和remote_ts对调
                remote_ts = 10.0.1.0/24 # sun给VPN客户端的虚拟IP地址,sun的配置和local_ts对调

                rekey_time = 5400
                rekey_bytes = 500000000
                rekey_packets = 1000000
                esp_proposals = aes128gcm128-x25519
            }
        }
        version = 2
        mobike = no
        reauth_time = 10800
        proposals = aes128-sha256-x25519
    }

# secrets区域要设置两个网关之间验证用的预共享密钥,moon和sun的密钥必须一样才能验证成功
secrets {
    ike_gw {
        id-3a = moon.strongswan.org
        id-3b = sun.strongswan.org
        secret = fSbIdAQ4sEe11D20D/vUR1xvtdYg3JucgwIdiqq7gtC1tNXeXPcJrJhUFJJh
    }
}

我把两个小鸡的VPN连接都仿照测试用例加好了配置,因为之前配置都是ctrl+cv微调的,给客户端的虚拟IP网段都一样,为了避免虚拟IP网段冲突还把其中一个服务器的虚拟IP段改了一下。心想把strongSwan重启一下应该就万事大吉了:我分别用电脑和手机连接moon和sun,之后在手机上ping电脑的虚拟IP,能收到电脑的回应,这样中转节点的事情应该就完事了。然而事实证明我实在是太幼稚了。

先讨论两个VPN服务器后面的计算机互相ping通的问题。ping自然是不可能ping通的,不然我也不会说我太幼稚了。既然自己亲自测试不通过,那就试着找一下自己的环境和测试用例比有哪些地方不一样。找到的第一个不一样的地方,是我的服务器里输入ip route list table 220命令查看IPsec路由表没有任何输出,但是测试用例是有路由表的。没有路由那就尝试加一个!我在服务器moon的终端输入了下面的命令尝试加一条路由直接指向sun,但是服务器不听我的,只会回过来Nexthop has invalid gateway的错误:

ip route add 10.0.1.0/24 via <服务器sun的IP> dev eth0 proto static table 220

看来IPsec的路由表不能这么玩,那么这个网关对网关的连接有没有随着重启strongSwan建立起来呢?输入swanctl --list-sas,一条有关gw-gw的输出都没有,IPsec隧道都没打通那ping通当然是天方夜谭。再看一下测试用例是怎么建立隧道的,看了网页给的console.log才发现,这个隧道需要手动输入命令才能开启:swanctl --initiate --child net-net,在服务器moon上输入这条命令,输出显示这个VPN连接已经建立成功了。

网关对网关的连接打通了,处在两个VPN网关后面两个计算机之间就能ping通了吗?拿衣服!还是不行。不行那就继续玩找不同的游戏,很快又能找到一个不一样的地方:我的VPN配置里网关服务器本身没有虚拟IP地址,服务器moon把10.1.0.1分配给了第一个连过来的客户端,而不是留给自己。为了给VPN服务器也配一个虚拟IP地址,我又股沟了strongSwan的文档,被强行科普了基于策略的IPsec VPN和基于路由的VPN有啥区别。简单来说,VPN配置里想给VPN网关也留一个虚拟IP地址,就需要把VPN配置成基于路由的,而我之前配置的VPN都是基于策略的。路由模式下,面向客户端的VPN连接会绑定在一个虚拟网卡上,方便网管手工写路由表更细致地处理分流规则。相反基于策略的VPN只会看设置的网段,把本地和远端网段都符合规则的IP包强行分流到通往IPsec隧道的路上。文档还说了为配置基于路由的VPN,Linux内核支持VTI接口,然后XFRM接口是替代VTI的墙裂推荐XFRM等等。我把这个路由模式当成了救命稻草,决定先配置起来XFRM接口的虚拟网卡试一下。这个建立和关闭虚拟网卡的操作需要在strongSwan启动和终止的时候进行,所以我照着测试用例route-based/rw-shared-xfrmi给moon和sun写了下面的脚本:

strongswan-up.sh:

#!/bin/sh

# moon的配置
ip link add ipsec0 type xfrm dev eth0 if_id 42
ip link set ipsec0 up 
ip addr add 10.0.0.1/24 dev ipsec0
firewall-cmd --zone=public --change-interface=ipsec0

strongswan-down.sh:

#!/bin/sh

# moon
firewall-cmd --zone=public --remove-interface=ipsec0
ip link del ipsec0

strongswan.conf:

charon {
    # ...

    start-scripts {
        start_1 = /etc/strongswan-up.sh
    }  

    stop-scripts {
        stop_1 = /etc/strongswan-down.sh 
    } 
}

同时swanctl.conf也要做一点修改。面向客户端的连接要把if_id_outif_id_in的值都设置成strongswan-up.sh里指定的42。给客户端分配的虚拟IP地址池需要同步改一下,把10.0.0.1那个地址排除掉,因为这个IP给了服务器自己,具体怎么改测试用例里都有。另外firewalld也要打开同zone(public)转发的选项(–add-forward)这里就不赘述了。后面甚至可以看到配置路由模式的VPN其实是走了个大弯路。

路由模式也配好了,我又试了一次我的手机和电脑之间能不能ping通,还是不行。网络配置已经尽可能做到和测试用例一致,再怀疑只能怀疑到防火墙firewalld头上。果然,把moon和sun的firewalld都关掉,连接两个服务器的计算机互相之间ping对方地址段的虚拟IP就能ping通了。更诡异的是,把其中一方的防火墙关上另一方仍然打开,关掉防火墙的一方是能ping通打开防火墙的一方的计算机的,这证明防火墙的拦截操作其实是发生在出站的一侧。然而我把firewalld的LogDenied选项打开,在系统日志里也找不到有关ping数据包被拦截的消息。

因为每天都要上班被掏空,这个问题困扰了我好几天。直到我在strongSwan文档里看到了这样一句话:

Local firewall stacks generally don’t treat packets with a matching IPsec policy any different from unprotected packets. That means NAT rules also apply to traffic that is supposed to be tunneled.

This often leads to problems, because many hosts have SNAT or MASQUERADE rules set up which change the source IP of the packets, making them not match the negotiated IPsec policies when IPsec processing of outgoing packets happens in the Netfilter packet flow xfrm lookup node. To fix this problem, packets with a matching IPsec policy should skip NAT rules in the POSTROUTING chain of the nat table. This is achieved by inserting a rule that accepts packets with a matching IPsec policy before any NAT rule in the POSTROUTING chain.

iptables -t nat -I POSTROUTING -m policy –pol ipsec –dir out -j ACCEPT

大意是:防火墙的NAT规则会在IP包进入IPsec隧道前生效。所以连接moon的计算机对sun的计算机发起ping请求时,ICMP包的包头的源IP10.0.0.0/24实际上会被防火墙给修改成moon的公网IP。这个IP包因此会由于不符合IPsec策略被流放到互联网,很显然在互联网上ping一个保留给私网的IPv4地址是不可能ping通的。这段话还给了解决办法,就是加上那条iptables规则。但是我试验了下,直接加iptables规则没法绕过firewalld的NAT规则(–query-masquerade输出yes),哪怕安装了iptables-backend-nft这个包。所以NAT的事情完全不应该由firewalld插手,而是得靠手输iptables给指定网段设置NAT规则。具体是在前面的两个服务器的strongswan-up.sh里加入下面的iptables命令:

# moon
iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -m policy --dir out --pol ipsec -j ACCEPT
iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE

两条命令的顺序不能调换,否则要进入moon和sun的IPsec隧道的IP包还是会被NAT规则修改。对应地,strongswan-down.sh里把这两条规则删掉:

# moon
iptables -t nat -D POSTROUTING -s 10.0.0.0/24 -o eth0 -m policy --dir out --pol ipsec -j ACCEPT
iptables -t nat -D POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE

最后别忘了关掉firewalld的NAT:

firewall-cmd --permanent --remove-masquerade
firewall-cmd --reload

把NAT搞定,连接两个小鸡的电脑终于能用虚拟IP互相ping通对方VPN网关的背后的电脑。但是这时候电脑连上小鸡sun打开ipip.net,看到的本机IP竟然还是sun的IP。原来两个内网之间能ping通仅仅是我要达成的“中转”节点的目标的第一步,这个测试用例并不能完全满足我的需求。

把连接服务器sun的计算机的流量转到moon上

现在两台VPN服务器moon和sun之间IPsec隧道已经成功打通了,两边连接上VPN的计算机都能通过隧道互相用虚拟IP与对面网关和网关后面的计算机进行通信。按理说,要实现把其中一个VPN的网关对外界通信的流量都转发到另一个上,稍微改一下网关与网关之间的IPsec过滤策略应该就够了。

此时两台小鸡moon和sun都设置好了基于路由的IPsec VPN,给VPN客户端分配的虚拟IP网段分别是10.0.0.2-10.0.0.25410.0.1.2-10.0.1.254,moon的虚拟IP是10.0.0.1,sun的是10.0.1.1

起先我的做法是把两个服务器的local_ts和remote_ts调整一下。用于中转的服务器sun,local_ts改成10.0.1.0/24,remote_ts改成0.0.0.0/0;接收方moon这两个值是反过来的0.0.0.0/010.0.1.0/24。修改的意图是让连接sun的客户端所有对互联网和VPN内网的连接都要路由到moon上去。改好了重启strongSwan然后initiate一下连接net-net,在服务器sun上traceroute google.com发现第一跳走的就是服务器moon的IP,我挺高兴的,以为成功了。但是这时候再测试连接sun的计算机,连接上去的客户端反而都上不了网了。

只会散弹枪编程的我,赌气地把两边的服务器的local_ts和remote_ts都改成0.0.0.0/0,再用命令把隧道打通,好家伙,两台服务器的SSH连接全都断了。复习下ts的全名,traffic selector,两边都设成0.0.0.0/0那服务器就只能接受来自IPsec隧道的连接,SSH连接不断才怪呢。所以之前设置的IP过滤策略(local_ts和remote_ts的值)大概是没什么问题的。

回到连不上网的问题,把配置改回到全设置成0.0.0.0/0之前的配置。此时的情形是这样的:
– 连接sun的计算机ping不通moon的虚拟IP10.0.0.1,连接moon的计算机也不行,甚至连服务器sun的虚拟IP10.0.1.1都ping不通。
– 在服务器sun上ping moon的虚拟IP10.0.0.1和连接moon的计算机都是通的。moon ping sun的虚拟IP10.0.1.1也能ping通。

第二条可以证明IPsec隧道本身是没有问题的。至于NAT的影响,遵照前面strongSwan文档的指导,iptables已经被配置成了对要进入IPsec隧道的IP包不做NAT,所以NAT也不是这里的影响因素。而且仔细想一下,在服务器sun上做NAT其实没有任何意义。最初的意图是连接sun的计算机的互联网流量,都通过sun与moon之间的IPsec隧道无脑转发到moon上。假如在sun上就把NAT做了,对应的IP包头里的源IP变成sun的公网IP,这样NAT后的IP包传到moon那里moon也不知道怎么处理。也就是说,按照原本意图配置好的VPN网络,整个网络应该只有moon一个网关处理客户端与互联网的连接,sun起到的作用仅仅相当于连在网关moon上的一台交换机,只不过moon和sun之间连接的介质不是点对点的光纤而是IPsec隧道。综上不管怎么样,都应该把服务器sun的NAT给关掉。

可是把NAT去掉以后,情况还是和之前一样。现在要么是基于路由的IPsec VPN没配好,要么是基于策略的网关对网关的IPsec隧道和基于路由的IPsec VPN冲突。只有知道了ICMP包跑到哪里去了,才能知道为什么连接sun的电脑上不了网还ping不通VPN内网中的电脑。我看了strongSwan的测试用例才知道linux上抓包是用tcpdump。参照man和google把测试用例里的tcpdump命令稍微改了下,分别在moon和sun上运行。这样就能通过IP包的TTL值判断ICMP包是如何在这两个服务器之间流转的:

tcpdump -l --immediate-mode -vv -i eth0 icmp

找台电脑连上sun的VPN,然后在连上VPN的电脑上ping服务器sun的虚拟IP10.0.1.1诊断下第一个问题。两个服务器会看到下面的输出:

# sun 
12:47:23.658780 IP (tos 0x0, ttl 63, id 39615, offset 0, flags [none], proto ICMP (1), length 60)
    10.0.1.1 > 10.0.1.2: ICMP echo reply, id 1, seq 7, length 40
# moon 
12:47:23.652539 IP (tos 0x0, ttl 64, id 39615, offset 0, flags [none], proto ICMP (1), length 60)
    10.0.1.1 > 10.0.1.2: ICMP echo reply, id 1, seq 7, length 40

试着分析一下。首先服务器sun一定收到了客户端发过来的ping,不然也不会生成回复的ICMP包。然后moon和sun都看到了这个响应的包,并且moon抓到的ICMP包头的TTL是64,sun是63。如果moon和sun之间没有为了中转搞的这个IPsec隧道,sun回复给连接VPN的电脑的ICMP包应该通过虚拟网卡ipsec0发回去。但是这个ICMP包被IPsec隧道配置的过滤策略截胡了,阴差阳错地转到了moon手里,moon收到这个ICMP包,发现这个包满足moon的IPsec路由规则(ip route list table 220命令查看得到输出10.0.1.0/24 via <gw> dev eth0 proto static),又把这个包发回给sun。sun通过网卡eth0收到自己发出去的一模一样的ICMP包,sun不知道怎么处理,只能把这个包丢掉。

这个回程的ICMP包经历的事情,充分证明了基于策略的IPsec隧道会影响到服务器所有的网络流量,包括为VPN设置的虚拟网卡。因此这里的问题是基于策略的IPsec隧道与基于路由的对客户端的VPN连接发生冲突了,不把moon与sun之间的IPsec连接也配置个虚拟网卡估计这个问题是解决不了的。为了爬个墙折腾虚拟网卡配路由表实在是太蛋疼了,每次改之前都必须把strongSwan关掉再修改,不然状态就错了。干脆把VPN改回基于策略的模式,大家都是基于策略的,应该就不会出现刚才说的半路截胡导致冲突的情况。并且服务器sun的作用相当于交换机,在交换机上搞基于路由的VPN就是不对劲的一件事情。

之后我把sun的VPN改回基于策略的模式,这时服务器sun在VPN网络中是没有自己的IP的,客户端的IP地址可以从10.0.1.1开始。然后按照之前对ts的理解,sun给VPN客户端提供10.0.1.0/24的虚拟IP;sun与moon之间的隧道,要把连接moon所有网段在10.0.1.0/24的客户端对外的通信都通过隧道无脑转发给moon。这不就是最初想要达到的目的么,我为啥要想不开去搞基于路由的VPN给自己找麻烦。此时服务器moon的配置还是基于路由的,所以电脑连上sun可以测试一下能不能ping通moon的虚拟IP10.0.0.1。试验是成功的。

现在这种情况下,连接moon和sun的客户端互相之间都能ping通了,但是连接sun的电脑还是上不了网。这个问题出在NAT上,moon的虚拟IP网段是10.0.0.0/24,sun的网段是10.0.1.0/24,因为现在流量都转到moon上了,moon还要把源IP在10.0.1.0/24的IP包给NAT掉,所以需要把moon启用NAT的网段10.0.0.0/24扩大一点。具体操作就是moon的strongswan-up.sh设置NAT的地方要改一下:

iptables -t nat -A POSTROUTING -s 10.0.0.0/16 -o eth0 -m policy --dir out --pol ipsec -j ACCEPT
iptables -t nat -A POSTROUTING -s 10.0.0.0/16 -o eth0 -j MASQUERADE

其实把24改成23就可以,这里只涉及到两个相邻网段的互联。不过为了配置更多的中转节点,改成16也不是不行,这样就能支持下挂254个中转服务器,虽然很明显这个很不现实。

至此服务器sun就变成了一个称职的VPN中转节点。电脑连接sun提供的VPN,浏览ipip.net看到自己的IP只会是moon的,而不是sun的。还有一个小问题需要解决,是个人肯定都会希望服务器重启时这个中转隧道能自动搭起来,两边的服务器任何一个重启,都要在sun的终端手动输入swanctl --initiate --child net-net整个这套配置才能正常跑起来,很显然没有人能忍受的了这种处理方式。解决办法也很简单,在sun的net-net连接下面手动加一行start_action = trap就可以。按照官方的文档,加上这行配置,只要连接sun的客户端对外发起连接(回忆下隧道的traffic selector是怎么配置的)strongSwan就会自动尝试建立moon与sun之间的连接,不需要人工干预。

虽然达成了开头所说的中转服务器的目的,但是这个配置还是有一些缺憾:其一是服务器sun10.0.1.0/24网段的IP互相之间的通信都要走一遍服务器moon,延迟会相对大一些,但是我的目的是爬墙,不考虑内部通信的需求,这个无伤大雅;其二是VPN配置没有处理IPv6的情况,浏览被墙网站DNS解析到了IPv6的IP仍然会收到来自功夫网的警告,但是我折腾IPv4已经很累了暂时不想讨论v6的情况。另外就是moon上配置的基于路由的VPN在中转节点这件事上也没有起到任何用处,不喜欢也可以把moon的基于路由的配置也改回到基于策略的配置。

在国内配置“中转”VPN服务器实现里通外国

我把我一整套的配置搬到了之前买的一台阿里云服务器上,妄想透过阿里云“优质”的线路帮我里通外国。事实证明我还是太幼稚。国内服务器用前面重复说好多次的命令去手动连接国外的服务器,第一步协商都过不去。国外的服务器开了swanctl --log监听,没有任何有关国内服务器发起协商的消息,国内服务器的输出则是直接卡住。所以我还是拿衣服了,ISAKMP这么钩直饵咸专门用来建立安全隧道的协议,阿里云的防火墙不给挡住,我当阿里巴巴那么多员工都是白痴吗?腾讯云干脆也懒得试了,得到的结果想想都知道会是一样的,即使一时半会真打通了,之后来自腾讯云义正词严的问候也是板上钉钉的事。因此,真要在国内机房的服务器里做这种违反《中华人民共和国计算机信息网络国际联网管理暂行规定》的勾当,必须得找一个管的没那么严的机房,这样的机房哪里有,我是真的不知道。

这个中转节点,我最后是在我自己家的路由器上搭起来的。家里的路由器是ARM开发板刷上了OpenWRT。OpenWRT的strongSwan软件包配置有一大堆的问题,最后搭建成功了,每次路由器重启还必须得手输一遍swanctl --load-all重灌一遍VPN连接配置,客户端才能正常连接。不过好歹比输入无数次swanctl --initiate重新挖点对点隧道强一百倍,有时间还得看一下为什么不能自动加载VPN配置。还有IPv6的坑也得填一下。

结论

说实在的,以爬墙为目的,给IPsec VPN做中转节点这种事情真的是一种愚蠢的行为。IPsec这种特征这么明显又被客户端限制而无法随意修改的协议,随时会被功夫网连根拔起。甚至阿里云的例子摆在那,骨干路由器设置一个防火墙规则就能轻松拿捏。因此在这里奉劝大家不要学我当井底之蛙,跳不出IP安全性协议这个巨坑,早日弃暗投明才是正道。另外搭这个东西的意外收获,是我被迫学会了journalctl、tcpdump、ip route这些东西到底怎么用,以及强行恶补了读大学的时候学的计算机网络课程,这些我都还给老师了,我真是个学渣。

这个东西我能想到的用途,是我的不正经的游戏加速器;以及公司使用政企宽带,无法直连国外代理服务器这种情况下进行中转;如果基于路由的VPN搞得好的话,这个中转服务器的配置可能还可以把国内服务器伪装成IPLC机场骗钱。也许还有别的实用场景我还不知道。

最后的最后,我把测试通过的配置文件放在这里。写了这么多的废话,各位肯定都看吐了,这里的配置可以直接拿过去复制粘贴。两个服务器都没有用到基于路由模式的配置。客户端连接时,服务器用证书表明身份(国内服务器用的自签名证书,国外用的Letsencrypt),客户端验证用EAP-MSCHAPv2输入用户名和密码;两个服务器互联用预共享密钥互相验证身份。

moon

swanctl.conf

include conf.d/*.conf

connections {   
    strongswan-win {
        version = 2
        rekey_time = 0

        local_addrs = 0.0.0.0
        send_certreq = no
        unique = never
        pools = ip_pool
        dpd_delay = 60s
        encap = yes

        proposals = 3des-aes128-aes192-aes256-sha1-sha256-sha384-modp1024,default

        local {
            auth = pubkey
            certs = fullchain.pem
        }

        remote {
            auth = eap-mschapv2
            eap_id = %any
        }

        children {
            net1 {
                esp_proposals = aes256-aes128-3des-des-null-sha1
                local_ts = 0.0.0.0/0, ::/0
            }
        }
    }

    mainland-relay {
        local_addrs = <moon>
        remote_addrs = <sun>

        local {
            auth = psk
            id = <moon>
        }
        remote {
            auth = psk
            id = <sun>
        }
        children {
            relay {
                local_ts = 0.0.0.0/0
                remote_ts = 10.0.1.0/24
                esp_proposals = aes128gcm128-x25519
            }
        }

        version = 2
        mobike = no
        reauth_time = 10800
        proposals = aes128-sha256-x25519
    }
}

pools {
    ip_pool {
        addrs = 10.0.0.0/24
        dns = 208.67.222.222, 208.67.220.220    
    }
}

secrets {
    private_1 {
        file = "privkey.pem"
        secret = ""
    }

    eap_1 {
        id_1 = youraccount
        secret = yourpassword
    }

    ike_gw {
        id_1 = <moon>
        id_2 = <sun>
        secret = "ZaPigkBvgSmLnjRIkplz8E1GKntzKoXnhbzs2uVxmPrvXaDQJrU9gTyJHK/9pp5abmJ0Ke6kLQLms+asyC+eZw=="
    }
}

/etc/strongswan-up.sh

#!/bin/sh

iptables -t nat -A POSTROUTING -s 10.0.0.0/16 -o eth0 -m policy --dir out --pol ipsec -j ACCEPT
iptables -t nat -A POSTROUTING -s 10.0.0.0/16 -o eth0 -j MASQUERADE

/etc/strongswan-down.sh

#!/bin/sh

iptables -t nat -D POSTROUTING -s 10.0.0.0/16 -o eth0 -m policy --dir out --pol ipsec -j ACCEPT
iptables -t nat -D POSTROUTING -s 10.0.0.0/16 -o eth0 -j MASQUERADE

strongswan.conf

# strongswan.conf - strongSwan configuration file
#
# Refer to the strongswan.conf(5) manpage for details
#
# Configuration changes should be made in the included files

charon {
    load_modular = yes
    plugins {
        include strongswan.d/charon/*.conf
        duplicheck {
            enable = no
        }
    }

    start-scripts {
        start_1 = /etc/strongswan-up.sh
    }

    stop-scripts {
        stop_1 = /etc/strongswan-down.sh 
    }
}

include strongswan.d/*.conf

sun

swanctl.conf

# Include config snippets
include conf.d/*.conf

connections {   
    windows-relay {
        version = 2
        rekey_time = 0

        local_addrs = 0.0.0.0
        send_certreq = no
        send_cert = always
        pools = ip_pool
        dpd_delay = 60s
        encap = yes

        proposals = 3des-aes128-aes192-aes256-sha1-sha256-sha384-modp1024,default

        local {
            auth = pubkey
            certs = cert_sun.pem
        }

        remote {
            auth = eap-mschapv2
            eap_id = %any
        }

        children {
            client-conn {
                esp_proposals = aes256-aes128-3des-des-null-sha1
                local_ts = 0.0.0.0/0, ::/0
            }
        }
    }

    relay {
        local_addrs = <sun>
        remote_addrs = <moon>


        local {
            auth = psk
            id = <sun>
        }
        remote {
            auth = psk
            id = <moon>
        }
        children {
            net-net {
                local_ts  = 10.0.1.0/24
                remote_ts = 0.0.0.0/0
                start_action = trap
                esp_proposals = aes128gcm128-x25519
            }
        }

        version = 2
        encap = no
        mobike = no
        reauth_time = 10800
        proposals = aes128-sha256-x25519
    }
}

pools {
    ip_pool {
        addrs = 10.0.1.0/24
        dns = 208.67.222.222, 208.67.220.220    
    }
}

secrets {
    private_1 {
        file = "key_sun.pem"
        secret = ""
    }

    eap_1 {
        id_1 = youraccount
        secret = yourpassword
    }

    ike_gw {
        id_1 = <sun> 
        id_2 = <moon>
        secret = "ZaPigkBvgSmLnjRIkplz8E1GKntzKoXnhbzs2uVxmPrvXaDQJrU9gTyJHK/9pp5abmJ0Ke6kLQLms+asyC+eZw=="
    }
}

/etc/strongswan-up.sh

/etc/strongswan-down.sh

strongswan.conf

# strongswan.conf - strongSwan configuration file
#
# Refer to the strongswan.conf(5) manpage for details
#
# Configuration changes should be made in the included files

charon {
    load_modular = yes
    plugins {
        include strongswan.d/charon/*.conf
        duplicheck {
            enable = no
        }
    }
}

include strongswan.d/*.conf