我靠,OpenVPN 你咋回事啊

warning: 这篇文章距离上次修改已过157天,其中的内容可能已经有所变动。

人生必做的 114514 件事:成功调通 OpenVPN(1/114514)。

本文严格来说,依然算是网络小故逝系列的番外,这次来讲OpenVPN,VPN界的扛把子选手。

全文掺杂大量笔者的思考,如果你也是一个喜欢折腾网络的人,不妨进来看看笔者的胡说八道和天马行空。

OpenVPN 长期以配置复杂而闻名OpenVPN 长期以配置复杂而闻名

1. 前言

老规矩,先放系列文章:

OpenVPN,玩网络的朋友应该对此并不陌生,不论你是干运维的,要连入机房管理网,还是出差/大学放假的,要连入公司/学校内网,亦或者是购买了大厂的服务用来上网的,都多多少少接触过这玩意。

长期以来,OpenVPN主要的名号在于:开源,久经审计,但速度较慢,而且配置复杂。就后两点而言,一方面是大厂VPN纷纷上马WireGuard,打广告的说法就是『极速上网,快如闪电』;另一方面是,如果你想自己搭建OpenVPN CE(社区版),你会发现网上存在相互冲突,且不知道多少年前的教程,也许你配不成功,也许能配成功,但首次开机之前还得蹲在那等他生成个几分钟的DH参数,连上一看还在用BF-CBC加密,列出配置文件一打就是一屏幕的证书。

所以,今天的网络小故逝,就来讨论一下这个让人又爱又恨的OpenVPN。

2. 千层饼配置

2.1 老资历的厚重

如果要区分一个VPN协议是否『传统』,笔者的习惯是按照能不能自定义端口来分,比如说PPTP,L2TP/IPSec,IKEv2/IPSec之类的,就比较传统;OpenVPN,WireGuard之类的,就不那么传统。但实际上,翻阅OpenVPN Changelogs可知,要真论辈分的话,首次推出于2001年5月的OpenVPN 0.9,其实比很多协议都要老一些,甚至传统中认为有板有眼的IKEv2(标准定义于2005年12月)也比他年轻不少,更不用说SSTP(2007年)之类的协议了。

第一份 Release Note第一份 Release Note

OpenVPN在此之后的发展,大家都有目共睹,因此不再赘述。但很显然,和形成标准后就固定不动的其他VPN协议相比,OpenVPN直到今天都还有更新,好处当然是可以随时间升级,引入更高强度/速度的加密方式,修复漏洞等;但坏处也很显然:可能引入代际不兼容问题,以及冗余配置问题,比如上图所示,OpenVPN刚发布时的默认加密模式是Blowfish,在现在新版本上已经默认禁用甚至逐步移除了,早期配置和客户端自然也就无法连接。

另一方面,OpenVPN的版本号其实也有些迷惑人,看起来只动了小版本号,但其实跨度非常大。比如说,2.3版本发布于2013年的1月,那时候笔者还在还在背乘法口诀表,第二天老师还要抽背唐诗;而到了2.6版本,就已经是2023年的1月25日(大年初四)了,那天笔者刚过完年在探望姥姥。你可以算算这中间差别有多大。

图源:endoflife.date图源:endoflife.date

加重这个问题的还有服务器本身的一些性质。网上大多数教程,由于历史惯性,都给基于CentOS 7或者Ubuntu 22.04这样的发行版来搭建的,考虑到以稳定为主,可能还会引入LTS版本,这就导致软件仓库中的OpenVPN版本依然很老,例如Ubuntu 22.04,笔者在网上看到过通过默认软件源安装的OpenVPN版本就有2.3.6和2.4.7这两种,如果你是用的CentOS 7,可能还会碰到2.2.2版本(笔者小学刚入学)的OpenVPN。

于是我们就会碰到:

  • 新版本怕支持的不多
  • 老教程还在被搜索引擎索引
  • 生产环境还是老版本Linux
  • 管理员怕改了引发崩溃
  • OpenVPN的向后兼容性还可以(比如上面的Blowfish,也只是逐步移除)
  • 本来就配置复杂,需要引入辅助脚本,但辅助脚本也可能多年未更新
  • 部分路由器自带的OpenVPN,因固件未更新也是老版本
  • 写教程的人,用老版本配置,『通了就走』

这些因素加起来,就导致依然有大量搭建教程还在炒冷饭,而且加热速度并不是太快。

更有甚者,因为配置相对较为容易,有朋友选择使用SoftEther VPN所内置的OpenVPN兼容服务器来搭建,这玩意对标什么OpenVPN版本,暂不清楚,但从上个月更新的README来看,他对于OpenVPN的一条注释是AEAD mode is supported in DE only.(DE指的是开发版),所以合理怀疑,稳定版里面的只会更老。

所以在2.9小节,笔者会给出一个扔掉了绝大多数历史包袱,在OpenVPN 2.6.3中测试通过的OpenVPN配置(也是作为第三大节的一系列测试实验的配置文件),并详细说明里面的每一个配置项的具体作用。不过在此之前,咱们先来看看,要手写一份配置文件,究竟难在何处。

2.2 到底啥模式哇

正如上面一节所说,OpenVPN诞生于2001年5月份。

在那个年代,菊花还只是一种花,南通还只是江苏的一个市,双子塔还竖立在纽约,看到好嗑的cp我们会叫做『荧幕情侣』,Linux内核大多数停留在2.2,甚至连Windows XP也仅仅存在于传闻中和几个测试版当中而已。

网络参差不齐,内核功能不多,TUN/TAP功能也是参差不齐,一切都是蛮荒的,OpenVPN所面对的就是这么一个环境,再加上OpenVPN的设计目标其实是场景驱动,又发展了这么多年,因此他的配置项就显得很多,各种配置的组合就更加多。

因此,我们先做一个限定,下面如无特殊说明,所有的配置文件决策,以及测试,均在此环境中进行:

服务器:

  • 硬件:i5-13600HX
  • 系统:Debian 12
  • 环境:LXC in PVE(内核6.17.2-2-pve),分配2核心
  • IP:10.0.0.160
  • OpenVPN 版本:2.6.3(OpenSSL 3.0.17)

客户端:

  • 硬件:i5-12600KF
  • 系统:Windows 10 22H2 19045
  • IP:10.0.0.11
  • OpenVPN Connect版本:3.8.0(4528)

两者位于同一个局域网,采用千兆以太网互联,平均RTT为0.4ms,网内无丢包。同时,下文的部分OpenVPN输出信息,为了方便展示,手动做了换行处理,但并不改变其意义。

这里需要补充一点:目前的OpenVPN Connect,实际内核为OpenVPN Core 3.11.3,应该可以理解为C++重写版。但从官方GitHub页面来看,只实现了客户端(的库),至于服务端,目前还是得用原版OpenVPN 2.x。重写的目的未知,但社区Wiki(的机翻版本)提到『它更注重让系统上的普通用户(非特权用户)能够启动和管理自己的 VPN 会话』,个人猜测可能还会带来性能的提升。

从理论上来说,OpenVPN Core 3兼容2.x的协议,但考虑到可能存在的性能提升,因此下面的基准测试部分不排除数据略高的问题。考虑到下面的只是定性测试和直观演示,且配置方面没有发现不兼容的地方,因此决定保留OpenVPN Connect的测试内容。

此外多说一句,和OpenVPN Connect相比,OpenVPN Community版本体积真的小太多,但代价是界面略微不太直观,因此看个人取舍吧。至于笔者,反正除了这次测试之外几乎不用OpenVPN,所以没啥所谓。

2.2.1 子网 vs P2P

那个年代,设计VPN的主要目标,是在不租用物理专线的情况下,模拟出来一根虚拟专线,因此当时的工作模型是:

  • VPN生成一条加密点对点三层链路
  • 双端配一条(或几条)静态路由
  • 原本指向专线的流量,被指向VPN接口
  • 实现了安全通信

换句话说,他就是一根网线,两头插俩终端/路由器,至于流量怎么路由,不归VPN管,典型例子就是IPSec,如果玩过商用硬件路由器,华为的也好思科的也好,配置IPSec的时候,都需要你显式指定感兴趣流/ACL,写好路由,才能正常工作。

如果你有过搭建WireGuard的经验,你就会发现,这和我们今天认知中的,基于虚拟子网的VPN拓扑结构(VPN端点承担一部分路由工作)并不太一致。事实上,OpenVPN刚诞生的时候也只支持这样的点对点模式,即使2.x版本开始支持多个客户端连上来,在程序内部也只是模拟了成吨的逻辑P2P链路而已。我们今天所相对熟悉的Subnet拓扑,是在2005年9月份发布的2.0.2版本中才引入的,甚至1.x版本的Changelogs里面根本找不到subnet或者topology等字眼。

2.0.2 引入 --topology2.0.2 引入 --topology

下文为了方便叙述起见,我们把这种模式叫做P2P模式,但为了防止混淆,需要注意的有两点:

第一点是,这里的P2P显然不代表只有两个端点可以加密通信,他更多的还是代表两个端点在网络内的拓扑属性,比如我们说『两台路由器用点对点专线连接』,显然,如果正确配置的话,路由器下面的设备也可以互相通信;

第二点是,虽然OpenVPN的--topology选项的参数值可以写P2P,但这里的P2P,指代的是『真的把TUN接口配置为点对点模式』,链路上可以按照RFC3021标准配置掩码为/31的地址段。而上文提到的OpenVPN一开始支持的点对点模式,按官方说法,其实应该叫net30模式,也就是使用/30掩码,四个地址,抽掉一个网络号一个广播地址,剩下两个可用地址,两端一人一个。

就目前来看,net30模式已经处于deprecated状态,他的应用场景已经可以被p2p模式取代,甚至如果你在2.6版本上配置为net30(或不明确配置拓扑结构),会直接弹警告:


# openvpn --config ovpn-server-net30.conf
2026-01-18 21:24:46 WARNING:
  --topology net30 support for server configs with IPv4 pools
  will be removed in a future release.
  Please migrate to --topology subnet as soon as possible.

对于一般用途,subnet模式好处更多(提高地址利用率,减少路由条目数量等),所以如果没什么特殊需求的话,用subnet模式即可,我们也采用这玩意。

2.2.2 TUN vs TAP

这里的TUN/TAP,指的是虚拟网卡的类型。可能部分读者已经知道,切换到TAP模式的OpenVPN支持二层通信,也就是说你完全可以用他来当作虚拟的以太网连接,在上面跑组播,广播,DHCP什么的,都是没毛病的。

但问题就在于,你真的需要TAP吗?

超过九成的场合下,我们所需要的『专线』,『安全通信』都是建立在三层(IP层)之上的,因为我们的设备已经习惯了处理IP数据包,互联网也是在处理IP数据包,如果还开TAP模式完整地传二层包,除了拖慢速度之外,并没有什么额外的好处。为数不多能应用到TAP的场景,可能也就是要处理二层组播广播什么的,对一般用户来说,很难碰到。

所以对于这个问题,笔者可以很老实告诉你,如果你在纠结自己需不需要TAP模式,就说明你不需要TAP模式,用TUN就好,能满足你需求的。这里举一个很神奇的例子:某国外大厂VPN,居然在应用程序里面提供了切换到TAP模式的选项(见下图):

厂商名称已打码厂商名称已打码

在厂商的官网可以发现,TAP模式的端口与TUN模式并不一致——然而这和TAP/TUN并没有什么关系,不存在说TAP能换端口而TUN不行。老实说,笔者没想明白他提供TUN/TAP切换的意义何在,如果有读者知道的话不妨告知一声?

端口不一致端口不一致

2.2.3 静态密钥 vs TLS

静态密钥,这是另一个老资历的东西了。

OpenVPN的第一个版本(0.9)只支持静态密钥。我们今天所熟知的,基于SSL/TLS的,需要证书的OpenVPN模式,其实是从接近一年后的OpenVPN 1.0才正式引入的:

这才有点正式版的味道这才有点正式版的味道

长期以来,静态密钥的应用场景一般被局限在临时测试,小规模使用,以及规避DPI检测 —— 有传闻称,在静态密钥模式下,OpenVPN不再(也无法)协商加密密钥,直接开始传输加密后的数据,在外部看来就是在传输高熵随机流量。至于这个说法是不是真的,笔者本来挺想亲自抓包考证一下的,但是在目前的测试环境下,服务端倒是可以勉强起来,但也需要修改一堆配置,还会打印一堆警告信息,提示你所有的无 TLS 模式将会在2.7版本彻底移除:


# openvpn --config ovpn-server-static.conf
2026-01-18 16:53:18 DEPRECATED OPTION: The option --secret is deprecated.
2026-01-18 16:53:18 DEPRECATION: 
  No tls-client or tls-server option in configuration detected.
  OpenVPN 2.7 will remove the functionality to run a VPN without TLS.
  See the examples section in the manual page for examples of
  a similar quick setup with peer-fingerprint.

至于客户端,折腾了半天,始终报错无法连接。我不知道是配置没写对,还是APP会额外注入一些辅助配置项,总之已经没什么动力再去进一步测试了。

看起来还是动了SSL看起来还是动了SSL

所以很显然,2026年的今天,别搁这折腾静态密钥了,要论规避DPI你有更好的选择,要论配置简便请直接到2.9小节抄作业,或者按照官方的说法去尝试使用新的对端指纹模式,不论怎么看都好,静态密钥已经没有生存空间了,还是用TLS模式吧。

2.2.4 ncp-cipher vs data-cipher

如果你综合这几个版本的配置来看,关于如何设置OpenVPN的数据通道加密算法,就有如下几种配置项:

cipher <算法>
ncp-ciphers <算法1>:<算法2>...
data-ciphers <算法1>:<算法2>...
data-ciphers-fallback <算法>

这就导致很多人想手写一份配置文件,却不知道怎么样使用这几个参数,又或者即使是使用了,也有可能是误用,或者因为优先级和向下兼容等原因,导致未能正确选择所需的加密算法。

开始叙述之前先一句话总结:在2.6版本中,你只需要使用data-ciphersdata-ciphers-fallback这两个,比如说,你可以写:

data-ciphers-fallback AES-256-CBC

对,只写这一个都行,塞进去2.6版本的软件里能用的,而且安全性也还行。如果你确定不会有(包括)2.3版本之前的客户端连上来,也就是不存在『无法协商加密算法』的情况下,那么连这一项都可以不用写。

原因很简单:根据2.6版本的手册data-ciphers如果不明确设置,其默认值是AES-256-GCM:AES-128-GCM,如果底层库提供了对 Chacha20-Poly1305 的支持,就会自动变成AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305,不需要管理员操心。当然了,因为列表中的第一个算法会被优先推送到客户端,因此如果你更钟爱某个算法,可以往前排。

现在就可以说说剩下那俩是咋回事了。

cipher:本来是用于在2.4版本之前,确定数据通道的加密方法的,因此写什么用什么。但在新版本当中,已被弃用,目前还保留支持,仅仅是因为在非TLS模式下还会用到他,但正如上文和手册所说,2.6版本在TLS模式下会忽略此设置,2.7版本直接移除非TLS模式,那么这个选项也没什么作用了。

ncp-ciphers:2.4版本开始,OpenVPN支持协商加密算法了,于是就用这个值来确定可用的算法列表。如果你觉得眼熟,是的,根据这个Patch,这个在2.4版本刚刚加入的配置项,就在2.5版本中正式更名为data-ciphers,就是我们上面提到的那个,所以你可以放心地替换为新命名。

2.3 哪来的 proto tcp 呐

虽然说在把可靠传输层包在另一个可靠传输层里面(如上网时使用的TCP,包裹在OpenVPN TCP中)并不是一个好主意,原因有很多,比如经典的TCP Meltdown以及队头阻塞问题。但如果你确实想用,OpenVPN也不会拦着你,在配置文件里面写明白就好。

问题来了,早年间部分OpenVPN教程,可能会出现这样的配置:

proto tcp
# ...其他配置

而另一部分教程会写:

proto tcp-server
# ...其他配置

我们结论很简单:第一种配置会引起混淆,已经不推荐了。也许这样的写法是早年间的配置方式,目前还保留兼容性支持,但保不准未来会引发什么问题,所以尽量避免。其实如果你翻阅2.6版本手册,你会发现proto项可选值只剩下3种:udptcp-servertcp-client(当然还包括另外六种,用来强制指定使用IPv4或者v6的,因原理一样,不再赘述)。显然,并没有出现proto tcp这种写法。

但如果你真的这么写了,正如上文所说,在部分场景下系统可以正常工作,如这份配置:

proto tcp
server 10.68.25.0 255.255.255.0
# ...其他配置

OpenVPN可以从第二行里面推出来,你想要创建一个服务器。但如果你去掉第二行,或者把第二行的server改为ifconfig,OpenVPN就无法再推断你想要的工作模式,会报错退出:

# openvpn --config ovpn-server-tcp.conf
Options error:
 --proto tcp is ambiguous in this context.
 Please specify --proto tcp-server or --proto tcp-client

所以,在TCP模式下还是写明白tcp-server/tcp-client比较好。

2.4 还守着 RSA 嘛

想要安全通信,我们很自然能想到对数据进行加密和校验处理,而且你可能已经了解过,OpenVPN建立在SSL/TLS之上,而SSL/TLS混合采用了对称加密和非对称加密。所以这一小节我们先来讨论非对称加密。请注意,笔者不是密码学的专家,因此下面的内容不能作为严谨的密码学技术讲解来看待,如有需要可自行参阅其他资料。

计算机课的知识告诉我们,比较典型的非对称加密算法是RSA,它依赖于质因数分解问题高困难性。也就是说,你把两个大质数乘起来很简单,但如果给你一个数,让你拆开成两个质数的乘积,就显得非常困难。我们经常看到RSA-512,RSA-1024,RSA-2048这样的说法,后面的数字指的就是『那两个大质数』的乘积在二进制下的长度(称之为密钥长),单位为Bit。

计算机科学中尚未解决的问题:
整数分解问题,能否在经典计算机上,以多项式时间求解?
—— Wikipedia

虽然这个问题至今没有解答,但实际上,已经有不少团队通过暴力分解等方式,配合逐步提高的计算机算力,证明了可以在合理的时间内对一定长度的大质数进行分解。部分朋友可能听说过RSA 因式分解挑战,虽然官方已经在2007年停办,但依然拦不住人们继续尝试,目前的破解记录是成功分解了RSA-829。虽然说远不及目前推荐的加密强度,而且也并不一定意味着确实存在高效解法,但显然,既然破解记录正在被不断打破,如果要传递一些机密信息,那么还是越安全越好。

对RSA来说,要想提高安全性,只需要加长密钥即可,所以你会看到现在经常有说法『不推荐1024,至少2048,4096及以上更好』,便是这个原因。不过你也许已经知道了,加大这个数字意味着生成密钥对和加解密的速度都会变慢,尤其是生成密钥(私钥),这种慢是以指数形式递增的。为了直观演示,笔者在服务器上跑了一个测试:

# time openssl genpkey -algorithm RSA -out 2048.pem -pkeyopt rsa_keygen_bits:2048
real    0m0.166s
user    0m0.127s
sys     0m0.012s

# time openssl genpkey -algorithm RSA -out 4096.pem -pkeyopt rsa_keygen_bits:4096
real    0m1.668s
user    0m1.663s
sys     0m0.005s

# time openssl genpkey -algorithm RSA -out 8192.pem -pkeyopt rsa_keygen_bits:8192
real    0m15.208s
user    0m15.200s
sys     0m0.007s

很显然,在长度达到8192的时候,生成时间已经来到了恐怖的15秒,这还是CPU单核性能足够强大的情况下测量出来的,如果说是低主频多核心的服务器CPU,恐怕生成4096都要花费不少时间。就算你能忍受这些时间,因为加长密钥就意味着密钥被加长(废话....),如果你真的执行了上面那些命令,现在请你把终端全屏,然后cat一下刚生成的几个私钥文件,你就会明白我说的是什么意思了。

# wc -l 2048.pem 4096.pem 8192.pem
   28 private_key.2048.pem
   52 private_key.4096.pem
  100 private_key.8192.pem
  180 总计

为了解决这个矛盾,数学家和密码学家开始捣鼓,然后发现了一个非常好用的方案,叫做ECC(椭圆曲线密码)。因为笔者的数学功夫实在是不过关,甚至目前在备考专升本但已经确定要牺牲掉高数靠其他科目拉分,所以没办法具体解释其工作原理。不过很显然的一点是,ECC可以用较小的密钥长度实现RSA较大密钥长度的安全性。比如有一条曲线叫Curve25519的,其衍生签名方案Ed25519在256位密钥长的情况下理论上提供相当于RSA 3072的安全性。这个密钥小到什么程度呢,可以搓一个给你看:

# openssl genpkey -algorithm Ed25519 -out - | cat
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIEAHLueVF/8Qkl7XIf0SqGo9gHTvB8UlC5JaU464oYB7
-----END PRIVATE KEY-----
# 没了!就这么多

既然密钥小了,计算速度也会快很多,后续扩展也会轻松很多,比如觉得256安全性不够,可以把长度提升到512及以上,对于CPU来说依然是微不足道的算力需求,但理论上就可以提供相当于RSA 15360的安全性。

所以这几年开始,新出/新升级的协议都开始转向ECC,毕竟又快又安全,谁不喜欢呢?

2.5 算啥 DH 参数啊

本来想不分节的,但写了一大半发现,还是分开比较好,毕竟要理解DH参数还是需要讲一下前置知识的。如果您想直接知道结局方案,请翻到2.5.3小节。

2.5.1 DH的作用?

上面讲的是非对称加密,而计算机课的知识告诉我们,非对称加密最大特点是速度慢,实际通信的数据量是很大的,非对称加密无法胜任。因此,我们可以用非对称加密来安全传递密码,再用这个密码,调用对称加密算法,进行高速安全通信。

info:名词定义

虽然从理论上来说,这里使用『密码』一词并不严谨,但笔者确实不想每次都打『对称密钥』这几个字,因此这里做个定义。

那么问题来了,密码要怎么传递呢?计算机课上,老师可能会说,我们可以这么做:

  1. 随机生成一个密码
  2. 用对方RSA公钥加密,发给对方
  3. 对方用RSA私钥解密,获得密码
  4. 双方用这个密码,配合对称加密算法,进行加密通信

听起来很美好,而且理论上确实能用,因为攻击者只能监听到到公钥加密后的内容,又因为缺乏私钥而无法解密(这里暂时不考虑中间人攻击的问题,这是另一个层面上的安全考虑了)。但不确定你有没有想过一个问题:如果对方私钥泄露了,咋办?

显然,攻击者可以使用泄露的私钥,来解密上面传递的密码,进而窃听/篡改通信内容。这就是我们说的,传统RSA密钥交换不具备前向保密性(PFS),说人话就是,一旦私钥泄露,过往所有通信内容都可以被解密。

为了改进这个问题,一个较好的密钥交换算法(Key Exchange,简称KE)需要考虑两个方面:

  • 不传输密码:直接不让密码(包括加密后的)出现在链路上,双方只能结合私钥等不公开的信息,进一步计算,才能得知具体的密码,窃听者光靠监听链路上的信息,根本无法获取密码
  • 临时密钥对:每次进行密码交换时,都生成新的公私钥对,用完就扔,这样即使泄露一对公私钥,也仅仅是本次通信受影响而已,之前和之后的都不会有问题

这两点一结合,就出现了改进的KE算法,就是大名鼎鼎的DHE。这玩意是Diffie–Hellman 密钥交换的一种变体,靠DH解决了第一个问题,而末尾的E(Ephemeral,临时的),就解决了上面提到的第二个问题。

出于历史原因,DHE一般很少单独使用,一般和RSA组合使用,就出现了DHE_RSA算法。这种情况下:

  • 服务端长期持有RSA密钥对
  • 建立会话时,临时生成DHE密钥对
  • 用RSA私钥对其(以及我们下面要提到的DH参数)进行签名
  • 发送给客户端

也就是说,RSA的作用,从传输密钥,变成了身份验证,客户端只需要用正确的服务器公钥进行验签,就知道发过来的DH参数是否被篡改,服务器是否有人冒充了。此时泄露RSA私钥,风险就变成了『有人可以冒充服务器』,而并非『通信内容可以被解密』。

2.5.2 为什么要DH参数

从上文我们可以看到,要想通过DHE进行密钥交换/协商,服务器需要准备的东西,除了DHE密钥对之外,还有一个被称之为DH参数的东西,这就是本小节的重点。

继续阅读下去之前,不妨先开一个终端,挂一旁,执行这条命令:

# 熟悉命令的朋友,可能会注意到,我故意漏了一个参数,
# 当然这里先卖个关子。如果你知道漏了什么参数,
# 那么你也应该可以猜到,下面我想说什么
time openssl dhparam -out dhparams.pem 4096

好,我们继续。

这里所谓的DH参数,其实包括两部分,一般被写成(p,g),后面的g表示生成元,一般比较小,因此我们可以忽略不管,现拿一个就行。重点来看前面的p,我们在OpenVPN中看到的设置项dh,nginx中被人遗忘的设置项ssl_dhparam,包括上面执行的命令所生成的东西,其实指的都是这个p其形式是一个大质数。

DH算法就是依靠这个大质数,来和对端协商密钥的,你可以理解为这就是生产原料,少了这个原料就无法生产出密钥来。这些参数无需保密,甚至可以完全公开,所以上文才会说RSA仅仅用来签名,而不是把这一大坨东西加密起来。

不过,DH参数不是随便什么阿猫阿狗都能胜任的,除了必须是质数之外,最基础的要求就是足够长,它的长度直接决定了DH算法的安全性。这里的足够长是多长呢?自从2015年Logjam攻击被发现之后,这个长度就变成了至少2048Bit,如果对安全性有更高要求,就得是上面写到的4096Bit。

生成并验证一个大质数,是计算密集型操作,说人话就是需要耗费大量的CPU性能和相当长的时间。所以在正式应用中,应用程序不会在运行时才创建这玩意,而是需要管理员手动生成一份DH参数,填入到配置项中。此外,有一定数学基础的朋友都知道,目前人们还没发现有某个神奇的公式可以直接一步到位生成质数,因此目前普遍的生成方法是:

  • 随机生成一个奇数
  • 看看它是不是质数
  • 如果不是,要么回到第一步,要么对这个数加2并回到第二步

不论是听起来,还是做起来,这个方法都很暴力,但事实就是如此。此外更惨的一点是,第二步的操作,虽然理论上可以用试除法,也就是2到sqrt(n)判断其能否整除n,但对于DH参数要用到的长度来说,试除法的效率是很低的,因此目前更多的还是采用类似Miller-Rabin素性测试之类的方法,来判断一个数字是不是『可能是个质数』,通过不断调整测试参数,如果反复多次都能通过,那么就可以认为这个数是质数了——当然,这依然不能排除实际上并非质数的可能性,不过计算机的世界里其实并没有那么多精确的东西,如果你真碰到一个通过了几十上百次测试,却发现最后结果是个合数的,笔者建议你赶紧去买张彩票。

看看你被你挂一旁的那个终端,出结果没?

如果没出结果的话,你应该能体会到『预先生成DH参数』这一步操作到底有多痛苦了,虽然不是你在算,但却实打实占用了机器性能和宝贵的时间。笔者写这段文字的时候,确实后台开了一个终端在跑这个,最后得到的结果是:

real    10m33.266s
user    10m33.166s
sys     0m0.044s

冬日暖手宝,酸爽不酸爽?

最后,我们揭晓答案,上面漏掉的参数是-dsaparam。启用这个选项之后,OpenSSL会更换生成方式,产生所谓的『DSA风格』参数,并自动转换为DH参数,而这个过程比默认所采用的方式要快非常多,生成4096位参数,也只需要几秒钟时间,而且安全性并没有明显下降。

当然了,笔者连这几秒钟的时间都不想浪费,怎么办呢?

2.5.3 椭圆曲线,救我狗命

椭圆曲线这个好东西,再一次降临了。

这次他带来的东西是ECDHE,正如你所猜想的那样,DHE就是上面的DHE,前面的EC就是椭圆曲线的意思。

有了ECDHE之后,我们再也不用费力生成DH参数了,因为ECDHE的参数(椭圆曲线方程、基点、阶数等)是公开的,经过严格审计,而且非常简短,简短到可以内置于各个加密库中,且仍不失其安全性。

OpenVPN于2.4版本引入了ECDHE,我们用的是2.6版本,因此我们可以这么写配置:

# 这是针对TLS 1.2的,你可以加上或删除其他支持ECDHE的协议
# 执行 openvpn --show-tls 查看完整列表
tls-cipher TLS-ECDHE-ECDSA-WITH-CHACHA20-POLY1305-SHA256:TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA256
dh none

指定ECDHE算法之后,DH配置可以直接写none,也不需要再费时费力生成DH参数了,好耶。

2.6 DCO 不挺好吗

OpenVPN长期以来被诟病的一点就在于他速度慢。根据其他博主的分析来看,他的慢有可能是因为线程问题(只能吃单核),也有可能是反复在用户态和内核态切换的问题,不知道具体是什么情况,可能两者兼备。

不过,在Linux Kernel 6.16,也就是几个月前,OpenVPN Data Channel Offload(简称DCO)被合入了内核中。从字面意思来看,DCO的功能就是把数据通道的加解密交给内核态来处理,理论上来说会提高一点性能(但据说还是不如WireGuard)。

为了测试这一点,笔者开了台VM(因LXC无法动内核),运行Debian 12,分别在有DCO和无DCO的情况下进行了对比测试。安装教程网上一搜一大把,这里不再赘述,顺带一提,DCO模块体积挺大的,安装时要注意。

接近800MB接近800MB

另外还要注意,PVE开UEFI虚拟机,会默认开启安全启动,导致DKMS无法安装。因为这里是测试环境,笔者选择重启进入固件,关闭安全启动,但如果你是在生产环境安装,建议按正确方法对模块签名处理后安装。

具体设置位置需根据不同固件而调整具体设置位置需根据不同固件而调整

首先先看没有DCO的情况,CPU占用率还是挺高的(因为跑满了千兆内网),而且能明显看到是用户态进程在占用CPU。

接着去掉--disable-dco选项,并确保你所选择的加密方式(--data-ciphers)是DCO所支持的方式。开启DCO选项后,查看OpenVPN日志,应该能看到DCO版本号,以及打开DCO设备的提示:

如图所示如图所示

启动DCO之后,htop看不到用户态在占用CPU,需要按下Shift+K,取消隐藏内核态程序,就可以看见不同了:

速度倒是没啥变化速度倒是没啥变化

可以看到,总体的CPU利用率有所降低,去掉iperf3的占用之后就显得更低了。

所以DCO有没有用?有,其实还真的有点用,如果你在给你公司或者别的什么地方搭建OpenVPN,又或者你在性能较弱的设备上搭建,那么笔者还是建议你装一个DCO,要么能降低占用率,要么能提升速度,要么都行,还是挺好的。

2.7 IPv6 空间咋这么小?

现在是IPv6的年代,那么我们自然是想要让OpenVPN里面也能传递IPv6数据包的。

正好,你查了查文档,发现可以这么写:

server 10.68.25.0 255.255.255.0
server-ipv6 2001:db8:cafe:1145::/64

保存好配置,然后启动OpenVPN,却发现控制台里面打印出来这么一条信息:

# openvpn --condig ipv6.conf
NOTE:
  IPv4 pool size is 253, IPv6 pool size is 65536.
  IPv4 pool size limits the number of clients that can be served from the pool

看起来只是一条平平无奇的提示,但却很奇怪:为什么IPv6地址池空间只有65536呢,不应该是2^(128-64)吗?

其实原因很简单:OpenVPN的IPv6支持,是有人为限制的。

打开OpenVPN 2.4的Changelog,进去就能看到,2.4.10版本新增了一条限制,把IPv6的可用最大地址数量限制在了65536:

甚至还排在第一位甚至还排在第一位

为了佐证这一点,不妨来查看OpenVPN的源码,可以找到更为直接的证据。

定位到src/openvpn/pool.c,往下滚动到230行附近,可以看到这么一段代码:

pool_ipv6_size =
   ipv6_netbits >= 112 ? (1 << (128 - ipv6_netbits)) - base : IFCONFIG_POOL_MAX;

相对应地,在src/openvpn/pool.h中,我们可以找到这个常量的定义:

#define IFCONFIG_POOL_MAX 65536

以上这段代码可以这么理解:如果前缀长度小于112,那么就被钳位在65536(正好就是2^(128-122)),大于这个数,才实际计算能用的地址空间。所以上面的示例地址2001:db8:cafe:1145::/64,实际上变成了2001:db8:cafe:1145::/112

实际上,这个限制在Patch 1136中被引入,根据作者的说法,增加这个限制是因为本来IPv6的地址空间就老设计中的IPv4地址池限制在这个大小(如上文提示信息所见,限制以小的为准),但现在OpenVPN允许IPv6-Only,失去了v4的约束,容易引起malloc()调用异常,所以为了规避这个路径依赖问题,于是人为地加上了这个限制。

需要注意的是,此处影响的仅仅是可以分配给客户端的IPv6地址数量,而并非可以通过隧道传输的IPv6地址限制,也不是连接到IPv6 OpenVPN服务器的限制。

其实可以认为,OpenVPN在三层模式下对于客户端数量是有一定限制的,存在六万多个客户端的硬上限(每个客户端一个/32的v4,一个/128的v6)。不过老实说,你都六万多个客户端了,一人收10块钱都有六十多万,去招个人想想有什么好技术办法呗,别让OpenVPN硬扛,不然它和服务器早晚得炸一个。

2.8 咋和 HTTPS 不像呀

这一节其实严格来说和配置关系不大,但既然来都来了,我们还是提一嘴比较好。

上面我们说过,OpenVPN是基于SSL/TLS的,现在已知HTTPS也是基于SSL/TLS的,我相信应该不止笔者一个人想过,如果我把端口调节为443,再开TCP模式,那是不是看起来和HTTPS差不多了呢?

不只是一般人,VPN厂家也是这么宣传的不只是一般人,VPN厂家也是这么宣传的

不过很抱歉,答案是否定的。要想证伪这个问题,只需简单的抓包即可。

WireShark内置了协议识别器,虽然有时候要求是在特定端口上才会调用对应的识别器,比如本例中的OpenVPN,必须使用1194端口,但很显然,对于更高级别的DPI审查设备来说,显然不存在『只有1194端口才会被识别』这个问题。

从结果中可以看到,TCP三次握手之后,WireShark就被捕捉到了客户端发送的,首字节为操作码0x38的控制信息,再结合后续的一些控制信息,可以很轻松判定目前的流量为OpenVPN流量。

作为参考,我们再来看一个正常的TLS握手:

可以看到,客户端发送的首字节为0x16,表示TLS握手,随后就是标准的TLS 1.3 Client Hello,WireShark也能正确读取到SNI信息。

这个简单的小实验可以看出,光靠TCP 443,OpenVPN可以绕过简单的四层防火墙,比如部分神奇的网络只允许53/80/443的TCP/UDP流量的那种。但显然,无法骗过真正的DPI设备(不然他也不叫『深度包检测』了)。顺带一提,进行这个实验的时候,tls-crypt处于开启状态,且已经正确配置了密钥,这也刚好证明了另一个观点,即tls-crypt无法保护流量不被DPI设备检测到。

如果你确实想要用OpenVPN来做此项用途,倒也不是不行,但你必须权衡利弊。此部分内容会在下面的3.2小节详细讨论,在此之前,我们需要先有一个能用的OpenVPN环境。

2.9 最终配置

OpenVPN配置可以拆分为四大项:

  • CA证书
  • 服务器证书,客户端证书
  • 服务器tls-crypt密钥
  • 两端的配置文件

按照以下步骤操作,你就可以获得和笔者同样的测试配置文件,至于运行环境和OpenVPN版本等信息,上面最开始已经提及,此处不再赘述。

2.9.1 CA证书

OpenVPN基于SSL/TLS,这其中最重要的东西就是CA证书,它是我们其他所有证书的信任根。

由于VPN服务器在现实场景中一般作为安全接入点来使用(比如授权人员通过VPN接入内网),考虑到服务器会暴露在公网,所以理论上为了安全起见,以下命令不应该在OpenVPN服务器上执行,最好是需要一台单独的签名机,防止CA私钥意外泄露,导致攻击者可以借此签署证书来实现身份伪造。不过我们现在只是测试环境,所以没什么所谓了,下面的操作假定你都在OpenVPN服务器上进行,知道一下有这个概念就行。

OpenVPN附带了一个名为easy-rsa的脚本来辅助生成,但本着知其然还要知其所以然的观点,笔者采用了手动调用OpenSSL的方式来生成。所以需要注意的是:笔者不是制作证书的专家,因此虽然尽力而为,但下面的配置不一定保证能满足高安全性需求的场合,毕竟没有经过审计。如果您有此类需求,建议查阅OpenSSL文档或相关生成标准,或直接使用easy-rsa(注意相应修改配置,以便生成Ed25519证书)。

# 生成Ed25519私钥
openssl genpkey -algorithm Ed25519 -out ca.key

# 生成一份配置文件,这样可以省去命令行交互的麻烦
# 请相应修改dn小节的值
# 比如 CN 字段的值,可以换一个更有意义的名字
cat <<EOF > ca.cnf
[ req ]
distinguished_name = dn
x509_extensions = v3_ca
prompt = no

[ dn ]
C = OV
ST = OV
L = OV
O = OV
OU = OpenVPN
CN = OpenVPN CA

[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:TRUE
keyUsage = critical, keyCertSign, cRLSign
EOF

# 最后利用上面的配置文件,对私钥进行自签名
# 因为此时是单机完成,所以我们直接用req命令出结果
# 设定有效期为3650天(10年),你可以根据需要进行调整
openssl req -new -x509 -key ca.key -out ca.pem -days 3650 -config ca.cnf

执行完上面的命令后,你应该可以看到目录下出现了ca.key和ca.pem这两个文件,为了安全起见,再次强调,ca.key务必需要保密。

2.9.2 双方的证书

接着,我们使用已经准备好的CA证书,来生成服务器证书。

# 生成Ed25519私钥
openssl genpkey -algorithm Ed25519 -out server.key

# 生成签名请求(req文件)
# 同样,请酌情修改后面的信息,尤其是 CN 字段
# 因为OpenVPN可以配置为校验对端的 CN 字段
# 来确保自己连接到了正确的对端
# 理论上,你应该在生成了req文件后,把他传输到签名机,进行签名
openssl req -key server.key -new -out server.key.req -subj "/C=OV/ST=OV/L=OV/O=OV/OU=OV/CN=OpenVPN Server"

# 如果你是分开单独的签名机,下面的命令请在签名机运行
# 如果你和笔者一样一台机器搞定,那么继续运行即可

# 首先是生成extfile
# 这个文件的内容较为重要,我们将在下面详细讨论
cat <<EOF > server.ext

authorityKeyIdentifier=keyid,issuer
basicConstraints = CA:false
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = openvpn.server

EOF

# 最后是签发证书
# 请酌情调整有效期等信息
openssl x509 -req -extfile server.ext -in server.key.req -CA ca.pem -CAkey ca.key -out server.pem -CAcreateserial -days 3650

这里重点提一嘴上面的extfile,重点是要了解几个字段的作用:

  • basicConstraints:目前不是制作CA证书,所以必须设置为CA:false
  • keyUsage:至少应当有digitalSignature,才能使用。至于keyEncipherment,有资料称对于ECDHE而言已经不需要这么操作,笔者未求证(因为也是后来才看到的),因此没有做测试
  • extendedKeyUsage:简称EKU,必须为serverAuth,表示这是个服务端证书,OpenVPN是会校验EKU的
  • DNS.1:这反而是最宽松的一项,OpenVPN默认不校验SAN,也就是不会校验实际连接地址与这里的对应关系。当然如果你确定有域名,写一致的也无妨就是了

执行完毕后,我们可以得到server.key与server.pem,同样,server.key需要保密。

有了制作服务器证书的经验,要想制作客户端证书也就很简单了,我们只需要修改:

  • 文件名:换成client(或其他名字)
  • CN字段:请务必务必务必确保每个客户端的CN字段都唯一,因为CN字段在OpenVPN中常被当作客户端或者服务器的逻辑ID,包括你看审计日志,下发策略,client-config-dir的匹配,都是借助CN字段来完成的,而且OpenVPN默认情况下也不允许同一个CN的并发连接(duplicate-cn是危险选项),所以一定要做到不同设备不同CN。
  • extendedKeyUsage:请修改为clientAuth

同样地,执行完毕后,就可以看到客户端使用的证书与私钥了,请把他们安全地传输到客户端。每次新增客户端,只需要修改CN字段,生成一套新的客户端证书和私钥,就可以了。

2.9.3 服务器tls-crypt密钥

这个相对简单,只需要在同一个目录下执行:

openvpn --genkey --secret tc.key

就可以了。

2.9.4 两端的配置文件

新建一个server.conf,填入以下内容(可以去掉注释):

# 网络选项,根据需要来填写即可
# 如果需要tcp,请改为proto tcp-server
proto udp
port 1194
dev tun

# 确保断连时不会反复开关tun
persist-key
persist-tun

# Linux服务器上请如此设置
# 就是告诉OpenVPN服务器,在运行后尽快退到低权限用户
user nobody
group nogroup

# 我们需要子网拓扑,并指定所用子网范围
# 注意这里的server配置项,其实就是告诉OpenVPN这里作为服务器使用
# 对于服务器的网内地址,默认是此范围内第一个可用主机地址
# 在本例中,就是 10.68.25.1,以及 fd07:19:10:29::1
topology subnet
server 10.68.25.0 255.255.255.0
server-ipv6 fd07:19:10:29::/64

# 此处指定我们的证书的路径
# 注意 dh 选项必须存在,否则会报错,设置为none即可
ca ca.pem
cert server.pem
key server.key
dh none

# 这是刚刚制作好的tls-crypt密钥
tls-crypt tc.key

# 我们要求最低TLS版本为1.2,且强制选择ECDHE-ECDSA系列算法
# 这样配置其实也兼容TLS 1.3客户端
# 因为1.3密码套件就三个,所以无需额外设置
tls-version-min 1.2
tls-cipher TLS-ECDHE-ECDSA-WITH-CHACHA20-POLY1305-SHA256:TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA256

# 只需设置回退即可,默认会使用AEAD系列算法的
# 有额外需求,再添加 data-ciphers 配置
data-ciphers-fallback AES-256-CBC

# 从理论上来说,只要你选择了AEAD加密算法,此处选项也没啥意义
# 但按文档说法,tls-auth会使用此值来确定算法
# 考虑到不确定tls-crypt是否有类似行为
# 以及上面回退到CBC这个非AEAD加密的可能性
# 因此保留此选项
auth SHA256

# 确定远端证书的EKU为clientAuth类型
# 理论上去掉这行不影响通信,但安全性可能略显不足
remote-cert-tls client

# 保活数据包,防止NAT映射取消等问题
keepalive 10 60

# 用于在 UDP 模式下,本端退出时通知对端
# TCP下,此设置会被自动忽略,所以问题不大
explicit-exit-notify 1

保存好以上文件,作为服务器配置。

接着,新建一个client.conf,填入以下内容:

# 如果没有明确设置 --pull 就需要加上 client 选项
client

# 下面的配置,含义同服务端一致
dev tun
proto udp
persist-key
persist-tun

# 远端地址与端口
remote 10.0.0.160 1194

# 一直尝试重新解析域名
# 本文件中服务器地址为IP,因此不影响
resolv-retry infinite

# 不要绑定到特定的接口/地址
nobind

# 为了方便起见,我们可以内联证书信息
<ca>
# 内联CA
</ca>

<key>
# 内联私钥
</key>

<cert>
# 内联证书
</cert>

<tls-crypt>
# 内联 tls-crypt 密钥
</tls-crypt>

# 含义同服务端一致
tls-version-min 1.2
auth SHA256

# 校验对端证书类型是否为serverAuth
remote-cert-tls server

# 校验服务器证书的CN字段,确保连接到正确的服务器
verify-x509-name "OpenVPN Server" name

# 日志级别
verb 3

保存好此文件,然后安全地传输到客户机,导入到VPN客户端中,点击连接,你就获取到了一条安全快速的OpenVPN链路。

上面的搭建过程只是一个基础,如果有更多需求,如推送其他路由,用户名密码认证等,请参阅OpenVPN文档。

3. VPN 大战 弱网

搭建好一个能用的链路之后,自然是想整些活,看看他的实际表现如何了。

在服务端安装iperf3,监听在5201端口。同时,考虑到我们在很长一段时间内,上网流量都是以TCP为主,因此就用iperf3默认的TCP模式进行测试。

实际测试下来,其实收获颇丰,因为这是用一次切身实际的经历,来直观感受到了笔者之前的几个断论:

  • 为什么在跨境链路上维持一条TCP连接不是件好事
  • 为什么混淆层很多情况下都导致严重降速
  • 为什么 OpenVPN TCP 不被推荐

当然需要注意的是,以上几点问题锅不在OpenVPN,甚至不在VPN,其实是TCP的常态,不论你是OpenVPN,WireGuard,IKEv2,甚至你真的去租用运营商的MPLS专线,或者裸纤,都会碰到这些问题。

3.1 基准速率

首先,我们需要在无干扰千兆内网下测量基准速度,避免因设备问题导致后期评分的瓶颈。

先看UDP模式,获得了挺不错的速度,八百多兆很不赖了。

UDP模式UDP模式

TCP模式速度也不错,这就排除了TCP相关代码存在bug导致所谓的『TCP慢』的可能性。

TCP模式TCP模式

不过,千兆无干扰内网毕竟还是太理想化了一点,现实情况下的互联网条件当然没有这么好,那又会怎么样呢?

3.2 如果在冬夜,一个数据包

结论放在前头:公网上的延迟抖动与丢包,才是导致所谓的『VPN慢』的真实元凶。

Linux下有个很好用的工具叫tc,是用来给数据包整形用的,正好可以用来模拟链路情况。为了模仿现实中的网络情况,笔者划分了4个等级:

  • 10ms延迟,1ms抖动
  • 75ms延迟,3ms抖动
  • 150ms延迟,5ms抖动,0.5%丢包
  • 180ms延迟,10ms抖动,2%丢包,25%丢包相关性

分别对应较好的省内链接(或广州到香港的极佳跨境连接),到新加坡的连接,到美国优化线路的连接,以及到美国拥堵线路的连接,来模拟常见的一些环境。需要注意的是,上面的几个对应概念仅供参考,实际情况下并非固定一一对应,比如你所在的地方WiFi信号很弱,那么即使是连接到省内站点,也很容易因为WiFi的问题导致延迟和丢包率飙升。

当然,还是免责声明:以下测试并非严谨,只能作为直观感受和定性分析使用,甚至笔者在模拟过程中也发现了类似问题,可能是因为服务端并没有针对性调优,导致测量出来的数据有点难反映生产环境的实际状况。如果您真的有实际部署需求,笔者建议安排一下实地测试。

3.2.1 勉强还行的UDP

在多数国外大厂VPN的宣传文案中,都能看到『UDP速度快,适合流媒体和游戏等用途』的说法,老实说,这个说法还不赖。

先上测试图:

『省内链路』,速度下降到150M附近,虽然不如千兆,但看个4K流媒体甚至4K原盘,还是没有一点问题的

10ms-1ms10ms-1ms

引入75ms延迟,可以明显看到前段有速度爬升的迹象,正好对应了内层iperf3的TCP的慢启动过程,最后速度稳定在40M上下,看高清视频还是没啥问题的。

75ms-3ms75ms-3ms

延迟提升,且引入丢包之后,速度开始变得不稳定,反复跳变,这就印证了之前关于TCP丢包重传问题的论点。

150ms-5ms-0.5%150ms-5ms-0.5%

引入更严重的丢包和延迟之后,速度反而略有改善,这可能是因为还引入了25%的丢包相关性(更贴切真实链路),让内层TCP偶尔能争取到一小段不丢包的提速空间。

180ms-10ms-2%-25%180ms-10ms-2%-25%

3.2.2 惨不忍睹的TCP

如果说UDP还看得过去(最慢速度还能开网页)的话,坐稳了,接下来的TCP才是真正的好戏开场。

笔者在之前的文章里讨论过TCP的队头阻塞和他在高延迟高丢包链路里的瓶颈问题,现在我们让OpenVPN改成TCP传输模式,除了会引入这两个问题,还会引入更严重的TCP Meltdown问题。关于这个的理论,我们已经讨论了不少,今天补一个演示,感受一下实际状况。

先来看『省内链路』,外层TCP的情况下,速度已经开始下降得比较严重了。

10ms-1ms10ms-1ms

引入延迟,可以发现速度跳变更厉害,从4M到23M,而且慢启动的速度也拖长了不少。

75ms-3ms75ms-3ms

引入丢包,速度基本归零,打开网页困难。

150ms-5ms-0.5%150ms-5ms-0.5%

恶劣链路环境下,速度跟上面差不多,也是勉勉强强打开网页的状况。

180ms-10ms-2%-25%180ms-10ms-2%-25%

如果你买过国外大厂VPN,不论哪家的都好,想必一定感受过这样的链路吧。即使速度能起来,也不要高兴太早,当你实际浏览网页的时候,TCP的队头阻塞还在等着你呢。

3.2 外层混淆?

长期接触这个的朋友,肯定多多少少听说过混淆(也有叫Stealth的)的事情,比如说OpenVPN over SSH/Stunnel/WSTunnel/obfs4。

比如这个,笔者在多年前用过比如这个,笔者在多年前用过

不论是在哪里看到的资料都好,一般都会注明,『混淆层会减慢速度』,但减慢多少呢?

同样是先把结论放前头:可能比你想象的要多很多。

大多数混淆层,只能处理TCP流量,也就是说你得使用OpenVPN(或者其他任何一种三层隧道协议)的TCP模式,这就已经慢了一层。此外,还有一个猜测:此时OpenVPN无法感知到链路的实际状况,一直都是本地loopback到混淆器,可能内部的部分调优机制(如果有的话,笔者并未研读源码)不会工作,导致慢上加慢。

笔者手头并没有安装Stunnel或者WSTunnel,但SSH却是系统自带的。众所知之,ssh -L可以实现加密端口转发,所以混淆层的测试交给他来进行。

首先同样是基准速度,我们直接给他安排最恶劣的链路环境(180ms那个),来表示问题瓶颈不在SSH加密转发上:

SSHSSH

80多兆,还行吧,而且同样的恶劣链路环境,这个速度可以说是吊打上面的OpenVPN UDP。具体原因笔者并不是很清楚,但一个可能的方向是,此时Linux的TCP协议栈直接对外,能更准确地针对链路进行优化/观测操作,丢包也能更准确地反映到某个段,而不是封装后一丢丢一片。到底是不是这样呢?还望各位大佬在评论区指点小弟一二。

接着让我们打开OpenVPN,注意需要修改配置文件,把remote改为127.0.0.1,并加上route-nopull配置,或者明确添加指向服务器真实地址的静态路由,防止流量回环。

此外,这里略微放点水,把链路环境调整到75ms延迟3ms抖动。在上面我们测试裸TCP的时候,这里的速度大概是10到20Mbps,看视频有些勉强但也不是不行。现在我们再来看看加上混淆层会变成什么样子:

OpenVPN + SSHOpenVPN + SSH

可以看到,有几个时刻速度直接归零了,即使不为0,速度也也只有1-2Mbps,变成了连看网页都很勉强,可能也就发个电子邮件的程度。同样的,这里为什么又会这么慢呢?笔者还是不清楚,只知道各方说法不一,但确实会慢,估计还是和TCP协议栈的感知有关。

由此可见,状况很差的链路上,本来三层隧道的效果就不算很好,如果外加混淆层,只会更差。这也就是为什么,近几年这样的方式应用得越来越少,更多的是在加密代理,把里面的数据拆下来再转发,中断了TCP语义——这也是部分企业级WAN优化器的工作思路,说明并非空穴来风。

4. 写在最后

感谢您阅读这篇长文,当然也感谢我的博客没有在撰写这篇文章的时候崩溃或系统错误。

其实OpenVPN的话题,笔者一直都想讨论,但苦于没有系统性的测试体系,主要是因为配置复杂,导致没有成功地搭建过一个可用的OpenVPN环境。其实直到今天,如果是没有什么非要不可的情况下,笔者还是会倾向于使用WireGuard,毕竟真的是太简单了,也太快了。这并不是说WireGuard没有缺点,其实有,笔者还写博文讨论过,但毕竟瑕不掩瑜,所以体验也会更加舒服。

先写到这里吧。

(完)


木头箱子脆脆,但是这样正好

如无特殊声明,本站内容遵循 CC BY-NC-SA 4.0 协议

转载请注明出处并保留作者信息,谢谢!

本站由 搬瓦工VPS 强力驱动

none
最后修改于:2026年01月23日 01:31

已有 6 条评论

  1. main main

    说到这不知道博主有没有看过谭泰锋的wireshark分析课 https://m.bilibili.com/space/676050680 感觉可以用来分析一下OpenVPN的工作方式

    1. 谢谢,确实没有,也许以后会去看看

  2. 为什么要用 OpenVPN Connect 呢,相比开源的社区版谁知道添了什么鬼东西(

    另外,如果我没记错,OpenVPN Connect 用的是 openvpn3,一个正用 C++ 重写的新轮子。

    如果你了解相关区别,能否在文章内指出?

    1. 谢谢,已补充,用OpenVPN Connect是因为我之前电脑就装了这个,而且有反复安装软件搞炸了各个TUN关系的经历,就没有重新安装Community

      对于OpenVPN Core 3,之前确实没有详细了解,因为手机上的OpenVPN for Android曾经有过OpenVPN 3的支持,但当时功能较缺,所以一直以为只是某个没有正式发布的测试版而已

      1. 欸欸,openvpn3 重写的目的是作为库使用,OpenVPN 2 只能作为应用

        1. 那看来还是有不同开发方向的考量,不过很好奇当2.x继续发展下去,版本号要咋办,该不会一直停在2了吧

添加新评论

提醒:『评论回复邮件提醒』功能正在测试中
评论后,如果站长有回复,会有邮件通知