描述
云查杀平台以nginx作为反向代理服务器,作为安全终端与云查询服务的桥梁。当安全终端需要查询黑文件时,HTTP请求及其响应都会经过nginx,为了获取并统计一天24小时查询的黑文件数量,就得先截获经过nginx的HTTP响应,再做数据分析。截获HTTP数据流有多种方法,为了简单高效,这里使用了挂接HTTP过滤模块的方法,另外为了不影响nginx本身的IO处理,将HTTP响应实体发送到另一个进程即统计服务,由统计服务来接收并分析HTTP响应,架构如下图
统计服务由1个接收线程和1个存储线程构成,其中接收线程负责接收从nginx过滤模块发来的HTTP响应实体,解析它并提取黑文件MD5,加入共享环形队列;而存储线程从共享环形队列移出黑文件MD5,插入到临时内存映射文件,于每天定时同步到磁盘文件。
特点
这种架构减少了nginx IO延迟,保证了nginx的稳定高效运行,从而不影响用户的业务运行;本地连接为非阻塞的,支持了统计服务的独立运行与升级。
实现
nginx过滤模块
该流程运行在nginx工作进程。
由于nginx采用了异步IO机制,因此仅当截获到HTTP响应实体也就是有数据经过时,才有后面的操作;若没有数据,则什么也不用做。这里每次发送前先判断是否连接了统计服务,是为了支持统计服务的独立运行与升级,换句话说,不管统计服务是否运行或崩溃,都不影响nginx的运行。
统计服务
接收线程
这里的接收线程也就是主线程。
存储线程
存储线程为另一个工作线程。
同步文件定时器的时间间隔要比新建文件定时器的短,由于定时器到期的事件处理是一种异步执行流,所以将它们当做并行,与“从q头移出黑文件MD5”操作画在了同一水平方向。
posted @
2016-08-25 11:10 春秋十二月 阅读(1064) |
评论 (0) |
编辑 收藏
本方法适用于linux 2.6.x内核。
1. 先获取dentry所属文件系统对应的挂载点,基本原理是遍历文件系统vfsmount树,找到与dentry有相同超级块的vfsmount,实现如下
next_mnt函数实现了
先根遍历法,遍历以root为根的文件系统挂载点,p为遍历过程中的当前结点,返回p的下一个挂载点;vfsmnt_lock可通过内核函数kallsyms_on_each_symbol或kallsyms_lookup_name查找获得。
2. 再调用内核函数d_path,接口封装如下
posted @
2016-08-24 19:22 春秋十二月 阅读(5680) |
评论 (0) |
编辑 收藏
描述
原始套接字具有广泛的用途,特别是用于自定义协议(标准协议TCP、UDP和ICMP等外)的数据收发。在Linux下拦截套接字IO的一般方法是拦截对应的套接字系统调用,对于发送为sendmsg和sendto,对于接收为recvmsg和recvfrom。这种方法虽然也能拦截原始套接字IO,但要先判断套接字的类型,如果为SOCK_RAW(原始套接字类型),那么进行拦截处理,这样一来由于每次IO都要判断套接字类型,性能就比较低了。因此为了直接针对原始套接字来拦截,提高性能,发明了本方法。
本方法可用于防火墙或主机防护系统中,丢弃接收和发送的攻击或病毒数据包。
特点
运行在内核态,直接拦截所有进程的原始套接字IO,支持IPv4和IPv6。
实现
原理
在Linux内核网络子系统中,struct proto_ops结构提供了协议无关的套接字层到协议相关的传输层的转接,而IPv4协议族中内置的inet_sockraw_ops为它的一个实例,对应着原始套接字。因此先找到inet_sockraw_ops,再替换它的成员函数指针recvmsg和sendmsg,就可以实现拦截了。下面以IPv4为例(IPv6同理),说明几个流程。
搜索inet_sockraw_ops
该流程在挂钩IO前进行。由于inet_sockraw_ops为Linux内核未导出的内部符号,因此需要通过特别的方法找到它,该特别的方法基于这样的一个事实:
◆ 所有原始套接字接口均存放在以SOCK_RAW为索引的双向循环链表中,而inet_sockraw_ops就在该链表的末尾。
◆ 内核提供了注册套接字接口的API inet_register_protosw,对于原始套接字类型,该API将输入的套接字接口插入到链表头后面。
算法如下
注册p前或注销p后,链表如下
注册p后,链表如下
挂钩IO
该流程在内核模块启动时进行。
卸钩IO
该流程在内核模块退出时进行。
运行部署
该方法实现在Linux内核模块中,为了防止其它内核模块可能也注册了原始套接字接口,因此需要在操作系统启动时优先加载。
posted @
2016-07-14 10:27 春秋十二月 阅读(2412) |
评论 (3) |
编辑 收藏
描述
TCP连接跟踪是网络流控和防火墙中的一项重要的基础技术,当运用于主机时,连接必与进程相关联,要么是主动发出的,要么是被动接受的,当后代进程被动态创建时,由于文件描述符的继承,一个连接就会被这个进程树中的所有进程共享;当一个进程发出或接受多个连接时,就拥有了多个连接。本方法可用于网络安全产品中,监控TCP连接及所属进程,能准确并动态地知道一个连接被哪些进程共享,一个进程拥有哪些连接。
特点
操作系统自带的netstat工具只是关联到了一个根进程,无法看到拥有该连接的所有进程,查看进程拥有的全部连接也不方便。该方法的特点是实时跟踪、查看连接与进程相关信息方便、支持连接的管控。
实现
本方法通过内核安全的十字链表实现了连接与进程的相关性,连接信息结构体含有一个所属进程链表头,进程信息结构体含有一个拥有连接链表头,通过十字链表结点链接,x方向链接到进程的连接链表,y方向链接到连接的进程链表,如下图所示
进程1为根进程,进程2,...,进程n为进程1的后代进程;连接1,连接2,...,连接n为进程1产生的连接。node(x,y)为十字链表结点,用于关联连接与进程,x对应进程编号,y对应连接编号,每个node包含了所属的连接和进程指针,每行和每列都是一个双向循环链表(循环未画出),每个链表用一个自旋锁同步操作。
动态跟踪的过程包括4个方面:进程创建、进程退出、连接产生、连接销毁。在Linux下,可通过拦截内核函数do_fork挂钩进程创建,拦截do_exit挂钩进程退出;可通过拦截inet_stream_ops的成员函数connect和accept挂钩连接产生,拦截成员函数release挂钩连接销毁。下面为4个方面对应的流程图,由于所有外层加锁前已禁止本地中断和内核抢占,因此内层加锁前就不必再禁止本地中断和内核抢占了。
进程创建
将copy_node插入到c的进程链表末尾,即为y方向增加(下同);插入到p的连接链表末尾,为x方向增加(下同)。
进程退出
从c的进程链表中移除node,即为y方向移除(下同);再从p的连接链表中移除node,即为x方向移除(下同)。
连接产生
当进程发出连接或接受连接时,调用此流程。
连接销毁
当某个进程销毁连接时,调用此流程。
posted @
2016-07-13 11:24 春秋十二月 阅读(1502) |
评论 (0) |
编辑 收藏
描述
在P2P应用系统中,当需要与处于不同私网的对方可靠通信时,先尝试TCP打洞穿透,若穿透失败,再通过处于公网上的代理服务器(下文简称proxy)转发与对方通信,如下图所示
终端A先连接并发消息msg1到proxy(连接为c1);proxy再发消息msg2给P2P服务器,P2P服务器收到消息后通过已有的连接发消息msg3给终端B,B收到msg3后,连接并发消息msg4到proxy(连接为c2)。这4个消息格式由应用层协议决定,但必须遵守的规范如下:
◆ msg1至少包含B的设备ID。
◆ msg2至少包含B的设备ID和c1的连接ID。
◆ msg3至少包含c1的连接ID和连接代理指示。
◆ msg4至少包含B的设备ID和c1的连接ID。
proxy为多线程架构,当接受到若干连接时,如果数据相互转发的两个连接(比如上图中的c1和c2)不在同一线程,由于一个连接写数据到另一连接的发送队列,而另一连接从发送队列读数据以发送,那么就要对另一连接的发送队列(或缓冲区)加锁,这样一来由于频繁的网络IO而频繁地加解锁,降低了转发效率,因此为了解决这一问题,就需要调度TCP连接到同一线程。
特点
本方法能将数据转发的两边连接放在同一线程,从而避免了数据转发时的加锁,由于是一对一的连接匹配,因此也做到了每个线程中连接数的均衡。
实现
工作原理
proxy的主线程负责绑定知名端口并监听连接,工作线程负责数据转发。当接受到一个连接时,按轮转法调度它到某个工作线程,在那个线程内接收分析应用层协议数据,以识别连接类型,即判断连接是来自数据请求方还是数据响应方,为统一描述,这里把前者特称为客户连接,后者为服务连接,连接所在的线程为宿主线程。如果是客户连接,那么先通知P2P服务器请求对应的客户端来连接代理,并等待匹配;如果是服务连接,那么就去匹配客户连接,在匹配过程中,如果服务连接和客户连接不在同一线程内,那么就会调度到客户连接的宿主线程,匹配成功后,就开始转发数据。
为了加快查找,分2个hash表存放连接,客户连接放在客户连接hash表中,以连接ID为键值;服务连接放在服务连接hash表中,以设备ID为键值。
TCP连接调度
包括新连接的轮转、识别连接类型、匹配客户连接1、匹配客户连接2和关闭连接共5个流程,如下一一所述。
新连接的轮转
该流程工作在主线程,如下图所示
索引i初始为0,对于新来的连接,由于还不明确连接类型,所以先放入客户连接表中。
识别连接类型
该流程工作在工作线程,当接受到一个连接时开始执行,如下图所示
当连接类型为服务连接时,从客户连接表转移到服务连接表。
匹配客户连接1
该流程工作在工作线程,当接受到一个服务连接时开始执行,如下图所示
匹配客户连接2
该流程工作在工作线程,当服务连接移到客户连接的宿主线程时开始执行,如下图所示
这里的流程和匹配客户连接流程1有些类似,看起来好像做了重复的判断操作,但这是必要的,因为在服务连接转移到另一线程这个瞬间内,客户连接有可能断开了,也有可能断开后又来了一个相同连接ID的其它客户连接,所以要重新去客户连接表查找一次,然后进行4个分支判断。
关闭连接
该流程工作在工作线程内,当连接断开或空闲时执行。当一边读数据出错时,不能马上关闭另一边连接,得在另一边缓冲区数据发送完后才能关闭;当一边连接写数据出错时,可以马上关闭另一边连接,如下图所示
posted @
2016-07-12 16:59 春秋十二月 阅读(1766) |
评论 (0) |
编辑 收藏
摘要: 为了使nginx支持windows服务,本文阐述以下主要的改进实现。ngx_main函数 为了在SCM服务中复用main函数的逻辑,将其重命名为ngx_main,并添加第3个参数is_scm以兼容控制台运行方式,声明在core/nginx.h中。
Code highligh...
阅读全文
posted @
2016-07-12 15:31 春秋十二月 阅读(4464) |
评论 (0) |
编辑 收藏
结构定义
1 struct state_machine {
2 int state;
3
4 };
5 6 enum {
7 s1,
8 s2,
9
10 sn
11 };
假设s1为初始状态,状态变迁为s1->s2->...->sn。
常规实现 状态机处理函数state_machine_handle通常在一个循环内或被事件驱动框架调用,输入data会随时序变化,从而引起状态的变迁,伪代码框架如下。
1
void handle_s1(struct state_machine *sm, void *data)
2![](http://www.cppblog.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/ContractedBlock.gif)
{
3
//do something about state 1
4
if(is_satisfy_s2(data))
5
sm->state = s2;
6
}
7![](http://www.cppblog.com/Images/OutliningIndicators/None.gif)
8
void handle_s2(struct state_machine *sm, void *data)
9![](http://www.cppblog.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/ContractedBlock.gif)
{
10
//do something about state 2
11
if(is_satisfy_s3(data))
12
sm->state = s3;
13
}
14![](http://www.cppblog.com/Images/OutliningIndicators/None.gif)
15
void handle_sn_1(struct state_machine *sm, void *data)
16![](http://www.cppblog.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/ContractedBlock.gif)
{
17
//do something about state n-1
18
if(is_satisfy_sn(data))
19
sm->state = sn;
20
}
21![](http://www.cppblog.com/Images/OutliningIndicators/None.gif)
22
void state_machine_handle(struct state_machine *sm, void *data)
23![](http://www.cppblog.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/ContractedBlock.gif)
{
24![](http://www.cppblog.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
switch(sm->state)
{
25
case s1:
26
handle_s1(sm,data);
27
break;
28
29
case s2:
30
handle_s2(sm,data);
31
break;
32
![](http://www.cppblog.com/Images/dot.gif)
33
case sn:
34
handle_sn(sm,data);
35
break;
36
}
37
} sm->state初始化为s1。
改进实现
为了免去丑陋的switch case分支结构,在state_machine内用成员函数指针handler替代了state,改进后的框架如下。
1
struct state_machine;
2
typedef void (*state_handler)(struct state_machine*,void*);
3![](/Images/OutliningIndicators/None.gif)
4![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
struct state_machine
{
5
state_handler handler;
6
![](http://www.cppblog.com/Images/dot.gif)
7
};
8![](/Images/OutliningIndicators/None.gif)
9
void handle_s1(struct state_machine *sm, void *data)
10![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](/Images/OutliningIndicators/ContractedBlock.gif)
{
11
//do something about state 1
12
if(is_satisfy_s2(data))
13
sm->handler = handle_s2;
14
}
15![](/Images/OutliningIndicators/None.gif)
16
void handle_s2(struct state_machine *sm, void *data)
17![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](/Images/OutliningIndicators/ContractedBlock.gif)
{
18
//do something about state 2
19
if(is_satisfy_s3(data))
20
sm->handler = handle_s3;
21
}
22![](/Images/OutliningIndicators/None.gif)
23
void handle_sn_1(struct state_machine *sm, void *data)
24![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](/Images/OutliningIndicators/ContractedBlock.gif)
{
25
//do something about state n-1
26
if(is_satisfy_sn(data))
27
sm->handler = handle_sn;
28
}
29![](/Images/OutliningIndicators/None.gif)
30
void state_machine_handle(struct state_machine *sm, void *data)
31![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](/Images/OutliningIndicators/ContractedBlock.gif)
{
32
sm->handler(sm, data);
33
}
sm->handler初始化为handle_s1,该方法在性能上应略优于常规方法,而且逻辑更清晰自然,非常适合于网络流的处理,在nginx中分析http和email协议时,得到了广泛应用。
posted @
2016-05-05 09:46 春秋十二月 阅读(3932) |
评论 (1) |
编辑 收藏
脚本概述 由于使用objdump反汇编linux内核的输出太多(2.6.32-220.el6.x86_64统计结果为1457706行),而很多时候只是想查看特定部分的机器码与汇编指令,例如函数的入口、堆栈、调用了哪个函数等,为了高效和通用,因此编写了一个简单的awk脚本,其命令行参数说明如下:
● SLINE表示匹配的起始行号(不小于1),SPAT表示匹配的起始行模式,这两个只能有一个生效,当都有效时,以SLINE为准。
● NUM表示从起始行开始的连续输出行数(不小于1,含起始行),EPAT表示匹配的结束行模式,这两个只能有一个生效,当都有效时,以NUM为准。
脚本实现 检查传值 由于向脚本传入的值在BEGIN块内没生效,在动作块{}和END块内有效,但若在{}内进行检查则太低效,因为处理每条记录都要判断,所以为了避免在{}内进行多余的判断,就在BEGIN块内解析命令行参数来间接获得传值,当传值无效时,给出提示并退出。
1
for(k=1;k<ARGC;++k){
2
str=ARGV[k]
3
if(1==match(str,"SLINE=")){
4
SLINE = substr(str,7)
5
}else if(1==match(str,"SPAT=")){
6
SPAT = substr(str,6)
7
}else if(1==match(str,"NUM=")){
8
NUM = substr(str,5)
9
}else if(1==match(str,"EPAT=")){
10
EPAT = substr(str,6)
11
}
12
}
13![](/Images/OutliningIndicators/None.gif)
14 if(SLINE<=0 && SPAT==""){
15 print "Usage: rangeshow must specifies valid SLINE which must be greater than 0, or SPAT which can't be empty"
16 exit 1
17 }
18![](/Images/OutliningIndicators/None.gif)
19 if(NUM<=0 && EPAT==""){
20 print "Usage: rangeshow must specifies valid NUM which must be greater than 0, or EPAT which can't be empty"
21
exit 1
22
} 结束处理 当处理了NUM条记录或匹配了结束行模式时,应退出动作块{}。
1
if(0==start_nr){
2
3
}else{
4
if(NUM>0) {
5
if(NR<start_nr+NUM) {
6
++matched_nr
7
print $0
8
}else
9
exit 0
10![](http://www.cppblog.com/Images/OutliningIndicators/None.gif)
11
}else{
12
++matched_nr
13
print $0
14
if(0!=match($0,EPAT))
15
exit 0
16
}
17
} 完整脚本下载:
rangeshow。
脚本示例
查看linux内核第10000行开始的10条指令,如下图
查看linux内核函数do_fork入口开始的10条指令,如下图
![](http://www.cppblog.com/images/cppblog_com/qinqing1984/awk_rangeshow_do_fork_10.png)
查看linux内核第10000行开始到callq的一段指令,如下图
![](http://www.cppblog.com/images/cppblog_com/qinqing1984/awk_rangeshow_10000_callq.png)
查看linux内核函数do_exit入口到调用profile_task_exit的一段指令,如下图
posted @
2015-10-27 15:36 春秋十二月 阅读(1717) |
评论 (1) |
编辑 收藏
本文根据RFC793协议规范和BSD 4.4的实现,总结了
TCP分组丢失时的状态变迁,如下图所示:实线箭头表示客户端的状态变迁,线段虚线箭头表示服务端的状态变迁,圆点虚线箭头表示客户端或服务端的状态变迁;黑色文字表示正常时的行为,红色文字表示分组丢失时的行为。
这里假设重传时分组依然会丢失,当在不同状态(CLOSED除外)分组丢失后,最终会关闭套接字而回到CLOSED状态。下面逐个分析各状态时的情景。
SYN_SENT
连接阶段第1次握手,客户端发送的SYN分组丢失,因此超时收不到服务端的SYN+ACK而重传SYN,尝试几次后放弃,关闭套接字。
SYN_RCVD
1)连接阶段第2次握手,服务端响应的SYN+ACK分组丢失,因此超时收不到客户端的ACK而重传SYN+ACK,尝试几次后放弃,发送RST并关闭套接字。
2)连接阶段第3次握手,客户端发送的ACK分组丢失,因此服务端超时收不到ACK而重传SYN+ACK,尝试几次后放弃,发送RST并关闭套接字。
3)同时打开第2次握手,本端响应的SYN+ACK分组丢失,因此对端超时收不到SYN+ACK而重传SYN、尝试几次后放弃、发送RST并关闭套接字,而此时本端收到RST。
ESTABLISHED
1)连接阶段第3次握手,客户端发送ACK分组后,虽然丢失但会进入该状态(因为ACK不需要确认),但此时服务端还处于SYN_RCVD状态,因为超时收不到客户端的ACK而重传SYN+ACK、尝试几次后放弃、发送RST并关闭套接字,而此时客户端收到RST。
2)数据传输阶段,本端发送的Data分组丢失,因此超时收不到对数据的确认而重传、尝试几次后放弃、发送RST并关闭套接字,而此时对端收到RST。
FIN_WAIT_1
1)关闭阶段第1次握手,客户端发送的FIN分组丢失,因此超时收不到服务端的ACK而重传FIN,尝试几次后放弃,发送RST并关闭套接字。
2)关闭阶段第2次握手,服务端响应的ACK分组丢失,因此客户端超时收不到ACK而重传FIN,尝试几次后放弃,发送RST并关闭套接字。
FIN_WAIT_2
关闭阶段第3次握手,服务端发送的FIN分组丢失,因此超时收不到客户端的ACK而重传FIN、尝试几次后放弃、发送RST并关闭套接字,而此时客户端收到RST。
CLOSING
同时关闭第2次握手,本端发送的ACK分组丢失,导致对端超时收不到ACK而重传FIN、尝试几次后放弃、发送RST并关闭套接字,而此时本端收到RST。
TIME_WAIT
关闭阶段第4次握手,客户端响应的ACK分组丢失,导致服务端超时收不到ACK而重传FIN、尝试几次后放弃、发送RST并关闭套接字,而此时客户端收到RST。
CLOSE_WAIT
关闭阶段第2次握手,服务端响应的ACK分组丢失,导致客户端超时收不到ACK而重传FIN、尝试几次后放弃、发送RST并关闭套接字,而此时服务端收到RST。
LAST_ACK
关闭阶段第3次握手,服务端发送的FIN分组丢失,导致超时收不到客户端的ACK而重传FIN、尝试几次后放弃、发送RST并关闭套接字。
posted @
2015-10-05 00:44 春秋十二月 阅读(3205) |
评论 (1) |
编辑 收藏
摘要: 由于linux内核中的struct list_head已经定义了指向前驱的prev指针和指向后继的next指针,并且提供了相关的链表操作方法,因此为方便复用,本文在它的基础上封装实现了一种使用开链法解决冲突的通用内核Hash表glib_htable,提供了初始化、增加、查找、删除、清空和销毁6种操作,除初始化和销毁外,其它操作都做了同步,适用于中断和进程上下文。...
阅读全文
posted @
2015-09-15 17:18 春秋十二月 阅读(2173) |
评论 (0) |
编辑 收藏