TCP/TLS 프록시 · SSL Inspection
VPP 프록시: TCP 프록시와 TPROXY 기본 원리, VPP TLS 종단 프록시 실전 구성, SSL/TLS Inspection 아키텍처·이중 세션·동적 인증서·SNI 바이패스·TLS 1.3 고려사항·운영 런북을 다룹니다.
IP_TRANSPARENT 소켓(Socket) 옵션과 정책 라우팅, Netfilter의 mangle 테이블이 배경 지식으로 필요합니다.
tlsopenssl 플러그인과 OpenSSL ENGINE 비동기 연동을 전제로 합니다. VPP 릴리스 노트상 TLS 프레임워크 자체는 production이지만, OpenSSL 엔진·QUIC·picotls·AF_XDP 등은 experimental로 분류됩니다. 또한 OpenSSL 3.x부터는 ENGINE API가 deprecated되고 provider API로 이전 중이므로 QAT 등 HW 가속 경로의 호환성을 프로덕션 배포 전 최신 릴리스 노트에서 반드시 재확인하세요.
핵심 요약
- TCP 프록시(TCP Proxy) — 클라이언트와 서버 사이에서 두 개의 독립적 TCP 연결을 유지하며 바이트 스트림(Byte Stream)을 중계합니다. L7 프록시와 달리 애플리케이션 프로토콜을 해석하지 않습니다.
- TPROXY(투명 프록시) — 원본 클라이언트 IP를 보존한 채 L7 프록시로 리다이렉트합니다. 커널 측은
IP_TRANSPARENT소켓 옵션과 mangle 테이블 규칙으로 구현하고, VPP 측은 session layer로 구현합니다. - VPP TLS 종단 프록시 — VCL + VPP TLS 엔진으로 TLS 세션을 유저스페이스에서 종단(Termination)한 뒤 평문 바이트 스트림을 애플리케이션 로직에 전달합니다. 커널 기반 프록시보다 처리량(Throughput)과 핸드셰이크 속도가 높습니다.
- SSL Inspection 이중 세션 — 인스펙션(Inspection) 프록시는 클라이언트와 한 TLS 세션을, 서버와 또 다른 TLS 세션을 유지하며 중간에서 평문으로 복호화(Decryption)·검사·재암호화(Re-encryption)를 수행합니다.
- 동적 인증서 생성 엔진 — 인스펙션 프록시가 클라이언트에 제시할 리프(Leaf) 인증서를 매 연결마다 온디맨드로 서명합니다. 루트 CA는 사전에 클라이언트 신뢰 저장소에 등록되어야 합니다.
- SNI 바이패스와 정책 엔진 — Server Name Indication을 ClientHello에서 추출하여 민감 도메인(뱅킹, 헬스케어)은 복호화 없이 바이패스하고, 나머지만 검사합니다. TLS 1.3 ECH(Encrypted ClientHello)는 SNI 자체를 암호화(Encryption)하여 사전 분류를 어렵게 합니다.
- IDS/IPS 연동 — 복호화된 평문을 Suricata, Snort, Zeek 같은 엔진에 memif 또는 UNIX 소켓으로 전달하여 시그니처 매칭과 이상 탐지를 수행합니다. SFC(Service Function Chaining) 구조로 분리 가능합니다.
- QUIC / ECH 도전 — QUIC은 TLS 1.3을 내부에 통합하고 UDP 위에서 핸드셰이크를 수행하므로 기존 TCP 기반 프록시 방식으로 가시성을 확보할 수 없습니다. UDP 인터셉트와 QUIC 전용 프록시가 필요합니다.
단계별 이해
- TCP 프록시 원리 이해
두 독립 TCP 연결의 상태·버퍼(Buffer)·혼잡 제어(Congestion Control)가 서로 분리되어 동작합니다. 스플라이싱(splice)으로 커널 내 제로카피 복사 경로를 확보하거나, 유저스페이스 복사 경로를 선택할 수 있습니다. - TPROXY 커널 구성 선행
iptables -t mangle -A PREROUTING -p tcp --dport 80 -j TPROXY --on-port 8080 --tproxy-mark 1/1와ip rule add fwmark 1 lookup 100,ip route add local default dev lo table 100으로 커널 측 기반을 준비합니다. VPP는 이 구성을 이해한 상태에서 session layer로 인라인 경로를 구현합니다. - VCL 기반 TLS 종단 프록시 작성
vppcom_session_accept()로 클라이언트 연결을 받고,vppcom_session_connect()로 업스트림 연결을 생성합니다. 양쪽 세션의 FIFO를 epoll로 폴링(Polling)하여 데이터를 릴레이(Relay)합니다. - 루트 CA 인프라 구축
OpenSSL 또는 cfssl로 루트 CA와 중간 CA를 생성하고, 엔터프라이즈 환경에서는 MDM(Mobile Device Management)이나 Group Policy로 클라이언트 신뢰 저장소에 루트 CA를 설치합니다. 이 과정 없이는 브라우저가 인증서 경고를 표시합니다. - 동적 인증서 생성 엔진 구현
ClientHello에서 SNI를 추출하고 실제 서버 인증서의 Subject/SAN을 복제한 리프 인증서를 루트 CA 키로 즉시 서명합니다. 생성 비용을 줄이기 위해 호스트별 캐시(Cache)와 LRU 정책을 함께 둡니다. - 정책 엔진과 관측성
SNI 바이패스 목록, IDS/IPS 시그니처 결과, 세션 로그, 복호화된 평문의 일부 해시(Hash)를 결합하여 정책 결정을 기록합니다. TLS 1.3 세션 티켓 재사용과 0-RTT는 운영 중 예외 케이스로 별도 추적합니다.
TCP 프록시와 TPROXY: VPP TLS 구현까지의 기술 경로
- 25.06: TLS ALPN 협상 지원(VCL 애플리케이션이 클라이언트로서 우선순위(Priority) 지정 가능), QUIC engine API 공식 출시, HTTP/2 전체 구현(HPACK·프레임·흐름제어·멀티플렉싱·클라이언트), HTTP 클라이언트 병렬 세션.
- 25.10: HTTP/2 CONNECT / PUT / extended CONNECT / UDP over HTTP/2 터널 지원. MASQUE 스타일의 UDP 터널링이 가능해졌습니다. Session 레이어에 trusted CA 구성·proxy_write_early_data 콜백·per-FIFO 최대 메모리가 들어왔습니다.
- 26.02: HTTP/3 core + H3 클라이언트, HTTP connect proxy client 내장, HTTP 클라이언트 basic redirect, 서버측 mTLS 지원 + peer cert 조회(VCL 차원).
VPP TLS 종단 프록시를 이해하려면, 먼저 TCP 프록시의 기본 원리와 리눅스 커널의 TPROXY(투명 프록시) 메커니즘을 이해해야 합니다. 이 섹션에서는 기존 커널 기반 프록시의 동작 원리부터 VPP 유저스페이스 TLS 프록시 구현까지의 전체 기술 경로를 단계별로 다룹니다.
TCP 프록시 기초
L4 프록시 개념
TCP 프록시(L4 프록시)는 클라이언트와 서버 사이에서 두 개의 독립적인 TCP 연결을 유지하며, 양쪽 간 데이터를 중계하는 중간자(intermediary) 역할을 합니다. L7 프록시(HTTP 리버스 프록시 등)와 달리 애플리케이션 프로토콜을 해석하지 않고, TCP 바이트 스트림을 그대로 전달합니다.
소켓 기반 TCP 릴레이 구현
가장 기본적인 TCP 프록시는 두 소켓 사이에서 recv()/send()를 반복하는 릴레이 방식입니다. 이 방식의 구조와 성능 한계를 이해하는 것이 VPP 프록시의 필요성을 파악하는 첫걸음입니다.
/* 기본 TCP 프록시 릴레이 — 소켓 기반 (단순화) */
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define BUF_SIZE 65536
/* 양방향 릴레이: client_fd ↔ backend_fd */
static void
relay_data (int src_fd, int dst_fd)
{
char buf[BUF_SIZE];
ssize_t nread, nwritten;
/* 1. 커널 → 유저 공간으로 복사 (recv) */
nread = recv (src_fd, buf, sizeof(buf), 0);
if (nread <= 0)
return;
/* 2. 유저 공간 → 커널로 복사 (send) */
nwritten = send (dst_fd, buf, nread, 0);
/* 최소 2회 syscall + 2회 메모리 복사 per 방향 */
}
/* epoll 기반 이벤트 루프 */
static void
proxy_event_loop (int epoll_fd)
{
struct epoll_event events[1024];
while (1) {
/* 3. epoll_wait — 3번째 syscall */
int n = epoll_wait (epoll_fd, events, 1024, -1);
for (int i = 0; i < n; i++) {
proxy_conn_t *conn = events[i].data.ptr;
if (events[i].events & EPOLLIN) {
/* client→backend 또는 backend→client 릴레이 */
relay_data (conn->src_fd, conn->peer->dst_fd);
}
}
}
}
/* 프록시 리스너: 새 연결마다 백엔드 연결 생성 */
static void
handle_new_connection (int listen_fd)
{
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
/* 클라이언트 연결 수락 */
int client_fd = accept4 (listen_fd,
(struct sockaddr *)&client_addr, &addr_len,
SOCK_NONBLOCK);
/* 백엔드 서버에 연결 */
int backend_fd = socket (AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
struct sockaddr_in backend_addr = {
.sin_family = AF_INET,
.sin_port = htons(8080),
.sin_addr.s_addr = inet_addr("192.168.1.10")
};
connect (backend_fd, (struct sockaddr *)&backend_addr,
sizeof(backend_addr));
/* 양쪽 fd를 epoll에 등록하여 릴레이 시작 */
register_proxy_pair (client_fd, backend_fd);
}
코드 설명
-
relay_data()
한 방향의 데이터 전달에
recv()+send()2회 syscall이 필요합니다. 양방향이면 4회이며, 각 syscall마다 커널-유저 간 데이터 복사가 발생합니다. 이것이 커널 프록시의 근본적 성능 병목(Bottleneck)입니다. - handle_new_connection() 클라이언트가 프록시에 연결하면, 프록시가 별도의 TCP 연결을 백엔드에 맺습니다. 클라이언트는 백엔드의 IP를 알 필요가 없으며, 프록시의 IP로만 통신합니다. 이 구조가 L4 프록시의 핵심입니다.
splice()를 이용한 제로카피 TCP 릴레이
리눅스 커널의 splice() 시스템 콜(System Call)은 커널 내부 파이프 버퍼를 활용하여 두 파일 디스크립터(File Descriptor) 간 데이터를 유저스페이스 복사 없이 전달합니다. TCP 프록시에서 성능을 크게 향상시키는 기법입니다.
/* splice()를 이용한 제로카피 TCP 릴레이 */
#include <fcntl.h>
static void
splice_relay (int src_fd, int dst_fd, int pipe_fd[2])
{
ssize_t n;
/* 1단계: 소켓 → 파이프 (커널 내부에서 페이지 참조 이동) */
n = splice (src_fd, NULL, pipe_fd[1], NULL,
65536, SPLICE_F_MOVE | SPLICE_F_NONBLOCK);
if (n <= 0)
return;
/* 2단계: 파이프 → 소켓 (유저스페이스 복사 없음) */
splice (pipe_fd[0], NULL, dst_fd, NULL,
n, SPLICE_F_MOVE | SPLICE_F_NONBLOCK);
}
/* 연결당 파이프 쌍 생성 — 양방향이므로 2쌍 필요 */
int pipe_c2b[2], pipe_b2c[2];
pipe2 (pipe_c2b, O_NONBLOCK); /* client → backend */
pipe2 (pipe_b2c, O_NONBLOCK); /* backend → client */
/* epoll에서 이벤트 발생 시 */
splice_relay (client_fd, backend_fd, pipe_c2b);
splice_relay (backend_fd, client_fd, pipe_b2c);
| 방식 | syscall 횟수/방향 | 메모리 복사 | fd 오버헤드 | 처리량 (1Gbps NIC) |
|---|---|---|---|---|
| recv/send | 2회 | 2회 (커널↔유저) | 없음 | ~600 Mbps |
| splice | 2회 | 0회 (페이지(Page) ref) | 파이프 2쌍/연결 | ~900 Mbps |
| VPP FIFO | 0회 | 0회 (SHM) | 없음 | ~10+ Gbps |
TPROXY (투명 프록시) 메커니즘
투명 프록시가 필요한 이유
일반 TCP 프록시(명시적 프록시)에서 클라이언트는 프록시의 IP에 직접 연결합니다. 그러나 많은 시나리오에서 클라이언트가 프록시의 존재를 인식하지 못한 채 통신하도록 해야 합니다. 투명 프록시(Transparent Proxy)는 네트워크 경로상의 트래픽을 가로채어, 클라이언트가 원래 목적지와 직접 통신하는 것처럼 보이게 합니다.
| 비교 항목 | 명시적 프록시 (Explicit) | 투명 프록시 (Transparent) |
|---|---|---|
| 클라이언트 설정 | 프록시 IP/포트 설정 필요 | 설정 불필요 (네트워크가 리다이렉트) |
| 트래픽 유인 | 클라이언트가 직접 프록시에 연결 | iptables/라우팅(Routing)으로 리다이렉트 |
| 원본 목적지 보존 | 프록시가 목적지 결정 | 원본 dst IP/포트 보존 필요 |
| 소켓 옵션 | 일반 소켓 | IP_TRANSPARENT 필요 |
| 커널 지원 | 불필요 | TPROXY 모듈 또는 REDIRECT 필요 |
| 대표 사용례 | HTTP 프록시, SOCKS | 방화벽, SSL Inspection, CDN |
리눅스 TPROXY 동작 원리
리눅스 커널의 TPROXY는 Netfilter(iptables)의 -j TPROXY 타겟을 통해 동작합니다. 패킷(Packet)의 원본 목적지 주소를 변경하지 않고 로컬 소켓으로 전달하는 것이 핵심입니다.
TPROXY iptables 설정 상세
TPROXY 투명 프록시를 구성하는 전체 iptables/ip rule 설정과 각 규칙의 역할입니다.
# ━━━━ 1단계: TPROXY iptables 규칙 설정 ━━━━
# HTTPS(443) 트래픽을 TPROXY로 리다이렉트
# mangle 테이블의 PREROUTING 체인에서 동작
iptables -t mangle -A PREROUTING \
-p tcp --dport 443 \
-j TPROXY \
--tproxy-mark 0x1/0x1 \
--on-ip 0.0.0.0 \
--on-port 8443
# --tproxy-mark: 패킷에 fwmark를 설정 (정책 라우팅에 사용)
# --on-port: 패킷을 전달할 로컬 리스닝 포트
# --on-ip: 0.0.0.0이면 원본 dst IP 유지
# ━━━━ 2단계: 정책 라우팅 설정 ━━━━
# fwmark 0x1 패킷에 대한 라우팅 테이블 지정
ip rule add fwmark 0x1 lookup 100
# 테이블 100: 모든 트래픽을 로컬로 전달
ip route add local 0.0.0.0/0 dev lo table 100
# ━━━━ 3단계: 역방향 경로 필터 비활성화 ━━━━
# TPROXY는 다른 IP를 목적지로 하는 패킷을 수신하므로
# rp_filter가 이를 스푸핑으로 간주하여 드롭할 수 있습니다
sysctl -w net.ipv4.conf.all.rp_filter=0
sysctl -w net.ipv4.conf.eth0.rp_filter=0
# IP 포워딩 활성화 (게이트웨이로 동작하는 경우)
sysctl -w net.ipv4.ip_forward=1
# ━━━━ 확인 명령 ━━━━
iptables -t mangle -L PREROUTING -nv
ip rule show
ip route show table 100
IP_TRANSPARENT 소켓을 이용한 TPROXY 프록시 구현
TPROXY로 리다이렉트된 트래픽을 수신하려면 프록시 소켓에 IP_TRANSPARENT 옵션을 설정해야 합니다. 이 옵션은 소켓이 로컬에 할당되지 않은 IP 주소의 트래픽도 수신하고, 해당 IP로 응답을 보낼 수 있게 합니다.
/* TPROXY 투명 프록시 서버 구현 (핵심 부분) */
#include <sys/socket.h>
#include <netinet/in.h>
#include <linux/netfilter_ipv4.h>
/* IP_TRANSPARENT 리스닝 소켓 생성 */
static int
create_tproxy_listener (uint16_t port)
{
int fd = socket (AF_INET, SOCK_STREAM, 0);
int one = 1;
/* IP_TRANSPARENT: 로컬에 없는 IP의 트래픽도 수신 가능 */
setsockopt (fd, SOL_IP, IP_TRANSPARENT, &one, sizeof(one));
/* SO_REUSEADDR: 빠른 포트 재사용 */
setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons (port),
.sin_addr.s_addr = INADDR_ANY
};
bind (fd, (struct sockaddr *)&addr, sizeof(addr));
listen (fd, 4096);
return fd;
}
/* 수락된 연결에서 원본 목적지 주소 획득 */
static int
get_original_dst (int client_fd,
struct sockaddr_in *orig_dst)
{
socklen_t len = sizeof(*orig_dst);
/* TPROXY는 SO_ORIGINAL_DST 대신 getsockname()으로 원본 dst 획득 */
/* TPROXY가 dst IP를 변경하지 않으므로, 소켓의 로컬 주소가
* 곧 클라이언트가 연결하려 했던 원본 목적지입니다 */
return getsockname (client_fd,
(struct sockaddr *)orig_dst, &len);
}
/* 백엔드 연결 시 IP_TRANSPARENT로 원본 src IP 유지 */
static int
connect_to_backend_transparent (
struct sockaddr_in *orig_dst,
struct sockaddr_in *client_addr)
{
int fd = socket (AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
int one = 1;
/* 백엔드에도 IP_TRANSPARENT 설정:
* 클라이언트의 원본 src IP로 바인딩하여
* 백엔드 서버가 프록시 존재를 인식하지 못하게 합니다 */
setsockopt (fd, SOL_IP, IP_TRANSPARENT, &one, sizeof(one));
/* 클라이언트 IP로 소스 주소 바인딩 (스푸핑) */
bind (fd, (struct sockaddr *)client_addr,
sizeof(*client_addr));
/* 원본 목적지 서버에 연결 */
connect (fd, (struct sockaddr *)orig_dst,
sizeof(*orig_dst));
return fd;
}
코드 설명
-
IP_TRANSPARENT
IP_TRANSPARENT소켓 옵션은CAP_NET_ADMIN권한이 필요합니다. 이 옵션이 설정된 소켓은 로컬에 할당되지 않은 IP 주소로의 바인드와 해당 IP의 트래픽 수신이 가능합니다. TPROXY의 핵심 요소입니다. -
getsockname()
TPROXY는 REDIRECT와 달리 패킷의 dst IP를 변경하지 않으므로,
accept()한 소켓의getsockname()이 원본 목적지 주소를 반환합니다. REDIRECT 방식에서 사용하는SO_ORIGINAL_DST와 다른 점에 주의하세요. -
connect_to_backend_transparent()
백엔드 연결에서도
IP_TRANSPARENT를 사용하여 클라이언트의 원본 소스 IP로 바인드합니다. 이렇게 하면 백엔드 서버의 로그에 실제 클라이언트 IP가 기록되며, 완전한 투명성을 달성합니다.
TPROXY vs REDIRECT 비교
리눅스에서 투명 프록시를 구현하는 두 가지 Netfilter 방식의 차이입니다.
| 항목 | TPROXY (-j TPROXY) | REDIRECT (-j REDIRECT) |
|---|---|---|
| Netfilter 테이블 | mangle (PREROUTING) | nat (PREROUTING) |
| 패킷 변조 | dst IP 변경 없음 | dst IP를 127.0.0.1로 변경 |
| 원본 목적지 획득 | getsockname() | getsockopt(SO_ORIGINAL_DST) |
| conntrack 의존 | 불필요 | 필요 (NAT 추적) |
| 성능 | 높음 (NAT 오버헤드 없음) | 낮음 (conntrack 부하) |
| 소스 IP 스푸핑 | 가능 (IP_TRANSPARENT) | 불가능 |
| 커널 모듈(Kernel Module) | xt_TPROXY | xt_REDIRECT + nf_conntrack |
| 권한 요구 | CAP_NET_ADMIN | CAP_NET_ADMIN |
| 적합 시나리오 | 고성능 프록시, 방화벽 | 간단한 로컬 리다이렉트 |
# REDIRECT 방식 (비교용) — conntrack 의존
iptables -t nat -A PREROUTING \
-p tcp --dport 443 \
-j REDIRECT --to-ports 8443
# 프록시에서 원본 목적지 획득 (REDIRECT 방식)
# getsockopt(client_fd, SOL_IP, SO_ORIGINAL_DST,
# &orig_dst, &len);
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# TPROXY 방식 — conntrack 불필요, 고성능
iptables -t mangle -A PREROUTING \
-p tcp --dport 443 \
-j TPROXY --on-port 8443 --tproxy-mark 0x1/0x1
# 프록시에서 원본 목적지 획득 (TPROXY 방식)
# getsockname(client_fd, &orig_dst, &len);
TPROXY + TLS의 한계: 커널 프록시의 성능 병목
TPROXY와 TLS를 조합하면 커널 기반 투명 TLS 프록시를 구성할 수 있지만, 심각한 성능 병목이 발생합니다.
커널 프록시에서 VPP TLS 프록시로의 전환
tls cert add, tls ca-cert add, tls key add, show tls ctx verbose, show tls certs, set tls cipher 같은 명령은 공식 tlsopenssl 플러그인 소스에 존재하지 않습니다. 실제 CLI는 tls openssl set·tls openssl set-tls·tls openssl load-provider 세 개이고, 인증서·CA·cipher·세션 티켓은 바이너리 API(app_add_cert_key_pair) 또는 OpenSSL 설정으로 다룹니다. 정정된 코드 경로는 Host Stack 문서의 VCL TLS 레시피와 TLS 운영 심화에 있습니다. 아래 블록은 "무엇을 해야 하는가"를 나타내는 가이드로 읽어 주시기 바랍니다.
기술 스택 비교: 커널 vs VPP
커널 기반 TPROXY+TLS 프록시와 VPP 기반 TLS 프록시의 전체 기술 스택을 비교합니다.
| 계층 | 커널 기반 (TPROXY + OpenSSL) | VPP 기반 |
|---|---|---|
| NIC 접근 | 커널 NIC 드라이버 (인터럽트(Interrupt) 기반) | DPDK/AF_XDP (폴링 기반, 제로카피) |
| 패킷 분류 | Netfilter (iptables hooks) | VPP classifier 노드 |
| 투명 프록시 | TPROXY (xt_TPROXY 모듈) | 세션 레이어 라우팅 (classify + output-feature) |
| TCP 스택 | 커널 TCP | VPP 내장 TCP (유저스페이스) |
| TLS 처리 | OpenSSL (유저스페이스 라이브러리) | TLS 엔진 플러그인 (세션 레이어 통합) |
| 데이터 전달 | recv/send syscall + 커널 복사 | SVM FIFO (공유 메모리, 제로카피) |
| 이벤트 모델 | epoll (syscall per wait) | 공유 메모리 이벤트 큐 (폴링) |
| 멀티코어 | 프로세스(Process)/스레드(Thread) (잠금 경합(Lock Contention)) | 워커 스레드 (RSS 해시 기반 무락) |
| 인증서 교체 | 프로세스 재시작(Reboot) 필요 | 무중단 CLI/API 교체 |
| 처리량 (10G NIC) | ~3 Gbps (TLS) | ~9+ Gbps (TLS) |
| CPS (연결/초) | ~30K (RSA-2048) | ~150K (QAT), ~80K (SW) |
VPP 프록시 애플리케이션 아키텍처
VPP에서 TCP/TLS 프록시를 구현하는 방법은 두 가지입니다: 내장 프록시 플러그인을 사용하거나, VCL 기반 외부 애플리케이션을 작성하는 것입니다.
VPP 프록시 세션 릴레이 소스 분석
VPP 내장 프록시의 핵심은 두 세션의 FIFO를 직접 연결하는 proxy_session 릴레이입니다. TLS 복호화 후의 평문 데이터를 백엔드 TCP 세션으로 전달하는 경로를 분석합니다.
/* VPP 프록시 세션 릴레이 — FIFO 직접 전달 (의사코드) */
static int
proxy_rx_callback (session_t *s)
{
proxy_session_t *ps = proxy_session_get (s);
session_t *peer;
svm_fifo_t *rx_fifo, *peer_tx_fifo;
svm_fifo_seg_t segs[2];
u32 n_segs, max_deq, max_enq, to_relay;
/* 피어 세션 (클라이언트↔백엔드) 조회 */
peer = session_get_from_handle (ps->peer_session_handle);
rx_fifo = s->rx_fifo;
peer_tx_fifo = peer->tx_fifo;
/* 릴레이 가능한 바이트 수 계산 */
max_deq = svm_fifo_max_dequeue_cons (rx_fifo);
max_enq = svm_fifo_max_enqueue_prod (peer_tx_fifo);
to_relay = clib_min (max_deq, max_enq);
if (to_relay == 0)
return 0;
/* 제로카피 세그먼트 접근: RX FIFO 내부 메모리 직접 참조 */
n_segs = 2;
svm_fifo_segments (rx_fifo, 0, segs, &n_segs, to_relay);
/* 피어 TX FIFO에 직접 enqueue (중간 버퍼 없이) */
for (u32 i = 0; i < n_segs; i++)
svm_fifo_enqueue (peer_tx_fifo, segs[i].len, segs[i].data);
/* 원본 FIFO에서 릴레이한 바이트 제거 */
svm_fifo_dequeue_drop (rx_fifo, to_relay);
/* 피어에 TX 이벤트 발생 → 전송 시작 */
session_send_io_evt_to_thread (peer_tx_fifo,
SESSION_IO_EVT_TX);
return 0;
}
코드 설명
- svm_fifo_segments() 이 함수는 FIFO 내부 메모리에 대한 직접 포인터를 반환합니다. 링버퍼이므로 데이터가 경계를 넘는 경우 최대 2개 세그먼트가 반환됩니다. 별도의 중간 버퍼 할당이 불필요하여 진정한 제로카피를 달성합니다.
- proxy_rx_callback() 이 콜백(Callback)은 세션 레이어가 RX 데이터를 감지할 때 자동 호출됩니다. TLS 세션인 경우, 이 시점에서 데이터는 이미 TLS 엔진에 의해 복호화된 평문 상태입니다. 따라서 프록시 릴레이 코드는 TLS 존재 여부에 무관하게 동일하게 동작합니다.
-
backpressure
clib_min(max_deq, max_enq)로 피어 FIFO의 남은 공간만큼만 릴레이합니다. 피어가 느려서 TX FIFO가 가득 차면 자동으로 backpressure가 적용되어, TCP 윈도우가 축소되고 결국 클라이언트의 전송 속도가 조절됩니다.
VPP에서의 투명 프록시 구현 방법
VPP는 커널의 TPROXY와 다른 방식으로 투명 프록시를 구현합니다. DPDK가 커널을 우회하여 패킷을 직접 수신하므로, iptables TPROXY 규칙이 동작하지 않습니다. 대신 VPP의 그래프 노드 파이프라인(Pipeline)에서 투명 프록시 기능을 구현합니다.
# ━━━━ VPP 투명 TLS 프록시 설정 ━━━━
# 1. 인터페이스 설정 (DPDK 바인딩)
vpp# create interface dpdk GigabitEthernet3/0/0
vpp# set interface state GigabitEthernet3/0/0 up
vpp# set interface ip address GigabitEthernet3/0/0 10.0.0.1/24
# 2. 세션 레이어 활성화
vpp# session enable
# 3. TLS 인증서 등록
vpp# tls cert add cert /etc/vpp/certs/server.pem \
key /etc/vpp/certs/server.key
# 4. 내장 프록시 플러그인 활성화
# http_proxy 또는 echo_proxy 사용
# 5. TLS 리스너 생성 (프론트엔드: 클라이언트 대면)
# 세션 레이어가 자동으로 TCP+TLS 핸드셰이크 처리
vpp# http static server www-root /var/www/html \
uri tls://0.0.0.0/443 \
cache-size 10m
# 또는 CLI 기반 프록시 설정
vpp# test proxy server uri tls://0.0.0.0/443 \
backend tcp://192.168.1.10/8080
# 6. 투명 모드: classify를 이용한 트래픽 리다이렉트
# 목적지 포트 443 트래픽을 로컬 세션 레이어로 유인
vpp# classify session acl-hit-next local \
table-index 0 match l4 dst_port 443
# 7. 동작 확인
vpp# show session verbose
vpp# show tls ctx verbose
vpp# show proxy statistics
- NIC 바인딩:
dpdk-devbind -b vfio-pci <PCI_ADDR>로 NIC을 DPDK에 바인딩합니다. 이후 iptables TPROXY 규칙은 해당 NIC에 적용되지 않습니다. - 세션 레이어 활성화:
session enable이 필수이며, FIFO 크기와 세션 수를 startup.conf에서 설정합니다. - 인증서 이전: 기존 프록시의 TLS 인증서를
tls cert add로 VPP에 등록합니다. - 트래픽 유인: VPP의 classify/ACL 노드로 iptables TPROXY 역할을 대체합니다.
- 라우팅 설정: 백엔드 서버로의 경로가 VPP의 FIB에 등록되어야 합니다.
TCP 프록시 → TLS 프록시 구현 경로
단계별 구현 경로
순수 TCP 프록시에서 출발하여 완전한 VPP TLS 종단 프록시까지 도달하는 구현 경로를 단계별로 정리합니다.
| 단계 | 구현 내용 | 핵심 API/기능 | 성능 이점 |
|---|---|---|---|
| 1. 커널 TCP 릴레이 | recv/send 기반 양방향 릴레이 | socket, epoll, recv, send | 기준선 (~600 Mbps) |
| 2. splice 최적화 | 커널 내부 제로카피 릴레이 | splice, pipe | ~1.5x (복사 제거) |
| 3. TPROXY 투명화 | iptables + IP_TRANSPARENT | xt_TPROXY, SO_ORIGINAL_DST | 동일 (투명성 확보) |
| 4. TLS 종단 추가 | OpenSSL로 TLS 복호화/암호화 | SSL_read, SSL_write | ~0.5x (TLS 오버헤드) |
| 5. VPP 이전: TCP | VPP 세션 레이어 기반 릴레이 | session_open, SVM FIFO | ~5x (syscall 제거) |
| 6. VPP TLS 통합 | VPP TLS 엔진 + FIFO 릴레이 | TRANSPORT_PROTO_TLS | ~10x (완전 유저스페이스) |
| 7. 비동기 + HW 오프로드 | QAT/Cryptodev 비동기 암호화 | tls_async, Cryptodev PMD | ~15x (HW 가속) |
VCL 기반 TLS 프록시 전체 구현 예제
VCL API를 사용한 TLS 종단 프록시의 전체 구현 예제입니다. 프론트엔드에서 TLS를 종단하고, 백엔드에는 평문 TCP로 연결합니다.
/* VCL 기반 TLS 종단 프록시 (전체 구현) */
#include <vcl/vppcom.h>
#include <pthread.h>
#define BACKEND_IP "192.168.1.10"
#define BACKEND_PORT 8080
#define LISTEN_PORT 443
#define BUF_SIZE 65536
/* 연결 쌍: TLS 프론트 ↔ TCP 백엔드 */
typedef struct {
int frontend; /* TLS 세션 (클라이언트) */
int backend; /* TCP 세션 (백엔드 서버) */
} proxy_pair_t;
/* 양방향 릴레이 스레드 */
static void *
relay_thread (void *arg)
{
proxy_pair_t *pp = (proxy_pair_t *)arg;
uint8_t buf[BUF_SIZE];
int epoll_fd, n_events;
struct epoll_event events[2];
/* VCL epoll 생성 (vppcom 전용) */
epoll_fd = vppcom_epoll_create ();
/* 양쪽 세션을 epoll에 등록 */
vppcom_epoll_ctl (epoll_fd, EPOLL_CTL_ADD,
pp->frontend, EPOLLIN);
vppcom_epoll_ctl (epoll_fd, EPOLL_CTL_ADD,
pp->backend, EPOLLIN);
while (1) {
n_events = vppcom_epoll_wait (epoll_fd, events,
2, -1);
for (int i = 0; i < n_events; i++) {
int src, dst;
if (events[i].data.fd == pp->frontend) {
src = pp->frontend; /* TLS (평문으로 읽힘) */
dst = pp->backend; /* TCP (평문으로 전송) */
} else {
src = pp->backend;
dst = pp->frontend;
}
/* VCL read: TLS 세션이면 자동 복호화 */
int nread = vppcom_session_read (src, buf,
BUF_SIZE);
if (nread <= 0) {
/* 연결 종료 */
vppcom_session_close (pp->frontend);
vppcom_session_close (pp->backend);
free (pp);
return NULL;
}
/* VCL write: TLS 세션이면 자동 암호화 */
vppcom_session_write (dst, buf, nread);
}
}
}
int main (void)
{
vppcom_endpt_t endpt = {0};
int listener, client;
/* 1. VCL 초기화 */
vppcom_app_create ("tls-proxy");
/* 2. TLS 리스너 생성 */
listener = vppcom_session_create (VPPCOM_PROTO_TLS, 0);
/* 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 (LISTEN_PORT);
vppcom_session_bind (listener, &endpt);
vppcom_session_listen (listener, 4096);
/* 5. 메인 루프: 연결 수락 + 백엔드 연결 + 릴레이 */
while (1) {
/* accept: TLS 핸드셰이크 완료된 세션 반환 */
client = vppcom_session_accept (listener, &endpt, 0);
if (client < 0) continue;
/* 백엔드 TCP 세션 생성 및 연결 */
int backend = vppcom_session_create (VPPCOM_PROTO_TCP, 0);
vppcom_endpt_t bk_ep = {
.is_ip4 = 1,
.port = htons (BACKEND_PORT),
};
inet_pton (AF_INET, BACKEND_IP, bk_ep.ip);
vppcom_session_connect (backend, &bk_ep);
/* 릴레이 스레드 시작 */
proxy_pair_t *pp = malloc (sizeof(*pp));
pp->frontend = client;
pp->backend = backend;
pthread_t tid;
pthread_create (&tid, NULL, relay_thread, pp);
pthread_detach (tid);
}
return 0;
}
코드 설명
-
VPPCOM_PROTO_TLS vs VPPCOM_PROTO_TCP
프론트엔드 리스너는
VPPCOM_PROTO_TLS로 생성하여 TLS 핸드셰이크를 자동 처리합니다.vppcom_session_accept()가 반환하는 세션은 이미 TLS 핸드셰이크가 완료된 상태이므로,vppcom_session_read()는 복호화된 평문을 반환합니다. 백엔드는VPPCOM_PROTO_TCP로 평문 TCP 연결을 생성합니다. -
릴레이 구조
VCL의
epollAPI는 커널 epoll과 동일한 인터페이스지만, 내부적으로 VPP의 공유 메모리 이벤트 큐를 사용합니다. 커널 epoll_wait()는 syscall이지만, VCL epoll_wait()는 유저스페이스에서 이벤트 큐를 폴링하므로 훨씬 빠릅니다. - 성능 참고 이 예제는 스레드 기반이지만, 프로덕션에서는 VPP 내장 프록시 플러그인이나 이벤트 기반 단일 스레드 모델이 더 효율적입니다. 내장 플러그인은 FIFO-to-FIFO 직접 릴레이로 중간 버퍼 복사를 완전히 제거합니다.
# VCL TLS 프록시 빌드 및 실행
$ gcc -o tls-proxy tls-proxy.c \
-I/usr/include/vpp \
-lvppcom -lvlibmemoryclient -lsvm -lpthread
# VCL 설정 파일
$ cat /etc/vpp/vcl-proxy.conf
vcl {
rx-fifo-size 4000000
tx-fifo-size 4000000
app-scope-global
use-mq-eventfd
api-socket-name /run/vpp/api.sock
}
# 실행 (VPP 프로세스가 미리 실행 중이어야 합니다)
$ VCL_CONFIG=/etc/vpp/vcl-proxy.conf ./tls-proxy
# 테스트
$ curl -k https://10.0.0.1:443/
# → VPP TLS 종단 → 평문으로 백엔드 192.168.1.10:8080에 전달
성능 벤치마크: 커널 TPROXY+OpenSSL vs VPP TLS
| 측정 항목 | 커널 TPROXY + OpenSSL | VPP TLS (SW) | VPP TLS (QAT) |
|---|---|---|---|
| CPS (RSA-2048) | ~25K | ~80K | ~150K |
| CPS (ECDSA P-256) | ~40K | ~120K | ~200K |
| 처리량 (AES-128-GCM) | ~3 Gbps | ~9 Gbps | ~40 Gbps |
| 지연(Latency)시간 (P99) | ~800 μs | ~200 μs | ~150 μs |
| CPU 사용률 (10K conn) | ~80% | ~30% | ~15% |
| 메모리 (50K ctx) | ~2.5 GB | ~1.7 GB | ~1.7 GB |
실전 예제: VPP TLS 종단 프록시
HTTPS 리버스 프록시 구성
VPP를 HTTPS 리버스 프록시로 구성하면, 외부 클라이언트의 TLS를 VPP에서 종단하고 내부 백엔드 서버에는 평문 HTTP로 전달할 수 있습니다:
# 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
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
TLS 디버깅
VPP TLS 문제를 진단하기 위한 CLI 명령과 기법입니다. 핸드셰이크 단계별 타임라인과 엔진 콜백의 의미는 TLS — 핸드셰이크 상세 타임라인에서, 호스트 스택 관점의 트러블슈팅(세션 누수·워커 편향)은 호스트 스택 — 트러블슈팅에서 다룹니다.
# 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
# 로드된 플러그인과 세션 레이어 상태
vpp# show plugins | grep -i tls
vpp# show session verbose
# 트레이싱으로 TLS 핸드셰이크 관찰 (TLS 노드에서 직접 trace)
vpp# trace add session-queue 10
vpp# show trace
show tls engines, show tls ctx, show tls certs, set tls cipher, tls ca-cert add 같은 명령이 등장합니다. 이들은 공식 tlsopenssl 플러그인 소스에 존재하지 않습니다. 실제 존재하는 TLS 전용 CLI는 tls openssl set, tls openssl set-tls, tls openssl load-provider 세 개뿐입니다. 표 안의 항목은 "같은 의미의 진단 행위"를 개념적으로 나타낸 것이며, 실행 시에는 show errors·show session verbose·show crypto engines·show log 조합으로 대체하시기 바랍니다. 인증서·CA 관리는 모두 바이너리 API(app_add_cert_key_pair)로 수행합니다.
| 증상 | 실제 진단 경로 | 일반적 원인 |
|---|---|---|
| 핸드셰이크 실패 | show errors | grep -iE 'tls|session', show log | 인증서 만료, cipher 불일치 |
| 성능 저하 | show runtime의 tls-*/crypto-*/session-* 노드, show crypto engines | 비동기 미활성, SW 암호화 병목 |
| 연결 끊김 | show session verbose | 세션 타임아웃, FIFO 오버플로 |
| 인증서 오류 | 애플리케이션이 BAPI로 CKPAIR 재조회, show log | 키 불일치, CA 체인 누락 |
| mTLS 거부 | show errors, 세션 레이어 trace | 클라이언트 인증서 미제출 |
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 session verbose에서 tlssession 상태 고착 | 방화벽(Firewall) 규칙 확인, 애플리케이션 idle 타임아웃 조정 |
| "no shared cipher" 오류 | show errors에 핸드셰이크 실패 카운터 증가 | OpenSSL 설정 파일 또는 애플리케이션 측 SSL_CTX_set_ciphersuites로 현대 cipher 보장 |
| 인증서 체인 오류 | 세션 레이어 trace, show log 내 verify 실패 | BAPI로 중간 CA를 포함한 전체 체인 CKPAIR 재등록 |
| 메모리 부족 | show memory verbose에서 session 세그먼트 고갈 | session { evt_qs_memfd_seg_size } 증가, Hugepage 추가 |
| 비대칭 성능 (한쪽만 느림) | show session verbose에서 FIFO 사용률 확인 | TX/RX FIFO 크기 조정: session { tx-fifo-size } |
| QAT 오프로드 미동작 | show crypto engines에 qat 엔진 없음 | QAT 드라이버 로드 확인, DPDK cryptodev 또는 tls openssl load-provider qatprovider |
| 세션 누수 | show session 연결 수가 계속 증가 | 애플리케이션에서 vppcom_session_close() 호출 누락 확인 |
| mTLS 클라이언트 거부 | show errors의 cert-verify 카운터 | 클라이언트 CA가 CKPAIR 체인 또는 플러그인 trust 경로에 포함되어 있는지 확인 |
# 종합 TLS 상태 진단 — 실제로 존재하는 명령만
vpp# show version
vpp# show plugins | grep -i tls
vpp# show crypto engines
vpp# show session verbose
vpp# show errors
vpp# show runtime
vpp# show memory verbose
vpp# show log | grep -iE 'tls|session'
고급 TLS 프록시 패턴 — Termination·Re-encrypt·Passthrough·SNI 라우팅
TLS 프록시는 목적에 따라 네 가지 토폴로지(Topology) 중 하나를 선택합니다. 같은 VPP 인스턴스에서도 인터페이스·포트·SNI별로 서로 다른 모드를 동시에 운영할 수 있습니다. 각 모드는 보안 경계, 성능, 기능 측면에서 트레이드오프가 명확하므로 설계 초기에 확정해두어야 운영 중 혼선이 줄어듭니다.
| 모드 | 클라이언트 ↔ VPP | VPP ↔ 업스트림 | VPP가 보는 평문 | 대표 용도 |
|---|---|---|---|---|
| Termination | TLS (VPP가 서버 인증서 제시) | 평문 TCP (내부망) | 요청·응답 전체 | 리버스 프록시, API 게이트웨이, 로드 밸런서 |
| Re-encrypt (bridging) | TLS (프록시 인증서) | TLS (업스트림 인증서 검증) | 요청·응답 전체 | WAF, DLP, 컨텐츠 스캔이 필요한 엔터프라이즈 프록시 |
| Passthrough (SNI 라우팅) | TLS 레코드 릴레이만 | TLS 레코드 릴레이만 | 없음 (SNI/ALPN만 보임) | 멀티 테넌트 프런트, 클라이언트 인증서 종단 불필요 |
| Inspection | TLS (동적 서명 인증서) | TLS (새 세션) | 전체 (복호화 후 재암호화) | Full SSL Inspection (다음 절 참조) |
Termination 구성 — ssl termination 래퍼
가장 단순한 패턴입니다. VPP의 TLS 리스너가 443을 열고, 업스트림은 내부 평문 TCP로 재분배합니다. 엔드 투 엔드 TLS가 요구되지 않을 때(내부망 신뢰, 별도 사이드카 암호화, 국내 규정) 가장 효율적입니다.
# CKPAIR 등록은 바이너리 API(app_add_cert_key_pair)로 수행 — 반환 index는 5라고 가정
# test proxy 플러그인 — server는 TLS(ckpair 5), client는 TCP로 설정
vpp# test proxy server uri tls://0.0.0.0/443 ckpair 5 client uri tcp://10.0.0.10/8080
vpp# test proxy server uri tls://0.0.0.0/443 ckpair 5 client uri tcp://10.0.0.11/8080 # 다중 백엔드
이 모드에서 가장 자주 놓치는 부분은 PROXY v2 헤더입니다. 백엔드가 오리진 IP를 알아야 한다면 proxy 플러그인을 PROXY protocol 지원 버전으로 빌드하거나, VPP 앞단에 haproxy-abns처럼 헤더를 주입하는 경량 리레이를 두시기 바랍니다. VPP 25.06 이후 proxy 플러그인에 proxy-proto v2 옵션이 추가되어 내장 처리도 가능합니다.
Re-encrypt 구성 — 업스트림 인증서 검증 포함
엄격한 보안 정책을 따르는 환경은 업스트림과도 TLS를 유지해야 합니다. 이 경우 VPP 세션은 두 개(클라이언트 측·업스트림 측)가 생성되며, 양쪽 모두 다른 CKPAIR를 씁니다.
/* 제어 평면(GoVPP/PAPI/자체 C 앱)에서 BAPI로 CKPAIR 2개를 등록합니다.
프론트용(proxy가 클라이언트에 제시), 백엔드용(업스트림 mTLS 연동) */
u32 ckpair_front = bapi_add_cert_key_pair (proxy_front_pem, proxy_front_key);
u32 ckpair_client = bapi_add_cert_key_pair (proxy_client_pem, proxy_client_key);
/* 업스트림 서버의 신뢰 루트와 hostname 검증은 애플리케이션 측
SSL_CTX 구성 경로에서 처리합니다 — 공식 CLI로 제공되지 않습니다. */
업스트림 검증을 끄지 마시기 바랍니다. Self-signed 서버 테스트를 위해 일시적으로 비활성화하는 것은 허용되지만, 운영 환경에서 꺼 둔 채 잊으면 MITM에 노출됩니다. 대신 내부 CA를 만들어 모든 내부 서비스에 이 CA가 서명한 인증서를 배포하고, 프록시가 참조하는 trust store에 루트 하나만 넣는 방식이 안전합니다.
Passthrough — SNI 기반 L4 라우팅
VPP가 TLS 레코드를 복호화하지 않고 SNI만 읽어 업스트림을 정하는 구성입니다. ClientHello 안의 SNI 확장을 파싱해 매핑 테이블로 분기하며, 이후 바이트들은 단순 L4 릴레이가 됩니다. 멀티 테넌트 런타임 프런트·쿠버네티스 인그레스 프런트에서 자주 씁니다.
/* ClientHello에서 SNI만 추출하는 핵심 함수 개요
— VPP의 session layer는 peek 콜백으로 초기 바이트를 건네 줍니다 */
static int
sni_extract_from_ch (u8 *data, u32 len, u8 **sni_out, u32 *sni_len_out)
{
/* TLS record: type(1) version(2) length(2) handshake(4+) */
if (len < 43 || data[0] != 0x16 /* handshake */ || data[5] != 0x01 /* ClientHello */)
return -1;
u8 *p = data + 43; /* session_id_len 위치 */
p += 1 + p[0]; /* session_id 건너뜀 */
u16 cs_len = (p[0] << 8) | p[1]; p += 2 + cs_len;
p += 1 + p[0]; /* compression methods */
u16 ext_total = (p[0] << 8) | p[1]; p += 2;
u8 *end = p + ext_total;
while (p + 4 <= end) {
u16 ext_type = (p[0] << 8) | p[1];
u16 ext_len = (p[2] << 8) | p[3]; p += 4;
if (ext_type == 0 /* server_name */) {
/* list_len(2) name_type(1) name_len(2) name... */
*sni_out = p + 5;
*sni_len_out = (p[3] << 8) | p[4];
return 0;
}
p += ext_len;
}
return -1;
}
ALPN 기반 라우팅과 HTTP/2/HTTP/3 분기
SNI가 같은 도메인이라도 프로토콜(h2, http/1.1, h3)에 따라 다른 업스트림으로 보내야 할 때가 있습니다. VPP proxy 플러그인은 ALPN 리스트를 클라이언트로부터 받아 선택 결과를 애플리케이션 콜백에 전달합니다. 다음은 ALPN에 따라 백엔드를 고르는 간단한 로직입니다.
static upstream_t *
pick_upstream (sni_t *sni, alpn_list_t *alpn)
{
if (alpn_contains (alpn, "h2"))
return sni->backend_h2;
if (alpn_contains (alpn, "http/1.1"))
return sni->backend_h1;
return sni->backend_default;
}
h3는 TCP 기반이 아니므로 같은 리스너에서 다루지 않습니다. QUIC 프록시를 별도로 두고, HTTPS RR 레코드로 클라이언트가 h3와 h2를 구분해 접속하도록 하시기 바랍니다. 구체적인 QUIC 종단 구성은 VPP QUIC 심화 문서에서 다룹니다.
인증서 전략 — 와일드카드 vs SAN vs LE 자동 갱신
| 전략 | 장점 | 단점 | VPP 설정 포인트 |
|---|---|---|---|
와일드카드 *.example.com | 단일 CKPAIR, 운영 단순 | 누출 시 범위가 넓음, 중첩 하위 도메인 커버 안 됨 | SNI별 분리 불필요 |
| SAN 다중 이름 | 정확한 범위 통제, 소수 도메인에 효율적 | 도메인 변경 시 인증서 재발급 | 하나의 CKPAIR가 여러 SNI 매칭 |
| SNI당 독립 인증서 | 격리 최고, 폐기 범위 최소 | CKPAIR 수 증가, 메모리 증가 | SNI마다 BAPI로 개별 CKPAIR 등록 후 애플리케이션이 맵 유지 |
| Let's Encrypt 자동 갱신 | 무료, 90일 주기로 기본 설계상 안전 | 인증서 교체 자동화가 필수 | 외부 certbot → VPP API로 롤오버 |
HTTP 애플리케이션 게이트웨이
앞 절들이 다룬 TCP/TLS 종단 프록시와 SSL Inspection은 모두 L4~L5 수준에서 끝납니다. 그러나 실전 데이터 센터에서 프록시가 처리해야 할 가장 흔한 작업은 L7 라우팅입니다. 호스트 헤더로 가상 호스트를 분기하고, URL 경로로 마이크로서비스를 골라 보내고, 헤더에 인증 토큰을 주입하고, 응답을 변조해 CORS를 추가합니다. 이 절은 FD.io VPP의 builtin HTTP 스택을 활용해 이런 게이트웨이 패턴을 구현하는 방법을 정리합니다.
게이트웨이 아키텍처 — 이중 세션과 라우팅 결정점
L7 게이트웨이는 본질적으로 두 개의 독립 세션을 묶어 둔 형태입니다. 클라이언트→프록시 세션에서 HTTP 요청을 완전히 파싱한 다음에야 어느 업스트림으로 보낼지 결정할 수 있고, 그 시점에 프록시→업스트림 세션이 새로 열립니다. TCP 프록시(앞 절)와 다른 점은 요청 라인과 헤더가 모두 도착하기 전까지 업스트림 연결을 보류한다는 것입니다.
라우팅 규칙 — Host/Path/Header 매칭
실전에서 가장 흔한 세 가지 매칭 차원과 VPP 빌딩 블록 매핑입니다.
| 매칭 차원 | 예시 | VPP 구현 포인트 |
|---|---|---|
| Host 헤더 | api.example.com → upstream-Aweb.example.com → upstream-B | http_msg → 헤더 룩업, hash table |
| 경로 prefix | /api/v1/* → upstream-A/static/* → upstream-B | radix trie 또는 mtrie 재사용 |
| 메서드 | GET은 캐시,POST는 직접 전달 | http_msg->method 분기 |
| 임의 헤더 | Authorization 존재 여부X-Tenant 값별 분기 | linear scan (헤더 수 적음) |
경로 매칭에 mtrie를 재사용하는 것은 VPP다운 선택입니다. IP 라우팅 테이블(Routing Table)에 쓰이는 multi-bit trie를 8비트씩 끊어 문자열 prefix lookup에 적용하면, 1000개 라우트 기준 100ns 미만으로 룩업이 끝납니다.
최소 게이트웨이 핸들러 — RX 콜백 안에서 라우팅
/* 라우팅 테이블(Routing Table): prefix → upstream URI */
typedef struct {
u8 *prefix; /* "/api/v1/" */
u8 *upstream_uri; /* "tcp://10.0.0.10/8080" */
} l7_route_t;
static l7_route_t *route_table; /* 부팅 시 채움 */
/* 라우팅 결정 */
static l7_route_t *
l7gw_route_lookup (http_msg_t *msg)
{
l7_route_t *r;
vec_foreach (r, route_table)
if (vec_len (msg->path) >= vec_len (r->prefix) &&
memcmp (msg->path, r->prefix, vec_len (r->prefix)) == 0)
return r;
return 0; /* 404 */
}
/* RX 콜백: 헤더 다 도착하면 업스트림 연결 */
static int
l7gw_rx_callback (session_t *cs)
{
l7gw_session_t *gs = l7gw_session_get (cs->opaque);
http_msg_t msg;
if (http1_parse_full (cs->rx_fifo, &msg) == HTTP_PARSE_NEED_MORE)
return 0; /* 다음 RX 이벤트 대기 */
l7_route_t *r = l7gw_route_lookup (&msg);
if (!r)
return l7gw_send_status (cs, 404, "Not Found");
/* 헤더 변조: X-Forwarded-For 추가 */
http_msg_header_append (&msg, "X-Forwarded-For",
format (0, "%U", format_ip4_address,
&cs->remote_ip4));
http_msg_header_append (&msg, "X-Forwarded-Proto", "https");
/* 업스트림 connect (논블로킹) */
vnet_connect_args_t a = {
.uri = (char *) r->upstream_uri,
.api_context = gs - l7gw_session_pool,
.app_index = l7gw_app_index,
};
if (vnet_connect (&a) < 0)
return l7gw_send_status (cs, 502, "Bad Gateway");
/* 직렬화한 변조 메시지를 임시 버퍼에 저장 → connected_callback에서 송신 */
gs->pending_request = http1_serialize (&msg);
gs->client_handle = session_handle (cs);
return 0;
}
/* 업스트림 연결 완료 콜백 */
static int
l7gw_connected_callback (u32 app_idx, u32 api_ctx, session_t *us, session_error_t err)
{
l7gw_session_t *gs = pool_elt_at_index (l7gw_session_pool, api_ctx);
if (err) {
session_t *cs = session_get_from_handle (gs->client_handle);
return l7gw_send_status (cs, 502, "Upstream connect failed");
}
/* 변조한 요청을 업스트림 tx_fifo에 enqueue */
svm_fifo_enqueue (us->tx_fifo, vec_len (gs->pending_request),
gs->pending_request);
session_send_io_evt_to_thread (us->tx_fifo, SESSION_IO_EVT_TX);
/* 두 세션 양방향 페어링: 이후 응답은 ① 절의 TCP 프록시처럼 그대로 릴레이 */
gs->upstream_handle = session_handle (us);
return 0;
}
응답 본문은 일반 TCP 프록시처럼 그대로 흘려보내도 되지만(streaming 모드), Set-Cookie 변조나 응답 헤더 추가가 필요하면 응답 RX 콜백에서도 같은 방식으로 한 번 더 파싱·변조해야 합니다. 이 비용은 작지 않으며, 변조 빈도가 낮다면 헤더 부분만 파싱하고 본문은 통과시키는 partial parse 모드가 효율적입니다.
API 게이트웨이 패턴 — 인증·레이트 리밋·캐싱
| 패턴 | VPP 구현 방법 | 주의점 |
|---|---|---|
| JWT 검증 | RX 콜백에서 Authorization 헤더 추출 → libjwt 또는 자체 HS256 검증 | 서명 검증(Signature Verification)은 워커 스레드 블로킹 회피 위해 캐시 필수 |
| 레이트 리밋 | 토큰 버킷 상태를 클라이언트 IP 키 hash table에 저장 | 분산 환경에서는 워커별 카운터 합산 필요 |
| 응답 캐싱 | 요청 hash → 응답 본문을 svm 영역에 보관 | HTTP/1.1의 Cache-Control·Vary 의미 직접 구현 |
| 회로 차단(circuit breaker) | 업스트림별 실패율 카운터 + 시간창 | 임계 도달 시 즉시 503 반환, half-open 복구 필요 |
| 요청 미러링 | vnet_connect로 두 업스트림 동시 연결, 응답은 한쪽만 사용 | FIFO 복제 비용 고려 |
CLI 예시 — 간단한 가상 호스트 분기
# 1) 게이트웨이 플러그인 활성화 (가상의 CLI 가정)
vppctl http gateway enable uri tls://0.0.0.0/443 \
cert /etc/vpp/cert.pem key /etc/vpp/key.pem
# 2) 라우트 추가
vppctl http gateway route add host api.example.com path /v1/ \
upstream tcp://10.0.0.10/8080
vppctl http gateway route add host api.example.com path /v2/ \
upstream tcp://10.0.0.20/8080
vppctl http gateway route add host www.example.com path / \
upstream tcp://10.0.0.30/80
# 3) 헤더 변조 규칙
vppctl http gateway rewrite add request \
header-set X-Forwarded-Proto https
# 4) 동작 확인
curl -k -H "Host: api.example.com" https://192.0.2.10/v1/version
curl -k -H "Host: api.example.com" https://192.0.2.10/v2/version
# 5) 라우팅 통계
vppctl show http gateway routes
vppctl show http gateway counters
위 CLI는 25.10 시점에는 일부가 패치 단계입니다. 실제 환경에서는 동일 동작을 VCL(libvcl)로 작성한 사용자 공간 게이트웨이 데몬이나, 위에서 보인 builtin RX 콜백 기반 플러그인으로 구현해야 합니다. 두 방식의 트레이드오프는 다음과 같습니다.
| 구현 방식 | 장점 | 단점 |
|---|---|---|
| builtin 플러그인 (RX 콜백) | 워커 스레드 안에서 zero-copy, 최고 성능 | VPP 빌드 필요, 충돌 시 데이터플레인 영향 |
| VCL 사용자 공간 데몬 | 독립 프로세스, 디버깅·재배포 용이 | SVM FIFO 통한 IPC 비용 (수십 ns) |
Common Pitfalls
- 요청 헤더가 한 번에 안 도착함 — 작은 MTU·느린 클라이언트에서는 RX 콜백이 헤더 절반만 받은 상태로 호출됩니다. 위 코드의
HTTP_PARSE_NEED_MORE분기를 빼먹으면 의도치 않은 400 에러가 폭발합니다. - 요청 본문(POST)을 잊음 —
Content-Length또는Transfer-Encoding: chunked가 있는 요청은 헤더 파싱이 끝나도 본문 수신이 남습니다. 본문을 받기 전에 업스트림에 헤더만 보내면 업스트림이 무한 대기에 빠집니다. - Host 헤더 신뢰 — 라우팅에 Host 헤더를 그대로 쓰면 외부 클라이언트가 가짜 Host를 보내 내부 서비스로 진입할 수 있습니다. Host 화이트리스트를 강제하거나 SNI와 교차 검증해야 합니다.
- Connection 헤더 hop-by-hop 처리 — RFC 9110의 hop-by-hop 헤더(
Connection,Keep-Alive,TE,Trailer,Transfer-Encoding,Upgrade)는 프록시 경계에서 제거해야 합니다. 그대로 흘리면 업스트림이 잘못된 keep-alive 설정을 따릅니다. - 대형 응답 backpressure — 업스트림이 빠르고 클라이언트가 느리면 client tx_fifo가 차서 enqueue가 실패합니다. 이때 업스트림 RX를 일시 정지(
session_app_rx_evt_pause)하지 않으면 메모리가 폭증합니다.
HTTP 캐싱 — RFC 9111과 VPP 구현 패턴
L7 게이트웨이가 다음으로 다루어야 할 가치는 캐싱입니다. 같은 응답을 원본 서버에 다시 묻지 않고 프록시 안에서 돌려주면, 업스트림 부하·평균 지연·외부 대역폭이 동시에 줄어듭니다. RFC 9111은 HTTP 캐싱의 의미론을 정의하며, 어떤 응답이 캐시 가능한지·언제까지 신선한지·언제 재검증해야 하는지를 헤더로 표현합니다. 이 절은 RFC 9111의 핵심 규칙을 정리하고, FD.io VPP의 빌딩 블록(svm 메모리·svm_fifo·hash table)으로 LRU 캐시를 어떻게 구현하는지 보입니다.
캐시 가능성 결정 트리
| 조건 | 결과 | RFC 9111 § 참조 |
|---|---|---|
| 요청 메서드가 GET/HEAD가 아님 | 캐시 불가 | §3 |
| 응답 상태 코드가 200/203/204/206/300/301/308/404/405/410/414/501 | 캐시 가능 (기본) | §3 |
Cache-Control: no-store | 저장 금지 | §5.2.2.5 |
Cache-Control: private + 공유 캐시 | 저장 금지 | §5.2.2.7 |
Authorization 헤더 + 공유 캐시 | 기본 금지 (예외 있음) | §3.5 |
Cache-Control: public 또는 명시적 max-age | 강제 캐시 가능 | §5.2.2.9 |
신선도(freshness)는 다음 우선순위로 계산합니다.
Cache-Control: s-maxage=N(공유 캐시 전용)Cache-Control: max-age=NExpires헤더 -Date헤더- 휴리스틱(
Last-Modified의 10% 등) — 신중히 사용
캐시 결정 흐름
캐시 키 — Vary 헤더와 변형 응답
같은 URL이라도 클라이언트 헤더에 따라 다른 응답이 돌아올 수 있습니다(언어·인코딩·디바이스). 원본 서버는 이를 Vary 응답 헤더로 알립니다.
HTTP/1.1 200 OK
Content-Type: text/html
Cache-Control: public, max-age=600
Vary: Accept-Encoding, Accept-Language
위 응답을 받으면 캐시는 키를 다음처럼 만들어야 합니다.
cache_key = SHA1(
method || host || path
|| "Accept-Encoding=" || req.headers["Accept-Encoding"]
|| "Accept-Language=" || req.headers["Accept-Language"]
)
Vary: *는 사실상 캐시 불가를 뜻합니다. Vary 누락은 잘못된 응답을 다른 클라이언트에 돌려주는 가장 흔한 캐시 오염 원인입니다.
VPP LRU 캐시 구현
/* 캐시 엔트리 — 본문은 svm 메모리에 보관 */
typedef struct {
u8 *key; /* SHA1(20byte) */
u8 *headers; /* 응답 헤더 직렬화 */
u8 *body; /* 응답 본문 */
u32 body_len;
f64 inserted_at;
f64 max_age; /* 초 */
u8 *etag; /* 약/강 ETag, 재검증용 */
u8 *last_modified;
u32 lru_prev, lru_next;
} cache_entry_t;
typedef struct {
cache_entry_t *pool; /* pool_get 기반 */
uword *by_key; /* hash_create_mem */
u32 lru_head, lru_tail;
u64 bytes_used, bytes_max;
} l7_cache_t;
/* 조회 + freshness 검사 */
static cache_entry_t *
cache_lookup_fresh (l7_cache_t *c, u8 *key, f64 now)
{
uword *p = hash_get_mem (c->by_key, key);
if (!p) return 0;
cache_entry_t *e = pool_elt_at_index (c->pool, p[0]);
if (now - e->inserted_at > e->max_age) {
/* stale: 재검증 후보로 반환만 함, 즉시 삭제하지 않음 */
return e;
}
cache_lru_touch (c, e);
return e;
}
/* 응답 저장 — RFC 9111 캐시 가능 검사 통과한 경우만 호출 */
static int
cache_store (l7_cache_t *c, u8 *key, http_msg_t *resp, f64 now)
{
cache_entry_t *e;
pool_get_zero (c->pool, e);
e->key = vec_dup (key);
e->headers = http_msg_serialize_headers (resp);
e->body = vec_dup (resp->body);
e->body_len = vec_len (resp->body);
e->inserted_at = now;
e->max_age = http_parse_max_age (resp);
e->etag = vec_dup (http_msg_header_get (resp, "ETag"));
e->last_modified = vec_dup (http_msg_header_get (resp, "Last-Modified"));
hash_set_mem (c->by_key, e->key, e - c->pool);
c->bytes_used += e->body_len + vec_len (e->headers);
cache_lru_push_front (c, e);
while (c->bytes_used > c->bytes_max)
cache_evict_tail (c);
return 0;
}
/* 재검증 — 304 처리 */
static int
cache_revalidate (session_t *cs, cache_entry_t *e)
{
http_msg_t req;
http1_build_revalidate (&req, e); /* If-None-Match: e->etag */
return upstream_send_and_handle (cs, &req,
/* on_response */ ^(http_msg_t *resp) {
if (resp->status == 304) {
e->inserted_at = unix_time_now ();
return reply_from_cache (cs, e);
}
cache_store (l7_cache, e->key, resp, unix_time_now ());
return reply_passthrough (cs, resp);
});
}
pool_* 매크로(Macro)와 svm 메모리는 워커별로 격리되거나(thread-local) 공유 영역에 둘 수 있어 NUMA 친화적입니다. 단순 구현은 워커별 캐시(중복 가능)로 시작하고, 적중률이 부족할 때만 공유 캐시로 승격하는 편이 안전합니다.
stale-while-revalidate와 stale-if-error
RFC 5861이 정의하고 RFC 9111이 통합한 두 확장은 캐시의 가용성을 크게 끌어올립니다.
| 지시어 | 의미 | 효과 |
|---|---|---|
stale-while-revalidate=N | 만료 후 N초 동안은 stale 응답을 즉시 돌려주고 백그라운드로 재검증 | 사용자 지연 0, 평균 지연 단축 |
stale-if-error=N | 업스트림 실패 시 만료 후 N초 동안 stale을 비상용으로 송출 | 업스트림 장애 시 가용성 보장 |
구현 측면에서는 위 cache_lookup_fresh가 stale 엔트리도 반환하도록 하고, 호출자가 ① 즉시 응답 ② 비동기 재검증 작업을 같은 워커의 이벤트 큐에 등록 ③ 업스트림 실패 시 stale-if-error 윈도 안인지 확인하는 분기를 두면 됩니다.
CLI 예시
# 1) 캐시 활성화
vppctl http gateway cache enable size 256m \
default-ttl 60 stale-while-revalidate 30 stale-if-error 86400
# 2) 캐시 통계
vppctl show http gateway cache
# entries: 12453 bytes: 198M / 256M
# hits: 8.2M misses: 1.4M revalidations: 412K
# hit ratio: 85.4%
# 3) 특정 키 무효화
vppctl http gateway cache purge host api.example.com path /v1/users
# 4) 전체 무효화 (배포 후)
vppctl http gateway cache flush
Common Pitfalls
- Vary 무시 — gzip/br 압축 응답을 비압축 클라이언트에 잘못 전달하는 사고가 가장 흔합니다.
Accept-Encoding은 거의 항상 Vary에 포함됩니다. - Authorization 헤더 + 공유 캐시 — 사용자 A의 인증된 응답이 사용자 B에게 노출되는 심각한 정보 누출이 됩니다. 기본은 캐시 금지이며,
Cache-Control: public등 명시적 허용이 있을 때만 캐시해야 합니다. - POST 응답 캐싱 — 일부 RFC 분기에서 POST 응답도 캐시 가능하지만, 같은 POST를 다음에 또 보낼 때 캐시를 매칭하기가 어렵고 위험합니다. 기본은 캐시 금지로 두는 편이 안전합니다.
- 휴리스틱 freshness 남용 — 명시적 만료 헤더 없이
Last-Modified의 10%로 freshness를 추정하는 휴리스틱은 정적 자산에는 무난하나 API에는 위험합니다. API 응답에는 캐시를 끄거나 명시적 max-age를 강제해야 합니다. - 캐시 폭주 vs thundering herd — 인기 엔트리가 동시에 만료되면 같은 요청 수천 개가 동시에 업스트림으로 향합니다. request coalescing(같은 키 inflight 요청을 한 번만 업스트림에 전달)이 필수입니다.
참고자료
VPP 프록시와 SSL Inspection은 구현·표준·법적 프레임워크가 함께 엮여 있어 한 곳에서 전체 그림을 잡기 어렵습니다. 아래는 구현(VPP/TPROXY), 프로토콜 표준(HTTP/TLS/QUIC), 비교 가능한 오픈소스 프록시, 법·컴플라이언스 문서를 구분해 정리한 1차 자료 목록입니다.
VPP 프록시·세션 관련 소스
- src/plugins/http_static/ — 정적 HTTP 서버 플러그인 (세션 API 레퍼런스)
- src/plugins/http/ — HTTP/1.1·HTTP/2·CONNECT 구현
- src/vnet/session/ — 세션 레이어와
app_add_cert_key_pairAPI - src/plugins/tlsopenssl/ — TLS 엔진 플러그인
Linux 커널 투명 프록시(TPROXY) 경로
- Linux Kernel — TPROXY 문서
net/netfilter/xt_TPROXY.c— iptables/nftables TPROXY 타깃net/ipv4/ip_input.c,net/core/sock.c—IP_TRANSPARENT/SO_ORIGINAL_DST경로- HAProxy 공식 사이트 —
transparent/usesrc clientip옵션
HTTP / 프록시 관련 RFC
- RFC 9110 — HTTP Semantics
- RFC 9111 — HTTP Caching
- RFC 9112 — HTTP/1.1
- RFC 9113 — HTTP/2
- RFC 9114 — HTTP/3
- RFC 7230 §5.7 — Forwarded/Via
- RFC 7239 — Forwarded HTTP 헤더
- RFC 8441 — HTTP/2 Extended CONNECT (WebSocket over HTTP/2)
- RFC 9298 — CONNECT-UDP
- draft-ietf-tls-esni — Encrypted ClientHello (ECH)
- RFC 7858 — DNS over TLS, RFC 8484 — DoH (우회 경로)
SSL Inspection · L7 가시성 참고
- mitmproxy 공식 문서 — 중간자 구현 관점의 전형
- Squid — SSL Bump — peek & splice, bump 패턴
- Suricata, Snort — IDS/IPS 연계
- Zeek — 프로토콜 로그 중심 네트워크 모니터
- RFC 8404 — Effects of Pervasive Encryption — 운영자 관점 배경
관련 문서
이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.
오픈소스 코드 인용 고지
| 프로젝트 | 저작권자 | 라이선스 | 공식 저장소 |
|---|---|---|---|
| VPP (Vector Packet Processing) | FD.io contributors | Apache License 2.0 | github.com/FDio/vpp |
| DPDK (Data Plane Development Kit) | DPDK contributors | BSD 3-Clause License | github.com/DPDK/dpdk |
코드 블록 내 주석에 원본 파일 경로가 표기된 부분은 해당 프로젝트 소스 코드에서 발췌·간략화한 것입니다. 전체 소스 코드는 위 공식 저장소에서 확인하시기 바랍니다.