PythonでバイナリデータをパックしてUDPメッセージを送ってみる

はじめに

PythonバイナリデータをパックしてUDPメッセージを送るには、structモジュールを使う。簡単な使い方はPy MOTW: struct – Working with Binary Dataで確認できる。本エントリでは、structモジュールの使い方を押さえ、UDP上のバイナリデータで構成されたプロトコルであるGTPv2のEcho Requestメッセージを試しに送信してみる。 GTPv2のEcho RequestメッセージのプロトコルフォーマットはドコモのネットワークにGTP接続(レイヤー2)するためのメモのエントリで参照した以下のドキュメントで確認できる。

structモジュールでバイナリデータをパック、アンパック

前準備として、structモジュールの使い方を押さえる。

structモジュールはデータのパック方法として、関数レベルとクラスレベルの2つを用意している。データをパックするときには、フォーマット文字列(例えば、データがネットワークバイトオーダで4オクテットである、とか)を指定する必要がある。Py MOTW: struct – Working with Binary Dataによると、関数レベルのパッキングを利用する場合、フォーマット文字列はパッキングの際に毎度コンパイルされるのに対し、クラスレベルのパッキングを利用する場合、オブジェクトをインスタンス化するときにフォーマット文字列をコンパイルするため、データのパッキングが高速になるとのこと。 よって、ここでは、クラスベースのパッキングをしてみる。

まず、フォーマット文字列として、データがネットワークバイトオーダで4オクテットであることを指定してオブジェクトをインスタンス化するには以下のように、ネットワークバイトオーダを示す"!"と4オクテットであることを示す"i"をコンストラクタの引数に与える。フォーマット文字列の詳細はpythonのマニュアルで確認することができる。

>>> import struct
>>> s = struct.Struct("!i")
>>> s
<Struct object at 0x7f7e09518880>

例えばバイナリデータとして、"0x04 0x03 0x02 0x01"で構成される4オクテットのデータを作り、上記のフォーマット文字列でパッキングするには以下のようにする。

>>> data = (0x04 << 24) + (0x03 << 16) + (0x02 << 8) + 0x01
>>> packed_data = s.pack(data)
>>> packed_data
b'\x04\x03\x02\x01'

上記の例は、フォーマット文字列として、ネットワークバイトオーダで1オクテットを4つ並べ、以下のようにもできる。

>>> s = struct.Struct("!BBBB")
>>> data = [0x04, 0x03, 0x02, 0x01]
>>> packed_data = s.pack(*data)
>>> packed_data
b'\x04\x03\x02\x01'

パックしたデータををUDPで送信するには、socketモジュールのsendtoの引数にパックしたデータを渡せば良い。

パックしたデータをアンパックするときもパックするときと同様に、まずフォーマット文字列を指定してオブジェクトをインスタンス化し、パックされたデータをunpackメソッドの引数に渡せば良い。

>>> s1 = struct.Struct("!BBBB")
>>> packed_data = s1.pack(*data)
>>> s1.unpack(packed_data)
(4, 3, 2, 1)

GTPv2のEcho RequestメッセージをUDPで送信

それでは、試しにGTPv2のEcho Requestメッセージをバイナリデータとして構成し、パッキングし、UDPで送信してみる。 用意したプログラムは下記の2つ。 * gtpv2c.py - GTPv2のメッセージは、ヘッダと情報要素(Information Element, IE)で構成される。本ファイルでは、これらのデータを定義する。 - 本ファイルでは、パッキングするフォーマット文字列とバイナリデータの構築のみを行う。 * send_gtpv2c.py - 本ファイルにおいて、gtpv2c.pyを読み込み、そこで定義されたEcho RequestのヘッダとIEを組み立て、パッキングし、UDPでデータ送信する。

gtpv2c.py

#!/usr/bin/env python3

PORT = 2123

ECHO_REQUEST = 1
# ...

RECOVERY_TYPE = 3
# ...

def header(msg_type, msg_len, teid=None, seq_no=None):
    version = 2
    p = 0 # piggyback off
    formatter = "!i"

    octets = []
    octets1_4 = 0
    octets1_4 += version << 29
    octets1_4 += p << 28
    if teid != None:
        octets1_4 += 1 << 27
    octets1_4 += msg_type << 16
    octets1_4 += msg_len
    octets.append(octets1_4)

    if teid != None:
        formatter += "i"
        octets.append(teid)

    if seq_no != None:
        formatter += "i"
        octets.append(seq_no << 8)

    return [ formatter, octets ]


def recovery_ie(ins_id, recovery_val):
    ie_len = 5
    formatter = 'ib'
    octets = []
    octets1_4 = 0
    octets1_4 += RECOVERY_TYPE << 24
    octets1_4 += 1 << 8
    octets1_4 += ins_id
    octets.append(octets1_4)
    octets.append(recovery_val)

    return [ ie_len, formatter, octets ]

send_gtpv2c.py

#!/usr/bin/env python3

import socket
import time
from contextlib import closing

import gtpv2c
import struct

if __name__ == '__main__':
    host = '127.0.0.1'
    port = gtpv2c.PORT
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    with closing(sock):
        ie_len, ie_f, ie_msg = gtpv2c.recovery_ie(0, 100)
        hdr_f, hdr_msg = gtpv2c.header(gtpv2c.ECHO_REQUEST, ie_len, None, 1234)

        s = struct.Struct(hdr_f + ie_f)
        msg = hdr_msg + ie_msg
        packed_data = s.pack(*msg)
        sock.sendto(packed_data, (host, port))

サンプルプログラムの実行

以下のようにコマンドを実行し、ローカルホストにgtpv2 echoメッセージを送信し、

$ chmod +x send_gtpv2c.py
$ ./send_gtpv2c.py

loデバイスをwiresharkでみてみると、うまくecho requestメッセージが送信できている:) f:id:kj_xxx:20160114014605j:plain