前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TCP流量复制工具,另一个tcpcopy

TCP流量复制工具,另一个tcpcopy

原创
作者头像
kamuszhou
修改2018-07-31 10:30:48
8.8K0
修改2018-07-31 10:30:48
举报
文章被收录于专栏:代码永生,思想不朽

很多年以前,网易推了一个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

1 介绍

1.1 TCPGO是什么,能做什么

1.2 使用简单,易扩展,对线上机几乎无影响

1.3 开发背景和意义

1.4 基于真实流量的测试工具的缺陷

2 详细使用说明

2.1 编译TCPGO

2.2 TCPGO的命令行选项

2.3 TCPGO的配置选项

2.4 部署TCPGO

2.5 TCPGO的Lua插件系统

2.6 TCPGO的调试控制台

3 TCPGO的原理

4 TCPGO的源代码

4.1 源文件

4.2 服务器模型

5 已知严重问题

致谢

1 介绍

1.1TCPGO是什么,能做什么

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状态转换。

1.2使用简单,易扩展,对线上机几乎无影响

使用TCPGO,不需要在线上生产服务器运行消耗大量资源的程序,只需在生产服务器运行两个准标准化UNIX工具:tcpdump负责抓包,和netcat(有些发行版名nc或ncat)负责建立和维持一个TCP链接,向TCPGO供应流量。如果TCPGO播放离线流量包*.pcap,而非实时的生产机流量,则可略去生产机上的操作。

在运行TCPGO的机器上,按照配置模板更改几个简单的配置选项即可使用。如指定测试服务器的IP,监听端口;模拟用户的并发数;以及高级一点的调整TCP会话的一些选项。

在测试服务器上需要更改路由表,使得发往外网的IP包不能去到外网。这些发往外网的IP包需要被最终发到运行TCPGO的机器,参见下文详细解释。

对于单台TCPGO模拟用户数还不能满足测试服务器压力要求的情况下,TCPGO还设计了串联的方式,让多个TCPGO实例在多台机器上工作,并同时为同一台测试服务器模拟用户。

1.3开发背景和意义

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的日志和调试控制台工具简单易懂。

1.4基于真实流量的测试工具的缺陷

对于不依赖会话上下文的请求,TCPGO把客户请求回放给测试用服务器,确实能给测试服务器提供了一个接近在线实际情况的运行环境。特殊地,对于TCP一问一答就立即关闭的短连接,可以被认为是不依赖上下文的特例。这种情况下,TCPGO完美支持。如果是长连接,TCPGO支持得不够好,它在TCP会话正常结束四挥手后才发送流量。所以,如果是一个持续很久的长链接,TCPGO不适用。需要改源代码,添加实时发送TCP流量的功能。

如果请求依赖上下文,生产服务器和测试服务器的回复有逻辑上的区别。那么,回放线上机上录制的客户请求给测试服务器,会让测试服务器发觉这并不是来自真实的用户的请求。

2详细使用说明

2.1编译TCPGO

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更新系统的动态链接库缓存。

2.2TCPGO的命令行选项

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

该选项打印版本信息。

2.3TCPGO的配置选项

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缓冲中的数据刷新到磁盘。

2.4部署TCPGO

设置路由:

部署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。

2.5TCPGO的Lua插件系统

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还有另一个插件系统,它基于动态链接态,但没有写完。暂时也没写完它的计划了。

2.6TCPGO的调试控制台

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文件缓冲区中的数据刷新到磁盘。这个命令没太大用处。

3TCPGO的原理

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欺骗工具在理论上已经没有障碍了。

4TCPGO的源代码

针对0.8.2版TCPGO。

4.1源文件

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

做一些初始化的工作。

4.2服务器模型

专有线程:

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会话的状态机。

5待改进和增强

对于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的线程组做统一管理,保证独立线程有足够多的资源。

6已知严重问题

对于在某些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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 介绍
    • 1.1TCPGO是什么,能做什么
      • 1.2使用简单,易扩展,对线上机几乎无影响
        • 1.3开发背景和意义
          • 1.4基于真实流量的测试工具的缺陷
          • 2详细使用说明
            • 2.1编译TCPGO
              • 2.2TCPGO的命令行选项
                • 2.3TCPGO的配置选项
                  • 2.4部署TCPGO
                    • 2.5TCPGO的Lua插件系统
                      • 2.6TCPGO的调试控制台
                      • 3TCPGO的原理
                      • 4TCPGO的源代码
                        • 4.1源文件
                          • 4.2服务器模型
                          • 5待改进和增强
                          • 6已知严重问题
                          相关产品与服务
                          云服务器
                          云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档