OpenVPN莫名其妙断线的问题及其解决_第1页
OpenVPN莫名其妙断线的问题及其解决_第2页
OpenVPN莫名其妙断线的问题及其解决_第3页
OpenVPN莫名其妙断线的问题及其解决_第4页
OpenVPN莫名其妙断线的问题及其解决_第5页
已阅读5页,还剩30页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

------------------------------------------------------------------------OpenVPN莫名其妙断线的问题及其解决OpenVPN莫名其妙断线的问题及其解决1.问题不得不说,这是一个OpenVPN的问题,该问题几乎每个使用OpenVPN的人都碰到过,也有很多人在网上发问,然而一直都没有人能给出解决办法,甚至很多帖子上表示因为这个问题而放弃了使用OpenVPN。说实话,我面临这个问题已经两年有余,自从第一次接触OpenVPN,这个问题就一直困扰着我,去过国内外各大论坛也没有找到满意的结果。这几天终于有点闲暇,我决定自己去摸索一下,要感谢公司能给我提供一个环境!最终,我取得了突破性的进展,还是那句话,我把这个结果贴了出来,就是为了以后人们再面临这个问题时可以多一个可选的答案。顺便说一下,并不能说明网上就没人解决过这个问题,因为我所能看到并理解的,只有中文或者英文的帖子或者文章,虽然日文的也在我老婆的帮忙翻译下看过一些,但是还有大量的德文,意大利文,韩文等作为母语的人写出的东西我无法找到并且理解它,因此为了通用性,我本应该用英文来写这篇文章,然而英文水平太垃圾,怕那样连中国人都不能理解了...问题是这样的,OpenVPN在跨越公网上连接时,会莫名其妙的时不时断开,但不经常,也不绝对!由于大部分人使用Windows版本的作为OpenVPN客户端,因此起初一直一为是Windows本身的问题,然而当我用Linux客户端连接时,还是一样,这就是说,很大程度上冤枉了Windows(也并不是完全冤枉,起码Linux就没有DHCP租约的问题),于是既然有了环境,那就折腾一番,因此又是一个惊魂48小时。以下是在客户端断开时服务端的日志(频繁的断开就会有频繁的日志,现在仅仅截取一段):2013-07-2416:53:15MULTI:REAPrange208->224...2013-07-2416:53:16Test证书/31:18014ACKoutputsequencebroken:[5]12342013-07-2416:53:16GETINSTBYREAL:31:18014[succeeded]2013-07-2416:53:16Test证书/31:18014UDPv4READ[22]from31:18014:P_ACK_V1kid=0[1]2013-07-2416:53:16Test证书/31:18014UDPv4WRITE[114]to31:18014:P_CONTROL_V1kid=0[]pid=5DATAlen=1002013-07-2416:53:16Test证书/31:18014ACKoutputsequencebroken:[6]52342013-07-2416:53:16GETINSTBYREAL:31:18014[succeeded]2013-07-2416:53:16Test证书/31:18014UDPv4READ[22]from31:18014:P_ACK_V1kid=0[2]2013-07-2416:53:16Test证书/31:18014UDPv4WRITE[114]to31:18014:P_CONTROL_V1kid=0[]pid=6DATAlen=1002013-07-2416:53:16Test证书/31:18014ACKoutputsequencebroken:[7]56342013-07-2416:53:16GETINSTBYREAL:31:18014[succeeded]2013-07-2416:53:16Test证书/31:18014UDPv4READ[22]from31:18014:P_ACK_V1kid=0[3]2013-07-2416:53:16Test证书/31:18014UDPv4WRITE[114]to31:18014:P_CONTROL_V1kid=0[]pid=7DATAlen=1002013-07-2416:53:16Test证书/31:18014ACKoutputsequencebroken:[8]56742013-07-2416:53:16GETINSTBYREAL:31:18014[succeeded]2013-07-2416:53:16Test证书/31:18014UDPv4READ[22]from31:18014:P_ACK_V1kid=0[4]2013-07-2416:53:16Test证书/31:18014UDPv4WRITE[114]to31:18014:P_CONTROL_V1kid=0[]pid=8DATAlen=1002013-07-2416:53:16Test证书/31:18014ACKoutputsequencebroken:[9]56782013-07-2416:53:16GETINSTBYREAL:31:18014[succeeded]2013-07-2416:53:16Test证书/31:18014UDPv4READ[22]from31:18014:P_ACK_V1kid=0[5]2013-07-2416:53:16Test证书/31:18014UDPv4WRITE[114]to31:18014:P_CONTROL_V1kid=0[]pid=9DATAlen=1002013-07-2416:53:16Test证书/31:18014ACKoutputsequencebroken:[10]96782013-07-2416:53:16GETINSTBYREAL:31:18014[succeeded]2013-07-2416:53:16Test证书/31:18014UDPv4READ[22]from31:18014:P_ACK_V1kid=0[6]2013-07-2416:53:16Test证书/31:18014UDPv4WRITE[114]to31:18014:P_CONTROL_V1kid=0[]pid=10DATAlen=1002013-07-2416:53:16Test证书/31:18014ACKoutputsequencebroken:[11]910782013-07-2416:53:16GETINSTBYREAL:31:18014[succeeded]2013-07-2416:53:16Test证书/31:18014UDPv4READ[22]from31:18014:P_ACK_V1kid=0[7]2013-07-2416:53:16Test证书/31:18014UDPv4WRITE[114]to31:18014:P_CONTROL_V1kid=0[]pid=11DATAlen=1002013-07-2416:53:16Test证书/31:18014ACKoutputsequencebroken:[12]9101182013-07-2416:53:16GETINSTBYREAL:31:18014[succeeded]2013-07-2416:53:16Test证书/31:18014UDPv4READ[22]from31:18014:P_ACK_V1kid=0[9]2013-07-2416:53:16Test证书/31:18014ACKoutputsequencebroken:[12]101182013-07-2416:53:17MULTI:REAPrange240->2562013-07-2416:53:17GETINSTBYREAL:31:18014[succeeded]2013-07-2416:53:17Test证书/31:18014UDPv4READ[22]from31:18014:P_ACK_V1kid=0[10]2013-07-2416:53:17Test证书/31:18014ACKoutputsequencebroken:[12]1182013-07-2416:53:17GETINSTBYREAL:31:18014[succeeded]2013-07-2416:53:17Test证书/31:18014UDPv4READ[22]from31:18014:P_ACK_V1kid=0[11]2013-07-2416:53:17Test证书/31:18014ACKoutputsequencebroken:[12]82013-07-2416:53:18MULTI:REAPrange0->162013-07-2416:53:18Test证书/31:18014TLS:tls_pre_encrypt:key_id=02013-07-2416:53:18Test证书/31:18014SENTPING2013-07-2416:53:18Test证书/31:18014ACKoutputsequencebroken:[12]82013-07-2416:53:18Test证书/31:18014UDPv4WRITE[53]to31:18014:P_DATA_V1kid=0DATAlen=522013-07-2416:53:18Test证书/31:18014ACKoutputsequencebroken:[12]8....持续了60秒没有收到ID为8的ACK,因此一直都是ACKoutputsequencebroken:[12]82013-07-2416:54:15Test证书/31:18014TLSError:TLSkeynegotiationfailedtooccurwithin60seconds(checkyournetworkconnectivity)2013-07-2416:54:15Test证书/31:18014TLSError:TLShandshakefailedwithpeer没隔一段时间就会断一次,并且重连还不一定总能重连成功!因此这里的问题有两点:a.连接正常时断开(ping-restart的情况,上述日志没有展示)b.重连时不成功(上述日志展示的)2.分析使用UDP的OpenVPN就是事多,为了避免重传叠加,在恶劣环境下还真得用UDP。然而OpenVPN实现的UDPreliable层是一个高度简化的“按序确认连接”层,它仅仅确保了数据安序到达,并且有确认机制,和TCP那是没法比。不过如果看一下TCP最初的方案,你会发现,TCP的精髓其实就是OpenVPN的reliable层,后来的复杂性都是针对特定情况的优化!和TCP的实现一样,不对ACK进行ACK对发送端提出了重传滑动窗口未确认包的要求,因为纯ACK可能会丢失,这里先不讨论捎带ACK。ACK一旦丢失,发送端肯定就要重传没有被ACK的包,关键是“什么时候去重传它?”,协议本身一般都有一个或者多个Timer,Timer到期就重传,然而我个人认为这个Timer不能依赖上层,而要在协议本身实现,毕竟重传这种事对上层是不可见的!然而,OpenVPN的reliable层在ACK丢失的应对方面却什么都没有实现,通过以上的日志可以看出,连续的:Test证书/31:18014ACKoutputsequencebroken:[12]8说明ID为8的包一直都得不到重传,并且从:2013-07-2416:53:16Test证书/31:18014UDPv4READ[22]from31:18014:P_ACK_V1kid=0[6]2013-07-2416:53:16Test证书/31:18014UDPv4WRITE[114]to31:18014:P_CONTROL_V1kid=0[]pid=10DATAlen=1002013-07-2416:53:16Test证书/31:18014ACKoutputsequencebroken:[11]910782013-07-2416:53:16GETINSTBYREAL:31:18014[succeeded]2013-07-2416:53:16Test证书/31:18014UDPv4READ[22]from31:18014:P_ACK_V1kid=0[7]2013-07-2416:53:16Test证书/31:18014UDPv4WRITE[114]to31:18014:P_CONTROL_V1kid=0[]pid=11DATAlen=1002013-07-2416:53:16Test证书/31:18014ACKoutputsequencebroken:[12]9101182013-07-2416:53:16GETINSTBYREAL:31:18014[succeeded]2013-07-2416:53:16Test证书/31:18014UDPv4READ[22]from31:18014:P_ACK_V1kid=0[9]2013-07-2416:53:16Test证书/31:18014ACKoutputsequencebroken:[12]101182013-07-2416:53:17MULTI:REAPrange240->2562013-07-2416:53:17GETINSTBYREAL:31:18014[succeeded]2013-07-2416:53:17Test证书/31:18014UDPv4READ[22]from31:18014:P_ACK_V1kid=0[10]这几行日志可以看出,确实是没有收到ID为8的包地ACK,说明它丢失了,接下来发送的数据包将持续填充发送窗口,直到填满,ID为8的包还未重传并且收到对端对其的ACK,因此就导致了ACKoutputsequencebroken,通过查代码,12-8=4,而4正是发送窗口的长度。持续了很久ACKoutputsequencebroken之后,还是没有重传,直到:a.隧道建立之后的ping-restart过期b.隧道建立阶段的TLShandshakefailed实际上,正确的方式应该是,检测到窗口爆满就应该马上重传。TCP通过三次重复ACK知晓丢包,而OpenVPN的reliable则通过ACKoutputsequencebroken知晓ACK丢失,这是一个信号,应该在获取这个信号后做点什么了!3.原始方案方案很简单,那就是在打印ACKoutputsequencebroken的逻辑块内重传所有未确认的包,然而作为一种优化,仅仅重传ID最小的包即可。这是因为,之所以到达ACKoutputsequencebroken,是因为窗口满了,之所以满是因为ID最小的包未确认,占据了很大的一块空间以及其后面的实际上可能已经确认了的空间,因此只要ID最小的包被确认,窗口就放开了,故而仅重传ID最小的包,以期待对端能再次给出确认。方案虽简单,但是不落实到代码还是0,以下是一些尝试4.第一次尝试-出错7月25日下班后,又睡不着了,自己躲在女儿的小屋,开始了coding。首先确认一下对于乱序或者重放的包,对端也能ACK,如果不能,那就要大改了,找到了ssl.c的代码,在tls_pre_decrypt中:[plain]viewplaincopyif(op!=P_ACK_V1&&reliable_can_get(ks->rec_reliable)){packet_id_typeid;/*ExtractthepacketIDfromthepacket*/if(reliable_ack_read_packet_id(buf,&id)){/*Avoiddeadlockbyrejectingpacketthatwouldde-sequentializereceivebuffer*/if(reliable_wont_break_tiality(ks->rec_reliable,id)){if(reliable_not_replay(ks->rec_reliable,id)){/*Saveincomingciphertextpackettoreliablebuffer*/structbuffer*in=reliable_get_buf(ks->rec_reliable);ASSERT(in);ASSERT(buf_copy(in,buf));reliable_mark_active_incoming(ks->rec_reliable,in,id,op);}//注意这个注释,即使是重放包也ACK!而我解决ACK丢失的思路正是重放那个迟迟收不到//ACK的包,期待对端发送ACK,按照随机丢包概率,针对该包的ACK总不能一直丢失吧!/*Processoutgoingacknowledgmentforpacketjustreceived,evenifit'sareplay*/reliable_ack_acknowledge_packet_id(ks->rec_ack,id);}}}有了以上的基础,起码我知道,针对OpenVPN的reliable层修改的代码不多!接下来就是找到修改哪里了,当然是哪里出问题修改哪里!之所以僵持在那里,就是因为“ACKoutputsequencebroken”,所以说我找到了打印这个的地方,在reliable_get_buf_output_sequenced函数中:[plain]viewplaincopystructbuffer*reliable_get_buf_output_sequenced(structreliable*rel){structgc_arenagc=gc_new();inti;packet_id_typemin_id=0;boolmin_id_defined=false;structbuffer*ret=NULL;/*findminimumactivepacket_id*/for(i=0;i<rel->size;++i){conststructreliable_entry*e=&rel->array[i];if(e->active){if(!min_id_defined||e->packet_id<min_id){min_id_defined=true;min_id=e->packet_id;}}}//以下判断没有通过的原因,在上面的日志中已经找到了://...ACKoutputsequencebroken:[12]8//12-8=4,而#defineTLS_RELIABLE_N_SEND_BUFFERS4if(!min_id_defined||(int)(rel->packet_id-min_id)<rel->size){ret=reliable_get_buf(rel);}else{dmsg(D_REL_LOW,"ACKoutputsequencebroken:%s",reliable_print_ids(rel,&gc));}gc_free(&gc);returnret;}因此仅仅需要在打印broken的地方重传packet_id为min_id的那个buf即可![plain]viewplaincopy#ifdefRETRYstructbuffer*reliable_get_buf_output_sequenced(structreliable*rel,int*flag)#elsestructbuffer*reliable_get_buf_output_sequenced(structreliable*rel)#endif{structgc_arenagc=gc_new();inti;packet_id_typemin_id=0;boolmin_id_defined=false;structbuffer*ret=NULL;#ifdefRETRYstructbuffer*retry_buff=NULL;//notnamedreplay_buffer!*flag=0;#endif/*findminimumactivepacket_id*/for(i=0;i<rel->size;++i){conststructreliable_entry*e=&rel->array[i];if(e->active){if(!min_id_defined||e->packet_id<min_id){min_id_defined=true;min_id=e->packet_id;#ifdefRETRY//retry_buff=e->buf;ret=&e->buf;#endif}}}//以下判断没有通过的原因,在上面的日志中已经找到了://...ACKoutputsequencebroken:[12]8//12-8=4,而#defineTLS_RELIABLE_N_SEND_BUFFERS4if(!min_id_defined||(int)(rel->packet_id-min_id)<rel->size){ret=reliable_get_buf(rel);}else{#ifdefRETRY*flag=1;#endifdmsg(D_REL_LOW,"ACKoutputsequencebroken:%s",reliable_print_ids(rel,&gc));}gc_free(&gc);returnret;}相应地,需要修改该函数的调用逻辑,即ssl.c的tls_process,这里不再给出ks->state==S_INITIAL的初始情况:[plain]viewplaincopyif(ks->state>=S_START){#ifdefRETRYintretry=0;intstatus=-1;buf=reliable_get_buf_output_sequenced(ks->send_reliable,&retry);#elsebuf=reliable_get_buf_output_sequenced(ks->send_reliable);#endifif(buf){#ifdefRETRYif(!retry){#endifstatus=key_state_read_ciphertext(multi,ks,buf,PAYLOAD_SIZE_DYNAMIC(&multi->opt.frame));if(status==-1){msg(D_TLS_ERRORS,"TLSError:Ciphertext->reliableTCP/UDPtransportreaderror");gotoerror;}#ifdefRETRY}else{status=1;}#endifif(status==1){reliable_mark_active_outgoing(ks->send_reliable,buf,P_CONTROL_V1);INCR_GENERATED;state_change=true;dmsg(D_TLS_DEBUG,"OutgoingCiphertext->Reliable");}}}洋洋洒洒的不合我风格的规整代码,COOL!可是运行之后,ASSERT失败,明明我重发了ID最小的包,却在write_control_auth的:ASSERT(session_id_write_prepend(&session->session_id,buf));这一句华丽丽得退出!发现buf竟然不是我要重传的那个buffer!作为单线程单进程的OpenVPN,不可能有另外什么地方触动这个buf啊!5.第二次尝试-成功失败!夜以沉默,心思向谁说?然而这个问题没有那么复杂,案件的侦破很简单,那就是看代码,终于找到了reliable_schedule_now函数,关键是它的注释:/*scheduleallpendingpacketsforimmediateretransmit*/重传!对的,是重传!既然OpenVPN本身有了重传,那么我的那个重传就是多此一举了!因此还是按照步骤来吧,直接调用这个接口即可,话说一定要用既有的接口,千万不要重复实现既有逻辑!于是patch变得更加简单了,仅仅修改一个reliable_get_buf_output_sequenced函数即可:[plain]viewplaincopystructbuffer*reliable_get_buf_output_sequenced(structreliable*rel){structgc_arenagc=gc_new();inti;packet_id_typemin_id=0;boolmin_id_defined=false;structbuffer*ret=NULL;/*findminimumactivepacket_id*/for(i=0;i<rel->size;++i){conststructreliable_entry*e=&rel->array[i];if(e->active){if(!min_id_defined||e->packet_id<min_id){min_id_defined=true;min_id=e->packet_id;}}}if(!min_id_defined||(int)(rel->packet_id-min_id)<rel->size){ret=reliable_get_buf(rel);}else{#ifdefRETRYreliable_schedule_now(rel);//顺便把日志也改了dmsg(D_REL_LOW,"ACKoutputsequencebroken:%s,retransmitimmediately",reliable_print_ids(rel,&gc));#elsedmsg(D_REL_LOW,"ACKoutputsequencebroken:%s",reliable_print_ids(rel,&gc));#endif}gc_free(&gc);returnret;}structbuffer*reliable_get_buf_output_sequenced(structreliable*rel){structgc_arenagc=gc_new();inti;packet_id_typemin_id=0;boolmin_id_defined=false;structbuffer*ret=NULL;/*findminimumactivepacket_id*/for(i=0;i<rel->size;++i){conststructreliable_entry*e=&rel->array[i];if(e->active){if(!min_id_defined||e->packet_id<min_id){min_id_defined=true;min_id=e->packet_id;}}}if(!min_id_defined||(int)(rel->packet_id-min_id)<rel->size){ret=reliable_get_buf(rel);}else{#ifdefRETRYreliable_schedule_now(rel);//顺便把日志也改了dmsg(D_REL_LOW,"ACKoutputsequencebroken:%s,retransmitimmediately",reliable_print_ids(rel,&gc));#elsedmsg(D_REL_LOW,"ACKoutputsequencebroken:%s",reliable_print_ids(rel,&gc));#endif}gc_free(&gc);returnret;}别的什么都不用改!6.优化(去掉副作用)上面的修改虽然简单,但是带来了一个副作用,那就是一次broken会带来reliable窗口里面所有的包都会重传,其实我们只需要重传ID最小的那个就可以了,毕竟它是罪魁祸首!如果因为网络环境导致的ACK丢失,继而重传了所有的包,可能会带来更多的丢包,这个在TCP上体现最深刻了,因此只重传最小ID的那个包,既然它的ACK丢失导致了broken,那么就再发它一次,保证网络管道上的包数量守恒!另外,如果毫无判断地重传,可能会误判很多ACK丢包,其实有些ID稍微大些的ACK并没有丢,它只是乱序到达了而已!没有免费的午餐,因此不得不作的就是修改reliable_schedule_now逻辑:[plain]viewplaincopystructbuffer*reliable_get_buf_output_sequenced(structreliable*rel){...#ifdefRETRYreliable_schedule_id(rel,0,min_id);//顺便把日志也改了dmsg(D_REL_LOW,"ACKoutputsequencebroken:%s,retransmitimmediately",reliable_print_ids(rel,&gc));#elsedmsg(D_REL_LOW,"ACKoutputsequencebroken:%s",reliable_print_ids(rel,&gc));#endif...}structbuffer*reliable_get_buf_output_sequenced(structreliable*rel){...#ifdefRETRYreliable_schedule_id(rel,0,min_id);//顺便把日志也改了dmsg(D_REL_LOW,"ACKoutputsequencebroken:%s,retransmitimmediately",reliable_print_ids(rel,&gc));#elsedmsg(D_REL_LOW,"ACKoutputsequencebroken:%s",reliable_print_ids(rel,&gc));#endif...}真正的修改是schedule逻辑:[plain]viewplaincopy#ifdefRETRYvoidreliable_schedule_id(structreliable*rel,time_ttimeout,packet_id_typeid){inti;dmsg(D_REL_DEBUG,"ACKreliable_schedule_now");rel->hold=false;for(i=0;i<rel->size;++i){structreliable_entry*e=&rel->array[i];if(e->active){if(id!=0&&e->packet_id==id){e->next_try=now+timeout;e->timeout=rel->initial_timeout;break;}else{e->next_try=now+timeout;e->timeout=rel->initial_timeout;}}}}#ifdefRETRYvoidreliable_schedule_id(structreliable*rel,time_ttimeout,packet_id_typeid){inti;dmsg(D_REL_DEBUG,"ACKreliable_schedule_now");rel->hold=false;for(i=0;i<rel->size;++i){structreliable_entry*e=&rel->array[i];if(e->active){if(id!=0&&e->packet_id==id){e->next_try=now+timeout;e->timeout=rel->initial_timeout;break;}else{e->next_try=now+timeout;e->timeout=rel->initial_timeout;}}}}#endif/*scheduleallpendingpacketsforimmediateretransmit*/voidreliable_schedule_now(structreliable*rel){#ifdefRETRY#调用新接口函数returnreliable_schedule_id(rel,0,0);#elseinti;dmsg(D_REL_DEBUG,"ACKreliable_schedule_now");rel->hold=false;for(i=0;i<rel->size;++i){structreliable_entry*e=&rel->array[i];if(e->active){e->next_try=now;e->timeout=rel->initial_timeout;}}#endif}新的reliable_schedule_id有3个参数,增加了一个timeout参数用来微调备用,另一个id参数用来指示哪一个ID的包需要重传。7.说明这次我给出的修改仅仅解决ACK丢失的问题,至于说乱序或者别的情况导致的问题,那还得需要上层的SSL发现并reset。如果说本来链路带宽就很低,且ping-restart时间又很短,那么解决办法就是增加ping-restart时间了。说白了本解决方案仅仅是“在由于ACK丢失导致发送窗口爆满情况下,尽快重传使窗口尽快恢复可用”。8.测试改完了代码只是说按照常规理论完成了修改,至于说有没有什么效果,还要测试数据来说明。今天公司上海对北京的公网测试环境不给力,我也总不能把希望全部寄托在公司的环境,我要模拟恶劣网络的丢包现象。鉴于对iptables的舍我其谁般的理解,我这次还是用了iptables,这次用的是其statisticmatch,测试拓扑如下:其中中间的LinuxBox作为一个路由器,顺便模拟恶劣网络,以下的规则模拟了按照百分比丢包的现场:#模拟30%左右的丢包,这个丢包率已经相当大了!iptables

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论