Solaris をスイッチングハブにしよう/其の1

目次

ご注意

ここで紹介しているプログラムは、カーネルモジュールです。そのため、プログラム上に問題があった場合にはシステムが panic し、場合によってはファイルシステムに重大な障害を引き起こす可能性があります。以下のプログラムはテストプログラムで、十分な検証を行ってはいません。したがってあくまで個人の検証ためにお使いいただき、重要なシステム上では動作させないでください。

概要とプログラムのイメージ

こんどは、余った Ethernet ポートを利用して、Solaris マシンを スイッチングハブ(Ethernet Switch) にしてしまおうというものです。Sparc マシンを使っていて、qfe(Sun の NIC で、1つのカードで4つの Ethernet ポートを持つ)を入れてるけど、実際には1つのポートしか使ってなくて、残りの3ポートがもったいない・・・なんて時は、このモジュールを使えば、使っていない Ethernet ポートをハブとして有効活用できます。きっとそんな人はいないと思いますけどね。(笑)もともとは、Firewall モジュールの機能拡張として ステルスモード(ルーターとしてではなく、ブリッジとして動作するファイアーウォール)を追加しようとして書き始めたのですが、なんでか独立したモジュールになってしまいました。
FreeBSD に bridge というモジュールがあって、まさに同じ事ができるようなので参考にしようと、ソース を入手し読んだのですが、途中で断念。ほとんどコードを追いきれませんでした。(泣)・・が一部参考にしてます。

プログラムとしては、パケットの転送を行う STREAMS モジュールと、設定用のコマンドの2つで構成されています。ちょうど以前書いた Firewall モジュールSolaris DLPI プログラミングの両方の要素が混じったものです。まず、設定用コマンド(brdgadm)にてネットワークインターフェースのデバイスファイルをオープンして STREAM を作成し、そこに今回のモジュール(brdg モジュール)を PUSH します。ハブのポートとして追加したいネットワークインターフェースを設定用コマンドを使ってオープンし、brdg モジュールを PUSH することで、brdg モジュールのインスタンス 同士がパケットの交換を行い、結果としてパケットが他のポートに転送されることになります。

brdgadmユーザコマンド。
STREAM の作成、モジュールの PUSH を行う。
brdgSTREAMS モジュール。
実際のパケットの転送処理を行う。

brdg モジュールはぶっちゃけパケットの転送をしているだけなので大した事はをしていません。が、brdgadm コマンドの方で行っている処理は少々特殊なことをやっていますので、そちらから説明します。以下が、brdgadm コマンドの流れです。


device_open.png


図1

  • brdgadm コマンドが、IP のドライバである /dev/ip をオープンする。また、ネットワークインターフェースのドライバである /dev/hme をオープンする。
  • これにより、hme ドライバ、ip ドライバを底辺とした2つの STREAM が作成される。


push_brdg.png



  • hme ドライバの STREAM に brdg モジュールを PUSH する。


i_plink.png


図3

  • I_PLINK ioctl() コマンドを使って、hme の STREAM を ip の STREAM の下にリンクさせる。これにより、2つの STREAM は実質1つの STREAM のようになる。


close_device.png


図4

  • /dev/ip と /dev/hme をオープンした FD をクローズし、brdgadm コマンドは終了する。これにより、brdgadm プロセスと STREAM の仲介役であった STREAM HEAD はなくなるが、I_PLINK しているので IP 以下の STREAM はそのまま残る。(関連したプロセスの無い STREAM が出来上がる)


link_2streams.png


図5

  • 同様に、/dev/le に対しても brdgadm コマンドを使って STREAM を作成する。
  • これで、hme から来たパケットが brdg モジュールを介して le に転送され、同様に le に来たパケットが hme にも転送されるようになる。


図4で、結局は hme driver を底辺とした STREAM が一つ残っているだけなので、何故 /dev/ip をオープンしたりしているのだろう?と疑問を持たれたのではないかの思います。一見無駄なことをしているように見えますが、実はこれがこの brdgadm コマンドの肝ともいえる重要なところなのです。
通常、プロセスが STREAMS デバイスをオープン してできた STREAM は、プロセスが終了してしまえば、無くなってしまいます。しかし、図3で行っているパーシストリンク (Persist Link)を使えば、プロセス終了後も STREAM が存続できるのです。このパーシストリンク(持続リンク・・かな?)については後ほどお話します。
まずは、図1でオープンしている /dev/ip ですが、/dev/ip といえば、 IP のデバイスファイルですが、ここで /dev/ip をオープンしているのは IP のプロトコルや、サービスを利用したいわけでは無いのです。IP ドライバーはマルチプレクサーと呼ばれる特殊なドライバーで、このマルチプレクサーの特殊な機能を利用しようとしているのです。マルチプレクサーとは複数の STREAM が接続可能なドライバーで、ある STREAM から流れてくるデータ(メッセージ)を他の STREAM へとルーティングを行うことができます。マルチプレクサーの上方に複数の STREAM が接続される場合には N-to-1、もしくはアッパーマルチプレクサー(Upper Multiplexer)と呼ばれ、下方に複数の STREAM が接続される場合には One-to-M、もしくはロアーマルチプレクサー(Lower Multiplexer)と呼ばれます。

upper_multiplexer.png


アッパーマルチプレクサー

マルチプレクサーの上方に複数の STREAM が接続されている。ユーザプロセスから来るデータは、他方の STREAM へルーティングされ、別のユーザプロセスに渡される。(ループバックドライバ等)

lower_multiplexer.png


ロアーマルチプレクサー

マルチプレクサーの下方に複数の STREAM が接続されている。ユーザプロセスからきたデータを、ルーティングを行ってどちらかのドライバーに渡す。もしくは、一方のドライバーから到着したデータを他方のドライバーへ渡す。
Device Driver を NIC と考えて、Lower Multiplexer を IP だと考えれば分かりやすいかと思います。(あて先 IP アドレスによって出力先のインターフェースを変える)


ちなみに、上記の両方を兼ね備えたマルチプレクサーを M-to-N マルチプレクサーとも呼びます。(ほぼ Sun のページの受け売りです)

アッパーマルチプレクサーは、マルチプレクサーの開発者がそのよう作れば、プロセス側では単にマルチプレクサーをデバイスとしてオープンするだけで、複数の STREAM 間のルーティングができるようになりますが、ロアーマルチプレクサーの場合にはそうはいきません。マルチプレクサーの下方に STREAM を接続するためには STREAMS IOCTL(man streamio(7I))の I_LINK コマンドを発行する必要があります。

STREAMS ioctl() I_LINK コマンド
概要STREAM デバイスの制御
インクルードファイル#include <unistd.h>
#include <stropts.h>
#include <sys/conf.h>
形式int ioctl(int FD1, I_LINK, int FD2);
引数int FD1 : アッパーマルチプレクサーのファイル記述子
I_LINK : 2つの STREAM をリンクさせるコマンド
int FD2 : マルチプレクサの下方に接続するデバイスのファイル記述子
戻り値成功時: 実数値
エラー: -1

これにより、2つの STREAM(マルチプレクサーの STREAM とデバイスの STREAM )が接続されることになります。
ここで、先ほどでてきたパーシストリンクに戻りますが、上記の I_LINK コマンドでは、マルチプレクサーの FD (ファイル記述子)がクローズされてしまうと、せっかく接続した2つの STREAM ともに無くなってしまいます。そこで I_LINK の代わりに I_PLINK コマンドを使います。I_PLINK は I_LINK とほぼ同じコマンドですが、I_PLINK の場合、マルチプレクサーの FD がクローズされても、マルチプレクサーの下方の STREAM は維持(Persist)されます。上図で言えば、図3では IP がマルチプレクサーで、hme がデバイスになり、I_PLINK を使って、IP マルチプレクサーの下方に、hme の STREAM を接続しています。ここで、IP の FD をクローズすると、元々の IP の STREAM はなくなってしまいますが、IP マルチプレクサを含む hme のストリームは残り、図4のようになります。 図のように、頂点に IP マルチプレクサが残ったままになっていますが、今回作成した brdg モジュールは IP マルチプレクサへのデータの転送は行いません。また、IP も brdg モジュールに対してデータを転送してくることもありません。つまり、図5の頂点にいる IP は、hme の STREAM を維持する(パーシストリンクする)という目的のため「だけ」に利用したものに過ぎず、以降はまったくもって出番はありません。

次に、brdg モジュールですが、STREAMS モジュールとしての骨格は Firewall モジュールとほぼ同じです、違うのはデータ(メッセージ)を受け取るときに呼ばれる put ルーチン内の処理です。 brdg モジュールではオープンされているネットワークインターフェース毎の情報を port_t という構造体で管理し、また、受け取ったパケットの発信元イーサネットアドレスを source_entry_t という構造体で保持しています。パケットを受け取った brdg モジュールは、これらの情報をもとに、どのネットワークインターフェースにパケットを転送すべきかを決定し、そのネットワークインターフェースドライバの write 側の put ルーチンにパケットを渡しています。パケットの処理の決定フローは以下のとおりです。

flow.png

ソースファイルとダウンロード

ソースファイル: bridge-0.1.tar.gz

tar ファイルに含まれるファイルの概要
Makefilebrdg モジュール、brdgadm コマンド用の makefile
brdg.cbrdg モジュールの本体。STREAMS モジュール。
あるネットワークインターフェースに到着したパケットを他のネットワークインターフェースへ転送する。
brdgadm.cbrdg モジュールのための設定用コマンド。
STREAM の作成、パーシストリンク、モジュールの PUSH を行う。
brdgadm -a
brdgadm -d

プログラムの解説

brdgadm.c

コードの大部分は、基本的には Solaris DLPI プログラミングで紹介している Packet Monitor のものと同じですのでこちらの解説を参考にしていただければと思います。brdgadm コマンドでは起動されるときのオプション(-a または -d)によって、add_interface() もしくは delete_interface() のどちらかが呼ばれることになります。
add_interface() の中ではネットワークインターフェースのデバイスファイルをオープンして、DLPI のコマンドを発行していますが、BIND 要求と PROMISCUOUS モードの設定が Packet Monitor プログラムの時と違っています。

   /*
    * SAP(Service Access Point)を 0 にして bind 要求
    */
   dlbindreq (if_fd, 0, 0, DL_CLDLS, 0, 0);
   dlbindack (if_fd, buf);
   /*
    * PROMISCOUS モードへ。DL_PROMISC_SAP、DL_PROMISC_PHYS どちらも ON
    */
   dlpromisconreq(if_fd, DL_PROMISC_SAP);
   dlokack(if_fd, buf);
   dlpromisconreq(if_fd, DL_PROMISC_PHYS);
   dlokack(if_fd, buf);

man hme によると、SAP 0 にバインドすることによって、Ethernet ヘッダのタイプフィールドが0〜1500の範囲のパケットが配信されるようになるとあります(下記)。しかしながら、バインド後にDL_PROMISC_SA フラグをセットしてプロミスキュアスモードリクエストを発行しているので、本当ならこのプロミスキュアスモードリクエストですべての SAP 値のパケットが配信されるはずなので、SAP 値0でバインドすることにどれくらい意味があるのかは分かりません。しかしながら、ここで SAP 0にバインドしておかないとパケットの受信ができません。

===== docs.sun.com hme の man page より ================
ユーザーが sap の値として 0 を選択すると、受信側は 802.3モードになります。タイプフィールドが [0-1500] の範囲内にある媒体から受信したすべてのフレームは 802.3 フレームとみなされ、sap値 が 0 のすべての開いているストリームに配信されます。複数のストリームが 802.3 モードにある場合は、フレームは複製され、DL_UNITDATA_IND メッセージとして複数のストリームに配信されます。
=====================================================

Packet Monitor では DL_PROMISC_PHY フラグをセットしてプロミスキュアスモードリクエストしていましたが、これだけではバインドしている SAP 値(Packet Monitor の場合 0x800=IPフレーム)のパケットしか配信されないため、ハブとして動作するためには不十分です。そのため、DL_PROMISC_SA フラグをセットしたプロミスキュアスモードリクエストも発行し、すべての SAP 値のパケットが配信されてくるようにしています。

   /*
    * ip の stream に interface の stream を LINK
    * (PLINK = persist link)
    */
   mux_id = ioctl(ip_fd, I_PLINK, if_fd);
   if( mux_id < 0){
       perror("I_PLINK ioctl failed");
       exit(0);
   }
   printf("mux_id = %d\n", mux_id);
   printf("Please write down mux_id.\nThis mux_id will be used for -d (=delete)\n");

add_interface() のなかに、先ほどお話した I_PLINK(パーシストリンク) ioclt コマンドを発行している部分がありますが、この ioctl() は返り値として mux_id という整数値を返してきます。この mux_id は IP マルチプレクサーが、接続されている STREAM を認識するためのユニークな ID で、後々、接続された STREAM を取り外すときに必要になります。先ほどパーシストリンクの説明のところでお話したように、brdgadm コマンドで作成された STREAM は関連したプロセスが存在しないので、ユーザはこの STREAM を制御することができず、STREAM を削除することもできません。 そのため、この STREAM を削除するためには、別途 IP の STREAM をオープンして、I_UNPLINK iolct コマンドを発行して IP マルチプレクサーにSTREAMを削除するように依頼するのです。このとき削除する STREAM を指定するためにこの muxid を与えます。 brdgadm コマンドでは -d オプションによって呼ばれる delete_interface() がこれを行っています。

   if ((ip_fd = open("/dev/ip", O_RDWR)) < 0)
       syserr("/dev/ip");
   if (ioctl(ip_fd, I_PUNLINK, mux_id) < 0) {
       perror("I_PUNLINK ioctl failed");
       exit(0);
   }

I_PUNLINK ioctl で指定している mux_id は brdgadm コマンドの -d オプションの引数として与えられるものです。これによりパーシストリンクしていたネットワークインターフェースの STREAM がアンリンク(リンク解除)されます。

brdg.c

brdg モジュールは brdgadm コマンドによって追加されたネットワークインターフェースを port_s という構造体で管理しています。port_s 構造体は brdgadm コマンドによって追加されたネットワークインターフェースのドライバの STREAM にある brdg モジュールのインスタンスの read queue のアドレスをメンバーとして持っています。

   typedef struct port_s
   {
       queue_t  *qptr;
       char     *ifname;
   }port_t;
   port_t port_list[MAXPORT];

この port_s 構造体は MAXPORT を最大数とした配列になっていて0〜MAXPORTまでのネットワークインターフェースが追加できます。

また、brdg モジュールは受信したパケットの発信元 Ethernet アドレスを source_hash_table というハッシュテーブルで保持しています。ハッシュテーブルの各エントリは source_entry_s という構造体で、発信元 Ethernet アドレス、受信したネットワークインターフェースを示す port_t 構造体をメンバーとして持っています。これによって発信元 Ethernet アドレスを確認することで、パケットがどのネットワークインターフェースから入ってきたかが分かるので、受信したネットワークインターフェースに対して同じパケットを再度流すことを防ぐことができます。

   typedef struct source_entry_s
   {
       struct  ether_addr ether_source;
       port_t  *port;
       int     state;
   } source_entry_t;
   source_entry_t source_hash_table[MAXHASH];

ハッシュテーブルの最大値は MAXHASH で決まっており、現在は 1024 としています。 Ethernet アドレスからハッシュ値を生成しているので、まれに異なる2つの Ethernet アドレスが同一のエントリを指してしまうことがあり得ます。その場合コリジョンが発生した旨が syslog に通知されます。このような事態はあんまり発生しないとはおもいますが、頻発する場合には MAXHASH 値をもっと大きくする必要があります。

インストール

コンパイル&インストール

コンパイルには gcc を使います。make をそのまま実行した場合 64 bit モジュールである brdg と 32 bit モジュールである brdg32、brdgadm コマンドをを生成します。

# make							  
/usr/local/bin/gcc -g -D_KERNEL -c  brdg.c  -o brdg32.o	  
/usr/ucb/ld -dn -r brdg32.o -o brdg32			  
/usr/local/bin/gcc -g -D_KERNEL -c  brdg.c -m64		  
/usr/ucb/ld -dn -r brdg.o -o brdg			  
/usr/local/bin/gcc -g -lnsl -lsocket brdgadm.c -o brdgadm 

brdg モジュールのインストールおよびカーネルへのロードは make install で行います。

# make install
/bin/cp brdg /kernel/strmod/sparcv9/brdg
/usr/sbin/modload brdg

32 bit カーネルで起動されていて、32 bit モジュールをインストールする場合には make install32 を実行します。

# make install32
/bin/cp brdg32 /kernel/strmod/brdg
/usr/sbin/modload brdg

アンインストール

モジュールのアンインストールは make uninstall もしくは make uninstall32 で行えます。

# make uninstall
/bin/rm /kernel/strmod/sparcv9/brdg
/usr/sbin/modunload -i `/usr/sbin/modinfo | awk '/brdg/{ print  }'`

同時に brdg モジュールのアンロードも行われますが、brdg モジュールを利用中の STREAM が存在していると、アンロードは失敗します。brdgadm コマンドを使って brdg モジュールを使っている STREAM をすべて削除するか、もしくはシステムを再起動してください。

# make uninstall
/bin/rm /kernel/strmod/sparcv9/brdg
/usr/sbin/modunload -i `/usr/sbin/modinfo | awk '/brdg/{ print  }'`
can't unload the module: Device busy
make: *** [uninstall] Error 16

使い方

インストールが終わってもモジュールのロードが終わっただけで、brdg モジュールはまだ利用可能状態にありません。brdgad コマンドをつかって、ハブのポートとして追加したいネットワークインターフェースを指定してください。

Usage: brdgadm [-a <interface> | -d mux_id ]

インターフェースの追加

インターフェースの追加には brdgad の -a オプションを使います。

例)

# ./brdgadm -a hme0			  
devname = hme, ppa = 0 devpath = /dev/hme 
mux_id = 11				  
Please write down mux_id.		  
This mux_id will be used for -d (=delete) 
				  
# ./brdgadm -a le0			  
devname = le, ppa = 0 devpath = /dev/le	  
mux_id = 12				  
Please write down mux_id.		  
This mux_id will be used for -d (=delete) 

上記の例では、hme0 と le0 をハブのポートとして追加しています。-a オプションが正常に終了すると、上記のプログラムの解説のところででお話した mux_id が出力されます。上の例ではパーシストリンクされた hme0 の STREAM の mux_id は「11」で、le0 の STREAM は「12」です。これらは次の「インターフェースの削除」で使われます。

インターフェースの削除

インターフェースの削除を行うには brdgadm の -d オプションを使います。

例)

# ./brdgadm -d 11
mux_id 11 is being deleted...
Done

# ./brdgadm -d 12
mux_id 12 is being deleted...
Done
  • d オプションの引数として先ほどの mux_id を指定しています。最初の例は 11 を指定しているので hme0 の STREAM を削除しています。12 を指定しているのが le0 の STREAM を削除しています。

注意

brdgadm コマンドで指定するネットワークインターフェースは ifconfig にて設定されていないインターフェースを想定しています。上記の例では ifconfig -a を実行すると、以下のようになり、hme0、le0 ともに 使われていないことが分かります。

# ifconfig -a
lo0: flags=1000849 mtu 8232 index 1
       inet 127.0.0.1 netmask ff000000
#

ifconfig で plumb されている(UP/DOWNかかわらず)ネットワークインターフェースを brdgadm で追加した場合、予想外の自体が発生する恐れがあります。(予想外といっているのは、本当にどう挙動するか分からないとうだけです・・想像できる不具合があるってわけでは無いです・・どうということはないかもしれません)

今後の課題

  1. あて先 Ethernet アドレスを見てパケットを転送するポートを特定できるようにする。(今は受信したポート以外すべてに転送しているので) ・・というか、これができてないならスイッチングハブじゃなくて、シェアードハブですよね。まだ。
  2. brdgadm コマンドで mux_id を意識せずにネットワークインターフェースの削除ができるようにする。(どっかに muxi_id を保持しておいて、インターフェース名で拾ってこれるようにしたい)

トップ   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2022-01-03 (月) 20:23:56