【第5回】Linuxでデータリンク層の通信を解析するツールをC言語で作成する

今回はデータリンク層で流れるフレームを解析してヘッダー値を表示したいと思います。データリンク層はハードウェアに依存するのですが、本記事ではイーサネットに限定して実装していきたいと思います。

イーサネットヘッダー自体は非常にシンプルで見るべきところは大してないため、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でデータリンク層からフレームを読み出す方法を解説しました。データリンク層はハードウェアに依存するので、あらゆるハードウェアに対応させようとすると大変ですが学習目的あればイーサネットだけ考慮すれば問題ないはずです。

データリンク層の解析ができるようになるとネットワークの理解が更に深まるはずです。

この記事は役に立ちましたか?

もし参考になりましたら、下記のボタンで教えてください。

関連記事

コメント

この記事へのコメントはありません。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)