読者です 読者をやめる 読者になる 読者になる

Jの衝動書き日記

さらりーまんSEの日記でございます。

ARPパケットの取り扱い

仕事 技術

お仕事で、ARPパケットを扱ったのでそのメモを残しておく。Linuxの話である。

ARPパケットの取り扱い方

次の手順で行う。

  1. バイスソケットを作成する。socketを使用。
  2. パケットを送受信したいIFにbindする。しない場合は、すべてのIFから受信する。
  3. 作成したソケットにread/writeして送受信する。

なんだ、普通のソケットプログラミングと一緒ではないかと思うかもしれないがその通りである。
openしたソケットに対する送受信に違いがないのがUnixの強みなのだ。

1.デバイスソケットの作成

int arp_sock = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ARP))

socketの第一引数にAF_PACKETを、第3引数にETH_P_ARP:0x0806(ネットワークオーダ変換)を設定しているのが通常のソケット作成と異なる。こうして作成したソケットから、ARPパケットが送受信可能となる。
ちなみに、第2引数にはSOCK_RAWを指定してもいいが、その場合、受信したパケットには、Ethernetフレームヘッダの送信元、送信先MACアドレス、フレームタイプが付くし、送信時のデータにはこれらを付与しておく必要がある。
SOCK_DGRAMの場合は、受信時にはこれらは取り除かれ、送信時には送信時の設定にしたがって付与される。
ちなみに、このソケットをオープンできるのは、実効ユーザーID が 0 (root)のプロセスか、CAP_NET_RAW ケーパビリティを持つプロセス(RAWソケットとPACKETソケットの使用を許可されたプロセス setpcap等で付与?)だけである。

2.デバイスソケットのbind

作成したソケットは、bindしない場合、すべてのIFのパケットを受信する。今回場合は、ARPパケットをすべてのIFから受信することになる。
特定のIFからのみに限定したい場合は、ソケットをbindする。

struct sockaddr_ll sockaddr;
memset(&sockaddr, 0x0, sizeof(sockaddr));
sockaddr.sll_family = AF_PACKET;
sockaddr.sll_protocol = htons(ETH_P_ARP);
sockaddr.sll_ifindex = if_nametoindex("eth0");  /* bind対象となるIF番号を取得して設定 */
if(bind(arp_sock, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) < 0) {

bindで使用する構造体が通常のソケットとは異なるが、あとは一緒である。sockaddr_llの定義はman -s 7 packetで見るとよい。

3.デバイスソケットの送受信

通常のソケットと同じである。ただ、送信の場合、sendtoでは引数にsockaddr_llを用いて、これに送信元のIF番号と送信先MACアドレスを設定する。

また、ソケットのデータ受信は、データをコピーしたものを受け取っているので、ソケットのデータを読み取ったからといって、本来受け取るべきモジュールにデータが行かないなんてことはない(そんなことになったらtcpdumpするとデータすべて吸われてしまう)。

受信したデータを解析する時と、送信用データを組み立てるときは定義済の構造体を用いる。

#include <netinet/if_ether.h>
struct  ether_arp {
    struct  arphdr ea_hdr;      /* fixed-size header */
    u_int8_t arp_sha[ETH_ALEN]; /* sender hardware address */
    u_int8_t arp_spa[4];        /* sender protocol address */
    u_int8_t arp_tha[ETH_ALEN]; /* target hardware address */
    u_int8_t arp_tpa[4];        /* target protocol address */
}
#include <net/if_arp.h>
struct arphdr
  {
    unsigned short int ar_hrd;      /* Format of hardware address.  */
    unsigned short int ar_pro;      /* Format of protocol address.  */
    unsigned char ar_hln;       /* Length of hardware address.  */
    unsigned char ar_pln;       /* Length of protocol address.  */
    unsigned short int ar_op;       /* ARP opcode (command).  */
  };

SOCK_RAWの場合は、Ethernetヘッダも必要だが、これも定義済だ。

#include <net/ethernet.h>
struct ether_header
{
  u_int8_t  ether_dhost[ETH_ALEN];  /* destination eth addr */
  u_int8_t  ether_shost[ETH_ALEN];  /* source ether addr    */
  u_int16_t ether_type;             /* packet type ID field */
}

サンプル

サンプルなので、エラー処理はめんどくさいため簡略化のため省略している。お仕事ではこんなコード書いてはダメ!絶対!(直値使うとかないわーと思うアナタが正しい)。

受信

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <netpacket/packet.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/if_ether.h>
#include <net/if_arp.h>
#include <net/if.h>

void static print_ip(char*,unsigned char*);
void static print_ethaddr(char*, unsigned char*);
int main(int argc, char* argv) {

    int arp_sock = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ARP));
    if(arp_sock < 0) {
        perror("create arp sock");
        printf("errno: %d\n",errno);
        return 1;
    }
    /* ARPパケットを受信するIFをeth0に限定 */
    struct sockaddr_ll sockaddr;
    memset(&sockaddr, 0x0, sizeof(sockaddr));
    sockaddr.sll_family = AF_PACKET;
    sockaddr.sll_protocol = htons(ETH_P_ARP);
    sockaddr.sll_ifindex = if_nametoindex("eth0");
    if(bind(arp_sock, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) < 0) {
        perror("bind arp sock");
        printf("errno: %d\n",errno);
        return 1;
    }

    while(1) {
      char buf[256];
      memset(buf,0x0,sizeof(buf));
      int arp_size = recvfrom(arp_sock, buf, sizeof(buf), 0, NULL, NULL);
      if(arp_size < 0) {
         perror("recvfrom");
         printf("errno: %d\n",errno);
      }

      /* 受信したデータはARPパケットなので、その形にキャストして情報にアクセスする */
      struct ether_arp *arppack = (struct ether_arp*) buf;
      printf("operation : %d\n", ntohs(arppack->ea_hdr.ar_op));
      print_ethaddr("sender hardware address", arppack->arp_sha);
      print_ip("sender protocol address", arppack->arp_spa);
      print_ethaddr("target hardware address", arppack->arp_tha);
      print_ip("target protocol address", arppack->arp_tpa);
    }
    return 0;
}

void print_ip(char* name, unsigned char ipaddr
) {
   printf("%s : %3d.%3d.%3d.%3d\n", name, ipaddr[0],ipaddr[1],ipaddr[2],ipaddr[3]);
}
void print_ethaddr(char* name, unsigned char ethaddr) {
   printf("%s : %02x:%02x:%02x:%02x:%02x:%02x\n",name, ethaddr[0],ethaddr[1],ethaddr[2],et
haddr[3],ethaddr[4],ethaddr[5]);
}

実行結果の例は以下

operation : 1
sender hardware address : aa:bb:cc:dd:ee:01
sender protocol address :  10. 18.152.121
target hardware address : 00:00:00:00:00:00
target protocol address :  10. 18.152. 71
operation : 2
sender hardware address : aa:bb:cc:dd:ee:02
sender protocol address :  10. 18.152. 21
target hardware address : aa:bb:cc:dd:ee:00
target protocol address :  10. 18.152. 71

送信

例はGratuitous arpの送信例である。Gratuitous arpはIPの重複検出で使うもの。

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <netpacket/packet.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/if_ether.h>
#include <net/if_arp.h>
#include <net/if.h>
#include <sys/ioctl.h>

int main(int argc, char* argv) {

    int arp_sock = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ARP));
    if(arp_sock < 0) {
        perror("create arp sock");
        printf("errno: %d\n",errno);
        return 1;
    }

    /* 送信設定
     * 送信元のIFにeth1を指定->eth1のMACアドレスがetherパケットに設定

     * 送信先はブロードキャストを設定
     */
    struct sockaddr_ll sockaddr;
    memset(&sockaddr, 0x0, sizeof(sockaddr));
    sockaddr.sll_family = AF_PACKET;
    sockaddr.sll_protocol = htons(ETH_P_ARP);
    sockaddr.sll_ifindex = if_nametoindex("eth1");
    sockaddr.sll_halen = 6;
    memset(&sockaddr.sll_addr, 0xff, 6);

    /* eth1のMACアドレス取得
     * arpパケットに設定するための情報
     */
    struct ifreq ifr;
    memset(&ifr, 0, sizeof(ifr));
    strncpy(ifr.ifr_name, "eth1", 4);
    if(ioctl(arp_sock,SIOCGIFHWADDR,&ifr) < 0) {
        perror("get mac adder");
        printf("errno: %d\n",errno);
        return 1;
    }

    /* Gratuitous arpパケットの設定
     * 送信元MACアドレス:eth1のもの
     * 送信元、ターゲットIPアドレス:10.18.152.19
     * ターゲットMACアドレスは未設定
     */
    struct  ether_arp arpPacket;
    memset(&arpPacket, 0x0, sizeof(arpPacket));
    arpPacket.arp_hrd = htons(1);
    arpPacket.arp_pro = htons(ETHERTYPE_IP);
    arpPacket.arp_hln = 6;
    arpPacket.arp_pln = 4;
    arpPacket.arp_op  = htons(ARPOP_REQUEST);
    memcpy(arpPacket.arp_sha, ifr.ifr_hwaddr.sa_data, 6);
    inet_aton("10.18.152.19",(struct in_addr *)&arpPacket.arp_spa);
    memcpy(&arpPacket.arp_tpa,&arpPacket.arp_spa,4);

   if(sendto(arp_sock,(char *)&arpPacket, sizeof(arpPacket),
             0, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0) {
        perror("sendto");
        printf("errno: %d\n",errno);
        return 1;
   }

   return 0;
}

結論

tcpdumpでパケットキャプチャして、Wiresharkで見ようね!

余談

ifconfigでIPを設定した場合、Gratuitous arpは投げられない。ただ、arpingコマンドを使えば投げれるっぽい。
お仕事のプロジェクトでは、両系ACTの検知用として、VIP設定時にGratuitous arpのReplyを投げたりしていた。
ちなみにarpキャッシュの更新は、RequestでもReplyでもされるようだ。

参考

詳解TCP/IP〈Vol.1〉プロトコル