ラズパイで作るネットワークエミュレータ(前編)
ネットワークが絡んだ通信プログラムを開発していると、テストのために遅延やパケロスを意図的に発生させたくなることがあります。いまどきは IDE にネットワークエミュレーション機能が組み込まれていたり、仮想環境で容易に再現できたりもしますが、箱物のネットワークエミュレータがあるとネットワークの構成を気にせずカンタンに設置できるのですごく便利だったりします。世の中にはそういった製品が沢山あるので安価なものを買ってもいいのですが、新たにラズパイが届いたばかりだったので、これを使って超小型のネットワークエミュレータを自作してみました。前編と後編の二回に分けて紹介します。
最近、社内で「ラズパイおじさん」と呼ばれるようになりました。@pandax381 です。
ラズパイ + Linux = ネットワークエミュレータ
「ネットワークエミュレータを自作」と言うとなんだか凄そうな感じがしますが、実はものすごく簡単につくれます。なぜなら、ネットワークエミュレータに必要な機能を Linux が全て備えているからです。Linux をインストールしたラズパイがあれば、数万円の価格帯の製品と同等のことができます(というより、この価格帯の製品は単に Linux の機能を利用しただけのものが多いです)。
動作イメージ
作成したネットワークエミュレータは、ブリッジ(中継機)として動作するタイプのものです。ネットワーク機器同士の間に接続し、通過するパケットに対して遅延やパケロスを発生させます。
また、イーサネットポートを備えていないラップトップやスマートフォンの場合には、ネットワークシミュレータを「アクセスポイント〜スイッチ間」に設置することで、そのアクセスポイントに接続している端末の通信パケットに対して制御を加えることができます。例えば、Wi-Fi につないだままモバイルネットワークと同等の品質で通信するといったことができるようになります。
イーサネットポートが足りない問題
ブリッジとして動作させると言ったものの、ラズパイに搭載されているイーサネットポートは1つだけなので、物理的にポートが足りなくてネットワークをブリッジできません(個人的には2ポートのモデルがあるとすごく幸せなんですけど、一般的にはあまり需要ないんですかねぇ...)。
これはもう足りないなら増やすしかないので、ちょっと格好悪いですがUSBイーサネットアダプタを接続してポートを増設します。ラズパイに内臓されているイーサネットポートは100Mなので、増設するイーサネットアダプタも100Mで十分です。無駄にギガビットのアダプタを接続しても、ブリッジした際にもう片方の性能に引っ張られるので意味がないです。個人的には Logitec LAN-TXU2C あたりが安価で入手性もいいのでオススメです。Raspbian にはこのアダプタが使っているチップのドライバ(asix.ko)が入っているので、USBポートに接続するだけで認識します。
なお、USBイーサネットアダプタはそれなりに電力を消費するため、調子に乗って何個も接続するとラズパイ本体が電力不足で不安定になって落ちたりするので自重しましょう。
VLANスイッチと組み合わせるとイーサネットポートを増設しなくても済むようになりますが、それは後編で紹介します。
ブリッジを作ってパケットを通過させる
まず、ブリッジを作成してラズパイの持つ2つのイーサネットポート間でパケットが通過できるようにします。
ブリッジを作成するには、bridge-utils がインストールされている必要があります。Raspbian のイメージならインストールされているはずですが、なければ apt でインストールします。
# apt-get install bridge-utils
ブリッジの作成や操作は brctl
コマンドで行います。brctl addbr
でブリッジ を作成し、brctl addif
でブリッジにインタフェースを追加します。ここでは、br0 という名前で新しくブリッジを作成し、そこに eth0 と eth1 を追加します。
# brctl addbr br0
# brctl addif br0 eth0 eth1
作成したブリッジは brctl show
で確認できます。
# brctl show
bridge name bridge id STP enabled interfaces
br0 8000.3495db294586 no eth0
eth1
仕上げに、ip
コマンドでインタフェースとブリッジを起動させれば、このラズパイは2ポートのハブのように振る舞いはじめます。試しに、PCとスイッチの間にこのラズパイを接続しても、PCは何事もなくこれまでと同じように通信できるはずです。
# ip link set eth0 up
# ip link set eth1 up
# ip link set br0 up
遅延とパケロスを発生させる
ブリッジを作成してパケットがラズパイを通過できるようになったので、次は通過するパケットに対して遅延やパケロスを発生させる設定をします。
Linux カーネルにはトラフィックコントロールやネットワークエミュレーションの機能が備わっていて、Raspbian を含めたほとんどのディストリビューションで有効になっています。
iproute2
に含まれる tc
コマンドを使って、送信トラフィックに対して以下の制御を加えることができます。
- 帯域制限
- 帯域保証
- 遅延
- パケットロス
- ビットエラー
- 重複
- 順序入れ替え
ネットワークエミュレータを名乗るからには全ての機能を取り入れたいところですが、今回は取り急ぎ必要だった 遅延
と パケロス
のみを扱います。
tc コマンドを使いこなすと高度なトラフィック制御ができるようになりますが、Linux のトラフィックコントロールの概念はなかなかに複雑で、tc のコマンド体系もそれに習って難解です。ここでは決まった使い方しか示しませんが、tc コマンドやトラフィックコントロールの詳細について興味がある方には、GREEさんのエンジニアブログに掲載されている「よくわかるLinux帯域制限」という記事が秀逸でオススメです。
tc コマンドは複雑ですが、遅延とパケロスを設定するだけなら簡単です。例えば、eth0 に対して遅延とパケットロスを設定するには以下のコマンドを実行するだけです。これで、eth0 から「出て行く」パケットに対して 100ms の遅延が加わり、5% の確率でパケットロスが発生します。
# tc qdisc add dev eth0 root netem delay 100ms loss 5%
なお、新しく設定を追加する時は tc qdisc add
ですが、設定済みの値を変更する際は tc qdisc change
です。値が設定されていないのに change を指定したり、設定済みなのに add を指定するとエラーになります。
# tc qdisc change dev eth0 root netem delay 200ms loss 10%
設定した値は tc qdisc del
で消せます。消去した後の再設定は tc qdisc add
です。
# tc qdisc del dev eth0 root
ここでは、遅延やパケロスを固定的に発生させる方法を示していますが、遅延時間やパケロス発生頻度を一定の範囲内で分散させたりすることもできます。
UIについて考える
このように、Linux の標準的な機能を組み合わせることで、ラズパイを超小型のネットワークシミュレータに仕立て上げることができます。ただし、この状態だとめちゃくちゃ使いにくいです。遅延やパケロスの値を変えるのに、その都度シリアル経由でログインして tc コマンドを実行するとかありえないです。自分が手元で使うにしても、もう少し気の利いた UI が欲しいと思ってしまいます。
ネットワーク機器なので、シリアルでCLIを提供するというのも個人的にはアリですが、他の人に勧めようとした際に「USBシリアル変換ケーブルとか持ってないよ」と言われてしまう可能性が高いので微妙です。無難に Web で GUI を提供するのが現実的だと思いますが、メンテナンス用のイーサネットポートがないのでIPアドレスを割り当てる先がなく、これも微妙です。
どうにも決めかねていたので、たまたま通りがかった同僚に相談してみたところ「直感的なやつ」「アナログな感じで」「つまみでぐりぐり設定したい」「液晶もつけよう」「アキバ行こう」などと煽られました。
「まぁラズパイを使ってるんだから電子工作でアナログな UI を作るのも面白いなぁ〜」と思いはじめ、悪乗りしてネタにマジレスすることにしました。
UI(物理)を試作
パーツ一覧
まず、ラズパイ本体とケース、外付けのUSBイーサネットアダプタです。前回はRSコンポーネンツ製の純正ケースを買ったのですが、Eleduino のアクリルケースがお洒落だったのでこちらを選んでみました。ヒートシンクの効果は不明ですがカッコいいので付けてます。
- Raspberry Pi 2 Model B
- Eleduino Raspberry Pi 2 Model B black Acrylic Case
- Logitec 有線LANアダプタ USB2.0 LAN-TXU2C
続いて、UI用の工作パーツです。全て秋月で調達できます。コンセプトの「ぐりぐりして設定する」を実現するために、ボリューム(可変抵抗)を使います。なお、ラズパイはアナログ入力を備えていないので、SPI接続のADコンバータを使ってボリュームの値を読み取ります。設定した値を表示するために、I2C接続のキャラクタLCDも用意します。
- ミニブレッドボード BB-601(白)
- 10ビット2ch ADコンバータ MCP3002-I/P
- 小型ボリューム 10KΩA(16K4)
- カラーつまみ(黒+青)
- カラーつまみ(黒+桃)
- I2C接続小型LCDモジュール(8x2行)ピッチ変換キット
UI用の工作パーツだけなら千円程度で揃います。
組み立て
回路図とか書けないクソザコなので、配線図的な何かを載せておきます。
設定した値を表示させるためのキャラクタLCDです。8文字×2行なので表示できる情報量は少ないですが、ものすごく小さいです。ピッチが狭いので変換基盤への半田付けが少し大変ですが、電子工作初心者で半田付けに不慣れな僕でもなんとかできたので大丈夫です。ちなみに、弊社では、その辺に FLUKE のオシロスコープが転がっていたりするのですが、なぜか半田ごてが備品として存在しておらず「これは由々しき問題だ」ということで、新たに温調式の半田ごてが備品に仲間入りすることなりました。
ボリュームは、GND(左)OUT(中)VCC(右)に接続するので、ジャンプワイヤを半田づけしておきます。
あとは、ブレッドボードとボリュームを両面テープでラズパイの上に固定して、ひたすら配線します。ブレッドボード付属の両面テープはかなり強力なので慎重に...貼ってしまうと剥がすのが大変です。ボリュームはクッション入りの両面テープで貼るとグラつかずに固定できます。
ちょっとジャンプワイヤがごちゃごちゃしていますが、まぁプロトタイプなのでこれでいいでしょう。ついでにUSBイーサネットアダプタも両面テープで側面に貼ってしまいます。なんだかそれっぽいガジェットに見えるようになってきましたね!
I2C接続のLCDが認識できない問題
秋月で調達したI2C接続のLCD「AE-AQM0802」が認識できないという問題に遭遇しました。ピッチ変換基盤への半田付けをミスったのかと思い、完成品を買ってきても認識してくれませんでした。少し調べてみたところ、同じようにラズパイで認識されないと報告している方がいました(逆にちゃんと動作したという方もいました)。
電子工作初心者のため、あまり良くわかってないのですが、どうも「I2C で使う GPIO ピンはラズパイ本体側に固定的にプルアップ抵抗が実装されていて、その抵抗値が小さすぎるためにうまく認識できない」ということのようです。解決策っぽいものの中で一番確実で分かりやすかったのが「ラズパイ本体側のプルアップ抵抗を取り除く」でした。ちょっと荒技な気もしますが、えいやでやってみたら見事に動いてくれました。
ラズパイ本体側のプルアップ抵抗とは、GPIOピンのすぐそばに表面実装されている R23 / R24 のチップ抵抗です。そのまま半田ごてを当ててもなかなか取れないので、半田を盛ってから吸い取り線で一緒に吸ってしまうとキレイに取れます。
ピッチ変換キットには10KΩのプルアップ抵抗が実装されていてるので、これをショートさせて有効にします。
その後、さらに調べてみたところ、ストロベリーリナックス製の LCD「SB0802GN」が、秋月のものとほぼ同じ仕様(形状もコントローラも同じ)で、これなら問題なく認識されるようです。ラズパイ本体に手を加えたくない場合には、代わりにこちらの LCD を使うのがいいかもしれません。
コードを書く
ラズパイの設定変更
ADコンバータとLCDの接続に、それぞれ SPI と I2C を使うので、これらを有効にするための設定をします。
/boot/config.txt
に以下の内容を追記します。
dtparam=i2c_arm=on
dtparam=spi=on
/etc/modules
に以下の内容を追記します。
i2c-bcm2708
i2c-dev
spi_bcm2835
再起動後に lsmod
を実行して指定したモジュールがロードされていれば大丈夫です。
# lsmod
Module Size Used by
sch_netem 8032 0
bridge 93064 0
ipv6 339514 17 bridge
stp 1479 1 bridge
llc 3600 2 stp,bridge
i2c_dev 6047 0
asix 19981 0
libphy 24139 1 asix
i2c_bcm2708 5014 0
spi_bcm2835 7248 0
uio_pdrv_genirq 2966 0
uio 8235 1 uio_pdrv_genirq
必要なツールとライブラリのインストール
制御プログラムをC言語で書きますが、超絶便利なライブラリ「Wiring Pi」を使います。Wiring Pi は、ラズパイの GPIO を制御するためのライブラリで SPI や I2C にも対応しています。今回はADコンバータからボリュームの値を読み出す処理で Wiring Pi を使います。LCDの制御に関しては手抜きをして i2c-tools に頼ります。
- i2c-tools
- libi2c-dev
- Wiring Pi
i2c-tools と libi2c-dev を apt でインストールします。Wiring Pi は git のリポジトリを clone してビルド&インストールします。
# apt-get install i2c-tools libi2c-dev
# git clone git://git.drogon.net/wiringPi
# cd wiringPi
# ./build
制御プログラム
アナログUIの制御プログラムです。一秒おきにADコンバータからボリュームの値を読み取り、前回の値から変改していたら tc コマンドの実行とLCDへの描画を行います。ほとんどの処理を system() 経由で外部コマンドに丸投げしていたり、エラー処理が手抜きだったり、かなり雑に作っていますがプロトタイプなのでご愛嬌。
/* * packdrop.c */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <wiringPi.h> #include <wiringPiSPI.h> #include <mcp3002.h> #define APP_NAME "PackDrop" #define APP_VERSION "v0.4" #define SPI_CHANNEL (0) #define PINBASE (100) #define VOLUME_DELAY (PINBASE) #define VOLUME_LOSS (PINBASE+1) char **devices; int devnum; static void tc_set (const char *dev, int delay, int loss) { char cmd[128]; snprintf(cmd, sizeof(cmd), "tc qdisc change dev %s root netem delay %dms loss %d%%", dev, delay, loss); system(cmd); } static void tc_init (const char *dev) { char cmd[128]; snprintf(cmd, sizeof(cmd), "tc qdisc del dev %s root >/dev/null 2>&1", dev); system(cmd); snprintf(cmd, sizeof(cmd), "tc qdisc add dev %s root netem delay 0ms loss 0%%", dev); system(cmd); } static void lcd_write_core (const char *upper, const char *lower) { char cmd[128]; system("i2cset -y 1 0x3e 0 0x80 b"); snprintf(cmd, sizeof(cmd), "i2cset -y 1 0x3e 0x40 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x i", upper[0], upper[1], upper[2], upper[3], upper[4], upper[5], upper[6], upper[7]); system(cmd); system("i2cset -y 1 0x3e 0 0xc0 b"); snprintf(cmd, sizeof(cmd), "i2cset -y 1 0x3e 0x40 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x i", lower[0], lower[1], lower[2], lower[3], lower[4], lower[5], lower[6], lower[7]); system(cmd); } static void lcd_write (int delay, int loss) { char upper[9], lower[9]; snprintf(upper, sizeof(upper), " %3d ms", delay); snprintf(lower, sizeof(lower), " %3d %%", loss); lcd_write_core(upper, lower); } static void lcd_init (void) { char name[9], ver[9]; system("i2cset -y 1 0x3e 0 0x38 0x39 0x14 0x78 0x5f 0x6a i"); system("i2cset -y 1 0x3e 0 0x0c 0x01 i"); system("i2cset -y 1 0x3e 0 0x06 i"); snprintf(name, sizeof(name), "%8s", APP_NAME); snprintf(ver, sizeof(ver), "%8s", APP_VERSION); lcd_write_core(name, ver); } int main (int argc, char *argv[]){ int n, v, d, l; int delay = 0, loss = 0; lcd_init(); if (mcp3002Setup(PINBASE, SPI_CHANNEL) < 0) { printf("mpc3002Setup: failed\n"); return -1; } devices = argv + 1; devnum = argc - 1; for (n = 0; n < devnum; n++) { tc_init(devices[n]); } while (1) { v = analogRead(VOLUME_DELAY); d = ++v / 2; v = analogRead(VOLUME_LOSS); l = ((float)++v / 1024) * 100; if (d != delay || l != loss) { delay = d; loss = l; for (n = 0; n < devnum; n++) { tc_set(devices[n], delay, loss); } lcd_write(delay, loss); } sleep(1); } return 0; }
Wiring Pi のライブラリを使っているので、-lwiringPi
を指定してコンパイルします。
# gcc -W -Wall -o packdrop packdrop.c -lwiringPi
動かしてみる
実際に制御プログラムを動かして、ちゃんと遅延とパケロスが発生するのかを確認します。
まず、事前にブリッジを作成しておきます。
# brctl addbr br0
# brctl addif br0 eth0 eth1
# ip link set eth0 up
# ip link set eth1 up
# ip link set br0 up
続けて、制御プログラムの packdrop を起動します。packdrop の引数には、ブリッジに追加したインタフェースを指定します。
# ./packdrop eth0 eth1
実際に動作させている動画です。
動画を見ると一目瞭然ですが、青いボリュームを回すと「遅延」が増加し、ピンクのボリュームを回すと「パケロス」が増加します。遅延もパケロスも「上りと下りの両方に適用」されるため、ping の RTT には設定した遅延の2倍の時間が加算され、パケロスが 10% の場合には 0.9 x 0.9 = 0.81(81%)の確率で応答パケットが届きます。
余談
このネットワークエミュレータには「PackDrop」という名前を付けたのですが、なぜか社内では「パケ落ちくん」という愛称で呼ばれています。
次回予告
半分ネタで雑に作ったネットワークエミュレータですが、実際に案件でデバッグに使ってみてもらったところ、わりと評判が良く、各所から「それ欲しい」という要望が出てきました。
ラズパイの在庫は豊富にあるので何台でも作れるのですが、指を引っ掛けたら配線が抜けてしまうんじゃないかとか、両面テープで固定しているだけなのでボリュームが取れてしまうんじゃないかとか、ちょっと物理的に脆弱すぎるという問題が...
他にも、ちゃんと起動スクリプトを作ったり電プチに対応したり、アプライアンス的に使えるようにした方が使い勝手が良さそうだったりします。そんな訳で、後編ではもう少し真面目に作った「量産型」を紹介します。