시작하며


지난 6장에서는 주입된 eBPF 프로그램이 커널을 망가뜨리지 않고 안전하게 동작함을 수학적/정적으로 증명하는 **eBPF 검증기(Verifier)**의 원리를 깊이 탐구했다.

검증기라는 강력한 안전벨트 덕분에, 우리는 커널 소스 코드를 한 줄도 수정하지 않고, 시스템을 재부팅할 필요도 없이 리눅스 커널의 핵심 서브시스템에 원하는 로직을 동적으로 탑재할 수 있게 되었다.

그렇다면 이렇게 로드된 eBPF 프로그램은 커널 내부의 어떤 이벤트에, 어떤 방식으로 결합되어 작동할까? 그리고 eBPF 프로그램이 부착되는 위치에 따라 다루는 데이터 구조와 활용법은 어떻게 달라질까?

리눅스 커널은 eBPF를 특정 목적의 도구로 한정하지 않고, 다양한 서브시스템을 확장할 수 있는 범용 가상 머신으로 설계했다. 그리고 그 핵심 접점을 제공하는 것이 바로 **프로그램 타입(prog_type)**과 **부착 타입(attach_type)**이다.

이번 7장에서는 eBPF 프로그램의 성격을 결정하는 프로그램 타입들의 지도와, 이들을 적재적소에 매끄럽게 바인딩하는 부착 메커니즘 및 현대적 관리 도구인 BPF Link를 살펴보겠다.

프로그램 타입을 결정하는 3대 요소 (Context, Helpers, Return Codes)


모든 eBPF 프로그램은 커널에 등록될 때 자신이 어떤 용도로 사용될지 bpf_prog_typeEnum 값을 통해 명시해야 한다. 이 타입에 따라 해당 eBPF 프로그램의 3대 구동 환경이 엄격하게 규정된다.

┌────────────────────────────────────────────────────────┐
│               eBPF 프로그램 구동 환경                    │
├───────────────────┬───────────────────┬────────────────┤
│ 1. Context (컨텍스트)│ 2. 허용 Helper 함수│ 3. Return Code │
│ - 입력 Argument   │ - 호출 가능한 API │ - 반환값의 의미│
│ - (예: struct md) │ - (예: 맵 업데이트)│ - (예: Drop/Pass)│
└───────────────────┴───────────────────┴────────────────┘

1. 컨텍스트 (Context)


eBPF 프로그램의 시작점(Entry Point)인 main 함수는 오직 **단 하나의 인자(Argument)**인 컨텍스트 포인터(ctx)만 전달받는다.

이 컨텍스트 포인터가 가리키는 실제 메모리 구조는 프로그램 타입에 따라 완전히 달라진다.

  • 트레이싱 프로그램: 현재 CPU의 레지스터 상태를 담은 struct pt_regs를 바라본다.
  • XDP 네트워킹 프로그램: 패킷 데이터의 시작과 끝 주소만 담고 있는 아주 원시적이고 가벼운 struct xdp_md를 바라본다.
  • TC 네트워킹 프로그램: 리눅스 네트워크 스택이 관리하는 메타데이터와 소켓 버퍼 정보를 풍부하게 품은 struct __sk_buff를 바라본다.

2. 허용 헬퍼 함수 (Allowed Helper Functions)


검증기는 프로그램 타입에 따라 호출할 수 있는 커널 헬퍼 함수 목록을 다르게 제한한다.

  • 시스템 성능 분석용인 kprobe 프로그램은 패킷을 다른 네트워크 카드로 전송하는 bpf_redirect() 함수를 호출할 수 없다.
  • 반대로 성능 최적화가 극도로 요구되는 XDP 프로그램은 시스템 호출 중인 프로세스의 PID나 이름을 읽는 bpf_get_current_pid_tgid() 함수를 원칙적으로 호출할 수 없다 (컨텍스트가 프로세스가 아닌 패킷 수신 인터럽트 컨텍스트이기 때문).

3. 반환값의 의미 (Return Codes)


eBPF 프로그램이 실행을 마치고 R0 레지스터를 통해 반환하는 정수값의 해석 또한 타입별로 정해져 있다.

  • XDP: 패킷을 통과시킬지(XDP_PASS), 폐기할지(XDP_DROP), 혹은 들어온 곳으로 돌려보낼지(XDP_TX)를 제어한다.
  • 소켓 필터: 사용자 공간 애플리케이션으로 패킷의 복사본을 몇 바이트까지 전달할 것인지를 정수로 지정한다.
  • 트레이싱(Kprobe): 반환값을 커널이 무시하는 경우가 많아 대개 0을 반환한다.

핵심 eBPF 프로그램 타입 분류


Learning eBPF 7장에서는 커널의 다양한 영역을 통제하는 대표적인 프로그램 타입들을 세 가지 주류 카테고리로 나누어 설명한다.

┌────────────────────────────────────────────────────────────────────────┐
│                        eBPF 프로그램 타입 분류                         │
├──────────────────────────┬──────────────────────────┬──────────────────┤
│ 트레이싱 및 관측성       │ 네트워킹                 │ 보안 및 LSM      │
├──────────────────────────┼──────────────────────────┼──────────────────┤
│ - Kprobe / Kretprobe     │ - XDP                    │ - BPF LSM        │
│ - Uprobe / Uretprobe     │ - TC (Traffic Control)   │                  │
│ - Tracepoint             │ - Socket Filter          │                  │
│ - Fentry / Fexit (BTF)   │ - Cgroup Socket          │                  │
└──────────────────────────┴──────────────────────────┴──────────────────┘

1. 트레이싱 및 관측성 (Tracing & Observability)


커널과 사용자 공간 애플리케이션의 행위를 동적으로 모니터링하기 위한 프로그램 타입들이다.

Kprobes & Kretprobes


  • 용도: 커널 내부 거의 대부분의 함수 진입점(Kprobe)과 반환점(Kretprobe)에 동적으로 훅을 건다.
  • 특징: 커널 코드를 변경하지 않고도 디버깅 지표를 뽑아낼 수 있어 매우 강력하지만, 커널의 내부 API(함수 이름 및 인자 구조)에 의존하므로 커널 버전이 바뀌면 프로그램이 정상 동작하지 않거나 컴파일이 깨질 수 있다.

Uprobes & Uretprobes


  • 용도: 커널이 아닌, 사용자 공간의 실행 바이너리(ELF)나 공유 라이브러리(예: libc.so) 내부 함수의 진입/반환점에 훅을 설치한다.
  • 특징: Go 언어의 가비지 컬렉터(runtime.gcStart), OpenSSL의 암호화 함수(SSL_write) 등을 추적하여 유저 공간 애플리케이션의 내부 작동 상태나 암호화되기 전의 평문 패킷을 들여다볼 수 있다.

Tracepoints (트레이스포인트)


  • 용도: 커널 개발자들이 소스 코드 내에 명시적으로 심어 놓은 정적 이벤트 훅(TRACE_EVENT)에 부착된다.
  • 특징: 커널 내부 구현이 리팩토링되어 함수 이름이 바뀌더라도 트레이스포인트는 안정적인 인터페이스(안정화된 명세)를 제공하므로, 상용 모니터링 도구를 빌드할 때 매우 선호된다.

Fentry & Fexit (BTF 기반 트레이싱)


  • 용도: Kprobe/Kretprobe의 현대적인 대체제다.
  • 특징: BTF(BPF Type Format) 메타데이터를 백하우스로 활용한다. 덕분에 구식 Kprobe처럼 CPU 레지스터(struct pt_regs)에서 수동으로 비트 시프트를 해가며 인자값을 꺼낼 필요 없이, 커널 함수의 실제 인자 이름과 타입을 소스 코드 그대로 직관적으로 참조할 수 있다. 성능 오버헤드 또한 kprobe에 비해 훨씬 적다.

2. 네트워킹 (Networking)


패킷을 검사, 수정, 라우팅, 통제하기 위한 강력한 네트워킹 프로그램 타입들이다.

XDP (eXpress Data Path)


  • 용도: 네트워크 카드(NIC)의 장치 드라이버 레이어, 즉 리눅스 커널이 패킷 메모리(sk_buff)를 할당하기도 전의 극초기 단계에서 작동한다.
  • 특징: 패킷당 소요되는 CPU 사이클을 극적으로 줄여 준다. 초당 수천만 개의 패킷이 쏟아지는 환경에서 DDoS 공격 패킷을 초고속으로 걸러 내거나(XDP_DROP), 초경량 로드 밸런싱을 수행하기에 최적의 위치다.

TC (Traffic Control, SCHED_CLS & SCHED_ACT)


  • 용도: 네트워크 카드 드라이버의 ingress(수신) 및 egress(송신) 큐 스케줄러 계층에 결합한다.
  • 특징: XDP보다 한 단계 후속 레이어지만, 커널이 정성스럽게 파싱해 놓은 소켓 버퍼 구조체(__sk_buff)를 사용하므로 IP 옵션, 프로토콜 헤더, 소켓 정보에 쉽게 접근할 수 있으며, 송신(egress) 패킷 제어도 가능하다는 강력한 이점을 지닌다.

Socket Filters & Cgroup Socket Programs


  • 용도: 특정 네트워크 소켓에 부착되거나, 특정 Cgroup(컨테이너 그룹) 내부에서 기동하는 모든 프로세스의 네트워크 연결 요청에 필터를 건다.
  • 특징: 쿠버네티스(Kubernetes) 환경에서 특정 파드(Pod) 집합 간의 트래픽을 격리하거나 모니터링할 때 핵심 인프라 기술로 사용된다.

3. 보안 및 정책 통제 (Security & LSM)


BPF LSM (Linux Security Modules)


  • 용도: 기존의 SELinux, AppArmor와 같은 보안 모듈이 사용하는 커널 내부의 보안 감사 훅(LSM hooks)에 eBPF 프로그램을 결합한다.
  • 특징: 파일 열기(file_open), 프로세스 생성(task_alloc), 네트워크 바인딩 등의 특권 동작이 일어나기 직전에 검사하여, 동적으로 승인하거나 거부(-EPERM)할 수 있다. 단순한 “사후 탐지 및 로깅"을 넘어, 실시간으로 해커의 악의적 커널 침투 동작을 차단(Enforce/Mitigate)할 수 있는 보안 혁신 기술로 주목받고 있다.

프로그램 타입 vs 부착 타입 (Program Type vs Attachment Type)


초보 eBPF 개발자들이 혼동하기 쉬운 두 개념의 차이는 다음과 같다.

  • 프로그램 타입 (prog_type): “이 프로그램의 인터페이스 규격은 무엇인가?“를 결정한다. (예: BPF_PROG_TYPE_CGROUP_SOCK_ADDR은 Cgroup 내부의 소켓 주소 조작용 프로그램임을 나타냄)
  • 부착 타입 (attach_type): “이 프로그램을 정확히 어느 시점에 결합할 것인가?“를 결정한다.

일부 프로그램 타입은 결합할 수 있는 곳이 단 하나뿐이어서 부착 타입을 명시하지 않아도 되지만, 많은 네트워킹 및 Cgroup 프로그램들은 한 타입이 여러 군데에 붙을 수 있기 때문에 부착 타입을 상세히 지정해야 한다.

예를 들어, 동일한 Cgroup 소켓 프로그램 타입이라도:

  • BPF_CGROUP_INET_INGRESS: 패킷이 들어올 때 실행
  • BPF_CGROUP_INET_EGRESS: 패킷이 나갈 때 실행

과 같이 바인딩 시점에 부착 타입을 구분하여 로드해 주어야 한다.

혁신적인 부착 관리자: BPF Link의 등장


초기 eBPF 아키텍처에서는 프로그램을 특정 훅에 부착하기 위해 로우레벨 파일 기술자(FD)나 원시 커널 이벤트를 직접 관리해야 했다. 이 방식은 프로그램을 업그레이드할 때 부착점이 잠깐 유실되거나, 로더 프로세스가 강제 종료되었을 때 커널 훅이 엉뚱하게 살아남아 좀비처럼 남는 등 리소스 누수와 안전성 문제를 안고 있었다.

이 문제를 극복하기 위해 최신 커널은 **BPF Link (struct bpf_link)**라는 강력한 추상화 개체를 도입했다.

┌──────────────────────────────┐
│          BPF Link            │ (원자적 생명주기 관리)
├──────────────────────────────┤
│  ┌────────────────────────┐  │
│  │   eBPF Program (.o)    │  │ ── (동적 업데이트 가능)
│  └────────────────────────┘  │
│              │ (안전한 결합)  │
│              ▼                │
│  ┌────────────────────────┐  │
│  │   Kernel Hook Point    │  │ ── (XDP, Kprobe, LSM 등)
│  └────────────────────────┘  │
└──────────────────────────────┘

BPF Link의 핵심 가치는 다음과 같다.

  1. 원자적 교체 (Atomic Swap): 서비스 중단 없이 실행 중인 eBPF 프로그램을 새로운 버전의 프로그램으로 안전하게 교체할 수 있다. 두 프로그램 간의 바인딩 교체가 원자적으로 이루어지므로 패킷 유실이나 트레이싱 공백이 발생하지 않는다.
  2. 자동 정리 (Auto-Cleanup): BPF Link를 소유하고 있는 사용자 공간 로더의 파일 기술자가 닫히면(프로세스가 종료되면), 커널은 해당 Link에 매핑된 커널 훅과 eBPF 프로그램의 연결을 자동으로 해제하고 리소스를 깨끗이 청소해 준다.
  3. 고정(Pinning) 지원: 반대로 사용자 프로그램이 종료되더라도 eBPF 기능을 백그라운드에서 영구히 유지하고 싶다면, BPF Link를 /sys/fs/bpf 가상 파일 시스템에 파일 형태로 고정(Pin)해 두어 파일 생명주기로 기능을 통제할 수 있다.

느낀점 / 결론


이번 7장을 정리하며 eBPF가 리눅스 운영체제 생태계 전체를 뒤흔들 수 있었던 진정한 원동력이 어디서 나오는지 실감할 수 있었다.

eBPF의 진정한 파워는 단순히 “가볍고 빠른 어셈블리 인터프리터가 커널에서 돈다"는 기계적 사실에만 있는 것이 아니다. 커널 구석구석에 뻗어 있는 다양한 서브시스템(트레이싱, 네트워킹, 보안 모듈)들의 게이트웨이가 prog_typeattach_type이라는 정형화된 약속을 통해 가상 머신과 아름답게 조화를 이루도록 설계되어 있기 때문이다.

특히 현대적인 BPF Link 기술은 개발자가 로우 레벨 바인딩 제어의 위험한 복잡성에서 벗어나 안정적이고 우아한 소프트웨어 라이프사이클을 누릴 수 있게 해 준 커널의 따뜻한 배려와도 같다.

커널 내부의 거대한 숲(네트워크 드라이버부터 LSM 보안 통제선까지)을 eBPF라는 렌즈를 통해 통일된 규격으로 바라보고 자유자재로 해킹할 수 있다는 사실은, 시스템 프로그래머에게 말할 수 없는 지적 유희와 엄청난 무기를 쥐여 준 셈이다. 이제 이 무기를 쥐었으니, 다음 장부터 펼쳐질 본격적인 네트워킹(8장)과 보안(9장)의 거친 바다로 나아갈 준비가 완벽히 끝났다고 확신한다.

참고 문헌


>> Home