読者です 読者をやめる 読者になる 読者になる

Packets are too Fast

slankdevの報告

BPFとLinuxでのL2インターフェースを扱うネットワークプログラミングでの違いについて

この記事を書こうと思ったきっかけ

osx上でc言語で書いたパケットキャプチャでパケットをキャプチャしていた時に並行して wiresharkと見比べていたのですが、なんだかwiresharkと比べてパケットを幾つかドロップしていたため、 少し調べた(他人に頼った)ところ、bpfでの確保したディスクリプタlinuxのそれと幾つか動作の 違いがあるとのことなので、簡単にまとめることにしました。 今回この記事を書く上で情報を提供してくれた方々にはとても感謝です。

どんな記事か

l2レベルでのネットワークプログラミングについて興味がある方を対象に bsdlinuxでの動作の違いをまとめました。 間違っていたりご意見がある方はtwitter(@slankdev)などにご反応いただけると幸いです。 ファイルディスクリプタを開いた後に、readシステムコールでパケットを受信する時の挙動に違いがあるため それについてまとめています。

Linuxでのパケットの受信

LinuxはsocketシステムコールでPF_PACKETっていうアドレスファイミリを指定してファイルディスクリプタを 開くとそれでL2レベルでパケットの送受信が可能になります。 その後はreadシステムコールでパケットを受信、writeシステムコールでパケットを送信することができます。

今回はLinuxに関しての説明は省略します。もしいってくだされば喜んで書きます。

BSDでのパケットの受信

BSDではPF_PACKETのアドレスファイミリはなく、socketシステムコールは使いません。 BPFというインターフェースからopenシステムコールを使ってディスクリプタを確保します。 開くところと、writeでパケットを送信するところまではいいのですが、readの動作にLinuxBSDで 決定的な動作の違いがあります。

先ほど説明したとうり、Linuxはreadを一回呼ぶとカーネルのバッファから受信したパケットを一つ受け取るのに 対して、BSDはreadを一回呼ぶと、その時カーネルバッファに溜まっている全てのパケットをreadします。 その代わりにパケット一つ一つに対してBPFヘッダというものをつけてreadします。

簡単に動作の違いを示します。

対応策

以下の図のようにしてしまえばいいわけなので簡単です。

今回はC++で簡単にBPFのディスクリプタをラップするクラスを作ってサンプルとして示します。 クラスのソースコードは以下のようになります。

#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <vector>

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <net/if.h>
#include <net/if_dl.h>
#include <net/bpf.h>
#include <fcntl.h>
#include <unistd.h>


class stream {
    private:
        int fd;
        std::vector<uint8_t> buffer;
        uint8_t* buffer_point;
        ssize_t buffer_size_readed;

        void ioctl(unsigned long l, void* p) {
            int res = ::ioctl(fd, l, p);
            if (res < 0) {
                close();
                printf("ioctl \n");
                exit(-1);
            }
        }
        size_t write(const void* buffer, size_t bufferlen) {
            ssize_t res = ::write(fd, buffer, bufferlen);
            if (res < 0) {
                perror("write");
                close();
                exit(-1);
            }
            return res;
        }
        size_t read(void* buffer, size_t bufferlen) {
            buffer_size_readed = ::read(fd, buffer, bufferlen);
            if (buffer_size_readed < 0) {
                perror("read");
                close();
                exit(-1);
            }
            return buffer_size_readed;
        }

    public:
        struct bpf_header {
            uint64_t timestamp;
            uint32_t caplen;
            uint32_t datalen;
            uint16_t headerlen;
        };
        void recv(void* user_buffer, size_t user_bufferlen) {
            if (buffer_point == NULL) {
                buffer_size_readed = read(buffer.data(), buffer.size());
                buffer_point = buffer.data();
            } 
            struct bpf_header* bh = (bpf_header*)buffer_point;
            size_t copylen = bh->caplen;
            if (user_bufferlen < bh->caplen)
                copylen = user_bufferlen;
            memcpy(user_buffer, buffer_point+(bh->headerlen), copylen);
            buffer_point += bh->headerlen;
            buffer_point += bh->caplen;
            if (buffer_point - buffer.data() >= buffer_size_readed)
                buffer_point = NULL;
        }
        void send(const void* buffer, size_t bufferlen) {
            write(buffer, bufferlen);
        }
        void open(const char* device_name, size_t buffer_size=0) {
            for (int i=0; i<4; i++) {
                std::string str = "/dev/bpf";
                str += std::to_string(i);
                fd = ::open(str.c_str(), O_RDWR);
                if (fd >= 0) break;
            }
            if (fd < 0) {
                printf("cant open \n");
                exit(-1);      
            }
            if (buffer_size == 0) {
                ioctl(BIOCGBLEN, &buffer_size);
                printf("set default buffer size: %zd \n", buffer_size);
            }
            
            buffer.resize(buffer_size);
            ioctl(BIOCSBLEN, &buffer_size);

            /* bind to device */
            struct ifreq ifr;
            memset(&ifr, 0, sizeof(ifr));
            strncpy(ifr.ifr_name, device_name, IFNAMSIZ);
            ioctl(BIOCSETIF, &ifr);

            /* other config */
            unsigned int one  = 1;
            ioctl(BIOCPROMISC, NULL);     // set promisc
            ioctl(BIOCIMMEDIATE, &one);   //if recv packet then call read fast
            ioctl(BIOCSSEESENT, &one);    // set recv sendPacket
            ioctl(BIOCFLUSH, NULL);       // flush recv buffer
            ioctl(BIOCSHDRCMPLT, &one);   // no complite src macaddr
            buffer_point = NULL; // init buffer_point
        }
        void close() {
            if (fd >= 0)
                ::close(fd);
        }
};

まとめ

他にもLinuxBSDで実装の違いがあるみたいで、新たに勉強の種になったので、 とっても楽しかったです。

参考情報