dpktのpack_hdrメソッドを読んだ
dpktはパケットを作成したり解析したりするPythonモジュールである。pcapファイルを読み込むときなどに使う。
バイト列をやりとりする通信プログラムを書く際に、バイト列の扱い方の参考になると思ってソースコードを読んでいた。
class Packet(_MetaPacket("Temp", (object,), {})): の中で定義されている def pack_hdr(self): は Packet オブジェクトのヘッダフィールドを定められたフォーマットに従ってバイト列にするメソッドである。
https://github.com/kbandla/dpkt/blob/9a5157f605f6f68661e58c8b44e4b6364c208507/dpkt/dpkt.py#L151
def pack_hdr(self):
"""Return packed header string."""
try:
return self._pack_hdr(
*[getattr(self, k) for k in self.__hdr_fields__]
)
except struct.error:
(省略)
このソースコードを見て2つの疑問を抱いた。
*[getattr(self, k) for k in self.__hdr_fields__]の頭にあるアスタリスク*は何???self._pack_hdr(引数)を呼び出しているけど、どこにもdef _pack_hdr(self, 引数)のような定義は見当たらないぞ???
1. *[getattr(self, k) for k in self.__hdr_fields__] の頭にあるアスタリスク * は何???
まず1点目のアスタリスク * は「引数リストのアンパック」を意味する。
* の後ろにリストがあるが、 メソッド _pack_hdr の引数にリストオブジェクトを1つ渡すのではなく、リストを展開して _pack_hdr の引数として渡す。
Python公式チュートリアル に軽く説明が載っていた。
IPを例に上記のコードの動作を考えてみる。
class IP(dpkt.Packet) で ヘッダフィールド __hdr__ は次のように定義されている。
__hdr__ = (
('_v_hl', 'B', (4 << 4) | (20 >> 2)),
('tos', 'B', 0),
('len', 'H', 20),
('id', 'H', 0),
('off', 'H', 0),
('ttl', 'B', 64),
('p', 'B', 0),
('sum', 'H', 0),
('src', '4s', b'\x00' * 4),
('dst', '4s', b'\x00' * 4)
)
https://github.com/kbandla/dpkt/blob/9a5157f605f6f68661e58c8b44e4b6364c208507/dpkt/ip.py#L21
クラス IP の親は Packet で、さらにその親は _MetaPacket である。 クラス _MetaPacket のメソッド __new__ は次のようになっている。
class _MetaPacket(type):
def __new__(cls, clsname, clsbases, clsdict):
t = type.__new__(cls, clsname, clsbases, clsdict)
st = getattr(t, '__hdr__', None)
if st is not None:
# XXX - __slots__ only created in __new__()
clsdict['__slots__'] = [x[0] for x in st] + ['data']
t = type.__new__(cls, clsname, clsbases, clsdict)
t.__hdr_fields__ = [x[0] for x in st]
t.__hdr_fmt__ = getattr(t, '__byte_order__', '>') + ''.join([x[1] for x in st])
t.__hdr_len__ = struct.calcsize(t.__hdr_fmt__)
t.__hdr_defaults__ = dict(compat_izip(
t.__hdr_fields__, [x[2] for x in st]))
return t
https://github.com/kbandla/dpkt/blob/9a5157f605f6f68661e58c8b44e4b6364c208507/dpkt/dpkt.py#L32
変数 st に先程の __hdr__ が代入され、 クラスオブジェクト IP のインスタンス変数 __hdr_fields__ は次のようになる。
__hdr_fields__ = (
'_v_hl',
'tos',
'len',
'id',
'off',
'ttl',
'p',
'sum',
'src',
'dst',
)
これらはクラスオブジェクト IP のインスタンス変数名となる。これらの変数に値を代入する処理はクラス Packet のメソッド __init__ にある。
self.unpack(args[0])
https://github.com/kbandla/dpkt/blob/9a5157f605f6f68661e58c8b44e4b6364c208507/dpkt/dpkt.py#L90
args[0] はバッファである。メソッド unpack は次の通り。バッファ、つまりパケットを作り上げるバイト列を struct.unpack で解釈して、出てきた値を __hdr_fields__ で定義された各インスタンス変数に代入している。
def unpack(self, buf):
"""Unpack packet header fields from buf, and set self.data."""
for k, v in compat_izip(self.__hdr_fields__,
struct.unpack(self.__hdr_fmt__, buf[:self.__hdr_len__])):
setattr(self, k, v)
self.data = buf[self.__hdr_len__:]
https://github.com/kbandla/dpkt/blob/9a5157f605f6f68661e58c8b44e4b6364c208507/dpkt/dpkt.py#L174
つまり、 ip = IP(buffer) とすると、 ip.src で送信元IPアドレスを取得できるというわけである。
ここまで読むと、本題の [getattr(self, k) for k in self.__hdr_fields__] が何をしているのか理解できるようになる。
__hdr_fields__ で定義されたIPパケット ヘッダフィールド名のリストを順番に参照し、各フィールドの値をリストにして返しているのである。
そのリストをアンパックし、メソッド _pack_hdr の引数に渡している、というのが1点目の疑問の答えだ。
2. self._pack_hdr(引数) を呼び出しているけど、どこにも def _pack_hdr(self, 引数) のような定義は見当たらないぞ???
続いて2点目の、メソッド _pack_hdr がどこで定義されているか?という疑問の答えはクラス Packet のメソッド __init__ の中にある。
self._pack_hdr = partial(struct.pack, self.__hdr_fmt__)
https://github.com/kbandla/dpkt/blob/9a5157f605f6f68661e58c8b44e4b6364c208507/dpkt/dpkt.py#L104
公式ドキュメントによると、 functools.partial(func, *args, **keywords) は
新しい partial オブジェクト を返します。このオブジェクトは呼び出されると位置引数 args とキーワード引数 keywords 付きで呼び出された func のように振る舞います。
とのこと。
つまり、_pack_hdr(foo, bar, baz) を呼び出すと、 struct.pack(self.__hdr_fmt__, foo, bar, baz) が実行されることになる。
self.__hdr_fmt__ は フィールド foo, bar, baz のそれぞれのフォーマット文字列を連結したもので、上述したIPの場合は __hdr_fmt__ = '>BBHHHBBH4s4s' となる。
以上で2点目の疑問も解決した。
めでたしめでたし。