RawSocketでVLANタグ付きフレームを受信する

ここにも同じ記事を書いてます。
Qiita初参戦も兼ねて、上記の記事の修正版がこちらです。

Linux Raw Socketを使って遊んでいたのですが、
VLANタグ付きフレームを受信してもraw socketではタグが消されてしまうことがわかった。
結論から言うと結構めんどくさかったのでそのメモ。

まず普通に書く

まずはこんな感じのコードを書きました。
単純にraw socketでフレームを受信するだけ。
(hexdump()のコードはここからコピペさせて頂きました)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cstdint>
#include <net/if.h>
#include <sys/ioctl.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#include <unistd.h>

void hexdump(uint8_t *p, int count)
{
    int i, j;

    for(i = 0; i < count; i += 16) {
        printf("%04x : ", i);
        for (j = 0; j < 16 && i + j < count; j++)
            printf("%2.2x ", p[i + j]);
        for (; j < 16; j++) {
            printf("   ");
        }
        printf(": ");
        for (j = 0; j < 16 && i + j < count; j++) {
            char c = toascii(p[i + j]);
            printf("%c", isalnum(c) ? c : '.');
        }
        printf("\n");
    }
}

int main(void){
    int pd = -1;
    char ifname[] = "enp4s0";
    int ifindex;
    struct ifreq ifr;
    struct sockaddr myaddr;
    struct sockaddr_ll sll;

    uint8_t recv_buf[2048];

    //socket作る
    if((pd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1){
        perror("socket()");
        exit(1);
    }

    //interfaceの名前からifindexを取ってくる
    ifr = {0};
    strncpy(ifr.ifr_name, ifname, IFNAMSIZ);
    if (ioctl(pd, SIOCGIFINDEX, &ifr) == -1) {
        perror("SIOCGIFINDEX");
        exit(1);
    }
    ifindex = ifr.ifr_ifindex;

    //HWADDR取得
    ifr = {0};
    strncpy(ifr.ifr_name, ifname, IFNAMSIZ);
    if(ioctl(pd, SIOCGIFHWADDR, &ifr) == -1){
        perror("SIOCGHIFWADDR");
        exit(1);
    }
    myaddr = ifr.ifr_hwaddr;

    sll = {0};

    //socketにinterfaceをbind
    sll.sll_family = AF_PACKET;
    sll.sll_protocol = htons(ETH_P_ALL);
    sll.sll_ifindex = ifindex;

    if (bind(pd, (struct sockaddr *)&(sll), sizeof(sll)) == -1) {
        perror("bind():");
        exit(1);
    }


    int len;
    for(;;){
        if((len = read(pd, recv_buf, sizeof(recv_buf))) > 0) {
            hexdump(recv_buf, len);
            printf("\n");
        }
    }

    return(0);
}

このコードでVLANタグ付きフレームを受信した場合(MACアドレスは念のため書き換えてます)

root@Capella:~/vlan# ./recv
0000 : ff ff ff ff ff ff 34 95 db aa aa aa 08 06 00 01 : ......4.........
0010 : 08 00 06 04 00 01 34 95 db aa aa aa ac 12 00 c8 : ......4........H
0020 : 00 00 00 00 00 00 ac 12 00 9c 00 00 00 00 00 00 : ................
0030 : 00 00 00 00 00 00 00 00 00 00 00 00             : ............

0000 : ff ff ff ff ff ff 34 95 db aa aa aa 08 06 00 01 : ......4.........
0010 : 08 00 06 04 00 01 34 95 db aa aa aa ac 12 00 c8 : ......4........H
0020 : 00 00 00 00 00 00 ac 12 00 9c 00 00 00 00 00 00 : ................
0030 : 00 00 00 00 00 00 00 00 00 00 00 00             : ............

0000 : ff ff ff ff ff ff 34 95 db aa aa aa 08 06 00 01 : ......4.........
0010 : 08 00 06 04 00 01 34 95 db aa aa aa ac 12 00 c8 : ......4........H
0020 : 00 00 00 00 00 00 ac 12 00 9c 00 00 00 00 00 00 : ................
0030 : 00 00 00 00 00 00 00 00 00 00 00 00             : ............

^C
root@Capella:~/vlan#

一方tcpdumpで同じフレームを見た場合

root@Capella:~# tcpdump -i enp4s0 -nn -XX
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp4s0, link-type EN10MB (Ethernet), capture size 262144 bytes
23:08:25.528388 ARP, Request who-has 172.18.0.156 tell 172.18.0.200, length 46
        0x0000:  ffff ffff ffff 3495 dbaa aaaa 8100 0c1c  ......4.........
        0x0010:  0806 0001 0800 0604 0001 3495 dbaa aaaa  ..........4.....
        0x0020:  ac12 00c8 0000 0000 0000 ac12 009c 0000  ................
        0x0030:  0000 0000 0000 0000 0000 0000 0000 0000  ................
23:08:26.544534 ARP, Request who-has 172.18.0.156 tell 172.18.0.200, length 46
        0x0000:  ffff ffff ffff 3495 dbaa aaaa 8100 0c1c  ......4.........
        0x0010:  0806 0001 0800 0604 0001 3495 dbaa aaaa  ..........4.....
        0x0020:  ac12 00c8 0000 0000 0000 ac12 009c 0000  ................
        0x0030:  0000 0000 0000 0000 0000 0000 0000 0000  ................
23:08:27.584449 ARP, Request who-has 172.18.0.156 tell 172.18.0.200, length 46
        0x0000:  ffff ffff ffff 3495 dbaa aaaa 8100 0c1c  ......4.........
        0x0010:  0806 0001 0800 0604 0001 3495 dbaa aaaa  ..........4.....
        0x0020:  ac12 00c8 0000 0000 0000 ac12 009c 0000  ................
        0x0030:  0000 0000 0000 0000 0000 0000 0000 0000  ................
^C
3 packets captured
3 packets received by filter
0 packets dropped by kernel

raw socketでは802.1Qタグが綺麗に取り除かれていますね。。
どうしてもタグが必要だったので色々調べたのですが、日本語情報もなく、英語情報を見ても「libpcap使え」と出てきます。
「じゃあそのlibpcapはどう実装されてるのさ!!」というツッコミは置いといて、特定のライブラリに依存してしまうのが嫌でした。
raw socketで単純にread()しても、VLANタグは取り除かれてタグなしフレームになるそうです。

既存のOSSでの実装

ところでソフトウェアスイッチに、Open vSwitchLagopus switchというOSSがあります。
どちらもraw socket実装があるそうですが、今回はLagopusの実装を見てみます。
lagopus/src/dataplane/mgr/sock_io.cがsocketとかの処理っぽいです。

lagopus_result_t
rawsock_configure_interface(struct interface *ifp) {
  struct nlreq {
    struct nlmsghdr nlh;
    struct ifinfomsg ifinfo;
    char buf[64];
  } req;
  struct rtattr *rta;
  uint32_t portid;
  struct ifreq ifreq;
  struct packet_mreq mreq;
  struct sockaddr_ll sll;
  unsigned int mtu;
  int fd, on;

  fd = socket(PF_PACKET, SOCK_RAW | SOCK_NONBLOCK, htons(ETH_P_ALL));
  if (fd == -1) {
    lagopus_msg_error("%s: %s\n",
                      ifp->info.eth_rawsock.device, strerror(errno));
    return LAGOPUS_RESULT_POSIX_API_ERROR;
  }
  on = 1;
  if (setsockopt(fd, SOL_PACKET, PACKET_AUXDATA, &on, sizeof(on)) != 0) {
    close(fd);
    lagopus_msg_warning("%s: %s\n",
                        ifp->info.eth_rawsock.device, strerror(errno));
    return LAGOPUS_RESULT_POSIX_API_ERROR;
  }
  snprintf(ifreq.ifr_name, sizeof(ifreq.ifr_name),
           "%s", ifp->info.eth_rawsock.device);
  if (ioctl(fd, SIOCGIFINDEX, &ifreq) != 0) {
    close(fd);
    lagopus_msg_warning("%s: %s\n",
                        ifp->info.eth_rawsock.device, strerror(errno));
    return LAGOPUS_RESULT_POSIX_API_ERROR;
  }
  ifp->fd = fd;
  portid = get_port_number(ifp);
  if (portid == UINT32_MAX) {
    close(fd);
    lagopus_msg_error("%s: too many port opened\n",
                      ifp->info.eth_rawsock.device);
    return LAGOPUS_RESULT_TOO_MANY_OBJECTS;
  }
  ifp->info.eth_rawsock.port_number = portid;
  ifp->ifindex = ifreq.ifr_ifindex;
  if (ioctl(fd, SIOCGIFHWADDR, &ifreq) != 0) {
    close(fd);
    lagopus_msg_warning("%s: %s\n",
                        ifp->info.eth_rawsock.device, strerror(errno));
  } else {
    memcpy(ifp->hw_addr, ifreq.ifr_hwaddr.sa_data, ETHER_ADDR_LEN);
  }
  lagopus_msg_info("Configuring %s, ifindex %d\n",
                   ifp->info.eth_rawsock.device, ifp->ifindex);
  sll.sll_family = AF_PACKET;
  sll.sll_protocol = htons(ETH_P_ALL);
  sll.sll_ifindex = ifp->ifindex;
  bind(fd, (struct sockaddr *)&sll, sizeof(sll));

  /*以下promiscuous modeの設定とか、MTUの設定なので省略*/
}

ただのソケットの作成なので、僕が書いたコードとやってることはそんなに変わらない。
その中で、
「if (setsockopt(fd, SOL_PACKET, PACKET_AUXDATA, &on, sizeof(on)) != 0)」
というのが何かしてそうです。
調べてみると、ソケットオプションを設定する機能らしい。
Man page of SOCKET
Man page of PACKET
Man page of CMSG
Man page of RECV

SOL_PACKETで受信したパケットすべてに対して適用、PACKET_AUXDATAをセットすることで補助データを受信できるようです。
recvmsg()でパケットデータとこの補助データを受信して、補助データについてはcmsgを使って読むそうです。

とはいえ、実際のコードがないと使い方もよくわからないので、続いてLagopusの受信部分を読んでみます。

static ssize_t
read_packet(int fd, uint8_t *buf, size_t buflen) {
  struct sockaddr from;
  struct iovec iov;
  struct msghdr msg;
  union {
    struct cmsghdr cmsg;
    uint8_t buf[CMSG_SPACE(sizeof(struct tpacket_auxdata))];
  } cmsgbuf;
  struct cmsghdr *cmsg;
  struct tpacket_auxdata *auxdata;
  uint16_t *p;
  ssize_t pktlen;
  uint16_t ether_type;

  iov.iov_base = buf;
  iov.iov_len = buflen;
  memset(buf, 0, buflen); /* XXX */

  msg.msg_name = &from;
  msg.msg_namelen = sizeof(from);
  msg.msg_iov = &iov;
  msg.msg_iovlen = 1;
  msg.msg_control = &cmsgbuf;
  msg.msg_controllen = sizeof(cmsgbuf);
  msg.msg_flags = 0;

  pktlen = recvmsg(fd, &msg, MSG_TRUNC);
  if (pktlen == -1) {
    if (errno == EAGAIN) {
      pktlen = 0;
    }
    return pktlen;
  }
  for (cmsg = CMSG_FIRSTHDR(&msg);
       cmsg != NULL;
       cmsg = CMSG_NXTHDR(&msg, cmsg)) {
    if (cmsg->cmsg_type != PACKET_AUXDATA) {
      continue;
    }
    auxdata = (struct tpacket_auxdata *)CMSG_DATA(cmsg);
#if defined (TP_STATUS_VLAN_VALID)
    if ((auxdata->tp_status & TP_STATUS_VLAN_VALID) == 0) {
      continue;
    }
#else
    if (auxdata->tp_vlan_tci == 0) {
      continue;
    }
#endif /* TP_STATUS_VLAN_VALID */
    p = (uint16_t *)(buf + ETHER_ADDR_LEN * 2);
    switch (OS_NTOHS(p[0])) {
      case ETHERTYPE_PBB:
      case ETHERTYPE_VLAN:
        ether_type = 0x88a8;
        break;
      default:
        ether_type = ETHERTYPE_VLAN;
        break;
    }
    memmove(&p[2], p, pktlen - ETHER_ADDR_LEN * 2);
    p[0] = OS_HTONS(ether_type);
    p[1] = OS_HTONS(auxdata->tp_vlan_tci);
    pktlen += 4;
  }
  return pktlen;
}

recvmsgでデータを受信したあと、何かしらやってますね。
これを参考に802.1Qタグ付きフレームを受信するコード書いてみます。

Lagopus参考にして書いてみる(ほぼコピペ)

個人的な理解のためにもコメント入れつつほぼコピペでまず書いてみる。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cstdint>
#include <net/if.h>
#include <sys/ioctl.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#include <unistd.h>

void hexdump(uint8_t *p, int count)
{
    int i, j;

    for(i = 0; i < count; i += 16) {
        printf("%04x : ", i);
        for (j = 0; j < 16 && i + j < count; j++)
            printf("%2.2x ", p[i + j]);
        for (; j < 16; j++) {
            printf("   ");
        }
        printf(": ");
        for (j = 0; j < 16 && i + j < count; j++) {
            char c = toascii(p[i + j]);
            printf("%c", isalnum(c) ? c : '.');
        }
        printf("\n");
    }
}

int main(void){
    int pd = -1;
    char ifname[] = "enp4s0";
    int ifindex;
    struct ifreq ifr;
    struct sockaddr myaddr;
    struct sockaddr_ll sll;

    uint8_t recv_buf[2048];

    //VLAN読み取りに必要なものたち
    struct iovec iov;
    struct msghdr msg;
    union {
        struct cmsghdr cmsg;
        uint8_t buf[CMSG_SPACE(sizeof(struct tpacket_auxdata))];
    } cmsgbuf;
    struct cmsghdr *cmsg;
    struct tpacket_auxdata *auxdata;

    //socket作る
    if((pd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1){
        perror("socket()");
        exit(1);
    }

    //option
    int on = 1;
    if (setsockopt(pd, SOL_PACKET, PACKET_AUXDATA, &on, sizeof(on)) == -1){
        perror("setsockopt():");
        exit(1);
    }

    //interfaceの名前からifindexを取ってくる
    ifr = {0};
    strncpy(ifr.ifr_name, ifname, IFNAMSIZ);
    if (ioctl(pd, SIOCGIFINDEX, &ifr) == -1) {
        perror("SIOCGIFINDEX");
        exit(1);
    }
    ifindex = ifr.ifr_ifindex;

    //HWADDR取得
    ifr = {0};
    strncpy(ifr.ifr_name, ifname, IFNAMSIZ);
    if(ioctl(pd, SIOCGIFHWADDR, &ifr) == -1){
        perror("SIOCGHIFWADDR");
        exit(1);
    }
    myaddr = ifr.ifr_hwaddr;

    sll = {0};

    //bind
    sll.sll_family = AF_PACKET;
    sll.sll_protocol = htons(ETH_P_ALL);
    sll.sll_ifindex = ifindex;

    if (bind(pd, (struct sockaddr *)&(sll), sizeof(sll)) == -1) {
        perror("bind():");
    }


    int len;
    for(;;){
        iov.iov_base = recv_buf;
        iov.iov_len = sizeof(recv_buf);
        memset(recv_buf, 0, sizeof(recv_buf));

        msg.msg_name = NULL;
        msg.msg_namelen = 0;
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;
        msg.msg_control = &cmsgbuf;
        msg.msg_controllen = sizeof(cmsgbuf);
        msg.msg_flags = 0;

        len = recvmsg(pd, &msg, MSG_TRUNC);
        if (len == -1) {
            if (errno == EAGAIN) {
                perror("recvmsg:");
                continue;
            }
        }
        if(len > 0) {
            for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
                //AUXDATAじゃなければスキップ
                if (cmsg->cmsg_type != PACKET_AUXDATA) {
                    continue;
                }

                //AUX_DATAを読み取る
                auxdata = (struct tpacket_auxdata *)CMSG_DATA(cmsg);

                //VLAN ID持ってないならスキップ
                //ステータスとTCIで判定
                if ((auxdata->tp_status & TP_STATUS_VLAN_VALID) == 0) {
                    continue;
                }
                if (auxdata->tp_vlan_tci == 0) {
                    continue;
                }

                //VLAN処理(frameにtag挿入)
                //raw socketで受信したフレームのEth Typeを調べる
                //raw socketで受けたフレームは802.1Qタグが外れているので、中のtypeが取得できる

                //buf(受信フレームの頭)から12byte目(6 * 2)から見る
                // 6byte = macアドレスのサイズ
                uint16_t *p = (uint16_t *)(recv_buf + 6 * 2);
                uint16_t ether_type;

                switch (ntohs(p[0])) {
                    case 0x8100:
                        //中のtyoeが802.1Qなら外側のtypeはQinQ(802.1d)の規定に従う
                        //未検証(いつか動作確認する)
                        ether_type = 0x88a8;
                        break;
                    default:
                        //外側のtypeに802.1Qを挿入
                        ether_type = 0x8100;
                        break;
                }

                //802.1Qタグを挿入するために4byte後ろにずらす
                //6 * 2 -> Ethernet headerのdst/src mac address分
                memmove((uint8_t *)&p[2], (uint8_t *)p, len - (6 * 2));

                //4byteずらして空けたスペースに802.1Qタグを突っ込む
                //Ethtype(前2byte)
                p[0] = htons(ether_type);
                //TCI(後ろ2bytem、後ろ12byteがVLAN ID)
                p[1] = htons(auxdata->tp_vlan_tci);

                //4byte(802.1Q tagのサイズ)伸ばす
                len += 4;
            }
            hexdump(recv_buf, len);
            printf("\n");
        }
    }
    return(0);

recvmsg()で受信したとき、802.1Qタグ情報は補助データとして受信フレームとは別で与えられるので、
補助データからタグに入れるべき内容を読んできて、フレームの適切な位置に挿入してるだけですね。

結構めんどくさい。。。

コードを動かして最初と同じようにタグ付きフレームを受信してみます。

root@Capella:~/vlan# g++ -o vlan_recv -std=c++11 vlan_recv.cpp
root@Capella:~/vlan# ./vlan_recv
0000 : ff ff ff ff ff ff 34 95 db aa aa aa 81 00 0c 1c : ......4.........
0010 : 08 06 00 01 08 00 06 04 00 01 34 95 db aa aa aa : ..........4.....
0020 : ac 12 00 c8 00 00 00 00 00 00 ac 12 00 9c 00 00 : ...H............
0030 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : ................

0000 : ff ff ff ff ff ff 34 95 db aa aa aa 81 00 0c 1c : ......4.........
0010 : 08 06 00 01 08 00 06 04 00 01 34 95 db aa aa aa : ..........4......
0020 : ac 12 00 c8 00 00 00 00 00 00 ac 12 00 9c 00 00 : ...H............
0030 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : ................

0000 : ff ff ff ff ff ff 34 95 db aa aa aa 81 00 0c 1c : ......4.........
0010 : 08 06 00 01 08 00 06 04 00 01 34 95 db aa aa aa : ..........4.....
0020 : ac 12 00 c8 00 00 00 00 00 00 ac 12 00 9c 00 00 : ...H............
0030 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : ................

^C
root@Capella:~/vlan#

できました。
お疲れ様でした。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.