Solaris DLPI プログラミング/パケットモニター

目次

概要

まず最初に白状しますが、ここで書いているプログラムの コアの部分は全くもって私のオリジナルではありません。internet 上にある DLPI をつかったプログラムを参考にして切り張りして作っています。ただ、いくつかのプログラムを見たところ、ほぼおんなじ source をつかっており、ものは違ってもつかっている関数名がまったく同じだったり、関数の中見が同じだったりするので、おそらくどこかにたたき台となっている source が存在するのでしょう。(tcpdump で利用する libpcap も似たようなものです)ここは開き直ってプログラムの簡略化と、説明とに注力したいと思います。
DLPI とは Data Link Provider Interface の略で solaris が提供しているデータリンク層のプロトコルに直接アクセスするためのインターフェースです。つまり DLPI を使えば socket プログラムの SOCK_RAW で入手していたような生パケットが送受できるって事です。かつ、データリンク層に直接アクセスできるわけですから、Ethernet ヘッダーさえもを自由に送受できると言う事です。これはなかなか夢が膨らむインターフェースです。
DLPI をつかえばいろいろ面白いものが出来そうですが、ここでは例によって 400 行足らずの簡易なパケットモニター(packet scanner, packet sniffer ?) をつくってみます。

プログラムのイメージ

このプログラムの main() の流れを以下に示します。最後の getmsg と print_pkt を loop しているだけで他は一直線なプログラムなので流れは分かりやすいかと思います。 PPA と SAP ってのが出てきますが、PPA は network Interface の instance 番号( /dev/hme0 の 0)のことで、SAP ってのは Ethernet のフレームタイプです。Ethernet フレームタイプには IP(0x0800)や、ARP(0x0806)などがあります。このプログラムでは フレームタイプとして IP(0x0800)を指定しています。

流れ解説
open()device(network Interface)をオープン
dlattachreq()オープンしたデバイスの PPA(physica point of attachment)に STREAM をアタッチ(関連付け)する
dlokack()上記の要求の ACK(確認応答)を受け取る。
dlpromisconreq()promiscast(無差別受信)モード へ移行を要求
dlokack()上記の要求の ACK を受け取る。
dlbindreq()データリンク層の SAP(service access point)を STREAM に BIND(関連付け)する。
dlokack()上記の要求の ACK を受け取る。
strioctl()ioctl() を通じて device を RAW モードに移行するよう要求。
ioctl(,I_FLUSH,FLUSHR)ioctl() を通じてSTREMA の READ QUEUE のフラッシュ を要求。
getmsg()STREAM の READ QUEUE から生データをを読み込む。(以下繰り返す)
print_pkt()あとは、読み込んだデータを Ether、IP、TCP の各構造体に当てはめて表示するだけ、、

ソースコード

    1  /*********************************************
    2   * sniff.c
    3   *
    4   * packet monitor using DLPI
    5   *
    6   * gcc sniff.c -lnsl -o sniff
    7   *
    8   ********************************************/
    9  #Include <netinet/in.h>
   10  #include <sys/types.h>
   11  #include <sys/socket.h>
   12  #include <sys/stropts.h>
   13  #include <sys/dlpi.h>
   14  #include <fcntl.h>
   15  #include <stdio.h>
   16  #include <sys/signal.h>
   17  #include <sys/stream.h>
   18  #include <string.h>
   19  #include <net/if.h>
   20  #include <netinet/if_ether.h>
   21  #include <netinet/in_systm.h>
   22  #include <netinet/tcp.h>
   23  #include <netinet/ip.h>
   24  #include <sys/varargs.h>
   25  #include <errno.h>
   26  #include <unistd.h>
   27
   28  #define  ERR_MSG_MAX 300
   29  #define  MAXDLBUFSIZE  8192
   30  #define  DLBUFSIZE     8192
   31  #define  TCP_DATA_PRINT_LENGTH  100
   32  #define  PRINT_MAC_ADDR(ether_addr_octet)     {         \
   33          int i;                                          \
   34          for ( i =0; i < 6; i++){                        \
   35              printf("%02x",(ether_addr_octet[i]));       \
   36              if(i != 5)                                  \
   37                  printf(":");                            \
   38          }                                               \
   39      }
   40
   41  void   print_packet(caddr_t,  int);
   42  void   print_usage(char *);
   43  int    dlattachreq(int, t_uscalar_t, caddr_t );
   44  int    dlpromisconreq(int, t_uscalar_t, caddr_t);
   45  int    dlbindreq(int, t_uscalar_t, t_uscalar_t, uint16_t, uint16_t, t_uscalar_t, caddr_t);
   46  void   print_err(int , char *, ...);
   47  int    dldetachreq(int , caddr_t);
   48  int    dlpromiscoffreq(int, t_uscalar_t, caddr_t);
   49
   50  int
   51  main(int argc, char *argv[])
   52  {
   53      char                   buf[DLBUFSIZE]; // getmsg(2) で利用するバッファ
   54      struct strbuf          databuf; // getmsg(2) に引数として渡す構造体
   55      union  DL_primitives  *dlp;
   56      int                    flags = 0;
   57      int                    ppa;  // NIC のインスタンス番号に相当(Physical Point ofAttachment)
   58      int                    sap;  // Ethernet フレームタイプに相当(Service Access Point)
   59      int                    fd;   // デバイスをオープンしたファイル記述子
   60      struct  strioctl       strioc; // stream デバイス用の ioctl(2) のコマンドを指定する構造体
   61      char                  *interface; // 引数で渡されたインターフェース名 (例: hme0)
   62      char                   devname[30] = {0};  // interface 名から instance を取ったもの (例:hme)
   63      char                   devpath[30] = {0};  // open() 時に指定する device file への path (例: /dev/hme)
   64      char                  *instance;  // interface の instance 番号。ppa となる (例: 0)
   65
   66      if (argc != 2)
   67          print_usage(argv[0]);
   68
   69      interface = argv[1];
   70      if ((instance = strpbrk(interface, "0123456789")) == NULL){
   71          fprintf(stderr, "%s: no instance specified\n", interface);
   72          print_usage(argv[0]);
   73      }
   74
   75      ppa = atoi(instance);
   76
   77      strncpy(devname, interface, instance - interface);
   78
   79      sprintf(devpath, "/dev/%s",devname);
   80
   81      /*
   82       * デバイスをオープンする
   83       */
   84      if((fd = open (devpath , O_RDWR)) < 0 ){
   85          perror("open");
   86          exit(1);
   87      }
   88
   89      /*
   90       * PPA(instance) にアッタチする
   91       */
   92      if(dlattachreq(fd, ppa, buf) < 0){
   93          fprintf(stderr, "%s: no such instance\n", interface);
   94          print_usage(argv[0]);
   95      }
   96
   97      /*
   98       * プロミスキャスモードをセット
   99       */
  100      if(dlpromisconreq(fd, DL_PROMISC_PHYS, buf) < 0){
  101          fprintf(stderr, "%s: Cannot set promiscuous mode\n", interface);
  102          exit(1);
  103      }
  104
  105      /*
  106       * SAP(flame type) にバインド
  107       */
  108      if(dlbindreq (fd, ETHERTYPE_IP, 0, DL_CLDLS, 0, 0, buf) < 0){
  109          fprintf(stderr, "%s: Cannot bind to ETHERTYPE_IP\n", interface);
  110          exit(1);
  111      }
  112
  113      /*
  114       * RAW モードにセット
  115       */
  116      strioc.ic_cmd    = DLIOCRAW;
  117      strioc.ic_timout = -1;
  118      strioc.ic_len    = 0;
  119      strioc.ic_dp     = NULL;
  120      if(ioctl(fd, I_STR, &strioc) < 0){
  121          perror("ioctl: I_STR: DLIOCRAW");
  122          exit(1);
  123      }
  124
  125      /*
  126       * キューをフラッシュ
  127       */
  128      if (ioctl(fd, I_FLUSH, FLUSHR) < 0){
  129          perror("ioctl: I_FLUSH");
  130          exit(1);
  131      }
  132
  133      databuf.maxlen = MAXDLBUFSIZE;
  134      databuf.len = 0;
  135      databuf.buf = (caddr_t)buf;
  136
  137      while (getmsg(fd, NULL, &databuf, &flags) == 0) {
  138          if (databuf.len > 0)
  139              print_packet(databuf.buf, databuf.len);
  140      }
  141
  142      perror("getmsg");
  143      exit(1);
  144  }
  145
  146  void
  147  print_packet(caddr_t buf, int len)
  148  {
  149      struct ether_header   *ether;
  150      struct tcphdr         *tcp;
  151      struct ip             *ip;
  152      u_char                *tcpdata;
  153      int                    etherlen;
  154      int                    iphlen;
  155      int                    iptotlen;
  156      int                    tcphlen;
  157      int                    tcpdatalen;
  158
  159      etherlen = sizeof(struct ether_header);
  160      ether = (struct ether_header *)buf;
  161
  162      /*
  163       * Ether Type が IP(0x800) じゃなかったらリターン
  164       */
  165      if(ether->ether_type != ETHERTYPE_IP)
  166          return;
  167
  168      /*
  169       * 全フレームサイズがデフォルト(最小)の ether, ip, tcp
  170       * ヘッダー長の合計よりも小さかったら不正パケットとみなして
  171       * リターン。
  172       */
  173      if(len < etherlen + sizeof(struct ip) + sizeof(struct tcphdr) )
  174          return;
  175
  176      /*
  177       * アライメントエラーを避けるため、IP ヘッダー以降を、別途確保
  178       * したメモリにコピーする。
  179       */
  180      ip = (struct ip *)malloc(len);
  181      memcpy(ip, buf + etherlen, len);
  182
  183      /*
  184       * TCP でなければリターン
  185       */
  186      if(ip->ip_p != IPPROTO_TCP)
  187          goto error;
  188
  189      iphlen = ip->ip_hl << 2;
  190
  191      /*
  192       * パケット内で申告されている IP ヘッダー長が分かった
  193       * ので改めて、フレーム長を確認。
  194       * もし小さかったら不正パケットとみなして無視。
  195       */
  196      if(len < etherlen + iphlen + sizeof(struct tcphdr) )
  197          goto error;
  198
  199      tcp = (struct tcphdr *)((u_char *)ip + iphlen);
  200      tcphlen = tcp->th_off << 2;
  201
  202      /*
  203       * パケット内で申告されている TCP ヘッダー長が分かった
  204       * ので改めて、フレーム長を確認。
  205       * もし小さかったら不正パケットとみなして無視。
  206       */
  207      if(len < etherlen + iphlen + tcphlen )
  208          goto error;
  209
  210      printf("\n----Ether Header----\n");
  211      printf("src addr    : ");
  212      PRINT_MAC_ADDR(ether->ether_shost.ether_addr_octet);
  213      printf("\n");
  214      printf("dest addr   : ");
  215      PRINT_MAC_ADDR(ether->ether_dhost.ether_addr_octet);
  216      printf("\n");
  217      printf("ether type  : 0x%x\n",ether->ether_type);
  218
  219      printf("----IP Header----\n");
  220      printf("version     : %d\n",ip->ip_v);
  221      printf("header len  : %d (%d bytes)\n",ip->ip_hl, ip->ip_hl <<2);
  222      printf("tos         : %d\n",ip->ip_tos);
  223      printf("total len   : %d\n",ntohs(ip->ip_len));
  224      printf("id          : %d\n",ntohs(ip->ip_id));
  225      printf("frag offset : %d\n",ip->ip_off);
  226      printf("ttl         : %d\n",ip->ip_ttl);
  227      printf("protocol    : %d\n",ip->ip_p);
  228      printf("checksum    : 0x%x\n",ip->ip_sum);
  229      printf("src address : %s\n",inet_ntoa(ip->ip_src));
  230      printf("dst address : %s\n",inet_ntoa(ip->ip_dst));
  231
  232      printf("----TCP Header----\n");
  233      printf("source port : %d\n",ntohs(tcp->th_sport));
  234      printf("dest port   : %d\n",ntohs(tcp->th_dport));
  235      printf("seq         : %u\n",ntohl(tcp->th_seq));
  236      printf("ack         : %u\n",ntohl(tcp->th_ack));
  237      printf("data offset : %d (%d bytes)\n",tcp->th_off, tcp->th_off <<2);
  238      printf("flags       : ");
  239      if((tcp->th_flags | TH_FIN) == tcp->th_flags)
  240          printf("FIN ");
  241      if((tcp->th_flags | TH_SYN) == tcp->th_flags)
  242          printf("SIN ");
  243      if((tcp->th_flags | TH_RST) == tcp->th_flags)
  244          printf("RST ");
  245      if((tcp->th_flags | TH_PUSH) == tcp->th_flags)
  246          printf("PUSH ");
  247      if((tcp->th_flags | TH_ACK) == tcp->th_flags)
  248          printf("ACK ");
  249      if((tcp->th_flags | TH_URG) == tcp->th_flags)
  250          printf("URG ");
  251      printf("\n");
  252      printf("window      : %d\n",ntohs(tcp->th_win));
  253      printf("check sum   : 0x%x\n",tcp->th_sum);
  254      printf("urt_ptr     : %d\n",tcp->th_urp);
  255
  256      /*
  257       *  ヘッダ情報から TCP データサイズを計算
  258       *  もしヘッダ情報から求めたTCPデータ長が残りの読み込み可能
  259       *  バイト数より大きかったら、残りのバイト数をデータ長とみなす。
  260       */
  261      iptotlen   = ntohs(ip->ip_len);
  262      tcpdatalen = iptotlen - iphlen - tcphlen;
  263      if( tcpdatalen > len - etherlen - iphlen - tcphlen)
  264          tcpdatalen = len - etherlen - iphlen - tcphlen;
  265
  266      if( tcpdatalen > 0){
  267          int   i = 0;
  268
  269          tcpdata = (u_char *)tcp + tcphlen;
  270          printf("------DATA-------\n");
  271          printf("data length : %d\n", tcpdatalen);
  272          /*
  273           * 表示可能データであれば最初の 100 文字だけ表示
  274           */
  275          while ( i < tcpdatalen && i < TCP_DATA_PRINT_LENGTH){
  276              if(isprint(tcpdata[i]))
  277                  printf("%c",tcpdata[i]);
  278              i++;
  279          }
  280      }
  281
  282      printf("\n\n");
  283
  284    error:
  285      free(ip);
  286      return;
  287  }
  288
  289  /*****************************************************************************
  290   * print_usage()
  291   *
  292   * Usage を表示し、終了する。
  293   *****************************************************************************/
  294  void
  295  print_usage(char *argv)
  296  {
  297      printf("Usage: %s ifname \n",argv);
  298      printf("  Example) %s eri0\n", argv);
  299      exit(1);
  300  }
  301
  302  /*****************************************************************************
  303   * dlattachreq()
  304   *
  305   * DLPI のルーチン。putmsg(9F) を使って DL_ATTACH_REQ をドライバに送る
  306   *
  307   *****************************************************************************/
  308  int
  309  dlattachreq(int fd, t_uscalar_t ppa ,caddr_t buf)
  310  {
  311      union DL_primitives  *primitive;
  312      dl_attach_req_t       attachreq;
  313      struct strbuf         ctlbuf;
  314      int                   flags = 0;
  315      int                   ret;
  316
  317      attachreq.dl_primitive = DL_ATTACH_REQ;
  318      attachreq.dl_ppa = ppa;
  319
  320      ctlbuf.maxlen = 0;
  321      ctlbuf.len    = sizeof(attachreq);
  322      ctlbuf.buf    = (caddr_t)&attachreq;
  323
  324      if (putmsg(fd, &ctlbuf, (struct strbuf*) NULL, flags) < 0){
  325          fprintf(stderr, "dlattachreq: putmsg: %s", strerror(errno));
  326          return(-1);
  327      }
  328
  329      ctlbuf.maxlen = MAXDLBUFSIZE;
  330      ctlbuf.len = 0;
  331      ctlbuf.buf = (caddr_t)buf;
  332
  333      if ((ret = getmsg(fd, &ctlbuf, (struct strbuf *)NULL, &flags)) < 0) {
  334          fprintf(stderr, "dlattachreq: getmsg: %s\n", strerror(errno));
  335          return(-1);
  336      }
  337
  338      primitive = (union DL_primitives *) ctlbuf.buf;
  339      if ( primitive->dl_primitive != DL_OK_ACK){
  340          fprintf(stderr, "dlattachreq: not DL_OK_ACK\n");
  341          return(-1);
  342      }
  343
  344      return(0);
  345  }
  346
  347  /*****************************************************************************
  348   * dlpromisconreq()
  349   *
  350   * DLPI のルーチン。 putmsg(9F) を使って DL_PROMISCON_REQ をドライバに送る
  351   *
  352   *****************************************************************************/
  353  int
  354  dlpromisconreq(int fd, t_uscalar_t level, caddr_t buf)
  355  {
  356      union DL_primitives  *primitive;
  357      dl_promiscon_req_t    promisconreq;
  358      struct strbuf         ctlbuf;
  359      int                   flags = 0;
  360      int                   ret;
  361
  362      promisconreq.dl_primitive = DL_PROMISCON_REQ;
  363      promisconreq.dl_level = level;
  364
  365      ctlbuf.maxlen = 0;
  366      ctlbuf.len    = sizeof (promisconreq);
  367      ctlbuf.buf    = (caddr_t)&promisconreq;
  368
  369      if (putmsg(fd, &ctlbuf, (struct strbuf*) NULL, flags) < 0){
  370          fprintf(stderr, "dlpromisconreq: putmsg: %s", strerror(errno));
  371          return(-1);
  372      }
  373
  374      ctlbuf.maxlen = MAXDLBUFSIZE;
  375      ctlbuf.len = 0;
  376      ctlbuf.buf = (caddr_t)buf;
  377
  378      if ((ret = getmsg(fd, &ctlbuf, (struct strbuf *)NULL, &flags)) < 0) {
  379          fprintf(stderr, "dlpromisconreq: getmsg: %s\n", strerror(errno));
  380          return(-1);
  381      }
  382
  383      primitive = (union DL_primitives *) ctlbuf.buf;
  384      if ( primitive->dl_primitive != DL_OK_ACK){
  385          fprintf(stderr, "dlpromisconreq: not DL_OK_ACK\n");
  386          return(-1);
  387      }
  388
  389      return(0);
  390  }
  391
  392  /*****************************************************************************
  393   * dlbindreq()
  394   *
  395   * DLPI のルーチン。 putmsg(9F) を使って DL_BIND_REQ をドライバに送る
  396   *
  397   *****************************************************************************/
  398  int
  399  dlbindreq(
  400      int fd,
  401      t_uscalar_t sap,
  402      t_uscalar_t max_conind,
  403      uint16_t    service_mode,
  404      uint16_t    conn_mgmt,
  405      t_uscalar_t xidtest_flg,
  406      caddr_t     buf
  407      )
  408  {
  409      union DL_primitives  *primitive;
  410      dl_bind_req_t         bindreq;
  411      struct strbuf         ctlbuf;
  412      int                   flags = 0;
  413      int                   ret;
  414
  415      bindreq.dl_primitive    = DL_BIND_REQ;
  416      bindreq.dl_sap          = sap;
  417      bindreq.dl_max_conind   = max_conind;
  418      bindreq.dl_service_mode = service_mode;
  419      bindreq.dl_conn_mgmt    = conn_mgmt;
  420      bindreq.dl_xidtest_flg  = xidtest_flg;
  421
  422      ctlbuf.maxlen = 0;
  423      ctlbuf.len    = sizeof(bindreq);
  424      ctlbuf.buf    = (caddr_t)&bindreq;
  425
  426      if (putmsg(fd, &ctlbuf, (struct strbuf*) NULL, flags) < 0){
  427          fprintf(stderr, "dlbindreq: putmsg: %s", strerror(errno));
  428          return(-1);
  429      }
  430
  431      ctlbuf.maxlen = MAXDLBUFSIZE;
  432      ctlbuf.len    = 0;
  433      ctlbuf.buf    = (caddr_t)buf;
  434
  435      if ((ret = getmsg(fd, &ctlbuf, (struct strbuf *)NULL, &flags)) < 0) {
  436          fprintf(stderr, "dlbindreq: getmsg: %s\n", strerror(errno));
  437          return(-1);
  438      }
  439
  440      primitive = (union DL_primitives *) ctlbuf.buf;
  441      if ( primitive->dl_primitive != DL_BIND_ACK){
  442          fprintf(stderr, "dlbindreq: not DL_BIND_ACK\n");
  443          return(-1);
  444      }
  445
  446      return(0);
  447  }

ソースファイル sniff.c

実行例

プログラム名の後に引数としてインターフェース名を入力します。
以下の 例では Sun の FastEthernet カードである hme0 を指定しています。
なお、このプログラムも root ユーザで実行する必要があります。
setuid(chmod 4755 sniff )してもいいですけど。

# ./sniff hme0	     
			     
----Ether Header----
src addr    : 08:00:20:c6:69:c7
dest addr   : 00:13:d3:a9:8f:40
ether type  : 0x800
----IP Header----
version     : 4
header len  : 5 (20 bytes)
tos         : 0
total len   : 1292
id          : 51935
frag offset : 16384
ttl         : 60
protocol    : 6
checksum    : 0x8497
src address : 172.29.73.55
dst address : 172.29.73.3
----TCP Header----
source port : 23
dest port   : 1277
seq         : 371774688
ack         : 3757417225
data offset : 5 (20 bytes)
flags       : ACK
window      : 50400
check sum   : 0x6420
urt_ptr     : 0
------DATA-------

NIC に到着したすべての packet を表示します。
内容をみると、telnet のセッションのパケットであることが分かるとおもいます。
この例では出ていませんが、データ部に文字列があればそれも--DATA--以降に表示します。

あと、言い忘れましたが これは TCP パケット専用です。ICMP や ARP はおろか、UDP パケットも表示できません。「表示」といっているのは packet の受信は出来るけど、UDP などのヘッダーを正しく表示できないって事です。 全部のヘッダー対応の source 書くぐらいなら tcpdump や snoop つかったほうがよいです(笑)。

解説

DLPI の説明をする上で STREAMS (ストリーム)機構 の解説を避けて通る事は出来ないのですが、はっきりいって一言で説明することは出来ないですし、なにより私がそれほど理解してないので非常ーにおおざっぱ説明させていただきます。

STREAMS 機構とは Solaris をはじめとする SVR4 準拠の system で採用されているネットワークプロトコルを開発するための下部構造であり、データの構造体、関数群、設計思想を現しています。つまり、IP や X25 などの異なるネットワークプロトコルを開発する場合にも同一の土俵(枠組み)にて開発する事ができるのです。実際に Solaris の TCP や IP もこの STEREAMS 機構にて実装されています。
STREAM とは論理的なデータの通り道であり、STREAM 内で 受け渡しされるデータはメッセージという形(実際には一連の構造体群)にて伝送され、ユーザプロセスはSTREAM HEAD(ストリームヘッド)と呼ばれる窓口を通してデータのやり取りを行う事になります。STREAM には STREAM HEAD から device driverに向かう道と、device driverから STREAM HEAD に向かう道とに別れており、それぞれを「アップストリーム」、「ダウンストリーム」と呼びます。

stream.gif

今回の場合上図の「ストリームデバイスドライバ」とは network Interface のデバイスドライバの事を指しまして、この device をデータリンクサービスプロバイダー(データリンク層のサービスを提供してくれるもの)などと呼びます。network Interface は ethenet などのデータリンク層 に該当する役割を持っている事は周知の事だと思いますので、分かりやすいと思います。
DLPI(Data Link Provider Interface)とは この network Interface ドライバーが提供しているデータリンク層に直接アクセスできるインターフェースという事になります。ユーザプロセスはストリームヘッドを通じてこの DLPI にアクセスし、必要な設定を行ったり、メッセージ(データ)を受け取ったりするわけです。

以上、簡単に DLPI と STREAM について説明させていただきました。かなりいいかげんですが、上記のような大体の構成が分かれば、以下の各関数の流れが分かりやすいとおもいます。 では source の各関数を順に見ていきましょう。

   55      union  DL_primitives  *dlp;

55 行目に DL_primitives という共用体を 宣言しています。primitives というのは後々も出てくるので、ここで説明しておきます。
上の STREAMS 機構の説明のところで、STREAM(データの通り道)は「メッセージ」という形でデータを運ぶと書きましたが、このメッセージはいくつかメッセージタイプ(message type)が存在しまして、それぞれが役割を持っています。以下にこのプログラムで用いられている3つのメッセージタイプを示します。

メッセージタイプ説明
M_DATAユーザプロセスとデバイスドライバ間のデータの受け渡しに使われる。
M_PROTOユーザプロセスとデバイスドライバ間の制御情報の受け渡しに使われる
M_PCPROTOM_PROTO と同じだが、より優先度の高いメッセージタイプ

DLPI で受け渡しされるメッセージは primitive という構造体の形をしており、どのような内容のメッセージを運ぶかによってその構造体の中で宣言されているメンバーが違います。そして primitive は 上記のストリームのメッセージタイプのどれかに該当しています。

   84      if((fd = open (devpath , O_RDWR)) < 0 ){

84 行目で device、つまり network Interface を open します。これにより上図の STREAM が新規に作成されるされることになります。 STREAM というのはあくまで概念ですので、実際には kernel memory 領域に STREAM のための 構造体群の領域が確保される事になります。(この辺りになると私も正直言ってかなり苦しいですが、、)

   92      if(dlattachreq(fd, ppa, buf) < 0){

84 行目で open した STREAM と、物理的な device を結び付けます。つまり system に接続されたどの物理 network Interface とやり取りを行うのかを指定しているわけです。ここで 309 行目の dlattachreq() に飛ぶわけですが、この関数の中では

  317      attachreq.dl_primitive = DL_ATTACH_REQ;

というのが宣言されています。これは上で説明した DLPI primitive でして、/usr/include/sys/dlpi.h 内で定義されています。
dl_attach_req_t 構造体は まさに STREAM と PPA を attach するのを要求する primitive であり、STREAM メッセージタイプでは M_PROTO タイプになります。

/usr/include/sys/dlpi.h より

typedef struct {
       t_uscalar_t     dl_primitive;           /* set to DL_ATTACH_REQ */
       t_uscalar_t     dl_ppa;                 /* id of the PPA */
} dl_attach_req_t;

ここでさらに strbuf 構造体(STREAM Buffer 構造体?)というのが出てきます。strbuf 構造体はユーザプロセスが直接 STREAM HEAD へメッセージを送受する際につかう putmsg()、getmsg() システムコールの引数として使われます。

/usr/include/sys/stropts.h より

struct strbuf {
       int     maxlen;         /* no. of bytes in buffer */
       int     len;            /* no. of bytes returned */
       caddr_t buf;            /* pointer to data */
};

strbuf 構造体の中の data のポインターとして dl_attach_req_t 構造体へのポインターを指定しているというわけです。

strbuf 構造体
maxlen(最大buffer長)
len(data長)
dl_attach_req_t 構造体
DL_ATTACH_REQ
ppa

strbuf と dl_attach_req_t 両構造体の各メンバーに値の代入が終わったら、strbuf 構造体へのアドレスを putmsg() システムコールに渡します。これにより メッセージが STREAM HEAD を通じてダウンストリームに渡される事になります。

  324      if (putmsg(fd, &ctlbuf, (struct strbuf*) NULL, flags) < 0){
putmsg()
概要STREAM へのメッセージの送信
インクルードファイル#include <stropts.h>
形式int putmsg(int fildes, const struct strbuf *ctlptr, const struct strbuf *da taptr, int flags);
引数int filedes : ファイル記述子
const struct clptr *ctlptr: 制御用の strbuf 構造体へのポインタ
const struct strbuf *dataptr: データ用の strbuf 構造体へのポインタ
int flags: フラグ
戻り値成功時: 0
エラー: -1

putmsg() システムコールでは STREAM に対してデータバッファと制御バッファの2つのバッファを一度に送信する事が出来ます。しかしながらここでは実際のデータの送信ではなく、device に対する制御情報(ATTACH 要求)を送るだけなので、324 行目の putmsg ではデータ用の strbuf 構造体の引数としては NULL 値を渡しています。
さて、これで ATTACHの要求の送信は終了しまして、つづいてATTACH 要求に対する確認応答を受け取る動作になります。 ATTACH要求に限らず、DLPI では常に以下のような要求・確認応答 という手順を踏みます。

dlpi_sequence.gif
  333      if ((ret = getmsg(fd, &ctlbuf, (struct strbuf *)NULL, &flags)) < 0) {

ここで今度は getmsg() というシステムコールが出てきます。

getmsg()
概要STREAM からのメッセージの受信
インクルードファイル#include <stropts.h>
形式int getmsg(int fildes, struct strbuf *ctlptr, struct strbuf *dataptr, int * flagsp);
引数int filedes : ファイル記述子
const struct clptr *ctlptr: 制御用の strbuf 構造体へのポインタ
const struct strbuf *dataptr: データ用の strbuf 構造体へのポインタ
int flags: フラグ
戻り値成功時: 実数値
0 --> すべてのメッセージの読み込み成功
MOREDATA --> まだデータが残っている
MORECTL --> まだ制御情報が残っている
エラー: -1

getmsg() は putmsg() とは逆に STREAM HEAD を通じてアップストリームからメッセージを読み込みという動作を行います。正常にいった場合は strbuf 構造体へのポインタである ctlbuf の各メンバーに値が入ることになるわけです。

  338      primitive = (union DL_primitives *) ctlbuf.buf;
  339      if ( primitive->dl_primitive != DL_OK_ACK){
  340          fprintf(stderr, "dlattachreq: not DL_OK_ACK\n");
  341          return(-1);
  342      }

ctlbuf は「buf」 というメンバーを持ち、「buf」は DLPI primitive 構造体であるはずです。(上図および、strbuf 構造体参照) われわれは、この primitive が DL_OK_ACK である事を期待しているわけですから、この DLPI primitive が DL_OK_ACK であるかどうかを判定しています。(339行目)ここまででエラーがなければ、main() に戻ってきます。

  100      if(dlpromisconreq(fd, DL_PROMISC_PHYS, buf) < 0){
  ...
  108      if(dlbindreq (fd, ETHERTYPE_IP, 0, DL_CLDLS, 0, 0, buf) < 0){

100 行目の dlpromisconreq() 及び、108 行目の dlbindreq() も動作の流れ的には上述の dlattachreq() と同じです。上図 のように 要求と確認応答というやり取りを行っています。違うのは要求内容(DLPI primitive)です。

dlpromisconreqDL_PROMISCON_REQ primitive にて promiscast (無差別受信)モード への移行を要求 (追記参照)
dlbindreqDL_BIND_REQ primitive にて データリンク層の SAPを STREAM に BIND(関連付け)を要求

さて、main() のつづきですが、つぎはいままでの処理とはちょっと違っています。

  120      if(ioctl(fd, I_STR, &strioc) < 0){
  121          perror("ioctl: I_STR: DLIOCRAW");
  122          exit(1);
  123      }

ここで呼んでいる ioctl() システムコールがまた重要な処理になっています。

ioctl()
概要STREAM デバイスの制御
インクルードファイル#include <unistd.h>
#include <stropts.h>
#include <sys/conf.h>
形式int ioctl(int fildes, int command, /* arg */ ...);
引数int filedes : ファイル記述子
int command: デバイスに実行させるコマンド
arg: strioctl 構造体へのポインタ
戻り値成功時: 実数値
エラー: -1

STREAMS ioctl() は ioctl() のサプセットで、ストリーム上の様々な機能を実行するために利用されます。120 行目にあるように command として I_STR を指定した場合、arg で指定しているstrioctl 構造体を STREAM のメッセージとしてDOWN STREAM をつうじて、デバイスへと伝えます。
では、この ioctl にてどのような内容のメッセージが DOWN STREAM に渡されているかを見てみます。以下は ioctl の3番目の引数として渡されている strioctl 構造体です。

struct strioctl {
   int ic_cmd;     /* コマンド*/
   int ic_timout;  /*タイムアウト値 */
   int ic_len;     /* データ長 */
   char *ic_dp;    /* データへのポインタ */
}; 

116 〜 119 行目を見ていただければ分かりますが、上記の構造体の各メンバーに代入されているのは、以下のようになります。

  116      strioc.ic_cmd    = DLIOCRAW;
  117      strioc.ic_timout = -1;
  118      strioc.ic_len    = 0;
  119      strioc.ic_dp     = NULL;

これは M_DATA タイプのメッセージに対して RAW モード、つまり、Ethernet ヘッダーを含んだ形での生のパケットを受け取るためのモード(M_DATA メッセージに対して何の変更も行わない)に移行するための設定になります。

RAW モードへの移行がおわったところで、さらに main() の処理がつづきます。

  128      if (ioctl(fd, I_FLUSH, FLUSHR) < 0){

ioctl() システムコール(ここで言っているのは STREAMS ioctl() です。詳しくは man streamio(7I))は前述したとおり、STREMA 上の様々な機能を実行するためのシステムコールですが、コマンドとして I_FLUSH を指定した場合は、第三引数で指定したオプションに従い、STERAM 上の queue をフラッシュ(消去)する事が出来ます。

第三引数のオプションは sys/stropts.h に定義してありまして、128 行目で指定しているのは READ queue(読み取りキュー)をフラッシュする事です。

sys/stropts.h より抜粋

      #define FLUSHR 0x01 /* flush read queue */

queue(キュー) という言葉が初めて出てきましたが、これも STREAM 機構の中では重要な用語の一つです。STREAM 機構とはデータを格納する構造体の集まりであると述べましたが、queue とは STREAM HEAD やデバイスが個々に持っているデータの一時保管場所のようなものです。つまり、STREAM HEAD にデータを渡すという事は、STREAM HEAD の queue にデータが(実際にはデータへのポインタが)格納されるという事です。 ここではその読み取り用の格納庫を明示的に空にしているという事になります。

さて、ここまでで packet のキャプチャーの準備が整いました。ここからが最後の データの獲得と、データの解釈、及び出力になります。

  133      databuf.maxlen = MAXDLBUFSIZE;
  134      databuf.len = 0;
  135      databuf.buf = (caddr_t)buf;
  136
  137      while (getmsg(fd, NULL, &databuf, &flags) == 0) {
  138          if (databuf.len > 0)
  139              print_packet(databuf.buf, databuf.len);
  140      }

「databuf」というのは strbuf 構造体で、54 行目に宣言しています。これは先の getmsg() のところで説明したように、STREAM から受け取ったメッセージを格納する場所になります。
getmsg はすでにお話しているのでさらっと流しますが、第二引数には NULL を指定していますので、制御情報の取得は行わず、データのみを受け取るようにしています。
STREAM HEAD から受け取った生データは strbuf 構造体 data の メンバである databuf.buf に格納されていますのでこのデータを 139 行目で最後の最後の処理になる print_packet() 関数に渡しています。

print_packet() 内で行っている事は 先ほど getmsg() にて受け取った生データを Ethernet、IP、TCP の各ヘッダー構造体に当てはめ、それぞれの構造体のメンバー、つまりヘッダーの内容をプリントしているだけです。行っている処理事体は「Linux socket プログラミング/パケットモニタリングプログラム」で書いたものをほぼそのまま持ってきています。
ただ違うのは 今回は Ethernet の ヘッダーも入るので、データを当てはめる構造体として ether_header 構造体を追加している事と、Solaris と Linux で、IP、TCP のヘッダー構造体のメンバーが若干違っていたので、printf のところでメンバー名の指定を Solaris 様に書き直したくらいです。ヘッダーの中で一番違ったのは TCP のフラグの表現の仕方です。Linux では各フラグが 1 bit づつの個別のメンバーだったのですが、Solaris では一つの tcp.th_flags という 8 bit のメンバーになっており、 どのフラグが立っているかどうかの判定は、各 bit を比較してやらなければなりませんでした。

最後に

というわけで非常ーに長々と、かつおおざっぱに解説させていただきました。 私自身本当に分からないところだらけで(だったら書くなという声が聞こえてきそうですが)苦しいのですが、DLPI の大体の概要が分かればコアの部分はそのまま使ってもいろいろ応用が出来るのではないかと思います。ただし、この手のパケットのキャプチャーを行うプログラムは使い方によっては不正利用にもつながりますのでお取り扱いは注意しましょう。まぁ、Solaris には端から snoop という packet capture tool がついているんだから問題ないでしょうけどね。

参考資料

このページを書く上で参考にした資料をご紹介いたします。

参考資料名メモ
UNIX カーネルの魔法
(プレンティスホール出版)
SVR4 の カーネル関連の本。しかしながら STREAM の部分しか読んでません。というか他の部分読んでも分からない、、(泣)
UNIX ネットワークプログラミング
(トッパン)
まさに UNIX 上でのネットワークプログラミングの本。情報がちょっと古いのが難点ですが、基本は押さえられます。
詳解TCP/IP
(ソフトバンク)
TCP/IP のプロトコル上の話に関してはバイブル的な本です。
man page一番身近な情報源
dlpi(7P), le(7D), streamio(7I), getmsg(2), putmsg(2)
docs.sun.comリンクしているのは Solaris 8 の hme の man ページですが、日本語で DLPI 周りの解説が書いてある数少ない情報の一つかと、、

追記

すべての フレームタイプを受信する

先に書きましたように、このプログラムでは IP のpacket しか受信しません。すべてのフレームタイプを受信するためには、100 行目の dlpromiscast を以下のように変更します。

  100      if(dlpromisconreq(fd, DL_PROMISC_SAP, buf) < 0){

dlpromisconreq() 関数の中で、この第二引数の DL_PROMISC_SAP は promiscon_req プリミティブの 「dl_level 」という変数に割り当てられています。 この promiscon_req プリミティブの dl_level に使われる値は /usr/include/sys/dlpi.h の中で以下のように定義されています。

/*
 * DLPI promiscuous mode definitions
 */
#define DL_PROMISC_PHYS         0x01    /* promiscuous mode at phys level */
#define DL_PROMISC_SAP          0x02    /* promiscous mode at sap level */
#define DL_PROMISC_MULTI        0x03    /* promiscuous mode for multicast */

これは promiscuous mode(無差別受信モード)をどのレベルで実行するかという事を設定するもので、上のプログラムでは phys レベルを指定しており、このレベルでは SAP(フレームタイプ) の種類によってフィルタリングが行われ、指定した SAP 以外の packet はうけとりません。ここを上記のように sap レベルとする事ですべてのフレームタイプの packet を受信することが出来ます。
名前のイメージ的には DL_PROMISC_PHYS が すべて受信できて DL_PROMISC_SAP が SAP でのフィルターを行うようにも思えるのですが、試してみると「DL_PROMISC_SAP」にセットすることによってすべてのフレームタイプを受信できるようになりました。
「DL_PROMISC_SAP」にセットすることによって ARP や REVARP の packet も受信できるようになりますが、当然上のプログラムの 100 行目を書き換えただけでは packet を受け取れるようになるだけなので、ヘッダーの表示はされません。

さらに追記

DL_PROMISC_PHYS(PHYが レベル)ではローカルホストによって生成されたフレームを含む、メディア上のすべてのフレームの受信を有効になり、DL_PROMISC_SAP(SAPレベル)ではすべての SAP 値 (フレームタイプ) の受信を有効します。


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