人間とウェブの未来

「ウェブの歴史は人類の歴史の繰り返し」という観点から色々勉強しています。

高速にリモートホストのポートがListenしているかを調べる

hb.matsumoto-r.jp

以下のエントリは一部誤認が含まれていたので、上記エントリにその旨をまとめましたので御覧ください。


とある事情でミドルウェア上から高速にリモートホストのポートのListenチェックをしたくなりました。ローカルホストのポートであれば、/procやnetlinkなどを使って素早くチェックする方法がありますが、今回は対象がリモートホストなのでソケットでなんとかする必要があります。

そこで、誰もがまず思いつくのは、connect()システムコールによってリモートホストのポートに接続しにいって、connectできればOK、できなければNGと判定する方法があり得るでしょう。(高負荷時に接続できないパターンはListenしていないと判定してよい)

そこで一旦、最低限socket()システムコールとconnect()システムコールで接続する時のパケットをtcpdumpで眺めてみます。

06:35:01.857749 IP (tos 0x0, ttl 64, id 27212, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.47498 > 127.0.0.1.53: Flags [S], cksum 0xfe30 (incorrect -> 0x73a5), seq 3957461550, win 43690, options [mss 65495,sackOK,TS val 22509666 ecr 0,nop,wscale 7], length 0
06:35:01.857757 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.53 > 127.0.0.1.47498: Flags [S.], cksum 0xfe30 (incorrect -> 0x2020), seq 2210551288, ack 3957461551, win 43690, options [mss 65495,sackOK,TS val 22509666 ecr 22509666,nop,wscale 7], length 0
06:35:01.857765 IP (tos 0x0, ttl 64, id 27213, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.47498 > 127.0.0.1.53: Flags [.], cksum 0xfe28 (incorrect -> 0xf264), seq 1, ack 1, win 342, options [nop,nop,TS val 22509666 ecr 22509666], length 0
06:35:01.858468 IP (tos 0x0, ttl 64, id 36578, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.47500 > 127.0.0.1.53: Flags [S], cksum 0xfe30 (incorrect -> 0xeada), seq 2421749376, win 43690, options [mss 65495,sackOK,TS val 22509666 ecr 0,nop,wscale 7], length 0
06:35:01.858474 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.53 > 127.0.0.1.47500: Flags [S.], cksum 0xfe30 (incorrect -> 0x77bf), seq 3628867844, ack 2421749377, win 43690, options [mss 65495,sackOK,TS val 22509666 ecr 22509666,nop,wscale 7], length 0
06:35:01.858539 IP (tos 0x0, ttl 64, id 36579, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.47500 > 127.0.0.1.53: Flags [.], cksum 0xfe28 (incorrect -> 0x4a04), seq 1, ack 1, win 342, options [nop,nop,TS val 22509666 ecr 22509666], length 0
06:35:01.858754 IP (tos 0x0, ttl 64, id 41537, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.47502 > 127.0.0.1.53: Flags [S], cksum 0xfe30 (incorrect -> 0x8919), seq 242604579, win 43690, options [mss 65495,sackOK,TS val 22509666 ecr 0,nop,wscale 7], length 0
06:35:01.858759 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.53 > 127.0.0.1.47502: Flags [S.], cksum 0xfe30 (incorrect -> 0x854e), seq 71402943, ack 242604580, win 43690, options [mss 65495,sackOK,TS val 22509666 ecr 22509666,nop,wscale 7], length 0
06:35:01.858765 IP (tos 0x0, ttl 64, id 41538, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.47502 > 127.0.0.1.53: Flags [.], cksum 0xfe28 (incorrect -> 0x5793), seq 1, ack 1, win 342, options [nop,nop,TS val 22509666 ecr 22509666], length 0

ふむむむ、connect成功時は9パケットのパケット送受信が発生していますね。これを見ると、ポートのチェックだけをしたいのに9パケットって、6パケット分ぐらいは無駄に感じますよね。TCPの3wayhandshake的には、synパケット送ってsyn+ackがかえってこればそれで基本はListenしているとみなせるのだから、synパケット送信→syn+ackパケット受信→rstパケット送信、の3パケットで十分なわけです。それだったら、ユーザランドに簡易TCPスタックを自作して、synパケットを作ってRAWソケットを介して戻りのパケットを受信してやれば良いわけですね。

2017/02/13追記

による指摘の通り、実際は普通にconnectしてcloseすると以下のようなtcpdumpでした。全部で普通は6パケットですね。

09:13:59.353115 IP (tos 0x0, ttl 64, id 45250, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.47536 > 127.0.0.1.53: Flags [S], cksum 0xfe30 (incorrect -> 0xd259), seq 3532090005, win 43690, options [mss 65495,sackOK,TS val 24894039 ecr 0,nop,wscale 7], length 0
09:13:59.353128 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.53 > 127.0.0.1.47536: Flags [S.], cksum 0xfe30 (incorrect -> 0x4130), seq 394239430, ack 3532090006, win 43690, options [mss 65495,sackOK,TS val 24894039 ecr 24894039,nop,wscale 7], length 0
09:13:59.353140 IP (tos 0x0, ttl 64, id 45251, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.47536 > 127.0.0.1.53: Flags [.], cksum 0xfe28 (incorrect -> 0x1375), seq 1, ack 1, win 342, options [nop,nop,TS val 24894039 ecr 24894039], length 0
09:14:04.641694 IP (tos 0x0, ttl 64, id 45252, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.47536 > 127.0.0.1.53: Flags [F.], cksum 0xfe28 (incorrect -> 0x0e49), seq 1, ack 1, win 342, options [nop,nop,TS val 24895362 ecr 24894039], length 0
09:14:04.642176 IP (tos 0x0, ttl 64, id 18320, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.53 > 127.0.0.1.47536: Flags [F.], cksum 0xfe28 (incorrect -> 0x091d), seq 1, ack 2, win 342, options [nop,nop,TS val 24895362 ecr 24895362], length 0
09:14:04.642189 IP (tos 0x0, ttl 64, id 45253, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.47536 > 127.0.0.1.53: Flags [.], cksum 0xfe28 (incorrect -> 0x091d), seq 2, ack 2, win 342, options [nop,nop,TS val 24895362 ecr 24895362], length 0

ユーザランドに簡易TCPスタックを作る

IPヘッダやTCPヘッダを構造体で定義するために、最初は以下のような構造体を自前で作ってパケットを作っていました。

#pragma pack(1)                                                                  
struct tcp {                                                                     
  __extension__ union {                                                          
    struct {                                                                     
      u_int16_t src_port;                                                        
      u_int16_t dst_port;                                                        
      u_int32_t seq;                                                             
      u_int32_t ack;                                                             
                                                                                 
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__                                    
      u_int8_t dummy : 4;                                                        
      u_int8_t offset : 4;                                                       
#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__                                     
      u_int8_t offset : 4;                                                       
      u_int8_t dummy : 4;                                                        
#endif                                                                           
                                                                                 
      u_int8_t flags;                                                            
                                                                                 
      u_int16_t win_size;                                                        
      u_int16_t cksum;                                                           
      u_int16_t urg_ptr;                                                         
    };                                                                           
  };
}

ちょうどsynパケットが送れるようになってきた時ぐらいに、よくよく調べていくと#include <netinet/ip.h>#include <netinet/tcp.h>あたりにIPヘッダやTCPヘッダの構造体が既に定義済みで、それを使った方がだいぶ楽だということが分かったので、それらのヘッダファイルを利用します。

以下のように、RAWソケットを作って、IPヘッダやTCPヘッダを自前で定義してパケットを送りつけます。それによって、ユーザランドで送受信するデータにもTCP/IPヘッダが含まれるようになります。概ね以下のように書くことになります。

  int s;
  int src_port, dst_port;
  char *dst_host;
  char packet[4096];
  char source_ip[20];
  struct sockaddr_in dest;
  struct pseudo_header psh;
  struct iphdr *iph = (struct iphdr *)packet;
  struct tcphdr *tcph = (struct tcphdr *)(packet + sizeof(struct ip));
  int flag = 1;
  const int *val = &flag;

  src_port = 54321;
  dst_host = argv[1];
  dst_port = atoi(argv[2]);

  s = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
  if (s < 0) {
    ERROR("socket");
  }

  if (inet_addr(dst_host) != -1) {
    dest_ip.s_addr = inet_addr(dst_host);
  } else {
    char *ip = hostname_to_ip(dst_host);
    if (ip != NULL) {
      printf("%s resolved to %s\n", dst_host, ip);
      dest_ip.s_addr = inet_addr(hostname_to_ip(dst_host));
    } else {
      printf("Unable to resolve hostname: %s", dst_host);
      exit(1);
    }
  }

  local_ip(source_ip);

  memset(packet, 0, 4096);

  iph->ihl = 5;
  iph->version = 4;
  iph->tos = 0;
  iph->tot_len = sizeof(struct ip) + sizeof(struct tcphdr);
  iph->id = htons(54321);
  iph->frag_off = htons(16384);
  iph->ttl = 64;
  iph->protocol = IPPROTO_TCP;
  iph->check = 0;
  iph->saddr = inet_addr(source_ip);
  iph->daddr = dest_ip.s_addr;
  iph->check = cksum((unsigned short *)packet, iph->tot_len >> 1);

  tcph->source = htons(src_port);
  tcph->dest = htons(80);
  tcph->seq = htonl(1105024978);
  tcph->ack_seq = 0;
  tcph->doff = sizeof(struct tcphdr) / 4;
  tcph->fin = 0;
  tcph->syn = 1;
  tcph->rst = 0;
  tcph->psh = 0;
  tcph->ack = 0;
  tcph->urg = 0;
  tcph->window = htons(14600);
  tcph->check = 0;
  tcph->urg_ptr = 0;

  if (setsockopt(s, IPPROTO_IP, IP_HDRINCL, val, sizeof(flag)) < 0) {
    printf("Error setting IP_HDRINCL. Error number : %d . Error message : %s \n", errno, strerror(errno));
    exit(0);
  }

  dest.sin_family = AF_INET;
  dest.sin_addr.s_addr = dest_ip.s_addr;
  dest.sin_port = ntohs(tcph->dest);

  tcph->dest = htons(dst_port);
  tcph->check = 0;

  psh.source_address = inet_addr(source_ip);
  psh.dest_address = dest.sin_addr.s_addr;
  psh.placeholder = 0;
  psh.protocol = IPPROTO_TCP;
  psh.tcp_length = htons(sizeof(struct tcphdr));

  memcpy(&psh.tcp, tcph, sizeof(struct tcphdr));

  tcph->check = cksum((unsigned short *)&psh, sizeof(struct pseudo_header));

  if (sendto(s, packet, sizeof(struct iphdr) + sizeof(struct tcphdr), 0, (struct sockaddr *)&dest, sizeof(dest)) < 0) {
    ERROR("sendto");
  }

IPヘッダとTCPヘッダの構造体およびヘッダサイズに気をつけながらpacketバッファ上のポインタの位置を移動させ、IPおよびTCPヘッダを定義していきます。今回はリモートホストが80ポートをListenしているかをチェックするために、それらの情報とsynフラグを立てたTCPヘッダを作りました。また、IPヘッダおよびTCPヘッダも以下のように計算します。チェックサムについては今回は流れを説明するため詳細を省略します。ググると沢山でてきますので。

unsigned short cksum(unsigned short *ptr, int nbytes)
{
  register long sum;
  unsigned short oddbyte;
  register short answer;

  sum = 0;
  while (nbytes > 1) {
    sum += *ptr++;
    nbytes -= 2;
  }
  if (nbytes == 1) {
    oddbyte = 0;
    *((u_char *)&oddbyte) = *(u_char *)ptr;
    sum += oddbyte;
  }

  sum = (sum >> 16) + (sum & 0xffff);
  sum = sum + (sum >> 16);
  answer = (short)~sum;

  return (answer);
}

そして、パケットを作ることができたら、それをsendto()システムコールによって送信します。これで、リモートホストに対してsynフラグのたったパケットをユーザランドで作って送ることができます。 実際に上記のようなパケットの作り方でtcpdumpを見ると以下のようになります。

06:54:15.278121 IP (tos 0x0, ttl 64, id 54321, offset 0, flags [DF], proto TCP (6), length 40)
    10.0.2.15.54321 > 10.0.2.15.80: Flags [S], cksum 0xf08b (correct), seq 1105024978, win 14600, length 0

ちゃんとsynパケットを送れていますね。

後はsynとackのフラグが立ったパケットを同様にrecvfrom()すれば良いだけ….となりますが、ここで大きな問題が発生します。

カーネルのTCPスタックに邪魔される

RAWソケットを介して、ユーザランドで自前で作ったパケットを送信し、その後リモートホストから返りのパケットを受信する場合、ユーザランドのTCPスタックだけでなくカーネルのTCPスタックも通過することになります。すると、カーネルのTCPスタックはこんな返りのパケット知らない!といって、不要なパケットとみなしrstフラグのたったパケットを返していしまいます。実際にRAWソケットでsynパケットを送った後のtcpdumpの出力しては以下のようになります。

06:54:15.278121 IP (tos 0x0, ttl 64, id 54321, offset 0, flags [DF], proto TCP (6), length 40)
    10.0.2.15.54321 > 10.0.2.15.80: Flags [S], cksum 0xf08b (correct), seq 1105024978, win 14600, length 0
06:54:15.278133 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 44)
    10.0.2.15.80 > 10.0.2.15.54321: Flags [S.], cksum 0x183c (incorrect -> 0xbe01), seq 1991915580, ack 1105024979, win 43690, options [mss 65495], length 0
06:54:15.278139 IP (tos 0x0, ttl 64, id 45579, offset 0, flags [DF], proto TCP (6), length 40)
    10.0.2.15.54321 > 10.0.2.15.80: Flags [R], cksum 0x2991 (correct), seq 1105024979, win 0, length 0

これは、ユーザランド側でTCP/IPヘッダを含むパケットをrecvfrom()しなくても発生します。というのも、カーネルのTCPスタックが先にrstパケットを送ってしまっているためです。そうなると、リモートホストは通信が終了したことになり、その後にユーザランドのTCPスタックからrecvfromしたとしても接続は切れていると判定(返り値0)され、パケットを受信できなくなります。結果、ユーザランドTCPスタック上でsyn+ackパケットが返ってきたことが判定できません。

そこで、ユーザランドでもそれを知る方法として以下の2つの戦略がありえます。

  1. カーネルのTCPスタックがrstを送るよりも速くユーザランドでパケットを受信する
  2. ユーザランドのTCPスタックでパケットを処理しながらBPF経由でパケットをモニタリングする

では、それぞれの方針で実装を進めてみましょう。

カーネルのTCPスタックがrstを送るよりも速くユーザランドでパケットを受信する

パケットがカーネルのTCPスタックを登る前に、RAWソケットとしてはユーザランド側にもコピーされるはずなので、カーネルのTCPスタックの処理よりも先にユーザランド側でパケットを受信する必要があります。そこで、ユーザランドのTCPスタックで作ったsynパケットを送信する前に、あらかじめ受信専用のスレッドを作って受信処理をループさせて待機しておき、synパケット送信後、返りのパケットをできるだけ速く受信できるように試みてみます。その場合、以下のような実装になります。

  pthread_t new_packet_reciever_thread;
  if (pthread_create(&new_packet_reciever_thread, NULL, new_packet_reciever, NULL) < 0) {
    ERROR("pthread_create");
  }

  dest.sin_family = AF_INET;
  dest.sin_addr.s_addr = dest_ip.s_addr;
  dest.sin_port = ntohs(tcph->dest);

  tcph->dest = htons(dst_port);
  tcph->check = 0;

  psh.source_address = inet_addr(source_ip);
  psh.dest_address = dest.sin_addr.s_addr;
  psh.placeholder = 0;
  psh.protocol = IPPROTO_TCP;
  psh.tcp_length = htons(sizeof(struct tcphdr));

  memcpy(&psh.tcp, tcph, sizeof(struct tcphdr));

  tcph->check = cksum((unsigned short *)&psh, sizeof(struct pseudo_header));

  if (sendto(s, packet, sizeof(struct iphdr) + sizeof(struct tcphdr), 0, (struct sockaddr *)&dest, sizeof(dest)) < 0) {
    ERROR("sendto");
  }

  pthread_join(new_packet_reciever_thread, NULL);

流れとしては、上記のようにsendto前から受信スレッドを走らせておき、スレッドの処理は以下のように実装しておきます。

/* thread */
void *new_packet_reciever(void *ptr)
{
  int socket_raw, saddr_size, data_size;
  struct sockaddr saddr;
  unsigned char *buffer = (unsigned char *)malloc(65536);

  socket_raw = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
  if (socket_raw < 0) {
    ERROR("socket");
  }
  saddr_size = sizeof(saddr);

  while (1) {
    data_size = recvfrom(socket_raw, buffer, 65536, 0, (struct sockaddr *)&saddr, &saddr_size);

    if (data_size < 0) {
      ERROR("recvfrom");
    }

    if (found_syn_ack_packet(buffer, data_size)) {
      printf("found syn_ack packet by new_packet_reciever thread\n");
      break;
    }
  }
  close(socket_raw);
}

その上で、運良くカーネルがrstを送りきるより速くsyn+ackパケットを受信できた場合に、それのパケットを表示するようにします。

static int found_syn_ack_packet(unsigned char *buffer, int size)
{
  struct iphdr *iph = (struct iphdr *)buffer;
  struct sockaddr_in source, dest;
  unsigned short iphdrlen;

  if (iph->protocol == 6) {
    struct iphdr *iph = (struct iphdr *)buffer;
    iphdrlen = iph->ihl * 4;

    struct tcphdr *tcph = (struct tcphdr *)(buffer + iphdrlen);

    memset(&source, 0, sizeof(source));
    source.sin_addr.s_addr = iph->saddr;

    memset(&dest, 0, sizeof(dest));
    dest.sin_addr.s_addr = iph->daddr;

    if (tcph->syn == 1 && tcph->ack == 1 && source.sin_addr.s_addr == dest_ip.s_addr) {
      ip_capture(buffer);
      return 1;
    } else {
      printf("raw packet recieved. but isn't syn_ack packet\n");
    }
  }
  return 0;
}

これで、理屈上はうまくいきそうです。また、TCPスタック同士の処理速度では勝負にならないはずなので、TCPスタックの処理が無視できるほどのレイテンシーの低いリモートホストであれば、rstがカーネルからリモートホストに届く(1往復半)よりも速く、ユーザランドでsyn+ackを受信(1往復)できる可能性は高くなりそうです。

ユーザランドのTCPスタックでパケットを処理しながらBPF経由でパケットをモニタリングする

もう一つの戦略はBPF経由でパケットをモニタリングする方法です。一般的にはlibpcapなどを使えばよいので、比較的簡単に実装できるでしょう。もはやユーザランドでやっていると言っていいのか不明ですが、とりあえず実装してみます。これも、一つ目の戦略と同様にパケットを見失わないように予めスレッドを動かして、パケットを監視するようにします。まずは、同様に以下のようにスレッドを作ります。

  pthread_t new_packet_capture_run_thread;
  if (pthread_create(&new_packet_capture_run_thread, NULL, new_packet_capture_run, NULL) < 0) {
    ERROR("pthread_create");
  }

  dest.sin_family = AF_INET;
  dest.sin_addr.s_addr = dest_ip.s_addr;
  dest.sin_port = ntohs(tcph->dest);

  tcph->dest = htons(dst_port);
  tcph->check = 0;

  psh.source_address = inet_addr(source_ip);
  psh.dest_address = dest.sin_addr.s_addr;
  psh.placeholder = 0;
  psh.protocol = IPPROTO_TCP;
  psh.tcp_length = htons(sizeof(struct tcphdr));

  memcpy(&psh.tcp, tcph, sizeof(struct tcphdr));

  tcph->check = cksum((unsigned short *)&psh, sizeof(struct pseudo_header));

  if (sendto(s, packet, sizeof(struct iphdr) + sizeof(struct tcphdr), 0, (struct sockaddr *)&dest, sizeof(dest)) < 0) {
    ERROR("sendto");
  }

  pthread_join(new_packet_capture_run_thread, NULL);

recvfromのループ戦略と同様ですね。さらに、スレッド上でBPFによるパケットモニタリングを実装します。

/* thread */
void *new_packet_capture_run(void *ptr)
{
  pcap_t *pd;
  char errbuf[PCAP_ERRBUF_SIZE];
  bpf_u_int32 netp;
  bpf_u_int32 maskp;
  struct bpf_program fprog;
  struct pcap_userdata pud;

  char *filter = "dst host 10.0.2.15 and src port 80 and dst port 54321";
  char *dev = "enp0s3";
  int dl = 0;
  int dl_len = 0;

  if ((pd = pcap_open_live(dev, 1514, 1, 500, errbuf)) == NULL) {
    ERROR("pcap_open_live");
  }

  pcap_lookupnet(dev, &netp, &maskp, errbuf);
  pcap_compile(pd, &fprog, filter, 0, netp);

  if (pcap_setfilter(pd, &fprog) == -1) {
    ERROR("pcap_setfilter");
  }

  pcap_freecode(&fprog);
  dl = pcap_datalink(pd);

  switch (dl) {
  case 1:
    dl_len = 14;
    break;
  default:
    dl_len = 14;
    break;
  }

  pud.l1_len = dl_len;
  pud.pd = pd;

  if (pcap_loop(pd, -1, raw_packet_receiver, (u_char *)&pud) < 0) {
    printf("found syn_ack packet by new_packet_capture_run thread\n");
  }
}

pcapでパケットをフィルタしつつ、pcap_loopでモニタリングし続けます。その上で、パケットを受信したらそのパケットのフラグを解析し、syn+ackなパケットであれば、pcap_loopを抜けるためにpcap_breakloopを実行します。このあたりはlibeventにも似たインターフェイスになっていますね。このコードのfound_syn_ack_packetrecvfromによる受信スレッドのものと同一です。

void raw_packet_receiver(u_char *udata, const struct pcap_pkthdr *pkthdr, const u_char *packet)
{
  struct pcap_userdata *pud = (struct pcap_userdata *)udata;

  if (found_syn_ack_packet((char *)(packet + pud->l1_len), sizeof(struct ip) + sizeof(struct tcphdr))) {
    pcap_breakloop(pud->pd);
  }
}

実際に試してみましょう

実際にこれらのコードを全てまとめたものが以下になります。長いです。

/*
 * gcc raw_packet.c -g -lpcap -lpthread -o tcp_port_check
 *
 */

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <errno.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/tcp.h>
#include <netinet/ip.h>
#include <pcap.h>
#include <pthread.h>

#define ERROR(name)                                                                                                    \
  perror(name);                                                                                                        \
  exit(EXIT_FAILURE)

struct in_addr dest_ip;

struct pcap_userdata {
  int l1_len;
  pcap_t *pd;
};

struct pseudo_header {
  unsigned int source_address;
  unsigned int dest_address;
  unsigned char placeholder;
  unsigned char protocol;
  unsigned short tcp_length;

  struct tcphdr tcp;
};

static void tcp_capture(char *packet)
{
  struct tcphdr *tcphdr;
  int i;

  tcphdr = (struct tcphdr *)packet;

  printf("src port: %d\n", ntohs(tcphdr->source));
  printf("dst port: %d\n", ntohs(tcphdr->dest));
  printf("sec num: %zu\n", (size_t)ntohl(tcphdr->seq));
  printf("ack num: %zu\n", (size_t)ntohl(tcphdr->ack_seq));
  printf("reserve : 0x%x\n", tcphdr->res1);
  printf("offset: 0x%x\n", ntohs(tcphdr->doff));
  printf("fin: %x\n", tcphdr->fin);
  printf("syn: %x\n", tcphdr->syn);
  printf("rst: %x\n", tcphdr->rst);
  printf("psh: %x\n", tcphdr->psh);
  printf("ack: %x\n", tcphdr->ack);
  printf("urg: %x\n", tcphdr->urg);
  printf("res2: %x\n", tcphdr->res2);
  printf("window size: %d\n", ntohs(tcphdr->window));
  printf("checksum: 0x%x\n", ntohs(tcphdr->check));
  printf("urgent ptr: 0x%x\n", ntohs(tcphdr->urg_ptr));
}

static void ip_capture(char *packet)
{
  struct ip *iphdr;

  iphdr = (struct ip *)packet;

  printf("ip version: %x\n", iphdr->ip_v);
  printf("header length: %xbyte\n", iphdr->ip_hl);
  printf("service type: 0x%.2x\n", iphdr->ip_tos);
  printf("packet length: �yte\n", ntohs(iphdr->ip_len));
  printf("identify: 0x%.4x\n", ntohs(iphdr->ip_id));
  printf("flag offset: 0x%.4x\n", ntohs(iphdr->ip_off));
  printf("ttl: 0x%.2x\n", iphdr->ip_ttl);
  switch (iphdr->ip_p) {
  case 1:
    printf("protocol: ICMP\n");
    break;
  case 6:
    printf("protocol: TCP\n");
    break;
  case 17:
    printf("protocol: UDP\n");
    break;
  default:
    printf("protocol: 0x%.2x\n", iphdr->ip_p);
    break;
  }
  printf("header checksum: 0x%.4x\n", ntohs(iphdr->ip_sum));
  printf("src ipaddress: %s\n", inet_ntoa(iphdr->ip_src));
  printf("dst ipaddress: %s\n", inet_ntoa(iphdr->ip_dst));

  if (iphdr->ip_p == 6)
    tcp_capture((char *)(packet + sizeof(struct ip)));
}

static int found_syn_ack_packet(unsigned char *buffer, int size)
{
  struct iphdr *iph = (struct iphdr *)buffer;
  struct sockaddr_in source, dest;
  unsigned short iphdrlen;

  if (iph->protocol == 6) {
    struct iphdr *iph = (struct iphdr *)buffer;
    iphdrlen = iph->ihl * 4;

    struct tcphdr *tcph = (struct tcphdr *)(buffer + iphdrlen);

    memset(&source, 0, sizeof(source));
    source.sin_addr.s_addr = iph->saddr;

    memset(&dest, 0, sizeof(dest));
    dest.sin_addr.s_addr = iph->daddr;

    if (tcph->syn == 1 && tcph->ack == 1 && source.sin_addr.s_addr == dest_ip.s_addr) {
      ip_capture(buffer);
      return 1;
    } else {
      printf("raw packet recieved. but isn't syn_ack packet\n");
    }
  }
  return 0;
}

void raw_packet_receiver(u_char *udata, const struct pcap_pkthdr *pkthdr, const u_char *packet)
{
  struct pcap_userdata *pud = (struct pcap_userdata *)udata;

  if (found_syn_ack_packet((char *)(packet + pud->l1_len), sizeof(struct ip) + sizeof(struct tcphdr))) {
    pcap_breakloop(pud->pd);
  }
}

/* thread */
void *new_packet_capture_run(void *ptr)
{
  pcap_t *pd;
  char errbuf[PCAP_ERRBUF_SIZE];
  bpf_u_int32 netp;
  bpf_u_int32 maskp;
  struct bpf_program fprog;
  struct pcap_userdata pud;

  char *filter = "dst host 10.0.2.15 and src port 80 and dst port 54321";
  char *dev = "enp0s3";
  int dl = 0;
  int dl_len = 0;

  if ((pd = pcap_open_live(dev, 1514, 1, 500, errbuf)) == NULL) {
    ERROR("pcap_open_live");
  }

  pcap_lookupnet(dev, &netp, &maskp, errbuf);
  pcap_compile(pd, &fprog, filter, 0, netp);

  if (pcap_setfilter(pd, &fprog) == -1) {
    ERROR("pcap_setfilter");
  }

  pcap_freecode(&fprog);
  dl = pcap_datalink(pd);

  switch (dl) {
  case 1:
    dl_len = 14;
    break;
  default:
    dl_len = 14;
    break;
  }

  pud.l1_len = dl_len;
  pud.pd = pd;

  if (pcap_loop(pd, -1, raw_packet_receiver, (u_char *)&pud) < 0) {
    printf("found syn_ack packet by new_packet_capture_run thread\n");
  }
}

unsigned short cksum(unsigned short *ptr, int nbytes)
{
  register long sum;
  unsigned short oddbyte;
  register short answer;

  sum = 0;
  while (nbytes > 1) {
    sum += *ptr++;
    nbytes -= 2;
  }
  if (nbytes == 1) {
    oddbyte = 0;
    *((u_char *)&oddbyte) = *(u_char *)ptr;
    sum += oddbyte;
  }

  sum = (sum >> 16) + (sum & 0xffff);
  sum = sum + (sum >> 16);
  answer = (short)~sum;

  return (answer);
}

char *hostname_to_ip(char *hostname)
{
  struct hostent *he;
  struct in_addr **addr_list;
  int i;

  if ((he = gethostbyname(hostname)) == NULL) {
    herror("gethostbyname");
    return NULL;
  }

  addr_list = (struct in_addr **)he->h_addr_list;

  for (i = 0; addr_list[i] != NULL; i++) {
    return inet_ntoa(*addr_list[i]);
  }

  return NULL;
}

int local_ip(char *buffer)
{
  int err;
  int sock;
  const char *p;
  struct sockaddr_in serv;
  struct sockaddr_in name;
  socklen_t namelen;

  sock = socket(AF_INET, SOCK_DGRAM, 0);

  memset(&serv, 0, sizeof(serv));
  serv.sin_family = AF_INET;
  serv.sin_addr.s_addr = inet_addr("8.8.8.8");
  serv.sin_port = htons(53);

  err = connect(sock, (const struct sockaddr *)&serv, sizeof(serv));

  namelen = sizeof(name);
  err = getsockname(sock, (struct sockaddr *)&name, &namelen);
  p = inet_ntop(AF_INET, &name.sin_addr, buffer, 100);

  close(sock);
}

/* thread */
void *new_packet_reciever(void *ptr)
{
  int socket_raw, saddr_size, data_size;
  struct sockaddr saddr;
  unsigned char *buffer = (unsigned char *)malloc(65536);

  socket_raw = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
  if (socket_raw < 0) {
    ERROR("socket");
  }
  saddr_size = sizeof(saddr);

  while (1) {
    data_size = recvfrom(socket_raw, buffer, 65536, 0, (struct sockaddr *)&saddr, &saddr_size);

    if (data_size < 0) {
      ERROR("recvfrom");
    }

    if (found_syn_ack_packet(buffer, data_size)) {
      printf("found syn_ack packet by new_packet_reciever thread\n");
      break;
    }
  }
  close(socket_raw);
}

int main(int argc, char **argv)
{
  int s;
  int src_port, dst_port;
  char *dst_host;
  char packet[4096];
  char source_ip[20];
  struct sockaddr_in dest;
  struct pseudo_header psh;
  struct iphdr *iph = (struct iphdr *)packet;
  struct tcphdr *tcph = (struct tcphdr *)(packet + sizeof(struct ip));
  int flag = 1;
  const int *val = &flag;

  if (argc != 3) {
    printf("dst_host dst_port \n");
    exit(1);
  }

  src_port = 54321;
  dst_host = argv[1];
  dst_port = atoi(argv[2]);

  s = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
  if (s < 0) {
    ERROR("socket");
  }

  if (inet_addr(dst_host) != -1) {
    dest_ip.s_addr = inet_addr(dst_host);
  } else {
    char *ip = hostname_to_ip(dst_host);
    if (ip != NULL) {
      printf("%s resolved to %s\n", dst_host, ip);
      dest_ip.s_addr = inet_addr(hostname_to_ip(dst_host));
    } else {
      printf("Unable to resolve hostname: %s", dst_host);
      exit(1);
    }
  }

  local_ip(source_ip);

  memset(packet, 0, 4096);

  iph->ihl = 5;
  iph->version = 4;
  iph->tos = 0;
  iph->tot_len = sizeof(struct ip) + sizeof(struct tcphdr);
  iph->id = htons(54321);
  iph->frag_off = htons(16384);
  iph->ttl = 64;
  iph->protocol = IPPROTO_TCP;
  iph->check = 0;
  iph->saddr = inet_addr(source_ip);
  iph->daddr = dest_ip.s_addr;
  iph->check = cksum((unsigned short *)packet, iph->tot_len >> 1);

  tcph->source = htons(src_port);
  tcph->dest = htons(80);
  tcph->seq = htonl(1105024978);
  tcph->ack_seq = 0;
  tcph->doff = sizeof(struct tcphdr) / 4;
  tcph->fin = 0;
  tcph->syn = 1;
  tcph->rst = 0;
  tcph->psh = 0;
  tcph->ack = 0;
  tcph->urg = 0;
  tcph->window = htons(14600);
  tcph->check = 0;
  tcph->urg_ptr = 0;

  if (setsockopt(s, IPPROTO_IP, IP_HDRINCL, val, sizeof(flag)) < 0) {
    printf("Error setting IP_HDRINCL. Error number : %d . Error message : %s \n", errno, strerror(errno));
    exit(0);
  }

  pthread_t new_packet_capture_run_thread;
  if (pthread_create(&new_packet_capture_run_thread, NULL, new_packet_capture_run, NULL) < 0) {
    ERROR("pthread_create");
  }

  pthread_t new_packet_reciever_thread;
  if (pthread_create(&new_packet_reciever_thread, NULL, new_packet_reciever, NULL) < 0) {
    ERROR("pthread_create");
  }

  dest.sin_family = AF_INET;
  dest.sin_addr.s_addr = dest_ip.s_addr;
  dest.sin_port = ntohs(tcph->dest);

  tcph->dest = htons(dst_port);
  tcph->check = 0;

  psh.source_address = inet_addr(source_ip);
  psh.dest_address = dest.sin_addr.s_addr;
  psh.placeholder = 0;
  psh.protocol = IPPROTO_TCP;
  psh.tcp_length = htons(sizeof(struct tcphdr));

  memcpy(&psh.tcp, tcph, sizeof(struct tcphdr));

  tcph->check = cksum((unsigned short *)&psh, sizeof(struct pseudo_header));

  if (sendto(s, packet, sizeof(struct iphdr) + sizeof(struct tcphdr), 0, (struct sockaddr *)&dest, sizeof(dest)) < 0) {
    ERROR("sendto");
  }

  pthread_join(new_packet_capture_run_thread, NULL);
  pthread_join(new_packet_reciever_thread, NULL);

  return 0;
}

これを以下のようにビルドします。

gcc raw_packet.c -g -lpcap -lpthread -o tcp_port_check

実行は、RAWソケットを扱うためCAP_NET_ADMINの特権が必要となるので、sudoで実行します。

sudo ./tcp_port_check 10.0.2.15 80

これにより、カーネルよりも速くユーザランドのTCPスタックでやりたいことができたらパケットのダンプが表示されるはず。では実行してみましょう。

raw packet recieved. but isn't syn_ack packet
raw packet recieved. but isn't syn_ack packet
raw packet recieved. but isn't syn_ack packet
raw packet recieved. but isn't syn_ack packet
raw packet recieved. but isn't syn_ack packet
ip version: 4
header length: 5byte
service type: 0x00
packet length: 44byte
identify: 0x9d0e
flag offset: 0x0000
ttl: 0x40
protocol: TCP
header checksum: 0xc97a
src ipaddress: xxx.xxx.xxx.xxx
dst ipaddress: 10.0.2.15
src port: 80
dst port: 54321
sec num: 1385297409
ack num: 1105024979
reserve : 0x0
offset: 0x600
fin: 0
syn: 1
rst: 0
psh: 0
ack: 1
urg: 0
res2: 0
window size: 65535
checksum: 0xcd0d
urgent ptr: 0x0

found syn_ack packet by new_packet_reciever thread  # ↑これは受信スレッド

ip version: 4
header length: 5byte
service type: 0x00
packet length: 44byte
identify: 0x9d0e
flag offset: 0x0000
ttl: 0x40
protocol: TCP
header checksum: 0xc97a
src ipaddress: xxx.xxx.xxx.xxx
dst ipaddress: 10.0.2.15
src port: 80
dst port: 54321
sec num: 1385297409
ack num: 1105024979
reserve : 0x0
offset: 0x600
fin: 0
syn: 1
rst: 0
psh: 0
ack: 1
urg: 0
res2: 0
window size: 65535
checksum: 0xcd0d
urgent ptr: 0x0

found syn_ack packet by new_packet_capture_run thread # ↑これはBPFスレッド

おおお!受信できた!パケットの数もたったの3パケットだ!

09:45:15.246002 IP (tos 0x0, ttl 64, id 54321, offset 0, flags [DF], proto TCP (6), length 40)
    10.0.2.15.54321 > 10.0.2.15.80: Flags [S], cksum 0xd948 (correct), seq 45298, win 14600, length 0
09:45:15.246015 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 44)
    10.0.2.15.80 > 10.0.2.15.54321: Flags [S.], cksum 0x183c (incorrect -> 0x5ca3), seq 3390123776, ack 45299, win 43690, options [mss 65495], length 0
09:45:15.246021 IP (tos 0x0, ttl 64, id 16593, offset 0, flags [DF], proto TCP (6), length 40)
    10.0.2.15.54321 > 10.0.2.15.80: Flags [R], cksum 0x124e (correct), seq 45299, win 0, length 0

やったーーーーー!

connectで9パケット送る必要無いし、9個も送ると遅くなるから、ユーザランドに簡易TCPスタックを作って、スレッドを駆使してカーネルがrst送りきるよりも速くsyn+ackパケット受信したり、BPFでパケットモニタリングすることによってパケット6個も減らして、全部で3パケットでポートのListenチェックできる処理が実現できたぞーーーーーー!!

あれ?

まとめ

ということで、結論としては高速にリモートホストのポートがListenしているかをチェックするには特権もいらないconnectで十分であることが分かりました(connectできなかった場合はたったの2パケット)。また、パケットを3つに抑えた所で、今回のような実装だと実際のチェックコストは下手したら1000倍ぐらいかかっており、パケット6個を減らすためにユーザランドTCPスタックの実装やパケットの自前実装、BPFによるフィルタリング等、色々頑張りましたが、ミドルウェアのようなソフトウェアから高速にリモートホストのポートのListen状態をチェックするという観点ではノーバリューでフィニッシュでした。

2017/02/13追記

冒頭で追記したように、

による指摘の通り、実際は普通にconnectしてcloseすると以下のようなtcpdumpでした。全部で普通は6パケットでした。

09:13:59.353115 IP (tos 0x0, ttl 64, id 45250, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.47536 > 127.0.0.1.53: Flags [S], cksum 0xfe30 (incorrect -> 0xd259), seq 3532090005, win 43690, options [mss 65495,sackOK,TS val 24894039 ecr 0,nop,wscale 7], length 0
09:13:59.353128 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.53 > 127.0.0.1.47536: Flags [S.], cksum 0xfe30 (incorrect -> 0x4130), seq 394239430, ack 3532090006, win 43690, options [mss 65495,sackOK,TS val 24894039 ecr 24894039,nop,wscale 7], length 0
09:13:59.353140 IP (tos 0x0, ttl 64, id 45251, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.47536 > 127.0.0.1.53: Flags [.], cksum 0xfe28 (incorrect -> 0x1375), seq 1, ack 1, win 342, options [nop,nop,TS val 24894039 ecr 24894039], length 0
09:14:04.641694 IP (tos 0x0, ttl 64, id 45252, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.47536 > 127.0.0.1.53: Flags [F.], cksum 0xfe28 (incorrect -> 0x0e49), seq 1, ack 1, win 342, options [nop,nop,TS val 24895362 ecr 24894039], length 0
09:14:04.642176 IP (tos 0x0, ttl 64, id 18320, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.53 > 127.0.0.1.47536: Flags [F.], cksum 0xfe28 (incorrect -> 0x091d), seq 1, ack 2, win 342, options [nop,nop,TS val 24895362 ecr 24895362], length 0
09:14:04.642189 IP (tos 0x0, ttl 64, id 45253, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.47536 > 127.0.0.1.53: Flags [.], cksum 0xfe28 (incorrect -> 0x091d), seq 2, ack 2, win 342, options [nop,nop,TS val 24895362 ecr 24895362], length 0

この方法により、カーネルのTCPスタックを使っても4パケットでフィニッシュ可能そうです。 ありがとうございます!

実際試すと(エラー処理は省略)、

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
  int s;
  int dst_port;
  char *dst_host;
  struct sockaddr_in dest;
  struct linger so_linger;

  dst_host = argv[1];
  dst_port = atoi(argv[2]);

  s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

  dest.sin_family = AF_INET;
  dest.sin_addr.s_addr = inet_addr(dst_host);
  dest.sin_port = ntohs(dst_port);

  connect(s, (struct sockaddr *)&dest, sizeof(dest));

  so_linger.l_onoff = 1;
  so_linger.l_linger = 0;
  setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger));

  close(s);

  return 0;
}

これだと、以下のようなtcpdump結果となりました。4パケットでフィニッシュしているので、カーネルのTCPのスタックを使う場合の最小パケット数と思われます。

09:32:01.527284 IP (tos 0x0, ttl 64, id 15485, offset 0, flags [DF], proto TCP (6), length 60)
    10.0.2.15.52154 > 10.0.2.15.80: Flags [S], cksum 0x184c (incorrect -> 0xdb93), seq 527746906, win 43690, options [mss 65495,sackOK,TS val 25164583 ecr 0,nop,wscale 7], length 0
09:32:01.527292 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    10.0.2.15.80 > 10.0.2.15.52154: Flags [S.], cksum 0x184c (incorrect -> 0xccf3), seq 2287569294, ack 527746907, win 43690, options [mss 65495,sackOK,TS val 25164583 ecr 25164583,nop,wscale 7], length 0
09:32:01.527299 IP (tos 0x0, ttl 64, id 15486, offset 0, flags [DF], proto TCP (6), length 52)
    10.0.2.15.52154 > 10.0.2.15.80: Flags [.], cksum 0x1844 (incorrect -> 0x9f38), seq 1, ack 1, win 342, options [nop,nop,TS val 25164583 ecr 25164583], length 0
09:32:01.527393 IP (tos 0x0, ttl 64, id 15487, offset 0, flags [DF], proto TCP (6), length 52)
    10.0.2.15.52154 > 10.0.2.15.80: Flags [R.], cksum 0x1844 (incorrect -> 0x9f34), seq 1, ack 1, win 342, options [nop,nop,TS val 25164583 ecr 25164583], length 0