1. 背景介绍

听着《梦中的额吉》,《天堂》... 女儿在睡觉... 外面细雨... 中秋小长假,完成自己的 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,希望能在下个版本中见到它的身影,如果有朋友觉得有改进的地方,那就大胆的改进它,然后提交... 这对于每一个开发人员都是一笔财富!

2. 尝试修改

前文《让 OpenVPN 实现 IKE 似的两阶段密钥协商》,代码已然完成,然则不便公开,同时代码丑陋之极,无颜!总的来讲,那只是一次初期的预研,逻辑如下:

2.1. 在 S_ACTIVE 之后定义新的状态码;

2.2. 当需要进行第二阶段协商的时候,像 client push 一个字符串,将本地状态设置一个中间状态;

2.3. client 端收到 server 端 push 下来的对应字符串之后,将本地状态设置为一个新定义的预协商状态,并且写入新的 key 协商消息,同时 reply 一个字符串;

2.4. server 端收到 client 端 reply 的字符串之后,将本地状态设置为预协商的状态,读取 key 协商消息;

2.5. 双方开始在以前的 SSL 加密信道上重新协商新的对称密钥;

2.6. 最终,二者到达一个新的 S_ACTIVE 状态;

2.7. 协商完成。

这个手术开始非常让我自豪,因为它有效的降低了传输延迟,然而经过后续的仔细测试,发现它引起了大量的丢包,使用 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 时间开始一段音乐很不错,引发了更多的想法)

3. 确定如何修改

当初的尝试之所以出现了丢包,那是因为我忽略了 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 重传可能是由于其拥塞避免阀值设置得过高所导致,或者由于其拥塞控制算法过于自私所导致,然而可悲的是,很多人都认为重传越多,网络状况越差!这样的人不在少数,还总是用什么业务逻辑之类词汇将真理打发)

4. 最终的补丁

起初我想直接提交这个补丁,但是由于 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-&gt;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, &amp;session-&gt;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 */

复制代码

5. 补丁的解释

该补丁很简单,主要做了四方面的事情:

5.1. 增加了一个帮助信息

增加了一个 reneg-second n,其中 n 指的是第二阶段重新协商的间隔。

5.2. 增加了一个新的状态 S_k

如果需要进行第二阶段的密钥协商,那么将本地 session 状态设置为 S_K,其实这个状态等同于 S_PRE_START,并且将其命令码设置为 P_CONTROL_SOFT_RESET_SECOND,然后由 server 端发起协商请求,而由 client 收到 P_CONTROL_SOFT_RESET_SECOND 操作码时执行协商,如果 client 拒不执行,那么 must-die 机制将其断开。

5.3. 增加了一个新的操作码 P_CONTROL_SOFT_RESET_SECOND

client 端收到这个操作码时,执行第二阶段协商,如果它不执行,server 会在一定时间之后将其断开

5.4. 对 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 结构体中。

5.5. 命名问题

本 patch 的命名极不规范,像 reset2 之类的...

6. 总结

经过修改,OpenVPN 再也不会因为重新协商密钥而重新推送策略,导致传输抖动... 了,经过测试,它还真的很不错。这个修改起初仅仅有个想法,只因为看了 IKE 的标准,但是随后进行的修改并不顺利,那是因为我使用了 push/pull 机制在控制通道里进行协商通知,然而由于 OpenVPN 的 session 以及 key 管理机制太过复杂,因此必然需要修改大量的代码,那么为何不仿造 OpenVPN 的现有协议在其本身的协商机制中做个扩展呢?通过看代码,OpenVPN 本身的握手协商过程使用的状态机也是在控制通道传输数据进行通知的,只需要增加一个通知消息即可,很简单,这就是返璞归真,然而前提是你必须对 OpenVPN 原有的机制很熟悉。


reference

  • 安全

    互联网安全是一门涉及计算机科学、网络技术、通信技术、密码技术等多种学科的综合性学科。

    64 引用
感谢    赞同    分享    收藏    关注    反对    举报    ...