QUIC · HTTP/3

FD.io VPP의 QUIC 프로토콜 구현과 HTTP/3 실전 운용을 심화 정리한 문서입니다. quicly 통합, QUIC engine API, ALPN 협상, 0-RTT 재개, VPP 25.10 제약사항까지를 한 곳에 모았으며, 본 문서는 호스트 스택 개요에서 QUIC/HTTP3 부분만을 별도로 분리한 심화 페이지(Page)입니다.

이 페이지에서 다루는 주요 h2 섹션은 다음과 같습니다. 첫째 VPP QUIC 프로토콜은 quicly 라이브러리 기반 통합 구조, 25.06 이후 도입된 QUIC engine API, 스트림·패킷(Packet) 처리 흐름, 운영 디버깅(Debugging) 팁을 다룹니다. 둘째 HTTP/3 실전 — ALPN 협상, 0-RTT, VPP 25.10 제약은 h2/h3 ALPN 협상 전략, 0-RTT 재개와 재전송 방어, 현재 VPP HTTP/3 어댑터의 실전 제약과 운영 고려사항을 정리합니다.

기초 개념: 세션 레이어, TCP 호스트 스택, TLS 기본 흐름은 호스트 스택 개요에서 먼저 확인해 주시기 바랍니다. TLS 심화 내용은 TLS를 함께 참고하시면 됩니다.

VPP QUIC 프로토콜

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

🔄 25.02 → 26.02 변경 타임라인:
  • 25.06QUIC engine API 공식 출시. 전에는 quicly 직결 구조였지만 플러그형 엔진 인터페이스가 생기면서 다른 QUIC 구현도 끼울 수 있게 됐습니다.
  • 25.10 — HTTP/2 쪽에서 extended CONNECTUDP 터널링 over HTTP/2가 들어왔습니다. HTTP/3 중계 경로가 정비되기 시작한 시점입니다.
  • 26.02HTTP/3 framing layer + core skeleton + H3 client sideQPACK 인코딩/디코딩이 릴리스 노트에 공식 항목으로 등장했습니다. 본 절의 기존 "HTTP/3 over VPP QUIC" 내용은 이 시점에 "실험 → 사용 가능한 파일럿" 상태로 올라왔습니다.
25.02 환경에서 이 절의 HTTP/3 관련 예시(클라이언트 API, QPACK 흐름 등)를 그대로 적용하실 수 없습니다. QUIC 종단만 가능하고 HTTP/3 애플리케이션 레이어는 자체 구현이 필요합니다.

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 내부 패킷 처리 흐름

VPP의 QUIC 플러그인은 quicly 라이브러리를 핵심 엔진으로 사용합니다. quicly는 QUIC 프로토콜의 패킷 파싱, 암호화(TLS 1.3 via picotls), 흐름 제어(Flow Control), 혼잡 제어(Congestion Control)를 모두 처리하는 C 라이브러리입니다. VPP는 quicly를 그래프 노드 기반 파이프라인(Pipeline)에 통합하여 고성능 QUIC 처리를 구현합니다.

QUIC 패킷 수신 경로

QUIC 패킷이 네트워크에서 도착하여 애플리케이션 데이터로 전달되기까지의 경로는 다음과 같습니다:

/* QUIC 수신 경로: UDP input → quicly → 애플리케이션 */

/* 1. VPP UDP 입력 노드가 QUIC 포트(443) 패킷을 QUIC 앱에 전달 */
/*    udp-input → session-queue → quic_app_rx_callback() 호출 */

static int
quic_app_rx_callback (session_t *udp_session)
{
  quic_ctx_t *ctx;
  svm_fifo_t *rx_fifo;
  u8 *data;
  u32 data_len;
  int rv;

  /* UDP RX FIFO에서 QUIC 패킷 읽기 */
  rx_fifo = udp_session->rx_fifo;
  data_len = svm_fifo_max_dequeue (rx_fifo);
  vec_validate (data, data_len - 1);
  svm_fifo_dequeue (rx_fifo, data_len, data);

  /* Connection ID로 QUIC 컨텍스트 검색 */
  ctx = quic_find_ctx_by_conn_id (data);

  /* 2. quicly_receive()로 패킷 처리 (복호화 + 프레임 파싱) */
  rv = quicly_receive (ctx->conn, NULL,
                       &udp_session->transport.rmt_ip,
                       data, data_len);

  if (rv != 0)
    {
      /* 패킷 처리 오류: 잘못된 패킷 또는 복호화 실패 */
      quic_proto_error (ctx, rv);
      return -1;
    }

  /* 3. 스트림 데이터가 있으면 앱 세션의 RX FIFO에 인큐 */
  quic_check_streams_for_data (ctx);

  /* 4. ACK 등 응답 패킷이 필요하면 송신 스케줄링 */
  quic_send_packets (ctx);

  return 0;
}
코드 분석: QUIC 패킷 수신 경로

quic_app_rx_callback()은 VPP 세션 레이어가 UDP 패킷을 수신할 때 호출되는 콜백(Callback)입니다. 핵심 동작은 quicly_receive() 호출입니다. quicly는 패킷 헤더를 파싱하여 Connection ID를 추출하고, AEAD(Authenticated Encryption with Associated Data)로 페이로드(Payload)를 복호화(Decryption)한 후, 내부 프레임(STREAM, ACK, MAX_DATA 등)을 처리합니다. STREAM 프레임에 포함된 애플리케이션 데이터는 해당 스트림의 수신 버퍼(Buffer)에 저장되며, 이후 quic_check_streams_for_data()가 이를 앱 세션의 RX FIFO에 복사합니다.

QUIC 패킷 송신 경로

애플리케이션 데이터가 QUIC 패킷으로 변환되어 네트워크로 전송되는 경로입니다:

/* QUIC 송신 경로: 앱 데이터 → quicly → UDP output */
static int
quic_send_packets (quic_ctx_t *ctx)
{
  quicly_address_t dest, src;
  struct iovec packets[QUIC_MAX_PACKET_BATCH];
  size_t num_packets = QUIC_MAX_PACKET_BATCH;
  int rv;

  /* 1. quicly_send()로 전송할 패킷 생성 (암호화 포함) */
  rv = quicly_send (ctx->conn, &dest, &src,
                    packets, &num_packets,
                    ctx->send_buf, sizeof(ctx->send_buf));

  if (rv != 0 || num_packets == 0)
    return 0;

  /* 2. 생성된 패킷을 UDP TX FIFO에 인큐 */
  for (size_t i = 0; i < num_packets; i++)
    {
      svm_fifo_enqueue (ctx->udp_session->tx_fifo,
                        packets[i].iov_len,
                        packets[i].iov_base);
    }

  /* 3. UDP 세션에 TX 이벤트 발생 → VPP가 패킷 전송 */
  session_send_io_evt_to_thread (ctx->udp_session->tx_fifo,
                                 SESSION_IO_EVT_TX);
  return num_packets;
}
코드 분석: QUIC 패킷 송신 경로

quic_send_packets()quicly_send()를 호출하여 전송 대기 중인 모든 데이터(스트림 데이터, ACK, 흐름 제어 프레임 등)를 QUIC 패킷으로 만듭니다. quicly는 내부적으로 혼잡 윈도우를 확인하고, 패킷 번호를 부여하며, AEAD로 암호화합니다. 생성된 패킷은 struct iovec 배열로 반환되며, VPP는 이를 UDP TX FIFO에 일괄 인큐(Enqueue)합니다. 배치 처리(QUIC_MAX_PACKET_BATCH)를 통해 시스템 콜(System Call) 오버헤드를 최소화합니다.

VPP QUIC 패킷 처리 파이프라인 수신 경로 (RX) DPDK/AF_XDP ip4-input udp-input session-queue quic_app_rx_callback quicly_receive() App RX FIFO 송신 경로 (TX) App TX FIFO quic_send_packets quicly_send() UDP TX FIFO udp-output ip4-output DPDK TX quicly 내부 처리 상세 패킷 파싱 헤더 · CID 추출 AEAD 복호화 AES-128-GCM · ChaCha20 프레임 처리 STREAM · ACK · FC 스트림 버퍼 순서 재조립 · 전달 FC = Flow Control (MAX_DATA, MAX_STREAM_DATA) CID = Connection ID | AEAD = Authenticated Encryption with Associated Data

QUIC Connection ID 관리

QUIC Connection ID(CID)는 연결을 5-튜플 대신 불투명한 식별자(Opaque Identifier)로 추상화합니다. 서버는 언제든지 새 CID를 NEW_CONNECTION_ID 프레임으로 발급하고, 클라이언트는 오래된 CID를 RETIRE_CONNECTION_ID 프레임으로 반납합니다. 올바른 CID 순환(Rotation)이 이루어지지 않으면 연결 마이그레이션 실패, 경쟁 조건(Race Condition), CID 테이블 고갈로 이어집니다.

CID 상태 머신

각 CID는 아래 세 가지 상태를 순서대로 거칩니다. VPP quicly 내부에서 quicly_cid_tstate 필드로 추적됩니다.

  ┌─────────┐   NEW_CONNECTION_ID 수신/발급   ┌──────────┐
  │  Active  │ ─────────────────────────────→ │ (대기 예비)│
  └─────────┘                                 └──────────┘
       │                                            │
       │  RETIRE_CONNECTION_ID 송신                │  교체 경로 확정
       ▼                                            ▼
  ┌──────────┐   ACK 수신 또는 타임아웃        ┌──────────┐
  │ Retiring │ ─────────────────────────────→ │ Retired  │
  └──────────┘                                └──────────┘

  Active   : 현재 패킷 송수신에 사용 중인 CID
  Retiring : RETIRE_CONNECTION_ID 프레임을 보냈으나
             상대방 ACK를 아직 받지 못한 과도 상태
  Retired  : 더 이상 유효하지 않음 — 해시 테이블에서 제거

RETIRE_CONNECTION_ID 프레임 처리 순서

RFC 9000 §5.1.2에 따른 처리 절차를 VPP quicly 코드 흐름과 함께 정리합니다.

  1. 프레임 수신quicly_receive() 내부에서 RETIRE_CONNECTION_ID 프레임을 파싱하고 대상 Sequence Number를 추출합니다.
  2. CID 테이블 룩업quic_find_ctx_by_cid_seq()로 해당 Sequence Number를 가진 CID를 CID 해시 맵에서 찾습니다. 없으면 프레임을 조용히 무시합니다(RFC 허용).
  3. Retiring 상태로 전환 — 찾은 CID의 stateQUIC_CID_RETIRING으로 변경합니다. 이 CID로 도착하는 패킷은 아직 수락합니다(전환 중 손실 방지).
  4. NEW_CONNECTION_ID 재발급 — 예비(Unused) CID 수가 active_connection_id_limit의 절반 아래로 떨어지면, quicly_emit_new_connection_id()를 호출해 신규 CID를 생성하고 상대방에게 전송합니다.
  5. Retired 전환 및 정리 — 재발급 ACK를 받은 후(또는 PTO 타임아웃 후) Retiring CID를 QUIC_CID_RETIRED로 전환하고 해시 맵에서 삭제합니다.

멀티패스 시나리오: CID 경쟁 조건

다음 두 시나리오는 CID 관리 버그가 가장 쉽게 드러나는 경우입니다. 이미 진행 중인 마이그레이션의 경로 검증(PATH_CHALLENGE/RESPONSE) 및 증폭 방어(Amplification Defense) 동작은 Connection Migration 운영 절을 참고하시기 바랍니다.

모바일 핸드오프 중 CID 고갈
Wi-Fi→LTE 전환 직후 PATH_CHALLENGE를 보내면서 동시에 이전 경로에서 RETIRE_CONNECTION_ID를 교환하면, 잠시 동안 두 경로가 서로 다른 CID를 요구합니다. active_connection_id_limit이 2이면 예비 CID가 하나뿐이므로 재발급 전에 두 경로 중 하나가 CID를 고갈시킬 수 있습니다. 권장값은 4 이상입니다.
NAT 리바인딩 경쟁 조건
동일 클라이언트에서 거의 동시에 두 UDP 패킷이 서로 다른 외부 포트로 도착하는 경우(NAT 재매핑 과도 구간), VPP는 같은 CID를 두 quic_ctx_t에 매핑하려 시도합니다. quicly는 CID 해시 맵에 atomic 삽입을 사용하므로 후착 패킷은 QUICLY_ERROR_STATELESS_RETRY를 반환받고 연결을 재설정합니다. 결과적으로 짧은 연결 중단(일반적으로 < 1 RTT)이 발생할 수 있습니다.

충돌 감지 및 NEW_CONNECTION_ID 재발급

  서버 CID 재발급 트리거 조건:

  1. 예비 CID < (active_connection_id_limit / 2)
     → NEW_CONNECTION_ID 프레임을 즉시 발급

  2. RETIRE_CONNECTION_ID 수신 → 재발급 (위 4단계)

  3. CID 시퀀스 번호 고갈(2^62 - 1 초과) → 연결 종료
     (실제로는 도달 불가)

  재발급 시 중복 방지:
  - CID는 quicly 내부의 CSPRNG로 생성 (16바이트 랜덤)
  - Stateless Reset Token과 함께 NEW_CONNECTION_ID에 포함
  - 테이블 삽입 전 기존 CID와 충돌 검사(해시 맵 lookup)
  - 충돌 시(극히 드물지만) 재생성 후 재시도
보안 주의: CID는 예측 불가능해야 합니다. 순차 증가 CID(예: 0x00, 0x01, 0x02…)를 발급하면 공격자가 다른 연결의 CID를 추측하여 패킷 인젝션을 시도할 수 있습니다. VPP quicly는 CSPRNG 기반 CID를 사용하므로 이 문제를 방지하지만, 외부 QUIC 라이브러리를 통합할 때는 반드시 랜덤성을 확인해야 합니다.

CLI: show quic 출력 해석

# 연결별 CID 상태 요약
vpp# show quic connections verbose

# 출력 예시 (주요 필드 설명)
# conn 0x7f1a2b3c  state=ESTABLISHED  cid_count=4  cid_retire_pending=1
#   active_cid_seq=3  peer_active_cid_limit=4
#   migration_count=2  active_path=10.0.0.50:52341→203.0.113.1:443
#
# cid_count          : 현재 발급된 CID 총 수 (Active + Retiring 포함)
# cid_retire_pending : Retiring 상태이지만 아직 ACK 받지 못한 CID 수
# active_cid_seq     : 현재 송신에 사용 중인 CID의 시퀀스 번호
# peer_active_cid_limit : 상대방이 허용하는 최대 활성 CID 수
# migration_count    : 경로 전환 횟수 (0이면 마이그레이션 없음)
# active_path        : 현재 패킷이 오가는 5-튜플

# CID 고갈 위험 확인: cid_count가 peer_active_cid_limit에 근접하면 주의
vpp# show quic connections verbose | grep cid_count

# 에러 카운터 — CID 관련 드롭 확인
vpp# show errors
# quic-unknown-cid      : CID 테이블에 없는 패킷 수신 → 정상 마이그레이션 또는 공격
# quic-cid-retire-fail  : RETIRE_CONNECTION_ID 처리 실패

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 스트림은 QUIC 연결 내부에서 독립적인 데이터 채널 역할을 합니다. VPP에서 스트림의 생성부터 종료까지의 전체 생명주기를 소스 코드 수준에서 분석합니다.

스트림 생성 (quic_connect_stream)

클라이언트가 새 스트림을 열 때 quic_connect_stream()이 호출됩니다. 이 함수는 quicly 연결 위에 새 스트림 객체를 생성하고 VPP 세션과 연결합니다:

/* QUIC 스트림 연결 (클라이언트 → 서버) */
static int
quic_connect_stream (session_t *quic_session,
                      session_endpoint_cfg_t *sep)
{
  quic_ctx_t *qctx, *sctx;
  quicly_stream_t *stream;
  session_t *stream_session;
  int rv;

  /* 상위 QUIC 연결 컨텍스트 조회 */
  qctx = quic_ctx_get (quic_session->connection_index);

  /* 1. quicly에 양방향 스트림 생성 요청 */
  rv = quicly_open_stream (qctx->conn, &stream, 0 /* bidi */);
  if (rv)
    {
      /* MAX_STREAMS 한도 초과 시 실패 */
      return SESSION_E_REFUSED;
    }

  /* 2. VPP 스트림 컨텍스트 할당 */
  sctx = quic_ctx_alloc (qctx->c_thread_index);
  sctx->parent_ctx_id = qctx->ctx_id;
  sctx->quicly_stream = stream;
  sctx->conn_state = QUIC_CONN_STATE_STREAM_OPEN;

  /* 3. VPP 세션 생성 및 FIFO 할당 */
  stream_session = session_alloc (qctx->c_thread_index);
  stream_session->session_type = SESSION_TYPE_QUIC_STREAM;
  svm_fifo_alloc_pair (&stream_session->rx_fifo,
                       &stream_session->tx_fifo,
                       QUIC_FIFO_SIZE);

  /* 4. quicly 스트림 콜백 등록 (데이터 수신 시 호출) */
  stream->callbacks = &quic_stream_callbacks;
  stream->data = sctx;  /* VPP 컨텍스트 연결 */

  /* 5. 애플리케이션에 연결 완료 알림 */
  app_worker_connect_notify (stream_session);

  return 0;
}
코드 분석: 스트림 생성 흐름

quic_connect_stream()의 핵심은 quicly 레벨 스트림과 VPP 세션 레벨 스트림을 양방향으로 연결하는 것입니다. quicly_open_stream()은 QUIC 프로토콜의 스트림 ID를 할당하고(클라이언트 시작 양방향 = 0, 4, 8, ...), MAX_STREAMS 한도를 확인합니다. 이후 VPP 세션을 할당하고 RX/TX FIFO 쌍을 만들어 애플리케이션과의 데이터 교환 통로를 설정합니다. stream->callbacks에 등록된 콜백은 quicly가 해당 스트림에 데이터를 수신할 때 자동으로 호출됩니다.

스트림 데이터 송수신

/* 스트림 수신: quicly → 앱 RX FIFO */
static int
quic_stream_rx (quicly_stream_t *stream, size_t off,
                const void *src, size_t len)
{
  quic_ctx_t *sctx = (quic_ctx_t *) stream->data;
  session_t *stream_session;

  stream_session = session_get (sctx->session_index);

  /* quicly가 순서 재조립 완료한 데이터를 앱 FIFO에 인큐 */
  svm_fifo_enqueue_with_offset (stream_session->rx_fifo,
                                off, len, src);

  /* 앱에 RX 이벤트 알림 */
  session_send_io_evt_to_thread (stream_session->rx_fifo,
                                 SESSION_IO_EVT_RX);
  return 0;
}

/* 스트림 송신: 앱 TX FIFO → quicly */
static int
quic_stream_tx (session_t *stream_session)
{
  quic_ctx_t *sctx;
  svm_fifo_t *tx_fifo = stream_session->tx_fifo;
  u32 deq_len;
  u8 *data;

  sctx = quic_ctx_get (stream_session->connection_index);
  deq_len = svm_fifo_max_dequeue (tx_fifo);

  if (deq_len == 0)
    return 0;

  /* TX FIFO에서 데이터 읽기 */
  vec_validate (data, deq_len - 1);
  svm_fifo_dequeue (tx_fifo, deq_len, data);

  /* quicly 스트림 버퍼에 데이터 추가 */
  quicly_streambuf_egress_write (sctx->quicly_stream, data, deq_len);

  /* 패킷 송신 스케줄링 */
  quic_send_packets (sctx->parent_ctx);
  return 0;
}
코드 분석: 스트림 데이터 경로

수신 방향에서 quic_stream_rx()는 quicly의 스트림 콜백으로 등록된 함수입니다. quicly가 STREAM 프레임을 복호화하고 순서를 재조립한 후 이 콜백을 호출합니다. off 파라미터는 스트림 내 바이트 오프셋(Offset)으로, 비순차 도착 패킷도 올바른 위치에 저장됩니다. 송신 방향에서 quic_stream_tx()는 앱이 TX FIFO에 쓴 데이터를 quicly_streambuf_egress_write()로 quicly 송신 버퍼에 전달하고, quic_send_packets()로 실제 UDP 패킷 생성을 트리거합니다.

스트림 상태 전이

QUIC 스트림은 RFC 9000에 정의된 상태 머신을 따릅니다. VPP에서 각 상태 전이는 conn_state 필드로 추적됩니다:

QUIC 스트림 상태 전이 (양방향 스트림) IDLE 스트림 미생성 open_stream() OPEN 송신 + 수신 활성 FIN 송신 HALF_CLOSED (local) 수신만 가능 FIN 수신 HALF_CLOSED (remote) 송신만 가능 FIN 수신 FIN 송신 CLOSED 정상 종료 비정상 종료 경로 (RESET) RESET_SENT RESET_STREAM 프레임 전송 RESET_RECVD RESET_STREAM 프레임 수신 RESET_STREAM은 어떤 상태에서든 발생 가능하며, 스트림을 즉시 종료합니다 STOP_SENDING은 수신측이 더 이상 데이터를 원하지 않을 때 전송합니다

스트림 종료 및 오류 처리

/* 스트림 정상 종료 */
static void
quic_stream_close (quic_ctx_t *sctx)
{
  /* 송신 방향 FIN 전송 */
  quicly_streambuf_egress_shutdown (sctx->quicly_stream);
  sctx->conn_state = QUIC_CONN_STATE_STREAM_HALF_CLOSED;

  /* 수신 FIN도 받았으면 완전 종료 */
  if (quicly_recvstate_transfer_complete (&sctx->quicly_stream->recvstate))
    {
      sctx->conn_state = QUIC_CONN_STATE_STREAM_CLOSED;
      quic_stream_cleanup (sctx);
    }
}

/* 스트림 리셋 (비정상 종료) */
static void
quic_stream_reset (quic_ctx_t *sctx, u64 error_code)
{
  /* RESET_STREAM 프레임 전송 (즉시 종료) */
  quicly_reset_stream (sctx->quicly_stream, error_code);

  /* 세션 정리: FIFO 해제, 컨텍스트 반환 */
  session_transport_reset_notify (sctx->session_index);
  quic_stream_cleanup (sctx);
}
코드 분석: 스트림 종료

QUIC 스트림의 정상 종료는 양방향 FIN 교환으로 이루어집니다. quicly_streambuf_egress_shutdown()은 송신 방향을 닫아 STREAM 프레임에 FIN 비트를 설정합니다. 수신 방향도 FIN을 받으면 quicly_recvstate_transfer_complete()가 true를 반환하여 스트림이 완전히 종료됩니다. 비정상 종료 시 quicly_reset_stream()은 RESET_STREAM 프레임을 전송하여 상대방에게 오류 코드와 함께 스트림 즉시 종료를 통보합니다. 오류 코드는 H3_NO_ERROR(0), H3_REQUEST_CANCELLED(0x10c) 등 애플리케이션 프로토콜에 따라 달라집니다.

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

quicly 혼잡 제어 알고리즘 상세

quicly는 기본적으로 Reno 혼잡 제어를 사용하며, VPP 24.x부터 BBRv2 옵션이 추가되었습니다. 두 알고리즘의 동작 방식과 적합한 사용 시나리오가 다릅니다:

항목Reno (기본)BBRv2
동작 원리패킷 손실 기반 (AIMD)대역폭(Bandwidth)·RTT 측정 기반 모델
혼잡 감지패킷 손실 발생 시대역폭 포화·큐잉 지연(Latency) 감지
슬로우 스타트지수 증가 → 손실 시 절반 감소대역폭 측정 후 모델 기반 전환
버퍼블로트(Bufferbloat) 대응약함 (큐 가득 차야 감지)강함 (큐잉 지연 직접 측정)
고손실 네트워크성능 급감안정적 처리량(Throughput) 유지
CPU 오버헤드(Overhead)낮음중간 (RTT 샘플링·모델 갱신)
적합 시나리오LAN, 저지연 데이터센터WAN, 위성 링크, 모바일 네트워크
# startup.conf — 혼잡 제어 알고리즘 선택
quic {
  # 기본 Reno 혼잡 제어 (명시적 지정 불필요)
  cc-algorithm reno

  # BBRv2 혼잡 제어 활성화 (VPP 24.x+)
  # cc-algorithm bbr
}

# CLI에서 런타임 확인
vpp# show quic connections verbose
# 출력에 cc_algorithm, cwnd, ssthresh, bytes_in_flight 표시

패킷 손실 감지 메커니즘

quicly의 손실 감지는 RFC 9002에 기반하여 두 가지 메커니즘을 병행합니다:

ACK 기반 손실 감지
수신측이 보낸 ACK 프레임을 분석하여, ACK된 가장 큰 패킷 번호보다 kPacketThreshold(기본 3) 이상 작은 번호의 미확인 패킷을 손실로 판정합니다. QUIC은 패킷 번호가 절대 재사용되지 않으므로 TCP의 재전송 모호성(Retransmission Ambiguity) 문제가 없습니다.
PTO(Probe Timeout) 기반 감지
일정 시간 동안 ACK를 받지 못하면 프로브(Probe) 패킷을 전송하여 경로 상태를 확인합니다. PTO는 smoothed_rtt + max(4 * rttvar, 1ms) + max_ack_delay로 계산됩니다. TCP의 RTO(Retransmission Timeout)보다 정밀한 RTT 측정이 가능합니다.
/* quicly 손실 감지 핵심 로직 (의사코드) */
static void
quicly_loss_on_ack_received (quicly_loss_t *loss,
                              u64 largest_acked,
                              int64_t now)
{
  quicly_sent_packet_t *sent;

  /* ACK 기반 손실 판정 */
  vec_foreach (sent, loss->sent_packets)
    {
      if (sent->acked)
        continue;

      /* 패킷 번호 임계값 초과 확인 */
      if (largest_acked - sent->pn >= QUICLY_LOSS_PACKET_THRESHOLD)
        {
          quicly_loss_mark_lost (loss, sent);
          continue;
        }

      /* 시간 임계값 초과 확인 */
      int64_t time_threshold = loss->smoothed_rtt * 9 / 8;
      if (now - sent->sent_at > time_threshold)
        quicly_loss_mark_lost (loss, sent);
    }

  /* 혼잡 제어 업데이트 */
  loss->cc->on_loss_detected (loss->cc, loss->num_lost);
}
코드 분석: 손실 감지

QUIC의 손실 감지는 패킷 번호 임계값과 시간 임계값을 모두 사용합니다. 패킷 번호 기반 판정은 ACK된 가장 큰 번호와 3 이상 차이나는 미확인 패킷을 즉시 손실로 처리합니다. 시간 기반 판정은 smoothed_rtt * 9/8을 초과하여 미확인 상태인 패킷을 손실로 처리합니다. 손실이 감지되면 혼잡 제어의 on_loss_detected 콜백이 호출되어 Reno의 경우 cwnd를 절반으로 줄이고, BBR의 경우 대역폭 모델을 업데이트합니다.

Reno vs BBR 혼잡 윈도우 동작 비교 혼잡 윈도우 (cwnd) 시간 → 0 25% 50% 75% 100% 손실 손실 손실 손실 BDP Reno (톱니파: 손실 시 cwnd 절반 감소) BBR (안정적: BDP 기반 모델링)

QUIC 흐름 제어 네트워크별 튜닝

네트워크 유형에 따라 흐름 제어 파라미터를 다르게 설정해야 최적의 성능을 얻을 수 있습니다.

네트워크 유형RTT 범위권장 초기 창 크기MAX_STREAMSCC 알고리즘
데이터센터 (RTT<1ms)<1ms4MB1000Reno
LTE (RTT 30-80ms)30–80ms512KB200BBRv2
위성 (RTT 500ms+)500ms 이상8MB50BBRv2

MAX_STREAMS 튜닝 규칙

허용 스트림 수는 RTT와 처리 용량을 기반으로 계산합니다. 기본 공식은 100ms당 허용 스트림 수로 표현됩니다:

WINDOW_UPDATE 합산 최적화

스트림 수가 많을 때 개별 스트림마다 MAX_STREAM_DATA 프레임을 즉시 전송하면 ACK 트래픽이 폭증합니다. quicly는 WINDOW_UPDATE 합산(coalescing)으로 이를 최적화합니다:

흐름 제어 교착상태 진단

흐름 제어가 잘못 설정되면 송신 측이 윈도우 소진으로 전송을 멈추는 교착상태(flow control deadlock)가 발생합니다. show quic 명령으로 진단할 수 있습니다:

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 오프로드 지원이 더 효과적일 수 있습니다.

QUIC 0-RTT / 1-RTT 타이밍 상세 분해

"QUIC은 0-RTT다"라는 말은 절반만 맞습니다. 실제로는 연결 상태에 따라 세 가지 모드가 존재하며, 각각의 유효 지연 시간이 다릅니다. VPP QUIC 플러그인이 이 세 모드를 어떻게 선택하는지와, TCP+TLS 1.3과의 정확한 타이밍 차이는 아래 표로 정리됩니다(RTT=20ms 기준).

시나리오TCP+TLS 1.3 지연QUIC 지연QUIC 모드선결 조건
신규 연결2×RTT + TLS = 40 ms1×RTT = 20 msInitial + Handshake
재연결 (PSK 보유)2×RTT = 40 ms1×RTT = 20 ms1-RTT resumptionsession ticket 캐시(Cache)
재연결 + 멱등 요청2×RTT = 40 ms0×RTT + early = 0 ms*0-RTTGET/HEAD 등 멱등
재연결 + POST2×RTT = 40 ms1×RTT = 20 ms1-RTT 강제POST는 0-RTT 거부
네트워크 변경 (NAT rebind)연결 리셋 + 2×RTT = 40+α ms0 msConnection MigrationConnection ID 유지

* 0-RTT의 "0 ms"는 첫 요청 바이트가 즉시 송신된다는 의미이며, 응답이 돌아오는 시간(1×RTT)은 별도입니다.

/* VPP QUIC 플러그인 — 0-RTT 허용 판정 (개념 코드) */
static int
quic_allow_early_data (quic_ctx_t *ctx, http_request_t *req)
{
  if (!ctx->session_ticket || ctx->session_ticket_expired)
    return 0;  /* PSK 없음 → 1-RTT로 강제 */

  if (!ctx->early_data_allowed)
    return 0;  /* 서버가 max_early_data_size=0 전달 */

  /* 멱등 메서드만 0-RTT 허용 — replay 공격 방어 */
  if (req->method != HTTP_GET &&
      req->method != HTTP_HEAD &&
      req->method != HTTP_OPTIONS)
    return 0;

  /* anti-replay 윈도우 내 중복 여부 확인 */
  if (anti_replay_cache_check (ctx->ticket_nonce))
    return 0;  /* 이미 본 nonce → 거부 */

  return 1;
}

실전 결정: 모바일 클라이언트 위주 서비스에서 0-RTT의 실효 절감은 평균 8~15%에 불과합니다(캐시 hit율, 멱등 요청 비율이 낮기 때문). 반면 Connection Migration은 모바일 이동 시 끊김 없는 전환을 제공해 체감 품질 개선이 훨씬 큽니다. VPP QUIC 튜닝 우선순위(Priority)는 ① Connection ID·migration 지원 → ② 1-RTT resumption → ③ 0-RTT 순서로 두는 것이 합리적입니다.

QUIC 디버깅 및 트러블슈팅

VPP QUIC 환경에서 문제를 진단하고 해결하기 위한 CLI 명령, 이벤트 로그, 일반적인 문제 패턴을 정리합니다.

QUIC 상태 확인 CLI 명령

# QUIC 연결 목록 조회
vpp# show quic connections
# 출력: conn_id, state, streams, rx/tx bytes, rtt

# QUIC 연결 상세 정보
vpp# show quic connections verbose
# 출력: 혼잡 윈도우, ssthresh, bytes_in_flight, 스트림 상세

# 활성 스트림 목록
vpp# show quic streams
# 출력: stream_id, state, tx/rx offset, flow_control_limit

# QUIC 플러그인 통계
vpp# show errors
# quic-input, quic-output 노드의 오류 카운터 확인

# QUIC 패킷 트레이싱
vpp# trace add session-queue 100
vpp# show trace
# QUIC 패킷의 수신/처리/송신 경로 확인

# UDP 레벨 패킷 트레이싱 (QUIC 하부 전송)
vpp# trace add udp-input 100
vpp# trace add udp-output 100

quicly 이벤트 로그 활용

quicly는 내부적으로 상세한 이벤트 로그를 생성할 수 있습니다. VPP에서 이를 활성화하면 QUIC 프로토콜 수준의 문제를 세밀하게 분석할 수 있습니다:

# startup.conf — quicly 이벤트 로깅 활성화
quic {
  # 이벤트 로그 파일 경로
  event-log /tmp/quicly-events.json

  # 로그 레벨: packet, cc(혼잡 제어), loss(손실), stream
  event-log-level packet
}
# quicly 이벤트 로그 분석 (JSON 형식)
# 연결 수립 이벤트
cat /tmp/quicly-events.json | jq '.[] | select(.type == "connect")'

# 패킷 손실 이벤트 필터링
cat /tmp/quicly-events.json | jq '.[] | select(.type == "packet-lost")'

# 혼잡 윈도우 변화 추적
cat /tmp/quicly-events.json | jq '.[] | select(.type == "cc-cwnd-update")'

# qvis (QUIC 시각화 도구)로 분석
# https://qvis.quictools.info/ 에서 JSON 파일 업로드

일반적인 QUIC 문제 및 해결

문제증상진단 방법해결책
핸드셰이크 실패연결 수립 불가, 타임아웃show errors에 quic-handshake-fail인증서 경로·유효기간 확인, TLS 1.3 지원 여부 점검
0-RTT 거부0-RTT 데이터가 무시됨이벤트 로그에 early-data-rejected서버의 PSK 캐시 유효기간 확인, ALPN 일치 여부 점검
스트림 생성 실패MAX_STREAMS 초과 오류show quic connections verbose에서 스트림 수 확인max-streams-per-connection 설정 증가
처리량 저하예상보다 낮은 대역폭show quic connections verbose에서 cwnd 확인혼잡 제어 알고리즘 변경(BBR), 초기 윈도우 크기 조정
연결 마이그레이션 실패IP 변경 시 연결 끊김이벤트 로그에 path-validation 실패방화벽(Firewall)에서 새 경로의 UDP 트래픽 허용
과도한 패킷 손실높은 재전송률이벤트 로그에 packet-lost 빈도 확인네트워크 경로 점검, MTU 설정 확인(PMTUD)
메모리 증가QUIC 세션 수 대비 과다 메모리show memory verbose로 quic 힙 확인idle-timeout 줄이기, 비활성 연결 정리
UDP 블로킹QUIC 패킷이 도달하지 않음show interface에서 UDP 드롭 확인방화벽·NAT에서 UDP 443 포트 허용

QUIC 성능 진단 절차

# 1. 기본 상태 확인
vpp# show version
vpp# show quic connections
vpp# show quic streams

# 2. 혼잡 제어 상태 확인
vpp# show quic connections verbose
# cwnd, ssthresh, bytes_in_flight 값 확인
# cwnd가 비정상적으로 작으면 손실 과다 의심

# 3. 세션 FIFO 상태 확인
vpp# show session verbose
# RX/TX FIFO 사용률이 100%에 가까우면 병목

# 4. 오류 카운터 확인
vpp# show errors
# quic-input, quic-output 노드별 오류 확인

# 5. 패킷 경로 트레이싱
vpp# trace add session-queue 50
vpp# show trace
# 패킷이 어느 노드에서 드롭되는지 확인

# 6. 워커 스레드별 부하 확인
vpp# show runtime
# quic 관련 노드의 vectors/call, clocks/vector 확인
디버깅 팁: QUIC 문제의 대부분은 네트워크 경로상의 UDP 차단(방화벽, NAT)에서 발생합니다. QUIC 디버깅을 시작하기 전에 먼저 ping과 간단한 UDP 에코 테스트로 UDP 연결성을 확인하는 것이 좋습니다. 또한 quicly의 이벤트 로그를 qvis 도구(https://qvis.quictools.info/)에 업로드하면 패킷 타임라인, 혼잡 윈도우 변화, 스트림별 데이터 흐름을 시각적으로 분석할 수 있습니다.

HTTP/3 실전 — ALPN 협상, 0-RTT, VPP 25.10 제약

HTTP/3는 HTTP/2의 의미를 거의 그대로 유지한 채 전송 계층만 TCP에서 QUIC로 바꾼 것입니다. 그러나 이 한 가지 변화가 HOL 블로킹·연결 마이그레이션·0-RTT 재개 같은 굵직한 개선을 가져옵니다. FD.io VPP 25.10은 quicly 라이브러리 위에 HTTP/3 어댑터를 얹은 실험적 지원을 제공하며, 이 절은 실전 배포 관점에서 마주치는 ALPN 협상, 0-RTT, 현재 제약을 정리합니다.

ALPN 협상 — h2 vs h3 vs 둘 다

같은 호스트가 HTTP/2와 HTTP/3를 모두 지원할 때, 클라이언트는 어느 쪽을 쓸지 어떻게 결정할까요. 두 단계가 함께 작동합니다.

  1. Alt-Svc 헤더 또는 HTTPS DNS 레코드로 클라이언트는 "이 서버가 HTTP/3를 지원한다"는 사실을 학습합니다.
  2. 다음 요청부터 클라이언트는 UDP/443으로 QUIC Initial 패킷을 보내면서 TLS 1.3 ClientHello의 ALPN 확장에 h3를 넣습니다.
  3. 서버가 ALPN으로 h3를 선택하면 HTTP/3로 협상이 끝납니다. UDP 경로가 막혀 있으면 클라이언트는 즉시 TCP/443로 폴백하고 ALPN으로 h2를 받습니다.
클라이언트 서버 (VPP+quicly) ① TCP/443 + TLS h2 (1차 방문) ② Alt-Svc: h3=":443"; ma=86400 (응답 헤더) ③ 다음 요청: UDP/443 QUIC Initial + ALPN [h3] ④ 0-RTT 가능 시 첫 패킷에 HTTP 요청 동봉

0-RTT 재개 — 빠르지만 위험한 지름길

QUIC의 0-RTT는 TLS 1.3의 PSK(pre-shared key)를 활용해, 핸드셰이크 완료를 기다리지 않고 첫 패킷에 HTTP 요청을 함께 실어 보내는 기능입니다. 한 번이라도 같은 서버와 정상 핸드셰이크를 한 적이 있어야 합니다. 효과는 RTT 1회 절감이며, 모바일 환경에서 체감 지연이 크게 줄어듭니다.

0-RTT 적용안전한 메서드금지 메서드
✅ 권장GET (멱등, 캐시 가능)
⚠️ 주의HEAD, OPTIONS
❌ 금지POST, PUT, DELETE 등 부수효과 있는 메서드

금지 이유는 리플레이(replay) 공격입니다. 0-RTT 데이터는 같은 PSK로 중간자가 그대로 재전송할 수 있어, "결제 실행" 같은 비멱등 요청이 두 번 일어날 수 있습니다. RFC 9001은 0-RTT에 대해 애플리케이션이 명시적 anti-replay 방어(서버측 nonce 캐시)를 두지 않는 한 비멱등 메서드를 허용해서는 안 된다고 못 박습니다.

/* 0-RTT 허용 정책: GET만 통과 */
static int
h3_validate_0rtt_request (http_msg_t *msg)
{
  if (msg->method != HTTP_METHOD_GET && msg->method != HTTP_METHOD_HEAD)
    return -1;  /* 425 Too Early로 응답 */
  /* 헤더에 If-None-Match 있으면 캐시 검증으로 안전 */
  return 0;
}

/* quicly 콜백: 0-RTT 데이터 도착 시 호출 */
static int
h3_on_early_data (quicly_conn_t *qc, ptls_iovec_t data)
{
  http_msg_t msg;
  if (http3_parse_headers (data, &msg) < 0) return -1;
  if (h3_validate_0rtt_request (&msg) < 0)
    return h3_send_status (qc, 425, "Too Early");
  return h3_dispatch_request (qc, &msg);
}

VPP 25.10 HTTP/3 현재 제약

기능상태비고
QUIC 전송 (quicly)✅ 안정클라이언트·서버 모두
HTTP/3 어댑터🟡 실험적http3_plugin (out-of-tree)
QPACK🟡 부분정적 테이블 위주, 동적 테이블 제한적
0-RTT🟡 실험적anti-replay 정책 수동 구성 필요
연결 마이그레이션🟡 실험적NAT rebinding 한정 검증
Datagram 확장 (RFC 9221)❌ 미지원WebTransport 의존 기능 불가
Multipath QUIC❌ 미지원표준화 진행 중
프로덕션 권고: VPP 25.10에서 HTTP/3는 가시성 확보·부하 분산(Load Balancing) 실험·내부 서비스 PoC까지가 안전 영역입니다. 외부 사용자에게 제공하는 운영 트래픽은 nginx-quic 또는 Cloudflare quiche 같은 성숙한 사용자 공간(User Space) 스택 뒤에 두는 편이 안정적입니다. VPP의 강점은 같은 워커에서 L2~L4까지 함께 처리할 수 있다는 점이므로, HTTP/3는 점진적 도입을 권합니다.

HTTP/2 → HTTP/3 마이그레이션 런북

HTTP/2를 운영 중인 서비스에 HTTP/3를 점진적으로 도입하는 5단계 체크리스트입니다. 각 단계에서 검증이 통과된 후에만 다음 단계로 넘어가야 합니다. 0-RTT 재전송 방어 세부 사항은 0-RTT 재개와 재전송 방어 절을, QPACK 동적 테이블 제약은 VPP 26.02 변경 요약 절을 함께 참고하시기 바랍니다.

5단계 체크리스트

  1. ALPN 협상 확인
    HTTP/2 서버가 먼저 Alt-Svc: h3=":443"; ma=86400 헤더를 응답에 포함하도록 구성합니다. curl -v --http2로 헤더 출력을 확인하고, DNS HTTPS 레코드를 선택적으로 추가합니다. TCP/443(h2)와 UDP/443(h3)가 모두 열려 있는지 방화벽 정책을 이중 확인합니다.
  2. Alt-Svc 헤더 설정 및 점진적 롤아웃
    ma(max-age)를 처음에는 300초(5분)로 짧게 잡습니다. 클라이언트가 HTTP/3로 전환한 뒤 오류율(error rate)이 허용 임계치(예: 0.1%) 이하임을 확인한 후 86400초(24시간)로 늘립니다. 전환 중 HTTP/3 연결 실패 시 클라이언트가 TCP/443으로 자동 폴백하는지 Common Pitfalls의 항목들을 점검합니다.
  3. 0-RTT 위험 평가
    0-RTT는 기본 비활성 상태로 시작합니다. 활성화 전 다음 사항을 확인합니다: (a) GET/HEAD/OPTIONS 이외의 메서드에 0-RTT가 허용되지 않도록 메서드 화이트리스트 적용, (b) 다중 서버 클러스터라면 session ticket store를 공유 저장소(예: Redis)로 중앙화하여 anti-replay 무력화 방지, (c) /quic/0rtt/rejected_replay 카운터 알람 임계를 1% 이상으로 설정.
  4. QPACK 에러 처리
    VPP 26.02 기준 QPACK은 정적 테이블(Static Table) 위주입니다. 동적 테이블을 기대하는 클라이언트와의 상호운용 문제를 대비해 아래 에러 코드를 로그에서 모니터링합니다.
    에러 코드의미대응
    QPACK_DECOMPRESSION_FAILED (0x200)헤더 블록 복호화 실패인코더/디코더 스트림 동기화 확인
    QPACK_ENCODER_STREAM_ERROR (0x201)인코더 스트림 프로토콜 오류클라이언트 QPACK 구현 버전 확인
    QPACK_DECODER_STREAM_ERROR (0x202)디코더 스트림 프로토콜 오류동적 테이블 크기 협상값을 0으로 강제 설정
    QPACK 인코더와 디코더는 각각 별도 단방향 스트림(Unidirectional Stream)으로 흐릅니다. 두 스트림이 비동기적으로 처리될 때 헤더 블록이 아직 디코딩되지 않은 인덱스를 참조하면 블로킹이 발생합니다. 단기 해결책은 동적 테이블 크기를 0으로 협상하여 정적 테이블만 사용하는 것입니다.
      QPACK 비동기 에러 복구 흐름:
    
      인코더 스트림  ──→  [헤더 블록: 인덱스 N 참조]
      디코더 스트림  ──→  [삽입 완료 알림: 아직 N-1까지]
    
      디코더가 인덱스 N을 받기 전 헤더 블록 처리 시도
            ↓
      QPACK_DECOMPRESSION_FAILED 발생
            ↓
      H3_GENERAL_PROTOCOL_ERROR(0x101)로 연결 종료
            ↓
      복구: 동적 테이블 크기 = 0 재협상 또는 클라이언트 재연결
  5. GOAWAY 타이밍 및 graceful shutdown
    서버 재시작이나 점검 시 HTTP/3는 GOAWAY 프레임으로 클라이언트에게 이전 스트림 ID까지만 처리하겠다는 신호를 보냅니다. HTTP/2의 GOAWAY와 시맨틱은 같지만, QUIC의 스트림 다중화로 인해 타이밍이 더 복잡합니다. GOAWAY 전송 후 최소 1 RTT(왕복 지연시간)를 기다린 뒤 기존 스트림을 종료해야 합니다. 워커 재시작 전 show quic streams로 활성 스트림이 0이 되었는지 확인합니다.

Fallback 테스트: HTTP/3 실패 시 HTTP/2 자동 전환 검증

UDP/443 차단 상황을 시뮬레이션하여 클라이언트가 HTTP/2로 자동 폴백하는지 확인합니다.

# 1. UDP/443 차단 시뮬레이션 (iptables 사용)
sudo iptables -I OUTPUT -p udp --dport 443 -j DROP

# 2. curl로 HTTP/3 시도 → 타임아웃 후 HTTP/2 폴백 확인
curl -v --http3-only https://192.0.2.10/ 2>&1 | grep -E '(HTTP/|Alt-Svc|QUIC)'
# HTTP/3 전용 실패 예상 (--http3-only는 폴백 없음)

curl -v --http3 https://192.0.2.10/ 2>&1 | grep -E '(HTTP/|protocol)'
# --http3 (폴백 허용): UDP 차단 → TCP/443 h2로 자동 전환 확인
# 출력에 "Using HTTP/2" 또는 "ALPN: h2" 포함 여부로 판단

# 3. 차단 해제
sudo iptables -D OUTPUT -p udp --dport 443 -j DROP

# 4. VPP CLI — 현재 HTTP 세션 프로토콜별 통계 확인
vpp# show http server sessions
# h2/h3 세션 수, 활성 스트림 수, 에러 카운터

vpp# show quic connections
# UDP/443으로 성공한 QUIC 연결 목록

vpp# show session verbose
# 세션 레이어 전체 상태 — 프로토콜 필드로 h2/h3 구분

# 5. 에러 카운터로 폴백 빈도 추적
vpp# show errors
# quic-input 드롭: UDP 차단 시 증가 → 클라이언트 폴백 발생 신호
# http-h3-upgrade-fail: HTTP/3 업그레이드 시도 후 실패 카운터
운영 주의: Alt-Svc ma(max-age)를 길게 설정한 뒤 UDP/443을 차단하면 클라이언트가 캐시된 HTTP/3 경로를 오랫동안 재시도합니다. 계획된 네트워크 점검 전에 ma=0을 응답해 캐시를 만료시키거나, DNS HTTPS 레코드에서 h3를 제거하는 절차를 포함해야 합니다.

CLI 예시 — HTTP/3 활성화와 관찰

# 1) QUIC 플러그인 로드
vppctl show plugins | grep -E '(quic|http3)'

# 2) HTTP/3 서버 활성화 (가상 CLI)
vppctl http3 server enable uri quic://0.0.0.0/443 \
    cert /etc/vpp/cert.pem key /etc/vpp/key.pem \
    enable-0rtt allowed-methods GET,HEAD

# 3) Alt-Svc 광고를 위한 HTTP/2 서버도 같이 실행
vppctl http server enable uri tls://0.0.0.0/443 \
    alt-svc 'h3=":443"; ma=86400'

# 4) curl로 HTTP/3 강제 요청
curl --http3 -k https://192.0.2.10/

# 5) QUIC 통계
vppctl show quic sessions
vppctl show quic stats
vppctl show http3 streams

Common Pitfalls

Connection Migration 운영

QUIC의 대표 기능인 Connection Migration은 클라이언트의 IP·포트가 바뀌어도 Connection ID(CID)로 같은 연결을 이어갑니다. TCP와 달리 5튜플 변경에 무관하게 세션이 유지되므로 모바일 셀 전환, Wi-Fi↔LTE 핸드오버, NAT 재바인딩에 유리합니다. 그러나 실제 운영에서는 CID 관리, 경로 검증(Path Validation), DoS 완화의 세 가지 축을 모두 튜닝해야 합니다.

핵심 개념: Connection Migration은 새 경로로 PATH_CHALLENGE/PATH_RESPONSE 프레임을 교환해 경로 도달성을 검증한 뒤, amplification limit(3배 제한)을 두고 트래픽을 옮겨갑니다. 서버는 active_connection_id_limit 만큼 예비 CID를 미리 발급해 두어야 하며, 이 값이 작으면 마이그레이션 시 CID 고갈로 연결이 끊어집니다.

시나리오 1: NAT 재바인딩

UDP NAT 매핑은 흔히 30~60초 만에 만료됩니다. 클라이언트가 PING 프레임을 보내지 않거나 마지막 트래픽 이후 시간이 지나면, NAT 게이트웨이는 새 외부 포트를 재할당합니다. 서버에서 관찰되는 증상은 기존 CID가 다른 5튜플에서 도착하는 것입니다.

시나리오 2: 모바일 셀 · Wi-Fi↔LTE 핸드오버

스마트폰이 Wi-Fi에서 LTE로 전환되는 순간 커널 소켓은 EHOSTUNREACH 또는 ENETUNREACH를 받습니다. QUIC 스택은 새 인터페이스에서 같은 CID로 QUIC 패킷을 재전송하고, 서버는 마이그레이션을 감지해 active path를 교체합니다. 관찰 가능한 지표:

지표정상 범위이상 신호
Migration 완료 시간< 200ms (1-RTT + path validation)> 1s — 패킷 손실 또는 스로틀링
Path validation 실패율< 1%> 5% — 새 경로의 MTU/방화벽 이슈
CID 고갈 이벤트0> 0 — active_connection_id_limit 증가 필요

Amplification 공격 방어

Connection Migration은 DoS 증폭 공격의 벡터가 될 수 있습니다. 공격자가 피해자의 주소로 위조된 마이그레이션 시도를 보내면, 서버는 그 경로로 응답을 쏟아냅니다. QUIC 표준(RFC 9000 §8)은 이를 막기 위해 서버가 검증되지 않은 경로로는 클라이언트가 보낸 바이트의 3배 이상을 보낼 수 없다고 규정합니다.

0-RTT 재개와 재전송 방어

0-RTT는 이전 세션에서 서버가 발급한 session ticket을 이용해 첫 패킷과 함께 응용 데이터를 전송하는 기능입니다. 지연이 줄어드는 대가로 재전송 공격(replay attack)에 노출되는데, 공격자가 0-RTT 데이터를 캡처해 재생하면 서버가 같은 요청을 두 번 처리할 수 있습니다. TLS 1.3과 QUIC은 공통적으로 "0-RTT는 idempotent 요청에만 쓸 것"을 권고합니다.

Replay Window 설계

VPP quicly는 기본적으로 anti-replay를 활성화합니다. 세 가지 보호 계층이 있습니다.

  1. Single-use ticket: 각 session ticket은 한 번만 사용 가능. quicly의 ticket_file에 사용 기록이 저장되고, 중복 사용 시 full handshake로 폴백합니다.
  2. Time-bound: Ticket lifetime은 기본 7200초. 이 창(window) 밖의 티켓은 거절합니다.
  3. Request class filter: HTTP 어댑터 레벨에서 GET·HEAD·OPTIONS와 일부 PUT(ETag/If-Match 조건부)만 0-RTT 수락. POST는 1-RTT로 강제 연기.
함정: Single-use ticket을 여러 프로세스가 공유하는 경우(서버 클러스터 뒤의 로드 밸런서) ticket store를 중앙화하지 않으면 anti-replay가 무력화됩니다. vpp-quic-ticket-store를 Redis 같은 공유 저장소로 연결하거나, 세션 스티키니스를 보장해야 합니다.

애플리케이션 계약

VPP HTTP 어댑터는 0-RTT에서 도착한 요청의 X-VPP-Early-Data: 1 헤더를 설정합니다. 애플리케이션은 이 헤더가 있는 요청이 재전송될 수 있음을 가정하고 다음 규칙을 따라야 합니다.

0-RTT 모니터링 지표

운영에서 추적해야 할 카운터:

카운터의미권장 임계
/quic/0rtt/accepted0-RTT 데이터를 수락한 수전체 연결의 20~60%
/quic/0rtt/rejected_replayanti-replay로 거절한 수가급적 0, 1% 초과 시 알람
/quic/0rtt/rejected_expiredticket 만료로 거절정상 — 튜닝 대상 아님
/http/early_data/425_response애플리케이션이 0-RTT 거절한 수설계 의도에 따름

0-RTT 보안 검증 체크리스트

0-RTT를 프로덕션에 적용하기 전에 다음 항목을 순서대로 검증합니다.

  1. 재전송 공격 테스트: 동일한 0-RTT 패킷을 두 번 전송하고 서버가 두 번째 요청을 거부하는지 확인합니다. rejected_replay 카운터가 증가해야 정상입니다.
  2. 타임스탬프 클록 스큐 허용 범위 설정: 클라이언트와 서버의 시계 차이가 ±5초를 초과하면 ticket 유효성 검증이 실패할 수 있습니다. NTP 동기화 상태를 확인하고 허용 범위를 ±5초로 설정합니다.
  3. 멱등성 보장 불가 요청에 0-RTT 사용 금지: POST, DELETE와 같이 서버 상태를 변경하는 요청은 0-RTT 경로에서 반드시 제외합니다. VPP HTTP 어댑터의 요청 클래스 필터에 POST·DELETE를 1-RTT 강제 목록에 추가합니다.
  4. 티켓 유효기간 제한: session ticket 유효기간을 7일 이하로 설정합니다. 장기 티켓은 키 노출 시 피해 범위가 넓어지므로 24시간~7일 범위를 권장합니다.
  5. 분산 환경에서의 티켓 캐시 공유: 서버 클러스터 환경에서는 anti-replay 상태를 모든 노드가 공유해야 합니다. Redis 또는 공유 메모리 기반 ticket store를 구성하고, 로드 밸런서 세션 스티키니스(session stickiness)와 병행하거나 중앙화된 단일 ticket store로 통일합니다.
주의: GET 요청도 사이드이펙트(side effect)가 있다면 0-RTT 허용은 위험합니다. 예를 들어 조회 시 로그를 기록하거나 카운터를 증가시키는 GET 엔드포인트는 재전송 시 중복 부작용이 발생합니다. 0-RTT 허용 여부는 HTTP 메서드가 아닌 엔드포인트의 실제 멱등성을 기준으로 판단해야 합니다.

VPP 25.10 → 26.02 변경 요약 (QUIC · HTTP/3)

본 문서의 기준 버전은 VPP v26.02(2026-02-25 릴리스)입니다. 앞 절에서 언급한 "VPP 25.10 제약" 일부는 26.02에서 개선되었으므로 운영 판단이 달라질 수 있습니다. 아래 표는 공식 26.02 릴리스 노트에 명시된 HTTP/3·QUIC 관련 변경을 추려 운영 관점에서 재정리한 것입니다.

영역VPP 25.10VPP 26.02운영 영향
HTTP/3 코어 quicly 기반 얇은 h3 어댑터(실험) H3 core skeletonH3 framing layer 도입 — HTTP/3 프레이밍을 VPP http 플러그인 내에서 처리하는 1급 경로가 생겼습니다. h3 요청 파싱·상태 기계가 VPP 측에 있으므로, 호스트 스택 세션 훅(rx_callback, 통계, ACL)을 h3 트래픽에도 그대로 적용할 수 있습니다.
HTTP/3 클라이언트 사실상 없음(테스트 경로만) H3 client side 공식 지원 VPP 자체를 HTTP/3 클라이언트로 사용하는 프로브·헬스체크·proxy origin 호출이 가능해졌습니다.
QPACK 미지원 QPACK encoder/decoder 도입(요청·응답 모두). 단, static table 전용으로 dynamic table은 아직 없습니다. 상호운용성은 확보되지만 동적 테이블 기반 고압축은 불가합니다. 대역폭 감축 기대치는 h2 HPACK 대비 소폭입니다.
HTTP CONNECT 프록시 없음(HTTP/2 extended CONNECT 서버 측만) 호스트 스택에 HTTP CONNECT proxy client 추가 VPP가 upstream proxy를 CONNECT로 경유하는 outbound 경로(기업망·사이드카)를 직접 구성할 수 있습니다.
HTTP 클라이언트 redirect 수동 처리 필요 HTTP 클라이언트에 basic redirect 지원 3xx 응답을 따라가는 프로브·파일 다운로드 시 애플리케이션 단순화가 가능합니다.
DPDK/RDMA 토대 DPDK 25.07 + rdma-core 58.0 DPDK 25.11 + rdma-core 60.0 QUIC 가속 오프로드(UDP GSO/GRO)와 관련한 PMD 옵션 정비. 업그레이드 시 DPDK 25.11 deprecated 옵션을 점검해야 합니다.
여전히 성숙도는 experimental: VPP 26.02의 HTTP/3는 코어 프레이밍과 클라이언트가 들어와 완성도가 크게 올랐지만, 공식 feature list 기준으로는 여전히 experimental 분류입니다. QPACK dynamic table 부재, 일부 extensions 미구현으로 인해 가용성 99.99% SLO를 약속하기 전에는 카나리아 트래픽과 공식 릴리스 노트 교차 확인이 필요합니다.
업그레이드 체크리스트: (1) QPACK static-table 전제에서 테스트 트래픽의 헤더 사이즈 측정, (2) HTTP CONNECT proxy client를 사용한다면 upstream proxy에 대한 상호 TLS(mTLS) 설정을 TLS 문서의 26.02 변경 절과 함께 검토, (3) DPDK 25.11 전환을 데이터 평면 문서의 PMD 변경 표와 함께 확인, (4) HTTP/3 경로를 h3 전용이 아닌 h2 fallback과 함께 서빙.

이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하시기 바랍니다.

오픈소스 코드 인용 고지

라이선스 고지: 이 문서의 코드 예제에는 아래 오픈소스 프로젝트의 소스 코드에서 발췌·간략화한 내용이 포함되어 있습니다. 해당 코드 블록에는 원본 프로젝트의 라이선스가 그대로 적용되며, 본 사이트의 CC BY-NC-SA 4.0 라이선스 대상에서 제외됩니다. 이들 코드의 포함은 한국 저작권법 제28조 및 제35조의5에 근거한 교육 목적의 공정 이용에 해당합니다.
프로젝트저작권자라이선스공식 저장소
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

코드 블록 내 주석에 원본 파일 경로가 표기된 부분은 해당 프로젝트 소스 코드에서 발췌·간략화한 것입니다. 전체 소스 코드는 위 공식 저장소에서 확인하시기 바랍니다.