前面写了不少关于OpenVPN的文章,那些文章大多数都是侧重于网络的,要么就是源码解析。其实OpenVPN还有更深的可挖掘的东西。只可惜我们很难搜到中文的,因此本文就增添几笔。实际上,很多人都很看好OpenVPN,虽然它看起来是很简陋,然而确实有很多值得挖掘的。
OpenVPN的基本框架答疑
如果用过OpenVPN并且了解过其实现,我们知道OpenVPN是一个巧妙构思的成果,网络通路部分使用了虚拟网卡构建一个虚拟的网络,这正合“虚拟专用网”之含义,而数据认证/加密则使用了SSL。然而如果我们真的去深究的话,事情又不是那么简单。
问题一:TCP隧道的问题
首先请阅读《TCP封装的隧道对于拥塞控制的意义》一文,内中叙述了TCP封装隧道的问题,那就是在有大量背景流量的情况下,TCP隧道的性能将严重下降,最终导致网络不可用。OpenVPN需要一个隧道封装,且是从用户态开始的封装,因此它需要要么使用TCP,要么使用UDP。CIPE的作者经过实测已经得到了TCP隧道将导致问题结论,由于站在巨人的肩上,OpenVPN首先选择了UDP(当然它还是支持TCP的)。
问题二:SSL协议的问题
SSL分为握手协议和记录协议,而记录协议最开始在TCP上开发,因为SSL记录协议要求下层链路的可靠性。然而又不是非用TCP不可,使用TCP只是用其两个特性:其一是有连接-这可以很方便的实现SSL握手协议,其二是可靠传输-这可以方便实现SSL记录协议。TCP的其它特性,诸如拥塞控制之类的,和SSL没有任何关系,只会限制其被广泛使用。最终,DTLS产生,在UDP上实现一个有连接且可靠传输的层,然后将SSL/TLS布置于其上,这样SSL/TLS就可以在TCP和UDP上跑了,实际上在某些场合是直接替代了TCP和UDP,成了名副其实的传输层协议。但是,这是2006年以后的事了,此时OpenVPN已经上路了。
问题三:综合的事实因素
SSL首先选择了TCP,而OpenVPN首先选择了UDP,那时DTLS还没有出生,这意味着OpenVPN不能直接使用SSL记录协议。然而OpenVPN确实需要SSL握手协议这个良好的,简单的,经过实战的协议,因此从历史和设计的角度,OpenVPN需要重新设计一套自己的协议,平滑适配这一系列的矛盾!
OpenVPN的协议设计思想
一个绝对的真理就是:增加一个层!由于OpenVPN需要SSL握手协议,不一定需要SSL记录协议,因此需要将SSL握手协议和自己的记录协议适配在一起。
到底为何需要SSL握手协议
OpenVPN到底为何需要SSL握手协议?是需要其认证功能,还是需要其最终协商好的那个对称密钥。我们可以说,是前者。那么很显然就可以在SSL握手协议最终协商好的安全通道上协商新的OpenVPN的密钥。事实上,由于OpenVPN不想使用SSL记录协议,因此它也就不便使用SSL握手协议最终协商的那个对称密钥。
IPSec的经验
IPSec使用IKE来协商密钥,我们知道,它分为两个阶段,第一阶段实行认证功能,最终协商好一条安全加密通道,接下来第二阶段在这个安全加密通道上协商真正的数据通道的加密密钥。这个经验十分有用,因为它实际上将认证和对称密钥的协商之间的耦合解除了,这样就可以实现各种灵活的密钥协商方式,并且可以将认证和密钥协商使用各自的策略各自进行互不影响。最显然的,需要重新协商密钥的时候,不需要再次进行认证。
两阶段协商对OpenVPN的影响
IKE两阶段的协商几乎很肯定的对OpenVPN产生了影响。OpenVPN需要自己的记录协议,而能将握手认证和记录协议分开的方式就是将通道分离。首先我们先看看这种OpenVPN实现的记录协议需要什么特征。由于OpenVPN记录协议只是作用于一个隧道,且它封装的是一个IP数据报,因此OpenVPN的协议只需要加密和HMAC即可,完全不需要可靠传输。
OpenVPN的记录协议和SSL记录协议的区别
SSL直接接于传输层协议之上,它在保证安全的同时一定不能更改应用的语义,比如http是tcp的,那么https也需要是tcp的,SSL原本就是设计用于加密应用数据而不是构建三层隧道的。而OpenVPN的记录协议保护的只是一个IP数据报的再封装,最终的端系统应用才负责传输层语义,因此OpenVPN不需要可靠传输。
如何将SSL握手协议和OpenVPN的记录协议封装在一个协议中
还是那句话:增加一个层!实际上就是增加一个“多路解复用码”,通过对该字段的解析,识别协议包是握手包还是记录包。这只是第一点,还有一点必须要实现的就是将SSL握手消息封装在OpenVPN协议中,由于OpenVPN使用了OpenSSL,这一点是通过BIO实现的。
OpenVPN协议
OpenVPN协议总揽
OpenVPN协议的封装格式如下图所示:
注意,正是OVPN头封装了OpenVPN的握手协议和记录协议,它起到了复用和解复用的功能,按信道划分,OpenVPN的信道有两类,一类是控制信道,另一类是数据信道,其中的操作码在该层协议上有“地址”的含义,正是操作码区分了控制信道和数据信道-换句话说就是控制协议和记录协议,其中控制协议分为握手协议和密钥协议,类似IP协议的IP地址,TCP/UDP的端口。操作码的含义如下:
P_CONTROL_HARD_RESET_CLIENT_V1:Key method 1, initial key from client, forget previous state. P_CONTROL_HARD_RESET_SERVER_V1:Key method 2, initial key from server, forget previous state. P_CONTROL_SOFT_RESET_V1:New key
P_CONTROL_V1:Control channel packet (usually TLS ciphertext).
P_ACK_V1:Acknowledgement for P_CONTROL packets received.
P_DATA_V1:Data channel packet containing actual tunnel data ciphertext.
P_CONTROL_HARD_RESET_CLIENT_V2:Key method 2, initial key from client, forget previous state.
P_CONTROL_HARD_RESET_SERVER_V2:Key method 2, initial key from server, forget previous state.
我们可以将上述的每一个操作码看做是一个子协议。KEY ID的作用很有意思,它表示一个key集合,OpenVPN中一共有3个key集合,想了解其中要旨需要再稍等片刻。
OpenVPN记录协议
OpenVPN记录格式如下图所示:
其中HMAC用于认证包的完整性,IV用于CBC解密,Packet ID用于防止重放,OpenVPN记录协议包含了一个非常简易的滑动窗口机制用于防止重放。通过和SSL记录协议做对比,我们发现它们真的很像,只不过SSL记录协议的下面是复杂的TCP协议,而OpenVPN记录协议下面首选的却是UDP协议。可以看到,OpenVPN记录协议并不是ACK的相关信息,因此它封装的是一个“尽力而为”的网络。
OpenVPN控制协议
这个协议包含了两个部分,第一部分是握手协议,第二部分是密钥协议。由于OpenVPN使用SSL握手协议,为何不使用SSL握手协议最终协商出来的那个对称密钥,这是一个值得思考的问题。这里给出一些思路:
a.IPSec所使用的IKE两阶段模式无疑对所有VPN几乎都有很大的影响,第一阶段构建的安全通道用于第二阶段的密钥协商而不是最终的数据传输;
b.SSL握手最终协商的那个密钥和SSL握手过程是分不开的,而OpenVPN最终的设计是希望将握手过程和数据传输过程完全分离,从而独立的实现单一的密钥重协商;
c.这样实现的OpenVPN协议更加简洁。我们看一下OpenVPN的控制协议
:
这里面最重要的恐怕要算ACK buffer了,由于SSL握手协议需要一个可靠的下层,因此需要确认机制,值得注意的是,不要一说确认就是TCP,UDP也可以实现的,只是这里的UDP实现的重传机制很简陋,确认机制也很简陋。另外还有一点值得注意,那就是OpenVPN只在控制协议中使用可靠机制的协议,在记录协议中是不使用的。没有必要浪费太多的时间在OpenVPN的握手协议上,它仅仅是对SSL握手的一个封装而已,最终协商出一个加密的安全通道,密钥协议将运行于其上,实际上密钥协议的载荷也是封装在OpenVPN的控制协议头里面的。接下来,我们来看一下OpenVPN的密钥交换/协商/管理协议
OpenVPN控制协议之-密钥协议
OpenVPN自己实现了密钥管理,密钥交换,密钥重协商等,需要注意的是,它并不使用SSL握手最终确定的那些密钥,OpenVPN使用SSL握手仅仅是完成了连接者身份认证以及为后续的密钥管理,密钥交换,密钥交换以及控制信道构建一个安全的加密通道,完全类似于IKE的第一阶段协商。
值得说明的是,虽然不断开SSL连接就可以重新协商密钥,也就是仅进行第二阶段,然而当前的OpenVPN实现中,reneg选项实现的是两个阶段同时重新协商,也就是说SSL握手也要重新进行。由于SSL的BIO直接接于一个内存BIO上,因此断开SSL连接并不意味着底层的传输层连接断开,OpenVPN实现了数据通道和控制通道的复用,因此任何通道都不是真正的物理通道,都是虚拟的,重新协商任何参数都不需要断开连接。在重新协商安全参数的时候,丝毫不影响数据通道的正常传输。
目前OpenVPN实现了两类密钥交换的方法,第一类比较简单,相当于直接告诉对方解密密钥了,虽然这样,依然很安全,因为这里的“告诉”并不是明文告知,而是在SSL握手后的加密通道中告知的,它的消息格式如下:
通过消息格式可知,一对连接中,一共需要4个密钥,一个方向2个,这也TM太像IPSec的SA了啊!
第二类方式稍微复杂一些,然而它又很像SSL握手协议中的密钥交换方法,使用了一个pre master的密钥,然后两方分别进行计算,最终算出相同的密钥。第二类密钥交换的数据格式如下:
最终,一个密钥导出函数计算出最终的密钥,关于密钥导出函数的细节,请google,或者Email给我,要是我不知道,我们一起学习。从第二类方式的消息格式中可以看出,pre-master的使用方式和标准的SSL握手中最终的密钥协商的方式一样,只不过SSL握手中这个pre-master是用server端的公钥加密的,而这里的pre-master直接使用SSL握手确定的对称密钥加密。第一类方式很像IPSec的SA,第二类方式很像SSL握手中的密钥协商,可见OpenVPN真的站在了巨人肩上啊。
最后,有个KEY ID十分奇妙,它的作用就是密钥更新时的平滑过渡。OpenVPN一共定义了3组密钥集合,其中只有一组是当前活动的,然而在新的活动密钥没有生效之前,还是可以用老的。值得再次强调的是,重新协商并不断开任何连接,虽然重新初始化了SSL,但是记住,SSL只是一个过滤,和Socket没有关系的,它可以用于任何管道性质的文件描述符之间的安全通道协商,在OpenVPN的场景下,重新协商SSL时,底层的UDP连接仍然没有断开(这里所说的UDP连接,是因为UDP之上实现了一个Reliability Layer),只要OpenVPN客户端连上服务器,它就不会再因为重新协商而主动断开,除非协商失败,比如证书已经被吊销等。
OpenVPN协议总结
现在总结一下OpenVPN的协议。它真的很复杂,我们先看一幅可以表示OpenVPN协议总体设计的图,我并没有按照传统的网络协议按照控制面和用户面来画图,那样太抽象,我选择的方式是直接的方式:
通过这幅图我们再来温习一下OpenVPN协议的设计。如图所示,虽然协议分了好几部分,每一个部分又分若干层次,然而它们在横向上是有联系的。从设计上说,首先是安全认证和第一阶段的握手,这和IKE一致,第一阶段握手为第二阶段密钥协商提供了安全通道,然后第二阶段最终协商出了加密/MAC密钥。从此记录协议和控制协议分离,直到再次协商。这是OpenVPN协议的第一个要点:分层次,分阶段。
OpenVPN协议的第二个要点就是多路复用到UDP。这样就不必为SSL协商之类的控制面专门设置一个通道,OpenVPN巧妙使用OpenSSL的BIO机制劫持了SSL握手包,实现了对其的封装,这样所有的协议都可以复用了一个UDP“连接”上,因此重新协商密钥的时候,数据传输不会停止。
OpenVPN的第三个要素有点历史原因,OpenVPN完全可以使用SSL记录协议来完成最终的数据加密,然而却面对着TCP重传叠加的问题,当时DTLS还没有出现,然而SSL握手实在太好了,因此OpenVPN做出了一个漂亮的折中,实现了协议的重新封装,保留了SSL握手,重新实现了“尽力而为”的记录协议。这样做的结果是,OpenVPN可以在本来已经很安全的SSL握手协商上又做了一层封装,可以实现比如tls-auth的功能,对TLS/SSL握手消息本身进行完整性验证,这样就预防了Dos攻击,消耗掉服务器的资源(SSL连接的处理很消耗资源,即使不消耗什么内存,消耗CPU是巨大的)…同时,大量显式的SSL连接也不复存在,再者,由于密钥交换和SSL握手彻底分离,这样可以实现更多的选择,比如可以配置cipher为none,然而SSL握手还是安全的,或者说仅仅提供数据的HMAC验证,此时SSL握手以及后面的HMAC密钥协商依然是安全的…要知道,SSL握手的结果永远都是协商出一条安全加密的通道,你在这通道上作甚,你自己配置,这就是分离的好处。
使用BIO封装SSL握手消息
这一点实际上并不是特别的重要,因为它是实现相关的,要不是OpenVPN使用了OpenSSL这个开源的SSL库或者OpenSSL中没有BIO,那OpenVPN还真得费点周折去思索如何得到SSL握手消息然后再封装成OpenVPN自己的控制消息。
BIO在这里我就不再深入介绍了,它很简单,就是一个堆栈式的IO部件,每一层都有一定的功能,比方说过滤,十分类似于Windows的IRP的概念,如果实在不懂,要么google,要么直接看OpenSSL的实现,但是千万别百度。为了说明如何使用BIO封装SSL握手消息,给出一幅图:
OpenVPN连接建立
OpenVPN连接的建立显得很诡异,不过本文看到此处,你就不觉得什么诡异了。如果不了解OpenVPN的协议设计,仅仅知道它使用了SSL握手的话,你会感到很费解,因为你用ssldump或者tcpdump抓取不到任何SSL握手的消息。实际上这很好解释,那就是SSL握手消息被OpenVPN的协议给封装了,上一小节讲述了如何封装SSL握手消息,使用OpenSSL的BIO可以很方便的做到这一点,实际上,OpenSSL本身将SSL类型的BIO设计成一个过滤型BIO,这才是精妙所在,这样SSL消息可以直接发往任何类型的IO实体了,而不一定必须是网络套接字了。
OpenVPN连接的建立大致分为4个阶段,每一个阶段完成不同的事情,如下图所示:
OpenVPN实现略!
最终,还是贴上OpenVPN源码中关于协议实现的注释:
OpenVPN Protocol, taken from ssl.h in OpenVPN source code.
TCP/UDP Packet: This represents the top-level encapsulation.
TCP/UDP packet format:
Packet length (16 bits, unsigned) -- TCP only, always sent as plaintext. Since TCP is a stream protocol, the packet length words define the packetization of the stream.
Packet opcode/key_id (8 bits) -- TLS only, not used in pre-shared secret mode. packet message type, a P_* constant (high 5 bits)
key_id (low 3 bits, see key_id in struct tls_session below for comment). The key_id refers to an already negotiated TLS session. OpenVPN seamlessly renegotiates the TLS session by using a new key_id for the new session. Overlap (controlled by user definable parameters) between old and new TLS sessions is allowed, providing a seamless transition during tunnel operation.
Payload (n bytes), which may be a P_CONTROL, P_ACK, or P_DATA message.
Message types:
P_CONTROL_HARD_RESET_CLIENT_V1 -- Key method 1, initial key from client, forget previous state.
P_CONTROL_HARD_RESET_SERVER_V1 -- Key method 2, initial key from server, forget previous state.
P_CONTROL_SOFT_RESET_V1 -- New key, with a graceful transition from old to new key in the sense that a transition window exists where both the old or new key_id can be used. OpenVPN uses two different forms of key_id. The first form is 64 bits and is used for all P_CONTROL messages. P_DATA messages on the other hand use a shortened key_id of 3 bits for efficiency reasons since the vast majority of OpenVPN packets in an active tunnel will be P_DATA messages. The 64 bit form is referred to as a session_id, while the 3 bit form is referred to as a key_id.
P_CONTROL_V1 -- Control channel packet (usually TLS ciphertext).
P_ACK_V1 -- Acknowledgement for P_CONTROL packets received.
P_DATA_V1 -- Data channel packet containing actual tunnel data ciphertext.
P_CONTROL_HARD_RESET_CLIENT_V2 -- Key method 2, initial key from client, forget previous state.
P_CONTROL_HARD_RESET_SERVER_V2 -- Key method 2, initial key from server, forget previous state.
P_CONTROL* and P_ACK Payload: The P_CONTROL message type indicates a TLS ciphertext packet which has been encapsulated inside of a reliability layer. The reliability layer is implemented as a straightforward ACK and retransmit model.
P_CONTROL message format:
local session_id (random 64 bit value to identify TLS session).
HMAC signature of entire encapsulation header for integrity check if --tls-auth is specified (usually 16 or 20 bytes).
packet-id for replay protection (4 or 8 bytes, includes sequence number and optional time_t timestamp).
P_ACK packet_id array length (1 byte).
P_ACK packet-id array (if length > 0).
P_ACK remote session_id (if length > 0).
message packet-id (4 bytes).
TLS payload ciphertext (n bytes) (only for P_CONTROL).
Once the TLS session has been initialized and authenticated, the TLS channel is used to exchange random key material for bidirectional cipher and HMAC keys which will be used to secure actual tunnel packets. OpenVPN currently implements two key methods.
Key method 1 directly derives keys using random bits obtained from the RAND_bytes OpenSSL function.
Key method 2 mixes random key material from both sides of the connection using the TLS PRF mixing function. Key method 2 is the preferred method and is the default for OpenVPN 2.0.
TLS plaintext content:
TLS plaintext packet (if key_method == 1):
Cipher key length in bytes (1 byte).
Cipher key (n bytes). HMAC key length in bytes (1 byte).
HMAC key (n bytes).
Options string (n bytes, null terminated, client/server options string should match).
TLS plaintext packet (if key_method == 2):
Literal 0 (4 bytes).
key_method type (1 byte).
key_source structure (pre_master only defined for client -> server).
options_string_length, including null (2 bytes).
Options string (n bytes, null terminated, client/server options string must match).
[The username/password data below is optional, record can end at this point.] username_string_length, including null (2 bytes).
Username string (n bytes, null terminated).
password_string_length, including null (2 bytes).
Password string (n bytes, null terminated).
The P_DATA payload represents encrypted, encapsulated tunnel packets which tend to be either IP packets or Ethernet frames. This is essentially the "payload" of the VPN.
P_DATA message content:
HMAC of ciphertext IV + ciphertext (if not disabled by --auth none). Ciphertext IV (size is cipher-dependent, if not disabled by --no-iv). Tunnel packet ciphertext.
P_DATA plaintext:
packet_id (4 or 8 bytes, if not disabled by --no-replay). In SSL/TLS mode, 4 bytes are used because the implementation can force a TLS renegotation before 2^32 packets are sent. In pre-shared key mode, 8 bytes are used (sequence number and time_t value) to allow long-term key usage without packet_id collisions. User plaintext (n bytes).
Notes:
(1) ACK messages can be encoded in either the dedicated P_ACK record or they can be prepended to a P_CONTROL message.
(2) P_DATA and P_CONTROL/P_ACK use independent packet-id sequences because P_DATA is an unreliable channel while P_CONTROL/P_ACK is a reliable channel. Each use their own independent HMAC keys.
(3) Note that when --tls-auth is used, all message types are protected with an HMAC signature, even the initial packets of the TLS handshake. This makes it easy for OpenVPN to throw away bogus packets quickly, without wasting resources on attempting a TLS handshake which will