背景:iptablesでは速度が足りなくなった時
私は50人のオフィスと小規模なデータセンターのネットワークを管理しています。かつて、約800MbpsのUDPフラッドによってWordPressサーバーが完全に麻痺するのを目の当たりにしたことがあります。CPUは100%に達し、iptablesがカーネルスタックを通じて一つひとつのパケットをパースしようとする一方で、サービスは徐々に死んでいきました。その障害をきっかけにXDPを調べ始め、数年前に知っておきたかったと心から思いました。
根本的な問題は、iptablesやnftablesはいずれもパケットがカーネルのネットワークスタック全体(ソケットバッファ、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をブロックリストに追加 — 手動操作は一切不要です。すべては数ミリ秒以内に完了します — ほとんどの自動攻撃に先手を打つのに十分な速さです。

