eBPF + P4 프로그래머블 NGFW 파이프라인
eBPF 기반 차세대 NGFW 파이프라인과 P4 프로그래머블 NGFW 오프로드: BPF netfilter(커널 6.4+), Cilium eBPF 방화벽, tc-bpf vs XDP 비교, P4 match-action 테이블, Tofino/FPGA P4 NGFW 구현, P4 vs TC flower vs eBPF 비교
프로그래머블 NGFW 파이프라인은 레스토랑 주문 시스템에 비유할 수 있습니다. 기존 방화벽(iptables/nftables)은 메뉴가 고정된 식당과 같습니다. 손님(패킷)이 오면 미리 정해진 메뉴(규칙)에서만 주문할 수 있고, 새 요리를 추가하려면 메뉴판 자체를 교체해야 합니다.
반면 eBPF 기반 NGFW는 셰프가 직접 레시피(BPF 프로그램)를 커널이라는 주방에 설치하는 것입니다. 커널 검증기(Verifier)가 레시피를 검수하여 주방에 불이 나지 않도록 보장합니다. 주문이 들어오면 셰프의 레시피에 따라 유연하게 처리합니다.
P4 프로그래머블 파이프라인은 한 단계 더 나아갑니다. 주방 자체의 설비 배치(파서, 매치-액션 테이블)를 설계하는 것입니다. ASIC이라는 전문 조리 라인에서 모든 주문이 동일한 시간(라인 레이트)에 처리됩니다. 새 요리가 필요하면 컨트롤 플레인(매니저)이 조리 라인의 레시피 카드(테이블 엔트리)만 교체하면 됩니다.
핵심 요약
- eBPF NGFW는 커널 내에서 JIT 컴파일된 프로그램으로 패킷을 처리하며, XDP(드라이버 수준), tc-bpf(TC 수준), BPF netfilter(Netfilter 훅)의 세 가지 부착점(Attach Point)을 사용합니다.
- BPF netfilter(커널 6.4+)는 Netfilter 훅에 BPF 프로그램을 직접 연결하여 nf_conntrack kfunc으로 커널 conntrack을 조회하고 상태를 업데이트할 수 있습니다.
- Cilium/Calico는 eBPF를 활용한 대표적 Kubernetes CNI로, 자체 BPF 맵 기반 conntrack, 정책 맵, NAT 맵으로 kube-proxy와 iptables를 완전히 대체합니다.
- P4 프로그래머블 파이프라인은 ASIC/FPGA에서 커스텀 파서와 매치-액션 테이블을 정의하여 라인 레이트(Tbps급) NGFW를 구현합니다. 컨트롤 플레인은 P4Runtime gRPC로 동적 업데이트합니다.
- 하이브리드 접근이 실무에서 가장 효과적입니다. P4(라인 레이트 ACL) + eBPF(유연한 DPI) + TC flower(eSwitch 오프로드)를 조합하여 각 기술의 장점을 극대화합니다.
단계별 이해
| 단계 | 주제 | 핵심 내용 | 관련 섹션 |
|---|---|---|---|
| 1 | 기존 방화벽 한계 | iptables/nftables의 순차 체인 처리, 고정 매치 필드, CPU 바운드 한계를 이해합니다. | eBPF 기반 차세대 NGFW |
| 2 | eBPF 부착점 이해 | XDP, tc-bpf, BPF netfilter 세 가지 부착점의 위치와 역할을 비교합니다. | tc-bpf vs XDP vs BPF netfilter |
| 3 | BPF netfilter 프로그래밍 | 커널 6.4+ BPF_PROG_TYPE_NETFILTER로 Netfilter 훅에 BPF 프로그램을 연결합니다. | BPF netfilter 프로그램 |
| 4 | kfunc conntrack API | bpf_xdp_ct_lookup, bpf_ct_insert_entry 등 커널 conntrack 직접 접근 API를 학습합니다. | BPF kfunc conntrack API |
| 5 | Cilium/Calico 아키텍처 | Kubernetes 환경의 eBPF 방화벽 구현체를 비교 분석합니다. | Cilium, Calico |
| 6 | eBPF 성능 최적화 | Per-CPU 맵, tail call, XDP multi-buffer, BPF arena 등 고급 최적화 기법을 익힙니다. | 성능 최적화 |
| 7 | P4 프로그래머블 파이프라인 | P416으로 커스텀 파서, 매치-액션 테이블, 스테이트풀 레지스터를 구현합니다. | P4 NGFW |
| 8 | P4Runtime 컨트롤 플레인 | gRPC 기반 P4Runtime API로 테이블 엔트리를 동적 관리합니다. | P4Runtime 연동 |
| 9 | 커널 소스 분석 | BPF netfilter, kfunc CT 구현의 커널 소스 코드를 분석합니다. | 커널 소스 분석 |
| 10 | 실습 | BPF 방화벽, Cilium 정책, P4 BMv2 테스트를 직접 실행합니다. | 실습 가이드 |
eBPF 기반 차세대 NGFW
eBPF(extended Berkeley Packet Filter)는 커널 내에서 안전하게 실행되는 프로그램을 통해 네트워크 패킷 처리를 프로그래밍할 수 있는 기술입니다. 전통적인 nftables/iptables 기반 방화벽을 넘어, eBPF는 더 유연하고 고성능인 NGFW 파이프라인을 구현할 수 있게 합니다.
eBPF NGFW의 장점과 한계
| 특성 | iptables | nftables | eBPF (tc-bpf/XDP) | BPF netfilter (6.4+) |
|---|---|---|---|---|
| 성능 (패킷/초) | 낮음 (순차 체인) | 중간 (집합 기반) | 높음 (JIT 컴파일) | 높음 (JIT + 훅 직결) |
| 유연성 | 고정 매칭 (모듈 확장) | 표현식 기반 (유연) | 최고 (C 프로그램) | 높음 (BPF 프로그램) |
| HW offload | 미지원 | flowtable (TC flower) | XDP HW offload (제한적) | 미지원 (SW only) |
| conntrack 연동 | nf_conntrack 직접 | nf_conntrack 직접 | 자체 CT map 또는 kfunc | nf_conntrack kfunc |
| flowtable 연동 | 불가 | flow add @ft | 불가 (자체 fast path) | 가능 (kfunc) |
| 동적 업데이트 | 전체 체인 교체 | 규칙 단위 추가/삭제 | 맵 업데이트 (무중단) | 맵 업데이트 (무중단) |
| 디버깅 | LOG 타겟 | log 액션 | bpftrace, bpf_printk | bpftrace, bpf_printk |
| 학습 곡선 | 낮음 | 중간 | 높음 (C + BPF 지식) | 높음 |
| 안정성 | 매우 높음 (수십 년) | 높음 | 중간 (verifier 의존) | 아직 초기 |
BPF netfilter 프로그램 (커널 6.4+)
Linux 커널 6.4에서 도입된 BPF_PROG_TYPE_NETFILTER는 Netfilter 훅 포인트에 BPF 프로그램을 직접 연결할 수 있게 합니다. 이를 통해 nftables 규칙 대신 BPF 프로그램으로 방화벽 로직을 구현할 수 있습니다.
/* BPF netfilter 프로그램 예시 — 세션 추적 + 오프로드 결정 */
/* 커널 6.4+ 필요, libbpf 사용 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/netfilter.h>
/* conntrack kfunc 선언 (커널 6.4+) */
extern struct nf_conn *
bpf_xdp_ct_lookup(struct xdp_md *ctx,
struct bpf_sock_tuple *tuple, u32 len,
struct bpf_ct_opts *opts, u32 opts_len) __ksym;
extern void
bpf_ct_release(struct nf_conn *ct) __ksym;
/* 세션별 DPI 결과를 저장하는 BPF 맵 */
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 1000000);
__type(key, struct flow_key); /* 5-tuple */
__type(value, struct flow_info); /* DPI 결과 + 상태 */
} flow_map SEC(".maps");
/* DPI 완료 + ALLOW된 세션을 기록하는 맵 */
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 500000);
__type(key, struct flow_key);
__type(value, __u64); /* 오프로드 타임스탬프 */
} offload_map SEC(".maps");
/* 통계 카운터 */
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 4); /* 0:pass, 1:drop, 2:offload, 3:dpi */
__type(key, __u32);
__type(value, __u64);
} stats SEC(".maps");
struct flow_key {
__be32 saddr;
__be32 daddr;
__be16 sport;
__be16 dport;
__u8 proto;
};
struct flow_info {
__u32 app_id; /* DPI 분류 결과 */
__u32 verdict; /* 0=pending, 1=allow, 2=deny */
__u64 pkt_count; /* 패킷 수 */
__u64 byte_count; /* 바이트 수 */
__u64 last_seen; /* 마지막 패킷 타임스탬프 */
};
SEC("netfilter")
int ngfw_filter(struct bpf_nf_ctx *ctx)
{
struct sk_buff *skb = ctx->skb;
struct flow_key key = {};
struct flow_info *info;
__u32 stat_key;
/* 5-tuple 추출 */
if (extract_flow_key(skb, &key) < 0)
return NF_ACCEPT;
/* flow_map에서 기존 세션 조회 */
info = bpf_map_lookup_elem(&flow_map, &key);
if (!info) {
/* 새 세션: flow_map에 등록하고 DPI로 전달 */
struct flow_info new_info = {
.verdict = 0, /* pending */
.pkt_count = 1,
};
bpf_map_update_elem(&flow_map, &key, &new_info, BPF_ANY);
stat_key = 3; /* DPI로 전달 */
update_stats(&stats, stat_key);
return NF_ACCEPT; /* Slow Path로 진행 */
}
/* DPI 결과에 따른 처리 */
switch (info->verdict) {
case 1: /* ALLOW */
info->pkt_count++;
info->byte_count += skb->len;
info->last_seen = bpf_ktime_get_ns();
/* 10패킷 이상이면 오프로드 대상으로 마킹 */
if (info->pkt_count > 10 &&
!bpf_map_lookup_elem(&offload_map, &key)) {
__u64 ts = bpf_ktime_get_ns();
bpf_map_update_elem(&offload_map, &key, &ts, BPF_ANY);
stat_key = 2;
update_stats(&stats, stat_key);
}
stat_key = 0;
update_stats(&stats, stat_key);
return NF_ACCEPT;
case 2: /* DENY */
stat_key = 1;
update_stats(&stats, stat_key);
return NF_DROP;
default: /* PENDING — 아직 DPI 진행 중 */
info->pkt_count++;
return NF_ACCEPT;
}
}
char _license[] SEC("license") = "GPL";
코드 설명
-
8-14행
커널 6.4+의 kfunc(kernel function) 인터페이스로 BPF 프로그램에서 커널 conntrack을 직접 조회할 수 있습니다.
__ksym은 커널 심볼(Kernel Symbol) 참조를 나타냅니다. -
17-22행
BPF_MAP_TYPE_LRU_HASH는 100만 엔트리의 세션 테이블입니다. LRU 정책으로 가득 차면 가장 오래된 엔트리가 자동 제거됩니다. -
55행
SEC("netfilter")는 이 프로그램이 Netfilter 훅에 연결되는 BPF_PROG_TYPE_NETFILTER 타입임을 선언합니다. -
69-77행
새 세션을 flow_map에 등록하고
NF_ACCEPT를 반환하여 이후 nftables NFQUEUE → DPI 경로로 진행하게 합니다. - 84-91행 DPI가 ALLOW 판정을 내린 세션에서 10패킷 이상 처리되면 offload_map에 등록합니다. 유저스페이스 관리 데몬이 이 맵을 폴링(Polling)하여 실제 flowtable 오프로드를 수행합니다.
BPF kfunc conntrack API 상세
Linux 커널 6.4부터 BPF 프로그램에서 커널의 nf_conntrack 서브시스템에 직접 접근할 수 있는 kfunc(Kernel Function) 인터페이스가 도입되었습니다. 기존에는 Cilium처럼 BPF 맵으로 자체 conntrack을 구현해야 했지만, kfunc를 사용하면 커널 conntrack과 완전히 일관된 세션 상태를 BPF 프로그램에서 활용할 수 있습니다.
kfunc conntrack 함수 목록 (커널 6.4~6.10)
| kfunc 이름 | 도입 커널 | 프로그램 타입 | 설명 |
|---|---|---|---|
bpf_xdp_ct_lookup | 6.4 | XDP | XDP 프로그램에서 conntrack 엔트리를 조회합니다 |
bpf_skb_ct_lookup | 6.4 | TC, Netfilter | skb 기반 프로그램에서 conntrack 엔트리를 조회합니다 |
bpf_ct_insert_entry | 6.4 | XDP, TC, Netfilter | 새로운 conntrack 엔트리를 삽입합니다 |
bpf_ct_release | 6.4 | XDP, TC, Netfilter | 조회된 conntrack 엔트리의 참조 카운트를 해제합니다 |
bpf_ct_set_timeout | 6.4 | XDP, TC, Netfilter | 새 엔트리 삽입 전 타임아웃을 설정합니다 |
bpf_ct_change_timeout | 6.4 | XDP, TC, Netfilter | 기존 엔트리의 타임아웃을 변경합니다 |
bpf_ct_set_status | 6.5 | XDP, TC, Netfilter | 새 엔트리 삽입 전 상태 플래그를 설정합니다 |
bpf_ct_change_status | 6.5 | XDP, TC, Netfilter | 기존 엔트리의 상태 플래그를 변경합니다 |
주요 kfunc 시그니처
/* bpf_xdp_ct_lookup — XDP 프로그램에서 conntrack 조회 */
struct nf_conn *
bpf_xdp_ct_lookup(struct xdp_md *xdp_ctx,
struct bpf_sock_tuple *tuple,
u32 tuple_size,
struct bpf_ct_opts *opts,
u32 opts_size) __ksym;
/* bpf_skb_ct_lookup — TC/Netfilter 프로그램에서 conntrack 조회 */
struct nf_conn *
bpf_skb_ct_lookup(struct __sk_buff *skb_ctx,
struct bpf_sock_tuple *tuple,
u32 tuple_size,
struct bpf_ct_opts *opts,
u32 opts_size) __ksym;
/* bpf_ct_insert_entry — 새 conntrack 엔트리 삽입 */
struct nf_conn *
bpf_ct_insert_entry(struct nf_conn___init *nfct_i,
struct bpf_sock_tuple *tuple,
u32 tuple_size,
struct bpf_ct_opts *opts,
u32 opts_size) __ksym;
/* bpf_ct_release — conntrack 엔트리 참조 해제 (반드시 호출 필수) */
void bpf_ct_release(struct nf_conn *nfct) __ksym;
/* bpf_ct_set_timeout / bpf_ct_change_timeout */
int bpf_ct_set_timeout(struct nf_conn___init *nfct_i, u32 timeout) __ksym;
int bpf_ct_change_timeout(struct nf_conn *nfct, u32 timeout) __ksym;
/* bpf_ct_set_status / bpf_ct_change_status */
int bpf_ct_set_status(struct nf_conn___init *nfct_i, u32 status) __ksym;
int bpf_ct_change_status(struct nf_conn *nfct, u32 status) __ksym;
/* bpf_ct_opts 구조체 */
struct bpf_ct_opts {
s32 netns_id; /* 네트워크 네임스페이스 ID (-1 = 현재) */
s32 error; /* 반환 시 에러 코드 */
u8 l4proto; /* IPPROTO_TCP, IPPROTO_UDP 등 */
u8 dir; /* IP_CT_DIR_ORIGINAL 또는 IP_CT_DIR_REPLY */
u8 reserved[2]; /* 패딩 */
};
코드 설명
-
bpf_xdp_ct_lookup / bpf_skb_ct_lookup
각각 XDP와 TC/Netfilter 프로그램용 conntrack 조회 함수입니다.
bpf_sock_tuple에 5-tuple을 채워 전달하면 커널 conntrack 테이블에서 매칭되는nf_conn엔트리를 반환합니다. 조회 실패 시 NULL을 반환하며, 이때opts->error에 에러 코드가 설정됩니다. -
bpf_ct_insert_entry
새로운 conntrack 엔트리를 커널 conntrack 테이블에 삽입합니다.
nf_conn___init타입은 아직 확정(confirm)되지 않은 초기화 중인 엔트리를 나타냅니다. 삽입 전에bpf_ct_set_timeout과bpf_ct_set_status로 타임아웃과 상태를 설정할 수 있습니다. -
bpf_ct_release
조회된
nf_conn포인터의 참조 카운트를 감소시킵니다. 모든 CT 조회 후 반드시 호출해야 합니다. 호출하지 않으면 BPF verifier가 프로그램 로딩을 거부합니다. -
bpf_ct_opts
netns_id를-1로 설정하면 현재 네트워크 네임스페이스의 conntrack을 조회합니다.l4proto는IPPROTO_TCP(6),IPPROTO_UDP(17)등의 L4 프로토콜을 지정합니다.
kfunc CT 활용 BPF 프로그램 예시
/* kfunc conntrack API를 활용한 스테이트풀 방화벽 */
/* 커널 6.4+, libbpf 1.2+ 필요 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
/* kfunc 선언 */
extern struct nf_conn *bpf_skb_ct_lookup(
struct __sk_buff *, struct bpf_sock_tuple *,
u32, struct bpf_ct_opts *, u32) __ksym;
extern struct nf_conn *bpf_ct_insert_entry(
struct nf_conn___init *, struct bpf_sock_tuple *,
u32, struct bpf_ct_opts *, u32) __ksym;
extern void bpf_ct_release(struct nf_conn *) __ksym;
extern int bpf_ct_set_timeout(
struct nf_conn___init *, u32) __ksym;
extern int bpf_ct_change_timeout(
struct nf_conn *, u32) __ksym;
/* 허용 포트 맵 */
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u16); /* 목적지 포트 */
__type(value, __u8); /* 1=허용 */
} allowed_ports SEC(".maps");
/* Per-CPU 통계 */
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 3); /* 0:허용, 1:차단, 2:CT히트 */
__type(key, __u32);
__type(value, __u64);
} fw_stats SEC(".maps");
static __always_inline void inc_stat(__u32 idx)
{
__u64 *val = bpf_map_lookup_elem(&fw_stats, &idx);
if (val) (*val)++;
}
SEC("tc")
int stateful_fw(struct __sk_buff *skb)
{
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return TC_ACT_OK;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return TC_ACT_OK;
if (ip->protocol != IPPROTO_TCP)
return TC_ACT_OK;
struct tcphdr *tcp = (void *)ip + (ip->ihl * 4);
if ((void *)(tcp + 1) > data_end)
return TC_ACT_OK;
/* 5-tuple 구성 */
struct bpf_sock_tuple tuple = {};
tuple.ipv4.saddr = ip->saddr;
tuple.ipv4.daddr = ip->daddr;
tuple.ipv4.sport = tcp->source;
tuple.ipv4.dport = tcp->dest;
struct bpf_ct_opts opts = {
.netns_id = -1,
.l4proto = IPPROTO_TCP,
};
/* 1단계: 커널 conntrack 조회 */
struct nf_conn *ct = bpf_skb_ct_lookup(
skb, &tuple, sizeof(tuple.ipv4),
&opts, sizeof(opts));
if (ct) {
/* 기존 세션 — 타임아웃 갱신 후 허용 */
bpf_ct_change_timeout(ct, 300); /* 5분 */
bpf_ct_release(ct);
inc_stat(2); /* CT 히트 */
return TC_ACT_OK;
}
/* 2단계: 새 세션 — 허용 포트인지 확인 */
__u16 dport = bpf_ntohs(tcp->dest);
__u8 *allowed = bpf_map_lookup_elem(&allowed_ports, &dport);
if (!allowed) {
inc_stat(1); /* 차단 */
return TC_ACT_SHOT;
}
/* 3단계: 새 conntrack 엔트리 삽입 */
opts.error = 0;
struct nf_conn *new_ct = bpf_ct_insert_entry(
(struct nf_conn___init *)ct,
&tuple, sizeof(tuple.ipv4),
&opts, sizeof(opts));
if (new_ct)
bpf_ct_release(new_ct);
inc_stat(0); /* 허용 */
return TC_ACT_OK;
}
char _license[] SEC("license") = "GPL";
코드 설명
-
kfunc 선언부
__ksym어트리뷰트는 이 함수가 커널 심볼에서 동적으로 해석(Resolve)됨을 나타냅니다. libbpf가 프로그램 로딩 시 커널의 BTF(BPF Type Format) 정보를 사용하여 실제 함수 주소로 연결합니다. -
bpf_skb_ct_lookup 호출
TC 프로그램에서 5-tuple로 커널 conntrack 테이블을 조회합니다.
netns_id = -1은 현재 네트워크 네임스페이스를 의미합니다. 기존 세션이 있으면nf_conn포인터를 반환합니다. - bpf_ct_change_timeout 기존 세션의 타임아웃을 300초(5분)로 갱신합니다. 이를 통해 활성 세션이 만료되지 않도록 합니다.
- bpf_ct_release CT 조회 후 반드시 release를 호출해야 합니다. 커널 conntrack 엔트리는 참조 카운트(Reference Count)로 관리되며, release 없이 프로그램을 종료하면 verifier가 거부합니다.
- bpf_ct_insert_entry 허용된 포트의 새 TCP 연결에 대해 커널 conntrack 엔트리를 생성합니다. 이후 같은 5-tuple의 패킷은 CT 조회에서 바로 매칭되어 Fast Path로 처리됩니다.
kfunc CT vs BPF 맵 기반 CT 비교
| 비교 항목 | kfunc CT (커널 conntrack) | BPF 맵 기반 CT (Cilium 방식) |
|---|---|---|
| 구현 위치 | 커널 nf_conntrack 서브시스템 | BPF 맵 (LRU_HASH) |
| nftables/iptables 일관성 | 완전 일관 (동일 CT 테이블) | 불일관 (별도 CT) |
| 성능 (CT 조회) | 중간 (~50ns, RCU lock) | 높음 (~20ns, lockless per-CPU) |
| 메모리 효율 | 높음 (커널 slab 관리) | 고정 할당 (max_entries 사전 설정) |
| NAT 통합 | 자동 (nf_nat 연동) | 수동 구현 필요 |
| Helper 추적 | 지원 (FTP, SIP 등) | 미지원 |
| GC (가비지 컬렉션) | 커널 자동 (conntrack GC) | 유저스페이스 에이전트 필요 |
| flowtable 연동 | 가능 (nf_flow_offload) | 불가 (자체 fast path 필요) |
| 최소 커널 | 6.4+ | 5.x+ (맵 기본 기능) |
| 적합 환경 | nftables 혼용, flowtable offload 필요 시 | 순수 eBPF 스택 (Cilium, Calico) |
Cilium의 eBPF 방화벽 아키텍처
Cilium은 Kubernetes 환경에서 가장 널리 사용되는 eBPF 기반 CNI + 방화벽 솔루션입니다. 커널의 nf_conntrack과 nftables를 사용하지 않고, eBPF 맵으로 자체 conntrack과 정책 엔진(Policy Engine)을 구현합니다.
| 구성요소 | 구현 방식 | BPF 맵 타입 | 역할 |
|---|---|---|---|
| CT map (conntrack) | BPF_MAP_TYPE_LRU_HASH | per-CPU LRU hash | 세션 추적, 상태 관리, NAT 매핑 |
| Policy map | BPF_MAP_TYPE_HASH | identity → policy 매핑 | K8s NetworkPolicy 평가 |
| NAT map | BPF_MAP_TYPE_LRU_HASH | 원본 → 변환 IP/port | ClusterIP, NodePort DNAT |
| Endpoints map | BPF_MAP_TYPE_HASH | IP → endpoint identity | Pod 식별, 보안 ID 할당 |
| Metrics map | BPF_MAP_TYPE_PERCPU_HASH | reason → counter | drop/forward 사유별 통계 |
| Events map | BPF_MAP_TYPE_PERF_EVENT_ARRAY | ring buffer | Hubble 이벤트 전달 (모니터링) |
Cilium의 데이터 플레인 파이프라인은 다음과 같은 순서로 실행됩니다:
- 1단계 — XDP pre-filter: 인입 트래픽에서 명백한 공격(SYN flood, 잘못된 패킷)을 드라이버 수준에서 차단
- 2단계 — tc-bpf ingress: 목적지 Pod의 identity를 lookup하고, CT map에서 기존 세션을 조회
- 3단계 — Policy 평가: source identity + destination identity 조합으로 policy map에서 ALLOW/DENY 판정
- 4단계 — CT update: 새 세션이면 CT map에 엔트리 생성, 기존 세션이면 카운터와 타임스탬프 갱신
- 5단계 — NAT/LB: ClusterIP → Pod IP 변환, NodePort → Pod IP 변환
- 6단계 — redirect:
bpf_redirect_peer()로 veth 피어를 통해 목적지 Pod에 직접 전달 (네트워크 스택 우회)
/* Cilium CT map 구조 (간소화) */
/* bpf/lib/conntrack.h 참고 */
struct ct_entry {
__u64 rx_packets; /* 수신 패킷 수 */
__u64 rx_bytes; /* 수신 바이트 */
__u64 tx_packets; /* 송신 패킷 수 */
__u64 tx_bytes; /* 송신 바이트 */
__u32 lifetime; /* 남은 수명 (초) */
__u16 rx_closing:1; /* FIN 수신 */
__u16 tx_closing:1; /* FIN 송신 */
__u16 nat46:1; /* NAT46 변환 */
__u16 lb_loopback:1; /* LB 루프백 */
__u16 seen_non_syn:1; /* SYN 이후 패킷 확인 */
__u16 node_port:1; /* NodePort 플래그 */
__u8 rev_nat_index; /* 역NAT 인덱스 */
__u8 slave; /* LB backend 슬롯 */
__u16 ifindex; /* 출력 인터페이스 */
__u32 src_sec_id; /* 소스 보안 identity */
};
/* CT GC (Garbage Collection) — 유저스페이스 에이전트 */
/* cilium-agent가 주기적으로 CT map을 순회하며 만료 엔트리 삭제 */
/* GC 주기: 기본 12초, conntrack-gc-interval 옵션으로 조절 */
/* GC 전략: */
/* 1. lifetime이 0인 엔트리 삭제 */
/* 2. TCP FIN/RST 후 grace period 만료 엔트리 삭제 */
/* 3. NAT map의 orphan 엔트리 정리 */
NF_STOLEN 반환과 유사한 효과를 제공하며, Pod-to-Pod 트래픽에서 ~30% 성능 향상을 달성합니다. 커널 5.10+에서 사용 가능합니다.
Calico eBPF 데이터 플레인
Calico는 Tigera가 개발한 Kubernetes 네트워킹 + 보안 솔루션으로, v3.13부터 eBPF 데이터 플레인을 지원합니다. Cilium과 달리 Calico는 기존 iptables 모드와 eBPF 모드를 선택적으로 전환할 수 있으며, BGP 기반 라우팅과 VXLAN 오버레이를 모두 지원합니다.
Calico iptables vs eBPF 모드 비교
| 비교 항목 | Calico iptables 모드 | Calico eBPF 모드 |
|---|---|---|
| 데이터 경로 | iptables/nftables 체인 | tc-bpf ingress/egress |
| kube-proxy | 필요 (또는 Calico 자체 IPVS) | 불필요 (BPF 내 LB 구현) |
| conntrack | nf_conntrack | BPF 맵 기반 자체 CT |
| 성능 (Pod-to-Pod) | 기준선 | ~20-30% 향상 |
| 성능 (NodePort) | 기준선 | ~40% 향상 |
| 첫 패킷 레이턴시 | 높음 (긴 iptables 체인) | 낮음 (BPF 직접 실행) |
| 최소 커널 | 3.10+ | 5.3+ (권장 5.8+) |
| DSR (Direct Server Return) | 미지원 | 지원 |
| Wireguard 연동 | 지원 | 지원 |
| Windows 지원 | HNS 모드 | 미지원 |
Calico eBPF 아키텍처
Calico eBPF 모드는 tc-bpf 프로그램을 각 인터페이스의 ingress와 egress에 부착하여 패킷을 처리합니다. Cilium과 유사하지만, 호스트 엔드포인트(Host Endpoint) 보호와 VXLAN + BPF 조합에서 차별화됩니다.
- Workload Endpoint(WEP): Pod의 veth 인터페이스에 tc-bpf 프로그램을 부착합니다. ingress에서 정책 평가, egress에서 conntrack 업데이트를 수행합니다.
- Host Endpoint(HEP): 호스트의 물리/가상 인터페이스에도 tc-bpf를 부착하여 호스트 레벨 방화벽을 구현합니다. Cilium은 기본적으로 Pod 간 트래픽에 집중하지만, Calico는 호스트 자체의 보안까지 커버합니다.
- VXLAN + BPF: VXLAN 터널 인터페이스에도 BPF 프로그램을 부착하여 터널링 전후로 정책을 적용합니다. 터널 내부 패킷에도 정책이 일관되게 적용됩니다.
Calico 핵심 BPF 맵
| BPF 맵 | 타입 | 키 | 값 | 용도 |
|---|---|---|---|---|
| cali_v4_ct | BPF_MAP_TYPE_HASH | 5-tuple + 방향 | CT 상태, 카운터, NAT 정보 | conntrack (세션 추적) |
| cali_v4_nat_fe | BPF_MAP_TYPE_HASH | VIP + port | backend 수, affinity 키 | 서비스 프론트엔드 (ClusterIP) |
| cali_v4_nat_be | BPF_MAP_TYPE_HASH | 서비스 ID + 인덱스 | backend IP + port | 서비스 백엔드 (Pod 주소) |
| cali_v4_routes | BPF_MAP_TYPE_LPM_TRIE | IP prefix | 다음 홉, 인터페이스 | BPF 내 라우팅 테이블 |
| cali_v4_fsafes | BPF_MAP_TYPE_HASH | 프로토콜 + 포트 | 플래그 | failsafe 포트 (항상 허용) |
| cali_v4_aff | BPF_MAP_TYPE_LRU_HASH | 클라이언트 IP + 서비스 | 선택된 backend | 세션 어피니티 (sticky LB) |
Calico eBPF 모드 설정
# 1. Calico 설치 (Operator 기반, v3.26+)
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.0/manifests/tigera-operator.yaml
# 2. eBPF 데이터 플레인 활성화
cat <<EOF | kubectl apply -f -
apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
name: default
spec:
calicoNetwork:
linuxDataplane: BPFDataplane
bgp: Enabled
ipPools:
- cidr: 10.244.0.0/16
encapsulation: VXLAN
EOF
# 3. kube-proxy 비활성화 (eBPF 모드에서 불필요)
kubectl patch ds -n kube-system kube-proxy \
-p '{"spec":{"template":{"spec":{"nodeSelector":{"non-calico": "true"}}}}}'
# 4. eBPF 모드 동작 확인
kubectl get felixconfiguration default -o yaml | grep bpfEnabled
# bpfEnabled: true
# 5. BPF 프로그램 부착 확인
tc filter show dev eth0 ingress
# filter protocol all pref 1 bpf chain 0
# handle 0x1 calico_from_host_ep ...
# 6. conntrack 맵 엔트리 확인
calico-bpf conntrack dump | head -20
Calico eBPF vs Cilium 성능 비교
| 측정 항목 | Calico iptables | Calico eBPF | Cilium (v1.14+) |
|---|---|---|---|
| Pod-to-Pod TCP 처리량 | 기준선 (1.0x) | 1.25x | 1.30x |
| NodePort 레이턴시 | 기준선 (1.0x) | 0.6x (40% 감소) | 0.55x (45% 감소) |
| 정책 규칙 100개 시 성능 | 급격히 감소 | 일정 유지 | 일정 유지 |
| 메모리 사용량 (1000 Pod) | ~200MB (iptables 규칙) | ~80MB (BPF 맵) | ~120MB (BPF 맵) |
| 호스트 레벨 방화벽 | 기본 지원 | 기본 지원 | 별도 설정 필요 |
| BGP 네이티브 라우팅 | 기본 지원 | 기본 지원 | 별도 설정 (BGP CP) |
| L7 정책 | 미지원 | 미지원 | Envoy 연동 지원 |
| Hubble 관측성 | 미지원 | 미지원 | 기본 지원 |
tc-bpf vs XDP vs BPF netfilter 비교
eBPF 기반 NGFW를 구현할 때 사용할 수 있는 3가지 BPF 프로그램 타입의 특성을 비교합니다:
| 특성 | XDP (eXpress Data Path) | tc-bpf (Traffic Control) | BPF netfilter (6.4+) |
|---|---|---|---|
| 실행 시점 | NIC 드라이버 직후 (skb 생성 전) | TC ingress/egress (skb 존재) | Netfilter 훅 (conntrack 후) |
| 성능 | 최고 (~100Mpps) | 높음 (~40Mpps) | nftables와 유사 (~20Mpps) |
| skb 접근 | 제한적 (xdp_md만) | 전체 skb 접근 | 전체 skb + nf_hook_state |
| conntrack 접근 | kfunc (6.4+) | kfunc (6.4+) | 자동 (훅 위치에 따라) |
| NAT | 수동 rewrite | 수동 rewrite | nf_nat 통합 가능 |
| redirect | bpf_redirect(), bpf_redirect_map() | bpf_redirect(), bpf_redirect_peer() | NF_ACCEPT/DROP만 |
| HW offload | 제한적 (Netronome 등) | 미지원 | 미지원 |
| NGFW 역할 | DDoS pre-filter, rate limit | L3/L4 정책, CT, LB, redirect | Netfilter 훅 대체, DPI 연동 |
| 대표 프로젝트 | Cloudflare Magic Transit | Cilium, Calico eBPF | 아직 초기 (실험적) |
각 BPF 타입의 NGFW에서의 역할을 종합하면:
- XDP: 최전방에서 볼륨 공격(SYN flood, UDP flood, amplification)을 드라이버 수준에서 차단합니다. skb 할당 전에 처리하므로 CPU 오버헤드가 최소입니다.
- tc-bpf: 메인 방화벽 엔진으로 작동합니다. conntrack 조회, 정책 평가, NAT, 라우팅/redirect를 모두 처리합니다. Cilium/Calico의 핵심 데이터 플레인입니다.
- BPF netfilter: 기존 nftables 인프라를 유지하면서 특정 훅에 BPF 로직을 삽입합니다. 커스텀 DPI 로직이나 복잡한 verdict 결정에 적합합니다.
eBPF NGFW 성능 최적화
eBPF 기반 NGFW는 높은 유연성을 제공하지만, 최대 성능을 달성하려면 BPF 프로그램 구조, 맵 선택, 커널 기능 활용에 대한 세밀한 최적화가 필요합니다.
Per-CPU 맵 vs 공유 맵
| 특성 | Per-CPU 맵 | 공유 맵 (일반) |
|---|---|---|
| 타입 예시 | BPF_MAP_TYPE_PERCPU_HASH, PERCPU_ARRAY | BPF_MAP_TYPE_HASH, LRU_HASH |
| 락 경합 | 없음 (CPU별 독립 사본) | 있음 (버킷 스핀락) |
| 캐시 효율 | 높음 (L1/L2 캐시 히트) | 낮음 (캐시 라인 바운싱) |
| 메모리 사용 | 높음 (CPU 수 × 엔트리 수) | 낮음 (1개 사본) |
| CPU 간 일관성 | 불일관 (각 CPU 독립) | 일관 (동일 데이터) |
| 적합 용도 | 통계 카운터, 속도 제한 토큰 | conntrack, 정책 맵, NAT 맵 |
HASH 맵을, 읽기/쓰기 빈번한 카운터에는 PERCPU_ARRAY를, 세션 테이블(conntrack)에는 LRU_HASH를 사용합니다. LRU_HASH는 내부적으로 per-CPU 캐시를 활용하여 공유 맵이면서도 락 경합을 최소화합니다.
Tail Call / BPF-to-BPF Call로 파이프라인 분할
복잡한 NGFW 로직을 단일 BPF 프로그램에 구현하면 verifier 복잡도 한계(100만 명령)에 도달할 수 있습니다. Tail call과 BPF-to-BPF call을 사용하여 파이프라인을 스테이지로 분할합니다.
/* Tail call 기반 NGFW 파이프라인 분할 */
/* 파이프라인 스테이지 인덱스 */
#define STAGE_PREFILTER 0
#define STAGE_CT_LOOKUP 1
#define STAGE_POLICY 2
#define STAGE_NAT 3
#define STAGE_REDIRECT 4
/* Tail call 맵 */
struct {
__uint(type, BPF_MAP_TYPE_PROG_ARRAY);
__uint(max_entries, 8);
__uint(key_size, sizeof(__u32));
__uint(value_size, sizeof(__u32));
} pipeline_stages SEC(".maps");
/* 스테이지 간 공유 메타데이터 */
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, struct pkt_meta);
} meta_map SEC(".maps");
struct pkt_meta {
__u32 src_ip;
__u32 dst_ip;
__u16 src_port;
__u16 dst_port;
__u8 proto;
__u8 ct_state; /* 0=NEW, 1=EST, 2=REL */
__u8 verdict; /* 0=PENDING, 1=ALLOW, 2=DENY */
__u32 nat_dst_ip;
__u16 nat_dst_port;
};
SEC("tc")
int stage_prefilter(struct __sk_buff *skb)
{
/* 헤더 파싱 + 메타데이터 저장 */
struct pkt_meta meta = {};
if (parse_headers(skb, &meta) < 0)
return TC_ACT_SHOT;
__u32 key = 0;
bpf_map_update_elem(&meta_map, &key, &meta, BPF_ANY);
/* 다음 스테이지로 tail call */
bpf_tail_call(skb, &pipeline_stages, STAGE_CT_LOOKUP);
return TC_ACT_OK; /* tail call 실패 시 fallback */
}
SEC("tc")
int stage_ct_lookup(struct __sk_buff *skb)
{
__u32 key = 0;
struct pkt_meta *meta = bpf_map_lookup_elem(&meta_map, &key);
if (!meta)
return TC_ACT_OK;
/* conntrack 조회 + 상태 업데이트 */
ct_lookup_and_update(skb, meta);
/* ESTABLISHED면 정책 건너뛰기 */
if (meta->ct_state == 1) {
bpf_tail_call(skb, &pipeline_stages, STAGE_NAT);
} else {
bpf_tail_call(skb, &pipeline_stages, STAGE_POLICY);
}
return TC_ACT_OK;
}
코드 설명
-
BPF_MAP_TYPE_PROG_ARRAY
Tail call 대상 프로그램을 인덱스로 관리하는 특수 맵입니다. 유저스페이스에서 각 인덱스에 BPF 프로그램 FD를 등록하면,
bpf_tail_call()로 해당 프로그램에 제어를 넘길 수 있습니다. - meta_map (PERCPU_ARRAY) Tail call 간에 파싱 결과와 처리 상태를 공유하기 위한 per-CPU 메타데이터 맵입니다. Tail call은 스택을 공유하지 않으므로 맵을 통해 상태를 전달합니다.
- bpf_tail_call 현재 프로그램의 실행을 종료하고 다음 스테이지 프로그램으로 제어를 넘깁니다. 호출 성공 시 현재 함수는 반환하지 않습니다. 실패 시(맵에 프로그램이 없는 경우) 다음 줄이 실행됩니다.
- CT 상태 기반 분기 ESTABLISHED 세션은 정책 평가를 건너뛰고 바로 NAT 스테이지로 진행합니다. 이렇게 Fast Path를 구현하면 확립된 세션의 처리 속도가 향상됩니다.
bpf_redirect 함수군 비교
| 함수 | 도입 커널 | 동작 | 네트워크 스택 경로 | 성능 |
|---|---|---|---|---|
bpf_redirect() | 4.15 | 지정 ifindex로 패킷 전달 | 수신 측 ingress TC/XDP 실행 | 기준선 |
bpf_redirect_peer() | 5.10 | veth 피어로 직접 전달 | 호스트 측 ingress 우회 | ~30% 향상 |
bpf_redirect_neigh() | 5.10 | 이웃(Neighbor) 조회 후 전달 | FIB lookup 수행, L2 헤더 설정 | ~15% 향상 |
bpf_redirect_map() | 4.14 | DEVMAP/CPUMAP으로 배치 전달 | bulk 전달, CPU 분산 | ~50% 향상 (bulk) |
XDP Multi-Buffer (커널 6.0+)
커널 6.0부터 XDP에서 멀티 버퍼(Multi-Buffer) 패킷을 처리할 수 있게 되었습니다. 기존 XDP는 단일 페이지(4KB) 이내의 패킷만 처리할 수 있어 점보 프레임(Jumbo Frame, 9000 바이트)이나 GRO(Generic Receive Offload) 병합 패킷을 다룰 수 없었습니다. 멀티 버퍼 XDP는 xdp_buff에 mb 플래그를 추가하고 bpf_xdp_get_buff_len() 헬퍼로 전체 패킷 길이를 조회할 수 있게 합니다.
BPF Arena (커널 6.9+)
커널 6.9에서 도입된 BPF arena는 BPF 프로그램에서 대용량 메모리를 동적으로 할당할 수 있는 메커니즘입니다. 기존 BPF 맵은 고정 크기 엔트리만 지원하여 가변 길이 DPI 시그니처나 대형 룩업 테이블 구현이 어려웠습니다. Arena는 mmap 기반으로 최대 수 GB의 연속 메모리를 BPF 프로그램과 유저스페이스가 공유할 수 있게 합니다.
IRQ 어피니티 + NAPI 최적화
eBPF NGFW의 성능은 하드웨어 인터럽트와 NAPI 폴링(Polling) 설정에 크게 영향받습니다.
- IRQ 어피니티: NIC의 각 RX 큐 인터럽트를 특정 CPU에 고정하여 캐시 효율을 극대화합니다.
/proc/irq/<N>/smp_affinity또는irqbalance로 설정합니다. - RPS/RFS: Receive Packet Steering / Receive Flow Steering으로 소프트웨어 수준의 패킷 분산을 수행합니다. XDP에서는
CPUMAP을 활용하여 BPF 내에서 직접 CPU를 지정할 수 있습니다. - NAPI busy-poll:
net.core.busy_poll과net.core.busy_readsysctl을 설정하여 폴링 모드를 활성화하면, 인터럽트 오버헤드 없이 패킷을 수신합니다. - GRO: XDP 멀티 버퍼 지원 시, GRO 병합된 대형 패킷을 XDP에서 직접 처리하여 패킷당 BPF 호출 횟수를 줄입니다.
Verifier 복잡도 최소화 팁
- Tail call 분할: 단일 프로그램의 명령 수를 줄여 verifier 부하를 분산합니다. Tail call 체인은 최대 33단계까지 가능합니다.
- BPF-to-BPF 서브함수:
static __noinline함수로 공통 로직을 분리하면 verifier가 함수 단위로 검증하여 효율적입니다. - bounded loop: 커널 5.3+에서
bpf_loop()또는 boundedfor루프를 사용합니다. 루프 상한을 명시적으로 지정해야 verifier가 승인합니다. - 불필요한 분기 제거: 컴파일러 힌트(
__builtin_expect,likely()/unlikely())를 활용하여 분기 예측을 돕고, verifier 탐색 경로를 줄입니다. - 맵 타입 선택:
ARRAY맵은 verifier가 범위 검사를 자동으로 수행하므로HASH보다 검증이 빠릅니다. 가능한 경우ARRAY를 우선 사용합니다.
eBPF 작업별 성능 수치
| 작업 | 처리량 (64B 패킷) | 레이턴시 (P99) | 비고 |
|---|---|---|---|
| XDP_DROP (빈 프로그램) | ~100 Mpps/core | <100ns | NIC 드라이버 직후 드롭 |
| XDP + LPM_TRIE 조회 | ~40 Mpps/core | ~200ns | IP 차단 목록 조회 |
| tc-bpf + HASH 맵 조회 | ~20 Mpps/core | ~400ns | 5-tuple 정책 매칭 |
| tc-bpf + CT lookup (BPF 맵) | ~15 Mpps/core | ~500ns | LRU_HASH conntrack 조회 |
| tc-bpf + kfunc CT lookup | ~12 Mpps/core | ~600ns | 커널 nf_conntrack 조회 |
| tc-bpf + CT + Policy + NAT | ~8 Mpps/core | ~1μs | Cilium 전체 파이프라인 |
| BPF netfilter (NF 훅) | ~10 Mpps/core | ~700ns | Netfilter 훅 오버헤드 포함 |
| tc-bpf + bpf_redirect_peer() | ~18 Mpps/core | ~350ns | veth 피어 직접 전달 |
| XDP + CPUMAP redirect | ~60 Mpps (총합) | ~250ns | 멀티코어 분산 처리 |
P4 프로그래머블 파이프라인 NGFW 오프로드
TC flower와 eSwitch 오프로드는 고정 함수(Fixed-Function) 파이프라인에서 미리 정의된 매치/액션을 수행합니다. 반면 P4(Programming Protocol-independent Packet Processors)는 NIC/스위치의 패킷 처리 파이프라인 자체를 프로그래밍할 수 있게 하여, 커스텀 프로토콜 파싱, 임의의 매치-액션 테이블, 스테이트풀 레지스터를 라인 레이트에서 실행합니다. 이 절에서는 P4 언어의 기본 개념부터 NGFW 구현까지 단계적으로 살펴봅니다.
P4 언어 개요
P4(Programming Protocol-independent Packet Processors)는 2014년 스탠퍼드 대학과 프린스턴 대학의 연구자들이 발표한 도메인 특화 언어(Domain-Specific Language, DSL)입니다. 네트워크 장비의 데이터 플레인(Data Plane) 동작을 소프트웨어처럼 정의할 수 있도록 설계되었으며, 컴파일 결과가 ASIC, FPGA, 소프트웨어 스위치 등 다양한 타깃에 직접 매핑됩니다.
P4의 핵심 설계 목표
- 프로토콜 독립성(Protocol Independence): 특정 프로토콜(IPv4, TCP 등)에 종속되지 않고, 사용자가 임의의 프로토콜 헤더 형식을 정의할 수 있습니다. 새로운 프로토콜이 등장해도 하드웨어 변경 없이 P4 프로그램만 수정하면 됩니다.
- 타깃 독립성(Target Independence): 동일한 P4 프로그램이 다양한 하드웨어/소프트웨어 타깃(Tofino ASIC, FPGA, BMv2 소프트웨어 스위치 등)에서 실행될 수 있습니다. 타깃별 차이는 아키텍처 모델(Architecture Model)이 추상화합니다.
- 재구성 가능성(Reconfigurability): 패킷 처리 파이프라인을 런타임에 재프로그래밍할 수 있습니다. 고정 함수(Fixed-Function) ASIC과 달리 새로운 기능 추가에 하드웨어 교체가 필요하지 않습니다.
P414 vs P416
P4는 두 가지 주요 버전이 존재합니다. 현재 표준은 P416이며, 대부분의 새 프로젝트에서 사용됩니다.
| 비교 항목 | P414 (2014) | P416 (2016~현재) |
|---|---|---|
| 파이프라인 모델 | 고정(Ingress → Egress) | 아키텍처 모델에 의해 정의 (유연) |
| 아키텍처 분리 | 없음 (언어에 내장) | 아키텍처 모델을 언어와 분리 |
| 타입 시스템 | 약한 타입 | 강한 타입 (bit<W>, int<W>, enum, struct 등) |
| 제네릭/다형성 | 미지원 | extern 객체, 타입 매개변수 지원 |
| 파서 정의 | parser 키워드 + 상태 함수 | parser 블록 + state 키워드 + transition select |
| 테이블 액션 | action_profile / action_selector | 동일 + const entries 정적 테이블 지원 |
| 표준화 | P4.org 초기 사양 | P4 Language Specification v1.2.x (P4.org) |
| 호환성 | 레거시(Legacy) | p4c에서 P414 → P416 자동 변환 지원 |
P416 핵심 문법 구조
P416 프로그램은 헤더(Header) 정의 → 파서(Parser) → 제어 블록(Control) → 디파서(Deparser)의 4단계로 구성됩니다. 각 요소는 타깃 아키텍처 모델이 정의한 파이프라인 구조에 매핑됩니다.
/* P4_16 프로그램의 기본 구조 (V1Model 아키텍처 기준) */
/* 1단계: 타입 및 헤더 정의 */
typedef bit<48> mac_addr_t; /* 고정 폭 비트 타입 */
typedef bit<32> ipv4_addr_t;
header ethernet_t { /* 헤더: 패킷에서 추출할 필드 묶음 */
mac_addr_t dst_addr;
mac_addr_t src_addr;
bit<16> ether_type;
}
struct headers_t { /* 구조체: 파서가 추출한 헤더 모음 */
ethernet_t ethernet;
}
struct metadata_t { /* 메타데이터: 패킷 간 전달되는 사용자 정의 상태 */
bit<1> is_valid;
}
/* 2단계: 파서 — 상태 머신 기반 헤더 추출 */
parser MyParser(packet_in pkt,
out headers_t hdr,
inout metadata_t meta,
inout standard_metadata_t std_meta) {
state start {
pkt.extract(hdr.ethernet); /* extract(): 패킷에서 헤더 필드 추출 */
transition accept; /* transition: 다음 상태로 이동 */
}
}
/* 3단계: 제어 블록 — 매치-액션 파이프라인 로직 */
control MyIngress(inout headers_t hdr,
inout metadata_t meta,
inout standard_metadata_t std_meta) {
action drop() { /* action: 테이블 매치 시 실행할 동작 */
mark_to_drop(std_meta);
}
action forward(bit<9> port) {
std_meta.egress_spec = port;
}
table dmac_table { /* table: 매치-액션 테이블 */
key = {
hdr.ethernet.dst_addr : exact; /* 매치 타입: exact, ternary, lpm, range */
}
actions = { forward; drop; }
size = 1024; /* 최대 엔트리 수 */
default_action = drop();
}
apply { /* apply: 제어 블록 실행 진입점 */
dmac_table.apply();
}
}
/* 4단계: 디파서 — 수정된 헤더를 패킷에 다시 조합 */
control MyDeparser(packet_out pkt, in headers_t hdr) {
apply {
pkt.emit(hdr.ethernet); /* emit(): 헤더를 출력 패킷에 직렬화 */
}
}
/* 파이프라인 인스턴스화 (V1Model 아키텍처) */
V1Switch(MyParser(), MyVerifyChecksum(), MyIngress(),
MyEgress(), MyComputeChecksum(), MyDeparser()) main;
코드 설명
-
header / struct
header는 패킷에서 추출 가능한 필드 묶음이며isValid()메서드를 가집니다.struct는 일반 데이터 묶음으로, 메타데이터나 여러 헤더를 모을 때 사용합니다. P416의 기본 타입은bit<W>(부호 없음)과int<W>(부호 있음)이며, W는 비트 폭입니다. -
parser + state + transition
파서는 유한 상태 머신(FSM)으로 동작합니다. 각
state에서extract()로 헤더를 추출하고,transition select()로 다음 상태를 결정합니다. 최종 상태는accept(정상) 또는reject(파싱 실패)입니다. -
table + key + actions
매치-액션 테이블의
key는 매치 필드와 매치 타입(exact,ternary,lpm,range)을 지정합니다.actions는 매치 시 실행 가능한 액션 목록이며,default_action은 미스(Miss) 시 실행됩니다. 테이블 엔트리는 P4Runtime을 통해 런타임에 추가/삭제합니다. - V1Switch(...) main V1Model 아키텍처의 파이프라인 인스턴스입니다. V1Model은 Parser → VerifyChecksum → Ingress → Egress → ComputeChecksum → Deparser 6단계 파이프라인을 정의합니다.
P416 타입 시스템
| 타입 | 문법 | 설명 | 예시 |
|---|---|---|---|
| 고정 폭 비트 | bit<W> | W비트 부호 없는 정수 | bit<48> mac_addr; |
| 부호 있는 정수 | int<W> | W비트 부호 있는 정수 (2의 보수) | int<16> offset; |
| 가변 폭 비트 | varbit<W> | 최대 W비트, 실제 길이는 런타임 결정 | varbit<320> options; (IPv4 옵션) |
| 불리언 | bool | 참/거짓 | bool hit = table.apply().hit; |
| 에러 | error | 파서 에러 코드 | error { InvalidHeader } |
| 열거형 | enum | 이름 있는 상수 집합 | enum bit<2> ct_state_t { NEW=0, EST=1 } |
| 헤더 | header | 패킷 추출 가능 필드 + validity 비트 | header ipv4_t { ... } |
| 헤더 유니온 | header_union | 한 번에 하나만 유효한 헤더 그룹 | header_union l4_t { tcp_t tcp; udp_t udp; } |
| 구조체 | struct | 이종(Heterogeneous) 필드 묶음 | struct metadata_t { ... } |
| 헤더 스택 | header[N] | 동일 헤더의 고정 크기 배열 | mpls_t[8] mpls_stack; |
| extern | extern | 타깃이 제공하는 외부 객체/함수 | extern register<T> { ... } |
P416 프로그램 컴파일 및 배포 흐름
P4 프로그램은 소스 코드 작성부터 디바이스 배포까지 아래 흐름을 따릅니다. 컴파일러(p4c)는 P4 소스와 아키텍처 모델을 입력받아 타깃별 바이너리와 P4Info(테이블/액션 메타데이터)를 생성합니다.
P4 아키텍처 모델
P416은 언어와 타깃 하드웨어 사이에 아키텍처 모델(Architecture Model) 계층을 도입하여 이식성을 확보합니다. 아키텍처 모델은 파이프라인 구조(파서, 제어 블록의 수와 순서), 사용 가능한 extern 객체, 표준 메타데이터 등을 정의합니다.
| 아키텍처 | 용도 | 파이프라인 구조 | 타깃 플랫폼 |
|---|---|---|---|
| V1Model | 학습/프로토타이핑 | Parser → VerifyChecksum → Ingress → Egress → ComputeChecksum → Deparser (6단계) | BMv2 (소프트웨어) |
| PSA (Portable Switch Architecture) | 표준 스위치 아키텍처 | Ingress (Parser→Control→Deparser) + Egress (Parser→Control→Deparser) | 표준 호환 스위치 |
| PNA (Portable NIC Architecture) | 표준 NIC 아키텍처 | Pre-control → Main (Parser→Control→Deparser) | SmartNIC/DPU |
| TNA (Tofino Native Architecture) | Intel Tofino 전용 | Ingress (Parser→MAU 12~20단계→Deparser) + Egress (동일) | Intel Tofino 1/2 |
| T2NA | Intel Tofino 2 전용 | TNA 확장 (20 MAU 스테이지, Ghost thread, Dynamic hash) | Intel Tofino 2 |
P4 개발 환경 및 도구
p4c — P4 레퍼런스 컴파일러
p4c는 P4.org에서 관리하는 오픈 소스 P4 컴파일러입니다. 프론트엔드(구문 분석, 타입 검사), 미드엔드(중간 표현 최적화), 백엔드(타깃별 코드 생성) 3단계로 구성됩니다.
# p4c 설치 (Ubuntu 22.04+)
sudo apt-get install p4lang-p4c
# BMv2 백엔드로 컴파일 (V1Model 아키텍처)
p4c --target bmv2 --arch v1model -o ngfw.json ngfw.p4
# Tofino 백엔드로 컴파일 (Intel P4 Studio 필요)
# p4c-tofino --target tofino --arch tna -o ngfw.bin ngfw.p4
# P4Info 파일도 함께 생성 (P4Runtime 컨트롤 플레인용)
p4c --target bmv2 --arch v1model \
--p4runtime-files ngfw_p4info.pb.txt \
-o ngfw.json ngfw.p4
BMv2 (Behavioral Model v2) — 소프트웨어 레퍼런스 스위치
BMv2는 P4 프로그램을 소프트웨어에서 실행하는 레퍼런스 구현체입니다. 실제 ASIC의 라인 레이트 성능은 제공하지 않지만, P4 프로그램의 기능 검증과 디버깅에 필수적인 도구입니다.
# BMv2 simple_switch 실행
sudo simple_switch --interface 0@veth0 --interface 1@veth2 \
--log-console --log-level trace \
ngfw.json
# BMv2 simple_switch_grpc (P4Runtime 지원)
sudo simple_switch_grpc --interface 0@veth0 --interface 1@veth2 \
--log-console \
-- --grpc-server-addr 0.0.0.0:9559 \
ngfw.json
# simple_switch_CLI로 테이블 엔트리 수동 추가
simple_switch_CLI --thrift-port 9090 <
table_add dmac_table forward 00:00:00:00:00:01 => 1
table_dump acl_table
EOF
P4 개발 도구 종합
| 도구 | 용도 | 라이선스 | 비고 |
|---|---|---|---|
| p4c | P4 컴파일러 (오픈 소스) | Apache 2.0 | BMv2, DPDK, eBPF 백엔드 포함 |
| BMv2 | 소프트웨어 레퍼런스 스위치 | Apache 2.0 | simple_switch / simple_switch_grpc |
| p4c-ebpf | P4 → eBPF/XDP 컴파일 | Apache 2.0 | p4c의 eBPF 백엔드, 일반 NIC에서 P4 로직 실행 |
| Mininet + BMv2 | 가상 네트워크 토폴로지 테스트 | 오픈 소스 | p4-utils / p4app으로 자동화 |
| P4Runtime Shell | 대화형 P4Runtime 클라이언트 | Apache 2.0 | Python 기반 REPL, 테이블 엔트리 조작 |
| PTF (Packet Test Framework) | P4 프로그램 단위 테스트 | Apache 2.0 | 패킷 송수신 검증, CI/CD 연동 |
| Intel P4 Studio / SDE | Tofino 전용 개발 환경 | 상용 (NDA) | p4c-tofino + Barefoot Runtime + 디버거 |
| Pensando SDK | AMD Pensando DSC 개발 | 상용 | PNA 아키텍처 기반 |
P4 파이프라인 개요와 NGFW 적용
P416은 패킷 처리 파이프라인을 정의하는 도메인 특화 언어(DSL)입니다. 네트워크 하드웨어의 데이터 플레인 동작을 소프트웨어처럼 프로그래밍하되, 컴파일 결과가 ASIC/FPGA에 직접 매핑되어 라인 레이트 처리를 보장합니다.
P4 파이프라인 핵심 구성 요소
| 구성 요소 | 역할 | NGFW 적용 |
|---|---|---|
| 프로그래머블 파서(Parser) | 패킷 헤더를 바이트 단위로 추출, 상태 머신 기반 프로토콜 탐색 | 커스텀 프로토콜(GTP, VXLAN-GPE, SRv6 등) 파싱, DPI 전처리 |
| 매치-액션 테이블(MAT) | 추출된 필드를 키로 테이블 조회, 매치 시 액션 실행 | 5-tuple ACL, 커스텀 헤더 기반 필터링, 동적 블랙리스트 |
| 매치 타입 | exact(정확), ternary(와일드카드), LPM(최장 프리픽스), range | exact: conntrack lookup, ternary: ACL, LPM: 라우팅 |
| 레지스터(Register) | 스테이트풀 메모리, 패킷 간 상태 유지 | 커넥션 트래킹 상태 머신 (SYN→ESTABLISHED→FIN) |
| 카운터/미터(Counter/Meter) | 패킷/바이트 카운트, 토큰 버킷 기반 속도 제한 | 플로우별 통계, QoS 속도 제한 |
| 다이제스트(Digest) | 패킷 메타데이터를 CPU(컨트롤 플레인)로 전송 | 새 플로우 알림, 이상 탐지 이벤트 전달 |
| 디파서(Deparser) | 수정된 헤더를 다시 조합하여 패킷 재구성 | NAT 변환 후 헤더 재조합, 터널 캡슐화 |
P4가 NGFW에 중요한 이유
- 커스텀 프로토콜 라인 레이트 파싱: TC flower는 미리 정의된 L2-L4 필드만 매치할 수 있지만, P4는 임의의 프로토콜 헤더를 파싱할 수 있습니다. GTP-U 내부 5-tuple, SRv6 SID, VXLAN-GPE 메타데이터 등을 하드웨어에서 직접 검사합니다.
- 하드웨어 스테이트풀 처리: 레지스터 어레이를 사용하여 커넥션 상태 머신을 ASIC 내부에서 실행합니다. 소프트웨어 conntrack 없이도 SYN flood 탐지, 상태 기반 필터링이 가능합니다.
- 라인 레이트 보장: P4 프로그램은 ASIC의 매치-액션 유닛(MAU) 스테이지에 직접 매핑되므로, 패킷당 처리 시간이 고정됩니다. TC flower 오프로드와 달리 복잡한 규칙에서도 성능 저하가 없습니다.
- 동적 정책 업데이트: 테이블 엔트리는 런타임에 컨트롤 플레인(P4Runtime, gRPC)에서 추가/삭제할 수 있어, 방화벽 규칙을 재컴파일 없이 업데이트합니다.
P4 NGFW 5-tuple ACL + Stateful 방화벽 구현
P416으로 5-tuple ACL과 스테이트풀 커넥션 트래킹을 구현하는 코드를 살펴봅니다. 이 코드는 P416 표준 아키텍처(V1Model)를 기준으로 하며, 실제 ASIC에 배포할 때는 플랫폼별 아키텍처(TNA, PNA 등)로 포팅(Porting)합니다.
헤더 정의
/* P4_16 NGFW 헤더 정의 */
header ethernet_t {
bit<48> dst_addr;
bit<48> src_addr;
bit<16> ether_type;
}
header ipv4_t {
bit<4> version;
bit<4> ihl;
bit<8> dscp_ecn;
bit<16> total_len;
bit<16> identification;
bit<3> flags;
bit<13> frag_offset;
bit<8> ttl;
bit<8> protocol;
bit<16> hdr_checksum;
bit<32> src_addr;
bit<32> dst_addr;
}
header tcp_t {
bit<16> src_port;
bit<16> dst_port;
bit<32> seq_no;
bit<32> ack_no;
bit<4> data_offset;
bit<3> res;
bit<9> flags; /* NS,CWR,ECE,URG,ACK,PSH,RST,SYN,FIN */
bit<16> window;
bit<16> checksum;
bit<16> urgent_ptr;
}
header udp_t {
bit<16> src_port;
bit<16> dst_port;
bit<16> length;
bit<16> checksum;
}
struct headers_t {
ethernet_t ethernet;
ipv4_t ipv4;
tcp_t tcp;
udp_t udp;
}
struct metadata_t {
bit<2> ct_state; /* 0=NEW, 1=ESTABLISHED, 2=RELATED, 3=INVALID */
bit<1> acl_permit;
bit<32> flow_hash;
bit<8> meter_color; /* 0=GREEN, 1=YELLOW, 2=RED */
}
프로그래머블 파서
P4 파서는 유한 상태 머신(Finite State Machine)으로 구현됩니다. 아래 다이어그램은 NGFW 파서의 상태 전이를 보여줍니다.
/* P4_16 프로그래머블 파서 */
parser NgfwParser(packet_in pkt,
out headers_t hdr,
inout metadata_t meta,
inout standard_metadata_t std_meta) {
state start {
pkt.extract(hdr.ethernet);
transition select(hdr.ethernet.ether_type) {
0x0800: parse_ipv4;
default: accept;
}
}
state parse_ipv4 {
pkt.extract(hdr.ipv4);
transition select(hdr.ipv4.protocol) {
6: parse_tcp;
17: parse_udp;
default: accept;
}
}
state parse_tcp {
pkt.extract(hdr.tcp);
transition accept;
}
state parse_udp {
pkt.extract(hdr.udp);
transition accept;
}
}
5-tuple ACL 테이블 + 스테이트풀 방화벽
P4 레지스터(Register)를 사용하여 하드웨어에서 직접 커넥션 트래킹 상태 머신을 구현합니다. 아래 다이어그램은 TCP 커넥션 상태 전이를 보여줍니다.
/* 커넥션 트래킹 상태 상수 */
const bit<2> CT_NEW = 0;
const bit<2> CT_ESTABLISHED = 1;
const bit<2> CT_RELATED = 2;
const bit<2> CT_INVALID = 3;
/* TCP 플래그 비트 위치 */
const bit<9> TCP_SYN = 0x002;
const bit<9> TCP_ACK = 0x010;
const bit<9> TCP_FIN = 0x001;
const bit<9> TCP_RST = 0x004;
control NgfwIngress(inout headers_t hdr,
inout metadata_t meta,
inout standard_metadata_t std_meta) {
/* --- 레지스터: 커넥션 상태 테이블 (해시 기반) --- */
register<bit<2>>(65536) ct_state_reg; /* 상태: NEW/EST/REL/INV */
register<bit<32>>(65536) ct_timeout_reg; /* 타임스탬프 기반 만료 */
/* --- 카운터 --- */
direct_counter(CounterType.packets_and_bytes) acl_counter;
/* --- 미터: 플로우별 속도 제한 --- */
meter(1024, MeterType.bytes) flow_meter;
/* --- 5-tuple ACL 테이블 (ternary 매치) --- */
action permit() {
meta.acl_permit = 1;
}
action deny() {
meta.acl_permit = 0;
mark_to_drop(std_meta);
}
action permit_and_meter(bit<32> meter_idx) {
meta.acl_permit = 1;
flow_meter.execute_meter(meter_idx, meta.meter_color);
}
table acl_table {
key = {
hdr.ipv4.src_addr : ternary;
hdr.ipv4.dst_addr : ternary;
hdr.ipv4.protocol : ternary;
hdr.tcp.src_port : ternary; /* TCP/UDP 공용 */
hdr.tcp.dst_port : ternary;
}
actions = {
permit;
deny;
permit_and_meter;
}
size = 16384;
default_action = deny();
counters = acl_counter;
}
/* --- 커넥션 트래킹 로직 --- */
action compute_flow_hash() {
hash(meta.flow_hash, HashAlgorithm.crc32,
(bit<32>)0,
{ hdr.ipv4.src_addr, hdr.ipv4.dst_addr,
hdr.ipv4.protocol, hdr.tcp.src_port, hdr.tcp.dst_port },
(bit<32>)65535);
}
action ct_lookup() {
bit<2> state;
ct_state_reg.read(state, meta.flow_hash);
meta.ct_state = state;
}
action ct_update_established() {
ct_state_reg.write(meta.flow_hash, CT_ESTABLISHED);
ct_timeout_reg.write(meta.flow_hash,
(bit<32>)std_meta.ingress_global_timestamp);
}
action ct_create_new() {
ct_state_reg.write(meta.flow_hash, CT_NEW);
ct_timeout_reg.write(meta.flow_hash,
(bit<32>)std_meta.ingress_global_timestamp);
}
/* --- 새 플로우 CPU 알림 (Digest) --- */
action send_digest_to_cpu() {
digest<headers_t>(1, { hdr.ethernet, hdr.ipv4, hdr.tcp, hdr.udp });
}
apply {
if (!hdr.ipv4.isValid()) {
return;
}
/* 1단계: 플로우 해시 계산 */
compute_flow_hash();
/* 2단계: CT 상태 조회 */
ct_lookup();
/* 3단계: TCP 상태 머신 */
if (hdr.tcp.isValid()) {
if (meta.ct_state == CT_NEW &&
(hdr.tcp.flags & TCP_ACK) != 0) {
/* SYN-ACK 응답 → ESTABLISHED */
ct_update_established();
meta.ct_state = CT_ESTABLISHED;
} else if (meta.ct_state == CT_NEW &&
(hdr.tcp.flags & TCP_SYN) != 0) {
/* SYN 패킷 → 새 플로우 등록, CPU 알림 */
ct_create_new();
send_digest_to_cpu();
} else if ((hdr.tcp.flags & TCP_RST) != 0 ||
(hdr.tcp.flags & TCP_FIN) != 0) {
/* RST/FIN → 상태 초기화 */
ct_state_reg.write(meta.flow_hash, CT_NEW);
}
}
/* 4단계: ESTABLISHED 세션은 ACL 건너뛰기 (Fast Path) */
if (meta.ct_state == CT_ESTABLISHED) {
meta.acl_permit = 1;
} else {
/* 5단계: 5-tuple ACL 테이블 조회 */
acl_table.apply();
}
/* 6단계: 속도 제한 확인 */
if (meta.meter_color == 2) { /* RED → 드롭 */
mark_to_drop(std_meta);
return;
}
/* 7단계: 허용/차단 */
if (meta.acl_permit == 0) {
mark_to_drop(std_meta);
}
}
}
NAT 리라이트 테이블
/* NAT 변환 테이블 */
action do_dnat(bit<32> new_dst_addr, bit<16> new_dst_port) {
hdr.ipv4.dst_addr = new_dst_addr;
hdr.tcp.dst_port = new_dst_port;
/* 체크섬은 디파서에서 재계산 */
}
action do_snat(bit<32> new_src_addr, bit<16> new_src_port) {
hdr.ipv4.src_addr = new_src_addr;
hdr.tcp.src_port = new_src_port;
}
table nat_table {
key = {
hdr.ipv4.dst_addr : exact;
hdr.tcp.dst_port : exact;
}
actions = {
do_dnat;
do_snat;
NoAction;
}
size = 8192;
default_action = NoAction();
}
/* 디파서: 수정된 헤더 재조합 */
control NgfwDeparser(packet_out pkt, in headers_t hdr) {
apply {
pkt.emit(hdr.ethernet);
pkt.emit(hdr.ipv4);
pkt.emit(hdr.tcp);
pkt.emit(hdr.udp);
}
}
플랫폼별 P4 NGFW 구현
P4 프로그램은 타깃 플랫폼의 아키텍처 모델에 맞게 컴파일됩니다. 각 플랫폼은 고유한 하드웨어 구조를 가지며, P4 NGFW 구현 시 플랫폼별 특성을 활용해야 최적 성능을 달성합니다.
(a) Intel Tofino
Tofino는 Intel(구 Barefoot Networks)의 P4 네이티브 ASIC으로, 데이터센터 스위치용으로 설계되었습니다.
| 항목 | Tofino 1 | Tofino 2 |
|---|---|---|
| 처리량 | 6.5 Tbps | 12.8 Tbps |
| 아키텍처 | TNA (Tofino Native Architecture) | T2NA |
| MAU 스테이지 | 12단계 | 20단계 |
| SRAM | ~80 MB | ~160 MB |
| TCAM | ~8 MB | ~16 MB |
| 레지스터 | 스테이지당 ALU 연산 지원 | 확장된 ALU + 해시 함수 |
| 특수 기능 | Mirror, Resubmit, Clone | + Ghost thread, Dynamic hash |
/* Tofino TNA 아키텍처용 NGFW 파이프라인 (개요) */
Pipeline(
NgfwIngressParser(), /* 프로그래머블 파서 */
NgfwIngress(), /* 인그레스: ACL + CT + NAT */
NgfwIngressDeparser(), /* 인그레스 디파서 */
NgfwEgressParser(), /* 이그레스 파서 */
NgfwEgress(), /* 이그레스: QoS 미터 + 미러링 */
NgfwEgressDeparser() /* 최종 디파서 */
) pipe;
Switch(pipe) main;
/* Tofino 특화: Resubmit으로 다단계 CT 조회 구현 */
/* 1차 패스: 정방향 해시로 CT 조회 */
/* miss → resubmit → 2차 패스: 역방향 해시로 CT 조회 */
(b) AMD Pensando DSC
AMD Pensando DSC(Distributed Services Card)는 Elba ASIC 기반의 SmartNIC/DPU로, P4 프로그래머블 파이프라인과 내장 ARM 코어를 결합합니다.
- 인라인 P4 파이프라인: 최대 200 Gbps 라인 레이트 처리
- 내장 ARM 코어: 예외 패킷 처리, 컨트롤 플레인, DPI 엔진 실행
- 통합 메모리: P4 테이블과 ARM 코어가 공유 메모리를 통해 빠르게 통신
- PNA(Portable NIC Architecture): P4 표준 NIC 아키텍처 지원
# Pensando DSC P4 프로그램 배포
# penctl: Pensando 관리 CLI
penctl system tech-support # 시스템 상태 확인
penctl p4 program load --name ngfw_pipeline --file ngfw.p4bin
penctl p4 table entry add --table acl_table \
--key "10.0.0.0/8, *, 6, *, 443" \
--action permit
penctl p4 table entry add --table acl_table \
--key "*, *, *, *, *" \
--action deny
(c) Intel IPU (Infrastructure Processing Unit)
Intel IPU(구 Mount Evans)는 IDPF(Infrastructure Data Path Function) 드라이버와 통합되는 인프라 오프로드 전용 프로세서입니다.
- P4 파이프라인 + 내장 Xeon 코어: 패킷 처리와 인프라 서비스를 동시에 오프로드
- IDPF 드라이버: 호스트 커널과 IPU 간 표준화된 인터페이스
- 인프라 오프로드: NGFW뿐 아니라 스토리지, 가상 네트워크, 텔레메트리를 통합 처리
- P4-SDE: Intel P4 Software Development Environment를 통해 프로그래밍
플랫폼 비교 종합
| 항목 | Intel Tofino | AMD Pensando DSC | Intel IPU |
|---|---|---|---|
| 폼 팩터 | 스위치 ASIC | SmartNIC (PCIe) | IPU (PCIe) |
| 처리량 | 6.5~12.8 Tbps | 최대 200 Gbps | 최대 200 Gbps |
| P4 아키텍처 | TNA/T2NA | PNA (Portable NIC Arch) | P4-SDE |
| 스테이트풀 지원 | 레지스터 + ALU | 레지스터 + ARM 코어 | 레지스터 + Xeon 코어 |
| 내장 CPU | 없음 (외부 CPU 필요) | ARM A72 (16코어) | Xeon-D (최대 24코어) |
| NGFW 적합성 | 초고속 스위치 방화벽 | 호스트 NIC 인라인 NGFW | 인프라 통합 NGFW |
| 생태계 | SDE + P4Studio | Pensando SDK | Intel P4-SDE + IPDK |
| 주요 용도 | 데이터센터 경계 방화벽 | 서버 단위 마이크로세그멘테이션 | 클라우드 인프라 보안 |
P4Runtime 컨트롤 플레인 연동
P4Runtime은 P4 프로그래머블 디바이스의 데이터 플레인을 원격으로 제어하기 위한 gRPC 기반 API입니다. P4Runtime v1.3 사양을 기준으로, 테이블 엔트리 CRUD, ActionProfile/Selector, PacketIn/Out, Digest 등의 기능을 제공합니다.
P4Runtime API 주요 서비스
| 서비스 | RPC 메서드 | 설명 |
|---|---|---|
| Write | Write(WriteRequest) | 테이블 엔트리, 카운터, 미터 등의 생성/수정/삭제 |
| Read | Read(ReadRequest) | 테이블 엔트리, 카운터, 미터 값 조회 |
| StreamChannel | 양방향 스트리밍 | PacketIn/Out, Digest, 마스터 선출(Arbitration) |
| SetForwardingPipelineConfig | 파이프라인 설정 | P4Info + 디바이스 바이너리를 디바이스에 로딩 |
| GetForwardingPipelineConfig | 파이프라인 조회 | 현재 디바이스에 로딩된 P4 프로그램 정보 조회 |
테이블 엔트리 CRUD 연산
P4Runtime의 WriteRequest는 하나 이상의 Update 메시지를 포함하며, 각 Update는 INSERT, MODIFY, DELETE 타입을 가집니다. 이를 통해 ACL 규칙을 런타임에 추가하거나 삭제할 수 있습니다.
ActionProfile / ActionSelector (ECMP)
ActionProfile은 테이블 액션을 간접 참조하여 여러 테이블이 동일한 액션 세트를 공유할 수 있게 합니다. ActionSelector는 ActionProfile에 해시 기반 선택 로직을 추가하여 ECMP(Equal-Cost Multi-Path) 로드 밸런싱을 구현합니다. NGFW에서는 다중 백엔드 서버로의 트래픽 분산에 활용됩니다.
PacketIn/PacketOut (CPU Punt)
PacketIn은 데이터 플레인에서 컨트롤 플레인(CPU)으로 패킷을 전달하는 메커니즘입니다. NGFW에서는 새 플로우의 첫 패킷, DPI가 필요한 패킷, 예외 패킷을 CPU로 punt합니다. PacketOut은 반대로 CPU에서 데이터 플레인으로 패킷을 주입합니다. ARP 응답, ICMP 에러, 컨트롤 패킷 전송에 사용됩니다.
DigestList (새 플로우 알림)
Digest는 PacketIn보다 가벼운 CPU 알림 메커니즘입니다. 전체 패킷을 전송하는 대신, P4 프로그램에서 지정한 필드(5-tuple, 타임스탬프 등)만 추출하여 컨트롤 플레인으로 전달합니다. 대역폭 소모가 적어 새 플로우 알림에 적합합니다.
Python P4Runtime 클라이언트 예시
# P4Runtime Python 클라이언트 — ACL 규칙 추가/삭제
# p4runtime-shell 또는 google.protobuf 기반
import grpc
from p4.v1 import p4runtime_pb2, p4runtime_pb2_grpc
from p4.config.v1 import p4info_pb2
import google.protobuf.text_format as tf
class NgfwP4RuntimeClient:
def __init__(self, address, device_id, election_id):
self.channel = grpc.insecure_channel(address)
self.stub = p4runtime_pb2_grpc.P4RuntimeStub(self.channel)
self.device_id = device_id
self.election_id = election_id
def master_arbitration(self):
"""마스터 선출 — 컨트롤 플레인 주도권 획득"""
req = p4runtime_pb2.StreamMessageRequest()
req.arbitration.device_id = self.device_id
req.arbitration.election_id.high = 0
req.arbitration.election_id.low = self.election_id
return req
def add_acl_entry(self, src_ip, dst_ip, dst_port,
protocol, action="permit"):
"""5-tuple ACL 엔트리 추가"""
entry = p4runtime_pb2.TableEntry()
entry.table_id = 33618978 # acl_table ID (P4Info 참조)
# 소스 IP 매치 (ternary)
m1 = entry.match.add()
m1.field_id = 1 # hdr.ipv4.src_addr
m1.ternary.value = src_ip.encode_to_bytes()
m1.ternary.mask = b'\xff\xff\xff\x00' # /24
# 목적지 IP 매치 (ternary)
m2 = entry.match.add()
m2.field_id = 2 # hdr.ipv4.dst_addr
m2.ternary.value = dst_ip.encode_to_bytes()
m2.ternary.mask = b'\xff\xff\xff\xff' # /32
# 목적지 포트 매치 (ternary)
m3 = entry.match.add()
m3.field_id = 5 # hdr.tcp.dst_port
m3.ternary.value = dst_port.to_bytes(2, 'big')
m3.ternary.mask = b'\xff\xff'
# 액션 설정
if action == "permit":
entry.action.action.action_id = 16829080 # permit()
else:
entry.action.action.action_id = 16805856 # deny()
# WriteRequest 전송
req = p4runtime_pb2.WriteRequest()
req.device_id = self.device_id
update = req.updates.add()
update.type = p4runtime_pb2.Update.INSERT
update.entity.table_entry.CopyFrom(entry)
self.stub.Write(req)
return entry
def delete_acl_entry(self, entry):
"""ACL 엔트리 삭제"""
req = p4runtime_pb2.WriteRequest()
req.device_id = self.device_id
update = req.updates.add()
update.type = p4runtime_pb2.Update.DELETE
update.entity.table_entry.CopyFrom(entry)
self.stub.Write(req)
def read_counters(self, counter_id):
"""카운터 값 읽기"""
req = p4runtime_pb2.ReadRequest()
req.device_id = self.device_id
entity = req.entities.add()
entity.counter_entry.counter_id = counter_id
responses = self.stub.Read(req)
for resp in responses:
for entity in resp.entities:
ce = entity.counter_entry
print(f"Counter {ce.counter_id}: "
f"packets={ce.data.packet_count}, "
f"bytes={ce.data.byte_count}")
def listen_digests(self):
"""Digest 수신 — 새 플로우 알림 처리"""
def stream_req():
yield self.master_arbitration()
responses = self.stub.StreamChannel(stream_req())
for resp in responses:
if resp.HasField("digest"):
digest = resp.digest
for data in digest.data:
# 새 플로우의 5-tuple 추출
src_ip = data.struct.members[0].bitstring
dst_ip = data.struct.members[1].bitstring
print(f"New flow: {src_ip} -> {dst_ip}")
# Digest ACK 전송
ack = p4runtime_pb2.StreamMessageRequest()
ack.digest_ack.digest_id = digest.digest_id
ack.digest_ack.list_id = digest.list_id
# 사용 예시
client = NgfwP4RuntimeClient("192.168.1.1:9559", device_id=0,
election_id=1)
# HTTPS(443) 허용 규칙 추가
client.add_acl_entry(
src_ip="10.0.1.0", dst_ip="10.0.2.100",
dst_port=443, protocol=6, action="permit"
)
# ACL 카운터 조회
client.read_counters(counter_id=302055013)
코드 설명
-
master_arbitration
P4Runtime은 여러 컨트롤러가 동시에 연결될 수 있으므로,
election_id가 가장 높은 컨트롤러가 마스터(Master)로 선출됩니다. 마스터만Write연산을 수행할 수 있습니다. -
add_acl_entry
P4Info 파일에서 테이블 ID와 필드 ID를 참조하여 ternary 매치 엔트리를 구성합니다.
ternary매치는 value와 mask 쌍으로 와일드카드 매칭을 지정합니다. - read_counters P4에서 정의한 direct_counter 또는 indirect counter의 패킷/바이트 카운트를 읽습니다. NGFW 모니터링과 로깅에 활용합니다.
- listen_digests StreamChannel의 양방향 스트리밍을 통해 데이터 플레인에서 전송한 Digest 메시지를 수신합니다. 새 플로우가 탐지되면 컨트롤 플레인에서 DPI를 수행하고 그 결과에 따라 ACL 엔트리를 추가합니다.
SDN 컨트롤러 통합
| SDN 컨트롤러 | P4Runtime 지원 | NGFW 활용 |
|---|---|---|
| ONOS | 네이티브 P4Runtime 지원 (PI 프레임워크) | fabric.p4 파이프라인으로 스위치 방화벽 구현, 중앙 집중식 정책 관리 |
| SONiC | SAI P4Runtime 확장 | 데이터센터 스위치 ACL, 커스텀 프로토콜 필터링 |
| Stratum | P4Runtime 레퍼런스 구현 | 하드웨어 추상화 계층(HAL), 멀티 벤더 호환 |
| 커스텀 컨트롤러 | gRPC 직접 사용 | 위 Python 예시처럼 직접 P4Runtime API 호출 |
모니터링: 카운터/미터 읽기
P4Runtime의 Read RPC로 데이터 플레인 카운터와 미터를 주기적으로 폴링하여 NGFW 모니터링 대시보드를 구성할 수 있습니다.
- Direct Counter: 테이블 엔트리별 패킷/바이트 카운트. ACL 규칙별 히트 수 모니터링에 사용합니다.
- Indirect Counter: 인덱스 기반 독립 카운터. 포트별, VLAN별 트래픽 통계에 사용합니다.
- Direct Meter: 테이블 엔트리별 토큰 버킷 속도 제한. 플로우별 QoS에 사용합니다.
- Meter 색상 읽기: 현재 미터 상태(GREEN/YELLOW/RED)를 조회하여 속도 제한 정책의 효과를 모니터링합니다.
P4 vs TC flower vs eBPF 비교
NGFW 오프로드에 사용할 수 있는 세 가지 주요 기술(P4, TC flower, eBPF)의 특성을 비교합니다. 각 기술은 서로 다른 설계 철학과 트레이드오프를 가지며, 환경에 따라 적합한 선택이 달라집니다.
| 비교 항목 | P4 | TC flower + eSwitch | eBPF (XDP/TC) |
|---|---|---|---|
| 프로그래밍 수준 | 데이터 플레인 전체 정의 | 고정 매치-액션 조합 | 커널 공간 바이트코드 |
| 라인 레이트 보장 | 보장 (ASIC 매핑) | 보장 (HW 오프로드 시) | 불가 (CPU 실행) |
| 커스텀 프로토콜 파싱 | 완전 지원 | 미지원 (사전 정의 필드만) | 제한적 지원 (바이트 접근) |
| 스테이트풀 처리 | 레지스터 (HW) | conntrack 오프로드 (HW) | BPF 맵 (SW) |
| 최대 처리량 | Tbps급 | 100~400 Gbps | 10~100 Gbps (CPU 의존) |
| 학습 곡선 | 높음 (P4 언어 + ASIC 이해) | 낮음 (tc 명령어) | 중간 (C 유사 + BPF 제약) |
| 생태계 성숙도 | 제한적 (전용 HW 필요) | 높음 (주요 NIC 벤더 지원) | 높음 (커널 내장, 활발한 커뮤니티) |
| 벤더 종속성 | ASIC별 아키텍처 차이 | NIC 벤더별 지원 범위 상이 | 없음 (커널 표준) |
| 커널 통합 | 별도 SDK (비커널) | 완전 통합 (TC subsystem) | 완전 통합 (BPF subsystem) |
| 동적 업데이트 | 테이블 엔트리 (P4Runtime) | tc filter add/del | BPF 맵 업데이트 + 프로그램 교체 |
| 적합 사용 사례 | 초고속 커스텀 프로토콜 NGFW | 표준 L2-L4 오프로드 NGFW | 유연한 커널 공간 보안 처리 |
P4 매치-액션 파이프라인 다이어그램
아래 다이어그램은 P4 프로그래머블 파이프라인에서 NGFW 패킷 처리 흐름을 보여줍니다. 각 매치-액션 테이블은 ASIC의 MAU 스테이지에 매핑되며, 라인 레이트로 실행됩니다.
커널 소스 분석
BPF netfilter와 kfunc conntrack 구현의 핵심 커널 소스 코드를 분석합니다. 이 분석은 커널 6.6 기준입니다.
nf_bpf_link.c — BPF netfilter 링크 등록
/* net/netfilter/nf_bpf_link.c */
/* BPF 프로그램을 Netfilter 훅에 연결하는 링크 구현 */
struct bpf_nf_link {
struct bpf_link link;
struct nf_hook_ops hook_ops;
struct net *net;
u32 dead;
};
static unsigned int
nf_hook_run_bpf(void *bpf_prog, struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct bpf_nf_ctx ctx = {
.state = state,
.skb = skb,
};
return bpf_prog_run(bpf_prog, &ctx);
}
static int bpf_nf_link_attach(const union bpf_attr *attr,
struct bpf_prog *prog)
{
struct bpf_nf_link *link;
struct net *net;
/* 프로그램 타입 검증 */
if (prog->type != BPF_PROG_TYPE_NETFILTER)
return -EINVAL;
link = kzalloc(sizeof(*link), GFP_USER);
if (!link)
return -ENOMEM;
bpf_link_init(&link->link, BPF_LINK_TYPE_NETFILTER,
&bpf_nf_link_lops, prog);
/* Netfilter 훅 등록 */
link->hook_ops.hook = nf_hook_run_bpf;
link->hook_ops.hook_ops_type = NF_HOOK_OP_BPF;
link->hook_ops.hooknum = attr->link_create.netfilter.hooknum;
link->hook_ops.pf = attr->link_create.netfilter.pf;
link->hook_ops.priority = attr->link_create.netfilter.priority;
link->hook_ops.priv = prog;
net = current->nsproxy->net_ns;
link->net = net;
return nf_register_net_hook(net, &link->hook_ops);
}
코드 설명
-
struct bpf_nf_link
bpf_link기반 구조체로, BPF 프로그램과 Netfilter 훅을 연결합니다.nf_hook_ops에 훅 번호(PREROUTING, INPUT 등), 프로토콜 패밀리, 우선순위를 설정합니다. -
nf_hook_run_bpf
Netfilter 훅에서 호출되는 콜백 함수입니다.
bpf_nf_ctx를 구성하여 BPF 프로그램에sk_buff와nf_hook_state를 전달합니다. 반환값은NF_ACCEPT,NF_DROP등의 Netfilter verdict입니다. -
bpf_nf_link_attach
유저스페이스에서
bpf(BPF_LINK_CREATE)시스콜 시 호출됩니다. 프로그램 타입이BPF_PROG_TYPE_NETFILTER인지 검증한 후,nf_register_net_hook()으로 커널 Netfilter에 훅을 등록합니다.
bpf.h — BPF_PROG_TYPE_NETFILTER 정의
/* include/linux/bpf.h (일부) */
/* include/uapi/linux/bpf.h */
enum bpf_prog_type {
BPF_PROG_TYPE_UNSPEC = 0,
BPF_PROG_TYPE_SOCKET_FILTER = 1,
BPF_PROG_TYPE_KPROBE = 2,
/* ... 생략 ... */
BPF_PROG_TYPE_XDP = 6,
/* ... 생략 ... */
BPF_PROG_TYPE_SCHED_CLS = 3, /* tc-bpf */
/* ... 생략 ... */
BPF_PROG_TYPE_NETFILTER = 31, /* 커널 6.4에서 추가 */
};
/* BPF netfilter 프로그램의 컨텍스트 구조체 */
struct bpf_nf_ctx {
const struct nf_hook_state *state;
struct sk_buff *skb;
};
코드 설명
- BPF_PROG_TYPE_NETFILTER = 31 커널 6.4에서 추가된 31번 프로그램 타입입니다. 이 타입의 BPF 프로그램은 Netfilter 훅 포인트(PREROUTING, INPUT, FORWARD, OUTPUT, POSTROUTING)에 직접 연결될 수 있습니다.
-
struct bpf_nf_ctx
BPF 프로그램에 전달되는 컨텍스트입니다.
nf_hook_state에는 훅 번호, 입출력 디바이스, 프로토콜 패밀리 정보가 포함되고,sk_buff에는 패킷 데이터가 포함됩니다.
nf_conntrack_bpf.c — kfunc CT 구현
/* net/netfilter/nf_conntrack_bpf.c */
/* BPF kfunc으로 nf_conntrack을 노출하는 구현 */
/* bpf_skb_ct_lookup 구현 */
__bpf_kfunc struct nf_conn *
bpf_skb_ct_lookup(struct __sk_buff *skb_ctx,
struct bpf_sock_tuple *bpf_tuple,
u32 tuple__sz,
struct bpf_ct_opts *opts,
u32 opts__sz)
{
struct sk_buff *skb = (struct sk_buff *)skb_ctx;
struct net *caller_net;
struct nf_conn *nfct;
/* 옵션 크기 검증 */
if (opts__sz < sizeof(struct bpf_ct_opts))
return NULL;
/* 네트워크 네임스페이스 해석 */
caller_net = skb->dev ? dev_net(skb->dev)
: sock_net(skb->sk);
/* 커널 conntrack 조회 */
nfct = __bpf_nf_ct_lookup(caller_net, bpf_tuple,
tuple__sz, opts, opts__sz);
return nfct;
}
/* 내부 조회 함수 */
static struct nf_conn *
__bpf_nf_ct_lookup(struct net *net,
struct bpf_sock_tuple *bpf_tuple,
u32 tuple_len,
struct bpf_ct_opts *opts,
u32 opts_len)
{
struct nf_conntrack_tuple_hash *hash;
struct nf_conntrack_tuple tuple = {};
struct nf_conn *ct;
/* bpf_sock_tuple → nf_conntrack_tuple 변환 */
if (tuple_len == sizeof(bpf_tuple->ipv4)) {
tuple.src.l3num = NFPROTO_IPV4;
tuple.src.u3.ip = bpf_tuple->ipv4.saddr;
tuple.dst.u3.ip = bpf_tuple->ipv4.daddr;
tuple.src.u.tcp.port = bpf_tuple->ipv4.sport;
tuple.dst.u.tcp.port = bpf_tuple->ipv4.dport;
}
tuple.dst.protonum = opts->l4proto;
tuple.dst.dir = opts->dir;
/* nf_conntrack_find_get() — RCU 보호 하에 해시 테이블 조회 */
hash = nf_conntrack_find_get(net, &nf_ct_zone_dflt,
&tuple);
if (!hash) {
opts->error = -ENOENT;
return NULL;
}
ct = nf_ct_tuplehash_to_ctrack(hash);
opts->dir = NF_CT_DIRECTION(hash);
return ct;
}
코드 설명
- __bpf_kfunc kfunc 매크로는 이 함수가 BPF 프로그램에서 호출 가능한 커널 함수임을 표시합니다. BTF(BPF Type Format)를 통해 함수 시그니처가 BPF verifier에 노출됩니다.
-
bpf_sock_tuple → nf_conntrack_tuple 변환
BPF에서 사용하는
bpf_sock_tuple구조체를 커널 conntrack의nf_conntrack_tuple로 변환합니다. IPv4와 IPv6는tuple_len으로 구분합니다. - nf_conntrack_find_get 커널 conntrack의 핵심 조회 함수입니다. RCU(Read-Copy-Update) 보호 하에 해시 테이블을 탐색하여 매칭되는 conntrack 엔트리를 찾고, 참조 카운트를 증가시킵니다.
verifier.c — netfilter 프로그램 검증
/* kernel/bpf/verifier.c (netfilter 관련 부분) */
/* BPF_PROG_TYPE_NETFILTER 검증 규칙 */
static const struct bpf_verifier_ops netfilter_verifier_ops = {
.get_func_proto = bpf_nf_get_func_proto,
.is_valid_access = bpf_nf_is_valid_access,
};
/* 컨텍스트 필드 접근 검증 */
static bool
bpf_nf_is_valid_access(int off, int size,
enum bpf_access_type type,
const struct bpf_prog *prog,
struct bpf_insn_access_aux *info)
{
/* bpf_nf_ctx의 state와 skb 필드만 읽기 허용 */
if (type != BPF_READ)
return false;
switch (off) {
case offsetof(struct bpf_nf_ctx, state):
info->reg_type = PTR_TO_BTF_ID;
info->btf_id = btf_nf_hook_state_id;
return true;
case offsetof(struct bpf_nf_ctx, skb):
info->reg_type = PTR_TO_BTF_ID;
info->btf_id = btf_sk_buff_id;
return true;
}
return false;
}
/* kfunc 등록 — CT 관련 kfunc를 BPF 서브시스템에 노출 */
BTF_SET8_START(nf_ct_kfunc_set)
BTF_ID_FLAGS(func, bpf_xdp_ct_lookup, KF_ACQUIRE | KF_RET_NULL)
BTF_ID_FLAGS(func, bpf_skb_ct_lookup, KF_ACQUIRE | KF_RET_NULL)
BTF_ID_FLAGS(func, bpf_ct_insert_entry, KF_ACQUIRE | KF_RET_NULL)
BTF_ID_FLAGS(func, bpf_ct_release, KF_RELEASE)
BTF_ID_FLAGS(func, bpf_ct_set_timeout)
BTF_ID_FLAGS(func, bpf_ct_change_timeout)
BTF_ID_FLAGS(func, bpf_ct_set_status)
BTF_ID_FLAGS(func, bpf_ct_change_status)
BTF_SET8_END(nf_ct_kfunc_set)
코드 설명
-
bpf_nf_is_valid_access
Verifier가 BPF 프로그램의 컨텍스트 접근을 검증할 때 호출됩니다.
bpf_nf_ctx구조체의state와skb필드만 읽기 접근을 허용합니다. 쓰기 접근은 모두 거부됩니다. -
KF_ACQUIRE / KF_RELEASE
KF_ACQUIRE는 이 kfunc가 참조 카운트를 증가시키는 포인터를 반환함을 의미합니다.KF_RELEASE는 참조 카운트를 감소시키는 함수임을 나타냅니다. Verifier는 모든ACQUIRE된 포인터가 반드시RELEASE되는지 확인합니다. - KF_RET_NULL 이 kfunc가 NULL을 반환할 수 있음을 의미합니다. Verifier는 BPF 프로그램이 반환값에 대해 반드시 NULL 체크를 수행하는지 검증합니다.
- BTF_SET8 / BTF_ID_FLAGS kfunc을 BPF 서브시스템에 등록하는 매크로입니다. BTF를 통해 함수 시그니처와 플래그가 verifier에 전달되어 타입 안전한 호출이 보장됩니다.
실습 가이드
아래 실습을 통해 eBPF 방화벽, Cilium 네트워크 정책, P4 파이프라인을 직접 구현하고 테스트할 수 있습니다.
Lab 1: BPF netfilter 프로그램으로 간단한 방화벽 구현
목표: libbpf + clang으로 BPF netfilter 프로그램을 컴파일하고, Netfilter 훅에 부착하여 특정 포트의 트래픽을 차단합니다.
사전 요구사항:
- 커널 6.4 이상 (
uname -r로 확인) - clang/llvm 15+, libbpf-dev, linux-headers 패키지
- root 권한 (BPF 프로그램 로딩)
# 1. 개발 환경 설정
sudo apt install clang llvm libbpf-dev linux-headers-$(uname -r) bpftool
# 2. BPF 프로그램 컴파일
clang -O2 -g -target bpf \
-D__TARGET_ARCH_x86 \
-I/usr/include/$(uname -m)-linux-gnu \
-c ngfw_filter.bpf.c -o ngfw_filter.bpf.o
# 3. BPF skeleton 생성 (로더용)
bpftool gen skeleton ngfw_filter.bpf.o > ngfw_filter.skel.h
# 4. 로더 프로그램 컴파일 및 실행
gcc -O2 -o ngfw_loader ngfw_loader.c -lbpf -lelf -lz
sudo ./ngfw_loader
# 5. BPF 프로그램 부착 확인
sudo bpftool link list
# 출력 예시:
# 42: netfilter prog 128
# hooknum PREROUTING pf INET priority -100
# 6. 차단 테스트
# 터미널 1: 서버 시작
nc -l -p 8080
# 터미널 2: 차단 확인 (타임아웃 예상)
nc -w 3 localhost 8080
# 7. 통계 확인
sudo bpftool map dump name fw_stats
# 8. 정리
sudo bpftool link detach id 42
Lab 2: Cilium으로 Kubernetes Pod 네트워크 정책 적용
목표: kind 클러스터에 Cilium을 배포하고, CiliumNetworkPolicy로 Pod 간 트래픽을 제어합니다.
사전 요구사항:
- Docker, kind (Kubernetes IN Docker), kubectl, helm
- cilium CLI (
curl -L --remote-name-all https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz)
# 1. kind 클러스터 생성 (eBPF 지원 설정)
cat <<EOF | kind create cluster --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
disableDefaultCNI: true # 기본 CNI 비활성화
kubeProxyMode: "none" # kube-proxy 비활성화
nodes:
- role: control-plane
extraMounts:
- hostPath: /opt/images
containerPath: /opt/images
- role: worker
- role: worker
EOF
# 2. Cilium 설치
cilium install --version 1.14.5 \
--set kubeProxyReplacement=true \
--set bpf.masquerade=true
# 3. Cilium 상태 확인
cilium status --wait
# 4. 테스트 Pod 배포
kubectl create deployment web --image=nginx --replicas=2
kubectl expose deployment web --port=80
kubectl create deployment client --image=busybox \
-- sleep 3600
# 5. 정책 적용 전 — 접근 가능 확인
kubectl exec -it deploy/client -- wget -qO- web
# 6. CiliumNetworkPolicy 적용 — web Pod는 label app=allowed만 허용
cat <<EOF | kubectl apply -f -
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: web-allow
spec:
endpointSelector:
matchLabels:
app: web
ingress:
- fromEndpoints:
- matchLabels:
app: allowed
toPorts:
- ports:
- port: "80"
protocol: TCP
EOF
# 7. 정책 적용 후 — 접근 차단 확인
kubectl exec -it deploy/client -- wget -qO- --timeout=3 web
# wget: download timed out
# 8. 허용 레이블 추가 후 — 접근 가능 확인
kubectl label pod -l app=client app=allowed --overwrite
kubectl exec -it deploy/client -- wget -qO- web
# 정상 응답
# 9. Hubble로 플로우 모니터링
cilium hubble port-forward &
hubble observe --type drop
# 10. 정리
kind delete cluster
Lab 3: P4 BMv2에서 NGFW 파이프라인 테스트
목표: P4 Behavioral Model(BMv2)에서 5-tuple ACL + 스테이트풀 방화벽을 컴파일하고 실행합니다.
사전 요구사항:
- p4c (P4 컴파일러), BMv2 (behavioral-model), simple_switch_grpc
- Python 3 + p4runtime-shell
# 1. P4 개발 환경 설치 (Ubuntu 22.04)
sudo apt install p4lang-p4c p4lang-bmv2
# 2. P4 프로그램 컴파일
p4c --target bmv2 --arch v1model \
--p4runtime-files ngfw.p4info.txt \
-o ngfw.json \
ngfw.p4
# 3. BMv2 (simple_switch_grpc) 시작
sudo simple_switch_grpc \
--device-id 0 \
-i 0@veth0 -i 1@veth2 \
--log-console --log-level info \
--no-p4 \
-- --grpc-server-addr 0.0.0.0:9559 &
# 4. P4Runtime으로 파이프라인 로딩
p4runtime-shell --grpc-addr localhost:9559 \
--device-id 0 \
--config ngfw.p4info.txt,ngfw.json \
--election-id 0,1
# 5. ACL 규칙 추가 (p4runtime-shell 내에서)
# te = table_entry["NgfwIngress.acl_table"]
# te.match["hdr.ipv4.src_addr"] = "10.0.0.0&&&0xffffff00"
# te.match["hdr.ipv4.dst_addr"] = "10.0.1.1&&&0xffffffff"
# te.match["hdr.tcp.dst_port"] = "80&&&0xffff"
# te.action["permit"] = {}
# te.insert()
# 6. 테스트 패킷 전송
sudo python3 send_test_pkt.py --src 10.0.0.1 --dst 10.0.1.1 \
--dport 80 --iface veth0
# 7. 카운터 확인
# ce = counter_entry["NgfwIngress.acl_counter"]
# ce.read()
# 8. 패킷 캡처로 결과 확인
sudo tcpdump -i veth2 -c 5
# 9. 정리
sudo killall simple_switch_grpc
흔한 실수와 안티패턴
eBPF/P4 기반 NGFW 구현에서 자주 발생하는 실수와 그 해결 방법을 정리합니다.
1. BPF 맵 크기 부족으로 CT 엔트리 소실
| 항목 | 내용 |
|---|---|
| 증상 | 트래픽 증가 시 기존 세션이 갑자기 끊기거나 재인증(Re-authentication)이 발생합니다. bpf_map_update_elem()이 -E2BIG를 반환합니다. |
| 원인 | max_entries를 실제 동시 세션 수보다 작게 설정했습니다. LRU_HASH는 가득 차면 가장 오래된 엔트리를 자동 제거하지만, 이것이 활성 세션일 수 있습니다. |
| 해결 | 예상 동시 세션 수의 1.5~2배로 max_entries를 설정합니다. 프로덕션에서는 100만 이상을 권장합니다. bpftool map show로 사용량을 모니터링합니다. |
2. XDP + tc-bpf 프로그램 순서 착각
| 항목 | 내용 |
|---|---|
| 증상 | XDP에서 XDP_PASS한 패킷이 tc-bpf에서 보이지 않거나, tc-bpf에서 설정한 메타데이터가 XDP에서 접근 불가합니다. |
| 원인 | 실행 순서는 항상 XDP → skb 할당 → tc ingress → Netfilter입니다. XDP는 skb 생성 전에 실행되므로 sk_buff 메타데이터에 접근할 수 없고, tc-bpf에서 XDP 메타데이터에 접근하려면 xdp_md->data_meta를 사용해야 합니다. |
| 해결 | XDP에서 tc-bpf로 메타데이터를 전달할 때는 bpf_xdp_adjust_meta()로 data_meta 영역에 기록합니다. tc-bpf에서는 skb->data_meta로 읽습니다. |
3. Per-CPU 맵에서 Cross-CPU 상태 불일치
| 항목 | 내용 |
|---|---|
| 증상 | Per-CPU 맵에 저장한 세션 상태가 다른 CPU에서 처리된 패킷에서 보이지 않아 동일 세션의 패킷이 불일치하게 처리됩니다. |
| 원인 | PERCPU_HASH/PERCPU_ARRAY는 CPU마다 독립된 사본을 유지합니다. RSS(Receive Side Scaling)로 동일 플로우가 다른 CPU에 분산되면 상태 불일치가 발생합니다. |
| 해결 | 세션 상태는 일반 HASH/LRU_HASH 맵을 사용합니다. Per-CPU 맵은 통계 카운터처럼 CPU 간 정확한 일관성이 불필요한 데이터에만 사용합니다. 또는 NIC의 RSS 해시를 5-tuple 기반으로 설정하여 동일 플로우가 동일 CPU에서 처리되도록 보장합니다. |
4. Verifier 복잡도 초과
| 항목 | 내용 |
|---|---|
| 증상 | BPF 프로그램 로딩 시 BPF program is too large. Processed N insns 또는 unreachable insn 에러가 발생합니다. |
| 원인 | 프로그램의 분기(branch) 수가 많아 verifier가 탐색해야 하는 경로가 100만 명령 한계를 초과합니다. 특히 switch-case문이 깊거나, 맵 조회 후 NULL 체크 없이 포인터를 사용하면 verifier 부하가 급증합니다. |
| 해결 | Tail call로 프로그램을 분할합니다. __always_inline 대신 __noinline 서브함수를 사용합니다. 불필요한 분기를 제거하고 volatile 변수를 최소화합니다. BPF_LOG_LEVEL=2로 verifier 로그를 확인하여 복잡한 경로를 식별합니다. |
5. P4 레지스터 해시 충돌 미처리
| 항목 | 내용 |
|---|---|
| 증상 | P4 레지스터 기반 conntrack에서 서로 다른 플로우가 동일한 상태를 공유하여 잘못된 verdict가 내려집니다. 정상 트래픽이 차단되거나 악성 트래픽이 허용됩니다. |
| 원인 | 단일 해시 함수로 레지스터 인덱스를 계산하면 해시 충돌(Collision)이 발생합니다. 레지스터 크기가 작을수록 충돌 확률이 높아집니다. |
| 해결 | d-left 해싱 또는 Cuckoo 해싱을 구현합니다. 2~4개의 독립적 해시 함수로 여러 위치를 확인하여 충돌을 최소화합니다. Tofino에서는 4개의 병렬 해시를 지원합니다. |
6. Cilium/Calico 호스트 레벨 보안 미활성화
| 항목 | 내용 |
|---|---|
| 증상 | Pod 간 정책은 정상 작동하지만, 호스트에서 직접 Pod로 접근하는 트래픽이나 NodePort를 통한 외부 트래픽에 정책이 적용되지 않습니다. |
| 원인 | Cilium은 기본적으로 호스트 트래픽에 대한 정책 적용이 비활성화되어 있습니다. Calico도 HostEndpoint 리소스를 명시적으로 생성해야 합니다. |
| 해결 | Cilium: --set hostFirewall.enabled=true로 호스트 방화벽을 활성화합니다. Calico: kubectl apply -f로 HostEndpoint 리소스를 생성하고 GlobalNetworkPolicy를 적용합니다. |
7. BPF Tail Call 체인 깊이 초과
| 항목 | 내용 |
|---|---|
| 증상 | 특정 패킷에서 Tail call이 실패하고 fallback 경로가 실행됩니다. 복잡한 프로토콜(GTP-in-GTP, 다중 터널)에서 발생하며, 패킷이 잘못된 verdict를 받습니다. |
| 원인 | 커널은 Tail call 체인 깊이를 최대 33단계로 제한합니다. 각 tail call은 스택 프레임을 소비하며, 깊이를 초과하면 bpf_tail_call()이 아무 동작 없이 반환됩니다. |
| 해결 | 파이프라인 스테이지를 33개 이내로 설계합니다. BPF-to-BPF 서브함수 호출은 tail call 깊이에 포함되지 않으므로, 복잡한 로직은 서브함수로 분리합니다. 프로토콜별 처리를 단일 스테이지 내에서 switch-case로 처리하는 것도 방법입니다. |
참고자료
- Linux Kernel: BPF Documentation
- Linux Kernel: XDP (eXpress Data Path)
- Cilium Documentation — eBPF 기반 CNI + 방화벽
- P4 Language Specification (P4.org)
- P4 Behavioral Model (BMv2) — P4 소프트웨어 시뮬레이터
- P4Runtime — 프로그래머블 데이터 플레인 제어 인터페이스
- Intel Tofino Programmable Ethernet Switch
- Linux Kernel: Netfilter Documentation
- NGFW HW 오프로드 — TC flower, flowtable, eSwitch 오프로드 상세
- BPF/eBPF/XDP — eBPF 프로그래밍 기초와 XDP 심층 분석
- TC (Traffic Control) — Traffic Control 서브시스템 상세
- 네트워크 디바이스 드라이버 — NIC 드라이버 수준 오프로드 인터페이스