大規模eBPFとXDP:毎秒1000万パケットのDDoS軽減とドライバーレベルフィルタリング

目次(8 項目)
ネットワーク層へのDDoS攻撃、特にSYNフラッドやUDP増幅は、依然として深刻な脅威です。従来のカーネル空間でのフィルタリングは効果的ですが、sk_buffのアロケーション、完全なネットワークスタックのトラバース、コンテキストスイッチにより、レイテンシとCPUオーバーヘッドが発生します。単一コアで毎秒1,000万パケット(Mpps)を超えるラインレートでは、これらのオーバーヘッドは許容できないものとなります。このガイドでは、eBPFとXDP(eXpress Data Path)を用いた高性能DDoS緩和策の実装について詳しく説明します。これにより、ドライバーレベルのパケット処理を活用してラインレートでのフィルタリングを実現します。
高性能フィルタリングのためのXDPの基礎
XDPは、ネットワークスタックの可能な限り早い段階、つまりネットワークインターフェースカード(NIC)ドライバーの受信(RX)リング内で直接動作します。このカーネル前、sk_buffアロケーション前の実行コンテキストは、パフォーマンスにとって極めて重要です。XDPプログラムによって処理されたパケットは、ドロップまたはリダイレクトされた場合、カーネルの完全なネットワークスタックには決して到達しません。これにより、sk_buffの作成とそれに続く処理に関連するメモリ割り当て、キャッシュミス、CPUサイクルが排除されます。
XDPプログラムは生のxdp_md構造体を受け取ります。これにはパケットデータの開始と終了へのポインタが含まれています。プログラムの戻り値は、実行されるアクションを決定します。
XDP_DROP: パケットは即座にドロップされます。これはDDoS緩和の主要なアクションです。XDP_PASS: パケットはカーネルのネットワークスタックに進むことが許可されます。XDP_TX: パケットは同じNICポートからリダイレクトされます。ロードバランシングやリフレクションに役立ちます。XDP_REDIRECT: パケットは別のNICポートまたはCPU間通信用のBPF CPUマップにリダイレクトされます。
XDPプログラムの実行コンテキスト
XDPプログラムは、制限されたeBPF環境で実行されます。任意のシステムコールを実行したり、任意のカーネルメモリにアクセスしたり、無限ループを実行したりすることはできません。これにより、安定性が確保され、悪意のあるプログラムやバグのあるプログラムがカーネルを危険にさらすのを防ぎます。主な制約は次のとおりです。
- バウンドループ: ループには既知の有限の上限が必要です。
- メモリアクセス: パケットデータとBPFマップメモリのみにアクセスできます。
- ヘルパー関数: マップルックアップ、チェックサム計算、パケット操作などのタスクには、限られたBPFヘルパー関数セットが利用可能です。
DDoS緩和のためのアーキテクチャ
当社の緩和アーキテクチャは以下を含みます。
- XDPプログラム (C): NICに直接デプロイされます。このプログラムは、IPアドレス、プロトコル、ポート番号に基づいて初期パケット解析とフィルタリングを実行します。動的なブラックリストとカウンタにはBPFマップを活用します。
- BPFマップ:
BPF_MAP_TYPE_LPM_TRIE: 効率的な最長プレフィックスマッチ(LPM)IPブラックリスト用。これにより、サブネット全体または個々のIPをブロックできます。BPF_MAP_TYPE_ARRAY: CPUごとのパケットカウンタ用。ドロップされたトラフィックをリアルタイムで可視化します。
- ユーザー空間エージェント (Go/Python/Rust):
- XDPプログラムをロードし、ターゲットネットワークインターフェースにアタッチします。
- 外部の脅威インテリジェンスまたはローカルの異常検出に基づいて、ブラックリストに登録されたIPを追加/削除することで
LPM_TRIEマップを管理します。 ARRAYマップからカウンタを読み取り、集計し、Prometheus経由で公開します。
動的IPブラックリストのためのLPMトライ
LPM_TRIEマップは、その効率的なプレフィックスマッチング機能により、IPブラックリストに最適です。これにより、すべての256個のIPを列挙するのではなく、単一のエントリで192.168.1.0/24をブロックできます。
// bpf_ddos_mitigator.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
// Define a structure for LPM trie keys
// This structure is critical for the LPM trie map.
// The 'prefixlen' field determines how many bits of the 'data' field are used for matching.
// For IPv4, prefixlen can be 32 for a host match, or less for a subnet.
// For IPv6, prefixlen can be 128 for a host match.
struct bpf_lpm_trie_key {
__u32 prefixlen; // Must be the first field
__u32 ip_addr; // IPv4 address in network byte order
};
// Map for blacklisted IPs (LPM Trie)
// Key: bpf_lpm_trie_key (prefixlen + IP)
// Value: __u8 (e.g., 1 to indicate blacklisted)
struct {
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
__uint(max_entries, 10240); // Max 10k blacklist entries
__uint(key_size, sizeof(struct bpf_lpm_trie_key));
__uint(value_size, sizeof(__u8));
__uint(map_flags, BPF_F_NO_PREALLOC); // Don't preallocate all memory
} blacklist_ips SEC(".maps");
// Map for packet counters (per-CPU array)
// Key: __u32 (index for counter type, e.g., 0 for dropped, 1 for passed)
// Value: __u64 (counter)
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 2); // 0: dropped_packets, 1: passed_packets
__uint(key_size, sizeof(__u32));
__uint(value_size, sizeof(__u64));
} xdp_stats_map SEC(".maps");
// Helper macro to increment a counter in the xdp_stats_map
static __always_inline void increment_counter(__u32 index) {
__u64 *counter = bpf_map_lookup_elem(&xdp_stats_map, &index);
if (counter) {
__sync_fetch_and_add(counter, 1);
}
}
SEC("xdp")
int xdp_ddos_mitigator(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if (eth + 1 > data_end) {
return XDP_PASS; // Malformed Ethernet header
}
// Only process IPv4 for this example
if (bpf_ntohs(eth->h_proto) != ETH_P_IP) {
return XDP_PASS;
}
struct iphdr *iph = data + sizeof(*eth);
if (iph + 1 > data_end) {
return XDP_PASS; // Malformed IP header
}
// Check if source IP is blacklisted
struct bpf_lpm_trie_key key = {
.prefixlen = 32, // Exact match for source IP
.ip_addr = iph->saddr // Source IP in network byte order
};
__u8 *blacklisted = bpf_map_lookup_elem(&blacklist_ips, &key);
if (blacklisted) {
// IP is blacklisted, drop the packet
increment_counter(0); // Increment dropped_packets counter
return XDP_DROP;
}
// Basic SYN flood mitigation (TCP SYN packets without ACK)
if (iph->protocol == IPPROTO_TCP) {
struct tcphdr *tcph = (void *)iph + (iph->ihl * 4);
if (tcph + 1 > data_end) {
return XDP_PASS; // Malformed TCP header
}
// Check for SYN flag set and ACK flag not set
if (tcph->syn && !tcph->ack) {
// Potentially a SYN flood packet.
// For production, this would be more sophisticated,
// e.g., rate limiting, SYN cookie implementation, or state tracking.
// For now, we'll just drop it if it's a simple SYN.
// This is a very aggressive rule and might drop legitimate SYNs.
// A real-world solution would involve more context.
// For demonstration, we'll drop if source IP is not whitelisted.
// (No whitelist implemented here, so it's a direct drop for SYN)
// increment_counter(0); // Increment dropped_packets counter
// return XDP_DROP;
}
}
// Basic UDP amplification mitigation (e.g., DNS, NTP, SSDP)
// This is a simplified example. Real mitigation involves
// checking payload size, specific protocol headers, and known amplification vectors.
if (iph->protocol == IPPROTO_UDP) {
struct udphdr *udph = (void *)iph + (iph->ihl * 4);
if (udph + 1 > data_end) {
return XDP_PASS; // Malformed UDP header
}
// Example: Drop UDP packets to common amplification ports if source is not trusted
// This is a placeholder. A real system would check for specific
// query types, response sizes, or use a dynamic trust list.
__u16 dest_port = bpf_ntohs(udph->dest);
if (dest_port == 53 || dest_port == 123 || dest_port == 1900) { // DNS, NTP, SSDP
// For demonstration, we'll drop these if source is not whitelisted.
// A real system would have more sophisticated logic.
// increment_counter(0); // Increment dropped_packets counter
// return XDP_DROP;
}
}
// If not dropped by any rule, pass to the kernel
increment_counter(1); // Increment passed_packets counter
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
このeBPFプログラムをコンパイルするには、BPFバックエンドをサポートするclangとllvm、およびlibbpfヘッダーが必要です。
# Install necessary packages on Debian/Ubuntu
sudo apt update
sudo apt install clang llvm libelf-dev libbpf-dev build-essential
# Compile the BPF program
clang -O2 -target bpf -g -c bpf_ddos_mitigator.c -o bpf_ddos_mitigator.o
ユーザー空間コントロールプレーン (Goの例)
ユーザー空間エージェントは、eBPFプログラムのロード、マップの管理、メトリクスの公開を担当します。
// main.go
package main
import (
"fmt"
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net/http"
)
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -cflags "-O2 -g -Wall" bpf bpf_ddos_mitigator.c -- -I./headers
// bpf_lpm_trie_key matches the C struct
type bpfLpmTrieKey struct {
Prefixlen uint32
IPAddr uint32 // Network byte order
}
var (
droppedPackets = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "xdp_dropped_packets_total",
Help: "Total number of packets dropped by XDP DDoS mitigator.",
},
)
passedPackets = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "xdp_passed_packets_total",
Help: "Total number of packets passed by XDP DDoS mitigator.",
},
)
)
func init() {
prometheus.MustRegister(droppedPackets)
prometheus.MustRegister(passedPackets)
}
func main() {
if len(os.Args) < 2 {
log.Fatalf("Usage: %s <interface>", os.Args[0])
}
ifaceName := os.Args[1]
// Allow the current process to lock memory for eBPF maps.
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatalf("Failed to remove memlock rlimit: %v", err)
}
// Load pre-compiled programs and maps into the kernel.
objs := bpfObjects{}
if err := loadBpfObjects(&objs, nil); err != nil {
log.Fatalf("Loading eBPF objects: %v", err)
}
defer objs.Close()
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
log.Fatalf("Getting interface %s: %v", ifaceName, err)
}
// Attach the XDP program to the network interface.
// Use XDP_FLAGS_DRV_MODE for best performance if driver supports it.
// Fallback to XDP_FLAGS_SKB_MODE if driver mode fails.
l, err := link.AttachXDP(link.XDPOptions{
Program: objs.XdpDdosMitigator,
Interface: iface,
Flags: uint32(link.XDPDriverMode),
})
if err != nil {
log.Printf("Failed to attach XDP in driver mode, trying SKB mode: %v", err)
l, err = link.AttachXDP(link.XDPOptions{
Program: objs.XdpDdosMitigator,
Interface: iface,
Flags: uint32(link.XDPSkbMode),
})
if err != nil {
log.Fatalf("Failed to attach XDP in SKB mode: %v", err)
}
}
defer l.Close()
log.Printf("Successfully attached XDP program to interface %q (ID: %d)", ifaceName, objs.XdpDdosMitigator.ID())
// Example: Add a blacklisted IP (e.g., 192.0.2.1)
// This would typically come from a dynamic threat feed or detection system.
blacklistIP := net.ParseIP("192.0.2.1").To4()
if blacklistIP == nil {
log.Fatalf("Invalid IP address")
}
key := bpfLpmTrieKey{
Prefixlen: 32, // Exact match
IPAddr: uint32(blacklistIP[0])<<24 | uint32(blacklistIP[1])<<16 | uint32(blacklistIP[2])<<8 | uint32(blacklistIP[3]),
}
value := uint8(1) // Value doesn't matter, just its presence
if err := objs.BlacklistIps.Put(key, value); err != nil {
log.Fatalf("Failed to add IP to blacklist: %v", err)
}
log.Printf("Added %s to blacklist.", blacklistIP.String())
// Start Prometheus metrics server
go func() {
http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(":9090", nil))
}()
log.Println("Prometheus metrics exposed on :9090/metrics")
// Periodically read and update Prometheus counters
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
var prevDropped, prevPassed uint64
for {
select {
case <-ticker.C:
var currentDropped, currentPassed uint64
var zero uint32 = 0
var one uint32 = 1
if err := objs.XdpStatsMap.Lookup(zero, ¤tDropped); err != nil {
log.Printf("Failed to lookup dropped_packets counter: %v", err)
}
if err := objs.XdpStatsMap.Lookup(one, ¤tPassed); err != nil {
log.Printf("Failed to lookup passed_packets counter: %v", err)
}
// Update Prometheus counters with delta
droppedPackets.Add(float64(currentDropped - prevDropped))
passedPackets.Add(float64(currentPassed - prevPassed))
prevDropped = currentDropped
prevPassed = currentPassed
log.Printf("Dropped: %d, Passed: %d", currentDropped, currentPassed)
case <-stop:
log.Println("Detaching XDP program...")
return
}
}
}
eBPFプログラムのGoバインディングを生成するには、以下を実行します。
go generate ./...
次に、Goプログラムをコンパイルして実行します。
go build -o xdp-mitigator main.go
sudo ./xdp-mitigator eth0 # Replace eth0 with your network interface
パフォーマンスベンチマークとトレードオフ
| 機能 | XDP (ドライバーモード) | カーネル空間 (Netfilter/iptables) |
|---|---|---|
| 実行ポイント | NIC RXリング、sk_buff前 | sk_buff割り当て後、カーネルスタック内 |
| パフォーマンス | 10-100 Mpps/コア (最新NICのラインレート) | 1-5 Mpps/コア (ルール複雑度に大きく依存) |
| CPUオーバーヘッド | 最小限、sk_buffなし、コンテキストスイッチなし | 著しい、sk_buff割り当て、スタックトラバース |
| メモリ使用量 | 最小限、パケットごとのsk_buffなし | 高い、パケットごとのsk_buff |
| 柔軟性 | 制限されたBPFヘルパー、Cライクな言語 | 完全なカーネルAPI、複雑なルールセット |
| ステートフル性 | ステートレスまたはBPFマップを介した限定的なステートフル性 | ステートフル (例: コネクション追跡) |
| デプロイ | libbpf、カーネル4.8+ (XDP)、5.x+ (フル)が必要 | 標準カーネル機能、広く利用可能 |
| ユースケース | 大容量DDoS、ロードバランシング、高速パス | 一般的なファイアウォール、NAT、複雑なポリシー適用 |
トレードオフ:
- 複雑さ: XDPプログラムはCで記述され、ネットワークプロトコルとeBPFの内部構造に対する深い理解が必要です。デバッグは困難な場合があります。
- ドライバーサポート: 最適なXDPパフォーマンス(
XDP_DRV_MODE)はNICドライバーのサポートに依存します。サポートがない場合、XDP_SKB_MODEはいくつかの利点を提供しますが、それでもsk_buffの割り当てが伴います。 - 限定されたコンテキスト: XDPプログラムはシステムのビューが制限されています。プロセス情報や複雑なカーネル状態に簡単にアクセスすることはできません。
本番環境での落とし穴とトラブルシューティング
-
XDP_DRV_MODEvs.XDP_SKB_MODE:- 落とし穴:
XDP_DRV_MODEでXDPプログラムをロードしようとしても、NICドライバーがそれをサポートしていない場合、失敗するか、XDP_FLAGS_UPDATE_IF_NOEXISTが使用されている場合はサイレントにXDP_SKB_MODEにフォールバックします。パフォーマンスは著しく低下します。 - 修正: 常に
ethtool -i <interface>でdriverとfirmware-versionを確認してください。ip link show dev <interface>でxdpフラグを参照してください。XDP_DRV_MODEを優先し、必要に応じてXDP_SKB_MODEに gracefully fall back しますが、パフォーマンスへの影響に注意してください。本番環境では、NIC(例:Intel ixgbe、i40e、mlx5)が適切なドライバーサポートを備えていることを確認してください。
- 落とし穴:
-
eBPFベリファイアエラー:
- 落とし穴: 複雑なeBPFプログラム、特にループや広範なメモリアクセスを含むものは、カーネルのeBPFベリファイアによって拒否されることがあります。一般的なエラーには、「プログラムが大きすぎる」、「ループがバウンドしていない」、「無効なメモリアクセス」などがあります。
- 修正: eBPFロジックを簡素化してください。複雑なタスクをより小さく、検証可能な関数に分割します。すべてのループに明示的な境界があることを確認してください。デバッグには
bpf_printkを使用します(sudo cat /sys/kernel/debug/tracing/trace_pipe経由で表示可能)。bpftool prog loadコマンドとlog_level=2は、詳細なベリファイア出力を提供します。
-
マップのピン留めと永続性:
- 落とし穴: ユーザー空間エージェントがクラッシュまたは再起動した場合、eBPFプログラムとそのマップがアンロードされ、サービス中断につながる可能性があります。
- 修正: BPFマップのピン留め(
bpf_obj_pin)を使用して、マップをBPFファイルシステム(/sys/fs/bpf)に永続化します。XDPプログラムは、ピン留めされたマップを参照してロードおよびアタッチできます。これにより、プログラムとその状態のライフサイクルを独立して管理できます。
-
XDP_REDIRECTによるパケットの順序変更/損失:- 落とし穴:
XDP_REDIRECTを使用してパケットを別のインターフェースまたはCPUに送信する場合、受信側がトラフィックを処理できることを確認してください。不適切なリダイレクトは、特に適切な同期なしに異なるキューやCPU間で、パケット損失や順序の狂った配信につながる可能性があります。 - 修正: リダイレクトロジックを慎重に設計してください。CPU間通信には
BPF_MAP_TYPE_CPUMAPを使用します。他のインターフェースへのリダイレクトには、ターゲットインターフェースが正しく設定され、十分な容量があることを確認してください。
- 落とし穴:
-
リソース制限 (
memlock):- 落とし穴: 大規模なeBPFプログラムやマップをロードすると、
memlockリソース制限に達し、bpf_load_programまたはbpf_create_mapが「Operation not permitted」または「Cannot allocate memory」で失敗する可能性があります。 - 修正: ユーザー空間プロセスの
memlock制限を増やします。systemdサービスの場合はLimitMEMLOCK=infinityを使用します。手動実行の場合は、プログラムを実行する前にulimit -l unlimitedを使用します。Goのrlimit.RemoveMemlock()ヘルパーがこれに対処します。
- 落とし穴: 大規模なeBPFプログラムやマップをロードすると、
よくある質問
-
XDPに必要なカーネルバージョンは何ですか? XDPはLinuxカーネル4.8で導入されました。安定した機能豊富なXDPには、カーネル4.18以降が推奨されます。
BPF_MAP_TYPE_LPM_TRIEは4.11から利用可能です。 -
XDPプログラムはヘッダー以外のパケットペイロードを検査できますか? はい、XDPプログラムは、
dataとdata_endポインタの範囲内であれば、パケットペイロード全体を検査できます。ただし、eBPFで複雑なアプリケーション層プロトコルを直接解析することは、ベリファイアの制限(例:バウンドループ、スタックサイズ)により困難な場合があります。ディープパケットインスペクションの場合、XDPは高速パスフィルターとして機能し、興味深いパケットをユーザー空間プロセスに渡してさらに分析することがよくあります。 -
XDPはDPDKと比較してどうですか? DPDK(Data Plane Development Kit)は、NICを直接ポーリングすることでカーネルを完全にバイパスするユーザー空間フレームワークです。究極の制御とパフォーマンスを提供しますが、専用のハードウェアが必要であり、多くの場合、アプリケーションレベルで大幅な変更が伴います。一方、XDPはカーネルに統合されており、既存のドライバーとカーネルのセキュリティモデルを活用します。XDPは一般的にデプロイが容易で、既存のLinuxシステムに統合しやすく、完全なカーネルバイパスのオーバーヘッドなしで、多くのユースケースでDPDKに近いパフォーマンスを提供します。
-
XDPでステートフルフィルタリングを実装することは可能ですか? 従来のTCPコネクション追跡のような直接的なステートフルフィルタリングは、XDPのステートレスな実行モデルとベリファイアの制約により困難です。ただし、BPFマップを使用して限定的なステートフル性を実現できます。たとえば、
BPF_MAP_TYPE_LRU_HASHマップは、コネクションタプル(送信元IP、宛先IP、送信元ポート、宛先ポート)とその状態(例:SYN_SENT、ESTABLISHED)を短期間保存できます。これには、ユーザー空間から、またはイベント通知用のBPFリングバッファを介して、マップエントリの慎重な管理(例:タイムアウト、ガベージコレクション)が必要です。 -
本番トラフィックに影響を与えずにXDPプログラムをテストするにはどうすればよいですか? XDPプログラムを安全にテストすることは非常に重要です。
- 仮想マシン/コンテナ: 仮想環境(例:KVM、
vethペアを持つDocker)を使用してネットワークトラフィックをシミュレートし、物理ハードウェアに影響を与えずにXDPプログラムをテストします。 XDP_SKB_MODE: 速度は遅いですが、XDP_SKB_MODEは一般的に異なるドライバー間でより堅牢であり、より安全な初期テスト環境となります。ip link set dev <interface> xdp obj <program.o> section xdp verbose: このコマンドはXDPプログラムのロードを可能にします。詳細なエラーメッセージを取得するにはverboseを使用します。bpftool:bpftoolユーティリティは、ロードされたプログラム、マップ、およびその状態を検査するのに非常に役立ちます。bpftool prog showとbpftool map showは不可欠です。- トラフィックジェネレータ:
pktgen、hping3、またはscapyなどのツールを使用して、特定のトラフィックパターン(例:SYNフラッド)を生成し、緩和ロジックをテストします。
- 仮想マシン/コンテナ: 仮想環境(例:KVM、
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

VPNとプロキシ:違い、フィンガープリント、漏洩、プライバシーテスト
LinuxでVPNとプロキシを数ヶ月テストし、IPv6漏洩を発見し、拡張機能でブラウザのフィンガープリントを壊し、プライバシーに本当に役立つことを見つけました。
Read more
LinuxでFirefoxを強化する方法(初心者向けガイド)
午後をかけてFirefoxをロックダウンし、トラッキングへの露出を劇的に減らしました。実際に効果があったこと、なかったこと、そして途中で犯した間違いを紹介します。
Read more
2026年にゼロトラストアーキテクチャを実装する
2026年にゼロトラストアーキテクチャ(ZTA)を実装するための実用的なガイド:IDファーストセキュリティ、SPIFFE/SPIRE、eBPFマイクロセグメンテーション、mTLSについて解説します。
Read more