無線通信エンジニアの備忘録

無線通信だったり、ITだったり、仕事で覚えた専門知識の備忘録

【Python】commpyとFuncAnimationでQPSK変調のIQ信号波形を動画で描いてみる

今回はPythonのFuncAnimationを使って↓な動画を作成します。
f:id:taekwongineer:20210619231754g:plain

QPSK変調信号のレイズドコサインフィルタでの帯域制限前後のIch、Qchの信号の時間波形とIch、Qchの散布図です。

QPSK変調信号を作成するのに便利なcommpyとMatplotlibのグラフで動画を作成するFuncAnimationの使い方を備忘録としてまとめます。

●この記事の目次

1. まずはソースコード

いきなりですがソースコードはこちら

import numpy as np
import commpy as cp
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation as fa

######## ①QPSK変調信号生成 ########
#### 変数定義 ####
N = 10000                   # 送信ビット数
k = 8                       # アップサンプリングの倍率
SymbolRate = 1000           # シンボルレート[Hz]
SampleRate = k * SymbolRate # サンプリングレート[Hz]
Tap = k * 4                 # レイズドコサインフィルタのタップ数
Alpha = 0.5                 # ロールオフ率

#### 送信ビットを生成 ####
TxData = np.random.randint(0, 2, N)

#### QPSKのシンボルマッピング ####
# 00 : (+1+i)/√2
# 01 : (+1-i)/√2
# 10 : (-1+i)/√2
# 11 : (-i-i)/√2
QpskSym = np.array([])    # QPSK変調のシンボルを格納する配列
tmp = 0  # QPSK変調のシンボルの計算結果を格納する一時変数
for i in range(0, N, 2):
    
    if TxData[i] == 0:
        tmp = 1
    else:
        tmp = -1
        
    if TxData[i+1] == 0:
        tmp = tmp + 1j
    else:
        tmp = tmp - 1j
    
    tmp = tmp / np.sqrt(2)
    
    QpskSym = np.append(QpskSym, tmp)

#### アップサンプリング ####
UpSamp = cp.utilities.upsample(QpskSym, k)

#### レイズドコサインフィルタによる帯域制限 ####
# レイズドコサインフィルタを作成
t, Filter = cp.filters.rcosfilter(Tap, Alpha, 1/SymbolRate, SampleRate)

# アップサンプリングされた信号とレイズドコサインフィルタを畳み込み積分
FilteredSamp = np.convolve(UpSamp, Filter)


######## ②QPSK変調のIQ信号を可視化 ########
#### 変数定義 ####
L = Tap * 2                 # グラフに表示する横軸のサンプル数
n = 0                       # 現在のサンプル

# グラフ表示用の配列の作成
IchPlotIn = np.zeros(0)     # Ichのレイズドコサインフィルタ入力
QchPlotIn = np.zeros(0)     # Qchのレイズドコサインフィルタ入力
IchPlotOut = np.zeros(0)    # Ichのレイズドコサインフィルタ出力
QchPlotOut = np.zeros(0)    # Qchのレイズドコサインフィルタ出力
TimePlot = np.zeros(0)      # 時刻

Ts = np.arange(0,len(FilteredSamp)) / SampleRate * 1000 # 時刻[ms]

# グラフ描画エリアの作成
fig = plt.figure(figsize=[12, 8])
plt.subplots_adjust(wspace=0.3, hspace=0.8)

#### FuncAnimation関数に入力する表示更新処理用関数 ####
def update(f):
    global IchPlotIn, QchPlotIn, IchPlotOut, QchPlotOut, TimePlot, n, FilteredSamp
    
    # グラフの表示内容をクリア
    plt.clf()
    
    # グラフの描画エリアを定義
    ax_IchIn = fig.add_subplot(421)
    ax_IchIn.set_ylim(-1.5, 1.5)
    ax_IchIn.set_title("Ich Signal(Input)", fontsize=10)
    ax_IchIn.set_xlabel("Time[ms]", fontsize=8)
    ax_IchIn.set_ylabel("Level", fontsize=8)
    
    ax_IchOut = fig.add_subplot(423)
    ax_IchOut.set_ylim(-1.5, 1.5)
    ax_IchOut.set_title("Ich Signal(Output)", fontsize=10)
    ax_IchOut.set_xlabel("Time[ms]", fontsize=8)
    ax_IchOut.set_ylabel("Level", fontsize=8)
    
    ax_QchIn = fig.add_subplot(425)
    ax_QchIn.set_ylim(-1.5, 1.5)
    ax_QchIn.set_title("Qch Signal(Input)", fontsize=10)
    ax_QchIn.set_xlabel("Time[ms]", fontsize=8)
    ax_QchIn.set_ylabel("Level", fontsize=8)
    
    ax_QchOut = fig.add_subplot(427)
    ax_QchOut.set_ylim(-1.5, 1.5)
    ax_QchOut.set_title("Qch Signal(Output)", fontsize=10)
    ax_QchOut.set_xlabel("Time[ms]", fontsize=8)
    ax_QchOut.set_ylabel("Level", fontsize=8)
    
    ax_IQ = fig.add_subplot(122, aspect=1) 
    ax_IQ.set_xlim(-1.5, 1.5)
    ax_IQ.set_ylim(-1.5, 1.5)
    ax_IQ.set_title("IQ Trajectory", fontsize=10)
    ax_IQ.set_xlabel("Ich Level", fontsize=8)
    ax_IQ.set_ylabel("Qch Level", fontsize=8)
    
    # グラフ表示用の配列の要素数に応じて処理を分岐
    if len(IchPlotIn) < L:
        # 要素数がLに満たない場合は、X軸の表示範囲は固定
        ax_IchIn.set_xlim(0, Ts[L-1])
        ax_IchOut.set_xlim(0, Ts[L-1])
        ax_QchIn.set_xlim(0, Ts[L-1])
        ax_QchOut.set_xlim(0, Ts[L-1])
        
        # 要素数がLに到達するまで、配列に要素を追加
        IchPlotIn = np.append(IchPlotIn, UpSamp[n].real)
        IchPlotOut = np.append(IchPlotOut, FilteredSamp[n].real)
        QchPlotIn = np.append(QchPlotIn, UpSamp[n].imag)
        QchPlotOut = np.append(QchPlotOut, FilteredSamp[n].imag)
        
        TimePlot = np.append(TimePlot, Ts[n])
        
    else:
        # 配列の要素数がLに到達した後は、先頭の要素を削除後に、次の要素を追加
        IchPlotIn = IchPlotIn[1:L]
        IchPlotIn = np.append(IchPlotIn, UpSamp[n].real)
        
        IchPlotOut = IchPlotOut[1:L]
        IchPlotOut = np.append(IchPlotOut, FilteredSamp[n].real)
        
        QchPlotIn = QchPlotIn[1:L]
        QchPlotIn = np.append(QchPlotIn, UpSamp[n].imag)
        
        QchPlotOut = QchPlotOut[1:L]
        QchPlotOut = np.append(QchPlotOut, FilteredSamp[n].imag)
        
        TimePlot = TimePlot[1:L]
        TimePlot = np.append(TimePlot, Ts[n])
        
    ax_IchIn.plot(TimePlot, IchPlotIn, "-xb")
    ax_IchIn.grid(True)
    
    ax_IchOut.plot(TimePlot, IchPlotOut, "-xm")
    ax_IchOut.grid(True)
    
    ax_QchIn.plot(TimePlot, QchPlotIn, "-xb")
    ax_QchIn.grid(True)
    
    ax_QchOut.plot(TimePlot, QchPlotOut, "-xm")
    ax_QchOut.grid(True)
    
    ax_IQ.plot(IchPlotOut, QchPlotOut)
    ax_IQ.plot(np.array([1,-1,1,-1])/np.sqrt(2), np.array([1,1,-1,-1])/np.sqrt(2), "ro")
    ax_IQ.plot(FilteredSamp[n].real, FilteredSamp[n].imag, "xk")
    ax_IQ.grid(True)
    
    # 参照する配列のインデックスを更新
    n = n + 1

#### FuncAnimationによるアニメーション描画の実行 ####
ani = fa(fig, update, interval=10, frames=500)
#ani.save("output.gif", writer="imagemagick")
plt.show()

2. commpyを活用した信号処理

commpyはMATLABのCommunications Toolboxのような機能を持ったパッケージで、

・変復/復調
・誤り訂正符号化/誤り訂正
フェージング生成
・フィルタ生成

などなど、ディジタル無線通信のシミュレーションで多用する各種機能の関数群が含まれています。

commpy.readthedocs.io


MATLABもCommunications Toolboxも個人のお小遣いで買えるような代物ではありませんからね、このようなパッケージがあるのは非常にありがたいことです。

今回はこれらの中から
・commpy.utilities.upsample :アップサンプリング
・commpy.filters.rcosfilter  :レイズドコサインフィルター

の機能を使用します。

2.1 commpy.utilities.upsample

upsampleは、その名の通りアップサンプリングを行う関数です。

使い方は

Output = commpy.utilities.upsample( Input, N)

・引数   Input (1-D ndarray(float)) : 入力サンプル
        N (int)  : アップサンプリングの倍率
・戻り値  Output (1-D ndarray(float)) : アップサンプリング後のデータ列

となります。

例えば、Input=[1, -1, 1]の配列を、N=4でアップサンプリングすると、

Output = [1, 0, 0, 0, -1, 0, 0, 0, 1, 0, 0, 0]

のようになります。

今回のサンプルのコードでは、42行目で
UpSamp = cp.utilities.upsample(QpskSym, k)
の形で使用しています。

2.2 commpy.filters.rcosfilter

rcosfilterはレイズドコサインフィルターを生成する関数です。

使い方は

time_idx, rc_filter = commpy.filters.rcosfilter(N, Alpha, Ts, Fs)

・引数    N(int) :生成するレイズドコサインフィルターのタップ数
    Alpha(float)  :ロールオフ率(0.0~1.0)
      Ts(float) : 1シンボル時間[s](シンボルレート[Hz]の逆数)
      Fs(float) : サンプリングレート[Hz]
・戻り値  time_idx(1-D ndarray(float)) : レイズドコサインフィルタの時刻インデックス
      rc_filter(1-D ndarray(float)) : レイズドコサインフィルタのインパルス応答

となります。

例えば
・N = 48
・Alpha = 0.5
・Ts = 0.001(=1/1000)
・Fs = 8000
とすると、出力されるレイズドコサインフィルタのインパルス応答は
f:id:taekwongineer:20210620221041p:plain
のようになります。

グラフの横軸は、戻り値のtime_idxを使用しています。

今回のサンプルのコードでは、46行目で
t, Filter = cp.filters.rcosfilter(Tap, Alpha, 1/SymbolRate, SampleRate)
の形で使用しています。

3. FuncAnimationによる動画の作成

次に、2.で作成したQPSK変調信号を動画として描画する方法について説明します。

今回は、matplotlib.animation.FuncAnimatitonを使用し、動画を作成します。

3.1 動画の作成方法

ここでは、FuncAnimationの簡単な使用方法のみ示します。

サンプルコードでは、

ani = fa(fig, update, interval=10, frames=500)
・引数: fig(figure) : 動画の描画対象となるグラフエリアのfigureオブジェクト
     update(function) : グラフの描画の更新処理を行う関数
     interval : 描画の更新周期(ms)(指定がない場合のデフォルト値は200ms)
     frames : 動画のフレーム数(指定がない場合は無限)

の形式で使用しています。

他にもいろいろ引数で条件は指定できるのですが、詳細は↓を参照してください。
matplotlib.org

3.2 動画の保存方法

動画を保存する場合は、

FuncAnimation.save(Filename, writer)
・引数: Filename(string) : 保存ファイル名(gif)
     writer(string) : gifファイルを作成するwriter

と記述します。

今回のサンプルコードでは、wriiterにimagemagickを使用していますが、デフォルトではインストールされていないので、別途インストールおよび設定が必要となります。
imagemagickのインストールと設定に関しては、↓のサイトに分かりやすく説明が書かれています。
imagingsolution.net

4. まとめ

今回はcommpyとFuncAnimationを使用して、QPSK変調波形を動画で描く方法についてまとめました。
commpyについては、誤り訂正符号化/誤り訂正の使い方を抑えておくと、誤り訂正付きのBERのシミュレーションを作成するのがとても楽になりそうなので、使い方を調査していきたいと思います。

【C#】UDPのソケット通信を同期/非同期で行う

約1年振りの投稿となってしまいました(;^_^A


仕事で時々ソケット通信を行う試験用のプログラムを作ることがあるのですが、これまでずっと同期方式でプログラムを作成してきました。


なぜかって?


非同期方式が難しくてよく理解できなかったからです・・・(´;ω;`)ウッ…


今回、非同期方式をあれこれ試行錯誤しながら勉強し、単純な仕組みのプログラムであれば扱えるようになったので、備忘録として残そうと思います。


●この記事の目次

1.今回作成するプログラム

今回は、Visual Studioを使用し、図1-1に示すようなシンプルなUDP通信のチャットアプリを例に、同期受信/非同期受信それぞれのケースでプログラムを作成します。送信側は、非同期方式にするメリットを今のところ感じじられないので、同期方式にしています。


f:id:taekwongineer:20210326211025p:plain
図1-1 今回作成するチャットアプリ


2.同期受信方式

まず同期受信方式から説明します。

図1-1で示したGUIの各パーツと、プログラムのソース上の変数名の対応を図2-1に示します。


f:id:taekwongineer:20210326213347p:plain
図2-1 GUI部品と変数名の対応

そして、以下にVusialStudioで自動生成されるForm1.csに、今回の同期受信方式のチャットアプリの処理を追記したコードを示します。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

// 追加定義分
using System.Net;
using System.Net.Sockets;
using System.Threading;


namespace UdpSocketTest
{
    public partial class Form1 : Form
    {
        //////// 変数の定義 ////////
        // 送信処理系
        public UdpClient txClient;          // 送信用ソケット
        public byte[] txData;               // 送信データ格納用配列

        // 受信処理系
        public UdpClient rxClient;          // 受信用ソケット
        public IPEndPoint remoteEP;         // RxClientのReceiveメソッドの引き数
        public byte[] rxData;               // 受信データ格納用配列
        public Thread thread;               // 受信処理用メソッド実行用
        public Boolean flag = false;        // 受信ループ制御用フラグ
        public string strRx = "受信";       // ボタン表示用
        public string strStop = "停止";     // ボタン表示用


        // その他
        public delegate void SetTextDelegate(string text);
        
        public Form1()
        {
            InitializeComponent();

            // 送信用のソケットを初期化
            txClient = new UdpClient();

        }

        // 「送信」ボタンクリック時の処理
        private void Button_TxCont_Click(object sender, EventArgs e)
        {
            // テキストボックスから文字列を取得し、バイト配列に変換
            txData = Encoding.UTF8.GetBytes(textBox_TxMsg.Text);

            // データを送信
            txClient.Send(txData, txData.Length, textBox_TxAddr.Text, int.Parse(textBox_TxPort.Text));

            // テキストボックスに表示
            richTextBox.AppendText("[送信]" + textBox_TxMsg.Text + "\n");
        }

        //「受信/停止」ボタンクリック時の処理
        private void Button_RxCont_Click(object sender, EventArgs e)
        {
            if(button_RxCont.Text == strRx)
            {
                // 受信処理メソッドをマルチスレッドで実行
                flag = true;
                thread = new Thread(new ThreadStart(Receive));
                thread.Start();


                // ボタン表示を変更
                button_RxCont.Text = strStop;

            }else if(button_RxCont.Text == strStop)
            {
                // ソケットを切断
                rxClient.Close();

                // 受信ループフラグを無効化
                flag = false;              

                // ボタン表示を変更
                button_RxCont.Text = strRx;
            }
        }

        // 受信処理用メソッド
        public void Receive()
        {
            // 受信用ソケットを初期化
            rxClient = new UdpClient(int.Parse(textBox_RxPort.Text));

            // 受信データの送信元情報が格納されるremoteEPはnullにしておく。
            remoteEP = null;

            while (flag)
            {
                try
                {
                    // 受信用ソケットからデータを読込
                    rxData = rxClient.Receive(ref remoteEP);
                    Console.WriteLine(remoteEP.Address + "," + remoteEP.Port);
                    // 受信データをテキストボックスに表示
                    Invoke(new SetTextDelegate(SetText), "[受信]" + Encoding.UTF8.GetString(rxData) + "\n");

                }catch(SocketException se)
                {
                    // Receiveによるブロック中にcloseを実行すると、
                    // SocketExceptionがスローされる。
                    // 何もしない
                    
                }catch(ObjectDisposedException obe)
                {
                    // close実行後にReceiveを実行した場合にスローされる。
                    break;
                }
            }

        }

        // サブスレッドからのテキストボックスへの書込み処理用メソッド
        public void SetText(string text)
        {
            // テキストボックスに引数の文字列を追記
            richTextBox.AppendText(text);

            // カーソルを最後尾に移動
            richTextBox.Select(richTextBox.TextLength, 0);
            richTextBox.ScrollToCaret();
        }

        // 「×」ボタンクリック時の処理
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            // ソケットを切断
            if(rxClient != null)
            {
                rxClient.Close();
            }
        }
    }
}

UDPデータの受信処理は、89行目からのReceive()メソッドの中で実施しています。
そして102行目の「rxData = rxClient.Receive(ref remoteEP);」を実行すると、92行目でバインドしたポート番号のUDPデータを受信するまで、プログラムの動作がブロックされた状態となります。

Receiveメソッドをメインスレッドで実行すると、ブロック時に何も操作ができなくなってしまうため、68~69行目に示すように、Threadクラスのオブジェクトを作成し、サブスレッドでReceive()メソッドを実行するようにします。

「rxData = rxClient.Receive(ref remoteEP);」によるデータ受信待ちのブロック状態を途中で解除するためには、78行目、139行目に示すように、UdpClientのオブジェクト(rxClient)に対してClose()メソッドを実行します。

すると、「rxData = rxClient.Receive(ref remoteEP);」はSocketExceptionの例外をスローするので、プログラムが異常終了しないように、try~catchで例外をキャッチできるようにしておきます。

また、タイミングによっては、Close()メソッドが実行されたrxClientに対してReceive()メソッドが実行され、ObjectDisposedExceptionの例外がスローされる可能性もあるので、SocketExeptionと併せてキャッチできるようにしておきます。

3.非同期受信方式

続いて非同期受信方式です。

GUIの各パーツの変数名は、同期方式と同じなので、ここではソースコードのみ記載します。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

// 追加定義分
using System.Net;
using System.Net.Sockets;


namespace UdpSocketTest
{
    public partial class Form1 : Form
    {
        //////// 変数の定義 ////////
        // 送信処理系
        public UdpClient txClient;          // 送信用ソケット
        public byte[] txData;               // 送信データ格納用配列

        // 受信処理系
        public UdpClient rxClient;          // 受信用ソケット
        public IPEndPoint remoteEP;         // RxClientのReceiveメソッドの引き数
        public byte[] rxData;               // 受信データ格納用配列
        public string strRx = "受信";       // ボタン表示用
        public string strStop = "停止";     // ボタン表示用


        // その他
        public delegate void SetTextDelegate(string text);
        
        public Form1()
        {
            InitializeComponent();

            // 送信用ソケットを初期化
            txClient = new UdpClient();
        }

        // 「送信」ボタンクリック時の処理
        private void Button_TxCont_Click(object sender, EventArgs e)
        {
            // テキストボックスから文字列を取得し、バイト配列に変換
            txData = Encoding.UTF8.GetBytes(textBox_TxMsg.Text);

            // データを送信
            txClient.Send(txData, txData.Length, textBox_TxAddr.Text, int.Parse(textBox_TxPort.Text));

            // テキストボックスに表示
            richTextBox.AppendText("[送信]" + textBox_TxMsg.Text + "\n");
        }

        //「受信/停止」ボタンクリック時の処理
        private void Button_RxCont_Click(object sender, EventArgs e)
        {
            if(button_RxCont.Text == strRx)
            {

                // 受信用ソケットを初期化
                rxClient = new UdpClient(int.Parse(textBox_RxPort.Text));
                rxClient.BeginReceive(ReceiveCallback, null);

                // ボタン表示を変更
                button_RxCont.Text = strStop;

            }else if(button_RxCont.Text == strStop)
            {

                // ソケットを切断
                rxClient.Close();

                // ボタン表示を変更
                button_RxCont.Text = strRx;
            }
        }

        // 受信処理用メソッド
        void ReceiveCallback(IAsyncResult ar)
        {
            // 受信データの送信元情報が格納されるremoteEPはnullにしておく。
            remoteEP = null;

            try
            {
                // 受信したデータを読込
                rxData = rxClient.EndReceive(ar, ref remoteEP);

                // 受信データをテキストボックスに表示
                Invoke(new SetTextDelegate(SetText), "[受信]" + Encoding.UTF8.GetString(rxData) + "\n");

                // 再び受信開始
                rxClient.BeginReceive(ReceiveCallback, null);
            }
            catch (ObjectDisposedException ode)
            {
                // BeginReceiveの実施中にソケットをCloseするとスローされる例外
                // 何もしない
            }

        }

        // サブスレッドからのテキストボックスへの書込み処理用メソッド
        public void SetText(string text)
        {
            // テキストボックスに引数の文字列を追記
            richTextBox.AppendText(text);

            // カーソルを最後尾に移動
            richTextBox.Select(richTextBox.TextLength, 0);
            richTextBox.ScrollToCaret();
        }


        // 「×」ボタンクリック時の処理
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            // ソケットを切断
            if(rxClient != null)
            {
                rxClient.Close();
            }
        }
    }
}

同期方式との差分を簡単に説明します。

まず非同期方式では、UDPデータの受信を実行する際に、rxClientに対し、Receive()メソッドではなく、BeginReceive()というメソッドを使用します。(65行目)

BeginReceive()は実行しても以降の処理のブロックが発生しません。

代わりに非同期方式では、BeginReceive()実行後に、バインドしたポート番号のUDPデータを受信すると、BeginReceive()の第1引数に指定したコールバックメソッド(ReceiveCallback)が呼び出されます。(82行目)

第2引数は、任意の型のデータを指定することができ、ReceiveCallback()の引数(IAsyncResult ar)の中に渡されるようですが、特に今回は何も渡すものがないので、nullとしています。

コールバックメソッドの引数はIAsyncResult arとするのがお約束のようです。


ReceiveCallback()の中では、rxClientに対し、EndReceive()を実行することで、受信したUDPデータを読み込みます。

BeginReceive()とEndReceive()はセットで使うと覚えておくとよさそうです(;^_^A

その後、ReceiveCallback()の中で、再びrxClientに対して、BeginReceive()を実行することで、次のデータの受信を待受けます。(96行目)


BeginReceive()を実行後に、UDPデータの待受けを中断する場合は、同期方式と同様、rxClientに対し、Close()を実行します。

すると、非同期方式の場合は、ObjectDisposedExceptionの例外がスローされるので、try~catchでプログラムが異常終了しないように例外をキャッチするようにします。

4.まとめ

UDPのソケット通信におけるデータの受信方法について、同期方式と非同期方式の場合のコード例をそれぞれ作成しました。

非同期方式のコードは初めて書きましたが、同期方式と比べると、マルチスレッドにしなくて良いくらいで、大した差はないかなという印象です。
(今回の例に挙げたチャットアプリが単純なためかもしれませんが・・・)

とは言っても多少は非同期方式の方がコードがシンプルになるので、今後は非同期方式でコードを書けるようにしておきたいと思います(;^_^A

なお、今回はUDPのソケット通信を取り上げましたが、TCPのソケット通信でも考え方は全く同じようです。

Pythonでディジタル変復調のBER(Bit Error Rate)計算をシミュレーションしてみる

こんにちは、ナベゾーです。

今回はPythonでディジタル変調の方式の1つであるBPSKによる変復調とビット誤り率(BER:Bit Error Rate)の計算のシミュレーションを行い、理論値との比較をグラフに描いてみたいと思います。

せっかくPythonの勉強をして資格まで取ったので、PythonのパッケージであるNumpyの基本的な使い方をディジタル変復調のBER計算のシミュレーションを通して抑えようというのが狙いです。

●この記事の目次

1.この記事で説明しないこと

Numpy、Matplotlibの使い方を説明することが目的であるため、BPSK、BERといったディジタル無線通信関連の専門用語の内容についてはこの記事では説明しません。

2.BPSK変調のBER測定シミュレーションのサンプルコード

ではさっそくPythonによるBPSK変調のBER測定シミュレーションのサンプルコードを以下に示します。

#パッケージのインポート
import numpy as np
import numpy.random as r
import matplotlib.pyplot as plt
import math as m

# データサンプル数
N = 10 ** 7

# BERの計算結果の格納用リストを定義(初期値は空でappendで追加していく)
BER_List = [] # シミュレーション値
BER_Theory_List = [] # 理論値

#(1)送信データの生成(0,1のランダムデータ)
Tx_Data = r.randint(0, 2, N)

#(2)SN比[dB]のリスト
SN_dB_List = np.arange(0, 12.5, 0.5)

# SN比[dB]の値を変化させながら、BERの値を計算
for SN_dB in SN_dB_List:
    
    #(3)雑音信号の生成
    Pn = 10 ** (-SN_dB / 10) # 雑音電力
    Noise_Sig = m.sqrt(Pn/2) * (r.randn(N) + 1j * r.randn(N))
    
    #BPSK変調信号(送信信号)の作成
    BPSK_Mod_Sig = np.where(Tx_Data == 1, Tx_Data, -1)
    
    # 受信信号(送信信号に雑音信号を加算)を作成
    Rx_Sig = BPSK_Mod_Sig + Noise_Sig
    
    #(4)BPSK変調信号の復調処理
    Rx_Data = np.where(Rx_Sig.real >= 0, 1, 0)
    
    #ビット誤り数を計算
    BER = sum(abs(Tx_Data - Rx_Data)) / N
    BER_List.append(BER)
    
    #BERの理論値を計算
    BER_Theory = 1 / 2 * m.erfc(m.sqrt(10**(SN_dB / 10)))
    BER_Theory_List.append(BER_Theory)

#BERの計算結果をグラフに描画
plt.plot(SN_dB_List, BER_List, "ro", label="Simulation")    # シミュレーション値のグラフの描画
plt.plot(SN_dB_List, BER_Theory_List, label="Theory")    # 理論値のグラフの描画
plt.yscale("log")    # Y軸を対数表示に設定
plt.xlabel("SN[dB]")    # X軸のラベルを"SN[dB]"に設定
plt.ylabel("BER")    # Y軸のラベルを"BER"に設定
plt.grid(True)    # 目盛線の表示を有効化
plt.minorticks_on()    # 補助目盛の表示を有効化
plt.legend()    # 凡例の表示を有効化
plt.show()    # グラフを画面に表示

BERのグラフ表示は以下のようになります。



図1-1 SN[dB] 対 BERグラフ

3.ディジタル変復調のシミュレーションで覚えておきたい関数

(1)numpy.random.randint()

0,1のランダムデータ列の生成には、numpy.random.randint()関数を使用します。

numpy.random.randint(X, Y, N)

の形式で、X~Y-1の範囲のランダム整数を含む1行N列のデータ列を生成します。

0,1のランダムデータを生成する場合は、

numpy.random.randint(0, 2, N)

とします。(2は含まれない)

サンプルコードでは記載していませんが、M行N列のランダムデータ列を生成する場合は、

numpy.random.randint(0, 2, [M, N])

とします。

(2)numpy.arange()

SN比を段階的に変化させながらfor文を回す際には、numpy.arange()で等差数列を生成します。

numpy.arrange(X, Y, Z)

の形式で、

XからZ刻みで最終項がY未満の値となる等差数列を生成します。

上記のサンプルコードでは、

numpy.arrange(0, 12.5, 0.5)

の形式で、0から0.5刻みで最終項が12.0となる等差数列を生成しています。

(3)numpy.random.randn()

白色雑音のデータ列の生成には、numpy.random.randn()関数を使用します。

numpy.random.randn(N)

の形式で、1行N列の標準正規分布の乱数データ列を生成します。

(4)numpy.where()

numpy.where()は、Numpy配列の各要素に対し、条件に応じた処理を行います。

numpy.where(Numpy配列の各要素
に対する条件式, 判定が真の場合の処理, 判定が偽の場合の処理)

の形式で使用します。

上記のサンプルコードでは、

Rx_Data = np.where(Rx_Sig.real >= 0, 1, 0)

の形式で、受信信号の実部が0以上であれば1、0未満であれば0という復調結果のデータ列を生成します。

4.まとめ

Pythonを使って、ディジタル変復調の中で最も基本とも言えるBPSKのBER測定のシミューレーションを作成しました。

・0,1のランダムデータ列生成
・標準正規分布の乱数データ列生成
・データ列の各要素に対し、条件に応じた処理

はディジタル変復調のシミュレーションでは必須とも言える内容なので、忘れずに覚えておきたいと思います。
(忘れた場合はここを開いて思い出す!)

【Wireshark】分割された独自プロトコルメッセージを再組立てして解析

前回、TCPの特徴として、1つのIPパケット内に複数メッセージが含まれる場合の独自プロトコル解析についてスクリプトの作成方法について紹介しました。

taekwongineer.hatenablog.jp

今回は、1つの独自プロトコルメッセージが複数のIPパケットに跨って、分割配信された場合の解析方法について紹介します。

ちなみに下記サイトでは、分割パケットの再組立てができないのであれば、TCPの解析プラグインは書くなと言っています・・・

TCP reassembly
You should not write a dissector for TCP payload if you cannot handle reassembly (i.e., don't add your Proto object to the DissectorTable for tcp).

Lua/Dissectors - The Wireshark Wiki

この記事の目次

1.解析対象の独自プロトコル

今回も前回の記事と同様、以下の図1-1のメッセージフォーマットのプロトコルを例に、解析プラグインを作成していきます。


f:id:taekwongineer:20200419163817p:plain
図1-1 解析対象プロトコルのメッセージフォーマット

2.解析プラグインのサンプルコード

前回の記事のサンプルコードに、分割されたメッセージの再組立て処理を行うコードを追記します。
★を付記したコメント部分が追記箇所となります。

--独自プロトコルの定義
SampleProto = Proto.new("sample","SampleProtocol")

--プロトコルフィールドの定義
f_MsgSize = ProtoField.new("MessageSize","sample.size",ftypes.UINT16)
f_Msg = ProtoField.new("Message","sample.msg",ftypes.STRING)

--定義したプロトコルフィールドをプロトコルフィールド配列に登録
SampleProto.fields = {f_MsgSize,f_Msg}

-- dissector関数の定義
function SampleProto.dissector(buffer, pinfo, tree)

    -- dissector関数内で使用するローカル変数の定義
    local MsgNum = 0    -- 1パケット内に含まれるSampleProtocolのメッセージ数
    local BufLeft = buffer:len()    -- buffer内の未読込データの残りバイト数
    local BufPos = 0    -- bufferのデータ読込開始位置
    local r_BufRange    -- サンプルプロトコルメッセージのデータ範囲
    local r_MsgSize    -- MessageSizeのデータ範囲
    local r_Msg    -- Messageのデータ範囲
    
    -- BufLeftが3以上のとき、解析処理を継続
    while BufLeft >= 3 do
        ---- bufferからデータを読込み ----
        -- MessageSizeを読込み
        r_MsgSize = buffer(BufPos, 2)

        -- buffer内の未読込データ数を更新
        BufLeft = BufLeft - 2
        
        -- ★★★★★ 追記ここから ★★★★★
        -- Message Sizeとbuffer内の残データ数を比較
        if r_MsgSize:uint() > BufLeft then

            -- buffer内の残データ数よりもMessage Sizeが大きい場合は、
            -- pinfo.desegment_lenに不足データ数をセットし、disecctor関数をreturn
            pinfo.desegment_len = r_MsgSize:uint() - BufLeft
            return buffer:len()
        end
        -- ★★★★★ 追記ここまで ★★★★★

        -- SampleProtocolメッセージ全体を読込み
        r_BufRange = buffer(BufPos, 2 + r_MsgSize:uint())

        -- bufferのデータ読込開始位置を更新
        BufPos = BufPos + 2

        -- Messageを読込み
        r_Msg = buffer(BufPos, r_MsgSize:uint())

        -- SampleProtocolメッセージ全体を読込み
        r_BufRange = buffer(BufPos, 2 + r_MsgSize:uint())

        ---- プロトコルツリーに情報を追加
        -- treeにサブツリーとして"SampleProtocol"を追加
        local subtree = tree:add(SampleProto, r_BufRange)

        -- subtreeに上記で定義した各プロトコルフィールドの値を追加
        subtree:add(f_MsgSize, r_MsgSize) -- MessageSize
        subtree:add(f_Msg, r_Msg, r_Msg:string(ENC_UTF_8), "Message:" .. r_Msg:string(ENC_UTF_8)) -- Msg

        -- subtreeのトップにMessageSizeとMessageの情報を追記する
       subtree:append_text(",MessasgeSize:" .. r_MsgSize:uint() .. ",Message:" .. r_Msg:string(ENC_UTF_8))

        ---- bufferのデータ読込開始位置を更新
        BufPos = BufPos + r_MsgSize:uint()

        ---- buffer内の未読込データ数を更新
        BufLeft = BufLeft - r_MsgSize:uint()

        -- SampleProtocolメッセージ数を更新
        MsgNum = MsgNum + 1
    end

    ---- プロトコル情報を設定
    -- "Protocol"列の表示
    pinfo.cols.protocol = "Sample"

    -- "info"列の表示(MsgNumの値に応じて分岐)
    if MsgNum == 1 then
    -- メッセージ数が1個の場合は、MessageSizeとMessageの値を表示
    pinfo.cols.info = "MessageSize:" .. r_MsgSize:uint() .. "Message:" .. r_Msg:string(ENC_UTF_8)
    else
    -- メッセージ数が2個以上の場合は、メッセージ数を表示
    pinfo.cols.info = "Thispacketcontains" .. MsgNum .. "messages."
    end
end

-- 定義したプロトコルをTCPポート番号を指定した紐づけ
tcp_table = DissectorTable.get("tcp.port")
tcp_table:add(20406, SampleProto)

追記部分の内容について解説します。

        -- ★★★★★ 追記ここから ★★★★★
        -- Message Sizeとbuffer内の残データ数を比較
        if r_MsgSize:uint() > BufLeft then

            -- buffer内の残データ数よりもMessage Sizeが大きい場合は、
            -- pinfo.desegment_lenに不足データ数をセットし、disecctor関数をreturn
            pinfo.desegment_len = r_MsgSize:uint() - BufLeft
            return buffer:len()
        end
        -- ★★★★★ 追記ここまで ★★★★★

Message Sizeに書かれているMessageのデータ数とbuffer内の残データ(未読込部分のデータ)数を比較し、Message Sizeの方が大きい場合は、Message部分のデータが複数IPパケットに分割されていると判断し、再組立ての処理を実行します。

やることはとても簡単で、
①pinfo.desegment_lenに不足データ数を設定
②bufferのサイズをdissector関数の戻り値として返す
の2つです。

上記の追記に関する情報の参照元は以下です。

TCP reassembly
You should not write a dissector for TCP payload if you cannot handle reassembly (i.e., don't add your Proto object to the DissectorTable for tcp). Like dissectors written in C, Lua dissectors can use Wireshark's ability to reassemble TCP streams:
You should make sure your dissector can handle the following conditions:
(1) The TCP packet segment might only have the first portion of your message.
(2) The TCP packet segment might contain multiple of your messages.
(3) The TCP packet might be in the middle of your message, because a previous segment was not captured. For example, if the capture started in the middle of a TCP session, then the first TCP segment will be given to your dissector function, but it may well be a second/third/etc. segment of your protocol's whole message, so appear to be malformed. Wireshark will keep trying your dissector for each subsequent segment as well, so that eventually you can find the beginning of a message format you understand.
(4) The TCP packet might be cut-off, because the user set Wireshark to limit the size of the packets being captured.
(5) Any combination of the above.


~中略~


For case (1), you have to dissect your message enough to figure out what the full length will be - if you can figure that out, then set the Pinfo's desegment_len to how many more bytes than are currently in the Tvb that you need in order to decode the full message;


~中略~


For the return value of your Proto's dissector() function, you should return one of the following:
If the packet does not belong to your dissector, return 0. You must
not set the Pinfo.desegment_len nor the desegment_offset if you return 0.
If you need more bytes, set the Pinfo's desegment_len/desegment_offset as described earlier, and return either nothing, or return the length of the Tvb. Either way is fine.
If you don't need more bytes, return either nothing, or return the length of the Tvb. Either way is fine.


Lua/Dissectors - The Wireshark Wiki


このサンプルコードを適用し、分割されたメッセージの解析結果を表示させると以下の図2-1のようになります。


f:id:taekwongineer:20200524120756p:plain
図2-1 分割されたサンプルプロトコルメッセージの再組立て後の解析結果表示

図2-1は、サンプルプロトコルメッセージが3つのIPパケットに分割された場合の再組立ての例です。

分割パケットが3つ揃った時点で初めてサンプルプロトコルとしての解析結果が表示されます。

3.まとめ

今回の記事では、複数のIPパケットに分割された独自プロトコルメッセージを再組立てして解析する方法について紹介しました。

wikiにも書かれていますが、TCPパケット上に独自プロトコルを定義して解析を実行する場合、

①1つのIPパケットに複数の独自プロトコルメッセージが含まれる
②1つの独自プロトコルメッセージが複数のIPパケットに跨って含まれる

という2つのケースを考慮してコードを書かないと、1つ目のメッセージしか解析されなかったり、解析の途中でエラーが発生してしまったりというケースが起こる可能性があるので注意が必要です。

Wiresharkで独自プロトコルのメッセージを統計解析し、結果をコンソールに表示する(その2)

前回の記事で、「Wireshark Developer’s Guide」の「Chapter 11. Wireshark’s Lua API Reference Manual」で規定されている

・Field
・FieldInfo
・TextWindow
・Listener

の4つのクラスの使用方法について、サンプルプロトコルのメッセージ内容を統計表示するコンソールアプリケーションの作成を例に紹介しました。
taekwongineer.hatenablog.jp

また、前回の記事のサンプルコードの手法では、1つのIPパケットに複数のサンプルプロトコルのメッセージが含まれていた場合に、先頭のメッセージしかカウントすることができないという欠点についても説明しました。

今回の記事では、前回の記事のサンプルコードに改良を加え、上記の欠点を解消する方法を紹介したいと思います。

この記事の目次

1.改良後のサンプルコード

では、早速改良後のサンプルコードを以下に示します。
前回の記事のサンプルコードからの変更箇所に★印を付けています。

-- ★(1)Fieldクラスのオブジェクトを定義
SampleMsg_f = Field.new("sample")

-- (2)サンプルプロトコルのメッセージ統計解析関数の定義
function MsgCount()

    -- メッセージの統計計算に使用する変数の定義
    local MsgCountTable = {}    -- 統計情報を格納するテーブル

    -- (3)解析結果を表示するコンソールの定義
    local tw = TextWindow.new("Message Counter")

    -- (4)リセットボタン追加
    tw:add_button("Reset", function()
        -- コンソールのテキストを削除
        tw:clear()
        -- メッセージ統計テーブルを初期化
        MsgCountTable = {}
    end)

    -- (5)Listenerオブジェクトを定義
    local listener = Listener.new(nil, "sample")

    -- (6)TextWindowのクローズ時の処理
    tw:set_atclose(function()
        -- Listenerを停止
        listener:remove()
    end)

    -- ★(7)サンプルプロトコルメッセージ受信時の解析処理の定義
    function listener.packet(pinfo, buffer)

        local finfo = SampleMsg_f()  -- サンプルプロトコルのFieldInfo
        local Offset = finfo.offset  -- サンプルプロトコルメッセージの開始位置
        local MsgSize    -- サンプルプロトコルのMessage Size
        local Msg    -- サンプルプロトコルのMessage
        local PktLen = buffer:len()    -- bufferのデータ長
        local Count

        while Offset <= (PktLen - 3) do
            -- MessageSizeを読込み
            MsgSize = buffer(Offset, 2):uint()
            -- Messageを読込み
            Msg = buffer(Offset + 2, MsgSize):string(ENC_UTF_8)
            -- サンプルプロトコルメッセージの読出し位置を更新
            Offset = Offset + 2 + MsgSize

            Count = MsgCountTable[Msg] or 0
            MsgCountTable[Msg] = Count + 1
        end
    end

    -- (8)メッセージ統計テーブルの内容をコンソールに書き込み
    function listener.draw()
        tw:clear()
        for msg, count in pairs(MsgCountTable) do
            tw:append(msg .. " : " .. count .. "\n")
        end
    end

    -- (9)キャプチャファイルの読込時に呼ばれる関数
    function listener.reset()
        tw:clear()
        MsgCountTable = {}
    end
end

-- (10)メニュー登録
register_menu("Test/Message Counter", MsgCount, MENU_TOOLS_UNSORTED)

以降、★を付けた変更箇所について内容を説明していきます。

(1)Fieldクラスのオブジェクトを定義

-- ★(1)Fieldクラスのオブジェクトを定義
SampleMsg_f = Field.new("sample")

前回の記事のサンプルコードでは、

SampleMsg_f = Field.new("sample.msg")

のように、Field.new()の引数にサンプルプロトコルのMessageフィールドを指定していましたが、今回はサンプルプロトコルメッセージ全体を指定します。

(7)サンプルプロトコルメッセージ受信時の解析処理の定義

    -- ★(7)サンプルプロトコルメッセージ受信時の解析処理の定義
    function listener.packet(pinfo, buffer)

        local finfo = SampleMsg_f()  -- サンプルプロトコルのFieldInfoの定義
        local Offset = finfo.offset  -- サンプルプロトコルメッセージの開始位置
        local MsgSize    -- サンプルプロトコルのMessage Size
        local Msg    -- サンプルプロトコルのMessage
        local PktLen = buffer:len()    -- bufferのデータ長
        local Count

        while Offset <= (PktLen - 3) do
            -- MessageSizeを読込み
            MsgSize = buffer(Offset, 2):uint()
            -- Messageを読込み
            Msg = buffer(Offset + 2, MsgSize):string(ENC_UTF_8)
            -- サンプルプロトコルメッセージの読出し位置を更新
            Offset = Offset + 2 + MsgSize

            Count = MsgCountTable[Msg] or 0
            MsgCountTable[Msg] = Count + 1
        end
    end

前回の記事のサンプルコードでは、

Msg = tostring(SampleMsg_f())

のように、SampleMsg_f()で取得したFieldInfoオブジェクトに対し、tostring()でMessageの値を取得していました。

一方で今回のサンプルコードでは、FieldInfoオブジェクトfinfoに対し、finfo.offsetで、buffer内のサンプルプロトコルメッセージの開始位置を取得しています。

その後は、サンプルプロトコルの解析プラグインの作成手順と同様、bufferからMessageSize、Messageの順にデータを読み出していき、Offsetがbufferの最後尾に到達するまでこれを繰り返していきます。

2.まとめ

今回の記事では、FieldInfoクラスのオブジェクトfinfoに対し、finfo.offsetによりbuffer内のサンプルプロトコルメッセージの開始位置を取得することで、1つのIPパケット内に複数のサンプルプロトコルメッセージが含まれている場合においても、Messageデータの統計を漏れなく実施する方法について紹介しました。

これまで4回に渡って、Wiresharkの独自プロトコル解析プラグインの作成方法について紹介してきましたが、Wiresharkに関する記事はここで一度一区切りつけようと思います。

また、何かこれは忘れずに覚えておきたい!という使い方が出てきたら記事にしようと思います。

Wiresharkで独自プロトコルのメッセージを統計解析し、結果をコンソールに表示する(その1)

ちょっと長ったらしいタイトルになってしまいました・・・(;^_^A

前回までに、Wiresharkで独自プロトコルのメッセージフォーマットを解析し、結果をGUIに反映するLuaプラグインの作成方法について紹介してきました。


taekwongineer.hatenablog.jp

taekwongineer.hatenablog.jp


今回は、「Wireshark Developer’s Guide」の「Chapter 11. Wireshark’s Lua API Reference Manual」で規定されている

・Field
・FieldInfo
・TextWindow
・Listener

の4つのクラスの使用方法について、タイトルのアプリケーションの作成を例に紹介したいと思います。


この記事の目次

1.今回作成するアプリケーションでできることの概要

今回の解析対象として扱う独自プロトコルは、前回の記事同様、図1-1のメッセージフォーマットを持つサンプルプロトコルを対象とします。


f:id:taekwongineer:20200419163817p:plain
図1-1 解析対象プロトコルのメッセージフォーマット

図1-1のメッセージのうち、Messageの値の内容を集計し、どの値をどれだけキャプチャしたかを図1-2のようにコンソール上に一覧で表示させます。


f:id:taekwongineer:20200425160904p:plain
図1-2 Messageの値の統計解析結果のコンソール表示

図1-2のコンソールアプリケーションの作成も、これまでに紹介した独自プロトコル解析プラグインと同様、Luaスクリプトプラグインで作成します。

2.アプリケーションの作成手順

まず、図1-2のコンソールアプリケーションのサンプルコードを以下に示します。

-- message_counter.lua
-- (1)Fieldクラスのオブジェクトを定義
SampleMsg_f = Field.new("sample.msg")

-- (2)GUIからコンソール呼び出し時に実行される関数の定義
function MsgCount()

    -- メッセージの統計計算に使用する変数の定義
    local MsgCountTable = {} -- 統計情報を格納するテーブル

    -- (3)解析結果を表示するTextWindowオブジェクトの定義
    local tw = TextWindow.new("Message Counter")

    -- (4)ボタン追加及びクリック時の動作の定義
    tw:add_button("Reset", function()
        -- コンソールのテキストを削除
        tw:clear()
        -- メッセージ統計テーブルの初期化
        MsgCountTable = {}
    end)

    -- (5)Listenerオブジェクトの定義
    local listener = Listener.new(nil, "sample")

    -- (6)コンソールウィンドウクローズ時の処理
    tw:set_atclose(function()
        -- Listenerを停止
        listener:remove()
    end)

    -- (7)サンプルプロトコルメッセージ受信時の解析処理の定義
    function listener.packet(pinfo, buffer)
        -- ローカル変数の定義
        local Msg    -- サンプルプロトコルのMessage
        local Count    -- Messageの数

        -- キャプチャしたサンプルプロトコルパケットのMessageフィールドの情報を取得
        Msg = tostring(SampleMsg_f())

        -- 取得したMessageを統計テーブルに追加
        Count = MsgCountTable[Msg] or 0
        MsgCountTable[Msg] = Count + 1
    end

    -- (8)コンソールウィンドウに統計結果を書き出し
    function listener.draw()
        tw:clear()
        for msg, count in pairs(MsgCountTable) do
             tw:append(msg .. " : " .. count .. "\n")
        end
    end

    -- (9)キャプチャファイルの読込時に呼ばれる関数
    function listener.reset()
        -- コンソールのテキストを削除
        tw:clear()
        -- メッセージ統計テーブルを初期化
        MsgCountTable = {}
    end
end

-- (10)メニュー登録
register_menu("Test/Message Counter", MsgCount, MENU_TOOLS_UNSORTED)

上記のサンプルコードは、「Wireshark Developer’s Guide」の10.4. Example: Listener written in Luaを参考に作成しました。

以降、(1)~(10)の手順の詳細について説明していきます。

(1)Fieldクラスのオブジェクトを定義

初めにFieldクラスのオブジェクトを定義します。

-- (1)Fieldクラスのオブジェクトを定義
SampleMsg_f = Field.new("sample.msg")

Fieldクラスについては、「Wireshark Developer’s Guide」で以下のように規定されています。

11.2.1. Field
A Field extractor to obtain field values. A Field object can only be created outside of the callback functions of dissectors, post-dissectors, heuristic-dissectors, and taps.
Once created, it is used inside the callback functions, to generate a FieldInfo object.
11.2.1.1. Field.new(fieldname)
Create a Field extractor.
Arguments
fieldname
The filter name of the field (e.g. ip.addr)
Returns
The field extractor
Errors
A Field extractor must be defined before Taps or Dissectors get called

Fieldクラスは、field.new()の引数で指定したプロトコルフィールドの値を取得するために使用します。
後に(7)でも説明しますが、SampleMsg_f()の形式で、サンプルプロトコルのMessageフィールドの情報として、FieldInfoクラスのオブジェクトを得ることができます。

(2)GUIからコンソール呼び出し時に実行される関数の定義

Wiresharkのメニューから、コンソールアプリケーションを起動するときに実行される巻数を定義します。

-- (2)GUIからコンソール呼び出し時に実行される関数の定義
function MsgCount()

    -- この中に(3)~(9)の処理を記述していく

end

(3)解析結果を表示するTextWindowオブジェクトの定義

TextWindow.new([タイトル])の形式で、TextWindowクラスのオブジェクトを初期化します。
コンソールウィンドウのテキストの書き込みやボタンの追加等はこのオブジェクトに対して行います。

    -- (3)解析結果を表示するTextWindowオブジェクトの定義
    local tw = TextWindow.new("Message Counter")

(4)ボタン追加及びクリック時の動作の定義

コンソールウィンドウにボタンを追加し、クリック時の動作を定義します。

    -- (4)ボタン追加及びクリック時の動作の定義
    tw:add_button("Reset", function()
        -- コンソールのテキストを削除
        tw:clear()
        -- メッセージ統計テーブルを初期化
        MsgCountTable = {}
    end)

ボタン追加及びクリック時の処理の定義は、TextWindowクラスのオブジェクトtwに対し、tw:add_button([表示名], [クリック時に実行する関数名])の形式で記述します。
第2引数には関数を代入しますが、上記のサンプルコードでは、無名関数(function() ~ end)の形式で代入しています。

(5)Listenerオブジェクトの定義

パケットのキャプチャを行うLiestenerクラスのオブジェクトを定義します。

     -- (5)Listenerオブジェクトの定義
    local listener = Listener.new(nil, "sample")

Listenerクラスの概要及びListenerクラスのオブジェクトの初期化方法については、「Wireshark Developer’s Guide」で以下のように規定されています。

11.4.1. Listener
A Listener is called once for every packet that matches a certain filter or has a certain tap. It can read the tree, the packet’s Tvb buffer as well as the tapped data, but it cannot add elements to the tree.
11.4.1.1. Listener.new([tap], [filter], [allfields])
Creates a new Listener tap object.
Arguments
tap (optional)

The name of this tap. See Listener.list() for a way to print valid listener names.
filter (optional)
A display filter to apply to the tap. The tap.packet function will be called for each matching packet. The default is nil, which matches every packet. Example: "m2tp".
allfields (optional)
Whether to generate all fields. The default is false. Note: This impacts performance.
Returns
The newly created Listener listener object
Errors
tap registration error

Listener.new()は、[tap]、[filter]、[allfield]の3つの引数を持ちますが、[filter]以外の引数は正直何に使うのかよく分かりません(;^_^A
今回はサンプルプロトコルのメッセージのみをキャプチャしたいため、第1引数にはnil、第2引数には"sample"を入力します。(第3引数は省略)
全ての引数を省略した場合は、全てのパケットがキャプチャされます。

(6)コンソールウィンドウクローズ時の処理

コンソールウィンドウにデフォルトで付いているcloseボタンもしくは右上の×ボタンをクリックしたときの動作です。
TextWindowクラスのオブジェクトtwに対し、tw:set_atclose([クリック時に実行する関数名])の形式で記述します。

    -- (6)コンソールウィンドウクローズ時の処理
    tw:set_atclose(function()
        -- Listenerを停止
        listener:remove()
    end)

ここでも引数には無名関数を使用しています。
また無名関数内での処理として、Listenerクラスのオブジェクトlistenerに対し、listener:remove()を実行していますが、これはListenerオブジェクトによるパケットのキャプチャを停止するための処理となります。

11.4.1.4. listener:remove()
Removes a tap Listener.
11.4. Post-Dissection Packet Analysis

(7)サンプルプロトコルメッセージ受信時の解析処理の定義

Listenerクラスのオブジェクトlistenerがサンプルプロトコルのパケットをキャプチャした際に実行する処理を
function listener.packet(pinfo, buffer)
 キャプチャ時の処理
end

の関数の形式で記述します。

2つの引数pinfo及びbufferはそれぞれ
・pinfo:Pinfoクラスオブジェクト
・buffer:Tvbクラスオブジェクト
であり、キャプチャしたパケットに関する情報や生データが格納されています。
(今回のサンプルコードでは特に使用しません)

    -- (7)サンプルプロトコルメッセージ受信時の解析処理の定義
    function listener.packet(pinfo, buffer)
        -- ローカル変数の定義
        local Msg    -- サンプルプロトコルのMessage
        local Count    -- Messageの数

        -- キャプチャしたサンプルプロトコルパケットのMessageフィールドの情報を取得
        Msg = tostring(SampleMsg_f())

        -- 取得したMessageを統計テーブルに追加
        Count = MsgCountTable[Msg] or 0
        MsgCountTable[Msg] = Count + 1
    end

本サンプルコードの関数内では、サンプルプロトコルのMessageフィールドの値を取得するために、(1)で定義したFieldクラスのオブジェクトSampleMsg_fを使用します。

SampleMsg_fに対し、SampleMsg_f()と記述することで、listenerがキャプチャしたサンプルプロトコルのメッセージから、Messageフィールドの値をFieldInfoクラスのオブジェクトとして取得することができます。

また本サンプルコードでは、SampleMsg_f()で取得したFieldInfoオブジェクトに対し、更にtostring(SampleMsg_f())とすることで、文字列変換を行っています。

Fieldinfoクラスの詳細については、以下を参照
11.2. Obtaining Dissection Data

(8)コンソールウィンドウに統計結果を書き出し

Listenerクラスオブジェクトlistenerに対し、draw()関数を定義し、コンソールウインドウにMessageの統計結果を書き出します。

    -- (8)コンソールウィンドウに統計結果を書き出し
    function listener.draw()
        tw:clear()
        for msg, count in pairs(MsgCountTable) do
             tw:append(msg .. " : " .. count .. "\n")
        end
    end

TextWindowクラスオブジェクトtwに対するテキストの書き込み方法はいくつかありますが、ここではtw:append([テキスト])を使用しています。

11.3.2.5. textwindow:append(text)
Appends text to the current window contents.
Arguments
text

The text to be appended.
Returns
The TextWindow object.
Errors
GUI not available
11.3. GUI Support

listener.draw()関数は、listenerがパケットをキャプチャした数秒後に実行される仕様となっているようで、パケットキャプチャ後、listener.draw()関数が実行される前に新たなパケットをキャプチャしても、一度関数が実行されるまでは新規のタイマーは設定されないみたいです。

短い時間に何度もTextWindowへの書き込みが行わることによる負荷を軽減するための仕様と思われます。

(9)キャプチャファイルの読込時に呼ばれる関数

listener.reset()関数は、listenerがパケットキャプチャを実行している状態で、キャプチャファイルの読込をしたときに呼ばれる関数のようです。
参考にしたサンプルコードに記載されていたので、本サンプルコードでも記載していますが、あまり重要ではないような気がします(;^_^A

    -- (9)キャプチャファイルの読込時に呼ばれる関数
    function listener.reset()
        -- コンソールのテキストを削除
        tw:clear()
        -- メッセージ統計テーブルを初期化
        MsgCountTable = {}
    end

(10)メニュー登録

最後に(2)~(9)で記述したMsgCount()関数をGUIから呼び出すための処理を記述します。

-- (10)メニュー登録
register_menu("Test/Message Counter", MsgCount, MENU_TOOLS_UNSORTED)


register_menu()関数に上記のように引数を与えることで、図10-1のようにGUIのメニューからMsgCount関数を呼び出すことができるようになります。


f:id:taekwongineer:20200426180556p:plain
図10-1 GUIへのメニュー登録

regiser_menu()関数の詳しい使用方法については、下記を参照

11.3.3.2. register_menu(name, action, [group])
Register a menu item in one of the main menus. Requires a GUI.
Arguments
name

The name of the menu item. Use slashes to separate submenus. (e.g. Lua Scripts → My Fancy Statistics). (string)
action
The function to be called when the menu item is invoked. The function must take no arguments and return nothing.
group (optional)
Where to place the item in the menu hierarchy. If omitted, defaults to MENU_STAT_GENERIC. One of:
MENU_STAT_UNSORTED: Statistics
MENU_STAT_GENERIC: Statistics, first section
MENU_STAT_CONVERSATION: Statistics → Conversation List
MENU_STAT_ENDPOINT: Statistics → Endpoint List
MENU_STAT_RESPONSE: Statistics → Service Response Time
MENU_STAT_TELEPHONY: Telephony
MENU_STAT_TELEPHONY_ANSI: Telephony → ANSI
MENU_STAT_TELEPHONY_GSM: Telephony → GSM
MENU_STAT_TELEPHONY_LTE: Telephony → LTE
MENU_STAT_TELEPHONY_MTP3: Telephony → MTP3
MENU_STAT_TELEPHONY_SCTP: Telephony → SCTP
MENU_ANALYZE: Analyze
MENU_ANALYZE_CONVERSATION: Analyze → Conversation Filter
MENU_TOOLS_UNSORTED: Tools
11.3. GUI Support

3.まとめ

ここまで

・Field
・FieldInfo
・TextWindow
・Listener

の4つのクラスのオブジェクトを使用し、サンプルプロトコルのMessageフィールドの値の統計結果をコンソールウィンドウに表示させるアプリケーションの作成方法について紹介しました。

listener.packet()関数内で、Field、FieldInfoクラスオブジェクトを使用することで、簡単にMessageフィールドの値を取得することができました。

ただ、このやり方ですと、1つのIPパケット内に複数のサンプルプロトコルメッセージが含まれていた場合については、先頭のメッセージに対してしか、listener.packet()関数が実行されません。

全てのサンプルプロトコルメッセージのMessageフィールドの値を読み込むためには、また別の方法が必要となります。

次回は1つのIPパケット内に複数のサンプルプロトコルメッセージが含まれている場合を考慮したアプリケーションの作成方法について紹介したいと思います。

Wiresharkで独自プロトコルを解析する(その2)

前回、UDPで伝送される独自プロトコルメッセージの解析プラグインの作成方法について記事をまとめました。

taekwongineer.hatenablog.jp

記事でも書いた通り、UDPで伝送されるメッセージについては、シンプルなメッセージフォーマットであれば、お作法さえ覚えてしまえば割と簡単に解析プラグインを作成することができました。
しかし、TCPの場合は、メッセージの送信タイミングによっては、1つのIPパケットの中に複数のメッセージが格納されて送信されてしまうことがあるため、これらを解析できるようにするためには一工夫が必要です。

今回は1つのIPパケットに複数メッセージが含まれるケースを考慮した独自プロトコル解析プラグインの作成方法についてまとめます。



この記事の目次

1.解析対象の独自プロトコル

今回は以下の図1-1のメッセージフォーマットのプロトコルを例に、解析プラグインを作成していきます。


f:id:taekwongineer:20200419163817p:plain
図1-1 解析対象プロトコルのメッセージフォーマット

Message SizeにはMessageのデータ長[byte]を格納し、Messageには、UTF-8エンコードされた文字列を格納します。
この記事では上記のメッセージフォーマットのメッセージをサンプルプロトコルと呼ぶことにします。

2.解析プラグインのサンプルコード(複数メッセージ対応未考慮)

まずは、複数メッセージ対応が考慮されていない場合のサンプルコードを以下に示します。

-- 独自プロトコルの定義
SampleProto = Proto.new("sample", "Sample Protocol")

-- プロトコルフィールドの定義
f_MsgSize = ProtoField.new("Message Size", "sample.size", ftypes.UINT16)
f_Msg = ProtoField.new("Message", "sample.msg", ftypes.STRING)

-- 定義したプロトコルフィールドをプロトコルフィールド配列に登録
SampleProto.fields = {f_MsgSize, f_Msg}

-- dissector関数の定義
function SampleProto.dissector(buffer, pinfo, tree)
	
-- dissector関数内で使用するローカル変数の定義
    local MsgNum = 0    -- 1パケット内に含まれるSample Protocolのメッセージ数
    local BufLeft = buffer:len()    -- buffer内の未読込データの残りバイト数(初期値は引数bufferのデータ長)
    local BufPos = 0    -- bufferのデータ読込開始位置
    local r_BufRange    -- サンプルプロトコルメッセージのデータ範囲
    local r_MsgSize     -- Message Sizeのデータ範囲
    local r_Msg    -- Messageのデータ範囲

    ---- bufferからデータを読込み ----
    -- Message Sizeを読込み
    r_MsgSize = buffer(0, 2)

    -- Messageを読込み
    r_Msg = buffer(2, r_MsgSize:uint())

    -- Sample Protocolメッセージ全体を読込み
    r_BufRange = buffer(0, 2 + r_MsgSize:uint())
    
    ---- プロトコルツリーに情報を追加
    -- treeにサブツリーとして"Sample Protocol"を追加
    local subtree = tree:add( SampleProto, r_BufRange )
	
    -- subtreeに上記で定義した各プロトコルフィールドの値を追加
    subtree:add( f_MsgSize, r_MsgSize )  -- Message Size
    subtree:add( f_Msg, r_Msg, r_Msg:string(ENC_UTF_8), "Message : " .. r_Msg:string(ENC_UTF_8) )  -- Msg

    ---- プロトコル情報を設定
    -- "Protocol"列の表示
    pinfo.cols.protocol = "Sample"
	
    -- "info"列の表示
    pinfo.cols.info = "Message Size:" .. r_MsgSize:uint() .. " Message:" .. r_Msg:string(ENC_UTF_8)
end

-- 定義したプロトコルをTCPポート番号を指定した紐づけ
tcp_table = DissectorTable.get("tcp.port")
tcp_table:add(20406, SampleProto)      


各行の細かい解説については今回は省略しますので、前回の記事を参照してください。

taekwongineer.hatenablog.jp

上記コードのプラグインでサンプルプロトコルの解析を行った場合、1つのIPパケットに複数のサンプルプロトコルメッセージが含まれる場合の解析結果は以下の図2-1のように表示されることになります。


f:id:taekwongineer:20200419171044p:plain
図2-1 複数メッセージを含むパケットの解析結果(複数メッセージ対応未考慮)

見てわかる通り、1番目のメッセージは解析され、Message SizeとMessageの内容が表示されますが、後に続くメッセージについては解析が行われず、何も表示されない状態となってしまっています。

そこで上記のコードに対して、以下の改良を行います。
(1)1つのIPパケットに複数のサンプルプロトコルメッセージが含まれていても、全て解析を実行できるようにする。
(2)各メッセージのMessage Size及びMesssageの値を、プロトコルツリーのSample Protocolの文字列の右側に表示できるようにする

(2)については、この記事の本筋からは外れますが、知っておくと便利なので、併せて解説します。

3.解析プラグインのサンプルコード(複数メッセージ対応考慮)

改良を行ったコードを以下に示します。

--独自プロトコルの定義
SampleProto = Proto.new("sample","SampleProtocol")

--プロトコルフィールドの定義
f_MsgSize = ProtoField.new("MessageSize","sample.size",ftypes.UINT16)
f_Msg = ProtoField.new("Message","sample.msg",ftypes.STRING)

--定義したプロトコルフィールドをプロトコルフィールド配列に登録
SampleProto.fields = {f_MsgSize,f_Msg}

-- dissector関数の定義
function SampleProto.dissector(buffer, pinfo, tree)

    -- dissector関数内で使用するローカル変数の定義
    local MsgNum = 0    -- 1パケット内に含まれるSampleProtocolのメッセージ数
    local BufLen = buffer:len()    -- 引数bufferのデータ長
    local BufLeft = buffer:len()    -- buffer内の未読込データの残りバイト数(初期値はBuflen)
    local BufPos = 0    -- bufferのデータ読込開始位置
    local r_BufRange    -- サンプルプロトコルメッセージのデータ範囲
    local r_MsgSize    -- MessageSizeのデータ範囲
    local r_Msg    -- Messageのデータ範囲
    
    -- BufLeftが3以上のとき、解析処理を継続
    while BufLeft >= 3 do
        ---- bufferからデータを読込み ----
        -- MessageSizeを読込み
        r_MsgSize = buffer(BufPos, 2)

        -- Messageを読込み
        r_Msg = buffer(BufPos + 2, r_MsgSize:uint())

        -- SampleProtocolメッセージ全体を読込み
        r_BufRange = buffer(BufPos, 2 + r_MsgSize:uint())

        ---- プロトコルツリーに情報を追加
        -- treeにサブツリーとして"SampleProtocol"を追加
        local subtree = tree:add(SampleProto, r_BufRange)

        -- subtreeに上記で定義した各プロトコルフィールドの値を追加
        subtree:add(f_MsgSize, r_MsgSize) -- MessageSize
        subtree:add(f_Msg, r_Msg, r_Msg:string(ENC_UTF_8), "Message:" .. r_Msg:string(ENC_UTF_8)) -- Msg

        -- subtreeのトップにMessageSizeとMessageの情報を追記する
       subtree:append_text(",MessasgeSize:" .. r_MsgSize:uint() .. ",Message:" .. r_Msg:string(ENC_UTF_8))

        ---- bufferのデータ読込開始位置を更新
        BufPos = BufPos + 2 + r_MsgSize:uint()

        ---- buffer内の未読込データ数を更新
        BufLeft = BufLeft - (2 + r_MsgSize:uint())

        -- SampleProtocolメッセージ数を更新
        MsgNum = MsgNum + 1
    end

    ---- プロトコル情報を設定
    -- "Protocol"列の表示
    pinfo.cols.protocol = "Sample"

    -- "info"列の表示(MsgNumの値に応じて分岐)
    if MsgNum == 1 then
    -- メッセージ数が1個の場合は、MessageSizeとMessageの値を表示
    pinfo.cols.info = "MessageSize:" .. r_MsgSize:uint() .. "Message:" .. r_Msg:string(ENC_UTF_8)
    else
    -- メッセージ数が2個以上の場合は、メッセージ数を表示
    pinfo.cols.info = "Thispacketcontains" .. MsgNum .. "messages."
    end
end

-- 定義したプロトコルをTCPポート番号を指定した紐づけ
tcp_table = DissectorTable.get("tcp.port")
tcp_table:add(20406, SampleProto)

改良といってもそんなに大したことはしていません。
(1)1つのIPパケットに複数のサンプルプロトコルメッセージが含まれていても、全て解析を実行できるようにする。
↑に対しては、whileループを組んで、bufferのデータ長に対して、未読込のデータ数が3byte以上残っていれば、解析処理を継続するようにコードを修正しました。

(2)各メッセージのMessage Size及びMesssageの値を、プロトコルツリーのSample Protocolの文字列の右側に表示できるようにする
↑に対しては、subtree:append_text("追記文")の形式で、プロトコルツリーの表示に追記を行いました。

上記コードのプラグインでサンプルプロトコルの解析を行うと、1つのIPパケットに複数メッセージが含まれる場合の解析結果は図3-1のようになります。


f:id:taekwongineer:20200419174152p:plain
図3-1 複数メッセージを含むパケットの解析結果(複数メッセージ対応考慮)

見てわかる通り、TCPプロコルのヘッダツリーの後に、サンプルプロトコルのツリーが2つ追加されるようになりました。

また、各ツリーのSample Protocolのテキスト表示の右側に、Message Size及びMeesageの内容を表示できるようになりました。

4.まとめ

今回の記事では、1つのIPパケットに独自プロトコルのメッセージが複数含まれる場合の解析プラグインの作成方法について解説しました。
一工夫がいると書きましたが、特別なAPIを使うわけではなく、一般的なプログラムのアルゴリズムだけで実現可能です。(;^_^A

また、今回のサンプルコードで使用したsubtree:append_text("追記文")は、知っておくととても便利かと思います。

2020/05/24 追記
独自プロトコルのメッセージが複数のIPパケットに跨って配信される場合に再組立てして解析する方法について記事を書きました。
taekwongineer.hatenablog.jp