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プログラミングができるようになったので、
家にいなくても(物理ケーブル繋ぎ変えもいらないので)遊べますね!
