今回はデータリンク層で流れるフレームを解析してヘッダー値を表示したいと思います。データリンク層はハードウェアに依存するのですが、本記事ではイーサネットに限定して実装していきたいと思います。
イーサネットヘッダー自体は非常にシンプルで見るべきところは大してないため、ARP/RARPヘッダーの解析を併せて実施していきたいと思います。
データリンク層を解析するサンプルプログラム
最初にサンプルプログラム掲載します。細かい解説はその後におこなっています。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/ethernet.h>
#include <net/if.h>
#include <netinet/ether.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <linux/if_packet.h>
char *
op2str(unsigned short op) {
switch (op) {
case ARPOP_REQUEST:
return "ARP request";
case ARPOP_REPLY:
return "ARP reply";
case ARPOP_RREQUEST:
return "RARP request";
case ARPOP_RREPLY:
return "RARP reply";
case ARPOP_InREQUEST:
return "InARP request";
case ARPOP_InREPLY:
return "InARP reply";
case ARPOP_NAK:
return "(ATM)ARP NAK";
default:
return "Unknown";
}
}
char *
proto2str(unsigned short p) {
switch (p) {
case ETHERTYPE_PUP:
return "Xerox PUP";
case ETHERTYPE_SPRITE:
return "Sprite";
case ETHERTYPE_IP:
return "IP";
case ETHERTYPE_ARP:
return "ARP";
case ETHERTYPE_REVARP:
return "RARP";
case ETHERTYPE_AT:
return "AppleTalk";
case ETHERTYPE_AARP:
return "AppleTalk ARP";
case ETHERTYPE_VLAN:
return "802.1q";
case ETHERTYPE_IPX:
return "IPX";
case ETHERTYPE_IPV6:
return "IPv6";
case ETHERTYPE_LOOPBACK:
return "Loopback";
default:
return "Unknown";
}
}
void
print_ipv4_header(unsigned char *p) {
struct ip *ip;
ip = (struct ip *) p;
if (ip->ip_v != 0x4)
return;
printf("------- IPv4 Header -------\n");
printf("ip_v: %d\n", ip->ip_v);
printf("ip_hl: %d\n", ip->ip_hl);
printf("ip_tos: %d\n", ip->ip_tos);
printf("ip_len: %d\n", ntohs(ip->ip_len));
printf("ip_id: %d\n", ntohs(ip->ip_id));
printf("ip_off: %d\n", ntohs(ip->ip_off));
printf("ip_ttl: %d\n", ip->ip_ttl);
printf("ip_p: %d\n", ip->ip_p);
printf("ip_sum: %d\n", ntohs(ip->ip_sum));
printf("ip_src: %s\n", inet_ntoa(ip->ip_src));
printf("ip_dst: %s\n", inet_ntoa(ip->ip_dst));
}
void
print_arp(unsigned char *p) {
struct arphdr *arp;
arp = (struct arphdr *) p;
printf("------- ARP/RARP -------\n");
printf("ar_hrd: %d", ntohs(arp->ar_hrd));
if (ntohs(arp->ar_hrd) == ARPHRD_ETHER)
printf("(Ethernet)\n");
else
printf("\n");
printf("ar_pro: 0x%x(%s)\n", ntohs(arp->ar_pro), proto2str(ntohs(arp->ar_pro)));
printf("ar_hln: %d\n", arp->ar_hln);
printf("ar_pln: %d\n", arp->ar_pln);
printf("ar_op: %d(%s)\n", ntohs(arp->ar_op), op2str(ntohs(arp->ar_op)));
if (ntohs(arp->ar_op) == ARPOP_REQUEST || ntohs(arp->ar_op) == ARPOP_REPLY
|| ntohs(arp->ar_op) == ARPOP_RREQUEST || ntohs(arp->ar_op) == ARPOP_RREPLY) {
struct ether_addr *sha, *tha;
struct in_addr sip, tip;
printf("--\n");
sha = (struct ether_addr *) (p + sizeof(struct arphdr));
memcpy(&sip.s_addr, (char *) sha + sizeof(struct ether_addr), sizeof(sip.s_addr));
tha = (struct ether_addr *) ((char *) sha + sizeof(struct ether_addr) + sizeof(uint32_t));
memcpy(&tip.s_addr, (char *) tha + sizeof(struct ether_addr), sizeof(tip.s_addr));
printf("source hardware address: %s\n", ether_ntoa(sha));
printf("source ip address: %s\n", inet_ntoa(sip));
printf("target hardware address: %s\n", ether_ntoa(tha));
printf("target ip address: %s\n", inet_ntoa(tip));
}
}
unsigned short
get_u2b(const unsigned char *p) {
return ((uint16_t) ntohs(*(uint16_t *) (p)));
}
unsigned short
print_dot1q(unsigned char *p) {
unsigned short tci;
unsigned short protocol;
printf("------- 802.1q -------\n");
tci = get_u2b((unsigned char *) p);
/* VLAN IDは下位12ビット */
printf("vlan: %u\n", tci & 0xfff);
/* PCPは上位3ビット */
printf("priority code point: %u\n", tci >> 13);
/* 13ビット目がDEI */
printf("drop eligible indicator: %u\n", (tci & 0x1000) ? 1 : 0);
/* 次の2バイト先にプロトコルが格納されている */
protocol = get_u2b(p+2);
return protocol;
}
int
main(int argc, char *argv[]) {
unsigned char buf[BUFSIZ];
unsigned char *p;
int sd;
unsigned int ifindex = 0;
struct ethhdr *e;
struct ether_addr *src, *dst;
unsigned short proto;
if (argc > 1) {
if ((ifindex = if_nametoindex(argv[1])) == 0) {
perror("if_nametoindex");
exit(1);
}
}
if ((sd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) < 0) {
perror("socket");
exit(1);
}
/*
* インタフェースを指定している場合はプロミスキャス・モードに設定する
*/
if (ifindex) {
struct sockaddr_ll sll;
struct packet_mreq mreq;
/*
* 指定したインタフェースのみ受信する
*/
memset(&sll, 0, sizeof(sll));
sll.sll_family = AF_PACKET;
sll.sll_protocol = htons(ETH_P_ALL);
sll.sll_ifindex = ifindex;
if (bind(sd, (struct sockaddr *) &sll, sizeof(sll)) < 0) {
perror("bind");
close(sd);
exit(1);
}
/*
* インタフェースをプロミスキャス・モードに設定する
*/
memset(&mreq, 0, sizeof(mreq));
mreq.mr_ifindex = ifindex;
mreq.mr_type = PACKET_MR_PROMISC;
if (setsockopt(sd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
perror("setsockopt");
exit(1);
}
printf("device %s entered promiscuous mode\n", argv[1]);
}
while (1) {
if (read(sd, buf, sizeof(buf)) < 0) {
perror("read");
exit(1);
}
e = (struct ethhdr *) buf;
dst = (struct ether_addr *) buf;
src = (struct ether_addr *) (buf + sizeof(struct ether_addr));
printf("======= Ethernet Header =======\n");
printf("src: %s\n", ether_ntoa(src));
printf("dst: %s\n", ether_ntoa(dst));
printf("protocol: %s(0x%x)\n", proto2str(ntohs(e->h_proto)), ntohs(e->h_proto));
/* ARPとRARP、IPv4だけヘッダーを表示させる */
p = buf + sizeof(struct ethhdr);
proto = ntohs(e->h_proto);
/* タグVLAN(802.1q)の場合 */
if (proto == ETHERTYPE_VLAN) {
proto = print_dot1q(p);
printf("protocol: %s(0x%x)\n", proto2str(proto), proto);
p += 4;
}
/* IPv4の場合 */
if (proto == ETHERTYPE_IP)
print_ipv4_header(p);
/* ARP/RARPの場合 */
if (proto == ETHERTYPE_ARP || proto == ETHERTYPE_REVARP)
print_arp(p);
printf("\n");
}
}Linuxはsocket(2)を使ってデータリンク層のフレームを読み出します。データリンク層から読み出すには、socket(2)のtypeに「SOCK_RAW」を指定します。これでデータリンク層にアクセスできます。
if ((sd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) < 0) {
perror("socket");
exit(1);
}}今回はインタフェースを指定できるようにしており、引数にインタフェースを指定するとbind(2)で特定のインタフェースのみ受信するように設定します。また、インタフェースを指定している場合、そのインタフェースをプロミスキャスモードに設定します。プロミスキャスモードというのは別名「無差別モード」とも呼ばれるもので、自分宛てでないフレームを破棄しません。自分宛てというのはIPアドレスではなくてMACアドレスを指します。
/*
* インタフェースを指定している場合はプロミスキャス・モードに設定する
*/
if (ifindex) {
struct sockaddr_ll sll;
struct packet_mreq mreq;
/*
* 指定したインタフェースのみ受信する
*/
memset(&sll, 0, sizeof(sll));
sll.sll_family = AF_PACKET;
sll.sll_protocol = htons(ETH_P_ALL);
sll.sll_ifindex = ifindex;
if (bind(sd, (struct sockaddr *) &sll, sizeof(sll)) < 0) {
perror("bind");
close(sd);
exit(1);
}
/*
* インタフェースをプロミスキャス・モードに設定する
*/
memset(&mreq, 0, sizeof(mreq));
mreq.mr_ifindex = ifindex;
mreq.mr_type = PACKET_MR_PROMISC;
if (setsockopt(sd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
perror("setsockopt");
exit(1);
}
printf("device %s entered promiscuous mode\n", argv[1]);
}フレームを読み出すと先頭にイーサネットヘッダーがあります。イーサネットヘッダーには送信元MACアドレスと送信先MACアドレス、プロトコルが含まれています。ここは単純にダンプするだけなので細かい解説は割愛します。
重要なのはプロトコルです。プロトコルを見ればイーサネットヘッダーが運んでいるものが分かります。パケット解析をしていてよく見かけるのは次のどれかのはずです。プロトコルは/usr/include/net/ethernet.hで定義されています。
- ETHERTYPE_IP … IPv4
- ETHERTYPE_IPV6 … IPv6
- ETHERTYPE_VLAN … VLAN(802.1q)
- ETHERTYPE_ARP … ARPリクエスト・リプライ
- ETHERTYPE_REVARP … リバースARP
本記事のサンプルプログラムでは「ETHERTYPE_IP 」「ETHERTYPE_VLAN 」「ETHERTYPE_ARP」「ETHERTYPE_REVARP」の4つを解析対象としています。
/* タグVLAN(802.1q)の場合 */
if (proto == ETHERTYPE_VLAN) {
proto = print_dot1q(p);
printf("protocol: %s(0x%x)\n", proto2str(proto), proto);
p += 4;
}
/* IPv4の場合 */
if (proto == ETHERTYPE_IP)
print_ipv4_header(p);
/* ARP/RARPの場合 */
if (proto == ETHERTYPE_ARP || proto == ETHERTYPE_REVARP)
print_arp(p);ETHERTYPE_IPについては、これまで何度も解説しているIPv4ヘッダーの解析なのでここでは割愛します。
ETHERTYPE_VLAN はVLAN(802.1q)で、いわゆるタグVLANと言われるものです。今回、802.1qを解析対象に入れて普段何気なく使っているタグVLANを自分で観察してみます。802.1qではイーサネットヘッダーの後ろに4バイトのフィールドを追加します。構造体にすると次のような内容です(今回この構造体は使っていません)。
struct tci {
unsigned int pcp:3; /* priority code point */
unsigned int dei:1; /* drop eligible indicator */
unsigned int vid:12; /* vlan id */
unsigned short protocol; /* protocol */
};これらを取り出して表示するために以下の関数を使っています。
unsigned short
print_dot1q(unsigned char *p) {
unsigned short tci;
unsigned short protocol;
printf("------- 802.1q -------\n");
tci = get_u2b((unsigned char *) p);
/* VLAN IDは下位12ビット */
printf("vlan: %u\n", tci & 0xfff);
/* PCPは上位3ビット */
printf("priority code point: %u\n", tci >> 13);
/* 13ビット目がDEI */
printf("drop eligible indicator: %u\n", (tci & 0x1000) ? 1 : 0);
/* 次の2バイト先にプロトコルが格納されている */
protocol = get_u2b(p+2);
return protocol;
}プロトコルがETHERTYPE_ARPもしくはETHERTYPE_REVARPである場合、それはARP通信です。ARPというのは、IPアドレスからMACアドレスを取得するためのプロトコルで、RARPはMACアドレスからIPアドレスを取得するためのプロトコルです。
ARPヘッダーは/usr/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). */
#if 0
/* Ethernet looks like this : This bit is variable sized
however... */
unsigned char __ar_sha[ETH_ALEN]; /* Sender hardware address. */
unsigned char __ar_sip[4]; /* Sender IP address. */
unsigned char __ar_tha[ETH_ALEN]; /* Target hardware address. */
unsigned char __ar_tip[4]; /* Target IP address. */
#endif
};先頭のar_hrdはハードウェアの種類です。この値は/usr/include/net/if_arp.hで定義されおり、値が1(ARPHRD_ETHER)であればイーサネットです。通常は「1」のはずです。
その次のar_proはアドレスのプロトコルタイプです。この値は/usr/include/net/ethernet.hで定義されており、値が0x800(ETHERTYPE_IP)であればIPv4です。ARPの場合、この値は通常「0x800」であるはずです。
その次のar_hlnはハードウェアアドレスのサイズです。イーサネットのMACアドレスは6バイトですから、この値は「6」になります。そしてar_plnはプロトコルアドレス(ar_pro)のサイズです。IPv4であれば値は「4」になります。
最後にar_opがありますが、これはARPのオペレーションコードです。この値は/usr/include/net/if_arp.hで定義されおり、値が1(ARPOP_REQUEST)であればARPリクエスト、値が2(ARPOP_REPLY)であればARPリプライ、値が3(ARPOP_RREQUEST)であればRARPリクエスト、値が4(ARPOP_RREPLY)であればRARPリプライです。
ここまでがARPヘッダーの解析となっています。ARP通信では、この後にARP/RARPリクエスト・リプライで使われるデータが続きます。
if (ntohs(arp->ar_op) == ARPOP_REQUEST || ntohs(arp->ar_op) == ARPOP_REPLY
|| ntohs(arp->ar_op) == ARPOP_RREQUEST || ntohs(arp->ar_op) == ARPOP_RREPLY) {
struct ether_addr *sha, *tha;
struct in_addr sip, tip;
printf("--\n");
sha = (struct ether_addr *) (p + sizeof(struct arphdr));
memcpy(&sip.s_addr, (char *) sha + sizeof(struct ether_addr), sizeof(sip.s_addr));
tha = (struct ether_addr *) ((char *) sha + sizeof(struct ether_addr) + sizeof(uint32_t));
memcpy(&tip.s_addr, (char *) tha + sizeof(struct ether_addr), sizeof(tip.s_addr));
printf("source hardware address: %s\n", ether_ntoa(sha));
printf("source ip address: %s\n", inet_ntoa(sip));
printf("target hardware address: %s\n", ether_ntoa(tha));
printf("target ip address: %s\n", inet_ntoa(tip));
}ARPヘッダーの後ろには次のデータが続きます。
- 送信元MACアドレス
- 送信元IPアドレス
- ターゲットMACアドレス
- ターゲットIPアドレス
上記の意味はリクエストとリプライで変わってきます。
ARPリクエストの場合
ARPリクエストはIPアドレスからMACアドレスを解決します。
- 送信元MACアドレス … ARPリクエストを行ったホストのMACアドレス
- 送信元IPアドレス … ARPリクエストを行ったホストのIPアドレス
- ターゲットMACドレス … 00:00:00:00:00:00(未解決のため)
- ターゲットIPアドレス … ARP解決したいIPアドレス(このIPアドレスに対応したMACアドレスを知りたい)
ARPリプライの場合
ARPリプライはARPリクエストへの応答で、問い合わせされたIPアドレスを持つホストが自分のMACアドレスを通知します。
- 送信元MACアドレス … ARPリクエストに応答するホストのMACアドレス(このMACアドレスを知りたかった)
- 送信元IPアドレス … ARPリクエストに応答するホストのIPアドレス
- ターゲットMACアドレス … 返信先のMACアドレス(ARPリクエスト時の送信元MACアドレス)
- ターゲットIPアドレス … 返信先のIPアドレス(ARPリクエスト時の送信元IPアドレス)
RARPリクエストの場合
RARPリクエストはMACアドレスからIPアドレスを解決します。
- 送信元MACアドレス … RARリクエストを行ったホストのMACアドレス
- 送信元IPアドレス … RARPリクエストを行ったホストのIPアドレス
- ターゲットMACアドレス … RARP解決したいMACアドレス(このMACアドレスに対応したIPアドレスを知りたい)
- ターゲットIPアドレス … 0.0.0.0(未解決のため)
RARPリプライの場合
RARPリプライはRARPリクエストへの応答で、問い合わせされたMACアドレスを持つホストが自分のIPアドレスを通知します。
- 送信元MACアドレス … RARPリクエストに応答するホストのMACアドレス
- 送信元IPアドレス … RARPリクエストに応答するホストのIPアドレス(このIPアドレスを知りたかった)
- ターゲットMACアドレス … 返信先のMACアドレス(RARPリクエスト時の送信元MACアドレス)
- ターゲットIPアドレス … 返信先のIPアドレス(RARPリクエスト時の送信元IPアドレス)
コンパイルして実行してみる
先ほど掲載したCコードを「linux-read-datalink.c」というファイル名で保存しコンパイルしました。コンパイルオプションは不要です。
cc linux-read-datalink.c
実行するにはroot権限が必要です。テスト環境でARPをクリアして意図的にARPリクエスト・リプライを発生させてみました。すると次のような表示が得られるはずです。ARPリクエストとARPリプライのときのARPヘッダーの各値に注目してください。
$ sudo ./a.out ======= Ethernet Header ======= src: 0:c:29:94:0:a4 dst: ff:ff:ff:ff:ff:ff protocol: ARP(0x806) ------- ARP/RARP ------- ar_hrd: 1(Ethernet) ar_pro: 0x800(IP) ar_hln: 6 ar_pln: 4 ar_op: 1(ARP request) -- source hardware address: 0:c:29:94:0:a4 source ip address: 192.168.218.130 target hardware address: 0:0:0:0:0:0 target ip address: 192.168.218.2 ======= Ethernet Header ======= src: 0:50:56:e4:8e:4c dst: 0:c:29:94:0:a4 protocol: ARP(0x806) ------- ARP/RARP ------- ar_hrd: 1(Ethernet) ar_pro: 0x800(IP) ar_hln: 6 ar_pln: 4 ar_op: 2(ARP reply) -- source hardware address: 0:50:56:e4:8e:4c source ip address: 192.168.218.2 target hardware address: 0:c:29:94:0:a4 target ip address: 192.168.218.130
同じ通信をtcpdumpでも取得してみました。上記のARPヘッダーの値を見て理解した後であれば、以下の表示を見ればARPヘッダーの値がすぐに頭に浮かぶはずです。
$ sudo tcpdump -i ens33 -e -t -vvv -nn arp 00:0c:29:94:00:a4 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.218.2 tell 192.168.218.130, length 28 00:50:56:e4:8e:4c > 00:0c:29:94:00:a4, ethertype ARP (0x0806), length 60: Ethernet (len 6), IPv4 (len 4), Reply 192.168.218.2 is-at 00:50:56:e4:8e:4c, length 46
802.1q(タグVLAN)を使っている環境では次のようにVLANを IDを表示させることができます。
======= Ethernet Header ======= src: 52:54:0:a:f2:98 dst: 68:54:5a:16:c6:b9 protocol: 802.1q(0x8100) ------- 802.1q ------- vlan: 100 priority code point: 0 drop eligible indicator: 0 protocol: IP(0x800) ------- IPv4 Header ------- ip_v: 4 ip_hl: 5 ip_tos: 0 ip_len: 84 ip_id: 63171 ip_off: 0 ip_ttl: 64 ip_p: 1 ip_sum: 605 ip_src: 192.168.0.43 ip_dst: 192.168.0.13
まとめ
今回はLinuxでデータリンク層からフレームを読み出す方法を解説しました。データリンク層はハードウェアに依存するので、あらゆるハードウェアに対応させようとすると大変ですが学習目的あればイーサネットだけ考慮すれば問題ないはずです。
データリンク層の解析ができるようになるとネットワークの理解が更に深まるはずです。
この記事は役に立ちましたか?
もし参考になりましたら、下記のボタンで教えてください。
コメント