VPP (FD.io) — 고성능 유저스페이스 패킷(Packet) 처리

FD.io VPP(Vector Packet Processing) 프레임워크: 벡터 패킷 처리 모델, 그래프 노드 아키텍처, DPDK 통합, 플러그인 시스템, TAP/TUN·vhost-user·AF_PACKET·AF_XDP 커널 인터페이스, L2/L3/ACL/NAT/IPsec/SRv6 기능, CLI/API, 성능 최적화, 활용 사례 종합 가이드. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅(Debugging) 절차까지 실무 관점으로 다룹니다.

전제 조건: 네트워크 스택(Network Stack)라우팅(Routing) 문서를 먼저 읽으세요. 제어 평면과 데이터 평면이 분리되어 동작하므로, 규칙 갱신 시점과 실제 적용 지연(Latency)을 함께 확인해야 합니다.
일상 비유: 이 주제는 도로 표지판 갱신과 교통 흐름 제어(Flow Control)와 비슷합니다. 표지판을 바꿔도 차량 흐름 반영에는 시간이 필요하듯이, 경로/정책 갱신과 패킷 처리 시점은 분리해서 봐야 합니다.

핵심 요약

  • VPP (Vector Packet Processing) — 패킷을 하나씩 처리하지 않고 벡터(배열) 단위로 묶어 처리하는 고성능 패킷 처리 프레임워크입니다. I-캐시 지역성을 극대화하여 처리량을 높입니다.
  • 그래프 노드(Graph Node) 아키텍처 — 패킷 처리 파이프라인을 방향성 그래프로 구성합니다. 각 노드가 하나의 처리 단계(파싱, 분류, 포워딩 등)를 담당하며, 벡터가 노드 간을 이동합니다.
  • vlib 프레임워크 — VPP의 핵심 인프라 라이브러리로, 노드 스케줄링, 버퍼 관리, 스레드 모델, 타이머 등을 제공합니다.
  • 플러그인(Plugin) 시스템 — VPP 기능을 동적으로 확장하는 구조입니다. 새로운 프로토콜이나 처리 노드를 플러그인으로 추가하며, VLIB_PLUGIN_REGISTER() 매크로로 등록합니다.
  • VNET (네트워크 스택) — L2 스위칭, L3 라우팅, ACL, NAT, IPsec 등 네트워크 기능을 그래프 노드로 구현한 VPP의 데이터 평면 계층입니다.
  • FIB (Forwarding Information Base) — VPP의 포워딩 테이블로, mtrie 기반 최장 접두사 매칭(LPM)을 수행합니다. 제어 평면에서 경로를 갱신하면 데이터 평면 FIB에 반영됩니다.
  • Binary API / CLI — 외부 애플리케이션은 공유 메모리 전송(Shared Memory Transport) 기반 Binary API로, 운영자는 대화형 CLI로 VPP를 제어합니다.
  • 하드웨어 인터페이스 — DPDK PMD, af_packet, virtio, memif 등 다양한 백엔드를 통해 물리·가상 NIC에 연결됩니다.

단계별 이해

  1. 벡터 처리 모델 이해 — 스칼라 처리와 벡터 처리의 차이를 파악합니다.

    전통적인 패킷별(scalar) 처리는 노드를 매번 전환하여 I-캐시 미스가 발생합니다. VPP는 동일 노드에서 수백 개 패킷을 연속 처리(벡터)하여 캐시 효율을 극대화합니다.

  2. 그래프 노드 구조 학습 — 패킷이 노드 그래프를 따라 이동하는 과정을 추적합니다.

    show vlib graph CLI 명령으로 노드 토폴로지를 확인하고, ip4-input → ip4-lookup → ip4-rewrite → interface-output 같은 기본 포워딩 경로를 따라갑니다.

  3. CLI로 기본 설정 실습 — 인터페이스 생성, IP 할당, 경로 추가를 CLI에서 직접 수행합니다.

    create host-interfaceaf_packet 인터페이스를 만들고, set interface ip addressip route add로 기본 포워딩을 구성합니다.

  4. 플러그인 작성 입문 — 커스텀 그래프 노드를 플러그인으로 추가합니다.

    VLIB_REGISTER_NODE()로 새 노드를 등록하고, VNET_FEATURE_INIT()으로 기존 그래프에 삽입합니다. 샘플 플러그인(src/plugins/sample/)을 템플릿으로 활용합니다.

  5. Binary API 연동 — 외부 프로그램에서 VPP를 프로그래밍 방식으로 제어합니다.

    공유 메모리 전송 기반 Binary API를 사용하여 Python(vpp_papi) 또는 Go 바인딩으로 인터페이스 설정, FIB 조회, 통계 수집을 자동화합니다.

  6. 성능 분석과 트레이싱 — 벡터 크기와 노드별 처리 시간을 측정합니다.

    show runtime으로 노드별 호출 횟수·벡터 크기·클럭 사이클을 확인하고, trace add로 개별 패킷의 노드 통과 경로를 추적합니다.

관련 표준: FD.io VPP Architecture (fd.io) — 고성능 패킷 처리 프레임워크 아키텍처입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
관련 기술: VPP는 커널 네트워크 스택을 우회(bypass)하여 유저스페이스에서 패킷을 처리하는 프레임워크입니다. 커널의 TAP/TUN(drivers/net/tun.c), vhost(drivers/vhost/), AF_PACKET(net/packet/af_packet.c), AF_XDP(net/xdp/xsk.c), UIO/VFIO(drivers/uio/, drivers/vfio/) 서브시스템과 밀접하게 연관됩니다.

VPP 개요

VPP(Vector Packet Processing)는 Linux Foundation 산하 FD.io(Fast Data Input/Output) 프로젝트의 핵심 구성 요소로, 고성능 유저스페이스 패킷 처리 플랫폼입니다. 원래 Cisco에서 개발한 상용 코드를 2016년에 오픈소스로 공개하였으며, 현재 Linux Foundation이 호스팅하는 커뮤니티 주도 프로젝트입니다.

VPP의 핵심 설계 철학은 벡터 패킷 처리(Vector Packet Processing)입니다. 전통적인 커널 네트워크 스택이 패킷 하나씩 전체 처리 경로를 순회하는 scalar 방식인 반면, VPP는 여러 패킷을 하나의 벡터(배열)로 묶어서 동일한 처리 노드를 일괄 통과시킵니다. 이를 통해 CPU의 명령어 캐시(I-cache) 히트율을 극대화하고, 분기 예측(branch prediction) 효율을 높여 범용 x86 CPU에서도 초당 수천만 패킷(수십 Mpps)을 처리할 수 있습니다.

FD.io / Linux Foundation 역사

연도주요 이벤트
2002~Cisco 내부에서 VPP 엔진 개발 시작 (상용 라우터/스위치용)
2016.02Linux Foundation 산하 FD.io 프로젝트로 오픈소스 공개
2017VPP 17.01 릴리스, DPDK 통합 본격화
2018Kubernetes CNI (Contiv-VPP) 지원, vhost-user 안정화
2020AF_XDP 호스트 인터페이스 지원, rdma 플러그인
2022~5G UPF 유스케이스 확대, SRv6 고도화, VPP 24.x 시리즈

패킷 처리 프레임워크 비교

항목커널 스택DPDK (raw)VPPOvS-DPDK
실행 공간커널유저스페이스유저스페이스유저스페이스
처리 모델Scalar (패킷 단위)Poll-mode (배치)Vector (그래프 노드)Flow-based (Megaflow)
성능 (64B, 단일 코어)~1 Mpps~15 Mpps~12 Mpps~8 Mpps
L2/L3 스택완전 내장직접 구현 필요완전 내장L2 + OpenFlow
확장성커널 모듈(Kernel Module)라이브러리플러그인 시스템OpenFlow 규칙
커널 통합네이티브UIO/VFIOTAP/vhost/AF_XDPvhost-user
라이선스GPL-2.0BSD/LGPLApache 2.0Apache 2.0

벡터 패킷 처리 모델

Scalar 처리 vs Vector 처리

전통적인 커널 네트워크 스택의 Scalar 처리는 패킷 하나를 수신하면 L2 → L3 → L4 → 소켓(Socket) 전달까지 전체 경로를 완주한 후 다음 패킷을 처리합니다. 각 단계마다 다른 함수 코드가 I-cache에 로드되므로 캐시 미스가 빈번합니다.

VPP의 Vector 처리는 다수의 패킷(벡터)을 동시에 한 노드에서 처리한 후, 다음 노드로 벡터 전체를 넘깁니다. 동일한 코드가 수백 패킷에 반복 적용되므로 I-cache에 상주하며, 분기 예측이 안정되어 파이프라인(Pipeline) 버블이 최소화됩니다.

Scalar 처리 vs Vector 처리 Scalar 처리 (커널) Pkt 1 L2 L3 L4 완료 Pkt 2 L2 L3 ... 순차 반복 I-cache miss ↕ Vector 처리 (VPP) Vector (256 pkts) Pkt 1, Pkt 2, ... ... Pkt N L2 Node 모든 패킷 일괄 L3 Node 모든 패킷 일괄 L4 Node 모든 패킷 일괄 완료 ✓ I-cache hot — 동일 코드 반복 실행 Scalar: 패킷마다 전체 경로 순회 → I-cache 냉각 | Vector: 노드별 일괄 처리 → I-cache 상주 VPP 기본 벡터 크기: VLIB_FRAME_SIZE = 256

캐시 효율성과 성능

VPP의 성능 우위는 CPU 마이크로아키텍처 수준의 최적화에서 비롯됩니다:

/* VPP 노드의 전형적인 dual-loop 패턴 (간략화) */
while (n_left_from >= 4) {
    /* 다음 2개 패킷 프리페치 */
    vlib_buffer_t *p2, *p3;
    p2 = vlib_get_buffer(vm, from[2]);
    p3 = vlib_get_buffer(vm, from[3]);
    CLIB_PREFETCH(p2->data, CLIB_CACHE_LINE_BYTES, LOAD);
    CLIB_PREFETCH(p3->data, CLIB_CACHE_LINE_BYTES, LOAD);

    /* 현재 2개 패킷 처리 */
    vlib_buffer_t *b0, *b1;
    b0 = vlib_get_buffer(vm, from[0]);
    b1 = vlib_get_buffer(vm, from[1]);

    /* 패킷 처리 로직... */
    next0 = process_packet(b0);
    next1 = process_packet(b1);

    vlib_validate_buffer_enqueue_x2(vm, node, next_index,
                                     to_next, n_left_to_next,
                                     bi0, bi1, next0, next1);
    from += 2;
    n_left_from -= 2;
}

쿼드 루프 패턴과 SIMD 최적화

VPP 노드의 가장 최적화된 형태는 quad-loop 패턴입니다. 4개의 패킷을 동시에 처리하여 CPU의 ILP(Instruction-Level Parallelism)를 극대화하고, 프리페치 거리를 확보합니다. 컴파일러가 SIMD 벡터화를 적용할 가능성도 높아집니다.

/* quad-loop 패턴 — 최고 성능 경로 */
while (n_left_from >= 8) {
    /* 8개 앞 패킷 프리페치 (4개 처리 동안 캐시 로드) */
    CLIB_PREFETCH(vlib_get_buffer(vm, from[4])->data,
                  CLIB_CACHE_LINE_BYTES, LOAD);
    CLIB_PREFETCH(vlib_get_buffer(vm, from[5])->data,
                  CLIB_CACHE_LINE_BYTES, LOAD);
    CLIB_PREFETCH(vlib_get_buffer(vm, from[6])->data,
                  CLIB_CACHE_LINE_BYTES, LOAD);
    CLIB_PREFETCH(vlib_get_buffer(vm, from[7])->data,
                  CLIB_CACHE_LINE_BYTES, LOAD);

    /* 현재 4개 패킷 일괄 처리 */
    vlib_buffer_t *b0, *b1, *b2, *b3;
    b0 = vlib_get_buffer(vm, from[0]);
    b1 = vlib_get_buffer(vm, from[1]);
    b2 = vlib_get_buffer(vm, from[2]);
    b3 = vlib_get_buffer(vm, from[3]);

    next0 = process_packet(b0);
    next1 = process_packet(b1);
    next2 = process_packet(b2);
    next3 = process_packet(b3);

    vlib_validate_buffer_enqueue_x4(vm, node, next_index,
        to_next, n_left_to_next,
        bi0, bi1, bi2, bi3, next0, next1, next2, next3);
    from += 4;
    n_left_from -= 4;
}

/* single-loop: 나머지 패킷 처리 (1~3개) */
while (n_left_from > 0) {
    vlib_buffer_t *b0 = vlib_get_buffer(vm, from[0]);
    next0 = process_packet(b0);
    vlib_validate_buffer_enqueue_x1(vm, node, next_index,
        to_next, n_left_to_next, bi0, next0);
    from += 1;
    n_left_from -= 1;
}
루프 패턴패킷/반복프리페치 거리ILP 활용적합한 상황
Single-loop10~1낮음복잡한 상태 기계, 잔여 패킷 처리
Dual-loop22중간중간 복잡도 노드, 분기 많은 처리
Quad-loop44~8높음단순 처리 (lookup, rewrite, forward)
패턴 선택: 노드의 처리 로직이 단순할수록 quad-loop이 유리합니다. ip4-lookup, l2-fwd 같은 핫 패스 노드는 quad-loop을 사용하고, 복잡한 상태 머신이 필요한 tcp-input은 single-loop으로 구현합니다.

벡터 크기 튜닝과 영향

VLIB_FRAME_SIZE는 한 번에 노드에 전달되는 최대 패킷 수를 결정합니다. 기본값 256은 대부분의 상황에서 최적이지만, 특수 워크로드에서는 조정이 필요할 수 있습니다.

벡터 크기I-cache 효과처리량(Throughput)테일 레이턴시메모리 사용
64보통낮음최소 (좋음)낮음
128좋음중간낮음중간
256 (기본)최적높음중간중간
512최적최고높음 (나쁨)높음
벡터 크기와 테일 레이턴시: 벡터가 클수록 처리량은 높아지지만, 벡터의 마지막 패킷은 첫 패킷이 노드에 도착한 후 전체 벡터 처리가 끝날 때까지 대기합니다. VoIP/실시간(Real-time) 트래픽이 중요한 환경에서는 벡터 크기를 줄여 테일 레이턴시를 낮출 수 있습니다.

VPP 아키텍처

VPP 패킷 처리 그래프 아키텍처 NIC DPDK/AF_XDP dpdk-input INPUT node ethernet-input L2 분류 ip4-input IPv4 처리 ip4-lookup FIB 검색 ip4-rewrite MAC 재작성 NAT44 ACL ip6-input IPv6 처리 l2-input L2 브릿징 l2-fwd MAC 학습/포워딩 interface-output TX queue 범례: INPUT/INTERNAL 노드 L3 처리 노드 기능 플러그인 (ACL, NAT...)

패킷 처리 그래프 (Graph Node Architecture)

VPP의 핵심은 방향성 비순환 그래프(DAG) 기반 패킷 처리 엔진입니다. 각 처리 단계는 노드(Node)로 구현되며, 노드 간 연결은 arc로 표현됩니다. 패킷(벡터)은 노드에서 노드로 arc를 따라 이동하며, 각 노드는 패킷의 다음 목적지 노드를 결정합니다.

/* 그래프 노드 등록 예제 — src/vnet/ethernet/node.c */
VLIB_REGISTER_NODE (ethernet_input_node) = {
    .function = ethernet_input,          /* 노드 처리 함수 */
    .name = "ethernet-input",
    .vector_size = sizeof(u32),          /* 벡터 원소 크기 (버퍼 인덱스) */
    .n_errors = ETHERNET_N_ERROR,
    .error_strings = ethernet_error_strings,
    .n_next_nodes = ETHERNET_INPUT_N_NEXT,
    .next_nodes = {
        [ETHERNET_INPUT_NEXT_IP4_INPUT] = "ip4-input",
        [ETHERNET_INPUT_NEXT_IP6_INPUT] = "ip6-input",
        [ETHERNET_INPUT_NEXT_L2_INPUT]  = "l2-input",
        [ETHERNET_INPUT_NEXT_DROP]      = "error-drop",
    },
};

노드 유형 (INTERNAL, INPUT, PROCESS, PRE_INPUT)

노드 유형설명실행 방식대표 예
VLIB_NODE_TYPE_INTERNAL그래프 중간 처리 노드이전 노드가 벡터를 전달할 때 실행ip4-input, ip4-lookup, l2-fwd
VLIB_NODE_TYPE_INPUT패킷 입력 노드매 메인 루프마다 폴링(Polling)dpdk-input, af-packet-input
VLIB_NODE_TYPE_PROCESS코루틴 기반 프로세스(Process)이벤트/타이머(Timer) 기반 실행dhcp-client-process, arp-reply
VLIB_NODE_TYPE_PRE_INPUT입력 전 처리INPUT 노드보다 먼저 실행epoll-input (이벤트 폴링)

벡터 크기와 처리 흐름

VPP의 벡터 프레임 크기는 VLIB_FRAME_SIZE로 정의되며, 기본값은 256입니다. 메인 루프(vlib_main_loop)는 다음 순서로 동작합니다:

  1. PRE_INPUT 노드 실행 (epoll 이벤트 수집 등)
  2. INPUT 노드 실행 (dpdk-input이 NIC에서 패킷 수집 → 벡터 생성)
  3. INTERNAL 노드: 보류 중인 프레임이 있는 노드를 순회하며 벡터 처리
  4. PROCESS 노드: 타이머/이벤트 만료된 코루틴 실행
  5. 1번으로 돌아감 (busy-loop 또는 sleep)
성능 팁: VPP의 메인 루프는 기본적으로 busy-polling이며, 패킷이 없을 때도 CPU를 100% 사용합니다. startup.confpoll-sleep-usec 옵션으로 유휴 시 절전 모드(Suspend)를 활성화할 수 있지만, 레이턴시가 증가할 수 있습니다.

vlib_buffer_t 구조체(Struct) 상세

vlib_buffer_t는 VPP에서 패킷 데이터를 관리하는 핵심 구조체로, 커널의 sk_buff에 대응합니다. 정확히 2개의 캐시라인(128바이트)으로 설계되어 캐시 효율을 극대화합니다.

vlib_buffer_t 메모리 레이아웃 (2 캐시라인) Cache Line 0 (오프셋 0x00 ~ 0x3F, 64바이트) — 핫 필드 current_data (i16) current_length (u16) flags (u32) flow_id (u32) next_buffer (u32) current_config_index error (u16) n_add_refs (u8) buffer_pool_index opaque[10] (u32×10, 40바이트) ← 플러그인별 메타데이터 (ip, tcp 등) Cache Line 1 (오프셋 0x40 ~ 0x7F, 64바이트) — 콜드 필드 opaque2[16] (u32×16, 64바이트) ← 추가 메타데이터 (vnet, trace 등) trace_handle (u32) ← 패킷 트레이싱 연결 데이터 영역 (오프셋 0x80+) pre_data → [ headroom ] → data[0..current_length] → 구조체 크기: 128바이트 (2 캐시라인) + 패킷 데이터 (기본 2048바이트)
필드오프셋(Offset)크기용도
current_data0x00i16data[0]부터 현재 패킷 시작 위치까지의 오프셋
current_length0x02u16현재 버퍼의 유효 데이터 길이
flags0x04u32VLIB_BUFFER_TOTAL_LENGTH_VALID, IS_TRACED 등 플래그
flow_id0x08u32NIC RSS 해시(Hash) 또는 플로우 분류 ID
next_buffer0x0Cu32체인 버퍼의 다음 버퍼 인덱스 (점보 프레임)
current_config_index0x10u32Feature arc 설정 인덱스
error0x14u16노드별 에러 코드 인덱스
n_add_refs0x16u8추가 참조 카운트 (복제 시)
buffer_pool_index0x17u8버퍼가 속한 풀 인덱스
opaque[10]0x1840B노드별 메타데이터 (ip4_header_t *, adjacency index 등)
opaque2[16]0x4064B확장 메타데이터 (vnet 계층, 트레이스 정보)
trace_handleu32show trace 결과와 연결되는 핸들
/* vlib_buffer_t 핵심 접근 API */

/* 버퍼 인덱스(u32)로 버퍼 포인터 획득 */
vlib_buffer_t *b = vlib_get_buffer(vm, buffer_index);

/* 현재 데이터 시작 포인터 */
void *data = vlib_buffer_get_current(b);

/* 헤더 추가/제거 (current_data 이동) */
vlib_buffer_advance(b, -sizeof(ethernet_header_t));  /* 헤더 추가 */
vlib_buffer_advance(b, sizeof(ip4_header_t));       /* 헤더 제거 */

/* 체인 버퍼의 전체 길이 */
u32 total = vlib_buffer_length_in_chain(vm, b);

/* 버퍼 복제 (멀티캐스트 등) */
u32 clone_bi;
vlib_buffer_clone(vm, buffer_index, &clone_bi, 1, CLIB_CACHE_LINE_BYTES);
/* opaque 영역 사용 예: ip4 노드가 저장하는 메타데이터 */
typedef struct {
    ip4_header_t *ip_header;
    u32 adj_index;
    u32 flow_hash;
    u32 fib_index;
} ip4_buffer_opaque_t;

/* opaque 접근 매크로 */
#define vnet_buffer(b) ((vnet_buffer_opaque_t *)(b)->opaque)
#define vnet_buffer2(b) ((vnet_buffer_opaque2_t *)(b)->opaque2)
버퍼 풀 고갈 증상: show buffers에서 free 카운트가 0에 가까워지면, 패킷이 error-drop 노드로 전달되며 show errorsno-buffer 에러가 증가합니다. buffers-per-numa 값을 늘리거나 hugepage를 추가 할당하세요.

메인 루프 내부 동작

VPP의 vlib_main_loop()는 모든 패킷 처리의 진입점(Entry Point)입니다. 단일 스레드(Thread) 내에서 4단계를 반복 실행하며, 각 단계의 순서가 성능에 직접 영향을 미칩니다.

VPP 메인 루프 (vlib_main_loop) 흐름 1. PRE_INPUT 노드 epoll, timer wheel 체크 2. INPUT 노드 폴링 dpdk-input → 벡터 생성 3. INTERNAL 노드 디스패치 보류 프레임이 있는 노드를 순회 ethernet-input → ip4-input → ip4-lookup → ... 4. PROCESS 노드 코루틴: ARP, DHCP, BGP 등 poll-sleep-usec > 0이면 sleep 단계별 소요 시간 PRE_INPUT: ~0.1 μs INPUT: ~2~5 μs (NIC 폴링) INTERNAL: 벡터 크기에 비례 PROCESS: 이벤트 시에만 실행 busy-loop 반복 루프 1회 = 1 dispatch cycle | show runtime의 Clocks/Call = INTERNAL 노드 기준
/* vlib_main_loop 의사코드 (src/vlib/main.c) */
static void
vlib_main_loop (vlib_main_t *vm) {
    while (1) {
        /* 1단계: PRE_INPUT — epoll, 시그널 체크 */
        vlib_node_runtime_update_stats(vm);
        dispatch_pre_input_nodes(vm);

        /* 2단계: INPUT — NIC 폴링, 벡터 수집 */
        dispatch_input_nodes(vm);
        /* dpdk-input이 NIC RX 링에서 패킷 배치를 가져와
           ethernet-input으로 향하는 프레임을 생성 */

        /* 3단계: INTERNAL — 보류 프레임 디스패치 */
        for (pending_frame in vm->pending_frames) {
            node = pending_frame->node;
            frame = pending_frame->frame;
            node->function(vm, node, frame);
            /* 노드가 새 프레임을 생성하면 pending 추가 */
        }

        /* 4단계: PROCESS — 코루틴 타이머/이벤트 체크 */
        dispatch_process_nodes(vm);

        /* 유휴 시 선택적 sleep */
        if (no_work && vm->poll_sleep_usec)
            usleep(vm->poll_sleep_usec);
    }
}
루프 단계실행 노드 유형실행 빈도주요 동작
PRE_INPUTVLIB_NODE_TYPE_PRE_INPUT매 사이클epoll 이벤트 수집, 타이머 휠 갱신
INPUTVLIB_NODE_TYPE_INPUT매 사이클NIC 폴링, 패킷 벡터 생성
INTERNALVLIB_NODE_TYPE_INTERNAL프레임 존재 시그래프 노드 순회, 패킷 처리
PROCESSVLIB_NODE_TYPE_PROCESS이벤트/타이머 시코루틴 재개 (ARP, DHCP 등)

Feature Arc 메커니즘

Feature Arc는 VPP의 핵심 확장 메커니즘으로, 패킷 처리 경로에 기능을 동적으로 삽입하거나 제거할 수 있게 합니다. 커널의 Netfilter 후크(Hook) 체인과 유사한 개념이지만, 컴파일 타임이 아닌 런타임에 노드 체인을 재구성합니다.

Feature Arc: ip4-unicast 경로 동적 삽입 기본 (Feature 없음): ip4-input ip4-lookup ip4-rewrite Feature Arc 활성 (ACL + NAT + IPsec 삽입): ip4-input ACL ip4-inacl NAT44 nat44-in2out IPsec ipsec-input ip4-lookup ip4-rewrite Feature Arc은 인터페이스별로 활성화 → 패킷의 current_config_index가 체인을 따라 이동 주요 내장 Feature Arc: ip4-unicast | ip4-multicast | ip4-output | ip6-unicast | ip6-multicast | ip6-output ethernet-output | interface-output | mpls-input | mpls-output | nsh-output
/* Feature Arc에 노드 등록 (VNET_FEATURE_INIT 매크로) */
VNET_FEATURE_INIT (my_acl_feature, static) = {
    .arc_name = "ip4-unicast",       /* 소속 Feature Arc */
    .node_name = "my-acl-node",       /* 노드 이름 */
    .runs_before = VNET_FEATURES("ip4-lookup"),  /* 이 노드보다 먼저 실행 */
    .runs_after  = VNET_FEATURES("ip4-input-no-checksum"),
};

/* 런타임에 인터페이스별 Feature 활성화 */
vpp# set interface feature GigabitEthernet0/8/0 my-acl-node arc ip4-unicast

/* Feature Arc 상태 확인 */
vpp# show features verbose
Feature Arc시작 노드기본 종착 노드용도
ip4-unicastip4-inputip4-lookupIPv4 유니캐스트 인입 처리
ip4-outputip4-rewriteinterface-outputIPv4 송출 처리
ip4-multicastip4-inputip4-mfib-forward-lookupIPv4 멀티캐스트
ip6-unicastip6-inputip6-lookupIPv6 유니캐스트 인입
ethernet-outputadj-l2-midchaininterface-output이더넷 출력 경로
interface-outputinterface-outputinterface-tx최종 인터페이스 출력

핵심 그래프 노드 내부 구현

VPP 패킷 그래프의 핵심 노드들은 고성능을 위해 정교하게 최적화된 내부 구현을 가지고 있습니다. 각 노드의 처리 함수는 quad-loop 또는 dual-loop 패턴을 사용하여 IPC(Instructions Per Cycle)를 극대화합니다.

ethernet-input 노드 (node.c)

ethernet-input 노드는 패킷 그래프의 최초 L2 처리 지점으로, 이더넷 프레임의 EtherType을 분류하여 적절한 다음 노드로 디스패치합니다. 핵심 구현은 ethernet_input_inline() 함수에 있으며, sparse vector를 이용한 O(1) EtherType 룩업이 특징입니다.

EtherType다음 노드설명
ETHERNET_TYPE_IP40x0800ip4-inputIPv4 패킷 처리
ETHERNET_TYPE_IP60x86DDip6-inputIPv6 패킷 처리
ETHERNET_TYPE_ARP0x0806arp-inputARP 요청/응답
ETHERNET_TYPE_MPLS0x8847mpls-inputMPLS 레이블 처리
ETHERNET_TYPE_VLAN0x8100(내부 처리)Single VLAN 태그
ETHERNET_TYPE_DOT1AD0x88A8(내부 처리)QinQ 외부 태그

VLAN 태그 처리에서는 single tagging(802.1Q)과 double tagging(QinQ/802.1ad)을 모두 지원합니다. VLAN 태그가 감지되면 외부 태그를 파싱하여 VLAN ID와 우선순위를 추출하고, 내부 EtherType을 기반으로 최종 디스패치를 수행합니다.

/* ethernet_input_inline() — quad-loop 패턴 (simplified pseudo-code) */
static_always_inline uword
ethernet_input_inline (vlib_main_t *vm,
                       vlib_node_runtime_t *node,
                       vlib_frame_t *frame)
{
  u32 n_left, *from, *to_next;
  u32 next_index;

  from = vlib_frame_vector_args (frame);
  n_left = frame->n_vectors;

  /* Quad-loop: 4개 패킷 동시 처리 */
  while (n_left >= 4)
    {
      vlib_buffer_t *b0, *b1, *b2, *b3;
      u16 type0, type1, type2, type3;

      /* 프리페치: 다음 4개 패킷 메타데이터 미리 로드 */
      vlib_prefetch_buffer_header (b[4], LOAD);
      vlib_prefetch_buffer_header (b[5], LOAD);
      vlib_prefetch_buffer_header (b[6], LOAD);
      vlib_prefetch_buffer_header (b[7], LOAD);

      b0 = vlib_get_buffer (vm, from[0]);
      b1 = vlib_get_buffer (vm, from[1]);
      b2 = vlib_get_buffer (vm, from[2]);
      b3 = vlib_get_buffer (vm, from[3]);

      /* 이더넷 헤더에서 EtherType 추출 */
      ethernet_header_t *e0 = vlib_buffer_get_current (b0);
      type0 = clib_net_to_host_u16 (e0->type);

      /* VLAN 태그 확인 및 처리 */
      if (PREDICT_FALSE (type0 == ETHERNET_TYPE_VLAN
                         || type0 == ETHERNET_TYPE_DOT1AD))
        {
          /* VLAN 헤더 파싱: tag, priority, inner EtherType */
          ethernet_vlan_header_t *v0 = (e0 + 1);
          u16 inner_type = clib_net_to_host_u16 (v0->type);

          /* QinQ: 이중 태그인 경우 한 단계 더 파싱 */
          if (inner_type == ETHERNET_TYPE_VLAN)
            inner_type = parse_second_vlan_tag (v0);

          type0 = inner_type;
          vlib_buffer_advance (b0, sizeof(*v0));
        }

      /* Sparse vector O(1) 룩업으로 다음 노드 결정 */
      next0 = sparse_vec_index (em->l3_next.input_next_by_type, type0);

      /* ... b1, b2, b3도 동일 패턴 반복 ... */

      vlib_validate_buffer_enqueue_x4 (vm, node, next_index,
                                        to_next, n_left_to_next,
                                        bi0, bi1, bi2, bi3,
                                        next0, next1, next2, next3);
      n_left -= 4;
      from += 4;
    }

  /* 잔여 패킷 single-loop 처리 */
  while (n_left > 0)
    { /* ... 단일 패킷 처리 ... */ }

  return frame->n_vectors;
}

ip4-input 노드 (ip4_input.c)

ip4-input 노드는 IPv4 패킷의 유효성을 검증하는 L3 진입점입니다. IP 헤더의 버전, 헤더 길이(IHL), 체크섬, TTL 등을 검사하며, 유효하지 않은 패킷은 ip4-drop 노드로 전달합니다. Feature arc가 활성화된 인터페이스에서는 vnet_feature_arc_start()를 호출하여 feature 체인을 시작합니다.

검증 항목조건실패 시 동작
IP 버전version == 4ip4-drop (BAD_VERSION)
헤더 길이ihl >= 5 (20바이트 이상)ip4-drop (BAD_LENGTH)
총 길이total_length <= buffer_lengthip4-drop (BAD_LENGTH)
체크섬ip4_header_checksum == 0ip4-drop (BAD_CHECKSUM)
TTLttl > 0icmp4-error (TTL_EXPIRED)
소스 주소멀티캐스트 소스 아님ip4-drop
/* ip4_input_inline() — 헤더 검증 로직 (simplified pseudo-code) */
static_always_inline uword
ip4_input_inline (vlib_main_t *vm,
                  vlib_node_runtime_t *node,
                  vlib_frame_t *frame,
                  int verify_checksum)
{
  u32 n_left, *from;
  ip4_input_next_t next;

  from = vlib_frame_vector_args (frame);
  n_left = frame->n_vectors;

  while (n_left > 0)
    {
      vlib_buffer_t *b0 = vlib_get_buffer (vm, from[0]);
      ip4_header_t *ip0 = vlib_buffer_get_current (b0);

      /* 1단계: IP 버전 및 헤더 길이 검증 */
      u8 version0 = (ip0->ip_version_and_header_length >> 4);
      u8 ihl0 = (ip0->ip_version_and_header_length & 0xF);

      if (PREDICT_FALSE (version0 != 4 || ihl0 < 5))
        {
          next = IP4_INPUT_NEXT_DROP;
          b0->error = node->errors[IP4_ERROR_BAD_VERSION];
          goto enqueue;
        }

      /* 2단계: 체크섬 검증 (하드웨어 오프로드 안 된 경우) */
      if (verify_checksum)
        {
          u16 sum0 = ip4_header_checksum (ip0);
          if (PREDICT_FALSE (sum0 != ip0->checksum))
            {
              next = IP4_INPUT_NEXT_DROP;
              b0->error = node->errors[IP4_ERROR_BAD_CHECKSUM];
              goto enqueue;
            }
        }

      /* 3단계: TTL 검사 */
      if (PREDICT_FALSE (ip0->ttl == 0))
        {
          next = IP4_INPUT_NEXT_ICMP_ERROR;
          b0->error = node->errors[IP4_ERROR_TTL_EXPIRED];
          goto enqueue;
        }

      /* 4단계: Feature arc 시작 (인터페이스에 feature 설정 시) */
      if (PREDICT_FALSE (b0->feature_arc_index != ~0))
        {
          vnet_feature_arc_start (im->feat_arc_index,
                                 b0->sw_if_index[VLIB_RX],
                                 &next, b0);
        }
      else
        next = IP4_INPUT_NEXT_LOOKUP;

    enqueue:
      vlib_validate_buffer_enqueue_x1 (vm, node, next_index,
                                        to_next, n_left_to_next,
                                        bi0, next);
      n_left--;
      from++;
    }

  return frame->n_vectors;
}

ip4-lookup 노드 (ip4_forward.c)

ip4-lookup 노드는 목적지 IP 주소를 기반으로 FIB(Forwarding Information Base) 검색을 수행합니다. VPP는 mtrie(Multibit Trie) 자료구조를 사용하여 최장 접두사 매칭(LPM)을 수행하며, 검색 결과로 load-balance 객체를 얻습니다. 다중 경로(ECMP)인 경우 5-tuple flow hash를 계산하여 특정 adjacency를 선택합니다.

mtrie 검색은 3단계로 진행됩니다. 먼저 상위 16비트로 첫 번째 플라이(ply)를 검색하고, 결과가 리프가 아니면 다음 8비트로 두 번째 플라이, 마지막 8비트로 세 번째 플라이를 검색합니다. 대부분의 경로는 첫 번째 또는 두 번째 단계에서 리프에 도달하므로 매우 빠릅니다.

/* ip4_lookup_inline() — mtrie 검색 및 ECMP (simplified pseudo-code) */
static_always_inline uword
ip4_lookup_inline (vlib_main_t *vm,
                   vlib_node_runtime_t *node,
                   vlib_frame_t *frame)
{
  ip4_fib_mtrie_t *mtrie0;
  u32 n_left, *from;

  from = vlib_frame_vector_args (frame);
  n_left = frame->n_vectors;

  while (n_left >= 4)
    {
      vlib_buffer_t *b0, *b1;
      ip4_header_t *ip0, *ip1;
      ip4_fib_mtrie_leaf_t leaf0, leaf1;

      /* 프리페치: 다음 패킷의 IP 헤더를 캐시에 미리 로드 */
      {
        vlib_buffer_t *p2, *p3;
        p2 = vlib_get_buffer (vm, from[2]);
        p3 = vlib_get_buffer (vm, from[3]);
        vlib_prefetch_buffer_header (p2, LOAD);
        vlib_prefetch_buffer_header (p3, LOAD);
        clib_prefetch_load (p2->data);
        clib_prefetch_load (p3->data);
      }

      b0 = vlib_get_buffer (vm, from[0]);
      b1 = vlib_get_buffer (vm, from[1]);
      ip0 = vlib_buffer_get_current (b0);
      ip1 = vlib_buffer_get_current (b1);

      /* 1단계: FIB 테이블 선택 (VRF 기반) */
      u32 fib_index0 = vec_elt (im->fib_index_by_sw_if_index,
                               b0->sw_if_index[VLIB_RX]);
      mtrie0 = &ip4_fib_get(fib_index0)->mtrie;

      /* 2단계: mtrie 검색 (최장 접두사 매칭) */
      leaf0 = ip4_fib_mtrie_lookup_step_one (mtrie0, &ip0->dst_address);
      leaf0 = ip4_fib_mtrie_lookup_step (mtrie0, leaf0,
                                          &ip0->dst_address, 2);
      leaf0 = ip4_fib_mtrie_lookup_step (mtrie0, leaf0,
                                          &ip0->dst_address, 3);

      /* 3단계: load-balance 객체 획득 */
      u32 lbi0 = ip4_fib_mtrie_leaf_get_adj_index (leaf0);
      load_balance_t *lb0 = load_balance_get (lbi0);

      /* 4단계: ECMP — flow hash로 경로 선택 */
      u32 hash0;
      if (PREDICT_FALSE (lb0->lb_n_buckets > 1))
        {
          /* 5-tuple hash: src_ip, dst_ip, proto, src_port, dst_port */
          hash0 = ip4_compute_flow_hash (ip0, lb0->lb_hash_config);
          /* hash 값으로 버킷 인덱스 계산 */
          hash0 = hash0 % lb0->lb_n_buckets;
        }
      else
        hash0 = 0;

      /* 5단계: adjacency 선택 → 다음 노드 결정 */
      dpo_id_t *dpo0 = load_balance_get_bucket_i (lb0, hash0);
      next0 = dpo0->dpoi_next_node;
      vnet_buffer(b0)->ip.adj_index[VLIB_TX] = dpo0->dpoi_index;

      /* ... b1도 동일 패턴 ... */

      vlib_validate_buffer_enqueue_x2 (vm, node, next_index,
                                        to_next, n_left_to_next,
                                        bi0, bi1, next0, next1);
      n_left -= 2;
      from += 2;
    }

  return frame->n_vectors;
}

ip4-rewrite 노드 (ip4_forward.c)

ip4-rewrite 노드는 패킷 전달의 마지막 L3 처리 단계로, adjacency에 저장된 rewrite data를 패킷에 적용합니다. MAC 주소 재작성, TTL 감소, IP 체크섬 incremental 업데이트를 수행하며, 완료된 패킷을 interface-output 노드로 전달합니다.

체크섬 업데이트는 전체 재계산 대신 incremental 방식을 사용합니다. TTL이 1 감소하면 체크섬에 0x0100을 더하는 것으로 충분하며, 이는 전체 헤더를 다시 순회하는 것보다 훨씬 빠릅니다. vnet_rewrite_one_header()vnet_rewrite_two_headers()는 adjacency의 rewrite string을 이더넷 헤더 위치에 복사하여 목적지/소스 MAC 주소와 EtherType을 한 번에 기록합니다.

/* ip4_rewrite_inline() — dual-loop 패턴 (simplified pseudo-code) */
static_always_inline uword
ip4_rewrite_inline (vlib_main_t *vm,
                    vlib_node_runtime_t *node,
                    vlib_frame_t *frame,
                    int do_counters, int is_midchain, int is_mcast)
{
  u32 n_left, *from;
  ip_adjacency_t *adj0, *adj1;

  from = vlib_frame_vector_args (frame);
  n_left = frame->n_vectors;

  while (n_left >= 2)
    {
      vlib_buffer_t *b0, *b1;
      ip4_header_t *ip0, *ip1;
      u32 adj_index0, adj_index1;
      u32 next0, next1;

      /* 프리페치: 다음 패킷 및 adjacency 데이터 */
      vlib_prefetch_buffer_header (b[2], LOAD);
      vlib_prefetch_buffer_header (b[3], LOAD);

      b0 = vlib_get_buffer (vm, from[0]);
      b1 = vlib_get_buffer (vm, from[1]);
      ip0 = vlib_buffer_get_current (b0);
      ip1 = vlib_buffer_get_current (b1);

      /* 1단계: adjacency 획득 (lookup에서 설정한 인덱스) */
      adj_index0 = vnet_buffer(b0)->ip.adj_index[VLIB_TX];
      adj0 = adj_get (adj_index0);

      /* 2단계: TTL 감소 */
      ip0->ttl -= 1;

      /* 3단계: 체크섬 incremental 업데이트
       * TTL 1 감소 = 체크섬에 0x0100 가산 (one's complement) */
      u32 sum0 = ip0->checksum + clib_host_to_net_u16 (0x0100);
      sum0 += (sum0 >= 0xFFFF);  /* carry 처리 */
      ip0->checksum = sum0;

      /* 4단계: TTL 만료 확인 → ICMP 생성 */
      if (PREDICT_FALSE (ip0->ttl == 0))
        {
          next0 = IP4_REWRITE_NEXT_ICMP_ERROR;
          goto enqueue;
        }

      /* 5단계: MAC 헤더 재작성 (adjacency rewrite data 복사) */
      vnet_rewrite_two_headers (adj0[0], adj1[0], ip0, ip1,
                               sizeof (ethernet_header_t));

      /* rewrite_two_headers는 다음을 수행합니다:
       * - 버퍼 포인터를 L2 헤더 시작으로 후퇴
       * - adjacency의 rewrite_data를 복사 (dst MAC + src MAC + EtherType)
       * - 두 패킷을 동시 처리하여 캐시 활용 극대화 */

      /* 6단계: 출력 인터페이스 설정 */
      vnet_buffer(b0)->sw_if_index[VLIB_TX] = adj0->rewrite_header.sw_if_index;
      next0 = adj0->rewrite_header.next_index;  /* → interface-output */

    enqueue:
      vlib_validate_buffer_enqueue_x2 (vm, node, next_index,
                                        to_next, n_left_to_next,
                                        bi0, bi1, next0, next1);
      n_left -= 2;
      from += 2;
    }

  /* 잔여 패킷 single-loop 처리 */
  while (n_left > 0)
    { /* ... 단일 패킷 처리 ... */ }

  return frame->n_vectors;
}

추가 그래프 노드 내부 구현

앞서 살펴본 ethernet-input, ip4-input, ip4-lookup, ip4-rewrite 외에도 VPP 그래프에는 수십 개의 핵심 노드가 존재합니다. 이 절에서는 DPDK 입력, TCP 처리, NAT44, L2 포워딩 노드의 내부 구현을 상세히 분석합니다.

dpdk-input 노드 (dpdk/device/dpdk.h)

dpdk-inputINPUT 타입 노드로, 그래프의 최상단에서 NIC 수신 링을 폴링하여 패킷 벡터를 생성합니다. VPP의 모든 패킷 처리는 이 노드에서 시작됩니다.

INPUT 노드 폴링 메커니즘은 다음과 같이 동작합니다:

/* dpdk_device_input() — DPDK RX 폴링 및 벡터 생성 (simplified pseudo-code) */
static uword
dpdk_device_input (vlib_main_t *vm, dpdk_device_t *xd,
                    vlib_node_runtime_t *node, u16 queue_id)
{
  u32 n_buffers, n_left;
  u32 next_index = VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT;
  u32 buffer_indices[VLIB_FRAME_SIZE];
  struct rte_mbuf *mbufs[VLIB_FRAME_SIZE];

  /* 1단계: DPDK PMD를 통해 NIC RX 링에서 패킷 배치 수집 */
  n_buffers = rte_eth_rx_burst (xd->port_id, queue_id,
                                mbufs, VLIB_FRAME_SIZE);
  if (n_buffers == 0)
    {
      /* adaptive polling: 패킷 없으면 인터럽트 모드 전환 고려 */
      if (xd->flags & DPDK_DEVICE_FLAG_INT_SUPPORTED)
        dpdk_device_enable_interrupt (xd, queue_id);
      return 0;
    }

  /* 2단계: rte_mbuf → vlib_buffer_t 변환 (제로 카피) */
  for (int i = 0; i < n_buffers; i++)
    {
      vlib_buffer_t *b;
      /* mbuf 주소에서 vlib_buffer_t 오프셋 계산 (공유 메모리) */
      b = vlib_buffer_from_rte_mbuf (mbufs[i]);
      buffer_indices[i] = vlib_get_buffer_index (vm, b);

      /* 버퍼 메타데이터 초기화 */
      b->current_length = mbufs[i]->pkt_len;
      b->current_data = 0;
      b->total_length_not_including_first_buffer = 0;

      /* 오프로드 플래그 전파: checksum, VLAN 등 */
      if (mbufs[i]->ol_flags & RTE_MBUF_F_RX_IP_CKSUM_GOOD)
        b->flags |= VNET_BUFFER_F_L4_CHECKSUM_COMPUTED;
    }

  /* 3단계: 벡터 프레임 생성 및 ethernet-input으로 enqueue */
  n_left = n_buffers;
  while (n_left > 0)
    {
      u32 *to_next, n_left_to_next;
      vlib_get_next_frame (vm, node, next_index,
                           to_next, n_left_to_next);

      u32 n_copy = clib_min (n_left, n_left_to_next);
      clib_memcpy_fast (to_next,
                        buffer_indices + (n_buffers - n_left),
                        n_copy * sizeof (u32));
      n_left_to_next -= n_copy;
      n_left -= n_copy;

      vlib_put_next_frame (vm, node, next_index, n_left_to_next);
    }

  /* 인터페이스 카운터 갱신 */
  vlib_increment_combined_counter (
    &vnet_main.interface_main.combined_sw_if_counters
      [VNET_INTERFACE_COUNTER_RX],
    vm->thread_index, xd->sw_if_index,
    n_buffers, /* packets */
    total_bytes  /* bytes */);

  return n_buffers;
}

tcp-input 노드 (tcp_input.c)

tcp-input 노드는 VPP의 호스트 스택(Host Stack) 내에서 TCP 세그먼트를 수신하고 상태 머신을 구동하는 핵심 노드입니다. 일반적인 커널 TCP 스택과 달리 VPP는 사용자 공간에서 세션 레이어를 직접 관리합니다.

TCP 상태 머신 처리 흐름은 다음과 같습니다:

/* tcp_input_inline() — ESTABLISHED fast-path (simplified pseudo-code) */
static uword
tcp_input_inline (vlib_main_t *vm, vlib_node_runtime_t *node,
                   vlib_frame_t *frame, u8 is_ip4)
{
  u32 n_left = frame->n_vectors;
  u32 *from = vlib_frame_vector_args (frame);

  while (n_left > 0)
    {
      vlib_buffer_t *b = vlib_get_buffer (vm, from[0]);
      tcp_header_t *tcp = vlib_buffer_get_current (b);
      tcp_connection_t *tc;

      /* 4-tuple 해시로 기존 연결 검색 */
      tc = tcp_lookup_connection (b, vm->thread_index, is_ip4);
      if (PREDICT_FALSE (!tc))
        {
          /* SYN 패킷이면 tcp-listen으로, 아니면 RST 전송 */
          goto next_packet;
        }

      /* ESTABLISHED 상태 fast-path */
      if (PREDICT_TRUE (tc->state == TCP_STATE_ESTABLISHED))
        {
          /* 시퀀스 번호 검증 */
          if (PREDICT_FALSE (
                !tcp_segment_in_rcv_wnd (tc, tcp)))
            {
              tcp_send_ack (tc);
              goto next_packet;
            }

          /* ACK 처리: snd_una 전진, 혼잡 윈도우 갱신 */
          if (tcp->flags & TCP_FLAG_ACK)
            {
              tcp_rcv_ack (tc, tcp, b);
              /* SACK 스코어보드 갱신 */
              if (tcp_opts_sack_present (&tc->rcv_opts))
                tcp_rcv_sacks (tc, tc->snd_una);
            }

          /* 데이터 수신: 순서대로면 즉시 전달, 아니면 재정렬 큐 */
          if (data_len > 0)
            {
              if (PREDICT_TRUE (
                    seq == tc->rcv_nxt))
                tcp_session_enqueue_data (tc, b, data_len);
              else
                tcp_session_enqueue_ooo (tc, b, data_len);

              /* 지연 ACK 타이머 시작 또는 즉시 ACK */
              tcp_maybe_send_ack (tc);
            }

          /* RTT 샘플 갱신 (RFC 6298) */
          if (tc->rtt_ts && tcp_ack_is_newer (tc, tcp))
            {
              f64 rtt_sample = tcp_time_now () - tc->rtt_ts;
              tcp_update_rto (tc, rtt_sample);
            }
        }
      else
        {
          /* slow-path: 상태별 핸들러 디스패치 */
          switch (tc->state)
            {
            case TCP_STATE_SYN_RCVD:
              tcp_rcv_state_syn_rcvd (tc, tcp, b);
              break;
            case TCP_STATE_FIN_WAIT_1:
              tcp_rcv_state_fin_wait_1 (tc, tcp, b);
              break;
            case TCP_STATE_FIN_WAIT_2:
              tcp_rcv_state_fin_wait_2 (tc, tcp, b);
              break;
            case TCP_STATE_CLOSE_WAIT:
              tcp_rcv_state_close_wait (tc, tcp, b);
              break;
            case TCP_STATE_TIME_WAIT:
              tcp_rcv_state_time_wait (tc, tcp, b);
              break;
            default:
              break;
            }
        }

    next_packet:
      from++;
      n_left--;
    }

  return frame->n_vectors;
}

nat44-in2out / nat44-out2in 노드

NAT44 ED(Endpoint-Dependent) 모드는 VPP의 고성능 NAT 구현으로, 전체 5-tuple을 기반으로 세션을 관리합니다. nat44-in2out은 내부에서 외부 방향, nat44-out2in은 외부에서 내부 방향의 주소 변환을 수행합니다.

NAT44 Endpoint-Dependent 처리 흐름은 다음과 같습니다:

/* nat44_ed_in2out_node_fn_inline() — NAT44 ED in2out 처리 (simplified pseudo-code) */
static uword
nat44_ed_in2out_node_fn_inline (vlib_main_t *vm,
                                 vlib_node_runtime_t *node,
                                 vlib_frame_t *frame,
                                 int is_slow_path)
{
  snat_main_t *sm = &snat_main;
  u32 n_left = frame->n_vectors;
  u32 *from = vlib_frame_vector_args (frame);
  u32 thread_index = vm->thread_index;

  while (n_left > 0)
    {
      vlib_buffer_t *b = vlib_get_buffer (vm, from[0]);
      ip4_header_t *ip = vlib_buffer_get_current (b);
      snat_session_t *s;
      u32 next = NAT44_ED_IN2OUT_NEXT_LOOKUP;

      /* 1단계: 5-tuple 추출 (src/dst IP, src/dst port, protocol) */
      nat_ed_ses_key_t kv;
      nat44_ed_make_key (&kv, ip->src_address, ip->dst_address,
                          ip4_get_src_port (ip),
                          ip4_get_dst_port (ip),
                          ip->protocol,
                          vnet_buffer(b)->sw_if_index[VLIB_RX]);

      /* 2단계: 세션 테이블 룩업 (clib_bihash) */
      if (clib_bihash_search_16_8 (&sm->flow_hash,
                                    &kv, &value) == 0)
        {
          /* 세션 히트: 기존 매핑 적용 */
          s = pool_elt_at_index (
                sm->per_thread_data[thread_index].sessions,
                value.value);
        }
      else
        {
          /* 세션 미스: 새 매핑 생성 (slow-path) */
          if (!is_slow_path)
            {
              next = NAT44_ED_IN2OUT_NEXT_SLOW_PATH;
              goto enqueue;
            }

          /* 주소 풀에서 외부 IP 선택 */
          snat_address_t *a;
          u16 ext_port;
          nat44_ed_alloc_addr_and_port (
            sm, thread_index, ip->protocol,
            &a, &ext_port);

          /* 새 세션 생성 및 양방향 해시 삽입 */
          s = nat44_ed_session_alloc (sm, thread_index);
          s->in2out.addr = ip->src_address;
          s->in2out.port = ip4_get_src_port (ip);
          s->out2in.addr = a->addr;
          s->out2in.port = ext_port;
          s->ext_host_addr = ip->dst_address;
          s->ext_host_port = ip4_get_dst_port (ip);

          /* in2out 방향 해시 삽입 */
          nat44_ed_session_add_to_flow_hash (sm, s, thread_index);
        }

      /* 3단계: 패킷 변환 — src IP/port 교체 */
      ip4_address_t old_addr = ip->src_address;
      u16 old_port = ip4_get_src_port (ip);
      ip->src_address = s->out2in.addr;
      ip4_set_src_port (ip, s->out2in.port);

      /* 4단계: 체크섬 incremental update */
      ip_csum_update (ip->checksum,
                      old_addr.as_u32, s->out2in.addr.as_u32,
                      ip4_header_t,
                      src_address);
      /* L4 체크섬도 incremental update (TCP/UDP) */
      tcp_udp_csum_update (ip, old_addr, old_port,
                           s->out2in.addr, s->out2in.port);

      /* 세션 타임스탬프 갱신 (LRU 에이징용) */
      s->last_heard = vlib_time_now (vm);

    enqueue:
      vlib_validate_buffer_enqueue_x1 (vm, node, next_index,
                                        to_next, n_left_to_next,
                                        from[0], next);
      from++;
      n_left--;
    }

  return frame->n_vectors;
}

l2-input / l2-fwd 노드

VPP의 L2 브릿지 도메인(Bridge Domain)은 전통적인 스위치의 포워딩 기능을 사용자 공간에서 구현합니다. l2-input 노드가 L2 처리 파이프라인의 진입점이고, l2-fwd 노드가 MAC 테이블 기반 포워딩을 수행합니다.

L2 브릿지 도메인 처리 흐름은 다음과 같습니다:

/* l2_fwd_node_fn() — L2 MAC 룩업 포워딩 (simplified pseudo-code) */
static uword
l2_fwd_node_fn (vlib_main_t *vm,
                vlib_node_runtime_t *node,
                vlib_frame_t *frame)
{
  l2fib_main_t *fm = &l2fib_main;
  u32 n_left = frame->n_vectors;
  u32 *from = vlib_frame_vector_args (frame);

  while (n_left > 0)
    {
      vlib_buffer_t *b = vlib_get_buffer (vm, from[0]);
      ethernet_header_t *eth = vlib_buffer_get_current (b);
      u32 bd_index = vnet_buffer(b)->l2.bd_index;
      u32 next;

      /* 1단계: destination MAC으로 L2 FIB 검색 */
      l2fib_entry_key_t key;
      l2fib_entry_result_t result;
      l2fib_make_key (&key, eth->dst_address, bd_index);

      if (clib_bihash_search_8_8 (&fm->mac_table,
                                   &key, &result) == 0)
        {
          /* MAC 히트: 출력 인터페이스 결정 */
          u32 sw_if_index = result.fields.sw_if_index;

          /* 입력 = 출력이면 드롭 (split-horizon) */
          if (PREDICT_FALSE (
                sw_if_index == vnet_buffer(b)->sw_if_index[VLIB_RX]))
            {
              next = L2FWD_NEXT_DROP;
              goto enqueue;
            }

          /* 출력 인터페이스 설정 */
          vnet_buffer(b)->sw_if_index[VLIB_TX] = sw_if_index;
          next = L2FWD_NEXT_L2_OUTPUT;

          /* 필터링 엔트리이면 드롭 */
          if (PREDICT_FALSE (result.fields.filter))
            next = L2FWD_NEXT_DROP;

          /* 정적 엔트리가 아니면 에이징 타임스탬프 갱신 */
          if (!result.fields.static_mac)
            result.fields.timestamp = l2fib_cur_age (fm);
        }
      else
        {
          /* MAC 미스: BUM 트래픽으로 간주, flood 경로 */
          next = L2FWD_NEXT_FLOOD;
        }

      /* 2단계: source MAC 러닝 */
      if (PREDICT_TRUE (
            vnet_buffer(b)->l2.feature_bitmap & L2INPUT_FEAT_LEARN))
        {
          l2fib_entry_key_t src_key;
          l2fib_entry_result_t src_result;
          l2fib_make_key (&src_key, eth->src_address, bd_index);

          if (clib_bihash_search_8_8 (&fm->mac_table,
                                       &src_key, &src_result) != 0)
            {
              /* 새 source MAC: 엔트리 삽입 (러닝) */
              src_result.fields.sw_if_index =
                vnet_buffer(b)->sw_if_index[VLIB_RX];
              src_result.fields.static_mac = 0;
              src_result.fields.timestamp = l2fib_cur_age (fm);
              clib_bihash_add_del_8_8 (&fm->mac_table,
                                        &src_key, &src_result,
                                        1 /* is_add */);

              /* MAC 러닝 이벤트 로그 */
              l2fib_learn_event (fm, eth->src_address,
                                  bd_index,
                                  vnet_buffer(b)->sw_if_index[VLIB_RX]);
            }
          else if (src_result.fields.sw_if_index !=
                     vnet_buffer(b)->sw_if_index[VLIB_RX])
            {
              /* MAC 이동: 인터페이스 갱신 */
              src_result.fields.sw_if_index =
                vnet_buffer(b)->sw_if_index[VLIB_RX];
              clib_bihash_add_del_8_8 (&fm->mac_table,
                                        &src_key, &src_result,
                                        1);
            }
        }

    enqueue:
      vlib_validate_buffer_enqueue_x1 (vm, node, next_index,
                                        to_next, n_left_to_next,
                                        from[0], next);
      from++;
      n_left--;
    }

  return frame->n_vectors;
}

메모리 관리(Memory Management) 아키텍처

힙/mheap 관리

VPP는 여러 개의 독립적인 메모리 영역을 관리합니다. 모든 영역은 hugepage 위에 할당되며, clib_mem 인프라가 NUMA 인식 할당을 제공합니다.

메모리 영역기본 크기용도설정 위치
Main Heap1 GBFIB, 세션 테이블, 일반 할당memory { main-heap-size 2G }
Buffer PoolsNUMA별 개별패킷 데이터 (vlib_buffer_t + data)buffers { buffers-per-numa 32768 }
API Segment64 MBBinary API 공유 메모리api-segment { global-size 256M }
Stats Segment32 MB통계 카운터 공유 메모리statseg { size 64M }
# VPP 메모리 사용량 확인
vpp# show memory main-heap
Thread 0 vpp_main
  base 0x7f8a00000000, size 1g, locked, unmap-on-destroy
    page stats: page-size 2M, total 512, mapped 310, not-mapped 202
    total: 1073741824, used: 536870912, free: 536870912

vpp# show memory api-segment
  Shared API segment:
    base 0x7f8900000000, size 64m

# NUMA별 메모리 분포
vpp# show memory numa

버퍼 풀 아키텍처

VPP 버퍼 풀은 고성능 패킷 처리의 핵심입니다. NUMA 노드별로 독립된 풀을 유지하여 원격 메모리 접근을 방지하고, 프리 리스트와 재활용(Recycling) 메커니즘으로 할당/해제 오버헤드를 최소화합니다.

VPP 버퍼 풀 아키텍처 (per-NUMA) NUMA Node 0 Hugepages (2MB/1GB 페이지) Buffer Pool 0 buf 0 buf 1 buf 2 ... buf N Free List (LIFO) Thread 0 캐시 32개 버퍼 로컬 Thread 1 캐시 32개 버퍼 로컬 NUMA Node 1 Hugepages (2MB/1GB 페이지) Buffer Pool 1 buf 0 buf 1 buf 2 ... buf N Free List (LIFO) Thread 2 캐시 32개 버퍼 로컬 Thread 3 캐시 32개 버퍼 로컬 버퍼 인덱스(u32)로 접근 — 포인터 대신 인덱스를 사용하여 32비트로 버퍼 참조 (메모리 절약)
/* 버퍼 풀 생성과 생명주기 */

/* 1. 시작 시: hugepage 위에 버퍼 풀 사전 할당 */
/*    buffers { buffers-per-numa 32768 } → NUMA별 32768개 버퍼 */

/* 2. INPUT 노드: 버퍼 할당 */
u32 n_alloc = vlib_buffer_alloc(vm, buffer_indices, n_buffers);
/* 스레드 로컬 캐시에서 먼저 할당 → 부족 시 글로벌 free list에서 보충 */

/* 3. 패킷 처리 중: 버퍼 체인 (점보 프레임 등) */
if (b->flags & VLIB_BUFFER_NEXT_PRESENT)
    next_b = vlib_get_buffer(vm, b->next_buffer);

/* 4. 처리 완료: 버퍼 해제 (free list 반환) */
vlib_buffer_free(vm, buffer_indices, n_buffers);
/* 로컬 캐시가 가득 차면 글로벌 free list로 반환 */
포인터 대신 인덱스(u32): VPP는 버퍼를 64비트 포인터 대신 32비트 인덱스로 참조합니다. 벡터 프레임의 각 원소가 4바이트(u32)이므로, 256개 패킷 벡터가 1KB만 차지합니다. 포인터라면 2KB가 필요합니다. 이 설계는 캐시 라인(Cache Line) 효율을 높이고, 프레임 복사/이동 비용을 절반으로 줄입니다.

멀티스레딩 아키텍처

워커 스레드 모델

VPP는 메인 스레드 + N개 워커 스레드 모델을 사용합니다. 메인 스레드는 CLI/API 처리와 제어 평면을 담당하고, 워커 스레드는 데이터 평면(패킷 처리)에 전담합니다. 각 워커는 독립된 메인 루프를 실행합니다.

VPP 멀티스레딩 아키텍처 NIC RX Queue 0 RX Queue 1 RX Queue 2 Main Thread (Core 0) CLI, API, 제어 평면, PROCESS 노드 Worker 0 (Core 2) dpdk-input → graph dispatch → interface-output Worker 1 (Core 4) dpdk-input → graph dispatch → interface-output Worker 2 (Core 6) dpdk-input → graph dispatch → interface-output Frame Handoff Worker 0 → Worker 1 (플로우 기반) lockfree ring per-thread queue Barrier Sync 설정 변경 시 전체 동기화 각 워커 = 독립 메인 루프 + 전용 RX 큐 | 워커 간 공유 데이터 없음 (lockfree)
항목Main ThreadWorker Thread
CPU 코어main-core (보통 0)corelist-workers (2,4,6...)
역할CLI, API, 제어 평면패킷 처리 (데이터 평면)
INPUT 노드실행 가능 (기본 비활성)dpdk-input 폴링
PROCESS 노드ARP, DHCP, BGP 등실행하지 않음
NIC 큐 매핑(Mapping)1:1 (큐 N → 워커 N)
공유 데이터 접근배리어 내에서 수정읽기 전용 (RCU 유사)
/* startup.conf — 스레딩 설정 */
cpu {
    main-core 0                /* 메인 스레드: 코어 0 */
    corelist-workers 2,4,6,8   /* 워커 4개: 물리 코어만 (HT 제외) */
    skip-cores 1               /* 코어 1 건너뛰기 (OS 예약) */
    scheduler-policy fifo      /* RT 스케줄링 (선택) */
    scheduler-priority 50      /* RT 우선순위 */
}

/* 워커 수에 따라 NIC RX 큐를 자동 매핑 */
dpdk {
    dev 0000:03:00.0 {
        num-rx-queues 4        /* 워커 수와 일치 */
    }
}

프레임 핸드오프

NIC의 RSS(Receive Side Scaling)는 5-tuple 해시를 기반으로 수신 큐를 분산합니다. 하지만 NAT44 세션 룩업, IPsec SA 선택, TCP 세그먼트 재조립 등 상태 기반 처리(Stateful Processing)는 동일 플로우의 모든 패킷이 반드시 같은 워커에서 처리되어야 합니다. RSS 해시가 이를 보장하지 못할 때, VPP의 frame handoff 메커니즘이 패킷을 올바른 워커로 재분배합니다.

Handoff 동작 원리

VPP의 handoff는 그래프 노드로 구현되며, 다음과 같은 단계로 동작합니다:

  1. 플로우 해시 계산: handoff 노드가 패킷의 5-tuple(src IP, dst IP, src port, dst port, protocol)로부터 해시를 계산합니다.
  2. 대상 워커 결정: hash % n_workers 연산으로 패킷이 처리될 워커 인덱스를 결정합니다.
  3. 바이패스 최적화: 패킷이 이미 올바른 워커에 있으면 handoff 없이 다음 노드로 직접 전달합니다. 이 경우 추가 비용이 발생하지 않습니다.
  4. 크로스 워커 전달: 다른 워커로 이동해야 하면, 해당 워커의 per-worker lockfree ring에 버퍼 인덱스를 enqueue합니다.
  5. 대상 워커 수신: 대상 워커가 다음 메인 루프(main loop) 반복에서 자신의 ring을 dequeue하여 패킷을 수신하고 그래프 처리를 계속합니다.
  6. Symmetrical 모드: symmetrical 옵션을 활성화하면, 양방향 플로우(A→B와 B→A)가 동일한 해시를 생성하여 같은 워커에서 처리됩니다. 이를 위해 src/dst를 정렬한 후 해시를 계산합니다.
Worker 0 (패킷 수신) handoff 노드 flow hash 계산 hash % n_workers = 2 → Worker 2로 전달 Lockfree Ring Worker 2 전용 큐 enqueue(buf_idx) Worker 2 dequeue → 그래프 처리 다음 노드 처리 nat44-in2out / ipsec 등 이미 올바른 워커 → bypass (비용 0)

핵심 구조체: vlib_frame_queue_t

Handoff의 핵심은 per-worker lockfree ring buffervlib_frame_queue_t 구조체입니다. 각 워커마다 하나의 ring이 존재하며, 다른 워커들이 이 ring에 패킷을 enqueue합니다:

필드타입설명
headvolatile u32Consumer(수신 워커)가 읽는 위치. 대상 워커만 증가시킵니다.
tailvolatile u32Producer(송신 워커)가 쓰는 위치. Atomic CAS로 증가시킵니다.
elts[]vlib_frame_queue_elt_tRing의 각 슬롯. 버퍼 인덱스 배열(buffer_index[VLIB_FRAME_SIZE])과 프레임 메타데이터를 포함합니다.
n_in_useu32현재 사용 중인 슬롯 수 (오버플로 감지용)
neltsu32Ring 크기. 기본값은 64이며, frame-queue-nelts 설정으로 조정합니다.
/* vlib_buffer_enqueue_to_thread() — 핵심 handoff 함수 (의사 코드) */
static_always_inline u32
vlib_buffer_enqueue_to_thread (vlib_main_t *vm,
                               vlib_node_runtime_t *node,
                               u32 *buffers, u16 *thread_indices,
                               u32 n_packets, u32 drop_on_full)
{
  vlib_frame_queue_t *fq;
  u32 n_left = n_packets;

  while (n_left)
  {
    u16 target_thread = thread_indices[0];

    /* 같은 워커이면 bypass — handoff 비용 없음 */
    if (target_thread == vm->thread_index)
    {
      /* 현재 노드의 next로 직접 enqueue */
      vlib_buffer_enqueue_to_next (vm, node, buffers, nexts, count);
      continue;
    }

    /* 대상 워커의 frame queue 획득 */
    fq = vlib_get_frame_queue (vm, fq_index, target_thread);

    /* ring tail을 atomic CAS로 확보 */
    u32 tail = __atomic_load_n (&fq->tail, __ATOMIC_ACQUIRE);
    u32 new_tail = tail + 1;

    /* 오버플로 검사 */
    if (PREDICT_FALSE (new_tail - fq->head >= fq->nelts))
    {
      if (drop_on_full)
        vlib_buffer_free (vm, buffers, count);
      continue;
    }

    /* 버퍼 인덱스를 ring 슬롯에 복사 */
    vlib_frame_queue_elt_t *elt = &fq->elts[tail & (fq->nelts - 1)];
    clib_memcpy_fast (elt->buffer_index, buffers, count * sizeof (u32));
    elt->n_vectors = count;

    /* tail 전진 (atomic store) — 대상 워커가 볼 수 있게 됨 */
    __atomic_store_n (&fq->tail, new_tail, __ATOMIC_RELEASE);

    /* 대상 워커의 pending interrupt 신호 */
    vlib_node_set_interrupt_pending (
        vlib_get_main_by_index (target_thread), node->node_index);
  }

  return n_packets;
}

Handoff CLI 명령

명령설명예시
set interface handoff인터페이스에 handoff 노드를 삽입합니다set interface handoff GigabitEthernet0/8/0 workers 0-3
symmetrical양방향 플로우가 같은 워커에서 처리되도록 합니다set interface handoff ... workers 0-3 symmetrical
show handoff워커별 handoff 큐 상태 및 드롭 수를 표시합니다show handoff
frame-queue-neltsstartup.conf에서 ring 크기를 조정합니다cpu { frame-queue-nelts 128 }
show runtimehandoff 노드의 벡터 수/클록 사이클을 확인합니다show runtime
# handoff 노드 삽입 (nat44 이전에 플로우 해시 기반 재분배)
vpp# set interface handoff GigabitEthernet0/8/0 workers 0-3 symmetrical

# handoff 큐 상태 확인
vpp# show handoff
  worker 0: 250000 frames, 0 drops
  worker 1: 248000 frames, 0 drops
  worker 2: 251000 frames, 0 drops
  worker 3: 249000 frames, 0 drops

Handoff 성능 영향

항목Handoff 없음Handoff 있음비고
패킷당 추가 사이클0~50-80 사이클캐시 미스 포함. bypass 시 ~5 사이클
메모리 대역폭기본+10-15%크로스 워커 버퍼 인덱스 복사
캐시 효율높음L1/L2 미스 증가패킷 메타데이터가 다른 코어 캐시에 존재
플로우 일관성보장 안 됨보장됨상태 기반 기능 정상 동작
frame queue 메모리0워커당 ~512KB64 슬롯 × VLIB_FRAME_SIZE 기준

실전 시나리오별 Handoff 설정

시나리오Handoff 필요 이유권장 설정
NAT44세션 테이블이 per-thread이므로 동일 플로우가 다른 워커에서 처리되면 세션 미스(Miss)가 발생합니다set interface handoff <if> workers 0-N symmetrical
IPsec (터널 모드)SA(Security Association) 상태와 시퀀스 번호(Sequence Number)가 per-thread이므로 플로우 분산 시 replay 검사가 실패합니다set interface handoff <if> workers 0-N symmetrical
TCP 스택 (Host Stack)TCP 연결 상태(윈도우, ACK 추적)는 단일 워커에서 관리해야 합니다set interface handoff <if> workers 0-N symmetrical
순수 L3 포워딩FIB 룩업은 stateless이므로 handoff가 불필요하며, 오히려 성능을 저하시킵니다Handoff 미설정 (RSS만 사용)
ACL 기반 필터링ACL 매칭은 stateless이므로 handoff 없이도 정상 동작합니다Handoff 미설정 (RSS만 사용)
핸드오프 큐 오버플로: 특정 워커가 과부하되면 handoff 큐가 가득 차서 패킷이 드롭됩니다. show handoff에서 drops가 증가하면, show runtime으로 해당 워커의 병목(Bottleneck) 노드를 확인하세요. RSS 해시가 불균형한 경우가 흔하며, frame-queue-nelts를 128이나 256으로 늘려 일시적 버스트를 흡수할 수 있습니다.

배리어 동기화

VPP의 데이터 평면은 lockfree로 동작하지만, FIB 업데이트나 인터페이스 추가 같은 제어 평면 변경은 모든 워커를 일시 정지시키는 배리어 동기화가 필요합니다.

작업배리어 필요이유
FIB 엔트리 추가/삭제필요mtrie 구조체 수정
인터페이스 생성/삭제필요sw_interface 벡터 재할당
ACL 규칙 변경필요ACL 매치 테이블 교체
통계 카운터 읽기불필요per-thread 카운터 (lockfree)
패킷 트레이싱불필요per-thread 트레이스 버퍼
show runtime불필요읽기 전용
배리어 장기 홀드: 배리어가 활성화되면 모든 워커의 패킷 처리가 정지합니다. show barrier로 배리어 점유 시간을 모니터링하세요. 대량의 FIB 업데이트(BGP 풀 테이블 로딩 등)는 배리어를 장시간 홀드하여 패킷 드롭을 유발할 수 있습니다. FIB 배치 업데이트 API를 사용하면 배리어 점유를 최소화합니다.

큐 어피니티(Queue Affinity)와 플로우 스티키

큐 어피니티(Queue Affinity)는 특정 플로우의 패킷이 항상 동일한 워커 스레드에서 처리되도록 보장하는 메커니즘입니다. NIC의 RSS, 플로우 디렉터(Flow Director), DPDK rte_flow API, VPP의 핸드오프(handoff) 노드 등 여러 계층에서 구현됩니다. 이를 통해 세션 상태의 경합 없는 처리와 캐시 지역성(Cache Locality)을 극대화합니다.

NIC 계층 — 하드웨어 플로우 분류 RSS (5-tuple 해시) Toeplitz 해시 → indirection table src/dst IP, port, proto Flow Director (exact match) 특정 플로우 → 지정 큐 NIC 하드웨어 필터 rte_flow (flexible match) 프로그래밍 가능 분류 규칙 prefix, mask, action chain DPDK 계층 — 큐 할당 RX Queue 0 RX Queue 1 RX Queue 2 RX Queue 3 VPP 계층 — 워커 어피니티 Worker 0 세션 어피니티 바인딩 TCP/TLS FIFO 전용 handoff 보정 Worker 1 세션 어피니티 바인딩 TCP/TLS FIFO 전용 handoff 보정 Worker 2 세션 어피니티 바인딩 TCP/TLS FIFO 전용 handoff 보정 Worker 3 세션 어피니티 바인딩 TCP/TLS FIFO 전용 handoff 보정

RSS(Receive Side Scaling) 상세

RSS는 NIC 하드웨어에서 Toeplitz 해시 함수를 사용하여 5-tuple(소스 IP, 목적지 IP, 소스 포트, 목적지 포트, 프로토콜)에 대한 해시값을 계산하고, 그 결과를 인다이렉션 테이블(Indirection Table)을 통해 RX 큐에 매핑합니다.

/* Toeplitz 해시 계산 과정 (개념적 수도코드) */
static u32 toeplitz_hash(const u8 *key, const u8 *input, int len)
{
    u32 hash = 0;
    for (int i = 0; i < len * 8; i++) {
        if (input[i / 8] & (1 << (7 - (i % 8))))
            hash ^= get_key_word(key, i);
    }
    return hash;
}

/* 해시 → 큐 매핑 */
queue_index = indirection_table[hash & (table_size - 1)];
# VPP startup.conf에서 RSS 설정
dpdk {
    dev 0000:03:00.0 {
        num-rx-queues 4           # 워커 수에 맞춰 큐 수 설정
        rss-fn ipv4-tcp           # TCP/IPv4에 대해 RSS 활성화
    }
}
RSS 한계: Toeplitz 해시는 확률적 분배이므로 해시 충돌로 인한 큐 불균형이 발생할 수 있습니다. 또한 요청과 응답의 src/dst가 반전되어 서로 다른 큐에 도착하는 비대칭 문제가 있으며, 이를 해결하려면 대칭 해시를 사용해야 합니다.

Flow Director / rte_flow 기반 정밀 제어

Flow Director는 NIC 하드웨어에서 exact-match 또는 prefix-match 규칙으로 특정 플로우를 지정된 큐로 스티어링(steering)합니다. DPDK의 rte_flow API는 이를 프로그래밍 가능한 방식으로 추상화합니다.

/* rte_flow API로 특정 VIP 트래픽을 큐 2로 스티어링하는 예시 */
struct rte_flow_attr attr = { .ingress = 1 };
struct rte_flow_item pattern[] = {
    { .type = RTE_FLOW_ITEM_TYPE_ETH },
    { .type = RTE_FLOW_ITEM_TYPE_IPV4,
      .spec = &(struct rte_flow_item_ipv4){
          .hdr.dst_addr = rte_cpu_to_be_32(RTE_IPV4(10,0,0,100))
      }},
    { .type = RTE_FLOW_ITEM_TYPE_END },
};
struct rte_flow_action actions[] = {
    { .type = RTE_FLOW_ACTION_TYPE_QUEUE,
      .conf = &(struct rte_flow_action_queue){ .index = 2 }},
    { .type = RTE_FLOW_ACTION_TYPE_END },
};
flow = rte_flow_create(port_id, &attr, pattern, actions, &error);
# VPP CLI에서 DPDK 플로우 분류 설정
set dpdk flow-classify interface GigabitEthernet3/0/0 \
    ip4-table 0

# 사용 사례: 특정 VIP(10.0.0.100) 트래픽을 전용 워커에 할당
# → NIC 하드웨어가 패킷을 지정 큐로 스티어링
# → 해당 큐에 매핑된 워커가 전담 처리

VPP 세션 워커 어피니티

TCP/TLS 세션은 accept 또는 connect 시점에 현재 워커 스레드에 바인딩됩니다. session_alloc() 함수가 vlib_get_thread_index()로 현재 워커 인덱스를 확인하고, 해당 세션을 그 워커에 고정합니다. 세션의 전체 생명주기(lifetime) 동안 워커 변경은 불가능하며, 이는 FIFO 버퍼를 스레드 간에 공유하지 않기 때문입니다.

/* src/vnet/session/session.c — 세션 할당과 워커 바인딩 */
session_t *
session_alloc (u32 thread_index)
{
    session_worker_t *wrk = &session_main.wrk[thread_index];
    session_t *s;

    /* per-worker 풀에서 세션 할당 — 락 불필요 */
    pool_get_aligned(wrk->sessions, s, CLIB_CACHE_LINE_BYTES);
    clib_memset(s, 0, sizeof(*s));
    s->session_index = s - wrk->sessions;
    s->thread_index = thread_index;  /* 현재 워커에 고정 */

    return s;
}

/* handoff 노드 — RSS 불일치 보정 */
/* 패킷이 잘못된 워커에 도착하면 올바른 워커로 재전달 */
static uword
session_queue_node_fn (vlib_main_t *vm, ...)
{
    /* session의 thread_index와 현재 thread_index 비교 */
    if (s->thread_index != thread_index)
        session_send_rpc_evt_to_thread(s->thread_index, ...);
        /* → 올바른 워커로 handoff */
}

대칭 플로우 해시(Symmetrical Flow Hash)

symmetrical 옵션을 사용하면 소스와 목적지를 교환(src↔dst swap)해도 동일한 해시값이 나오도록 보장합니다. 이 기능은 NAT, IPsec 등에서 인바운드/아웃바운드 경로가 같은 워커에서 처리되도록 하는 데 필수적입니다.

/* src/vnet/ip/ip4_forward.c — 대칭 해시 구현 */
static inline u32
ip4_compute_flow_hash (const ip4_header_t *ip,
                        flow_hash_config_t cfg)
{
    u32 a, b, c;
    u32 src = clib_net_to_host_u32(ip->src_address.as_u32);
    u32 dst = clib_net_to_host_u32(ip->dst_address.as_u32);

    /* 대칭 해시: src/dst를 정렬하여 방향 무관하게 동일 해시 */
    if (cfg & IP_FLOW_HASH_SYMMETRIC) {
        if (src > dst) {
            u32 tmp = src; src = dst; dst = tmp;
        }
    }
    a = src; b = dst; c = ip->protocol;
    hash_v3_mix32(&a, &b, &c);
    return c;
}
# VPP CLI에서 대칭 해시 활성화
set ip flow-hash table 0 src dst sport dport proto symmetric

# 확인
show ip flow-hash table 0
# 출력: src dst sport dport proto symmetric

실전 설정 가이드

NIC 속도권장 큐 수권장 워커 수handoff 설정비고
1 GbE1–21–2불필요단일 큐로 충분
10 GbE2–42–4세션 앱 시 필요RSS 기본 분배
25 GbE4–84–8권장Flow Director 병용 권장
40 GbE4–84–8권장NUMA 교차 배치 주의
100 GbE8–168–16필수rte_flow + 대칭 해시 필수

문제 진단 CLI 명령

# NIC 큐 ↔ 워커 매핑 확인
vpp# show interface placement
Thread 1 (vpp_wk_0):
  GigabitEthernet3/0/0 queue 0
Thread 2 (vpp_wk_1):
  GigabitEthernet3/0/0 queue 1

# 핸드오프 통계 — 큐 불일치로 인한 핸드오프 빈도 확인
vpp# show handoff
  worker 0: 15234 handoffs
  worker 1: 12891 handoffs

# 세션별 워커 확인 — 세션이 어느 워커에 바인딩되었는지 확인
vpp# show session verbose
  [0:0] 10.0.0.1:80->10.0.0.2:54321 ESTABLISHED thread 1
  [0:1] 10.0.0.1:80->10.0.0.3:54322 ESTABLISHED thread 2

# DPDK 큐 배치 — DPDK 레벨 큐-코어 매핑
vpp# show dpdk interface placement
  GigabitEthernet3/0/0
    queue 0 -> core 2 (worker 0)
    queue 1 -> core 4 (worker 1)
    queue 2 -> core 6 (worker 2)
    queue 3 -> core 8 (worker 3)

DPDK 통합

VPP는 DPDK(Data Plane Development Kit)를 기본 패킷 I/O 백엔드로 사용합니다. DPDK는 커널의 네트워크 드라이버를 우회하여 NIC 하드웨어에 직접 접근하는 Poll Mode Driver(PMD)를 제공합니다.

DPDK 드라이버 바인딩

DPDK PMD를 사용하려면 NIC를 커널 드라이버에서 분리(unbind)하고 UIO 또는 VFIO 드라이버에 바인딩해야 합니다:

# 현재 NIC 드라이버 확인
$ lspci -k -s 0000:03:00.0
03:00.0 Ethernet controller: Intel Corporation 82599ES ...
    Kernel driver in use: ixgbe

# VFIO-PCI 드라이버로 바인딩 (IOMMU 필요)
$ modprobe vfio-pci
$ dpdk-devbind --bind=vfio-pci 0000:03:00.0

# 또는 UIO 드라이버 (IOMMU 불필요, 보안 취약)
$ modprobe uio_pci_generic
$ dpdk-devbind --bind=uio_pci_generic 0000:03:00.0

# 상태 확인
$ dpdk-devbind --status
커널 관점: VFIO(drivers/vfio/)는 IOMMU를 통한 디바이스 격리(Isolation)를 제공하여 DMA 공격을 방지합니다. UIO(drivers/uio/)는 IOMMU 없이 동작하지만, 잘못된 DMA 요청이 임의 메모리를 덮어쓸 수 있어 프로덕션에서는 VFIO를 권장합니다.

Hugepages 설정

DPDK와 VPP는 Hugepages를 사용하여 TLB 미스를 줄이고 메모리 접근 성능을 높입니다. 커널의 hugetlbfs(fs/hugetlbfs/)를 활용합니다:

# 부팅 시 1GB hugepage 할당 (GRUB)
GRUB_CMDLINE_LINUX="default_hugepagesz=1G hugepagesz=1G hugepages=4 iommu=pt intel_iommu=on"

# 런타임 2MB hugepage 할당
$ echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages

# hugetlbfs 마운트 (VPP가 자동으로 사용)
$ mount -t hugetlbfs none /dev/hugepages

# 할당 상태 확인
$ cat /proc/meminfo | grep -i huge
HugePages_Total:    1024
HugePages_Free:      512
Hugepagesize:       2048 kB

멀티큐와 RSS

최신 NIC은 여러 개의 RX/TX 큐를 제공하며, RSS(Receive Side Scaling)를 통해 패킷을 큐에 분산합니다. VPP는 각 워커 스레드에 큐를 할당하여 병렬 처리합니다:

/* startup.conf — DPDK 멀티큐 설정 */
dpdk {
    dev 0000:03:00.0 {
        num-rx-queues 4        /* RX 큐 4개 */
        num-tx-queues 4        /* TX 큐 4개 */
    }
}

cpu {
    main-core 0                /* 메인 스레드: 코어 0 */
    corelist-workers 1-3       /* 워커 스레드: 코어 1,2,3 */
}
dpdk-input 노드와 NIC RX 링 상호작용 NIC Hardware RX Descriptor Ring DMA Engine RSS Hash → Queue N Flow Director DMA Hugepage Buffers mbuf 0 mbuf 1 mbuf 2 mbuf 3 zero-copy DMA 대상 poll dpdk-input rte_eth_rx_burst() mbuf → vlib_buffer 변환 벡터 프레임 생성 Vector Frame [bi0, bi1, bi2, ..., biN] → ethernet-input 전달 NIC DMA → hugepage 버퍼 → dpdk-input 폴링 → vlib_buffer 변환 → 벡터 프레임 디스패치

DPDK Crypto 디바이스 연동

VPP는 DPDK의 Cryptodev API를 통해 하드웨어/소프트웨어 암호화(Encryption) 가속을 지원합니다. IPsec의 AES-GCM, ChaCha20-Poly1305 등의 암호화 연산을 전용 하드웨어(QAT, Mellanox ConnectX 등)로 오프로드하거나, 소프트웨어 라이브러리(IPSec-MB, OpenSSL)를 사용합니다.

/* startup.conf — Crypto 디바이스 설정 */
dpdk {
    dev 0000:03:00.0             /* 네트워크 NIC */
    dev 0000:0b:00.0 {           /* Intel QAT 디바이스 */
        num-rx-queues 2
    }
    uio-driver vfio-pci
}

/* VPP crypto 엔진 설정 */
vpp# set crypto handler aes-256-gcm ipsecmb
vpp# show crypto handlers
크립토 엔진유형지원 알고리즘성능 (1500B 기준)비고
nativeSW (VPP 내장)AES-GCM (AES-NI)~15 Gbps/코어x86 AES-NI 필수
ipsecmbSW (Intel)AES-CBC/GCM, SHA, ChaCha20~12 Gbps/코어Intel 최적화 라이브러리
opensslSW모든 OpenSSL 알고리즘~8 Gbps/코어범용, 호환성 우수
dpdk-cryptodevHW디바이스 의존~40+ GbpsQAT, Mellanox 등
HW vs SW 크립토: 소규모 패킷(64~256B)에서는 HW 오프로드의 DMA 전송 지연이 SW 처리보다 느릴 수 있습니다. show crypto engines로 현재 엔진별 처리량을 비교하고, show ipsec sa에서 SA별 바이트/패킷 카운터로 실제 성능을 측정하세요.

플러그인 시스템

VPP는 모듈식 플러그인 아키텍처를 채택하여 기능을 동적으로 확장할 수 있습니다. 각 플러그인은 공유 라이브러리(.so)로 빌드되며, VPP 시작 시 /usr/lib/vpp_plugins/에서 자동 로드됩니다.

플러그인 구조

VPP 플러그인의 기본 디렉터리 구조:

VPP 플러그인 디렉토리 구조 src/plugins/my_plugin/ CMakeLists.txt ← 빌드 설정 (플러그인 등록) my_plugin.h ← 공용 헤더 (구조체, 매크로) my_plugin.c ← 플러그인 초기화 (VLIB_INIT_FUNCTION) my_plugin.api ← Binary API 정의 (.api → .c/.h 자동 생성) node.c ← 그래프 노드 구현 (VLIB_REGISTER_NODE) cli.c ← CLI 커맨드 (VLIB_CLI_COMMAND) setup.pg ← 패킷 생성기 스크립트 (테스트용)

커스텀 플러그인 개발

/* my_plugin.c — 플러그인 초기화 */
#include <vnet/vnet.h>
#include <vlib/vlib.h>
#include <vpp/app/version.h>

typedef struct {
    u32 sw_if_index;
    u32 next_index;
} my_plugin_main_t;

my_plugin_main_t my_plugin_main;

static clib_error_t *
my_plugin_init (vlib_main_t *vm) {
    my_plugin_main_t *mpm = &my_plugin_main;
    mpm->sw_if_index = ~0;
    return 0;
}

VLIB_INIT_FUNCTION(my_plugin_init);

/* 플러그인 등록 매크로 */
VLIB_PLUGIN_REGISTER() = {
    .version = VPP_BUILD_VER,
    .description = "My Custom Plugin",
};
/* node.c — 커스텀 그래프 노드 */
#include <vlib/vlib.h>
#include <vnet/vnet.h>

typedef enum {
    MY_NODE_NEXT_DROP,
    MY_NODE_NEXT_IFACE_OUTPUT,
    MY_NODE_N_NEXT,
} my_node_next_t;

static uword
my_node_fn (vlib_main_t *vm, vlib_node_runtime_t *node,
            vlib_frame_t *frame) {
    u32 n_left_from, *from, *to_next;
    my_node_next_t next_index;

    from = vlib_frame_vector_args(frame);
    n_left_from = frame->n_vectors;
    next_index = node->cached_next_index;

    while (n_left_from > 0) {
        u32 n_left_to_next;
        vlib_get_next_frame(vm, node, next_index,
                           to_next, n_left_to_next);

        while (n_left_from > 0 && n_left_to_next > 0) {
            u32 bi0 = from[0];
            vlib_buffer_t *b0 = vlib_get_buffer(vm, bi0);
            u32 next0 = MY_NODE_NEXT_IFACE_OUTPUT;

            /* 패킷 처리 로직 */
            /* ... */

            to_next[0] = bi0;
            from += 1;
            to_next += 1;
            n_left_from -= 1;
            n_left_to_next -= 1;

            vlib_validate_buffer_enqueue_x1(vm, node, next_index,
                to_next, n_left_to_next, bi0, next0);
        }
        vlib_put_next_frame(vm, node, next_index, n_left_to_next);
    }
    return frame->n_vectors;
}

VLIB_REGISTER_NODE(my_node) = {
    .function = my_node_fn,
    .name = "my-custom-node",
    .vector_size = sizeof(u32),
    .type = VLIB_NODE_TYPE_INTERNAL,
    .n_next_nodes = MY_NODE_N_NEXT,
    .next_nodes = {
        [MY_NODE_NEXT_DROP] = "error-drop",
        [MY_NODE_NEXT_IFACE_OUTPUT] = "interface-output",
    },
};

커널 인터페이스

VPP는 유저스페이스에서 동작하지만, 커널 네트워크 스택과의 연동이 필수적인 경우가 많습니다. VPP는 다양한 커널 인터페이스를 통해 커널과 패킷을 교환합니다.

커널 네트워크 스택 vs VPP 데이터패스 커널 공간 (Kernel Space) 커널 네트워크 스택 NIC 드라이버 (ixgbe...) TAP/TUN vhost AF_PACKET AF_XDP UIO / VFIO (커널 bypass) 유저 공간 (User Space) VPP Process 그래프 노드 엔진 DPDK PMD 플러그인 Binary API CLI (vppctl) Shared Memory Physical NIC (10GbE / 25GbE / 100GbE)

TAP/TUN 인터페이스

TAP 인터페이스는 VPP와 커널 네트워크 스택 간 L2/L3 패킷을 교환하는 가장 일반적인 방법입니다. 커널의 drivers/net/tun.c가 구현합니다.

/* VPP CLI: TAP 인터페이스 생성 */
vpp# create tap id 0 host-if-name vpp-tap0 host-ip4-addr 192.168.1.1/24

/* 호스트에서 확인 */
$ ip addr show vpp-tap0
vpp-tap0: <BROADCAST,MULTICAST,UP> mtu 1500 ...
    inet 192.168.1.1/24 scope global vpp-tap0

/* VPP 측 IP 할당 */
vpp# set interface ip address tap0 192.168.1.2/24
vpp# set interface state tap0 up
virtio 기반 TAP: VPP의 TAP은 내부적으로 virtio 링을 사용합니다. /dev/net/tun + IFF_VNET_HDR 플래그로 virtio 헤더를 포함하여 체크섬(Checksum) 오프로드와 GSO를 지원합니다. 이는 전통적인 TAP보다 성능이 우수합니다.

vhost-user / virtio

vhost-user는 VM과 VPP 간 고성능 패킷 교환을 위한 메커니즘입니다. 공유 메모리와 virtio 링을 사용하여 데이터 복사 없이 패킷을 전달합니다. 커널의 drivers/vhost/ 서브시스템과 유사한 개념이지만, VPP는 유저스페이스에서 vhost 백엔드를 직접 구현합니다.

/* VPP: vhost-user 인터페이스 생성 (서버 모드) */
vpp# create vhost-user socket /var/run/vpp/sock0.sock server
vpp# set interface state VirtualEthernet0/0/0 up

/* QEMU VM 연결 */
$ qemu-system-x86_64 \
    -chardev socket,id=char0,path=/var/run/vpp/sock0.sock \
    -netdev vhost-user,id=net0,chardev=char0 \
    -device virtio-net-pci,netdev=net0

AF_PACKET

AF_PACKET은 커널의 기존 네트워크 인터페이스를 통해 raw 패킷을 송수신하는 소켓 인터페이스입니다(net/packet/af_packet.c). DPDK처럼 NIC를 독점하지 않으므로 기존 커널 스택과 공존 가능합니다.

/* VPP: AF_PACKET 인터페이스 (커널의 eth1에 연결) */
vpp# create host-interface name eth1
vpp# set interface state host-eth1 up
vpp# set interface ip address host-eth1 10.0.0.1/24
용도: AF_PACKET은 DPDK보다 성능이 낮지만(커널 경유), NIC를 커널에서 분리할 필요가 없어 개발/테스트 환경에 적합합니다. PACKET_MMAP 링 버퍼(Ring Buffer)를 사용하여 시스템 콜(System Call) 오버헤드를 줄입니다.

VPP의 linux-cp(Linux Control Plane) 플러그인은 Netlink를 통해 커널 라우팅 테이블(Routing Table), ARP 엔트리, 인터페이스 상태를 VPP와 동기화합니다. 이를 통해 커널의 ip route, ip neigh 등의 명령이 VPP에도 반영됩니다.

/* linux-cp 플러그인 활성화 (startup.conf) */
plugins {
    plugin linux_cp_plugin.so { enable }
    plugin linux_cp_unittest_plugin.so { enable }
}

linux-cp {
    default netns dataplane
}

/* VPP CLI: 리눅스 인터페이스 미러링 */
vpp# lcp create tap0 host-if lcp-tap0

host-interface (AF_XDP)

AF_XDP(net/xdp/xsk.c)는 커널 5.4+에서 사용 가능한 고성능 패킷 I/O 인터페이스입니다. eBPF/XDP 프로그램과 연계하여 커널 네트워크 스택을 우회하면서도 NIC 드라이버를 교체할 필요가 없습니다.

/* VPP: AF_XDP 인터페이스 생성 */
vpp# create interface af_xdp host-if eth0 num-rx-queues 4
vpp# set interface state af_xdp-eth0 up
DPDK vs AF_XDP: DPDK는 NIC를 커널에서 완전히 분리(UIO/VFIO)하여 최고 성능을 제공하지만 커널 스택과 공존이 불가합니다. AF_XDP는 NIC 드라이버를 유지하면서 XDP 후킹으로 VPP에 패킷을 전달하므로, 일부 트래픽은 커널 스택이 처리하고 특정 트래픽만 VPP로 보낼 수 있습니다.

주요 기능

L2 브릿징/스위칭

VPP는 커널의 net/bridge/와 유사한 L2 브릿지 도메인을 제공합니다. MAC 학습, 플러딩, BUM(Broadcast/Unknown unicast/Multicast) 트래픽 처리를 지원합니다.

브릿지 도메인 내부적으로 VPP는 해시 기반 MAC 학습 테이블을 유지하며, 각 엔트리는 MAC 주소와 수신 인터페이스(sw_if_index)의 매핑으로 구성됩니다. 학습된 MAC 엔트리에는 에이징 타이머(aging timer)가 적용되어 기본 300초 동안 트래픽이 없으면 자동으로 삭제됩니다. 목적지 MAC이 학습 테이블에 없는 유니캐스트(Unknown Unicast), 브로드캐스트, 멀티캐스트를 통칭하는 BUM 트래픽은 브릿지 도메인 내 모든 멤버 포트로 플러딩(flooding)됩니다. 이때 스플릿 호라이즌 그룹(Split-Horizon Group)을 설정하면 동일 그룹에 속한 포트 간에는 플러딩이 억제되어 루프 방지 및 불필요한 트래픽 전파를 차단할 수 있습니다. BVI(Bridge Virtual Interface)는 브릿지 도메인에 L3 라우팅 기능을 연결하는 가상 인터페이스로, BVI에 IP 주소를 할당하면 해당 브릿지 도메인의 트래픽이 VPP의 라우팅 테이블로 전달됩니다. 이를 통해 동일 브릿지 도메인 내에서 L2 스위칭과 L3 라우팅을 동시에 수행하는 IRB(Integrated Routing and Bridging) 구성이 가능합니다.

브릿지 도메인 (Bridge Domain 1) GigE0/8/0 (포트1) GigE0/9/0 (포트2) tap0 (포트3) BVI (L3 Gateway) IP: 192.168.1.1/24 MAC 학습 테이블 (Hash Table) MAC 주소 포트 aa:bb:cc:00:01:01 GigE0/8/0 aa:bb:cc:00:02:02 GigE0/9/0 aa:bb:cc:00:03:03 tap0 에이징 타이머 기본값: 300초 만료 시 엔트리 자동 삭제 BUM 트래픽 플러딩 경로 Broadcast → 모든 멤버 포트로 복제 전송 Unknown Unicast → MAC 미학습 시 플러딩 Multicast → IGMP 스누핑 미적용 시 플러딩 Split-Horizon 같은 그룹 간 플러딩 억제 L3 라우팅 테이블 IRB: L2 스위칭 + L3 라우팅 동시 수행 ip4-lookup 노드 연결
/* 브릿지 도메인 생성 및 인터페이스 추가 */
vpp# create bridge-domain 1 learn 1 forward 1 flood 1
vpp# set interface l2 bridge GigabitEthernet0/8/0 1
vpp# set interface l2 bridge GigabitEthernet0/9/0 1
vpp# set interface l2 bridge tap0 1 bvi
vpp# show bridge-domain 1 detail

L3 라우팅 (FIB)

VPP의 FIB(Forwarding Information Base)는 mtrie(multi-way trie) 구조를 사용하여 O(1) 시간에 longest-prefix match를 수행합니다. 8-16-8 stride 구조로 최대 3회의 메모리 접근으로 lookup이 완료됩니다.

FIB mtrie 룩업 흐름 (8-16-8 stride) Dest IP 10.1.2.3 Ply 0 8비트 (256 엔트리) index = 10 Ply 1 16비트 (65536 엔트리) index = 1.2 (0x0102) Ply 2 8비트 (256 엔트리) index = 3 Load Balance → Adjacency → ip4-rewrite mtrie 경로: 첫 8비트 → 다음 16비트 → 마지막 8비트 = 최대 3회 메모리 접근 결과: load-balance 객체 → adjacency → ip4-rewrite 노드에서 MAC 헤더 재작성 FIB 엔트리 유형: attached | attached-host | connected | local | drop | receive | special | deag (재귀 룩업) ECMP: load-balance 객체가 여러 adjacency를 해시 기반으로 분산
/* IP 라우팅 설정 */
vpp# ip route add 10.0.0.0/8 via 192.168.1.1
vpp# ip route add 0.0.0.0/0 via 192.168.1.1

/* ECMP (Equal-Cost Multi-Path) */
vpp# ip route add 10.0.0.0/8 via 192.168.1.1 via 192.168.2.1

/* FIB 테이블 확인 */
vpp# show ip fib
vpp# show ip fib table 0 summary

ACL

VPP의 ACL 플러그인(acl_plugin.so)은 커널의 Netfilter/nftables에 대응하는 고성능 패킷 필터링 엔진입니다. 이 플러그인은 두 가지 동작 모드를 제공합니다. Stateless 모드는 각 패킷을 독립적으로 5-tuple(소스 IP, 목적지 IP, 프로토콜, 소스 포트, 목적지 포트) 기반으로 규칙 테이블과 매칭하여 permit 또는 deny 결정을 내립니다. Stateful 모드는 세션 기반 연결 추적(Connection Tracking)을 수행하며, 첫 번째 패킷이 규칙에 매칭되면 해당 연결의 세션 항목을 세션 테이블에 생성합니다. 이후 동일 연결에 속하는 패킷은 규칙 테이블을 다시 순회하지 않고 세션 테이블에서 직접 조회하여 빠르게 처리합니다.

Reflexive ACL은 stateful 모드의 핵심 기능으로, 아웃바운드 트래픽에 대한 허용 규칙이 존재하면 그에 대응하는 인바운드 응답 트래픽을 자동으로 허용합니다. 이를 통해 명시적인 양방향 규칙 없이도 양방향 통신이 가능합니다. 플러그인은 acl-plugin-in-ip4-faacl-plugin-out-ip4-fa 피처 노드를 통해 인터페이스의 입력(input) 및 출력(output) 경로에 각각 삽입되며, 패킷 그래프 내에서 인라인으로 실행됩니다.

패킷 수신 5-tuple 추출 세션 테이블 조회 PERMIT 세션 히트 규칙 테이블 순차 매칭 미스 permit DENY deny REFLECT (세션 생성) reflect acl-plugin-in-ip4-fa ACL 규칙 #0, #1, ... #N 세션 테이블에 역방향 항목 추가
/* ACL 규칙 생성 */
vpp# set acl-plugin acl permit src 192.168.1.0/24 dst 10.0.0.0/8 \
     proto 6 sport 1024-65535 dport 80

/* 인터페이스에 적용 */
vpp# set acl-plugin interface GigabitEthernet0/8/0 input acl 0
vpp# show acl-plugin acl

NAT44/NAT64

NAT44 in2out / out2in 패킷 흐름 내부 (in) 192.168.1.0/24 in2out NAT44 Endpoint-Dependent 세션 테이블 (5-tuple → 변환 규칙) 주소 풀 관리 | 포트 할당 | 세션 타이머 외부 (out) 203.0.113.1 out2in in2out: src 192.168.1.100:45000 → src 203.0.113.1:10001 | out2in: dst 203.0.113.1:10001 → dst 192.168.1.100:45000 ED(Endpoint-Dependent) 모드: 전체 5-tuple 기반 세션 매칭 — NAT44의 권장 모드 Simple 모드(레거시): 3-tuple(src-ip, src-port, proto) — 더 이상 권장하지 않음
ED mode vs Simple mode: VPP 21.06+에서 NAT44은 Endpoint-Dependent (ED) 모드가 기본입니다. ED 모드는 전체 5-tuple(src/dst IP, src/dst port, proto)로 세션을 식별하여 동일 src-ip:port가 다른 목적지로 갈 때도 올바르게 변환합니다. 레거시 simple 모드는 포트 충돌 문제가 있으므로 신규 배포에서 사용하지 마세요.
/* NAT44: 내부 → 외부 주소 변환 */
vpp# nat44 add interface address GigabitEthernet0/8/0
vpp# set interface nat44 in GigabitEthernet0/9/0 out GigabitEthernet0/8/0

/* 정적 매핑 (DNAT) */
vpp# nat44 add static mapping local 192.168.1.100 22 \
     external GigabitEthernet0/8/0 2222 tcp

/* 세션 확인 */
vpp# show nat44 sessions

IPsec

VPP의 IPsec 구현은 크게 ESP 터널 모드(Tunnel Mode)전송 모드(Transport Mode) 두 가지를 지원합니다. 터널 모드에서는 원본 IP 패킷 전체를 새로운 IP 헤더로 감싸서 암호화하며, 전송 모드에서는 원본 IP 헤더를 유지한 채 페이로드만 암호화합니다. IPsec의 핵심 데이터 구조는 SA(Security Association)SPD(Security Policy Database)입니다. SA는 암호화 알고리즘, 키, SPI(Security Parameter Index), 터널 엔드포인트 등 보안 매개변수를 정의하며, SPD는 트래픽 셀렉터(소스/목적지 IP 범위, 포트, 프로토콜)를 기반으로 어떤 SA를 적용할지 또는 패킷을 통과/차단할지 결정하는 정책을 담고 있습니다.

아웃바운드 처리에서는 평문 패킷이 SPD 룩업을 거쳐 매칭되는 정책의 SA를 선택하고, ESP 캡슐화(헤더/트레일러 추가) 후 암호화를 수행한 뒤 ip4-rewrite 노드를 통해 전송합니다. 인바운드 처리에서는 수신된 암호화 패킷의 SPI를 기반으로 SA를 조회하여 복호화하고, ESP 디캡슐화를 수행한 뒤 SPD 정책 검증을 통과하면 내부 네트워크로 포워딩합니다.

VPP는 크립토 엔진 선택 프레임워크를 제공하여, 동일한 IPsec 파이프라인에서 소프트웨어 구현(native, OpenSSL, ipsecmb)과 하드웨어 가속(DPDK Cryptodev, Intel QAT)을 플러그인 방식으로 교체할 수 있습니다. 또한 비동기 크립토 프레임워크(Async Crypto Framework)를 통해 암호화/복호화 작업을 별도의 워커 스레드나 하드웨어 큐에 오프로드하여 메인 패킷 처리 루프의 지연을 최소화합니다. DPDK Cryptodev 통합 시에는 dpdk_cryptodev 플러그인을 로드하고, set crypto handler CLI로 엔진 우선순위를 지정하여 특정 알고리즘에 대해 하드웨어 가속을 활성화할 수 있습니다.

IPsec 패킷 처리 흐름 아웃바운드 (Outbound) 평문 패킷 Plain Packet SPD 룩업 Policy Match SA 선택 SA Lookup ESP 캡슐화 Encapsulate 암호화 Encrypt ip4-rewrite 전송 인바운드 (Inbound) 암호화 패킷 Encrypted SA 조회 SPI → SA 복호화 Decrypt ESP 디캡슐화 Decapsulate SPD 검증 Policy Check 포워딩 Forward 크립토 엔진 (Crypto Engine) native (SW) ipsecmb (SW) OpenSSL (SW) DPDK Cryptodev Intel QAT (HW)
/* IPsec 터널 모드 설정 */
vpp# ipsec sa add 10 spi 1001 esp crypto-alg aes-gcm-256 \
     crypto-key 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
     tunnel src 10.0.0.1 dst 10.0.0.2

vpp# ipsec sa add 20 spi 1002 esp crypto-alg aes-gcm-256 \
     crypto-key fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210 \
     tunnel src 10.0.0.2 dst 10.0.0.1

vpp# ipsec policy add spd 1 priority 100 outbound action protect \
     sa 10 local-ip-range 192.168.1.0 - 192.168.1.255 \
     remote-ip-range 192.168.2.0 - 192.168.2.255

IPsec Policy 기반 vs Route 기반

VPP는 IPsec 트래픽 선택 방식으로 Policy 기반(정책 기반)Route 기반(라우팅 기반) 두 가지를 모두 지원합니다. 전통적인 IPsec 구현은 Policy 기반이 기본이지만, 대규모 네트워크에서는 Route 기반이 유연성과 확장성 면에서 유리합니다.

Policy-based IPsec (정책 기반)

정책 기반 IPsec은 SPD(Security Policy Database)에 등록된 정책 규칙으로 트래픽을 선택합니다. 각 정책은 5-tuple(출발지 IP, 목적지 IP, 프로토콜, 출발지 포트, 목적지 포트) 매칭 조건을 가지며, 매칭된 패킷에 대해 PROTECT(암호화), BYPASS(통과), DISCARD(폐기) 중 하나의 동작을 수행합니다.

/* SPD(Security Policy Database) 구조체 - src/vnet/ipsec/ipsec_spd.h */
typedef struct {
  u32 id;                          /* SPD 식별자 */
  u32 *policies[IPSEC_SPD_POLICY_N_TYPES]; /* 정책 타입별 인덱스 배열 */
  /* IPSEC_SPD_POLICY_IP4_OUTBOUND, IPSEC_SPD_POLICY_IP4_INBOUND 등 */
} ipsec_spd_t;

/* 개별 정책 항목 - src/vnet/ipsec/ipsec_spd_policy.h */
typedef struct {
  u32 id;                          /* 정책 ID */
  i32 priority;                    /* 우선순위 (높을수록 먼저 매칭) */

  /* 5-tuple 매칭 조건 */
  ip46_address_t laddr_start;      /* 로컬 IP 범위 시작 */
  ip46_address_t laddr_stop;       /* 로컬 IP 범위 끝 */
  ip46_address_t raddr_start;      /* 원격 IP 범위 시작 */
  ip46_address_t raddr_stop;       /* 원격 IP 범위 끝 */
  u16 lport_start, lport_stop;     /* 로컬 포트 범위 */
  u16 rport_start, rport_stop;     /* 원격 포트 범위 */
  u8 protocol;                     /* IP 프로토콜 번호 */

  ipsec_policy_action_t policy;    /* PROTECT | BYPASS | DISCARD */
  u32 sa_id;                       /* PROTECT 시 사용할 SA 식별자 */
} ipsec_policy_t;

Outbound 패킷 처리 시 ipsec-output-ip4 노드가 SPD를 검색하여 매칭되는 정책을 찾습니다. 정책 검색은 우선순위가 높은 항목부터 순차적으로 5-tuple 비교를 수행하며, 첫 번째 매칭 정책의 동작을 적용합니다. Inbound에서는 복호화 후 ipsec-input-ip4 노드가 SPD inbound 정책과 대조하여 허용 여부를 판정합니다.

/* SPD 정책 설정 CLI 예시 */
/* 인터페이스에 SPD 바인딩 */
vpp# ipsec spd add 1
vpp# set interface ipsec spd GigabitEthernet0/8/0 1

/* outbound 정책: 192.168.1.0/24 → 192.168.2.0/24 트래픽 보호 */
vpp# ipsec policy add spd 1 priority 100 outbound action protect \
     sa 10 local-ip-range 192.168.1.0 - 192.168.1.255 \
     remote-ip-range 192.168.2.0 - 192.168.2.255

/* inbound 정책: 반대 방향 트래픽 허용 */
vpp# ipsec policy add spd 1 priority 100 inbound action protect \
     sa 20 local-ip-range 192.168.2.0 - 192.168.2.255 \
     remote-ip-range 192.168.1.0 - 192.168.1.255

Route-based IPsec (라우팅 기반)

라우팅 기반 IPsec은 create ipsec tunnel CLI로 가상 터널 인터페이스(ipsecN)를 생성하고, FIB 라우팅 테이블의 경로 설정을 통해 트래픽을 선택합니다. 이 방식에서는 SPD 정책 대신 일반 IP 라우팅이 트래픽을 IPsec 터널로 전달하므로, BGP나 OSPF 같은 동적 라우팅 프로토콜과 자연스럽게 통합됩니다.

/* IPsec 터널 인터페이스 구조체 - src/vnet/ipsec/ipsec_tun.h */
typedef struct {
  u32 input_sa_id;                 /* inbound SA 식별자 */
  u32 output_sa_id;                /* outbound SA 식별자 */
  u32 hw_if_index;                 /* 하드웨어 인터페이스 인덱스 */
  u32 sw_if_index;                 /* 소프트웨어 인터페이스 인덱스 */
  ip46_address_t tunnel_src;       /* 터널 출발지 IP */
  ip46_address_t tunnel_dst;       /* 터널 목적지 IP */
} ipsec_tunnel_if_t;
/* Route 기반 IPsec 설정 CLI 예시 */
/* IPsec 터널 인터페이스 생성 (SA 자동 연결) */
vpp# create ipsec tunnel local-ip 10.0.0.1 remote-ip 10.0.0.2 \
     local-spi 1001 remote-spi 1002 \
     local-crypto-key 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
     remote-crypto-key fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210 \
     crypto-alg aes-gcm-256

/* 터널 인터페이스 활성화 */
vpp# set interface state ipsec0 up
vpp# set interface ip address ipsec0 172.16.0.1/30

/* FIB 라우팅으로 트래픽 선택 (SPD 불필요) */
vpp# ip route add 192.168.2.0/24 via 172.16.0.2 ipsec0

/* 동적 라우팅 프로토콜과 통합 가능 */
/* BGP neighbor가 ipsec0 인터페이스를 통해 경로를 교환 */

Policy vs Route 방식 비교

항목Policy 기반Route 기반
트래픽 선택SPD 5-tuple 매칭FIB 라우팅 테이블
인터페이스물리 인터페이스에 SPD 바인딩가상 ipsecN 터널 인터페이스
동적 라우팅직접 통합 어려움BGP, OSPF 등 자연스러운 통합
설정 복잡도트래픽 패턴별 개별 정책 필요라우팅 테이블로 일괄 관리
확장성정책 수 증가 시 검색 비용 증가FIB longest-prefix match로 O(log N)
다중 VRFSPD별 별도 관리인터페이스별 VRF 할당으로 자연스러운 분리
적용 사례소규모, 고정 터널대규모 SD-WAN, 멀티사이트 VPN
VPP CLIipsec spd add + ipsec policy addcreate ipsec tunnel + ip route add

ipsec_sa_t 구조체와 ESP 노드 그래프

VPP의 IPsec 구현에서 가장 핵심적인 데이터 구조는 ipsec_sa_t(Security Association)입니다. 이 구조체는 암호화 알고리즘, 키, 터널 엔드포인트, 시퀀스 번호 등 ESP 처리에 필요한 모든 상태를 담고 있습니다.

ipsec_sa_t 핵심 필드 분석

/* Security Association 핵심 구조체 - src/vnet/ipsec/ipsec_sa.h */
typedef struct {
  u32 id;                          /* SA 식별자 */
  u32 spi;                         /* Security Parameter Index (네트워크 바이트 순서) */
  u32 stat_index;                  /* 통계 카운터 인덱스 */

  /* 암호화 설정 */
  ipsec_crypto_alg_t crypto_alg;   /* AES-CBC-128/256, AES-GCM-128/256 등 */
  ipsec_key_t crypto_key;          /* 암호화 키 (최대 32바이트) */
  u8 crypto_iv_size;               /* 초기화 벡터 크기 */
  u8 crypto_block_size;            /* 블록 암호 크기 */

  /* 무결성 검증 설정 (AES-GCM은 AEAD이므로 별도 불필요) */
  ipsec_integ_alg_t integ_alg;     /* SHA1-96, SHA-256-128 등 */
  ipsec_key_t integ_key;           /* 무결성 키 */
  u8 integ_icv_size;               /* ICV(Integrity Check Value) 크기 */

  /* 터널 모드 엔드포인트 */
  ip46_address_t tunnel_src_addr;  /* 터널 출발지 IP */
  ip46_address_t tunnel_dst_addr;  /* 터널 목적지 IP */

  /* 시퀀스 번호 (ESN: Extended Sequence Number 지원) */
  u32 seq;                         /* 하위 32비트 시퀀스 번호 */
  u32 seq_hi;                      /* 상위 32비트 (ESN 활성 시) */
  u64 last_seq;                    /* 수신 측 마지막 시퀀스 번호 */
  u64 replay_window;               /* anti-replay 윈도우 비트맵 (64비트) */

  /* 플래그 (동작 모드 제어) */
  ipsec_sa_flags_t flags;
#define IPSEC_SA_FLAG_IS_TUNNEL       (1 << 0)  /* 터널 모드 */
#define IPSEC_SA_FLAG_USE_ESN         (1 << 1)  /* 확장 시퀀스 번호 */
#define IPSEC_SA_FLAG_USE_ANTI_REPLAY (1 << 2)  /* anti-replay 검사 */
#define IPSEC_SA_FLAG_UDP_ENCAP       (1 << 4)  /* NAT-T UDP 캡슐화 */
#define IPSEC_SA_FLAG_IS_INBOUND      (1 << 6)  /* inbound SA */

  /* 암호 엔진 연동 */
  vnet_crypto_op_id_t crypto_enc_op_id;  /* 암호화 연산 ID */
  vnet_crypto_op_id_t crypto_dec_op_id;  /* 복호화 연산 ID */
  vnet_crypto_op_id_t integ_op_id;       /* 무결성 연산 ID */
  vnet_crypto_key_index_t crypto_key_index; /* 키 인덱스 */
  vnet_crypto_key_index_t integ_key_index;  /* 무결성 키 인덱스 */
} ipsec_sa_t;

Anti-replay 메커니즘은 재전송 공격을 방어하기 위한 핵심 기능입니다. 수신 측은 64비트 슬라이딩 윈도우 비트맵(replay_window)을 유지하며, 이미 수신한 시퀀스 번호의 패킷을 탐지하여 폐기합니다. last_seq는 윈도우의 우측 경계(가장 최근 수신된 시퀀스 번호)를 나타내며, 윈도우 범위 밖의 오래된 패킷도 폐기됩니다. ESN(Extended Sequence Number) 사용 시 64비트 시퀀스 공간을 활용하여 고속 링크에서의 시퀀스 번호 고갈 문제를 해결합니다.

ESP 그래프 노드 상세

VPP의 ESP 처리는 그래프 노드 체인을 통해 이루어집니다. Outbound(송신)와 Inbound(수신) 경로는 각각 별도의 노드 체인을 구성하며, 각 노드가 ESP 프로토콜의 특정 단계를 담당합니다.

ESP 노드 그래프: Outbound / Inbound 처리 경로 Outbound (송신) 경로 ip4-output ipsec-output-ip4 esp4-encrypt ip4-rewrite interface-output esp4-encrypt 내부 처리 단계 1. SA 검색 SPD/터널 → ipsec_sa_t 2. 헤더 구성 SPI + 시퀀스 번호 삽입 IV(초기화 벡터) 생성 3. 패딩 + 암호화 블록 크기 정렬 패딩 vnet_crypto 암호화 호출 4. ICV 계산 HMAC / GCM 태그 생성 ESP 트레일러에 부착 5. 터널 캡슐화 외부 IP 헤더 추가 (터널 모드 시) Inbound (수신) 경로 ip4-local ipsec-if-input / esp4-decrypt esp4-decrypt ip4-input ip4-lookup esp4-decrypt 내부 처리 단계 1. SA 검색 SPI → ipsec_sa_t 해시 검색 2. Anti-replay 검사 시퀀스 번호 윈도우 확인 중복/만료 패킷 폐기 3. ICV 검증 HMAC / GCM 태그 검증 불일치 시 패킷 폐기 4. 복호화 vnet_crypto 복호화 호출 패딩 제거 + 검증 5. 디캡슐화 외부 IP 헤더 제거 내부 패킷 추출 범례 ESP 핵심 처리 SA/터널 관리 암호화/검증 헤더/보안 검사

Outbound 경로에서 패킷은 ip4-output에서 시작하여 ipsec-output-ip4 노드가 SPD 정책 또는 터널 인터페이스를 통해 암호화 대상 여부를 판별합니다. 대상 패킷은 esp4-encrypt 노드로 전달되어 ESP 헤더 생성, IV 삽입, 패딩, 암호화, ICV 계산을 수행한 후 ip4-rewrite를 거쳐 물리 인터페이스로 전송됩니다.

Inbound 경로에서는 ip4-local 노드가 ESP 프로토콜(IP 프로토콜 번호 50)을 감지하면 esp4-decrypt 노드로 전달합니다. Route 기반 IPsec의 경우 ipsec-if-input 노드가 터널 인터페이스별 SA를 먼저 매칭합니다. 복호화 노드에서는 SPI로 SA를 검색하고, anti-replay 검사, ICV 검증, 복호화, 패딩 제거, 디캡슐화를 순차적으로 수행한 후 내부 패킷을 ip4-input으로 재주입합니다.

esp4-encrypt 처리 의사 코드

/* esp4-encrypt 노드 벡터 처리 의사 코드 */
static uword
esp_encrypt_inline (vlib_main_t *vm, vlib_node_runtime_t *node,
                    vlib_frame_t *frame, int is_ip6)
{
  u32 n_left = frame->n_vectors;
  vlib_buffer_t **b = bufs;

  while (n_left > 0)
  {
    ipsec_sa_t *sa = ipsec_sa_get (sa_index);

    /* 1단계: 시퀀스 번호 할당 및 증가 */
    u32 seq = clib_atomic_fetch_add (&sa->seq, 1);
    u32 seq_hi = sa->seq_hi; /* ESN 상위 32비트 */

    /* 2단계: 패딩 크기 계산 (블록 크기 정렬) */
    u8 pad_len = (sa->crypto_block_size -
                  (payload_len + 2) % sa->crypto_block_size)
                 % sa->crypto_block_size;

    /* 3단계: ESP 헤더 삽입 */
    esp_header_t *esp = (esp_header_t *) payload_start;
    esp->spi = sa->spi;       /* 네트워크 바이트 순서 */
    esp->seq = clib_host_to_net_u32 (seq);

    /* 4단계: IV 생성 및 삽입 */
    u8 *iv = (u8 *)(esp + 1);
    if (sa->crypto_alg == IPSEC_CRYPTO_ALG_AES_GCM_256)
      *(u64 *)iv = clib_host_to_net_u64 (sa->gcm_iv_counter++);

    /* 5단계: ESP 트레일러 구성 (패딩 + pad_length + next_header) */
    u8 *padding = payload_end;
    for (int i = 0; i < pad_len; i++)
      padding[i] = i + 1;     /* RFC 4303 패딩 패턴 */
    padding[pad_len] = pad_len;
    padding[pad_len + 1] = next_hdr; /* 원본 프로토콜 번호 */

    /* 6단계: vnet_crypto를 통한 암호화 연산 큐잉 */
    vnet_crypto_op_t *op = crypto_ops + n_ops++;
    op->op = sa->crypto_enc_op_id;
    op->src = op->dst = payload;
    op->len = payload_len + pad_len + 2;
    op->key_index = sa->crypto_key_index;
    op->iv = iv;

    /* AEAD(AES-GCM): AAD = ESP 헤더, 태그 = ICV */
    if (is_aead) {
      op->aad = (u8 *)esp;
      op->aad_len = 8; /* SPI(4) + Seq(4) */
      op->tag = payload_end + pad_len + 2;
      op->tag_len = sa->integ_icv_size;
    }

    /* 7단계: 터널 모드 시 외부 IP 헤더 추가 */
    if (sa->flags & IPSEC_SA_FLAG_IS_TUNNEL) {
      ip4_header_t *oh = vlib_buffer_push_ip4 (vm, b[0],
          &sa->tunnel_src_addr.ip4,
          &sa->tunnel_dst_addr.ip4,
          IP_PROTOCOL_IPSEC_ESP, 1);
    }

    n_left--;
    b++;
  }

  /* 배치 암호화 실행 (DPDK cryptodev 또는 OpenSSL) */
  vnet_crypto_process_ops (vm, crypto_ops, n_ops);
}

위 의사 코드에서 핵심적인 부분은 vnet_crypto_process_ops() 호출입니다. VPP는 개별 패킷마다 암호화를 수행하지 않고, 벡터의 모든 패킷에 대한 암호화 연산을 배치로 모아 한 번에 실행합니다. 이 방식은 CPU 캐시 효율성을 높이고, 하드웨어 가속기(Intel QAT 등)의 배치 처리 성능을 최대로 활용합니다. AES-GCM과 같은 AEAD 알고리즘 사용 시 암호화와 무결성 검증이 단일 연산으로 수행되어 별도의 HMAC 계산이 불필요합니다.

IKEv2 플러그인 내부 구현

VPP의 IKEv2 플러그인(src/plugins/ikev2/)은 IKEv2(RFC 7296) 프로토콜을 사용자 공간에서 직접 구현합니다. 이 플러그인은 IPsec SA를 동적으로 생성·갱신·삭제하는 키 관리 데몬 역할을 수행하며, VPP의 그래프 노드 구조에 통합되어 IKE 패킷을 벡터 처리 파이프라인에서 직접 수신합니다.

IKE SA 상태 머신은 다음과 같은 전이를 따릅니다. 초기 상태에서 IKE_SA_INIT 교환을 통해 Diffie-Hellman 공유 비밀을 수립하고, IKE_AUTH 교환으로 상호 인증을 완료하면 ESTABLISHED 상태에 진입합니다. 이후 SA의 수명이 만료에 가까워지면 REKEYING 상태로 전환하여 새로운 키를 협상하고, SA 삭제 시 DELETED 상태로 전이합니다.

ikev2_sa_t 구조체는 IKE SA의 전체 상태를 관리합니다. 핵심 필드로는 양측의 식별자(i_id, r_id), DH 교환에 사용된 난스 값(i_nonce, r_nonce), 도출된 키 집합(keys), 그리고 이 IKE SA 하에서 생성된 Child SA 배열(child_sa[])이 포함됩니다.

/* IKEv2 SA 구조체 핵심 필드 (src/plugins/ikev2/ikev2_priv.h 기반) */
typedef struct {
  /* SA 식별 */
  u64 ispi;                        /* Initiator SPI (8바이트) */
  u64 rspi;                        /* Responder SPI (8바이트) */
  ikev2_state_t state;             /* SA 상태 머신 현재 상태 */

  /* 신원 식별자 */
  ikev2_id_t i_id;                 /* Initiator 식별자 (FQDN, IP, 이메일 등) */
  ikev2_id_t r_id;                 /* Responder 식별자 */

  /* 난스(Nonce) — 키 도출의 신선도 보장 */
  u8 *i_nonce;                     /* Initiator Nonce (최소 16, 최대 256바이트) */
  u8 *r_nonce;                     /* Responder Nonce */

  /* Diffie-Hellman 교환 */
  ikev2_dh_group_t dh_group;       /* 협상된 DH 그룹 (14, 19, 20 등) */
  u8 *dh_shared_key;               /* DH 공유 비밀 */
  u8 *dh_private_key;              /* DH 개인 키 (Initiator만 보유) */

  /* 도출된 키 집합 */
  ikev2_sa_keys_t keys;            /* SK_d, SK_ai/ar, SK_ei/er, SK_pi/pr */

  /* 암호 스위트 */
  ikev2_sa_transform_t *transforms; /* 협상된 ENCR, PRF, INTEG, DH */

  /* Child SA (IPsec SA) 배열 */
  ikev2_child_sa_t *childs;        /* vec — 이 IKE SA 하의 모든 Child SA */

  /* 수명 관리 */
  f64 time_to_expiration;          /* SA 만료까지 남은 시간 */
  f64 last_sa_init_req_packet_data; /* 재전송 타이머 */
  u8 initial_contact;              /* INITIAL_CONTACT 알림 수신 여부 */
} ikev2_sa_t;

키 도출 과정은 RFC 7296 Section 2.14에 정의된 절차를 따릅니다. IKE_SA_INIT 교환에서 수립된 DH 공유 비밀과 양측 난스를 사용하여 SKEYSEED를 계산한 뒤, PRF+(Pseudo-Random Function Plus) 확장으로 7개의 세션 키를 도출합니다.

Child SA 생성 시에는 SK_d와 새로운 난스를 사용하여 IPsec SA용 암호화/무결성 키를 별도로 도출합니다. 이 분리 구조 덕분에 IKE SA를 유지한 채 Child SA만 독립적으로 리키잉할 수 있습니다.

/* IKE_SA_INIT 응답 처리 — Responder 측 (간략화된 의사 코드) */
static int
ikev2_process_sa_init_resp (ikev2_sa_t *sa,
                            ike_header_t *ike,
                            u8 *sa_payload,
                            u8 *ke_payload,
                            u8 *nonce_payload)
{
  /* 1단계: 응답 페이로드 파싱 */
  sa->rspi = ike->rspi;
  sa->r_nonce = ikev2_parse_nonce (nonce_payload);
  sa->dh_shared_key = ikev2_calc_dh_shared_key (
      sa->dh_group, ke_payload, sa->dh_private_key);

  /* 2단계: SKEYSEED 도출 */
  u8 *nonce_concat = vec_concat (sa->i_nonce, sa->r_nonce);
  u8 *skeyseed = ikev2_calc_prf (
      nonce_concat, sa->dh_shared_key);  /* PRF(Ni|Nr, g^ir) */

  /* 3단계: PRF+ 확장으로 7개 키 도출 */
  /* S = Ni | Nr | SPIi | SPIr */
  u8 *s_material = format (0, "%v%v%U%U",
      sa->i_nonce, sa->r_nonce,
      format_u64, sa->ispi,
      format_u64, sa->rspi);

  u8 *keymat = ikev2_calc_prfplus (
      skeyseed, s_material,
      /* 필요한 총 키 길이: SK_d + SK_ai + SK_ar
         + SK_ei + SK_er + SK_pi + SK_pr */
      key_len_d + 2 * key_len_a + 2 * key_len_e + 2 * key_len_p);

  /* 4단계: keymat을 순서대로 분할하여 각 키에 할당 */
  int pos = 0;
  sa->keys.sk_d  = vec_slice (keymat, pos, key_len_d); pos += key_len_d;
  sa->keys.sk_ai = vec_slice (keymat, pos, key_len_a); pos += key_len_a;
  sa->keys.sk_ar = vec_slice (keymat, pos, key_len_a); pos += key_len_a;
  sa->keys.sk_ei = vec_slice (keymat, pos, key_len_e); pos += key_len_e;
  sa->keys.sk_er = vec_slice (keymat, pos, key_len_e); pos += key_len_e;
  sa->keys.sk_pi = vec_slice (keymat, pos, key_len_p); pos += key_len_p;
  sa->keys.sk_pr = vec_slice (keymat, pos, key_len_p);

  sa->state = IKEV2_STATE_SA_INIT;
  return 0;
}

IPsec 터널 인터페이스 구현 상세

VPP의 IPsec 터널 인터페이스는 일반적인 네트워크 인터페이스처럼 동작하면서 내부적으로 IPsec 암호화/복호화를 수행합니다. 이 구현은 ipsec_tun_protect_t 구조를 중심으로 인터페이스와 SA를 연결하며, 라우팅 기반 VPN 구성의 핵심 요소입니다.

ipsec_tunnel_if_t 구조체는 터널 인터페이스의 상태를 관리합니다. 각 터널 인터페이스는 VPP의 sw_if_index를 가지며, 이를 통해 일반 인터페이스와 동일하게 IP 주소 할당, 라우팅, ACL 적용이 가능합니다.

/* IPsec 터널 보호 구조체 (src/vnet/ipsec/ipsec_tun.h 기반) */
typedef struct {
  /* 보호 대상 인터페이스 */
  u32 itp_sw_if_index;             /* 연결된 sw_interface 인덱스 */
  index_t itp_out_sa;              /* 송신(outbound) SA 인덱스 */
  index_t *itp_in_sas;             /* 수신(inbound) SA 인덱스 배열 */

  /* 터널 엔드포인트 */
  ip_address_t itp_tun_src;        /* 터널 소스 주소 */
  ip_address_t itp_tun_dst;        /* 터널 목적지 주소 */

  /* adjacency rewrite 데이터 */
  fib_node_t itp_node;             /* FIB 그래프 노드 — adjacency 추적 */
  u32 itp_n_sa_in;                 /* 수신 SA 개수 */
  ipsec_protect_flags_t itp_flags; /* ITF, ENCAPED_ENABLED 등 플래그 */
} ipsec_tun_protect_t;

/* 터널 인터페이스 생성 및 SA 연결 과정 (의사 코드) */
static int
ipsec_tun_protect_update (u32 sw_if_index,
                          const ip_address_t *nh,
                          u32 sa_out, u32 *sas_in)
{
  ipsec_tun_protect_t *itp;

  /* 1단계: 보호 객체 할당 또는 기존 객체 검색 */
  itp = ipsec_tun_protect_find (sw_if_index, nh);
  if (!itp) {
    pool_get (ipsec_tun_protect_pool, itp);
    itp->itp_sw_if_index = sw_if_index;
  }

  /* 2단계: 송신/수신 SA 연결 */
  itp->itp_out_sa = sa_out;
  itp->itp_in_sas = vec_dup (sas_in);
  itp->itp_n_sa_in = vec_len (sas_in);

  /* 3단계: adjacency rewrite 갱신
     — 패킷이 이 인터페이스로 전달될 때 ESP 캡슐화 적용 */
  ipsec_tun_protect_adj_update (itp);

  /* 4단계: 수신 SA의 SPI → 인터페이스 매핑 등록
     — esp4-decrypt 노드가 SPI로 터널 인터페이스를 역방향 검색 */
  for (int i = 0; i < itp->itp_n_sa_in; i++) {
    ipsec_sa_t *sa = ipsec_sa_get (sas_in[i]);
    ipsec_tun_register (sa->spi, itp->itp_sw_if_index);
  }

  return 0;
}

GRE over IPsec 패턴은 GRE 터널 인터페이스를 먼저 생성한 뒤, ipsec tunnel protect로 해당 인터페이스에 IPsec 보호를 연결하는 방식입니다. GRE가 캡슐화를 담당하고, IPsec이 암호화를 담당하는 분리 구조로 동작합니다.

/* GRE over IPsec 구성 CLI 예시 */

/* 1단계: IPsec SA 생성 (송신/수신) */
ipsec sa add 10 spi 1000 esp \
  crypto-alg aes-gcm-256 crypto-key 0123456789abcdef... \
  tunnel-src 10.0.0.1 tunnel-dst 10.0.0.2

ipsec sa add 20 spi 2000 esp \
  crypto-alg aes-gcm-256 crypto-key fedcba9876543210... \
  tunnel-src 10.0.0.2 tunnel-dst 10.0.0.1

/* 2단계: GRE 터널 인터페이스 생성 */
create gre tunnel src 10.0.0.1 dst 10.0.0.2

/* 3단계: GRE 인터페이스에 IPsec 보호 적용 */
ipsec tunnel protect gre0 sa-out 10 sa-in 20

/* 4단계: 터널 인터페이스에 IP 주소 할당 및 라우팅 */
set interface ip address gre0 192.168.100.1/30
set interface state gre0 up
ip route add 172.16.0.0/16 via 192.168.100.2 gre0

VXLAN over IPsec 패턴은 오버레이 네트워크(VXLAN)와 암호화(IPsec)를 결합합니다. VXLAN 터널을 먼저 생성한 뒤, 해당 터널이 사용하는 외부 IP 경로에 IPsec 터널 보호를 적용하면 VXLAN 패킷이 자동으로 ESP로 암호화됩니다.

/* VXLAN over IPsec 구성 CLI 예시 */

/* 1단계: IPsec SA 생성 */
ipsec sa add 30 spi 3000 esp \
  crypto-alg aes-gcm-256 crypto-key aabbccdd... \
  tunnel-src 10.0.0.1 tunnel-dst 10.0.0.2
ipsec sa add 40 spi 4000 esp \
  crypto-alg aes-gcm-256 crypto-key ddccbbaa... \
  tunnel-src 10.0.0.2 tunnel-dst 10.0.0.1

/* 2단계: IPsec 터널 인터페이스 생성 및 보호 설정 */
create ipip tunnel src 10.0.0.1 dst 10.0.0.2
set interface unnumbered ipip0 use loop0
ipsec tunnel protect ipip0 sa-out 30 sa-in 40
set interface state ipip0 up

/* 3단계: VXLAN 터널을 IPsec 터널 위에 생성
   — 외부 경로가 ipip0을 경유하므로 자동 암호화 */
create vxlan tunnel src 10.0.0.1 dst 10.0.0.2 vni 100 \
  encap-vrf-id 0

/* 4단계: VXLAN 인터페이스를 브릿지 도메인에 연결 */
set interface l2 bridge vxlan_tunnel0 100
set interface l2 bridge GigabitEthernet0/0/1 100

Anti-Replay 윈도우 구현 상세

IPsec의 Anti-Replay 메커니즘(RFC 4302/4303)은 공격자가 캡처한 ESP/AH 패킷을 재전송하는 재생 공격(Replay Attack)을 방지합니다. VPP는 64비트 비트맵 기반의 슬라이딩 윈도우 알고리즘으로 이를 구현하며, 수신된 각 패킷의 시퀀스 번호를 검사하여 중복 또는 너무 오래된 패킷을 탐지합니다.

슬라이딩 윈도우 동작 원리는 다음과 같습니다. SA별로 마지막으로 수신한 최대 시퀀스 번호(last_seq)와 64비트 비트맵(replay_window)을 유지합니다. 비트맵의 각 비트는 last_seq 기준으로 최근 64개 시퀀스 번호의 수신 여부를 기록합니다.

ESN(Extended Sequence Number, RFC 4304) 환경에서는 시퀀스 번호가 64비트로 확장되지만, ESP 헤더에는 하위 32비트만 전송됩니다. 수신 측은 상위 32비트(seq_hi)를 로컬 카운터를 기반으로 추론해야 합니다. VPP는 수신된 하위 32비트(seq)와 현재 last_seq의 상위 32비트를 비교하여 오버플로우 여부를 판단하고, 필요시 seq_hi를 1 증가시킵니다. 추론된 전체 64비트 시퀀스 번호는 무결성 검증(ICV) 계산에 포함되어 정확성이 암호학적으로 보장됩니다.

/* Anti-Replay 윈도우 검사 (src/vnet/ipsec/ipsec_sa.h 기반 의사 코드) */
static inline int
ipsec_sa_anti_replay_check (ipsec_sa_t *sa, u32 seq)
{
  u64 last_seq = sa->last_seq;
  u64 replay_window = sa->replay_window;
  u32 diff;

  /* ESN 상위 32비트 추론 */
  u32 seq_hi = sa->seq_hi;
  if (PREDICT_TRUE (sa->flags & IPSEC_SA_FLAG_USE_ESN)) {
    /* 하위 32비트 오버플로우 감지:
       수신 seq가 last_seq의 하위 32비트보다 크게 작으면
       아직 이전 epoch의 패킷일 수 있습니다 */
    u32 last_seq_lo = (u32) last_seq;
    if (seq < last_seq_lo &&
        (last_seq_lo - seq) > 0x80000000u) {
      /* 상위 비트 증가 (다음 epoch 패킷) */
      seq_hi = sa->seq_hi + 1;
    }
  }

  u64 full_seq = ((u64) seq_hi << 32) | seq;

  /* Case 1: 윈도우 범위 왼쪽 밖 — 너무 오래된 패킷 */
  if (PREDICT_FALSE (
      full_seq + IPSEC_SA_ANTI_REPLAY_WINDOW_SIZE < last_seq)) {
    return -1;  /* 폐기 */
  }

  /* Case 2: 윈도우 범위 내 — 중복 검사 */
  if (full_seq <= last_seq) {
    diff = last_seq - full_seq;
    if (replay_window & (1ULL << diff)) {
      return -1;  /* 중복 패킷 — 폐기 */
    }
  }

  /* Case 3: seq > last_seq 또는 윈도우 내 미수신 — 통과 */
  return 0;
}

/* Anti-Replay 윈도우 전진 — 무결성 검증 통과 후 호출 */
static inline void
ipsec_sa_anti_replay_advance (ipsec_sa_t *sa, u32 seq, u32 seq_hi)
{
  u64 full_seq = ((u64) seq_hi << 32) | seq;
  u64 last_seq = sa->last_seq;

  if (full_seq > last_seq) {
    /* 윈도우 시프트: 차이만큼 비트맵을 왼쪽으로 밀기 */
    u64 diff = full_seq - last_seq;
    if (diff < IPSEC_SA_ANTI_REPLAY_WINDOW_SIZE)
      sa->replay_window = (sa->replay_window << diff) | 1;
    else
      sa->replay_window = 1;  /* 큰 점프 시 윈도우 초기화 */

    sa->last_seq = full_seq;
    sa->seq_hi = seq_hi;
  } else {
    /* 윈도우 내 이전 시퀀스: 해당 비트만 설정 */
    u64 diff = last_seq - full_seq;
    sa->replay_window |= (1ULL << diff);
  }
}

Anti-Replay 검사는 esp4-decrypt 노드에서 무결성 검증(ICV 확인) 이전에 수행됩니다. 이는 비용이 큰 암호화 연산을 수행하기 전에 명백한 재생 공격 패킷을 조기에 걸러내기 위함입니다. 무결성 검증이 통과한 후에만 ipsec_sa_anti_replay_advance()를 호출하여 윈도우를 실제로 전진시킵니다. 이 2단계 구조는 위조 패킷이 윈도우 상태를 오염시키는 것을 방지합니다.

Segment Routing (SRv6)

VPP는 IPv6 Segment Routing(SRv6)을 데이터 플레인 수준에서 완전히 구현합니다. SRv6는 IPv6 확장 헤더인 Segment Routing Header(SRH)를 사용하여 패킷 경로를 소스에서 명시적으로 지정합니다. SRH에는 128비트 IPv6 주소 형식의 SID(Segment Identifier) 목록이 역순으로 저장되며, Segments Left 카운터가 현재 활성 세그먼트를 가리킵니다. 각 SRv6 노드는 Segments Left 값을 1씩 감소시키고 다음 SID를 IPv6 목적지 주소에 복사하여 패킷을 다음 홉으로 전달합니다.

VPP는 두 가지 SRv6 적용 모드를 지원합니다. 캡슐화(Encapsulation) 모드는 원본 패킷을 새로운 IPv6 + SRH 외부 헤더로 감싸는 방식이며, 삽입(Insertion) 모드는 기존 IPv6 헤더에 SRH를 직접 삽입합니다. 캡슐화 모드가 기본이며, 삽입 모드는 중간 라우터가 SRv6를 인식하지 못하는 환경에서 유용합니다.

로컬 SID 동작(Local SID Behavior)은 각 노드에서 SID가 자신에게 할당된 것일 때 수행하는 처리를 정의합니다. VPP가 지원하는 주요 동작은 다음과 같습니다:

BSID(Binding SID)는 SRv6 정책 전체를 하나의 SID로 추상화하여 트래픽 엔지니어링을 단순화합니다. 입구 노드는 BSID만 참조하면 되므로 정책 내부의 세그먼트 목록 변경이 투명하게 처리됩니다. VPP에서는 sr policy add bsid 명령으로 BSID를 정책에 바인딩하고, sr steer 명령으로 특정 트래픽을 해당 정책으로 유도합니다.

VPP는 SRv6 Mobile 기능도 제공하여 5G 환경에서 UPF(User Plane Function) 역할을 수행합니다. GTP-U 터널 패킷을 SRv6 세그먼트로 변환(T.M.GTP4.D)하거나, SRv6 패킷을 다시 GTP-U로 복원(End.M.GTP4.E)하는 상호 변환을 지원합니다. 이를 통해 모바일 백홀 네트워크에서 GTP-U 터널링 대신 SRv6 기반의 유연한 트래픽 엔지니어링을 적용할 수 있습니다.

SRv6 패킷 처리 흐름 (캡슐화 모드) 원본 패킷 IPv4/IPv6 Payload SR 캡슐화 (입구 노드) SID 목록 + SRH 부착 BSID → 정책 매핑 중간 노드 (End) SL 감소, 다음 SID 전달 최종 SID (출구 노드) End.DT4/DT6: 탈캡슐화 VRF 라우팅 테이블 조회 SRH(Segment Routing Header) 구조 Next Header 확장 헤더 타입 Hdr Ext Len 헤더 길이 Routing Type 4 (SRH) Segments Left 활성 세그먼트 인덱스 Last Entry 마지막 SID 인덱스 Flags HMAC 등 Tag 패킷 그룹 SID 목록 (역순 저장, 하위부터 처리) Segment List[0]: fc00::1 (최종) Segment List[1]: fc00::2 (중간) Segment List[2]: fc00::3 (입구) Segments Left 카운터 변화 입구 노드 (캡슐화) SL = 2, DA = fc00::3 SL-- 중간 노드 (End) SL = 1, DA = fc00::2 SL-- 최종 노드 (End.DT4/DT6) SL = 0, DA = fc00::1 → 탈캡슐화 SRv6 Mobile (5G UPF 연동) T.M.GTP4.D: GTP-U 패킷 → SRv6 세그먼트 변환 (GTP TEID를 SID에 인코딩) End.M.GTP4.E: SRv6 패킷 → GTP-U 터널 복원 (SID에서 TEID 디코딩)
/* SRv6 정책 설정 */
vpp# sr localsid address fc00::1 behavior end
vpp# sr policy add bsid fc00::999 next fc00::2 next fc00::3 encap
vpp# sr steer l3 10.0.0.0/8 via bsid fc00::999
알고리즘유형nativeipsecmbopensslQAT HW
AES-GCM-128/256AEADOOOO
AES-CBC-128/256암호화OOO
ChaCha20-Poly1305AEADOO
HMAC-SHA-256/512인증OOO
AES-CTR암호화OOO
NULL테스트OOO

QoS (Quality of Service)

VPP는 패킷 마킹, 폴리싱, 스케줄링 기능을 제공합니다:

# 폴리서 생성 (2r3c: 2-rate 3-color marker)
vpp# configure policer name rate-limiter cir 100000 cb 10000 \
     eir 200000 eb 20000 rate kbps color-aware

# 인터페이스에 폴리서 적용
vpp# set policer classify interface GigabitEthernet0/8/0 ip4-table 0
vpp# policer input rate-limiter GigabitEthernet0/8/0

# QoS 마킹 (DSCP 설정)
vpp# set qos record interface GigabitEthernet0/8/0 input ip
vpp# set qos mark interface GigabitEthernet0/9/0 output ip table 0

# 상태 확인
vpp# show policer
vpp# show qos record
vpp# show qos mark
QoS 기능설명노드
Policer2r3c (RFC 2698) 기반 트래픽 폴리싱policer-input
QoS Record인입 패킷의 QoS 값(DSCP/MPLS TC) 기록qos-record
QoS Mark송출 패킷에 QoS 값 마킹qos-mark
QoS Store패킷에 고정 QoS 값 저장qos-store

VXLAN / GENEVE 터널(Tunnel)

VXLAN(Virtual Extensible LAN, RFC 7348)은 L2 프레임을 UDP 패킷으로 캡슐화하여 L3 네트워크 위에 가상의 L2 오버레이(Overlay) 네트워크를 구성하는 터널링 프로토콜입니다. 원본 이더넷 프레임 앞에 외부 이더넷 헤더(Outer Ethernet), 외부 IP 헤더(Outer IP), 외부 UDP 헤더(목적지 포트 4789), 그리고 8바이트 VXLAN 헤더가 추가됩니다. VXLAN 헤더에는 24비트 VNI(VXLAN Network Identifier)가 포함되어 최대 약 1,600만 개의 논리적 네트워크 세그먼트를 지원합니다. BUM(Broadcast, Unknown unicast, Multicast) 트래픽 처리에는 멀티캐스트 그룹을 사용한 복제 방식과 헤드엔드 유니캐스트 복제(Head-End Replication) 방식이 있으며, VPP는 두 가지 모두 지원합니다.

GENEVE(Generic Network Virtualization Encapsulation, RFC 8926)는 VXLAN의 후속 프로토콜로, 고정 헤더 뒤에 가변 길이 TLV(Type-Length-Value) 옵션 필드를 추가할 수 있습니다. 이를 통해 OAM(Operations, Administration, Maintenance) 정보, 보안 태그, 텔레메트리 메타데이터 등을 터널 헤더에 직접 포함할 수 있어 확장성이 크게 향상됩니다. UDP 목적지 포트는 6081을 사용하며, VXLAN과 동일하게 24비트 VNI를 지원합니다.

VPP의 패킷 그래프에서 VXLAN 디캡슐화는 ip4-inputip4-lookupip4-localudp-localvxlan4-input 경로를 거치며, vxlan4-input 노드에서 VNI를 기반으로 해당 터널 인터페이스를 식별하고 내부 프레임을 추출합니다. 캡슐화 경로는 l2-outputvxlan4-encapip4-rewriteinterface-output 순서로 진행됩니다. 최신 NIC(Intel X710, Mellanox ConnectX-5 이상 등)은 VTEP(VXLAN Tunnel End Point) 오프로드를 지원하여 외부 헤더의 추가·제거를 하드웨어에서 처리함으로써 CPU 부담을 줄이고 처리량을 높일 수 있습니다.

VXLAN 캡슐화 패킷 구조 및 VPP 그래프 노드 경로 캡슐화 패킷 구조 Outer Ethernet 14 bytes Outer IP 20 bytes Outer UDP dst port: 4789 VXLAN Header VNI (24-bit) · 8 bytes Inner Ethernet 14 bytes Inner IP 20 bytes Inner Payload 가변 길이 오버레이 헤더 (Outer + VXLAN) — 50 bytes 오버헤드 원본 프레임 (Inner Frame) 디캡슐화 경로 (Decap) ip4-input ip4-lookup ip4-local udp-local vxlan4-input l2-input / ip4-input VNI 매칭 → 터널 인터페이스 식별 캡슐화 경로 (Encap) l2-input l2-fwd / l2-flood l2-output vxlan4-encap ip4-rewrite interface-output VXLAN 헤더 + Outer UDP/IP 추가 GENEVE 확장 헤더 Outer Eth+IP+UDP GENEVE Header VNI · port 6081 TLV Options OAM · 보안 태그 · 메타데이터 Inner Frame (원본) GENEVE TLV: 가변 길이 메타데이터로 VXLAN 대비 확장성 우수
# VXLAN 터널 생성
vpp# create vxlan tunnel src 10.0.0.1 dst 10.0.0.2 vni 100
vpp# set interface state vxlan_tunnel0 up

# 브릿지 도메인에 추가 (L2 오버레이)
vpp# set interface l2 bridge vxlan_tunnel0 100
vpp# set interface l2 bridge GigabitEthernet0/9/0 100

# GENEVE 터널 생성
vpp# create geneve tunnel src 10.0.0.1 dst 10.0.0.2 vni 200
vpp# set interface state geneve_tunnel0 up

# 터널 상태 확인
vpp# show vxlan tunnel
vpp# show geneve tunnel
VXLAN GPE vs 표준 VXLAN: 표준 VXLAN(RFC 7348)은 L2 프레임만 캡슐화(Encapsulation)하지만, VXLAN-GPE(Generic Protocol Extension, draft-ietf-nvo3-vxlan-gpe)는 L3/NSH/MPLS 등 다양한 프로토콜을 캡슐화합니다. VPP는 VXLAN-GPE + SRv6 조합으로 SFC(Service Function Chaining)를 구현합니다.

VCL과 세션 레이어

VPP 세션 레이어

VPP는 L4 전송 프로토콜(TCP, UDP, QUIC)을 유저스페이스에서 직접 구현합니다. 세션 레이어가 소켓과 유사한 추상화를 제공하며, 애플리케이션은 VCL(VPP Communications Library)을 통해 접근합니다.

항목커널 소켓 APIVPP 세션 API
APIsocket(), bind(), listen(), accept()공유 메모리 기반 세션 큐
데이터 전달send()/recv() (커널 복사)공유 메모리 FIFO (zero-copy)
이벤트epoll/selectVPP 이벤트 큐 (eventfd)
멀티플렉싱epoll_wait()vcl_epoll_wait()
성능~200K conn/s~1M+ conn/s
호환성모든 애플리케이션VCL 또는 LD_PRELOAD 필요
/* startup.conf — 세션 레이어 활성화 */
session {
    evt_qs_memfd_seg              /* memfd 기반 이벤트 큐 */
    event-queue-length 100000    /* 이벤트 큐 크기 */
}

/* 세션 활성화 확인 */
vpp# session enable
vpp# show session verbose

세션 레이어 아키텍처 상세

VPP 세션 레이어는 커널의 소켓 서브시스템에 해당하는 유저스페이스 구현체입니다. src/vnet/session/ 디렉터리에 위치하며, 전송 프로토콜(TCP, UDP, TLS, QUIC)과 애플리케이션 사이의 추상화 계층을 제공합니다.

session_state_t — 세션 상태 머신

세션은 생성부터 종료까지 다음과 같은 상태를 거칩니다.

상태설명
SESSION_STATE_CREATED0세션 구조체 할당 완료, 아직 연결되지 않은 상태입니다
SESSION_STATE_LISTENING1session_listen() 호출 후 수신 대기 중인 리스너 세션입니다
SESSION_STATE_CONNECTING2session_open() 호출 후 비동기 연결 진행 중입니다
SESSION_STATE_ACCEPTING3전송 계층에서 SYN을 수신하여 수락 처리 중입니다
SESSION_STATE_READY4연결이 완료되어 데이터 송수신이 가능한 상태입니다
SESSION_STATE_TRANSPORT_CLOSING5전송 계층에서 종료를 시작했습니다 (원격 FIN 수신)
SESSION_STATE_CLOSING6애플리케이션이 session_close()를 호출하여 종료 진행 중입니다
SESSION_STATE_CLOSED7세션이 완전히 종료되어 리소스 해제 대기 중입니다

session_t 핵심 필드

session_t는 VPP 세션 레이어의 중심 데이터 구조체입니다. 하나의 세션은 하나의 전송 연결(TCP 커넥션, UDP 바인딩 등)에 대응합니다.

/* src/vnet/session/session_types.h — session_t 핵심 필드 (간략화) */
typedef struct session_ {
    /** 세션 풀 내 인덱스 — 세션 식별에 사용됩니다 */
    u32 session_index;

    /** 이 세션을 소유한 워커 스레드 인덱스 */
    u32 thread_index;

    /** 세션 유형: 전송 프로토콜 + FIB 프로토콜 인코딩 */
    session_type_t session_type;

    /** 현재 세션 상태 (CREATED → READY → CLOSED) */
    volatile session_state_t session_state;

    /** 소유 애플리케이션의 워커 인덱스 */
    u32 app_wrk_index;

    /** 전송 계층 연결 인덱스 (TCP/UDP connection) */
    u32 connection_index;

    /** 공유 메모리 FIFO — 수신/송신 데이터 버퍼 */
    svm_fifo_t *rx_fifo;
    svm_fifo_t *tx_fifo;

    /** 세션 플래그 (EVT_Q_SHM, IS_DGRAM 등) */
    session_flags_t flags;

    /** 리스너 세션인 경우 수락된 세션 수 추적 */
    u32 n_accepted;
} session_t;

session_type_t 인코딩

session_type_t는 전송 프로토콜과 네트워크 프로토콜을 하나의 값으로 인코딩합니다. 상위 비트에 transport_proto_t(TCP, UDP, TLS, QUIC 등)를, 하위 비트에 fib_protocol_t(IP4, IP6)를 저장합니다.

/* session_type_t 인코딩 — 전송 프로토콜 × FIB 프로토콜 */
static inline session_type_t
session_type_from_proto_and_ip (transport_proto_t proto,
                                u8 is_ip4)
{
    return (proto << 1 | is_ip4);
}

/* 디코딩 예시 */
transport_proto_t tp = session_type >> 1;    /* TCP=0, UDP=1, TLS=2, QUIC=3 */
u8 is_ip4 = session_type & 0x1;               /* 0=IPv6, 1=IPv4 */

/* 예: TCP+IPv4 = (0 << 1 | 1) = 1
 *     UDP+IPv6 = (1 << 1 | 0) = 2
 *     QUIC+IPv4 = (3 << 1 | 1) = 7 */

세션 생명주기

세션은 소켓 API와 유사한 생명주기를 따르지만, 내부적으로는 비동기·이벤트 기반으로 동작합니다.

단계함수동작
Listensession_listen()transport_start_listen()을 호출하여 리스너 세션을 생성합니다. 리스너는 SESSION_STATE_LISTENING 상태로 전환됩니다
Connectsession_open()transport_connect()를 호출하여 비동기 연결을 시작합니다. 세션은 CONNECTING 상태가 되며, 연결 완료 시 SESSION_CTRL_EVT_CONNECTED 이벤트가 발생합니다
Acceptsession_stream_accept()전송 계층에서 SYN을 수신하면 새 세션을 할당하고 FIFO를 생성한 뒤, 애플리케이션에 SESSION_CTRL_EVT_ACCEPTED 이벤트를 전달합니다
Datasession_enqueue_stream_connection()수신 데이터를 rx_fifo에 enqueue하고, SESSION_IO_EVT_RX 이벤트로 애플리케이션에 알립니다
Closesession_close()session_transport_close()를 통해 전송 계층에 종료를 요청합니다. FIN 교환 후 세션이 CLOSED 상태로 전환됩니다
/* session_open() — 비동기 연결 흐름 (간략화) */
int
session_open (session_endpoint_cfg_t *rmt, u32 opaque)
{
    transport_proto_t tp = rmt->transport_proto;
    transport_connection_t *tc;
    session_t *s;
    int rv;

    /* 1. 전송 계층에 연결 요청 (TCP SYN 전송 등) */
    rv = transport_connect (tp, rmt);
    if (rv < 0)
        return rv;

    /* 2. 전송 연결 객체 조회 */
    tc = transport_get_half_open (tp, rv);

    /* 3. 세션 할당 및 초기화 */
    s = session_alloc_for_connection (tc);
    s->session_state = SESSION_STATE_CONNECTING;
    s->app_wrk_index = rmt->app_wrk_index;
    s->opaque = opaque;

    /* 4. 연결 완료는 비동기 — transport에서 콜백으로 알림
     *    → session_connected_callback()
     *    → SESSION_CTRL_EVT_CONNECTED 이벤트 발생 */
    return 0;
}

/* 연결 완료 콜백 — transport에서 호출됩니다 */
static void
session_connected_callback (u32 app_wrk_index,
                            u32 opaque,
                            transport_connection_t *tc,
                            session_error_t err)
{
    session_t *s = session_get (tc->s_index, tc->thread_index);

    /* FIFO 할당 */
    session_alloc_fifos (s);

    /* 상태 전환: CONNECTING → READY */
    s->session_state = SESSION_STATE_READY;

    /* 애플리케이션에 연결 완료 이벤트 전달 */
    app_worker_connect_notify (s->app_wrk_index, s, err);
}

세션 FIFO 메커니즘

VPP 세션 레이어의 핵심 성능 비결은 공유 메모리 FIFO입니다. 커널 소켓이 send()/recv()마다 커널-유저 복사를 수행하는 것과 달리, VPP는 애플리케이션과 동일한 FIFO를 공유하여 zero-copy 데이터 전달을 달성합니다.

svm_fifo_t 구조체

svm_fifo_t는 공유 메모리 위에 구현된 원형 버퍼입니다. Lock-free 단일 생산자/단일 소비자(SPSC) 설계로, VPP 워커 스레드(생산자)와 애플리케이션 스레드(소비자)가 잠금 없이 동시에 접근할 수 있습니다.

/* src/svm/svm_fifo.h — svm_fifo_t 핵심 필드 (간략화) */
typedef struct svm_fifo_ {
    /** 공유 구조체 — VPP와 app이 mmap으로 공유합니다 */
    svm_fifo_shared_t *shr;

    /** FIFO 최대 크기 (바이트) */
    u32 nitems;

    /** 현재 저장된 데이터 크기 */
    u32 cursize;

    /** 읽기 위치 — 소비자(app)가 갱신합니다 */
    u32 head;

    /** 쓰기 위치 — 생산자(VPP)가 갱신합니다 */
    u32 tail;

    /** 소유 워커 스레드 인덱스 */
    u32 master_thread_index;

    /** 소유 세션 인덱스 */
    u32 master_session_index;

    /** 세그먼트 관리자 — 다중 청크 지원 */
    svm_fifo_chunk_t *start_chunk;
    svm_fifo_chunk_t *end_chunk;
} svm_fifo_t;

FIFO 크기 설정과 Backpressure

FIFO 크기는 startup.confsession 섹션에서 설정합니다. FIFO가 가득 차면 VPP가 전송 계층에 backpressure를 적용하여 TCP 수신 윈도우를 축소합니다.

/* startup.conf — FIFO 크기 설정 */
session {
    rx-fifo-size 64K       /* 수신 FIFO 기본 크기 */
    tx-fifo-size 64K       /* 송신 FIFO 기본 크기 */
    evt_qs_memfd_seg       /* memfd 기반 이벤트 큐 */
    event-queue-length 100000
    preallocated-sessions 1024    /* 세션 사전 할당 */
}

/* 런타임 FIFO 크기 확인 */
vpp# show session [verbose]
vpp# show session fifo trace

FIFO 기반 backpressure 흐름은 다음과 같습니다.

  1. 데이터 수신 → svm_fifo_enqueue()rx_fifo에 저장합니다
  2. svm_fifo_max_enqueue()로 남은 공간을 확인합니다
  3. 남은 공간이 임계값 이하이면, TCP 윈도우 크기를 축소하여 송신 측에 감속을 요청합니다
  4. 애플리케이션이 svm_fifo_dequeue()로 데이터를 소비하면 윈도우가 다시 확장됩니다

주요 FIFO API

함수용도
svm_fifo_enqueue()FIFO에 데이터를 기록합니다 (VPP → app 방향)
svm_fifo_dequeue()FIFO에서 데이터를 읽고 소비합니다 (app → VPP 방향)
svm_fifo_peek()데이터를 소비하지 않고 읽습니다 (head 이동 없음)
svm_fifo_dequeue_drop()데이터를 읽지 않고 소비합니다 (skip 용도)
svm_fifo_max_enqueue()FIFO의 남은 쓰기 공간을 반환합니다
svm_fifo_max_dequeue()FIFO에서 읽을 수 있는 데이터 크기를 반환합니다
svm_fifo_segments()zero-copy 직접 포인터 접근 (wrap-around 시 2개 세그먼트 반환)
/* FIFO enqueue/dequeue 기본 패턴 */

/* 생산자 (VPP 워커) — 수신 데이터를 rx_fifo에 저장 */
int
session_enqueue_stream_connection (session_t *s,
                                   vlib_buffer_t *b)
{
    u32 enqueued;
    u32 max_enq = svm_fifo_max_enqueue (s->rx_fifo);

    if (max_enq == 0)
        return 0;    /* FIFO 가득 참 — backpressure */

    enqueued = svm_fifo_enqueue (s->rx_fifo,
                                vlib_buffer_length_in_chain (vm, b),
                                vlib_buffer_get_current (b));

    /* 애플리케이션에 수신 이벤트 알림 */
    if (enqueued > 0)
        session_send_io_evt_to_thread (s->rx_fifo,
                                       SESSION_IO_EVT_RX);
    return enqueued;
}

/* 소비자 (애플리케이션/VCL) — rx_fifo에서 데이터 읽기 */
int
app_recv_stream (session_t *s, u8 *buf, u32 len)
{
    u32 max_deq = svm_fifo_max_dequeue (s->rx_fifo);
    u32 to_read = clib_min (max_deq, len);

    if (to_read == 0)
        return 0;

    svm_fifo_dequeue (s->rx_fifo, to_read, buf);
    return to_read;
}

/* zero-copy 패턴 — 데이터 복사 없이 직접 접근 */
svm_fifo_seg_t segs[2];
u32 n_segs = 2;

/* wrap-around 시 최대 2개 세그먼트로 분할됩니다 */
svm_fifo_segments (s->rx_fifo, segs, &n_segs);

for (int i = 0; i < n_segs; i++)
    process_data (segs[i].data, segs[i].len);

/* 처리 완료 후 소비 확정 */
svm_fifo_dequeue_drop (s->rx_fifo, total_len);

세션 이벤트 큐와 애플리케이션 통신

VPP와 애플리케이션 사이의 이벤트 전달은 공유 메모리 메시지 큐(svm_msg_q_t)를 통해 이루어집니다. 커널의 epoll 메커니즘에 대응하지만, 시스템 콜 오버헤드 없이 동작합니다.

session_event_t 이벤트 유형

이벤트방향설명
SESSION_IO_EVT_RXVPP → App수신 데이터가 rx_fifo에 도착했음을 알립니다. 애플리케이션은 svm_fifo_dequeue()로 데이터를 읽습니다
SESSION_IO_EVT_TXApp → VPP애플리케이션이 tx_fifo에 데이터를 기록했음을 알립니다. VPP가 전송을 시작합니다
SESSION_CTRL_EVT_ACCEPTEDVPP → App리스너에 새 연결이 수락되었습니다. 애플리케이션이 accept 응답을 해야 합니다
SESSION_CTRL_EVT_CONNECTEDVPP → App비동기 session_open()의 연결이 완료(또는 실패)되었습니다
SESSION_CTRL_EVT_DISCONNECTEDVPP → App원격 측에서 연결을 정상 종료했습니다 (FIN 수신)
SESSION_CTRL_EVT_RESETVPP → App연결이 비정상 리셋되었습니다 (RST 수신)

svm_msg_q_t — 공유 메모리 메시지 큐

VPP는 각 애플리케이션 워커마다 별도의 svm_msg_q_t 이벤트 큐를 할당합니다. 이 큐는 공유 메모리 세그먼트 위에 위치하며, lock-free ring buffer로 구현되어 있습니다.

/* VCL 애플리케이션의 이벤트 처리 루프 (간략화) */
while (1) {
    svm_msg_q_msg_t msg;
    session_event_t *evt;

    /* 이벤트 큐에서 대기 — eventfd로 블로킹 */
    svm_msg_q_wait (app_mq, SVM_MQ_WAIT_EMPTY);

    while (svm_msg_q_sub (app_mq, &msg, SVM_Q_NOWAIT, 0) == 0) {
        evt = (session_event_t *) svm_msg_q_msg_data (app_mq, &msg);

        switch (evt->event_type) {
        case SESSION_IO_EVT_RX:
            /* 수신 데이터 처리 */
            s = session_get_from_handle (evt->session_handle);
            n = svm_fifo_dequeue (s->rx_fifo, buf_sz, buf);
            handle_rx_data (s, buf, n);
            break;

        case SESSION_IO_EVT_TX:
            /* 송신 공간 확보 — 추가 데이터 전송 가능 */
            s = session_get_from_handle (evt->session_handle);
            resume_sending (s);
            break;

        case SESSION_CTRL_EVT_ACCEPTED:
            /* 새 연결 수락 */
            accepted_msg = (session_accepted_msg_t *) evt->data;
            handle_accept (accepted_msg);
            break;

        case SESSION_CTRL_EVT_CONNECTED:
            /* 연결 완료 */
            connected_msg = (session_connected_msg_t *) evt->data;
            if (connected_msg->retval == 0)
                handle_connected (connected_msg);
            else
                handle_connect_failed (connected_msg);
            break;

        case SESSION_CTRL_EVT_DISCONNECTED:
            /* 원격 종료 — 정리 후 disconnect reply 전송 */
            handle_disconnect (evt);
            break;

        case SESSION_CTRL_EVT_RESET:
            /* 비정상 리셋 — 즉시 세션 정리 */
            handle_reset (evt);
            break;
        }

        /* 메시지 소비 완료 — 슬롯 반환 */
        svm_msg_q_free_msg (app_mq, &msg);
    }
}

이 이벤트 기반 모델의 핵심 장점은 시스템 콜 횟수의 최소화입니다. 커널 소켓에서는 epoll_wait() + recv()마다 2회의 시스템 콜이 필요하지만, VPP 세션 레이어에서는 eventfd read() 1회로 여러 이벤트를 배치 처리할 수 있습니다. 고부하 상황에서는 eventfd 없이 공유 메모리를 직접 폴링하여 시스템 콜을 완전히 제거할 수도 있습니다.

LD_PRELOAD 투명 가속

VCL의 LD_PRELOAD 기능은 기존 POSIX 소켓 기반 애플리케이션을 수정 없이 VPP 세션 레이어로 가속합니다. libvcl_ldpreload.solibc의 소켓 함수를 가로채어 VPP와 통신합니다.

VCL LD_PRELOAD 투명 가속 아키텍처 Application nginx, iperf3, curl ... socket()/send()/recv() libvcl_ldpreload.so POSIX → VCL 변환 공유 메모리 FIFO VPP Process Session Layer TCP/UDP 스택 DPDK / NIC wire-speed I/O 기존 경로: App → libc → syscall → 커널 TCP → NIC 드라이버 (복사 2회 + 컨텍스트 스위치) VCL 경로: App → VCL → 공유 메모리 FIFO → VPP TCP → DPDK (zero-copy, 유저스페이스)
# iperf3에 VCL LD_PRELOAD 적용
$ VCL_CONFIG=/etc/vpp/vcl.conf \
  LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libvcl_ldpreload.so \
  iperf3 -s

# nginx에 적용
$ VCL_CONFIG=/etc/vpp/vcl.conf \
  LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libvcl_ldpreload.so \
  nginx -c /etc/nginx/nginx.conf

# vcl.conf 예제
vcl {
    rx-fifo-size 4000000          /* RX FIFO 4MB */
    tx-fifo-size 4000000          /* TX FIFO 4MB */
    app-scope-global              /* 글로벌 세션 네임스페이스 */
    api-socket-name /run/vpp/api.sock
}
POSIX 소켓 함수VCL 대응지원 수준
socket()vls_create()TCP/UDP/TLS
bind()vls_bind()완전
listen()vls_listen()완전
accept()vls_accept()완전
connect()vls_connect()완전
send()/write()vls_write()완전
recv()/read()vls_read()완전
epoll_*()vls_epoll_*()완전
select()/poll()vls_select()부분 (epoll 권장)
sendmsg()/recvmsg()미지원
VCL vs 커널 워크로드 선택: VCL은 대량 동시 연결(10K+)과 높은 처리량 시나리오에서 효과적입니다. 소규모 연결(수백 개)이나 복잡한 소켓 옵션(SO_REUSEPORT, SCM_RIGHTS 등)이 필요한 워크로드는 커널 소켓이 더 적합합니다.

VPP TLS 아키텍처

VPP는 유저스페이스 TLS 종단을 자체 세션 레이어 위에 구현합니다. 커널의 kTLS가 소켓 계층에서 암호화를 처리하는 것과 달리, VPP는 세션 레이어의 전송 프로토콜 추상화(transport_register_protocol())를 활용하여 TLS를 TCP와 동일한 레벨의 전송 프로토콜로 등록합니다. 이를 통해 애플리케이션은 TLS 여부와 무관하게 동일한 세션 API를 사용할 수 있습니다.

TLS 세션 레이어 통합

VPP의 TLS는 세션 레이어(src/vnet/session/)와 TLS 플러그인(src/plugins/tlsopenssl/, src/plugins/tlsmbedtls/) 사이의 추상 계층인 src/vnet/tls/tls.c를 통해 동작합니다:

/* src/vnet/tls/tls.h — TLS 컨텍스트 구조체 */
typedef struct tls_ctx_
{
  union {
    transport_connection_t connection;  /* 세션 레이어 전송 연결 */
  };
  u32 tls_session_handle;              /* 하부 TCP 세션 */
  u32 app_session_handle;              /* 상위 애플리케이션 세션 */
  u32 listener_ctx_index;              /* 리스너 컨텍스트 인덱스 */
  u8  is_passive_close;                /* 수동 종료 플래그 */
  u8  resume;                          /* 비동기 핸드셰이크 재개 */
  u8  app_closed;                      /* 애플리케이션 종료 여부 */
  tls_ctx_type_t tls_type;             /* openssl / mbedtls / picotls */
  u32 ckpair_index;                    /* 인증서/키 쌍 인덱스 */
  /* ... 엔진별 opaque 데이터 ... */
} tls_ctx_t;

/* TLS를 전송 프로토콜로 등록 */
static const transport_proto_vft_t tls_proto = {
  .connect      = tls_connect,
  .close        = tls_disconnect,
  .send_params  = tls_send_params,
  .get_connection = tls_connection_get,
  .get_listener   = tls_listener_get,
  .custom_tx      = tls_custom_tx_callback,
  .format_connection = format_tls_connection,
};

/* 초기화 시 TRANSPORT_PROTO_TLS로 등록 */
transport_register_protocol (TRANSPORT_PROTO_TLS,
                             &tls_proto, FIB_PROTOCOL_IP4, ~0);
transport_register_protocol (TRANSPORT_PROTO_TLS,
                             &tls_proto, FIB_PROTOCOL_IP6, ~0);

이 구조 덕분에 세션 레이어는 TLS를 별도 처리 없이 일반 전송 프로토콜처럼 취급하며, session_open() 호출 시 TRANSPORT_PROTO_TLS를 지정하면 자동으로 TCP 연결 위에 TLS 핸드셰이크가 수행됩니다.

TLS 컨텍스트의 생명 주기는 다음과 같은 상태 전이를 따릅니다:

  1. TLS_CONN_STATE_NONE: tls_ctx_alloc()로 컨텍스트 할당, 엔진 유형 결정
  2. TLS_CONN_STATE_HANDSHAKE: TCP 연결 수립 후 엔진별 핸드셰이크 시작 (ctx_init_server() 또는 ctx_init_client())
  3. TLS_CONN_STATE_ESTABLISHED: 핸드셰이크 완료, 양방향 암호화 데이터 전송 가능
  4. TLS_CONN_STATE_PASSIVE_CLOSE: 원격 측이 close_notify 전송, 잔여 데이터 드레인
  5. TLS_CONN_STATE_CLOSED: 양방향 종료 완료, 컨텍스트 해제

각 상태 전이에서 TLS 추상 계층은 엔진 VFT(Virtual Function Table)를 통해 실제 암호 라이브러리 호출을 위임합니다:

/* src/vnet/tls/tls.h — TLS 엔진 플러그인 인터페이스 */
typedef struct tls_engine_vft_
{
  u32 (*ctx_alloc) (void);                    /* 컨텍스트 할당 */
  void (*ctx_free) (tls_ctx_t *ctx);          /* 컨텍스트 해제 */
  tls_ctx_t *(*ctx_get) (u32 ctx_index);      /* 인덱스로 컨텍스트 조회 */

  /* 핸드셰이크: 서버/클라이언트 초기화 */
  int (*ctx_init_server) (tls_ctx_t *ctx);
  int (*ctx_init_client) (tls_ctx_t *ctx);

  /* 데이터 전송: 애플리케이션 FIFO ↔ TLS 레코드 */
  int (*ctx_write) (tls_ctx_t *ctx,           /* 평문→암호화→TCP */
                    session_t *app_session,
                    transport_send_params_t *sp);
  int (*ctx_read) (tls_ctx_t *ctx,            /* TCP→복호화→평문 */
                   session_t *tls_session);

  /* 핸드셰이크 진행 (WANT_READ/WRITE 시 재호출) */
  int (*ctx_handshake_is_over) (tls_ctx_t *ctx);

  /* 전송 종료 및 리셋 */
  int (*ctx_transport_close) (tls_ctx_t *ctx);
  int (*ctx_app_close) (tls_ctx_t *ctx);
} tls_engine_vft_t;

/* 엔진 등록 (각 플러그인의 init 함수에서 호출) */
void tls_register_engine (const tls_engine_vft_t *vft,
                          tls_engine_type_t type);
플러그인 확장: 새로운 TLS 라이브러리(예: wolfSSL, AWS-LC)를 VPP에 통합하려면 tls_engine_vft_t의 콜백(Callback)들만 구현하면 됩니다. 세션 레이어와 TLS 추상 계층의 코드는 변경할 필요가 없으며, 이것이 VPP TLS의 핵심 설계 원칙입니다.
VPP TLS 세션 레이어 스택 VCL / 내장 앱 (echo_server 등) Session Layer (session_open / session_send) TLS 추상 계층 (tls.c) transport_register_protocol(TRANSPORT_PROTO_TLS) OpenSSL 엔진 mbedTLS 엔진 picotls 엔진 TCP 전송 계층 (TRANSPORT_PROTO_TCP) App Session TLS Transport

TLS 데이터 경로: FIFO 기반 암복호화

VPP TLS의 데이터 경로는 이중 세션(dual-session) 모델을 사용합니다. 하나의 TLS 연결은 내부적으로 두 개의 세션을 유지합니다: 하부 TCP 세션(암호문)과 상위 애플리케이션 세션(평문). 각 세션은 독립적인 FIFO(rx/tx) 쌍을 가지며, TLS 엔진이 양쪽 FIFO 사이에서 암복호화를 수행합니다:

VPP TLS 데이터 경로: 이중 세션 FIFO 모델 송신 경로 (App → Network) App 세션 session_send(평문) App TX FIFO 평문 데이터 TLS 엔진 SSL_write() TCP TX FIFO TLS 레코드 TCP 세션 tcp_output() 수신 경로 (Network → App) TCP 세션 tcp_input() TCP RX FIFO TLS 레코드 TLS 엔진 SSL_read() App RX FIFO 평문 데이터 App 세션 session_read(평문) custom_tx 콜백: TLS 송신 트리거 메커니즘 세션 레이어가 App TX FIFO에 데이터가 있음을 감지 → tls_custom_tx_callback() 호출 → ctx_write() VFT 콜백 → SSL_write()로 App TX FIFO 소비, TCP TX FIFO에 TLS 레코드 생산 → 메인 루프의 다음 벡터 처리에서 TCP가 TLS 레코드를 네트워크로 전송 OpenSSL 엔진 내부: BIO 메모리 체인 SSL* 객체 ↔ BIO_s_mem (읽기) ↔ TLS 레코드 레이어 ↔ BIO_s_mem (쓰기) VPP는 커널 소켓 BIO(BIO_s_socket) 대신 메모리 BIO를 사용하여 FIFO와 직접 연결 → syscall 제로

제로-카피(zero-copy)에 가까운 데이터 경로가 VPP TLS의 성능 핵심입니다. 커널 kTLS가 sendfile()splice()를 통해 커널-유저 경계를 최소화하려는 것과 달리, VPP는 애초에 전체 경로가 유저스페이스이므로 경계 자체가 존재하지 않습니다.

VPP TLS vs 커널 kTLS 아키텍처 비교

VPP TLS와 커널 kTLS는 근본적으로 다른 설계 철학을 따릅니다. 두 접근 방식의 구조적 차이를 이해하면 적합한 사용 시나리오를 판단할 수 있습니다:

VPP TLS vs 커널 kTLS 아키텍처 비교 VPP TLS (유저스페이스) 유저스페이스 (단일 프로세스) VCL / 애플리케이션 세션 레이어 + TLS 엔진 TCP 스택 (VPP 내장) DPDK / AF_XDP (NIC 직접) syscall: 0 컨텍스트 스위칭: 0 복사: FIFO 내 제로카피 핸드셰이크: 유저스페이스 내 인증서 교체: 무중단 kTLS (커널) 유저스페이스 nginx / 애플리케이션 OpenSSL (핸드셰이크만) syscall 경계 (send/recv/setsockopt) 커널 공간 kTLS 모듈 (데이터 암복호화) TCP 스택 (커널) NIC 드라이버 syscall: send/recv마다 컨텍스트 스위칭: 매 I/O 복사: sendfile() 제로카피 가능 핵심 차이: 핸드셰이크 위치 VPP: 핸드셰이크 + 데이터 모두 유저스페이스 — 높은 CPS (연결/초), 인증서 동적 관리 kTLS: 핸드셰이크는 유저스페이스, 데이터만 커널 — sendfile 최적화, 기존 앱 호환성
비교 항목VPP TLS커널 kTLS
핸드셰이크유저스페이스 (VPP 내)유저스페이스 (OpenSSL)
데이터 암복호화유저스페이스 (VPP 내)커널 (tls_sw/tls_device)
TCP 스택유저스페이스 (VPP 내장)커널 TCP
NIC 접근DPDK/AF_XDP (직접)커널 드라이버
syscall 횟수0 (완전 유저스페이스)send/recv 호출마다
sendfile 지원미지원 (FIFO 기반)지원 (제로카피 가능)
NIC HW TLSDPDK Cryptodev 경유tls_device 네이티브
기존 앱 호환LD_PRELOAD 또는 VCL 전환소켓 옵션만 추가
인증서 교체무중단 (CLI/API)프로세스 재시작(Reboot) 필요
적합 워크로드고 CPS, 대규모 동시 연결고 처리량, 파일 서빙

TLS 플러그인 비교: OpenSSL vs mbedTLS vs picotls

VPP는 세 가지 TLS 엔진 플러그인을 제공합니다. 각각 다른 유스케이스에 최적화되어 있으며, startup.conf에서 선택할 수 있습니다:

항목tlsopenssltlsmbedtlspicotls
라이브러리OpenSSL / BoringSSLMbed TLS (ARM)picotls (H2O)
TLS 1.2완전 지원완전 지원미지원
TLS 1.3완전 지원3.x에서 지원완전 지원 (전용)
비동기 암호화지원 (ENGINE API)미지원미지원
HW 오프로드QAT, Cryptodev미지원미지원
Cipher Suite전체제한적TLS 1.3 전용
메모리 사용높음 (~50KB/ctx)낮음 (~10KB/ctx)매우 낮음 (~5KB/ctx)
성능 (SW)높음중간매우 높음 (1.3 전용)
라이선스Apache 2.0Apache 2.0MIT
적합 시나리오범용, HW 가속임베디드, IoTTLS 1.3 전용 고성능
엔진 선택 가이드: 대부분의 프로덕션 환경에서는 tlsopenssl이 권장됩니다. TLS 1.3만 필요하고 최대 성능이 목표라면 picotls가 적합하며, 메모리가 극히 제한된 임베디드 환경에서는 tlsmbedtls를 고려합니다.

각 엔진은 VPP 소스 트리의 독립적인 플러그인으로 존재합니다:

src/plugins/
├── tlsopenssl/          # OpenSSL/BoringSSL 엔진
│   ├── tls_openssl.c    # VFT 구현: ctx_init, ctx_write, ctx_read
│   ├── tls_async.c      # 비동기 ENGINE 연동 (QAT 등)
│   └── tls_bio.c        # BIO_s_mem 기반 FIFO 브릿지
├── tlsmbedtls/          # Mbed TLS 엔진
│   └── tls_mbedtls.c    # VFT 구현, mbedtls_ssl_* 래핑
├── tlspicotls/          # picotls 엔진 (TLS 1.3 전용)
│   └── tls_picotls.c    # VFT 구현, ptls_* 래핑
└── quic/                # QUIC 플러그인 (quicly 기반)
    └── quic.c           # TLS 1.3은 quicly 내부에서 처리
TLS 엔진 플러그인 내부 구조 (OpenSSL 기준) tls.c — tls_engine_vft_t 디스패치 tlsopenssl 플러그인 SSL_CTX (리스너당 1개) SSL* (연결당 1개) BIO_read BIO_write TCP FIFO (rx/tx) 비동기: ENGINE API (QAT) ~50KB/컨텍스트 tlsmbedtls 플러그인 mbedtls_ssl_config mbedtls_ssl_context mbedtls_ssl_send/recv 콜백 TCP FIFO (rx/tx) 동기 전용 ~10KB/컨텍스트 tlspicotls 플러그인 ptls_context_t (TLS 1.3 only) ptls_t* (연결당) ptls_send/receive 직접 호출 TCP FIFO (rx/tx) 동기 전용, 최소 오버헤드 ~5KB/컨텍스트

TLS 엔진 플러그인 내부 구현 분석

VPP의 TLS 엔진 플러그인은 각각 고유한 I/O 패턴으로 암복호화를 수행합니다. 여기서는 OpenSSL, mbedTLS, picotls 세 엔진의 내부 데이터 경로와 구현 차이를 상세히 분석합니다.

OpenSSL 엔진 ctx_write() 분석

tls_openssl_ctx_write() 함수는 애플리케이션이 보낸 평문 데이터를 TLS 레코드로 암호화하여 TCP 스택에 전달하는 핵심 경로입니다. OpenSSL의 BIO(Basic I/O) 추상화 계층을 활용하여 메모리 기반 I/O를 수행합니다:

  1. App TX FIFO에서 평문 dequeue: svm_fifo_peek()로 애플리케이션이 기록한 평문 데이터를 읽어옵니다
  2. BIO_write() → SSL_write(): 평문 데이터를 OpenSSL의 내부 BIO 버퍼에 기록하면, SSL_write()가 TLS 레코드를 생성하고 암호화를 수행합니다
  3. BIO_read()로 암호문 추출: 암호화된 TLS 레코드를 출력 BIO에서 읽어옵니다
  4. TCP TX FIFO에 암호문 enqueue: svm_fifo_enqueue()로 암호문을 TCP 전송 큐에 삽입합니다
/* tls_openssl_ctx_write() — 평문 → TLS 레코드 암호화 경로 */
static int
tls_openssl_ctx_write (tls_ctx_t *ctx, session_t *app_session,
                       transport_send_params_t *sp)
{
  openssl_ctx_t *oc = (openssl_ctx_t *) ctx;
  svm_fifo_t *f = app_session->tx_fifo;
  u32 deq_max, wrote = 0;
  int rv;

  /* 1단계: App TX FIFO에서 전송 가능한 최대 바이트 확인 */
  deq_max = svm_fifo_max_dequeue_cons (f);
  if (!deq_max)
    return 0;

  /* 2단계: FIFO에서 평문 데이터를 읽어 SSL_write()로 암호화 */
  while (deq_max > 0)
    {
      u32 len = clib_min (deq_max, TLS_CHUNK_SIZE);
      svm_fifo_peek (f, wrote, len, oc->write_buf);

      /* SSL_write()가 내부적으로 BIO를 통해 TLS 레코드 생성 */
      rv = SSL_write (oc->ssl, oc->write_buf, len);
      if (rv <= 0)
        {
          int err = SSL_get_error (oc->ssl, rv);
          if (err == SSL_ERROR_WANT_WRITE)
            break;
          return -1;
        }

      wrote += rv;
      deq_max -= rv;
    }

  /* 3단계: 출력 BIO에서 암호문을 읽어 TCP TX FIFO에 enqueue */
  openssl_write_from_bio_to_fifo (oc->ssl, app_session);

  /* 4단계: 소비된 바이트만큼 App TX FIFO에서 제거 */
  if (wrote)
    svm_fifo_dequeue_drop (f, wrote);

  return wrote;
}

openssl_write_from_bio_to_fifo() 내부에서는 BIO_ctrl_pending()으로 출력 BIO에 대기 중인 암호문 크기를 확인한 후, BIO_read()로 추출하여 TCP 세션의 TX FIFO에 기록합니다. 이 BIO 기반 간접 경로가 OpenSSL 엔진의 특징이며, 동시에 성능 오버헤드의 원인이기도 합니다.

OpenSSL 엔진 ctx_read() 분석

tls_openssl_ctx_read()는 TCP에서 수신한 암호문을 복호화하여 애플리케이션에 전달하는 수신 경로입니다. 쓰기 경로의 역방향으로 BIO를 활용합니다:

  1. TCP RX FIFO에서 암호문 dequeue: TCP 스택이 수신한 TLS 레코드를 읽어옵니다
  2. BIO_write()로 입력 BIO에 주입: 암호문을 OpenSSL의 입력 BIO 버퍼에 기록합니다
  3. SSL_read()로 복호화: OpenSSL이 TLS 레코드를 파싱하고 복호화하여 평문을 반환합니다
  4. App RX FIFO에 평문 enqueue: 복호화된 평문을 애플리케이션 수신 큐에 삽입합니다
/* tls_openssl_ctx_read() — TLS 레코드 복호화 → 평문 경로 */
static int
tls_openssl_ctx_read (tls_ctx_t *ctx, session_t *tls_session)
{
  openssl_ctx_t *oc = (openssl_ctx_t *) ctx;
  session_t *app_session;
  svm_fifo_t *app_rx_fifo;
  int read = 0, rv;

  /* 1단계: TCP RX FIFO → 입력 BIO로 암호문 전달 */
  openssl_read_from_fifo_to_bio (oc->ssl, tls_session);

  app_session = session_get_from_handle (ctx->app_session_handle);
  app_rx_fifo = app_session->rx_fifo;

  /* 2단계: SSL_read()로 복호화된 평문 추출 */
  while (1)
    {
      rv = SSL_read (oc->ssl, oc->read_buf, TLS_CHUNK_SIZE);
      if (rv <= 0)
        {
          int err = SSL_get_error (oc->ssl, rv);
          if (err == SSL_ERROR_WANT_READ)
            break;  /* 추가 데이터 대기 필요 */
          if (err == SSL_ERROR_ZERO_RETURN)
            break;  /* TLS close_notify 수신 */
          return -1;
        }

      /* 3단계: 복호화된 평문을 App RX FIFO에 enqueue */
      rv = svm_fifo_enqueue (app_rx_fifo, rv, oc->read_buf);
      if (rv < 0)
        break;

      read += rv;
    }

  return read;
}

SSL_ERROR_WANT_READ는 OpenSSL이 완전한 TLS 레코드를 구성하기에 데이터가 부족할 때 반환됩니다. 이 경우 VPP는 이벤트 루프로 제어를 반환하고, TCP로부터 추가 데이터가 도착하면 다시 ctx_read()를 호출합니다. SSL_ERROR_ZERO_RETURN은 상대방이 TLS close_notify를 전송했음을 의미하며, 정상적인 연결 종료 절차를 시작합니다.

mbedTLS 엔진 차이점

mbedTLS 엔진은 OpenSSL의 BIO 추상화 대신 콜백 기반 I/O 모델을 사용합니다. mbedtls_ssl_set_bio()로 등록한 커스텀 send/recv 콜백이 FIFO에 직접 접근하므로, 별도의 BIO 계층 오버헤드가 발생하지 않습니다:

/* mbedTLS 엔진 — 콜백 기반 I/O 설정 */
static void
mbedtls_ctx_init (tls_ctx_t *ctx)
{
  mbedtls_ctx_t *mc = (mbedtls_ctx_t *) ctx;

  mbedtls_ssl_init (&mc->ssl);
  mbedtls_ssl_setup (&mc->ssl, &mc->conf);

  /* BIO 대신 커스텀 콜백으로 FIFO 직접 연결 */
  mbedtls_ssl_set_bio (&mc->ssl, ctx,
                       tls_mbedtls_send_cb,   /* 송신 콜백 */
                       tls_mbedtls_recv_cb,   /* 수신 콜백 */
                       NULL);                 /* 타임아웃 콜백 없음 */
}

/* 송신 콜백: 암호문을 TCP TX FIFO에 직접 기록 */
static int
tls_mbedtls_send_cb (void *ctx_ptr, const unsigned char *buf,
                     size_t len)
{
  tls_ctx_t *ctx = (tls_ctx_t *) ctx_ptr;
  session_t *tls_session;
  int rv;

  tls_session = session_get_from_handle (ctx->tls_session_handle);
  rv = svm_fifo_enqueue (tls_session->tx_fifo, len, buf);
  if (rv < 0)
    return MBEDTLS_ERR_SSL_WANT_WRITE;

  return rv;
}

/* 수신 콜백: TCP RX FIFO에서 암호문을 직접 읽기 */
static int
tls_mbedtls_recv_cb (void *ctx_ptr, unsigned char *buf,
                     size_t len)
{
  tls_ctx_t *ctx = (tls_ctx_t *) ctx_ptr;
  session_t *tls_session;
  int rv;

  tls_session = session_get_from_handle (ctx->tls_session_handle);
  rv = svm_fifo_dequeue (tls_session->rx_fifo, len, buf);
  if (rv < 0)
    return MBEDTLS_ERR_SSL_WANT_READ;

  return rv;
}

콜백 내부에서 svm_fifo_enqueue() / svm_fifo_dequeue()를 직접 호출하기 때문에, OpenSSL처럼 BIO 버퍼를 거치는 중간 복사가 제거됩니다. 다만 mbedTLS 자체의 암호화 연산 속도가 OpenSSL 대비 느리므로, 전체 처리량에서는 큰 이점을 얻기 어렵습니다. mbedTLS 엔진은 주로 메모리 제약이 있는 임베디드 환경이나 라이선스 요건(Apache 2.0)이 중요한 경우에 선택됩니다.

picotls 엔진 최적화

picotls 엔진은 TLS 1.3 전용 구현체로, TLS 1.0~1.2 레거시 코드가 전혀 없어 코드베이스가 극히 작고 성능이 우수합니다. VPP의 picotls 엔진이 최고 성능을 달성하는 핵심 요인은 다음과 같습니다:

/* picotls 엔진 — 직접 버퍼 기반 암호화 경로 */
static int
picotls_ctx_write (tls_ctx_t *ctx, session_t *app_session,
                    transport_send_params_t *sp)
{
  picotls_ctx_t *ptc = (picotls_ctx_t *) ctx;
  svm_fifo_t *f = app_session->tx_fifo;
  ptls_buffer_t sendbuf;
  u32 deq_max, wrote = 0;
  int rv;

  deq_max = svm_fifo_max_dequeue_cons (f);
  if (!deq_max)
    return 0;

  /* ptls_buffer를 스택에 초기화 — 힙 할당 회피 */
  ptls_buffer_init (&sendbuf, "", 0);

  while (deq_max > 0)
    {
      u32 len = clib_min (deq_max, TLS_CHUNK_SIZE);
      svm_fifo_peek (f, wrote, len, ptc->write_buf);

      /* ptls_send()가 TLS 1.3 레코드를 직접 생성 */
      rv = ptls_send (ptc->tls, &sendbuf,
                     ptc->write_buf, len);
      if (rv != 0)
        break;

      wrote += len;
      deq_max -= len;
    }

  /* sendbuf에 축적된 암호문을 TCP TX FIFO에 일괄 전송 */
  if (sendbuf.off > 0)
    {
      session_t *tls_session;
      tls_session = session_get_from_handle (ctx->tls_session_handle);
      svm_fifo_enqueue (tls_session->tx_fifo,
                       sendbuf.off, sendbuf.base);
    }

  if (wrote)
    svm_fifo_dequeue_drop (f, wrote);

  ptls_buffer_dispose (&sendbuf);
  return wrote;
}

/* picotls 엔진 — 직접 버퍼 기반 복호화 경로 */
static int
picotls_ctx_read (tls_ctx_t *ctx, session_t *tls_session)
{
  picotls_ctx_t *ptc = (picotls_ctx_t *) ctx;
  ptls_buffer_t decryptbuf;
  session_t *app_session;
  u32 deq_max;
  int rv;

  deq_max = svm_fifo_max_dequeue_cons (tls_session->rx_fifo);
  if (!deq_max)
    return 0;

  ptls_buffer_init (&decryptbuf, "", 0);

  /* TCP RX FIFO에서 암호문을 읽어 직접 복호화 */
  svm_fifo_dequeue (tls_session->rx_fifo, deq_max,
                    ptc->read_buf);

  size_t consumed = deq_max;
  rv = ptls_receive (ptc->tls, &decryptbuf,
                    ptc->read_buf, &consumed);

  if (rv == 0 && decryptbuf.off > 0)
    {
      app_session = session_get_from_handle (
        ctx->app_session_handle);
      svm_fifo_enqueue (app_session->rx_fifo,
                       decryptbuf.off, decryptbuf.base);
    }

  ptls_buffer_dispose (&decryptbuf);
  return (rv == 0) ? decryptbuf.off : -1;
}

ptls_send()는 입력 평문에서 TLS 1.3 레코드를 직접 생성하여 ptls_buffer_t에 축적합니다. ptls_receive()consumed 포인터를 통해 실제 처리된 바이트 수를 반환하므로, 부분 레코드 도착 시에도 정확한 FIFO 관리가 가능합니다.

TLS 엔진 내부 API 비교

항목 OpenSSL mbedTLS picotls
I/O 모델 BIO (메모리 BIO 쌍) 콜백 (send/recv) 직접 버퍼 (ptls_buffer_t)
암호화 호출 SSL_write() mbedtls_ssl_write() ptls_send()
복호화 호출 SSL_read() mbedtls_ssl_read() ptls_receive()
FIFO 연동 방식 BIO_write → BIO_read 간접 콜백 내 FIFO 직접 접근 버퍼 포인터 직접 전달
중간 복사 횟수 2회 (BIO 입출력) 1회 (콜백 버퍼) 0~1회 (zero-copy 가능)
TLS 버전 지원 1.0 / 1.1 / 1.2 / 1.3 1.2 / 1.3 1.3 전용
세션당 메모리 ~34KB ~10KB ~5KB
핸드셰이크 모드 비동기 (WANT_READ/WRITE) 비동기 (WANT_READ/WRITE) 동기 (단일 RTT)
라이선스 Apache 2.0 Apache 2.0 MIT
비동기 HW 가속 ENGINE API 지원 ALT 함수 교체 미지원

TLS 핸드셰이크 유저스페이스 처리 흐름

VPP의 TLS 핸드셰이크는 전적으로 유저스페이스에서 수행됩니다. 커널 소켓을 거치지 않으므로 syscall 오버헤드가 없으며, VPP의 이벤트 기반 메인 루프와 통합되어 비차단(non-blocking) 방식으로 처리됩니다:

  1. session_open(TRANSPORT_PROTO_TLS): 세션 레이어가 TLS 연결 요청
  2. tls_connect(): TLS 추상 계층이 먼저 TCP 연결 수립
  3. TCP 3-way 핸드셰이크: VPP TCP 스택에서 수행
  4. tls_session_connected_cb(): TCP 연결 완료 콜백
  5. tls_ctx_handshake_*(): 엔진별 TLS 핸드셰이크 시작
  6. 비동기 핸드셰이크: SSL_do_handshake()SSL_ERROR_WANT_READ/WRITE 반환 시 이벤트 대기
  7. 핸드셰이크 완료: 애플리케이션에 연결 완료 통지
VPP TLS 1.3 핸드셰이크 시퀀스 VCL App Session Layer TLS Engine TCP / 원격 session_open(TLS) TCP SYN → ← TCP SYN+ACK → ACK tls_ctx_handshake() ClientHello + key_share ServerHello + EE + Cert + Finished Client Finished 1-RTT 핸드셰이크 완료 session_connected_cb() 암호화된 애플리케이션 데이터 송수신 TLS 1.3 0-RTT (Early Data) PSK 기반 재연결 시 ClientHello와 함께 애플리케이션 데이터 전송 가능 (replay 위험 주의)

암호화 엔진 선택

VPP CLI에서 런타임에 TLS 암호화 엔진을 변경할 수 있습니다:

# 현재 TLS 엔진 확인
vpp# show tls engines

# OpenSSL 엔진 선택
vpp# set tls crypto handler openssl

# picotls 엔진 선택 (TLS 1.3 전용)
vpp# set tls crypto handler picotls

# startup.conf에서 기본 엔진 지정
# tls {
#   default-crypto-engine openssl
# }
엔진 전환 주의: 런타임 엔진 변경은 새로운 연결에만 적용됩니다. 기존 TLS 세션은 원래 엔진으로 계속 동작합니다. 또한 picotls는 TLS 1.3만 지원하므로, TLS 1.2 연결이 필요한 클라이언트가 있다면 openssl을 유지해야 합니다.

OpenSSL BIO 메모리 체인 구현 분석

VPP의 tls_openssl 엔진은 OpenSSL의 BIO(Basic I/O) 추상화 계층을 활용하여 TLS 프로토콜 처리와 세션 FIFO 간의 데이터 흐름을 연결합니다. 핵심은 두 개의 BIO_s_mem 객체를 생성하여 각각 수신(RX)과 송신(TX) 방향의 TLS 레코드 버퍼로 사용하는 것입니다.

BIO_s_mem → FIFO 브릿지 구조: VPP는 OpenSSL의 표준 소켓 BIO 대신 메모리 BIO를 사용합니다. tls_openssl_bio_read() 콜백은 TCP RX FIFO에서 암호화된 TLS 레코드를 디큐(dequeue)하여 OpenSSL에 전달하고, tls_openssl_bio_write() 콜백은 OpenSSL이 생성한 암호화된 레코드를 TCP TX FIFO에 인큐(enqueue)합니다. 이 구조 덕분에 OpenSSL은 실제 소켓 없이도 TLS 처리를 수행할 수 있습니다.

/* OpenSSL 서버 컨텍스트 초기화 의사코드 */
static int
openssl_ctx_init_server (tls_ctx_t *ctx)
{
  openssl_ctx_t *oc = (openssl_ctx_t *) ctx;
  openssl_main_t *om = &openssl_main;

  /* 1. SSL_CTX 생성 및 인증서 설정 */
  SSL_CTX *ssl_ctx = SSL_CTX_new (TLS_server_method ());
  SSL_CTX_use_certificate_chain_file (ssl_ctx, ctx->tls_cert_file);
  SSL_CTX_use_PrivateKey_file (ssl_ctx, ctx->tls_key_file, SSL_FILETYPE_PEM);
  SSL_CTX_set_verify (ssl_ctx, SSL_VERIFY_NONE, NULL);

  /* 2. SSL 세션 객체 생성 */
  oc->ssl = SSL_new (ssl_ctx);

  /* 3. 메모리 BIO 쌍 생성 및 연결 */
  BIO *rbio = BIO_new (BIO_s_mem ());  /* TCP RX → OpenSSL 입력 */
  BIO *wbio = BIO_new (BIO_s_mem ());  /* OpenSSL 출력 → TCP TX */
  BIO_set_mem_eof_return (rbio, -1);
  BIO_set_mem_eof_return (wbio, -1);

  /* SSL 객체에 BIO 연결 (SSL이 소유권 획득) */
  SSL_set_bio (oc->ssl, rbio, wbio);

  /* 4. 서버 모드(accept)로 설정 */
  SSL_set_accept_state (oc->ssl);

  return 0;
}

데이터 송신 경로에서는 애플리케이션 FIFO의 평문 데이터가 OpenSSL을 거쳐 암호화된 후 TCP FIFO로 전달됩니다. 이 과정은 openssl_ctx_write() 함수에서 처리됩니다:

/* 데이터 송신: 앱 FIFO → OpenSSL 암호화 → TCP TX FIFO */
static int
openssl_ctx_write (tls_ctx_t *ctx, session_t *app_session,
                   transport_send_params_t *sp)
{
  openssl_ctx_t *oc = (openssl_ctx_t *) ctx;
  svm_fifo_t *app_rx_fifo = app_session->rx_fifo;  /* 앱→VPP 방향 */
  svm_fifo_t *tls_tx_fifo = ctx->tls_session->tx_fifo;
  int wrote = 0, rv;
  u32 deq_max, deq_now;

  deq_max = svm_fifo_max_dequeue_cons (app_rx_fifo);
  if (!deq_max)
    return 0;

  /* 1단계: 앱 FIFO에서 평문 데이터를 peek */
  deq_now = clib_min (deq_max, sp->max_burst_size);
  u8 *buf = vec_new (u8, deq_now);
  svm_fifo_peek (app_rx_fifo, 0, deq_now, buf);

  /* 2단계: SSL_write()로 OpenSSL에 평문 전달 → 내부 암호화 */
  rv = SSL_write (oc->ssl, buf, deq_now);
  if (rv > 0)
    {
      wrote = rv;
      svm_fifo_dequeue_drop (app_rx_fifo, rv);

      /* 3단계: wbio에서 암호화된 데이터를 읽어 TCP TX FIFO에 인큐 */
      BIO *wbio = SSL_get_wbio (oc->ssl);
      int pending = BIO_ctrl_pending (wbio);
      if (pending > 0)
        {
          u8 *tls_buf = vec_new (u8, pending);
          BIO_read (wbio, tls_buf, pending);
          svm_fifo_enqueue (tls_tx_fifo, pending, tls_buf);
          vec_free (tls_buf);
        }
    }

  vec_free (buf);
  return wrote;
}

수신 경로는 반대 방향으로 동작합니다. TCP RX FIFO의 암호화된 데이터가 OpenSSL을 거쳐 복호화된 후 애플리케이션 FIFO로 전달됩니다:

/* 데이터 수신: TCP RX FIFO → OpenSSL 복호화 → 앱 FIFO */
static int
openssl_ctx_read (tls_ctx_t *ctx, session_t *tls_session)
{
  openssl_ctx_t *oc = (openssl_ctx_t *) ctx;
  svm_fifo_t *tls_rx_fifo = tls_session->rx_fifo;
  svm_fifo_t *app_tx_fifo = ctx->app_session->tx_fifo;
  int read = 0, rv;
  u32 deq_max;

  deq_max = svm_fifo_max_dequeue_cons (tls_rx_fifo);
  if (!deq_max)
    return 0;

  /* 1단계: TCP RX FIFO에서 암호화된 TLS 레코드를 peek */
  u8 *buf = vec_new (u8, deq_max);
  svm_fifo_peek (tls_rx_fifo, 0, deq_max, buf);

  /* 2단계: rbio에 암호화된 데이터 기록 → OpenSSL 입력 */
  BIO *rbio = SSL_get_rbio (oc->ssl);
  BIO_write (rbio, buf, deq_max);
  svm_fifo_dequeue_drop (tls_rx_fifo, deq_max);

  /* 3단계: SSL_read()로 복호화된 평문 추출 */
  u8 plain_buf[16384];  /* TLS 레코드 최대 크기 */
  rv = SSL_read (oc->ssl, plain_buf, sizeof (plain_buf));
  if (rv > 0)
    {
      read = rv;
      /* 4단계: 복호화된 데이터를 앱 FIFO에 인큐 */
      svm_fifo_enqueue (app_tx_fifo, rv, plain_buf);
    }

  vec_free (buf);
  return read;
}

FIFO 제로카피 최적화: 위의 의사코드는 이해를 위해 버퍼 복사 방식으로 작성되었지만, 실제 VPP 구현에서는 svm_fifo_segments()를 사용하여 FIFO 내부 메모리에 대한 직접 포인터를 얻습니다. 이를 통해 중간 버퍼 할당과 복사를 제거하여 성능을 극대화합니다:

/* 제로카피 FIFO 세그먼트 접근 */
svm_fifo_seg_t segs[2];  /* 링버퍼이므로 최대 2개 세그먼트 */
u32 n_segs = 2;

/* FIFO 내부 메모리에 대한 직접 포인터 획득 (복사 없음) */
svm_fifo_segments (tls_rx_fifo, 0, segs, &n_segs, deq_max);

/* 세그먼트 데이터를 직접 BIO에 기록 */
for (int i = 0; i < n_segs; i++)
  BIO_write (rbio, segs[i].data, segs[i].len);

/* 처리 완료 후 FIFO에서 제거 */
svm_fifo_dequeue_drop (tls_rx_fifo, deq_max);

이 최적화는 특히 대용량 트래픽 처리 시 효과적입니다. SVM FIFO가 공유 메모리 위의 링버퍼로 구현되어 있기 때문에, 데이터가 버퍼 경계를 넘는 경우 최대 2개의 세그먼트가 반환됩니다. 각 세그먼트는 연속 메모리 영역을 가리키므로, BIO_write()에 직접 전달할 수 있습니다.

TLS 세션 재사용 내부 구현

TLS 핸드셰이크는 CPU 집약적인 비대칭 암호 연산을 포함하므로, 세션 재사용은 대규모 연결 환경에서 필수적인 최적화입니다. VPP는 TLS 1.2의 Session Ticket과 TLS 1.3의 PSK(Pre-Shared Key) 메커니즘을 모두 지원합니다.

Session Ticket 메커니즘

VPP는 SSL_CTX_set_tlsext_ticket_key_cb()를 통해 세션 티켓의 암호화/복호화 키를 직접 관리합니다. 이 방식은 다중 워커 스레드 환경에서 모든 워커가 동일한 티켓 키를 공유할 수 있게 합니다:

/* Session Ticket 키 콜백 의사코드 */
typedef struct
{
  u8 key_name[16];     /* 티켓 식별용 키 이름 */
  u8 aes_key[32];       /* AES-256-CBC 암호화 키 */
  u8 hmac_key[32];      /* HMAC-SHA-256 무결성 키 */
  f64 created_at;       /* 키 생성 시각 */
} tls_ticket_key_t;

typedef struct
{
  tls_ticket_key_t current;   /* 새 티켓 암호화에 사용 */
  tls_ticket_key_t previous;  /* 기존 티켓 복호화용 */
  f64 rotation_interval;       /* 키 로테이션 주기 (초) */
} tls_ticket_key_ctx_t;

static int
tls_ticket_key_cb (SSL *ssl, u8 *key_name,
                   u8 *iv, EVP_CIPHER_CTX *ectx,
                   HMAC_CTX *hctx, int enc)
{
  tls_ticket_key_ctx_t *tkc = get_ticket_key_ctx (ssl);

  if (enc)  /* 암호화: 새 티켓 생성 */
    {
      /* 현재 키의 이름을 티켓에 기록 */
      clib_memcpy (key_name, tkc->current.key_name, 16);

      /* 랜덤 IV 생성 */
      RAND_bytes (iv, EVP_MAX_IV_LENGTH);

      /* AES-256-CBC로 세션 상태 암호화 */
      EVP_EncryptInit_ex (ectx, EVP_aes_256_cbc (),
                          NULL, tkc->current.aes_key, iv);

      /* HMAC-SHA-256으로 무결성 서명 */
      HMAC_Init_ex (hctx, tkc->current.hmac_key, 32,
                    EVP_sha256 (), NULL);
      return 1;  /* 성공 */
    }
  else  /* 복호화: 기존 티켓 검증 */
    {
      tls_ticket_key_t *key = NULL;

      /* 키 이름으로 현재/이전 키 매칭 */
      if (!memcmp (key_name, tkc->current.key_name, 16))
        key = &tkc->current;
      else if (!memcmp (key_name, tkc->previous.key_name, 16))
        key = &tkc->previous;
      else
        return 0;  /* 알 수 없는 키 → 전체 핸드셰이크 수행 */

      HMAC_Init_ex (hctx, key->hmac_key, 32,
                    EVP_sha256 (), NULL);
      EVP_DecryptInit_ex (ectx, EVP_aes_256_cbc (),
                          NULL, key->aes_key, iv);

      /* 이전 키로 복호화된 경우 새 티켓 발급 권고 */
      return (key == &tkc->previous) ? 2 : 1;
    }
}

PSK (Pre-Shared Key) 재연결

TLS 1.3에서는 Session Ticket 대신 PSK 기반의 세션 재사용을 사용합니다. 서버는 핸드셰이크 완료 후 New Session Ticket(NST) 메시지를 전송하여 클라이언트에게 PSK를 제공합니다:

/* TLS 1.3 PSK / 0-RTT 설정 의사코드 */
static void
openssl_configure_tls13_resumption (SSL_CTX *ssl_ctx,
                                     tls_cfg_t *cfg)
{
  /* 세션 캐시 모드: 서버측 캐시 + 자동 NST 발행 */
  SSL_CTX_set_session_cache_mode (ssl_ctx,
    SSL_SESS_CACHE_SERVER | SSL_SESS_CACHE_NO_INTERNAL);

  /* 새 세션 생성 시 콜백 등록 */
  SSL_CTX_sess_set_new_cb (ssl_ctx, tls_session_new_cb);

  /* 세션 티켓 수명 설정 (기본 7200초) */
  SSL_CTX_set_timeout (ssl_ctx, cfg->session_timeout);

  /* 0-RTT Early Data 설정 */
  if (cfg->enable_early_data)
    {
      /* 최대 early data 크기 (16KB 권장) */
      SSL_CTX_set_max_early_data (ssl_ctx, 16384);

      /* 재전송 방어 활성화 (기본값) */
      SSL_CTX_set_options (ssl_ctx, SSL_OP_ANTI_REPLAY);
    }
}

/* 새 세션 콜백: NST 수신 시 호출 */
static int
tls_session_new_cb (SSL *ssl, SSL_SESSION *session)
{
  tls_ctx_t *ctx = SSL_get_app_data (ssl);

  /* 세션 티켓을 직렬화하여 캐시에 저장 */
  u32 len;
  u8 *data = NULL;
  len = i2d_SSL_SESSION (session, &data);
  if (len > 0)
    tls_session_cache_add (ctx->listener_index, data, len);
  OPENSSL_free (data);

  return 0;  /* OpenSSL이 세션 참조 해제 */
}

/* 서버측 0-RTT early data 수신 처리 */
static int
openssl_handle_early_data (tls_ctx_t *ctx)
{
  openssl_ctx_t *oc = (openssl_ctx_t *) ctx;
  u8 buf[16384];
  size_t read_bytes;
  int status;

  status = SSL_read_early_data (oc->ssl, buf,
                                 sizeof (buf), &read_bytes);
  switch (status)
    {
    case SSL_READ_EARLY_DATA_SUCCESS:
      /* 복호화된 0-RTT 데이터를 앱 FIFO에 전달 */
      svm_fifo_enqueue (ctx->app_session->tx_fifo,
                        read_bytes, buf);
      return read_bytes;

    case SSL_READ_EARLY_DATA_FINISH:
      /* Early data 완료, 일반 핸드셰이크로 전환 */
      return 0;

    case SSL_READ_EARLY_DATA_ERROR:
      /* 재전송 감지 또는 PSK 불일치 → 전체 핸드셰이크 */
      return -1;
    }

  return -1;
}
성능 효과: TLS 세션 재사용을 활성화하면 재연결 시 전체 핸드셰이크(2-RTT)를 생략하고 약식 핸드셰이크(1-RTT) 또는 0-RTT로 연결을 재개할 수 있습니다. 이는 연결 설정 지연을 50~100% 감소시키며, 서버의 CPU 부하도 비대칭 암호 연산 생략으로 크게 줄어듭니다.

TLS 레코드 레이어와 프래그먼테이션

VPP의 TLS 엔진은 애플리케이션 데이터를 TLS 레코드(최대 16KB)로 분할하여 TCP FIFO에 기록합니다. 이 과정에서 VPP 특유의 최적화가 적용됩니다:

/* TLS 레코드 구조 (RFC 8446 Section 5.1) */
struct {
  ContentType type;         /* 0x17=application_data, 0x16=handshake */
  ProtocolVersion version;  /* TLS 1.2: 0x0303, TLS 1.3: 0x0303 (호환) */
  uint16 length;            /* 최대 16384 + 256 (패딩) */
  opaque fragment[length];  /* 암호화된 페이로드 */
} TLSPlaintext;

/* VPP 최적화: 레코드 크기 조절 */
/*
 * - 핸드셰이크 시: 작은 레코드 (빠른 첫 바이트)
 * - 벌크 전송 시: 최대 레코드 (오버헤드 최소화)
 * - FIFO 잔량 기반 동적 크기 결정
 * - AES-GCM: 레코드당 16바이트 인증 태그 + 8바이트 nonce
 */
레코드 크기오버헤드적합 시나리오
1KB~2.3% (24B/1024B)인터랙티브, 낮은 지연
4KB~0.6%웹 페이지(Page) 전송
16KB (최대)~0.15%대용량 파일, 스트리밍
레코드 크기와 성능: VPP는 FIFO에 축적된 데이터량에 따라 레코드 크기를 자동 조절합니다. 소량 데이터는 작은 레코드로 즉시 전송하고(지연 최소화), 대량 데이터는 최대 레코드로 묶어 전송합니다(처리량 최대화). 이는 커널 kTLS의 정적 레코드 크기 대비 유연한 접근입니다.

VPP TLS 설정 및 인증서 관리

startup.conf TLS 설정

VPP의 TLS 관련 설정은 startup.conf의 여러 섹션에 분산되어 있습니다:

# /etc/vpp/startup.conf — TLS 관련 설정

tls {
  # 기본 암호화 엔진
  default-crypto-engine openssl

  # 최소 TLS 버전 (1.2 또는 1.3)
  tls-min-version 1.2

  # TLS 세션 캐시 크기 (세션 재사용)
  ca-cert-path /etc/vpp/certs/ca.pem

  # 비동기 암호화 활성화 (openssl 엔진만)
  async
}

# OpenSSL 엔진 특정 설정
tlsopenssl {
  # OpenSSL ENGINE 로드 (QAT 등)
  engine qat

  # 비동기 모드 (연산 큐잉)
  async

  # 최대 비동기 대기 프레임 수
  max-async-frames 256
}

인증서 및 개인 키 관리

VPP는 CLI와 API를 통해 TLS 인증서를 동적으로 관리합니다. 인증서는 인증서-키 쌍(ckpair) 단위로 등록되며, 애플리케이션이 리스너를 생성할 때 ckpair 인덱스를 참조합니다:

# PEM 형식 인증서/키 추가
vpp# tls cert add cert /etc/vpp/certs/server.pem \
                  key /etc/vpp/certs/server.key

# 인증서 목록 확인
vpp# show tls certs

# DER 형식도 지원
vpp# tls cert add cert /etc/vpp/certs/server.der \
                  key /etc/vpp/certs/server.key.der \
                  format der

# CA 인증서 (mTLS 클라이언트 검증용)
vpp# tls ca-cert add /etc/vpp/certs/ca-chain.pem
VPP TLS 인증서 로딩 흐름 CLI / Binary API ckpair 관리자 app_cert_key_pair_add() 인증서 저장소 ckpair_index → {cert, key} app_listener_alloc() TLS 리스너 생성 ckpair_index 참조 SSL_CTX_use_certificate() 엔진별 인증서/키 로드 연결별 SSL* 객체 생성 → 핸드셰이크 지원 형식 • PEM (Base64) • DER (Binary) • PKCS#12 (API)

SNI 기반 멀티 도메인 인증서 관리

VPP는 SNI(Server Name Indication)를 기반으로 하나의 리스너에서 여러 도메인의 인증서를 제공할 수 있습니다. 클라이언트가 ClientHello에 포함한 서버 이름에 따라 적절한 인증서가 자동으로 선택됩니다:

# 여러 도메인 인증서 등록
vpp# tls cert add cert /etc/vpp/certs/example-com.pem \
                  key /etc/vpp/certs/example-com.key
# → ckpair_index = 0

vpp# tls cert add cert /etc/vpp/certs/api-example-com.pem \
                  key /etc/vpp/certs/api-example-com.key
# → ckpair_index = 1

# 와일드카드 인증서도 지원
vpp# tls cert add cert /etc/vpp/certs/wildcard-example-com.pem \
                  key /etc/vpp/certs/wildcard-example-com.key
# → ckpair_index = 2  (*.example.com 매칭)
/* SNI 콜백을 통한 인증서 선택 (OpenSSL 엔진) */
/*
 * tls_openssl.c에서 SSL_CTX_set_tlsext_servername_callback()으로
 * SNI 콜백 등록. ClientHello의 server_name 확장을 파싱하여
 * 매칭되는 ckpair_index의 SSL_CTX로 전환.
 *
 * 매칭 우선순위:
 * 1. 정확한 도메인 매칭 (api.example.com)
 * 2. 와일드카드 매칭 (*.example.com)
 * 3. 기본 인증서 (fallback)
 */

인증서 무중단 교체

VPP의 유저스페이스 TLS는 인증서를 프로세스 재시작 없이 동적으로 교체할 수 있습니다. 이는 커널 kTLS나 nginx에서 인증서 교체 시 reload/restart가 필요한 것과 대비되는 핵심 장점입니다:

# 1. 현재 인증서 상태 확인
vpp# show tls certs
[0] CN=example.com  expires=2026-12-31
    active connections: 15234

# 2. 새 인증서 추가 (기존 인덱스 덮어쓰기)
vpp# tls cert update index 0 \
     cert /etc/vpp/certs/new-server.pem \
     key /etc/vpp/certs/new-server.key

# 3. 기존 연결: 원래 인증서로 계속 동작 (중단 없음)
# 4. 새 연결: 새 인증서 사용

# Let's Encrypt ACME 자동화 스크립트 예시:
# certbot renew --deploy-hook \
#   "vppctl tls cert update index 0 \
#    cert /etc/letsencrypt/live/example.com/fullchain.pem \
#    key /etc/letsencrypt/live/example.com/privkey.pem"

Cipher Suite 설정

TLS 1.2와 1.3은 Cipher Suite 협상 방식이 근본적으로 다릅니다. VPP에서의 설정 방법:

# TLS 1.2 cipher suite 설정 (OpenSSL 형식)
vpp# set tls cipher ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256

# TLS 1.3 ciphersuite 설정 (별도 설정)
vpp# set tls ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256

# 현재 설정 확인
vpp# show tls config
TLS 버전권장 Cipher Suite보안 수준
TLS 1.3TLS_AES_256_GCM_SHA384256-bit, AEAD
TLS 1.3TLS_CHACHA20_POLY1305_SHA256256-bit, 소프트웨어 최적
TLS 1.2ECDHE-RSA-AES256-GCM-SHA384PFS + 256-bit AEAD
TLS 1.2ECDHE-ECDSA-AES128-GCM-SHA256PFS + ECDSA + AEAD

상호 인증(mTLS) 구성

mTLS(Mutual TLS)는 서버뿐 아니라 클라이언트도 인증서를 제시하여 양방향 인증을 수행합니다. VPP에서의 mTLS 구성:

# 1. 서버 인증서/키 등록
vpp# tls cert add cert /etc/vpp/certs/server.pem \
                  key /etc/vpp/certs/server.key

# 2. CA 인증서 등록 (클라이언트 인증서 검증용)
vpp# tls ca-cert add /etc/vpp/certs/client-ca.pem

# 3. 클라이언트 인증서 검증 활성화
# VCL 애플리케이션에서 session_attr에 설정:
#   transport_cfg.is_mtls = 1;
/* mTLS 서버 VCL 설정 예시 */
vppcom_session_tls_set_verify (session_handle,
    VPPCOM_TLS_VERIFY_PEER |
    VPPCOM_TLS_VERIFY_FAIL_IF_NO_PEER_CERT);

/* 클라이언트 인증서 DN 확인 콜백 */
vppcom_session_tls_set_verify_cb (session_handle,
    my_verify_callback, my_ctx);
mTLS 유스케이스: 서비스 메시(Istio, Envoy 연동), 제로 트러스트 네트워크, API 게이트웨이 인증에서 VPP mTLS가 활용됩니다. 커널 kTLS 대비 인증서 교체가 재시작 없이 가능하다는 장점이 있습니다.
VPP mTLS (상호 인증) 핸드셰이크 흐름 클라이언트 VPP 서버 (mTLS) ClientHello + supported_versions + key_share ServerHello + EncryptedExtensions CertificateRequest (CA 목록 포함) Certificate (서버 인증서) CertificateVerify + Finished 클라이언트: 서버 인증서 검증 (CA 체인) Certificate (클라이언트 인증서) CertificateVerify (개인 키로 서명) Finished VPP: 클라이언트 인증서 검증 CA 체인 확인 + CRL/OCSP + DN 콜백 양방향 인증 완료 — 암호화 데이터 전송 mTLS 실패 시나리오 클라이언트 인증서 미제출 → handshake_failure | 만료/CA 불일치 → bad_certificate | CRL 등재 → certificate_revoked

TLS 세션 재사용과 0-RTT

VPP는 TLS 1.3의 세션 티켓(Session Ticket)PSK(Pre-Shared Key)를 지원하여 재연결 시 핸드셰이크를 생략하거나 0-RTT로 단축할 수 있습니다:

/* TLS 1.3 세션 티켓 흐름 */
/*
 * [최초 연결]
 * 1. 전체 핸드셰이크 수행 (1-RTT)
 * 2. 서버가 NewSessionTicket 메시지 전송
 * 3. 클라이언트가 티켓 + resumption_master_secret 저장
 *
 * [재연결]
 * 1. ClientHello에 pre_shared_key 확장 포함
 * 2. 서버가 PSK 검증 → 0-RTT 또는 1-RTT 재개
 * 3. 0-RTT 시 ClientHello와 함께 Early Data 전송 가능
 */

/* VPP에서 세션 티켓 설정 */
/* startup.conf 또는 CLI: */
/*   tls {
/*     session-ticket-lifetime 3600   # 티켓 유효기간 (초) */
/*     session-ticket-key-rotation 1800 # 키 교체 주기 */
/*   }                                                      */
0-RTT 보안 주의: 0-RTT Early Data는 재전송(Retransmission) 공격(replay attack)에 취약합니다. 공격자가 0-RTT 패킷을 캡처하여 재전송하면 서버가 중복 처리할 수 있습니다. 따라서 0-RTT는 멱등(idempotent) 요청(GET 등)에만 허용하고, 상태 변경 요청(POST, PUT)에는 사용하지 않아야 합니다. VPP의 애플리케이션 레벨에서 SSL_get_early_data_status()로 Early Data 여부를 확인하여 적절히 처리하세요.

VPP QUIC 프로토콜

VPP는 quicly 라이브러리(H2O 프로젝트)를 기반으로 QUIC 전송 프로토콜을 네이티브 지원합니다. QUIC은 UDP 위에 TLS 1.3을 내장한 다중화(Multiplexing) 전송 프로토콜로, VPP의 유저스페이스 스택과 결합하면 커널 경유 없이 고성능 암호화 통신이 가능합니다.

QUIC 전송 아키텍처

VPP의 QUIC 구현(src/plugins/quic/)은 세션 레이어의 전송 프로토콜로 등록되며, 단일 QUIC 연결 위에 여러 스트림을 다중화합니다:

/* QUIC 전송 프로토콜 등록 */
transport_register_protocol (TRANSPORT_PROTO_QUIC,
                             &quic_proto, FIB_PROTOCOL_IP4, ~0);

/* QUIC 세션 계층 구조 */
/*
 * Application Stream ──→ QUIC Stream (session)
 *                            │
 *      여러 스트림 ──────→ QUIC Connection (session)
 *                            │
 *                    ──→ UDP Transport (session)
 */

typedef struct quic_ctx_
{
  union {
    transport_connection_t connection;
  };
  quicly_conn_t *conn;          /* quicly 연결 객체 */
  u32 listener_ctx_id;
  u32 udp_session_handle;       /* 하부 UDP 세션 */
  u8 conn_state;                /* 연결 상태 머신 */
  u8 udp_is_ip4;
  /* TLS 1.3은 quicly 내부에서 처리 */
} quic_ctx_t;
VPP QUIC 세션 계층 구조 애플리케이션 (VCL / builtin) Stream 0 (bidi) Stream 4 (bidi) Stream 2 (uni) QUIC Connection (quicly) TLS 1.3 내장 · 연결 마이그레이션 · 0-RTT UDP 전송 (TRANSPORT_PROTO_UDP) VPP 네트워크 스택 (ip4/ip6 → DPDK/AF_XDP) 다중화 암호화 전송

QUIC 세션 API

QUIC은 연결(Connection)스트림(Stream)의 2계층 세션 모델을 사용합니다. VCL에서의 QUIC 사용:

/* QUIC 서버 — 연결 수락 + 스트림 수락 */
/* 1. QUIC 리스너 생성 */
int listener = vppcom_session_create (VPPCOM_PROTO_QUIC, 0);
vppcom_session_bind (listener, &addr);
vppcom_session_listen (listener, 10);

/* 2. QUIC 연결 수락 (connection-level) */
int quic_conn = vppcom_session_accept (listener, &client_ep, 0);

/* 3. 스트림 수락 (stream-level, 실제 데이터 교환) */
int stream = vppcom_session_accept (quic_conn, &stream_ep, 0);

/* 4. 스트림에서 데이터 송수신 */
vppcom_session_read (stream, buf, sizeof(buf));
vppcom_session_write (stream, response, resp_len);

/* QUIC 클라이언트 — 연결 + 스트림 생성 */
int quic_conn = vppcom_session_create (VPPCOM_PROTO_QUIC, 0);
vppcom_session_connect (quic_conn, &server_ep);

/* 동일 연결에 여러 스트림 생성 */
int stream1 = vppcom_session_create (VPPCOM_PROTO_QUIC, 0);
vppcom_session_stream_connect (stream1, quic_conn);

QUIC 연결 마이그레이션

QUIC의 가장 혁신적인 기능 중 하나가 연결 마이그레이션(Connection Migration)입니다. TCP는 (src_ip, src_port, dst_ip, dst_port) 4-tuple로 연결을 식별하므로 IP가 변경되면 연결이 끊어지지만, QUIC은 Connection ID로 연결을 식별하여 IP/포트 변경 시에도 연결을 유지합니다:

QUIC 연결 마이그레이션 (Connection Migration) 모바일 클라이언트 VPP QUIC 서버 WiFi: 192.168.1.100 CID=0xA1B2 | Stream 데이터 전송 중 CID=0xC3D4 | 응답 데이터 WiFi → LTE 전환 IP 변경: 10.0.0.50 TCP라면 여기서 연결 끊김! LTE: 10.0.0.50 CID=0xA1B2 | PATH_CHALLENGE (경로 검증) CID=0xC3D4 | PATH_RESPONSE (검증 완료) VPP: CID 기반 연결 식별 IP 변경 무관, CID=0xA1B2 → 동일 quic_ctx CID=0xA1B2 | Stream 데이터 계속 전송 (중단 없음) TCP: 재연결 필요 (2-RTT 지연) vs QUIC: 즉시 전환 (0-RTT, 데이터 손실 없음) 모바일 5G/WiFi 핸드오버, VPN IP 변경, 다중 경로(multi-path) 시나리오에서 핵심 이점

QUIC 흐름 제어와 혼잡 제어(Congestion Control)

QUIC은 TCP와 달리 2계층 흐름 제어를 제공합니다:

흐름 제어 레벨프레임설명
스트림 레벨MAX_STREAM_DATA개별 스트림의 수신 버퍼 한도
연결 레벨MAX_DATA전체 연결의 총 수신 데이터 한도
스트림 수MAX_STREAMS동시 활성 스트림 수 제한
/* VPP QUIC 흐름 제어 파라미터 */
/*
 * quicly 내부에서 관리:
 * - initial_max_data: 연결 초기 수신 윈도우 (기본: 1MB)
 * - initial_max_stream_data_bidi: 양방향 스트림 윈도우 (기본: 256KB)
 * - initial_max_stream_data_uni: 단방향 스트림 윈도우
 * - initial_max_streams_bidi: 최대 양방향 스트림 수 (기본: 100)
 *
 * 혼잡 제어: quicly 기본 Reno + ECN 지원
 * VPP 24.x부터 BBRv2 혼잡 제어 옵션 추가
 */

QUIC의 손실 감지(Loss Detection)는 TCP의 재전송 메커니즘보다 정교합니다. 각 패킷에 고유한 번호가 부여되어(모호성 없음) ACK 기반의 정확한 RTT 측정과 빠른 손실 감지가 가능합니다. 또한 스트림별 독립 복구로 하나의 스트림 손실이 다른 스트림을 차단하지 않습니다(Head-of-Line blocking 해결).

HTTP/3 over VPP QUIC

HTTP/3는 QUIC 위에서 동작하는 HTTP의 차세대 버전으로, VPP의 QUIC 플러그인 위에 구현할 수 있습니다:

HTTP/3 프로토콜 스택 (VPP 환경)

┌────────────────────────────────┐
│  HTTP/3 (QPACK 헤더 압축)      │  ← HTTP 시맨틱
├────────────────────────────────┤
│  QUIC Streams (다중화)          │  ← 스트림별 독립 전송
├────────────────────────────────┤
│  QUIC Connection (quicly)      │  ← TLS 1.3 내장, 혼잡 제어
├────────────────────────────────┤
│  UDP (TRANSPORT_PROTO_UDP)     │  ← 포트 443 (IANA 표준)
├────────────────────────────────┤
│  VPP ip4/ip6 → DPDK/AF_XDP    │  ← 유저스페이스 네트워크
└────────────────────────────────┘

주요 차이: HTTP/2 vs HTTP/3
• HTTP/2: TCP 위 TLS → HoL blocking 존재
• HTTP/3: QUIC 위 → 스트림별 독립 → HoL blocking 해결
• 서버 푸시, 우선순위, 헤더 압축은 동일 시맨틱

QUIC 설정과 활성화

# startup.conf — QUIC 플러그인 활성화
plugins {
  plugin quic_plugin.so { enable }
}

quic {
  # 최대 동시 연결 수
  max-connections 100000

  # 연결당 최대 스트림 수
  max-streams-per-connection 100

  # 0-RTT 활성화
  enable-0rtt

  # 유휴 타임아웃 (초)
  idle-timeout 60
}

QUIC vs TCP+TLS 성능 비교

VPP 환경에서 QUIC과 TCP+TLS의 주요 차이점:

항목TCP + TLS 1.3QUIC
연결 수립2-RTT (TCP + TLS)1-RTT (통합 핸드셰이크)
재연결2-RTT0-RTT (PSK)
HoL Blocking있음 (TCP 순서 보장(Ordering))없음 (스트림별 독립)
다중화불가 (HTTP/2로 해결)네이티브 스트림 다중화
연결 마이그레이션불가Connection ID 기반 가능
패킷 손실 복구TCP 재전송 (느림)스트림별 독립 복구 (빠름)
VPP 처리량~40 Gbps (단일 워커)~25 Gbps (단일 워커)
CPU 오버헤드낮음중간 (UDP+QUIC 레이어)
HW 오프로드QAT, NIC Crypto제한적 (UDP checksum만)
QUIC vs TCP+TLS 핸드셰이크 비교 TCP + TLS 1.3 (2-RTT) Client Server SYN SYN+ACK ACK RTT 1 ClientHello ServerHello+Fin Client Fin RTT 2 데이터 전송 시작 QUIC (1-RTT / 0-RTT) Client Server Initial (ClientHello+key) Initial+Handshake (SH+Fin) Handshake Fin 1 RTT 데이터 전송 시작 0-RTT 재연결 시 Initial + Early Data (첫 패킷에 데이터 포함) 1-RTT 절약 → 지연시간 50% 감소
QUIC 적합 시나리오: 다수의 독립 스트림이 필요한 HTTP/3, IoT 디바이스 연결, 모바일 환경(IP 변경이 잦은 경우)에서 QUIC이 TCP+TLS 대비 확실한 이점을 제공합니다. 반면 단일 고대역폭 스트림(대용량 파일 전송)에서는 TCP+TLS의 HW 오프로드 지원이 더 효과적일 수 있습니다.

VPP TLS 성능 최적화

비동기 암호화 프레임워크

VPP의 비동기 암호화(src/vnet/tls/tls_async.c)는 TLS 핸드셰이크와 데이터 암복호화를 VPP 메인 루프에서 분리하여, 암호 연산이 완료될 때까지 다른 패킷 처리를 계속할 수 있게 합니다:

/* 비동기 암호화 흐름 */
/* 1. 암호 연산 큐잉 */
tls_async_enqueue_op (vm, ctx, op_type, data, len);

/* 2. 메인 루프가 다른 노드 처리 계속 */
/* ... 벡터 패킷 처리 ... */

/* 3. 완료된 연산 결과 수거 */
n_ops = tls_async_dequeue (vm, &completed_ops);

/* 4. 결과 처리 (암호문 전송 또는 평문 전달) */
for (i = 0; i < n_ops; i++)
  tls_async_process_completed (completed_ops[i]);
VPP 비동기 TLS 암호화 파이프라인 VPP 메인 루프 (벡터 처리) input → classify → ip4-lookup → tls-encrypt → output (계속 처리) enqueue Crypto Op 큐 encrypt / decrypt / handshake SW 엔진 (OpenSSL) HW 엔진 (QAT/Cryptodev) 완료 큐 dequeue 비동기 암호화 이점 • 메인 루프 블로킹 방지 • HW 가속기 파이프라이닝 • 배치 처리로 처리량 극대화 • 동기 대비 2~5x 핸드셰이크 처리량

비동기와 동기 모드의 핵심 차이를 이해하는 것이 중요합니다:

항목동기 모드 (기본)비동기 모드
암호화 실행메인 루프에서 즉시 수행큐에 넣고 나중에 결과 수거
메인 루프 블로킹암호화 완료까지 대기다른 패킷 계속 처리
처리량CPU 바운드HW 가속기 파이프라이닝으로 향상
핸드셰이크 CPS~30K (RSA-2048)~80K (SW), ~150K (QAT)
지연시간안정적 (즉시 완료)약간 증가 (큐잉 오버헤드)
적합 시나리오적은 연결, 낮은 지연 필수대량 연결, 높은 CPS 필요
/* 비동기 암호화 상세 흐름 (tls_async.c) */

/* 핸드셰이크 비동기 처리 */
int tls_async_openssl_ctx_init (tls_ctx_t *ctx)
{
  openssl_ctx_t *oc = (openssl_ctx_t *) ctx;

  /* SSL_MODE_ASYNC 활성화 → SSL_do_handshake()가
   * ASYNC_pause_job()으로 제어 반환 가능 */
  SSL_set_mode (oc->ssl, SSL_MODE_ASYNC);

  /* ENGINE 설정 (QAT 등): RSA/ECDH 연산을
   * 하드웨어에 오프로드 */
  SSL_set_engine (oc->ssl, async_engine);

  return 0;
}

/* 메인 루프 통합: tls-input 노드에서 호출 */
static uword
tls_async_process_node_fn (vlib_main_t *vm, ...)
{
  /* 1단계: 완료된 비동기 연산 수거 */
  n = openssl_async_poll_events (&events);

  /* 2단계: 각 완료 이벤트 처리 */
  for (i = 0; i < n; i++) {
    ctx = tls_ctx_get (events[i].ctx_index);
    if (ctx->resume)
      /* 핸드셰이크 재개 또는 데이터 전달 */
      tls_ctx_resume_handshake (ctx);
  }
}

DPDK Cryptodev TLS 오프로드

VPP는 DPDK의 Cryptodev 프레임워크를 통해 하드웨어 암호화 가속기에 TLS 연산을 오프로드할 수 있습니다. 대표적인 하드웨어:

디바이스인터페이스지원 알고리즘성능
Intel QATCryptodev PMDAES-GCM, ChaCha20, RSA, ECDH~100 Gbps (bulk), ~50K CPS
NVIDIA ConnectX-6+inline TLSAES-128/256-GCM~200 Gbps (NIC inline)
ARM CryptoCellCryptodev PMDAES-GCM, SHA-256~10 Gbps
SW fallbackOpenSSL PMD전체CPU 의존
# startup.conf — QAT Cryptodev 설정
dpdk {
  dev 0000:3d:01.0 {
    name crypto0
  }
}

tlsopenssl {
  # DPDK Cryptodev 엔진 사용
  engine cryptodev

  # 비동기 모드 (QAT 파이프라이닝)
  async

  # Cryptodev 큐 쌍 수
  cryptodev-queue-pairs 4
}
Intel QAT 하드웨어 TLS 오프로드 아키텍처 VPP 프로세스 (유저스페이스) TLS 세션 레이어 OpenSSL 엔진 SSL_MODE_ASYNC ENGINE API qat_engine 핸드셰이크 연산 (비대칭) • RSA-2048/4096 서명/검증 • ECDHE P-256/P-384 키교환 • X25519 키교환 벌크 암호화 (대칭) • AES-128/256-GCM • ChaCha20-Poly1305 • AES-CBC + HMAC-SHA PCIe 3.0 x16 Intel QAT 하드웨어 (C62x / 4xxx) PKE 엔진 RSA/ECDH Cipher 엔진 AES/ChaCha Hash 엔진 SHA/GCM-GHASH 압축 엔진 Deflate/LZ4 링 기반 큐 쌍 (Queue Pair) × N — DPDK Cryptodev PMD로 접근 QAT C62x: 최대 128 큐 쌍, ~100 Gbps bulk, ~50K RSA-2048 ops/sec 오프로드 효과 비교 RSA-2048 서명 (핸드셰이크) CPU: ~30K/s QAT: ~50K/s AES-256-GCM (벌크) CPU: 40G QAT: 100G CPU 사용률 (100K conn) SW: ~85% QAT: ~25% 절약된 CPU 코어를 패킷 처리에 활용 가능 TCO(총 소유 비용) 절감 전력 소비 ~40% 감소

멀티 워커 TLS 분산

VPP의 멀티 워커 환경에서 TLS 세션은 워커 간에 분산됩니다. 각 워커는 독립적인 TLS 컨텍스트 풀을 유지하며, NUMA 인지 메모리 할당으로 원격 메모리 접근을 최소화합니다:

# startup.conf — 멀티 워커 TLS 최적화
cpu {
  main-core 0
  corelist-workers 1-7
  # TLS 워커를 NUMA 0/1에 분산
}

session {
  # 워커별 TLS 세션 풀 크기
  preallocated-sessions 128000
  v4-session-table-buckets 64000
  v4-session-table-memory 512m

  # 이벤트 큐 크기 (TLS 비동기에 충분히)
  event-queue-length 100000
}
NUMA 인지 배치: TLS 컨텍스트(~50KB/연결)는 워커가 실행되는 NUMA 노드의 로컬 메모리에 할당됩니다. cpu { ... } 섹션에서 워커-NUMA 매핑을 정확히 설정하면, 원격 NUMA 접근에 의한 ~30% 지연 증가를 방지할 수 있습니다.
NUMA 인지 멀티 워커 TLS 토폴로지 NUMA 노드 0 NIC (PCIe NUMA 0) RSS Worker 1 Core 1 TLS ctx 풀 Worker 2 Core 2 TLS ctx 풀 Worker 3 Core 3 TLS ctx 풀 W 4 Core 4 로컬 메모리 (NUMA 0) TLS 컨텍스트 풀 + 세션 테이블 + FIFO 세그먼트 QAT 0 (PCIe NUMA 0) 모든 접근이 로컬 NUMA → 메모리 지연: ~80ns NUMA 노드 1 NIC (PCIe NUMA 1) Worker 5 Core 8 TLS ctx 풀 Worker 6 Core 9 TLS ctx 풀 Worker 7 Core 10 TLS ctx 풀 W 8 Core 11 로컬 메모리 (NUMA 1) TLS 컨텍스트 풀 + 세션 테이블 + FIFO 세그먼트 QAT 1 (PCIe NUMA 1) 모든 접근이 로컬 NUMA → 메모리 지연: ~80ns NUMA 교차 접근 시 성능 저하 원격 NUMA 메모리 접근: ~130ns (+60%) | NIC-워커 NUMA 불일치: PCIe 교차 트래픽 + 캐시 바운싱

멀티 워커 환경에서 TLS 세션의 워커 할당은 RSS(Receive Side Scaling) 해시에 의해 결정됩니다. NIC의 RSS가 5-tuple 해시로 패킷을 특정 큐에 분배하면, 해당 큐를 담당하는 VPP 워커가 TCP 연결과 TLS 세션을 모두 소유합니다. 이 구조에서 중요한 최적화 포인트:

설정 항목올바른 설정잘못된 설정 시 영향
NIC 큐 수= 워커 수일부 워커 유휴 또는 과부하
NIC-워커 NUMA 매핑NIC과 같은 NUMA의 코어PCIe 교차 트래픽, +30~60% 지연
QAT-워커 NUMA 매핑QAT와 같은 NUMA의 워커암호화 연산 원격 메모리 접근
Hugepage NUMA 분배각 NUMA 노드에 균등 할당한쪽 NUMA에서 메모리 부족
TLS 세션 풀워커별 독립 풀풀 공유 시 락 경합(Contention)

성능 비교: VPP TLS vs kTLS vs nginx

동일 하드웨어(Xeon Platinum 8380, 2.3 GHz, 128GB)에서의 대표적 TLS 성능 비교:

항목VPP TLS (SW)VPP TLS (QAT)kTLSnginx (userspace)
새 연결 (CPS)~80,000~150,000~50,000~30,000
처리량 (Gbps)~40~80~30~15
p99 지연 (ms)0.30.20.51.2
동시 연결500K+500K+100K50K
CPU 사용률높음 (전용 코어)낮음 (오프로드)중간 (커널)높음 (프로세스)
컨텍스트 스위칭(Context Switching)없음없음있음많음
TLS 성능 비교 (새 연결/초, 처리량 Gbps) 상대 성능 100% 66% 33% 0 150K 80G VPP+QAT 80K 40G VPP SW 50K 30G kTLS 30K 15G nginx CPS (진한색) Gbps (연한색)

TLS 성능 튜닝 체크리스트

항목권장 설정효과
비동기 암호화tlsopenssl { async }핸드셰이크 처리량 2~5x 향상
세션 캐시TLS 1.3 PSK 활성화재연결 0-RTT, CPU 절약
Cipher 선택AES-128-GCM (AES-NI 있을 때)AES-256 대비 ~15% 빠름
Cipher 선택ChaCha20 (AES-NI 없을 때)소프트웨어 최적, ARM 유리
Hugepages1GB 또는 2MB 페이지TLB 미스 감소, ~10% 처리량 향상
워커 수NIC 큐 수와 동일RSS 기반 워커 분산 최적화
NUMA 배치NIC과 같은 NUMA 노드원격 NUMA 접근 ~30% 지연 방지
세션 프리얼로케이션preallocated-sessions 128000런타임 할당 지연 제거
QAT 오프로드engine cryptodevCPU 사용률 ~70% 감소

실전 예제: VPP TLS 종단 프록시

HTTPS 리버스 프록시 구성

VPP를 HTTPS 리버스 프록시로 구성하면, 외부 클라이언트의 TLS를 VPP에서 종단하고 내부 백엔드 서버에는 평문 HTTP로 전달할 수 있습니다:

VPP HTTPS 리버스 프록시 토폴로지 클라이언트 HTTPS 요청 TLS 1.3 VPP TLS 종단 프록시 TLS 복호화 HTTP 프록시 http_static / http_proxy 플러그인 DPDK / AF_XDP NIC 인터페이스 평문 HTTP 백엔드 서버 1 백엔드 서버 2 암호화 구간 평문 구간 (내부 네트워크)
# VPP HTTPS 리버스 프록시 설정 예시

# 1. 인터페이스 설정
vpp# create host-interface name eth0
vpp# set interface state host-eth0 up
vpp# set interface ip address host-eth0 10.0.0.1/24

# 2. TLS 인증서 로드
vpp# tls cert add cert /etc/vpp/certs/server.pem \
                  key /etc/vpp/certs/server.key

# 3. HTTP static 서버 + TLS 활성화
vpp# http static server www-root /var/www/html \
     uri tls://0.0.0.0/443 \
     cache-size 10m \
     fifo-size 32k

# 또는 리버스 프록시 모드
vpp# http connect-proxy uri tls://0.0.0.0/443

# 4. 동작 확인
vpp# show session verbose
vpp# show tls ctx verbose

VCL + TLS 서버 C 코드 예제

VCL API를 사용한 TLS 에코 서버의 핵심 코드:

/* VCL TLS Echo Server — 핵심 흐름 */
#include <vcl/vppcom.h>

int main (void)
{
  vppcom_endpt_t endpt = {0};
  uint8_t buf[4096];
  int rv, listener, client;

  /* 1. VCL 초기화 */
  rv = vppcom_app_create ("tls-echo-server");
  if (rv) return rv;

  /* 2. TLS 리스너 생성 (VPPCOM_PROTO_TLS) */
  listener = vppcom_session_create (VPPCOM_PROTO_TLS,
                                    0 /* non-blocking */);

  /* 3. 인증서/키 설정 */
  vppcom_session_tls_add_cert (listener,
      cert_pem, cert_len);
  vppcom_session_tls_add_key (listener,
      key_pem, key_len);

  /* 4. 바인드 + 리슨 */
  endpt.is_ip4 = 1;
  endpt.port = htons (8443);
  vppcom_session_bind (listener, &endpt);
  vppcom_session_listen (listener, 128);

  /* 5. 이벤트 루프 */
  while (1) {
    /* accept는 이미 TLS 핸드셰이크 완료된 세션 반환 */
    client = vppcom_session_accept (listener,
                                    &endpt, 0);
    if (client < 0) continue;

    /* 평문 읽기 (TLS 복호화 투명 처리) */
    int n = vppcom_session_read (client, buf,
                                 sizeof(buf));
    if (n > 0)
      vppcom_session_write (client, buf, n);

    vppcom_session_close (client);
  }

  vppcom_session_close (listener);
  vppcom_app_destroy ();
  return 0;
}
# 빌드 및 실행
$ gcc -o tls-echo tls-echo.c \
    -I/usr/include/vpp \
    -lvppcom -lvlibmemoryclient -lsvm

$ VCL_CONFIG=/etc/vpp/vcl.conf LD_PRELOAD="" ./tls-echo

LD_PRELOAD TLS 투명 가속

VCL의 LD_PRELOAD 방식으로 기존 애플리케이션의 TLS 통신을 VPP 경유로 투명하게 가속할 수 있습니다. 애플리케이션 수정이 필요 없으며, 커널 TLS 대신 VPP의 유저스페이스 TLS가 사용됩니다:

# vcl.conf — TLS 활성화 설정
vcl {
  rx-fifo-size 4000000
  tx-fifo-size 4000000
  app-scope-local
  app-scope-global
  use-mq-eventfd

  # TLS 설정
  tls {
    cert /etc/vpp/certs/server.pem
    key /etc/vpp/certs/server.key
    ca-cert /etc/vpp/certs/ca.pem
  }
}

# nginx를 VPP TLS로 가속
$ VCL_CONFIG=/etc/vpp/vcl.conf \
  LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libvcl_ldpreload.so \
  nginx -c /etc/nginx/nginx-vpp.conf

# curl을 VPP TLS로 가속
$ VCL_CONFIG=/etc/vpp/vcl.conf \
  LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libvcl_ldpreload.so \
  curl -k https://10.0.0.1:8443/

# HAProxy를 VPP TLS로 가속
$ VCL_CONFIG=/etc/vpp/vcl.conf \
  LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libvcl_ldpreload.so \
  haproxy -f /etc/haproxy/haproxy.cfg
LD_PRELOAD TLS 제약: sendmsg()/recvmsg(), SO_REUSEPORT, SCM_RIGHTS 등 VCL이 지원하지 않는 소켓 기능을 사용하는 애플리케이션에서는 호환성 문제가 발생할 수 있습니다. 사전에 VCL 호환성 테이블(위 VCL 세션 섹션)을 확인하세요.

엔드-투-엔드 TLS 종단 워크스루

VPP에서 HTTPS 요청이 처리되는 전체 과정을 단계별로 추적합니다:

HTTPS 요청 처리 전체 흐름 (클라이언트 → VPP → 백엔드)

1. 클라이언트가 HTTPS 요청 전송 (TLS ClientHello)
      │
2. NIC(DPDK)가 패킷 수신 → RSS 해시 → 워커 N의 rx 큐
      │
3. dpdk-input 노드: 벡터로 패킷 수집
      │
4. ip4-input → ip4-lookup → tcp-input
      │
5. tcp-input: SYN → TCP 핸드셰이크 (VPP 내장 TCP)
      │
6. TCP 연결 완료 → session_connected_cb()
      │
7. tls_session_connected_cb(): TLS 핸드셰이크 시작
   ├─ ctx_init_client/server(): SSL_CTX + SSL 객체 생성
   ├─ SSL_do_handshake(): ClientHello/ServerHello 교환
   ├─ (비동기 시) WANT_READ/WRITE → 이벤트 큐 등록
   └─ 핸드셰이크 완료 → app에 session_connected 통지
      │
8. 애플리케이션 데이터 수신:
   ├─ tcp-input → TCP RX FIFO에 TLS 레코드 적재
   ├─ tls_ctx_read(): SSL_read() → 복호화
   ├─ 평문을 App RX FIFO에 적재
   └─ 애플리케이션에 rx 이벤트 통지
      │
9. HTTP 프록시: 평문 HTTP 요청 파싱
   ├─ 백엔드 서버로 TCP 연결 (평문)
   ├─ 요청 포워딩 → 응답 수신
   └─ 응답을 TLS 암호화하여 클라이언트에 전달
      │
10. 응답 전송:
    ├─ 애플리케이션이 App TX FIFO에 평문 기록
    ├─ tls_custom_tx_callback(): SSL_write() → 암호화
    ├─ TLS 레코드를 TCP TX FIFO에 적재
    └─ tcp-output → ip4-output → dpdk-output
VPP TLS 세션 생명 주기 상태 머신 NONE ctx_alloc() TCP연결 HANDSHAKE SSL_do_handshake() 비동기 시 WANT_READ/WRITE async resume 완료 ESTABLISHED ctx_write() / ctx_read() 양방향 암호화 데이터 전송 app_close APP_CLOSED close_notify 전송 원격 close PASSIVE_CLOSE 원격 close_notify 수신 잔여 데이터 드레인 CLOSED ctx_free() → 풀 반환 핸드셰이크 실패/타임아웃 TLS 컨텍스트 메모리 관리: per-worker 풀에서 할당/반환 (락 불필요) 핸드셰이크 타임아웃: 기본 20초 | 유휴 타임아웃: session { idle-timeout } | 최대 컨텍스트: session { preallocated-sessions }

TLS 디버깅

VPP TLS 문제를 진단하기 위한 CLI 명령과 기법:

# TLS 컨텍스트 상세 정보
vpp# show tls ctx verbose
[0] engine: openssl  state: established
    cipher: TLS_AES_256_GCM_SHA384  version: TLSv1.3
    app_session: 0x7f001234  tls_session: 0x7f005678
    bytes_in: 1048576  bytes_out: 524288
    handshake_time: 2.3ms

# TLS 에러 카운터
vpp# show errors
tls-handshake-fail                12
tls-cert-verify-fail               3
tls-session-timeout                45

# TLS 엔진별 통계
vpp# show tls engines
Engine: openssl
  Active contexts: 15234
  Handshakes completed: 892341
  Async operations pending: 42

# 세션 레이어 TLS 필터
vpp# show session verbose proto tls

# 트레이싱으로 TLS 핸드셰이크 관찰
vpp# trace add tls-input 10
vpp# show trace
증상진단 명령일반적 원인
핸드셰이크 실패show errors | grep tls인증서 만료, cipher 불일치
성능 저하show tls ctx verbose비동기 미활성, SW 암호화 병목
연결 끊김show session verbose세션 타임아웃, FIFO 오버플로
인증서 오류show tls certs키 불일치, CA 체인 누락
mTLS 거부trace add tls-input클라이언트 인증서 미제출
kTLS와의 비교: VPP TLS 디버깅은 유저스페이스에서 모든 상태를 직접 확인할 수 있어 커널 kTLS보다 훨씬 용이합니다. kTLS는 커널 로그와 bpftrace에 의존하지만, VPP는 CLI 한 줄로 모든 TLS 컨텍스트, 핸드셰이크 통계, 에러 카운터를 확인할 수 있습니다.

TLS 패킷 캡처와 복호화(Decryption)

VPP는 내장 pcap 기능을 제공하여 TLS 트래픽을 캡처하고, 개인 키를 이용해 Wireshark에서 복호화할 수 있습니다:

# VPP 내장 pcap 캡처
vpp# pcap trace rx tx max 10000 \
     intfc host-eth0 \
     file /tmp/vpp-tls-trace.pcap

# 캡처 중지
vpp# pcap trace off

# TLS 1.3 복호화를 위한 키 로그
# startup.conf에 설정:
# tls {
#   keylog-file /tmp/tls-keylog.txt
# }

# Wireshark에서 복호화:
# Edit → Preferences → Protocols → TLS
# → (Pre)-Master-Secret log filename: /tmp/tls-keylog.txt
# → 파일 → 열기 → /tmp/vpp-tls-trace.pcap

일반적 문제 해결 시나리오

문제진단 방법해결책
핸드셰이크 타임아웃show tls ctx에서 HANDSHAKE 상태 고착방화벽(Firewall) 규칙 확인, session { idle-timeout } 조정
"no shared cipher" 오류show errors에 tls-handshake-fail 증가set tls cipher로 양측 호환 cipher 설정
인증서 체인 오류trace add tls-input에서 verify 실패tls ca-cert add로 중간 CA 포함한 전체 체인 등록
메모리 부족show memory verbose에서 session 세그먼트 고갈session { evt_qs_memfd_seg_size } 증가, Hugepage 추가
비대칭 성능 (한쪽만 느림)show session verbose에서 FIFO 사용률 확인TX/RX FIFO 크기 조정: session { tx-fifo-size }
QAT 오프로드 미동작show dpdk crypto devices에 디바이스 없음QAT 드라이버 로드 확인, dpdk { dev PCI_ADDR } 설정
세션 누수show session 연결 수가 계속 증가애플리케이션에서 session_close() 호출 누락 확인
mTLS 클라이언트 거부show errors에 cert-verify-fail클라이언트 CA 인증서가 tls ca-cert에 등록되었는지 확인
# 종합 TLS 상태 진단 스크립트
vpp# show version
vpp# show tls engines
vpp# show tls certs
vpp# show tls ctx verbose
vpp# show session verbose
vpp# show errors
vpp# show memory verbose
vpp# show dpdk crypto devices  # QAT 사용 시

VPP CLI 및 API

VPP CLI (vppctl)

VPP는 유닉스 도메인 소켓(/run/vpp/cli.sock)을 통한 CLI를 제공합니다:

# VPP CLI 접속
$ vppctl
    _______    _        _   _____  ___
 __/ __/ _ \  (_)__    | | / / _ \/ _ \
 _/ _// // / / / _ \   | |/ / ___/ ___/
 /_/ /____(_)_/\___/   |___/_/  /_/

vpp# show version
vpp# show interface
vpp# show hardware-interfaces
vpp# show runtime               /* 노드별 성능 통계 */
vpp# show errors                /* 에러 카운터 */
vpp# show trace                 /* 패킷 트레이스 */

# 패킷 트레이싱 (디버깅)
vpp# trace add dpdk-input 100   /* DPDK 입력 100 패킷 추적 */
vpp# show trace                 /* 추적 결과 확인 */

# 비대화형 실행
$ vppctl show interface

Binary API

VPP Binary API는 공유 메모리를 통한 고성능 프로그래밍 인터페이스입니다. .api 파일에서 메시지를 정의하고, 코드 생성기가 C/Python/Go 바인딩을 자동 생성합니다:

/* my_plugin.api — API 메시지 정의 */
autoreply define my_plugin_enable_disable {
    u32 client_index;
    u32 context;
    bool enable_disable;
    vl_api_interface_index_t sw_if_index;
};

define my_plugin_details {
    u32 context;
    vl_api_interface_index_t sw_if_index;
    u64 packet_count;
};

VAPI (VPP API)

VAPI는 C/C++용 고수준 API 라이브러리로, Binary API 위에서 동기/비동기 호출, 콜백, 자동 직렬화(Serialization)를 제공합니다:

/* VAPI 사용 예제 (C) */
#include <vapi/vapi.h>
#include <vapi/vpe.api.vapi.h>

vapi_ctx_t ctx;
vapi_ctx_alloc(&ctx);
vapi_connect(ctx, "my_app", NULL,
             64,  /* max outstanding requests */
             32,  /* response queue depth */
             VAPI_MODE_BLOCKING, true);

/* 인터페이스 목록 조회 */
vapi_msg_sw_interface_dump *msg = vapi_alloc_sw_interface_dump(ctx);
msg->payload.name_filter_valid = false;
vapi_sw_interface_dump(ctx, msg, callback_fn, NULL);

vapi_disconnect(ctx);
vapi_ctx_free(ctx);

GoVPP / Python bindings

# Python (vpp_papi) 예제
from vpp_papi import VPPApiClient

vpp = VPPApiClient(apifiles=['/usr/share/vpp/api/core/*.api.json'])
vpp.connect('my-python-app')

# 인터페이스 목록
ifaces = vpp.api.sw_interface_dump()
for iface in ifaces:
    print(f"{iface.interface_name}: {iface.sw_if_index}")

# TAP 생성
rv = vpp.api.tap_create_v2(id=0, host_if_name='vpp-tap0',
                            host_ip4_prefix='192.168.1.1/24')

vpp.disconnect()
VPP API 아키텍처 CLI (vppctl) Python (papi) GoVPP (Go) VAPI (C/C++) Prometheus Binary API 공유 메모리 전송 /run/vpp/api.sock CLI: /run/vpp/cli.sock 자동 생성: .api → C/Go/Py Stats Segment 공유 메모리 (mmap) lockfree 읽기 전용 VPP Core API Message Handler CLI Handler Graph Node Engine (데이터 평면) Per-thread 통계 카운터 (lockfree) FIB / Session / Interface 테이블

Stats Segment

VPP의 Stats Segment는 통계 카운터를 외부 프로세스에 노출하는 공유 메모리 영역입니다. 배리어 없이 읽기 전용으로 접근할 수 있어, 모니터링 시스템이 VPP 성능에 영향을 주지 않습니다.

/* C에서 Stats Segment 접근 */
#include <vpp-api/client/stat_client.h>

stat_client_main_t *sm = stat_client_get();
stat_segment_connect("/run/vpp/stats.sock");

/* 패턴으로 카운터 조회 */
u8 **patterns = 0;
vec_add1(patterns, (u8 *)"/if/rx");
stat_segment_data_t *data = stat_segment_dump(patterns);

/* Python에서 접근 */
# from vpp_papi.vpp_stats import VPPStats
# stats = VPPStats(socketname='/run/vpp/stats.sock')
# print(stats['/if/rx'])
카운터 경로유형설명
/if/rxcombined인터페이스별 RX 패킷/바이트
/if/txcombined인터페이스별 TX 패킷/바이트
/if/dropssimple인터페이스별 드롭 패킷
/err/{node}/{error}simple노드별 에러 카운터
/sys/heartbeatscalarVPP 활성 여부 (증가하는 카운터)
/mem/statsegscalarStats Segment 메모리 사용량
/if/namesname_vector인터페이스 이름 목록
Prometheus/Grafana 연동: vpp_prometheus_export 플러그인 또는 외부 vpp-exporter가 Stats Segment를 Prometheus 메트릭으로 변환합니다. /if/rx, /if/tx, /err/* 카운터를 Grafana 대시보드로 시각화하여 실시간 모니터링이 가능합니다.

성능 최적화

VPP 성능 최적화 계층 하드웨어: NIC RSS, AES-NI, QAT, NUMA 토폴로지 커널: isolcpus, nohz_full, hugepages, VFIO DPDK: 큐 수, descriptor 수, RX burst size VPP: 버퍼 풀 크기, 워커 수, 벡터 크기, Feature 최소화 노드: quad-loop, 프리페치 거리, 불필요한 Feature 비활성 영향도 조정 용이 쉬움 아래 계층부터 최적화 → 위 계층은 하드웨어 교체 필요

CPU 핀닝과 워커 스레드

VPP 성능의 핵심은 CPU 코어 격리와 적절한 워커 스레드 배치입니다:

/* startup.conf — CPU 핀닝 */
cpu {
    main-core 0                    /* 메인 스레드 (API, CLI 처리) */
    corelist-workers 2,4,6,8       /* 워커: 물리 코어만 (HT 제외) */
    skip-cores 1                   /* 코어 1 건너뛰기 (OS용) */
}

# 커널 부팅 파라미터: VPP 코어를 OS 스케줄러에서 격리
GRUB_CMDLINE_LINUX="isolcpus=2,4,6,8 nohz_full=2,4,6,8 rcu_nocbs=2,4,6,8"
커널 관점: isolcpus는 지정 코어를 CFS 로드 밸런싱에서 제외합니다. nohz_full은 해당 코어의 스케줄러(Scheduler) 틱을 비활성화하여 인터럽트(Interrupt)를 최소화합니다. rcu_nocbs는 RCU 콜백을 다른 코어로 오프로드합니다. 이 조합이 VPP의 busy-polling 성능을 극대화합니다.

NUMA 인식

NIC과 VPP 워커 스레드를 동일 NUMA 노드에 배치하여 원격 메모리 접근(remote memory access)을 방지합니다:

# NIC의 NUMA 노드 확인
$ cat /sys/bus/pci/devices/0000:03:00.0/numa_node
0

# NUMA 노드 0의 CPU 확인
$ lscpu | grep "NUMA node0"
NUMA node0 CPU(s): 0-7

/* startup.conf — NUMA 인식 설정 */
buffers {
    buffers-per-numa 16384        /* NUMA 노드당 버퍼 수 */
    default data-size 2048
}

dpdk {
    dev 0000:03:00.0 {            /* NUMA 0에 위치한 NIC */
        num-rx-queues 4
    }
}

cpu {
    main-core 0                   /* NUMA 0 코어 */
    corelist-workers 2,4,6        /* NUMA 0 코어 */
}

인터럽트 모드 vs 폴링 모드

모드장점단점적용 시나리오
Polling (기본)최소 레이턴시, 최대 throughputCPU 100% 사용고성능 필수 환경
Interrupt유휴 시 CPU 절약인터럽트 오버헤드트래픽 변동이 큰 환경
Adaptive부하에 따라 자동 전환전환 지연 발생 가능범용 환경
/* startup.conf — 인터럽트 모드 활성화 */
dpdk {
    dev 0000:03:00.0 {
        num-rx-queues 4
    }
}

/* 적응형 모드: 유휴 시 sleep */
unix {
    poll-sleep-usec 100           /* 유휴 시 100μs sleep */
}

성능 벤치마킹

# VPP 내장 패킷 생성기로 벤치마크
vpp# packet-generator new {
    name pg0
    limit 10000000
    size 64-64
    interface pg0
    node ethernet-input
    data { IP4: 1.2.3 -> 4.5.6
           UDP: 192.168.1.1 -> 192.168.2.1
           UDP: 1234 -> 5678
           incrementing 100
    }
}
vpp# packet-generator enable

# 런타임 통계 확인
vpp# show runtime
# 출력:
# Name                 State    Calls   Vectors  Suspends  Clocks   Vectors/Call
# dpdk-input           polling  1000000 10000000 0         24.50    10.00
# ethernet-input       active   1000000 10000000 0         18.30    10.00
# ip4-input            active   1000000 10000000 0         15.20    10.00
# ip4-lookup           active   1000000 10000000 0         12.80    10.00

# TRex 외부 트래픽 생성기 사용
$ ./t-rex-64 -f stl/bench.py -m 10mpps -d 60

버퍼 풀 최적화

NIC 속도권장 buffers-per-numaHugepage 요구량비고
1 GbE8,192~32 MB (2M pages)저트래픽 환경
10 GbE32,768~128 MB일반적 프로덕션
25 GbE65,536~256 MB고트래픽 환경
40/100 GbE131,072+~512 MB+1G hugepage 권장
# 버퍼 풀 상태 확인
vpp# show buffers
 Thread             Name           Index    Size     Alloc    Free     #Alloc   #Free
      0 default-numa-0                0     2048     32768    31200     1568     31200
      1 default-numa-0                0     2048     32768    30800     1968     30800

# 부족 시 증상: show errors에서 no-buffer 증가
vpp# show errors
   Count  Node           Reason
   15230  dpdk-input      no-buffer         ← 버퍼 부족!
       0  dpdk-input      buffer-alloc-fail
버퍼 부족 증상: dpdk-inputno-buffer 에러가 증가하면 NIC에서 패킷을 가져오지 못합니다. buffers-per-numa를 늘리고, /proc/meminfo에서 HugePages_Free가 충분한지 확인하세요. 버퍼 한 개 = sizeof(vlib_buffer_t) + data-size ≈ 2,176바이트입니다.

perf/VPP 성능 카운터 분석

# perf stat로 VPP 워커의 CPU 카운터 측정
$ perf stat -e cycles,instructions,cache-misses,cache-references,\
branch-misses,branch-instructions -p $(pidof vpp) -t $(pgrep -P $(pidof vpp)) sleep 10

# VPP 내장 성능 카운터
vpp# show runtime
# Clocks/Call: 노드당 평균 CPU 사이클 (낮을수록 좋음)
# Vectors/Call: 호출당 평균 벡터 크기 (높을수록 좋음)

# 노드별 상세 통계
vpp# show runtime max
vpp# clear runtime
CPU 카운터정상 범위 (VPP)문제 징후대응
IPC (inst/cycle)2.0~3.5< 1.5메모리 바운드, 프리페치 확인
L1 I-cache miss%< 2%> 5%노드 그래프 단순화, Feature 최소화
L1 D-cache miss%< 5%> 10%CLIB_PREFETCH 추가, NUMA 확인
Branch miss%< 1%> 3%벡터 크기 확인, 조건 분기 최소화
LLC miss%< 1%> 3%워킹셋 크기 초과, FIB 크기 확인

디버깅 및 트러블슈팅

패킷 트레이싱

VPP의 패킷 트레이싱은 가장 강력한 디버깅 도구입니다. 패킷이 어떤 노드를 거쳐 어떤 결정을 받았는지 라인별로 확인할 수 있습니다.

# 트레이싱 활성화 (dpdk-input 노드에서 10개 패킷)
vpp# trace add dpdk-input 10

# 트래픽 발생 후 결과 확인
vpp# show trace

Packet 1

00:00:01:123456: dpdk-input
  GigabitEthernet0/8/0 rx queue 0
  buffer 0x9a340: current data 0, length 98, buffer-pool 0
  trace_handle 0x1000001
  l2-hdr-offset 0 ip4-hdr-offset 14

00:00:01:123458: ethernet-input              ← L2 분류
  IP4: fa:16:3e:aa:bb:cc -> fa:16:3e:dd:ee:ff

00:00:01:123459: ip4-input                   ← IPv4 처리 진입
  TCP: 192.168.1.100 -> 10.0.0.1
    tos 0x00, ttl 64, length 84, checksum 0x1234 dscp CS0 ecn NON_ECN
    fragment id 0x0001, flags DONT_FRAGMENT
  TCP: 45000 -> 80
    seq 0x12345678 ack 0x00000000
    flags [SYN] window 65535

00:00:01:123460: ip4-lookup                  ← FIB 룩업
  fib 0 dpo-idx 7 flow hash: 0x00001234
  TCP: 192.168.1.100 -> 10.0.0.1

00:00:01:123461: ip4-rewrite                 ← MAC 재작성
  tx_sw_if_index 2 dpo-idx 7
  adjacency rewrite: GigabitEthernet0/9/0
    dst aa:bb:cc:dd:ee:01 src fa:16:3e:xx:yy:zz

00:00:01:123462: GigabitEthernet0/9/0-output ← 인터페이스 출력
  GigabitEthernet0/9/0
  IP4: fa:16:3e:xx:yy:zz -> aa:bb:cc:dd:ee:01

00:00:01:123463: GigabitEthernet0/9/0-tx     ← TX 큐 전송
  buffer 0x9a340: current data -14, length 112
디버깅 시나리오trace add 대상확인 포인트
패킷이 VPP에 도달하지 않음dpdk-input 또는 af-xdp-inputRX 큐 수신 여부
라우팅 문제ip4-inputip4-lookup의 dpo-idx
NAT 변환 문제nat44-in2out-output세션 매칭, 주소 변환(Address Translation)
ACL 드롭acl-plugin-in-ip4-fa매치 규칙, deny 여부
IPsec 터널 문제ipsec-input-ip4SA 매칭, 복호화 에러
인터페이스 미출력interface-outputTX 큐 도달 여부

에러 카운터 분석

vpp# show errors
   Count   Node                  Reason
   15230   dpdk-input            no buffer available
     842   ip4-input             ip4 spoofed local-address packet drops
     156   ip4-lookup            ip4 destination lookup miss
      23   acl-plugin-in-ip4-fa  acl deny
       5   nat44-in2out          no translation
       2   ipsec-input-ip4       SA not found
에러노드원인해결
no bufferdpdk-input버퍼 풀 고갈buffers-per-numa 증가, hugepage 추가
ttl-exceededip4-inputTTL이 0에 도달라우팅 루프 확인
destination lookup missip4-lookupFIB에 경로 없음show ip fib로 경로 확인
adjacency incompleteip4-rewriteARP 미해석show ip neighbor, ARP 상태 확인
acl denyacl-pluginACL 규칙에 의한 차단show acl-plugin acl로 규칙 확인
no translationnat44NAT 세션/주소 풀 소진show nat44 sessions, 주소 풀 확인
SA not foundipsec-inputIPsec SA 매칭 실패show ipsec sa, SPI 확인
interface downinterface-output출력 인터페이스 downshow interface, admin up 확인
worker handoff dropshandoff핸드오프 큐 오버플로워커 부하 분산(Load Balancing), RSS 확인
dpdk driver init faildpdkNIC 바인딩 실패dpdk-devbind --status, VFIO/UIO 확인

PCAP 캡처

# 인입 패킷 PCAP 캡처
vpp# pcap trace rx max 1000 file rx-capture.pcap

# 송출 패킷 캡처
vpp# pcap trace tx max 1000 file tx-capture.pcap

# 드롭 패킷 캡처 (문제 진단에 가장 유용)
vpp# pcap trace drop max 1000 file drop-capture.pcap

# 특정 인터페이스만 캡처
vpp# pcap trace rx max 500 intfc GigabitEthernet0/8/0 file intf-rx.pcap

# 캡처 중지 및 저장
vpp# pcap trace off

# 파일 위치: /tmp/*.pcap
Wireshark 연계: /tmp/*.pcap 파일을 Wireshark에서 바로 열 수 있습니다. pcap trace drop은 VPP가 드롭한 패킷만 캡처하므로, "왜 패킷이 전달되지 않는가?"에 대한 직접적인 답을 제공합니다.

이벤트 로거와 코어 덤프(Core Dump)

VPP 디버깅 의사결정 트리 패킷이 전달되지 않음 1. show interface → RX 증가? No NIC/DPDK 확인 Yes 2. show errors → 에러 있음? Yes 에러 테이블 참조 No 3. trace add → 경로 추적 4. pcap trace drop → 드롭 패킷 Wireshark 분석 5. show ip fib / show ip neighbor 확인
# 이벤트 로거 활성화
vpp# event-logger resize 1000000
vpp# event-logger restart
vpp# event-logger save /tmp/elog.dat

# GDB로 VPP 디버깅 (디버그 빌드 필요)
$ gdb /usr/bin/vpp_main core.12345
(gdb) bt                          # 백트레이스
(gdb) thread apply all bt         # 모든 스레드 백트레이스
(gdb) frame 3                     # 특정 프레임으로 이동
(gdb) p *b                        # vlib_buffer_t 내용 출력

# startup.conf에서 코어 덤프 활성화
# unix { full-coredump }
# coredump 크기 제한 해제: ulimit -c unlimited
코어 덤프 분석: VPP 코어 덤프 크기가 수 GB에 달할 수 있습니다(hugepage 매핑 포함). coredump_filter를 조정하여 hugepage를 제외하면 코어 크기를 줄일 수 있습니다: echo 0x33 > /proc/$(pidof vpp)/coredump_filter

자주 발생하는 문제와 해결

문제증상진단 방법해결
버퍼 고갈RX 드롭 급증show errors no-bufferbuffers-per-numa 증가
DPDK 초기화 실패VPP 시작 실패/var/log/vpp/vpp.logVFIO/UIO 바인딩, IOMMU 확인
Hugepage 부족VPP 시작 실패 또는 OOMcat /proc/meminfohugepage 추가 할당
워커 정지특정 큐 처리 중단show runtime Suspends배리어 장기 홀드 확인
ARP 미해석adjacency incompleteshow ip neighbor연결성 확인, proxy-arp
NAT 세션 소진no translation 에러show nat44 sessions세션 한도/타이머 조정
RSS 불균형일부 워커만 과부하show runtime per-workerRSS 해시 키 변경, handoff
MTU 불일치패킷 드롭/단편화(Fragmentation)show hardware인터페이스 MTU 통일
CLI 소켓 거부vppctl 연결 실패소켓 권한 확인api-segment { gid vpp }
플러그인 충돌VPP 크래시코어 덤프 분석문제 플러그인 비활성, 버전 확인

보안 고려사항

API 소켓 보안

접근 제어(Access Control)설정설명
소켓 그룹api-segment { gid vpp }API 소켓을 vpp 그룹만 접근
소켓 경로cli-listen /run/vpp/cli.sockCLI 소켓 위치 제어
API 추적api-trace { on }모든 API 호출 로깅
메시지 필터API 메시지별 권한특정 API만 허용 (플러그인 개발)
/* 보안 API 설정 예제 (startup.conf) */
unix {
    cli-listen /run/vpp/cli.sock   /* 로컬 유닉스 소켓만 (TCP 금지) */
    gid vpp                         /* vpp 그룹만 CLI 접근 */
}

api-segment {
    gid vpp                         /* API 세그먼트 그룹 제한 */
    api-pvt-heap-size 64M
}

api-trace {
    on                              /* API 호출 감사 추적 */
    save-api-table /tmp/api-table   /* API 테이블 저장 */
}

DPDK/VFIO 보안 격리

UIO vs VFIO 보안: uio_pci_generic은 IOMMU 없이 NIC에 직접 DMA 접근을 허용합니다. 악의적인 VPP 프로세스나 NIC 펌웨어(Firmware)가 임의 메모리를 읽고 쓸 수 있습니다. 프로덕션에서는 반드시 VFIO-PCI를 사용하세요. VFIO는 IOMMU를 통해 DMA 영역을 제한하여 디바이스 격리를 보장합니다.

네트워크 보안 기능

보안 기능플러그인/노드설명
ACLacl_pluginStateful/Stateless L3/L4 패킷 필터링
COPcop_plugin소스 IP 화이트리스트 (uRPF 유사)
RPFip4-source-checkReverse Path Forwarding 검증
Rate Limitingpolicer2r3c 기반 트래픽 폴리싱
IPsecipsec_pluginESP/AH 기반 터널/전송 모드 암호화
WireGuardwireguard_pluginWireGuard VPN (ChaCha20)
TLStlsopenssl/tlsmbedtlsTLS 종단 (세션 레이어 연동)
DDoS 방어flowprobe + 외부IPFIX 내보내기로 외부 DDoS 탐지 연동

설치 및 설정

패키지 설치

# Ubuntu/Debian — FD.io 공식 저장소
$ curl -s https://packagecloud.io/install/repositories/fdio/release/script.deb.sh | sudo bash
$ sudo apt-get install vpp vpp-plugin-core vpp-plugin-dpdk vpp-dev

# CentOS/RHEL
$ curl -s https://packagecloud.io/install/repositories/fdio/release/script.rpm.sh | sudo bash
$ sudo yum install vpp vpp-plugins vpp-devel

# 서비스 시작
$ sudo systemctl start vpp
$ sudo systemctl enable vpp

소스 빌드

# 소스 클론
$ git clone https://gerrit.fd.io/r/vpp
$ cd vpp

# 의존성 설치 (Ubuntu)
$ make install-dep
$ make install-ext-deps

# 빌드
$ make build             /* 디버그 빌드 */
$ make build-release     /* 릴리스 빌드 */

# 실행
$ make run               /* 빌드 디렉터리에서 직접 실행 */

startup.conf 설정

/* /etc/vpp/startup.conf — 프로덕션 설정 예제 */

unix {
    nodaemon                       /* 포그라운드 실행 (디버깅 시) */
    cli-listen /run/vpp/cli.sock   /* CLI 소켓 경로 */
    log /var/log/vpp/vpp.log       /* 로그 파일 */
    full-coredump                  /* 코어 덤프 활성화 */
    exec /etc/vpp/setup.gate       /* 시작 시 실행할 CLI 스크립트 */
}

api-trace {
    on                             /* API 추적 활성화 */
}

api-segment {
    gid vpp                        /* API 소켓 그룹 */
}

cpu {
    main-core 0
    corelist-workers 2,4,6,8
}

dpdk {
    dev default {
        num-rx-queues 4
        num-tx-queues 4
        num-rx-desc 1024
        num-tx-desc 1024
    }
    dev 0000:03:00.0
    dev 0000:03:00.1
    uio-driver vfio-pci            /* VFIO 사용 */
}

buffers {
    buffers-per-numa 32768
    default data-size 2048
}

plugins {
    plugin default { disable }     /* 기본 비활성화 */
    plugin dpdk_plugin.so { enable }
    plugin acl_plugin.so { enable }
    plugin nat_plugin.so { enable }
    plugin linux_cp_plugin.so { enable }
}

활용 사례

가상 스위치 (vSwitch)

VPP는 데이터센터의 가상 스위치로 활용됩니다. VM 간 트래픽을 vhost-user 인터페이스로 고성능 전달하며, OvS-DPDK 대비 더 높은 pps 성능을 제공합니다. Kubernetes 환경에서는 Calico/VPP CNI 플러그인으로 Pod 네트워킹을 가속합니다.

CPE/VNF

통신사의 고객 구내 장비(CPE)를 소프트웨어로 구현하는 VNF(Virtual Network Function)에 VPP가 사용됩니다. 라우팅, NAT, IPsec, QoS를 단일 VPP 인스턴스에서 처리하여 전용 하드웨어를 대체합니다.

5G UPF

5G 코어 네트워크의 UPF(User Plane Function)에서 GTP-U 터널 종단, PDU 세션 관리, SRv6 기반 모바일 유저플레인을 VPP로 구현합니다.

5G UPF 아키텍처 (VPP 기반) gNB 기지국 N3 GTP-U UPF (VPP) GTP-U 디캡슐레이션 PDR/FAR 매칭 QoS / 폴리싱 SRv6 Mobile URR (사용량 보고) PFCP (N4 제어) N6 DN 인터넷/엣지 N9 (UPF 간) SMF (N4)
# GTP-U 터널 설정 (upf 플러그인)
vpp# upf pfcp association setup-params
vpp# upf nwi create name internet
vpp# create upf application name internet

# 또는 travelping/upg-vpp 사용 시
vpp# upf gtpu endpoint ip 10.200.0.1 nwi access teid-range-timeout 30
오픈소스 5G UPFVPP 플러그인PFCP 지원비고
Travelping UPGupg-vpp (커스텀)완전프로덕션 검증, 상용 지원
free5GC UPFgtp-u (외부)부분Go 기반 제어 + VPP 데이터
Open5GS없음 (커널 GTP)완전VPP 미사용 (참고용)
OAI-CN5G없음 (eBPF/커널)부분연구용

Kubernetes CNI 통합

Calico/VPP — Kubernetes CNI 아키텍처 Kubernetes Node Pod A Pod B Pod C TAP VPP (Calico dataplane) L3 라우팅 | ACL (NetworkPolicy) | IPsec | memif DPDK 또는 AF_XDP | BGP (Calico agent) Physical NIC (25/100GbE) Remote Node Pod D VPP IPsec 터널 (노드 간 암호화) IPsec
# Calico/VPP CNI 설치 (Helm)
$ helm repo add projectcalico https://docs.tigera.io/calico/charts
$ helm install calico projectcalico/tigera-operator \
    --set installation.cni.type=Calico \
    --set installation.calicoNetwork.linuxDataplane=VPP
CNI데이터플레인암호화성능 (Pod 간)비고
Calico/VPPVPP (DPDK)IPsec (VPP)~25 Gbps+최고 성능, memif 지원
Calico/eBPFeBPFWireGuard~15 Gbps커널 내 처리, 범용
CiliumeBPFWireGuard/IPsec~15 GbpsL7 관측성 우수
OVN-KubernetesOvSIPsec~10 GbpsOpenShift 기본
Contiv-VPP: 초기 VPP 기반 CNI 프로젝트였으나 현재 유지보수가 중단되었습니다. Kubernetes VPP CNI는 Calico/VPP가 사실상 표준입니다.

로드 밸런서

VPP의 lb 플러그인은 Maglev 해시 기반 L3/L4 로드 밸런서를 제공합니다. GRE 또는 VXLAN으로 백엔드 서버에 패킷을 분산합니다:

vpp# lb vip 10.0.0.100/32 protocol tcp port 80 encap gre4
vpp# lb as 10.0.0.100/32 protocol tcp port 80 192.168.1.10
vpp# lb as 10.0.0.100/32 protocol tcp port 80 192.168.1.11

CSIT 테스팅 프레임워크

CSIT 개요

CSIT(Continuous System Integration and Testing)은 FD.io 프로젝트의 공식 테스트 프레임워크입니다. VPP의 기능 검증, 성능 측정, 회귀 테스트를 자동화합니다.

테스트 유형목적환경도구
Functional기능 정확성 검증VM / 컨테이너(Container)Robot Framework + PAPI
Performance처리량/레이턴시 측정전용 물리 서버TRex + Robot Framework
DeviceNIC 호환성 테스트다양한 NIC 장착 서버DPDK testpmd + VPP
make test단위 테스트로컬 빌드 환경Python unittest + scapy

로컬 테스트 실행

# VPP 소스에서 단위 테스트 실행
$ cd vpp
$ make test                        /* 전체 단위 테스트 */
$ make test TEST=test_ip4          /* 특정 테스트만 */
$ make test-debug                  /* 디버그 빌드 테스트 */
$ make test V=2                    /* 상세 출력 */

# CSIT 기능 테스트 실행 (Docker 필요)
$ git clone https://gerrit.fd.io/r/csit
$ cd csit
$ ./bootstrap-verify-perf.sh       /* 성능 테스트 */
make test: VPP의 make test는 Python + scapy 기반 단위 테스트입니다. VPP 인스턴스를 자동으로 시작하고, scapy로 패킷을 주입하여 출력을 검증합니다. 플러그인 개발 시 test/test_my_plugin.py를 작성하면 CI에서 자동 검증됩니다.

실전 예제: L3 라우터 + NAT44 구성

VPP를 이용하여 DPDK 기반 L3 라우터NAT44를 결합한 실제 운영 환경 구성입니다. 물리 NIC 2개를 DPDK에 바인딩하고, LAN → WAN 트래픽에 NAT을 적용하는 전형적인 엣지 라우터 시나리오입니다.

VPP L3 라우터 + NAT44 데이터 흐름 LAN (192.168.1.0/24) 호스트 192.168.1.10 호스트 192.168.1.20 VPP L3 라우터 GigabitEthernet0/8/0 192.168.1.1/24 dpdk-input → ethernet-input → ip4-input ip4-lookup → nat44-in2out-slowpath ip4-rewrite → GigabitEthernet0/9/0-output GigabitEthernet0/9/0 203.0.113.1/24 NAT44: 192.168.1.x → 203.0.113.1 WAN / Internet 203.0.113.0/24 LAN 호스트 → DPDK 수신 → IP 룩업 → NAT 변환 → WAN 송신

startup.conf 구성

# /etc/vpp/startup.conf — L3 라우터 + NAT44 실전 구성
unix {
    cli-listen /run/vpp/cli.sock
    log /var/log/vpp/vpp.log
    full-coredump
    gid vpp
}

api-trace { on }
api-segment { gid vpp }

dpdk {
    dev 0000:00:08.0 {                 /* LAN NIC */
        name GigabitEthernet0/8/0
        num-rx-queues 2
    }
    dev 0000:00:09.0 {                 /* WAN NIC */
        name GigabitEthernet0/9/0
        num-rx-queues 2
    }
    no-multi-seg                        /* 단일 세그먼트 버퍼 (성능 향상) */
    no-tx-checksum-offload
}

cpu {
    main-core 0
    corelist-workers 1-3               /* 워커 3개 (RSS 큐 분산) */
}

buffers {
    buffers-per-numa 32768
    default data-size 2048
}

plugins {
    plugin default { disable }
    plugin dpdk_plugin.so { enable }
    plugin nat_plugin.so { enable }
    plugin acl_plugin.so { enable }
    plugin ping_plugin.so { enable }
}

L3 라우팅 + NAT44 설정

# 인터페이스 설정
vpp# set interface ip address GigabitEthernet0/8/0 192.168.1.1/24
vpp# set interface ip address GigabitEthernet0/9/0 203.0.113.1/24
vpp# set interface state GigabitEthernet0/8/0 up
vpp# set interface state GigabitEthernet0/9/0 up

# 기본 라우팅 (WAN 게이트웨이)
vpp# ip route add 0.0.0.0/0 via 203.0.113.254 GigabitEthernet0/9/0

# NAT44 활성화
vpp# nat44 plugin enable sessions 65536
vpp# nat44 add interface address GigabitEthernet0/9/0
vpp# set interface nat44 in GigabitEthernet0/8/0 out GigabitEthernet0/9/0

# 포트 포워딩: 외부 TCP 8080 → 내부 서버 192.168.1.100:80
vpp# nat44 add static mapping local 192.168.1.100 80 \
     external GigabitEthernet0/9/0 8080 tcp

# ACL: 외부에서 들어오는 SSH 차단
vpp# set acl-plugin acl deny proto 6 dport 22
vpp# set acl-plugin interface GigabitEthernet0/9/0 input acl 0

# 검증
vpp# show ip fib
vpp# show nat44 sessions
vpp# show interface addr
vpp# show acl-plugin acl
linux-cp 연동: VPP 라우터에서 호스트 OS의 라우팅 테이블과 동기화하려면 linux-cp 플러그인을 활성화하세요. create linux-cp lcp GigabitEthernet0/8/0 host-if vpp-lan으로 미러 인터페이스를 생성하면 FRR/BIRD 같은 라우팅 데몬과 연동할 수 있습니다.

실전 예제: memif 서비스 체이닝

VPP의 memif(memory interface)는 공유 메모리 기반 인터페이스로, VPP 인스턴스 간 또는 VPP-DPDK 앱 간 제로카피 패킷 전달을 제공합니다. 이를 활용하면 여러 네트워크 기능(방화벽, DPI, NAT 등)을 체이닝하여 서비스 펑션 체인(SFC)을 구성할 수 있습니다.

memif 기반 서비스 펑션 체이닝 (SFC) NIC RX DPDK VPP #1 (분류기) dpdk-input classify-table memif1/0 → tx memif2/0 → tx VPP #2 (방화벽) memif1/0 → ACL memif3/0 → tx VPP #3 (DPI+NAT) memif2/0 → DPI nat44 → memif4/0 VPP #4 (이그레스) memif3/0 + memif4/0 ip4-rewrite dpdk-output NIC TX DPDK memif1 memif2 memif3 memif4 memif: 공유 메모리 기반 제로카피 인터페이스 — VPP 인스턴스 간 최대 100Gbps+ 전달 가능 각 VPP 인스턴스는 독립 프로세스로 실행되며, 장애 격리와 독립 스케일링 가능

memif 소켓 및 인터페이스 구성

# VPP #1 (분류기) — memif master 역할
vpp1# create memif socket id 1 filename /run/vpp/memif-fw.sock
vpp1# create memif socket id 2 filename /run/vpp/memif-dpi.sock
vpp1# create interface memif id 0 socket-id 1 master
vpp1# create interface memif id 0 socket-id 2 master
vpp1# set interface state memif1/0 up
vpp1# set interface state memif2/0 up

# VPP #2 (방화벽) — memif slave 역할
vpp2# create memif socket id 1 filename /run/vpp/memif-fw.sock
vpp2# create memif socket id 3 filename /run/vpp/memif-egress-fw.sock
vpp2# create interface memif id 0 socket-id 1 slave
vpp2# create interface memif id 0 socket-id 3 master
vpp2# set interface state memif1/0 up
vpp2# set interface state memif3/0 up

# L2 cross-connect (방화벽 통과 후 이그레스로)
vpp2# set interface l2 xconnect memif1/0 memif3/0
vpp2# set interface l2 xconnect memif3/0 memif1/0

# VPP #3 (DPI+NAT) — 유사하게 memif slave로 구성
vpp3# create memif socket id 2 filename /run/vpp/memif-dpi.sock
vpp3# create interface memif id 0 socket-id 2 slave
# ... NAT 설정 생략 (L3 NAT 예제 참조)

# VPP #4 (이그레스) — 여러 memif에서 수신, DPDK로 송신
vpp4# create memif socket id 3 filename /run/vpp/memif-egress-fw.sock
vpp4# create memif socket id 4 filename /run/vpp/memif-egress-dpi.sock
vpp4# create interface memif id 0 socket-id 3 slave
vpp4# create interface memif id 0 socket-id 4 slave
소켓 권한: memif 소켓 파일은 양쪽 VPP 프로세스가 모두 접근할 수 있어야 합니다. 서로 다른 사용자로 실행 시 chmod 770 또는 공통 그룹(gid vpp) 설정이 필요합니다. 소켓 경로의 디렉터리에도 실행 권한이 있어야 합니다.
memif 성능 팁: ring-size 2048buffer-size 2048로 생성하면 64B 패킷 기준 단일 memif 쌍에서 30Mpps 이상의 처리량을 달성할 수 있습니다. show memif로 링 사용률과 오류 카운터를 확인하세요.

실전 예제: IPsec VPN 게이트웨이

VPP는 하드웨어 가속(AES-NI, QAT)을 활용한 고성능 IPsec VPN 게이트웨이를 제공합니다. 커널 IPsec 대비 5~10배 높은 처리량을 달성할 수 있어, 데이터센터 간 암호화 터널이나 원격 사이트 VPN에 적합합니다.

Site-to-Site IPsec 터널 구성

# === 사이트 A (VPP GW: 203.0.113.1) ===

# 1. IPsec 터널 인터페이스 생성
vpp# create ipsec tunnel local-ip 203.0.113.1 remote-ip 198.51.100.1 \
     local-spi 1000 remote-spi 2000 \
     local-crypto-key 6162636465666768696a6b6c6d6e6f70 \
     remote-crypto-key 7172737475767778797a414243444546 \
     crypto-alg aes-gcm-256 \
     instance 0

# 2. 터널 인터페이스 활성화 및 IP 할당
vpp# set interface state ipsec0 up
vpp# set interface ip address ipsec0 10.10.10.1/30

# 3. 원격 사이트 서브넷 라우팅
vpp# ip route add 172.16.0.0/16 via 10.10.10.2 ipsec0

# 4. 검증
vpp# show ipsec tunnel
vpp# show ipsec sa
vpp# ping 10.10.10.2

# === 사이트 B (VPP GW: 198.51.100.1) ===

# 대칭 설정 (SPI 반대, 키 반대)
vpp# create ipsec tunnel local-ip 198.51.100.1 remote-ip 203.0.113.1 \
     local-spi 2000 remote-spi 1000 \
     local-crypto-key 7172737475767778797a414243444546 \
     remote-crypto-key 6162636465666768696a6b6c6d6e6f70 \
     crypto-alg aes-gcm-256 \
     instance 0

vpp# set interface state ipsec0 up
vpp# set interface ip address ipsec0 10.10.10.2/30
vpp# ip route add 192.168.0.0/16 via 10.10.10.1 ipsec0

IKEv2 동적 키 교환

# IKEv2 프로파일 생성 (정적 키 대신 동적 협상)
vpp# ikev2 profile add pr1
vpp# ikev2 profile set pr1 auth shared-key-mic string MySharedSecret123
vpp# ikev2 profile set pr1 id local ip4-addr 203.0.113.1
vpp# ikev2 profile set pr1 id remote ip4-addr 198.51.100.1
vpp# ikev2 profile set pr1 traffic-selector local \
     ip-range 192.168.0.0 - 192.168.255.255 port-range 0 - 65535 protocol 0
vpp# ikev2 profile set pr1 traffic-selector remote \
     ip-range 172.16.0.0 - 172.16.255.255 port-range 0 - 65535 protocol 0

# 암호화 알고리즘 제안
vpp# ikev2 profile set pr1 proposals crypto-alg aes-cbc-256 \
     integ-alg sha-512 dh-group modp-2048

# IKE 세션 시작
vpp# ikev2 initiate sa-init pr1
vpp# show ikev2 sa
QAT 가속: Intel QAT(QuickAssist Technology) 카드가 있으면 dpdk_plugin.so의 cryptodev 설정으로 하드웨어 암호화 오프로딩이 가능합니다. startup.confdpdk { dev 0000:XX:XX.0 { name cryptodev0 } }를 추가하고, set crypto handler all dpdk로 활성화하세요.

흔한 실수와 안티패턴

VPP 운영 시 자주 발생하는 실수와 권장 해결 방법을 정리합니다. 특히 초기 구축 시 환경 설정 오류가 대부분의 장애 원인입니다.

Hugepage 설정 누락/부족

증상원인해결 방법
VPP 시작 실패: rte_eal_init errorHugepage 미할당커널 부트 파라미터에 hugepages=2048 추가
VPP 시작 후 버퍼 부족 경고Hugepage 부족워커 수 × 4GB 이상 확보 (NUMA 당)
DPDK 인터페이스 생성 실패1G hugepage만 할당, 2M 미할당hugeadm --pool-pages-min 2MB:2048
NUMA 편향 성능 저하NIC NUMA와 hugepage NUMA 불일치NIC NUMA 노드에 hugepage 분배: echo 1024 > /sys/devices/system/node/node0/hugepages/.../nr_hugepages
# Hugepage 상태 확인
$ cat /proc/meminfo | grep -i huge
HugePages_Total:    2048
HugePages_Free:     1536
HugePages_Rsvd:        0
Hugepagesize:       2048 kB

# 부팅 시 영구 설정 (GRUB)
GRUB_CMDLINE_LINUX="default_hugepagesz=2M hugepagesz=2M hugepages=2048 \
    hugepagesz=1G hugepages=4 iommu=pt intel_iommu=on"

# 런타임 할당 (재부팅 없이)
$ echo 2048 | sudo tee /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
$ sudo mkdir -p /dev/hugepages
$ sudo mount -t hugetlbfs none /dev/hugepages

DPDK 인터페이스 바인딩 오류

드라이버IOMMU 필요장점주의사항
vfio-pci (권장)예 (intel_iommu=on)DMA 격리, 보안, SR-IOV 지원IOMMU 미활성 시 바인딩 실패
uio_pci_generic아니요IOMMU 없는 환경에서 동작보안 격리 없음, root 필요
igb_uio (DPDK)아니요레거시 NIC 호환별도 모듈 빌드 필요, 유지보수 중단 추세
# 잘못된 예: IOMMU 없이 vfio-pci 바인딩 시도
$ sudo dpdk-devbind.py --bind=vfio-pci 0000:00:08.0
# Error: Cannot bind to driver vfio-pci: No IOMMU group

# 올바른 해결 방법 1: IOMMU 활성화
# GRUB에 intel_iommu=on iommu=pt 추가 후 재부팅

# 올바른 해결 방법 2: IOMMU 없는 환경에서 vfio 사용
$ sudo modprobe vfio enable_unsafe_noiommu_mode=1
$ echo 1 | sudo tee /sys/module/vfio/parameters/enable_unsafe_noiommu_mode
$ sudo dpdk-devbind.py --bind=vfio-pci 0000:00:08.0

# NIC 현재 상태 확인
$ dpdk-devbind.py --status
Network devices using DPDK-compatible driver
============================================
0000:00:08.0 'Virtio network device' drv=vfio-pci

Network devices using kernel driver
====================================
0000:00:03.0 'Virtio network device' drv=virtio-pci
커널 드라이버 복귀 실패: DPDK에 바인딩된 NIC를 커널로 돌리려면 dpdk-devbind.py --bind=virtio-pci 0000:00:08.0을 사용합니다. 관리 인터페이스(SSH 접속용)를 실수로 DPDK에 바인딩하면 원격 접속이 끊어지므로 반드시 OOB(Out-of-Band) 관리 인터페이스를 별도 확보하세요.

startup.conf 워커 스레드 / CPU 핀닝 실수

실수 패턴증상올바른 설정
main-corecorelist-workers 중복메인/워커 경합, 불안정겹치지 않는 CPU 코어 할당
하이퍼스레딩 형제 코어에 워커 배치캐시 경합으로 성능 50% 저하물리 코어만 사용: lscpu -e로 확인
NUMA 노드 교차 배치원격 메모리 접근 레이턴시NIC와 같은 NUMA 노드의 코어 사용
워커 수 > RSS 큐 수일부 워커 유휴num-rx-queues와 워커 수 일치
isolcpus 미적용OS 프로세스가 VPP 코어에 스케줄됨커널 부트 파라미터: isolcpus=1-3
# CPU 토폴로지 확인
$ lscpu -e
CPU NODE SOCKET CORE L1d L2 L3 ONLINE
  0    0      0    0   0  0  0      yes  ← main-core (관리용)
  1    0      0    1   1  1  0      yes  ← worker 0
  2    0      0    2   2  2  0      yes  ← worker 1
  3    0      0    3   3  3  0      yes  ← worker 2
  4    0      0    0   0  0  0      yes  ← HT sibling of core 0 (사용 금지)

# NIC NUMA 노드 확인
$ cat /sys/bus/pci/devices/0000:00:08.0/numa_node
0

# 올바른 startup.conf
cpu {
    main-core 0                        /* 관리 전용 */
    corelist-workers 1-3               /* NIC와 같은 NUMA, HT 제외 */
    skip-cores 0                       /* 기본값 유지 */
}

# 커널 부트 파라미터 (VPP 코어 격리)
GRUB_CMDLINE_LINUX="isolcpus=1-3 nohz_full=1-3 rcu_nocbs=1-3"

벡터 크기와 배치 처리 오해

VPP의 벡터 처리 모델에서 벡터 크기(VLIB_FRAME_SIZE)는 한 번에 처리되는 패킷 묶음의 크기입니다. 벡터가 클수록 I-cache 효율이 높아지지만, 레이턴시는 증가합니다.

오해실제
벡터 크기가 클수록 항상 좋다처리량은 증가하지만 레이턴시도 증가. 실시간 트래픽(VoIP 등)에서는 VLIB_FRAME_SIZE를 줄이는 것이 유리
벡터 크기를 임의로 조절할 수 있다입력 노드의 수신 버스(Bus)트 크기(DPDK rx-burst)가 실질적 벡터 크기 결정. show runtime의 Vectors/Call로 확인
모든 노드가 동일한 벡터 크기를 처리분기 노드(classify, ACL)에서 벡터가 분할될 수 있음. show runtime에서 노드별 Vectors/Call 차이 확인
패킷이 없으면 CPU를 쉰다기본 DPDK 폴링은 항상 100% CPU 사용. poll-sleep-poll 또는 interrupt mode로 완화 가능 (아래 성능 최적화 참조)
벡터 크기 모니터링: show runtimeVectors/Call 값이 1에 가까우면 패킷이 산발적으로 도착하는 것이므로 벡터 처리의 이점이 줄어듭니다. 이 경우 배치 크기를 늘리거나(num-rx-desc 2048), 트래픽 패턴을 분석하여 폴링 주기를 조정하세요.

memif 연결 시 소켓 권한 문제

# 흔한 오류: Permission denied
vpp# create interface memif id 0 socket-id 1 slave
# create memif: connect error (Permission denied)

# 원인 진단
$ ls -la /run/vpp/
srwxrwx--- 1 root root 0 memif-chain.sock     ← 그룹 접근 필요

# 해결: 공통 그룹 사용
$ sudo groupadd vpp-memif
$ sudo usermod -aG vpp-memif vpp-user1
$ sudo usermod -aG vpp-memif vpp-user2

# startup.conf에서 그룹 지정
unix {
    gid vpp-memif
}

# 소켓 디렉터리 권한 확인 (실행 권한 필수)
$ sudo chmod 2770 /run/vpp/
$ sudo chgrp vpp-memif /run/vpp/

# 연결 상태 확인
vpp# show memif
interface memif1/0
  socket-id 1 id 0 mode ethernet
  status: connected                     ← 정상
  ring 0: region 0 num-s 2048 ...
  link up

VPP 운영 가이드

프로덕션 환경에서 VPP를 안정적으로 운영하려면 무중단 설정 변경, 재시작 절차, 고가용성 구성, 용량 산정, 로깅 체계를 체계적으로 관리해야 합니다. 이 섹션에서는 실무에서 반드시 알아야 할 운영 지침을 다룹니다.

무중단 설정 변경

VPP는 많은 설정을 런타임에 변경할 수 있지만, 일부 설정은 프로세스 재시작이 필요합니다. 운영 중 서비스 중단 없이 변경 가능한 항목과 그렇지 않은 항목을 명확히 구분해야 합니다.

설정 항목런타임 변경재시작 필요비고
인터페이스 IP 주소Oset interface ip address
라우팅 테이블 (route 추가/삭제)Oip route add/del
ACL 규칙Oacl-plugin CLI
NAT44 매핑 추가/삭제Onat44 add static mapping
인터페이스 생성 (tap, memif 등)O새 인터페이스 동적 생성 가능
로그 레벨 변경Oset logging class
Hugepage 크기/수량Ostartup.conf buffers 섹션
워커 스레드 수Ostartup.conf cpu 섹션
DPDK 인터페이스 바인딩Ostartup.conf dpdk 섹션
메인 코어 지정Ostartup.conf cpu 섹션
플러그인 로드/언로드Ostartup.conf plugins 섹션
소켓 경로 변경Ostartup.conf unix 섹션
# 안전한 런타임 설정 변경 예시

# 1) 인터페이스 IP 변경
vpp# set interface state GigabitEthernet0/8/0 up
vpp# set interface ip address GigabitEthernet0/8/0 192.168.1.1/24

# 2) 라우팅 테이블 업데이트
vpp# ip route add 10.0.0.0/8 via 192.168.1.254
vpp# ip route del 172.16.0.0/12 via 192.168.1.254

# 3) NAT44 매핑 추가
vpp# nat44 add static mapping local 10.0.0.100 22 external 203.0.113.50 2222 tcp
vpp# show nat44 static mappings

# 4) ACL 규칙 동적 적용
vpp# set acl-plugin acl permit src 10.0.0.0/24 dst 0.0.0.0/0
vpp# set acl-plugin interface GigabitEthernet0/8/0 input acl 0

exec 명령을 사용하면 여러 CLI 명령을 하나의 스크립트 파일로 묶어 원자적으로 실행할 수 있습니다. 개별 명령을 하나씩 입력하는 것보다 설정 오류를 줄이고, 일관된 상태를 보장합니다.

# /etc/vpp/reconfigure-nat.cli — NAT 재구성 배치 스크립트
nat44 del static mapping local 10.0.0.100 22 external 203.0.113.50 2222 tcp
nat44 add static mapping local 10.0.0.200 22 external 203.0.113.50 2222 tcp
nat44 add static mapping local 10.0.0.201 80 external 203.0.113.51 80 tcp
nat44 add static mapping local 10.0.0.201 443 external 203.0.113.51 443 tcp

# VPP CLI에서 배치 실행
vpp# exec /etc/vpp/reconfigure-nat.cli

# 결과 확인
vpp# show nat44 static mappings
위험한 변경 주의: 인터페이스 삭제(delete interface)는 해당 인터페이스에 연결된 모든 세션, 라우팅, NAT 매핑을 즉시 제거합니다. 프로덕션에서는 먼저 set interface state ... down으로 비활성화하고 트래픽이 다른 경로로 전환된 것을 확인한 후 삭제하세요.

우아한 재시작과 설정 영속성

VPP는 자체적인 설정 영속성 메커니즘을 제공하지 않습니다. startup.confexec 지시문을 통해 부팅 시 CLI 스크립트를 자동 실행하여 원하는 상태를 재구성합니다.

# /etc/vpp/startup.conf — exec 스크립트 지정
unix {
    nodaemon
    log /var/log/vpp/vpp.log
    full-coredump
    cli-listen /run/vpp/cli.sock
    exec /etc/vpp/setup.gate           ← 부팅 시 자동 실행
}

cpu {
    main-core 0
    corelist-workers 1-3
}

buffers {
    buffers-per-numa 16384
    default data-size 2048
}

dpdk {
    dev 0000:00:08.0
}
# /etc/vpp/setup.gate — 부팅 시 자동 실행 스크립트
# 순서가 중요합니다: 인터페이스 → IP → 라우팅 → NAT → ACL

# 1단계: 인터페이스 활성화
set interface state GigabitEthernet0/8/0 up
set interface state GigabitEthernet0/9/0 up

# 2단계: IP 주소 할당
set interface ip address GigabitEthernet0/8/0 192.168.1.1/24
set interface ip address GigabitEthernet0/9/0 10.0.0.1/24

# 3단계: 라우팅 설정
ip route add 0.0.0.0/0 via 192.168.1.254
ip route add 172.16.0.0/12 via 10.0.0.254

# 4단계: NAT44 설정
nat44 plugin enable sessions 131072
set interface nat44 in GigabitEthernet0/9/0 out GigabitEthernet0/8/0
nat44 add static mapping local 10.0.0.100 22 external 192.168.1.1 2222 tcp
nat44 add static mapping local 10.0.0.101 80 external 192.168.1.1 80 tcp

# 5단계: ACL 설정
set acl-plugin acl permit+reflect src 10.0.0.0/24 dst 0.0.0.0/0
set acl-plugin interface GigabitEthernet0/9/0 input acl 0
세션 상태 비영속: VPP 재시작 시 모든 TCP 연결, NAT 세션, IPsec SA가 초기화됩니다. VPP는 이러한 상태를 디스크에 저장하지 않으므로, 재시작 후 클라이언트는 새로운 연결을 수립해야 합니다. 무중단 서비스가 필요하면 아래 HA 구성을 통해 세션을 standby 노드에 동기화하세요.

고가용성(HA)과 장애 조치

프로덕션 환경에서는 단일 VPP 인스턴스의 장애가 서비스 중단으로 이어지지 않도록 고가용성 구성이 필수입니다. VPP는 여러 HA 패턴을 지원합니다.

HA 패턴구성 방식장점한계
Active-Standby (VRRP)linux-cp 플러그인으로 커널 VRRP와 연동구성 단순, 검증된 방식절체 시 1~3초 다운타임
Active-Active (ECMP)상위 라우터에서 ECMP로 분산다운타임 없음, 확장 용이세션 고정(affinity) 필요
NAT 세션 미러링nat44 ha CLI로 standby에 세션 복제NAT 세션 유지대역폭 소모, 지연 발생 가능
IPsec SA 재수립IKEv2 re-keying으로 SA 재수립표준 기반, 호환성 높음재수립 동안 패킷 손실 가능
# NAT44 HA 구성 — 세션 미러링

# Primary 노드 설정
vpp# nat44 ha listener 10.0.0.1:20000 path-mtu 1400
vpp# nat44 ha failover 10.0.0.2:20000

# Standby 노드 설정
vpp# nat44 ha listener 10.0.0.2:20000 path-mtu 1400
vpp# nat44 ha failover 10.0.0.1:20000

# 세션 동기화 확인
vpp# show nat44 ha
  listener 10.0.0.1:20000
  failover 10.0.0.2:20000
  in-sync sessions: 45832
  pending updates: 3                   ← 0에 가까울수록 양호
  last resync: 2.3s ago
# Active-Standby VRRP 연동 (linux-cp 플러그인)

# linux-cp 활성화: startup.conf에 추가
# plugins { plugin linux_cp_plugin.so { enable } }

# 커널에 미러 인터페이스 생성
vpp# lcp create GigabitEthernet0/8/0 host-if eth-vpp0
vpp# set interface state GigabitEthernet0/8/0 up
vpp# set interface ip address GigabitEthernet0/8/0 192.168.1.1/24

# 커널에서 keepalived(VRRP) 구성
# /etc/keepalived/keepalived.conf에서 eth-vpp0 사용
# VIP 192.168.1.254를 VRRP로 관리

VPP 인스턴스의 활성(liveness) 상태를 외부에서 확인하는 방법은 다음과 같습니다.

# 방법 1: CLI 소켓을 통한 헬스 체크
$ vppctl -s /run/vpp/cli.sock show version 2>&1 | grep -q "vpp" && echo "UP" || echo "DOWN"

# 방법 2: API 소켓 존재 여부 확인
$ test -S /run/vpp/api.sock && echo "API socket exists"

# 방법 3: systemd 상태 확인
$ systemctl is-active vpp.service

# 방법 4: 프로세스 체크 + CLI 응답 확인 (모니터링 스크립트)
#!/bin/bash
if pgrep -x vpp_main > /dev/null; then
    RESULT=$(timeout 3 vppctl show runtime 2>&1)
    if [ $? -eq 0 ]; then
        echo "VPP healthy"
    else
        echo "VPP process alive but CLI unresponsive"
    fi
else
    echo "VPP process not running"
fi

용량 산정과 사이징

VPP의 성능은 메모리, CPU 코어 수, Hugepage 설정에 크게 의존합니다. 워크로드에 맞는 리소스를 사전에 산정해야 운영 중 성능 저하나 OOM(Out of Memory) 장애를 방지할 수 있습니다.

리소스 항목산정 공식참고 수치
버퍼 풀 메모리buffers-per-numa × 2.2KB16K 버퍼 ≈ 35MB, 32K ≈ 70MB
FIB 테이블 (BGP 풀 테이블)경로 수 비례~900K 경로 ≈ ~200MB
NAT44 세션 테이블세션 수 × ~500B1M 세션 ≈ ~500MB
TCP 호스트 스택 세션세션 수 × ~2KB (세션 + FIFO 쌍)1M 세션 ≈ ~2GB
TLS 컨텍스트 (OpenSSL)동시 연결 × ~50KB10K 연결 ≈ ~500MB
TLS 컨텍스트 (picotls)동시 연결 × ~5KB10K 연결 ≈ ~50MB
워크로드코어당 처리량 (64B 패킷)코어당 처리량 (IMIX)비고
L3 포워딩 (IPv4)~15 Mpps~10 Gbps기본 ip4-lookup 노드
NAT44 (endpoint-dependent)~5 Mpps~6 Gbps세션 수에 따라 감소
IPsec (AES-128-GCM)~3 Mpps~5 GbpsQAT 가속 시 ~8 Gbps
TLS 종단 (RSA-2048)~2K CPS~5 Gbps (bulk)핸드셰이크가 병목
ACL (stateful, 1K 규칙)~8 Mpps~8 Gbps규칙 수에 따라 감소
# Hugepage 산정 예시: 25GbE NIC, 워커 3개

# 1) 필요 버퍼 수 계산
# NIC 링 크기: rx 2048 + tx 2048 = 4096/NIC
# 파이프라인 버퍼: 워커당 ~8192개 (벡터 처리 + 큐잉)
# 총 버퍼: 4096 + (3 × 8192) = 28672 → 32768 (2의 거듭제곱)

# 2) 메모리 = 32768 × 2.2KB ≈ 72MB (버퍼)
#    + FIB/NAT/세션 등 추가 메모리
#    → 총 1GB (여유 포함) ~ 2GB 권장

# 3) 2MB Hugepage 기준: 1GB / 2MB = 512페이지
#    1GB Hugepage 기준: 1개 (또는 2개 권장)

# 커널 부트 파라미터
GRUB_CMDLINE_LINUX="default_hugepagesz=2M hugepagesz=2M hugepages=1024"

# 또는 런타임 할당 (권장하지 않음 — 단편화 가능)
$ echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages

# 할당 상태 확인
$ cat /proc/meminfo | grep Huge
HugePages_Total:    1024
HugePages_Free:      980
HugePages_Rsvd:       44
Hugepagesize:       2048 kB
NUMA 고려: 멀티 NUMA 시스템에서는 NIC이 연결된 NUMA 노드에 Hugepage를 할당해야 합니다. echo 512 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages로 노드별 할당이 가능합니다. startup.confbuffers-per-numa도 NUMA 노드별로 적용됩니다.

로깅과 Syslog 통합

VPP는 모듈별 로그 레벨 제어, 파일 로깅, syslog 전달을 지원합니다. 운영 환경에서는 로그를 중앙 로그 시스템으로 전달하여 장애 추적과 감사(audit)에 활용합니다.

# 파일 로그: startup.conf에서 설정
unix {
    log /var/log/vpp/vpp.log           ← VPP 메인 로그 파일
    full-coredump
}

# 로그 파일 로테이션: logrotate 연동
# /etc/logrotate.d/vpp
# /var/log/vpp/vpp.log {
#     daily
#     rotate 14
#     compress
#     missingok
#     notifempty
#     postrotate
#         vppctl set logging size 0   ← 로그 버퍼 리셋
#     endscript
# }
# 모듈별 로그 레벨 설정 (런타임)

# 현재 로그 설정 확인
vpp# show logging
  default level: warn
  unthrottled:   0

# 특정 모듈의 로그 레벨 변경
vpp# set logging class dpdk level debug
vpp# set logging class nat level info
vpp# set logging class acl-plugin level warn
vpp# set logging class session level notice

# 사용 가능한 로그 클래스 확인
vpp# show logging class
  Name                  Level
  acl-plugin            warn
  dpdk                  debug
  nat                   info
  session               notice
  ...
# Syslog 전달 설정

# 원격 syslog 서버로 전달
vpp# set logging syslog-address 10.0.0.10 port 514

# 실시간 로그 확인 (디버깅 시 유용)
vpp# show logging

# 특정 로그 엔트리 수 제한하여 확인
vpp# show logging count 50

# 로그 버퍼 크기 설정
vpp# set logging size 4096
프로덕션 로그 레벨: 기본 로그 레벨은 warn으로 유지하세요. debug 레벨은 대량의 로그를 생성하여 성능에 영향을 줄 수 있으므로, 장애 추적 시에만 일시적으로 활성화하고 완료 후 원래 레벨로 복원하세요. set logging class dpdk level warn으로 즉시 변경 가능합니다.

VPP 플러그인 개발

VPP 플러그인은 그래프 노드(graph node)를 등록하여 패킷 처리 파이프라인에 새로운 기능을 삽입합니다. 커널 모듈과 달리 유저스페이스에서 동작하므로 크래시가 시스템 전체에 영향을 주지 않으며, 핫 리로드가 가능합니다.

VPP 플러그인 노드 그래프 — 커스텀 노드 삽입 기존 패킷 처리 그래프 dpdk-input ethernet-input ip4-input ip4-lookup ip4-rewrite tx 커스텀 플러그인 노드 삽입 (ip4-lookup 앞) ip4-input my-filter-node (플러그인 커스텀 노드) ip4-lookup drop NEXT_PASS NEXT_DROP 플러그인 핵심 매크로와 함수 VLIB_REGISTER_NODE 노드 등록 (이름, 함수, next[]) VLIB_NODE_FN(node) 노드 처리 함수 (벡터 루프) VNET_FEATURE_INIT 기존 그래프에 feature arc 삽입 vlib_get_buffer / vlib_buffer_advance 패킷 버퍼 접근 및 헤더 파싱 vlib_validate_buffer_enqueue_x1/x2/x4 다음 노드로 패킷 전달 (벡터 배치)

VPP 핵심 API 함수 상세 분석

VPP 플러그인 개발에서 가장 빈번하게 사용되는 핵심 API 함수들을 상세히 분석합니다. 이 함수들은 패킷 처리 노드의 fn() 콜백 내에서 호출되며, 각 함수의 동작 원리와 올바른 사용 패턴을 이해하는 것이 고성능 플러그인 작성의 핵심입니다.

vlib_frame_vector_args() — 프레임에서 버퍼 인덱스 배열 획득

vlib_frame_vector_args()는 노드 함수에 전달된 프레임(vlib_frame_t)에서 버퍼 인덱스 배열의 시작 포인터를 반환합니다. 반환 타입은 u32*이며, 각 원소는 vlib_buffer_pool 내의 버퍼 인덱스입니다. 배열의 유효 크기는 frame->n_vectors로 확인할 수 있습니다.

/* vlib_frame_vector_args() 기본 사용 패턴 */
static uword
my_node_fn (vlib_main_t *vm,
            vlib_node_runtime_t *node,
            vlib_frame_t *frame)
{
    u32 n_left_from, *from;

    /* 프레임에서 버퍼 인덱스 배열 포인터 획득 */
    from = vlib_frame_vector_args (frame);

    /* 처리할 패킷 수 (1~VLIB_FRAME_SIZE, 최대 256) */
    n_left_from = frame->n_vectors;

    while (n_left_from > 0)
    {
        u32 bi0 = from[0];
        vlib_buffer_t *b0 = vlib_get_buffer (vm, bi0);

        /* b0->data + b0->current_data 위치부터 패킷 데이터 접근 */
        ip4_header_t *ip0 = vlib_buffer_get_current (b0);

        /* 패킷 처리 로직 ... */

        from += 1;
        n_left_from -= 1;
    }

    return frame->n_vectors;
}

vlib_frame_vector_args()가 반환하는 포인터는 프레임 구조체 바로 뒤에 위치한 인라인 배열을 가리킵니다. 프레임은 노드 런타임에 의해 사전 할당되므로 별도의 메모리 할당이 발생하지 않습니다. 반환 값의 수명은 현재 노드 함수 실행 동안에만 유효합니다.

vlib_get_next_frame() / vlib_put_next_frame() — 다음 노드로의 프레임 관리

vlib_get_next_frame()은 다음 노드로 패킷을 전달하기 위한 출력 프레임의 참조를 획득합니다. vlib_put_next_frame()은 처리 완료 후 프레임을 반환하며, 실제로 전달할 벡터 수를 갱신합니다. 이 두 함수는 반드시 쌍(pair)으로 호출해야 합니다.

/* get_next_frame / put_next_frame 쌍 사용 패턴 */
u32 n_left_from, *from;
u32 n_left_to_next, *to_next;
u32 next_index;

from = vlib_frame_vector_args (frame);
n_left_from = frame->n_vectors;
next_index = node->cached_next_index;

while (n_left_from > 0)
{
    /* 다음 노드의 출력 프레임 참조 획득 */
    vlib_get_next_frame (vm, node, next_index,
                        to_next, n_left_to_next);

    while (n_left_from > 0 && n_left_to_next > 0)
    {
        u32 bi0 = from[0];
        u32 next0 = next_index;

        to_next[0] = bi0;
        from += 1;
        to_next += 1;
        n_left_from -= 1;
        n_left_to_next -= 1;

        /* next0가 변경될 수 있는 경우 validate_buffer_enqueue 사용 */
        vlib_validate_buffer_enqueue_x1 (vm, node, next_index,
                                         to_next, n_left_to_next,
                                         bi0, next0);
    }

    /* 프레임 반환: n_left_to_next 값으로 실제 전달 수 계산 */
    vlib_put_next_frame (vm, node, next_index, n_left_to_next);
}

vlib_get_next_frame()은 내부적으로 next_index에 해당하는 다음 노드의 보류 프레임(pending frame)을 조회합니다. 보류 프레임이 없으면 새로 할당합니다. to_next는 해당 프레임의 버퍼 인덱스 배열 끝 위치를, n_left_to_next는 남은 슬롯 수를 반환합니다.

vlib_put_next_frame()n_left_to_next 값을 사용하여 실제로 프레임에 추가된 벡터 수를 계산하고, 프레임을 보류 큐에 등록합니다. 이 함수를 호출하지 않으면 프레임이 다음 노드에 전달되지 않으며, 버퍼가 누수됩니다. 반대로 get 없이 put을 호출하면 초기화되지 않은 상태에서 접근하여 정의되지 않은 동작이 발생합니다.

vlib_validate_buffer_enqueue_x1/x2/x4() — 버퍼 큐잉 최적화

vlib_validate_buffer_enqueue 매크로 계열은 패킷을 다음 노드로 전달할 때 next_index 변경을 효율적으로 처리합니다. 같은 next_index로 연속 전달 시에는 fast-path로 동작하여 추가 복사가 불필요하지만, next_index가 변경되면 slow-path에서 현재 프레임을 반환하고 새 프레임을 획득합니다.

/* vlib_validate_buffer_enqueue_x1 매크로 의사 코드 (pseudo-code) */
/*
 * 매개변수:
 *   vm            — vlib_main_t*
 *   node          — vlib_node_runtime_t*
 *   next_index    — 현재 프레임이 가리키는 다음 노드 인덱스 (변경 가능)
 *   to_next       — 출력 프레임 버퍼 인덱스 배열 포인터
 *   n_left_to_next — 출력 프레임 남은 슬롯 수
 *   bi0           — 현재 패킷 버퍼 인덱스
 *   next0         — 이 패킷이 가야 할 다음 노드 인덱스
 */
if (PREDICT_FALSE (next0 != next_index))
{
    /* slow-path: next_index가 변경됨 */
    /* 1. 현재 프레임에서 방금 기록한 bi0를 제거 (to_next 롤백) */
    /* 2. 현재 프레임을 반환 (put_next_frame) */
    /* 3. next0에 해당하는 새 프레임 획득 (get_next_frame) */
    /* 4. 새 프레임에 bi0 기록 */
    /* 5. 새 프레임 반환 (put_next_frame) */
    /* 6. 원래 next_index의 프레임 재획득 (get_next_frame) */
    vlib_set_next_frame_buffer (vm, node, next0, bi0);
    n_left_to_next += 1;  /* 롤백한 슬롯 복원 */
}
/* fast-path: next0 == next_index → 이미 to_next에 기록되어 있으므로 추가 작업 없음 */

x2x4 변형은 각각 2개, 4개의 패킷을 동시에 처리하여 명령어 수준 병렬성(ILP, Instruction-Level Parallelism)을 활용합니다. 현대 CPU의 슈퍼스칼라 파이프라인에서 독립적인 메모리 접근과 분기 예측을 병렬로 수행할 수 있습니다.

/* x2 변형: 2개 패킷 동시 큐잉 */
while (n_left_from >= 4 && n_left_to_next >= 2)
{
    u32 bi0, bi1;
    u32 next0, next1;
    vlib_buffer_t *b0, *b1;

    /* 프리페치: 2개 앞의 패킷을 미리 캐시에 적재 */
    {
        vlib_buffer_t *p2, *p3;
        p2 = vlib_get_buffer (vm, from[2]);
        p3 = vlib_get_buffer (vm, from[3]);
        vlib_prefetch_buffer_header (p2, LOAD);
        vlib_prefetch_buffer_header (p3, LOAD);
    }

    bi0 = from[0]; bi1 = from[1];
    to_next[0] = bi0; to_next[1] = bi1;

    b0 = vlib_get_buffer (vm, bi0);
    b1 = vlib_get_buffer (vm, bi1);

    /* 각 패킷의 next_index 결정 */
    next0 = process_packet (b0);
    next1 = process_packet (b1);

    from += 2; to_next += 2;
    n_left_from -= 2; n_left_to_next -= 2;

    vlib_validate_buffer_enqueue_x2 (vm, node, next_index,
                                     to_next, n_left_to_next,
                                     bi0, bi1, next0, next1);
}

x1은 패킷 단위 처리로 코드가 단순하지만 ILP를 활용하지 못합니다. x2는 2개 패킷을 병렬 처리하여 L1/L2 캐시 미스 대기 시간을 숨길 수 있으며, x4는 4개 패킷을 병렬 처리하여 최대 처리량을 달성합니다. 일반적으로 dual-loop(x2)와 single-loop(x1)를 조합하여 n_left_from이 2 미만일 때 x1으로 나머지를 처리하는 패턴을 사용합니다.

vlib_buffer_advance() — 헤더 파싱을 위한 포인터 이동

vlib_buffer_advance()는 버퍼의 current_data 오프셋과 current_length를 동시에 조정하여 패킷 데이터 내에서 논리적 시작 위치를 이동합니다. 양수 값을 전달하면 헤더를 건너뛰어 디캡슐화(decapsulation) 효과를 내고, 음수 값을 전달하면 헤더 공간을 확보하여 캡슐화(encapsulation)를 수행합니다.

/* vlib_buffer_advance() 내부 동작 */
/*
 * b->current_data += advance;
 * b->current_length -= advance;
 *
 * 양수(+N): current_data를 N바이트 뒤로 이동 → 헤더 스킵
 * 음수(-N): current_data를 N바이트 앞으로 이동 → 헤더 공간 확보
 */

/* 예시 1: 이더넷 헤더 스킵 후 IP 헤더 접근 (디캡슐화) */
ethernet_header_t *eth = vlib_buffer_get_current (b0);
u16 ethertype = clib_net_to_host_u16 (eth->type);

/* current_data += sizeof(ethernet_header_t) */
vlib_buffer_advance (b0, sizeof (ethernet_header_t));

/* 이제 vlib_buffer_get_current()는 IP 헤더를 가리킵니다 */
ip4_header_t *ip0 = vlib_buffer_get_current (b0);

/* 예시 2: 새 헤더 추가 (캡슐화) */
/* current_data -= sizeof(my_header_t) → 앞쪽 headroom 사용 */
vlib_buffer_advance (b0, -(i32) sizeof (my_header_t));
my_header_t *hdr = vlib_buffer_get_current (b0);
hdr->version = 1;
hdr->length = clib_host_to_net_u16 (payload_len);

캡슐화 시에는 current_data가 감소하면서 사전에 확보된 headroom 영역을 사용합니다. 기본 headroom은 VLIB_BUFFER_PRE_DATA_SIZE(기본 128바이트)이며, 이 범위를 초과하면 데이터 손상이 발생합니다. 체인 버퍼(chained buffer)의 경우 vlib_buffer_advance()는 첫 번째 세그먼트의 current_data만 조정합니다. total_length_not_including_first_buffer 필드는 변경되지 않으므로, 전체 패킷 길이를 계산할 때는 b->current_length + b->total_length_not_including_first_buffer를 사용해야 합니다.

vnet_buffer() / vnet_buffer2() — 패킷 메타데이터 접근

vnet_buffer()vnet_buffer2() 매크로는 vlib_buffer_t 내의 opaque[10]opaque2[12] 배열을 프로토콜별 메타데이터 구조체로 오버레이(overlay)하여 접근합니다. 이 영역은 패킷이 노드 그래프를 따라 전달될 때 노드 간 정보를 공유하는 공유 메모리 역할을 합니다.

/* vnet_buffer_opaque_t — opaque[10] 영역의 union 구조 (일부) */
typedef struct
{
    union
    {
        struct
        {
            u32 sw_if_index[VLIB_N_RX_TX]; /* [0]=RX, [1]=TX 인터페이스 */
            i16 l2_hdr_offset;              /* L2 헤더 시작 오프셋 */
            i16 l3_hdr_offset;              /* L3 헤더 시작 오프셋 */
            i16 l4_hdr_offset;              /* L4 헤더 시작 오프셋 */
            u8  feature_arc_index;           /* 피처 아크 인덱스 */
        };
        struct { u32 adj_index[VLIB_N_RX_TX]; } ip;
        struct { u32 fib_index; } tcp;
        struct { u32 sa_index; u32 seq; } ipsec;
    };
} vnet_buffer_opaque_t;

/* 노드 함수에서의 메타데이터 접근 */
vlib_buffer_t *b0 = vlib_get_buffer (vm, bi0);

/* 수신 인터페이스 인덱스 읽기 */
u32 rx_sw_if_index = vnet_buffer(b0)->sw_if_index[VLIB_RX];

/* 송신 인터페이스 설정 */
vnet_buffer(b0)->sw_if_index[VLIB_TX] = tx_sw_if_index;

/* IP adjacency 인덱스 접근 */
u32 adj_idx = vnet_buffer(b0)->ip.adj_index[VLIB_TX];

/* vnet_buffer2: 확장 메타데이터 (opaque2[12] 영역) */
vnet_buffer2(b0)->loop_counter = 0;

플러그인에서 커스텀 메타데이터를 저장하려면 opaque/opaque2의 사용되지 않는 영역을 활용합니다. 다만 노드 그래프 상의 다른 노드와 필드가 겹치지 않도록 주의해야 합니다. 가장 안전한 방법은 vnet_buffer_opaque_t의 union에 플러그인 전용 구조체를 추가하고, 해당 필드가 사용되는 구간(노드 경로)에서만 유효하도록 문서화하는 것입니다.

/* 플러그인 커스텀 메타데이터 정의 예시 */
/* vnet_buffer_opaque_t union에 추가 */
struct
{
    u32 session_id;
    u16 action_flags;
    u16 reserved;
} my_plugin;

/* 사용 */
vnet_buffer(b0)->my_plugin.session_id = sid;
vnet_buffer(b0)->my_plugin.action_flags = MY_ACTION_FORWARD;

CLIB_PREFETCH() — 성능 최적화를 위한 캐시 프리페치

CLIB_PREFETCH()는 지정된 메모리 주소의 캐시 라인을 L1 캐시에 미리 적재하는 프리페치 명령을 생성합니다. 패킷 처리에서 메모리 접근 지연(L3 캐시 미스 시 수십~수백 나노초)이 처리량의 주된 병목이므로, 프리페치는 VPP 성능 최적화의 핵심 기법입니다.

/* CLIB_PREFETCH 힌트 유형 */
/*
 * LOAD  (T0) — 읽기 전용 접근 예정, 공유 상태로 캐시 적재
 * STORE (T1) — 쓰기 접근 예정, 배타적 상태로 캐시 적재
 * WRITE = STORE의 별칭, 동일 동작
 */

/* dual-loop에서의 프리페치 패턴 */
while (n_left_from >= 4 && n_left_to_next >= 2)
{
    /* 2개 앞 패킷의 버퍼 헤더를 프리페치 */
    {
        vlib_buffer_t *p2, *p3;
        p2 = vlib_get_buffer (vm, from[2]);
        p3 = vlib_get_buffer (vm, from[3]);

        /* 버퍼 헤더 프리페치 (메타데이터 읽기용) */
        vlib_prefetch_buffer_header (p2, LOAD);
        vlib_prefetch_buffer_header (p3, LOAD);

        /* 패킷 데이터 프리페치 (헤더 파싱용) */
        CLIB_PREFETCH (p2->data, CLIB_CACHE_LINE_BYTES, LOAD);
        CLIB_PREFETCH (p3->data, CLIB_CACHE_LINE_BYTES, LOAD);
    }

    /* 현재 2개 패킷 처리 — 프리페치된 데이터 활용 */
    u32 bi0 = from[0];
    u32 bi1 = from[1];
    vlib_buffer_t *b0 = vlib_get_buffer (vm, bi0);
    vlib_buffer_t *b1 = vlib_get_buffer (vm, bi1);

    /* 패킷 처리 ... */

    from += 2;
    n_left_from -= 2;
    to_next += 2;
    n_left_to_next -= 2;
}

프리페치 거리(prefetch distance)는 "현재 처리 중인 패킷"과 "프리페치 대상 패킷" 사이의 간격입니다. dual-loop에서는 거리가 2(현재 0,1번 처리하면서 2,3번 프리페치)이며, quad-loop에서는 거리가 4입니다. 이 거리는 프리페치 명령 발행 후 실제 캐시 적재까지의 지연 시간 동안 CPU가 현재 패킷을 처리할 수 있도록 설정합니다. 거리가 너무 짧으면 프리페치 효과가 없고, 너무 길면 프리페치된 데이터가 캐시에서 밀려날 수 있습니다.

VPP 핵심 API 요약

API용도호출 위치성능 영향
vlib_frame_vector_args()입력 프레임에서 버퍼 인덱스 배열 획득노드 함수 시작부O(1), 포인터 연산만 수행
vlib_get_next_frame()다음 노드 출력 프레임 참조 획득패킷 처리 루프 시작보류 프레임 조회/할당 비용
vlib_put_next_frame()출력 프레임 반환 및 전달 등록패킷 처리 루프 종료O(1), 벡터 수 갱신만 수행
vlib_validate_buffer_enqueue_x1/x2/x4()next_index 변경 시 프레임 교체 자동 처리패킷 큐잉 시점fast-path O(1), slow-path 프레임 교체 비용
vlib_buffer_advance()패킷 데이터 포인터 이동 (디캡슐화/캡슐화)헤더 파싱/추가 시O(1), 정수 덧셈 2회
vnet_buffer() / vnet_buffer2()패킷 메타데이터(sw_if_index, adj 등) 접근패킷 처리 전 구간O(1), 구조체 포인터 캐스팅
CLIB_PREFETCH()다음 패킷 데이터를 L1 캐시에 미리 적재dual/quad loop 선두캐시 미스 50~200ns 회피

clib 인프라 라이브러리 상세 분석

VPP의 clib(C Library) 인프라는 src/vlib/, src/vppinfra/ 디렉터리에 위치하며, 표준 C 라이브러리를 대체하는 고성능 유틸리티 함수 모음입니다. 메모리 관리, 동적 배열, 해시 테이블, 바이트 순서 변환, 원자적 연산 등 VPP 전체에서 사용되는 기반 기능을 제공합니다. 모든 clib 함수는 성능에 최적화되어 있으며, 대부분 static inline으로 선언되어 함수 호출 오버헤드 없이 사용됩니다.

clib 인프라 계층 구조 VPP Application (플러그인, 그래프 노드) vlib (노드 프레임워크, 버퍼 관리) vnet (네트워크 스택) clib / vppinfra 인프라 계층 clib_mem clib_vec clib_pool clib_bihash clib_time clib_atomic clib_error clib_memcpy 모든 clib 함수는 static inline으로 선언되어 함수 호출 오버헤드를 제거합니다 src/vppinfra/ — hugepage 기반 메모리, NUMA 인식, 캐시 최적화

clib_mem — 메모리 할당/해제

Hugepage 기반 할당: VPP는 표준 malloc 대신 clib_mem_alloc() / clib_mem_free()를 사용합니다. 내부적으로 hugepage 위의 mheap(또는 dlmalloc)을 사용하여 TLB 미스를 최소화합니다.

NUMA 인식 할당: clib_mem_alloc_aligned_at_offset()는 지정된 NUMA 노드의 hugepage에서 캐시라인 정렬 메모리를 할당합니다. 멀티 소켓 서버에서 원격 NUMA 접근으로 인한 성능 저하를 방지하는 핵심 메커니즘입니다.

주요 함수 목록 및 분석

함수설명비고
clib_mem_alloc(size)기본 힙 메모리 할당내부적으로 정렬 할당 호출
clib_mem_alloc_aligned(size, align)지정 정렬 힙 메모리 할당캐시라인(64B) 정렬에 주로 사용
clib_mem_free(ptr)힙 메모리 해제NULL-safe
clib_mem_alloc_no_fail(size)할당 실패 시 abortfast-path에서 에러 처리 회피용
clib_mem_set_heap(heap)현재 스레드의 활성 힙 전환이전 힙 포인터 반환
clib_mem_get_heap()현재 스레드의 활성 힙 반환TLS(Thread-Local Storage) 기반
clib_mem_get_heap_size()현재 힙의 총 크기 반환모니터링/디버깅용
clib_mem_get_heap_free_space()현재 힙의 잔여 공간 반환메모리 부족 감지에 활용

clib_mem_alloc_aligned 내부 흐름

static inline void *
clib_mem_alloc_aligned (uword size, uword align)
{
    clib_mem_heap_t *heap = clib_mem_get_per_cpu_heap ();
    void *p;

    /* dlmalloc의 정렬 할당 사용
     * hugepage 위에서 동작하므로 TLB 미스가 발생하지 않습니다 */
    p = mspace_memalign (heap->mspace, align, size);

    /* 할당 통계 갱신 (디버그 빌드에서만 활성) */
    clib_mem_set_thread_alloc_stats (heap, size, 1);

    return p;
}

NUMA 인식 할당 예제

/* NUMA 노드 1에서 캐시라인 정렬 메모리 할당 */
clib_mem_heap_t *prev_heap;
void *data;

/* NUMA 노드 1의 힙으로 전환 */
prev_heap = clib_mem_set_heap (numa_heaps[1]);

/* 64바이트 캐시라인 정렬로 4096바이트 할당 */
data = clib_mem_alloc_aligned (4096, CLIB_CACHE_LINE_BYTES);

/* 원래 힙으로 복원 */
clib_mem_set_heap (prev_heap);

스레드별 힙 전환 패턴

/* 플러그인 초기화 시 전용 힙 생성 및 전환 패턴 */
void
my_plugin_init (my_plugin_main_t *mpm)
{
    clib_mem_heap_t *prev_heap;
    u8 *heap;

    /* 플러그인 전용 힙 생성 (256MB, "my-plugin" 이름) */
    heap = clib_mem_create_heap (0, 256 << 20, 1 /* locked */,
                                  "my-plugin");

    /* 전용 힙으로 전환 후 자원 할당 */
    prev_heap = clib_mem_set_heap (heap);

    /* 이 구간의 모든 clib_mem_alloc()은 전용 힙에서 할당됩니다 */
    pool_alloc (mpm->sessions, 1024);
    vec_validate (mpm->per_thread_data, vlib_num_workers ());

    /* 메인 힙으로 복원 */
    clib_mem_set_heap (prev_heap);
    mpm->heap = heap;
}

clib_vec — 동적 배열 (Vector)

VPP의 기본 컬렉션: vec_* 매크로 군은 C 배열을 동적으로 관리합니다. 배열 포인터 자체가 핸들이며, 포인터 앞 공간(negative offset)에 length와 header를 저장합니다. 이 설계 덕분에 vec_len(v)v가 NULL이면 0을 반환하므로 별도의 NULL 검사가 필요 없습니다.

헤더 레이아웃: vec_header_tlen(u32)과 선택적 사용자 헤더를 포함하며, 사용자가 받는 포인터는 data[0]을 가리킵니다. 즉, ((vec_header_t *)v)[-1].len으로 길이를 참조합니다.

주요 매크로 분석

매크로동작성능
vec_len(v)배열 길이 반환 (NULL이면 0)O(1), 포인터 역참조 1회
vec_add1(v, e)원소 1개 추가 (필요 시 realloc)amortized O(1), 확장 시 O(n)
vec_add2(v, p, n)n개 공간 확보, 포인터 p 반환amortized O(1)
vec_validate(v, i)인덱스 i까지 크기 보장 (0 초기화)확장 필요 시 O(n)
vec_free(v)메모리 해제, v를 NULL로 설정O(1)
vec_reset_length(v)길이만 0으로 설정 (메모리 유지)O(1), 재할당 없는 재사용
vec_elt_at_index(v, i)인덱스 접근 (디버그 시 바운드 체크)O(1)
vec_foreach(var, v)전체 원소 순회 매크로O(n)
vec_sort_with_function(v, cmp)비교 함수 기반 정렬O(n log n)
vec_dup(v)전체 배열 복제O(n)

vec_add1 내부 구현

/* vec_add1(V, E) 매크로 확장 후 핵심 로직 */
#define vec_add1(V, E)                          \
do {                                             \
    word _v_len = vec_len (V);                   \
    /* 현재 용량 초과 여부 확인 */                \
    V = _vec_resize (V, _v_len + 1,              \
                     (_v_len + 1) * sizeof (V[0]),  \
                     sizeof (vec_header_t),      \
                     0);                            \
    /* _vec_resize 내부:
     *   if (새 크기 > 현재 용량)
     *     새 용량 = max(요청 크기, 현재 용량 * 2)
     *     clib_mem_alloc(새 용량)
     *     memcpy(기존 데이터)
     *     clib_mem_free(기존 버퍼)
     */                                          \
    V[_v_len] = (E);                             \
} while (0)

vec_header_t 메모리 레이아웃

벡터 포인터 v가 가리키는 위치 기준으로 메모리 레이아웃은 다음과 같습니다. 포인터 앞(negative offset)에 헤더가 위치하며, 사용자는 v[0]부터 데이터에 접근합니다.

/*
 * 메모리 레이아웃 (주소 낮은 쪽 → 높은 쪽):
 *
 *   [user_header (선택)]  ← vec_header_bytes(user_hdr_size)
 *   [len: u32]           ← ((vec_header_t *)v)[-1].len
 *   [data[0]]            ← v 포인터가 가리키는 위치
 *   [data[1]]
 *   ...
 *   [data[len-1]]
 *
 * vec_len(v) → v가 NULL이면 0, 아니면 헤더의 len 필드
 * vec_bytes(v) → vec_len(v) * sizeof(v[0])
 */
typedef struct {
    u32 len;           /* 현재 원소 수 */
    u8  hdr_size;      /* 사용자 헤더 크기 (바이트) */
    u8  log2_header;   /* 정렬 정보 */
    u8  data[0];       /* 실제 데이터 시작점 */
} vec_header_t;

실제 사용 예제: 인터페이스 목록 관리

/* 활성 인터페이스 인덱스 목록을 vec로 관리하는 패턴 */
u32 *active_sw_if_indices = 0;  /* NULL 초기화 = 빈 벡터 */

/* 인터페이스 활성화 시 추가 */
vec_add1 (active_sw_if_indices, sw_if_index);

/* 전체 순회: vec_foreach는 포인터를 순회합니다 */
u32 *sw_if_idx;
vec_foreach (sw_if_idx, active_sw_if_indices)
{
    vnet_sw_interface_t *swif;
    swif = vnet_get_sw_interface (vnm, *sw_if_idx);
    /* 인터페이스 처리 ... */
}

/* 특정 인덱스 제거 (순서 무관 시 O(1) 삭제) */
vec_del1 (active_sw_if_indices, position);

/* 재사용을 위해 길이만 초기화 (메모리는 유지) */
vec_reset_length (active_sw_if_indices);

/* 완전 해제 */
vec_free (active_sw_if_indices);
/* 이 시점에서 active_sw_if_indices == NULL */

clib_pool — 인덱스 기반 객체 풀

pool_* 매크로는 동일 크기 객체의 할당/해제를 O(1)로 수행합니다. 내부적으로 free bitmap과 vec를 조합하여 구현됩니다. 풀 인덱스(u32)로 객체에 접근하므로, 포인터 대신 인덱스를 저장하여 메모리를 절약하고 직렬화가 용이합니다.

풀의 핵심 장점은 인덱스 안정성(index stability)입니다. 객체가 할당/해제되어도 기존 객체의 인덱스는 변하지 않습니다. 이는 VPP에서 세션 ID, FIB 엔트리 인덱스 등 외부에서 참조하는 핸들로 풀 인덱스를 직접 사용할 수 있게 합니다.

주요 매크로

매크로동작성능
pool_get(pool, elt)빈 슬롯 할당, elt에 포인터 반환O(1), free bitmap에서 first set bit
pool_put(pool, elt)슬롯 반환 (free bitmap에 마킹)O(1)
pool_elt_at_index(pool, i)인덱스로 객체 접근O(1), 배열 인덱싱
pool_foreach(elt, pool)활성 원소만 순회O(n), free bitmap 스킵
pool_elts(pool)현재 활성 원소 수 반환O(1)
pool_free(pool)전체 풀 메모리 해제O(1)

pool_get 내부 동작

/* pool_get(P, E) 매크로 확장 후 핵심 로직 */
#define pool_get(P, E)                                      \
do {                                                         \
    pool_header_t *_pool_hdr = pool_header (P);             \
    uword _pool_len = vec_len (P);                          \
                                                              \
    /* free_bitmap에서 첫 번째 빈 슬롯 탐색 */             \
    if (PREDICT_TRUE (_pool_hdr->free_bitmap != 0))          \
    {                                                         \
        /* O(1): clib_bitmap_first_set()는 하드웨어            \
         * BSF/TZCNT 명령어를 사용합니다 */                   \
        uword _free_idx =                                     \
            clib_bitmap_first_set (_pool_hdr->free_bitmap);   \
        _pool_hdr->free_bitmap =                              \
            clib_bitmap_andnoti (_pool_hdr->free_bitmap,       \
                                  _free_idx);                  \
        (E) = (P) + _free_idx;                                \
    }                                                         \
    else                                                      \
    {                                                         \
        /* 빈 슬롯 없음 → 벡터 확장 */                       \
        P = _vec_resize (P, _pool_len + 1, ...);              \
        (E) = (P) + _pool_len;                                \
    }                                                         \
    /* 슬롯을 0으로 초기화 */                                \
    clib_memset (E, 0, sizeof (*E));                        \
} while (0)

실제 사용 예제: TLS 컨텍스트 풀 관리

/* TLS 세션 컨텍스트를 풀로 관리하는 패턴 */
typedef struct {
    u32 session_index;        /* 연결된 세션의 풀 인덱스 */
    u32 tls_ctx_handle;       /* 엔진별 핸들 */
    u8  is_client;
    u8  *hostname;             /* SNI 호스트명 (vec) */
} tls_ctx_t;

typedef struct {
    tls_ctx_t *ctx_pool;       /* 풀: 스레드별 독립 */
    u32 *free_ctx_indices;     /* 사전 할당된 인덱스 (선택적) */
} tls_main_t;

/* 새 TLS 컨텍스트 할당 */
tls_ctx_t *ctx;
pool_get (tm->ctx_pool, ctx);
clib_memset (ctx, 0, sizeof (*ctx));
ctx->session_index = session_idx;

/* 풀 인덱스를 핸들로 사용 — 포인터 대신 u32 인덱스 저장 */
u32 ctx_index = ctx - tm->ctx_pool;
session->opaque = ctx_index;   /* 32비트 인덱스만 저장 */

/* 인덱스로 컨텍스트 접근 */
tls_ctx_t *ctx = pool_elt_at_index (tm->ctx_pool, ctx_index);

/* TLS 연결 종료 시 반환 */
vec_free (ctx->hostname);       /* 내부 vec 먼저 해제 */
pool_put (tm->ctx_pool, ctx);   /* 슬롯을 free bitmap에 반환 */

/* 활성 컨텍스트만 순회하여 타임아웃 검사 */
pool_foreach (ctx, tm->ctx_pool)
{
    if (now - ctx->last_active > timeout)
        tls_ctx_close (ctx);
}

clib_bihash — 고성능 해시 테이블 구현

Bounded-Index Extensible Hash(BIHASH)는 VPP 전체에서 사용되는 핵심 데이터 구조입니다. FIB 테이블, 세션 테이블, MAC 학습 테이블, NAT 세션 관리 등 고성능 조회가 필요한 거의 모든 서브시스템에서 활용됩니다. Reader는 lock-free로 동작하며, writer는 VPP 배리어(barrier) 하에서만 수행되므로 reader-writer 경합이 발생하지 않습니다.

각 bucket은 4개의 (key, value) 쌍을 저장할 수 있습니다. bucket이 가득 차면 체인(chaining) 대신 새로운 페이지를 할당하여 확장하는 방식을 사용합니다. 이 구조 덕분에 캐시 라인 내에서 4-way 비교가 완료되어 메모리 접근 패턴이 매우 효율적입니다.

주요 API 함수

bihash_search_inline() 내부 로직

/* clib_bihash_search_inline() 내부 의사 코드 */
static inline int
clib_bihash_search_inline (BVT(clib_bihash) *h,
                           BVT(clib_bihash_kv) *search_key)
{
    u64 hash;
    u32 bucket_index;
    BVT(clib_bihash_bucket) *b;
    BVT(clib_bihash_value) *v;

    /* 1단계: 해시 계산 및 버킷 선택 */
    hash = BV(clib_bihash_hash) (search_key);
    bucket_index = hash & (h->nbuckets - 1);
    b = &h->buckets[bucket_index];

    /* 2단계: 빈 버킷이면 즉시 miss 반환 */
    if (PREDICT_FALSE (BV(clib_bihash_bucket_is_empty) (b)))
        return -1;

    /* 3단계: 버킷 내 value 배열 획득 */
    hash >>= h->log2_nbuckets;
    v = BV(clib_bihash_get_value) (h, b->offset);

    /* 4단계: 4-way 비교 — 캐시 라인 내에서 완료 */
    for (int i = 0; i < BIHASH_KVP_PER_PAGE; i++)
    {
        if (BV(clib_bihash_key_compare) (v->kvp[i].key,
                                         search_key->key))
        {
            /* hit: 결과를 search_key에 복사 */
            *search_key = v->kvp[i];
            return 0;
        }
    }

    /* 5단계: overflow 페이지가 있으면 체인 탐색 */
    if (PREDICT_FALSE (b->linear_search))
    {
        /* 선형 탐색 모드: 모든 페이지 순회 */
        for (int page = 1; page < b->refcnt; page++)
        {
            v++;
            for (int i = 0; i < BIHASH_KVP_PER_PAGE; i++)
            {
                if (BV(clib_bihash_key_compare) (v->kvp[i].key,
                                                 search_key->key))
                {
                    *search_key = v->kvp[i];
                    return 0;
                }
            }
        }
    }

    return -1;  /* miss */
}

사용 예제: NAT 세션 테이블 생성 및 검색

/* NAT 세션 테이블에서 bihash 활용 예제 */
typedef struct {
    BVT(clib_bihash) session_hash;
    /* ... 기타 NAT 상태 ... */
} nat_main_t;

/* 1. 해시 테이블 초기화 */
static void
nat_session_table_init (nat_main_t *nm)
{
    /* 이름, 버킷 수(1024), 메모리 한도(64MB) */
    BV(clib_bihash_init) (&nm->session_hash,
                          "nat-session-table",
                          1024,    /* nbuckets */
                          64 << 20 /* memory_size = 64MB */);
}

/* 2. 세션 추가 */
static void
nat_session_add (nat_main_t *nm,
                 ip4_address_t src, u16 sport,
                 ip4_address_t dst, u16 dport,
                 u32 session_index)
{
    BVT(clib_bihash_kv) kv;

    /* 키 구성: src_ip + dst_ip + ports */
    kv.key = (u64) src.as_u32 << 32 | dst.as_u32;
    kv.value = session_index;

    BV(clib_bihash_add_del) (&nm->session_hash, &kv,
                             1 /* is_add */);
}

/* 3. 세션 검색 — fast-path에서 호출 */
static inline int
nat_session_lookup (nat_main_t *nm,
                    ip4_address_t src, ip4_address_t dst,
                    u32 *session_index)
{
    BVT(clib_bihash_kv) kv, result;

    kv.key = (u64) src.as_u32 << 32 | dst.as_u32;

    if (BV(clib_bihash_search_inline) (&nm->session_hash, &kv) == 0)
    {
        *session_index = kv.value;
        return 0;  /* 세션 발견 */
    }
    return -1;    /* 세션 없음 */
}

clib_memcpy_fast / clib_memset — 최적화된 메모리 조작

VPP는 패킷 처리 hot-path에서 glibc의 memcpy() 대신 자체 최적화된 메모리 조작 함수를 사용합니다. clib_memcpy_fast()는 내부적으로 x86의 __builtin_memcpy() 또는 SSE/AVX 명령을 활용하여 컴파일 타임에 크기가 결정될 때 최적의 성능을 제공합니다.

주요 함수

MAC 주소 복사 최적화

ethernet_mac_address_copy(dst, src) 내부에서는 clib_memcpy_fast를 6바이트에 대해 호출합니다. 또한 ip4_header_t 복사 시 20바이트 고정 크기를 사용하여 컴파일 타임 최적화가 적용됩니다.

/* MAC 주소 복사 — 6바이트 고정 크기 최적화 */
static_always_inline void
ethernet_mac_address_copy (u8 *dst, const u8 *src)
{
    clib_memcpy_fast (dst, src, 6);
}

/* 패킷 처리에서의 전형적인 사용 패턴 */
static_always_inline u32
my_node_inline (vlib_main_t *vm, vlib_node_runtime_t *node,
                vlib_frame_t *frame)
{
    u32 *from = vlib_frame_vector_args (frame);
    u32 n_left = frame->n_vectors;

    while (n_left > 0)
    {
        vlib_buffer_t *b0 = vlib_get_buffer (vm, from[0]);
        ethernet_header_t *eth = vlib_buffer_get_current (b0);

        /* src MAC을 dst MAC 위치로 복사 (6바이트) */
        clib_memcpy_fast (eth->dst_address, eth->src_address, 6);

        /* IP 헤더 전체 복사 (20바이트 고정 크기) */
        ip4_header_t saved_hdr;
        clib_memcpy_fast (&saved_hdr, ip4_next_header (eth),
                          sizeof (ip4_header_t));

        from += 1;
        n_left -= 1;
    }
    return frame->n_vectors;
}

clib_host_to_net / clib_net_to_host — 바이트 순서 변환

패킷 헤더 파싱과 생성에서 네트워크 바이트 순서(Big-endian)와 호스트 바이트 순서 간의 변환은 필수적입니다. VPP는 clib_host_to_net_*clib_net_to_host_* 매크로 계열을 제공합니다.

주요 함수

구현 원리

x86 아키텍처에서는 __builtin_bswap16/32/64를 사용하며, 단일 BSWAP 명령으로 컴파일됩니다. Big-endian 아키텍처에서는 변환이 필요 없으므로 no-op(아무 작업도 하지 않음)으로 처리됩니다.

/* EtherType 비교 — 네트워크 바이트 순서 상수와 비교 */
#define ETHERNET_TYPE_IP4_NBO  clib_host_to_net_u16 (0x0800)
#define ETHERNET_TYPE_IP6_NBO  clib_host_to_net_u16 (0x86DD)
#define ETHERNET_TYPE_ARP_NBO  clib_host_to_net_u16 (0x0806)

static_always_inline u32
classify_packet (ethernet_header_t *eth)
{
    /* 컴파일 타임에 상수가 변환되므로 런타임 비용 없음 */
    if (eth->type == ETHERNET_TYPE_IP4_NBO)
        return NEXT_IP4_INPUT;
    else if (eth->type == ETHERNET_TYPE_IP6_NBO)
        return NEXT_IP6_INPUT;
    return NEXT_DROP;
}

/* IP 주소 변환 — CLI 출력 시 호스트 순서로 변환 */
static clib_error_t *
show_session_command_fn (vlib_main_t *vm, session_t *s)
{
    ip4_address_t src = s->src_ip;
    u16 src_port = clib_net_to_host_u16 (s->src_port);
    u16 dst_port = clib_net_to_host_u16 (s->dst_port);

    vlib_cli_output (vm, "session: %U:%d -> %U:%d",
                     format_ip4_address, &src, src_port,
                     format_ip4_address, &s->dst_ip, dst_port);
    return 0;
}

/* TCP 포트 파싱 — 패킷에서 포트 번호 추출 */
static_always_inline u16
tcp_get_src_port (tcp_header_t *tcp)
{
    return clib_net_to_host_u16 (tcp->src_port);
}

clib_atomic — 원자적 연산

VPP의 멀티 워커(multi-worker) 환경에서는 여러 스레드가 동시에 공유 데이터에 접근합니다. clib_atomic 계열 함수는 lock-free 프로그래밍을 위한 원자적 연산을 제공하며, 공유 카운터, 플래그, 시퀀스 번호 등의 안전한 업데이트에 사용됩니다.

주요 함수

사용 사례

IPsec 시퀀스 번호 증가, per-thread 카운터 집계, 배리어 동기화 플래그 등에서 활용됩니다.

/* IPsec SA 시퀀스 번호 원자적 증가 */
static_always_inline u32
ipsec_sa_assign_seq (ipsec_sa_t *sa)
{
    /* 여러 워커가 동시에 패킷을 암호화할 때 시퀀스 번호 충돌 방지 */
    return clib_atomic_fetch_add (&sa->seq, 1);
}

/* 멀티 워커 환경에서 공유 카운터 업데이트 */
typedef struct
{
    u64 packets;
    u64 bytes;
} shared_counter_t;

static_always_inline void
update_shared_counter (shared_counter_t *ctr, u32 n_packets, u32 n_bytes)
{
    clib_atomic_fetch_add (&ctr->packets, n_packets);
    clib_atomic_fetch_add (&ctr->bytes, n_bytes);
}

/* CAS를 사용한 lock-free 상태 전이 */
static_always_inline int
try_acquire_session (session_t *s)
{
    u32 expected = SESSION_STATE_IDLE;
    u32 desired = SESSION_STATE_ACTIVE;

    /* IDLE 상태일 때만 ACTIVE로 전이 */
    return clib_atomic_cmp_and_swap (&s->state, expected, desired);
}

/* acquire/release 시맨틱을 사용한 플래그 동기화 */
/* 메인 스레드: 설정 변경 완료 후 플래그 설정 */
void main_thread_update_config (my_config_t *cfg)
{
    cfg->new_value = 42;
    /* release: new_value 쓰기가 이 이전에 완료됨을 보장 */
    clib_atomic_store_rel_n (&cfg->ready, 1);
}

/* 워커 스레드: 플래그 확인 후 설정 읽기 */
void worker_thread_read_config (my_config_t *cfg)
{
    /* acquire: ready 읽기 이후에 new_value 읽기가 수행됨을 보장 */
    if (clib_atomic_load_acq_n (&cfg->ready))
    {
        u32 val = cfg->new_value; /* 안전하게 42를 읽음 */
        process_value (val);
    }
}

clib_time — 고해상도 시간 측정

VPP는 syscall 오버헤드를 피하기 위해 CPU의 TSC(Time Stamp Counter)를 직접 읽어 시간을 측정합니다. 패킷 처리 루프에서는 시간 측정도 성능에 영향을 미치므로, 여러 수준의 시간 함수가 제공됩니다.

주요 함수

성능 특성

vlib_time_now()는 메인 루프 시작 시 1회 TSC를 읽어 캐시하므로, 루프 내에서 반복 호출해도 오버헤드가 0입니다. 반면 clib_cpu_time_now()는 매번 rdtsc를 실행합니다.

/* 이벤트 타이머 — 세션 타임아웃 체크 */
static_always_inline int
session_is_expired (session_t *s, f64 now, f64 timeout)
{
    return (now - s->last_active_time) > timeout;
}

static uword
session_cleanup_process (vlib_main_t *vm, vlib_node_runtime_t *rt,
                         vlib_frame_t *f)
{
    f64 now = vlib_time_now (vm);  /* 캐시된 시간 — 오버헤드 0 */
    f64 timeout = 300.0;          /* 5분 타임아웃 */

    pool_foreach (s, session_pool)
    {
        if (session_is_expired (s, now, timeout))
            session_close (s);
    }
    return 0;
}

/* 성능 벤치마킹 — TSC 기반 정밀 측정 */
static_always_inline void
benchmark_function (vlib_main_t *vm)
{
    u64 start = clib_cpu_time_now ();

    /* 측정 대상 코드 */
    do_expensive_work ();

    u64 end = clib_cpu_time_now ();
    u64 cycles = end - start;

    vlib_cli_output (vm, "소요 사이클: %llu (%.2f us)",
                     cycles,
                     (f64) cycles / vm->clib_time.clocks_per_second * 1e6);
}

/* 프로세스 노드에서의 주기적 타이머 */
static uword
my_periodic_process (vlib_main_t *vm, vlib_node_runtime_t *rt,
                      vlib_frame_t *f)
{
    while (1)
    {
        /* 10초마다 깨어남 — 이벤트 기반이므로 CPU를 소비하지 않음 */
        vlib_process_wait_for_event_or_clock (vm, 10.0);

        f64 now = vlib_time_now (vm);
        do_periodic_work (now);
    }
    return 0;
}

clib_error — 에러 처리 프레임워크

clib_error_t는 에러 메시지 문자열과 코드를 담는 구조체이며, 함수 반환값으로 사용됩니다. NULL이면 성공, non-NULL이면 에러를 나타냅니다. VPP 전체에서 일관된 에러 처리 패턴을 제공합니다.

주요 매크로

노드별 에러 카운터

VLIB_REGISTER_NODE.error_stringsvlib_node_increment_counter()로 노드별 에러 통계를 관리합니다. show errors CLI 명령으로 확인할 수 있습니다.

/* 초기화 함수에서의 에러 처리 패턴 */
static clib_error_t *
my_plugin_init (vlib_main_t *vm)
{
    my_plugin_main_t *mp = &my_plugin_main;
    clib_error_t *error = 0;

    mp->vlib_main = vm;

    /* 리소스 할당 실패 시 에러 반환 */
    mp->table = clib_mem_alloc (TABLE_SIZE);
    if (!mp->table)
        return clib_error_return (0, "테이블 메모리 할당 실패 (%d 바이트)",
                                  TABLE_SIZE);

    /* 하위 모듈 초기화 — 에러 전파 */
    error = submodule_init (vm);
    if (error)
        return clib_error_return (error, "서브모듈 초기화 실패");

    return 0;  /* 성공 */
}
VLIB_INIT_FUNCTION (my_plugin_init);

/* CLI 핸들러에서의 에러 처리 */
static clib_error_t *
my_command_fn (vlib_main_t *vm, unformat_input_t *input,
               vlib_cli_command_t *cmd)
{
    u32 sw_if_index = ~0;

    while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
    {
        if (unformat (input, "interface %U",
                      unformat_vnet_sw_interface, vnm, &sw_if_index))
            ;
        else
            return clib_error_return (0, "알 수 없는 입력: '%U'",
                                      format_unformat_error, input);
    }

    if (sw_if_index == ~0)
        return clib_error_return (0, "인터페이스를 지정해야 합니다");

    return 0;
}

/* 노드별 에러 카운터 정의 */
#define foreach_my_node_error \
  _(PROCESSED, "packets processed") \
  _(DROPPED, "packets dropped") \
  _(NO_BUFFER, "no buffer available")

typedef enum
{
#define _(sym, str) MY_NODE_ERROR_##sym,
    foreach_my_node_error
#undef _
    MY_NODE_N_ERROR,
} my_node_error_t;

static char *my_node_error_strings[] = {
#define _(sym, str) str,
    foreach_my_node_error
#undef _
};

/* 노드 함수에서 에러 카운터 증가 */
static uword
my_node_fn (vlib_main_t *vm, vlib_node_runtime_t *node,
            vlib_frame_t *frame)
{
    u32 n_processed = 0, n_dropped = 0;

    /* ... 패킷 처리 ... */

    vlib_node_increment_counter (vm, node->node_index,
                                 MY_NODE_ERROR_PROCESSED, n_processed);
    vlib_node_increment_counter (vm, node->node_index,
                                 MY_NODE_ERROR_DROPPED, n_dropped);
    return frame->n_vectors;
}

VLIB_REGISTER_NODE (my_node) = {
    .function = my_node_fn,
    .name = "my-node",
    .vector_size = sizeof (u32),
    .n_errors = MY_NODE_N_ERROR,
    .error_strings = my_node_error_strings,
};

vnet_crypto — 암호화 프레임워크 API

VPP의 vnet_crypto 프레임워크는 IPsec과 TLS가 공통으로 사용하는 암호화 추상화 계층입니다. 다양한 하드웨어 가속기(Intel QAT, AES-NI 등)와 소프트웨어 구현을 통합 인터페이스로 관리하며, 엔진 교체 시 상위 프로토콜 코드를 수정할 필요가 없습니다.

핵심 구조체와 동기/비동기 API

vnet_crypto_op_t 구조체는 개별 암호화 연산을 기술합니다. 연산 유형(AES-GCM 암호화/복호화 등), 키 인덱스, IV(Initialization Vector), 소스/대상 버퍼 포인터, 인증 태그를 포함합니다.

엔진 등록 패턴

암호화 엔진은 vnet_crypto_register_engine()으로 등록됩니다. 각 엔진은 우선순위를 가지며, 동일 알고리즘에 대해 우선순위가 높은 엔진이 자동 선택됩니다. 알고리즘별 핸들러는 vnet_crypto_register_ops_handler()로 개별 등록합니다.

키 관리

IPsec에서 crypto op 생성 및 배치 실행

/* IPsec ESP 암호화에서 vnet_crypto 사용 의사 코드 */
static uword
esp_encrypt_inline (vlib_main_t *vm,
                    vlib_node_runtime_t *node,
                    vlib_frame_t *frame)
{
    u32 n_left = frame->n_vectors;
    u32 *from = vlib_frame_vector_args (frame);
    vnet_crypto_op_t ops[VLIB_FRAME_SIZE];
    vnet_crypto_op_t *op = ops;
    u32 n_ops = 0;

    while (n_left > 0)
    {
        vlib_buffer_t *b = vlib_get_buffer (vm, from[0]);
        ipsec_sa_t *sa = ipsec_sa_get (sa_index);

        /* 1. crypto op 초기화 */
        vnet_crypto_op_init (op, sa->crypto_enc_op_id);

        /* 2. 키 인덱스 설정 */
        op->key_index = sa->crypto_key_index;

        /* 3. IV 설정 (패킷별 고유) */
        op->iv = esp_hdr->iv;
        op->iv_len = sa->crypto_iv_size;

        /* 4. 소스/대상 버퍼 포인터 설정 */
        op->src = op->dst = vlib_buffer_get_current (b)
                           + sizeof (esp_header_t)
                           + sa->crypto_iv_size;
        op->len = payload_len;

        /* 5. AES-GCM: AAD와 태그 설정 */
        op->aad = esp_hdr;
        op->aad_len = 8;
        op->tag = tag_ptr;
        op->tag_len = 16;

        op++;
        n_ops++;
        n_left--;
        from++;
    }

    /* 6. 배치 실행 — 모든 op를 한 번에 처리 */
    vnet_crypto_process_ops (vm, ops, n_ops);

    /* 각 op의 status 필드로 성공/실패 확인 */
    for (u32 i = 0; i < n_ops; i++)
    {
        if (ops[i].status != VNET_CRYPTO_OP_STATUS_COMPLETED)
            vlib_node_increment_counter (vm, node->node_index,
                                         ESP_ENCRYPT_ERROR_CRYPTO, 1);
    }

    return frame->n_vectors;
}

커스텀 crypto 엔진 등록 패턴

/* 커스텀 암호화 엔진 등록 예제 */
typedef struct {
    u32 engine_index;
    /* HW 가속기 상태 */
    void *hw_ctx;
} my_crypto_engine_t;

static u32
my_crypto_aes_gcm_encrypt (vlib_main_t *vm,
                           vnet_crypto_op_t *ops[],
                           u32 n_ops)
{
    u32 n_completed = 0;

    for (u32 i = 0; i < n_ops; i++)
    {
        vnet_crypto_op_t *op = ops[i];

        /* HW 가속기로 AES-GCM 암호화 수행 */
        my_hw_aes_gcm_encrypt (op->iv, op->iv_len,
                               op->src, op->dst, op->len,
                               op->aad, op->aad_len,
                               op->tag, op->tag_len,
                               op->key_index);

        op->status = VNET_CRYPTO_OP_STATUS_COMPLETED;
        n_completed++;
    }

    return n_completed;
}

static clib_error_t *
my_crypto_engine_init (vlib_main_t *vm)
{
    my_crypto_engine_t *em = &my_crypto_engine_main;
    vnet_crypto_main_t *cm = &crypto_main;

    /* 1. 엔진 등록 (이름, 우선순위 80) */
    em->engine_index =
        vnet_crypto_register_engine (vm,
                                     "my_crypto_hw",
                                     80,    /* priority */
                                     "My HW Crypto Engine");

    /* 2. AES-128-GCM 암호화 핸들러 등록 */
    vnet_crypto_register_ops_handler (
        vm, em->engine_index,
        VNET_CRYPTO_OP_AES_128_GCM_ENC,
        my_crypto_aes_gcm_encrypt);

    /* 3. AES-128-GCM 복호화 핸들러 등록 */
    vnet_crypto_register_ops_handler (
        vm, em->engine_index,
        VNET_CRYPTO_OP_AES_128_GCM_DEC,
        my_crypto_aes_gcm_decrypt);

    /* 4. AES-256-GCM 등 추가 알고리즘도 동일 패턴 */
    vnet_crypto_register_ops_handler (
        vm, em->engine_index,
        VNET_CRYPTO_OP_AES_256_GCM_ENC,
        my_crypto_aes_256_gcm_encrypt);

    return 0;
}

/* VPP 부팅 시 자동 호출 */
VLIB_INIT_FUNCTION (my_crypto_engine_init);

플러그인 스켈레톤 코드

/* my_filter/my_filter.c — 커스텀 필터 플러그인 예제 */
#include <vlib/vlib.h>
#include <vnet/vnet.h>
#include <vnet/ip/ip4_packet.h>
#include <vnet/feature/feature.h>

/* 플러그인 등록 */
#include <vpp/app/version.h>
VLIB_PLUGIN_REGISTER () = {
    .version = VPP_BUILD_VER,
    .description = "My custom packet filter",
};

/* next 노드 인덱스 */
typedef enum {
    MY_FILTER_NEXT_PASS,      /* 통과 → ip4-lookup */
    MY_FILTER_NEXT_DROP,      /* 드롭 */
    MY_FILTER_N_NEXT,
} my_filter_next_t;

/* 노드 처리 함수 (벡터 단위) */
VLIB_NODE_FN (my_filter_node) (vlib_main_t *vm,
                                vlib_node_runtime_t *node,
                                vlib_frame_t *frame)
{
    u32 n_left_from, *from, *to_next;
    my_filter_next_t next_index;

    from = vlib_frame_vector_args (frame);
    n_left_from = frame->n_vectors;
    next_index = node->cached_next_index;

    while (n_left_from > 0)
    {
        u32 n_left_to_next;
        vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next);

        while (n_left_from > 0 && n_left_to_next > 0)
        {
            vlib_buffer_t *b0;
            ip4_header_t *ip0;
            u32 bi0, next0 = MY_FILTER_NEXT_PASS;

            bi0 = from[0];
            from += 1;
            n_left_from -= 1;
            to_next[0] = bi0;
            to_next += 1;
            n_left_to_next -= 1;

            b0 = vlib_get_buffer (vm, bi0);
            ip0 = vlib_buffer_get_current (b0);

            /* 예: 특정 목적지 IP 차단 */
            if (ip0->dst_address.as_u32 == clib_host_to_net_u32 (0x0a000001))
                next0 = MY_FILTER_NEXT_DROP;

            vlib_validate_buffer_enqueue_x1 (vm, node, next_index,
                to_next, n_left_to_next, bi0, next0);
        }
        vlib_put_next_frame (vm, node, next_index, n_left_to_next);
    }
    return frame->n_vectors;
}

/* 노드 등록 */
VLIB_REGISTER_NODE (my_filter_node) = {
    .name = "my-filter",
    .vector_size = sizeof (u32),
    .format_trace = format_my_filter_trace,
    .type = VLIB_NODE_TYPE_INTERNAL,
    .n_next_nodes = MY_FILTER_N_NEXT,
    .next_nodes = {
        [MY_FILTER_NEXT_PASS] = "ip4-lookup",
        [MY_FILTER_NEXT_DROP] = "error-drop",
    },
};

/* feature arc에 삽입 (ip4-unicast arc, ip4-lookup 앞) */
VNET_FEATURE_INIT (my_filter_feat, static) = {
    .arc_name = "ip4-unicast",
    .node_name = "my-filter",
    .runs_before = VNET_FEATURES ("ip4-lookup"),
    .runs_after = VNET_FEATURES ("ip4-input"),
};
# 플러그인 빌드 및 테스트
$ cd vpp
$ mkdir -p src/plugins/my_filter

# CMakeLists.txt 작성
$ cat > src/plugins/my_filter/CMakeLists.txt <<'EOF'
add_vpp_plugin(my_filter
  SOURCES
  my_filter.c
)
EOF

# 빌드
$ make build-release

# 플러그인 활성화
vpp# plugin my_filter_plugin.so enable

# feature 적용 (인터페이스별)
vpp# set interface feature GigabitEthernet0/8/0 my-filter arc ip4-unicast

# 검증
vpp# show node my-filter
vpp# show runtime
vpp# trace add my-filter 10

성능 최적화

VPP의 기본 DPDK 폴링 모드는 100% CPU 사용으로 최대 처리량을 제공하지만, 전력 소비와 유휴 시간 낭비가 문제됩니다. VPP는 여러 폴링 전략을 제공하여 처리량과 전력 소비 사이의 균형을 조정할 수 있습니다.

폴링 전략 비교

전략CPU 사용률 (유휴 시)레이턴시처리량적합한 환경
Busy poll (기본)100%최소최대전용 네트워크 어플라이언스
poll-sleep-poll5~30%낮음높음범용 서버 (전력 절감)
Interrupt mode~0% (유휴)중간 (첫 패킷 지연)중간저트래픽 환경
Adaptive동적동적동적가변 트래픽 패턴
# startup.conf — poll-sleep-poll 모드
cpu {
    main-core 0
    corelist-workers 1-3
}

# poll-sleep-poll: 패킷 없으면 짧은 sleep 후 재시도
unix {
    poll-sleep-usec 100                /* 100μs sleep (유휴 시) */
}

# interrupt 모드 (AF_XDP 등 지원 드라이버)
dpdk {
    dev 0000:00:08.0 {
        num-rx-queues 2
    }
}

# 런타임 모드 전환
vpp# set interface rx-mode GigabitEthernet0/8/0 queue 0 interrupt
vpp# set interface rx-mode GigabitEthernet0/8/0 queue 0 polling
vpp# set interface rx-mode GigabitEthernet0/8/0 queue 0 adaptive

성능 튜닝 체크리스트

항목확인 방법권장값
Hugepagecat /proc/meminfo | grep Huge워커당 2GB+, NUMA 균등 분배
CPU 격리cat /proc/cmdlineisolcpus, nohz_full, rcu_nocbs
NIC RSSshow hardware-interfaces detail큐 수 = 워커 수
버퍼 크기show buffersbuffers-per-numa 32768+
벡터 효율show runtime → Vectors/Call64+ (높을수록 좋음)
에러 카운터show errors0 (증가 시 원인 분석)
인터럽트 친화도(Affinity)/proc/interruptsVPP 코어와 겹치지 않게
NUMA 배치numactl --hardwareNIC/CPU/메모리 동일 노드
MTU 최적화show interface점보프레임(9000) 가능 시 활성화
TCP 튜닝 (VCL)show session소켓 버퍼, 윈도우 크기 조정
show runtime 분석: 가장 중요한 성능 진단 명령입니다. Clocks/Call이 높은 노드가 병목이며, Vectors/Call이 1이면 배치 효율이 낮습니다. clear runtime → 부하 인가 → show runtime으로 실시간 프로파일링(Profiling)하세요.

VPP + Kubernetes 통합

Kubernetes 환경에서 VPP는 CNI 플러그인의 데이터플레인으로 동작하여 Pod 네트워킹을 가속합니다. 현재 Calico/VPP가 사실상의 표준이며, 기존 Contiv-VPP는 유지보수가 중단되었습니다.

Calico/VPP — Kubernetes 네트워크 아키텍처 Kubernetes 컨트롤 플레인 kube-apiserver calico-kube-controllers etcd (BGP 정책) calico-node (BIRD BGP) Worker Node 1 Pod A 10.244.0.10 Pod B 10.244.0.11 Pod C 10.244.0.12 TAP/memif VPP (Calico VPP Agent) L3 라우팅 | ACL (NetworkPolicy) | IPsec | WireGuard | memif linux-cp DPDK / AF_XDP NIC (25/100GbE) Worker Node 2 Pod D 10.244.1.10 VPP (Calico VPP Agent) BGP 피어링 | IPsec 노드 간 암호화 DPDK / AF_XDP NIC IPsec/WireGuard BGP Calico VPP Agent가 Kubernetes NetworkPolicy를 VPP ACL로 변환, BGP로 Pod CIDR 광고

Calico/VPP 배포 및 설정

# 1. Calico operator 설치
$ kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27/manifests/tigera-operator.yaml

# 2. Calico/VPP 커스텀 리소스 생성
$ cat <<'EOF' | kubectl apply -f -
apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
  name: default
spec:
  cni:
    type: Calico
  calicoNetwork:
    linuxDataplane: VPP
    bgp: Enabled
    ipPools:
    - cidr: 10.244.0.0/16
      encapsulation: None
      natOutgoing: Enabled
---
apiVersion: operator.tigera.io/v1
kind: APIServer
metadata:
  name: default
spec: {}
EOF

# 3. VPP 관련 ConfigMap (워커 노드별 설정)
$ cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: calico-vpp-config
  namespace: calico-vpp-dataplane
data:
  CALICOVPP_INTERFACES: |-
    {
      "uplinkInterfaces": [{
        "interfaceName": "eth0",
        "vppDriver": "af_xdp"
      }]
    }
  CALICOVPP_CONFIG_TEMPLATE: |-
    unix {
      nodaemon
      full-coredump
      cli-listen /var/run/vpp/cli.sock
    }
    cpu { main-core 0 workers 2 }
    buffers { buffers-per-numa 32768 }
EOF

# 4. 확인
$ kubectl get pods -n calico-vpp-dataplane
NAME                    READY   STATUS    RESTARTS
calico-vpp-node-xxxxx   2/2     Running   0

# 5. VPP CLI 접근 (노드 내에서)
$ kubectl exec -n calico-vpp-dataplane calico-vpp-node-xxxxx \
    -c vpp -- vppctl show interface
$ kubectl exec -n calico-vpp-dataplane calico-vpp-node-xxxxx \
    -c vpp -- vppctl show runtime

NetworkPolicy 적용

# Calico/VPP는 Kubernetes NetworkPolicy를 VPP ACL로 변환
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-web-only
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: web-server
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          role: frontend
    ports:
    - protocol: TCP
      port: 80
    - protocol: TCP
      port: 443
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: database
    ports:
    - protocol: TCP
      port: 5432
# VPP에서 변환된 ACL 확인
vpp# show acl-plugin acl
# acl-index 0: permit proto 6 src 10.244.0.0/16 dport 80 ...

# Pod 간 memif 직접 연결 확인 (동일 노드 내)
vpp# show memif
# interface memif0/0 ... status: connected (Pod A)
# interface memif0/1 ... status: connected (Pod B)
AF_XDP vs DPDK: Calico/VPP에서 vppDriver: "af_xdp"를 사용하면 NIC를 커널과 공유할 수 있어 노드 관리(SSH 등)가 편리합니다. 최대 성능이 필요하면 "dpdk"를 사용하되, 관리 인터페이스를 별도 확보하세요.

VPP 모니터링 체계

VPP는 stats segment(공유 메모리 기반 통계 영역)를 통해 실시간 성능 지표를 제공합니다. Prometheus, Grafana와 연동하여 운영 가시성을 확보할 수 있습니다.

Stats Segment 구조

카테고리경로 패턴예시설명
인터페이스/if/<stat>/if/rx-packets, /if/tx-bytes인터페이스별 패킷/바이트 카운터
노드/node/<name>/<stat>/node/ip4-input/calls노드별 호출 횟수, 벡터, 클럭
에러/err/<node>/<error>/err/ip4-input/ip4 spoofed노드별 에러 카운터
시스템/sys/<stat>/sys/vector_rate글로벌 벡터 처리율
버퍼/buffer-pools/<pool>/buffer-pools/default/available버퍼 풀 사용률
NAT/nat44/<stat>/nat44/total-sessionsNAT 세션 수
# stats segment 직접 접근 (C 클라이언트)
$ vpp_get_stats dump /if/rx-packets
[0]: 1523456
[1]: 892341

# CLI로 확인
vpp# show stats /if/rx
vpp# show stats /sys/vector_rate
vpp# show stats dump

# Python API로 접근
$ python3 -c "
from vpp_papi.vpp_stats import VPPStats
stats = VPPStats(socketname='/run/vpp/stats.sock')
stats.connect()
print('Vector rate:', stats['/sys/vector_rate'])
print('RX packets:', stats['/if/rx-packets'])
stats.disconnect()
"

Prometheus Exporter 연동

# vpp-exporter 실행 (공식 vpp_prometheus_export 또는 서드파티)
$ vpp_prometheus_export --stats-socket /run/vpp/stats.sock \
    --listen 0.0.0.0:9482

# Prometheus 설정 (prometheus.yml)
scrape_configs:
  - job_name: 'vpp'
    static_configs:
      - targets: ['vpp-host:9482']
    scrape_interval: 5s

# 주요 메트릭
# vpp_interface_rx_packets{interface="GigabitEthernet0/8/0"} 1523456
# vpp_interface_tx_bytes{interface="GigabitEthernet0/9/0"} 892341234
# vpp_node_calls{node="ip4-input"} 7654321
# vpp_node_vectors_per_call{node="ip4-lookup"} 42.3
# vpp_buffer_available{pool="default"} 28672
# vpp_nat44_sessions_total 12345
# vpp_system_vector_rate 2.4e6

Grafana 대시보드 주요 패널

패널쿼리 (PromQL)의미
인터페이스 PPSrate(vpp_interface_rx_packets[5m])초당 수신 패킷 수
벡터 효율vpp_node_vectors_per_call{node="dpdk-input"}배치 크기 (높을수록 효율적)
버퍼 사용률1 - vpp_buffer_available / vpp_buffer_total버퍼 풀 소진 경고
드롭 비율rate(vpp_interface_drops[5m]) / rate(vpp_interface_rx_packets[5m])패킷 드롭 비율
NAT 세션vpp_nat44_sessions_total활성 NAT 세션 수
CPU 사이클/패킷rate(vpp_node_clocks[5m]) / rate(vpp_node_vectors[5m])노드별 처리 효율
알림 설정: 버퍼 사용률 80% 이상, 드롭 비율 0.1% 이상, NAT 세션 수 최대값의 80% 도달 시 알림을 설정하세요. VPP의 show errors에서 증가하는 에러 카운터도 Prometheus로 수집하여 조기 경고에 활용할 수 있습니다.

실습 랩

랩 토폴로지(Topology)

3개의 네트워크 네임스페이스(Namespace)와 VPP TAP 인터페이스를 사용한 라우팅/NAT 실습 환경입니다. 물리 NIC 없이 로컬에서 VPP의 핵심 기능을 검증할 수 있습니다.

VPP 실습 랩 토폴로지 ns-client vpp-tap0 192.168.1.1/24 클라이언트 역할 VPP tap0 192.168.1.2 tap1 10.0.0.1 ip4-lookup | NAT44 ACL | trace ns-server vpp-tap1 10.0.0.2/24 서버 역할 호스트 (모니터링) vppctl, tcpdump, curl

환경 구성

#!/bin/bash — VPP 실습 랩 환경 구성

# 1. 네임스페이스 생성
sudo ip netns add ns-client
sudo ip netns add ns-server

# 2. VPP 시작 (minimal startup.conf)
cat <<'EOF' | sudo tee /tmp/lab-startup.conf
unix {
    cli-listen /run/vpp/cli.sock
    log /tmp/vpp-lab.log
    nodaemon
}
api-segment { gid $(id -gn) }
plugins {
    plugin default { disable }
    plugin nat_plugin.so { enable }
    plugin acl_plugin.so { enable }
    plugin ping_plugin.so { enable }
}
EOF

sudo vpp -c /tmp/lab-startup.conf &
sleep 2

# 3. TAP 인터페이스 생성
sudo vppctl create tap id 0 host-if-name vpp-tap0 host-ns ns-client \
    host-ip4-addr 192.168.1.1/24
sudo vppctl set interface ip address tap0 192.168.1.2/24
sudo vppctl set interface state tap0 up

sudo vppctl create tap id 1 host-if-name vpp-tap1 host-ns ns-server \
    host-ip4-addr 10.0.0.2/24
sudo vppctl set interface ip address tap1 10.0.0.1/24
sudo vppctl set interface state tap1 up

# 4. 네임스페이스 기본 게이트웨이 설정
sudo ip netns exec ns-client ip route add default via 192.168.1.2
sudo ip netns exec ns-server ip route add default via 10.0.0.1

echo "Lab ready! Test: sudo ip netns exec ns-client ping 10.0.0.2"

라우팅 및 NAT 실습

# IP 라우팅 확인
vpp# show ip fib
# 192.168.1.0/24 → tap0, 10.0.0.0/24 → tap1 확인

# NAT44 설정 (ns-client → ns-server 시 주소 변환)
vpp# nat44 plugin enable sessions 1024
vpp# nat44 add interface address tap1
vpp# set interface nat44 in tap0 out tap1

# 정적 매핑 (ns-server의 포트 8080 → ns-client)
vpp# nat44 add static mapping local 192.168.1.1 8080 \
     external tap1 8080 tcp

# ACL 설정 (ICMP만 허용 테스트)
vpp# set acl-plugin acl permit proto 1       /* ICMP */
vpp# set acl-plugin acl permit proto 6 dport 8080  /* TCP 8080 */
vpp# set acl-plugin acl deny
vpp# set acl-plugin interface tap0 input acl 0
vpp# set acl-plugin interface tap0 input acl 1
vpp# set acl-plugin interface tap0 input acl 2

검증 및 모니터링

# 1. 기본 연결 테스트
$ sudo ip netns exec ns-client ping -c 3 10.0.0.2
PING 10.0.0.2: 64 bytes from 10.0.0.2: icmp_seq=1 ttl=63 time=0.12 ms

# 2. NAT 세션 확인
vpp# show nat44 sessions
  192.168.1.1:45000 -> 10.0.0.1:45000 -> 10.0.0.2:8080 [TCP]

# 3. 패킷 트레이싱으로 전체 경로 확인
vpp# trace add virtio-input 10
$ sudo ip netns exec ns-client ping -c 1 10.0.0.2
vpp# show trace
# virtio-input → ethernet-input → ip4-input → nat44-in2out →
#   ip4-lookup → ip4-rewrite → tap1-output 경로 확인

# 4. 런타임 통계
vpp# show runtime
# 각 노드의 Calls, Vectors, Clocks/Call 확인

# 5. 에러 확인
vpp# show errors
# 에러 카운터가 0이면 정상

# 6. 정리
$ sudo killall vpp
$ sudo ip netns del ns-client
$ sudo ip netns del ns-server
trace로 패킷 흐름 시각화: trace add virtio-input 10 → ping → show trace는 VPP 학습의 가장 효과적인 방법입니다. 각 패킷이 어떤 노드를 거치고, 어떤 결정(FIB lookup, NAT 변환, ACL 매칭)을 받는지 한눈에 파악할 수 있습니다.

커널 네트워크 스택과의 비교

성능 비교

시나리오커널 스택VPP (DPDK)VPP (AF_XDP)
L3 포워딩 (64B, 10GbE)~1.2 Mpps~14.8 Mpps (라인레이트)~11 Mpps
L2 브릿징 (64B, 10GbE)~2 Mpps~14.8 Mpps (라인레이트)~12 Mpps
IPsec (AES-GCM-256, 1500B)~2 Gbps~20 Gbps (AES-NI)
NAT44 (64B)~800 Kpps~10 Mpps~8 Mpps
TCP 연결 처리우수 (네이티브)제한적 (VCL)제한적
주의: 성능 수치는 하드웨어, 패킷 크기, 규칙 수에 따라 크게 달라집니다. 위 표는 단일 코어, 10GbE NIC 기준의 대략적인 참고값입니다.

적용 시나리오 선택 기준

기준커널 스택 적합VPP 적합
TCP 애플리케이션웹 서버, DB, 범용 서비스L3/L4 포워딩, 터널링
성능 요구< 5 Gbps, 범용 처리> 10 Gbps, 라인레이트 포워딩
기능 복잡도소켓 API, iptables, tcL2/L3/VPN/NAT 고성능 처리
운영 편의성표준 Linux 도구 사용별도 CLI/API 학습 필요
NIC 공유여러 앱이 NIC 공유NIC 독점(DPDK) 또는 AF_XDP
에코시스템모든 Linux 도구 호환linux-cp로 일부 통합

커널 관련 소스 및 참고 코드

VPP가 활용하는 커널 서브시스템과 관련 소스 파일 매핑:

커널 서브시스템소스 파일VPP 연관
TUN/TAPdrivers/net/tun.cVPP TAP 인터페이스, /dev/net/tun
vhostdrivers/vhost/net.c, drivers/vhost/vhost.cvhost-user 백엔드, virtio 링
virtiodrivers/virtio/virtio_ring.cTAP virtio, VM 네트워킹
AF_PACKETnet/packet/af_packet.chost-interface, PACKET_MMAP
AF_XDPnet/xdp/xsk.c, net/xdp/xsk_buff_pool.cAF_XDP 인터페이스
XDPnet/core/dev.c (XDP hooks)AF_XDP용 XDP 프로그램
UIOdrivers/uio/uio_pci_generic.cDPDK UIO 드라이버
VFIOdrivers/vfio/pci/vfio_pci_core.cDPDK VFIO 드라이버 (IOMMU)
Hugepagesmm/hugetlb.c, fs/hugetlbfs/DPDK/VPP 메모리 할당
IOMMUdrivers/iommu/intel/iommu.c, drivers/iommu/amd/iommu.cVFIO DMA 격리
Netlinknet/netlink/af_netlink.clinux-cp 플러그인, 라우팅 동기화
Namespacenet/core/net_namespace.cTAP 네임스페이스 격리
소스 탐색: VPP 소스 코드는 https://gerrit.fd.io/r/gitweb?p=vpp.git에서 확인할 수 있습니다. 주요 디렉터리: src/vnet/(네트워킹 코어), src/vlib/(벡터 처리 엔진), src/plugins/(플러그인), src/vpp/(메인 프로세스).

참고자료

공식 문서

아키텍처 및 설계

성능 및 벤치마크

통합 및 생태계

주요 참고 글

커널 소스 경로 (VPP 연동)

이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.