BPF/eBPF/XDP (BPF and XDP)

Linux BPF/eBPF/XDP를 실무 관점에서 심층 다룹니다. verifier 제약과 BTF·CO-RE 기반 이식성, map 설계 패턴, redirect/tail call, XDP_DROP/PASS/TX/REDIRECT 동작 경로, bpftool·tracepoint 기반 관측, 고 PPS 환경에서의 병목(Bottleneck) 분석과 디버깅(Debugging) 절차까지 단계적으로 정리합니다.

전제 조건: 네트워크 스택(Network Stack)네트워크 디바이스 드라이버 문서를 먼저 읽으세요. 고성능 패킷(Packet) 경로는 큐 구조, 메모리 배치, 드롭 위치가 성능을 좌우하므로 드라이버 경계를 먼저 이해하는 것이 중요합니다.
일상 비유: 이 주제는 고속 톨게이트 차선 분리와 비슷합니다. 일반 차선(커널 스택)과 하이패스 차선(XDP/DPDK)을 구분해 보면 왜 지연(Latency)과 처리량(Throughput)이 달라지는지 명확해집니다.

핵심 요약

  • 패킷 수명주기 — ingress, 처리, egress 경로를 연결합니다.
  • 큐/버퍼(Buffer) 모델 — sk_buff와 큐 지점의 역할을 분리합니다.
  • 정책/데이터 분리 — 제어 평면과 데이터 평면을 구분합니다.
  • 성능 지표 — PPS, 지연, 드롭 원인을 함께 분석합니다.
  • 오프로딩(Offloading) 경계 — NIC/XDP/DPDK 경계를 명확히 유지합니다.

단계별 이해

  1. 경로 고정
    문제가 발생한 ingress/egress 지점을 먼저 특정합니다.
  2. 큐 관찰
    백로그와 드롭 위치를 계측합니다.
  3. 정책 반영 확인
    라우팅(Routing)/필터 변경이 데이터 경로에 반영됐는지 봅니다.
  4. 부하 검증
    실제 트래픽 패턴에서 재현성을 확인합니다.
관련 표준: IEEE 802.3 (Ethernet), RFC 791 (IPv4), RFC 8200 (IPv6) — XDP는 NIC 드라이버 레벨에서 이 표준 프레임/패킷을 직접 처리합니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

BPF/eBPF 개요

eBPF(extended Berkeley Packet Filter)는 커널 내에서 안전하게 실행되는 프로그래밍 프레임워크입니다. 커널을 재컴파일하지 않고 네트워킹, 보안, 추적, 성능 분석 등의 기능을 확장할 수 있습니다.

eBPF 실행 흐름 BPF 프로그램 (C → clang → .o) BPF Verifier (안전성 검증) JIT Compiler (네이티브 코드) 커널 내 실행 (hook point) eBPF Hook Points: XDP TC (Traffic) kprobe/kretprobe tracepoint cgroup LSM BPF Maps (커널↔유저 공유 데이터): Hash, Array, LRU, Ring Buffer, Per-CPU, Stack, Queue ...
eBPF: 사용자 작성 프로그램이 검증 후 커널 내에서 안전하게 실행

개발 환경 설정

BPF/XDP 프로그램을 빌드하고 로드하려면 clang(LLVM 기반 BPF 백엔드), 커널 헤더, libbpf 개발 패키지가 필요합니다. 주요 배포판별 설치 방법은 다음과 같습니다.

배포판별 패키지 설치

# Fedora / RHEL 계열
sudo dnf install clang llvm gcc \
    libbpf-devel bpftool \
    kernel-headers kernel-devel \
    elfutils-libelf-devel zlib-devel

# Debian / Ubuntu 계열
sudo apt install clang llvm gcc \
    libbpf-dev bpftool \
    linux-headers-$(uname -r) \
    libelf-dev zlib1g-dev

# openSUSE 계열
sudo zypper install clang llvm gcc \
    libbpf-devel bpftool \
    kernel-devel \
    libelf-devel zlib-devel

기본 컴파일 흐름

BPF 프로그램은 clang으로 BPF 바이트코드(.o)를 생성한 뒤 커널에 로드합니다.

# 1단계: C → BPF 오브젝트 컴파일
clang -O2 -g -target bpf \
    -D__TARGET_ARCH_x86 \
    -c xdp_prog.c -o xdp_prog.o

# (참고) clang + llc 2단계 파이프라인 (레거시 방식)
# clang -O2 -emit-llvm -c xdp_prog.c -o - | llc -march=bpf -filetype=obj -o xdp_prog.o

# 2단계: 오브젝트 검사
llvm-objdump -d xdp_prog.o       # 명령어 디스어셈블
readelf -S xdp_prog.o             # 섹션 확인 (xdp, maps, license 등)

최소 XDP 프로그램 (XDP_PASS)

/* xdp_pass.c — 모든 패킷을 통과시키는 최소 프로그램 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

SEC("xdp")
int xdp_pass_func(struct xdp_md *ctx)
{
    return XDP_PASS;
}

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

로드 및 확인

# XDP 프로그램 로드 (generic 모드, 모든 드라이버 지원)
sudo ip link set dev eth0 xdpgeneric obj xdp_pass.o sec xdp

# XDP 프로그램 로드 (native 모드, 드라이버 지원 필요)
sudo ip link set dev eth0 xdp obj xdp_pass.o sec xdp

# 로드 확인
ip link show dev eth0
# ... xdp/id:42 ... 형태로 표시

# bpftool로 상세 확인
sudo bpftool prog list
sudo bpftool prog show id 42

# XDP 프로그램 제거
sudo ip link set dev eth0 xdp off
XDP 모드 차이:
  • xdpgeneric — 소프트웨어 에뮬레이션, 모든 드라이버 지원, 성능 낮음 (개발/테스트용)
  • xdpdrv (native) — 드라이버 레벨 실행, 고성능 (프로덕션 권장)
  • xdpoffload — NIC 하드웨어에서 실행 (Netronome 등 지원 NIC 필요)

XDP (eXpress Data Path)

XDP는 NIC 드라이버 수준에서 패킷을 처리하는 고성능 데이터 경로입니다. 전체 네트워크 스택을 거치지 않으므로 초저지연 패킷 처리가 가능합니다.

/* XDP program example (C for BPF) */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>

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

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    if (eth->h_proto != htons(ETH_P_IP))
        return XDP_PASS;

    struct iphdr *iph = (void *)(eth + 1);
    if ((void *)(iph + 1) > data_end)
        return XDP_PASS;

    if (iph->protocol == IPPROTO_ICMP)
        return XDP_DROP;  /* Drop ICMP packets */

    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";
ICE XDP 프레임 크기 제한: Intel E810(ice) 드라이버에서 XDP를 사용할 때 프레임 크기가 3KB(3072바이트)를 초과하면 XDP가 동작하지 않습니다. 따라서 Jumbo Frame(MTU > 1500)과 XDP를 동시에 사용할 수 없습니다. MTU 9000 등 Jumbo Frame이 필요한 환경에서는 XDP 대신 tc-bpf를 검토하세요. XDP를 활성화하면 드라이버가 자동으로 frame 크기를 검증하며, 초과 시 -EINVAL로 거부합니다.
# ICE XDP 로드 전 MTU 확인
ip link show eth0 | grep mtu
# MTU 1500이면 XDP 사용 가능 (프레임 = MTU + 헤더 ≈ 1514 < 3072)

# XDP 프로그램 로드
ip link set dev eth0 xdp obj xdp_prog.o sec xdp

# Jumbo Frame + XDP 조합 시 에러 예시
# ip link set eth0 mtu 9000 → XDP 로드 실패 (-EINVAL)

BPF Maps

맵 유형설명용도
BPF_MAP_TYPE_HASH해시 테이블(Hash Table)키-값 저장
BPF_MAP_TYPE_ARRAY배열인덱스 기반 접근
BPF_MAP_TYPE_PERCPU_HASHPer-CPU 해시(Hash)고속 카운터
BPF_MAP_TYPE_RINGBUF링 버퍼(Ring Buffer)이벤트 스트리밍
BPF_MAP_TYPE_LRU_HASHLRU 해시연결 추적(Connection Tracking)
BPF_MAP_TYPE_PROG_ARRAY프로그램 배열tail call

CO-RE (Compile Once, Run Everywhere)

CO-RE는 BPF 프로그램을 한 번 컴파일하여 다양한 커널 버전에서 실행할 수 있게 합니다. BTF(BPF Type Format)를 활용하여 구조체(Struct) 레이아웃 차이를 자동으로 조정합니다.

💡

bpftool prog list로 현재 로드된 BPF 프로그램을, bpftool map list로 BPF 맵을 확인할 수 있습니다. bpftool prog dump xlated id <ID>로 JIT 컴파일된 코드를 볼 수 있습니다.

BPF Verifier

BPF verifier는 프로그램 로드 시 안전성을 검증합니다. 모든 경로가 종료되는지, 메모리 범위를 벗어나지 않는지, 무한 루프가 없는지 등을 정적으로 분석합니다.

/* Verifier가 검증하는 주요 사항 */
/* 1. 경계 검사: 모든 포인터 접근에 범위 확인 필요 */
if (data + sizeof(struct ethhdr) > data_end)
    return XDP_PASS;  /* 이 검사 없으면 verifier 거부 */

/* 2. 레지스터 상태 추적: R0-R10의 타입과 범위 */
/* 3. 루프 제한: bounded loop만 허용 (5.3+) */
for (int i = 0; i < 256; i++) { /* OK: bounded */
    if (condition) break;
}
/* 4. 명령어 수 제한: 최대 1M instructions (5.2+) */
/* 5. 맵 접근: NULL 체크 필수 */
__u64 *val = bpf_map_lookup_elem(&my_map, &key);
if (val)  /* NULL 체크 없으면 verifier 거부 */
    *val += 1;

BPF 헬퍼 함수

헬퍼용도사용 가능 프로그램
bpf_map_lookup_elem()맵에서 값 조회전체
bpf_map_update_elem()맵에 값 저장전체
bpf_probe_read_kernel()커널 메모리 읽기kprobe, tracepoint
bpf_get_current_pid_tgid()현재 프로세스(Process) PIDkprobe, tracepoint
bpf_ktime_get_ns()나노초 타임스탬프전체
bpf_trace_printk()디버그 출력전체
bpf_redirect()패킷 리다이렉트XDP, TC
bpf_skb_store_bytes()패킷 데이터 수정TC
bpf_ringbuf_output()링 버퍼로 이벤트 전송전체

kprobe/tracepoint BPF 프로그램

/* kprobe: 커널 함수 진입 시 실행 */
SEC("kprobe/do_sys_openat2")
int BPF_KPROBE(trace_openat2, int dfd, const char *filename)
{
    u64 pid_tgid = bpf_get_current_pid_tgid();
    u32 pid = pid_tgid >> 32;

    char comm[16];
    bpf_get_current_comm(&comm, sizeof(comm));

    bpf_printk("PID %d (%s) opening file\\n", pid, comm);
    return 0;
}

/* tracepoint: 정적 트레이스포인트에 연결 */
SEC("tracepoint/sched/sched_process_exec")
int trace_exec(struct trace_event_raw_sched_process_exec *ctx)
{
    char comm[16];
    bpf_get_current_comm(&comm, sizeof(comm));
    bpf_printk("exec: %s\\n", comm);
    return 0;
}

BPF Ring Buffer

Ring Buffer는 BPF 프로그램에서 유저스페이스로 이벤트 데이터를 효율적으로 전송하는 메커니즘입니다. perf buffer보다 오버헤드(Overhead)가 낮고 메모리 효율이 좋습니다.

/* BPF 측: 이벤트 전송 */
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);  /* 256KB */
} events SEC(".maps");

struct event {
    u32 pid;
    char comm[16];
};

SEC("tp/sched/sched_process_fork")
int trace_fork(void *ctx)
{
    struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) return 0;

    e->pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&e->comm, sizeof(e->comm));
    bpf_ringbuf_submit(e, 0);
    return 0;
}

libbpf와 BPF 스켈레톤

/* 유저스페이스: libbpf 스켈레톤 활용 */
#include "trace.skel.h"  /* bpftool gen skeleton으로 생성 */

int main(void)
{
    struct trace_bpf *skel;

    skel = trace_bpf__open_and_load();  /* BPF 프로그램 로드 */
    trace_bpf__attach(skel);            /* hook에 연결 */

    /* Ring buffer 폴링 */
    struct ring_buffer *rb;
    rb = ring_buffer__new(
        bpf_map__fd(skel->maps.events),
        handle_event, NULL, NULL);

    while (!exiting)
        ring_buffer__poll(rb, 100);  /* 100ms 타임아웃 */

    trace_bpf__destroy(skel);
}

TC (Traffic Control) BPF

TC BPF는 XDP보다 상위 계층에서 실행되며, 전체 sk_buff 구조체에 접근할 수 있어 더 풍부한 패킷 조작이 가능합니다.

# TC BPF 프로그램 로드
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj my_prog.o sec tc_ingress
tc filter add dev eth0 egress  bpf da obj my_prog.o sec tc_egress

# 확인
tc filter show dev eth0 ingress
bpftool prog list
⚠️

BPF 프로그램은 CAP_BPF (또는 CAP_SYS_ADMIN) 권한이 필요합니다. sysctl kernel.unprivileged_bpf_disabled=1이 설정된 환경에서는 root만 BPF 프로그램을 로드할 수 있습니다.

SmartNIC과 하드웨어 오프로드

SmartNIC(또는 DPU — Data Processing Unit)은 네트워크 처리를 NIC 자체의 프로세서/FPGA에서 수행하여 호스트 CPU를 해방시키는 차세대 네트워크 인프라 기술입니다.

SmartNIC 유형과 커널 지원

제조사제품군아키텍처커널 드라이버XDP 지원
NVIDIA (Mellanox) ConnectX-6/7, BlueField-2/3 ASIC + ARM SoC (DPU) mlx5_core XDP native + TC flower HW offload
Netronome Agilio CX NFP (Network Flow Processor) nfp XDP HW offload (BPF → NFP JIT)
Intel E810 (IPU), Mount Evans ASIC + FPGA ice XDP native, ADQ (Application Device Queues)
Broadcom Stingray PS1100 ARM SoC bnxt_en XDP native
Marvell OCTEON 10 DPU ARM SoC + HW accel octeontx2 XDP native

XDP 하드웨어 오프로드

# XDP 실행 모드 비교

# 1. Generic XDP — 모든 NIC에서 동작 (성능 낮음, 테스트/개발용)
ip link set dev eth0 xdpgeneric obj prog.o sec xdp

# 2. Native XDP — NIC 드라이버 지원 필요 (고성능)
ip link set dev eth0 xdp obj prog.o sec xdp

# 3. HW Offload XDP — SmartNIC에서 실행 (최고 성능, 호스트 CPU 0%)
ip link set dev eth0 xdpoffload obj prog.o sec xdp

# 현재 XDP 모드 확인
ip link show dev eth0
# ... xdp/id:42 ... (native)
# ... xdpoffload/id:42 ... (hw offload)

# 성능 비교 (64B 패킷, 단일 코어 기준)
# Generic XDP:  ~5 Mpps
# Native XDP:   ~24 Mpps
# HW Offload:   ~40+ Mpps (NIC 와이어 레이트)

TC Flower 하드웨어 오프로드

# TC flower 규칙을 SmartNIC HW로 오프로드
# — 매칭/액션을 NIC의 eSwitch에서 처리

# switchdev 모드 설정 (eSwitch 활성화)
devlink dev eswitch set pci/0000:03:00.0 mode switchdev

# VF representor를 통한 플로우 오프로드
tc qdisc add dev enp3s0f0_0 ingress
tc filter add dev enp3s0f0_0 ingress protocol ip     flower src_ip 10.0.0.0/24 dst_ip 192.168.1.0/24     action mirred egress redirect dev enp3s0f0_1     skip_sw   # ← skip_sw: HW 전용 (소프트웨어 경로 스킵)

# 오프로드된 플로우 확인
tc -s filter show dev enp3s0f0_0 ingress
# in_hw  ← 하드웨어에서 실행 중임을 표시

SmartNIC 커널 개발 시 고려사항

SmartNIC 도입 시 주의사항:
  • BPF 명령어 제한 — HW offload 시 NIC 프로세서가 지원하는 BPF 명령어 서브셋만 사용 가능. 복잡한 맵 조회, 루프, 헬퍼 함수 일부가 제한됨
  • 맵 동기화 — 오프로드된 BPF 맵은 NIC 메모리에 존재. 호스트에서 맵 업데이트 시 지연 발생 가능
  • switchdev 모드 전환 — eSwitch 모드 변경 시 네트워크 순간 단절. 운영 중 변경 주의
  • 펌웨어(Firmware) 호환성 — SmartNIC 펌웨어와 커널 드라이버 버전 호환성 확인 필수. 불일치 시 오프로드 실패
  • 디버깅 어려움 — HW 오프로드된 로직은 tcpdump/BPF trace로 관찰 불가. skip_sw 대신 skip_hw로 먼저 S/W 테스트 후 전환
  • Fallback 전략 — HW offload 실패 시 자동으로 SW 경로로 fallback되지만 성능 급락. 모니터링 필수
  • CT offload — conntrack 오프로드(tc ct action)는 제한적. 지원되는 연결 수, 타임아웃 동작이 SW와 다를 수 있음

SmartNIC과 DPDK/AF_XDP 연동

고성능 패킷 처리 파이프라인(Pipeline):
  • AF_XDP — XDP에서 사용자 공간(User Space)으로 제로카피 패킷 전달. XDP_REDIRECT → AF_XDP 소켓(Socket)으로 커널 바이패스 수준의 성능
  • DPDK + SmartNIC — 완전한 커널 바이패스. PMD(Poll Mode Driver)로 NIC 직접 제어. 최대 성능이지만 커널 네트워크 스택 기능(방화벽(Firewall), 라우팅) 사용 불가
  • 하이브리드 — SmartNIC eSwitch에서 fast path(TC flower offload)와 slow path(커널 스택) 분리. 일반 트래픽은 HW, 예외는 SW 처리

BPF/eBPF 주요 보안 취약점(Vulnerability) 사례

BPF/eBPF는 커널 내에서 사용자 정의 코드를 실행하는 강력한 메커니즘이지만, Verifier 우회, JIT 취약점, 투기적 실행(Speculative Execution) 등 다양한 공격 벡터가 존재합니다. 아래는 실제로 보고된 주요 보안 취약점 사례와 그 대응 방법입니다.

BPF Verifier 우회 취약점 (CVE-2021-3490)

심각도: 높음 (로컬 권한 상승) — ALU32 비트 연산에서 Verifier의 범위 추적(bound tracking) 결함으로 비특권 사용자가 커널 메모리에 임의 읽기/쓰기가 가능했습니다.

이 취약점은 BPF Verifier가 ALU32 연산에서 비트 연산(AND, OR, XOR)의 결과 범위를 잘못 계산하는 데서 발생합니다. Verifier는 각 레지스터(Register)의 가능한 값 범위를 tnum(tracked number) 구조체로 추적하는데, 비트 연산 후 tnum 값과 min/max 경계가 불일치하면 실제로는 불가능한 값 범위를 허용하게 됩니다.

/* 취약점을 유발하는 BPF 코드 패턴 (개념 예시) */
/* Verifier가 r0의 범위를 [0, 1]로 추적하지만 */
/* 실제 ALU32 비트 연산 후 범위가 확장될 수 있음 */

BPF_ALU64_IMM(BPF_MOV, BPF_REG_0, 0),
BPF_ALU64_IMM(BPF_OR, BPF_REG_0, 0x7FFFFF00),
/* Verifier는 r0 ≤ 0x7FFFFFFF로 추적하지만 */
/* tnum 분석과 범위 분석의 불일치로 */
/* out-of-bounds 맵 접근이 허용됨 */

BPF_ALU32_IMM(BPF_AND, BPF_REG_0, idx),
/* 이 시점에서 Verifier의 범위 계산 오류 발생 */
BPF_STX_MEM(BPF_DW, BPF_REG_MAP, BPF_REG_0, 0),
/* 맵 경계 밖의 커널 메모리에 쓰기 가능 */

수정 패치(Patch)에서는 ALU 연산의 tnum 검증을 강화하여 비트 연산 후 tnum.valuetnum.maskmin/max 경계와 일관성을 유지하도록 했습니다.

권장 완화 조치: 비특권 사용자의 BPF 프로그램 로드를 차단하는 것이 가장 효과적입니다.
# 비특권 BPF 비활성화 (재부팅 시 초기화)
sysctl -w kernel.unprivileged_bpf_disabled=1

# 영구 설정 (/etc/sysctl.d/99-bpf.conf)
kernel.unprivileged_bpf_disabled = 1

BPF JIT Spraying (CVE-2020-8835)

심각도: 높음 (로컬 권한 상승) — JIT 컴파일된 BPF 코드에서 32비트와 64비트 레지스터 값 불일치를 악용하여 커널 메모리에 대한 범위 밖 접근이 가능했습니다.

이 취약점은 BPF JIT 컴파일러가 ALU32 연산을 처리할 때 32비트 값의 상위 32비트를 적절히 truncation하지 않아 발생합니다. Verifier는 32비트 연산의 결과를 올바르게 추적하지만, JIT가 생성한 네이티브 코드에서는 64비트 레지스터의 상위 비트가 보존되어 실제 값이 Verifier가 추적한 범위를 벗어나게 됩니다.

/* CVE-2020-8835: 32비트/64비트 불일치 악용 패턴 */

/* 1단계: 64비트 레지스터에 큰 값 로드 */
r1 = 0x100000001    /* 상위 32비트: 1, 하위 32비트: 1 */

/* 2단계: ALU32 연산 — Verifier는 하위 32비트만 추적 */
w1 += 0              /* 32비트 ADD: Verifier는 w1=1로 추적 */
                     /* 그러나 JIT 코드는 상위 비트를 truncate하지 않음 */
                     /* 실제 r1 = 0x100000001 (Verifier 추적 범위 초과) */

/* 3단계: 이 불일치를 이용한 OOB 접근 */
r2 = map_ptr         /* BPF 맵 포인터 */
r2 += r1             /* Verifier: map + 1 (안전) */
                     /* 실제: map + 0x100000001 (OOB!) */
*(r2) = 0            /* 커널 메모리 arbitrary write */

수정 패치에서는 JIT 컴파일러가 모든 ALU32 연산 후에 명시적으로 상위 32비트를 zero-extend하도록 하여 레지스터 값이 Verifier의 추적 범위와 일치하도록 했습니다.

CONFIG_BPF_JIT_ALWAYS_ON의 보안 영향: 이 옵션을 활성화하면 BPF 인터프리터가 비활성화되고 항상 JIT를 사용합니다. JIT는 성능에 유리하지만, JIT 관련 취약점이 발견되면 인터프리터로 fallback할 수 없습니다. 반면 인터프리터 방식은 Spectre 기반 공격에 더 취약할 수 있으므로, 보안 정책에 따라 신중하게 선택해야 합니다.
# JIT 상태 확인
cat /proc/sys/net/core/bpf_jit_enable
# 0: 비활성화 (인터프리터)
# 1: 활성화 (기본값, fallback 가능)
# 2: 디버깅 모드 (JIT 이미지 덤프)

# CONFIG_BPF_JIT_ALWAYS_ON=y 시 인터프리터 완전 제거
# JIT 비활성화 불가 — 항상 네이티브 코드 실행

Spectre v1과 BPF (CVE-2019-7308)

심각도: 중간 (정보 누출) — BPF 프로그램에서 배열 인덱싱 시 투기적 실행(speculative execution)을 통해 커널 메모리 정보가 누출될 수 있었습니다.

Spectre v1(bounds check bypass) 공격은 CPU의 분기 예측(Branch Prediction)기를 훈련시켜, 배열 경계 검사가 완료되기 전에 투기적으로 범위 밖의 데이터를 로드하게 만듭니다. BPF 프로그램은 사용자가 작성한 코드를 커널 내에서 실행하므로, 투기적 실행의 영향을 받는 패턴을 쉽게 만들 수 있었습니다.

/* Spectre v1 공격 패턴 (BPF 맵 배열 접근) */

/* 공격자가 제어하는 인덱스 값 */
r0 = *(u32 *)(r1 + offsetof(ctx, data))  /* 외부 입력 */

/* Verifier는 이 분기를 확인하여 r0 < array_size 보장 */
if r0 >= array_size goto out

/* 그러나 CPU는 투기적으로 분기를 무시하고 실행 */
/* r0가 array_size를 초과하는 값으로 투기적 로드 */
r1 = map_base + r0 * 8   /* 투기적 OOB 포인터 계산 */
r2 = *(u64 *)(r1)        /* 투기적 OOB 로드 — 커널 메모리 읽기 */

/* 캐시 사이드 채널을 통해 r2 값 추출 */
r3 = probe_array + r2 * 64  /* 캐시 라인 크기 단위 접근 */
r4 = *(u64 *)(r3)            /* 캐시에 r2 값의 흔적을 남김 */

수정 패치에서는 Verifier가 투기적 실행에 취약한 패턴을 탐지하여 lfence 명령(직렬화(Serialization) 배리어)을 삽입합니다. 또한 포인터 산술에 대한 BPF_ALU64_REG sanitization을 도입하여, 투기적 실행 경로에서도 포인터가 허용된 범위를 벗어나지 않도록 마스킹합니다.

/* 수정 후: Verifier가 삽입하는 방어 코드 */

if r0 >= array_size goto out

/* Verifier가 자동 삽입하는 sanitization */
r0 &= array_mask        /* 투기적 실행에서도 범위 제한 */
/* x86에서는 lfence 삽입으로 투기적 실행 직렬화 */

r1 = map_base + r0 * 8  /* 이제 투기적 OOB 불가 */
r2 = *(u64 *)(r1)
Spectre 완화 현황: 커널 5.x 이후 BPF Verifier는 투기적 실행 분석 패스를 내장하고 있으며, 포인터 연산에 대한 마스킹(sanitization)을 자동으로 수행합니다. 그러나 새로운 투기적 실행 변종이 지속적으로 발견되므로, 최신 커널 업데이트와 CPU 마이크로코드 업데이트를 병행해야 합니다.

BPF 맵 경쟁 조건(Race Condition) 사례

데이터 무결성(Integrity) 위험: BPF 맵은 커널과 사용자 공간, 또는 여러 CPU 간에 공유되는 데이터 구조이므로 동시 접근 시 경쟁 조건(race condition)이 발생할 수 있습니다.

BPF_MAP_TYPE_ARRAY는 고정 크기 배열로, 개별 요소에 대한 읽기/쓰기가 원자적(Atomic)이지 않습니다. 멀티 코어 환경에서 동일 요소를 동시에 업데이트하면 torn read/write가 발생하여 일부만 업데이트된 값을 읽을 수 있습니다. BPF_MAP_TYPE_HASH에서는 해시 테이블 resize 과정에서 element가 손실되거나 중복되는 문제가 보고되었습니다.

/* 문제: torn read/write가 발생하는 맵 접근 */
struct stats {
    __u64 packets;
    __u64 bytes;
};

/* CPU 0과 CPU 1이 동시에 같은 키의 stats를 업데이트 */
struct stats *val = bpf_map_lookup_elem(&stats_map, &key);
if (val) {
    val->packets += 1;    /* 비원자적: read-modify-write 경쟁 */
    val->bytes += pkt_len; /* packets과 bytes 사이에 다른 CPU가 끼어들 수 있음 */
    /* 결과: 카운터 손실 또는 packets/bytes 불일치 */
}

bpf_spin_lock을 사용하면 맵 요소에 대한 동기화된 접근이 가능합니다. 커널 5.1부터 지원되며, 맵 value 구조체에 struct bpf_spin_lock 필드를 포함시켜야 합니다.

/* 해결책 1: bpf_spin_lock으로 동기화 */
struct stats_locked {
    struct bpf_spin_lock lock;
    __u64 packets;
    __u64 bytes;
};

struct stats_locked *val = bpf_map_lookup_elem(&stats_map, &key);
if (val) {
    bpf_spin_lock(&val->lock);
    val->packets += 1;
    val->bytes += pkt_len;
    bpf_spin_unlock(&val->lock);
}
/* 해결책 2: per-CPU 맵으로 락 없는 병렬 접근 */
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 256);
    __type(key, __u32);
    __type(value, struct stats);
} stats_percpu SEC(".maps");

/* 각 CPU가 독립적인 복사본에 접근 — 락 불필요 */
struct stats *val = bpf_map_lookup_elem(&stats_percpu, &key);
if (val) {
    val->packets += 1;    /* 경쟁 없음: CPU별 독립 메모리 */
    val->bytes += pkt_len;
}

/* 사용자 공간에서 모든 CPU 값 합산 */
/* bpf_map_lookup_elem()은 ncpus개의 값을 반환 */
__u64 total_packets = 0;
for (int i = 0; i < ncpus; i++)
    total_packets += percpu_vals[i].packets;
맵 유형별 동기화 전략:
  • BPF_MAP_TYPE_ARRAY / HASH — 공유 맵. 다중 CPU에서 동시 업데이트 시 bpf_spin_lock 필요
  • BPF_MAP_TYPE_PERCPU_ARRAY / PERCPU_HASH — CPU별 독립 복사본. 락 없이 안전하지만 사용자 공간에서 합산 필요
  • __sync_fetch_and_add() — 단순 카운터에 대해 원자적 증가. 구조체 전체 동기화에는 부적합
  • BPF_MAP_TYPE_HASH resize — 해시 맵의 동적 확장 중 요소 접근은 RCU로 보호되지만, 대량 삽입 시 성능 저하 주의

eBPF Verifier 상세

BPF Verifier는 커널에서 가장 복잡한 정적 분석기 중 하나입니다. 사용자 공간에서 제출한 BPF 바이트코드가 커널 내에서 안전하게 실행될 수 있는지 로드 시점에 검증합니다. 검증을 통과하지 못한 프로그램은 절대로 커널에서 실행되지 않습니다.

Verifier 아키텍처

Verifier는 추상 해석(abstract interpretation) 기법을 사용합니다. 프로그램의 모든 가능한 실행 경로를 탐색하면서 각 명령어 실행 후의 레지스터 상태를 추적합니다. 핵심 구조체는 struct bpf_verifier_env이며, 검증 과정은 크게 두 단계로 나뉩니다.

BPF Verifier 검증 파이프라인 1단계: CFG 검증 check_cfg() 2단계: 상태 시뮬레이션 do_check() 3단계: Spectre 완화 opt_subreg_zext_lo32() JIT 컴파일 bpf_int_jit_compile() CFG 검증 항목 - 도달 불가 코드 탐지 - 무한 루프 탐지 - 유효한 점프 대상 - DAG 구조 확인 - back-edge 탐지 상태 시뮬레이션 항목 - 레지스터 타입/범위 추적 - 포인터 산술 검증 - 맵 접근 NULL 체크 - 헬퍼 함수 인자 검증 - 스택 깊이 제한 (512B) 레지스터 상태 타입 NOT_INIT | SCALAR_VALUE PTR_TO_CTX | PTR_TO_MAP_VALUE PTR_TO_STACK | PTR_TO_PACKET PTR_TO_BTF_ID | PTR_TO_MEM PTR_TO_FUNC | CONST_PTR_TO_MAP Verifier 제한 (커널 6.x 기준) 최대 명령어: 1,000,000 최대 상태: 64 per insn 스택: 512바이트 레지스터: R0-R10 중첩 호출: 8 depth bounded loop (5.3+)
BPF Verifier: CFG 검증 → 상태 시뮬레이션 → Spectre 완화 → JIT 컴파일 순서로 진행

BPF 레지스터 규약

eBPF 가상 머신은 11개의 64비트 레지스터(R0~R10)와 512바이트 스택, 프로그램 카운터(PC)를 제공합니다. 이 규약은 호출 규약(calling convention)을 정의하며 BPF 프로그램 작성과 Verifier 이해의 기본이 됩니다.

레지스터용도특성
R0반환값 / BPF 프로그램 종료 코드헬퍼 함수 반환값, XDP 결과 코드(XDP_PASS 등)
R1첫 번째 인자 / 컨텍스트 포인터프로그램 진입 시 ctx (예: xdp_md *)
R2두 번째 인자헬퍼 함수 호출 시 인자
R3세 번째 인자헬퍼 함수 호출 시 인자
R4네 번째 인자헬퍼 함수 호출 시 인자
R5다섯 번째 인자헬퍼 함수 호출 시 인자
R6callee-saved함수 호출 후 값 보존
R7callee-saved함수 호출 후 값 보존
R8callee-saved함수 호출 후 값 보존
R9callee-saved함수 호출 후 값 보존
R10스택 프레임(Stack Frame) 포인터 (FP)읽기 전용(Read-Only) — 수정 불가
호출 규약 핵심:
  • R1~R5는 scratch 레지스터입니다. 헬퍼 함수 호출 후 값이 파괴됩니다.
  • R6~R9는 callee-saved입니다. 헬퍼 호출 전후 값이 보존됩니다.
  • BPF 프로그램 진입 시 R1 = ctx, R10 = FP이며 나머지는 미초기화 상태입니다.
  • 스택은 R10을 기준으로 음수 오프셋(Offset)(R10 - 8, R10 - 16 …)으로 접근하며 최대 512바이트입니다.
/* BPF 레지스터 사용 예시 */
SEC("xdp")
int xdp_example(struct xdp_md *ctx)  /* R1 = ctx */
{
    /* R6에 ctx 백업 (callee-saved → 헬퍼 호출 후에도 안전) */
    void *data = (void *)(long)ctx->data;       /* R2 = data */
    void *data_end = (void *)(long)ctx->data_end; /* R3 = data_end */

    /* 헬퍼 호출 시 R1~R5에 인자 전달, R0에 결과 반환 */
    __u64 ts = bpf_ktime_get_ns();  /* R0 = 타임스탬프 */

    /* 스택 변수는 R10(FP) 기준 음수 오프셋 */
    __u32 key = 0;  /* [R10 - 4] = 0 */

    return XDP_PASS;  /* R0 = XDP_PASS (2) */
}

BPF 명령어 집합 (ISA)

eBPF 명령어는 고정 길이 64비트(8바이트) 포맷입니다. 128비트 즉시값(wide immediate)이 필요한 LD_IMM64만 예외적으로 16바이트를 사용합니다. BPF ISA는 2024년 10월 IETF RFC 9669로 공식 표준화되었습니다.

명령어 포맷

BPF 명령어 포맷 (64-bit) 0 8 12 16 32 64 (bit) opcode 8 bits dst 4 bits src 4 bits offset 16 bits (signed) imm (immediate) 32 bits (signed) opcode 세부 [2:0] class ALU: [7:4] op [3] src [2:0] class JMP: [7:4] op [3] src [2:0] class MEM: [7:5] mode [4:3] size [2:0] class
BPF 명령어: opcode(8) + dst_reg(4) + src_reg(4) + offset(16) + imm(32) = 64비트

산술/논리 연산 (ALU)

ALU 연산은 32비트(BPF_ALU, class 0x04)와 64비트(BPF_ALU64, class 0x07) 두 가지 클래스로 나뉩니다.

연산opcode동작
ADD0x00dst += src
SUB0x10dst -= src
MUL0x20dst *= src
DIV0x30dst /= src
OR0x40dst |= src
AND0x50dst &= src
LSH0x60dst <<= src
RSH0x70dst >>= src (논리)
NEG0x80dst = -dst
MOD0x90dst %= src
XOR0xa0dst ^= src
MOV0xb0dst = src
ARSH0xc0dst >>= src (산술, 부호 확장)
END0xd0바이트 순서(Byte Order) 변환 (endian)
ALU32 vs ALU64: BPF_ALU는 32비트 연산을 수행하고 상위 32비트를 0으로 클리어합니다. BPF_ALU64는 전체 64비트에 대해 연산합니다. Verifier가 32비트 서브레지스터 최적화(opt_subreg_zext_lo32)를 자동 적용합니다.

점프 연산 (JMP)

점프 연산은 32비트(BPF_JMP32, class 0x06)와 64비트(BPF_JMP, class 0x05) 클래스가 있습니다.

연산opcode조건
JA0x00무조건 점프 (PC += offset)
JEQ0x10dst == src
JGT0x20dst > src (부호 없는)
JGE0x30dst >= src (부호 없는)
JSET0x40dst & src != 0
JNE0x50dst != src
JSGT0x60dst > src (부호 있는)
JSGE0x70dst >= src (부호 있는)
CALL0x80헬퍼 함수 호출 (imm = func_id)
EXIT0x90프로그램 종료 (R0 반환)
JLT0xa0dst < src (부호 없는)
JLE0xb0dst <= src (부호 없는)
JSLT0xc0dst < src (부호 있는)
JSLE0xd0dst <= src (부호 있는)

메모리 연산

메모리 접근은 LD/LDX/ST/STX 4가지 클래스를 사용합니다.

클래스코드동작
BPF_LD0x00즉시값 로드 (LD_IMM64 등)
BPF_LDX0x01dst = *(size *)(src + offset)
BPF_ST0x02*(size *)(dst + offset) = imm
BPF_STX0x03*(size *)(dst + offset) = src

크기 지정자:

크기코드바이트
BPF_W0x004 (word)
BPF_H0x082 (half-word)
BPF_B0x101 (byte)
BPF_DW0x188 (double-word)

원자 연산

커널 5.12부터 BPF_STX 클래스에 BPF_ATOMIC 모드(0xc0)가 추가되었습니다. imm 필드에 연산 유형을 지정합니다.

연산imm동작
ATOMIC_ADD0x00*(dst + offset) += src
ATOMIC_OR0x40*(dst + offset) |= src
ATOMIC_AND0x50*(dst + offset) &= src
ATOMIC_XOR0xa0*(dst + offset) ^= src
ATOMIC_FETCH_ADD0x01src = atomic_fetch_add(dst+off, src)
ATOMIC_XCHG0xe1src = xchg(dst + offset, src)
ATOMIC_CMPXCHG0xf1R0 = cmpxchg(dst+off, R0, src)
# bpftool로 BPF 프로그램의 명령어 덤프 (JIT 전)
$ sudo bpftool prog dump xlated id 42
   0: (b7) r0 = 2                    # MOV r0, 2 (XDP_PASS)
   1: (95) exit                       # EXIT

# JIT 컴파일된 네이티브 코드 확인
$ sudo bpftool prog dump jited id 42
   0: mov    $0x2,%eax
   5: retq

레지스터 상태 추적

Verifier는 R0~R10 총 11개 레지스터의 상태를 명령어마다 추적합니다. 각 레지스터는 타입(enum bpf_reg_type)과 값 범위(struct tnum — tracked number)를 가지며, 분기 조건에 따라 상태가 정제(refine)됩니다.

/* 레지스터 상태 구조체 (kernel/bpf/verifier.c) */
struct bpf_reg_state {
    enum bpf_reg_type type;  /* PTR_TO_MAP_VALUE, SCALAR_VALUE 등 */
    struct tnum var_off;     /* 비트 단위 알려진 값 추적 */
    s64 smin_value;           /* 부호 있는 최소값 */
    s64 smax_value;           /* 부호 있는 최대값 */
    u64 umin_value;           /* 부호 없는 최소값 */
    u64 umax_value;           /* 부호 없는 최대값 */
    s32 s32_min_value;        /* 32비트 부호 있는 최소 */
    s32 s32_max_value;        /* 32비트 부호 있는 최대 */
    u32 u32_min_value;        /* 32비트 부호 없는 최소 */
    u32 u32_max_value;        /* 32비트 부호 없는 최대 */
};

/* tnum: 비트 단위 추적으로 정밀한 범위 분석 */
struct tnum {
    u64 value;  /* 확실히 1인 비트 */
    u64 mask;   /* 알 수 없는 비트 (1=unknown) */
};
/* 예: value=0x10, mask=0xFF → 값은 0x10~0x1F 또는 0x10|0x00~0xFF 범위 */

루프 검증 (Bounded Loops)

커널 5.3부터 Verifier는 bounded loop를 허용합니다. 루프가 허용되려면 다음 조건을 모두 만족해야 합니다:

/* OK: Verifier가 허용하는 bounded loop */
for (int i = 0; i < 256; i++) {
    if (data + i >= data_end)
        break;
    /* 패킷 데이터 처리 */
}

/* OK: bpf_loop() 헬퍼 (커널 5.17+) — 콜백 기반 루프 */
static long loop_callback(u32 index, void *ctx)
{
    /* 루프 본문 */
    return 0;  /* 0=계속, 1=중단 */
}
bpf_loop(1000, loop_callback, &ctx, 0);

/* FAIL: Verifier가 거부하는 패턴 */
while (ptr != NULL) {  /* ← 상한을 알 수 없음 */
    ptr = ptr->next;
}

맵 접근 검증

bpf_map_lookup_elem() 반환값은 PTR_TO_MAP_VALUE_OR_NULL 타입입니다. Verifier는 이 포인터를 사용하기 전에 반드시 NULL 체크가 있는지 확인하며, 체크 후에는 PTR_TO_MAP_VALUE로 타입을 정제합니다. 맵 값에 대한 접근은 value_size 범위 내로 제한됩니다.

/* Verifier의 맵 접근 검증 과정 */
__u32 key = 0;
struct data *val = bpf_map_lookup_elem(&my_map, &key);
/* 여기서 val의 타입: PTR_TO_MAP_VALUE_OR_NULL */

/* ❌ Verifier 거부: NULL일 수 있는 포인터 역참조 */
// val->count++;  // R0 invalid mem access 'map_value_or_null' */

if (!val)
    return 0;

/* ✅ 여기서 val의 타입: PTR_TO_MAP_VALUE (NULL 아님 확정) */
val->count++;  /* offset 0, value_size 내 → OK */

/* ❌ Verifier 거부: value_size 초과 접근 */
// *((char *)val + 4096) = 0;  // invalid access to map value */

Verifier 로그 읽기

프로그램이 검증에 실패하면 Verifier는 상세한 로그를 출력합니다. 이 로그를 읽는 능력은 BPF 개발에서 매우 중요합니다.

# Verifier 로그 확인 방법

# 1. bpftool로 상세 로그 출력
bpftool prog load prog.o /sys/fs/bpf/prog -d 2>&1 | head -100

# 2. libbpf 로그 레벨 조정
# libbpf_set_print(my_print_fn) 또는 LIBBPF_LOG_LEVEL=debug

# Verifier 로그 출력 예시:
# func#0 @0
# 0: (b7) r1 = 0                    ; R1_w=0
# 1: (63) *(u32 *)(r10 -4) = r1     ; R1_w=0 R10=fp0
# 2: (bf) r2 = r10                  ; R2_w=fp0 R10=fp0
# 3: (07) r2 += -4                  ; R2_w=fp-4
# 4: (18) r1 = 0xffff888012345678   ; R1_w=map_ptr(id=0,...)
# 6: (85) call bpf_map_lookup_elem#1
# 7: (15) if r0 == 0x0 goto pc+3    ; R0_w=map_value_or_null
# 8: (61) r1 = *(u32 *)(r0 +0)      ; R0_w=map_value R1_w=scalar
# ...
# 로그에서 R#_w= 은 레지스터의 현재 타입과 추적 범위를 표시
Verifier 로그 핵심 패턴:
  • R0=scalar(id=X,umax=Y) — 레지스터가 스칼라 값이고 최대값이 Y
  • R1=map_value(off=0,ks=4,vs=16) — 맵 값 포인터, key 4바이트, value 16바이트
  • invalid mem access 'map_value_or_null' — NULL 체크 누락
  • back-edge from insn X to Y — 루프 탐지, bounded 여부 확인
  • processed N insns ... complexity limit — 복잡도 초과, 프로그램 단순화 필요

BPF 맵 타입별 상세

BPF 맵은 커널과 유저스페이스, 또는 BPF 프로그램 간에 데이터를 공유하는 핵심 메커니즘입니다. 용도에 따라 적절한 맵 타입을 선택하는 것이 성능과 정확성에 직접적인 영향을 줍니다.

맵 타입자료구조키 조회동시성주요 용도성능 특성
HASH 해시 테이블 O(1) 평균 RCU + spin_lock 연결 추적, 세션 테이블 동적 크기, 충돌 시 체이닝
ARRAY 고정 배열 O(1) 원자적 아님 설정, 전역 카운터 사전 할당, 삭제 불가
PERCPU_HASH CPU별 해시 O(1) 평균 CPU별 독립 고속 통계, 플로우 카운터 락 없음, CPU수 x value 메모리
PERCPU_ARRAY CPU별 배열 O(1) CPU별 독립 per-CPU 카운터, 히스토그램 락 없음, 사용자 공간에서 합산
LRU_HASH LRU 해시 O(1) 평균 내부 LRU 락 캐시(Cache), 연결 테이블 (자동 퇴거) max_entries 초과 시 LRU 제거
LPM_TRIE Longest Prefix Match O(key bits) spin_lock IP 주소 매칭, 라우팅 CIDR 기반 검색
RINGBUF 링 버퍼 FIFO lock-free MPSC 이벤트 스트리밍 perf_buffer 대비 메모리 효율
STACK_TRACE 스택 트레이스 ID 기반 해시 기반 프로파일링(Profiling), 콜 그래프 bpf_get_stackid()로 저장
QUEUE / STACK 큐/스택 FIFO/LIFO spin_lock 작업 큐(Workqueue), 이벤트 버퍼링 키 없음, push/pop/peek
CGROUP_STORAGE cgroup별 저장소 자동 키 cgroup별 독립 cgroup별 정책, 제한 cgroup attach 시 자동 할당
STRUCT_OPS 구조체 콜백(Callback) 등록 기반 TCP 혼잡 제어(Congestion Control), 스케줄러(Scheduler) 커널 ops 테이블 교체
/* 주요 맵 타입 선언 예시 */

/* 1. LRU_HASH: 자동 퇴거되는 연결 추적 테이블 */
struct {
    __uint(type, BPF_MAP_TYPE_LRU_HASH);
    __uint(max_entries, 100000);
    __type(key, struct flow_key);   /* src_ip, dst_ip, proto, ports */
    __type(value, struct flow_stats);
} conn_table SEC(".maps");

/* 2. LPM_TRIE: CIDR 기반 IP 매칭 */
struct {
    __uint(type, BPF_MAP_TYPE_LPM_TRIE);
    __uint(max_entries, 10000);
    __uint(map_flags, BPF_F_NO_PREALLOC);
    __type(key, struct bpf_lpm_trie_key_u8);  /* prefixlen + data */
    __type(value, __u32);
} ip_blocklist SEC(".maps");

/* 3. STACK_TRACE: 프로파일링용 스택 저장 */
struct {
    __uint(type, BPF_MAP_TYPE_STACK_TRACE);
    __uint(max_entries, 8192);
    __uint(key_size, sizeof(__u32));
    __uint(value_size, 100 * sizeof(__u64));
} stackmap SEC(".maps");

/* 4. QUEUE: 키 없는 FIFO */
struct {
    __uint(type, BPF_MAP_TYPE_QUEUE);
    __uint(max_entries, 1024);
    __type(value, struct event);
} event_queue SEC(".maps");

/* QUEUE 사용법 */
struct event e = { .pid = pid, .ts = ts };
bpf_map_push_elem(&event_queue, &e, BPF_ANY);  /* enqueue */
/* 사용자 공간에서: bpf_map_lookup_and_delete_elem() → dequeue */
맵 타입 선택 가이드:
  • 고속 카운터PERCPU_ARRAY (락 없이 CPU별 독립 증가, 사용자 공간에서 합산)
  • 연결 추적LRU_HASH (메모리 한도 초과 시 자동 퇴거, prealloc 주의)
  • IP 블랙리스트LPM_TRIE (CIDR 서브넷 단위 매칭)
  • 이벤트 전송RINGBUF (가변 크기 이벤트, 공유 버퍼로 메모리 효율)
  • TCP 혼잡 제어STRUCT_OPS (커널의 tcp_congestion_ops 교체)

CO-RE (Compile Once, Run Everywhere) 상세

CO-RE는 BPF 프로그램의 이식성 문제를 해결하는 핵심 기술입니다. 커널 구조체의 레이아웃은 버전마다 달라질 수 있는데, CO-RE는 BTF(BPF Type Format) 정보를 활용하여 컴파일 시점의 구조체 오프셋을 실행 시점에 자동으로 재조정(relocate)합니다.

BTF (BPF Type Format)

BTF는 BPF 프로그램과 커널의 타입 정보를 컴팩트하게 인코딩하는 형식입니다. DWARF 디버그 정보를 축소한 것으로, 커널 이미지에 포함되어 /sys/kernel/btf/vmlinux에서 접근할 수 있습니다.

# BTF 지원 확인
ls -la /sys/kernel/btf/vmlinux
# 파일이 있으면 BTF 지원 커널

# vmlinux.h 생성 — 모든 커널 타입 정의를 담은 헤더
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 약 10만 줄 이상의 C 헤더 생성
# 이 헤더만 include하면 커널 헤더 의존성 제거

# 특정 구조체의 BTF 정보 확인
bpftool btf dump file /sys/kernel/btf/vmlinux | grep -A 20 "struct task_struct"

# 커널 모듈의 BTF 확인
bpftool btf dump file /sys/kernel/btf/nf_conntrack

bpf_core_read 매크로(Macro)와 재배치(Relocation)

BPF_CORE_READ() 매크로는 컴파일 시점에 CO-RE 재배치(relocation) 정보를 생성합니다. libbpf가 프로그램을 로드할 때, 컴파일 시점의 오프셋을 실행 중인 커널의 BTF와 비교하여 자동으로 수정합니다.

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>

/* CO-RE 방식: 필드 오프셋이 자동 재배치됨 */
SEC("tp/sched/sched_process_exec")
int trace_exec(void *ctx)
{
    struct task_struct *task = (struct task_struct *)bpf_get_current_task();

    /* BPF_CORE_READ: 중첩 포인터를 안전하게 따라감 */
    pid_t pid = BPF_CORE_READ(task, tgid);
    pid_t ppid = BPF_CORE_READ(task, real_parent, tgid);

    /* 구조체 필드 존재 여부 런타임 확인 */
    if (bpf_core_field_exists(task->loginuid)) {
        uid_t loginuid = BPF_CORE_READ(task, loginuid.val);
    }

    /* 필드 크기 확인 (커널 버전별 다를 수 있음) */
    if (bpf_core_field_size(task->comm) == 16) {
        /* TASK_COMM_LEN == 16인 커널 */
    }

    return 0;
}

/* struct flavor: 커널 버전별로 다른 구조체 레이아웃 대응 */
struct task_struct___old {   /* ___old 접미사 = flavor */
    int prio;
    int static_prio;
    /* 이전 커널에서 다른 위치에 있던 필드 */
};

/* bpf_core_type_matches()로 어떤 flavor인지 런타임 판별 */
if (bpf_core_type_matches(struct task_struct___old)) {
    /* 이전 구조체 레이아웃 사용 */
}

CO-RE 워크플로

# CO-RE 개발 워크플로

# 1. vmlinux.h 생성 (개발 머신에서 한 번)
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

# 2. BPF 프로그램 컴파일 (CO-RE 활성화)
clang -g -O2 -target bpf \
    -D__TARGET_ARCH_x86 \
    -c prog.bpf.c -o prog.bpf.o

# 3. 스켈레톤 생성
bpftool gen skeleton prog.bpf.o > prog.skel.h

# 4. 사용자 프로그램 컴파일
gcc -g -O2 -o prog prog.c -lbpf -lelf -lz

# 이 바이너리를 다른 커널 버전 시스템에 복사해도 동작!
# libbpf가 로드 시 BTF 재배치를 자동 수행

libbpf 프로그래밍 상세

libbpf는 BPF 프로그램의 로드, 검증, 어태치를 관리하는 공식 사용자 공간 라이브러리입니다. 스켈레톤(skeleton) API는 BPF 프로그램의 맵, 프로그램, 전역 변수에 대한 타입 안전한 접근을 제공합니다.

Skeleton API

/* === BPF 측 (prog.bpf.c) === */
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

/* 전역 변수: 사용자 공간에서 직접 읽기/쓰기 가능 */
const volatile pid_t target_pid = 0;  /* .rodata → 로드 시 설정 */
__u64 event_count = 0;                  /* .bss → 런타임 업데이트 */

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");

SEC("tp/sched/sched_process_exec")
int handle_exec(void *ctx)
{
    pid_t pid = bpf_get_current_pid_tgid() >> 32;
    if (target_pid && pid != target_pid)
        return 0;

    __sync_fetch_and_add(&event_count, 1);
    /* ... 이벤트 전송 ... */
    return 0;
}

/* === 사용자 측 (prog.c) === */
#include "prog.skel.h"  /* bpftool gen skeleton으로 생성 */

int main(int argc, char **argv)
{
    struct prog_bpf *skel;

    /* 1단계: open — ELF 파싱, 맵/프로그램 탐색 */
    skel = prog_bpf__open();
    if (!skel) return 1;

    /* 2단계: 로드 전 설정 — 전역 변수, 맵 크기 조정 */
    skel->rodata->target_pid = atoi(argv[1]);
    bpf_map__set_max_entries(skel->maps.events, 512 * 1024);

    /* 3단계: load — verifier 검증, JIT 컴파일, 맵 생성 */
    if (prog_bpf__load(skel)) {
        fprintf(stderr, "BPF load failed\n");
        goto cleanup;
    }

    /* 4단계: attach — hook point에 연결 */
    if (prog_bpf__attach(skel)) {
        fprintf(stderr, "BPF attach failed\n");
        goto cleanup;
    }

    /* 5단계: 이벤트 수신 (ring buffer) */
    struct ring_buffer *rb = ring_buffer__new(
        bpf_map__fd(skel->maps.events),
        handle_event, NULL, NULL);

    while (!exiting) {
        ring_buffer__poll(rb, 100);
        /* 전역 변수 읽기 (mmap 기반) */
        printf("events: %llu\n", skel->bss->event_count);
    }

cleanup:
    ring_buffer__free(rb);
    prog_bpf__destroy(skel);
    return 0;
}

프로그램 타입별 Attach

/* libbpf 수동 attach 예시 */

/* kprobe — 커널 함수 진입점 */
struct bpf_link *link = bpf_program__attach_kprobe(
    skel->progs.my_kprobe, false, "tcp_v4_connect");

/* tracepoint — 정적 트레이스포인트 */
link = bpf_program__attach_tracepoint(
    skel->progs.my_tp, "net", "net_dev_xmit");

/* perf event — 하드웨어/소프트웨어 이벤트 */
link = bpf_program__attach_perf_event(
    skel->progs.my_perf, pmu_fd);

/* XDP — 네트워크 인터페이스 */
link = bpf_program__attach_xdp(
    skel->progs.my_xdp, ifindex);

/* cgroup — cgroup 경로 기반 */
link = bpf_program__attach_cgroup(
    skel->progs.my_cgroup, cgroup_fd);

/* LSM — 보안 모듈 hook */
/* SEC("lsm/bprm_check_security") → 자동 attach */

/* struct_ops — 커널 ops 테이블 교체 */
link = bpf_map__attach_struct_ops(skel->maps.my_tcp_cc);

Perf Buffer vs Ring Buffer

특성Perf BufferRing Buffer
메모리 모델CPU별 독립 버퍼전체 공유 버퍼
메모리 효율낮음 (CPU수 x 크기)높음 (단일 공유)
이벤트 순서CPU별 순서만 보장전역 순서 보장(Ordering)
가변 크기제한적네이티브 지원
APIbpf_perf_event_output()bpf_ringbuf_reserve/submit()
오버헤드데이터 복사 1회제로카피 가능 (reserve/submit)
커널 버전4.4+5.8+

XDP 드라이버 모드별 성능 비교

XDP는 세 가지 실행 모드를 제공하며, 각 모드는 패킷 처리 위치와 성능이 크게 다릅니다. 실무에서는 NIC 드라이버 지원 여부와 성능 요구사항에 따라 적절한 모드를 선택해야 합니다.

XDP 모드별 패킷 처리 위치 NIC 하드웨어 (SmartNIC/DPU) XDP Offload (xdpoffload) — BPF를 NIC 프로세서에서 실행, 호스트 CPU 0% ~40+ Mpps (와이어 레이트) NIC 드라이버 (NAPI poll 콜백) XDP Native (xdp) — sk_buff 할당 전에 xdp_buff로 직접 처리 ~24 Mpps (단일 코어) 네트워크 스택 진입점 (netif_receive_skb) XDP Generic (xdpgeneric) — sk_buff 할당 후 처리, 모든 NIC 지원 ~5 Mpps (개발/테스트용) 커널 네트워크 스택 (TC → Netfilter → Routing → Socket) XDP 없이 전체 스택 통과 — 기능 풍부하지만 지연 높음 패킷 흐름 (인그레스) 최고 성능 최저 성능 위로 갈수록 패킷이 일찍 처리되어 지연이 낮고 처리량이 높음 — 아래로 갈수록 기능이 풍부함
XDP 모드: 패킷 처리 위치가 높을수록 성능이 좋지만 기능이 제한됨

XDP Actions

Action동작사용 사례
XDP_PASS 2 패킷을 커널 스택으로 전달 정상 트래픽 통과
XDP_DROP 1 패킷을 즉시 폐기 (sk_buff 미생성) DDoS 방어, 블랙리스트
XDP_TX 3 수신한 NIC으로 패킷 반환 (hairpin) 로드밸런서, 패킷 리플렉터
XDP_REDIRECT 4 다른 NIC, CPU, 또는 AF_XDP로 전달 포워딩, AF_XDP, CPU 분산
XDP_ABORTED 0 오류 발생, 패킷 폐기 + trace 이벤트 비정상 종료 (디버깅용)

XDP_REDIRECT와 특수 맵

/* 1. DEVMAP: 다른 NIC으로 패킷 포워딩 */
struct {
    __uint(type, BPF_MAP_TYPE_DEVMAP);
    __uint(max_entries, 256);
    __type(key, __u32);
    __type(value, __u32);  /* ifindex */
} tx_port SEC(".maps");

SEC("xdp")
int xdp_redirect_prog(struct xdp_md *ctx) {
    /* 인덱스 0의 인터페이스로 리다이렉트 */
    return bpf_redirect_map(&tx_port, 0, 0);
}

/* 2. CPUMAP: 특정 CPU로 패킷 분산 */
struct {
    __uint(type, BPF_MAP_TYPE_CPUMAP);
    __uint(max_entries, 64);   /* CPU 수 */
    __type(key, __u32);
    __type(value, __u32);       /* 큐 크기 */
} cpu_map SEC(".maps");

SEC("xdp")
int xdp_cpu_balance(struct xdp_md *ctx) {
    /* 해시 기반으로 CPU 선택 */
    __u32 cpu = compute_hash(ctx) % num_cpus;
    return bpf_redirect_map(&cpu_map, cpu, 0);
}

/* 3. XSKMAP: AF_XDP 소켓으로 전달 */
struct {
    __uint(type, BPF_MAP_TYPE_XSKMAP);
    __uint(max_entries, 64);
    __type(key, __u32);
    __type(value, __u32);  /* xsk fd */
} xsks_map SEC(".maps");

SEC("xdp")
int xdp_to_af_xdp(struct xdp_md *ctx) {
    return bpf_redirect_map(&xsks_map, ctx->rx_queue_index, XDP_PASS);
}

AF_XDP 소켓 아키텍처

AF_XDP는 커널 바이패스에 가까운 성능을 제공하면서도 커널의 XDP 프레임워크와 통합되는 고성능 소켓입니다. 4개의 링(Fill/RX/TX/Completion)과 UMEM 공유 메모리를 통해 사용자 공간에서 20+ Mpps의 제로카피 패킷 처리를 지원합니다.

참고: AF_XDP 소켓의 아키텍처, UMEM 설정, 제로카피 모드, libbpf xsk API 사용법 등 상세 내용은 AF_XDP 소켓 전용 페이지(Page)를 참고하세요.

BPF 프로그램 타입

BPF 프로그램은 타입에 따라 커널의 특정 hook point에 부착됩니다. 각 타입은 컨텍스트 구조체, 사용 가능한 헬퍼 함수, 반환값의 의미가 다릅니다.

프로그램 타입SEC() 이름컨텍스트부착 위치주요 용도
BPF_PROG_TYPE_XDP xdp xdp_md NIC 드라이버 고속 패킷 필터링/포워딩
BPF_PROG_TYPE_SCHED_CLS tc __sk_buff TC ingress/egress 패킷 분류, 수정, 폴리싱
BPF_PROG_TYPE_KPROBE kprobe/func pt_regs 커널 함수 진입/반환 동적 트레이싱
BPF_PROG_TYPE_TRACEPOINT tp/cat/name 트레이스포인트별 정적 트레이스포인트 안정적 커널 이벤트 관찰
BPF_PROG_TYPE_RAW_TRACEPOINT raw_tp/name bpf_raw_tracepoint_args 원시 트레이스포인트 오버헤드 최소화 트레이싱
BPF_PROG_TYPE_PERF_EVENT perf_event bpf_perf_event_data PMU 이벤트 CPU 프로파일링, 샘플링
BPF_PROG_TYPE_CGROUP_SKB cgroup_skb/ingress __sk_buff cgroup ingress/egress cgroup별 네트워크 정책
BPF_PROG_TYPE_SOCKET_FILTER socket __sk_buff 소켓 패킷 캡처 필터링
BPF_PROG_TYPE_STRUCT_OPS struct_ops/name ops별 커널 ops 테이블 TCP CC, 스케줄러 교체
BPF_PROG_TYPE_LSM lsm/hook hook별 LSM hook 동적 보안 정책
BPF_PROG_TYPE_TRACING fentry/func 함수 인자 직접 커널 함수 (BTF 기반) kprobe 대체 (오버헤드 낮음)
BPF_PROG_TYPE_SK_LOOKUP sk_lookup bpf_sk_lookup 소켓 검색 커스텀 소켓 바인딩
/* 주요 프로그램 타입 예시 */

/* fentry/fexit — kprobe보다 오버헤드가 낮은 BTF 기반 트레이싱 */
SEC("fentry/tcp_v4_connect")
int BPF_PROG(trace_tcp_connect, struct sock *sk,
            struct sockaddr *uaddr, int addr_len)
{
    /* BTF 기반: 인자를 직접 타입으로 받음 (pt_regs 불필요) */
    __u16 dport = sk->__sk_common.skc_dport;
    bpf_printk("connecting to port %d\n", ntohs(dport));
    return 0;
}

/* struct_ops — TCP 혼잡 제어 알고리즘 BPF로 구현 */
SEC("struct_ops/my_cong_init")
void BPF_PROG(my_cong_init, struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    tp->snd_cwnd = 10;  /* 초기 혼잡 윈도우 */
}

SEC(".struct_ops")
struct tcp_congestion_ops my_bpf_cc = {
    .init       = (void *)my_cong_init,
    .ssthresh   = (void *)my_ssthresh,
    .cong_avoid = (void *)my_cong_avoid,
    .name       = "bpf_my_cc",
};

/* LSM — 동적 보안 정책 */
SEC("lsm/bprm_check_security")
int BPF_PROG(restrict_exec, struct linux_binprm *bprm)
{
    /* 특정 바이너리의 실행을 차단 */
    const char *filename = BPF_CORE_READ(bprm, filename);
    /* 정책에 따라 -EPERM 반환으로 차단 */
    return 0;  /* 0=허용, 음수=차단 */
}

BPF 헬퍼 함수 상세

BPF 헬퍼 함수는 BPF 프로그램이 커널 기능에 접근하는 유일한 인터페이스입니다. 각 프로그램 타입마다 사용 가능한 헬퍼가 다르며, Verifier가 컴파일 시점에 이를 강제합니다.

맵 조작 헬퍼

/* 맵 조작 — 모든 프로그램 타입에서 사용 가능 */

/* 조회: 키에 해당하는 값의 포인터 반환 (NULL 가능) */
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key);

/* 갱신: flags로 동작 제어 */
long bpf_map_update_elem(struct bpf_map *map,
    const void *key, const void *value, __u64 flags);
/* flags: BPF_ANY(0)=삽입 또는 갱신
         BPF_NOEXIST(1)=없을 때만 삽입
         BPF_EXIST(2)=있을 때만 갱신 */

/* 삭제: 해시 맵 전용 (배열은 삭제 불가) */
long bpf_map_delete_elem(struct bpf_map *map, const void *key);

/* 원자적 갱신: per-CPU가 아닌 맵에서 카운터 증가 */
__u64 *val = bpf_map_lookup_elem(&counter_map, &key);
if (val)
    __sync_fetch_and_add(val, 1);  /* 원자적 증가 */

메모리 읽기와 트레이싱 헬퍼

/* 커널 메모리 안전 읽기 (kprobe, tracepoint, fentry) */
long bpf_probe_read_kernel(void *dst, __u32 size, const void *unsafe_ptr);
long bpf_probe_read_kernel_str(void *dst, __u32 size, const void *unsafe_ptr);

/* 사용자 공간 메모리 읽기 */
long bpf_probe_read_user(void *dst, __u32 size, const void *unsafe_ptr);
long bpf_probe_read_user_str(void *dst, __u32 size, const void *unsafe_ptr);

/* 프로세스 정보 */
__u64 bpf_get_current_pid_tgid(void);    /* 상위 32비트=tgid, 하위=pid */
__u64 bpf_get_current_uid_gid(void);    /* 상위 32비트=gid, 하위=uid */
long  bpf_get_current_comm(void *buf, __u32 size);
struct task_struct *bpf_get_current_task(void);

/* 타임스탬프 */
__u64 bpf_ktime_get_ns(void);            /* 부팅 후 나노초 */
__u64 bpf_ktime_get_boot_ns(void);       /* 슬립 포함 나노초 */

/* 디버그 출력 — /sys/kernel/debug/tracing/trace_pipe에서 확인 */
long bpf_trace_printk(const char *fmt, __u32 fmt_size, ...);
/* 최대 3개 인자, %d/%u/%x/%lld/%llu/%llx/%p/%s 지원 */
/* 프로덕션에서는 사용 금지 — 성능 오버헤드 큼 */

네트워크 헬퍼

/* XDP/TC 전용 네트워크 헬퍼 */

/* 패킷 리다이렉트 */
long bpf_redirect(__u32 ifindex, __u64 flags);
long bpf_redirect_map(struct bpf_map *map, __u32 key, __u64 flags);

/* 패킷 데이터 수정 (TC) */
long bpf_skb_store_bytes(struct __sk_buff *skb,
    __u32 offset, const void *from, __u32 len, __u64 flags);

/* L3/L4 체크섬 재계산 */
long bpf_l3_csum_replace(struct __sk_buff *skb,
    __u32 offset, __u64 from, __u64 to, __u64 flags);
long bpf_l4_csum_replace(struct __sk_buff *skb,
    __u32 offset, __u64 from, __u64 to, __u64 flags);

/* 패킷 크기 조정 */
long bpf_skb_change_head(struct __sk_buff *skb, __u32 len, __u64 flags);
long bpf_skb_change_tail(struct __sk_buff *skb, __u32 len, __u64 flags);

/* XDP 패킷 크기 조정 */
long bpf_xdp_adjust_head(struct xdp_md *xdp, int delta);
long bpf_xdp_adjust_tail(struct xdp_md *xdp, int delta);

/* FIB (라우팅 테이블) 조회 */
long bpf_fib_lookup(void *ctx, struct bpf_fib_lookup *params,
    int plen, __u32 flags);
/* XDP/TC에서 커널 라우팅 테이블을 직접 조회하여 L2 헤더 완성 */

BPF 개발 도구

BPF 프로그램 개발, 디버깅, 관리를 위한 도구 생태계는 빠르게 성장하고 있습니다. 기본 도구인 bpftool부터 고수준 프레임워크까지 용도에 따라 적절한 도구를 선택합니다.

bpftool

bpftool은 BPF 프로그램과 맵을 관리하는 공식 CLI 도구입니다. 커널 소스 트리의 tools/bpf/bpftool/에서 빌드됩니다.

# 프로그램 관리
bpftool prog list                          # 로드된 BPF 프로그램 목록
bpftool prog show id 42                    # 특정 프로그램 상세 정보
bpftool prog dump xlated id 42             # BPF 바이트코드 덤프
bpftool prog dump jited id 42              # JIT 네이티브 코드 덤프
bpftool prog dump xlated id 42 visual      # DOT 그래프 (graphviz용)

# 맵 관리
bpftool map list                            # BPF 맵 목록
bpftool map dump id 5                       # 맵 전체 내용 덤프
bpftool map lookup id 5 key 0x01 0x00 0x00 0x00  # 특정 키 조회
bpftool map update id 5 key 1 0 0 0 value 100 0 0 0  # 값 갱신

# BTF 관리
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
bpftool btf dump id 1                       # BTF 정보 덤프

# 스켈레톤 생성
bpftool gen skeleton prog.bpf.o > prog.skel.h

# 프로그램 pin/unpin (BPF 파일시스템)
bpftool prog pin id 42 /sys/fs/bpf/my_prog
bpftool prog load prog.o /sys/fs/bpf/my_prog type xdp

# 네트워크 관련
bpftool net list                            # XDP/TC에 부착된 프로그램 목록

# 기능 지원 확인
bpftool feature probe                       # 현재 커널의 BPF 기능 점검

bpftrace

bpftrace는 AWK와 유사한 고수준 트레이싱 언어로, 한 줄짜리 스크립트로 강력한 커널 분석이 가능합니다.

# bpftrace 원라이너 예시

# 시스템 콜 빈도 상위 10개
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'

# TCP 연결 추적
bpftrace -e 'kprobe:tcp_v4_connect { printf("%s -> %s\n", comm, str(arg0)); }'

# 프로세스별 read 지연 히스토그램
bpftrace -e '
kprobe:vfs_read { @start[tid] = nsecs; }
kretprobe:vfs_read /@start[tid]/ {
    @us[comm] = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}'

# XDP 드롭 패킷 수 모니터링
bpftrace -e 'tracepoint:xdp:xdp_bulk_tx { @drops = count(); }'

# CPU별 소프트 IRQ 처리 시간
bpftrace -e '
tracepoint:irq:softirq_entry { @start[cpu] = nsecs; }
tracepoint:irq:softirq_exit /@start[cpu]/ {
    @ns[cpu, args->vec] = hist(nsecs - @start[cpu]);
    delete(@start[cpu]);
}'

BPF 라이브러리 생태계

라이브러리언어특징주요 용도
libbpf C 공식 라이브러리, CO-RE 지원, 스켈레톤 API 커널 트레이싱, 네트워킹, 보안
cilium/ebpf Go 순수 Go 구현, CGo 없음, CO-RE 지원 Kubernetes 네트워킹 (Cilium)
libbpf-rs Rust libbpf C 바인딩, 안전한 래퍼 Rust 기반 시스템 도구
Aya Rust 순수 Rust 구현, libbpf 의존 없음 Rust 네이티브 BPF 개발
bcc Python/C++ 런타임 컴파일, 풍부한 예제 빠른 프로토타이핑, 교육
// cilium/ebpf (Go) 사용 예시
package main

import (
    "github.com/cilium/ebpf"
    "github.com/cilium/ebpf/link"
)

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go counter counter.bpf.c

func main() {
    objs := counterObjects{}
    if err := loadCounterObjects(&objs, nil); err != nil {
        log.Fatal(err)
    }
    defer objs.Close()

    // kprobe에 BPF 프로그램 부착
    kp, _ := link.Kprobe("tcp_v4_connect", objs.TraceTcpConnect, nil)
    defer kp.Close()

    // 맵에서 값 읽기
    var count uint64
    objs.CounterMap.Lookup(uint32(0), &count)
}
// Aya (Rust) 사용 예시
use aya::{Bpf, programs::Xdp, maps::HashMap};
use aya::programs::XdpFlags;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut bpf = Bpf::load_file("prog.o")?;

    // XDP 프로그램 로드 및 부착
    let program: &mut Xdp = bpf.program_mut("xdp_prog")?
        .try_into()?;
    program.load()?;
    program.attach("eth0", XdpFlags::default())?;

    // 맵 접근
    let blocklist: HashMap<_, u32, u32> =
        HashMap::try_from(bpf.map_mut("blocklist")?)?;
    blocklist.insert(167772161, 1, 0)?;  // 10.0.0.1 차단

    Ok(())
}
도구 선택 가이드:
  • 빠른 분석/디버깅 → bpftrace (한 줄 스크립트)
  • 프로덕션 C 프로그램 → libbpf + CO-RE (최고의 이식성과 성능)
  • Kubernetes/클라우드 → cilium/ebpf (Go, Cilium 생태계)
  • 시스템 도구 (Rust) → Aya (순수 Rust, 안전성)
  • 프로토타이핑/교육 → bcc (Python, 풍부한 예제)
  • BPF 관리/검사 → bpftool (공식 CLI)

BPF 검증기 — 상태 탐색과 Pruning

BPF Verifier는 프로그램의 모든 실행 경로를 추상 해석(abstract interpretation)으로 탐색합니다. 각 분기점에서 레지스터의 타입(type)값 범위(range)를 추적하며, 이미 검증된 상태와 동일한 상태에 도달하면 가지치기(pruning)로 중복 탐색을 제거합니다. 이 메커니즘이 없으면 명령어 100만 개 제한 내에서 실제 프로그램 대부분을 검증할 수 없습니다.

BPF Verifier 상태 탐색 트리와 Pruning 진입점 (insn 0) R1 > 0 (참) R1 <= 0 (거짓) 상태 S1 R1: SCALAR [1, MAX] 상태 S2 R1: SCALAR [MIN, 0] 맵 조회 성공 PTR_TO_MAP_VALUE 맵 조회 NULL PTR_TO_MAP_VALUE_OR_NULL XDP_DROP 반환 R0 = XDP_DROP Pruning: 상태 동등성 검사 이전에 검증된 prune point의 상태 S_old와 현재 상태 S_new를 비교하여 S_new ⊆ S_old이면 탐색 중단 범위 추적 (tnum + smin/smax/umin/umax) tnum: 비트 단위 known/unknown 마스크 smin/smax: 부호 있는 범위, umin/umax: 부호 없는 범위 무한 루프 방지 (bounded loops) 커널 5.3+: 루프 반복 횟수 정적 증명 필수 back-edge 탐지 → 루프 카운터 범위 검증
BPF Verifier가 분기점마다 레지스터 상태를 추적하고, pruning으로 중복 경로를 제거하는 과정

상태 추적 구조체

Verifier는 각 명령어 실행 후의 레지스터 상태를 struct bpf_reg_state로 추적합니다. 핵심 필드를 살펴보면:

struct bpf_reg_state {
    enum bpf_reg_type type;     /* PTR_TO_MAP_VALUE, SCALAR_VALUE 등 */
    struct tnum var_off;           /* 비트 단위 known/unknown 마스크 */
    s64 smin_value, smax_value;    /* 부호 있는 최솟값/최댓값 */
    u64 umin_value, umax_value;    /* 부호 없는 최솟값/최댓값 */
    s32 s32_min_value, s32_max_value;
    u32 u32_min_value, u32_max_value;
    s64 off;                       /* 포인터 오프셋 */
    u32 id;                        /* 조건부 NULL 검사 연결 ID */
};

/* 프레임 상태: 레지스터 + 스택 슬롯 */
struct bpf_func_state {
    struct bpf_reg_state regs[MAX_BPF_REG]; /* R0-R10 */
    struct bpf_stack_state *stack;
    int allocated_stack;
    int callsite;
};

Pruning 알고리즘

상태 가지치기(state pruning)는 Verifier 성능의 핵심입니다. 매 분기점(prune point)에서 이전에 검증된 상태 목록과 현재 상태를 비교합니다:

/* kernel/bpf/verifier.c — states_equal() 핵심 로직 */
static bool states_equal(struct bpf_verifier_env *env,
                         struct bpf_verifier_state *old,
                         struct bpf_verifier_state *cur)
{
    /* 모든 프레임의 모든 레지스터를 비교 */
    for (i = 0; i <= cur->curframe; i++) {
        for (j = 0; j < MAX_BPF_REG; j++) {
            /* cur 상태가 old 상태의 부분집합인지 확인 */
            if (!regsafe(env, &old_regs[j], &cur_regs[j], idmap))
                return false;
        }
    }
    return true;
}
실무 팁: Verifier 거부 시 bpftool prog load ... verbose로 상세 로그를 확인하세요. 특히 R0 invalid mem access 'scalar' 같은 메시지는 NULL 체크 누락을 의미하고, back-edge from insn N to insn M은 bounded loop 검증 실패를 나타냅니다.

Bounded Loop 검증

커널 5.3 이후 BPF 프로그램에서 제한된 루프가 허용됩니다. Verifier는 루프 카운터의 범위를 추적하여 반복 횟수가 유한함을 정적으로 증명합니다:

/* 커널이 허용하는 bounded loop 패턴 */
SEC("xdp")
int xdp_parse_headers(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    struct ethhdr *eth = data;

    /* Verifier가 허용: 루프 상한 명확 */
    #pragma unroll
    for (int i = 0; i < 10; i++) {
        if ((void *)(eth + 1) > data_end)
            break;
        /* VLAN 태그 파싱 */
        if (eth->h_proto == bpf_htons(ETH_P_8021Q))
            eth = (void *)eth + 4;
        else
            break;
    }
    return XDP_PASS;
}
Verifier 제한 항목설명
최대 명령어 수1,000,000비권한 프로그램은 4,096
최대 상태 per insn64pruning 비교 대상 상한
스택 크기512바이트프레임당 고정
중첩 호출 깊이8BPF-to-BPF + tail call 합산
bounded loop커널 5.3+루프 카운터 범위 정적 증명 필수
log_level=2 출력전체 경로디버깅 시 모든 상태 변화 출력

XDP Redirect — devmap, cpumap, AF_XDP

XDP 프로그램의 XDP_REDIRECT 반환값은 단순한 포워딩이 아닙니다. devmap으로 다른 NIC로 전달하거나, cpumap으로 특정 CPU에서 처리하거나, AF_XDP 소켓으로 제로카피 수신할 수 있습니다. 이 세 가지 redirect 경로를 이해하는 것이 XDP 기반 네트워킹의 핵심입니다.

XDP_REDIRECT 경로별 동작 NIC RX (패킷 수신) XDP 프로그램 실행 devmap cpumap xskmap DEVMAP / DEVMAP_HASH bpf_redirect_map() 다른 NIC으로 직접 전달 대상 NIC의 TX 큐로 bulk 전송 CPUMAP bpf_redirect_map() 특정 CPU로 패킷 분배 대상 CPU의 backlog 큐 → 커널 스택 XSKMAP (AF_XDP) bpf_redirect_map() 제로카피 유저스페이스 수신 UMEM 공유 메모리 → 유저 앱 devmap: 10-14 Mpps (로드밸런서) cpumap: RSS 보완 (멀티코어 분산) AF_XDP: 20+ Mpps (제로카피)
XDP_REDIRECT의 세 가지 맵 타입: devmap(NIC 간 전달), cpumap(CPU 분배), xskmap(AF_XDP 제로카피)

devmap을 이용한 패킷 포워딩

BPF_MAP_TYPE_DEVMAP은 XDP 프로그램이 패킷을 다른 네트워크 인터페이스로 직접 전달할 수 있게 합니다. L2 스위칭, 로드밸런서, hairpin 모드 구현에 핵심적으로 사용됩니다:

struct {
    __uint(type, BPF_MAP_TYPE_DEVMAP);
    __uint(max_entries, 256);
    __type(key, __u32);
    __type(value, __u32);     /* ifindex */
} tx_port SEC(".maps");

/* devmap_val 확장: egress 프로그램 연결 */
struct {
    __uint(type, BPF_MAP_TYPE_DEVMAP);
    __uint(max_entries, 256);
    __type(key, __u32);
    __type(value, struct bpf_devmap_val);
} tx_port_prog SEC(".maps");

SEC("xdp")
int xdp_redirect_port(struct xdp_md *ctx)
{
    __u32 key = 0;  /* 대상 포트 인덱스 */
    return bpf_redirect_map(&tx_port, key, 0);
}

/* devmap egress 프로그램: redirect 직전 패킷 수정 */
SEC("xdp/devmap")
int xdp_egress_rewrite(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    struct ethhdr *eth = data;
    /* MAC 주소 재작성 */
    __builtin_memcpy(eth->h_source, new_src_mac, ETH_ALEN);
    return XDP_PASS;
}

cpumap을 이용한 CPU 분배

RSS(Receive Side Scaling)가 특정 플로우를 하나의 CPU에 집중시키는 문제를 BPF_MAP_TYPE_CPUMAP으로 해결할 수 있습니다:

struct {
    __uint(type, BPF_MAP_TYPE_CPUMAP);
    __uint(max_entries, 64);   /* CPU 수 */
    __type(key, __u32);
    __type(value, __u32);     /* queue_size */
} cpu_map SEC(".maps");

SEC("xdp")
int xdp_cpu_distribute(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    struct iphdr *iph = data + sizeof(struct ethhdr);

    if ((void *)(iph + 1) > data_end)
        return XDP_PASS;

    /* 5-tuple 해시로 CPU 선택 */
    __u32 cpu = jhash_2words(iph->saddr, iph->daddr, 0) % 8;
    return bpf_redirect_map(&cpu_map, cpu, 0);
}

AF_XDP 제로카피 수신

AF_XDP 소켓은 BPF_MAP_TYPE_XSKMAP을 통해 XDP 프로그램에서 유저스페이스로 직접 패킷을 전달합니다. UMEM(공유 메모리)을 사용한 제로카피 모드에서 최대 성능을 달성합니다:

struct {
    __uint(type, BPF_MAP_TYPE_XSKMAP);
    __uint(max_entries, 64);
    __type(key, __u32);
    __type(value, __u32);
} xsks_map SEC(".maps");

SEC("xdp")
int xdp_sock_prog(struct xdp_md *ctx)
{
    __u32 index = ctx->rx_queue_index;
    /* 해당 큐의 AF_XDP 소켓으로 제로카피 전달 */
    if (bpf_map_lookup_elem(&xsks_map, &index))
        return bpf_redirect_map(&xsks_map, index, 0);
    return XDP_PASS;
}
제로카피 요구사항: AF_XDP 제로카피 모드는 NIC 드라이버의 명시적 지원이 필요합니다(i40e, mlx5, ice 등). 미지원 드라이버에서는 자동으로 카피 모드로 폴백되며, ethtool -i <dev>로 드라이버를 확인하세요.

BPF 맵 타입 비교와 선택 가이드

BPF 맵은 BPF 프로그램과 유저스페이스 간의 데이터 공유 채널이자, BPF 프로그램 간 상태 공유 수단입니다. 맵 타입 선택이 성능과 동시성 특성을 결정하므로, 각 타입의 특성을 정확히 이해해야 합니다.

BPF 맵 타입별 내부 구조와 사용 패턴 HASH / PERCPU_HASH 키-값 해시 테이블 O(1) lookup/update spin_lock per bucket ARRAY / PERCPU_ARRAY 고정 크기 인덱스 배열 O(1) 직접 인덱스 접근 RCU 보호, 삭제 불가 LRU_HASH 자동 축출 해시맵 연결 추적에 적합 per-CPU LRU 리스트 RINGBUF 단일 링 버퍼 (5.8+) 가변 크기 레코드 lock-free, epoll 지원 QUEUE / STACK FIFO / LIFO 구조 키 없는 순차 접근 PROG_ARRAY tail call 대상 배열 프로그램 체이닝 용도 DEVMAP XDP redirect 대상 NIC ifindex 매핑 STRUCT_OPS (5.6+) 커널 ops 구조체 대체 TCP congestion 등 맵 타입 선택 가이드 동적 키-값 저장? → HASH (범용), LRU_HASH (자동 축출), PERCPU_HASH (고성능) 인덱스 기반 접근? → ARRAY (설정/카운터), PERCPU_ARRAY (고빈도 카운터) 이벤트 스트림 전송? → RINGBUF (가변 크기, 순서 보장), PERF_EVENT_ARRAY (레거시) 프로그램 체이닝? → PROG_ARRAY (tail call), BPF_MAP_TYPE_HASH_OF_MAPS (중첩) XDP redirect? → DEVMAP (NIC), CPUMAP (CPU 분배), XSKMAP (AF_XDP) 소켓 매핑? → SOCKMAP/SOCKHASH (소켓 레벨 redirect, L7 프록시)
주요 BPF 맵 타입 구조와 용도별 선택 기준

맵 타입 상세 비교

맵 타입최대 엔트리동시성주요 용도
HASH임의임의설정 가능bucket lock연결 테이블, ACL
PERCPU_HASH임의CPU별 복사본설정 가능lock-free고빈도 카운터
ARRAYu32 인덱스임의고정RCU설정, 룩업 테이블
PERCPU_ARRAYu32 인덱스CPU별 복사본고정lock-free통계 카운터
LRU_HASH임의임의설정 가능per-CPU LRU연결 추적, 캐시
RINGBUF없음가변 크기버퍼 크기lock-free이벤트 스트림
QUEUE/STACK없음고정설정 가능spin_lock작업 큐
PROG_ARRAYu32 인덱스prog fd고정RCUtail call
DEVMAPu32 인덱스ifindex설정 가능RCUXDP 포워딩
CPUMAPu32 CPUqueue_sizeCPU 수ptr_ringCPU 분배
XSKMAPu32 큐xsk fd큐 수RCUAF_XDP

PERCPU 맵 패턴: 카운터 집계

PERCPU 맵은 각 CPU가 독립적인 값 복사본을 가지므로 lock 없이 고빈도 업데이트가 가능합니다. 단, 읽기 시 모든 CPU의 값을 집계해야 합니다:

/* BPF 측: PERCPU_ARRAY로 패킷/바이트 카운터 */
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 1);
    __type(key, __u32);
    __type(value, struct counters);
} stats SEC(".maps");

struct counters {
    __u64 packets;
    __u64 bytes;
};

SEC("xdp")
int xdp_count(struct xdp_md *ctx)
{
    __u32 key = 0;
    struct counters *cnt = bpf_map_lookup_elem(&stats, &key);
    if (cnt) {
        cnt->packets++;
        cnt->bytes += (ctx->data_end - ctx->data);
    }
    return XDP_PASS;
}
/* 유저스페이스: PERCPU 값 집계 */
int ncpus = libbpf_num_possible_cpus();
struct counters values[ncpus];
__u32 key = 0;

bpf_map_lookup_elem(map_fd, &key, values);

__u64 total_pkts = 0, total_bytes = 0;
for (int i = 0; i < ncpus; i++) {
    total_pkts += values[i].packets;
    total_bytes += values[i].bytes;
}

XDP 오프로드 — NIC 하드웨어 실행

XDP 오프로드(XDP_FLAGS_HW_MODE)는 BPF 프로그램을 NIC의 프로그래머블 하드웨어(SmartNIC)에서 직접 실행합니다. CPU를 전혀 사용하지 않으므로 최고의 패킷 처리 성능을 달성할 수 있지만, 지원되는 BPF 기능 집합이 제한적입니다.

XDP 실행 계층: Generic → Native → Offload Generic XDP net/core/dev.c sk_buff 할당 후 실행 성능: ~3 Mpps 모든 드라이버 지원 Native XDP 드라이버 ndo_bpf 콜백 xdp_buff 직접 접근 성능: ~24 Mpps i40e, mlx5, ice 등 Offload XDP SmartNIC 하드웨어 실행 CPU 사용 없음 성능: 와이어레이트 Netronome, Mellanox CX-6+ 빠름 최적 하드웨어 오프로드 지원 현황 Netronome Agilio (NFP) - 완전한 XDP 오프로드 지원 - BPF 맵 오프로드 (해시, 배열) - JIT → NFP 마이크로코드 변환 - 40/100 Gbps 와이어레이트 - 제한: 루프, 일부 헬퍼 미지원 - 드라이버: nfp (drivers/net/ethernet/netronome/) - bpftool prog load ... offload dev <nfp_if> NVIDIA/Mellanox ConnectX-6+ - Native XDP: mlx5_core 드라이버 - HW Steering + eSwitch 규칙 오프로드 - TC flower 오프로드 연동 - AF_XDP 제로카피 완벽 지원 - 25/50/100/200 Gbps - 드라이버: mlx5 (drivers/net/ethernet/mellanox/) - devlink dev eswitch set mode switchdev
XDP 실행 계층별 성능 차이와 주요 SmartNIC 오프로드 지원 현황

오프로드 설정 절차

/* XDP 오프로드 프로그램 로드 (ip 명령) */
// # ip link set dev eth0 xdpoffload obj xdp_prog.o sec xdp

/* bpftool로 오프로드 로드 */
// # bpftool prog load xdp_prog.o /sys/fs/bpf/xdp_offload \
//     type xdp dev eth0 offload

/* 오프로드 BPF 프로그램 제약사항 */
/*
 * 1. 지원 헬퍼 제한: bpf_map_lookup_elem, bpf_map_update_elem 등
 * 2. 루프 불가 (모든 경로가 정적으로 결정)
 * 3. 맵 크기 제한 (하드웨어 메모리에 의존)
 * 4. BPF-to-BPF 호출 미지원
 * 5. 스택 크기 추가 제약 (하드웨어별 상이)
 */
기능Generic XDPNative XDPOffload XDP
실행 위치netif_receive_skb()드라이버 NAPI pollNIC 하드웨어
데이터 접근sk_buff (복사)xdp_buff (제로카피)NIC 메모리
CPU 사용있음있음 (최소화)없음
성능 (64B)~3 Mpps~24 Mpps와이어레이트
BPF 맵전체전체hash, array만
헬퍼 함수전체전체제한적
bounded loop지원지원미지원
설정 플래그XDP_FLAGS_SKB_MODEXDP_FLAGS_DRV_MODEXDP_FLAGS_HW_MODE
오프로드 확인: ip link show dev eth0에서 xdpoffload 키워드가 표시되면 하드웨어에서 실행 중입니다. xdpdrv는 Native, xdpgeneric는 Generic 모드입니다.

테일 콜과 BPF-to-BPF 호출

BPF 프로그램의 로직이 복잡해지면 단일 프로그램의 명령어 제한(1M)과 스택 제한(512B)에 부딪힙니다. 테일 콜(tail call)BPF-to-BPF 호출(BPF subprogram)은 이 문제를 해결하는 두 가지 메커니즘입니다.

Tail Call vs BPF-to-BPF 호출 Tail Call (bpf_tail_call) prog_A tail prog_B tail prog_C - 스택 재사용 (리셋) - 호출자로 복귀 없음 - 최대 33회 체이닝 - PROG_ARRAY 맵 필요 BPF-to-BPF 호출 (subprogram) main_prog func_parse() func_action() 스택 누적 (프레임당 512B) / 호출자로 복귀 / 최대 8 depth 스택 깊이와 체이닝 제약 Tail Call 스택 prog_A 스택 (512B) → 리셋 prog_B 스택 (512B) → 리셋 prog_C 스택 (512B) → 총 512B 사용 스택 고정 512B (재사용) BPF-to-BPF 호출 스택 main_prog 프레임 (512B) func_parse 프레임 (512B) func_action 프레임 (512B) 스택 누적: 512B x depth (최대 4096B)
Tail Call은 스택을 재사용하며 최대 33회 체이닝, BPF-to-BPF 호출은 스택이 누적되며 최대 8 depth

테일 콜 구현 패턴

/* PROG_ARRAY 맵 정의 */
struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    __uint(max_entries, 8);
    __type(key, __u32);
    __type(value, __u32);
} jmp_table SEC(".maps");

/* 프로토콜별 파서를 분리하는 패턴 */
SEC("xdp")
int xdp_dispatcher(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    struct ethhdr *eth = data;

    if ((void *)(eth + 1) > data_end)
        return XDP_DROP;

    switch (bpf_ntohs(eth->h_proto)) {
    case ETH_P_IP:
        bpf_tail_call(ctx, &jmp_table, 0);  /* IPv4 파서로 */
        break;
    case ETH_P_IPV6:
        bpf_tail_call(ctx, &jmp_table, 1);  /* IPv6 파서로 */
        break;
    case ETH_P_ARP:
        bpf_tail_call(ctx, &jmp_table, 2);  /* ARP 처리기로 */
        break;
    }
    /* tail call 실패 시 폴백 */
    return XDP_PASS;
}

SEC("xdp/ipv4")
int xdp_parse_ipv4(struct xdp_md *ctx)
{
    /* IPv4 전용 처리 로직 */
    return XDP_PASS;
}

BPF-to-BPF 호출 (서브프로그램)

/* static 함수로 선언하면 BPF-to-BPF 호출로 컴파일 */
static __always_inline int
parse_eth_header(struct xdp_md *ctx, struct ethhdr **eth_out)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    struct ethhdr *eth = data;

    if ((void *)(eth + 1) > data_end)
        return -1;

    *eth_out = eth;
    return 0;
}

/* 인라인하지 않는 서브프로그램 (스택 프레임 별도) */
static __noinline int
process_packet(struct xdp_md *ctx, __u32 src_ip)
{
    __u32 *blocked = bpf_map_lookup_elem(&blocklist, &src_ip);
    if (blocked)
        return XDP_DROP;
    return XDP_PASS;
}

SEC("xdp")
int xdp_main(struct xdp_md *ctx)
{
    struct ethhdr *eth;
    if (parse_eth_header(ctx, &eth) < 0)
        return XDP_DROP;
    /* ... IPv4 파싱 후 process_packet 호출 ... */
    return process_packet(ctx, iph->saddr);
}
특성Tail CallBPF-to-BPF 호출
스택 동작리셋 (재사용)누적 (프레임 추가)
복귀불가 (단방향)호출자로 복귀
최대 깊이33회8 depth
맵 필요PROG_ARRAY불필요 (같은 ELF)
런타임 교체가능 (맵 업데이트)불가 (컴파일 시 결정)
상태 전달ctx만 전달인자 5개까지
Verifier 검증각 프로그램 독립합산 검증
혼합 사용가능: tail call + subprogram 조합 (총 깊이 제한 주의)
주의: 커널 5.10 이후 tail call과 BPF-to-BPF 호출을 동시에 사용할 수 있지만, Verifier가 합산 스택 깊이를 검증합니다. 복잡한 체이닝에서는 bpftool prog show로 각 프로그램의 스택 사용량을 확인하세요.

ftrace/bpftrace를 이용한 XDP 성능 분석

XDP 프로그램의 성능 문제를 진단할 때 ftrace tracepoint와 bpftrace는 가장 강력한 도구입니다. 패킷 드롭 원인, 처리 지연, CPU 병목을 실시간(Real-time)으로 관찰할 수 있습니다.

XDP 성능 분석 도구 체계 NIC RX XDP 프로그램 커널 스택 소켓 전달 유저 앱 Tracepoint 위치 xdp:xdp_exception XDP 프로그램 에러 시 발생 act: XDP_ABORTED 반환 값 ifindex, prog_id 포함 드롭 원인 디버깅 핵심 xdp:xdp_redirect redirect 성공/실패 추적 to_ifindex, map_id 포함 devmap/cpumap 경로 확인 err 필드로 실패 원인 파악 xdp:xdp_bulk_tx bulk 전송 완료 이벤트 sent, drops, err 필드 devmap 배치 전송 효율 TX 큐 병목 감지 bpftrace 실전 분석 도구 패킷 처리 지연 측정 kprobe:napi_gro_receive 진입 → kretprobe 반환 시간 측정 히스토그램 분포 출력 p99 지연 식별 드롭 위치 역추적 tracepoint:skb:kfree_skb → 드롭 이유(reason) 수집 → 스택 트레이스 출력 커널 내 정확한 드롭 지점 CPU 사용률 분포 softirq NET_RX 시간 측정 → CPU별 처리량 비교 → RSS 불균형 탐지 IRQ affinity 튜닝 근거
XDP 성능 분석: tracepoint 위치와 bpftrace 활용 패턴

XDP Tracepoint 활용

커널은 XDP 관련 tracepoint를 /sys/kernel/debug/tracing/events/xdp/에 제공합니다:

# XDP 예외(에러) 모니터링
# bpftrace -e 'tracepoint:xdp:xdp_exception { \
#   @drops[args->act] = count(); }'

# XDP redirect 성공/실패 추적
# bpftrace -e 'tracepoint:xdp:xdp_redirect_err { \
#   printf("redirect err: ifindex=%d map=%d err=%d\n", \
#          args->to_ifindex, args->map_id, args->err); }'

# XDP 액션별 패킷 수 집계 (1초 간격)
# bpftrace -e 'tracepoint:xdp:xdp_redirect, \
#              tracepoint:xdp:xdp_exception { \
#   @[probe] = count(); } \
#   interval:s:1 { print(@); clear(@); }'

bpftrace 성능 분석 스크립트

/* XDP 프로그램 실행 시간 히스토그램 (bpftrace 스크립트) */
// #!/usr/bin/env bpftrace
// kprobe:bpf_prog_run_xdp {
//     @start[tid] = nsecs;
// }
// kretprobe:bpf_prog_run_xdp /@start[tid]/ {
//     @ns = hist(nsecs - @start[tid]);
//     delete(@start[tid]);
// }
// interval:s:5 { print(@ns); clear(@ns); }
/* 패킷 드롭 역추적 (bpftrace 스크립트) */
// #!/usr/bin/env bpftrace
// tracepoint:skb:kfree_skb {
//     @reason[args->reason] = count();
//     if (args->reason == 2 /* SKB_DROP_REASON_NOT_SPECIFIED */) {
//         @stacks[kstack] = count();
//     }
// }
// interval:s:10 {
//     printf("\n--- Drop reasons ---\n");
//     print(@reason); clear(@reason);
//     printf("\n--- Drop stacks ---\n");
//     print(@stacks, 5); clear(@stacks);
// }

XDP 디버깅 체크리스트

증상진단 명령원인 및 해결
패킷이 전달 안 됨bpftool prog showXDP 프로그램이 로드되었는지, 올바른 인터페이스에 연결되었는지 확인
간헐적 드롭ethtool -S <dev> | grep xdpxdp_drop, xdp_tx_err 카운터 확인
높은 지연mpstat -P ALL 1softirq CPU 사용률 확인, RSS 불균형
redirect 실패xdp:xdp_redirect_err tracepoint대상 인터페이스 상태, devmap 설정 확인
Verifier 거부bpftool prog load -d상세 Verifier 로그로 원인 파악
메모리 부족cat /proc/meminfo | grep BPFBPF 맵 메모리 사용량, RLIMIT_MEMLOCK 확인
실무 팁: bpftool prog profile id <ID> duration 5 cycles instructions로 특정 BPF 프로그램의 CPU 사이클과 명령어 수를 프로파일링할 수 있습니다. IPC(Instructions Per Cycle)가 낮으면 캐시 미스를 의심하세요.

XDP 성능 비교와 튜닝 가이드

고성능 패킷 처리에서 XDP, iptables(Netfilter), DPDK는 각각 다른 계층에서 동작하며 성능 특성이 크게 다릅니다. 이 섹션에서는 실측 기반 벤치마크와 실전 튜닝 포인트를 정리합니다.

패킷 처리 솔루션 성능 비교 (단일 코어, 64B 패킷) 처리량 (Mpps) iptables ~2 Mpps nftables ~3 Mpps TC BPF ~6 Mpps XDP generic ~3 Mpps XDP native ~24 Mpps AF_XDP ~20 Mpps DPDK ~30 Mpps 0 5 10 15 20 30 Mpps XDP 성능 튜닝 포인트 하드웨어 튜닝 - RSS 큐 수 = CPU 코어 수 - IRQ affinity 1:1 매핑 - Ring buffer 크기 최적화 - ethtool -G -L -C 설정 BPF 프로그램 최적화 - 맵 접근 최소화 - PERCPU 맵 사용 - 분기 예측 친화적 구조 - batch API 활용 시스템 튜닝 - busy_poll 활성화 - NUMA 로컬 메모리 할당 - CPU isolation (isolcpus) - netdev_budget 조정
패킷 처리 솔루션별 성능 비교 (테스트 환경: Intel Xeon, 25GbE, 64B 패킷)

솔루션별 상세 비교

항목iptablesXDP (Native)AF_XDPDPDK
실행 계층Netfilter (L3/L4)드라이버 NAPI드라이버 + 유저유저스페이스 PMD
sk_buff 할당필수불필요 (xdp_buff)불필요 (UMEM)불필요 (mbuf)
64B drop PPS~2M~24M~20M~30M
64B fwd PPS~1.5M~14M~15M~25M
지연 (p99)~50us~5us~3us~2us
커널 통합완전완전부분 (소켓)없음 (바이패스)
안전성높음높음 (Verifier)높음낮음 (root)
배포 복잡도낮음중간중간높음
핫 업데이트규칙 추가/삭제프로그램 교체프로그램 교체프로세스 재시작(Reboot)
멀티테넌트네임스페이스(Namespace)네임스페이스네임스페이스SR-IOV 필요

XDP 성능 튜닝 가이드

## 1. NIC 하드웨어 설정
# RSS 큐 수를 CPU 수에 맞춤
# ethtool -L eth0 combined 16

# Ring buffer 크기 최대화
# ethtool -G eth0 rx 4096 tx 4096

# 인터럽트 coalescing 조정 (저지연 vs 처리량)
# ethtool -C eth0 rx-usecs 0 rx-frames 1    # 저지연
# ethtool -C eth0 rx-usecs 50 rx-frames 64   # 고처리량

## 2. IRQ affinity 설정
# CPU 코어별 1:1 매핑
# for i in $(seq 0 15); do
#   echo $i > /proc/irq/$(cat /sys/class/net/eth0/device/msi_irqs/ \
#     | sed -n "$((i+1))p")/smp_affinity_list
# done

## 3. CPU isolation (성능 크리티컬)
# GRUB: isolcpus=4-15 nohz_full=4-15 rcu_nocbs=4-15
# XDP 전용 CPU에서 다른 워크로드 배제

## 4. busy_poll 활성화 (AF_XDP와 함께)
# sysctl -w net.core.busy_poll=50
# sysctl -w net.core.busy_read=50

## 5. NUMA 로컬 메모리 확인
# numactl --cpubind=0 --membind=0 ./xdp_app

벤치마크 방법론

정확한 XDP 벤치마크를 위해 다음 조건을 통제해야 합니다:

항목권장 설정이유
패킷 크기64B (최소) + 1518B (MTU)PPS는 작은 패킷에서 병목, BPS는 큰 패킷에서 병목
트래픽 생성기T-Rex, MoonGen, pktgen하드웨어 기반 또는 DPDK 기반 생성기 사용
워밍업30초 이상JIT 컴파일, 캐시 프라이밍
측정 시간60초 이상안정적인 평균/p99 값 확보
CPU 주파수고정 (performance governor)cpufreq-set -g performance
NUMANIC과 같은 NUMA 노드크로스 NUMA 접근 시 30-40% 성능 저하
Turbo Boost비활성화일관된 결과를 위해
선택 기준 요약:
  • 단순 필터링/라우팅 → XDP Native (커널 통합, 안전성, 충분한 성능)
  • 유저스페이스 패킷 처리 → AF_XDP (제로카피 + 커널 통합)
  • 최대 성능 + 전용 NIC → DPDK (와이어레이트, 높은 복잡도 감수)
  • 기존 인프라 호환 → iptables/nftables (가장 쉬운 운영)

실전 예제: XDP 기반 DDoS 방어

XDP의 가장 대표적인 실전 활용은 DDoS 방어입니다. NIC 드라이버 레벨에서 악의적 패킷을 즉시 드롭하여 커널 네트워크 스택 진입 전에 처리를 완료합니다. iptables 대비 10배 이상의 PPS 처리가 가능합니다.

인터넷 (공격) NIC RX Queue XDP 프로그램 블랙리스트 맵 조회 Rate Limit 체크 XDP_DROP 악성 패킷 커널 네트워크 스택 정상 패킷 XDP_TX 반사 BPF Maps (제어 평면) blacklist_map (LPM_TRIE) — IP 대역 rate_map (PER_CPU_HASH) — 속도제한 stats_map (PERCPU_ARRAY) — 통계 allowlist_map (HASH) — 화이트리스트 사용자 공간 데몬 블랙리스트 업데이트 통계 수집 · 알림

XDP DDoS 방어 프로그램 코드

다음은 IP 블랙리스트 기반 DDoS 방어 XDP 프로그램의 핵심 구조입니다. LPM_TRIE 맵을 사용하여 CIDR 대역 단위 차단을 지원합니다.

// xdp_ddos_filter.bpf.c — XDP DDoS 방어 프로그램
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

#define ETH_P_IP   0x0800
#define MAX_ENTRIES 100000

struct lpm_key {
    __u32 prefixlen;
    __u32 addr;
};

/* IP 블랙리스트 (LPM_TRIE — CIDR 대역 지원) */
struct {
    __uint(type, BPF_MAP_TYPE_LPM_TRIE);
    __type(key, struct lpm_key);
    __type(value, __u64);          /* 차단 시작 시각 (ns) */
    __uint(max_entries, MAX_ENTRIES);
    __uint(map_flags, BPF_F_NO_PREALLOC);
} blacklist_map SEC(".maps");

/* per-CPU 패킷 카운터 */
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, 4);        /* 0:pass, 1:drop, 2:tx, 3:total */
} stats_map SEC(".maps");

/* per-CPU rate limiter */
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_HASH);
    __type(key, __u32);            /* source IP */
    __type(value, __u64);          /* 마지막 패킷 타임스탬프 */
    __uint(max_entries, MAX_ENTRIES);
} rate_map SEC(".maps");

static __always_inline void update_stats(__u32 idx)
{
    __u64 *cnt = bpf_map_lookup_elem(&stats_map, &idx);
    if (cnt)
        __sync_fetch_and_add(cnt, 1);
}

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

    /* 이더넷 헤더 범위 검사 */
    if ((void *)(eth + 1) > data_end)
        return XDP_DROP;

    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return XDP_PASS;   /* IPv4 외 트래픽은 통과 */

    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end)
        return XDP_DROP;

    /* 1단계: IP 블랙리스트 조회 (LPM) */
    struct lpm_key key = {
        .prefixlen = 32,
        .addr      = ip->saddr,
    };
    if (bpf_map_lookup_elem(&blacklist_map, &key)) {
        update_stats(1);  /* drop counter */
        return XDP_DROP;
    }

    /* 2단계: Rate Limiting (초당 패킷 제한) */
    __u64 now = bpf_ktime_get_ns();
    __u64 *last = bpf_map_lookup_elem(&rate_map, &ip->saddr);
    if (last && (now - *last) < 1000) {  /* 1μs 간격 미만 → 의심 */
        update_stats(1);
        return XDP_DROP;
    }
    bpf_map_update_elem(&rate_map, &ip->saddr, &now, BPF_ANY);

    /* 정상 패킷 → 커널 스택 전달 */
    update_stats(0);  /* pass counter */
    return XDP_PASS;
}

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

사용자 공간 관리 프로그램

// ddos_manager.c — 블랙리스트 관리 및 통계 수집
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include "xdp_ddos_filter.skel.h"

int main(int argc, char **argv)
{
    struct xdp_ddos_filter_bpf *skel;
    int err;

    skel = xdp_ddos_filter_bpf__open_and_load();
    if (!skel) {
        fprintf(stderr, "Failed to open and load BPF skeleton\n");
        return 1;
    }

    /* eth0에 XDP 프로그램 부착 */
    int ifindex = if_nametoindex("eth0");
    err = bpf_xdp_attach(ifindex, bpf_program__fd(skel->progs.xdp_ddos_filter),
                          XDP_FLAGS_DRV_MODE, NULL);
    if (err) {
        fprintf(stderr, "Failed to attach XDP: %d\n", err);
        goto cleanup;
    }

    /* 블랙리스트 추가: 192.168.1.0/24 */
    struct lpm_key key = { .prefixlen = 24 };
    inet_pton(AF_INET, "192.168.1.0", &key.addr);
    __u64 ts = time(NULL);
    bpf_map_update_elem(bpf_map__fd(skel->maps.blacklist_map),
                        &key, &ts, BPF_ANY);

    printf("XDP DDoS filter attached. Monitoring...\n");

    /* 통계 주기적 출력 */
    while (1) {
        sleep(1);
        __u32 key_stat;
        __u64 values[libbpf_num_possible_cpus()];
        for (key_stat = 0; key_stat < 4; key_stat++) {
            bpf_map_lookup_elem(bpf_map__fd(skel->maps.stats_map),
                                &key_stat, values);
            __u64 total = 0;
            for (int i = 0; i < libbpf_num_possible_cpus(); i++)
                total += values[i];
            printf("stat[%u] = %llu  ", key_stat, total);
        }
        printf("\n");
    }

cleanup:
    xdp_ddos_filter_bpf__destroy(skel);
    return err;
}
운영 팁: 프로덕션에서는 BPF_MAP_TYPE_LRU_PERCPU_HASH를 rate_map에 사용하여 메모리 사용량을 자동 관리합니다. 또한 bpf_map_lookup_and_delete_elem을 통해 통계를 원자적으로 읽고 초기화할 수 있습니다.

실전 예제: BPF 기반 L4 로드밸런서

XDP_REDIRECT와 BPF 맵을 결합하면 커널 공간(Kernel Space)에서 고성능 L4 로드밸런서를 구현할 수 있습니다. 이 방식은 Facebook의 Katran, Cilium의 로드밸런서에서 사용하는 패턴입니다.

클라이언트 XDP LB VIP: 10.0.0.1 Hash → Backend Consistent Hash Maglev 알고리즘 Backend A (10.1.0.1) weight: 3 Backend B (10.1.0.2) weight: 2 Backend C (10.1.0.3) weight: 1 XDP_REDIRECT BPF Maps vip_map (HASH) — VIP→서비스 backend_map (ARRAY) — 서버 목록 ct_map (LRU_HASH) — 연결추적 stats_map (PERCPU) — 통계 maglev_map (ARRAY) — 해시 테이블

L4 로드밸런서 핵심 코드

// xdp_lb.bpf.c — XDP L4 로드밸런서
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

struct vip_key {
    __u32 vip;
    __u16 port;
    __u8  proto;
    __u8  pad;
};

struct backend_info {
    __u32 addr;
    unsigned char mac[6];
    __u16 port;
    __u32 ifindex;      /* redirect 대상 인터페이스 */
};

struct conn_key {
    __u32 saddr;
    __u32 daddr;
    __u16 sport;
    __u16 dport;
    __u8  proto;
};

/* VIP → 서비스 ID 매핑 */
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, struct vip_key);
    __type(value, __u32);           /* service_id */
    __uint(max_entries, 256);
} vip_map SEC(".maps");

/* 백엔드 서버 배열 (Maglev 해시 결과 인덱스) */
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, __u32);
    __type(value, struct backend_info);
    __uint(max_entries, 65537);     /* Maglev table size (prime) */
} backend_map SEC(".maps");

/* 연결 추적 (기존 연결 유지) */
struct {
    __uint(type, BPF_MAP_TYPE_LRU_HASH);
    __type(key, struct conn_key);
    __type(value, __u32);           /* backend index */
    __uint(max_entries, 1000000);
} ct_map SEC(".maps");

/* devmap for redirect */
struct {
    __uint(type, BPF_MAP_TYPE_DEVMAP_HASH);
    __type(key, __u32);
    __type(value, struct bpf_devmap_val);
    __uint(max_entries, 256);
} tx_port SEC(".maps");

static __always_inline __u32 hash_tuple(struct conn_key *key)
{
    /* 간단한 해시 — 프로덕션에서는 jhash 사용 */
    return (key->saddr ^ key->daddr ^ key->sport ^ key->dport) % 65537;
}

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

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_DROP;
    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return XDP_PASS;

    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end)
        return XDP_DROP;

    /* TCP만 처리 (UDP LB도 동일 패턴) */
    if (ip->protocol != IPPROTO_TCP)
        return XDP_PASS;

    struct tcphdr *tcp = (void *)ip + (ip->ihl * 4);
    if ((void *)(tcp + 1) > data_end)
        return XDP_DROP;

    /* VIP 조회 */
    struct vip_key vk = {
        .vip   = ip->daddr,
        .port  = tcp->dest,
        .proto = ip->protocol,
    };
    __u32 *svc_id = bpf_map_lookup_elem(&vip_map, &vk);
    if (!svc_id)
        return XDP_PASS;  /* VIP 아님 → 정상 경로 */

    /* 연결 추적 조회 (기존 연결 유지) */
    struct conn_key ck = {
        .saddr = ip->saddr, .daddr = ip->daddr,
        .sport = tcp->source, .dport = tcp->dest,
        .proto = ip->protocol,
    };
    __u32 *existing = bpf_map_lookup_elem(&ct_map, &ck);
    __u32 backend_idx;

    if (existing) {
        backend_idx = *existing;
    } else {
        backend_idx = hash_tuple(&ck);
        bpf_map_update_elem(&ct_map, &ck, &backend_idx, BPF_ANY);
    }

    /* 백엔드 조회 */
    struct backend_info *be = bpf_map_lookup_elem(&backend_map, &backend_idx);
    if (!be)
        return XDP_DROP;

    /* IP 헤더 재작성 (DNAT) */
    ip->daddr = be->addr;
    ip->check = 0;  /* 체크섬은 offload 또는 incremental 업데이트 */

    /* MAC 헤더 재작성 */
    __builtin_memcpy(eth->h_dest, be->mac, 6);

    /* XDP_REDIRECT로 백엔드 인터페이스로 전달 */
    return bpf_redirect_map(&tx_port, be->ifindex, 0);
}

char LICENSE[] SEC("license") = "GPL";
Maglev 해싱: Google Maglev 논문의 consistent hashing은 백엔드 추가/제거 시 기존 연결의 99% 이상을 유지합니다. Cilium과 Katran은 이 알고리즘을 BPF_MAP_TYPE_ARRAY 기반으로 구현합니다.

실전 예제: TC BPF 트래픽 셰이핑

TC(Traffic Control) BPF를 사용하면 XDP로는 처리하기 어려운 egress 방향 트래픽 제어(Traffic Control)와 세밀한 폴리싱(policing)이 가능합니다. TC BPF는 sk_buff 기반이므로 L4 이상의 정보에 쉽게 접근할 수 있습니다.

// tc_police.bpf.c — TC BPF 기반 트래픽 폴리서
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

#define TC_ACT_OK       0
#define TC_ACT_SHOT     2

struct rate_info {
    __u64 tokens;           /* 현재 토큰 수 */
    __u64 last_update;      /* 마지막 업데이트 시각 (ns) */
};

/* 클래스별 토큰 버킷 */
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, __u32);             /* 분류 키 (IP 해시) */
    __type(value, struct rate_info);
    __uint(max_entries, 65536);
} token_bucket SEC(".maps");

/* 설정: 최대 속도 (bytes/sec) */
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, 1);         /* [0] = rate_limit (bytes/ns) */
} config_map SEC(".maps");

SEC("tc")
int tc_rate_limiter(struct __sk_buff *skb)
{
    /* 설정값 조회 */
    __u32 cfg_key = 0;
    __u64 *rate = bpf_map_lookup_elem(&config_map, &cfg_key);
    if (!rate || *rate == 0)
        return TC_ACT_OK;

    /* 분류: 소스 IP 기반 */
    void *data     = (void *)(long)skb->data;
    void *data_end = (void *)(long)skb->data_end;
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return TC_ACT_OK;

    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return TC_ACT_OK;

    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end)
        return TC_ACT_OK;

    __u32 key = ip->saddr;

    /* Token Bucket Algorithm */
    __u64 now = bpf_ktime_get_ns();
    struct rate_info *ri = bpf_map_lookup_elem(&token_bucket, &key);
    struct rate_info new_ri = {};

    if (ri) {
        __u64 elapsed = now - ri->last_update;
        __u64 new_tokens = ri->tokens + elapsed * (*rate);
        __u64 burst = (*rate) * 1000000;  /* 1ms burst */
        if (new_tokens > burst)
            new_tokens = burst;

        if (new_tokens < skb->len) {
            /* 토큰 부족 → 드롭 */
            return TC_ACT_SHOT;
        }
        new_ri.tokens = new_tokens - skb->len;
    } else {
        new_ri.tokens = 0;
    }
    new_ri.last_update = now;
    bpf_map_update_elem(&token_bucket, &key, &new_ri, BPF_ANY);

    return TC_ACT_OK;
}

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

TC BPF 프로그램 부착 명령

# TC BPF 프로그램 부착 (egress 방향)
tc qdisc add dev eth0 clsact
tc filter add dev eth0 egress bpf direct-action obj tc_police.bpf.o sec tc

# 상태 확인
tc filter show dev eth0 egress

# ingress 방향도 가능
tc filter add dev eth0 ingress bpf direct-action obj tc_police.bpf.o sec tc

# 분리
tc filter del dev eth0 egress
tc qdisc del dev eth0 clsact
XDP vs TC BPF 선택:
  • XDP — ingress 전용, 최고 성능, xdp_buff 기반 (sk_buff 없음)
  • TC BPF — ingress + egress 모두 가능, sk_buff 기반, L4+ 정보 접근 용이
  • 두 가지를 조합하는 것이 일반적: XDP에서 1차 필터링 → TC에서 세밀한 정책 적용

실전 예제: eBPF 기반 네트워크 모니터링

소켓 필터와 tracepoint BPF 프로그램을 활용하면 시스템의 네트워크 동작을 실시간으로 관측할 수 있습니다. tcpdump와 달리 커널 내부에서 집계하므로 오버헤드가 극히 낮습니다.

// net_monitor.bpf.c — 연결별 트래픽 모니터링
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>

struct conn_info {
    __u64 bytes_sent;
    __u64 bytes_recv;
    __u64 pkts_sent;
    __u64 pkts_recv;
    __u64 retransmits;
    __u32 srtt_us;          /* smoothed RTT (μs) */
    __u32 last_cwnd;        /* congestion window */
};

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, __u64);             /* socket cookie */
    __type(value, struct conn_info);
    __uint(max_entries, 65536);
} conn_stats SEC(".maps");

/* Ring buffer for events */
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");

struct event {
    __u32 pid;
    __u32 saddr;
    __u32 daddr;
    __u16 sport;
    __u16 dport;
    __u64 bytes;
    __u32 srtt_us;
};

SEC("tracepoint/tcp/tcp_probe")
int trace_tcp_probe(struct trace_event_raw_tcp_probe *ctx)
{
    __u64 cookie = BPF_CORE_READ(ctx, sock_cookie);
    struct conn_info *ci = bpf_map_lookup_elem(&conn_stats, &cookie);

    if (!ci) {
        struct conn_info new_ci = {};
        bpf_map_update_elem(&conn_stats, &cookie, &new_ci, BPF_NOEXIST);
        ci = bpf_map_lookup_elem(&conn_stats, &cookie);
        if (!ci) return 0;
    }

    ci->srtt_us   = BPF_CORE_READ(ctx, srtt_us);
    ci->last_cwnd = BPF_CORE_READ(ctx, snd_cwnd);
    ci->bytes_sent += BPF_CORE_READ(ctx, data_len);
    ci->pkts_sent++;

    return 0;
}

SEC("tracepoint/tcp/tcp_retransmit_skb")
int trace_tcp_retransmit(struct trace_event_raw_tcp_retransmit_skb *ctx)
{
    __u64 cookie = BPF_CORE_READ(ctx, sock_cookie);
    struct conn_info *ci = bpf_map_lookup_elem(&conn_stats, &cookie);
    if (ci)
        __sync_fetch_and_add(&ci->retransmits, 1);

    /* 재전송 이벤트 ring buffer 전달 */
    struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (e) {
        e->pid   = bpf_get_current_pid_tgid() >> 32;
        e->saddr = BPF_CORE_READ(ctx, saddr);
        e->daddr = BPF_CORE_READ(ctx, daddr);
        e->sport = BPF_CORE_READ(ctx, sport);
        e->dport = BPF_CORE_READ(ctx, dport);
        bpf_ringbuf_submit(e, 0);
    }
    return 0;
}

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

bpftrace를 이용한 빠른 모니터링

# TCP 연결별 재전송 카운트 (상위 10)
bpftrace -e 'tracepoint:tcp:tcp_retransmit_skb {
    @retrans[ntop(args->saddr), ntop(args->daddr), args->sport, args->dport] = count();
}
interval:s:5 { print(@retrans, 10); clear(@retrans); }'

# XDP 프로그램 처리 시간 분포 (ns)
bpftrace -e 'kprobe:bpf_prog_run_xdp { @start[tid] = nsecs; }
kretprobe:bpf_prog_run_xdp /@start[tid]/ {
    @latency_ns = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}'

# 소켓별 송수신 바이트 실시간 추적
bpftrace -e 'tracepoint:sock:sock_send_length {
    @send_bytes[comm, args->length] = count();
}
tracepoint:sock:sock_recv_length {
    @recv_bytes[comm, args->length] = count();
}'

흔한 실수와 안티패턴

BPF/XDP 개발에서 자주 발생하는 실수와 안티패턴을 정리합니다. 이 패턴들을 미리 파악하면 디버깅 시간을 크게 줄일 수 있습니다.

BPF 프로그램 제출 1. CFG(제어 흐름 그래프) 구성 2. DAG 검사 (루프 탐지) 에러: unbounded loop 루프 발견 3. 추상 해석 (레지스터 상태 추적) 에러: invalid mem access 4. 스택 깊이 검사 (512B 제한) 에러: stack overflow 5. 복잡도 검사 (1M insn 제한) 에러: complexity limit 검증 통과 → JIT Pruning 최적화 동일 레지스터 상태 → 중복 탐색 생략 5.x: log2 pruning 6.x: precision tracking 상태 수 ∝ 명령어 복잡도 bounded loop: 5.3+ open-coded iter: 6.4+ bpf_loop(): 5.17+ max complexity: 1,000,000 insns

Verifier 에러 패턴

에러 메시지원인해결 방법
R1 invalid mem access 'scalar' 포인터 범위 검사 누락 데이터 접근 전 반드시 if (ptr + size > data_end) 검사
back-edge from insn X to Y 무한 루프 감지 bpf_loop()(5.17+) 또는 bpf_for()(6.4+) 사용
combined stack size exceeds 512 bytes 지역 변수 + BPF-to-BPF 호출 스택 초과 큰 구조체는 BPF_MAP_TYPE_PERCPU_ARRAY에 저장
program is too large (X insns) 명령어 수 1M 초과 또는 탐색 복잡도 초과 tail call로 분할, 불필요한 분기 제거
R0 !read_ok 맵 lookup 후 NULL 체크 누락 bpf_map_lookup_elem() 반환값 반드시 NULL 검사
unreachable insn 도달 불가능한 코드 존재 dead code 제거 또는 컴파일러 최적화(Compiler Optimization) 확인
tail_call abusing MAX_TAIL_CALL_CNT tail call 깊이 33회 초과 체인 구조 재설계, 불필요한 tail call 제거

Map 크기 설계 실수

/* 안티패턴: 단일 CPU에서 전역 해시 맵으로 고 PPS 카운터 */
struct {
    __uint(type, BPF_MAP_TYPE_HASH);      /* ✗ 잠금 경합 발생 */
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, 1000000);
} bad_counter SEC(".maps");

/* 올바른 패턴: per-CPU 맵 사용 */
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_HASH);  /* ✓ CPU별 독립 */
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, 1000000);
} good_counter SEC(".maps");

/* 안티패턴: 고정 크기 해시 맵으로 동적 엔트리 관리 */
struct {
    __uint(type, BPF_MAP_TYPE_HASH);      /* ✗ 가득 차면 새 엔트리 삽입 실패 */
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, 1024);
} bad_flow_table SEC(".maps");

/* 올바른 패턴: LRU 해시로 자동 퇴거 */
struct {
    __uint(type, BPF_MAP_TYPE_LRU_HASH);  /* ✓ 오래된 엔트리 자동 제거 */
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, 1024);
} good_flow_table SEC(".maps");

XDP MTU/프래그먼트 처리 누락

주의: XDP 프로그램에서 가장 흔한 실전 버그는 점보 프레임과 멀티버퍼 패킷 처리 누락입니다. MTU가 1500을 초과하거나 GRO/LRO가 활성화된 환경에서는 반드시 xdp_buff의 프래그먼트를 확인해야 합니다.
/* 안티패턴: 단일 선형 버퍼만 가정 */
SEC("xdp")
int bad_xdp(struct xdp_md *ctx)
{
    void *data     = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    /* ✗ 멀티버퍼 패킷이면 data~data_end가 전체가 아님 */
    int pkt_len = data_end - data;  /* 선형 부분만 */
    ...
}

/* 올바른 패턴: 멀티버퍼 인식 (6.x+) */
SEC("xdp.frags")
int good_xdp(struct xdp_md *ctx)
{
    void *data     = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    /* 선형 부분 + 프래그먼트 = 전체 패킷 */
    int linear_len = data_end - data;
    /* bpf_xdp_get_buff_len()으로 전체 길이 확인 (6.5+) */
    __u64 total_len = bpf_xdp_get_buff_len(ctx);

    /* 프래그먼트 접근은 bpf_xdp_load_bytes() 사용 */
    char buf[64];
    if (bpf_xdp_load_bytes(ctx, linear_len, buf, sizeof(buf)) < 0)
        return XDP_PASS;  /* 프래그먼트 없으면 정상 처리 */
    ...
}

CO-RE 미사용으로 인한 호환성 문제

/* 안티패턴: 커널 헤더 직접 포함 (빌드 환경 커널에 종속) */
#include <linux/sched.h>

SEC("tp_btf/sched_switch")
int bad_trace(u64 *ctx)
{
    struct task_struct *prev = (void *)ctx[0];
    /* ✗ task_struct 오프셋이 커널 버전마다 다름 */
    int pid = prev->pid;
    ...
}

/* 올바른 패턴: CO-RE + vmlinux.h + BPF_CORE_READ */
#include "vmlinux.h"
#include <bpf/bpf_core_read.h>

SEC("tp_btf/sched_switch")
int good_trace(u64 *ctx)
{
    struct task_struct *prev = (void *)ctx[0];
    /* ✓ BTF 기반으로 런타임에 오프셋 재배치 */
    int pid = BPF_CORE_READ(prev, pid);
    ...
}

/* CO-RE 필드 존재 확인 (조건부 접근) */
SEC("tp_btf/sched_switch")
int safe_trace(u64 *ctx)
{
    struct task_struct *prev = (void *)ctx[0];

    /* 필드가 존재하는 커널에서만 접근 */
    if (bpf_core_field_exists(prev->se.cfs_rq)) {
        void *cfs_rq = BPF_CORE_READ(prev, se.cfs_rq);
        /* ... */
    }
    ...
}
CO-RE 체크리스트:
  • vmlinux.h 사용 (bpftool btf dump file /sys/kernel/btf/vmlinux format c로 생성)
  • BPF_CORE_READ() / BPF_CORE_READ_STR_INTO() 매크로 사용
  • bpf_core_field_exists()로 선택적 필드 접근
  • bpf_core_type_exists()로 타입 호환성 확인
  • 커널 5.5+ BTF 지원 필수 (CONFIG_DEBUG_INFO_BTF=y)

실습 가이드: XDP 프로그램 작성부터 테스트까지

이 실습에서는 XDP 프로그램을 처음부터 작성, 컴파일, 로드, 테스트하는 전 과정을 단계별로 안내합니다.

1단계: 개발 환경 준비

# 필수 패키지 설치 (Ubuntu/Debian)
sudo apt install -y clang llvm libbpf-dev linux-tools-common \
    linux-tools-$(uname -r) bpftool gcc-multilib

# Fedora/RHEL
sudo dnf install -y clang llvm libbpf-devel bpftool kernel-devel

# BTF 지원 확인
ls /sys/kernel/btf/vmlinux
# 파일이 있으면 BTF 활성 (CO-RE 사용 가능)

# vmlinux.h 생성
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

# 프로젝트 구조
mkdir -p xdp-lab && cd xdp-lab
# xdp-lab/
# ├── vmlinux.h
# ├── xdp_counter.bpf.c    (BPF 프로그램)
# ├── xdp_counter.c         (사용자 공간 로더)
# └── Makefile

2단계: XDP 프로그램 작성

// xdp_counter.bpf.c — 프로토콜별 패킷 카운터
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

#define ETH_P_IP    0x0800
#define ETH_P_IPV6  0x86DD
#define ETH_P_ARP   0x0806

enum proto_idx {
    PROTO_IPV4 = 0,
    PROTO_IPV6 = 1,
    PROTO_ARP  = 2,
    PROTO_OTHER = 3,
    PROTO_MAX  = 4,
};

struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, PROTO_MAX);
} pkt_count SEC(".maps");

static __always_inline void count_packet(__u32 idx)
{
    __u64 *cnt = bpf_map_lookup_elem(&pkt_count, &idx);
    if (cnt)
        *cnt += 1;
}

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

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    switch (bpf_ntohs(eth->h_proto)) {
    case ETH_P_IP:
        count_packet(PROTO_IPV4);
        break;
    case ETH_P_IPV6:
        count_packet(PROTO_IPV6);
        break;
    case ETH_P_ARP:
        count_packet(PROTO_ARP);
        break;
    default:
        count_packet(PROTO_OTHER);
        break;
    }

    return XDP_PASS;  /* 모든 패킷 통과 (관측만) */
}

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

3단계: 컴파일과 스켈레톤 생성

# BPF 프로그램 컴파일
clang -O2 -g -target bpf -D__TARGET_ARCH_x86 \
    -c xdp_counter.bpf.c -o xdp_counter.bpf.o

# BPF 스켈레톤 헤더 생성
bpftool gen skeleton xdp_counter.bpf.o > xdp_counter.skel.h

# 사용자 공간 프로그램 컴파일
gcc -O2 -Wall -o xdp_counter xdp_counter.c -lbpf -lelf -lz

4단계: 로드 및 테스트

# 방법 1: ip 명령으로 직접 부착
sudo ip link set dev lo xdpgeneric obj xdp_counter.bpf.o sec xdp

# 프로그램 확인
sudo bpftool prog list
# ID  Type  Name          Attach
# 42  xdp   xdp_counter   lo

# 맵 내용 확인 (per-CPU 값 합산)
sudo bpftool map dump name pkt_count

# 트래픽 생성 (테스트)
ping -c 100 127.0.0.1

# 통계 확인
sudo bpftool map dump name pkt_count
# key: 00 00 00 00  value (per-CPU):
#   cpu00: 200 ...

# 프로그램 분리
sudo ip link set dev lo xdpgeneric off

# 방법 2: xdp-loader 사용 (권장)
sudo xdp-loader load -m skb lo xdp_counter.bpf.o
sudo xdp-loader status
sudo xdp-loader unload lo --all

5단계: veth 쌍을 이용한 격리(Isolation) 테스트

# 네트워크 네임스페이스로 안전한 테스트 환경 구성
sudo ip netns add test_ns
sudo ip link add veth0 type veth peer name veth1
sudo ip link set veth1 netns test_ns

sudo ip addr add 10.0.0.1/24 dev veth0
sudo ip link set veth0 up

sudo ip netns exec test_ns ip addr add 10.0.0.2/24 dev veth1
sudo ip netns exec test_ns ip link set veth1 up
sudo ip netns exec test_ns ip link set lo up

# XDP 부착 (veth에는 generic 모드)
sudo ip link set dev veth0 xdpgeneric obj xdp_counter.bpf.o sec xdp

# 다른 네임스페이스에서 트래픽 생성
sudo ip netns exec test_ns ping -c 50 10.0.0.1

# 결과 확인
sudo bpftool map dump name pkt_count

# 정리
sudo ip link set dev veth0 xdpgeneric off
sudo ip link del veth0
sudo ip netns del test_ns

실습 가이드: bpftool/bpftrace 디버깅

BPF 프로그램의 디버깅은 일반 프로그램과 다릅니다. 커널 내 실행이므로 GDB를 직접 사용할 수 없지만 bpftool, bpftrace, tracepoint을 활용하면 효과적으로 디버깅할 수 있습니다.

bpftool 디버깅 기법

# 로드된 BPF 프로그램 목록 (상세)
sudo bpftool prog list -j | python3 -m json.tool

# 프로그램 상세 정보 (JIT, 맵 바인딩)
sudo bpftool prog show id 42 --pretty

# BPF 바이트코드 덤프 (verifier가 본 명령어)
sudo bpftool prog dump xlated id 42

# JIT 어셈블리 덤프 (실제 실행 코드)
sudo bpftool prog dump jited id 42

# 프로그램 실행 통계 (활성화 필요)
sudo sysctl kernel.bpf_stats_enabled=1
sudo bpftool prog show id 42
# run_cnt: 15234  run_time_ns: 892340
# → 평균 실행 시간: 892340/15234 ≈ 58.6ns

# 맵 조회/수정
sudo bpftool map show
sudo bpftool map dump id 5
sudo bpftool map update id 5 key 0x01 0x00 0x00 0x00 value 0xFF 0x00 0x00 0x00

# BTF 정보 확인
sudo bpftool btf show
sudo bpftool btf dump id 1 format c | head -50

# 프로그램 Pin (파일시스템에 고정)
sudo bpftool prog pin id 42 /sys/fs/bpf/my_xdp_prog
ls -la /sys/fs/bpf/my_xdp_prog

bpftrace 활용 디버깅

# XDP 프로그램 호출 빈도 추적
sudo bpftrace -e 'tracepoint:xdp:xdp_bulk_tx { @bulk[args->action] = count(); }'

# XDP 액션별 카운트
sudo bpftrace -e '
tracepoint:xdp:xdp_redirect { @redirect = count(); }
tracepoint:xdp:xdp_redirect_err { @redirect_err = count(); }
tracepoint:xdp:xdp_exception { @exception[args->act] = count(); }
'

# BPF 프로그램 로드/언로드 이벤트
sudo bpftrace -e '
tracepoint:bpf:bpf_prog_load {
    printf("LOAD prog_id=%d type=%d name=%s\n",
           args->prog_id, args->prog_type, str(args->prog_name));
}
tracepoint:bpf:bpf_prog_free {
    printf("FREE prog_id=%d\n", args->prog_id);
}'

# verifier 로그 확인 (로드 실패 시)
# libbpf 로그 레벨 설정
export LIBBPF_LOG_LEVEL=debug

# 또는 코드에서:
# struct bpf_object_open_opts opts = { .sz = sizeof(opts) };
# libbpf_set_print(my_log_fn);

# 맵 연산 추적
sudo bpftrace -e '
kprobe:htab_map_update_elem {
    @update_map = count();
}
kprobe:htab_map_lookup_elem {
    @lookup_map = count();
}
interval:s:1 { print(@update_map); print(@lookup_map);
    clear(@update_map); clear(@lookup_map); }'
디버깅 체크리스트:
  • bpftool prog show — 프로그램 로드 상태, JIT 여부 확인
  • bpf_stats_enabled=1 — 실행 횟수/시간 프로파일링
  • bpf_printk() — 간단한 디버그 출력 (cat /sys/kernel/debug/tracing/trace_pipe)
  • BPF_MAP_TYPE_RINGBUF — 구조화된 이벤트 전달
  • Verifier 로그: bpf_object__load() 실패 시 stderr 확인

BPF 성능 최적화

BPF 프로그램의 성능을 극대화하기 위한 JIT 최적화, batch 연산, 효율적 맵 선택 등의 기법을 다룹니다.

JIT 컴파일 최적화

# JIT 활성화 확인 및 설정
cat /proc/sys/net/core/bpf_jit_enable
# 0: 비활성, 1: 활성, 2: 디버그 모드 (프로덕션에서 사용 금지)
sudo sysctl net.core.bpf_jit_enable=1

# JIT kallsyms 활성화 (perf에서 BPF 함수명 표시)
sudo sysctl net.core.bpf_jit_kallsyms=1

# JIT 하드닝 (보안 강화, 약간의 성능 비용)
cat /proc/sys/net/core/bpf_jit_harden
# 0: 비활성, 1: unprivileged만, 2: 모든 프로그램
sudo sysctl net.core.bpf_jit_harden=1

Batch 연산

/* 맵 Batch 연산 (5.6+) — 대량 읽기/쓰기 성능 향상 */
#include <bpf/bpf.h>

/* 일반 방식: O(n) 시스템 콜 */
for (int i = 0; i < 10000; i++)
    bpf_map_lookup_elem(fd, &keys[i], &values[i]);  /* 10,000번 syscall */

/* Batch 방식: O(1) 시스템 콜 */
__u32 count = 10000;
DECLARE_LIBBPF_OPTS(bpf_map_batch_opts, opts, .elem_flags = 0, .flags = 0);

/* 한 번의 syscall로 10,000개 항목 조회 */
bpf_map_lookup_batch(fd, NULL, &next_key,
                     keys, values, &count, &opts);

/* Batch 업데이트 */
bpf_map_update_batch(fd, keys, values, &count, &opts);

/* Batch 삭제 */
bpf_map_delete_batch(fd, keys, &count, &opts);

/* Lookup-and-delete Batch (통계 수집에 이상적) */
bpf_map_lookup_and_delete_batch(fd, NULL, &next_key,
                                keys, values, &count, &opts);

Bloom Filter Map (5.16+)

/* BPF_MAP_TYPE_BLOOM_FILTER — 빠른 존재 확인 (false positive 허용) */
struct {
    __uint(type, BPF_MAP_TYPE_BLOOM_FILTER);
    __type(value, __u32);
    __uint(max_entries, 1000000);
    __uint(map_extra, 3);       /* hash 함수 개수 (2-5 권장) */
} bloom SEC(".maps");

SEC("xdp")
int xdp_with_bloom(struct xdp_md *ctx)
{
    /* ... 패킷 파싱 ... */
    __u32 saddr = ip->saddr;

    /* 1단계: Bloom Filter로 빠른 검사 (O(k), k=hash 함수 수) */
    if (bpf_map_peek_elem(&bloom, &saddr) != 0)
        return XDP_PASS;  /* 확실히 블랙리스트 아님 */

    /* 2단계: 정확한 해시 맵 조회 (Bloom 양성일 때만) */
    if (bpf_map_lookup_elem(&blacklist, &saddr))
        return XDP_DROP;

    return XDP_PASS;
}
/* → Bloom Filter가 대부분의 정상 트래픽을 O(1)로 통과시켜 성능 향상 */
성능 최적화 우선순위(Priority):
  1. 맵 타입 선택 — per-CPU, LRU, Bloom Filter로 경합(Contention) 최소화
  2. JIT 활성화 — 인터프리터 대비 2-5배 성능 향상
  3. Batch 연산 — 사용자 공간 ↔ 맵 상호작용 최적화
  4. tail call / BPF-to-BPF — 코드 크기 분할로 verifier 부하 감소
  5. 인라인 최적화 — __always_inline으로 함수 호출 오버헤드 제거

XDP 멀티버퍼와 메타데이터

커널 6.x에서 도입된 XDP 멀티버퍼 지원은 점보 프레임과 GRO 패킷을 XDP에서 처리할 수 있게 합니다. 또한 XDP 메타데이터를 통해 NIC에서 커널 스택으로 추가 정보를 전달할 수 있습니다.

xdp_buff (멀티버퍼) 메타데이터 영역 data_meta → data (최대 256B) 선형 데이터 (headroom) data → data_end 직접 접근 가능 (포인터 산술) Fragment #1 (skb_frag_t) bpf_xdp_load_bytes()로 접근 Fragment #2 (skb_frag_t) bpf_xdp_store_bytes()로 수정 Fragment #N MAX_SKB_FRAGS (17) 개까지 cpumap / devmap BPF_MAP_TYPE_CPUMAP BPF_MAP_TYPE_DEVMAP XDP_REDIRECT → 다른 CPU/디바이스로 패킷 분산 (RSS 대체/보완) XDP 메타데이터 활용 NIC → XDP (HW 힌트 전달) RSS hash, VLAN tag, HW timestamp XDP → TC/스택 (커스텀 데이터) mark, priority, 분류 결과 bpf_xdp_adjust_meta()로 영역 확장

멀티버퍼 XDP 프로그램

/* XDP 멀티버퍼 프로그램 (6.x+)
 * SEC("xdp.frags") 사용으로 멀티버퍼 지원 선언 */

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

    /* 전체 패킷 길이 (선형 + 프래그먼트) */
    __u64 total = bpf_xdp_get_buff_len(ctx);
    int linear  = data_end - data;

    /* 선형 부분에서 이더넷 + IP 헤더 파싱 */
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    /* 프래그먼트에 있는 페이로드 접근 */
    if (total > linear) {
        char payload[128];
        int offset = linear;  /* 프래그먼트 시작 오프셋 */
        int ret = bpf_xdp_load_bytes(ctx, offset, payload, sizeof(payload));
        if (ret < 0)
            return XDP_PASS;
        /* payload 검사 로직 ... */
    }

    return XDP_PASS;
}

/* cpumap을 이용한 CPU 분산 */
struct {
    __uint(type, BPF_MAP_TYPE_CPUMAP);
    __type(key, __u32);
    __type(value, struct bpf_cpumap_val);
    __uint(max_entries, 64);        /* CPU 수 */
} cpu_map SEC(".maps");

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

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    /* 소스 IP 기반 CPU 분산 */
    if (eth->h_proto == bpf_htons(ETH_P_IP)) {
        struct iphdr *ip = (void *)(eth + 1);
        if ((void *)(ip + 1) > data_end)
            return XDP_PASS;

        __u32 cpu = ip->saddr % bpf_num_possible_cpus();
        return bpf_redirect_map(&cpu_map, cpu, 0);
    }
    return XDP_PASS;
}

/* XDP 메타데이터 전달 (XDP → TC) */
struct xdp_meta {
    __u32 mark;
    __u16 class_id;
};

SEC("xdp")
int xdp_set_meta(struct xdp_md *ctx)
{
    /* 메타데이터 영역 확장 */
    int ret = bpf_xdp_adjust_meta(ctx, -(int)sizeof(struct xdp_meta));
    if (ret < 0)
        return XDP_PASS;

    void *data      = (void *)(long)ctx->data;
    void *data_meta = (void *)(long)ctx->data_meta;

    struct xdp_meta *meta = data_meta;
    if ((void *)(meta + 1) > data)
        return XDP_PASS;

    /* 분류 결과를 메타데이터에 저장 */
    meta->mark     = 0x42;
    meta->class_id = 10;

    return XDP_PASS;  /* TC BPF에서 data_meta로 접근 가능 */
}

struct_ops 기반 커널 확장

BPF_PROG_TYPE_STRUCT_OPS(5.6+)는 커널 내부의 함수 포인터 테이블을 BPF 프로그램으로 대체하는 메커니즘입니다. TCP 혼잡 제어 알고리즘, 스케줄러(sched_ext) 등을 커널 모듈(Kernel Module) 없이 BPF로 구현할 수 있습니다.

사용자 공간 BPF struct_ops 프로그램 로드 libbpf → bpf() syscall 커널 공간 tcp_congestion_ops .init() .cong_avoid() .ssthresh() .undo_cwnd() → BPF로 대체 bpf_cubic, bpf_bbr sched_ext_ops (6.12+) .select_cpu() .enqueue() .dispatch() .running() / .stopping() → BPF로 대체 scx_rusty, scx_lavd 기타 struct_ops bpf_tcp_ca (5.6+) sched_ext (6.12+) bpf_qdisc (진행중) 커널 모듈 없이 동적 커널 확장

BPF TCP 혼잡 제어 예제

// bpf_cc.bpf.c — BPF 기반 TCP 혼잡 제어
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

/* 간단한 AIMD (Additive Increase, Multiplicative Decrease) 구현 */

SEC("struct_ops")
void BPF_PROG(bpf_aimd_init, struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    tp->snd_cwnd = 10;  /* 초기 cwnd */
}

SEC("struct_ops")
void BPF_PROG(bpf_aimd_cong_avoid, struct sock *sk, __u32 ack, __u32 acked)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (tp->snd_cwnd < tp->snd_ssthresh) {
        /* Slow Start: 지수적 증가 */
        tp->snd_cwnd += acked;
    } else {
        /* Congestion Avoidance: 선형 증가 */
        tp->snd_cwnd_cnt += acked;
        if (tp->snd_cwnd_cnt >= tp->snd_cwnd) {
            tp->snd_cwnd++;
            tp->snd_cwnd_cnt = 0;
        }
    }
}

SEC("struct_ops")
__u32 BPF_PROG(bpf_aimd_ssthresh, struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    /* Multiplicative Decrease: cwnd / 2 */
    return max(tp->snd_cwnd >> 1, 2U);
}

SEC("struct_ops")
__u32 BPF_PROG(bpf_aimd_undo_cwnd, struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    return max(tp->snd_cwnd, tp->snd_ssthresh);
}

SEC(".struct_ops")
struct tcp_congestion_ops bpf_aimd = {
    .init       = (void *)bpf_aimd_init,
    .cong_avoid = (void *)bpf_aimd_cong_avoid,
    .ssthresh   = (void *)bpf_aimd_ssthresh,
    .undo_cwnd  = (void *)bpf_aimd_undo_cwnd,
    .name       = "bpf_aimd",
};

char LICENSE[] SEC("license") = "GPL";
# struct_ops 등록
sudo bpftool struct_ops register bpf_cc.bpf.o

# 등록된 struct_ops 확인
sudo bpftool struct_ops list

# 소켓에 혼잡 제어 알고리즘 적용
sysctl net.ipv4.tcp_congestion_control=bpf_aimd

# 또는 소켓 옵션으로 개별 적용
# setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, "bpf_aimd", 9);

# 해제
sudo bpftool struct_ops unregister name bpf_aimd

BPF 토큰과 보안 모델 (6.9+)

BPF 토큰은 컨테이너(Container)/샌드박스(Sandbox) 환경에서 BPF 기능에 대한 세밀한 권한 위임을 가능하게 하는 보안 메커니즘입니다. 기존의 CAP_BPF/CAP_SYS_ADMIN 바이너리 권한 모델을 대체합니다.

기존 모델 (6.8 이전) CAP_SYS_ADMIN — 모든 BPF 작업 허용 (너무 넓은 권한) CAP_BPF (5.8+) — BPF 프로그램 로드 + CAP_NET_ADMIN (네트워크 부착) CAP_PERFMON — perf/kprobe BPF + CAP_BPF 조합 필요 문제점 컨테이너에 CAP_BPF 부여 = 보안 위험 프로그램 타입별 세분화 불가 BPF 토큰 모델 (6.9+) 호스트 관리자: BPF 토큰 생성 BPF 토큰 (FD 기반) 허용 프로그램 타입: XDP, TC, CGROUP 허용 맵 타입: HASH, ARRAY, RINGBUF 컨테이너: 토큰으로 BPF 사용 CAP_BPF 불필요, 토큰 범위 내 작업만 가능 장점 최소 권한 원칙 적용 프로그램/맵 타입별 세밀한 권한 위임
/* BPF 토큰 생성 (호스트에서) */
#include <linux/bpf.h>
#include <bpf/bpf.h>

/* 토큰이 허용할 BPF 기능 정의 */
LIBBPF_OPTS(bpf_token_create_opts, token_opts,
    .flags = 0,
);

/* BPF 파일시스템에 토큰 고정 */
int bpffs_fd = open("/sys/fs/bpf/container_a", O_DIRECTORY);
int token_fd = bpf_token_create(bpffs_fd, &token_opts);

/* 토큰을 컨테이너에 전달 (fd 또는 UDS 경유) */

/* 컨테이너 내부에서 토큰 사용 */
LIBBPF_OPTS(bpf_prog_load_opts, prog_opts,
    .token_fd = token_fd,
);
/* 토큰 범위 내의 프로그램만 로드 가능 */
BPF 보안 계층 요약:
계층메커니즘커널 버전
CapabilityCAP_BPF, CAP_NET_ADMIN, CAP_PERFMON5.8+
Verifier메모리 안전성, 종료 보장3.18+
LSM HookSELinux/AppArmor BPF 정책5.7+
BPF 토큰세밀한 권한 위임6.9+
Signed BPF서명된 프로그램만 허용진행중

BPF Map 타입별 구조 비교

HASH / PERCPU_HASH h[0] h[1] h[2] k:v k:v k:v O(1) lookup/update spin_lock per-bucket PERCPU: lock-free 가장 범용적 LRU_HASH MRU → k1:v1 k2:v2 k3:v3 LRU → k4:v4 (퇴거 대상) 가득 차면 LRU 자동 퇴거 conntrack, flow cache에 적합 메모리 자동 관리 ARRAY / PERCPU_ARRAY [0] value [1] value [2] value O(1) index 접근 삭제 불가 (0으로 초기화) 카운터, 설정값에 적합 가장 빠름 LPM_TRIE root 0* 1* /24 /32 Longest Prefix Match IP 대역 매칭에 최적 CIDR 블랙리스트 RINGBUF (5.8+) ring buffer producer consumer 커널→유저 이벤트 전달 perf_buffer 대체 (효율적) 가변 크기 레코드 BLOOM_FILTER (5.16+) bit array + k hash functions O(k) 존재 확인 false positive 허용 해시 맵 앞단 필터에 사용 메모리 절약형 필터
맵 타입키 타입조회 복잡도삭제per-CPU주요 용도
HASH임의O(1)가능가능범용 key-value
ARRAY__u32 인덱스O(1)불가가능카운터, 설정값
LRU_HASH임의O(1)자동가능연결 추적, 캐시
LPM_TRIEprefixlen+dataO(k)가능불가IP 대역 매칭
RINGBUF없음N/AN/A공유이벤트 전달
BLOOM_FILTER없음O(k)불가불가존재 확인 필터
DEVMAPifindexO(1)가능불가XDP redirect
CPUMAPCPU IDO(1)가능불가XDP CPU 분산
SOCKMAP__u32O(1)가능불가소켓 redirect
PROG_ARRAY__u32O(1)가능불가tail call 테이블
맵 선택 가이드:
  • 고 PPS 카운터PERCPU_ARRAY (lock-free, 최소 오버헤드)
  • 동적 연결 추적LRU_PERCPU_HASH (자동 퇴거, CPU별 독립)
  • IP 블랙리스트LPM_TRIE + BLOOM_FILTER 조합
  • 이벤트 전달RINGBUF (perf_buffer 대비 메모리 효율적)
  • 패킷 분산DEVMAP_HASH + CPUMAP

참고자료

표준 및 공식 사양

커널 공식 문서

커뮤니티 및 생태계

도구 및 라이브러리

주요 참고 글

커널 소스 경로

BPF/XDP와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.