很多年以前,网易推了一个tcp流量复制工具叫tcpcopy。2013年07月我入职新公司,大概10月份接触到tcpcopy,为tcpcopy修了两个bug,一个是由于公司内网的IP tunnel的问题tcpcopy无法正常工作;另一个是一个严重的性能bug。两个bug都用邮件方式向原作者反馈了,尤其第二个bug原作者在博客上发文感谢。在接下来的二次开发中,由于没办法看懂tcpcopy的tcp会话部分的代码,当时建议作者按照tcp的11个状态写成状态机,作者拒绝了。于是,我根据当时的业务情况重写了一个新的TCPCOPY叫TCPGO。技术原理和tcpcopy是一样的,但tcp会话部分写成了标准 的11个tcp状态的状态机(见源代码中的tcpsession类,漂亮的运行在应用空间而不是内核态的精简的tcp状态机)。另部署方式很不一样,要简单很多。为了开发效率,开发语言用了C++,用了boost库还加了lua帮助写业务代码。
最近腾讯云技术开发者论坛的产品经理邀请各位同事多写技术文章,确信可以把以前内网上发的文章脱敏后发出。于是,就把这个4年多以前(2013年12月--2014年4月,达到预期研发目标后就再也没更新过了)的项目的技术文档贴出来了。
以下是正文,重点在“原理”小结,结尾有关键代码:
//////////////////////
TCPGO:基于真实TCP流量的测试工具
for version 0.8.2
Document Version v0.1.3
April 15 2014
Contents
TCPGO是一个基于TCP欺骗技术的伪造TCP会话的工具。内部开发时的工程项目名为Horos,是占星术Horoscope的缩写,喻意使用本工具可以占卜未来。
TCPGO从线上机上复制用户的真实请求,并把这些请求实时播放给测试服务器;或者把请求保存下来,以后再离线播放给测试服务器。从测试服务器的视角来来,它如同在和真实的用户交互。一个简单的示意图如下:
举例来说,TCPGO大概能做以下几件事情:
1.能够把线上机的真实用户的TCP请求实时复制到测试环境的机器上,使测试环境的TCP服务器误以为和真实的用户通信,观察测试服务器的运行情况。使得TCP服务器在正式灰度前暴露出更多的问题。
2.先把真实用户的TCP请求保存在PCAP抓包文件中,TCPGO可以以这些抓包文件为素材,把流量重放给服务器。
3.TCPGO也是一个TCP准压力测试工具。它可以设定并发的用户连接数,模拟大量用户并发,测试TCP服务器的性能。单个TCPGO,无串联,关闭Lua插件功能,同网段,针对nginx部署时TCPGO每秒完成TCP会话的峰值为近10K个TCP会话,15分钟内的平均指标为:每秒完成6K个TCP会话。TCPGO的配置文件配置选项设定不同时,会有不同的性能表现。另TCPGO本身还有优化的空间,但目前TCPGO已经基本够用了,暂时未有继续开发的计划。
4.TCPGO还可以是一个自动化功能测试工具。通过TCPGO的Lua插件功能,可以快编写针对真实流量的测试用例。其实,可以用Lua插件功能做测试之外的更多事情。
5.TCPGO为能服务器开发工程师搭建一个开发用模拟环境,让码农们可以像写桌面应用一样边写边调。
6.TCPGO可以作为学习TCP/IP的一个非常简单的参考实现。TCPGO在用户空间实现了一个简单可用的TCP/IP栈的IP层和TCP层。TCP层实现了一个粗糙的滑动窗口,有包重传机制,标准的11个TCP状态转换。
使用TCPGO,不需要在线上生产服务器运行消耗大量资源的程序,只需在生产服务器运行两个准标准化UNIX工具:tcpdump负责抓包,和netcat(有些发行版名nc或ncat)负责建立和维持一个TCP链接,向TCPGO供应流量。如果TCPGO播放离线流量包*.pcap,而非实时的生产机流量,则可略去生产机上的操作。
在运行TCPGO的机器上,按照配置模板更改几个简单的配置选项即可使用。如指定测试服务器的IP,监听端口;模拟用户的并发数;以及高级一点的调整TCP会话的一些选项。
在测试服务器上需要更改路由表,使得发往外网的IP包不能去到外网。这些发往外网的IP包需要被最终发到运行TCPGO的机器,参见下文详细解释。
对于单台TCPGO模拟用户数还不能满足测试服务器压力要求的情况下,TCPGO还设计了串联的方式,让多个TCPGO实例在多台机器上工作,并同时为同一台测试服务器模拟用户。
TCPCOPY是类似的一个开源的TCP流量复制工具。在使用TCPCOPY,以及在TCPCOPY的基础上进行二次开发的过程中,遇到了很多问题。最主要的问题是调试时很难追踪,调试效率很低。TCPCOPY的代码的最核心部分:TCP状态机的实现并不严格遵守TCP的工业标准。TCP标准指定的11个标准状态很难和TCPCOPY的代码对应起来。在和TCPCOPY的作者的沟通中,笔者提出按照TCP的11个标准状态实现TCP会话相关代码,以方便理解和调试。作者认为这样做使得代码太复杂,而笔者认为按照标准做事是让代码变得简单。截止本文档撰写的时间2014年04月11号,TCPCOPY的最新版本0.9.8的实现中,最核心的TCP状态机部分,仍然晦涩难懂。
2013年12月初准备在TCPCOPY的基础上二次开发个性定制的TCP流量回放工具时,再一次迷失在TCPCOPY的TCP会话状态的代码中。与其再投入时间去理解这些复杂的不遵守标准的代码,不如按照TCP标准的11个状态写一个小巧的TCP流量复制和回放工具。
在研发层面上, TCPGO的研发成功使得在TCP流量复制和重放相关的领域继续二次开发,调试定位错误变得得心应手。
在使用部署层面上,TCPGO更易于安装和部署。而且,当都工作在实时流量复制状态时,TCPGO对线上机的性能影响要比TCPCOPY小。另外,TCPGO还支持TCPGO和测试服务器跨网段部署,TCPGO支持Lua插件,TCPGO的日志和调试控制台工具简单易懂。
对于不依赖会话上下文的请求,TCPGO把客户请求回放给测试用服务器,确实能给测试服务器提供了一个接近在线实际情况的运行环境。特殊地,对于TCP一问一答就立即关闭的短连接,可以被认为是不依赖上下文的特例。这种情况下,TCPGO完美支持。如果是长连接,TCPGO支持得不够好,它在TCP会话正常结束四挥手后才发送流量。所以,如果是一个持续很久的长链接,TCPGO不适用。需要改源代码,添加实时发送TCP流量的功能。
如果请求依赖上下文,生产服务器和测试服务器的回复有逻辑上的区别。那么,回放线上机上录制的客户请求给测试服务器,会让测试服务器发觉这并不是来自真实的用户的请求。
TCPGO预计以二进制包形式发布的计划因时间原因被无限期推后了。编译安装是唯一官方途径。
2.1.1 TCPGO依赖的库
BOOST:
TCPGO依赖BOOST库中的filesystem,regex,thread三个需要编译(NOT header only)的库。实际上filesystem是C++ TR2的一部分,较新的gcc和msvc都支持filesystem;而regex和thread则进入了C++11标准。但考虑到生产环境和开发环境中的旧编译系统,需要先编译BOOST的这三个库文件。另外,为提高代码生产效率和代码质量,为了快速开发和编写安全C++代码,TCPGO还非常广泛的使用了BOOST的大量基础设施。实际上这些设施的大部分都已经进入C++11标准。
libpcap 和 libpcap-dev:
TCPGO依赖libpcap抓包。大部分机器上都有这两个库。如果编译时提示没有找到pcap.h头文件,请确定libpcap-dev开发包是否已经安装。大多数linux发行版取libpcap-dev类似的名字。
Lua:
TCPGO的插件系统和调试控制台基于Lua技术。TCPGO在编译期会静态链接liblua.a。如果需要自已编译lua,可能会碰到缺少libreadline的问题。TCPGO目前的makefile会编译搭载的lua源文件,如果已经有lua静态库或者动态库,需要自行修改makefile。
libreadline:
TCPGO并不直接依赖libreadline,但lua虚拟机依赖libreadline以提供一个相对友好的控制台。
2.1.2 编译BOOST
开发TCPGO时用的是1.5.5版的BOOST库,未试验其它版本。推荐在官网http://www.boost.org/users/download/ 下载1.5.5版。
需要编译filesystem, regex, thread三个库。
参考安装步骤:
1.解压boost.1.55压缩包,cd到 boost_1_55_0 目录。
2../bootstrap.sh –with-libraries= filesystem,regex,thread --prefix=/usr/local
3../b2 install
4.ldconfig
对于有包管理的Linux发行版,可用各自的包管理工具更快捷安装。
2.1.3 编译Lua
TCPGO目前版本0.8.2(2014年4月11号止),搭载了Lua 5.2.3的源代码。TCPGO的根目录下的makefile会编译搭载的Lua代码。按照Lua5.2.3官方的说法:如果编译Lua失败,请先确认已经安装libreadline库,如果链接Lua失败,请用make linux MYLIBS=-ltermcap。
如果想使用自行编译的Lua,需要适当修改TCPGO的makefile文件。为了节省时间,不建议这么做。
2.1.4 编译TCPGO
准备好BOOST以后,编译TCPGO实际上非常简单。只需要一个make命令。make成功后,在bins目录下得到生成的二进制文件,horos或者tcpgo。Tcpgo是horos的硬链接,horos是tcpgo项目最开始的名字。Bins目录下还有一个my.conf文件,和一个my.conf.template文件,提供了供参考的配置文件。配置文件的说明可参见下文。
如果make失败,报链接时找不到boost相关的库文件,请确认boost库是否安装,是否运行了ldconfig更新系统的动态链接库缓存。
TCPGO有配置文件,更为方便,而且可配置的字段更多。如果配置好了配置文件,则可以不指定任何命令行选项,运行TCPGO。对于在命令行和配置文件中同时指定的字段,命令行覆盖配置文件中的指定。下面列出TCPGO的命令行字段:
-x conf_file_path , --conf conf_file_path
该选项指定TCPGO将读取的配置文件。缺省情况下读取当前工作目录下的my.conf。
-f pcap_file_path, --pcapfile pcap_file_path
该选项对应配置文件中的可选配置项MAIN. pcap_file_path,它指定TCPGO需要在正式工作前加载离线流量文件pcap_file_path。流量文件一般由tcpdump工具生成。
-d testing_server_ip, --dst-addr pcap_file_path
该选项对应配置文件中必选配置项MAIN.dst_addr,它指定测试服务器的IP地址,TCPGO将模拟用户向该IP发送报文。
-p testing_server_port, --dst_port testing_server_port
该选项对应配置文件中必须配置项MAIN.dst_port,它指定测试服务器的服务端口,TCPGO的请求报文将发向该端口。
-c concurrency, --concurrency-limit concurrency
该选项对应配置文件中的可选配置项MAIN. concurrency_limit,它指定TCPGO将同时最多维持多少TCP会话与测试服务器交互。
-r on_or_off, --random-port on_or_off
该选项对应配置文件中的可选配置项MAIN. onoff_random_port,这是一个开关值,0表示关闭,非0表示开启。开启时,真实客户机的IP包的源端口会被一个随机值填充。这样,方便播放离线流量文件。因为,如果在很短的时候向测试服务器发送相同源地址和源端口组合的IP报文,会有非常大的概率很到测试服务器的RST或者无法建立连接。此时,测试服务器的相应TCP会话还没有结束。所以,选择一个随机端口号规避这种情况。
-h, --help
该选项打印帮助信息。这个选项的功能很久没有维护了。
-v, --version
该选项打印版本信息。
TCPGO 0.8.2的配置选项分四大节: MAIN,SESSION,TESTSUITE,LOG。注释由分号;指示。
一个已知且不会被修复的BUG是,即使由分号;起头的注释行,如果该行有等号=,等号前的部分被认为是选项名,如果有重名,TCPGO会报告有重复的选项并退出。该BUG源于使用的配置文件解析库,没有计划修复它,如果遇到,简单地在行首加上任意字符规僻这个BUG。你很可能不会遇到这个BUG。
2.3.1 MAIN节:
本节中列出一些基本配置。
pcap_file_path:
可选非必需普通选择。它指定TCPGO在正式运行前读取的PCAP流量文件路径。
对应命令行中的-x或-- conf选项。
无缺省值。
dst_addr:
必选普通选项。它指定测试服务器的IP地址,TCPGO将模拟用户向该IP发送报文。
对应命令行中的-d或—dst_addr选项。
无缺省值。
dst_port:
必选普通选项。它指定测试服务器的端口。TCPGO将把IP报文发向该端口。
对应命令行中的-p或—dst-port选项。
无缺省值。
concurrency_limit:
可选非必需普通选项。它指定TCPGO将同时维持的TCP会话的最大数量。
对应命令行中的-c或--concurrency-limit选项。
0.8.2版缺省值为1000.
onoff_random_port:
可选非必需高级选项。建议开启为1。开启时,TCPGO随机改变客户机的源端口。原因参见命令行的-r或—random-port选项。
0.8.2版缺省值为1。
accidental_death_pcap_file_limit:
可选非必需高级选项。对于每个TCP会话,如果不是正常的四次挥手终止,这个会话的收发包记录会被记到一个pcap文件中,用于分析为什么这个会话会非正常终止,从而帮助定位分析问题。这个数值指定最多保存这类文件的数目。
没有对应的命令行选项。
0.8.2版的缺省值为100。
sniff_method:
该选项指定如何抓取测试服务器的回包,可以有:raw,pcap,tcp三种方式。当指定为raw方式抓取回复包时,TCPGO使用linux的RAW SOCKET抓取包;当使用pcap方式时,TCPGO使用libpcap库抓取包;当使用tcp方式时,TCPGO从1992端口抓取回复包。对于raw和pcap方式,需要测试服务器设置路由表把发往外网的IP包发到TCPGO的机器;而使用tcp方式时,虽然测试服务器仍然需要设置路由表,但并不必须使得发往外网的IP包路由到TCPGO的机器,而是通过tcpdump和netcat的命令组合向TCPGO供应测试服务器的回包。因此,使用tcp的嗅探方式,可以跨网段部署TCPGO。
没有对应的命令行选项。
0.8.2版的缺省值为RAW。
asio_thrd_num:
可选非必需高级选项。该选项指定TCPGO的PROACTOR服务器模型启用多少个线程。如果asio_thrd_num设置为负1或者0,TCPGO会检查机器上有多少个CPU核心。设核心数目是n,则asio_thrd_num被设置为max{n-1, 2}。建议用户设定这个选项的值为负1或者0,让TCPGO自行计算需要的线程数目。
没有对应的命令行选项。
0.8.2版的缺省值为负1。
pkt_pass_rate:
可选非必需一般选项。该选项指定实时导入的流量以多大的概率被TCPGO接收。它的单位是千分之一,如果设定为1000,则所有的流量都被接收。如果设定为0,则所有的流量都被丢弃。如果设定为500,则刚好一半的流量被接收。
没有对应的命令行选项。
0.8.2版的缺省值为1000。
2.3.2 SESSION节:
本节是用来调优TCP会话的配置选项。本文中所有的选项都没有对应的命令行选项。
session_count_limit:
可选非必需一般选项。该选项指定TCPGO存于内存中的TCP会话总数的最大值。这些会话有可能在执行状态,也有可能还在等待执行的状态。同时处于执行状态的TCP会话最大数目由MAIN节中的concurrency_limit选项指定。
如果TCPGO存于内存中的TCP会话数目超过了指定的最大值。TCPGO就不会再接受新的TCP会话的流量注入,开始执行流量控制的逻辑,所有在执行流量控制期间带来的新的TCP会话的流量会被丢弃。6
0.8.2版的缺省值为10000。
response_from_peer_time_out:
可选非必需高级选项。该选项指定测试服务器超时的时间,单位为百分之一秒。如果对于某个TCP会话,测试服务器在该时间段内没有响应,则该TCP会话会因服务器响应超时退出。
0.8.2版的缺省值为300,即3秒。建议调整这个值。
have_to_send_data_within_this_timeperiod:
可选非必需高级选项。该选项指定TCPGO模拟的用户必须在多长的时间内向服务器发送报文,单位为百分之一秒。如果对于某个TCP会话,模拟的用户在该时间段内没有发送报文给测试服务器,则该TCP会话会因模拟用户无响应退出。
0.8.2版的缺省值为300,即3秒。建议调整这个值。
injecting_rt_traffic_timeout:
可选非必需高级选项。在向TCPGO注入实时流量的时候,对于每一个新到的TCP会话,TCPGO会先记录下来,并继续等待属于这个会话的其它报文,一直等到握手报文和挥手报文,以及之间的报文都收完整。如果在一个比较长的时间内仍然没有收完整,这个会话就会被放弃。该选项指定的时间即这个等待时间阀值,单位百分之一秒。
0.8.2版的缺省值为4000,即40秒。建议调整这个值。
retransmit_time_interval:
可选非必需高级选项。对于一个TCP会话中的IP报文,如果过了一段时间还没有被对方确认,则该报文会被重发。该选项指定这个重发时限,单位百分之一秒。
0.8.2版的缺省值为25,即0.25秒。建议调整这个值。
wait_for_fin_from_peer_time_out:
可选非必需高级选项。如果TCPGO模拟的用户主动关闭会话,则会话会进入FIN_WAIT1或者FIN_WAIT2状态,此时会等待测试服务器发送FIN挥手报文。如果等待时间超时,测试服务器仍然没有发送FIN报文,则对应会话会退出。该选项指定这个超时时间,单位为百分之一秒。
0.8.2版的缺省值为400,即4秒。建议调整这个值。
enable_active_close:
可选,但必须清楚这选设定的高级选项。指定TCPGO模拟的用户会不会主动关闭。该值为1时,启用主动关闭,该值为0时,关闭主动关闭。假如测试服务器是Apache Web服务器,这个值应该设置为1,因为Apache服务器不主动关闭TCP会话。
0.8.2版的缺省值为0,即关闭。TCPGO用户务必清楚这项设定产生的后果。
clone:
可选非必须高级选项。
这个值指定:针对注入TCPGO的每个TCP会话的流量,将复制多少个相同类容的TCP会话。复制的会话和原会话的内容相同,源端口号相同,但是源IP地址不同。IP地址由更改原会话的源IP地址的主机号得到。如果该值为0,表示不复制。如果该值为1,表示复制一份。此项设定的最大值是253。
0.8.2版的缺省值为0,即关闭。
request_pattern:
可选非必须一般选项。
指定正则式筛选流量。如果启用这个选项,那么这个正则式会被应用到请求报文上,如果匹配,对应的TCP会话才会被发往测试机。
0.8.2版的缺省值是不指定该选项,也就是不启用正则式筛选流量功能。建议不需要这个功能时在配置文件中注释掉该选项,或者不指定。复杂的正则式匹配比较耗时,所以尽可能的使用简单正则式并使用anchor字符。
2.3.3 TESTSUITE节:
该节指定测试插件的相关选项,实际上编写的插件不仅仅可以用来测试。
lua_scripts_home:
可选非必须一般选项。指定lua插件所在的目录,插件有.lua扩展名。参见下文的TCPGO的插件部分。
so_home:
可选非必须一般选项。指定so插件所在的目录。
2.3.4 LOG节:
log_on:
可选非必须一般选项。指定是否开启日志。日志会写到当前工作目录下的h.log中。每次TCPGO运行,如果开启了日志,都会清空上次的所有日志。关闭日志将得到少许性能提升。
duplicate_log_to_stdout:
可选非必须一般选项。指定是否把日志内存复制到标准输出。这个选项只在调试,或者对性能无要求时使用。开启此选项后,每一行日志输出都会调用glibc函数fflush(),使标准C缓冲中的数据刷新到磁盘。
设置路由:
部署TCPGO的必不可少的一步是让测试服务器的回复,发往外网的IP包不能走到外网。否则,一般情况下会从外网收到RST包,或者会干扰处于外网的真实TCP会话。
目前的做法是设置路由,让测试服务器的发往外网的IP包被发到一台没有路由功能的机器。这样,这台没有路由功能的机器就会把IP包丢弃。
一个参考做法是:
# route del default 删掉默认路由
# route add default gw 10.217.152.190 dev eth0 改默认路由为另一个不是路由器的机器
更多思考:
尝试设置iptable,让测试服务器的发往外网的IP包被丢弃。笔者不清楚通过iptable丢充发往外网的IP包,会不会妨碍抓取这些被丢弃的IP包。
同网段部署:
同网段部署指的是TCPGO和测试服务器在同一个网段,而对线上机是否和TCPGO在同一个网段不做要求。
同网段部署的一个例子如下图所示:
上图的第一个步骤更改路由,让测试服务器回复的IP包被路由到运行TCPGO的机器192.168.1.200,这样TCPGO就能抓取到这些回复包。另外,这些IP包将被192.168.1.200丢弃,不会发到外网。
第二个步骤运行TCPGO,在命令行简单的指定了两个选项。它们是测试服务器的IP地址和监听端口。实际上,也可以不指定命令行选项,而是定制配置文件。默认的配置文件是工作目录下的my.conf,参见-x命令行选项指定配置文件路径。
第三个步骤是可选的。它从离线流量文件中读取流量发给TCPGO。
第四个步骤是在线上服务器192.168.1.2用tcpdump抓取实时流量,发往标准输出;然后管道到netcat,netcat把从标准输入中读到流量,通过TCP转发给运行在192.168.1.200上的1993端口;TCPGO运行在192.168.1.200上,监听1993端口获得流量。
当然,可以同时从多台线上机器抓取流量供给TCPGO。
跨网段部署:
跨网段部署的操作和同网段部署差不多。只是,测试服务器和运行TCPGO的机器不在同一个网段时,测试服务器回复的IP报文不能通过路由的方式发到运行TCPGO的机器上。这时,通过TCP的方式在测试服务器用tcpdump抓取流量,再用netcat把流量跨网段发给TCPGO监听的1992端口。这个操作与抓取线上流量类似。这种情况下,TCPGO的配置文件的MAIN.sniff_method需要设置为TCP。
需要注意的是,仅管已经不是通过路由的方式向TCPGO发送测试服务器的回复报文。但设置路由表这一步仍然不可缺少,仍然需要把这些IP包发给与测试服务器同一网段的任意一台没有路由功能的机器。
跨网段发送测试服务器回复报文的技术也可用在同网段部署。
TCPGO的串联:
这个功能并没有被严格测试过,但在开发过程中验证到,证明切实可行。TCPGO的串联是为了解决单个TCPGO没有办法达到测试服务器期望的压力,需要多个TCPGO串联起来模拟。设计时的粗略想法是,假设带宽足够,抓包的丢包概率也很低,如果一个TCPGO支撑每秒钟完成3K个TCP短连接,两台TCPGO串联后支撑每秒钟完成6K个TCP短连接。
TCPGO的串联是由一个有名管道/tmp/horos.fifo支持的。每个运行的TCPGO都会把得到的测试机回复报文写到/tmp/horos.fifo。于是,可以用标准UNIX工具把回复报文从这个有名管道读出,再用netcat传给下一个TCPGO。如图所示:
上图的例子中,两个TCPGO被串联起来。第一个TCPGO从两台线上机上得到实时流量,第二个TCPGO从一台线上机上得到实时流量。测试服务器把回复IP包路由到运行第一个TCPGO的机器上。第一个TCPGO又把这些回复报文写往有名管道/tmp/horos.fifo。于是,用cat命令从该有名管道读出测试服务器的回复报文,再通过netcat发往第二个TCPGO。因此,亦可用同样的方式连接更多的TCPGO。
TCPGO的插件系统基于Lua语言。所有的插件以扩展名.lua结尾,都放在一个目录下面,可以充许有子目录的树结构。这个存放插件的目录的路径由配置文件的TESTSUITE. lua_scripts_home选项指定。
该插件系统实际上只针对短连接的TCP实现,对于长连接的价值应该相当有限。TCPGO每完成一个TCP会话,会把客户机的IP,客户机的端口号,客户机的请求,测试服务器的回复这四个参数传给Lua插件。所有的Lua插件会得到这四个参数并顺序执行。
Lua插件可以随时修改,并可从运行时的TCPGO热拨插。参加TCPGO控制台的reload_testsuite()命令。
从Lua的语法的角度来讲,每一个lua插件都定义了一个lua模块。Lua模块的详细解释可参见文章:Lua Modules Tutorial http://lua-users.org/wiki/ModulesTutorial。
TCPGO定义的Lua插件写法除了遵守Lua Module的写法外,额外的规定每个插件必须实现一个函数main,它接受三个参数:client_ip,client_port, req, resp。client_ip是字符串型,传入客户机的IP地址;client_port是数值型,传入客户机的端口号;req是字符串型,表示客户机的请求;resp是字符串型,表示测试服务器的回复。与C语言的字符串不一样,req和resp字符串中间有可能出现比特位全0的字节。
另外,Lua插件文件的名字,除去扩展名,会被注册到TCPGO的Lua虚拟机环境中,可被TCPGO的调试控制台使用。(对于调试控制台请参见下文。)假如,一个Lua插件名为foo.lua,则foo这个名字会被注册到Lua虚拟机环境中。从Lua语法的角度看,foo成为一个Lua全局变量,它指向一个Lua模块。对于熟悉Lua5.2语法的看官,可能会注意到,为了不污染全局名字域,Lua5.2标准的模块并不注册到Lua的全局全间。但为什么上文说到foo被注册到了Lua的全局空间咧,这是TCPGO额外做的工作。
举例说明,一个简单的Lua插件如下图:
插件的入口是main函数。入到main函数后,先打印客户机的IP和端口号,然后调用两个函数分别处理请求和回复。这两个函数亦都只做简单的打印。
看另一个稍微复杂的例子:
这个插件做的事情是:对于每个TCP会话,99%的概率会被忽略。对于其它1%的会话,它的请求会被写入一个文件,把回复写入另一个文件。文件名是IP地址加上端口号,再加上后缀req或者resp。在实际实现中,在文件写完成之前,文件名有.tmp扩展名,文件写完成后,.tmp扩展名被去掉。
在第三个Lua插件的例子,将使用一个供Lua使用的TCPGO定制的扩展API,它的名字是save_traffic(pcap_file_path),作用是把当前TCP会话的流量保存到文件pcap_file_path中。其次,第三个例子中还有意定义了一个自定义函数(非mail函数),这个函数将可以在TCPGO的调试控制台中被调用。
这个例子做的事情是:对于每个TCP会话,检查请求中是否匹配某个子字符串ad_type=TP&l=4002(实际上Lua的字符串匹配支持一个正则式的子集),如果匹配正确,则把匹配计算器加一。再检查匹配计数器是否小于10,而且响应报文的长度是否大于400,满足条件就把当前TCP会话的流量存在流量文件中。这个流量文件名由string.format()拼出来。此后,既然得到了流量文件,就可以再次播放这个流量文件。注意到这个插件中定义了一个函数desp()。该函数返回匹配的TCP会话次数。假如这个插件名为bar.lua,那么可以在TCPGO的调试控制台用命令 bar.desp() 得到命中条件的TCP会话次数。
TCPGO还有另一个插件系统,它基于动态链接态,但没有写完。暂时也没写完它的计划了。
TCPGO的调试控制台系统是日志系统的一个非常有利的补充。它提供了一个便捷地,在运行时向TCPGO主动查询状态,发布命令的机制。
TCPGO开放了TCP 1994端口作为调试控制台的接入端口。可以在任意一台可以TCP连接到运行TCPGO的机器上,用netcat或者telnet进行连接。
比如:
# netcat 192.168.1.200 1994
会得到 Welcome to horos(TCPGO) console v0.8.2 的欢迎信息,和控制台提示符 horos>,在提示符后面键入命令。
为什么是命令提示符是horos,不是TCPGO呢,因为TCPGO以前叫Horos,提示符还没改过来。
TCPGO的调试控制台是基于Lua技术的,所以每个命令实际上是一个合法的Lua语句,被传给了TCPGO的Lua解析器执行。0.8.2版TCPGO的控制台几个命令是:version(), stat(), reload_testsuite(), log_on(), flush_log()。先详细介绍一下最重要的stat()命令,其它命令很简单,只做简单的描述。
如下图,进入TCPGO控制台后键入stat()命令,返回了很多与TCP会话有关的信息。实际上,这些信息都有了详细的自解释。
上图给出的信息依次是:
75935 sessions are now in memory:目前有75935个TCP会话在内存中。参见SESSION. session_count_limit选项指定
75851 sessions are healthy,其中的75851个会话已经接收完整了TCP会话需要的所有IP包。
0 sessions have aborted 还没有会话因为超时时还没收完整TCP会话的所有IP包而被放弃,参见配置文件的SESSION. injecting_rt_traffic_timeout选项。
10219 sessions ended via active close,已经有10219个会话由主动关闭的形式关闭。这个例子中,测试服务器是Apache,TCPGO的配置文件中开启了主动关闭,所以TCPGO模拟的客户机会在发完有负载的TCP报文后,继续发送FIN报文。因此大多数情况下,TCP会话会终止于主动关闭。
0 sessions ended via passive close,还没有会话以被动关闭的形式关闭。
0 sessions ended prematurely because of no response from peer within 300000 milliseconds. 300秒内没有测试服务器没有回复,则关闭会话。
0 sessions ended prematurely because sended FIN didn't elicit FIN from server within 250000 milliseconds. 如果模拟的客户端主动关闭,在FIN包发出后250秒内,测试服务器没有发送FIN,则强制关闭。
0 sessions ended prematurely because no traffice has been sent to peer within 200000 milliseconds. 模拟的客户端在200秒内没有发送内容给测试服务器。
361 sessions were killed by RESET. 361个会话是因为测试服务器回复了RESET。
Average Session Time Duration: 398.116 millisconds. 平均每个TCP会话的生命期是398毫秒。
Retransmit Rate: 0.0583178 每次发送IP包,该包存在5.8%的概率会在重发计时器超时后被重发,在重发计时器超时之内没有收到测试服务器对应的ACK。
Success Rate: 0.965879 有96.5%的会话是通过正常的四次挥手完成的。
Up 0 min(s) and 6 second(s). TCPGO已经运行了6秒钟。
Average Connections Per Second in the past 15mins, 5mins, 1min, 15seconds, 5seconds, 1second:
-1 -1 -1 -1 1731 1650 (active_closed + passive_closed) / time elapsed in second) 统计数据:每秒钟完成的TCP会话数目。统计的数据是在过去的15分钟,5分钟,1分钟,15秒钟,5秒钟,1秒钟内的平均每秒完成TCP会话数。如果显示负1,表示TCPGO运行的时间还不够长,这个数据还没计算出来。比如,TCPGO只运行了11分钟,则对应15分钟的统计数据显示-1。
下面简单的介绍一下TCPGO其它的命令:
version() 返回版本号。
stat() 返回TCP会话的详细信息,下文会重点介绍这个命令。
reload_testsuite() TCPGO卸载所有Lua插件,重新遍历放置Lua插件的文件夹,再加载所有找到的.lua文件。
log_on(true_or_false) 这个命令接受一个参数,如果调用log_on(true),会确保日志打开,如果调用log_on(false),日志会被停止。
flush_log() 把标准C文件缓冲区中的数据刷新到磁盘。这个命令没太大用处。
TCPGO用真实生产机器上的抓取的流量包作为素材,模拟出外网用户与测试服务器交互;又通过使得测试服务器发往外网的IP包不可能到达外网,而是被TCPGO截获,从而TCPGO通过截得的信息可以维持一个假的TCP会话,使测试服务器误以为在与外网用户交互。
TCP协议是一个点对点的,有状态的,设计初期非常简单的传输层协议。TCP本身没有现代密钥体系的身份验证,并不保证通信的对方是可信的。它区分每一个会话的唯一标志被称为SOCKET PAIR,它由四个字段构成:源IP地址,源端口,目标IP地址,目标端口。而IP协议也不校验源地址真伪。所以,理论上只要可以伪造SOCKET PAIR的四个字段,便可以入侵TCP会话。当然可以在IP层用IPSec做IP地址的可靠性验证,那就不能轻易入侵TCP会话了。
那么如何欺骗TCP连接方新建一个假的TCP会话,让TCP连接误以为在和某个(IP,PORT)通讯。
假如我要假冒IP 20.20.20.20,端口号2000与服务器30.30.30.30,端口号80通讯。于是,伪造一个SYN包,源地址端口号填上20.20.20.20和2000,目标地址填上30.30.30.30和80,然后把包发出去。服务器30.30.30.30,端口80便收到了SYN包,服务器回SYN+ACK给20.20.20.20,端口号2000。SYN+ACK翻山越水经过各种路由器,终于到达了真实的20.20.20.20。如果20.20.20.20真实在线的话,绝大多数情况下,20.20.20.20认为它没有向30.30.30.30发送过SYN,于是果断的回了一个RESET。如果20.20.20.20不在线,最后一跳路由可能会回一个DESTINATION UNREACHABLE的ICMP包给30.30.30.30。结果都是30.30.30.30立即撤消了这个TCP会话的创建,反应在应用层,则是accept()系统调用返回-1,设置相应的errno。如下图所示:
一次失败的TCP欺骗示例
这个问题如何解决呢,不让SYN-ACK(其它包也一样)发向20.20.20.20就解决了。这就是前文叙述如何使用TCPGO时要设置发往外网的路由要改成一台没有路由功能的机器的原因。这样,SYN-ACK被发到了一台没路由功能的机器,这台机器不是路由器,所以会把SYN-ACK丢弃,于是SYN-ACK到不了20.20.20.20,自然就收不到撤消会话的REST包或DESTINATION UNREACHABLE的ICMP包。如下图所示:
成功建立TCP会话
当然,还有其它的方法解决这个问题,比如使用IPQueue或者NFQueue内核模块。这两个内核模块用来在应用层编写防火墙,可以通知内核如何裁决每个包。因为公司有些机器上没有IPQueue或NFQueue模块,而且这两个模块的编程接口很容易出错,各种宏和各种指针偏移,相对设置路由的实现方式而言几乎没有什么优势,因此TCPGO里面使用的是设置路由的解决方案。
另外,TCP协议通过给字节流编序号,接收方ACK收到的序号,发送方重发没有被ACK的负载也是每个程序员公知的协议特性。如果能截获发送方发送的TCP报文,解决ACK的问题自然不在话下了。如何截获服务器发送的报文呢,上文提到的IPQueue和NFQueue就可以。另外,广泛使用的libpcap库和AF_PACKET协议族,SOCK_RAW原生套接字类型都是现成的解决方案。TCPGO可以使用三种方式截获测试服务器的回复报文。参见配置文件的MAIN.sniff_method选项。
至此,解决了两个主要问题:1.不让拥有真实IP机器或互联网上的路由器影响假TCP会话的建立。2.发送伪造的ACK让发送方认为对方已经成功接收。编写TCP欺骗工具在理论上已经没有障碍了。
针对0.8.2版TCPGO。
tcpsession.cpp,tcpsession.h:
实现了TCP会话的状态机转换。
ip_pkt.h, ip_pkt.cpp,ip_pkt_hdr_only.h,ip_pkt_hdr_only.cpp:
解析IP包。
session_manager.h, session_manager.cpp
管理所有TCP会话。
postman.h,postman.cpp,raw_postman.h,raw_postman.cpp,pcap_postman.h,pcap_postman.cpp,tcp_postman.h,tcp_postman.cpp
抓取回包和发送自填充的IP包。有三种方式,用RAW SOCKET,用libpcap库,用TCP方式,参见配置文件的MAIN.sniff_method选项。
postoffice.h, postoffice.cpp
字面意思邮政局,作为TCP会话和发包收包postman的中介。
statistic_bureau.h,statistic_bureau.cpp
字面意思统计局,当然负责统计TCPGO运行的各种数据。
realtime_capture.h,realtime_capture.cpp
从1993端口中得到实时TCP流量,和流量拥塞控制。
reactor.h,reactor.cpp
Reactor服务器模型。较新版TCPGO引入Proactor服务器模型后,大量以前依赖Reactor的代码改用Proactor实现,截止0.8.2只有少数代码使用reactor。
proactor.h,proactor.cpp
Proactor服务器模型,基于BOOST的asio库,是TCPGO的重要基础设施。
politburo.h,politburo.cpp
字面意思政治局,这是权力的中心,和政令发出的地方。
cascade.h,cascade.cpp
把TCPGO收到的测试服务器的回复报文再写入/tmp/horos.fifo有名管道,用来帮助串联多个TCPGO。
listmap.h
实现了一个STL-like的容器,它是map和list 杂交。既可以像map那样有O(log n)(红黑树map),O(1)(Hash Map)的查找时间复杂度,又可以像list一样存下来容器中插入元素的时序关系。
mylua.cpp,mylua.h
TCPGO嵌入的Lua虚拟机,支持Lua插件。调试控制台也在这里实现。
testsuite.h,testsuite.cpp
支持编写测试插件。最开始计划编写Lua和动态链接库两套插件系统,截止0.8.2已实现Lua插件系统,还未实现动态链接库的插件。
thetimer.h, thetimer.cpp
时钟系统,模拟Linux内核的jiffies,或HZ。还有一个非常简陋的时钟事件处理系统,早期的TCPGO用的比较多。因为后来采用了BOOST的asio,这个简单的时钟事务系统基本被弃用。但thetimer的jiffies仍然在TCPGO的代码中广泛使用。
utils.h,utils.cpp
一些全局的工具函数。
configuration.h,configuration.cpp
管理配置文件和配置选项。
cute_logger.h,cute_logger.cpp
一个非常简单的日志系统,没考虑非线程安全。将来可能会被BOOST日志库取代。
main.cpp
做一些初始化的工作。
专有线程:
1.主线程:执行reactor模型。
2.发IP包线程:不停地从多个队列中得到需要发送的自构建IP包,并发送这些IP包。
3.收测试服务器回复报文的线程:配置文件MAIN.sniff_method选项指定接收测试服务器回复报文的方式。不停地接收回复报文,并把收到的IP报文分配到正确的队列。
4.运行Lua插件的线程。如果配置文件中的TESTSUITE.lua_scripts_home没有配置,该线程会一直阻塞。
5.把得到的测试服务器回复报文写到有名管道/tmp/horos.fifo的专有线程。实际上,如果没有进程读取有名管道/tmp/horos.fifo,该专有线程会一直阻塞。
Proactor的线程组:
TCPGO的Proactor模型的会根据CPU的核心数目自动计算最合适的线程数,用它运行Proactor模型。参见配置文件的MAIN.asio_thrd_num选项。Proactor不断地从多个队列中收取测试服务器的回包,并发送伪造的报文到发送多个队列,同时运转每个TCP会话的状态机。
对于TCPGO当前版本,在回放TCP长链接的流量时,会把一个TCP会话的所有TCP包一次性(不等待)发给测试服务器。所以,如果请求和回复没有上下文的联系,用TCPGO模拟用户请求没有问题。如果需要完美支持请求和回复有上下文联系的TCP长链接,TCPGO需要进一步开发一个如果回放长链接TCP流量的策略的机制,该机制决定TCP流量什么时候可以发送给测试服务器,什么时候需要等待测试服务器的回复。
TCPGO的统计信息还不够详细。因此,在排查问题的时候,会因为信息不够多而不能快速定位问题。一个改进方向是,增加详尽的统计信息,比如流量流入和流出的情况,更细粒度地统计TCP会话持续的时长,在需要时可以查询每个TCP会话的交互情况。
TCPGO的单机性能还有希望得到明显地提高。目前TCPGO代码不停地遍历所有得到机会发送IP包的TCP会话,用的是轮询的方式,非常的耗CPU。不同于网易tcpcopy,TCPGO运行在独立的机器上,CPU资源相对生产机器是廉价的,为了压缩开发时间所以写成了轮询。另外,线程模型也不是最优的。目前的线程模型是独立线程结合PROACTOR/BOOST ASIO线程组的方式。如果BOOST ASIO的线程组数量设置不合理,会抢占需要最多资源的独立线程。一个简单的改进点,则是把独立线程纳入BOOST ASIO的线程组做统一管理,保证独立线程有足够多的资源。
对于在某些Linux内核下运行的nginx(并不是所有),用TCPGO测试时,运行nginx的Linux就死机了!不知道是TCPGO触发了Linux的Bug,还是nginx的BUG。还是其它原因,还不清楚~~。
//////////////////////////
后记:
关键代码贴在这里 https://github.com/zausiu/tcpgo_another_tcpcopy_core
有一个小问题,当时对智能指针shared_ptr的理解不够,代码中很多函数没必要以传值的形式传shared_ptr,应该以传引用的方式传shared_ptr,这样能收获一点点性能提升。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。