0%

pyshark源码解析——如何制作一个python wrapper

接触网络编程的程序员或多或少会用到wireshark这个程序,除去前端GUI就是tshark,可以在命令行下执行。wireshark功能非常强大,常用于抓包(sniffing)和网络流量(network traffic)分析。为了方便程序员对network traffic进行更深层次的分析,pyshark应运而生:利用tshark分析network traffic,然后将输出的数据再包裹在自己定义的class中。如此一来,程序员只需要访问这些class生成的object就能获得network traffic内的信息,而不用去管tshark如何分析底层的二进制数据。

关于wireshark这一块,读者可以自行上网检索,本文主要是讲pyshark这个library的代码分析。事实上,python有很多优秀的网络分析library,并且大部分性能都优于pyshark。本文我们研究pyshark,主要想学习借鉴:如何制作一个python wrapper来调用一个命令行应用程序,并且给python提供友好的interface以便于high-level的程序开发。举一反三,我们就能wrap一切!

  • 撰写本文的时候,pyshark版本为0.4.3

什么是packet

介绍pyshark之前,我们必须要了解它打交道的对象:网络数据。

网络数据用一个形象的比喻,就是一个个套娃。在网线、wifi上传输的就是一个个称作frame的套娃;frame打开来,里面套着mac信息和packet;packet打开来,里面套着ip信息和真正的数据,这些数据一般通过TCP和UDP协议构建。所以我们需要继续解析TCP或者UDP,掐头去尾之后才最终拿到我们想要的数据(data)。请注意,这个数据层面还是二进制的,存在很多种套娃方法,其中最流行的就是HTTP协议。其实就算在这个层面确定了是HTTP协议,程序员还可以继续套娃以适应自己的上层应用。

套娃有点头晕,我们可以通过七层网络结构来感受一下:

示意图来自:https://www.bmc.com/blogs/osi-model-7-layers/

当network analyzer(如wireshark)接收到二进制数据后,它就会通过dissector来解析数据,也就是一层层打开套娃的过程。然后它会把解析的结果返还给客户。一般来说,我们最关注每个frame的timestamp,里面的各种layer(IP啦,UDP/TCP等等)。而这些layer其实就是一层层套娃,是一层套一层的。当network analyzer以文本形式返还给用户时,我们很难进行高级分析,所以pyshark在这里就是帮我们解析这些包含frame信息的文本,并将数据以packet,layer等形式返还给用户,至此用户可以自由地调用这些信息并进行分析。

pyshark的架构猜想

通过上一个section的讨论,我们大概能体会pyshark要做的工作:

  • 根据用户的需求,调用tshark程序(sniff抓包或者直接打开pcap文件)
  • 解析tshark的文本输出,并放入实现设计好的数据容器:packet,layer。。。用户通过操作这些容器来访问数据

所以我们从零开始编写这个library的话,有以下几点需要思考:

  • tshark这个程序能接受怎样的输入参数,那在我们的接口中就要提供这些参数的输入,相当于传话。我们需要阅读tshark的文档。
  • tshark这个程序能返回怎样的结果?结果储存在哪里,如何传递到我们的程序中?如何判断tshark已经输出完毕?
  • tshark返回值是什么结构,怎样设计class来包裹这些数据,应该写哪些method/property去更好地提供api,应该如何去写__str__和__repr__去提供信息展示?

带着疑惑我们来看看pyshark是如何implement这些技术细节的。

pyshark模块讲解

这个小节我们具体分析pyshark。进入详解之前,让我们先俯瞰一下这个文件夹目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pyshark
├── __init__.py
├── capture
│   ├── __init__.py
│   ├── capture.py
│   ├── file_capture.py
│   ├── inmem_capture.py
│   ├── live_capture.py
│   ├── live_ring_capture.py
│   ├── pipe_capture.py
│   └── remote_capture.py
├── config.ini
├── config.py
├── packet
│   ├── __init__.py
│   ├── common.py
│   ├── consts.py
│   ├── fields.py
│   ├── layer.py
│   ├── packet.py
│   └── packet_summary.py
└── tshark
├── __init__.py
├── tshark.py
├── tshark_json.py
└── tshark_xml.py

我们可以看到,pyshark的结构非常简单明了,分为capture,packet,tshark三个部分。

  • tshark模块处理tshark调用路径,处理如何解析tshark的输出(json/xml)。
  • packet模块可以理解为数据容器,因为我们看到了packet,layer,field这些关键字。
  • capture模块就是用户会用到的模块,处理tshark和客户端的启动和运行。

tshark模块

在这个模块中,我们主要看作者如何将tshark的返回结果放入到他所定义的数据结构中。

tshark.py

我们先看tshark.py这个文件。通过文件中各个function的名字,我们就能看到是去检查tshark的路径、版本、以及和版本有关的feature。

get_process_path这个function中,我们可以看到他是如何处理不同平台的路径。我们做多平台的python library的时候,经常要用到sys.platform这个参数来判断是windows,linux还是mac。另外一个有用的函数是os.getenv,用来获取当前运行环境中的变量名的。

刚学习python的朋友可能觉得TSharkNotFoundExceptionTSharkVersionException两个class的定义莫名其妙,继承了系统自带的Exception,却啥事都不做。这里的话,他主要是想让程序报错更加有指向性,一看错误就知道是tshark路径没找到或者说tshark版本有误。

说到版本,在检查、比较版本的function中,我们看到他用了LooseVersion这个函数,来自distutils.version。这个function还是挺实用的,可以留意一下。

get_tshark_interfaces我们可以看出来执行了一个命令行tshark -D来观察系统的interface。

tshark_xml.py

这个模块顾名思义,就是研究如何把tshark返回的xml数据转换成pyshark所定义的数据(object)。代码短短几十行,主要是用lxml解析xml文本,然后把相应的数据放入到PacketSummaryPacket以及Layer这三个容器中。后面我们会讲到他是如何设计和实现这些容器的。

tshark_json.py

这个模块的主要是把tshark返回的json数据转换成pyshark所定义的数据。这里我们发现他除了使用Packet,还用了一个叫做JsonLayer的容器。因为LayerJsonLayer的instance组成的List都能以layers参数放入Packet之中,所以我们可以断定他们是有一定关系的:要么是继承,要么sibling。

关于json解析,我们一般调用python标准库自带的json模块。在有些追求性能的代码中,我们还看到过simplejson这样的模块(可以自行去研究这几个库在不同环境下的benchmark)。而在pyshark中,我们看到了ujson,也就是ultrajson。这几个库都是很好的,但是feature不一样,所以具体问题具体分析。纯文本的解析的话,ujson应该是最快的,但是它自身并不支持object_hookobject_pairs_hook。我们在这段代码中可以看到,作者尽可能地想用ujson去做解析,但是遇到一个dict有重复的key的时候,由于他想保留所有信息(利用duplicate_object_hook这个函数),就只能用原生的json模块去做解析了。

虽然pyshark的性能一直被人吐槽,但是我们看到作者还是做了一些努力。

packet模块

我们在上个小节看到作者把tshark的返回结果放入到了他自定义的数据结构中,那这个小节我们就看看这一个个数据结构都是如何实现的吧。

consts.py

这个文件只有一行,定义了TRANSPORT_LAYERS这个list,申明这个layer里面就是放UDP和TCP的。这个定义经常会用到,所以单独放在一个文件中,如此以来就不必重复声明了。

common.py

这个文件中我们看到作者定义了两个class:PickleableSlotsPickleable。我们后面会发现所有pyshark所定义的数据结构(包括PacketLayerLayerField等)都是继承了这两个class。但是这两个class本身好像又没做什么事情,就定义了__getstate____setstate__两个magic method。那么作者到底是想要给这些数据结构提供什么feature呢?

问题的关键在于__getstate____setstate__是做什么的。其实我们通过“pickle”、“state”这两个关键词就能感受到是和数据的serialization有关的。所谓serialization就是将object的整个状态保存成二进制文件,又可以通过一定方法把二进制文件读出来还原这个object。

我们从官方文档中找到pickle模块对于这两个magic method的解读:https://docs.python.org/3/library/pickle.html#pickling-class-instances

正常情况下,如果你不改写这两个method,pickle就是通过__dict__这个attribute中读写数据的,非常自然地,它会去调用__getattr__来访问数据。问题就在于pyshark为了让用户有更好的体验,改写了__getattr__这个函数,所以pickle模块在进行serialization的过程中会出现问题!所以这里作者必须写__getstate____setstate__这两个函数,pickle就会用这两个函数去获取或者设置attribute!

总之,写这两个class就是为了不妨碍正常的serialization。至于作者为何要改写__getattr__,我们后面会提到。

packet_summary.py

这个模块就是一个简单的容器,直接继承于object,值得注意的是我们看看作者如何写__repr____str__。有了这两个函数,我们可以通过print函数或者repr函数来查看一个packet的summary信息。我们从tshark那里得到的信息对用户不太友好,这个PacketSummary模块起到了重组信息的功能,你可以理解成prettify。后面其他数据容器都有这两个函数,起的同一个作用,我就不再重复介绍了。

packet.py

Packet可以说是pyshark这个library中最重要的数据容器,是用户去查看一个网络packet具体内容的交互界面。

首先我们看到Packet是继承自上文提到的Pickleable,所以我们看看作者对__getattr__动了什么手脚。

1
2
3
4
5
6
7
8
def __getattr__(self, item):
"""
Allows layers to be retrieved via get attr. For instance: pkt.ip
"""
for layer in self.layers:
if layer.layer_name == item:
return layer
raise AttributeError("No attribute named %s" % item)

所以我们看到了,作者想要让用户直接通过.这个操作直接去访问包含在这个packet中的layer,如此以来就会让人觉得十分intuitive。比如一个packet中含有TCP这个layer,那么我们可以直接通过packet.TCP来访问,而不是自己去遍历packet.layers这个list,然后查看layer.layer_name去看有没有TCP这个layer。也就是说,作者通过改写__getattr__,让原先不属于packet的layers成为了它本身的attribute。

有时候我们想要知道这个packet中有没有某种layer,在python中我们喜欢用in去询问。所以作者这里写了__getitem____contains__来实现。

另外一个值得我们关注的是get_raw_packet这个函数。这个函数的意思就是:别解析了,直接给我看这个packet的原始二进制数据。我们可以看到,pyshark只有在use_jsoninclude_raw两个选项都有的时候,才能开启这个功能(具体原因我们在capture模块中讲解)。当packet中有frame_raw这个attribute的时候(明显是以Layer形式存在的),作者利用binascii将hex字符串转换成了二进制bytes。python中很容易混淆bytes和str,并且你在处理、转换它们的时候非常容易出问题,可以开一个专题来探讨。这里我们学习下作者如何将一个hex字符串转换成bytes的:

1
2
3
4
raw_packet = b''
byte_values = [''.join(x) for x in zip(self.frame_raw.value[0::2], self.frame_raw.value[1::2])]
for value in byte_values:
raw_packet += binascii.unhexlify(value)
layer.py

packet中除去这个frame的信息,我们最关注的就是里面的layer,这也是真正储存数据的地方。
我们扫了一眼代码,解开了之前的疑惑:LayerJsonLayer到底是什么关系。答案很清晰,JsonLayer继承自Layer,在Layer基础上拓展或者改写了一些功能。那么我们先通过Layer__init__函数来了解下这个class的数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def __init__(self, xml_obj=None, raw_mode=False):
self.raw_mode = raw_mode

self._layer_name = xml_obj.attrib['name']
self._all_fields = {}

# We copy over all the fields from the XML object
# Note: we don't read lazily from the XML because the lxml objects are very memory-inefficient
# so we'd rather not save them.
for field in xml_obj.findall('.//field'):
attributes = dict(field.attrib)
field_obj = LayerField(**attributes)
if attributes['name'] in self._all_fields:
# Field name already exists, add this field to the container.
self._all_fields[attributes['name']].add_field(field_obj)
else:
self._all_fields[attributes['name']] = LayerFieldsContainer(field_obj)

原来tshark生成的xml文本被lxml解析之后生成了xml_obj,传给Layer之后直接被消化储存,而不是保留在这个Layer的object中,作者解释是lxml object比较占用内存(没有reference之后,xml_obj会被销毁,内存得以回收)。我们在这个函数中还看到,一个Layer除了本身的name,是由一系列的field组成的。我们用LayerField这个容器来储存field数据。关键来了,如果一个layer里有重名的field,我们就没办法用dict来储存了,会有duplicated key的问题。所以我们统一用LayerFieldsContainer来装LayerField,你可以想像成一个高级版本的list。这样以来我们就实现了{field_name: [field_data, field_data]}的结构。

Layer中其他的函数大部分是解决数据的access和pretty print,与前面讲的大同小异。我觉得还有一个可以注意的点就是_sanitize_field_name这个method。我们在初始化的时候,是将field存到一个dict中,dict的key是字符串,想怎么写就怎么写。但是当你想要以.符号来读取attribute数据的时候,是不能有.-这些符号的,所以作者在这里是把这些都换成了_

那我们继续看看JsonLayer有什么不同之处。扫一眼代码,我们能看到它直接放弃了Layer__init__,所以内部的数据结构就完全不同了,继承的唯一目的就是复用pretty print的代码,中间是用get_field这个函数串联起来的。这样的设计我个人不是很喜欢,看着乱糟糟的,真不如先写一个LayerBase,然后分别写XmlLayerJsonLayer。从class的代码来看,主要就是实现了一个nested dict的访问,然后实时返回一个LayerFieldsContainerobject。由于代码有点乱也不是本文的重点,有兴趣的朋友可以自己去研究下,这里就不赘述了。

fields.py

我们在Layer中看到用了LayerFieldsContainerLayerField,那这个文件自然是去定义这两个数据结构的。比起其他几个数据结构有意思的是,LayerField继承自SlotPickleable,而LayerFieldsContainer更是同时继承了strPickleable。后者出乎了我们的想象,为什么继承str呢?

在回答这个疑问之前,我们先看看LayerField,毕竟LayerFieldsContainer是用来装这个玩意儿的。这个class出奇得简单,无非就是把所有传入的参数都放到object当中,然后提供了hex、int、binary等多种方式的数值表示。值得学习的点是他用了__slots__声明来节省了内存空间。感兴趣的朋友可以阅读stackoverflow上的一个帖子:https://stackoverflow.com/questions/472000/usage-of-slots

知道了LayerField不过是个简单的class,那么我们继续来看LayerFieldsContainer。作者继承了python原生的str后,改写了__new__这个method。咦,啥也没做,就是搞了一个叫做fields的attribute,还是个list。那么,作者到底想要干嘛?为什么要继承str,让这个class成为一个str,有str的feature?我无法理解。也许作者希望得到str的一些操作符,比如+,以方便pretty print的编写,但这也是完全没有必要的。

capture模块(划重点)

我们前面花了很多篇幅去介绍tshark模块和packet模块,其实都是在做准备工作。要学习如何制作pyhton wrapper,还要重点关注这一节!

我们在capture模块目录下看到很多文件,但是都以“capture”结尾,很容易猜到所有的类都是由catpure.py中定义的Capture类派生出来的。正如我们前面介绍的,pyshark要做的事情无非是启动tshark,从tshark获取ouput,处理output并以一个有友好API的object形式展现给用户。所以不同的capture其实改变的只是第一步:启动tshark的方式。也就是说,不同的capture让我们可以命令tshark以不同方式获取network traffic,比如从网卡sniff抓包,直接打开pcap文件等等。

我们先来分析catpure.py。500行的代码还是有点长,并且只定义了一个Capture,所以我们先看下__init__函数,然后再找用户常用的method去研究。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Capture(object):
"""Base class for packet captures."""
DEFAULT_BATCH_SIZE = 2 ** 16
SUMMARIES_BATCH_SIZE = 64
DEFAULT_LOG_LEVEL = logging.CRITICAL
SUPPORTED_ENCRYPTION_STANDARDS = ["wep", "wpa-pwk", "wpa-pwd", "wpa-psk"]

def __init__(self, display_filter=None, only_summaries=False, eventloop=None,
decryption_key=None, encryption_type="wpa-pwd", output_file=None,
decode_as=None, disable_protocol=None, tshark_path=None,
override_prefs=None, capture_filter=None, use_json=False, include_raw=False,
custom_parameters=None, debug=False):

self.loaded = False
self.tshark_path = tshark_path
self._override_prefs = override_prefs
self.debug = debug
self.use_json = use_json
self.include_raw = include_raw
self._packets = []
self._current_packet = 0
self._display_filter = display_filter
self._capture_filter = capture_filter
self._only_summaries = only_summaries
self._output_file = output_file
self._running_processes = set()
self._decode_as = decode_as
self._disable_protocol = disable_protocol
self._json_has_duplicate_keys = True
self._log = logging.Logger(self.__class__.__name__, level=self.DEFAULT_LOG_LEVEL)
self._closed = False
self._custom_parameters = custom_parameters
self._eof_reached = False
self.__tshark_version = None

if include_raw and not use_json:
raise RawMustUseJsonException("use_json must be True if include_raw")

if self.debug:
self.set_debug()

self.eventloop = eventloop
if self.eventloop is None:
self._setup_eventloop()
if encryption_type and encryption_type.lower() in self.SUPPORTED_ENCRYPTION_STANDARDS:
self.encryption = (decryption_key, encryption_type.lower())
else:
raise UnknownEncyptionStandardException("Only the following standards are supported: %s."
% ", ".join(self.SUPPORTED_ENCRYPTION_STANDARDS))

我们通过阅读输入参数,可以了解到大部分是和tshark有关的,也就是说这里面一大部分参数都是为了控制tshark的行为。eventloop的话是为了让pyshark这个library可以在异步环境下使用。有意思的是我们再次看到了“include_raw”和“use_json”,而raw和json这两个选项必然是要传达给tshark的,所以我们直接跳转到用到这两个关键词的method,然后看看pyshark如何运行tshark。映入眼帘的是get_parameters。我们可以看到作者的解释,只有让tshark返回json的时候,才能得到二进制的raw data。这个method根据用户启动Capture的参数,返回了一个参数list,显然是要给subprocess用了,所以我们跳转到用到get_parameters的method:_get_tshark_process。到了这里,我们就知道作者如何启动tshark进程了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
async def _get_tshark_process(self, packet_count=None, stdin=None):
"""Returns a new tshark process with previously-set parameters."""
output_parameters = []
if self.use_json:
output_type = "json"
if not tshark_supports_json(self._get_tshark_version()):
raise TSharkVersionException("JSON only supported on Wireshark >= 2.2.0")
if tshark_supports_duplicate_keys(self._get_tshark_version()):
output_parameters.append("--no-duplicate-keys")
self._json_has_duplicate_keys = False
else:
output_type = "psml" if self._only_summaries else "pdml"
parameters = [self._get_tshark_path(), "-l", "-n", "-T", output_type] + \
self.get_parameters(packet_count=packet_count) + output_parameters

self._log.debug("Creating TShark subprocess with parameters: " + " ".join(parameters))
self._log.debug("Executable: %s" % parameters[0])
tshark_process = await asyncio.create_subprocess_exec(*parameters,
stdout=subprocess.PIPE,
stderr=self._stderr_output(),
stdin=stdin)
self._created_new_process(parameters, tshark_process)
return tshark_process

def _created_new_process(self, parameters, process, process_name="TShark"):
self._log.debug(process_name + " subprocess created")
if process.returncode is not None and process.returncode != 0:
raise TSharkCrashException(
"%s seems to have crashed. Try updating it. (command ran: '%s')" % (
process_name, " ".join(parameters)))
self._running_processes.add(process)

原来组装完tshark的参数之后,作者利用了asyncio.create_subprocess_exec创造一个进程来运行tshark,stdout直接传给subprocess.PIPEstderr传给self._stderr_out()subprocess.PIPE意味着tshark的输出被缓存到buffer中了,并且没有设置buffer limit。所谓的self._stderr_out其实就是根据是否debug来决定输出到terminal还是直接送到subprocess.DEVNULL废弃掉。而进程本身呢,在程序判断没有crash之后被注册在我们__init__中声明的self._running_processes这个set当中。当我们的程序结束的时候,则会调用close_async将线程一个个关掉。

知道tshark如何开启和关闭之后,我们最大的疑问还没被回答:用户怎么从tshark进程的subprocess.PIPE拿到数据并解析呢?那么我们最好根据用户常用的method按图索骥。下面是一个最简单的pyshark使用案例:

1
2
3
4
5
6
7
8
# 载入pyshark
import pyshark

# 打开文件
cap = pyshark.FileCapture('/tmp/mycapture.pcap')
# 从cap中拿到一个个packet
for pkt in cap:
print(pkt)

我们发现了初始化FileCapture的过程中,有这么一句:self._packet_generator = self._packets_from_tshark_sync()。答案来了,就在capture.py_packets_from_tshark_syncmethod中。这个method用yield返回数值,所以返回的是一个generator,而内部的变量则可以一直保持状态不被销毁。在打开本地文件后,作者先获取我们上面提到的tshark的进程,然后开始从tshark进程的stdout中不断地获取数据直到结束。读取数据和packet是由_get_packet_from_stream执行的,每次这个函数都会返回一个完整的packet object,以及残留的还没有被parse的data。这个data会在下一次循环中被传给_get_packet_from_stream用以解析下一个packet。所以我们看出来,这个method需要有判断packet起点和结尾的能力(所以这是一个关于字符串处理的题目)。我们看看具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
async def _get_packet_from_stream(self, stream, existing_data, got_first_packet=True, psml_structure=None):
"""A coroutine which returns a single packet if it can be read from the given StreamReader.

:return a tuple of (packet, remaining_data). The packet will be None if there was not enough XML data to create
a packet. remaining_data is the leftover data which was not enough to create a packet from.
:raises EOFError if EOF was reached.
"""
# yield each packet in existing_data
if self.use_json:
packet, existing_data = self._extract_packet_json_from_data(existing_data,
got_first_packet=got_first_packet)
else:
packet, existing_data = self._extract_tag_from_data(existing_data)

if packet:
if self.use_json:
packet = packet_from_json_packet(packet, deduplicate_fields=self._json_has_duplicate_keys)
else:
packet = packet_from_xml_packet(packet, psml_structure=psml_structure)
return packet, existing_data

new_data = await stream.read(self.DEFAULT_BATCH_SIZE)
existing_data += new_data

if not new_data:
# Reached EOF
self._eof_reached = True
raise EOFError()
return None, existing_data

这里我们会发现这个method不断以self.DEFAULT_BATCH_SIZE这个大小去读取tshark的stdout,如果手头的数据可以解析出packet,就返回packet和残留的data;如果手头的数据不够,则返回None和data,等待下一次数据读取;如果无法读取新的数据了,说明tshark已经输出了所有的信息,那么就通过EOFError来提醒程序,到头啦。这里我想提一嘴,做python wrapper的时候,万一python跑得比wrap住的程序还快,这种判断逻辑就容易误判EOF了。

回到正题,这个method中调用的_extract_packet_json_from_data_extract_tag_from_data是比较关键的字符串处理函数,感兴趣的同学可以自行研究。两个函数的作用是把tshark的stdout分成完整的、一段段的packet数据,然后传给相对应的parser,也就是我们前面介绍的packet_from_json_packetpacket_from_xml_packet,如此一来,我们就能把一个packet object传给用户了。

故事到了这里还没完,上面我们利用了FileCapture来举例子,没有用到aysnc的特性,那么这一定是用在了其他种类的capture中。我们来探索下live_capture.py中的LiveCapture。同样的,先上用法:

1
2
3
4
capture = pyshark.LiveCapture(interface='eth0')
capture.sniff(timeout=50)
for packet in capture.sniff_continuously(packet_count=5):
print 'Just arrived:', packet

我们可以看到这个LiveCapture的用法不太一样。先看第一个用法sniff,从代码中看,就是Capture.load_packets这个method的别名。跳转过去,我们看到无非就是从一个interface读取特性数量的packet,或者读取特性的时间。如果把packet_count这个参数设为0,代码就会从网卡一直读取packet,这对机器处理的能力还是有相当大的要求,处理速度不到位的话buffer就爆了,导致数据的损失。我们看看这个method是如何读取数据的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def load_packets(self, packet_count=0, timeout=None):
"""Reads the packets from the source (cap, interface, etc.) and adds it to the internal list.

If 0 as the packet_count is given, reads forever

:param packet_count: The amount of packets to add to the packet list (0 to read forever)
:param timeout: If given, automatically stops after a given amount of time.
"""
initial_packet_amount = len(self._packets)

def keep_packet(pkt):
self._packets.append(pkt)

if packet_count != 0 and len(self._packets) - initial_packet_amount >= packet_count:
raise StopCapture()

try:
self.apply_on_packets(keep_packet, timeout=timeout, packet_count=packet_count)
self.loaded = True
except asyncTimeoutError:
pass

没想到继续套娃,那么我们就去apply_on_packets看看。结果继续套了一个packets_from_tshark,与我们之前研究的_packets_from_tshark_sync遥相呼应。到了这里我们就明白了,搞定这个我们就大功告成了。由于这个method调用了_go_through_packets_from_fd,我们把两段代码同时打出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
async def packets_from_tshark(self, packet_callback, packet_count=None, close_tshark=True):
"""
A coroutine which creates a tshark process, runs the given callback on each packet that is received from it and
closes the process when it is done.

Do not use interactively. Can be used in order to insert packets into your own eventloop.
"""
tshark_process = await self._get_tshark_process(packet_count=packet_count)
try:
await self._go_through_packets_from_fd(tshark_process.stdout, packet_callback, packet_count=packet_count)
except StopCapture:
pass
finally:
if close_tshark:
await self.close_async()

async def _go_through_packets_from_fd(self, fd, packet_callback, packet_count=None):
"""A coroutine which goes through a stream and calls a given callback for each XML packet seen in it."""
packets_captured = 0
self._log.debug("Starting to go through packets")

psml_struct, data = await self._get_psml_struct(fd)

while True:
try:
packet, data = await self._get_packet_from_stream(fd, data, got_first_packet=packets_captured > 0,
psml_structure=psml_struct)
except EOFError:
self._log.debug("EOF reached")
self._eof_reached = True
break

if packet:
packets_captured += 1
try:
packet_callback(packet)
except StopCapture:
self._log.debug("User-initiated capture stop in callback")
break

if packet_count and packets_captured >= packet_count:
break

我们发现,packets_from_tshark_go_through_packets_from_fd合起来的逻辑就等于_packets_from_tshark_sync:先获得tshark的进程,然后调用_get_packet_from_stream开始从进程的stdout中读取数据。在这里我们要注意,它时时刻刻要传递packet_count这个参数,因为它最终要传给tshark。所以如果用户限定了packet的读取数量,就会在python和tshark两个层面把关,我不知道这是否有必要。

看了两个版本的capture,我们终于厘清头绪,是_get_packet_from_stream从buffer中读取文本并产生了packet object。其他几个版本的capture我没用过,但是原理和工作流程是一模一样的。

至于作者还写了诸如__enter____exit____del__等magic method,就是为了释放资源。

python wrapper写法总结

至此,我们基本就把pyshark这个library分析了一遍。我们可以看到做一个目标程序的wrapper需要以下几个部件:

  • 主程序,把用户输入的参数转化成命令行,调用subprocess启动目标程序,把目标程序的输出存在stdout的buffer中
  • 字符串处理工具,从buffer中读取文本,识别一段数据的首位,并有效地分成一段段可以解析(json/xml等)的文本。
  • 数据容器,以业务逻辑组合,并能接受解析得到的json/xml object。数据容器要提供友好的API。

有了以上三部分,我们就能根据业务逻辑进一步开发、完善我们的wrapper。在目标程序进行版本变动的时候,也要紧跟步伐做出适配。除了这种命令行程序的wrapper,python还可以通过C接口进行C/C++代码的wrap,那是另一种玩法并且更加高效,我们有时间再写文章讨论。


之前工作用到过pyshark,所以一直想阅读源码并写一篇笔记,从年初拖到现在,终于完成了这个小心愿。由于水平有限外加时间仓促,文中难免有纰漏,欢迎读者朋友留言指教。