eBPF 완벽 가이드 - 차세대 Observability & 성능 최적화
목차
- eBPF란 무엇인가?
- eBPF 아키텍처
- eBPF 프로그램 작성 방법
- eBPF Maps - 데이터 공유
- 실습 1: TCP 연결 추적
- 실습 2: 패킷 필터링
- Golang + eBPF
- Kubernetes에서 eBPF
- 실전 활용 사례
- 성능 분석
eBPF란 무엇인가?
정의
eBPF (extended Berkeley Packet Filter) 는 Linux 커널 내부에서 샌드박스 환경으로 안전하게 코드를 실행할 수 있는 혁명적인 기술입니다.
역사
1992: BPF (Berkeley Packet Filter)
- tcpdump에서 패킷 필터링용으로 개발
- 매우 제한적인 기능
2014: eBPF (extended BPF)
- Alexei Starovoitov이 Linux 커널에 추가
- 범용 실행 환경으로 확장
2016~현재: 폭발적 성장
- Cilium (네트워킹)
- Falco (보안)
- Pixie (관찰성)
- Katran (로드밸런서)왜 혁명적인가?
전통적인 방법 (커널 모듈)
┌──────────────────────────────────────────────────────┐
│ User Space │
│ │
│ ┌────────────────┐ │
│ │ Application │ │
│ └────────┬───────┘ │
│ │ System Call │
└───────────┼──────────────────────────────────────────┘
│
┌───────────▼──────────────────────────────────────────┐
│ Kernel Space │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ Kernel Module (커널 모듈) │ │
│ │ - C 코드 │ │
│ │ - 커널 크래시 위험 ⚠️ │ │
│ │ - 재부팅 필요 (수정 시) │ │
│ │ - 디버깅 어려움 │ │
│ │ - 보안 위험 │ │
│ └────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘문제점:
- ❌ 커널 크래시 가능 (버그 하나로 시스템 전체 다운)
- ❌ 재부팅 필요 (커널 모듈 업데이트마다)
- ❌ 디버깅 극도로 어려움
- ❌ 보안 위험 (무제한 권한)
eBPF 방법
┌──────────────────────────────────────────────────────┐
│ User Space │
│ │
│ ┌────────────────┐ ┌──────────────────────┐ │
│ │ Application │ │ eBPF Loader │ │
│ └────────┬───────┘ │ (bpftool, libbpf) │ │
│ │ └──────────┬───────────┘ │
│ │ System Call │ bpf() │
└───────────┼─────────────────────────┼───────────────┘
│ │
┌───────────▼─────────────────────────▼───────────────┐
│ Kernel Space │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ eBPF Verifier (검증기) │ │
│ │ - 안전성 검증 ✅ │ │
│ │ - 무한 루프 방지 │ │
│ │ - 메모리 접근 제한 │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ OK │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ eBPF JIT Compiler │ │
│ │ - 바이트코드 → 네이티브 코드 (x86/ARM) │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ eBPF VM (가상 머신) │ │
│ │ - 샌드박스 환경 실행 🔒 │ │
│ │ - 커널 크래시 없음 │ │
│ │ - 동적 로딩/언로딩 │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Hook Points (연결 지점) │ │
│ │ - XDP (네트워크 드라이버) │ │
│ │ - TC (Traffic Control) │ │
│ │ - kprobe (커널 함수) │ │
│ │ - tracepoint (커널 이벤트) │ │
│ │ - uprobe (유저 함수) │ │
│ └──────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘장점:
- ✅ 안전 (Verifier가 검증, 크래시 없음)
- ✅ 동적 로딩 (재부팅 불필요)
- ✅ 고성능 (JIT 컴파일로 네이티브 속도)
- ✅ 디버깅 가능
- ✅ 보안 (제한된 권한)
eBPF의 핵심 개념
1. Hook Points (연결 지점)
eBPF 프로그램이 실행되는 위치:
┌─────────────────────────────────────────────────────┐
│ Network Stack │
├─────────────────────────────────────────────────────┤
│ │
│ User Space Application │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ uprobe │ ← 유저 공간 함수 추적 │
│ └──────────────────┘ │
│ │ │
│ ═════════╪═════════════════════════════ │
│ Kernel │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Socket Filter │ ← 소켓 레벨 필터링 │
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ TC (ingress/ │ ← Traffic Control │
│ │ egress) │ (패킷 수정/드롭) │
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ XDP │ ← 최고 성능! (드라이버 레벨) │
│ │ (eXpress Data │ 패킷 처리 │
│ │ Path) │ │
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ Network Driver (eth0, wlan0...) │
│ │ │
│ ▼ │
│ NIC (Network Interface Card) │
│ │
└─────────────────────────────────────────────────────┘
Other Hook Points:
- kprobe/kretprobe: 커널 함수 진입/종료
- tracepoint: 커널 정적 추적 포인트
- perf_event: 성능 이벤트
- cgroup: cgroup 이벤트
- LSM: Linux Security Module2. eBPF Maps
커널과 유저 공간 간 데이터 공유:
┌──────────────────────────────────────────────────────┐
│ User Space │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ User Application │ │
│ │ - 통계 읽기 │ │
│ │ - 설정 쓰기 │ │
│ └───────────────┬────────────────────────────┘ │
│ │ bpf_map_lookup_elem() │
│ │ bpf_map_update_elem() │
└──────────────────┼──────────────────────────────────┘
│
┌──────────────────▼──────────────────────────────────┐
│ Kernel Space │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ eBPF Maps (공유 메모리) │ │
│ │ │ │
│ │ - BPF_MAP_TYPE_HASH │ │
│ │ - BPF_MAP_TYPE_ARRAY │ │
│ │ - BPF_MAP_TYPE_PERCPU_HASH │ │
│ │ - BPF_MAP_TYPE_RINGBUF │ │
│ │ - BPF_MAP_TYPE_LRU_HASH │ │
│ │ - ... │ │
│ └───────────────┬──────────────────────────────┘ │
│ │ bpf_map_lookup_elem() │
│ │ bpf_map_update_elem() │
│ ┌───────────────▼──────────────────────────────┐ │
│ │ eBPF Programs │ │
│ │ - 이벤트 발생 시 map 업데이트 │ │
│ │ - 설정 읽기 │ │
│ └──────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘3. eBPF Verifier
안전성 보장:
eBPF 프로그램 제출
│
▼
┌────────────────────────┐
│ eBPF Verifier │
├────────────────────────┤
│ │
│ 검증 항목: │
│ │
│ 1. 무한 루프 검사 │
│ → 모든 경로가 │
│ 종료되는지 확인 │
│ │
│ 2. 메모리 접근 검사 │
│ → 허용된 영역만 │
│ 접근하는지 확인 │
│ │
│ 3. 포인터 검증 │
│ → NULL 체크 │
│ → 범위 체크 │
│ │
│ 4. 명령어 개수 제한 │
│ → 최대 1M 명령어 │
│ │
│ 5. Helper 함수 검증 │
│ → 허용된 함수만 │
│ 호출하는지 확인 │
│ │
└────────┬───────────────┘
│
├─ PASS → JIT Compile → Load
│
└─ FAIL → Reject (에러 메시지)eBPF 아키텍처
eBPF 실행 흐름
┌─────────────────────────────────────────────────────────┐
│ Step 1: eBPF 프로그램 작성 │
└─────────────────────────────────────────────────────────┘
// C로 작성
// hello.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
SEC("xdp")
int hello_world(struct xdp_md *ctx) {
bpf_printk("Hello, eBPF!");
return XDP_PASS;
}
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 2: 컴파일 (clang) │
└─────────────────────────────────────────────────────────┘
clang -O2 -target bpf -c hello.bpf.c -o hello.bpf.o
결과: eBPF 바이트코드 (.o 파일)
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 3: 로딩 (bpf() system call) │
└─────────────────────────────────────────────────────────┘
// User Space Loader
int fd = bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 4: Verifier 검증 │
└─────────────────────────────────────────────────────────┘
Verifier가 안전성 검증
- PASS: 다음 단계
- FAIL: 에러 반환
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 5: JIT 컴파일 │
└─────────────────────────────────────────────────────────┘
eBPF 바이트코드 → 네이티브 기계어 (x86_64, ARM64...)
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 6: Hook에 연결 │
└─────────────────────────────────────────────────────────┘
bpf_prog_attach(fd, target_fd, BPF_XDP, ...);
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 7: 실행 │
└─────────────────────────────────────────────────────────┘
이벤트 발생 시 자동 실행
- 패킷 도착 (XDP)
- 함수 호출 (kprobe)
- 시스템 콜 (tracepoint)
- ...eBPF 명령어 세트
eBPF는 11개 레지스터를 가진 RISC-like 아키텍처:
레지스터:
R0: 반환 값
R1-R5: 함수 인자
R6-R9: Callee-saved
R10: 스택 포인터 (read-only)
명령어 예시:
BPF_MOV64_IMM(BPF_REG_0, 0) // r0 = 0
BPF_LD_MAP_FD(BPF_REG_1, map_fd) // r1 = map_fd
BPF_CALL_FUNC(bpf_map_lookup_elem) // 함수 호출
BPF_EXIT_INSN() // returneBPF 프로그램 작성 방법
3가지 주요 방법
1. BCC (BPF Compiler Collection) - 가장 쉬움
python
# hello_bcc.py
#!/usr/bin/env python3
from bcc import BPF
# eBPF 프로그램 (C 코드)
prog = """
#include <uapi/linux/ptrace.h>
int hello(struct pt_regs *ctx) {
bpf_trace_printk("Hello, World!\\n");
return 0;
}
"""
# 컴파일 및 로딩
b = BPF(text=prog)
# sys_clone에 연결 (프로세스 생성 시 실행)
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello")
print("Tracing... Hit Ctrl-C to end.")
# 출력 읽기
b.trace_print()장점:
- Python으로 작성 가능
- 빠른 프로토타이핑
- 풍부한 예제
단점:
- 런타임에 컴파일 (LLVM/Clang 필요)
- 배포 어려움
- 구형 커널(< 4.1) 지원 안 함
2. libbpf (C) - 프로덕션 권장
c
// hello_libbpf.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "GPL";
SEC("kprobe/sys_clone")
int hello(struct pt_regs *ctx) {
char msg[] = "Hello, World!";
bpf_trace_printk(msg, sizeof(msg));
return 0;
}c
// hello_libbpf_user.c
#include <stdio.h>
#include <unistd.h>
#include <bpf/libbpf.h>
#include "hello_libbpf.skel.h"
int main() {
struct hello_libbpf_bpf *skel;
int err;
// eBPF 프로그램 열기
skel = hello_libbpf_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
// 로딩 및 검증
err = hello_libbpf_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load BPF skeleton\n");
goto cleanup;
}
// 연결
err = hello_libbpf_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
printf("Successfully started! Press Ctrl+C to stop.\n");
// 실행 (무한 대기)
while (1) {
sleep(1);
}
cleanup:
hello_libbpf_bpf__destroy(skel);
return err;
}컴파일:
bash
# eBPF 프로그램 컴파일
clang -O2 -target bpf -c hello_libbpf.bpf.c -o hello_libbpf.bpf.o
# Skeleton 생성
bpftool gen skeleton hello_libbpf.bpf.o > hello_libbpf.skel.h
# User space 프로그램 컴파일
gcc -o hello_libbpf hello_libbpf_user.c -lbpf장점:
- CO-RE (Compile Once, Run Everywhere)
- 미리 컴파일된 바이너리 배포 가능
- 프로덕션 환경에 적합
- 최고 성능
단점:
- 학습 곡선
- C 코드 작성 필요
3. cilium/ebpf (Golang) - DevOps 최적
go
// hello_golang.go
package main
import (
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
)
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang hello hello.bpf.c -- -I/usr/include/bpf
func main() {
// eBPF 프로그램 로딩
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
}
objs := helloObjects{}
if err := loadHelloObjects(&objs, nil); err != nil {
log.Fatalf("loading objects: %v", err)
}
defer objs.Close()
// kprobe 연결
kp, err := link.Kprobe("sys_clone", objs.Hello, nil)
if err != nil {
log.Fatalf("opening kprobe: %v", err)
}
defer kp.Close()
fmt.Println("Successfully started! Press Ctrl+C to stop.")
// 시그널 대기
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
}c
// hello.bpf.c
//go:build ignore
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
char __license[] SEC("license") = "GPL";
SEC("kprobe/sys_clone")
int hello(struct pt_regs *ctx) {
char msg[] = "Hello from eBPF!";
bpf_trace_printk(msg, sizeof(msg));
return 0;
}컴파일 및 실행:
bash
# go generate로 자동 컴파일
go generate
# 실행
sudo go run hello_golang.go장점:
- Golang 생태계 활용
- 타입 안정성
- 크로스 컴파일 용이
- DevOps 도구와 통합하기 좋음
단점:
- 비교적 새로운 기술
- 예제가 BCC보다 적음
eBPF Maps - 데이터 공유
Map 타입
c
// 1. Hash Map
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10000);
__type(key, __u32); // PID
__type(value, __u64); // 카운터
} pid_map SEC(".maps");
// 2. Array
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 256);
__type(key, __u32);
__type(value, __u64);
} array_map SEC(".maps");
// 3. Per-CPU Hash (성능 최적화)
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_HASH);
__uint(max_entries, 10000);
__type(key, __u32);
__type(value, __u64);
} percpu_map SEC(".maps");
// 4. Ring Buffer (효율적인 이벤트 전달)
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024); // 256KB
} events SEC(".maps");
// 5. LRU Hash (자동 eviction)
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 10000);
__type(key, __u32);
__type(value, __u64);
} lru_map SEC(".maps");Map 사용 예시
c
// eBPF 프로그램에서 (Kernel Space)
SEC("kprobe/sys_write")
int trace_write(struct pt_regs *ctx) {
__u32 pid = bpf_get_current_pid_tgid() >> 32;
__u64 *count, init_val = 1;
// Map에서 값 조회
count = bpf_map_lookup_elem(&pid_map, &pid);
if (count) {
// 존재하면 증가
__sync_fetch_and_add(count, 1);
} else {
// 없으면 삽입
bpf_map_update_elem(&pid_map, &pid, &init_val, BPF_ANY);
}
return 0;
}c
// User Space에서 읽기
int map_fd = bpf_obj_get("/sys/fs/bpf/pid_map");
__u32 key = 1234; // PID
__u64 value;
if (bpf_map_lookup_elem(map_fd, &key, &value) == 0) {
printf("PID %u: %llu writes\n", key, value);
}실습 1: TCP 연결 추적
목표
모든 TCP 연결을 추적하고 통계 수집
eBPF 프로그램 (C)
c
// tcp_connect.bpf.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
char LICENSE[] SEC("license") = "GPL";
// 연결 정보 구조체
struct conn_info {
__u32 saddr;
__u32 daddr;
__u16 sport;
__u16 dport;
__u64 timestamp;
};
// 이벤트 구조체
struct event {
__u32 pid;
__u32 saddr;
__u32 daddr;
__u16 sport;
__u16 dport;
char comm[16];
};
// Map: 연결 통계
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10000);
__type(key, struct conn_info);
__type(value, __u64);
} conn_stats SEC(".maps");
// Map: 이벤트 전달
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");
// TCP connect 추적
SEC("kprobe/tcp_v4_connect")
int trace_tcp_connect(struct pt_regs *ctx) {
struct event *e;
struct sock *sk = (struct sock *)PT_REGS_PARM1(ctx);
__u32 saddr, daddr;
__u16 sport, dport;
// 소스/목적지 주소 읽기
BPF_CORE_READ_INTO(&saddr, sk, __sk_common.skc_rcv_saddr);
BPF_CORE_READ_INTO(&daddr, sk, __sk_common.skc_daddr);
BPF_CORE_READ_INTO(&sport, sk, __sk_common.skc_num);
BPF_CORE_READ_INTO(&dport, sk, __sk_common.skc_dport);
dport = bpf_ntohs(dport);
// 이벤트 생성
e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e)
return 0;
e->pid = bpf_get_current_pid_tgid() >> 32;
e->saddr = saddr;
e->daddr = daddr;
e->sport = sport;
e->dport = dport;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
// 이벤트 전송
bpf_ringbuf_submit(e, 0);
// 통계 업데이트
struct conn_info conn = {
.saddr = saddr,
.daddr = daddr,
.sport = sport,
.dport = dport,
.timestamp = bpf_ktime_get_ns(),
};
__u64 *count, init_val = 1;
count = bpf_map_lookup_elem(&conn_stats, &conn);
if (count) {
__sync_fetch_and_add(count, 1);
} else {
bpf_map_update_elem(&conn_stats, &conn, &init_val, BPF_ANY);
}
return 0;
}Golang User Space 프로그램
go
// tcp_connect.go
package main
import (
"bytes"
"encoding/binary"
"fmt"
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
"github.com/cilium/ebpf/rlimit"
)
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -type event tcp_connect tcp_connect.bpf.c
type Event struct {
PID uint32
Saddr uint32
Daddr uint32
Sport uint16
Dport uint16
Comm [16]byte
}
func main() {
// Memlock 제한 제거
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
}
// eBPF 프로그램 로딩
objs := tcp_connectObjects{}
if err := loadTcp_connectObjects(&objs, nil); err != nil {
log.Fatalf("loading objects: %v", err)
}
defer objs.Close()
// kprobe 연결
kp, err := link.Kprobe("tcp_v4_connect", objs.TraceTcpConnect, nil)
if err != nil {
log.Fatalf("opening kprobe: %v", err)
}
defer kp.Close()
fmt.Println("╔═══════════════════════════════════════════════════════════════════╗")
fmt.Println("║ TCP Connection Tracker Started ║")
fmt.Println("╠═══════════════════════════════════════════════════════════════════╣")
fmt.Println("║ Tracking all TCP connections... ║")
fmt.Println("╚═══════════════════════════════════════════════════════════════════╝")
fmt.Println()
fmt.Printf("%-8s %-16s %-20s %-20s\n", "PID", "COMM", "SOURCE", "DESTINATION")
fmt.Println("────────────────────────────────────────────────────────────────────")
// Ring buffer 읽기
rd, err := ringbuf.NewReader(objs.Events)
if err != nil {
log.Fatalf("opening ringbuf reader: %v", err)
}
defer rd.Close()
// 시그널 처리
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sig
fmt.Println("\n\nShutting down...")
// 통계 출력
printStats(&objs)
os.Exit(0)
}()
// 이벤트 처리
for {
record, err := rd.Read()
if err != nil {
if ringbuf.IsClosed(err) {
return
}
log.Printf("reading from reader: %v", err)
continue
}
var event Event
if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil {
log.Printf("parsing event: %v", err)
continue
}
// 이벤트 출력
printEvent(&event)
}
}
func printEvent(e *Event) {
saddr := intToIP(e.Saddr)
daddr := intToIP(e.Daddr)
comm := string(bytes.TrimRight(e.Comm[:], "\x00"))
fmt.Printf("%-8d %-16s %s:%-15d → %s:%-15d\n",
e.PID,
comm,
saddr, e.Sport,
daddr, e.Dport,
)
}
func intToIP(ip uint32) net.IP {
return net.IPv4(byte(ip), byte(ip>>8), byte(ip>>16), byte(ip>>24))
}
func printStats(objs *tcp_connectObjects) {
fmt.Println("\n╔═══════════════════════════════════════════════════════════════════╗")
fmt.Println("║ Connection Statistics ║")
fmt.Println("╠═══════════════════════════════════════════════════════════════════╣")
var (
key tcp_connectConnInfo
value uint64
)
iter := objs.ConnStats.Iterate()
count := 0
for iter.Next(&key, &value) {
saddr := intToIP(key.Saddr)
daddr := intToIP(key.Daddr)
fmt.Printf("║ %s:%-5d → %s:%-5d Count: %-10d║\n",
saddr, key.Sport,
daddr, key.Dport,
value,
)
count++
}
if count == 0 {
fmt.Println("║ No connections tracked ║")
}
fmt.Println("╚═══════════════════════════════════════════════════════════════════╝")
}실행
bash
# 의존성 설치
go get github.com/cilium/ebpf
# 생성
go generate
# 실행
sudo go run tcp_connect.go테스트
다른 터미널에서:
bash
# HTTP 요청
curl http://example.com
# SSH 연결
ssh user@192.168.1.100
# MySQL 연결
mysql -h db.example.com -u user -p출력 예시:
╔═══════════════════════════════════════════════════════════════════╗
║ TCP Connection Tracker Started ║
╠═══════════════════════════════════════════════════════════════════╣
║ Tracking all TCP connections... ║
╚═══════════════════════════════════════════════════════════════════╝
PID COMM SOURCE DESTINATION
────────────────────────────────────────────────────────────────────
12345 curl 192.168.1.10:45678 → 93.184.216.34:80
12346 ssh 192.168.1.10:45679 → 192.168.1.100:22
12347 mysql 192.168.1.10:45680 → 10.0.0.50:3306
^C
Shutting down...
╔═══════════════════════════════════════════════════════════════════╗
║ Connection Statistics ║
╠═══════════════════════════════════════════════════════════════════╣
║ 192.168.1.10:45678 → 93.184.216.34:80 Count: 1 ║
║ 192.168.1.10:45679 → 192.168.1.100:22 Count: 3 ║
║ 192.168.1.10:45680 → 10.0.0.50:3306 Count: 10 ║
╚═══════════════════════════════════════════════════════════════════╝실습 2: 패킷 필터링 (XDP)
목표
특정 IP 주소에서 오는 패킷을 XDP에서 드롭
eBPF 프로그램 (C)
c
// xdp_filter.bpf.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
char LICENSE[] SEC("license") = "GPL";
// Blocked IPs map
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10000);
__type(key, __u32); // IP address
__type(value, __u64); // Drop count
} blocked_ips SEC(".maps");
// Statistics
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 4);
__type(key, __u32);
__type(value, __u64);
} stats SEC(".maps");
enum {
STAT_TOTAL_PACKETS = 0,
STAT_PASSED_PACKETS = 1,
STAT_DROPPED_PACKETS = 2,
STAT_INVALID_PACKETS = 3,
};
static __always_inline void update_stat(__u32 key) {
__u64 *value;
value = bpf_map_lookup_elem(&stats, &key);
if (value) {
__sync_fetch_and_add(value, 1);
}
}
SEC("xdp")
int xdp_filter_func(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
// Ethernet header
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end) {
update_stat(STAT_INVALID_PACKETS);
return XDP_PASS;
}
// IPv4만 처리
if (eth->h_proto != bpf_htons(ETH_P_IP)) {
update_stat(STAT_TOTAL_PACKETS);
update_stat(STAT_PASSED_PACKETS);
return XDP_PASS;
}
// IP header
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end) {
update_stat(STAT_INVALID_PACKETS);
return XDP_PASS;
}
update_stat(STAT_TOTAL_PACKETS);
// Check if source IP is blocked
__u32 saddr = ip->saddr;
__u64 *drop_count;
drop_count = bpf_map_lookup_elem(&blocked_ips, &saddr);
if (drop_count) {
// IP is blocked - drop packet
__sync_fetch_and_add(drop_count, 1);
update_stat(STAT_DROPPED_PACKETS);
bpf_printk("XDP: Dropped packet from %pI4\n", &saddr);
return XDP_DROP;
}
update_stat(STAT_PASSED_PACKETS);
return XDP_PASS;
}Golang User Space 프로그램
go
// xdp_filter.go
package main
import (
"fmt"
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
)
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang xdp_filter xdp_filter.bpf.c
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: sudo ./xdp_filter <interface>")
fmt.Println("Example: sudo ./xdp_filter eth0")
os.Exit(1)
}
ifaceName := os.Args[1]
// Memlock 제한 제거
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
}
// 인터페이스 찾기
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
log.Fatalf("lookup network iface %q: %v", ifaceName, err)
}
// eBPF 프로그램 로딩
objs := xdp_filterObjects{}
if err := loadXdp_filterObjects(&objs, nil); err != nil {
log.Fatalf("loading objects: %v", err)
}
defer objs.Close()
// XDP 연결
l, err := link.AttachXDP(link.XDPOptions{
Program: objs.XdpFilterFunc,
Interface: iface.Index,
})
if err != nil {
log.Fatalf("could not attach XDP program: %v", err)
}
defer l.Close()
fmt.Println("╔═══════════════════════════════════════════════════════════════════╗")
fmt.Printf("║ XDP Packet Filter Started on %s%-34s║\n", ifaceName, "")
fmt.Println("╠═══════════════════════════════════════════════════════════════════╣")
fmt.Println("║ Commands: ║")
fmt.Println("║ block <ip> - Block IP address ║")
fmt.Println("║ unblock <ip> - Unblock IP address ║")
fmt.Println("║ list - List blocked IPs ║")
fmt.Println("║ stats - Show statistics ║")
fmt.Println("║ quit - Exit ║")
fmt.Println("╚═══════════════════════════════════════════════════════════════════╝")
fmt.Println()
// 시그널 처리
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
// 통계 출력 고루틴
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
printStats(&objs)
}
}()
// 커맨드 처리
go handleCommands(&objs)
<-sig
fmt.Println("\n\nShutting down...")
printStats(&objs)
}
func handleCommands(objs *xdp_filterObjects) {
var cmd, ipStr string
for {
fmt.Print("> ")
fmt.Scan(&cmd)
switch cmd {
case "block":
fmt.Scan(&ipStr)
if err := blockIP(objs, ipStr); err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Blocked: %s\n", ipStr)
}
case "unblock":
fmt.Scan(&ipStr)
if err := unblockIP(objs, ipStr); err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Unblocked: %s\n", ipStr)
}
case "list":
listBlockedIPs(objs)
case "stats":
printStats(objs)
case "quit":
os.Exit(0)
default:
fmt.Println("Unknown command")
}
}
}
func blockIP(objs *xdp_filterObjects, ipStr string) error {
ip := net.ParseIP(ipStr)
if ip == nil {
return fmt.Errorf("invalid IP address: %s", ipStr)
}
// IPv4로 변환
ip = ip.To4()
if ip == nil {
return fmt.Errorf("only IPv4 supported")
}
// Little endian으로 변환
ipInt := uint32(ip[0]) | uint32(ip[1])<<8 | uint32(ip[2])<<16 | uint32(ip[3])<<24
var count uint64 = 0
return objs.BlockedIps.Put(ipInt, count)
}
func unblockIP(objs *xdp_filterObjects, ipStr string) error {
ip := net.ParseIP(ipStr)
if ip == nil {
return fmt.Errorf("invalid IP address: %s", ipStr)
}
ip = ip.To4()
if ip == nil {
return fmt.Errorf("only IPv4 supported")
}
ipInt := uint32(ip[0]) | uint32(ip[1])<<8 | uint32(ip[2])<<16 | uint32(ip[3])<<24
return objs.BlockedIps.Delete(ipInt)
}
func listBlockedIPs(objs *xdp_filterObjects) {
fmt.Println("\n╔════════════════════════════════════════════════════════╗")
fmt.Println("║ Blocked IP Addresses ║")
fmt.Println("╠════════════════════════════════════════════════════════╣")
var (
key uint32
value uint64
)
iter := objs.BlockedIps.Iterate()
count := 0
for iter.Next(&key, &value) {
ip := intToIP(key)
fmt.Printf("║ %-20s Dropped: %-10d ║\n", ip, value)
count++
}
if count == 0 {
fmt.Println("║ No blocked IPs ║")
}
fmt.Println("╚════════════════════════════════════════════════════════╝\n")
}
func printStats(objs *xdp_filterObjects) {
var total, passed, dropped, invalid uint64
objs.Stats.Lookup(uint32(0), &total)
objs.Stats.Lookup(uint32(1), &passed)
objs.Stats.Lookup(uint32(2), &dropped)
objs.Stats.Lookup(uint32(3), &invalid)
fmt.Println("\n╔════════════════════════════════════════════════════════╗")
fmt.Println("║ XDP Statistics ║")
fmt.Println("╠════════════════════════════════════════════════════════╣")
fmt.Printf("║ Total Packets: %-10d ║\n", total)
fmt.Printf("║ Passed Packets: %-10d (%.2f%%) ║\n",
passed, percentage(passed, total))
fmt.Printf("║ Dropped Packets: %-10d (%.2f%%) ║\n",
dropped, percentage(dropped, total))
fmt.Printf("║ Invalid Packets: %-10d ║\n", invalid)
fmt.Println("╚════════════════════════════════════════════════════════╝\n")
}
func intToIP(ip uint32) net.IP {
return net.IPv4(byte(ip), byte(ip>>8), byte(ip>>16), byte(ip>>24))
}
func percentage(part, total uint64) float64 {
if total == 0 {
return 0
}
return float64(part) * 100 / float64(total)
}실행
bash
# 생성
go generate
# 실행 (eth0 인터페이스)
sudo go run xdp_filter.go eth0테스트
> block 192.168.1.100
Blocked: 192.168.1.100
> list
╔════════════════════════════════════════════════════════╗
║ Blocked IP Addresses ║
╠════════════════════════════════════════════════════════╣
║ 192.168.1.100 Dropped: 0 ║
╚════════════════════════════════════════════════════════╝
# 다른 터미널에서
ping 192.168.1.100 # 패킷이 드롭됨
> stats
╔════════════════════════════════════════════════════════╗
║ XDP Statistics ║
╠════════════════════════════════════════════════════════╣
║ Total Packets: 1523 ║
║ Passed Packets: 1500 (98.49%) ║
║ Dropped Packets: 23 (1.51%) ║
║ Invalid Packets: 0 ║
╚════════════════════════════════════════════════════════╝
> unblock 192.168.1.100
Unblocked: 192.168.1.100Kubernetes에서 eBPF
Cilium - eBPF 기반 CNI
Cilium은 eBPF를 사용하여 Kubernetes 네트워킹을 구현합니다.
설치
bash
# Cilium CLI 설치
curl -L --remote-name-all https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz
sudo tar xzvfC cilium-linux-amd64.tar.gz /usr/local/bin
rm cilium-linux-amd64.tar.gz
# Kubernetes 클러스터에 Cilium 설치
cilium install
# 상태 확인
cilium statusCilium이 하는 일
┌──────────────────────────────────────────────────────────┐
│ Cilium Architecture │
├──────────────────────────────────────────────────────────┤
│ │
│ Pod A Pod B │
│ (10.1.1.10) (10.1.2.10) │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ │
│ │ veth │ │ veth │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ eBPF Programs (TC/XDP) │ │
│ │ - 패킷 라우팅 (VXLAN 대신) │ │
│ │ - 로드 밸런싱 (kube-proxy 대신) │ │
│ │ - Network Policy 적용 │ │
│ │ - 관찰성 (Hubble) │ │
│ └──────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘Cilium의 장점:
- kube-proxy 불필요: eBPF로 Service 로드밸런싱
- 더 빠른 네트워킹: iptables 대신 eBPF
- 풍부한 관찰성: Hubble (eBPF 기반)
- Network Policy: L3/L4/L7 레벨
Hubble - eBPF 기반 관찰성
bash
# Hubble 활성화
cilium hubble enable
# Hubble CLI 설치
export HUBBLE_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/hubble/master/stable.txt)
curl -L --remote-name-all https://github.com/cilium/hubble/releases/download/$HUBBLE_VERSION/hubble-linux-amd64.tar.gz
sudo tar xzvfC hubble-linux-amd64.tar.gz /usr/local/bin
rm hubble-linux-amd64.tar.gz
# Hubble 연결
cilium hubble port-forward &
# 트래픽 관찰
hubble observe출력:
Nov 1 10:30:45.123: default/pod-a:45678 -> default/pod-b:80 to-endpoint FORWARDED (TCP Flags: SYN)
Nov 1 10:30:45.125: default/pod-b:80 -> default/pod-a:45678 to-endpoint FORWARDED (TCP Flags: SYN, ACK)
Nov 1 10:30:45.126: default/pod-a:45678 -> default/pod-b:80 to-endpoint FORWARDED (TCP Flags: ACK)
Nov 1 10:30:45.130: default/pod-a:45678 -> default/pod-b:80 to-endpoint FORWARDED (HTTP/1.1 GET /)
Nov 1 10:30:45.135: default/pod-b:80 -> default/pod-a:45678 to-endpoint FORWARDED (HTTP/1.1 200 OK)Hubble은 eBPF로 모든 네트워크 트래픽을 추적합니다!
실전 활용 사례
1. Falco - Runtime Security
yaml
# Falco 룰 예시
- rule: Unauthorized Process in Container
desc: Detect processes not in allowed list
condition: >
container and
spawned_process and
not proc.name in (allowed_processes)
output: >
Unauthorized process started
(user=%user.name command=%proc.cmdline container=%container.name)
priority: WARNINGFalco는 eBPF로 모든 시스템 콜을 모니터링하여 비정상 행위를 탐지합니다.
2. Pixie - Auto-instrumentation Observability
Pixie는 애플리케이션 수정 없이 eBPF로 자동 계측:
bash
# Pixie 설치
px deploy
# 실시간 모니터링
px livePixie가 자동으로 수집하는 정보:
- HTTP/HTTPS 요청/응답
- gRPC 호출
- MySQL/PostgreSQL 쿼리
- Redis 명령어
- Kafka 메시지
- DNS 쿼리
3. Katran - Facebook의 L4 로드밸런서
XDP + eBPF로 구현된 초고속 로드밸런서:
성능:
- 10M+ PPS (Packets Per Second)
- < 10μs 지연 시간
- CPU 사용률 < 5%4. Calico eBPF Dataplane
bash
# Calico eBPF 모드 활성화
kubectl patch felixconfiguration default --type='merge' -p \
'{"spec":{"bpfEnabled":true}}'장점:
- kube-proxy 불필요
- iptables 대신 eBPF
- 더 빠른 성능
성능 분석
eBPF vs iptables
벤치마크: Service 로드밸런싱
┌─────────────────┬──────────┬──────────┬─────────────┐
│ 방법 │ 처리량 │ 지연시간 │ CPU 사용률 │
├─────────────────┼──────────┼──────────┼─────────────┤
│ iptables │ 10 Gbps │ 100 μs │ 40% │
│ (kube-proxy) │ │ │ │
├─────────────────┼──────────┼──────────┼─────────────┤
│ eBPF (Cilium) │ 25 Gbps │ 20 μs │ 15% │
│ │ │ │ │
├─────────────────┼──────────┼──────────┼─────────────┤
│ 성능 향상 │ 2.5배 │ 5배 │ 2.7배 │
└─────────────────┴──────────┴──────────┴─────────────┘XDP vs Kernel Network Stack
패킷 처리 위치에 따른 성능:
┌──────────────────────────────────────────────────┐
│ Userspace (tcpdump) │
│ - 5M PPS │
│ - 가장 느림 (context switch 많음) │
└──────────────────────────────────────────────────┘
▲
┌──────────────────────────────────────────────────┐
│ Kernel Network Stack │
│ - 10M PPS │
│ - iptables, routing 등 처리 │
└──────────────────────────────────────────────────┘
▲
┌──────────────────────────────────────────────────┐
│ XDP (Driver Level) │
│ - 20M+ PPS │
│ - 드라이버 레벨에서 즉시 처리 │
│ - DDoS 방어, 로드밸런싱에 최적 │
└──────────────────────────────────────────────────┘정리
eBPF의 핵심 가치
- 안전성: Verifier가 보장, 커널 크래시 없음
- 성능: JIT 컴파일, 최소 오버헤드
- 유연성: 동적 로딩, 재부팅 불필요
- 관찰성: 모든 커널 이벤트 추적 가능
- 프로그래밍 가능: 커스텀 로직 구현
DevOps에서의 활용
관찰성 (Observability):
- Pixie: 자동 계측
- Hubble: 네트워크 트래픽 가시성
- bpftrace: 커널 프로파일링
네트워킹 (Networking):
- Cilium: CNI
- Katran: L4 로드밸런서
- Calico eBPF: 네트워크 정책
보안 (Security):
- Falco: Runtime 보안
- Tetragon: 프로세스 실행 제어
- Tracee: 위협 탐지
성능 (Performance):
- XDP: DDoS 방어
- TC: QoS, Traffic shaping
- kprobe: 성능 병목 분석학습 로드맵
1단계: 기초 (1-2주)
- eBPF 개념 이해
- BCC로 간단한 예제 작성
- bpftool 사용법
2단계: 중급 (2-4주)
- libbpf로 프로그램 작성
- Map 활용
- XDP, TC 프로그램
3단계: 고급 (1-2개월)
- Golang + eBPF
- CO-RE (Compile Once, Run Everywhere)
- 복잡한 프로그램 작성
4단계: 실전 (ongoing)
- Cilium, Falco 같은 도구 활용
- 프로덕션 환경에 적용
- 커스텀 솔루션 개발다음 단계
이제 eBPF의 기본을 마스터했으니:
- Cilium 심화: Service Mesh, Network Policy
- 성능 분석: bpftrace로 프로덕션 병목 찾기
- 보안: Falco로 Runtime 보안 구축
- 커스텀 도구: 회사 특화 eBPF 도구 개발
eBPF는 Linux 커널 프로그래밍의 미래입니다. 이제 당신도 eBPF 전문가가 되었습니다!