VPP 실전 구성 시나리오

VPP(Vector Packet Processing) 프레임워크를 활용한 실전 네트워크 구성 시나리오를 다룹니다. DPDK 기반 L3 라우터와 NAT44 결합, memif 기반 서비스 펑션 체이닝(SFC), IPsec VPN 게이트웨이, SSL/TLS Inspection까지 실무에서 바로 적용할 수 있는 구성을 포괄합니다.

전제 조건: VPP 코어 아키텍처 문서를 먼저 읽으세요. VPP의 그래프 노드 모델, DPDK 통합, 커널 인터페이스에 대한 이해가 필요합니다.

개요

이 문서에서는 VPP를 실제 운영 환경에 배포하는 대표적인 시나리오를 다룹니다. 각 시나리오는 startup.conf 설정부터 인터페이스 구성, 데이터 경로 분석, 성능 확인까지 엔드-투-엔드 절차를 제공합니다.

시나리오주요 기능인터페이스적합한 환경
L3 라우터 + NAT44IP 포워딩, 주소 변환DPDK PMD엣지 라우터, 게이트웨이
memif 서비스 체이닝제로카피 패킷 전달, SFCmemif (공유 메모리)NFV, 다단계 패킷 처리
IPsec VPN터널 암호화, IKEv2DPDK + ipsec사이트 간 VPN, 원격 접속
SSL/TLS InspectionTLS 종단·복호화·재암호화VCL + TLS 플러그인NGFW, 보안 게이트웨이, DLP
공통 전제: 모든 시나리오는 Hugepage 2MB x 1024 이상, CPU 격리(isolcpus), DPDK 호환 NIC를 가정합니다. VPP 설치 및 설정을 참고하여 기본 환경을 먼저 구성하세요.

실전 예제: 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 같은 라우팅 데몬과 연동할 수 있습니다.

첫 플로우 생성과 fast path 전환

NAT44를 실무에서 이해할 때 핵심은 첫 패킷그다음 패킷을 분리해서 보는 것입니다. 첫 패킷은 BIB(Basic Information Base)와 세션을 만들기 위해 상대적으로 무거운 경로를 거치고, 이후 동일 플로우는 이미 만들어진 상태를 조회하는 빠른 경로로 내려갑니다.

NAT44는 첫 패킷에서 상태를 만들고, 이후 패킷은 그 상태를 바로 재사용합니다. 첫 패킷: slow path LAN 클라이언트 192.168.1.10:45000 nat44-in2out-slowpath 정책 확인, 주소 선택 상태 테이블 생성 BIB: 192.168.1.10:45000 → 203.0.113.1:61000 세션과 타이머 생성 ip4-lookup 다음 홉 결정 WAN 송신 203.0.113.1 이후 패킷: fast path nat44-in2out / nat44-out2in 기존 세션 키로 바로 검색 slow path 재진입 없이 주소와 포트만 재작성 운영에서 봐야 할 관찰 포인트 첫 요청 직후 세션 수가 1 증가하고, 이후 부하에서는 fast path 호출 비중이 커져야 합니다. 계속 slow path만 늘어나면 세션 재사용 실패, 비대칭 경로, 짧은 타이머를 의심해야 합니다.
# 1. 상태를 깨끗하게 초기화
vpp# clear runtime
vpp# clear trace
vpp# trace add dpdk-input 10
vpp# show nat44 sessions detail

# 2. 첫 플로우 생성
# LAN 측 클라이언트에서 외부 HTTP 서버로 연결 1개를 생성합니다.
$ curl http://198.51.100.10/

# 3. VPP에서 상태 확인
vpp# show nat44 sessions detail
vpp# show runtime
vpp# show trace

# 4. 같은 연결을 반복해서 부하를 줍니다.
$ for i in $(seq 1 1000); do curl -s http://203.0.113.1:8080/ > /dev/null; done

# 5. fast path 전환 여부 확인
vpp# show nat44 sessions detail
vpp# show runtime
vpp# show errors
시점관찰해야 할 출력해석
트래픽 전show nat44 sessions detail에 세션이 거의 없습니다.기준선입니다. 이전 테스트 세션이 남아 있으면 결과 해석이 흐려집니다.
첫 요청 직후세션이 1개 이상 생기고, trace에 nat44-in2out-slowpath가 보일 수 있습니다.상태 생성이 정상입니다. 여기서 세션이 안 생기면 인터페이스 방향, 주소 풀, ACL 차단을 먼저 의심해야 합니다.
반복 부하 후show runtime에서 fast path 노드 호출이 꾸준히 증가합니다.이미 생성된 세션을 재사용하고 있음을 뜻합니다. 처리량이 올라가도 slow path 비중이 높지 않아야 합니다.
에러 증가 시show errors에 NAT 드롭 또는 no translation 관련 카운터가 보입니다.세션 용량, 번역 주소 고갈, 비대칭 응답 경로를 함께 봐야 합니다.
실전 해석: 첫 요청에서만 느리고 이후 빨라지는 것은 정상입니다. 반대로 모든 패킷이 계속 느리다면 NAT 자체보다도 세션 재사용이 깨지는 경로 비대칭, 너무 짧은 타이머, 또는 플로우가 매번 다른 큐와 워커로 갈라지는 문제를 먼저 봐야 합니다.

NAT44 세션 테이블 내부 구조

VPP NAT44의 성능을 이해하려면 세션 테이블이 어떻게 구성되는지 알아야 합니다. NAT44는 내부적으로 BIHash(Bounded-index Extensible Hash) 기반의 세션 테이블을 사용하며, 각 세션은 5-튜플(src IP, dst IP, src port, dst port, protocol)을 키로 합니다.

/* src/plugins/nat/nat44-ed/nat44_ed.h — NAT44 ED 세션 구조체 */
typedef struct {
  /* in2out 키 (LAN → WAN) */
  nat_6t_flow_t i2o;
  /* out2in 키 (WAN → LAN) — 역방향 매핑 */
  nat_6t_flow_t o2i;

  u32 flags;              /* 정적/동적, TCP 상태, FIN 감지 등 */
  u32 thread_index;       /* 이 세션을 소유한 워커 스레드 */
  u32 per_user_index;     /* 사용자별 세션 목록 인덱스 */

  /* 타이머: 세션 만료 관리 */
  f64 last_heard;         /* 마지막 패킷 수신 시각 (vlib_time_now) */
  u32 lru_head_index;     /* LRU 리스트 위치 (GC 우선순위) */

  /* 카운터 */
  u64 total_pkts;
  u64 total_bytes;
} snat_session_t;

세션이 생성되면 i2oo2i 양방향 키가 동시에 해시 테이블에 삽입됩니다. 이후 패킷은 키 조회 한 번으로 변환 정보를 가져오므로, 세션 수가 늘어나도 조회 시간은 O(1)에 근접합니다.

/* src/plugins/nat/nat44-ed/nat44_ed_in2out.c — fast path 핵심 로직 */
static_always_inline int
nat44_ed_in2out_fast_path (snat_main_t *sm, vlib_buffer_t *b,
                           ip4_header_t *ip, u32 rx_fib_index)
{
  clib_bihash_kv_16_8_t kv, value;

  /* 5-튜플로 해시 키 생성 */
  init_ed_k (&kv, ip->src_address, src_port,
             ip->dst_address, dst_port, rx_fib_index, ip->protocol);

  /* BIHash 조회 — 히트하면 바로 변환 */
  if (clib_bihash_search_16_8 (&sm->flow_hash, &kv, &value) == 0)
    {
      snat_session_t *s = pool_elt_at_index (tsm->sessions, value.value);
      s->last_heard = vlib_time_now (vm);
      s->total_pkts++;
      s->total_bytes += vlib_buffer_length_in_chain (vm, b);

      /* IP 주소와 포트 재작성 */
      nat_6t_flow_ip4_translate (sm, b, ip, &s->i2o);
      return 0;  /* fast path 성공 */
    }

  return 1;  /* miss → slow path로 전환 */
}
BIHash 특성: clib_bihash_16_8은 16바이트 키, 8바이트 값의 bounded-index 해시로, 버킷 수가 2의 거듭제곱이며 충돌 시 체인이 아닌 페이지 단위 확장을 합니다. 세션 수가 수백만에 도달해도 캐시 미스율이 낮아 일정한 조회 성능을 유지합니다.

NAT44 성능 튜닝 파라미터

운영 환경에서 NAT44의 처리량과 안정성을 좌우하는 핵심 파라미터입니다. 기본값은 소규모 테스트에 맞춰져 있어, 실제 트래픽에서는 반드시 조정해야 합니다.

파라미터CLI 명령기본값권장 (10Gbps급)영향
최대 세션 수nat44 plugin enable sessions N655361048576세션 고갈 시 신규 연결 차단
사용자당 세션nat44 plugin enable user-sessions N1000050000단일 내부 IP의 세션 상한
TCP established 타이머nat set timeouts tcp-established N7440초3600초너무 길면 세션 낭비, 짧으면 끊김
TCP transitory 타이머nat set timeouts tcp-transitory N240초120초SYN/FIN 상태 세션 유지 시간
UDP 타이머nat set timeouts udp N300초120초DNS 등 짧은 플로우에 영향
주소 풀 크기nat44 add address1개가능한 많이포트 고갈 방지 (IP당 ~64K 포트)
# 실전 튜닝 예시: 10Gbps 엣지 라우터
vpp# nat44 plugin enable sessions 1048576
vpp# nat set timeouts tcp-established 3600
vpp# nat set timeouts tcp-transitory 120
vpp# nat set timeouts udp 120

# 주소 풀 확장: WAN IP 여러 개 등록
vpp# nat44 add address 203.0.113.1 - 203.0.113.4

# 워커별 세션 분포 확인 (불균형 → RSS 설정 점검)
vpp# show nat44 sessions count
vpp# show nat44 summary

# 세션 GC(Garbage Collection) 동작 확인
vpp# show nat44 sessions detail | grep "last heard"
포트 고갈 주의: NAT44는 IP당 최대 약 64,000개의 포트를 사용할 수 있습니다. 단일 외부 IP로 100,000 세션 이상을 처리해야 한다면 반드시 주소 풀을 확장하세요. show errors에서 nat44-ed-in2out no free external addr 카운터가 증가하면 풀 고갈 징후입니다.

실전 예제: 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로 링 사용률과 오류 카운터를 확인하세요.

memif 링 동작과 병목 확인 방법

memif는 단순한 "가상 케이블"이 아니라, 두 프로세스가 공유 메모리 위에서 서술자 링(descriptor ring)을 교환하는 구조입니다. 성능 병목이 생기면 대부분 패킷 처리 로직보다 링 고갈, peer 지연, 큐 배치 불일치에서 먼저 징후가 나타납니다.

memif는 공유 메모리의 두 개 링을 통해 동작합니다. VPP #1 (master) memif1/0 RX queue memif1/0 TX queue 공유 메모리 영역 S2M ring (slave → master) M2S ring (master → slave) 각 슬롯은 buffer offset, length, flags를 가리킵니다. 패킷 자체를 다시 복사하지 않고, 어느 버퍼를 읽을지 기술합니다. VPP #2 (slave) memif1/0 RX queue memif1/0 TX queue 한쪽 프로세스가 링을 제때 비우지 못하면 다른 쪽은 no free tx slots나 큐 정체로 바로 드러납니다.
# 보다 구체적인 생성 예시
vpp1# create memif socket id 1 filename /run/vpp/memif-sfc.sock
vpp1# create interface memif id 0 socket-id 1 master ring-size 1024 buffer-size 2048
vpp1# set interface state memif1/0 up

vpp2# create memif socket id 1 filename /run/vpp/memif-sfc.sock
vpp2# create interface memif id 0 socket-id 1 slave ring-size 1024 buffer-size 2048
vpp2# set interface state memif1/0 up

# 병목 확인
vpp1# show memif
vpp1# show hardware-interfaces memif1/0
vpp1# show interface rx-placement
vpp1# show runtime
vpp1# show errors
관찰 값의미우선 점검 항목
connected 플래그 미표시핸드셰이크 실패소켓 경로, 권한, master/slave 역할
RX ring 사용률만 높음수신 측이 처리 속도를 따라가지 못함워커 배치, downstream 노드 clocks/call
TX 슬롯 부족peer가 링을 제때 비우지 못함상대 프로세스 CPU 핀닝, 큐 정체
에러는 없지만 처리량 저하링 자체보다 feature/lookup 비용 문제show runtime의 핫 노드 확인
디버깅 순서: memif 장애는 먼저 연결 상태와 링 고갈 여부를 보고, 그다음에 그래프 노드 비용을 봐야 합니다. 곧바로 패킷 페이로드를 캡처하는 것보다 show memifshow runtime을 함께 보는 편이 훨씬 빠릅니다.

memif 분류기(Classifier) 구성

서비스 체이닝에서 핵심은 어떤 트래픽을 어느 체인으로 보낼지 결정하는 분류기입니다. VPP의 classify 테이블은 ACL과 달리 패킷 헤더의 임의 오프셋을 마스크 매칭할 수 있어 유연한 분류가 가능합니다.

# 분류 테이블 생성: TCP 트래픽(proto=6)을 방화벽 체인으로
# mask: IP 프로토콜 필드(offset 23)만 검사
vpp1# classify table mask l3 ip4 proto

# 매치 규칙: TCP(0x06) → memif1/0 (방화벽 체인)
vpp1# classify session acl-hit-next permit table-index 0 \
      match l3 ip4 proto 6 action set-ip4-fib-id 0
vpp1# set interface input acl intfc GigabitEthernet0/8/0 ip4-table 0

# 비TCP 트래픽 → memif2/0 (DPI+NAT 체인)으로 L2 xconnect
vpp1# set interface l2 xconnect GigabitEthernet0/8/0 memif2/0
vpp1# set interface l2 xconnect memif2/0 GigabitEthernet0/8/0

보다 세밀한 분류가 필요하면 다단계 테이블 체인을 구성할 수 있습니다.

# 1단계: 프로토콜 분류 (TCP vs UDP vs 기타)
vpp1# classify table mask l3 ip4 proto

# 2단계: TCP 중 대상 포트별 세분화 (80/443 → 웹, 나머지 → 기본)
vpp1# classify table mask l4 dst_port next-table 0
vpp1# classify session table-index 1 match l4 dst_port 80 \
      action set-ip4-fib-id 1
vpp1# classify session table-index 1 match l4 dst_port 443 \
      action set-ip4-fib-id 1

# FIB 1은 memif1/0(웹 방화벽)으로 라우팅,
# FIB 0(기본)은 memif2/0(DPI)으로 라우팅

memif vs 다른 가상 인터페이스 비교

VPP에서 인스턴스 간 패킷을 전달하는 방법은 여러 가지가 있습니다. 각 방법의 특성과 적합한 용도를 비교합니다.

인터페이스카피 횟수64B 처리량지연적합한 환경
memif제로카피30+ Mpps~1μsVPP ↔ VPP, VPP ↔ DPDK 앱
veth pair2회 (커널 경유)~2 Mpps~10μsVPP ↔ 커널 네임스페이스
TAP v21회 (virtio)~5 Mpps~5μsVPP ↔ 호스트 OS
AF_XDP제로카피 가능~15 Mpps~2μsVPP ↔ XDP 프로그램
vhost-user1회 (virtio)~8 Mpps~3μsVPP ↔ VM (QEMU)
memif 선택 기준: 동일 호스트에서 VPP 인스턴스끼리 연결하거나, libmemif를 사용하는 커스텀 DPDK 앱과 연결할 때는 memif가 압도적으로 유리합니다. 반면, 커널 네트워크 스택이나 컨테이너 네임스페이스와 연동해야 한다면 TAP v2나 AF_XDP를 고려하세요.

memif 공유 메모리 내부 구현

memif의 제로카피가 실제로 어떻게 동작하는지, 핵심 자료구조를 살펴봅니다.

/* src/plugins/memif/memif.h — 서술자 링 구조 */
typedef struct {
  u16 flags;           /* MEMIF_DESC_FLAG_NEXT: 체인 다음 버퍼 존재 */
  u32 region;          /* 공유 메모리 영역 인덱스 */
  u32 offset;          /* 영역 내 버퍼 시작 오프셋 */
  u32 length;          /* 패킷 데이터 길이 */
  u8  metadata[0];     /* 가변 길이 메타데이터 */
} memif_desc_t;

/* 링 구조: head/tail 포인터로 생산자/소비자 패턴 구현 */
typedef struct {
  volatile u16 head;   /* 생산자가 전진 (새 서술자 추가) */
  volatile u16 tail;   /* 소비자가 전진 (서술자 소비) */
  u16 cookie;           /* 버전 검증용 매직 넘버 */
  u16 flags;            /* MEMIF_RING_FLAG_MASK_INT */
  memif_desc_t desc[0]; /* ring-size 개의 서술자 배열 */
} memif_ring_t;
memif 서술자 링: head/tail 기반 생산자-소비자 패턴 M2S ring (master → slave 방향, ring-size=8 예시) desc[0] desc[1] desc[2] desc[3] desc[4] desc[5] desc[6] desc[7] tail=2 (소비자: slave) head=5 (생산자: master) 이미 소비된 슬롯 (재사용 가능) 대기 중인 패킷 (tail ≤ i < head) 빈 슬롯 동작 원리: master가 head를 전진시키며 서술자를 채우고, slave가 tail을 전진시키며 소비합니다. head == tail이면 링이 비었고, (head+1) % size == tail이면 가득 찼습니다. 서술자는 실제 패킷 데이터가 아닌 공유 메모리 영역 내 offset만 가리키므로 복사가 없습니다.
ring-size 선택: 링 크기가 너무 작으면 burst 트래픽에서 바로 가득 차서 드롭이 발생하고, 너무 크면 메모리를 낭비합니다. 일반적으로 ring-size 1024(기본값)는 10Gbps 이하에서 충분하며, 25Gbps 이상에서는 ring-size 2048 또는 ring-size 4096을 고려하세요.

실전 예제: 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 설정으로 하드웨어 암호화 오프로딩(Offloading)이 가능합니다. startup.confdpdk { dev 0000:XX:XX.0 { name cryptodev0 } }를 추가하고, set crypto handler all dpdk로 활성화하세요.

IPsec 패킷 처리 흐름

VPP에서 IPsec 패킷이 어떤 그래프 노드를 거치는지 이해하면, 성능 병목과 설정 오류를 빠르게 찾을 수 있습니다. 아웃바운드(암호화)와 인바운드(복호화) 경로는 완전히 다른 노드 체인을 거칩니다.

VPP IPsec 패킷 처리 흐름 (아웃바운드 / 인바운드) 아웃바운드: 평문 → 암호화 → WAN 송신 LAN 수신 dpdk-input ip4-input 헤더 검증 ip4-lookup FIB → ipsec0 매치 esp-encrypt SA 조회, AES-GCM ip4-rewrite 외부 IP 헤더 WAN TX dpdk-output 인바운드: WAN 수신 → 복호화 → LAN 전달 WAN 수신 dpdk-input ip4-input ESP(proto 50) ip4-lookup ESP → ipsec 노드 esp-decrypt SPI → SA, 복호화 ip4-input-no-chk 내부 패킷 파싱 LAN TX dpdk-output Security Association (SA) — 암호화/복호화의 모든 정보를 담는 구조 SPI (Security Parameter Index): 인바운드 패킷에서 어떤 SA를 사용할지 결정하는 키 crypto-alg: 암호화 알고리즘 (AES-GCM-256 권장 — 인증+암호화 통합, HW 가속 효율적) integ-alg: 무결성 알고리즘 (AES-GCM 사용 시 별도 불필요 — AEAD 모드) tunnel src/dst: 외부 IP 헤더에 사용할 터널 양 끝단 주소 replay-window: anti-replay 윈도우 크기 (기본 64, 고속에서 128~256 권장)

암호화 엔진 선택과 QAT 상세 구성

VPP는 여러 암호화 백엔드를 지원합니다. 하드웨어 가용성에 따라 최적의 엔진을 선택하는 것이 IPsec 성능의 핵심입니다.

엔진구현AES-GCM-256 처리량CPU 사용적합 환경
ipsecmbIntel IPSec-MB 라이브러리~5 Gbps/코어높음AES-NI 지원 CPU
nativeVPP 내장 (AES-NI 직접)~4 Gbps/코어높음추가 라이브러리 없이
opensslOpenSSL EVP API~2 Gbps/코어매우 높음범용, 비 Intel CPU
dpdk cryptodevQAT/AESNI-MB PMD~20 Gbps/장치매우 낮음QAT 카드 보유 시
# startup.conf — QAT 하드웨어 가속 IPsec 구성
dpdk {
    dev 0000:00:08.0 { name GigabitEthernet0/8/0 }
    dev 0000:00:09.0 { name GigabitEthernet0/9/0 }
    /* QAT VF (Virtual Function) — lspci로 확인 */
    dev 0000:3d:01.0 { name cryptodev0 }
    dev 0000:3d:01.1 { name cryptodev1 }
}

plugins {
    plugin default { disable }
    plugin dpdk_plugin.so { enable }
    plugin crypto_ipsecmb_plugin.so { enable }   /* SW 폴백 */
}
# 암호화 엔진 우선순위 설정
# QAT를 1순위, IPsec-MB를 2순위(폴백)로 구성
vpp# set crypto handler aes-256-gcm dpdk
vpp# set crypto handler aes-256-gcm ipsecmb 50   /* 우선순위 50 (기본 100) */

# 현재 암호화 핸들러 확인
vpp# show crypto handlers
vpp# show crypto engines

# QAT 상태 확인
vpp# show dpdk crypto devices
QAT VF 준비: QAT 하드웨어를 사용하려면 먼저 PF(Physical Function)에서 VF를 생성해야 합니다. echo 16 > /sys/bus/pci/devices/0000:3d:00.0/sriov_numvfs로 VF를 만들고, dpdk-devbind.py -b vfio-pci 0000:3d:01.0으로 DPDK에 바인딩하세요. VF 수는 워커 스레드 수 이상으로 설정해야 각 워커가 전용 큐를 가집니다.

IPsec 성능 모니터링 및 디버깅

# SA(Security Association) 상태 전체 조회
vpp# show ipsec sa detail
# 출력 예시:
# sa-id 0 spi 1000 mode tunnel protocol esp
#   crypto aes-gcm-256 integrity none
#   tunnel src 203.0.113.1 dst 198.51.100.1
#   seq 1234567 seq-hi 0 replay-window 64
#   packets 5234121 bytes 3140472600

# 터널 인터페이스 카운터
vpp# show interface counters ipsec0

# 암호화 노드 성능 확인
vpp# show runtime | grep -E "esp-(encrypt|decrypt)"
# 출력 예시:
# esp4-encrypt     987654   1.23e7    12.5   44.3    22 (clocks/pkt)
# esp4-decrypt     876543   1.10e7    12.6   42.1    24 (clocks/pkt)

# 에러 카운터 (드롭 원인 파악)
vpp# show errors | grep -i ipsec
# 주요 에러:
# esp-decrypt: REPLAY — anti-replay 윈도우 초과 (패킷 순서 뒤바뀜)
# esp-decrypt: INTEG_ERROR — 무결성 검증 실패 (키 불일치 또는 손상)
# esp-encrypt: NO_BUFFERS — 버퍼 부족 (buffers-per-numa 증가 필요)

# 트레이스로 패킷별 경로 확인
vpp# clear trace
vpp# trace add dpdk-input 5
# 트래픽 발생 후:
vpp# show trace
문제 증상에러 카운터원인해결 방법
터널 트래픽 0없음라우팅 미스매치show ip fib로 원격 서브넷이 ipsec0으로 라우팅되는지 확인
단방향만 동작INTEG_ERROR양쪽 키/SPI 불일치local/remote 키와 SPI가 대칭인지 교차 검증
간헐적 드롭REPLAY패킷 순서 뒤바뀜replay-window를 128 이상으로 증가, 멀티패스 라우팅 확인
처리량 저조없음SW 암호화 병목show crypto handlers로 HW 엔진 활성화 확인
높은 CPU 사용없음openssl 폴백ipsecmb 또는 dpdk cryptodev로 전환

IPsec 소스 코드 구조

VPP IPsec의 암호화 처리가 실제로 어떻게 구현되는지, 핵심 함수를 살펴봅니다.

/* src/vnet/ipsec/esp_encrypt.c — ESP 암호화 노드 핵심 루프 */
static uword
esp_encrypt_inline (vlib_main_t *vm, vlib_node_runtime_t *node,
                    vlib_frame_t *frame, int is_ip6, int is_tun)
{
  u32 n_left = frame->n_vectors;
  vlib_buffer_t *bufs[VLIB_FRAME_SIZE];
  vlib_get_buffers (vm, from, bufs, n_left);

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

      /* 1. ESP 헤더 삽입 (SPI + Sequence Number) */
      esp_header_t *esp = vlib_buffer_push_uninit (b, sizeof (*esp));
      esp->spi = clib_host_to_net_u32 (sa->spi);
      esp->seq = clib_host_to_net_u32 (sa->seq++);

      /* 2. IV(Initialization Vector) 생성 */
      /* AES-GCM: 8바이트 nonce + 4바이트 salt */

      /* 3. 암호화 연산 요청 (비동기 가능) */
      vnet_crypto_op_t *op = ops + n_ops++;
      op->op = VNET_CRYPTO_OP_AES_256_GCM_ENC;
      op->src = payload;
      op->dst = payload;  /* in-place 암호화 */
      op->len = payload_len;
      op->tag = tag;      /* GCM 인증 태그 */

      /* 4. 외부 IP 헤더 재작성 */
      ip4_header_set_len (oh, new_len);
      oh->protocol = IP_PROTOCOL_ESP; /* proto 50 */
    }

  /* 비동기 엔진이면 여기서 일괄 제출 */
  if (n_ops)
    vnet_crypto_enqueue_ops (vm, ops, n_ops);
}
비동기 암호화: QAT 같은 하드웨어 엔진은 비동기 모드로 동작합니다. esp-encrypt 노드가 암호화 요청을 큐에 넣고, 별도의 crypto-dispatch 노드가 완료된 패킷을 수집하여 다음 노드로 전달합니다. 이 방식은 CPU가 암호화 대기 중에 다른 패킷을 처리할 수 있어 처리량이 크게 향상됩니다.

VPP IPsec vs 커널 IPsec 성능 비교

동일 하드웨어(Xeon E-2388G, 25GbE NIC)에서 AES-GCM-256 Site-to-Site 터널의 성능을 비교한 참고 수치입니다.

구성패킷 크기처리량 (Gbps)PPSCPU 코어 수
커널 IPsec (xfrm)1400B4.2375K4
커널 IPsec (xfrm)64B0.3520K4
VPP IPsec (ipsecmb)1400B18.51.65M4
VPP IPsec (ipsecmb)64B2.14.1M4
VPP IPsec (QAT)1400B23.82.12M2
VPP IPsec (QAT)64B3.87.4M2
성능 해석: VPP는 벡터 처리와 배치 암호화 덕분에 커널 대비 소형 패킷에서 8배, 대형 패킷에서 4~5배 높은 처리량을 보입니다. QAT를 추가하면 CPU 코어를 절반만 사용하면서도 더 높은 처리량을 달성합니다. 실제 환경에서는 패킷 크기 분포, MTU, anti-replay 윈도우 크기에 따라 수치가 달라집니다.

실전 예제: SSL/TLS Inspection

현대 네트워크 트래픽의 90% 이상이 TLS로 암호화되어 있습니다. 방화벽이나 IDS/IPS가 암호화된 페이로드를 검사하려면 SSL/TLS Inspection(SSL 가시성 확보)이 필수입니다. VPP는 유저스페이스에서 TLS 종단(Termination)과 재암호화(Re-encryption)를 수행할 수 있어, 커널 기반 프록시 대비 훨씬 높은 처리량으로 SSL Inspection을 구현할 수 있습니다.

SSL Inspection의 원리

SSL Inspection은 본질적으로 중간자(Man-in-the-Middle) 구조입니다. 클라이언트와 실제 서버 사이에 검사 장비가 위치하여, 양쪽과 각각 독립적인 TLS 세션을 맺습니다. 이 구조에서 검사 장비는 잠시 평문 상태의 데이터를 볼 수 있고, 여기에 DPI(Deep Packet Inspection), 악성코드 탐지, DLP(Data Loss Prevention) 등의 보안 정책을 적용합니다.

SSL Inspection: 클라이언트-검사기-서버 이중 TLS 세션 클라이언트 브라우저 / 앱 검사기의 CA 인증서를 신뢰 저장소에 설치 TLS 세션 ① 클라이언트 ↔ 검사기 VPP SSL 검사기 TLS 복호화 평문 검사 TLS 재암호화 인증서 생성 DPI · 악성코드 · DLP · URL 필터링 TLS 세션 ② 검사기 ↔ 원본 서버 원본 서버 example.com 실제 서버 인증서 (Let's Encrypt 등) 패킷 처리 단계별 흐름 ① ClientHello SNI 추출 ② 서버 연결 원본 인증서 수신 ③ 인증서 위조 CA로 동적 서명 ④ 클라이언트 응답 위조 인증서 전달 ⑤ 평문 검사 DPI / 정책 적용 ⑥ 재암호화·전달 양방향 릴레이 검사기는 자체 CA 인증서로 원본 서버의 인증서를 동적으로 위조(re-sign)합니다. 클라이언트가 이 CA를 신뢰하면 브라우저 경고 없이 투명하게 동작합니다. 바이패스(Bypass)가 필요한 경우 Certificate Pinning 모바일 앱, 금융 서비스 등 위조 인증서 거부 → 바이패스 필수 상호 TLS (mTLS) 클라이언트 인증서 필요 중간자가 클라이언트 키 없음 법적/개인정보 예외 의료·금융·법률 사이트 규정상 복호화 금지 대상 이런 트래픽은 SNI 기반으로 탐지하여 TLS 세션을 맺지 않고 그대로 통과시켜야 합니다.

VPP SSL Inspection 아키텍처

VPP에서 SSL Inspection을 구현하는 방식은 크게 두 가지입니다. 각각의 장단점을 이해해야 환경에 맞는 설계를 선택할 수 있습니다.

구현 방식구조장점단점적합 환경
인라인 프록시VPP 내 VCL + TLS 플러그인단일 프로세스, 낮은 지연VPP 플러그인 개발 필요고성능 NGFW
SFC 분리형VPP L4 분류 → memif → 외부 프록시기존 프록시 재활용, 유연memif 오버헤드, 복잡도기존 인프라 연동
SSL Inspection 구현 방식 비교 방식 A: 인라인 프록시 (VPP 내부에서 모두 처리) LAN NIC dpdk-input ip4-lookup TCP:443 감지 TLS 종단 VCL + tlsopenssl DPI 엔진 평문 HTTP 검사 TLS 재암호화 서버측 세션 WAN NIC dpdk-output 단일 VPP 프로세스 내부 — 모든 처리가 그래프 노드 체인에서 발생 방식 B: SFC 분리형 (VPP 분류 + 외부 SSL 프록시) LAN NIC dpdk-input L4 분류기 TLS(443) → memif memif 전달 제로카피 외부 SSL 프록시 Squid / mitmproxy / 커스텀 memif 복귀 검사 완료 WAN NIC dpdk-output 구현 방식별 비교 방식 A: 인라인 프록시 + 지연 최소 (memif 왕복 없음) + 단일 프로세스 관리 − VPP 플러그인으로 DPI 구현 필요 − 장애 시 전체 경로 영향 방식 B: SFC 분리형 + 기존 프록시(Squid 등) 재활용 + 프록시 장애 격리 가능 − memif 왕복 지연 추가 (~2μs) − 관리 복잡도 증가

인라인 SSL Inspection 구현

VPP의 VCL(VPP Communication Library)과 TLS 플러그인을 활용하여 단일 VPP 프로세스 내에서 SSL Inspection을 구현하는 방법입니다. 핵심은 두 개의 독립적인 TLS 컨텍스트를 관리하는 것입니다.

# /etc/vpp/startup.conf — SSL Inspection용 구성
unix {
    cli-listen /run/vpp/cli.sock
    log /var/log/vpp/vpp.log
    full-coredump
    gid vpp
}

dpdk {
    dev 0000:00:08.0 { name GigabitEthernet0/8/0 }  /* LAN */
    dev 0000:00:09.0 { name GigabitEthernet0/9/0 }  /* WAN */
    no-multi-seg
}

cpu {
    main-core 0
    corelist-workers 1-7          /* TLS 핸드셰이크가 CPU 집약적 */
}

buffers {
    buffers-per-numa 65536        /* TLS 레코드 버퍼링 위해 증가 */
    default data-size 2048
}

session {
    evt_qs_memfd_seg              /* 세션 이벤트 큐 공유 메모리 */
    event-queue-length 100000     /* 동시 세션 수에 비례 */
}

plugins {
    plugin default { disable }
    plugin dpdk_plugin.so { enable }
    plugin tlsopenssl_plugin.so { enable }
    plugin http_plugin.so { enable }
    plugin nat_plugin.so { enable }
    plugin acl_plugin.so { enable }
    plugin ping_plugin.so { enable }
}

CA 인증서 인프라 구성

SSL Inspection의 기반은 내부 CA(Certificate Authority)입니다. 검사기가 원본 서버의 인증서 정보를 복사하여 이 CA로 동적 서명한 인증서를 클라이언트에 제시합니다.

# 1. 내부 Root CA 키 쌍 생성
$ openssl ecparam -genkey -name prime256v1 -out inspection-ca.key
$ openssl req -new -x509 -key inspection-ca.key -sha256 -days 3650 \
    -out inspection-ca.crt \
    -subj "/CN=Internal SSL Inspection CA/O=MyOrg/C=KR"

# 2. VPP TLS에서 사용할 서버 키 (동적 인증서 서명용)
$ openssl ecparam -genkey -name prime256v1 -out inspection-server.key

# 3. 클라이언트(브라우저)에 CA 인증서 배포
# Linux: /usr/local/share/ca-certificates/ 에 복사 후 update-ca-certificates
# Windows: GPO로 Trusted Root CA에 배포
# macOS: Keychain Access에서 "항상 신뢰"로 추가

# 4. VPP에 TLS 자격증명 등록
vpp# tls ca-cert /etc/vpp/certs/inspection-ca.crt
vpp# tls cert /etc/vpp/certs/inspection-server.crt
vpp# tls key /etc/vpp/certs/inspection-server.key
보안 경고: 내부 CA의 개인키(Private Key)가 유출되면 네트워크 내 모든 TLS 통신이 위험해집니다. 개인키는 반드시 HSM(Hardware Security Module) 또는 TPM에 보관하고, 파일 시스템 권한을 0600으로 제한하세요. CA 인증서의 유효기간도 운영 정책에 맞게 설정해야 합니다.

동적 인증서 생성 구현

SSL Inspection의 핵심 기술은 원본 서버 인증서의 Subject/SAN을 복제하여 내부 CA로 즉시 서명하는 것입니다. 이 과정이 TLS 핸드셰이크 지연의 가장 큰 비중을 차지합니다.

/* SSL Inspection 플러그인: 동적 인증서 생성 핵심 로직 */
/* OpenSSL API를 사용한 구현 예시 */

static X509 *
generate_inspection_cert (X509 *orig_cert,
                          EVP_PKEY *ca_key,
                          X509 *ca_cert)
{
  X509 *cert = X509_new ();
  X509_NAME *subj = X509_get_subject_name (orig_cert);

  /* 원본 인증서에서 Subject 복사 */
  X509_set_subject_name (cert, subj);

  /* Issuer를 내부 CA로 설정 */
  X509_set_issuer_name (cert, X509_get_subject_name (ca_cert));

  /* 원본의 SAN(Subject Alternative Name) 복사 — 필수! */
  int san_idx = X509_get_ext_by_NID (orig_cert, NID_subject_alt_name, -1);
  if (san_idx >= 0)
    {
      X509_EXTENSION *san_ext = X509_get_ext (orig_cert, san_idx);
      X509_add_ext (cert, san_ext, -1);
    }

  /* 일련번호, 유효기간 설정 */
  ASN1_INTEGER_set (X509_get_serialNumber (cert),
                    generate_unique_serial ());
  X509_gmtime_adj (X509_get_notBefore (cert), 0);
  X509_gmtime_adj (X509_get_notAfter (cert), 86400 * 365);

  /* 검사기 서버의 공개키 설정 */
  X509_set_pubkey (cert, inspection_server_key);

  /* 내부 CA 개인키로 서명 */
  X509_sign (cert, ca_key, EVP_sha256 ());

  return cert;
}

/* 캐시: 동일 서버에 대한 반복 생성 방지 */
typedef struct {
  u8 *server_name;        /* SNI 또는 Subject CN */
  X509 *cached_cert;      /* 생성된 위조 인증서 */
  f64 created_at;         /* 캐시 생성 시각 */
  f64 ttl;                /* 캐시 유효 시간 */
} ssl_cert_cache_entry_t;
인증서 캐싱: 동적 인증서 생성(특히 RSA 서명)은 비용이 큽니다. ECDSA P-256 서명은 RSA-2048 대비 약 10배 빠르므로, CA 키를 ECDSA로 생성하는 것이 성능에 유리합니다. 또한 SNI 기반 캐시를 두면 동일 서버에 대한 반복 핸드셰이크에서 인증서 생성을 건너뛸 수 있습니다. 일반적으로 캐시 TTL은 1~24시간이 적절합니다.

SNI 기반 바이패스와 정책 분류

모든 TLS 트래픽을 복호화하는 것은 성능 낭비이자 법적 리스크입니다. SNI(Server Name Indication)를 기반으로 검사 대상과 바이패스 대상을 분류해야 합니다. SNI는 TLS ClientHello의 확장 필드에 평문으로 포함되므로, TLS 세션을 맺기 전에 읽을 수 있습니다.

SNI 기반 트래픽 분류: 검사 vs 바이패스 TCP 연결 수립 dst port 443 ClientHello 파싱 SNI 확장 필드 추출 정책 테이블 조회 도메인 → 동작 매핑 SSL Inspection TLS 종단 → 검사 → 재암호화 검사 바이패스 TLS 세션 그대로 전달 통과 차단 (RST) 정책 위반 도메인 차단 WAN 전달 dpdk-output 정책 테이블 예시 도메인 패턴 동작 사유 *.bank.com, *.finance.kr 바이패스 금융 — 법적 복호화 금지, Certificate Pinning *.medical.org, health.* 바이패스 의료 — 개인건강정보(PHI) 보호 *.malware-domain.xyz 차단 위협 인텔리전스 차단 목록 * (기본) 검사 기본 정책 — 모든 나머지 TLS 트래픽
/* SNI 파싱: ClientHello에서 서버 이름 추출 */
static int
parse_sni_from_client_hello (u8 *data, u32 len,
                             u8 **sni_out, u16 *sni_len_out)
{
  /* TLS Record: ContentType(1) + Version(2) + Length(2) */
  if (len < 5 || data[0] != 0x16)  /* 0x16 = Handshake */
    return -1;

  /* Handshake: HandshakeType(1) + Length(3) */
  u8 *hs = data + 5;
  if (hs[0] != 0x01)  /* 0x01 = ClientHello */
    return -1;

  /* ClientHello: Version(2) + Random(32) + SessionID(var)
   * + CipherSuites(var) + Compression(var) + Extensions(var) */
  u32 offset = 5 + 4;  /* record header + handshake header */
  offset += 2 + 32;     /* version + random */
  offset += 1 + data[offset]; /* session ID length + data */

  u16 cs_len = (data[offset] << 8) | data[offset + 1];
  offset += 2 + cs_len;  /* cipher suites */
  offset += 1 + data[offset]; /* compression */

  /* Extensions 순회 → SNI(type=0x0000) 찾기 */
  u16 ext_total = (data[offset] << 8) | data[offset + 1];
  offset += 2;
  u32 ext_end = offset + ext_total;

  while (offset + 4 <= ext_end)
    {
      u16 ext_type = (data[offset] << 8) | data[offset + 1];
      u16 ext_len  = (data[offset + 2] << 8) | data[offset + 3];
      offset += 4;

      if (ext_type == 0x0000)  /* SNI extension */
        {
          /* SNI list: ListLen(2) + Type(1) + NameLen(2) + Name */
          u16 name_len = (data[offset + 3] << 8) | data[offset + 4];
          *sni_out = data + offset + 5;
          *sni_len_out = name_len;
          return 0;
        }
      offset += ext_len;
    }
  return -1;  /* SNI 없음 (IP 직접 연결 등) */
}
ECH(Encrypted Client Hello): TLS 1.3의 ECH 확장이 활성화되면 SNI가 암호화되어 위 파싱이 불가능합니다. ECH 대응으로는 DNS(DoH/DoT) 수준에서 도메인을 추적하거나, ECH 트래픽 자체를 차단하는 정책이 필요합니다. 현재 대부분의 브라우저에서 ECH는 실험적 기능이지만, 향후 보편화될 가능성이 높으므로 대비가 필요합니다.

SFC 분리형: VPP + 외부 SSL 프록시

기존 프록시(Squid SSL Bump, mitmproxy 등)를 재활용하면서 VPP의 고성능 패킷 분류만 활용하는 구조입니다. VPP가 TLS 트래픽을 식별하여 memif로 외부 프록시에 전달하고, 검사 완료 후 다시 VPP로 복귀시킵니다.

# VPP: TLS 트래픽 분류 및 memif 전달 구성

# 1. SSL 프록시용 memif 생성
vpp# create memif socket id 10 filename /run/vpp/memif-ssl-proxy.sock
vpp# create interface memif id 0 socket-id 10 master ring-size 2048
vpp# set interface state memif10/0 up
vpp# set interface ip address memif10/0 10.255.0.1/30

# 2. 프록시로부터 복귀용 memif
vpp# create memif socket id 11 filename /run/vpp/memif-ssl-return.sock
vpp# create interface memif id 0 socket-id 11 master ring-size 2048
vpp# set interface state memif11/0 up
vpp# set interface ip address memif11/0 10.255.1.1/30

# 3. ACL로 TCP 443 트래픽 분류 → 프록시로 라우팅
vpp# set acl-plugin acl permit+reflect proto 6 dport 443
vpp# set acl-plugin interface GigabitEthernet0/8/0 input acl 0

# 4. PBR(Policy-Based Routing): TCP 443 매칭 시 프록시로 전달
vpp# ip table add 100
vpp# ip route add 0.0.0.0/0 table 100 via 10.255.0.2 memif10/0

# classify 테이블로 TLS 트래픽을 FIB 100으로 전환
vpp# classify table mask l3 ip4 proto l4 dst_port
vpp# classify session table-index 0 match l3 ip4 proto 6 l4 dst_port 443 \
     action set-ip4-fib-id 100

# 5. 프록시에서 복귀한 트래픽은 기본 FIB로 WAN 전달
vpp# ip route add 0.0.0.0/0 via 203.0.113.254 GigabitEthernet0/9/0
# 외부 프록시 측: libmemif를 사용한 Squid SSL Bump 연동
# 또는 mitmproxy 기반 간이 구성

# mitmproxy 실행 (memif 대신 TAP으로 연결하는 간이 예시)
$ sudo ip link set vpp-proxy up
$ sudo ip addr add 10.255.0.2/30 dev vpp-proxy
$ sudo ip route add default via 10.255.0.1 dev vpp-proxy

# mitmproxy를 투명 프록시 모드로 실행
$ mitmproxy --mode transparent --listen-host 10.255.0.2 --listen-port 8443 \
    --set ssl_insecure=true \
    --set confdir=/etc/mitmproxy \
    -s inspection_policy.py

# iptables로 투명 리다이렉트 (프록시 호스트에서)
$ sudo iptables -t nat -A PREROUTING -i vpp-proxy -p tcp --dport 443 \
    -j REDIRECT --to-port 8443
libmemif 직접 연동: 최고 성능을 위해서는 외부 프록시가 libmemif를 직접 사용하여 VPP와 공유 메모리로 패킷을 교환해야 합니다. libmemif는 C/C++ 라이브러리로 제공되며, 기존 프록시에 패치를 적용하거나 래퍼를 작성하여 통합할 수 있습니다. TAP 인터페이스를 경유하는 위 예시는 개발/테스트 용도에 적합합니다.

TLS 1.3 Inspection 고려사항

TLS 1.3은 핸드셰이크 구조가 크게 바뀌어 SSL Inspection 구현에 영향을 줍니다.

변경점TLS 1.2TLS 1.3Inspection 영향
핸드셰이크 왕복2-RTT1-RTT (0-RTT 가능)인증서 위조 시간 압박 증가
서버 인증서 위치평문 전송암호화 후 전송검사기가 먼저 서버와 핸드셰이크 완료해야 원본 인증서 확인 가능
세션 재개Session ID/TicketPSK (0-RTT)0-RTT 데이터는 재생 공격 위험 → 검사기에서 0-RTT 차단 또는 제한 필요
암호 스위트다양 (RC4, CBC 등)AEAD만 (GCM, ChaCha20)오래된 암호 스위트 다운그레이드 불가 → 검사기도 AEAD 지원 필수
SNI 암호화 (ECH)없음선택적 (초안)ECH 활성 시 SNI 파싱 불가 → DNS 기반 정책으로 전환 필요
/* TLS 1.3 핸드셰이크 시 Inspection 순서 */

/* 1. ClientHello 수신 → SNI 추출 + 바이패스 판단 */
/* 2. 검사 대상이면: 검사기가 원본 서버에 먼저 TLS 1.3 연결 */
/* 3. 서버의 EncryptedExtensions + Certificate 수신 (복호화 후) */
/* 4. 원본 인증서 정보로 위조 인증서 생성 */
/* 5. 클라이언트에게 위조 인증서로 ServerHello 응답 */

static int
tls13_inspection_handshake (ssl_inspect_ctx_t *ctx)
{
  /* 서버측 TLS 1.3 핸드셰이크 먼저 완료 */
  int rv = SSL_connect (ctx->server_ssl);
  if (rv != 1)
    return handle_ssl_error (ctx, rv);

  /* 서버 인증서 체인 가져오기 */
  X509 *server_cert = SSL_get_peer_certificate (ctx->server_ssl);
  if (!server_cert)
    return -1;

  /* 원본 인증서 검증 (OCSP, CRL 확인) */
  long verify_result = SSL_get_verify_result (ctx->server_ssl);
  if (verify_result != X509_V_OK)
    {
      /* 원본 서버 인증서 검증 실패 → 정책에 따라 차단 또는 경고 */
      log_cert_error (ctx, verify_result);
      if (ctx->policy & SSL_INSPECT_BLOCK_INVALID_CERT)
        return -1;
    }

  /* 동적 인증서 생성 (캐시 우선 확인) */
  X509 *forged = cert_cache_lookup (ctx->sni);
  if (!forged)
    {
      forged = generate_inspection_cert (server_cert,
                                          ctx->ca_key, ctx->ca_cert);
      cert_cache_insert (ctx->sni, forged, CERT_CACHE_TTL);
    }

  /* 클라이언트측 TLS 컨텍스트에 위조 인증서 설정 */
  SSL_use_certificate (ctx->client_ssl, forged);
  SSL_use_PrivateKey (ctx->client_ssl, ctx->server_key);

  /* 클라이언트 핸드셰이크 완료 */
  return SSL_accept (ctx->client_ssl);
}
0-RTT 주의: TLS 1.3의 0-RTT(Early Data)는 핸드셰이크 완료 전에 애플리케이션 데이터를 전송합니다. 이 데이터는 재생 공격(Replay Attack)에 취약하며, 검사기가 아직 서버 인증서를 확인하지 못한 상태에서 도착합니다. SSL Inspection 환경에서는 SSL_CTX_set_max_early_data(ctx, 0)으로 0-RTT를 비활성화하는 것이 안전합니다.

양방향 데이터 릴레이 구현

핸드셰이크가 완료되면 검사기는 양쪽 TLS 세션의 평문 데이터를 릴레이하면서, 중간에 DPI 엔진을 통과시킵니다. VPP의 세션 레이어를 활용하면 이 릴레이를 이벤트 기반으로 효율적으로 구현할 수 있습니다.

데이터 릴레이: 복호화 → 검사 → 재암호화 (요청 방향) 클라이언트 TLS 세션 TCP RX FIFO (암호문) SSL_read() → 평문 App RX FIFO (평문) DPI / 보안 검사 엔진 HTTP 파싱 (메서드, URI, 헤더) 악성코드 시그니처 매칭 DLP 정책 (개인정보 패턴) 허용 재암호화 진행 차단 RST 또는 블록 페이지 서버 TLS 세션 평문 → SSL_write() TCP TX FIFO (재암호문) 원본 서버로 전송 평문 평문 응답 방향: 서버 → DPI → 클라이언트 (역방향 동일 구조) 서버 응답 복호화 응답 DPI (악성코드, 데이터 유출) 클라이언트로 재암호화 성능 핵심: FIFO 간 평문 데이터 복사를 최소화해야 합니다. VPP 세션 레이어의 svm_fifo_peek()으로 zero-copy 읽기를 수행하고, DPI 판정 후 svm_fifo_dequeue()로 소비합니다.
/* 양방향 릴레이 핵심 로직 — VCL 세션 기반 */

static void
ssl_inspect_relay_data (ssl_inspect_ctx_t *ctx)
{
  u8 buf[16384];  /* TLS 레코드 최대 크기 */
  int n;

  /* 클라이언트 → 서버 방향 */
  while ((n = SSL_read (ctx->client_ssl, buf, sizeof (buf))) > 0)
    {
      /* DPI 엔진에 평문 전달 */
      dpi_verdict_t verdict = dpi_inspect (ctx->dpi_ctx,
                                           buf, n,
                                           DPI_DIR_CLIENT_TO_SERVER);

      if (verdict == DPI_BLOCK)
        {
          /* 차단: 클라이언트에 블록 페이지 전송 후 세션 종료 */
          send_block_page (ctx->client_ssl, ctx->block_reason);
          ssl_inspect_close (ctx);
          return;
        }

      /* 허용: 원본 서버로 재암호화하여 전달 */
      int written = SSL_write (ctx->server_ssl, buf, n);
      if (written <= 0)
        {
          handle_ssl_error (ctx, written);
          return;
        }
    }

  /* 서버 → 클라이언트 방향 (대칭) */
  while ((n = SSL_read (ctx->server_ssl, buf, sizeof (buf))) > 0)
    {
      dpi_verdict_t verdict = dpi_inspect (ctx->dpi_ctx,
                                           buf, n,
                                           DPI_DIR_SERVER_TO_CLIENT);

      if (verdict == DPI_BLOCK)
        {
          ssl_inspect_close (ctx);
          return;
        }

      SSL_write (ctx->client_ssl, buf, n);
    }
}

SSL Inspection 성능 고려사항

SSL Inspection은 TLS 핸드셰이크(특히 RSA/ECDSA 연산)와 대칭 암호화/복호화를 이중으로 수행하므로, 일반 패킷 포워딩 대비 CPU 부담이 매우 큽니다.

구성 요소CPU 비용 (상대)최적화 방법
RSA-2048 서명 (인증서 생성)매우 높음ECDSA P-256 전환 (10배 빠름), 인증서 캐싱
TLS 핸드셰이크높음세션 재개(PSK), QAT 비동기 오프로드
AES-GCM 대칭 암복호화중간AES-NI 활용 (기본 활성), QAT
DPI 시그니처 매칭가변 (룰 수 의존)Hyperscan/Vectorscan 가속, 룰 최적화
인증서 검증 (OCSP/CRL)네트워크 의존OCSP Stapling, 로컬 CRL 캐싱
# SSL Inspection 성능 모니터링

# TLS 핸드셰이크 횟수 및 소요 시간 확인
vpp# show tls
# 출력 예시:
# OpenSSL engine:
#   Ctx allocated: 45231
#   Handshakes:    12847 (active: 23)
#   Rx bytes:      4.2 GB
#   Tx bytes:      1.8 GB

# 세션 레이어 상태 (동시 세션 수)
vpp# show session verbose
# Transport   Active   Listening
# tcp         2341     4
# tls         1892     2

# 워커별 부하 분포 확인
vpp# show runtime
# session-queue 노드의 Vectors/Call이 워커 간 균등해야 합니다.
# 편향이 심하면 TCP 해시 분산(RSS/Flow Director) 조정 필요

# 인증서 캐시 적중률 (커스텀 플러그인 시)
# 적중률 90% 이상이면 양호, 50% 이하면 캐시 크기 확대 필요
구성동시 세션신규 핸드셰이크/초처리량 (Gbps)CPU 코어
VPP + OpenSSL (SW)50K~2,000 CPS~58
VPP + OpenSSL + QAT100K~8,000 CPS~124
커널 Squid SSL Bump10K~500 CPS~18
CPS(Connections Per Second)가 핵심: SSL Inspection에서 처리량(Gbps)보다 초당 새 연결 수(CPS)가 더 중요한 병목입니다. 핸드셰이크가 끝난 세션의 데이터 릴레이는 AES-NI 덕분에 빠르지만, 매 초 수천 개의 새 TLS 핸드셰이크를 처리하는 것이 CPU 집약적입니다. ECDSA CA + 인증서 캐싱 + QAT 조합이 CPS를 극대화합니다.

SSL Inspection 트러블슈팅

증상진단원인해결
브라우저 인증서 경고브라우저에서 인증서 체인 확인내부 CA가 신뢰 저장소에 미설치GPO/MDM으로 CA 인증서 배포
특정 앱 연결 실패앱의 인증서 요구사항 확인Certificate Pinning해당 도메인을 바이패스 목록에 추가
핸드셰이크 타임아웃show tls active 핸드셰이크 수서버 연결 지연 또는 인증서 생성 병목ECDSA CA 전환, QAT 추가, 타임아웃 조정
CPS 저하show runtime session-queue 클럭RSA 서명 CPU 부하인증서 캐시 확대, CA 키를 ECDSA로 변경
QUIC/HTTP3 통과 안됨UDP 443 트래픽 확인QUIC은 UDP 기반 → TCP 분류기 미매칭UDP 443도 분류 대상에 추가, 또는 QUIC 차단 후 TCP 폴백 유도
mTLS 서비스 장애서버가 클라이언트 인증서 요구검사기에 원본 클라이언트 인증서 없음mTLS 대상을 바이패스, 또는 클라이언트 인증서 릴레이 구현
법적 고려사항: SSL Inspection은 사용자의 암호화 통신을 복호화하는 행위이므로, 반드시 조직의 보안 정책과 관련 법규(개인정보보호법, 전기통신사업법 등)를 준수해야 합니다. 사용자에게 SSL Inspection이 적용됨을 명확히 고지하고, 의료·금융·법률 등 민감한 카테고리는 반드시 바이패스하세요. 규정 위반 시 법적 책임이 발생할 수 있습니다.

성능 측정 및 벤치마크

VPP 구성을 완료한 뒤, 실제 처리량과 지연을 측정하는 체계적인 방법을 소개합니다. 수치 없는 "잘 되는 것 같다"는 운영에서 통하지 않습니다.

TRex를 이용한 트래픽 생성

TRex는 DPDK 기반 고성능 트래픽 생성기로, VPP 벤치마크의 사실상 표준입니다. CSIT(Continuous System Integration Testing)에서도 TRex를 사용합니다.

# TRex STL(Stateless) 프로파일 — NAT44 부하 테스트
from trex_stl_lib.api import *

class STLS1(object):
    def create_stream(self):
        base_pkt = Ether() / IP(
            src="192.168.1.10",
            dst="198.51.100.10"
        ) / UDP(
            sport=1024,
            dport=80
        ) / ("X" * 16)

        # 소스 IP와 포트를 변경하여 다양한 플로우 생성
        vm = STLScVmRaw([
            STLVmFlowVar(name="src_ip",
                         min_value="192.168.1.10",
                         max_value="192.168.1.250",
                         size=4, op="inc"),
            STLVmWrFlowVar(fv_name="src_ip",
                           pkt_offset="IP.src"),
            STLVmFlowVar(name="src_port",
                         min_value=1024,
                         max_value=60000,
                         size=2, op="inc"),
            STLVmWrFlowVar(fv_name="src_port",
                           pkt_offset="UDP.sport"),
            STLVmFixIpv4(offset="IP"),
        ])

        return STLStream(
            packet=STLPktBuilder(pkt=base_pkt, vm=vm),
            mode=STLTXCont(pps=1000000)  # 1Mpps부터 시작
        )

    def get_streams(self, **kwargs):
        return [self.create_stream()]

def register():
    return STLS1()
# TRex 실행 및 결과 확인
$ sudo ./t-rex-64 -i -c 4 --cfg /etc/trex_cfg.yaml

# TRex 콘솔에서:
trex> start -f nat44_test.py -m 5mpps -d 60
trex> stats
# TX: 5.00 Mpps, RX: 4.98 Mpps → 드롭율 0.04%

# VPP 측에서 동시에 확인:
vpp# show runtime
vpp# show interface GigabitEthernet0/8/0
vpp# show nat44 sessions count

VPP 내부 성능 카운터 해석

show runtime 출력의 각 열이 무엇을 뜻하는지 정확히 알아야 병목을 찾을 수 있습니다.

Name                 State    Calls    Vectors  Suspends  Clocks   Vectors/Call
dpdk-input           polling  1234567  9.87e7   0         22.1     79.9
ethernet-input       active   1234567  9.87e7   0         8.3      79.9
ip4-input            active   1234567  9.87e7   0         12.5     79.9
ip4-lookup           active   1234567  9.87e7   0         15.2     79.9
nat44-ed-in2out      active   1200000  9.60e7   0         44.3     80.0
ip4-rewrite          active   1200000  9.60e7   0         9.8      80.0
GigE0/9/0-output     active   1200000  9.60e7   0         5.1      80.0
GigE0/9/0-tx         active   1200000  9.60e7   0         18.7     80.0
의미정상 범위이상 징후
Calls노드가 호출된 횟수
Vectors처리한 총 패킷 수이전 노드보다 현저히 적으면 드롭 발생
Vectors/Call호출당 평균 배치 크기64~256<10이면 배치 효율 저하
Clocks패킷당 평균 CPU 클럭노드 의존특정 노드가 100+ 이면 병목
Suspends자발적 중단 횟수0>0이면 스케줄링 문제
병목 찾기: Clocks가 가장 높은 노드가 처리 비용의 병목입니다. NAT의 경우 nat44-ed-in2out이 보통 가장 높고, IPsec에서는 esp-encrypt/esp-decrypt가 지배적입니다. Vectors/Call이 낮다면 입력 큐가 적절히 배치되지 않아 벡터 효율이 떨어지는 것이므로 RSS 큐와 워커 매핑을 점검하세요.

트러블슈팅 가이드

VPP 실전 구성에서 자주 만나는 문제와 체계적인 진단 방법을 정리합니다.

패킷이 전달되지 않는 경우

# 1단계: 인터페이스 상태 확인
vpp# show interface
# 모든 인터페이스가 'up'인지 확인
# 'admin-down'이면: set interface state  up

# 2단계: 수신 패킷 확인
vpp# clear interface counters
# 트래픽 생성 후:
vpp# show interface
# rx-pkts가 증가하는지 확인
# 증가하지 않으면: DPDK 바인딩, Hugepage, NIC 드라이버 문제

# 3단계: 패킷 추적
vpp# clear trace
vpp# trace add dpdk-input 20
# 트래픽 생성 후:
vpp# show trace
# 패킷이 어느 노드에서 드롭되는지 확인
# 'error' 또는 'drop' 노드에서 끝나면 해당 에러 메시지 확인

# 4단계: 에러 카운터 확인
vpp# show errors
# 가장 높은 에러 카운터부터 추적

# 5단계: FIB(라우팅 테이블) 확인
vpp# show ip fib                  # 전체 라우팅 테이블
vpp# show ip fib 198.51.100.0/24  # 특정 서브넷 경로

자주 발생하는 문제와 해결법

증상진단 명령원인해결
VPP 시작 실패journalctl -u vppHugepage 부족, DPDK 바인딩 실패echo 1024 > /proc/sys/vm/nr_hugepages, dpdk-devbind.py -b vfio-pci
DPDK 인터페이스 미표시show interfaceNIC가 DPDK에 바인딩되지 않음dpdk-devbind.py --status로 확인, modprobe vfio-pci
ARP 미응답show ip neighbors인터페이스에 IP 미할당set interface ip address 재확인
NAT 세션 미생성show nat44 sessionsin/out 방향 설정 오류set interface nat44 in/out 방향 교차 확인
memif 연결 안됨show memif소켓 경로/권한, master/slave 역할양쪽 소켓 파일 경로 동일 확인, 권한 chmod 770
IPsec 단방향만 동작show ipsec sa detailSPI/키 불일치, 라우팅 미설정양쪽 설정 교차 검증, show ip fib
처리량 기대 이하show runtime워커 불균형, 배치 효율 저하RSS 큐 수와 워커 수 일치, isolcpus 확인

패킷 캡처 (pcap)

패킷 내용을 직접 확인해야 할 때는 VPP의 내장 pcap 기능을 사용합니다. tcpdump와 달리 커널을 거치지 않으므로 DPDK 인터페이스의 패킷도 캡처할 수 있습니다.

# 특정 인터페이스의 수신 패킷 캡처
vpp# pcap trace rx tx max 1000 intfc GigabitEthernet0/8/0 \
     file /tmp/vpp-capture.pcap

# 트래픽 발생 후 캡처 중지
vpp# pcap trace off

# 호스트에서 Wireshark로 분석
$ wireshark /tmp/vpp-capture.pcap

# 또는 tcpdump로 빠르게 확인
$ tcpdump -r /tmp/vpp-capture.pcap -nn | head -20

# 그래프 노드 단위 캡처 (더 세밀한 분석)
vpp# pcap trace rx tx max 500 \
     classify-table-index 0 \
     file /tmp/vpp-classified.pcap
주의: pcap 캡처는 성능에 영향을 줍니다. 운영 환경에서는 max를 작게 설정하고, 분석이 끝나면 즉시 pcap trace off로 중지하세요. 대량 캡처가 필요하면 trace addshow trace로 노드 경로만 먼저 확인하는 것이 효율적입니다.

이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.