Gratuitous ARPを送信したい
お客様にLinux上で動作するデーモンからGratuitous ARPを送信したいと相談頂いたので、そのお手伝いをしました。ARPに限らず自由にパケットを作成して送信したい時の方法をまとめておきます。
Gratuitous ARPとは
通常、ARPはIPv4アドレスをMACアドレスに変換するのに使われますが、Gratuitous ARPパケットは同一LANセグメントに自分と同じIPアドレスを使っているノードがいないか確認したり、他ノードのARPキャッシュに登録されているMACを更新させるのに使います。
パケットの特徴としては、通常のARPリクエストではTarget IP Addressに解決したいIPv4アドレスを設定しますが、Gratuitous ARPではTarget IP AddressにSender IP Addressと同じものを設定してBoadcastします。
今回はパケット送信方法の解説が目的なので、Gratuitous ARP自体の説明はこのくらいにしておきます。
AF_PACKETソケット
ネットワーク通信はソケットを介して行いますが、ソケット通信で使うのはほとんどTCPやUDPだと思います。socket()関数に渡すdomain/typeの組み合わせはTCPならAF_INET / SOCK_STREAM、UDPならAF_INET / SOCK_DGRAMです。
socket()に渡すdomain引数にはAF_PACKET(参照 man 7 packet)というものがあり、これを使うと自由にパケットを作成して送信することができます。今回のようにARPパケットを送信したい場合は、このAF_PACKETのソケットを使います。
AF_PACKETのソケットのtypeとしてはSOCK_DGRAMかSOCK_RAWのいずれかが選択できます。
SOCK_DGRAMを使うとL2ヘッダより上位層のパケットを作成して送信できます。L2ヘッダは送信時にカーネルによって自動で作成されます。SOCK_RAWの場合、L2ヘッダも含めてパケットを作成できます。このためSource Macなども自由に設定することができます。
SOCK_DGRAM、SOCK_RAWの違いを図1にまとめました。

AF_PACKETソケットではsendto()で送信する際、送信先をstruct sockaddr_llで指定しますが、この指定方法にも違いがあります。.sll_ifindexで送信インタフェースを指定するのは SOCK_DGRAM, SOCK_RAW どちらも同じですが、SOCK_DGRAMでは、ペイロードのプロトコル種別や宛先MACも指定する必要があります。カーネルはこの情報からL2ヘッダを作成して送信しています。
今回の用途ではAF_PACKET / SOCK_DGRAMのソケットを使います。
いきなりですが、Gratuitous ARPを送信するソースは以下のようになります。
garp.c (AF_PACKET / SOCK_DGRAM)
#include <stdio.h> #include <arpa/inet.h> #include <errno.h> #include <string.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/socket.h> #include <stdlib.h> #include <unistd.h> #include <net/ethernet.h> #include <net/if.h> #include <net/if_arp.h> #include <netpacket/packet.h> struct arppkt { uint16_t ar_hrd; /* format of hardware address */ uint16_t ar_pro; /* format of protocol address */ uint8_t ar_hln; /* length of hardware address */ uint8_t ar_pln; /* length of protocol address */ uint16_t ar_op; /* ARP opcode (command) */ uint8_t ar_sha[ETH_ALEN]; /* sender hardware address */ uint8_t ar_sip[4]; /* sender IP address */ uint8_t ar_tha[ETH_ALEN]; /* target hardware address */ uint8_t ar_tip[4]; /* target IP address */ }; void usage() { fprintf(stderr, "Usage:\n"); fprintf(stderr, "garp_dgram <if name>\n"); exit(-1); } /* AF_PACKET / SOCK_DGRAMソケットの作成 */ int create_socket() { int fd; if ((fd = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL))) == -1) { perror("socket()"); return -1; } return fd; } int send_packet(const unsigned char *buff, size_t size, const struct sockaddr_ll *sall) { int fd; fd = create_socket(); if (fd == -1) { return -1; } if (sendto(fd, buff, size, 0, (const struct sockaddr *) sall, sizeof(struct sockaddr_ll)) == -1) { perror("send_packet"); close(fd); return -1; } close(fd); return 0; } unsigned char *create_garp_packet(const struct sockaddr *mac, const struct sockaddr_in *saddr, size_t *size) { unsigned char *buff; struct arppkt *arphdr; *size = sizeof(struct arppkt); buff = malloc(*size); if (buff == NULL) { return NULL; } arphdr = (struct arppkt *) buff; arphdr->ar_hrd = htons(0x0001); /* Ethernet */ arphdr->ar_pro = htons(ETH_P_IP); /* 0x0800 */ arphdr->ar_hln = ETH_ALEN; arphdr->ar_pln = sizeof(struct in_addr); arphdr->ar_op = htons(0x0001); /* Request */ memcpy(arphdr->ar_sha, mac->sa_data, ETH_ALEN); memcpy(arphdr->ar_sip, &saddr->sin_addr, sizeof(struct in_addr)); memset(arphdr->ar_tha, 0, ETH_ALEN); memcpy(arphdr->ar_tip, &saddr->sin_addr, sizeof(struct in_addr)); return buff; } int send_garp(const struct sockaddr *hwaddr, const struct sockaddr_in *saddr, const char *if_name) { unsigned int ifindex; unsigned char *packet; size_t packet_size; struct sockaddr_ll sall; ifindex = if_nametoindex(if_name); if (ifindex == 0) { perror("if_nametoindex()"); return -1; } packet = create_garp_packet(hwaddr, saddr, &packet_size); if (packet == NULL) { return -1; } /* 宛先設定。L2ヘッダはこの情報を元に生成される */ memset(&sall, 0, sizeof(sall)); sall.sll_family = AF_PACKET; sall.sll_protocol = htons(ETH_P_ARP); /* 0x0806 */ sall.sll_ifindex = ifindex; /* 送信IF */ sall.sll_halen = ETH_ALEN; memset(&sall.sll_addr, 0xff, ETH_ALEN); /* broadcast */ if (send_packet(packet, packet_size, &sall) == -1) { free(packet); return -1; } free(packet); return 0; } /* 指定インタフェースのIPアドレスを取得 */ int get_inaddr(const char *if_name, struct sockaddr_in *saddr) { int fd; struct ifreq ifr; if ((fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket()"); return -1; } memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, if_name, sizeof(ifr.ifr_name) - 1); if(ioctl(fd, SIOCGIFADDR, &ifr) < 0){ perror("ioctl(SIOCGIFADDR)"); close(fd); return -1; } *saddr = *((struct sockaddr_in *) &ifr.ifr_addr); return 0; } /* 指定インタフェースのMACアドレスを取得 */ int get_hwaddr(const char *if_name, struct sockaddr *addr) { int fd; struct ifreq ifr; if ((fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket()"); return -1; } memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, if_name, sizeof(ifr.ifr_name) - 1); if(ioctl(fd, SIOCGIFHWADDR, &ifr) < 0){ perror("ioctl(SIOCGIFHWADDR)"); close(fd); return -1; } *addr = ifr.ifr_hwaddr; return 0; } int main(int argc, char *argv[]) { const char *ifname; struct sockaddr_in saddr; struct sockaddr hwaddr; if (argc < 2) { usage(); } ifname = argv[1]; if (get_inaddr(ifname, &saddr) == -1) { return -1; } if (get_hwaddr(ifname, &hwaddr) == -1) { return -1; } if (hwaddr.sa_family != ARPHRD_ETHER) { fprintf(stderr, "The interface is not ethernet."); return -1; } if (send_garp(&hwaddr, &saddr, ifname) == -1) { return -1; } printf("Sended a gratuitous ARP.\n"); return 0; }
使い方は以下のようにGratuitous ARPを送信したいインタフェース名を指定します(要root権限)。
# gcc -Wall -O2 garp.c -o garp # ./garp ens33
IPアドレスやMACアドレスを取得したり色々余計なものがありますが、パケット送信に重要なのは以下の関数になります。
- create_socket()
- create_garp_packet()
- send_garp()
- send_packet()
create_socket()ではAF_PACKET / SOCK_DGRAMソケットを作成しています。
create_garp_packet()ではGratuitous ARPパケットのデータを作成します。L2ヘッダを除く部分を作成しています。
send_garp()ではcreate_garp_packet()で作成したパケットをsend_packet()を使って送信しています。この時、宛先をstruct sockaddr_llに設定しています。sockaddr_llの.sll_ifindexで送信インタフェースを指定し、.sll_protocol, .sll_addrの情報に基づいてL2ヘッダが作成されます。
AF_PACKET / SOCK_RAWソケット
今回の用途では AF_PACKET / SOCK_DGRAM のソケットで片付きますが、おまけで、AF_PACKET / SOCK_RAWのソケットでの例も書いておきます。
garp.c (AF_PACKET / SOCK_RAW)
#include <stdio.h> #include <arpa/inet.h> #include <errno.h> #include <string.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/socket.h> #include <stdlib.h> #include <unistd.h> #include <net/ethernet.h> #include <net/if.h> #include <net/if_arp.h> #include <netpacket/packet.h> /* 以下の定義は先と同じなので省略 * struct arppkt * usage() * send_packet() * get_inaddr() * get_hwaddr() */ int create_socket() { int fd; if ((fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1) { perror("socket()"); return -1; } return fd; } unsigned char *create_garp_packet(const struct sockaddr *mac, const struct sockaddr_in *saddr, size_t *size) { unsigned char *buff; struct ethhdr *etherhdr; struct arppkt *arphdr; *size = sizeof(struct ethhdr) + sizeof(struct arppkt); buff = malloc(*size); if (buff == NULL) { return NULL; } etherhdr = (struct ethhdr *) buff; arphdr = (struct arppkt *) (buff + sizeof(struct ethhdr)); memset(etherhdr->h_dest, 0xff, ETH_ALEN); memcpy(etherhdr->h_source, mac->sa_data, ETH_ALEN); etherhdr->h_proto = htons(ETH_P_ARP); /* 0x0806 */ arphdr->ar_hrd = htons(0x0001); /* Ethernet */ arphdr->ar_pro = htons(ETH_P_IP); /* 0x0800 */ arphdr->ar_hln = ETH_ALEN; arphdr->ar_pln = sizeof(struct in_addr); arphdr->ar_op = htons(0x0001); /* Request */ memcpy(arphdr->ar_sha, mac->sa_data, ETH_ALEN); memcpy(arphdr->ar_sip, &saddr->sin_addr, sizeof(struct in_addr)); memset(arphdr->ar_tha, 0, ETH_ALEN); memcpy(arphdr->ar_tip, &saddr->sin_addr, sizeof(struct in_addr)); return buff; } int send_garp(const struct sockaddr *hwaddr, const struct sockaddr_in *saddr, const char *if_name) { unsigned int ifindex; unsigned char *packet; size_t packet_size; struct sockaddr_ll saddr_oif; ifindex = if_nametoindex(if_name); if (ifindex == 0) { perror("if_nametoindex()"); return -1; } packet = create_garp_packet(hwaddr, saddr, &packet_size); if (packet == NULL) { return -1; } /* 送信IFを指定。SOCK_RAWではsockaddr_llにifindexを指定する。 */ memset(&saddr_oif, 0, sizeof(saddr_oif)); saddr_oif.sll_family = AF_PACKET; saddr_oif.sll_ifindex = ifindex; if (send_packet(packet, packet_size, &saddr_oif) == -1) { free(packet); return -1; } free(packet); return 0; } /* main()の定義は同じなので省略 */
大きな違いは以下になります。
- create_garp_packet()でL2ヘッダも含めたパケットデータを作成している。
- 宛先の指定はsockaddr_ll.sll_ifindexで送信インタフェースを指定するだけ。
L2フレームはEthernet V2(いわゆるDIX仕様)形式で作成しています。SOCK_RAWではL2ヘッダも自前で作成しなければならないことから、Tagged VLANやIEEE 802.3等に対応したい場合は自分でL2ヘッダを作成する必要があります。
SOCK_RAWの場合は、L2ヘッダごと作成できるのでSource MACを書き換えることもできます。L2ヘッダまで作成したくなることは稀ですが、対ネットワーク機器(ルータ/スイッチ、etc)のテストツールを作る時などには重宝します。
ちなみに以前はSOCK_RAWではなく AF_INET / SOCK_PACKET の組み合わせが使われていましたが、現在は廃止されています(*1)。また、AF_PACKET / SOCK_PACKET でも動作はするようですが、SOCK_PACKETでは送信インタフェースの指定を以下のように名前で指定しなければいけなかったりとイケてない点もあります。今後はSOCK_RAWの方を使っていきましょう。
SOCK_PACKETでの宛先指定
/* ifname is 'char *if_name' */ struct sockaddr saddr_oif; memset(&saddr_oif, 0, sizeof(saddr_oif)); srncpy(saddr_oif.sa_data, if_name, sizeof(saddr_oif.sa_data) - 1);
[参考]
- man 2 socket
- man 7 packet
[関連記事]
(*1) 動作はしますがdmesgに"xxxx uses obsolete (PF_INET,SOCK_PACKET)"とログが出力されます。
投稿日:2021/11/01 13:55