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 HW 오프로드, BPF/eBPF/XDP 문서를 먼저 읽으세요.
일상적 비유 — 레스토랑 주문 시스템
프로그래머블 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
2eBPF 부착점 이해XDP, tc-bpf, BPF netfilter 세 가지 부착점의 위치와 역할을 비교합니다.tc-bpf vs XDP vs BPF netfilter
3BPF netfilter 프로그래밍커널 6.4+ BPF_PROG_TYPE_NETFILTER로 Netfilter 훅에 BPF 프로그램을 연결합니다.BPF netfilter 프로그램
4kfunc conntrack APIbpf_xdp_ct_lookup, bpf_ct_insert_entry 등 커널 conntrack 직접 접근 API를 학습합니다.BPF kfunc conntrack API
5Cilium/Calico 아키텍처Kubernetes 환경의 eBPF 방화벽 구현체를 비교 분석합니다.Cilium, Calico
6eBPF 성능 최적화Per-CPU 맵, tail call, XDP multi-buffer, BPF arena 등 고급 최적화 기법을 익힙니다.성능 최적화
7P4 프로그래머블 파이프라인P416으로 커스텀 파서, 매치-액션 테이블, 스테이트풀 레지스터를 구현합니다.P4 NGFW
8P4Runtime 컨트롤 플레인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의 장점과 한계

특성iptablesnftableseBPF (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 또는 kfuncnf_conntrack kfunc
flowtable 연동불가flow add @ft불가 (자체 fast path)가능 (kfunc)
동적 업데이트전체 체인 교체규칙 단위 추가/삭제맵 업데이트 (무중단)맵 업데이트 (무중단)
디버깅LOG 타겟log 액션bpftrace, bpf_printkbpftrace, bpf_printk
학습 곡선낮음중간높음 (C + BPF 지식)높음
안정성매우 높음 (수십 년)높음중간 (verifier 의존)아직 초기
eBPF vs nftables 선택 기준: 일반적인 L3/L4 방화벽 + flowtable offload가 목적이라면 nftables가 적합합니다. 커스텀 프로토콜 파싱, 머신러닝 기반 탐지, 복잡한 상태 머신이 필요하거나 Kubernetes 환경에서 수만 개의 Pod 정책을 관리해야 한다면 eBPF가 유리합니다. 실무에서는 nftables(기본 방화벽) + eBPF(XDP DDoS 필터, 커스텀 DPI)를 조합하는 하이브리드 접근이 가장 효과적입니다.

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_lookup6.4XDPXDP 프로그램에서 conntrack 엔트리를 조회합니다
bpf_skb_ct_lookup6.4TC, Netfilterskb 기반 프로그램에서 conntrack 엔트리를 조회합니다
bpf_ct_insert_entry6.4XDP, TC, Netfilter새로운 conntrack 엔트리를 삽입합니다
bpf_ct_release6.4XDP, TC, Netfilter조회된 conntrack 엔트리의 참조 카운트를 해제합니다
bpf_ct_set_timeout6.4XDP, TC, Netfilter새 엔트리 삽입 전 타임아웃을 설정합니다
bpf_ct_change_timeout6.4XDP, TC, Netfilter기존 엔트리의 타임아웃을 변경합니다
bpf_ct_set_status6.5XDP, TC, Netfilter새 엔트리 삽입 전 상태 플래그를 설정합니다
bpf_ct_change_status6.5XDP, 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_timeoutbpf_ct_set_status로 타임아웃과 상태를 설정할 수 있습니다.
  • bpf_ct_release 조회된 nf_conn 포인터의 참조 카운트를 감소시킵니다. 모든 CT 조회 후 반드시 호출해야 합니다. 호출하지 않으면 BPF verifier가 프로그램 로딩을 거부합니다.
  • bpf_ct_opts netns_id-1로 설정하면 현재 네트워크 네임스페이스의 conntrack을 조회합니다. l4protoIPPROTO_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)
선택 가이드: 기존 nftables 인프라와 공존해야 하거나 flowtable HW 오프로드가 필요하면 kfunc CT가 적합합니다. Kubernetes 환경에서 kube-proxy를 완전히 대체하고 최대 성능이 필요하면 Cilium/Calico처럼 BPF 맵 기반 CT가 유리합니다. 커널 6.4 이상에서는 두 방식을 혼용할 수도 있습니다.

Cilium의 eBPF 방화벽 아키텍처

Cilium은 Kubernetes 환경에서 가장 널리 사용되는 eBPF 기반 CNI + 방화벽 솔루션입니다. 커널의 nf_conntrack과 nftables를 사용하지 않고, eBPF 맵으로 자체 conntrack과 정책 엔진(Policy Engine)을 구현합니다.

구성요소구현 방식BPF 맵 타입역할
CT map (conntrack)BPF_MAP_TYPE_LRU_HASHper-CPU LRU hash세션 추적, 상태 관리, NAT 매핑
Policy mapBPF_MAP_TYPE_HASHidentity → policy 매핑K8s NetworkPolicy 평가
NAT mapBPF_MAP_TYPE_LRU_HASH원본 → 변환 IP/portClusterIP, NodePort DNAT
Endpoints mapBPF_MAP_TYPE_HASHIP → endpoint identityPod 식별, 보안 ID 할당
Metrics mapBPF_MAP_TYPE_PERCPU_HASHreason → counterdrop/forward 사유별 통계
Events mapBPF_MAP_TYPE_PERF_EVENT_ARRAYring bufferHubble 이벤트 전달 (모니터링)

Cilium의 데이터 플레인 파이프라인은 다음과 같은 순서로 실행됩니다:

Cilium eBPF 데이터 플레인 파이프라인 호스트 네트워크 스택 NIC RX 패킷 수신 ① XDP pre-filter DDoS 차단 ② tc-bpf identity lookup CT map 조회 ③ Policy identity 기반 ALLOW/DENY ④ CT update 세션 생성/갱신 카운터 증가 ⑤ NAT/LB ClusterIP→PodIP DNAT rewrite ⑥ redirect bpf_redirect_peer() veth 직접 전달 DROP DENY → DROP 목적지 Pod veth 피어 (네트워크 NS) 호스트 스택 우회 cilium-agent (유저스페이스) K8s API Watch Pod/Service/Policy Identity Allocator label → numeric ID BPF Map 관리 정책/CT/NAT 동기화 CT GC 만료 엔트리 정리 (12s) Hubble 이벤트 모니터링 Metrics Prometheus 연동 ① XDP DDoS 차단 → ② identity 조회 + CT 검색 → ③ 정책 평가 → ④ CT 갱신 → ⑤ NAT/LB 변환 → ⑥ veth 피어로 직접 redirect
/* 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 엔트리 정리 */
Cilium의 bpf_redirect_peer(): 이 BPF 헬퍼 함수는 veth 피어를 통해 패킷을 직접 전달하여 호스트 측 네트워크 스택의 ingress 경로를 완전히 우회합니다. 이는 nftables flowtable의 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 구현)
conntracknf_conntrackBPF 맵 기반 자체 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 조합에서 차별화됩니다.

Calico 핵심 BPF 맵

BPF 맵타입용도
cali_v4_ctBPF_MAP_TYPE_HASH5-tuple + 방향CT 상태, 카운터, NAT 정보conntrack (세션 추적)
cali_v4_nat_feBPF_MAP_TYPE_HASHVIP + portbackend 수, affinity 키서비스 프론트엔드 (ClusterIP)
cali_v4_nat_beBPF_MAP_TYPE_HASH서비스 ID + 인덱스backend IP + port서비스 백엔드 (Pod 주소)
cali_v4_routesBPF_MAP_TYPE_LPM_TRIEIP prefix다음 홉, 인터페이스BPF 내 라우팅 테이블
cali_v4_fsafesBPF_MAP_TYPE_HASH프로토콜 + 포트플래그failsafe 포트 (항상 허용)
cali_v4_affBPF_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 iptablesCalico eBPFCilium (v1.14+)
Pod-to-Pod TCP 처리량기준선 (1.0x)1.25x1.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 관측성미지원미지원기본 지원
Calico vs Cilium 선택: BGP 기반 네이티브 라우팅과 호스트 레벨 보안이 핵심 요구사항이면 Calico eBPF가 적합합니다. L7 정책, Hubble 관측성, 고급 네트워크 정책(DNS 기반 등)이 필요하면 Cilium이 유리합니다. 두 솔루션 모두 kube-proxy를 대체하며 유사한 수준의 데이터 플레인 성능을 제공합니다.

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수동 rewritenf_nat 통합 가능
redirectbpf_redirect(), bpf_redirect_map()bpf_redirect(), bpf_redirect_peer()NF_ACCEPT/DROP만
HW offload제한적 (Netronome 등)미지원미지원
NGFW 역할DDoS pre-filter, rate limitL3/L4 정책, CT, LB, redirectNetfilter 훅 대체, DPI 연동
대표 프로젝트Cloudflare Magic TransitCilium, Calico eBPF아직 초기 (실험적)

각 BPF 타입의 NGFW에서의 역할을 종합하면:

eBPF NGFW 파이프라인 (XDP → tc-bpf → BPF CT → Policy → Redirect) NIC RX 패킷 수신 XDP DDoS Pre-filter Rate Limiter XDP_DROP tc-bpf ingress 정책 identity lookup BPF CT map 세션 조회/생성 LRU Hash Policy map ALLOW/DENY identity 기반 NAT/LB DNAT rewrite backend 선택 TX redirect TC_ACT_SHOT BPF Maps (커널 메모리) XDP Blocklist LPM_TRIE: IP 차단 Rate: per-src 제한 CT map LRU_HASH: 1M entries 5-tuple → ct_entry Policy map HASH: identity 기반 src_id+dst_id → allow/deny NAT map LRU_HASH: DNAT 매핑 ClusterIP → PodIP Events/Metrics PERF_EVENT: Hubble PERCPU: 드롭 통계 Userspace Agent 맵 업데이트 (정책 변경) CT GC (만료 엔트리 정리) 이벤트 소비 (모니터링) cilium-agent | bpftool | prometheus-exporter

eBPF NGFW 성능 최적화

eBPF 기반 NGFW는 높은 유연성을 제공하지만, 최대 성능을 달성하려면 BPF 프로그램 구조, 맵 선택, 커널 기능 활용에 대한 세밀한 최적화가 필요합니다.

Per-CPU 맵 vs 공유 맵

특성Per-CPU 맵공유 맵 (일반)
타입 예시BPF_MAP_TYPE_PERCPU_HASH, PERCPU_ARRAYBPF_MAP_TYPE_HASH, LRU_HASH
락 경합없음 (CPU별 독립 사본)있음 (버킷 스핀락)
캐시 효율높음 (L1/L2 캐시 히트)낮음 (캐시 라인 바운싱)
메모리 사용높음 (CPU 수 × 엔트리 수)낮음 (1개 사본)
CPU 간 일관성불일관 (각 CPU 독립)일관 (동일 데이터)
적합 용도통계 카운터, 속도 제한 토큰conntrack, 정책 맵, NAT 맵
실전 전략: NGFW에서는 읽기 전용 데이터(정책 규칙, ACL)에는 일반 HASH 맵을, 읽기/쓰기 빈번한 카운터에는 PERCPU_ARRAY를, 세션 테이블(conntrack)에는 LRU_HASH를 사용합니다. LRU_HASH는 내부적으로 per-CPU 캐시를 활용하여 공유 맵이면서도 락 경합을 최소화합니다.

Tail Call / BPF-to-BPF Call로 파이프라인 분할

복잡한 NGFW 로직을 단일 BPF 프로그램에 구현하면 verifier 복잡도 한계(100만 명령)에 도달할 수 있습니다. Tail callBPF-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.10veth 피어로 직접 전달호스트 측 ingress 우회~30% 향상
bpf_redirect_neigh()5.10이웃(Neighbor) 조회 후 전달FIB lookup 수행, L2 헤더 설정~15% 향상
bpf_redirect_map()4.14DEVMAP/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_buffmb 플래그를 추가하고 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) 설정에 크게 영향받습니다.

Verifier 복잡도 최소화 팁

eBPF 작업별 성능 수치

작업처리량 (64B 패킷)레이턴시 (P99)비고
XDP_DROP (빈 프로그램)~100 Mpps/core<100nsNIC 드라이버 직후 드롭
XDP + LPM_TRIE 조회~40 Mpps/core~200nsIP 차단 목록 조회
tc-bpf + HASH 맵 조회~20 Mpps/core~400ns5-tuple 정책 매칭
tc-bpf + CT lookup (BPF 맵)~15 Mpps/core~500nsLRU_HASH conntrack 조회
tc-bpf + kfunc CT lookup~12 Mpps/core~600ns커널 nf_conntrack 조회
tc-bpf + CT + Policy + NAT~8 Mpps/core~1μsCilium 전체 파이프라인
BPF netfilter (NF 훅)~10 Mpps/core~700nsNetfilter 훅 오버헤드 포함
tc-bpf + bpf_redirect_peer()~18 Mpps/core~350nsveth 피어 직접 전달
XDP + CPUMAP redirect~60 Mpps (총합)~250ns멀티코어 분산 처리
성능 측정 환경: 위 수치는 Intel Xeon Platinum 8380 (2.3GHz), 100GbE NIC (Mellanox ConnectX-6), 커널 6.6 기준의 참고값입니다. 실제 성능은 NIC 드라이버, 커널 버전, BPF 프로그램 복잡도, 시스템 설정에 따라 크게 달라질 수 있습니다.

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의 핵심 설계 목표

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;
externextern타깃이 제공하는 외부 객체/함수extern register<T> { ... }

P416 프로그램 컴파일 및 배포 흐름

P4 프로그램은 소스 코드 작성부터 디바이스 배포까지 아래 흐름을 따릅니다. 컴파일러(p4c)는 P4 소스와 아키텍처 모델을 입력받아 타깃별 바이너리와 P4Info(테이블/액션 메타데이터)를 생성합니다.

P4 프로그램 컴파일 및 배포 흐름 P4 소스 코드 ngfw.p4 (P4₁₆ 문법) 아키텍처 모델 V1Model / TNA / PNA p4c 컴파일러 프론트엔드: 구문/타입 검사 미드엔드: IR 최적화 백엔드: 타깃별 코드 생성 타깃 바이너리 ngfw.bin (ASIC/FPGA 이미지) P4Info 테이블/액션 메타데이터 (Protobuf) P4Runtime Agent SetPipelineConfig() 바이너리 + P4Info 로딩 gRPC 서버 타깃 디바이스 Tofino ASIC FPGA / BMv2 SmartNIC SDN 컨트롤러 ONOS / SONiC / 커스텀 테이블 엔트리 CRUD P4Runtime gRPC 흐름 요약: ① P4 소스 + 아키텍처 모델 → p4c 컴파일 → 타깃 바이너리 + P4Info 생성 ② P4Runtime Agent가 바이너리를 디바이스에 로딩 (SetForwardingPipelineConfig) ③ SDN 컨트롤러가 P4Runtime API로 테이블 엔트리를 런타임에 추가/수정/삭제

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
T2NAIntel Tofino 2 전용TNA 확장 (20 MAU 스테이지, Ghost thread, Dynamic hash)Intel Tofino 2
V1Model vs PSA: V1Model은 BMv2 소프트웨어 스위치에서 학습과 프로토타이핑에 주로 사용됩니다. PSA는 V1Model을 대체할 표준화된 스위치 아키텍처로 설계되었으며, 더 명확한 인그레스/이그레스 분리와 풍부한 extern 정의를 제공합니다. 프로덕션 환경에서는 TNA, PNA 등 타깃 네이티브 아키텍처를 사용하여 하드웨어 성능을 최대한 활용합니다.

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 개발 도구 종합

도구용도라이선스비고
p4cP4 컴파일러 (오픈 소스)Apache 2.0BMv2, DPDK, eBPF 백엔드 포함
BMv2소프트웨어 레퍼런스 스위치Apache 2.0simple_switch / simple_switch_grpc
p4c-ebpfP4 → eBPF/XDP 컴파일Apache 2.0p4c의 eBPF 백엔드, 일반 NIC에서 P4 로직 실행
Mininet + BMv2가상 네트워크 토폴로지 테스트오픈 소스p4-utils / p4app으로 자동화
P4Runtime Shell대화형 P4Runtime 클라이언트Apache 2.0Python 기반 REPL, 테이블 엔트리 조작
PTF (Packet Test Framework)P4 프로그램 단위 테스트Apache 2.0패킷 송수신 검증, CI/CD 연동
Intel P4 Studio / SDETofino 전용 개발 환경상용 (NDA)p4c-tofino + Barefoot Runtime + 디버거
Pensando SDKAMD Pensando DSC 개발상용PNA 아키텍처 기반
빠른 시작: P4 학습을 시작할 때는 p4lang/tutorials 저장소를 활용하면 좋습니다. Vagrant VM 기반으로 p4c + BMv2 + Mininet 환경이 미리 구성되어 있어, 설치 없이 바로 P4 프로그래밍을 실습할 수 있습니다.

P4 파이프라인 개요와 NGFW 적용

P416은 패킷 처리 파이프라인을 정의하는 도메인 특화 언어(DSL)입니다. 네트워크 하드웨어의 데이터 플레인 동작을 소프트웨어처럼 프로그래밍하되, 컴파일 결과가 ASIC/FPGA에 직접 매핑되어 라인 레이트 처리를 보장합니다.

P4 파이프라인 핵심 구성 요소

구성 요소역할NGFW 적용
프로그래머블 파서(Parser)패킷 헤더를 바이트 단위로 추출, 상태 머신 기반 프로토콜 탐색커스텀 프로토콜(GTP, VXLAN-GPE, SRv6 등) 파싱, DPI 전처리
매치-액션 테이블(MAT)추출된 필드를 키로 테이블 조회, 매치 시 액션 실행5-tuple ACL, 커스텀 헤더 기반 필터링, 동적 블랙리스트
매치 타입exact(정확), ternary(와일드카드), LPM(최장 프리픽스), rangeexact: conntrack lookup, ternary: ACL, LPM: 라우팅
레지스터(Register)스테이트풀 메모리, 패킷 간 상태 유지커넥션 트래킹 상태 머신 (SYN→ESTABLISHED→FIN)
카운터/미터(Counter/Meter)패킷/바이트 카운트, 토큰 버킷 기반 속도 제한플로우별 통계, QoS 속도 제한
다이제스트(Digest)패킷 메타데이터를 CPU(컨트롤 플레인)로 전송새 플로우 알림, 이상 탐지 이벤트 전달
디파서(Deparser)수정된 헤더를 다시 조합하여 패킷 재구성NAT 변환 후 헤더 재조합, 터널 캡슐화

P4가 NGFW에 중요한 이유

P4 vs 고정 함수: TC flower + eSwitch 오프로드는 NIC 벤더가 하드웨어에 구현한 고정 매치-액션 기능만 사용할 수 있습니다. P4 프로그래머블 디바이스는 사용자가 파서와 테이블을 직접 정의하므로, 새로운 프로토콜이나 보안 정책을 하드웨어에 즉시 적용할 수 있습니다. 다만 P4 지원 하드웨어는 고정 함수 NIC보다 고가이며, 개발 복잡도가 높습니다.

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 NGFW 파서 상태 머신 (Parser FSM) start extract(ethernet) parse_ipv4 extract(ipv4) parse_tcp extract(tcp) parse_udp extract(udp) accept ether_type == 0x0800 default (비-IPv4 → 바로 accept) protocol == 6 protocol == 17 default (ICMP 등 → accept) 각 상태에서 extract()로 헤더 필드를 추출하고, transition select()로 프로토콜 필드 값에 따라 다음 상태를 결정합니다.
/* 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 커넥션 상태 전이를 보여줍니다.

P4 레지스터 기반 TCP 커넥션 트래킹 상태 머신 엔트리 없음 ct_state_reg[hash] NEW (0) SYN 수신 → 등록 ESTABLISHED (1) ACL 건너뛰기 (Fast Path) INVALID (3) 타임아웃 만료 CPU Digest 새 플로우 알림 리셋/종료 상태 초기화 SYN ACK RST/FIN ct_state_reg.write(hash, CT_NEW) → 상태 초기화 타임아웃 → 엔트리 삭제 ESTABLISHED 상태의 패킷은 ACL 테이블을 건너뛰는 Fast Path로 처리되어 레이턴시가 최소화됩니다.
/* 커넥션 트래킹 상태 상수 */
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);
        }
    }
}
실전 팁: 위 코드의 CT 구현은 레지스터 해시 충돌 문제가 있으므로, 프로덕션(Production) 환경에서는 d-left 해싱(d-left hashing) 또는 Cuckoo 해시 테이블을 P4 레지스터로 구현하여 충돌을 최소화합니다. Tofino는 최대 4개의 해시 함수를 병렬 실행할 수 있어 d-left 해싱에 적합합니다.

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 1Tofino 2
처리량6.5 Tbps12.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 코어를 결합합니다.

# 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) 드라이버와 통합되는 인프라 오프로드 전용 프로세서입니다.

플랫폼 비교 종합

항목Intel TofinoAMD Pensando DSCIntel IPU
폼 팩터스위치 ASICSmartNIC (PCIe)IPU (PCIe)
처리량6.5~12.8 Tbps최대 200 Gbps최대 200 Gbps
P4 아키텍처TNA/T2NAPNA (Portable NIC Arch)P4-SDE
스테이트풀 지원레지스터 + ALU레지스터 + ARM 코어레지스터 + Xeon 코어
내장 CPU없음 (외부 CPU 필요)ARM A72 (16코어)Xeon-D (최대 24코어)
NGFW 적합성초고속 스위치 방화벽호스트 NIC 인라인 NGFW인프라 통합 NGFW
생태계SDE + P4StudioPensando SDKIntel P4-SDE + IPDK
주요 용도데이터센터 경계 방화벽서버 단위 마이크로세그멘테이션클라우드 인프라 보안

P4Runtime 컨트롤 플레인 연동

P4Runtime은 P4 프로그래머블 디바이스의 데이터 플레인을 원격으로 제어하기 위한 gRPC 기반 API입니다. P4Runtime v1.3 사양을 기준으로, 테이블 엔트리 CRUD, ActionProfile/Selector, PacketIn/Out, Digest 등의 기능을 제공합니다.

P4Runtime 컨트롤 플레인 아키텍처 컨트롤 플레인 (Control Plane) SDN 컨트롤러 ONOS / SONiC / 커스텀 정책 결정, DPI 분석 P4Runtime 클라이언트 (gRPC) NGFW 관리자 ACL 정책 DB Digest 처리 (새 플로우) 카운터/미터 모니터링 모니터링 Prometheus Exporter Grafana 대시보드 알림 시스템 P4Info 테이블/액션 메타데이터 (Protobuf 직렬화) gRPC 채널 (P4Runtime v1.3) 데이터 플레인 (Data Plane) — ASIC/FPGA/SmartNIC P4Runtime Agent gRPC 서버 Write/Read 요청 처리 StreamChannel 관리 ACL 테이블 5-tuple 매치 permit / deny CT 레지스터 상태 머신 NEW→EST→FIN NAT 테이블 SNAT / DNAT 헤더 리라이트 카운터/미터 패킷/바이트 통계 QoS 속도 제한 PacketI/O PacketIn (punt) PacketOut (inject) Write/Read StreamChannel Read(Counter) 컨트롤 플레인은 gRPC 채널을 통해 데이터 플레인의 테이블 엔트리를 런타임에 추가/수정/삭제하며, Digest와 PacketIn으로 예외 패킷을 수신합니다.

P4Runtime API 주요 서비스

서비스RPC 메서드설명
WriteWrite(WriteRequest)테이블 엔트리, 카운터, 미터 등의 생성/수정/삭제
ReadRead(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 파이프라인으로 스위치 방화벽 구현, 중앙 집중식 정책 관리
SONiCSAI P4Runtime 확장데이터센터 스위치 ACL, 커스텀 프로토콜 필터링
StratumP4Runtime 레퍼런스 구현하드웨어 추상화 계층(HAL), 멀티 벤더 호환
커스텀 컨트롤러gRPC 직접 사용위 Python 예시처럼 직접 P4Runtime API 호출

모니터링: 카운터/미터 읽기

P4Runtime의 Read RPC로 데이터 플레인 카운터와 미터를 주기적으로 폴링하여 NGFW 모니터링 대시보드를 구성할 수 있습니다.

프로덕션 팁: P4Runtime 카운터 폴링 주기는 보통 1~5초입니다. Prometheus exporter를 구현하여 P4Runtime 카운터를 Prometheus 메트릭으로 변환하면 Grafana 대시보드에서 실시간 NGFW 모니터링이 가능합니다.

P4 vs TC flower vs eBPF 비교

NGFW 오프로드에 사용할 수 있는 세 가지 주요 기술(P4, TC flower, eBPF)의 특성을 비교합니다. 각 기술은 서로 다른 설계 철학과 트레이드오프를 가지며, 환경에 따라 적합한 선택이 달라집니다.

비교 항목P4TC flower + eSwitcheBPF (XDP/TC)
프로그래밍 수준데이터 플레인 전체 정의고정 매치-액션 조합커널 공간 바이트코드
라인 레이트 보장보장 (ASIC 매핑)보장 (HW 오프로드 시)불가 (CPU 실행)
커스텀 프로토콜 파싱완전 지원미지원 (사전 정의 필드만)제한적 지원 (바이트 접근)
스테이트풀 처리레지스터 (HW)conntrack 오프로드 (HW)BPF 맵 (SW)
최대 처리량Tbps급100~400 Gbps10~100 Gbps (CPU 의존)
학습 곡선높음 (P4 언어 + ASIC 이해)낮음 (tc 명령어)중간 (C 유사 + BPF 제약)
생태계 성숙도제한적 (전용 HW 필요)높음 (주요 NIC 벤더 지원)높음 (커널 내장, 활발한 커뮤니티)
벤더 종속성ASIC별 아키텍처 차이NIC 벤더별 지원 범위 상이없음 (커널 표준)
커널 통합별도 SDK (비커널)완전 통합 (TC subsystem)완전 통합 (BPF subsystem)
동적 업데이트테이블 엔트리 (P4Runtime)tc filter add/delBPF 맵 업데이트 + 프로그램 교체
적합 사용 사례초고속 커스텀 프로토콜 NGFW표준 L2-L4 오프로드 NGFW유연한 커널 공간 보안 처리
하이브리드(Hybrid) 접근: 실제 대규모 배포에서는 세 기술을 조합하여 사용하는 것이 일반적입니다. 예를 들어 P4로 라인 레이트 5-tuple ACL + 기본 CT를 처리하고, 매치되지 않는 패킷(exception)은 eBPF XDP로 전달하여 DPI를 수행하며, 확립된 세션은 TC flower로 eSwitch에 오프로드합니다.

P4 매치-액션 파이프라인 다이어그램

아래 다이어그램은 P4 프로그래머블 파이프라인에서 NGFW 패킷 처리 흐름을 보여줍니다. 각 매치-액션 테이블은 ASIC의 MAU 스테이지에 매핑되며, 라인 레이트로 실행됩니다.

P4 NGFW 매치-액션 파이프라인 (ASIC 라인 레이트) Packet In Wire → RX Parser ETH 추출 → IPv4 추출 → TCP/UDP 추출 MAU Stage 0 5-tuple ACL ternary match permit / deny MAU Stage 1-2 CT State register lookup SYN→EST 상태머신 MAU Stage 3 NAT Rewrite exact match DNAT / SNAT MAU Stage 4 QoS Meter token bucket GREEN/YELLOW/RED Deparser 헤더 재조합 Packet Out DROP (deny) DROP (RED) CPU (컨트롤 플레인) Digest: 새 플로우 알림 P4Runtime gRPC 테이블 엔트리 추가/삭제 범례: 라인 레이트 데이터 경로 DROP 경로 CPU 예외 경로 (Digest) MAU Stage N = ASIC 매치-액션 유닛 스테이지 (고정 레이턴시, 파이프라인 병렬 처리) 모든 테이블이 파이프라인으로 연결되어 패킷당 고정 시간(수십 ns)에 처리 완료
P4 한계: P4는 DPI(Deep Packet Inspection)를 직접 구현하기 어렵습니다. 파서는 헤더 구조 기반이므로 페이로드(Payload) 패턴 매칭에는 부적합합니다. DPI가 필요한 경우 P4에서 digest로 첫 패킷을 CPU에 전달하고, CPU에서 DPI 수행 후 결과에 따라 P4 테이블에 permit/deny 엔트리를 추가하는 하이브리드 구조를 사용합니다.

커널 소스 분석

BPF netfilter와 kfunc conntrack 구현의 핵심 커널 소스 코드를 분석합니다. 이 분석은 커널 6.6 기준입니다.

/* 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_buffnf_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 구조체의 stateskb 필드만 읽기 접근을 허용합니다. 쓰기 접근은 모두 거부됩니다.
  • 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 훅에 부착하여 특정 포트의 트래픽을 차단합니다.

사전 요구사항:

# 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 간 트래픽을 제어합니다.

사전 요구사항:

# 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 + 스테이트풀 방화벽을 컴파일하고 실행합니다.

사전 요구사항:

# 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로 처리하는 것도 방법입니다.

참고자료

다음 학습: