背景介绍
听着《梦中的额吉》,《天堂》…女儿在睡觉…外面细雨…中秋小长假,完成自己的OpenVPN patch 编码中充满了快乐!前提是你知道自己在做什么! OpenVPN不给力,虽然它给出了N多的Renegotiate选项,然则其实现却不尽人意。难道设计者以为我们众人就这么好忽悠吗?
OpenVPN实现重新密钥协商是极其重量级的,需要做以下几件事:
1.断开当前的SSL连接;
2.重新开始一次SSL连接,建立一次SSL session;
3.在重新生成的SSL加密信道上重新开始OpenVPN的密钥协商;
记住,不能单纯的理解这3个事件,是因为如果从底层看,底层的UDP连接(不考虑TCP的情况)并没有断开,需要理解的是,SSL协商和数据通道复用一个信道。这个过程之所以说是重量级的是因为1和2并不总是必要的,要知道SSL的目的有两点,第一是相互认证,确保客户端连接的是自己要连接的服务器,确保客户端有权接入VPN;第二是在协商出的SSL加密信道上协商OpenVPN的对称密钥。如果我们只是要保护VPN通信的内容,那么大可不必重复认证,也就是没有必要重新进行SSL握手。要知道,如果SSL加密信道可以被攻破,那么后续的通信将是不再安全的,幸运的是,SSL协议本身的完备性保证了SSL最终协商出的信道是安全的,否则SSL协议也不会这么流行,因此除非SSL本身的规范要求,在OpenVPN中,如果仅仅为了保证数据的机密性,SSL重协商是没有必要的。
这只是其一。任何伟大的事情,都有无数的先行者,或者说它本身就需要借鉴前辈的成果。我们可以考虑IKE,ISAKMP的规范,它明确规定了两个阶段的协商,为了效率,一般情况下,通信双方只需要进行第二阶段的重新协商,在IKE的规范中,第一阶段的协商所完成的工作和SSL握手完成的工作很类似(也许正好说反了,…我女儿长得和我很像,而不是我长得和我女儿很像!)。对称密钥由于密钥长度不够,算法相对简单,被暴破的可能性很大,因此应该不断重新协商对称密钥。在使用SSL协议的情况下有两种方式重新协商出对称密钥,第一种方式就是重新进行SSL握手,使用SSL最终协商出的那个对称密钥,第二就是在直接在SSL加密信道上协商新的对称密钥,这就使对称密钥的结果和SSL协议分离了,无疑这是一种效率更高且配置更灵活的方式,也许这也是对IKE/ISAKMP的一种呼应吧。
然而很可惜,OpenVPN并没有使用这种优雅的方式,每当OpenVPN需要重新协商,都要从新建SSL session开始!key_state_soft_reset这个函数并没有做到其名称所描述的那样soft,而实则是一种very hard的恶毒方式,因此需要改变这种方式!我对OpenVPN的已经关注超过两年了,由于工作需要,加之自己的兴趣,着手对之做个外科手术,手术很成功,以数据为证,效率有很大的提升。在学习的过程中,Internet对我帮助很大,Internet本身就是共享的,因此将patch献出,也希望共勉者完善之。本patch也以委托的方式提交到了OpenVPN的maillist,希望能在下个版本中见到它的身影,如果有朋友觉得有改进的地方,那就大胆的改进它,然后提交…这对于每一个开发人员都是一笔财富!
尝试修改
前文《让OpenVPN实现IKE似的两阶段密钥协商》,代码已然完成,然则不便公开,同时代码丑陋之极,无颜!总的来讲,那只是一次初期的预研,逻辑如下:
在S_ACTIVE之后定义新的状态码;
当需要进行第二阶段协商的时候,像client push一个字符串,将本地状态设置一个中间状态;
client端收到server端push下来的对应字符串之后,将本地状态设置为一个新定义的预协商状态,并且写入新的key协商消息,同时reply一个字符串;
server端收到client端reply的字符串之后,将本地状态设置为预协商的状态,读取key协商消息;
双方开始在以前的SSL加密信道上重新协商新的对称密钥;
最终,二者到达一个新的S_ACTIVE状态;
协商完成。
这个手术开始非常让我自豪,因为它有效的降低了传输延迟,然而经过后续的仔细测试,发现它引起了大量的丢包,使用ping进行的简单的测试就会发现丢包,因为echo reply的序号会有断续…这个结果极大的打击了我。
但是,我的选择只能是继续尝试其他的方法,否则我的努力将是前功尽弃…(腾格尔的《天堂》,从时间4:45开始的一段音乐很不错[哦…Belala…],我很喜欢)。我的push/pull/reply的想法并没有错啊,server端总是需要一种方式通知client需要重新协商了,这本身很合理,然而哪里错了?后来看代码,发现就算是硬协商,也就是重新开始SSL握手,标准的OpenVPN也是这么做的,所不同的是,标准的OpenVPN并没有使用push,而是发送了一个很特别的P_CONTROL_SOFT_RESET_V1消息,然后再在client端的tls_pre_decrypt函数中对其进行特殊的处理,从而进行正常的伪造的“SOFT”reset。
既然如果,我又何必出力不讨好,直接使用标准的实现不就可以了吗!
还好OpenVPN协议的定义很灵活,其op码为5位,最大可以有31个,现在才使用了8个,我扩展一个是很简单的事情啊。想到这里,再也忍不住了,先吸一根烟再说!(《梦中的额吉》在1:54时间开始一段音乐很不错,引发了更多的想法)
确定如何修改
当初的尝试之所以出现了丢包,那是因为我忽略了OpenVPN对session和key的管理。在OpenVPN中,为了实现在重新协商以及clilent断开中不丢包,业务数据传输不受影响的正常进行,OpenVPN设计了3个session和2个key state,在client断开重连时,server管理session,以至于正常连接成为可能,在重新协商密钥时,server端管理key state,从而平滑过渡到新的密钥。
因此,如果不吃透OpenVPN复杂的协议处理,实现第二阶段的协商是没戏的。我的做法是,先动手再说,先修改掉代码再慢慢测试排错,我觉得程序员就应该这样,毕竟我们不是研究院的专家有那么多的时间和经费。具体实现见第4节。
实现了之后,测试,效果良好,先给出数据,先看一个ping的结果,测试中为了比较重新协商SSL和仅仅重新协商对称密钥,我使用了常数4秒,测试原生OpenVPN时,使用reneg-sec 4参数,而测试我修改过的OpenVPN时,使用reneg-second参数,首先看一下没有修改的原生的OpenVPN的ping结果:
可以看出,每隔4秒左右,就会出现一次时延非常大的抖动,再看看我做过手术的OpenVPN的结果:
可以看到,抖动虽然还有,但是平缓了很多。仅仅通过ping还不足以看出个究竟,那么我们看看tcp传输的丢包重传统计值:
结果几乎是肯定的,当然TCP协议是复杂的,不能单纯从丢包或者重传来判断,正好比,有一条比较超豪华的高速公路,其事故率并不比国道少的原因一样(不得不说的插曲:TCP重传可能是由于其拥塞避免阀值设置得过高所导致,或者由于其拥塞控制算法过于自私所导致,然而可悲的是,很多人都认为重传越多,网络状况越差!这样的人不在少数,还总是用什么业务逻辑之类词汇将真理打发)
最终的补丁
起初我想直接提交这个补丁,但是由于Unix邮箱环境坏了,web邮箱提交patch有存在编码问题,因此采用了委托的方式,希望社区的家伙能帮我这个忙,至于作者是谁是无所谓的,Internet上的作者大多是虚拟的,像dog250之类的,呵呵~~。
最终的补丁其实很简单,之所以称为返璞归真,是因为OpenVPN本身做的就很好,其协议的操作码预留了5位的空间,而它仅仅用了不到8个,这就是值得欣慰的事情,这意味着我们只需要增加一个操作码就能实现额外的功能,对于第二阶段协商来讲,用这个方法实现再好不过了,补丁如下,如果有谁索要常规编码的补丁,请发邮件(不要为补丁所迷惑,后面有解释):
TO everyone.As we know,IKE takes only phase II to consult the final KEY IF WE DO NOT CARE ABOUT THE RESULT ABOUT RE-AUTJENTICATION in the special condition.This idea is very true especially if we use SSL protocol because of the secure test in very bitter surrounding. The SSL protocol can ensure the security of the whole vitrual line after the entire handshack. OpenVPN use the protocol of itself not only SSL.Its channel is build on its protocol of itself.Thus if somebody crack the SSL key, (S)HE must take sometime that may be long to crack the OpenVPN channel key. AS THE RESULT,WE CAN ONLY TO Renegotiate ITS CHANNEL KEY NOT THE WHOLE SSL SESSION. Unfortunately,as we show,OpenVPN takes a pool way to renegotiate the new key, not only the channel key but also the SSL record protocol key. This patch rework the whole thing.Its only take the phase II to renegotiate the channel key. *****************************************************************************
diff -uNr openvpn-2.2.1/options.c openvpn-2.2.1_my/options.c
--- openvpn-2.2.1/options.c 2011-09-09 16:25:49.000000000 +0800
+++ openvpn-2.2.1_my/options.c 2011-09-09 17:12:03.000000000 +0800
@@ -668,6 +668,7 @@
"--show-pkcs11-ids provider [cert_private] : Show PKCS#11 available ids.\n"
" --verb option can be added *BEFORE* this.\n"
#endif /* ENABLE_PKCS11 */
+ "--reneg-second sec :xxx"
;
#endif /* !ENABLE_SMALL */
@@ -3544,6 +3545,7 @@
int msglevel_fc = msglevel_forward_compatible (options, msglevel);
ASSERT (MAX_PARMS >= 5);
+ reneg_sec_time = 0;
if (!file)
{
file = "[CMD-LINE]";
@@ -5781,6 +5783,10 @@
VERIFY_PERMISSION (OPT_P_GENERAL);
options->crl_file = p[1];
}
+ else if (streq (p[0], "reneg-second") && p[1]) {
+ reneg_sec_time = atoi(p[1]);
+ }
+
else if (streq (p[0], "tls-verify") && p[1])
{
VERIFY_PERMISSION (OPT_P_SCRIPT);
diff -uNr openvpn-2.2.1/ssl.c openvpn-2.2.1_my/ssl.c
--- openvpn-2.2.1/ssl.c 2011-09-09 16:25:49.000000000 +0800
+++ openvpn-2.2.1_my/ssl.c 2011-09-09 17:16:28.000000000 +0800
@@ -2329,6 +2329,7 @@
* to/from memory BIOs.
*/
CLEAR (*ks);
+ session->ks_size = KS_SIZE;
ks->ssl = SSL_new (session->opt->ssl_ctx);
if (!ks->ssl)
@@ -2469,6 +2470,7 @@
dmsg (D_TLS_DEBUG, "TLS: tls_session_init: entry");
CLEAR (*session);
+ multi->tm_size = TM_SIZE;
/* Set options data to point to parent's option structure */
session->opt = &multi->opt;
@@ -2520,7 +2522,7 @@
if (session->tls_auth.packet_id)
packet_id_free (session->tls_auth.packet_id);
- for (i = 0; i < KS_SIZE; ++i)
+ for (i = 0; i < session->ks_size; ++i)
key_state_free (&session->key[i], false);
if (session->common_name)
@@ -2725,8 +2727,8 @@
cert_hash_free (multi->locked_cert_hash_set);
- for (i = 0; i < TM_SIZE; ++i)
- tls_session_free (&multi->session[i], false);
+ for (i = 0; i < multi->tm_size; ++i)
+ tls_session_free (&multi->session[i], false);
if (clear)
CLEAR (*multi);
@@ -3256,6 +3258,26 @@
ks->remote_addr = ks_lame->remote_addr;
}
+void
+key_state_soft_reset2(struct tls_session *session, struct tls_multi *multi)
+{
+ update_time ();
+ ks->must_die = now + session->opt->transition_window; /* remaining lifetime of old key */
+ *ks_lame = *ks;
+ session->ks_size = 1;
+ multi->tm_size = 1;
+
+ session->initial_opcode = P_CONTROL_SOFT_RESET_SECOND;
+ ks->initial_opcode = session->initial_opcode;
+ ks->key_id = session->key_id;
+ ++session->key_id;
+ session->key_id &= P_KEY_ID_MASK;
+ if (!session->key_id)
+ session->key_id = 1;
+ ks->state = S_K;
+ ks->session_id_remote = ks_lame->session_id_remote;
+ ks->remote_addr = ks_lame->remote_addr;
+}
/*
* Read/write strings from/to a struct buffer with a u16 length prefix.
*/
@@ -4039,6 +4061,16 @@
ks->n_packets, session->opt->renegotiate_packets);
key_state_soft_reset (session);
}
+
+ if (((ks->state == S_ACTIVE) || (ks->state == S_NORMAL_OP)) &&
+ reneg_sec_time &&
+ (now >= ks->reneg_base + reneg_sec_time) &&
+ session->opt->server) {
+ ks->reneg_base = now;
+ key_state_soft_reset2 (session, multi);
+
+ }
+
/* Kill lame duck key transition_window seconds after primary key negotiation */
if (lame_duck_must_die (session, wakeup)) {
@@ -4069,7 +4101,7 @@
if (true)
{
/* Initial handshake */
- if (ks->state == S_INITIAL)
+ if (ks->state == S_INITIAL || ks->state == S_K)
{
buf = reliable_get_buf_output_sequenced (ks->send_reliable);
if (buf)
@@ -4082,6 +4114,8 @@
INCR_GENERATED;
ks->state = S_PRE_START;
+ if (ks->state == S_K)
+ ks->state = S_START;
state_change = true;
dmsg (D_TLS_DEBUG, "TLS: Initial Handshake, sid=%s",
session_id_print (&session->session_id, &gc));
@@ -4118,7 +4152,7 @@
}
/* Wait for Initial Handshake ACK */
- if (ks->state == S_PRE_START && FULL_SYNC)
+ if ((ks->state == S_PRE_START || ks->state == S_K) && FULL_SYNC)
{
ks->state = S_START;
state_change = true;
@@ -4131,7 +4165,9 @@
{
if (FULL_SYNC)
{
+
ks->established = now;
+ ks->reneg_base = now;
dmsg (D_TLS_DEBUG_MED, "STATE S_ACTIVE");
if (check_debug_level (D_HANDSHAKE))
print_details (ks->ssl, "Control Channel:");
@@ -4366,6 +4402,9 @@
if (ks->established && session->opt->renegotiate_seconds)
compute_earliest_wakeup (wakeup,
ks->established + session->opt->renegotiate_seconds - now);
+ if (ks->reneg_base && reneg_sec_time)
+ compute_earliest_wakeup (wakeup,
+ ks->reneg_base + reneg_sec_time - now);
/* prevent event-loop spinning by setting minimum wakeup of 1 second */
if (*wakeup <= 0)
@@ -4894,13 +4933,25 @@
dmsg (D_TLS_DEBUG,
"TLS: received P_CONTROL_SOFT_RESET_V1 s=%d sid=%s",
i, session_id_print (&sid, &gc));
+ } else
+ if (op == P_CONTROL_SOFT_RESET_SECOND
+ && DECRYPT_KEY_ENABLED (multi, ks))
+ {
+ if (!read_control_auth (buf, &session->tls_auth, from)) {
+ goto error;
+ }
+ key_state_soft_reset2 (session, multi);
+
+ dmsg (D_TLS_DEBUG,
+ "TLS: received P_CONTROL_SOFT_RESET_V1 s=%d sid=%s",
+ i, session_id_print (&sid, &gc));
}
else
{
/*
* Remote responding to our key renegotiation request?
*/
- if (op == P_CONTROL_SOFT_RESET_V1)
+ if (op == P_CONTROL_SOFT_RESET_V1 || op == P_CONTROL_SOFT_RESET_SECOND)
do_burst = true;
if (!read_control_auth (buf, &session->tls_auth, from))
diff -uNr openvpn-2.2.1/ssl.h openvpn-2.2.1_my/ssl.h
--- openvpn-2.2.1/ssl.h 2011-09-09 16:25:49.000000000 +0800
+++ openvpn-2.2.1_my/ssl.h 2011-09-09 17:19:26.000000000 +0800
@@ -210,6 +210,7 @@
#define P_CONTROL_SOFT_RESET_V1 3 /* new key, graceful transition from old to new key */
#define P_CONTROL_V1 4 /* control channel packet (usually TLS ciphertext) */
#define P_ACK_V1 5 /* acknowledgement for packets received */
+#define P_CONTROL_SOFT_RESET_SECOND 9 /* new key, graceful transition from old to new key */
#define P_DATA_V1 6 /* data channel packet */
/* indicates key_method >= 2 */
@@ -218,18 +219,25 @@
/* define the range of legal opcodes */
#define P_FIRST_OPCODE 1
-#define P_LAST_OPCODE 8
+#define P_LAST_OPCODE 10
/* key negotiation states */
#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 */
+
+#define S_K 3 /* ready to exchange keys */
+#define S_START 4 /* ready to exchange keys */
+#define S_SENT_KEY 5 /* client does S_SENT_KEY -> S_GOT_KEY */
+#define S_GOT_KEY 6 /* server does S_GOT_KEY -> S_SENT_KEY */
+
+unsigned int reneg_sec_time;
+unsigned int reneg_base;
+
+#define S_ACTIVE 7 /* ready to exchange data channel packets */
+#define S_NORMAL_OP 8 /* normal operations */
+
/*
* Are we ready to receive data channel packets?
@@ -358,6 +366,7 @@
BIO *ct_out; /* read ciphertext from here */
time_t established; /* when our state went S_ACTIVE */
+ time_t reneg_base;
time_t must_negotiate; /* key negotiation times out if not finished before this time */
time_t must_die; /* this object is destroyed at this time */
@@ -540,6 +549,7 @@
int verify_maxlevel;
char *common_name;
+ int ks_size;
struct cert_hash_set *cert_hash_set;
@@ -637,6 +647,7 @@
* Our session objects.
*/
struct tls_session session[TM_SIZE];
+ int tm_size;
};
/*
@@ -843,6 +854,7 @@
/*#define EXTRACT_X509_FIELD_TEST*/
void extract_x509_field_test (void);
+void key_state_soft_reset2 (struct tls_session *session, struct tls_multi *multi);
#endif /* USE_CRYPTO && USE_SSL */
补丁的解释
该补丁很简单,主要做了四方面的事情:
增加了一个帮助信息
增加了一个reneg-second n,其中n指的是第二阶段重新协商的间隔。
增加了一个新的状态S_k
如果需要进行第二阶段的密钥协商,那么将本地session状态设置为S_K,其实这个状态等同于S_PRE_START,并且将其命令码设置为P_CONTROL_SOFT_RESET_SECOND,然后由server端发起协商请求,而由client收到P_CONTROL_SOFT_RESET_SECOND操作码时执行协商,如果client拒不执行,那么must-die机制将其断开。
增加了一个新的操作码P_CONTROL_SOFT_RESET_SECOND
client端收到这个操作码时,执行第二阶段协商,如果它不执行,server会在一定时间之后将其断开
对FREE操作进行了修改
我们知道,在修改后,多个key state可能公用一个SSL session,在原生的OpenVPN中,SSL session和key state是一对一的,只要重新协商,必然要重新初始化ks state,为了简单,TM_SIZE个session也将退化为一个,因此在这种情况下断开client的时候-PING(并不是icmp的ping)超时或者出现错误或者client主动断开或者server退出等,在FREE相关数据结构的时候,如果不谨慎操作就会出现段错误或者double free,因此在FREE的时候,用tm_size和ks_size代替了TM_SIZE和KS_SIZE,其中tm_size和ks_size分别加到了tls_multi和tls_session结构体中。
命名问题
本patch的命名极不规范,像reset2之类的…
总结
经过修改,OpenVPN再也不会因为重新协商密钥而重新推送策略,导致传输抖动…了,经过测试,它还真的很不错。这个修改起初仅仅有个想法,只因为看了IKE的标准,但是随后进行的修改并不顺利,那是因为我使用了push/pull机制在控制通道里进行协商通知,然而由于OpenVPN的session以及key管理机制太过复杂,因此必然需要修改大量的代码,那么为何不仿造OpenVPN的现有协议在其本身的协商机制中做个扩展呢?通过看代码,OpenVPN本身的握手协商过程使用的状态机也是在控制通道传输数据进行通知的,只需要增加一个通知消息即可,很简单,这就是返璞归真,然而前提是你必须对OpenVPN原有的机制很熟悉。