Raw socketでVLANタグ付きフレームを受信する
この記事はビール飲みながら書きました。アルコール入ってるので間違ってたり変なこと言ってたらごめんなさい。。おびーるおいちい!!!!
Qiitaデビュー兼ねてこの記事の微修正入れたりしつつ投稿しました。
技術的な記事はもうそっちに寄せようかなー(このブログの存在意義がなくなる)
Linux Raw Socketを使って遊んでいたのですが、VLANタグ付きフレームを受信してもraw socketではタグが消されてしまうことがわかった。
結論から言うと結構弄めんどくさかったのでそのメモ。
まずはこんな感じのコードを書きました。
単純にraw socketでフレームを受信するだけ。
(hexdump()のコードはここからコピペさせて頂きました)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
|
#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};
//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);
}
|
ビルド
1 |
g++ -std=c++11 -o recv recv.cpp
|
このコードでVLANタグ付きフレームを受信した場合(MACアドレスは念のため書き換えてます)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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で同じフレームを見た場合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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 db2d b3f2 ..........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タグは早い段階で取り除かれ、ユーザーランドに上がってくる頃にはタグなしフレームになってるそうです。
ところでソフトウェアスイッチに、Open vSwitchとLagopus switchというOSSがあります。
どちらもraw socket実装もあるそうなので、Lagopusの実装を見てみましょう。
「lagopus/src/dataplane/mgr/sock_io.c」がsocketとかの処理っぽいです。
まずは、socket作成部分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
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の設定なので省略*/
}
|
ただのソケットの作成なので、僕が書いたコードとやってることはそんなに変わらない。
(Lagopusで扱うために色々書かれてますがその部分を無視すると)
その中で、
「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の受信部分を読んでみます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
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タグ付きフレームを受信するコード書いてみました。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
|
#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);
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タグ情報は補助データとして受信フレームとは別で与えられるので、
補助データからタグに入れるべき内容を読んできて、フレームの適切な位置に挿入してるだけです。
結構めんどくさい。。。
ビルドしてコードを動かして最初と同じようにタグ付きフレームを受信してみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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#
|
できました。
お疲れ様でした。
(今回は単一インターフェースでの受信だけだったので面白みもないループで回しましたが、
複数のinterfaceを扱うときはpoll()とかepoll()とか使ってノンブロッキングにしたほうが良いです。)
ググっても日本語情報どころか、他言語でも有力な情報がなく、結局OSSのコードリーディングをすることに。。
(LagopusがRaw Socket対応してるという話は知ってたので、今回参考にできました。)
OSSの偉大さを実感できました。。
余談ですが、職業がネットワーク屋さんということで普段はルータとかスイッチばっかり触ってます。
昔からプログラミングは趣味でやっているのですが、最近ネットワーク知識も加わったので、Raw Socketを使ったL2SWみたいなのを作ってます。
salacia-forwarder
(pull requestとかはあまり受け付ける気はないです、ごめんなさい)
最初はCで実装してたのですが、だんだんしんどくなってC++に。(C++初めてなので勉強しながら書いてます)
パケット送受信はLinuxシステムコールに依存するので仕方ないですが、
極力標準ライブラリに頼らず(iostreamとかcstdintくらいは使ってる)、スマートポインタも使わないつもりで作っていこうと思ってます。
(趣味です。メモリ管理とかも含めて学びたいので、生産性とかよりお勉強優先です。)
今回、L2SWを実装するにあたり、Raw socketのVLANの扱いでハマったので、この記事を書きました。
たったこれだけの内容で、ぼくのお休み2日が潰れました。
参考になれば幸いです。。