j3iiifn’s blog

ネットワーク、インフラ、プログラミングについての備忘録

dpktのpack_hdrメソッドを読んだ

dpktはパケットを作成したり解析したりするPythonモジュールである。pcapファイルを読み込むときなどに使う。

kbandla/dpkt: fast, simple packet creation / parsing, with definitions for the basic TCP/IP protocols

バイト列をやりとりする通信プログラムを書く際に、バイト列の扱い方の参考になると思ってソースコードを読んでいた。

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つの疑問を抱いた。

  1. *[getattr(self, k) for k in self.__hdr_fields__] の頭にあるアスタリスク * は何???
  2. 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点目の疑問も解決した。

めでたしめでたし。