一. Fprobe简介
Fprobe 是一款在 FreeBSD下运行的软件,它可以将其接口收到的数据转化为Netflow 数据,并发送至Netflow 分析端。
其中的Netflow 是 Cisco 公司开发出的一套协议,用于与门解决原始流量方式所产生的问题。当在网络设备戒其接口上开启 Netflow 功能后,网络设备会对需要迚行分析的流量迚行采样分析,并把采样分析的结果发送到分析端进行流量分析,当然这些采样分析的结果要比原始数据小的多的多。其中网络设备采样分析的结果数据会包括源地址、目的地址、源端口、目的端口、数据流的大小、数据流经过的接口、数据流的到达时间、数据流的送出时间等参数。
二. Fprobe工作原理
Fprobe的工作原理主要调用pcap类库从数据链路层进行抓包,并把抓取数据包封装成Netflow格式,然后发到相应的分析端。所以我们首先来分析一下pcap的使用,然后看fprobe是怎样使用pcap来工作的。其简单流程见下图:
首先数据包的获取主要是调用pcap类库来工作的,下面是一个基于pcap的嗅探程序的总体布局。流程如下:
1) 我们从决定用哪一个接口进行嗅探开始。在Linux中,这可能是eth0, 而在BSD系统中则可能是xl1,等等。我们也可以用一个字符串来定义这个设备,或者采用pcap提供的接口名来工作。相关函数dev = pcap_lookupdev()。
2) 初始化pcap。在这里我们要告诉pcap对什么设备进行嗅探。假如愿意的话,我们还可以嗅探多个设备。怎样区分它们呢?使用文件句柄。就像打开一个文件进行读写一样,必须命名我们的嗅探“会话”,以此使它们各自区别开来。相关函数handle = pcap_open_live()。
3) 如果我们只想嗅探特定的传输(如TCP/IP包,发往端口23的包,等等),我们必须创建一个规则集合,编译并且使用它。这个过程分为三个相互紧密关联的阶段。规则集合被置于一个字符串内,并且被转换成能被 pcap 读的格式(因此编译它)。编译实际上就是在我们的程序里调用一个不被外部程序使用的函数。接下来我们要告诉pcap使用它来过滤出我们想要的那一个会话。相关函数pcap_compile()。
4) 最后, 我们告诉pcap进入它的主体执行循环。在这个阶段内, pcap一直工作到它接收了所有我们想要的包为止。每当它收到一个包就调用另一个已经定义好的函数, 这个函数可以做我们想要的任何工作,它可以剖析所部获的包并给用户打印出结果, 它可以将结果保存为一个文件, 或者什么也不作。相关函数pcap_loop()。
5) 在嗅探到所需的数据后, 我们要关闭会话并结束。相关函数pcap_close(handle)。
这是实际上一个很简单的过程。一共五个步骤,其中一个(第3个)是可选的。
其次在调用pcap抓到包后,对所抓的包进行提取、封装成Netflow格式,发送出去,为了考虑效率,发包并非实时发送的,而是在达到Netflow的最大值时才Send给指定的地址。
需要说明的是,上面的过程是在Fprobe里是调用四个线程pcap_thread, unpending_thread, scan_thread, emit_thread来完成以上 “抓取—分析、封装—发射” 过程的。
三. Fprobe线程工作流程图。
四. Fprobe四个线程详细解析。
1. pcap_thread() 抓取数据包的线程操作。
Pcap_thread这个线程主要功能是调用系统库函数从数据链路层进行抓包,然后在抓取的包提取相关的信息并存入FLOW这个链表中(如下图所示)。
1) 首先这个线程调用系统库函数pcap_loop,,接着此库函数调用pcap_callback;这个回调函数(回调函数的意思就是系统会自己调用,而不用程序员自己写调用的时机)。
2) Pcap_callback会自动在链路层抓取到包,抓取到在包放在packet里。
3) 对于以上抓取到的数据包packet除去包头包尾,中间的就是IP数据包的相关信息,我们可以根据IP的格式就可以提取相关的信息,并把相关的信息抽出拷贝在Flow这个类型的链表里,注意放入链表时要检查链表是否为空。
条件合适的话copy到链表中:
4) 拷贝完成后,发射unpending信号,激活unpending_thread的相关操作。
2. unpending_thread()等待线程的操作。
Unpending_thread线程的主要功能是不停的把FLOW链表里的数据通过hash计算放入到FLOWS[]这个指针数组里。在放入的过程中,如果FLOWS[]之前没有此数据,则直接COPY进来;如果之前有数据,则更新。(如下图所示)
具体过程如下:
1) 首先unpend_thread是个死循环的线程,保证一直监控处理消息,这一点有别于callback函数。
2) 接着处理Flow这个链表,如果链表里的数据没有在抓取线程里填冲数据,则就不会有FLOW_PENDING这个标志,就会等待。
3) 对已经填冲过数据的链表元素进行hash散列操作,散列后的数据放在flows[1<<16]指针数组所关联的链表里。
要注意的是put_into()函数的操作,首先它会在flows[]这个数组里面找查是否有flow数据已经放入flows[h]所关联的链表里,
如果没有找到,就直接拷贝到散列表中,
如果找到了,就更新散列表里的相关数据,包括包的修改时间,大小,标志位等等。
这里要注意,在更新散列表数据时如果发现flow的数据是不完整的(FLOW_FRAG),
则要判断本次更新是不是能够组装完整,
如果能够组装完整,则把相应的标志位设置为完整,Flow里id设置为0.
4) 就这样一直循环的做拷贝操作,flows[]数组里也会不断被添加或更新数据。
3. scan_thread()扫描线程的操作。
扫描线程的操作很简单,就是把FLOWS[]里所关联的数据根据不同的属性送到不同的链表flow_emit,scan_flag_drag中,以便进行下一步的操作。(如下图所示)
具体过程如下:
1) 首先扫描线程也是进行死循环的操作。也就是不出来了,线程一直运行。
2) 接着顺序从头开始处理flows[]这个数组里的数据,以下分为三种情况。
3) 第一种情况:如果是不完整的包,并且已经超过生存期了,
则把它放在特殊的链表scan_frag_dreg中,等待处理。
4) 第二种情况:如果是完整的包,并且满足活动期的要求,
则把它放在flow_emit链表中,等待处理。
5) 第三种情况:如是是完整的包,但是没有超过活动期,什么特殊的事情也不做,只是指向下一个要处理元素。
6) 全部处理完后,放出发射信号,通知发射线程做事。
7) 最后做一些清理工作,把不完整的包清理掉。
4. emit_thread发射线程操作。
最后一个操作是发射线程的操作,因为fprobe的功能是能够提供NetFlow格式的包,NetFlow格式的包是分析网络传输数据常用的格式,emit_thread线程的主要功能就是发射这种格式的包。(如下图所示)
具体过程如下:
1) 初始化操作,设置p指向发射数据包里要存入数据的位置。
其中,emit_packet[1500]是个char 类型的数组,它也是要发送包的空间。
2) 同样进入死循环,不断的处理各种消息。
3) 要是flow_emit链表为空,说明在flows[]里还没有找到可用的数据(原因有几种:可能是抓包线程没有抓到包,也可能抓到的包是不完整的,还有可能是扫描线程还没有找到合适的数据,等等),则等待,直到从扫描线程里收到信号。
4) 如果不为空,则按netflow的格式填入emit_packet数组里p所指向的位置。
5) 填完后,清空flow,也即是设flows_emit链表中的元素为可用。
6) 接着看是否填满,
没填满的话,循环上去接着填,填满的话,加入头部,
形成一个完整的包。
7) 最后把emit_packet这个包发到指定的地方。