MacOS Xでdivert socketを使う

divert socketとは,IPパケットの入出力を迂回させて,ユーザランドまで持って行くことが出来る機構です.もともとFreeBSDに実装されたものですが,FreeBSDを元にしているMacOS Xでもdivert socketを利用することが出来ます.

divert socketはipfwと一緒に利用します.ipfwはフィルタルールに基づき,IPパケットの出(入)力をフィルタします.フィルタされたパケットはdivert socketにforwardされ,divert socketを開いているアプリケーションはIPパケットを得ることが出来ます.

サンプルプログラムは以下の通りです.

#include <strings.h>
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>

int
open_divert(uint16_t port)
{
  struct sockaddr_in bind_port;
  int fd;

  fd = socket(PF_INET, SOCK_RAW, IPPROTO_DIVERT);
  if (fd < 0) {
    perror("socket");
    return -1;
  }

  bzero(&bind_port, sizeof(bind_port));

  bind_port.sin_family = PF_INET;
  bind_port.sin_port   = htons(port);

  if (bind(fd, (struct sockaddr*)&bind_port, sizeof(bind_port)) < 0) {
    close(fd);
    perror("bind");
    return -1;
  }

  return fd;
}

void
read_loop(int fd)
{
  struct sockaddr_in  sin;
  struct in_addr      addr;
  struct ip          *hdr;
  socklen_t sin_len;
  ssize_t   size;
  uint8_t   buf[1024 * 100];

  sin_len = sizeof(sin);
  for(;;) {
    size = recvfrom(fd, buf, sizeof(buf), 0,
                    (struct sockaddr*)&sin, &sin_len);
    if (size < 0) {
      perror("recvfrom");
      exit(-1);
    }

    hdr = (struct ip*)buf;

    printf("recv %d[bytes]\n", size);
    printf("src: %s\n", inet_ntoa(hdr->ip_src));
    printf("dst: %s\n", inet_ntoa(hdr->ip_dst));
    printf("if addr: %s\n", inet_ntoa(sin.sin_addr));
    printf("protocol: %d\n\n", hdr->ip_p);

    sendto(fd, buf, size, 0, (struct sockaddr*)&sin, sin_len);
  }
}

int
main(int argc, char *argv[])
{
  int fd;

  if (argc < 2) {
    printf("usage: ./a.out port");
    return -1;
  }

  fd = open_divert(atoi(argv[1]));
  if (fd < 0)
    return -1;

  read_loop(fd);

  return 0;
}

socket(PF_INET, SOCK_RAW, IPPROTO_DIVERT)として,divert socketを開きます.開いた後は,bind関数で,divert socketを特定のポート番号にバインドします.このポート番号に対して,ipfwでforwardします.

たとえば,divert socketの10000番ポートを開き,UDPパケットの全てをこのポートに迂回させるようにしたいときは,以下のようにipfwを設定します.

$ sudo ipfw add 100 divert 10000 udp from any to any

パケットを開いた後は,recvfrom関数でパケットを読み込みます.

recvfrom(fd, buf, sizeof(buf), 0,
         (struct sockaddr*)&sin, &sin_len);

とありますが,もしも受信したパケットが出力パケットであるなら,sin.sin_addr == INADDR_ANYとなり,入力パケットであるならパケットを受信したインターフェースのアドレスとなります.bufには,実際のIPパケットが生で入っており,IPヘッダに書かれている情報を知りたい場合は,ip構造体を利用します.

divert socketに書き込むときは,sendtoを利用します.

sendto(fd, buf, size, 0, (struct sockaddr*)&sin, sin_len);

sin.sin_addr == INADDR_ANYの場合は出力とみなされ,IPの出力関数へ渡されます.それ以外の場合は,特定のインターフェースに対する入力パケットとして,IPの入力関数へ渡されます.

上記のサンプルプログラムを実行すると,以下のようになります.

$ sudo ipfw add 100 divert 10000 udp from any to any
$ sudo ./a.out 10000
recv 62[bytes]
src: 192.168.10.115
dst: 192.168.10.1
if addr: 0.0.0.0
protocol: 17

recv 252[bytes]
src: 192.168.10.1
dst: 192.168.10.115
if addr: 192.168.10.115
protocol: 17

上が,何らかのプロセスから出力されたIPパケットを横取りしたもので,下が,インターフェースから入ってきたIPパケットを横取りしたものです.

divert socketへのforwardをやめる場合は,以下のようにします.

$ sudo ipfw delete 100