metron-sensors pycapa详解

metron-sensors pycapa详解

pycapa 采集器

pycapa网络数据采集器是metron-sensors模块下的一个低效能网络数据采集。其虽然采集的效率比不上fastcapa,但是其结构简单,实用性比较强。特别方便测试使用。在笔者的另外一篇关于Metron的文章中已经对pycapa进行了一些简单的介绍,这里就不再累述,而是重点说明在使用它的过程中遇到的一些问题以及介绍其代码实现的逻辑。

pycapa 发送到Kafka的数据格式

在前面的文章中,笔者采集了网络上的数据,并利用pycapa自产自消,将消费的数据转化成了pcap的格式。那么是否可以认为pcap发送到kafka中的数据就是pcap格式的了,答案是否定的。
我们采用和上文一样的方法,只是将消费的代码替换为Kafka自带的程序,此时发现image,虽然是ASCII的格式,但是从内容上仍然可以明显看出这不是pcap格式的。但是我们又确实利用其自带的consumer程序,生成了一个pcap 文件,这似乎是矛盾的,那么producer发送到kafa中的格式是怎样了?
先看官方对于发送和消费的说明


Pycapa has two primary runtime modes.

Producer Mode: Pycapa can capture packets from a network interface and forward those packets to a Kafka topic. Pycapa embeds the raw network packet data in the Kafka message body. The message key contains the timestamp indicating when the packet was captured in microseconds from the epoch, in network byte order.

根据其上的说法,kafka的message中用原始的packet作为数据负载,一个时间戳作为key值。

Consumer Mode: Pycapa can also perform the reverse operation. It can consume packets from Kafka and reconstruct each network packet. This can then be used to create a libpcap-compliant file or even to feed directly into a tool like Wireshark to monitor activity。

这里特别说明了,在消费模式下,它消费了Kafka中的数据以后,可以生成一个兼容libpcap的文件。这样看来,可以猜测,发送到kafka中的数据,并不是标准的pcap的格式,之所以用pycap自带的脚本消费可以生成标准的pcap文件,是因为在消费的脚本里面做了相关的动作所致,而kafka原始的消费者由于没有做对应的动作,所以生成的文件也不能是pcap文件。


虽然上面的解释说明了,设计就是这样,并不是BUG或者使用上的问题,但是仍然没有说明其具体的格式,没办法,只能看代码了。
下面是producer.py的核心发送代码:

def producer(args, sniff_timeout_ms=500, sniff_promisc=True):
    """ Captures packets from a network interface and sends them to a Kafka topic. """

    # setup the signal handler
    signal.signal(signal.SIGINT, signal_handler)

    global producer_args
    producer_args = args

    # connect to kafka
    logging.info("Connecting to Kafka; %s", args.kafka_configs)
    kafka_producer = Producer(args.kafka_configs)

    # initialize packet capture
    logging.info("Starting packet capture")
    capture = pcapy.open_live(args.interface, args.snaplen,
    sniff_promisc, sniff_timeout_ms)
    pkts_in = 0
    try:
        while not finished.is_set() and (args.max_packets <= 0 or pkts_in < args.max_packets):

            # capture a packet
            (pkt_hdr, pkt_raw) = capture.next()
    

注意上面的pcap_open_live函数,其主要的功能就是采集数据。以前提到过,pcapy本质上就是在原来的libpcap上面封装的python库,所以其返回和定义都有C的影子在里面。
首先说明一下pcap_open_live的参数
第一个参数:device,抓取的网卡名称.
第二个参数:snaplen(maximum number of bytes to capture _per_packet)抓取的数据包的最大长度,如以太网的话,一般就是1500
第三个参数:promiscious mode (1 for true)混杂模式否。
第四个参数: timeout 时间(单位是毫秒)。
返回值是一个对象指针。因为是一个对象指针,所以可以在下面循环的代码中可以看到其实pycap只取了pkt_hdr和pkt_raw这两个结构体。
先看pkt_hdr,当本地化的时候,该结构体里面其实就是3个参数:ts,caplen,len

image

当调用p_getts的时候,就将返回两个时间。
image
具体代码可以参考:
https://github.com/CoreSecurity/pcapy

在获取了对象以后,其实真正采集的是

 # capture a packet
    (pkt_hdr, pkt_raw) = capture.next()

这里面的pkt_hdr代表pcap里面的一些基础信息,
pkt_raw 就是真正的数据包。从下面链接的这段代码中可以比较明显的看出区别:https://gist.github.com/dbrown29/3816908
在了解了pcap的大致逻辑后,我们继续追踪producer代码:

if pkt_hdr is not None:
    logging.debug("Packet received: pkts_in=%d, pkt_len=%s", pkts_in, pkt_hdr.getlen())
    pkts_in += 1
    pkt_ts = timestamp(pkt_hdr)
    kafka_producer.produce(args.kafka_topic, key=pack_ts(pkt_ts), value=pkt_raw, callback=delivery_callback)

    # pretty print, if needed
    if args.pretty_print > 0 and pkts_in % args.pretty_print == 0:
    print 'Packet received[%s]' % (pkts_in)

    # serve the callback queue
    kafka_producer.poll(0)

我们先看Kafka的key值得来源:

pkt_ts = timestamp(pkt_hdr)
def timestamp(pkt_hdr):
    """ Returns the timestamp of the packet in epoch milliseconds. """

    (epoch_secs, delta_micros) = pkt_hdr.getts()
    epoch_micros = (epoch_secs * 1000000.0) + delta_micros
    return epoch_micros

结合上面提到的pkt_hdr.getts返回的将是两个时间,可以得出这个转换为ms的时间戳,并将其作为messages的key发送到kafka。 这里就可以看出使用这个Key的优越性了,由于Kafka的消息分发,默认是按照即hash(key) % numPartitions 的方式进行分发的,根据时间的话也就是说一定时间范围内的消息基本上都是分发到同一个分区的,这样就加快了Kafka检索的速度。(可以参看:https://www.cnblogs.com/huxi2b/p/4757098.html)
所以Kafka中的消息实际上是与其他Kafka消息字段相同,只是Key(时间戳)+Value(raw packet)而已。
具体可以看下面的运行情况:

$ pycapa --producer \
    --interface eth0 \
    --kafka-broker localhost:9092 \
    --kafka-topic pcap \
    --pretty-print 5
  INFO:root:Connecting to Kafka; {'bootstrap.servers': 'localhost:9092', 'group.id': 'UAWINMBDNQEH'}
  INFO:root:Starting packet capture
  Packet received[5]
  Packet delivered[5]: date=2017-05-08 14:48:54.474031 topic=pcap partition=0 offset=29086 len=42
  Packet received[10]
  Packet received[15]
  Packet delivered[10]: date=2017-05-08 14:48:58.879710 topic=pcap partition=0 offset=0 len=187
  Packet delivered[15]: date=2017-05-08 14:48:59.633127 topic=pcap partition=0 offset=0 len=43
  Packet received[20]
  Packet delivered[20]: date=2017-05-08 14:49:01.949628 topic=pcap partition=0 offset=29101 len=134
  Packet received[25]
  ^C
  INFO:root:Clean shutdown process started
  Packet delivered[25]: date=2017-05-08 14:49:03.589940 topic=pcap partition=0 offset=0 len=142
  INFO:root:Waiting for '1' message(s) to flush
  INFO:root:'27' packet(s) in, '27' packet(s) out

从上面我们可以看到时间戳,分区编号等。对应到代码上则是这样:

def delivery_callback(err, msg):
    """ Callback executed when message delivery either succeeds or fails. """

    # initialize counter, if needed
    if not hasattr(delivery_callback, "pkts_out"):
         delivery_callback.pkts_out = 0

    if err:
        logging.error("message delivery failed: error=%s", err)

    elif msg is not None:
        delivery_callback.pkts_out += 1

        pretty_print = 0
        pretty_print = producer_args.pretty_print

        if pretty_print > 0 and delivery_callback.pkts_out % pretty_print == 0:
            print 'Packet delivered[%s]: date=%s topic=%s partition=%s offset=%s len=%s' % (
                delivery_callback.pkts_out, to_date(unpack_ts(msg.key())), msg.topic(),
                msg.partition(), msg.offset(), len(msg.value()))

仔细看起打印的日志,时间戳的来源其实就是msg中的key。分区长度等就是kafk消息中的内容。其他类似,整个kafka中的消息如下所示:
image
需要注意的是,在Kafka的文档以及源码中,消息(Message)并不包括它的offset。Kafka的log是由一条一条的记录构成的,Kafka并没有给这种记录起个专门的名字,但是需要记住的是这个“记录”并不等于"Message"。Offset MessageSize Message加在一起,构成一条记录。而在Kafka Protocol中,Message具体的格式为

Message => Crc MagicByte Attributes Key Value
  Crc => int32
  MagicByte => int8
  Attributes => int8
  Key => bytes
  Value => bytes

为了进一步核实代码的情况,我们将producer.py的代码略做修改,将value的值单独写成一个文件:

  pkt_ts = timestamp(pkt_hdr)
  kafka_producer.produce(args.kafka_topic, key=pack_ts(pkt_ts), value=pkt_raw, callback=delivery_callback)
  f=open("test123.pcap","ab+")
  f.write(pkt_raw)
  f.close()

将生成的文件用Hex方式打开查看如下:
image
可以看到,value 就是一个完整的数据包。而不是一个pcap格式的数据。

消费网络数据

前面解释了Kafka中的消息,如果要解析其中的消息,显然主要就是要解析其中的value值。前面提到,在使用pycpa的时候,是可以生成标准的pcap文件的,但根据producer的代码看,发出的msg_value是原始数据,key是时间,如果要生成pcap的格式,需要重新汇总一下应该才可以的。
查看conusmer.Py发现:

  # write the packet header and packet
                    sys.stdout.write(packet_header(msg))
                    sys.stdout.write(msg.value())
                    sys.stdout.flush()

consumer在输出的时候,除了输出msg.value(实际就是发送的原始包以外)还输出了2个包头。

#构建pcap global header
def global_header(args, magic=0xa1b2c3d4L, version_major=2, version_minor=4, zone=0,
                  sigfigs=0, network=1):
    """ Returns the global header used in libpcap-compliant file. """

    return struct.pack("IHHIIII", magic, version_major, version_minor, zone,
        sigfigs, args.snaplen, network)


 # if 'pretty-print' not set, write libpcap global header
    if args.pretty_print == 0:
        sys.stdout.write(global_header(args))
        sys.stdout.flush()
#包信息
def packet_header(msg):
    """ Returns the packet header used in a libpcap-compliant file. """

    epoch_micros = struct.unpack_from(">Q", bytes(msg.key()), 0)[0]
    secs = epoch_micros / 1000000
    usec = epoch_micros % 1000000
    caplen = wirelen = len(msg.value())
    hdr = struct.pack('IIII', secs, usec, caplen, wirelen)
    return hdr

第一个全局包,就是标准的pcap格式包(参见:https://blog.csdn.net/u013793399/article/details/51474831)共24byte,后面一部分16个字节组成了一个packer包头,其包含secs,usec,caplen,wirelen几个参数。在consumer文件中的位置如图所示:image。正因为这两个header的存在,所以consumer才可以转存成pcap的文件格式。

posted @ 2018-03-29 14:25  angelxp  阅读(455)  评论(0编辑  收藏  举报