OpenVPN通道
OpenVPN通道
OpenVPN内置了两个通道,一个用于控制,另一个用于数据传输,在设计上,很多网络协议都实现了多通道,也有N种方式实现多通道,其中常用的有两种,一种是使用带外数据传输控制信息,另一种就是将控制信息多路复用到数据通道。OpenVPN使用的就是第二种方式。
为何要这么设计,OpenVPN不是可以直接使用SSL记录协议来加密数据么?由于性能和历史的原因,OpenVPN没有这么做,而是实现了自己的协议,其中大部分的思想是借鉴于IPSec的,SSL协议对OpenVPN的帮助只是第一阶段的认证以及控制通道的密钥协商。OpenVPN设计两个通道而不是直接使用SSL协议的另一个原因是这样可以使安全配置更加灵活,比如可以实现只认证不加密,或者不认证只加密之类的灵活配置,否则这些就都要基于SSL协议来配置,那样灵活性将大大降低。
控制通道
顾名思义,控制通道中传输的是控制信息,诸如认证,策略推送,事件通知等非数据业务都要在控制通道中传输。实际上,控制通道就是SSL握手协议最终确定的一条加密通道,其上的数据完全受SSL握手协商的安全密钥保护。
OpenVPN在完成了SSL握手之后,会在其安全通道上使用SSL记录协议进行数据通道的密钥协商,协商好密钥之后,其实OpenVPN的握手还没有完成,OpenVPN的握手由以下步骤组成:
OpenVPN连接三次握手
此阶段没有任何数据载荷,和标准TCP一样,只是互相告知连接信息,初始化Reliable层。
TLS/SSL握手
此阶段在SSL内部进行,OpenVPN并不参与,它只是复杂封装从内存BIO抓取的SSL握手消息,然后将其封装成OpenVPN控制通道的消息格式。
OpenVPN密钥协商
完成了SSL握手之后,SSL记录协议得以工作,OpenVPN此阶段在SSL加密通道上传输密钥消息,进行密钥协商。
OpenVPN配置协商以及策略推送
在协商好密钥之后,控制通道的安全策略以及数据通道的密钥都已经准备好了。此阶段的目的是为数据通道的部署做准备,配置协商是保持两端的通道配置一致,比如MTU要一致,这类似于物理以太网卡两端的自协商,另外还要互相告知keepalive信息;策略推送是OpenVPN的一大特色,服务器端会把一系列的配置,比如路由推送到客户端,这样客户端可以自动添加虚拟网卡的路由信息。
握手完成
此阶段并不是真正的阶段,只是一个点。 在OpenVPN的数据通道传输数据的同时,控制通道也仍然可以传输控制信息,它们之间是互不干扰的,此时的控制信息包括:
密钥重新协商
根据配置,在数据通道传输数据的同时,在达到一定条件的时候,会引发重新协商消息的传送,这个消息在控制通道中传输。
RESTART信息
根据keepalive配置,在PING超时之后,或者在发生解密错误的时候,会引发RESTART消息的传送,对端接收到这个消息后,会执行重新初始化全部两个通道或者直接退出进程等策略
错误信息
这个消息是一类消息的统称,当收到这种消息时,执行一系列配置好的策略
1.2.9. OCC消息
本文不谈这类消息,它是可以在编译期间禁止的,因此它不是核心的。
关于具体的控制通道的数据封装格式,请参考《OpenVPN协议解析-网络结构之外》以及《OpenVPN协议解析-握手数据包分析》
数据通道
在OpenVPN完成了握手的同时,虚拟网卡开始初始化,一旦虚拟网卡初始化完毕,数据通道也就随即准备好,数据传输也就开始了。值得注意的是,数据通道和控制是并行工作的,它们是并行工作的,数据通道的安全策略完全独立于控制通道的安全策略,这个在具体配置上会体现的很明显。这样好处在于,在数据通道传输数据的同时还可以在不影响数据通道的情况下传输控制信息。
关于具体的数据通道的数据封装格式,请参考《OpenVPN协议解析-网络结构之外》,这里就不再赘述了。
两个通道的图示
给出一幅图来加深一下印象
OpenVPN状态机
OpenVPN拥有一个完整的协议集,这一点和IPSec是一样的,所不同的是,IPSec是一个框架,仅仅提供了一个体系的说明,而不提供数据封装格式的说明,具体使用哪个协议是可以配置的,比如可以使用ESP封装,也可以使用AH封装,并且分为隧道模式和传输模式,所有的协议封装都是外插式的。OpenVPN拥有一套不能外插的特定的协议封装格式,然后在传输层用UDP封装(也可支持TCP),仅仅使用它的尽力而为的服务。
因此,OpenVPN拥有一个完整的状态机,具体来讲OpenVPN分为:
#define S_ERROR -1
#define S_UNDEF 0
#define S_INITIAL 1 /* tls_init() was called */
#define S_PRE_START 2 /* waiting for initial reset & acknowledgement */
#define S_START 3 /* ready to exchange keys */
#define S_SENT_KEY 4 /* client does S_SENT_KEY -> S_GOT_KEY */
#define S_GOT_KEY 5 /* server does S_GOT_KEY -> S_SENT_KEY */
#define S_ACTIVE 6 /* ready to exchange data channel packets */
#define S_NORMAL_OP 7 /* normal operations */
这些状态机状态和SSL握手状态机状态有所不同,它们是属于不同层次的,然则OpenVPN状态机中的S_START状态包含了SSL握手状态机,这一点必须要注意。总的状态机如下图所示:
OpenVPN配置
对于学习OpenVPN,其过程要分至少两步,第一步就是学会使用它,第二步就是学习其源码,加深对其深层次细节的理解。任何学习者都逃不过使用OpenVPN的步骤,我们可以从其配置中感受到控制通道和数据通道的分离。
TLS Mode Options
在OpenVPN的man手册中,专门这么一节:TLS Mode Options,顾名思义,这一节都是和TLS相关的,而TLS控制的就是OpenVPN的控制通道,其中tls-server以及tls-client展现的是控制通道的C/S关系,也仅仅是控制通道的C/S关系,它和OpenVPN的C/S关系没有联系,OpenVPN的C/S关系在proto参数以及mode参数中体现。
另外一个重要的TLS配置参数是key-method,该参数配置了密钥交换的方式是方式一还是方式二。
TLS Cipher和Data Cipher
这是特别值得注意的一件事,有时候,明明配置了cipher为none,可是为何最终SSL握手还是协商出一个密钥呢?实际上,OpenVPN的配置中有两个cipher,第一个是总体的cipher配置,第二个是tls-cipher参数,第一个cipher指的是数据通道的cipher,而第二个tls-cipher则是控制通道SSL记录协议使用的cipher,最终的密钥由SSL握手协议确定。
Reliability-layer
这是OpenVPN的重头戏,OpenVPN的控制通道的设计中,多亏了它!本质上,Reliability-layer在尽力而为的UDP上封装了一个可靠传输的机制,使得传输的数据得到了确认,它比TCP的效率某些时候要高,因为它是精简版的可靠连接的协议,没有流控,没有拥塞控制。
OpenVPN为何自己实现Reliability-layer,而没有使用OpenSSL的DTLS的实现,部分原因是历史的,因为在OpenVPN对这个世界Say HI的时候,DTLS还没有出现…
总体设计
Reliability-layer的实现非常简单,它只是在尽力而为的UDP上实现了一个ACK的功能,对UDP的增强也很简单,对于发送端,那就是在发出一个数据包后,直到收到该数据包的ACK后才从发送缓存中将之删除,对于接收端,那就是收到一个数据包之后,向对端发出一个ACK。
数据结构
struct reliable
struct reliable
{
int size;
interval_t initial_timeout;
packet_id_type packet_id; //该字段用于确认
int offset;
bool hold; /* don't xmit until reliable_schedule_now is called */
struct reliable_entry array[RELIABLE_CAPACITY];
};
其中packet_id字段对于接收端很重要,它用于确保数据包是按顺序的,因此可想而知,对于每一个数据包都很保存一个序列号,而确保数据包按序到达应用,就意味着向应用提交的数据包的序列号是连续的。 其中array代表一组entry,每一个entry表示了一个Reliability-layer的逻辑数据包,由于Reliability-layer实现了一个按序到达的可靠传输逻辑,因此每一端维护了两个reliable,一个用于发送,另一个用于接收,对于接收的reliable,其packet_id表示下一个期望接收的数据包的序列号,而对于发送的reliable,它主要维持待发送的数据包以及发送了以后还没有收到确认的数据包
到此为止,Reliability-layer的逻辑我们已经很清楚了,大体上分为以下:
a. 发送一个数据包后,收到确认之前,保持它;
b. 发送一个数据包后,递增发送数据包序列号;
c. 收到一个数据包后,如果本端没有数据要发送,发送单独的确认包确认它;
d. 收到一个数据包后,如果本端有数据包要发送,发送捎带确认,具体封装见协议格式;
e. 收到一个数据包后,如果是按序到达的,那么向上层提交它,否则缓存它。
f. 收到一个确认包后,删除发送reliable结构中对应的数据包。
struct reliable_entry
struct reliable_entry
{
bool active;
interval_t timeout;
time_t next_try;
packet_id_type packet_id;
int opcode;
struct buffer buf;
};
该结构体表示了一个reliable层的逻辑数据包,其packet_id字段用于维持一个单调递增的序列号,指示一种顺序信息;timeout字段维持了一个超时时间,影响发送前缓存的间隔;opcode为该逻辑数据包的操作码,封装OpenVPN协议时的那个操作码;buf字段是OpenVPN的核心,它代表一块缓冲区。
struct reliable_ack
struct reliable_ack
{
int len;
packet_id_type packet_id[RELIABLE_ACK_SIZE];
};
该数据结构没有什么好说的,它保存了确认信息,既然packet_id是一个数组,那么它就表示可以一次确认多个数据包。
Reliability-layer的实现
R eliability-layer的实现非常简单,主要就是实现4.2.1节中的a-f即可,最为一种简化,我们不按照a-f逐一来谈,而是按照正常的数据流来分析一个控制通道的数据传输场景,该场景发生在SSL握手期间:
在send_reliable中寻找一个空闲的reliable_entry,取出其buf
该实现详见OpenVPN源代码ssl.c中tls_process函数调用的reliable_get_buf_output_sequenced
从SSL的wbio这个内存bio中读取SSL握手消息,写入buf后封装reliable_entry,标记为待发送
该步骤详见ssl.c中tls_process中key_state_read_ciphertext的调用以及随后的reliable_mark_active_outgoing调用
发送端将这个封装好的reliable_entry又封装了一个OpenVPN头发送了出去
该步骤主要还是ssl.c中的tls_process中的write_control_auth调用以及其后的*to_link = *buf;这样就把buf交给下面的UDP层了。注意,在write_control_auth中已经实现了控制通道的OpenVPN协议的封装,也就是说,凡是交给to_link的buf,都是可以直接发往UDP的。
对端收到了这个数据包之后,根据opcode发现是控制通道数据,因此将其写入到Reliability-layer
该步骤的实现在ssl.c的tls_pre_decrypt函数中的read_control_auth调用以及reliable_get_buf以及reliable_mark_active_incoming调用,最终buf和一个reliable_entry联系了起来,然后处理逻辑将ack信息写入该端的reliable_ack结构体以方便ACK,具体参见随后的reliable_ack_acknowledge_packet_id的调用。
接收端最终进入了tls_process后,看一下有没有按序的数据包可以交由上层,如果有,则直接写入上层,并且在Reliability-layer删除该数据包
该步骤的实现参见tls_process的reliable_get_buf_sequenced调用以及随后的reliable_mark_deleted调用。在reliable_get_buf_sequenced中,会检查有没有收到的数据包的packet_id等于负责接收的reliable结构体的packet_id,如果有则返回该entry。如果真的返回了一个entry,那么将之交给SSL的rbio,随后调用的reliable_mark_deleted负责将负责接收的reliable的packet_id加1,表示希望接收下一个数据包。
接收端发现其ack结构体不为空,则往发送端发送一个单独封装的ACK,表示收到了一个数据包
发送端在收到了接收端发来的ACK信息后,删除了负责发送的send reliable中的相应的数据包
该步骤的实现在tls_pre_decrypt的reliable_ack_read调用读取了对端发来的ack信息,得到了对端已经确认了某些数据包,然后调用reliable_send_purge删除这些数据包。
总结
上面的叙述展示一个场景的完整实现,事实上很简单,然而OpenVPN的代码组织有些人认为凌乱了一些,可是一旦理解了OpenVPN的协议封装机制,状态机,以及Reliable层,再凌乱的代码也不会影响我们对其的理解。
关键宏FULL_SYNC
该宏的定义如下:
#define FULL_SYNC \
(reliable_empty(ks->send_reliable) && reliable_ack_empty (ks->rec_ack))
它表示的是既没有数据要发送,也没有ACK要发送。其中send_reliable为空有两层含义,第一层就是确实没有数据要发送了,第二层就是所有发送的数据都已经被确认了,而rec_ack为空的含义则是,所有对端发送来的数据我们已经全部确认过了,可是这个确认可能会中途丢失。
这个宏主要的意义在于状态机的切换,一旦满足了某些切换条件,还不能切换状态机,必须还要满足FULL_SYNC为真的情况下才能切换,这也是为了解决一些同步问题。
OpenVPN协议的意义
很多人问,OpenVPN兼容IPSec吗?这个问题显得很不合时宜,因为这根本就不是一回事,IPSec是一个安全框架,而OpenVPN是一个特定的VPN实现,可以说不遵循任何框架,而是实现了自己的私有协议,因此根本就没有兼容不兼容问题。然而可以肯定的是,OpenVPN的设计肯定是受到了IPSec实现的影响。具体来讲:
IKE握手与SSL握手
在安全策略上,OpenVPN同样是两阶段的,这和IKE是一致的,然而却没有使用IKE协议,而仅仅接受了它的思想,使用了灵活的用户态SSL协议完成之。
加密数据封装
IPSec可以插入ESP,AH等协议封装数据,然而OpenVPN却使用UDP(或者TCP)来封装数据,真正的安全加密和摘要在用户态完成,这就是说它使用自己的用户态私有封装来实现安全加密封装
使用场合
既然OpenVPN使用用户态封装,那么它的性能也就肯定有个瓶颈,然而这不是问题。毕竟在设计定位上,OpenVPN就是一个私人VPN的实现,它主要用于主机-主机或者主机-网络的安全访问,而对于网络-网络的需求,还是使用IPSec的好。
OpenVPN安装在客户端主机操作系统之上,就意味着它的用武之地也就限制在了这里,不能指望它被安装在核心网的高端路由器之上实现TCP隧道,但是对于个人应用,比如收发邮件,Web浏览,访问OA,那足够了。
对于学习OpenVPN的路线,给出一些建议:
1). 下载使用它,起码要搭建一个测试环境。通读man手册
2). 理解其安全架构,知道它使用SSL完成安全策略协商
3). 理解tap虚拟网卡原理,和SSL一起,理解OpenVPN的整体架构
4). 看源码,理解其实现,这时会遇到一些问题,发现很多东西并不是一开始想象的那样
5). 使用tcpdump等抓包工具抓取数据包,分析OpenVPN协议
6). 理解OpenVPN协议,在此基础上配置协议参数,性能期望达到最优化
7). 整体调整配置参数,追求安全基础上的高性能