Skip to content

eBPF 완벽 가이드 - 차세대 Observability & 성능 최적화

목차

  1. eBPF란 무엇인가?
  2. eBPF 아키텍처
  3. eBPF 프로그램 작성 방법
  4. eBPF Maps - 데이터 공유
  5. 실습 1: TCP 연결 추적
  6. 실습 2: 패킷 필터링
  7. Golang + eBPF
  8. Kubernetes에서 eBPF
  9. 실전 활용 사례
  10. 성능 분석

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 Module

2. 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()                   // return

eBPF 프로그램 작성 방법

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.100

Kubernetes에서 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 status

Cilium이 하는 일

┌──────────────────────────────────────────────────────────┐
│               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의 장점:

  1. kube-proxy 불필요: eBPF로 Service 로드밸런싱
  2. 더 빠른 네트워킹: iptables 대신 eBPF
  3. 풍부한 관찰성: Hubble (eBPF 기반)
  4. 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: WARNING

Falco는 eBPF로 모든 시스템 콜을 모니터링하여 비정상 행위를 탐지합니다.

2. Pixie - Auto-instrumentation Observability

Pixie는 애플리케이션 수정 없이 eBPF로 자동 계측:

bash
# Pixie 설치
px deploy

# 실시간 모니터링
px live

Pixie가 자동으로 수집하는 정보:

  • 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의 핵심 가치

  1. 안전성: Verifier가 보장, 커널 크래시 없음
  2. 성능: JIT 컴파일, 최소 오버헤드
  3. 유연성: 동적 로딩, 재부팅 불필요
  4. 관찰성: 모든 커널 이벤트 추적 가능
  5. 프로그래밍 가능: 커스텀 로직 구현

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의 기본을 마스터했으니:

  1. Cilium 심화: Service Mesh, Network Policy
  2. 성능 분석: bpftrace로 프로덕션 병목 찾기
  3. 보안: Falco로 Runtime 보안 구축
  4. 커스텀 도구: 회사 특화 eBPF 도구 개발

eBPF는 Linux 커널 프로그래밍의 미래입니다. 이제 당신도 eBPF 전문가가 되었습니다!