Home > ブログ > AF_PACKETによるパケットの受信

ブログ

AF_PACKETによるパケットの受信

前回の記事でAF_PACKETを使ったパケット送信について書いたので、受信処理についてもまとめておきます。

AF_PACKET ソケットを使えばパケットを自由に作成して送信できたように、AF_PACKET ソケットで受信をするとパケット全体のデータを受信することができます。これを使ってtcpdumpやwiresharkのようなLANアナライザ的なものを作ることができます。

以下はデータをキャプチャしてダンプする処理の例です。

afpacket_recv.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <net/ethernet.h>
#include <net/if.h>
#include <netpacket/packet.h>

#define MAX_DUMP_SIZE  200

void usage()
{
	fprintf(stderr, "Usage:\n");
	fprintf(stderr, "afpacket_recv <options>\n\n");
	fprintf(stderr, "Options:\n");
	fprintf(stderr, "-i interface      Receiving interface\n");
	fprintf(stderr, "-p                promiscuous mode\n");
	exit(-1);
}

int create_socket(unsigned int ifindex, int promiscuous)
{
	int fd;
	struct sockaddr_ll if_sall;
	struct packet_mreq mr;

	/*
	 * AF_PACKET / SOCK_RAW ソケット作成
	 * 全プロトコルを受信するのでhtons(ETH_P_ALL)
	 * SOCK_DGRAMだとL2ヘッダより上位レイヤーのデータを受信する
	 */
	if ((fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1) {
		perror("socket()");
		return -1;
	}

	if (ifindex) {
		/*
		 * 受信インタフェースの指定。
		 * bindしないと全インタフェースからの受信
		 */
		memset(&if_sall, 0, sizeof(if_sall)); 
		if_sall.sll_family = AF_PACKET;
		if_sall.sll_ifindex = ifindex;
		if(bind(fd, (struct sockaddr *) &if_sall, sizeof(if_sall)) < 0) {
			perror("bind");
			close(fd);
			return -1;
		}
	} else {
		printf("Receive from all interfaces.\n");
	}

	/* promiscuous mode */
	if (ifindex && promiscuous) {
		printf("Promiscuous mode.\n");
		memset(&mr, 0, sizeof(mr));
		mr.mr_ifindex = ifindex;
		mr.mr_type    = PACKET_MR_PROMISC;
		if (setsockopt(fd, SOL_PACKET, 
			       PACKET_ADD_MEMBERSHIP, &mr, sizeof(mr)) == -1) {
			perror("setsockopt");
			close(fd);
			return -1;
		}
	}

	return fd;
}

void dump_buffer(const u_char *buff, size_t n, size_t limit)
{
	int i;

	for (i = 0 ; i < n ; i++) {
		if (i == limit) {
			printf("snipped...");
			break;
		}
		if (i % 16 == 0) {
			printf("%08x  ", i);
		}
		printf("%02x ", buff[i]);
		if (i % 16 == 15) {
			printf("\n");
		}
	}
	printf("\n");
}

void dump_sockaddr_ll(const struct sockaddr_ll *sall)
{
	int i;
	char name_buff[IF_NAMESIZE];
	int if_found = 0;

	if (if_indextoname(sall->sll_ifindex, name_buff) != NULL) {
		if_found = 1;
	}

	printf(".sll_family:   %d\n", sall->sll_family);
	printf(".sll_protocol: %04x\n", ntohs(sall->sll_protocol));
	printf(".sll_ifindex:  %d(%s)\n",
	       sall->sll_ifindex, if_found ? name_buff : "----");
	printf(".sll_hatype:   %04x\n", sall->sll_hatype);
	printf(".sll_pkttype:  %d\n", sall->sll_pkttype);
	printf(".sll_halen:    %d\n", sall->sll_halen);
	printf(".sll_addr:     ");
	for (i = 0 ; i < sall->sll_halen && i < 8 ; i++) {
		printf("%02x ", sall->sll_addr[i]);
	}
	printf("\n");
}

int main(int argc, char *argv[])
{
	char if_name[IF_NAMESIZE];
	unsigned int ifindex = 0;
	int opt;
	int promiscuous = 0;
	int fd;
	u_char buff[ETHERMTU];
	ssize_t pkt_size, received_size;
	struct sockaddr_ll from;
	socklen_t from_len = sizeof(from);

	while ((opt = getopt(argc, argv, "i:p")) != -1) {
		switch (opt) {
		case 'i':
			if_name[IF_NAMESIZE - 1] = 0;
			strncpy(if_name, optarg, IF_NAMESIZE - 1);	
			ifindex = if_nametoindex(if_name);
			if (ifindex == 0) {
				perror("if_nametoindex()");
				return -1;
			}
			break;
		case 'p':
			promiscuous = 1;
			break;
		default:
			usage();
		}
	}

	fd = create_socket(ifindex, promiscuous);
	if (fd == -1) {
		return -1;
	}

	while (1) {
		pkt_size = recvfrom(fd, buff, sizeof(buff), MSG_TRUNC,
				(struct sockaddr *)&from, &from_len);
		if (pkt_size == -1) {
			perror("recvfrom");
			break;
		}
		received_size = pkt_size > sizeof(buff) ?
			sizeof(buff) : pkt_size;
		printf("Received Length: %ld, Packet Length: %ld\n", received_size, pkt_size);

		if (from.sll_family != AF_PACKET) {
			continue;
		}
		dump_sockaddr_ll(&from);
		dump_buffer(buff, received_size, MAX_DUMP_SIZE);
	}

	close(fd);

	return 0;
}

使い方は以下のようになります(要root権限)。

# gcc -Wall -O2 afpacket_recv.c -o afpacket_recv
# ./afpacket_recv                全インタフェースのデータを受信
# ./afpacket_recv -i ens33       指定インタフェースのデータを受信
# ./afpacket_recv -i ens33 -p    promiscuousモードを設定

実行すると以下のように受信時のL2ヘッダの情報(sockaddr_ll)とパケットデータの16進ダンプを出力します。

Received Length: 115, Packet Length: 115
.sll_family:   17
.sll_protocol: 0800
.sll_ifindex:  2(ens33)
.sll_hatype:   0001
.sll_pkttype:  0
.sll_halen:    6
.sll_addr:     7a 7b 8a 8b 88 65 
00000000  00 0c 29 fc 55 24 7a 7b 8a 8b 88 65 08 00 45 00 
00000010  00 65 a8 5a 00 00 40 11 06 0a ac 10 3a 01 ac 10 
--略--

やっていることは基本的に前回の記事で作成したAF_PACKET / SOCK_RAWのソケットでrecv()するだけですが、要点を説明していきます。

まずはソケットの作成です。AF_PACKET / SOCK_RAW ソケットを作成していますが、protocol引数には受信したいプロトコルを指定します。ここでいうプロトコルとは、イーサネットフレームのEther Typeフィールドに設定される上位層プロトコルの値のことを示します。IPv4なら0x0800(ETH_P_IP)、ARPなら0x0806(ETH_P_ARP)、IPv6なら0x086dd(ETH_P_IPV6)という形になります(*1)。TCP/UDPとかHTTPとかのより上位層レベルのものではありません。

全プロトコルを受信したいならETH_P_ALLを指定します。ちなみに送信だけならprotocolは0で構いません。

	if ((fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1) {
		perror("socket()");
		return -1;
	}

ここではSOCK_RAWを指定しましたが、SOCK_DGRAMを指定した場合は、受信データはL2ヘッダより上位層部分のみになります。

次に受信インタフェースの指定です。AF_PACKETソケットをbind()すると受信インタフェースを指定することができます。bind()しなければ、全インタフェースからデータを受信します。インタフェースの指定はsockaddr_ll構造体を使います。

インタフェースの指定だけでなく.sll_protocolでプロトコルを絞り込むこともできます。.sll_protocolが0ならsocket()で指定したprotocolに従いますが、.sll_protocolを指定すれば、socket()で指定したprotocolを上書き設定できます。

	memset(&if_sall, 0, sizeof(if_sall)); 
	if_sall.sll_family = AF_PACKET;
	if_sall.sll_ifindex = ifindex;
	/* if_sall.sll_protocol = htons(ETH_P_IP); */  /* ここでprotocolを指定することも可能 */
	if(bind(fd, (struct sockaddr *) &if_sall, sizeof(if_sall)) < 0) {
		perror("bind");
		close(fd);
		return -1;
	}

これで作成したソケットから"自分宛"のパケットを受信することができます。回線上に流れている他ノード宛のパケットも受信したい場合は、Promiscuousモードにする必要があります。

	memset(&mr, 0, sizeof(mr));
	mr.mr_ifindex = ifindex;
	mr.mr_type    = PACKET_MR_PROMISC;
	if (setsockopt(fd, SOL_PACKET, 
		       PACKET_ADD_MEMBERSHIP, &mr, sizeof(mr)) == -1) {
		perror("setsockopt");
		close(fd);
		return -1;
	}

受信はrecv()でも行えますが、ここではrecvfrom()を使って送信者の情報も取得しています。AF_PACKETソケットの場合、recvfrom()のsrc_addr引数にはsockaddr_llを使います。sockaddr_llには受信インタフェースやSource MACアドレス等の情報が設定されて返されます。

AF_PACKET / SOCK_DGRAM ソケットを使った場合は、受信データにL2ヘッダは存在しませんが、sockaddr_llからL2ヘッダの情報を取得することができます。

	pkt_size = recvfrom(fd, buff, sizeof(buff), MSG_TRUNC,
			(struct sockaddr *)&from, &from_len);

あと、MSG_TRUNCフラグを指定していますが、これは受信バッファーのサイズが足りなかった時でも、実際のパケットサイズを取得するためにこのようにしています。

pcapの方が楽

以上でAF_PACKETでのパケット受信の説明は終わりですが、man 7 packet には、移植性が必要ならpcapを使えとあります。

AF_PACKETはLinux固有のものなので、例えばBSD系(おそらくMacも)で同様のパケット受信を行おうとするとBPF(Berkeley Packet Filter)を使う必要があります。以前、Linux/FreeBSDで動作するプロトコルシミュレータのようなものを作ったことがありますが、ソケット処理を抽象化するのが面倒だった記憶があります(*2)。現在では私も受信に関してはAF_PACKETを直接使うことはまずなく、pcapを使います。

おまけでpcapを使った受信処理の例です。

pcap.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <pcap/pcap.h>

#define MAX_DUMP_SIZE  200

void usage()
{
	fprintf(stderr, "Usage:\n");
	fprintf(stderr, "pcap <if name>\n");
	exit(-1);
}

void dump_buffer(const u_char *buff, size_t n, size_t limit)
{
	int i;

	for (i = 0 ; i < n ; i++) {
		if (i == limit) {
			printf("snipped...");
			break;
		}
		if (i % 16 == 0) {
			printf("%08x  ", i);
		}
		printf("%02x ", buff[i]);
		if (i % 16 == 15) {
			printf("\n");
		}
	}
	printf("\n");
}

void proc_packet(u_char *user, struct pcap_pkthdr *info, const u_char *buff)
{
	struct tm t;

	localtime_r(&(info->ts.tv_sec), &t);

	printf("%4d-%02d-%02d %02d:%02d:%02d.%ld Received a Packet\n",
	       t.tm_year + 1900,
	       t.tm_mon  + 1,
	       t.tm_mday,
	       t.tm_hour,
	       t.tm_min,
	       t.tm_sec,
	       info->ts.tv_usec);
	printf("Captured Length: %d, Packet Length: %d\n", info->caplen, info->len);

	dump_buffer(buff, info->caplen, MAX_DUMP_SIZE);
}

int main(int argc, char *argv[])
{
	char *if_name;
	pcap_t *capt = NULL;
	char err_str[PCAP_ERRBUF_SIZE];

	if (argc < 2) {
		usage();
	}

	if_name = argv[1];

	if((capt = pcap_open_live(if_name, 65536, 1, 250, err_str)) == NULL) {
		fprintf(stderr, "%s", err_str);
		return(-1);
	}

	printf("-- Start capture --\n");

	while(1) {
		if(pcap_loop(capt, -1, (pcap_handler) proc_packet, (u_char *) NULL) == -1) {
			fprintf(stderr, "err");
			return -1;
		}
	}

	return 0;
}

ビルドと実行。

# gcc -Wall -O2  pcap.c -lpcap -o pcap
# ./pcap ens33

[参考]

  • man 7 packet
  • man 2 recvfrom

[関連記事]

(*1) ネットワークバイトオーダーで指定する必要があるのでhtons()する必要があります。

(*2) 受信はpcapを使えばいいのですが、送信に関しては適当なライブラリがなかったので自分で抽象化する必要がありました。今では何かあるのかもしれませんが。

投稿日:2021/11/10 12:15

タグ: ネットワーク プログラミング C

Top

アーカイブ

タグ

Server (15) 作業実績 (12) プログラミング (7) ネットワーク (6) C++ (6) Webアプリ (5) PHP (5) Linux (4) laravel (4) C (4) JavaScript (3) Nginx (3) AWS (2) Golang (2) EC-CUBE (2) Vue.js (2) 書籍 (2) Apache (1) デモ (1) Rust (1) CreateJS (1) OSS (1)

技術的な情報は以下にもあります。