XDP Linux:DDoSを防ぐためのドライバ層での超高速パケットフィルタ構築

Network tutorial - IT technology blog
Network tutorial - IT technology blog

背景:iptablesでは速度が足りなくなった時

私は50人のオフィスと小規模なデータセンターのネットワークを管理しています。かつて、約800MbpsのUDPフラッドによってWordPressサーバーが完全に麻痺するのを目の当たりにしたことがあります。CPUは100%に達し、iptablesがカーネルスタックを通じて一つひとつのパケットをパースしようとする一方で、サービスは徐々に死んでいきました。その障害をきっかけにXDPを調べ始め、数年前に知っておきたかったと心から思いました。

根本的な問題は、iptablesnftablesはいずれもパケットがカーネルのネットワークスタック全体(ソケットバッファ、netfilterフック、ルーティングテーブル)を通過した後に処理するということです。通常のトラフィックであれば問題になりませんが、毎秒数百万パケットのフラッドが来ると、その処理ステップ一つひとつがCPUを猛烈に消費します。

XDP(eXpress Data Path)は、NICのドライバ層でパケットがカーネルスタックに入る前に処理することでこの問題を解決します。パケットはナノ秒単位でドロップされ、同じトラフィック条件下でiptablesと比べてCPU消費が80〜90%削減されます。

XDPと他のソリューションの比較

  • iptables/nftables:カーネルスタック後に処理、設定は簡単だがトラフィック増加時に遅い
  • XDP:ドライバ層で処理、10〜100倍高速、現在のカーネルスタックとシームレスに統合
  • DPDK:XDPと同等の速度だがカーネルを完全にバイパス — 専用ドライバが必要で稼働中のシステムへの統合が難しい

実際のところ、XDPは両者の中間に位置するスイートスポットです。大規模DDoS対応に十分な速度を持ちながら、DPDKのようにネットワークスタックを再構築する必要がありません。

XDPはeBPFをベースに動作しています — CiliumやCloudflareのファイアウォール、bpftraceと同じ技術です。XDPプログラムは4つのアクションのいずれかを返します:XDP_DROP(その場でパケットを破棄)、XDP_PASS(カーネルスタックを通常通り通過させる)、XDP_TX(同じインターフェースから送り返す)、またはXDP_REDIRECT別のインターフェースに転送する)。DDoS対策においては、XDP_DROPが最も多く使われるアクションです。

環境のセットアップ

カーネル4.8以降でXDPの基本機能が利用できますが、すべての機能を活用するにはカーネル5.10以降を推奨します。まず確認しましょう:

uname -r
# 例:5.15.0-91-generic

Ubuntu/Debianへの依存パッケージのインストール

sudo apt update
sudo apt install -y clang llvm libelf-dev libbpf-dev \
    linux-headers-$(uname -r) iproute2 bpftool gcc make

RHEL/AlmaLinux/Rockyへのインストール

sudo dnf install -y clang llvm elfutils-libelf-devel \
    libbpf-devel kernel-devel bpftool iproute

libbpfの準備を確認:

pkg-config --libs libbpf
# 出力:-lbpf

このステップはよく省略されがちですが重要です:NICがXDPネイティブモードをサポートしているか確認します。ネイティブモードはジェネリックモードより大幅に高速で、同じハードウェアで通常3〜5倍の差があります:

ethtool -i eth0 | grep driver
# driver: ixgbe  → XDPネイティブモード OK
# driver: i40e   → XDPネイティブモード OK
# driver: virtio_net → ジェネリックモードのみ使用可能(仮想マシン)

詳細設定:DDoSをブロックするXDPプログラムの作成

管理しやすいよう、プロジェクトを専用のディレクトリにまとめます:

mkdir ~/xdp-filter && cd ~/xdp-filter

CでXDPプログラムを作成する

xdp_block.cファイルを作成します。これはrestricted C(Cのサブセット)で書かれたeBPFプログラムで、カーネル内のeBPF VM上で直接動作し、実行前にverifierによる安全チェックが行われます:

// xdp_block.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <bpf/bpf_helpers.h>

// BPF Map: ブロックするIPリストを保存(key = IP uint32, value = 1)
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, __u32);
    __type(value, __u8);
    __uint(max_entries, 65536);
    __uint(pinning, LIBBPF_PIN_BY_NAME);
} blocked_ips SEC(".maps");

// BPF Map: ドロップしたパケット数をカウント(ロック回避のためper-CPU)
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, 1);
    __uint(pinning, LIBBPF_PIN_BY_NAME);
} drop_counter SEC(".maps");

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

    // Ethernetヘッダーをパース — eBPF verifierの境界チェックは必須
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    // IPv4のみ処理
    if (eth->h_proto != __constant_htons(ETH_P_IP))
        return XDP_PASS;

    // IPヘッダーをパース
    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end)
        return XDP_PASS;

    // ソースIPをマップで検索
    __u8 *blocked = bpf_map_lookup_elem(&blocked_ips, &ip->saddr);
    if (blocked && *blocked == 1) {
        __u32 key = 0;
        __u64 *cnt = bpf_map_lookup_elem(&drop_counter, &key);
        if (cnt) (*cnt)++;
        return XDP_DROP;
    }

    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

XDPプログラムのコンパイル

clang -O2 -g -Wall -target bpf \
    -I/usr/include/$(uname -m)-linux-gnu \
    -c xdp_block.c -o xdp_block.o

# 出力を確認
file xdp_block.o
# xdp_block.o: ELF 64-bit LSB relocatable, eBPF, version 1 (SYSV)

XDPをカーネルにロードしてインターフェースにアタッチする

# BPFファイルシステムをマウント(未マウントの場合)
sudo mount -t bpf bpf /sys/fs/bpf 2>/dev/null || true
sudo mkdir -p /sys/fs/bpf/xdp-filter

# マシン上の実際のインターフェース名を確認
ip link show

# XDPネイティブモードをロード(NICドライバがサポート — 最速)
sudo ip link set eth0 xdpdrv obj xdp_block.o sec xdp \
    pinpath /sys/fs/bpf/xdp-filter

# NICがネイティブをサポートしない場合はジェネリックモードを使用
sudo ip link set eth0 xdpgeneric obj xdp_block.o sec xdp \
    pinpath /sys/fs/bpf/xdp-filter

ブロックIPリストの管理 — 再起動不要

BPF Mapの優れた点は、ブロックリストへのIPの追加・削除がリアルタイムで行えること。プログラムのリロードや再起動は一切不要です。XDPはカーネル内で実行し続けたまま — その下のデータを更新するだけです:

#!/usr/bin/env python3
# manage_block.py
import socket
import subprocess
import sys

MAP_PATH = "/sys/fs/bpf/xdp-filter/blocked_ips"

def ip_to_hex(ip):
    packed = socket.inet_aton(ip)
    return ' '.join(f'{b:02x}' for b in packed)

def block_ip(ip):
    key = ip_to_hex(ip)
    subprocess.run(
        f"bpftool map update pinned {MAP_PATH} key hex {key} value hex 01",
        shell=True, check=True
    )
    print(f"Blocked: {ip}")

def unblock_ip(ip):
    key = ip_to_hex(ip)
    subprocess.run(
        f"bpftool map delete pinned {MAP_PATH} key hex {key}",
        shell=True, check=True
    )
    print(f"Unblocked: {ip}")

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("Usage: sudo python3 manage_block.py [block|unblock] <IP>")
        sys.exit(1)
    action, ip = sys.argv[1], sys.argv[2]
    if action == "block":
        block_ip(ip)
    elif action == "unblock":
        unblock_ip(ip)
# 攻撃中のIPをブロック
sudo python3 manage_block.py block 203.0.113.50

# ブロック解除
sudo python3 manage_block.py unblock 203.0.113.50

# ブロック中のIPリスト全体を表示
sudo bpftool map dump pinned /sys/fs/bpf/xdp-filter/blocked_ips

テストとモニタリング

XDPが正常にアタッチされたことを確認

ip link show eth0
# 成功した場合:xdp/id:42 またはxdpgeneric/id:42 が表示される

# 実行中のプログラムの詳細を確認
sudo bpftool prog list | grep xdp
sudo bpftool prog show id 42

ドロップパケット数の監視

# BPF MapのDropカウンターを確認
sudo bpftool map dump pinned /sys/fs/bpf/xdp-filter/drop_counter

# ドライバのXDP統計を確認(NICがサポートする場合)
ethtool -S eth0 | grep -i xdp

# 1秒ごとにリアルタイム監視
watch -n 1 'sudo bpftool map dump pinned /sys/fs/bpf/xdp-filter/drop_counter'

hping3を使った実際のテスト

# テストマシンから — SYNフラッドを送信
hping3 -S --flood -V -p 80 <server-ip>

# サーバー上 — CPUとパケット統計を監視
watch -n 1 'cat /proc/net/dev | grep eth0'
# rxパケットは急増するがCPUは低いまま → XDPがドライバ層でDropしている

不要になったXDPの取り外し

# Detach XDP
sudo ip link set eth0 xdpdrv off
# または
sudo ip link set eth0 xdpgeneric off

# ピン留めされたファイルシステムからBPF Mapを削除
sudo rm -rf /sys/fs/bpf/xdp-filter

本番環境での実際の結果

データセンターのゲートウェイサーバーにXDPをデプロイした後、数字がすべてを物語っています:同じ約800Mbpsのフラッドに対して、iptablesはCPUを85〜90%まで押し上げ、レイテンシが急増しました。Intel i40eカードでXDPネイティブモードに切り替えたところ、CPUは15〜20%まで低下しました。内部サービスへの影響はゼロ — XDPがカーネルスタックに触れる前にパケットをドロップしていたからです。

blocked_ipsの65536エントリは実際には十分すぎるほどです — 攻撃を受けている最中でも、同時に3,000以上のIPをブロックする必要があったことはありません。次のステップとして自動モニタリングの統合をお勧めします:bpftraceで異常なパターンを検出し、上記のPythonスクリプトを呼び出してIPをブロックリストに追加 — 手動操作は一切不要です。すべては数ミリ秒以内に完了します — ほとんどの自動攻撃に先手を打つのに十分な速さです。

Share: