Solaris でファイアーウォールを作ろう/其の1

目次

概要

最近では自宅で ADSL などを利用して常時接続を実現されている方も多いのではないかと思います。常時接続の場合モデムでの接続と違い、ダイナミックADSLサービス等を利用して自宅サーバを公開できるなどのメリットがありますが、その反面、常にインターネットに自宅のマシンを公開していることになり、ハッカーからの攻撃されたらどうしよう?と考えると夜も眠れません(私だけ?)そのために、ブロードバンドルーターには簡易ファイアーウォール機能がついているものがほとんどです。しかし、あくまで簡易ファイアーウォールなので、ちょっとこったルール設定したいと思っても、そのようなルールが作れません。企業用のファイアーウォールソフト(CheckPoint 社の FireWall-1等)では、考えられるさまざまな条件のルールが設定できますが、高くてとても手が出ません。というわけで、今回は「自分の望みのフィルタリングができる」を目標にファイアーウォールを作ってみたいと思います。

Solaris 9 では SunScreen 3.2 というファイアーウォールソフトがバンドルされています。Solaris 8 では SunScreen Lite 3.1 というファイアーウォールソフトがフリーで手に入りますね。また、コードが公開されているものでは、IP Filter というファイアーウォールソフトがあります。
「まじめに」?Solaris にファイアーウォールを導入されようとされている方はこれらのソフトを使いましょう!ここでは、ファイアーウォールの実現方法の探究を目的にしています。

以前に DLPI を利用したパケットモニタープログラムを紹介しましたが、今度はこれをさらに推し進めて自作のファイアーウォールに挑戦しみます。

ご注意

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

ストリームの概要

以前にも書きましたが Solaris では各ネットワークプロトコルが STREAMS(ストリーム)機構 というしくみで作られています。TCP やIP などのプロトコル STREAMS 機構の共通のプログラミング規則に沿って作られており、各プロトコルは STREAMS module (ストリームモジュール)という形で実装されています。
下図は TCP をつかっているプロセスからネットワークデバイスドライバまでのデータの流れをモデル化してあらわしたものです。

tcpip.gif


  • ストリームヘッド
    ストリームヘッドはカーネル内のSTREAMSモジュールとユーザプロセスとの橋渡しをする役割を持っています。(カーネルのメモリー空間とユーザプロセスのメモリ空間のやりとりの仲介をしています)
  • ダウンストリーム
    図の上から下へのデータの流れる方向。
    ユーザプロセスから書き込まれたデータは右図の下方に向かってデバイスドライバまで運ばれ、ネットワークに流れていきます。
  • アップストリーム
    図の下から上へデータが流れる方向。
    逆にネットワークから来るデータは、デバイスドライバから上方に向かってユーザプロセスまで運ばれていきます。
  • TCPモジュールと IPモジュール
    この2つのモジュールがいわゆる TCP/IP プロトコルのさまざまな機能を提供しています。(ルーティング、パケット再送など)
  • ストリームデバイスドライバ
    ハードウェア(ネットワークカード)とデータのやり取りを行うデバイスドライバです。


各モジュール/ドライバ間のデータのやり取りにはそれぞれ取り決めがあり、その約束にしたがってデータのやり取りを行います。たとえば、ストリームヘッドと TCPモジュール間は トランスポートインターフェース(TI)という取り決めがあり、デバイスドライバと IP モジュールの間にはデータリンクプロバイダーインターフェース(DLPI)という取り決めがあります。TCPモジュールとIPもジュール間については公開ドキュメントに記述がありません。Solaris 内部独自の取り決めでデータのやりとりをしているものと思われます。

STREAMS については Sun のドキュメントページにあるSTREAMS Programming Guide が参考になると思います。残念ながら英語版しかありません。日本語化してくれると大変ありがたいのですが・・。

プログラムのイメージ

上図のとおり、ネットワークから来るデータは、デバイスドライバ -> IPモジュール -> TCPモジュールという風に進んでいきます。ここで、デバイスドライバ と IPモジュールの間自分で作ったモジュールを挿入できれば、IP にデータを渡すも破棄するも自由ということになります。つまりファイアーウォールもどきのことができます。

さて、肝心の追加する自作 STREAMS モジュールですが、これは Sun のページでいくつかサンプルのコードが掲載されているので、これを元に作っていきます。簡単なものなら意外に短いコードでできます。 以下のコードで実現しているのは、ソースコード内で指定した特定の IP アドレス(172.29.73.88)から来たパケットを強制的に破棄し、syslog にロギングするというものです。とりあえず最初のテストプログラムなので実用性は低い(無い?)かも知れませんが、このパケットの破棄はカーネルレベルで行われるため、アプリケーションも、はたまた TCP モジュールさえもこの IP アドレスからくるデータを受け取ることはできません。立派にファイアーウォールです。(笑)

STREAMS モジュールをつくる上必ずで必要となる「エントリーポイント」について説明します。これは、自分が作ったモジュールを KERNEL が利用できるようにするための決まり事のようなものです。KERNEL に対してこの約束事を守ってさえいれば、モジュールそのものの中身の処理は自分で勝手に作れます。

  • コンフィグレーションエントリーポイント
    これは、KERNEL がモジュールを ロードしたり、アンロードしたりするときに必要になるルーチンです。
    これらには _init(9E)、_info(9E),、_fini(9E). があります。
_init(9E)モジュールの初期化ルーチン
_info(9E)モジュール情報取得ルーチン
_fini(9E)モジュール終了処理ルーチン

これらは絶対必要です。・・・が下記のソースのように大して書くことはありません。重要なのは、それぞれの中で使われている modlinkage という構造体です。この構造体はモジュールの特性を決めるさまざまな情報へのリンク情報が入っていて、kernel はこれをたどってモジュールがどのような特徴をもっていて、どのような呼び出しをサポートしているのかを知ることができるので、きわめて重要です。 ソース上では分かりにくいかも知れませんが、modlinkage 構造体は以下のようにさまざまな構造体へのリンクを形成しています。

   modlinkage (modlinkage構造体)
      |
    +--- modlstrmod (modlstrmod構造体)
             |
             +--- mod_strmodops 
             |
             +--- fw_fmodsw (fmodsw構造体)
                      |
                      +--- fwallinfo (streamtab構造体)
                               |
                               +--- rinit(qinit構造体)
                               |      |
                               |      +--- fwall_rput()
                               |      +--- fwall_open()
                               |      +--- fwall_close()
                               |      +--- minfo
                               |     
                               +--- winit (qinit構造体)
                                      |
                                      +--- fwall_rput()
                                      +--- minfo

途中の構造体群の説明は省きます。man page や Sun のページ を参照してください。
このツリーの最後に出てくる fwall_rput, fwall_open, fwall_close, fwall_rput について説明します。

fwall_open()このモジュールのオープンルーチン。モジュールが open されたときに呼ばれる。
fwall_close()このモジュールのクローズルーチン。モジュールが close されるときに呼ばれる。
fwall_rput()下方のモジュール(上図ではデバイスドライバ)から渡された packet を処理するルーチン
fwall_wput()上方のモジュール(上図では IP モジュール)から渡された packet を処理するルーチン

open、close のルーチンはこのモジュールでは特に何もやってません。open された instance 毎の固有の情報を格納する q_ptr という構造体へのポインタをセットしたりするのですが、このモジュールでは利用していないので NULL をセットしています。
このモジュールが「何をするか?」を定義する一番重要なところが fwall_rput と fwall_wput と呼ばれるルーチンです。これらは、他のモジュールから渡された packet に対して行う処理を記述するルーチンで、今回の場合はまさに packet を破棄するかどうかの判定を行っています。なお、本来上から下、つまりアプリケーションがネットワークに送信する packet も調査すべきですが、このモジュールでは下から上、つまりネットワークから受信した packet のみを調査しています。そのため、fwall_wput は何もしていません。
fwall_rput() も fwall_wput() も最後に putnext() という関数を呼び出しています。これは次のモジュールに packet を渡すための関数です。
fwall_rput() が putnext() を呼んだ場合、次のモジュールは IP モジュールとなり、IP モジュールに packet を渡すことになります。

ソースコード

    1  /* Simple filter
    2  * /usr/local/bin/gcc -D_KERNEL fwall.c -c
    3  * ld -dn -r fwall.o -o fwall
    4  */
    5
    6  /* added headers */
    7  #include <sys/modctl.h>
    8
    9  /* sample headers */
   10  #include <sys/types.h>
   11  #include <sys/param.h>
   12  #include <sys/stream.h>
   13  #include <sys/stropts.h>
   14  #include <sys/ddi.h>
   15  #include <sys/sunddi.h>
   16
   17  /* header's for network */
   18  #include        <netinet/in.h>
   19  #include        <sys/types.h>
   20  #include        <sys/socket.h>
   21  #include        <sys/stropts.h>
   22  #include        <sys/dlpi.h>
   23  #include        <fcntl.h>
   24  #include        <sys/signal.h>
   25  #include        <sys/stream.h>
   26  #include        <net/if.h>
   27  #include        <netinet/if_ether.h>
   28  #include        <netinet/in_systm.h>
   29  #include        <netinet/tcp.h>
   30  #include        <netinet/ip.h>
   31
   32  #define REJECTADDR 0xac1d4958 /* 172.29.73.88 */
   33
   34
   35  static int fwall_open (queue_t*, dev_t*, int, int, cred_t*);
   36  static int fwall_rput (queue_t*, mblk_t*);
   37  static int fwall_wput (queue_t*, mblk_t*);
   38  static int fwall_close (queue_t*, int, int, cred_t*);
   39
   40  static struct module_info minfo =
   41  { 0xdefe, "fwall", 1, INFPSZ, 512, 128 };
   42
   43  static struct qinit rinit = {
   44    fwall_rput, NULL, fwall_open, fwall_close, NULL, &minfo, NULL};
   45
   46  static struct qinit winit = {
   47    fwall_wput, NULL, NULL, NULL, NULL, &minfo, NULL};
   48
   49  struct streamtab fwmdinfo={
   50    &rinit, &winit, NULL, NULL};
   51
   52  static struct fmodsw fw_fmodsw ={
   53    "fwall", &fwmdinfo, D_NEW | D_MP |D_MTQPAIR };
   54
   55  struct modlstrmod modlstrmod ={
   56    &mod_strmodops, "simple module for test", &fw_fmodsw };
   57
   58  static struct modlinkage modlinkage ={
   59    MODREV_1, (void *)&modlstrmod, NULL };
   60
   61  _init()
   62  {
   63          return (mod_install(&modlinkage));
   64  }
   65
   66  _info(modinfop)
   67          struct modinfo *modinfop;
   68  {
   69          return (mod_info(&modlinkage, modinfop));
   70  }
   71
   72  _fini(void)
   73  {
   74          return (mod_remove(&modlinkage));
   75  }
   76
   77
   78  static int fwall_open (queue_t* q, dev_t *dev, int oflag, int sflag, cred_t *cred)
   79  {
   80    if (sflag != MODOPEN)
   81      return EINVAL;
   82    q->q_ptr = WR(q)->q_ptr = NULL;
   83    qprocson(q);
   84    return (0);
   85  }
   86
   87  static int fwall_close (queue_t *q, int flag, int sflag, cred_t *cred)
   88  {
   89    qprocsoff(q);
   90    q->q_ptr = WR(q)->q_ptr = NULL;
   91    return(0);
   92  }
   93
   94  static int
   95  fwall_wput(queue_t *q, mblk_t *mp)
   96  {
   97    putnext(q, mp);
   98    return (0);
   99  }
  100
  101  static int
  102  fwall_rput(queue_t *q, mblk_t *mp)
  103  {
  104    struct ip *ip;
  105
  106    if (mp->b_datap->db_type == M_DATA) {
  107      u_char  *rptr = mp->b_rptr;
  108      int     dlen =  mp->b_wptr - mp->b_rptr;
  109      char msg[100];
  110
  111      ip = (struct ip *)&rptr[0];
  112
  113      sprintf(msg, "%d.%d.%d.%d -> %d.%d.%d.%d Len:%d Protocol:%d",
  114              ip->ip_src._S_un._S_un_b.s_b1, ip->ip_src._S_un._S_un_b.s_b2,
ip->ip_src._S_un._S_un_b.s_b3, ip->ip_src._S_un._S_un_b.s_b4,
  115              ip->ip_dst._S_un._S_un_b.s_b1, ip->ip_dst._S_un._S_un_b.s_b2,
ip->ip_dst._S_un._S_un_b.s_b3, ip->ip_dst._S_un._S_un_b.s_b4,
  116              ip->ip_len, ip->ip_p);
  117      cmn_err(CE_CONT, "%s",msg);
  118
  119      if( ip->ip_src._S_un._S_addr == REJECTADDR){
  120        freemsg(mp);
  121        cmn_err(CE_CONT, "Packet Dropped");
  122        return(0);
  123      }
  124
  125    } /* if M_DATA */
  126
  127    putnext(q, mp);
  128    return (0);
  129  }

ソースファイル fwall.c

使い方

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

コンパイルは本当は kernel モジュールなので Sun の compiler を使ったほうがよいと思うのですが、そんなもの無いので gcc を使いました。あと、私のマシンが sun4m マシンなので、作るモジュールは 32bit モジュールです。最近の sun4u マシンを使って 64bit kernel をあげている人は 64bit モジュールを作る必要があります。Sun の sparc compiler などであれば -xarch=v9 というオプションを使えば、64bit モジュールができるはずです。gcc の version 3.2.2 では -m64 というオプションで 64bit モジュールを作成できます。(其の2 を参照してください。)

# /usr/local/bin/gcc fwall.c -D_KERNEL -c
# /usr/ucb/ld -dn -r fwall.o -o fwall

あとは、出来上がった fwall モジュールを しかるべきところにコピーしておきます。

# /bin/cp fwall /kernel/strmod/fwall

sun4u マシンで 64bit kernel をつかっていて、64bit モジュールを作成した場合にはコピーする path がちょっと変わります。

# /bin/cp fwall /kernel/strmod/sparcv9/fwall

ファイアーウォールモジュールをロードする

上でインストールしたモジュールをロードします。これは modload コマンドを使います。モジュールが正しい位置にコピーされていれば、モジュールの path を指定する必要はありません。

# /usr/sbin/modload fwall

続いて、ロードしたモジュールをデバイスドライバ と IPモジュールの間に挿入します。これは ifconfig コマンドを使います。最後の @2 というのはモジュールの追加位置です。 普通は @2 で大丈夫です。

# ifconfig le0 modinsert fwall@2

モジュールが追加されているかどうかを確認します。

# ifconfig le0 modlist
0 arp
1 ip
2 fwall <------ le(デバイスドライバ)と IP モジュールの間にいます!
3 le

比較のため、追加される前は以下のように見えます。

# ifconfig le0 modlist
0 arp
1 ip
2 le

上記手順がちょっと面倒くさいので Makefile を作って一気に行えるようにしました。

# make
# make install

・・で、コンパイル、ロード、モジュールの追加まで一気に行います。

# make uninstall

・・ で、モジュールの取り外し、アンロード、アンインストールを一気に行います。これで元の状態にもどります(のはず!)

    1  CC = /usr/local/bin/gcc
    2  PRODUCTS =  fwall
    3  AUTOPUSH = /etc/autopush
    4  ECHO = /bin/echo
    5  CP = /bin/cp
    6  RM = /bin/rm
    7  LD = /usr/ucb/ld
    8  RM = /bin/rm
    9  CAT = /bin/cat
   10  AWK = /bin/awk
   11  MODLOAD = /usr/sbin/modload
   12  MODUNLOAD = /usr/sbin/modunload
   13  MODINFO = /usr/sbin/modinfo
   14
   15
   16  all: $(PRODUCTS)
   17
   18  clean:
   19          rm -f fwall fwall.o
   20
   21  fwall: fwall.c
   22          $(CC) fwall.c -D_KERNEL -c
   23          $(LD) -dn -r fwall.o -o fwall
   24
   25  install:
   26          -$(CP) fwall /kernel/strmod/fwall
   27          $(MODLOAD) fwall
   28          ifconfig le0 modinsert fwall@2
   29
   30  uninstall:
   31          ifconfig le0 modremove fwall@2
   32          -$(MODUNLOAD) -i `$(MODINFO) | $(AWK) '/fwall/{ print $1 }'`
   33          -$(RM) /kernel/strmod/fwall

これで、あなたのマシンにも自家製ファイアーウォールが導入されました(笑)。 このコードでは 32 行目で指定している 172.29.73.88 というアドレスからきた packet を破棄します。

   32  #define REJECTADDR 0xac1d4958 /* 172.29.73.88 */

アドレスは比較が簡単なように16進で表現しました。ここを貴方の「拒否したい」アドレスに変更すれば、このモジュールにてそのアドレスからのパケットは破棄されます。
あと、ひとつの IP アドレスからのパケット破棄だけではちょっと機能的に寂しかったので、受け取った全 IPパケットのログを syslog に吐き出すようにしています。全パケット毎にログ書くので結構負荷高いです。ログの必要が無ければ、113〜117行は削除しても大丈夫です。

  113      sprintf(msg, "%d.%d.%d.%d -> %d.%d.%d.%d Len:%d Protocol:%d",
  114              ip->ip_src._S_un._S_un_b.s_b1, ip->ip_src._S_un._S_un_b.s_b2,
  115              ip->ip_dst._S_un._S_un_b.s_b1, ip->ip_dst._S_un._S_un_b.s_b2,
  116              ip->ip_len, ip->ip_p);
  117      cmn_err(CE_CONT, "%s",msg);

次回はもーちょっと機能を充実したものに挑戦してみます。


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