프로그램 유형
프로그램 유형 (Program Types)
이 문서는 BPF 프로그램 유형을 소개하고, 네트워킹에서 핵심인 두 가지 유형인 XDP와 tc BPF 프로그램을 심층 설명해요. 각 프로그램 유형의 아키텍처, 개념, 사용 사례를 다룹니다.
출처: Program Types
본문
이 글을 쓰는 시점에 18가지의 서로 다른 BPF 프로그램 유형이 있으며, 네트워킹용 주요 유형 두 가지(XDP BPF 프로그램과 tc BPF 프로그램)가 아래 하위 섹션에서 더 설명돼요. 두 프로그램 유형에 대한 LLVM, iproute2 및 기타 도구의 광범위한 사용 예시는 toolchain 섹션 전반에 퍼져 있으며 여기서 다루지 않아요. 대신 이 섹션은 그들의 아키텍처, 개념, 사용 사례에 초점을 맞춥니다.
XDP
XDP는 eXpress Data Path를 뜻하며, 리눅스 커널에서 고성능 프로그래머블 패킷 처리를 가능하게 하는 BPF 프레임워크를 제공해요. BPF 프로그램을 소프트웨어에서 가능한 가장 이른 시점, 즉 네트워크 드라이버가 패킷을 수신하는 순간에 실행해요.
fast-path의 이 시점에 드라이버는 수신 링에서 패킷을 막 집었으며, 패킷을 네트워킹 스택 위로 올리기 위한 skb 할당이나 GRO 엔진으로의 푸시 같은 비용이 드는 작업을 수행하지 않았어요. 따라서 XDP BPF 프로그램은 패킷이 CPU 처리에 사용 가능해지는 가장 이른 시점에 실행돼요.
XDP는 리눅스 커널과 그 인프라와 함께 작동하는데, 즉 사용자 공간에서만 동작하는 다양한 네트워킹 프레임워크처럼 커널을 우회하지 않아요. 패킷을 커널 공간에 유지하는 데는 몇 가지 주요 장점이 있어요:
- XDP는 상류에서 개발된 모든 커널 네트워킹 드라이버, 사용자 공간 도구, 또는 라우팅 테이블·소켓 같은 사용 가능한 다른 커널 내 인프라를 BPF 헬퍼 호출 자체에서 재사용할 수 있어요.
- 커널 공간에 상주하므로 XDP는 하드웨어 접근에 대해 커널의 나머지 부분과 같은 보안 모델을 가져요.
- 처리된 패킷이 이미 커널에 있으므로 커널/사용자 공간 경계를 넘을 필요가 없고, 따라서 컨테이너가 사용하는 네임스페이스나 커널의 네트워킹 스택 자체 같은 다른 커널 내 엔티티로 패킷을 유연하게 전달할 수 있어요. 이는 Meltdown과 Spectre 시대에 특히 관련돼요.
- XDP에서 커널의 견고하고 널리 사용되며 효율적인 TCP/IP 스택으로 패킷을 푸시하는 것은 간단하게 가능하고, 완전한 재사용을 허용하며 사용자 공간 프레임워크처럼 별도의 TCP/IP 스택을 유지할 필요가 없어요.
- BPF 사용은 완전한 프로그래밍 가능성을 허용하며, 커널의 시스템 콜 ABI와 같은 '사용자 공간을 절대 깨지 않음' 보장과 안정적인 ABI를 유지하고, 모듈과 비교해 커널 동작의 안정성을 보장하는 BPF verifier 덕분에 안전 조치도 제공해요.
- XDP는 네트워크 트래픽 중단이나 커널/시스템 재부팅 없이 런타임 중 프로그램을 원자적으로 교체하는 것을 간단히 허용해요.
- XDP는 커널에 통합된 워크로드의 유연한 구조화를 허용해요. 예를 들어 "busy polling" 또는 "interrupt driven" 모드로 작동할 수 있어요. XDP에 CPU를 명시적으로 전용화할 필요는 없어요. 특수 하드웨어 요구사항이 없고 hugepage에 의존하지 않아요.
- XDP는 서드파티 커널 모듈이나 라이선싱이 필요 없어요. 장기적 아키텍처 솔루션이며 리눅스 커널의 핵심 부분이고 커널 커뮤니티가 개발해요.
- XDP는 이미 4.8 이상과 동등한 커널을 실행하는 주요 배포판에서 어디에나 활성화·배포되며, 대부분의 주요 10G 이상 네트워킹 드라이버를 지원해요.
드라이버에서 BPF를 실행하는 프레임워크로서 XDP는 패킷이 선형으로 배치되고 BPF 프로그램이 읽고 쓸 수 있는 단일 DMA 페이지에 맞음을 추가로 보장해요. XDP는 또한 프로그램이 bpf_xdp_adjust_head() BPF 헬퍼로 커스텀 캡슐화 헤더를 구현하거나 bpf_xdp_adjust_meta()로 패킷 앞에 커스텀 메타데이터를 추가할 수 있도록 256바이트의 추가 헤드룸(headroom)이 사용 가능함을 보장해요.
프레임워크는 아래 섹션에서 더 설명하는 XDP 액션 코드를 포함하며, BPF 프로그램이 이를 반환해 드라이버에 패킷을 어떻게 처리할지 지시하고, XDP 계층에서 실행 중인 BPF 프로그램을 원자적으로 교체하는 가능성을 제공해요. XDP는 설계상 고성능에 맞춰져 있어요. BPF는 '직접 패킷 접근(direct packet access)'을 통해 패킷 데이터에 접근할 수 있으며, 이는 프로그램이 레지스터에 직접 데이터 포인터를 보유하고 내용을 레지스터로 로드하거나 거기서 패킷으로 각각 쓴다는 뜻이에요.
BPF 컨텍스트로 BPF 프로그램에 전달되는 XDP의 패킷 표현은 다음과 같아요:
struct xdp_buff {
void *data;
void *data_end;
void *data_meta;
void *data_hard_start;
struct xdp_rxq_info *rxq;
};
data는 페이지의 패킷 데이터 시작점을 가리키고, 이름이 시사하듯 data_end는 패킷 데이터의 끝을 가리켜요. XDP가 헤드룸을 허용하므로 data_hard_start는 페이지에서 가능한 최대 헤드룸 시작점을 가리키는데, 즉 패킷을 캡슐화해야 할 때 data가 bpf_xdp_adjust_head()를 통해 data_hard_start에 더 가깝게 이동한다는 뜻이에요. 같은 BPF 헬퍼 함수는 역캡슐화도 허용하며, 이 경우 data가 data_hard_start에서 더 멀어져요.
data_meta는 처음에 data와 같은 위치를 가리키지만 bpf_xdp_adjust_meta()가 일반 커널 네트워킹 스택에는 보이지 않지만 XDP에서 skb로 전달되므로 tc BPF 프로그램이 읽을 수 있는 커스텀 메타데이터 공간을 제공하기 위해 포인터를 data_hard_start 쪽으로 이동할 수 있어요. 반대로 같은 BPF 헬퍼 함수로 data_meta를 다시 data_hard_start에서 멀어지게 이동해 커스텀 메타데이터를 제거하거나 크기를 줄일 수 있어요. data_meta는 tc BPF 프로그램에서 접근할 수 있는 skb->cb[] 컨트롤 블록처럼 tail call 사이에 상태를 전달하는 데만 사용할 수도 있어요.
이것은 struct xdp_buff 패킷 포인터에 대해 다음 관계·불변식을 제공해요: data_hard_start <= data_meta <= data < data_end.
rxq 필드는 링 설정 시점(런타임이 아닌)에 채워지는 수신 큐별 추가 메타데이터를 가리켜요:
struct xdp_rxq_info {
struct net_device *dev;
u32 queue_index;
u32 reg_state;
} ____cacheline_aligned;
BPF 프로그램은 queue_index와 ifindex 같은 netdevice 자체의 추가 데이터를 검색할 수 있어요.
BPF 프로그램 반환 코드
XDP BPF 프로그램을 실행한 후 프로그램에서 판결(verdict)이 반환되어 드라이버에 패킷을 다음에 어떻게 처리할지 알려줘요. linux/bpf.h 시스템 헤더 파일에 사용 가능한 모든 반환 판결이 열거돼 있어요:
enum xdp_action {
XDP_ABORTED = 0,
XDP_DROP,
XDP_PASS,
XDP_TX,
XDP_REDIRECT,
};
이름이 시사하듯 XDP_DROP은 추가 리소스를 낭비하지 않고 드라이버 수준에서 바로 패킷을 드롭ㅈ해요. 이는 특히 DDoS 완화 메커니즘 또는 일반적인 방화벽을 구현하는 BPF 프로그램에 유용해요. XDP_PASS 반환 코드는 패킷이 커널의 네트워킹 스택으로 전달되도록 허용됨을 의미해요. 즉 이 패킷을 처리하던 현재 CPU가 skb를 할당하고 채운 다음 GRO 엔진으로 전달해요. 이는 XDP 없이의 기본 패킷 처리 동작과 동등해요. XDP_TX로 BPF 프로그램은 방금 도착한 같은 NIC 밖으로 네트워크 패킷을 다시 전송하는 효율적인 옵션을 가져요. 이는 보통 소수의 노드가 예를 들어 클러스터에서 방화벽 후속 로드밸런싱을 구현하고, XDP BPF에서 패킷을 다시 쓴 뒤 헤어핀드(hairpinned) 로드밸런서로 작동해 들어오는 패킷을 스위치로 다시 보낼 때 유용해요. XDP_REDIRECT는 XDP 패킷을 전송할 수 있다는 점에서 XDP_TX와 유사하지만, 다른 NIC를 통해 전송해요. XDP_REDIRECT의 또 다른 옵션은 BPF cpumap으로 리다이렉트하는 것인데, 즉 NIC 수신 큐에서 XDP를 서빙하는 CPU가 계속 그렇게 하면서 상위 커널 스택 처리를 위해 패킷을 원격 CPU로 푸시할 수 있어요. 이는 XDP_PASS와 유사하지만, XDP BPF 프로그램이 상위 계층으로 푸시하는 데 현재 패킷에 일시적으로 작업을 소비하는 대신 들어오는 높은 부하를 계속 서빙할 수 있다는 능력이 있어요. 마지막으로 XDP_ABORTED는 프로그램의 예외 같은 상태를 나타내는 데 쓰이며 XDP_DROP과 같은 동작을 하되 trace_xdp_exception 트레이스포인트를 통과시키는 점만 달라, 추가로 모니터링해 오동작을 감지할 수 있어요.
XDP 사용 사례
이 하위 섹션은 XDP의 주요 사용 사례 몇 가지를 제시해요. 목록은 완전하지 않으며, XDP와 BPF가 가능하게 하는 프로그래밍 가능성과 효율성 덕분에 매우 특정한 사용 사례를 해결하도록 쉽게 적응할 수 있어요.
- DDoS 완화, 방화벽
XDP BPF의 기본 기능 중 하나는 이 이른 단계에서
XDP_DROP으로 패킷을 드롭하도록 드라이버에 지시하는 것이며, 이는 패킷당 비용이 극히 낮은 어떤 종류의 효율적인 네트워크 정책 적용도 허용해요. 이는 어떤 종류의 DDoS 공격에도 대처해야 하는 상황에 이상적이며, 더 일반적으로는 BPF에서 거의 오버헤드 없이 어떤 종류의 방화벽 정책도 구현할 수 있어요. 예를 들어 독립형 어플라이언스(예:XDP_TX로 '깨끗한' 트래픽을 스크러빙)로 또는 엔드 호스트 자체를 보호하는 노드에 널리 배포(좋은 트래픽에 대해XDP_PASS또는 cpumapXDP_REDIRECT통해)하는 경우 모두요. 오프로드된 XDP는 이미 작은 패킷당 비용을 전적으로 NIC로 옮겨 라인레이트(line-rate)로 처리함으로써 이를 한 단계 더 끌어올려요. - 포워딩 및 로드밸런싱
XDP의 또 다른 주요 사용 사례는
XDP_TX또는XDP_REDIRECT액션을 통한 패킷 포워딩과 로드밸런싱이에요. XDP 계층에서 실행되는 BPF 프로그램이 패킷을 임의로 변조할 수 있으며, 패킷을 다시 내보내기 전에 임의로 캡슐화·역캡슐화하도록 패킷의 헤드룸을 늘리거나 줄이는 BPF 헬퍼 함수도 사용할 수 있어요.XDP_TX로 원래 도착한 것과 같은 네트워킹 장치 밖으로 패킷을 내보내는 헤어핀드 로드밸런서를 구현할 수 있고,XDP_REDIRECT액션으로 전송을 위해 다른 NIC로 전달할 수도 있어요. 후자의 반환 코드는 BPF의 cpumap과 결합해 로컬 스택으로 올리는 패킷을 로드밸런싱할 수도 있지만, 원격의 비-XDP 처리 CPU에서 하게 돼요. - 스택 전 필터링/처리
정책 적용 외에도 XDP는
XDP_DROP사례로 커널의 네트워킹 스택을 하드닝하는 데 쓸 수 있는데, 즉 로컬 노드에 무관한 패킷을 네트워킹 스택이 보기 전에 가장 이른 시점에 드롭할 수 있어요. 예를 들어 노드가 TCP 트래픽만 서빙한다는 걸 알면 UDP, SCTP 또는 다른 L4 트래픽을 바로 드롭할 수 있어요. 이는 패킷이 드롭으로 판정되기 전에 GRO 엔진, 커널의 흐름 분해기(flow dissector) 등의 여러 엔티티를 거칠 필요가 없어 커널의 공격 표면을 줄이는 장점이 있어요. XDP의 이른 처리 단계 덕분에 이는 커널의 네트워킹 스택에 이 패킷이 네트워킹 장치에서 결코 보이지 않았다고 효과적으로 '속여요'. 또한 스택 수신 경로의 잠재적 버그가 드러나 'ping of death' 같은 시나리오를 일으킨다면, 커널을 재부팅하거나 서비스를 재시작하지 않고 XDP로 그러한 패킷을 바로 드롭할 수 있어요. 이러한 프로그램을 원자적으로 교체해 나쁜 패킷 드롭을 강제할 수 있으므로 호스트에서 네트워크 트래픽이 전혀 중단되지 않아요. 스택 전 처리의 또 다른 사용 사례는 커널이 패킷에 대한skb를 아직 할당하지 않았으므로 BPF 프로그램이 패킷을 자유롭게 수정하고, 다시 스택에 네트워킹 장치가 이렇게 수신했다고 '속임'을 허용하는 거예요. 이는 커스텀 패킷 변조·캡슐화 프로토콜이 있는 경우를 허용하며, GRO가 커스텀 프로토콜을 알지 못해 어떤 집계도 수행할 수 없는 GRO 집계에 들어가기 전에 패킷을 역캡슐화할 수 있어요. XDP는 또한 패킷 앞에 메타데이터(비패킷 데이터)를 푸시할 수 있어요. 이는 일반 커널 스택에 '보이지' 않고, GRO 집계될 수 있으며(일치하는 메타데이터에 대해), 나중에skb컨텍스트를 사용할 수 있는 tc ingress BPF 프로그램과 협조해 처리돼요(예: 다양한 skb 필드 설정). - 흐름 샘플링, 모니터링 XDP는 패킷 모니터링, 샘플링 또는 기타 네트워크 분석 같은 경우에도 쓸 수 있어요. 예를 들어 경로의 중간 노드나 엔드 호스트에서 앞서 언급한 사용 사례와 결합해서요. 복잡한 패킷 분석을 위해 XDP는 네트워크 패킷(잘리거나 전체 페이로드)과 커스텀 메타데이터를 리눅스 perf 인프라가 제공하는 빠른 잠금 없는 per-CPU 메모리 매핑 링 버퍼로 효율적으로 푸시해 사용자 공간 애플리케이션에 전달하는 시설을 제공해요. 이는 흐름의 초기 데이터만 분석하고 좋은 트래픽으로 판정되면 모니터링을 우회하는 경우도 허용해요. BPF가 가져온 유연성 덕분에 어떤 종류의 커스텀 모니터링이나 샘플링도 구현할 수 있어요.
XDP BPF 프로덕션 사용의 한 예는 Facebook의 SHIV와 Droplet 인프라로, 이는 L4 로드밸런싱과 DDoS 대책을 구현해요. 프로덕션 인프라를 netfilter의 IPVS(IP Virtual Server)에서 XDP BPF로 마이그레이션하면 이전 IPVS 설정보다 10배 속도 향상을 허용했어요. 이는 처음 netdev 2.1 컨퍼런스에서 발표됐어요:
- Slides: https://netdevconf.info/2.1/slides/apr6/zhou-netdev-xdp-2017.pdf
- Video: https://youtu.be/YEU2ClcGqts
또 다른 예는 XDP를 Cloudflare의 DDoS 완화 파이프라인에 통합한 것이며, 원래는 iptables의 xt_bpf 모듈을 통한 공격 시그니처 매칭에 cBPF 대신 eBPF를 사용했어요. iptables 사용으로 인해 공격 중 심각한 성능 문제가 발생했고, 사용자 공간 우회 솔루션이 필요해 보였지만 NIC를 busy poll하고 커널 스택에 패킷을 재주입하는 비용 같은 단점도 있었어요. eBPF와 XDP로의 마이그레이션은 커널 안에서 직접 고성능 프로그래머블 패킷 처리를 가짐으로써 두 세계의 장점을 결합했어요:
- Slides: https://netdevconf.info/2.1/slides/apr6/bertin_Netdev-XDP.pdf
- Video: https://youtu.be/7OuOukmuivg
XDP 동작 모드
XDP는 세 가지 동작 모드가 있으며 '네이티브(native)' XDP가 기본 모드예요. XDP라고 말할 때는 보통 이 모드를 의미해요.
- 네이티브 XDP (Native XDP) 기본 모드로, XDP BPF 프로그램이 네트워킹 드라이버의 이른 수신 경로에서 직접 실행돼요. 10G 이상을 위한 가장 널리 쓰이는 NIC 대부분이 이미 네이티브 XDP를 지원해요.
- 오프로드 XDP (Offloaded XDP) 오프로드 XDP 모드에서 XDP BPF 프로그램은 호스트 CPU에서 실행되는 대신 NIC로 직접 오프로드돼요. 따라서 이미 극히 낮은 패킷당 비용이 전적으로 호스트 CPU에서 떨어져 NIC에서 실행되며, 네이티브 XDP에서 실행하는 것보다 더 높은 성능을 제공해요. 이 오프로드는 보통 멀티스레드·멀티코어 흐름 프로세서를 포함하는 SmartNIC가 구현하며, 커널 내 JIT 컴파일러가 이를 위해 BPF를 네이티브 명령어로 변환해요. 오프로드 XDP를 지원하는 드라이버는 보통 일부 BPF 헬퍼가 아직 없거나 네이티브 모드에서만 사용 가능한 경우를 위해 네이티브 XDP도 지원해요.
- 제네릭 XDP (Generic XDP) 아직 네이티브나 오프로드 XDP를 구현하지 않은 드라이버를 위해 커널은 제네릭 XDP 옵션을 제공하는데, 이는 네트워킹 스택에서 훨씬 나중 시점에 실행되므로 드라이버 변경이 필요 없어요. 이 설정은 주로 커널의 XDP API에 대해 프로그램을 작성·테스트하려는 개발자를 대상으로 하며, 네이티브나 오프로드 모드의 성능 속도로 작동하지 않아요. 프로덕션 환경에서의 XDP 사용에는 네이티브 또는 오프로드 모드가 더 적합하며 권장되는 XDP 실행 방식이에요.
드라이버 지원
네이티브 XDP를 지원하는 드라이버
네이티브 XDP를 지원하는 드라이버 목록은 아래 표에서 찾을 수 있어요. 인터페이스의 해당 네트워크 드라이버 이름은 다음과 같이 결정할 수 있어요:
# ethtool -i eth0
driver: nfp
[...]
| 벤더 | 드라이버 | XDP 지원 | | Amazon | ena | >= 5.6 | | Aquantia | atlantic | >= 5.19 | | Broadcom | bnxt_en | >= 4.11 | | Cavium | thunderx | >= 4.12 | | Engleder | tsne (TSN Express Path) | >= 6.3 | | Freescale | dpaa | >= 5.11 | | | dpaa2 | >= 5.0 | | | enetc | >= 5.13 | | | fec_enet | >= 6.2 | | Fungible | fun | >= 5.18 | | Google | gve | >= 6.4 | | Intel | ice | >= 5.5 | | | igb | >= 5.10 | | | igc | >= 5.13 | | | i40e | >= 4.13 | | | ixgbe | >= 4.12 | | | ixgbevf | >= 4.17 | | Marvell | mvneta | >= 5.5 | | | mvpp2 | >= 5.9 | | | otx2 | >= 5.16 | | Mediatek | mtk | >= 6.0 | | Mellanox | mlx4 | >= 4.8 | | | mlx5 | >= 4.9 | | Microchip | lan966x | >= 6.2 | | Microsoft | hv_netvsc (Hyper-V) | >= 5.6 | | | mana | >= 5.17 | | Netronome | nfp | >= 4.10 | | Others | bonding | >= 5.15 | | | netdevsim | >= 4.16 | | | tun/tap | >= 4.14 | | | virtio_net | >= 4.10 | | | xen-netfront | >= 5.9 | | | veth | >= 4.19 | | QLogic | qede | >= 4.10 | | Socionext | netsec | >= 5.3 | | Solarflare | SFC Efx | >= 5.5 | | STMicro | stmmac | >= 5.13 | | Texas Instruments | cpsw | >= 5.3 | | VMware | vmxnet3 | >= 6.6 |
오프로드 XDP를 지원하는 드라이버
- Netronome
- nfp
참고
XDP 프로그램 작성·로드 예시는 해당 도구 아래의 Development Tools 섹션에 포함돼 있어요.
tc (트래픽 컨트롤)
XDP 같은 다른 프로그램 유형 외에도 BPF는 네트워킹 데이터패스의 커널 tc(트래픽 컨트롤) 계층에서도 사용할 수 있어요. 높은 수준에서 XDP BPF 프로그램을 tc BPF 프로그램과 비교할 때 세 가지 주요 차이가 있어요:
- BPF 입력 컨텍스트는
xdp_buff가 아닌sk_buff예요. 커널의 네트워킹 스택이 패킷을 받으면 XDP 계층 이후 버퍼를 할당하고 패킷에 대한 메타데이터를 저장하도록 패킷을 파싱해요. 이 표현을sk_buff라고 해요. 이 구조는 BPF 입력 컨텍스트에 노출되므로 tc ingress 계층의 BPF 프로그램이 스택이 패킷에서 추출한 메타데이터를 사용할 수 있어요. 이는 유용하지만, 스택이 이 할당과 메타데이터 추출을 수행하고 패킷이 tc 훅에 도달할 때까지 처리하는 데 따르는 관련 비용이 있어요. 정의상xdp_buff는 XDP 훅이 이 작업 전에 호출되므로 이 메타데이터에 접근할 수 없어요. 이는 XDP와 tc 훅 사이의 성능 차이에 크게 기여해요. 따라서 tc BPF 훅에 부착된 BPF 프로그램은 예를 들어 skb의mark,pkt_type,protocol,priority,queue_mapping,napi_id,cb[]배열,hash,tc_classid또는tc_index, vlan 메타데이터, XDP가 전달한 커스텀 메타데이터 및 다양한 다른 정보를 읽거나 쓸 수 있어요. tc BPF에서 사용되는struct __sk_buffBPF 컨텍스트의 모든 멤버는linux/bpf.h시스템 헤더에 정의돼 있어요. 일반적으로sk_buff는xdp_buff와 완전히 다른 성질이며, 둘 다 장단점이 있어요. 예를 들어sk_buff의 경우 관련 메타데이터를 변조하기가 상당히 간단하다는 장점이 있지만, 프로토콜 특정 정보(예: GSO 관련 상태)를 많이 포함해 패킷 데이터만 다시 써서 간단히 프로토콜을 전환하기 어려워요. 이는 스택이 매번 패킷 내용에 접근하는 비용을 들이지 않고 메타데이터 기반으로 패킷을 처리하기 때문이에요. 따라서sk_buff내부도 올바르게 변환되도록 처리하는 BPF 헬퍼 함수의 추가 변환이 필요해요. 그러나xdp_buff의 경우 커널이 아직sk_buff조차 할당하지 않은 이른 단계에서 오므로 이런 문제가 없어 어떤 종류의 패킷 다시 쓰기도 간단히 실현할 수 있어요. 하지만xdp_buff의 경우 이 단계에서sk_buff메타데이터를 변조에 사용할 수 없다는 단점이 있어요. 후자는 XDP BPF에서 tc BPF로 커스텀 메타데이터를 전달함으로써 극복돼요. 이런 방식으로 사용 사례에 맞게 두 유형의 보완 프로그램을 운영함으로써 각 프로그램 유형의 한계를 극복할 수 있어요. - XDP와 비교해 tc BPF 프로그램은 네트워킹 데이터패스의 ingress와 egress 지점에서 모두 트리거될 수 있는 반면, XDP의 경우 ingress에서만 가능해요. 커널의 두 훅 지점
sch_handle_ingress()와sch_handle_egress()는 각각__netif_receive_skb_core()와__dev_queue_xmit()에서 트리거돼요. 후자 둘은 XDP를 제외하고 노드로 들어오거나 나가는 모든 네트워크 패킷에 대해 트리거되는 데이터패스의 주요 수신·전송 함수로, 이 훅 지점에서 tc BPF 프로그램에 대한 완전한 가시성을 허용해요. - tc BPF 프로그램은 네트워킹 스택의 일반 계층에 있는 훅 지점에서 실행되므로 드라이버 변경이 필요 없어요. 따라서 어떤 유형의 네트워킹 장치에도 부착할 수 있어요. 이는 유연성을 제공하지만 네이티브 XDP 계층에서 실행하는 것과 비교해 성능을 맞바꿔요. 하지만 tc BPF 프로그램은 GRO가 실행된 후이지만 프로토콜 처리, iptables PREROUTING 같은 전통적인 iptables 방화벽 또는 nftables ingress 훅 등 다른 패킷 처리 전에 일반 커널 네트워킹 데이터패스의 가장 이른 지점에 여전히 오게 돼요. 마찬가지로 egress에서 tc BPF 프로그램은 전송을 위해 패킷을 드라이버 자체에 넘기기 직전의 가장 늦은 지점에서 실행되는데, 즉 iptables POSTROUTING 같은 전통적인 iptables 방화벽 훅 이후이지만 패킷을 커널의 GSO 엔진에 넘기기 전이에요. 드라이버 변경이 필요한 한 가지 예외는 오프로드된 tc BPF 프로그램이며, 보통 SmartNIC가 오프로드 XDP와 유사하게 제공하되 BPF 입력 컨텍스트, 헬퍼 함수, 판결 코드의 차이로 인해 기능 세트가 달라요.
tc 계층에서 실행되는 BPF 프로그램은 cls_bpf 분류기에서 실행돼요. tc 용어가 BPF 부착 지점을 "분류기(classifier)"로 설명하지만, 이는 cls_bpf가 할 수 있는 것을 과소평가하므로 약간 오해의 소지가 있어요. 즉 skb 메타데이터와 패킷 데이터를 읽을 수 있을 뿐만 아니라 둘 다 임의로 변조하고 액션 판결로 tc 처리를 종료할 수 있는 완전히 프로그래머블한 패킷 프로세서예요. 따라서 cls_bpf는 tc BPF 프로그램을 관리·실행하는 자족적(self-contained) 엔티티로 볼 수 있어요.
cls_bpf는 하나 이상의 tc BPF 프로그램을 보유할 수 있어요. Cilium이 cls_bpf 프로그램을 배포하는 경우 주어진 훅에 대해 direct-action 모드에서 단일 프로그램만 부착해요. 일반적으로 전통적인 tc 체계에서는 분류기와 액션 모듈이 분리되어 있으며, 분류기가 일치하면 트리거되는 하나 이상의 액션이 부착돼 있어요. 소프트웨어 데이터패스에서 tc를 사용하는 현대적인 세계에서 이 모델은 복잡한 패킷 처리에 잘 확장되지 않아요. cls_bpf에 부착된 tc BPF 프로그램은 완전히 자족적이므로 파싱과 액션 과정을 단일 유닛으로 효과적으로 융합해요. cls_bpf의 direct-action 모드 덕분에 tc 액션 판결을 반환하고 처리 파이프라인을 즉시 종료해요. 이를 통해 액션의 선형 반복을 피함으로써 네트워킹 데이터패스에서 확장 가능한 프로그래머블 패킷 처리를 구현할 수 있어요. cls_bpf는 tc 계층에서 이런 fast-path가 가능한 유일한 "분류기" 모듈이에요.
XDP BPF 프로그램처럼 tc BPF 프로그램은 네트워크 트래픽을 중단하거나 서비스를 재시작하지 않고 cls_bpf를 통해 런타임에 원자적으로 업데이트할 수 있어요.
cls_bpf 자체가 부착될 수 있는 tc ingress와 egress 훅 둘 다 sch_clsact라는 의사 qdisc가 관리해요. 이는 ingress와 egress tc 훅 둘 다 관리할 수 있으므로 ingress qdisc의 드롭인 교체이자 적절한 상위 집합(superset)이에요. __dev_queue_xmit()의 tc egress 훅은 커널의 qdisc root lock 하에서 실행되지 않는다는 점을 강조하는 것이 중요해요. 따라서 tc ingress·egress 훅 둘 다 fast-path에서 잠금 없이 실행돼요. 어느 경우든 선점(preemption)이 비활성화되고 RCU 읽기 측에서 실행돼요.
일반적으로 egress에서는 sch_mq, sch_fq, sch_fq_codel 또는 sch_htb 같은 qdisc가 netdevice에 부착되며, 그중 일부는 하위 클래스를 포함하는 classful qdisc로 패킷을 어디로 디멀티플렉싱할지 판결을 결정하는 패킷 분류 메커니즘이 필요해요. 이는 존재하는 경우 tc 분류기로 호출되는 tcf_classify() 호출로 처리돼요. cls_bpf도 그러한 경우에 부착·사용할 수 있어요. 이러한 연산은 보통 qdisc root lock 하에서 발생하며 잠금 경합이 있을 수 있어요. 그러나 sch_clsact qdisc의 egress 훅은 훨씬 이른 지점에서 오므로 그에 속하지 않고 기존 egress qdisc와 완전히 독립적으로 작동해요. 따라서 sch_htb 같은 경우 sch_clsact qdisc가 qdisc root lock 밖에서 tc BPF를 통해 무거운 패킷 분류를 수행하고, 거기서 skb->mark 또는 skb->priority를 설정해 sch_htb가 root lock 하의 값비싼 패킷 분류 없이 평면 매핑만 요구하도록 하여 경합을 줄일 수 있어요.
오프로드된 tc BPF 프로그램은 sch_clsact가 cls_bpf와 결합된 경우 지원되며, 이전에 로드된 BPF 프로그램이 NIC에서 네이티브로 실행되도록 SmartNIC 드라이버가 JIT했어요. direct-action 모드로 작동하는 cls_bpf 프로그램만 오프로드가 지원돼요. cls_bpf는 단일 프로그램만 오프로드할 수 있고 여러 프로그램은 오프로드할 수 없어요. 또한 ingress 훅만 BPF 프로그램 오프로드를 지원해요.
하나의 cls_bpf 인스턴스는 내부적으로 여러 tc BPF 프로그램을 보유할 수 있어요. 이 경우 TC_ACT_UNSPEC 프로그램 반환 코드가 그 목록의 다음 tc BPF 프로그램으로 실행을 계속해요. 하지만 이는 여러 프로그램이 패킷을 반복해서 파싱해야 해 성능이 저하되는 단점이 있어요.
BPF 프로그램 반환 코드
tc ingress·egress 훅 둘 다 tc BPF 프로그램이 사용할 수 있는 같은 액션 반환 판결을 공유해요. 이들은 linux/pkt_cls.h 시스템 헤더에 정의돼 있어요:
#define TC_ACT_UNSPEC (-1)
#define TC_ACT_OK 0
#define TC_ACT_SHOT 2
#define TC_ACT_STOLEN 4
#define TC_ACT_REDIRECT 7
시스템 헤더 파일에 두 훅에서도 사용되는 TC_ACT_* 판결이 몇 가지 더 있어요. 하지만 이들은 위의 것들과 같은 의미를 공유해요. 즉 tc BPF 관점에서 TC_ACT_OK와 TC_ACT_RECLASSIFY는 같은 의미이며, 세 개의 TC_ACT_STOLEN, TC_ACT_QUEUED, TC_ACT_TRAP opcode도 마찬가지예요. 따라서 이 경우 두 그룹에 대해 TC_ACT_OK와 TC_ACT_STOLEN opcode만 설명해요.
TC_ACT_UNSPEC부터 시작해요. 이는 "지정되지 않은 액션(unspecified action)"의 의미이며 세 경우에 사용돼요: i) 오프로드된 tc BPF 프로그램이 부착되고 오프로드 프로그램의 cls_bpf 표현이 TC_ACT_UNSPEC을 반환하는 tc ingress 훅이 실행될 때, ii) 다중 프로그램 경우 cls_bpf의 다음 tc BPF 프로그램으로 계속하기 위해. 후자는 i) 지점의 오프로드 tc BPF 프로그램과도 조합해 작동하며, 여기서의 TC_ACT_UNSPEC이 오프로드되지 않은 경우에만 실행되는 다음 tc BPF 프로그램으로 계속해요. 마지막으로 iii) TC_ACT_UNSPEC은 단일 프로그램 경우에도 단순히 추가 부작용 없이 커널이 skb로 계속하도록 지시하는 데 사용돼요. TC_ACT_UNSPEC은 두 액션 코드 모두 skb를 ingress에서 스택의 상위 계층으로, egress에서 전송을 위해 네트워킹 장치 드라이버로 각각 전달한다는 점에서 TC_ACT_OK 액션 코드와 매우 유사해요. TC_ACT_OK와의 유일한 차이는 TC_ACT_OK가 tc BPF 프로그램이 설정한 classid를 기반으로 skb->tc_index를 설정한다는 점이에요. 후자는 tc BPF 프로그램 자체가 BPF 컨텍스트의 skb->tc_classid를 통해 설정해요.
TC_ACT_SHOT은 커널에 패킷을 드롭하도록 지시하며, 즉 네트워킹 스택의 상위 계층이 ingress에서 skb를 결코 볼 수 없고 마찬가지로 egress에서 패킷이 전송을 위해 제출되지 않아요. TC_ACT_SHOT과 TC_ACT_STOLEN은 몇 가지 차이만 있고 본질적으로 유사해요: TC_ACT_SHOT은 kfree_skb()를 통해 skb가 해제됐음을 커널에 나타내고 즉각적인 피드백을 위해 호출자에게 NET_XMIT_DROP을 반환하는 반면, TC_ACT_STOLEN은 consume_skb()를 통해 skb를 해제하고 NET_XMIT_SUCCESS를 통해 전송이 성공했다고 상위 계층에 가장해요. 따라서 kfree_skb() 추적을 기록하는 perf의 drop monitor는 TC_ACT_STOLEN에서 어떤 드롭 표시도 보지 않는데, 그 의미가 skb가 "소비"되거나 큐에 들어갔지만 확실히 "드롭"되지는 않았기 때문이에요.
마지막으로 tc BPF 프로그램에서도 사용 가능한 TC_ACT_REDIRECT 액션. 이는 bpf_redirect() 헬퍼와 함께 skb를 같은 또는 다른 장치의 ingress·egress 경로로 리다이렉트할 수 있게 해줘요. 패킷을 다른 장치의 ingress·egress 방향으로 주입할 수 있는 것은 BPF로 패킷 포워딩에 완전한 유연성을 허용해요. 대상 네트워킹 장치가 네트워킹 장치일 것 외의 요구사항은 없으며, 대상 장치에서 다른 cls_bpf 인스턴스를 실행하거나 그런 제한이 필요 없어요.
tc BPF FAQ
이 섹션은 tc BPF 프로그램과 관련해 때때로 묻는 여러 잡다한 질문·답변 쌍을 포함해요.
- 질문: tc 액션 모듈인
act_bpf는 어떠한가요, 여전히 관련 있나요? - 답변: 그렇지 않아요.
cls_bpf와act_bpf가 tc BPF 프로그램에 대해 같은 기능을 공유하지만,cls_bpf가act_bpf의 적절한 상위 집합이므로 더 유연해요. tc가 동작하는 방식은 tc 액션이 tc 분류기에 부착되어야 한다는 거예요.cls_bpf와 같은 유연성을 얻으려면act_bpf를cls_matchall분류기에 부착해야 해요. 이름이 말하듯 이것은 부착된 tc 액션 처리를 위해 패킷을 통과시키기 위해 모든 패킷에서 매칭해요.act_bpf의 경우 이는cls_bpf를direct-action모드로 직접 사용하는 것보다 덜 효율적인 패킷 처리를 초래해요.act_bpf가cls_bpf나cls_matchall이 아닌 다른 분류기와 함께 사용되면 tc 분류기의 작동 방식 때문에 훨씬 더 나빠져요. 즉 분류기 A가 불일치하면 패킷이 분류기 B로 전달되어 패킷을 다시 파싱하는 등, 따라서 일반적인 경우 최악의 경우 패킷이 일치를 찾고 그에 대해act_bpf를 실행하려면 N개의 분류기를 거쳐야 하는 선형 처리가 있을 거예요. 따라서act_bpf는 크게 관련된 적이 없었어요. 또한act_bpf는cls_bpf와 비교해 tc 오프로딩 인터페이스도 제공하지 않아요. - 질문:
cls_bpf를direct-action모드가 아닌 것으로 사용하는 것이 권장되나요? - 답변: 아니요. 답변은 위와 유사한데, 그렇지 않으면 더 복잡한 처리에 확장할 수 없기 때문이에요. tc BPF는 이미 필요한 모든 것을 스스로 효율적으로 할 수 있으므로
direct-action모드 외의 것은 필요 없어요. - 질문: 오프로드된
cls_bpf와 오프로드된 XDP 사이에 성능 차이가 있나요? - 답변: 아니요. 둘 다 SmartNIC로의 오프로드를 처리하는 커널의 같은 컴파일러로 JIT되며 로딩 메커니즘도 매우 유사해요. 따라서 BPF 프로그램은 NIC에서 네이티브로 실행될 수 있도록 같은 대상 명령어 집합으로 변환돼요. 두 tc BPF와 XDP BPF 프로그램 유형은 기능 집합이 다르므로, 예를 들어 오프로드 경우 특정 헬퍼 함수의 사용 가능성에 따라 사용 사례에 따라 하나가 다른 것보다 선택될 수 있어요.
tc BPF 사용 사례
이 하위 섹션은 tc BPF 프로그램의 주요 사용 사례 몇 가지를 제시해요. 여기서도 목록은 완전하지 않으며, tc BPF의 프로그래밍 가능성과 효율성 덕분에 매우 특정한 사용 사례를 해결하도록 쉽게 맞춰 오케스트레이션 시스템에 통합할 수 있어요. 일부 사용 사례는 XDP와 겹칠 수 있지만, tc BPF와 XDP BPF는 서로 보완적이며 주어진 문제를 해결하는 데 가장 적합한 것에 따라 동시에 또는 하나를 우선해 사용할 수 있어요.
- 컨테이너 정책 적용
tc BPF 프로그램이 적합한 한 애플리케이션은 컨테이너 또는 파드에 대한 정책 적용, 커스텀 방화벽 또는 유사한 보안 조치를 구현하는 것이에요. 일반적인 경우 컨테이너 격리는 호스트의 초기 네임스페이스와 전용 컨테이너 네임스페이스를 연결하는 veth 네트워킹 장치가 있는 네트워크 네임스페이스를 통해 구현돼요. veth 쌍의 한쪽 끝이 컨테이너의 네임스페이스로 이동했지만 다른 쪽 끝은 호스트의 초기 네임스페이스에 남으므로, 컨테이너의 모든 네트워크 트래픽은 호스트 방향 veth 장치를 통과해야 하므로 veth의 tc ingress·egress 훅에 tc BPF 프로그램을 부착할 수 있어요. 컨테이너로 들어가는 네트워크 트래픽은 호스트 방향 veth의 tc egress 훅을 통과하고, 컨테이너에서 오는 네트워크 트래픽은 호스트 방향 veth의 tc ingress 훅을 통과해요.
veth 장치 같은 가상 장치의 경우 커널이 여기서
skb에서만 작동하고 제네릭 XDP는 복제된skb로 작동하지 않는 몇 가지 제한이 있으므로 이 경우 XDP는 부적합해요. 후자는 재전송을 위해 데이터 세그먼트를 보유하기 위해 TCP/IP 스택이 크게 사용하는 것으로, 제네릭 XDP 훅은 그 대신 간단히 우회돼요. 게다가 제네릭 XDP는 전체skb를 선형화해야 해 성능이 크게 저하돼요. 반면 tc BPF는skb입력 컨텍스트 경우를 특화하므로 더 유연하며 제네릭 XDP의 제한을 처리할 필요가 없어요. - 포워딩 및 로드밸런싱
포워딩·로드밸런싱 사용 사례는 XDP와 상당히 유사하지만, 노스-사우스 트래픽보다는 이스트-웨스트 컨테이너 워크로드에 약간 더 초점을 맞춰요(두 기술 모두 어느 경우든 쓸 수 있지만). XDP는 ingress 쪽에서만 사용할 수 있으므로 tc BPF 프로그램은 특히 egress에 적용되는 추가 사용 사례를 허용하는데, 예를 들어 컨테이너 기반 트래픽이 이미 초기 네임스페이스 밖의 BPF를 통해 egress 쪽에서 NAT·로드밸런싱되어 컨테이너 자체에 투명할 수 있어요. egress 트래픽은 커널 네트워킹 스택의 특성상 이미
sk_buff구조 기반이므로 패킷 다시 쓰기와 리다이렉트가 tc BPF에 적합해요.bpf_redirect()헬퍼 함수를 활용해 BPF가 포워딩 로직을 인계받아 다른 네트워킹 장치의 ingress 또는 egress 경로로 패킷을 밀어 넣을 수 있어요. 따라서 tc BPF를 포워딩 패브릭으로 활용하면 브리지 같은 장치도 사용할 필요가 없어져요. - 흐름 샘플링, 모니터링
XDP 경우처럼 흐름 샘플링·모니터링은 BPF 프로그램이 커스텀 데이터, 전체 또는 잘린 패킷 내용, 또는 둘 다를 사용자 공간 애플리케이션으로 푸시할 수 있는 고성능 잠금 없는 per-CPU 메모리 매핑 perf 링 버퍼를 통해 실현할 수 있어요. tc BPF 프로그램에서 이는
bpf_xdp_event_output()과 같은 함수 시그니처와 의미를 가진bpf_skb_event_output()BPF 헬퍼 함수를 통해 실현돼요. tc BPF 프로그램이 XDP BPF 경우의 ingress에만 있는 것과 달리 ingress와 egress에 모두 부착될 수 있고 두 tc 훅이 (일반) 네트워킹 스택의 가장 낮은 계층에 있으므로, 특정 노드의 모든 네트워크 트래픽에 대한 양방향 모니터링을 허용해요. 이는 tcpdump와 Wireshark가 사용하는 cBPF 경우와 다소 관련될 수 있지만,skb를 클론할 필요가 없고 프로그래밍 가능성 측면에서 훨씬 더 유연해요. 예를 들어 BPF가 모든 것을 사용자 공간으로 푸시하는 대신 커널 내 집계를 이미 수행하고 링 버퍼로 푸시되는 패킷에 커스텀 주석을 달 수 있어요. 후자는 Cilium에서도 크게 사용되는데, 패킷 드롭에 엔드포인트 라벨과 주어진 패킷이 드롭돼야 했던 이유(예: 정책 위반)를 연관시키는 주석을 추가해 더 풍부한 컨텍스트를 제공해요. - 패킷 스케줄러 사전 처리
sch_handle_egress()라고 불리는sch_clsact의 egress 훅은 커널의 qdisc root lock을 잡기 직전에 실행되므로, tc BPF 프로그램을 활용해 패킷이sch_htb같은 실제 본격적인 qdisc로 전송되기 전에 모든 무거운 패킷 분류·변조를 수행할 수 있게 해요.sch_clsact와sch_htb같은 실제 qdisc의 이런 상호작용은 전송 단계에서 더 나중에 오므로,sch_clsact의 egress 훅이 잠금 없이 실행되므로 전송에 대한 잠금 경합을 줄일 수 있어요.
tc BPF와 XDP BPF 프로그램 사용자의 한 구체적인 예는 Cilium이에요. Cilium은 Docker와 Kubernetes 같은 리눅스 컨테이너 관리 플랫폼을 사용해 배포된 애플리케이션 서비스 사이의 네트워크 연결을 투명하게 보호하는 오픈소스 소프트웨어이며 Layer 3/4 및 Layer 7에서 동작해요. Cilium의 핵심에서 BPF가 작동해 정책 적용, 로드밸런싱, 모니터링을 구현해요.
- Slides: https://www.slideshare.net/ThomasGraf5/dockercon-2017-cilium-network-and-application-security-with-bpf-and-xdp
- Video: https://youtu.be/ilKlmTDdFgk
- Github: https://github.com/cilium/cilium
드라이버 지원
tc BPF 프로그램은 드라이버에서 직접이 아니라 커널의 네트워킹 스택에서 트리거되므로 추가 드라이버 수정이 필요 없고 따라서 어떤 네트워킹 장치에서도 실행할 수 있어요. 아래 나열된 유일한 예외는 tc BPF 프로그램을 NIC로 오프로딩하는 경우예요.
오프로드된 tc BPF를 지원하는 드라이버
- Netronome
- nfp
참고
tc BPF 프로그램 작성·로드 예시는 해당 도구 아래의 Development Tools 섹션에 포함돼 있어요.