19
2024
02
02:23:12

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

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

要点:
– 以下这种思路是不对的:服务器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

Dev-C++ 一日游

敝司搞了个IT技能竞赛,说白了就是搞编程竞赛,就是做算法题那一套。但是题目限定了语言只能用Java和C,不能用C++。然而我没写过Java更别提参加过Java项目,只能写C语言,所以一切都得从头来,这就吃了大亏。比赛居然还要专门的电脑上提交代码,上面没有C++的开发环境。眼看比赛要开始了,20个G的visual studio铁定是复制不过去的,安装也是很费时间的。所以需要一个迷你的开发环境。我突然想起了Dev-C++,大学的时候用过这个东西,当时体验是起码看起来比VC6要好一点。

google了下才知道,2021年Dev-C++居然还在更新。现在是Embarcadero,那个Delphi现任的维护者在维护这个项目。代码提交还很活跃,GitHub上看到最新的提交日竟然是六天前。看到更新日志里说,现在最新的版本还加上了对C++17/20的语法支持,好像还可以的样子。所以2021年用Dev-C++写代码是什么样的一种体验呢?怀着好奇的心理我把Embarcadero维护的Dev-C++下载了下来。毕竟标榜自己是C++的IDE,代码提示和调试这些可能不会差到哪里去。没想到等待我的是噩梦般的体会。

绑定mingw和gdb的 Dev-C++ 很快下载好了。首先是启动界面, Embarcadero为这个古老的IDE加上了时髦的自定义界面主题,还贴心地问你要不要关联h/cpp文件(算了)。再进到启动画面,这界面和喷气大脑和巨硬的竞品比好像也没差到哪里去。可惜,当我把代码复制过来的一瞬间,我被雷到了。代码里的中文字符怎么都消失了?按了下Ctrl+A全选,咦,原来中文还是在的,很明显这是个挺严重的BUG:中文字符不能正常显示。忍了吧,毕竟C语言用中文标识符是作死的一种行为,不影响写代码那就忽略掉。

然后是搬代码的体验,Dev-C++的自动完成还是功能还是像旧版本一样蛋疼。输入括号引号尖括号之类字符的时候,它还是会自作主张地给你多补一个对应的括号。举个例子,输入”{}”在新版本的Dev-C++里面还是会打成”{}}”。在以前这种类型的自动完成可能还不错,适应下就可以了。但是这已经是2021年了,自动完成做成这个样子还是不要标榜自己是IDE了。

代码写完了来看看调试,这个是我感到最蛋疼的地方。首先是看变量的值,不像visual studio和别的商业IDE,Dev-C++要看某个变量的值必须在界面里手动添加才可以。它没法自动把函数里的局部变量的值都给你展示出来。然后是显示指针指向的东西,这个就更蛋疼了。调试界面只给你显示指针的值,不告诉你指针指向的东西到底是什么样的。虽然加个*p的监视也可以,但是这样仍然很不方便。尤其是你需要调试链表和树这种用指针构建起来的数据结构的时候。写的编程题要用到树,调试的时候把我给搞疯了。更蛋疼的是,Dev-C++在调试的时候总会崩溃,甚至还没法把gdb.exe结束掉,任务管理器都没用。

所以到2021年了,被Embarcadero接手继续维护的Dev-C++可以用吗?我的看法是,算了吧。确实得承认,无数OIer对它有深厚的感情。我也祝愿Delphi的维护者能成功让Dev-C++这个老项目焕发第二春。然而2021年如果有人还在推荐它作为初学者入手C/C++的软件,这就是在谋财害命了。我觉得残废的自动完成和不友好的调试界面对初学者的打击是很大的,只会疯狂劝退。不在乎大小和专有软件,Visual Studio一直有Express版,甚至本家Embarcadero自己的C++ Builder也出了免费版,嫌卸载麻烦不便携,然而自己的电脑根本没有这个问题。至于打开cpp文件就可以编译出程序更方便,嗯,vs新建个项目真的那么麻烦吗?最最重要的是,已经没有人用Dev-C++写项目了,举个例子,CMake根本没有给你生成Dev-C++工程文件的选项,然而Code::Blocks却是有的。 那些根本不能用IDE的地方,例如内核开发,更没有Dev-C++什么事情。它真的老了,千万别再说它适合初学者这种话了。

Dev-C++被你说的这么垃圾,对你说的你的比赛有什么影响吗?我当时卡在了怎么从头实现一个环形缓冲区上,数据结构真的忘记了,题还是得刷。然后调试太蛋疼,看不到数据结构的样子,对我也是帮了很大的倒忙。不管怎么说还是感谢下吧,不然连工具链都没的用也就别干活了。


ps: 我知道Dev-C++一些国人的Fork对这些不足是做了改进的,例如小熊猫版解决了局部变量的问题。然而实际项目没有人用才是最大的问题。

在 IPv4/IPv6 双栈网络环境下配置基于 strongSwan 的 IPsec VPN

Update 2021-11-06: 1.strongswan.org 被墙了,功夫网威武。2.更正了同一用户使用不同终端登录时会挤掉已有的VPN连接的问题。

要爬墙为啥想不开要用VPN?代理服务器他不香吗?代理比VPN不知道高到哪里去了。用代理的话国内国外能分流,配置一般来说只要改一个配置文件就可以即开即用。用VPN的话,首先国内外分流就特别麻烦,不省VPS流量,访问国内网站或者想打个国服还得忍受延迟爆表;之后,VPN报文的特征那么明显;功夫网想墙掉是分分钟的事情;最后,VPN的配置起来比代理难多了,要配置个能用的VPN你要考虑好IP地址,还要改内核设置使得客户端连上VPN后正常爬墙。只为了爬墙用代理就能轻松爬,你为什么想不开要用VPN?

背景

在我这里ss和v2ray真的不香,所以我被迫捡起来了VPN的传统艺能。配好了IPsec VPN发现IPsec意外地香。同一台服务器上代理全部趴窝的时候,连上IPsec VPN看油管却能看4K@60fps的视频毫无中断问题。

虽然IPsec VPN确实爽了,但是还是有个地方很不如意:照以往经验配置出来的IPsec VPN只支持IPv4网络,不支持IPv6。在工信部大力推动IPv6部署的情况下,目前的VPN配置已经跟不上了时代的潮流,VPN只支持IPv4,IPv6的流量就仍然是直连的,没法走VPN翻过高墙。为啥要大力推进IPv6部署啊?不就是因为v6的墙建好了。 有双栈网络接入且只有IPv4的流量能走VPN的情况下,要访问一个被墙的网站,如果被墙的域名有AAAA的记录且客户端这里得到了正确的IPv6结果,又由于IPv6是直连的,收到来自功夫网的RST包警告便是必然的事情。

要解决这个问题有两种思路,第一种思路是关掉操作系统的IPv6支持。 不更改家庭网关设置的情况下,电脑上操作起来还是很容易的,windows和linux都能方便地关掉IPv6,只有v4地址也就不存在v6流量走不了VPN的问题了。但是手机上想全关掉IPv6的支持不容易,以Android为例,Android确实可以关掉4G网络的IPv6支持,选项藏的很深而已(Settings -> Connections -> Mobile networks -> Access Point Names -> 选择接入点 -> 把 APN Protocol 和 APN roaming protocol 都设成IPv4),但是碰到支持IPv6的wifi接入点怎么办呢?对不起,Android不支持。当wifi接入点的设置不是自己能控制的时候这就很难受。第二种思路,是在连上VPN的时候让VPN服务器给客户端分配一个能联网的IPv6地址。 这样IPv6的流量就也可以走VPN了,这也是本文要讨论的问题。

怎样用strongSwan搭建IPsec VPN的教程网上有很多。之前我是照 openSUSE wiki 上 MargueriteSu 大佬写的教程学会怎样配置的。推荐看一下,能学到许多东西。然而教程的完成日期已经是7年前了,一些情况和现在不一样。还有其它的教程,它们都告诉你需要去修改ipsec.confipsec.secrets这两个配置文件来配置VPN,且对IPv6的情况只字未提。在防火墙和配置证书的部分和现在的情况也不一样,例如MargueriteSu的教程里还在用SuSEFirewall2,可是openSUSE已经完全弃用了SuSEFirewall投奔firewalld;有的发行版,例如Ubuntu,预置的strongSwan包也是不带生成证书的功能的。找现成的教程无果,去翻了strongSwan项目的主页,我惊奇地发现strongSwan的示例里已经没有这两个文件了,它已经过时了,取而代之的是单个文件swanctl.conf。为了跟上潮流,遇到问题的时候能在官方处得到启发,当然也要用新的配置方式。怎样用新的配置文件配置VPN也是本文要讨论的话题。

目标

这里要达到的目标是:在VPS上配置一个通过IPv4和IPv6地址都能访问的IPsec VPN。通过这两种方式连接到VPN后,客户端都能同时获取到能联网的IPv4和IPv6地址。VPN服务端通过合法的证书来表明服务器的正确身份,客户端的认证协议采用EAP-MSCHAPv2。Windows自带的VPN客户端和Android的strongSwan APP都能连接上VPN服务器且能正常上网。

VPN服务器能给客户端分配能用的IPv6地址,IPv6流量不能走VPN的问题也就迎刃而解了。

环境

我的VPS用的linux发行版是openSUSE Tumbleweed。写文章的时候发行版预置的strongSwan版本是5.8.4。

既然我们要让VPN客户端获得IPv6地址,那么VPS有IPv6地址便是实践这篇文章所述事情的一个必要条件。VPS没IPv6地址,不能访问v6网络,那还怎么给客户端v6地址呢?另外,你的VPS获得的IPv6地址最好是/64的,并且支持SLAAC,也就是说后半部分64位的分配权在你手里,你的VPS可以在后面的64位后缀里任意选一个。这样我们给VPN客户端分配IPv6地址会更方便一些。如果不明白,可以向你的VPS运营商咨询,这些都是服务商需要给你解决的问题。

假如VPS服务商给你的IPv6地址是/128的,也就是你只拥有一个IPv6地址的分配权,那就不好玩了,必须得用NAT64这种没有办法的办法。这不在本文的讨论范围之内。

配置VPN连接

配置IPv4环境下的VPN服务器

在IPv4环境下配置VPN没什么好说的,网上的教程完全可以用来参考。这里也几乎就是把其它教程的意思重复一遍。最大的问题是如何把ipsec.confipsec.secrets转成swanctl.conf里面的语法格式。

安装软件

用zypper命令安装strongSwan:

zypper in strongswan

因为用的是suse,所以想要gui的话可以去YaST控制台的Software Management界面搜,具体过程不赘述。

配置服务端证书

之前说了,VPN服务端这边需要一个有效的证书来表明服务端的身份。

你有自己的域名的话,你可以把你手上的域名解析到你的vps上,然后向letsencrypt申请SSL证书。和架网站不一样,这里解析域名最好是IPv4和IPv6分开解析。例如把v4地址解析到gateway.your.domain,v6地址解析到gateway-v6.your.domain。理由不仅有方便区分,还有强国的IPv6路由配置比较奇葩,使用IPv6地址连接时很容易碰到延迟爆炸连线质量极差的情况。申请LetsEncrypt证书的环节我这里直接用了EFF提供的certbot脚本。

安装certbot:

zypper in python3-certbot

打开防火墙:

firewall-cmd --add-service http
firewall-cmd --add-service https
firewall-cmd --permanent --add-service http
firewall-cmd --permanent --add-service https

用Certbot申请证书:

certbot run --domain gateway.your.domain --domain gateway-v6.your.domain

证书申请成功的时候,脚本会告诉你申请过来的合法证书存放的位置,类似于这样:/etc/letsencrypt/live/gateway.your.domain/fullchain.pem。之后配置VPN连接的时候会用上。

因为letsencrypt证书只有三个月的有效期,所以我们需要有个systemd服务和定时器,用来定时检查letsencrypt证书是否过期。风滚草提供了一个包certbot-systemd-timer帮我们代劳,它会在/usr/lib/systemd/system目录下安装刚才说的服务和定时器,名字叫certbot-renew

zypper in certbot-systemd-timer

假如你用的是别的发行版,你可能需要自己写一个service和timer。service要做的事情很简单,就是调用一下/usr/bin/certbot renew命令,当然视发行版不同文件路径可能有变化。

解决证书问题,还有个途径是自建一套PKI体系。可以用strongSwan自带的pki功能也可以用OpenSSL,这不在本文的讨论范围内。用OpenSSL这里有篇文章可供参考:
https://jamielinux.com/docs/openssl-certificate-authority/index.html

配置VPN服务端

swanctl.conf

因为ipsec.confipsec.secrets被宣告为过时了,所以这里我们在swanctl.conf里面配置VPN连接。strongSwan wiki里有把设置从这两个文件迁移到swanctl.conf的教程。这里就照着wiki页面的内容把其他教程里的配置迁移了一下:

include conf.d/*.conf

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

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

        proposals = aes256-sha384-modp1024,default

        local {
            auth = pubkey
            certs = fullchain.pem
        }

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

        children {
            net1 {
                local_ts = 0.0.0.0/0
            }
        }
    }
}

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

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

    eap_1 {
        id_1 = fuckgfw
        secret = "makevpngreatagain"
    }
}

显然swanctl.conf分为三个部分,connectionspoolssecrets。其中,connections对应的是ipsec.conf里的内容;pools指定了要分配给VPN客户端的虚拟IP地址池的DNS服务器,这里定义了一个名叫ip_pool的IP池;secrets对应的是ipsec.secrets文件的内容,以private做前缀的项定义了VPN服务端要使用的服务端证书私钥,eap前缀的项定义了VPN连接要用到的用户名和密码,这里的例子里,用户名是fuckgfw密码是makevpngreatagain

再简单解释下connections栏里的内容:
– version = 2: 使用IKEv2认证方法。
– rekey_time = 0: 应对Windows内置的IPsec客户端的BUG,见MargueriteSu写的教程。
– local_addrs = 0.0.0.0: VPN服务端要监听服务端所有可用的IP地址。
– send_certreq = no: 是否要求客户端发送证书,这里我们要求客户端使用用户名密码验证,所以设为no。
– pools = ip_pool: 使用名为ip_pool的地址池用来给VPN客户端分配地址。
– unique = never: 永远不把已经登录的用户踢下线,即使新连接的VPN客户端发送了INITIAL_CONTACT报文。这项默认的设定值是”no”,也就是服务端只有收到INITIAL_CONTACT报文的时候才会把已建立连接的用户踢下来。但是不同客户端的行为会有差异,例如windows自带的VPN客户端不会发送这个报文,但是strongSwan for Android会发送,导致的结果就是你的Android手机登录VPN时,你电脑的VPN会断线。因此把这个值设成never。
– dpd_delay = 60sdpd的全称是dead peer detection,顾名思义是用来检测VPN客户端是否还在线。这里设定检测的时间间隔为一分钟。
– encap = yes: 强制把ESP包封装至端口号为4500的的udp包中,无视VPN客户端是否在NAT后面,这可以避免IPsec ESP包被一些错误配置的骨干路由器丢掉。
– proposals = aes256-sha384-modp1024,default: 指定IPsec连接使用的加密方式。前面的aes256-sha384-modp1024是为windows客户端量身定制的,这是windows支持的几种IPsec加密方式之一;后面的default是为其它类型的客户端(例如linux和Android)准备的。
– local: 指定验证服务器身份的方式,这里设定的方式为服务器出示有效的证书,证书文件名叫fullchain.pem
– remote: 指定客户端身份验证的方式,这里使用了eap-mschapv2。eap_id = %any表示secrets栏规定的任何EAP身份都有效.
– children: 配置IPsec SA的行为,我的理解是这里控制了客户端连接上VPN服务器后的一些行为,例如能访问的网段。里面的local_ts就是控制这个的。

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

这里只改了一个设置:duplicheck.enable = no,使得同一套用户凭证可以多终端在线。

启动strongSwan

把之前申请好的letsencrypt证书在swanctl目录下创建软链接,让strongSwan能找到这些证书:

mkdir /etc/swanctl/x509
mkdir /etc/swanctl/x509ca
mkdir /etc/swanctl/private
ln -s /etc/letsencrypt/live/gateway.your.domain/fullchain.pem /etc/swanctl/x509/fullchain.pem
ln -s /etc/letsencrypt/live/gateway.your.domain/chain.pem /etc/swanctl/x509ca/chain.pem
ln -s /etc/letsencrypt/live/gateway.your.domain/privkey.pem /etc/swanctl/private/privkey.pem

之后启动strongSwan:

systemctl enable strongswan
systemctl start strongswan

启动成功没有报错的情况下,可以输入命令swanctl --list-certs验证下服务器证书是否已经加载成功。
至此,strongSwan这边的配置完毕。

配置防火墙和系统内核设置

要让strongSwan VPN 能正常工作能爬墙,还需要改下防火墙和系统设置。
首先配置防火墙:

firewall-cmd --add-service ipsec
firewall-cmd --permanent --add-service ipsec
firewall-cmd --add-masquerade
firewall-cmd --permanent --add-masquerade

这里的含义是放行IPsec所需的500、4500端口以及ESP包,并且开启IPv4 NAT。

然后改内核设置,打开/etc/sysctl.conf加上以下几行:

net.ipv4.ip_forward = 1
net.ipv6.conf.all.accept_ra = 2

第一行的意思是打开IPv4转发。第二行的意思是在开启转发状态下让linux能接受IPv6的RA(Router advertisement)消息,不加上这个选项你的VPS就不能获得IPv6地址了。原因是打开了转发,Linux会把自己认成路由器,然后自动禁用掉IPv6自动配置,把值设成2就是要让这两者共存。
最后重启下VPS或者输入sysctl -p,你的VPN服务器应该就可以正常工作了。

配置VPN客户端

假如之前配置服务器证书的时候你选择了自建PKI体系,那么如何给客户端系统添加根证书信任不在本文的讨论范围内。

Linux

Linux下的IPsec VPN客户端就是strongSwan本身,strongSwan项目开发了一个networkmanager图形界面的接口,可以通过图形界面方便地连接VPN。

在networkmanager的管理界面里新建一个IPsec/IKEv2(strongswan)类型的连接,按照下图里的方式配置就可以。需要注意的是,在图形界面里配置的时候自己得手动把服务器证书拉下来,然后在连接里指定。

Windows

Windows下用自带的IPsec VPN客户端。

添加VPN连接需要去网络和共享中心,选择添加新连接,弹出的向导里选择“连接到工作区”->“否,创建新连接”->“使用我的网络连接”->地址填上VPS的地址或者你的域名。

之后回到网络和共享中心,点“更改适配器设置”,找到刚才添加的VPN连接,右键属性,点“安全”选项卡,VPN类型选IKEv2,数据加密类型选”最大程度的加密”,认证方式选“EAP-MSCHAPv2”点确定。
连接VPN时,输入用户名密码fuckgfw/makevpngreatagain,连接成功后你就可以上谷歌看油管了。

Windows 10 最新版可能会有点小问题,连接的时候会提示你“IKE身份验证凭据不可接受”。原因是win10智障,不会顺着证书路径判断VPN证书是否有效(也可能仅仅LetsEncrypt会有这个问题,因为LetsEncrypt曾经换过CA)。解决办法是把VPN服务端的证书放在“受信任的根证书办法机构”里,具体位置需要打开certmgr.msc管理证书。吊诡的是,这样操作然后连接VPN成功以后,即使把添加到根证书的VPN证书删掉也能连接成功。

Android

Android本身内置了IPsec VPN的支持,但是我们配的认证类型Android是不支持的。不过strongSwan项目官方做了一个Android客户端。

别想了,国内的应用市场是不可能上架的。你能找到这个APP算我输。 所幸strongSwan官网有apk,地址是 https://download.strongswan.org/Android/ 自然我们要下载最新版。

打开APP,选”添加VPN”配置,服务器地址填上VPS地址或者域名,VPN类型选”IKEv2 EAP(用户名/密码)”。用户名密码填上fuckgfw/makevpngreatagain,点保存。最后连接新添加的VPN。

iOSmacOS

抱歉,我太穷了,买不起高贵的苹果设备。所以对应的客户端设置欠奉。

让IPsec VPN支持IPv6

假如你当前的网络环境只能获得IPv4地址,当你做完上面的事情之后,你应该就可以看油管刷推特了。但是你有IPv6的话,你现在还不能连接上正常的国际互联网。原因前面说了,IPv6的流量没法走IPv4的VPN。假如你不想舍弃掉IPv6网络,那么你就得想办法让VPN客户端得到一个能用的IPv6地址。或者假如你想在IPv4的网络环境下获得访问IPv6网络的能力,也可以照下面的内容做。

这里添加IPv6地址的方法,要感谢imbushuo大佬的鼎力支持。大概两年前我问过他怎样让openConnect VPN客户端得到IPv6地址的问题,他帮我解决了然后整理成了文章。相同的办法也可以套用到strongSwan VPN上。

给客户端分配IPv6地址

swanctl.conf里定义一个IPv6地址池,且让之前配置的VPN连接使用这个IPv6地址池。

connections {
    strongswan-win {
        ...
        pools = ip_pool, ip_pool6 # 让strongSwan在ip_pool6地址池里也挑个IPv6的IP分配给客户端
        ...
        children {
            net1 {
                local_ts = 0.0.0.0/0, ::/0 # 加上::/0允许VPN客户端可以访问IPv6网络
        }
    }
}

pools {
    ip_pool {
        addrs = 10.0.1.0/24
        dns = 208.67.222.222, 208.67.220.220
    }
    ip_pool6 {
        # 前64位是VPS服务商给你的IPv6前缀,后面可以随便写,但是如果你有其他VPN服务注意不要相互冲突
        # 这里给分配的IPv6地址池一共有2^24 - 2 = 16777214个IPv6地址,
        # 从200a:abcd:abcd:abcd:1234:1234:3400:1 到 200a:abcd:abcd:abcd:1234:1234:34ff:fffe。
        addrs = 200a:abcd:abcd:abcd:1234:1234:3400::/104 
        dns = 2620:119:35::35, 2620:119:53::53
    }
}

修改内核设置和防火墙

修改完swanctl.conf然后重启strongswan服务,再连VPN,你可以发现你已经有了一个IPv6地址了。但是访问 https://ipv6-test.com/ 测试IPv6连接时,网站还是提示你不能访问IPv6网络。不过这时候在命令行里ping你的VPS的IPv6地址是可以ping通的,traceroute的时候也是只有一条记录(VPN模式下你的VPS就是你的网关,所以应该不会有中间地址)。要让客户端能连上IPv6互联网,还需要一些善后的工作。

sysctl.conf里加上以下设置,打开对IPv6流量的转发和ND Proxy:

net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.all.proxy_ndp = 1

修改防火墙设置,让防火墙放行通过VPS转发的IPv6包:

firewall-cmd --permanent --direct --add-rule ipv6 filter FORWARD 0 -j ACCEPT

为了让连上VPN的客户端对外部可见,需要把连接VPN的客户端宣告为VPS的邻居,这样外部的有IPv6网络的机器才能通过VPS找到VPN客户端获得的IPv6地址(不知道我的理解对不对)。可以用下面的命令来完成:

ip -6 neigh add proxy <VPN客户端获得的IPv6 IP> dev <获得VPS IP的网卡名>

显然,连上VPN后为了获得能用的IPv6地址还要登上VPS输一条命令是一件麻烦的事。所以strongSwan和ocserv一样,也提供了客户端连接/断开连接时自动调用Shell脚本的功能。脚本的输出会一并输出到strongSwan的log里。strongSwan提供了一个脚本示例,用来展示默认情况下VPN服务端的行为和传入到脚本中的环境变量。仿照示例我写了个这样的脚本,当然是照官网给的实例剪掉了不需要的部分做出来的:

# define a minimum PATH environment in case it is not set
PATH="/sbin:/bin:/usr/sbin:/usr/bin:@sbindir@"
export PATH

# comment to disable logging VPN connections to syslog
VPN_LOGGING=1
#
# tag put in front of each log entry:
TAG=vpn
#
# syslog facility and priority used:
FAC_PRIO=local0.notice

case "$PLUTO_VERB:$1" in
up-client:)
    # connection to my client subnet coming up
    # If you are doing a custom version, firewall commands go here.
    echo "a client connected"
    echo "ip neigh add proxy $PLUTO_PEER_SOURCEIP dev ens3"
    ip neigh add proxy $PLUTO_PEER_SOURCEIP4_1 dev ens3
    ;;
down-client:)
    # connection to my client subnet going down
    # If you are doing a custom version, firewall commands go here.
    echo "a client disconnected"
    ip neigh delete proxy $PLUTO_PEER_SOURCEIP4_1 dev ens3
    ;;
up-client-v6:)
    # connection to my client subnet coming up
    # If you are doing a custom version, firewall commands go here.
    echo "a client connected v6"
    echo "ip -6 neigh add proxy $PLUTO_PEER_SOURCEIP6_1 dev ens3"
    ip -6 neigh add proxy $PLUTO_PEER_SOURCEIP6_1 dev ens3
    ;;
down-client-v6:)
    # connection to my client subnet going down
    # If you are doing a custom version, firewall commands go here.
    echo "a client disconnected v6"
    ip -6 neigh delete proxy $PLUTO_PEER_SOURCEIP6_1 dev ens3
    ;;
esac

写好了脚本,还需要在swanctl.conf里声明一下:

不要忘了给脚本加上可执行的权限!

connections {
    strongswan-win {
        ...
        children {
            net1 {
                local_ts = 0.0.0.0/0, ::/0
                updown = /etc/swanctl/strongswan-ip-neigh.sh
            }
        }
    }
}

最后重启下strongswan服务。

systemctl restart strongswan

对Windows的特别处理

现在,你可以用strongSwan的Android客户端连接下VPN,然后访问 https://ipv6-test.com/ 检测下试试看。网站应该会提示你你已经可以连接上IPv6网络了。并且网站提示你你的IPv6地址是swanctl.conf里设定的IPv6地址池里的地址。再尝试连接下 ipv6.google.com,应该也能正确显示网页。

Linux上的结果应该也是一样。

但是当你在windows下连接VPN时,VPN连接状态里的IPv6网络还是无网络访问权限。这是windows的问题,解决办法是对VPN适配器手工加一条到::/0的路由,让windows知道所有的IPv6连接都要走这个VPN。以管理员权限打开一个命令提示符窗口,输入命令:

netsh interface ipv6 add route ::/0 "<windows里你的VPN连接名>"

也登录下网站测试试试看,结果应该和Android/Linux是一样的。

让IPsec VPN连接在IPv6网络可用

VPN服务器在IPv6网络下的配置和IPv4下几乎一样。不同的是除了监听地址要变成”::”,还需要把encap = yes去掉。Linux和windows在IPv6网络环境下没法把ESP包封装进UDP协议里,加上这个选项windows会提示invalid payload received。

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

        local_addrs = ::
        send_certreq = no
        pools = ip_pool, ip_pool6
        dpd_delay = 60s

        proposals = aes256-sha384-modp1024,default

        local {
            auth = pubkey
            certs = fullchain.pem
        }

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

        children {
            net1 {
                local_ts = 0.0.0.0/0, ::/0
                updown = /etc/swanctl/strongswan-ip-neigh.sh
            }
        }
    }

当然如果你就是看encap = yes这个选项不顺眼,也可以不给IPv6连接单独弄VPN配置。在原来的配置里把这个选项拿掉,然后local_addrs的值改成0.0.0.0, ::,这样也是可以的。

在纯IPv6网络环境下建立VPN连接,甚至可以让你的电脑获得访问IPv4网络的能力。

另外多说一嘴,strongSwan的Android客户端并不支持连接只有IPv6地址的IPsec VPN服务器。毕竟客户端在非Root环境下也能用,所以它建立VPN连接只能依赖把IPsec流量封装进UDP这个手段,无论系统本身是否能获得公网IP。众所周知linux下要发裸IP包是需要root权限的。然而linux在IPv6情况下不支持把IPsec怼进UDP里,所以就只好不支持了。见 https://wiki.strongswan.org/issues/892 。

排错

查看服务端日志能定位绝大部分的问题。

VPN配置文件出错时,使用systemd重启VPN服务会失败。这时候用systemctl status看状态能看到strongSwan解析配置文件解析到哪里出了错。

要在服务端跟踪VPN连接的日志,可以输入swanctl --log命令。有了日志了,报错了,然后怎么办?当然是用咕狗搜日志内容啦!

后记

在我“自力更生”在风滚草上成功搭上IPv6隧道之后,我又试了别的发行版。目前试验过的发行版有 openSUSE Leap 15.1 和 CentOS 8。相同配置下出现了和风滚草不太一样的情况,具体是为什么需要求助下。

情况是这样的:sysctl.conf里IPv4和IPv6的转发都打开、给VPN客户端的IPv6地址池也写上、但是ND Proxy的记录没有添加的情况下(就是最后的脚本做的事情),客户端获得的IPv6地址居然就已经可以联网了!只不过网站显示的IPv6 IP是VPS的而不是VPN客户端获得的。别的地方ping VPN客户端获得的IP也ping不通。加上了ND Proxy虽然能ping通了,在VPN客户端搭web服务器监听v6地址然后curl也能出结果,但是网站给出的IPv6 IP还是VPS的。

IPsec真的是深不可测的一个协议。 没准哪里就会碰到坑。这也是刚开始要吐槽的原因。

代理服务器不香吗!

参考资料

strongSwan 项目

大佬们的教程

声明:仅分享心得,不提供答疑服务。




推荐本站淘宝优惠价购买喜欢的宝贝:

image.png

本文链接:https://hqyman.cn/post/5159.html 非本站原创文章欢迎转载,原创文章需保留本站地址!

分享到:
打赏





休息一下~~


« 上一篇 下一篇 »

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

请先 登录 再评论,若不是会员请先 注册

您的IP地址是: