今回はデータリンク層で流れるフレームを解析してヘッダー値を表示したいと思います。データリンク層はハードウェアに依存するのですが、本記事ではイーサネットに限定して実装していきたいと思います。
イーサネットヘッダー自体は非常にシンプルで見るべきところは大してないため、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でデータリンク層からフレームを読み出す方法を解説しました。データリンク層はハードウェアに依存するので、あらゆるハードウェアに対応させようとすると大変ですが学習目的あればイーサネットだけ考慮すれば問題ないはずです。
データリンク層の解析ができるようになるとネットワークの理解が更に深まるはずです。
この記事は役に立ちましたか?
もし参考になりましたら、下記のボタンで教えてください。
コメント