Dockerの仮想NICでraw socketプログラミングをやってみる
前回に引き続き、Dockerのネットワーク周りです。
今回はDockerの仮想NICをraw socketで扱ってみようと思います。
Dockerの仮想NICはホストOS上で見えるvethとip linkの関係にあり、対応するvethにパケットを送信するとコンテナ内のethに転送されます。
そしてホストOS上で見えるvethは、通常のNICと同じように扱えます。
Docker標準ではこのvethがLinux bridgeであるdocker0というブリッジに接続されますが、前回はvethをdocker0から剥がしてOpenvSwitchに接続しました。
これによりVLANが扱えたり、OpenFlowでパケット転送を制御したりが可能となりました。
今回は、そのOpenvSwitchに当たる転送処理を自前で実装してみようというのが趣旨になります。
まずは簡単なバカハブ(リピーターハブ)を実装してみたいと思います。
まずは適当にコンテナを2つほど立てます。
1
2
3
4
5
6
7
8
9
10
|
root@Capella:~# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
root@Capella:~# docker run -itd --name host1 alpine
3870e235f4224610a8798a6b7ce2553cec98ec844984806d0951570692ca4a55
root@Capella:~# docker run -itd --name host2 alpine
7f4e92e133e75808c9ad25cc6268775a7266ff8db2ad706138078fc45a55f614
root@Capella:~# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7f4e92e133e7 alpine "/bin/sh" 2 seconds ago Up 1 seconds host2
3870e235f422 alpine "/bin/sh" 6 seconds ago Up 5 seconds host1
|
次に各コンテナのeth0に相当するvethを探します。
毎回手で探すのは面倒なので、今後のためにこんなコードを書きました。
(前回、コンテナ内のNICのiflinkと、ホストOS側vethのifindexが同値であることを利用して探す)
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
|
#include <stdio.h>
#include <net/if.h>
#include <stdlib.h>
int main(int argc, char *argv[]){
if(argc == 2){
int ifindex;
char ifname[IF_NAMESIZE];
ifindex = atoi(argv[1]);
if(ifindex == 0){
fprintf(stdout, "Invaild Args. ifindex is must integer.\n");
return(-1);
}
if(if_indextoname(ifindex, ifname) != NULL){
//printf("ifindex = %d , ifname = %s\n", ifindex, ifname);
printf("%s\n", ifname);
return(0);
}
else{
fprintf(stdout, "ifindex %d is not found.\n", ifindex);
return(1);
}
}
fprintf(stdout, "Invaild Args\n");
return(-1);
}
|
ビルドして実行してみる
1
2
3
|
root@Capella:~# gcc -o indextoname indextoname.c
root@Capella:~# ./indextoname 2
enp1s0
|
ifindexからinterfaceの名前を取ってこれます。
これをうまいこと利用して、こんな感じで。
1
2
3
4
5
6
7
8
|
root@Capella:~# docker exec host1 cat /sys/class/net/eth0/iflink
13
root@Capella:~# docker exec host2 cat /sys/class/net/eth0/iflink
15
root@Capella:~# docker exec host1 cat /sys/class/net/eth0/iflink | xargs ./indextoname
veth31c0595
root@Capella:~# docker exec host2 cat /sys/class/net/eth0/iflink | xargs ./indextoname
veth7d0cb2a
|
各コンテナ内のiflinkの値を引数として渡してあげると、ホストOS側のvethが一発で取得可能。
便利便利。
で、例によってデフォルトでvethはdocker0に接続されているので、接続を解除。
1
2
3
4
5
6
7
8
9
10
11
12
|
root@Capella:~# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.02423f57a40d no veth31c0595
veth7d0cb2a
virbr0 8000.000000000000 yes
root@Capella:~#
root@Capella:~# brctl delif docker0 veth31c0595
root@Capella:~# brctl delif docker0 veth7d0cb2a
root@Capella:~# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.02423f57a40d no
virbr0 8000.000000000000 yes
|
あと、コンテナ内のNICのアドレスも確認しておきましょう。
必要があればアドレスの変更も。
(今回はハブを作るので、2つのコンテナは同じサブネット上にいる必要があります。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
root@Capella:~# docker exec host1 ifconfig eth0
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02
inet addr:172.17.0.2 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:acff:fe11:2/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:83 errors:0 dropped:0 overruns:0 frame:0
TX packets:14 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:9330 (9.1 KiB) TX bytes:1076 (1.0 KiB)
root@Capella:~# docker exec host2 ifconfig eth0
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:03
inet addr:172.17.0.3 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:acff:fe11:3/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:57 errors:0 dropped:0 overruns:0 frame:0
TX packets:13 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:6118 (5.9 KiB) TX bytes:1006 (1006.0 B)
|
host1[eth0] : 172.17.0.2/16
host2[eth0] : 172.17.0.3/16
これで準備は完了です。
ここからは普通のraw socketプログラミングです。
raw socketはイーサフレームを直接送受信できるとても便利な機能です。
ちょうど今勉強中であまり詳しくないので、コードが汚いのは許してください。。
一応Ubuntu16.04 + Intel Celeron J1900で動かしています。
バイトオーダ等の違いで動かないとかあるかもしれませんが、そのあたりはご愛嬌。
(ハードウェアを意識したプログラミングって難しい。。)
ちなみに、このあたりを参考にしました(結構コピペ)
How to send/receive raw packets on Linux
イーサネットフレーム転送プログラム(RAWソケットプログラム) – ちーちーの小ネタ部屋
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
|
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <poll.h>
char *interface = NULL;
int pd = -1;
#define TYPE_ARP 0x0806
#define TYPE_IP4 0x0800
#define IFMAX 4
//ARP structure
struct ARP{
uint8_t hw_type[2];
uint8_t proto_type[2];
uint8_t hlen[1];
uint8_t plen[1];
uint8_t op_code[2];
uint8_t src_mac[6];
uint8_t src_ip[4];
uint8_t dst_mac[6];
uint8_t dst_ip[4];
uint8_t padding[18];
} __attribute__((__packed__));
//Ether frame structure
struct ETHER{
uint8_t dst_mac[6];
uint8_t src_mac[6];
uint8_t eth_type[2];
union {
struct ARP arp;
}payload;
} __attribute__((__packed__));
//network interface
struct NETIF{
char ifname[IFNAMSIZ];
int pd;
int ifindex;
struct ifreq ifr;
struct sockaddr myaddr;
struct sockaddr_ll sll;
};
void hexdump(unsigned char *buf, int nbytes);
int main(int argc, char **argv)
{
struct ifreq ifr;
int ifindex;
struct sockaddr myaddr;
int i, j, s;
unsigned char buf[2048];
struct sockaddr *myaddr_p;
int addrlen;
struct sockaddr_ll sll;
unsigned int type = 0;
struct ETHER ether;
struct ETHER *pether;
char tmp[2048];
int ret;
int inter_n;
char ifnames[IFMAX][IFNAMSIZ];
struct NETIF netif[IFMAX];
struct pollfd pfds[IFMAX];
//引数からbridgeするinterfaceを取ってくる
inter_n = argc - 1;
if(inter_n <= 1){
fprintf(stdout, "Args Error : You need to specify 2 interfaces at leaset.\n");
exit(-1);
}
if(inter_n > IFMAX){
fprintf(stdout, "Args Error : Too many interfaces. IFMAX : %d.\n", IFMAX);
exit(-1);
}
for(i = 0; i < inter_n; i++){
if(strlen(argv[i+1]) > IFNAMSIZ){
fprintf(stdout, "Invaild Args.\n");
exit(-1);
}
strcpy(ifnames[i], argv[i+1]);
}
//raw socketで扱うために色々やる
for(i = 0; i < inter_n; i++){
//socket作る
strcpy(netif[i].ifname, ifnames[i]);
netif[i].pd = -1;
if((netif[i].pd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1){
perror("socket()");
exit(1);
}
//interfaceの名前からifindexを取ってくる
memset(&netif[i].ifr, 0, sizeof(netif[i].ifr));
strncpy(netif[i].ifr.ifr_name, netif[i].ifname, IFNAMSIZ);
if (ioctl(netif[i].pd, SIOCGIFINDEX, &netif[i].ifr) == -1) {
perror("SIOCGIFINDEX");
exit(1);
}
netif[i].ifindex = netif[i].ifr.ifr_ifindex;
//HWADDR取得
memset(&netif[i].ifr, 0, sizeof(netif[i].ifr));
strncpy(netif[i].ifr.ifr_name, netif[i].ifname, IFNAMSIZ);
if(ioctl(netif[i].pd, SIOCGIFHWADDR, &netif[i].ifr) == -1){
perror("SIOCGHIFWADDR");
exit(1);
}
netif[i].myaddr = netif[i].ifr.ifr_hwaddr;
memset(&netif[i].sll, 0x00, sizeof(netif[i].sll));
//socketにinterfaceをbind
netif[i].sll.sll_family = AF_PACKET; //allways AF_PACKET
netif[i].sll.sll_protocol = htons(ETH_P_ALL);
netif[i].sll.sll_ifindex = netif[i].ifindex;
if (bind(netif[i].pd, (struct sockaddr *)&netif[i].sll, sizeof(netif[i].sll)) == -1) {
perror("bind():");
exit(1);
}
//ノンブロッキングモードに
ioctl(netif[i].pd, FIONBIO, 1);
//poll()を使うのでそっちで使う構造体にもデータ渡しておく
pfds[i].fd = netif[i].pd;
pfds[i].events = POLLIN|POLLERR;
if (i == 0){
printf("bridge interfaces : ");
}
printf("%s ", netif[i].ifname);
}
printf("\n");
for(;;){
switch(poll(pfds, inter_n, 10)){
case -1:
perror("polling");
break;
case 0:
break;
default:
//イベントを受けたらパケット処理
for(i = 0; i < inter_n; i++){
if(pfds[i].revents&(POLLIN|POLLERR)){
//何かしらデータを受けたら
if((s=read(netif[i].pd, buf, sizeof(buf))) <= 0){
perror("read");
}
else{
//受信したデータをdump(debug)
pether = (struct ETHER *)buf;
type = pether->eth_type[0];
type = (type << 8) + pether->eth_type[1];
printf("[receive]interface:%s\n ", netif[i].ifname);
switch (type) {
case TYPE_ARP:
printf("eth_type -> %d[ARP]\n", TYPE_ARP);
break;
case TYPE_IP4:
printf("eth_type -> %d[IPv4]\n", TYPE_IP4);
break;
default:
printf("eth_type -> [Unknown type]\n");
continue;
break;
}
//hexdump(buf, s);
//受信したinterface以外のinterfaceに送信する
for (j = 0; j < inter_n; j++){
if(i == j){
continue;
}
if(write(netif[j].pd, buf, s) <= 0){
perror("read");
}
printf("[send]interface:%s\n", netif[j].ifname);
}
printf("\n");
}
}
}
break;
}
}
return(0);
}
void hexdump(unsigned char *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");
}
}
|
ビルドはこんな感じで。
1 |
root@Capella:~# gcc -o l2hub l2hub.c
|
これがリピーターハブとして動作するコードになります。
あるポートから受信したイーサフレームを、受信ポート以外のポートへフラッディングするだけです。
引数でNICを渡すと、それらのNICの全通信を他のポートに送信します。
では試してみましょう。
まずは作成したハブ動かしてない状態。
vethがどこにも接続されていないため、当然通信はできませんし、ARPすら解決できてない。
1
2
3
4
5
6
7
8
9
|
root@Capella:~# docker exec host1 ping 172.17.0.3 -c 5
PING 172.17.0.3 (172.17.0.3): 56 data bytes
--- 172.17.0.3 ping statistics ---
5 packets transmitted, 0 packets received, 100% packet loss
root@Capella:~#
root@Capella:~# docker exec host1 arp -n
? (172.17.0.3) at <incomplete> on eth0
? (172.17.0.1) at <incomplete> on eth0
|
そして次にハブを起動してみる。
引数としてブリッジしたいインターフェース(今回はホストOS側で認識しているveth)を指定します。
1 |
root@Capella:~# ./l2hub veth31c0595 veth7d0cb2a
|
再びPingを打つと正常に通信できていることが確認できました。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
root@Capella:~# docker exec host1 ping 172.17.0.3 -c 5
PING 172.17.0.3 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.316 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.654 ms
64 bytes from 172.17.0.3: seq=2 ttl=64 time=0.637 ms
64 bytes from 172.17.0.3: seq=3 ttl=64 time=0.653 ms
64 bytes from 172.17.0.3: seq=4 ttl=64 time=0.655 ms
--- 172.17.0.3 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.316/0.583/0.655 ms
root@Capella:~# docker exec host1 arp -n
? (172.17.0.3) at 02:42:ac:11:00:03 [ether] on eth0
? (172.17.0.1) at <incomplete> on eth0
root@Capella:~#
|
今回はデバッグのために通過したパケットの種別、受信インターフェイス、送信インターフェイスを標準出力するようにしています。
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
|
root@Capella:~# ./l2hub veth31c0595 veth7d0cb2a
bridge interfaces : veth31c0595 veth7d0cb2a
[receive]interface:veth31c0595
eth_type -> 2054[ARP]
[send]interface:veth7d0cb2a
[receive]interface:veth7d0cb2a
eth_type -> 2054[ARP]
[send]interface:veth31c0595
[receive]interface:veth31c0595
eth_type -> 2048[IPv4]
[send]interface:veth7d0cb2a
[receive]interface:veth7d0cb2a
eth_type -> 2048[IPv4]
[send]interface:veth31c0595
[receive]interface:veth31c0595
eth_type -> 2048[IPv4]
[send]interface:veth7d0cb2a
[receive]interface:veth7d0cb2a
eth_type -> 2048[IPv4]
[send]interface:veth31c0595
[receive]interface:veth31c0595
eth_type -> 2048[IPv4]
[send]interface:veth7d0cb2a
[receive]interface:veth7d0cb2a
eth_type -> 2048[IPv4]
[send]interface:veth31c0595
[receive]interface:veth31c0595
eth_type -> 2048[IPv4]
[send]interface:veth7d0cb2a
[receive]interface:veth7d0cb2a
eth_type -> 2048[IPv4]
[send]interface:veth31c0595
[receive]interface:veth31c0595
eth_type -> 2048[IPv4]
[send]interface:veth7d0cb2a
[receive]interface:veth7d0cb2a
eth_type -> 2048[IPv4]
[send]interface:veth31c0595
[receive]interface:veth7d0cb2a
eth_type -> 2054[ARP]
[send]interface:veth31c0595
[receive]interface:veth31c0595
eth_type -> 2054[ARP]
[send]interface:veth7d0cb2a
^C
root@Capella:~#
|
この出力から分かるように、コンテナ内のeth0に送出されたICMPパケットが、ホストOS側のvethにやってきています。
そのパケットをraw socketで吸い上げて、他のインターフェイス(veth)に送出し、それが再びコンテナ内のeth0に転送されています。
物理NICであっても、vethであっても全く同じコードでraw socketが動作するのが非常に便利です。
簡単に物理NICとDokcerのコンテナをブリッジできたりします。
今回はハブを作りましたが、少し頭を良くしてスイッチにしたり、VLANを扱えるようにしたり、ルータを作ったり、色々楽しそうですね。
フレーム単位でパケットを処理できて、TCP/UDPのペイロード等もその気になれば見れるので、DPIみたいな高度なものを実装するのもいいと思います。
(ただしraw socketだとあまりパフォーマンスは出ないと思うので、
本気でやるならカーネルモジュールとして実装したり、netmapやDPDK等のフレームワークの使用を検討した方がいいでしょう。)
これでraw socketプログラミングやるためだけに、多ポートサーバを用意しなくていいので良いですね。
(買ったあとにこの方法思いついて微妙に無駄な出費になった。)
特にケーブルの繋ぎ変えもいらないのが気に入ってます。
ホストを増やしたくなったらコンテナ立てるだけでいいですし、いらなくなったら消すだけ。
サーバやトラフィック/パケットジェネレータもコンテナで立ててしまえば、試験が簡単にできます。
これでリモート環境のみでraw socketプログラミングができるようになったので、
家にいなくても(物理ケーブル繋ぎ変えもいらないので)遊べますね!