手っ取り早くやってみる 超XDP入門 Part3 (ICMP Echo Reply編 その1)
サーバ ネットワーク プログラミング
Lastmod: 2025-03-06
Published: 2025-03-05

前回の記事では、XDPでeBPF Mapを使い、ユーザーランドとのデータのやり取りを行いました。

前々回の記事では、XDPのプログラムを使って全てのパケットをDROPするプログラムを書きました。

今回は、ICMP Echo Requestに対してICMP Echo Replyを返すプログラムを書いてみます。

ICMP Echo Replyを返すには

ICMP Echo Requestに対してICMP Echo Replyを返すには、以下の手順が必要です。

  1. ICMP Echo Requestのパケットを受信する
  2. ICMP Echo Replyにフラグを変える
  3. 送信元と送信先のIPアドレスを入れ替える
  4. Ethernetヘッダの送信元と送信先のMACアドレスを入れ替える
  5. パケットを送信する

ICMP Echo RequestだけをDROPする

まずは、ICMP Echo Requestを処理できるようにしたいので、ICMP Echo RequestのみをDROPするプログラムを書いてみます。

  • xdp_echo_request_drop.c
    #include <linux/bpf.h>
    #include <bpf/bpf_helpers.h>
    #include <linux/if_ether.h>
    #include <linux/ip.h>
    #include <linux/icmp.h>
    #include <arpa/inet.h>
    
    SEC("xdp")
    int xdp_drop_icmp(struct xdp_md *ctx) {
        void *data_end = (void *)(unsigned long)ctx->data_end;
        void *data = (void *)(unsigned long)ctx->data;
    
        // Ethernetヘッダのポインタを取得
        struct ethhdr *eth = data;
        if ((void *)eth + sizeof(*eth) > data_end)
            return XDP_PASS;
    
        // EthernetフレームがIPパケットを含んでいるか確認
        if (eth->h_proto != htons(ETH_P_IP))
            return XDP_PASS;
    
        // IPヘッダのポインタを取得
        struct iphdr *ip = data + sizeof(*eth);
        if ((void *)ip + sizeof(*ip) > data_end)
            return XDP_PASS;
    
        // プロトコルがICMPであるか確認
        if (ip->protocol != IPPROTO_ICMP)
            return XDP_PASS;
    
        // ICMPヘッダのポインタを取得
        struct icmphdr *icmp = (void *)ip + sizeof(*ip);
        if ((void *)icmp + sizeof(*icmp) > data_end)
            return XDP_PASS;
    
        // ICMP Echo Request以外はパケットを通す
        if (icmp->type != ICMP_ECHO)
            return XDP_PASS;
    
        // ICMP Echo Requestのパケットをドロップ
        return XDP_DROP;
    }
    
    char _license[] SEC("license") = "MIT";
    

手っ取り早くやってみる 超XDP入門で作成したプログラムとほぼ同じですね。

  // ICMP Echo Request以外はパケットを通す
  if (icmp->type != ICMP_ECHO)
      return XDP_PASS;

この部分で、ICMP Echo Request以外のパケットは通すようにしています。

ICMP EchoをReplyにして返す

次に、ICMP Echo Requestに対してICMP Echo Replyを返すためのプログラムを書いてみます。

  1. ICMP Echo Replyにフラグを変える
// ICMP Echo Replyにフラグを変える
icmp->type = ICMP_ECHOREPLY;

typeをICMP_ECHO (8)からICMP_ECHOREPLY (0)に変更します。

  1. 送信元と送信先のIPアドレスを入れ替える
// 送信元と送信先のIPアドレスを入れ替える
__u32 tmp = ip->saddr;
ip->saddr = ip->daddr;
ip->daddr = tmp;
  1. Ethernetヘッダの送信元と送信先のMACアドレスを入れ替える
// Ethernetヘッダの送信元と宛先を入れ替える
unsigned char tmp_mac[ETH_ALEN];
__builtin_memcpy(tmp_mac, eth->h_dest, ETH_ALEN);
__builtin_memcpy(eth->h_dest, eth->h_source, ETH_ALEN);
__builtin_memcpy(eth->h_source, tmp_mac, ETH_ALEN);
  1. XDP_TXでパケットを送信する
// パケットを送信する
return XDP_TX;

これでひとまずはICMP Echo Replyを返すプログラムが完成です。

SEC("xdp")
int xdp_echo_reply(struct xdp_md *ctx)
{
    void *data_end = (void *)(unsigned long)ctx->data_end;
    void *data = (void *)(unsigned long)ctx->data;

    // Ethernetヘッダのポインタを取得
    struct ethhdr *eth = data;
    if ((void *)eth + sizeof(*eth) > data_end)
        return XDP_PASS;

    // EthernetフレームがIPパケットを含んでいるか確認
    if (eth->h_proto != htons(ETH_P_IP))
        return XDP_PASS;

    // IPヘッダのポインタを取得
    struct iphdr *ip = data + sizeof(*eth);
    if ((void *)ip + sizeof(*ip) > data_end)
        return XDP_PASS;

    // プロトコルがICMPであるか確認
    if (ip->protocol != IPPROTO_ICMP)
        return XDP_PASS;

    // ICMPヘッダのポインタを取得
    struct icmphdr *icmp = (void *)ip + sizeof(*ip);
    if ((void *)icmp + sizeof(*icmp) > data_end)
        return XDP_PASS;

    // ICMP Echo Request以外はパケットを通す
    if (icmp->type != ICMP_ECHO)
        return XDP_PASS;

    // ICMP Echo Replyにフラグを変える
    icmp->type = ICMP_ECHOREPLY;

    // IPヘッダの送信元と宛先を入れ替える
    __u32 tmp = ip->saddr;
    ip->saddr = ip->daddr;
    ip->daddr = tmp;

    // Ethernetヘッダの送信元と宛先を入れ替える
    unsigned char tmp_mac[ETH_ALEN];
    __builtin_memcpy(tmp_mac, eth->h_dest, ETH_ALEN);
    __builtin_memcpy(eth->h_dest, eth->h_source, ETH_ALEN);
    __builtin_memcpy(eth->h_source, tmp_mac, ETH_ALEN);

    // ICMP Echo Requestのパケットをドロップ
    // return XDP_DROP;
    return XDP_TX;
}

NICにプログラムをロードする

プログラムをコンパイルして、NICにロードします。

$ clang -O2 -target bpf -c xdp_echo_reply.c -o xdp_echo_reply.o
$ sudo ip link set dev eth0 xdp obj xdp_echo_reply.o

動作確認

  • クライアントからpingを送信

    $ ping 192.168.XXX.XXX
    
  • サーバでtcpdumpを実行して、ICMP Requestを受信していないことを確認します。 XDPが直接パケットを処理しているため、tcpdumpでは確認できずLinux kernelでEcho Replyを返していないことがわかります。

    $ sudo tcpdump -i eth0 icmp
    
  • クライアントからpingを送信すると、ICMP Echo Replyが返ってくることを確認します。

    $ sudo tcpdump -i eth0 icmp -vvv
    XX:XX:XX.160507 IP (tos 0x0, ttl 64, id 53521, offset 0, flags [DF], proto ICMP (1), length 84)
        192.168.XXX.1 > 192.168.XXX.2: ICMP echo request, id 58371, seq 684, length 64
    XX:XX:XX.161394 IP (tos 0x0, ttl 64, id 53521, offset 0, flags [DF], proto ICMP (1), length 84)
        192.168.XXX.2 > 192.168.XXX.1: ICMP echo reply, id 58371, seq 684, length 64 (wrong icmp cksum 767b (->7e7b)!)
    

ICMP Echo Replyが返ってきていることが確認できました。
しかしながら、wrong icmp cksumと表示されているので、ICMPヘッダのチェックサムが正しく計算されていないことがわかります。

ICMPのチェックサムを計算して、正しく返すにはどうすればいいのか、怠惰にならず記事を書く気が出たら、書いていきたいと思います。

まとめ

  • XDP を用いてICMP Echo Requestに対してICMP Echo Replyを返すXDPプログラムを実装しました。
  • 手順としては、ICMP Echo Request を検出し、返信パケットを生成して送信元・宛先の MAC アドレスと IP アドレスを入れ替え、XDP_TX でカーネルを通さずに直接返信する方法を紹介しました。
  • 最終的に wrong icmp cksum というエラーが発生し、ICMP チェックサムの再計算が必要であることが分かりました。

次のステップ (2025/03/06 追記)

手っ取り早くやってみる 超XDP入門 Part3 (ICMP Echo Reply編 その2)