QUIC 연결은 클라이언트 IP가 바뀌어도 왜 이어질까: connection ID, 경로 검증, 로드 밸런서가 할 일
QUIC는 주소가 아니라 connection ID로 패킷을 연결에 붙인다. RFC 9000을 기준으로 클라이언트 주소가 바뀔 때 서버가 하는 경로 검증, 새 경로마다 새 connection ID가 필요한 이유, 로드 밸런서 뒤에서 이동을 지원하거나 막는 방법을 정리했다.
QUIC 서버 로그에서 같은 연결의 패킷이 어느 순간부터 다른 IP 주소나 포트에서 들어오는 것을 볼 수 있다. 클라이언트가 다른 네트워크로 옮겼거나, 중간의 NAT가 바깥쪽 포트를 새로 잡았을 수 있다. QUIC는 이런 경우에도 연결을 이어 가도록 설계됐다. 다만 그냥 이어지는 것은 아니고, 받는 쪽은 새 주소를 확인해야 하며 서버 앞의 로드 밸런서는 그 패킷을 같은 서버로 보내 줄 수 있어야 한다.
이 글은 QUIC 버전 1을 정의한 RFC 9000(2021년 5월)의 5.1절, 5.2절, 8장, 9장, 17.217.3절, 18.2절, 19.1519.18절을 기준으로 한다(2026-10-19 확인). 특정 QUIC 구현이나 브라우저, 운영체제가 실제로 언제 이동을 시작하는지는 다루지 않는다. RFC 9000 밖의 확장도 다루지 않는다.
연결을 찾는 열쇠는 주소가 아니라 connection ID
QUIC 연결에는 connection ID가 여러 개 있을 수 있고, 그중 어느 것으로도 연결을 식별할 수 있다. 중요한 점은 각 끝점이 상대가 쓸 connection ID를 고른다는 것이다. 클라이언트가 서버로 보내는 패킷의 Destination Connection ID는 서버가 발급한 값이고, 반대 방향은 클라이언트가 발급한 값이다(5.1절).
RFC가 밝히는 connection ID의 1차 목적은 UDP나 IP 같은 아래 계층의 주소가 바뀌어도 패킷이 엉뚱한 끝점으로 가지 않게 하는 것이다. 받은 패킷의 Destination Connection ID가 길이 0이 아니고 기존 연결에 해당하면, 그 패킷은 보낸 주소와 상관없이 그 연결의 패킷으로 처리된다(5.2절). 9장 첫 문장도 같은 내용이다. connection ID를 쓰기 때문에 연결이 끝점의 주소(IP 주소와 포트) 변화에서 살아남을 수 있다.
알아 두면 좋은 형식 규칙이 몇 가지 있다.
- 1-RTT 패킷에 쓰이는 짧은 헤더(short header, 17.3절)에는 Destination Connection ID만 있고 길이 필드가 없다. 받는 쪽이 길이를 미리 알고 있어야 한다는 뜻이다. RFC는 connection ID로 라우팅하는 로드 밸런서를 쓰는 경우 길이를 고정하거나, 고정된 앞부분에 길이를 인코딩하는 방식을 로드 밸런서와 합의할 수 있다고 적는다(5.1절).
- QUIC 버전 1에서 connection ID는 최대 20바이트다(17.2절). NEW_CONNECTION_ID 프레임으로 주는 connection ID는 1~20바이트여야 한다(19.15절).
- connection ID에는 협조하지 않는 외부 관찰자가 같은 연결의 다른 connection ID와 연결 지을 수 있는 정보를 넣으면 안 된다(MUST NOT). 같은 연결에서 같은 값을 두 번 발급해서도 안 된다(5.1절).
길이 0인 connection ID도 허용된다. 이때는 주소로 연결을 구분해야 하므로, 같은 IP 주소와 포트에서 길이 0 connection ID로 여러 연결을 동시에 받으면 상대의 연결 이동, NAT 재바인딩, 클라이언트의 포트 재사용이 있을 때 실패한다고 RFC는 적는다. 그런 기능이 쓰이지 않는다고 확신할 때가 아니면 그렇게 하면 안 된다(MUST NOT, 5.1절).
이동은 누가, 언제 할 수 있나
RFC 9000은 주소가 바뀌는 경우를 둘로 나눈다(9장).
| 구분 | 무엇이 일어나나 | 끝점이 의도했나 |
|---|---|---|
| 능동 이동(active migration) | 끝점이 새 로컬 주소에서 패킷을 보내기 시작함 | 예 |
| NAT 재바인딩 | NAT 같은 중간 장비가 흐름에 새 바깥 포트나 IP 주소를 배정함 | 아니오 |
어느 쪽이든, 받는 쪽은 상대 주소가 바뀐 것을 감지하면 이전에 검증한 주소가 아닌 한 경로 검증을 해야 한다(MUST).
이동에는 제한이 있다.
- 핸드셰이크가 확인(confirmed)되기 전에는 이동을 시작하면 안 된다(MUST NOT). “확인”의 정의는 RFC 9001 4.1.2절에 있다고 RFC 9000은 적는다.
- 이 버전의 QUIC는 이동을 새 클라이언트 주소로 한정한다. 이동은 모두 클라이언트가 시작한다. 클라이언트가 모르는 서버 주소에서 패킷을 받으면 버려야 한다(MUST). 유일한 예외가 뒤에서 다룰 서버의 선호 주소(preferred address)다.
- 서버가
disable_active_migration전송 매개변수를 보냈다면, 클라이언트는 핸드셰이크 때 서버가 쓴 주소로 새 로컬 주소에서 패킷을 보내면 안 된다. 탐색용 패킷도 마찬가지다(MUST NOT).
탐색 패킷과 비탐색 패킷
이동 절차를 읽으려면 9.1절의 구분을 먼저 알아야 한다.
| 분류 | 해당 프레임 |
|---|---|
| 탐색 프레임(probing frame) | PATH_CHALLENGE, PATH_RESPONSE, NEW_CONNECTION_ID, PADDING |
| 비탐색 프레임 | 그 밖의 모든 프레임 (STREAM, ACK 등) |
탐색 프레임만 담은 패킷이 탐색 패킷이고, 다른 프레임이 하나라도 있으면 비탐색 패킷이다. 끝점은 이동하기 전에 새 로컬 주소에서 탐색 패킷으로 경로를 시험해 볼 수 있다(MAY). 검증이 실패해도 그 경로를 쓸 수 없다는 뜻일 뿐이고, 다른 쓸 수 있는 경로가 있으면 연결은 끝나지 않는다.
실제 이동은 새 로컬 주소에서 비탐색 프레임을 담은 패킷을 보내는 것으로 이뤄진다(9.2절). 받는 쪽에서 보면, 새 주소에서 비탐색 프레임을 담은 패킷이 오면 상대가 그 주소로 옮겼다는 신호다(9.3절).
클라이언트 주소가 바뀌었을 때 서버가 하는 일
클라이언트가 주소 A에서 주소 B로 옮긴 경우를 RFC 9000의 규칙대로 따라가 보면 다음과 같다. 그림의 connection ID 이름과 순서는 설명을 위한 예다.
클라이언트 (A → B) 서버
|-- B에서: STREAM 등, DCID=s2 ---------->| 새 주소 B에서 비탐색 패킷 도착
| | → 이후 패킷은 B로 보낸다
|<-- B로: PATH_CHALLENGE(x), DCID=c2 ----| B 검증 시작. 검증 전에는
| | B에서 받은 바이트의 3배까지만 보낸다
|<-- A로: PATH_CHALLENGE(y), DCID=c1 ----| 이전 경로 A도 검증한다
|-- B에서: PATH_RESPONSE(x), DCID=s2 --->| 같은 8바이트가 돌아옴 → B 검증 완료
| | 혼잡 제어와 RTT 추정을 초기값으로
| | (포트만 바뀌었다면 유지해도 됨)
s2: 서버가 발급. 클라이언트가 B에서 첫 패킷을 보내기 전에는 쓰인 적 없는 값이고,
클라이언트는 B에서 보내는 패킷에 이 값을 쓴다.
c2: 클라이언트가 발급. 서버가 B로 첫 패킷을 보내기 전에는 쓰인 적 없는 값이고,
서버는 B로 보내는 패킷에 이 값을 쓴다.
c1: 클라이언트가 발급. 이미 경로 A에 묶인 값으로, 서버가 이동 전부터 A로 보낼 때
쓰던 것이다. c2는 이제 B로 보내는 데 쓰였으므로 A로 보낼 때 함께 쓸 수
없다(9.5절).
단계별로 걸린 요구 수준은 이렇다.
새 주소로 보내고 검증을 시작한다. 이동을 받아들이면 이후 패킷을 새 주소로 보내야 하고(MUST), 이미 진행 중이 아니라면 그 주소를 상대가 실제로 가지고 있는지 경로 검증을 시작해야 한다(MUST, 9.3절). 패킷 순서가 뒤바뀌어 도착할 수 있으므로, 보낼 주소는 패킷 번호가 가장 큰 비탐색 패킷을 기준으로만 바꾼다.
검증 전에는 많이 보내지 않는다. 출발지 주소를 위조한 공격자가 이동을 이용해 엉뚱한 피해자에게 데이터를 쏟아붓게 만들 수 있다(9.3.1절). 그래서 검증되지 않은 주소로는 그 주소에서 받은 양의 3배까지만 보낼 수 있다(MUST, 8장). 이를 증폭 방지 한도(anti-amplification limit)라고 한다.
PATH_CHALLENGE와 PATH_RESPONSE. 검증하는 쪽은 예측할 수 없는 8바이트를 PATH_CHALLENGE에 담아 보내고(MUST), 받은 쪽은 같은 값을 PATH_RESPONSE로 돌려줘야 한다(MUST, 8.2.1절, 8.2.2절, 19.17절, 19.18절). 응답은 PATH_CHALLENGE를 받은 그 경로로 보내야 하고, 혼잡 제어 때문이 아니면 늦추면 안 된다. 1200바이트 규칙은 두 프레임에 다르게 적용된다.
- PATH_CHALLENGE를 담은 데이터그램은 1200바이트 이상으로 채워야 한다(MUST). 예외는 그 경로의 증폭 방지 한도 때문에 그만한 크기를 보낼 수 없을 때뿐이다(8.2.1절). 이 크기로 보내면 보내는 쪽에서 상대로 가는 경로를 QUIC에 쓸 수 있는지 확인된다.
- PATH_RESPONSE를 담은 데이터그램도 1200바이트 이상으로 채워야 한다(MUST). 다만 채운 결과가 증폭 방지 한도를 넘는다면 채우면 안 된다(MUST NOT, 8.2.2절). RFC는 이런 일이 받은 PATH_CHALLENGE 자체가 1200바이트로 채워지지 않았을 때만 생길 것으로 본다.
검증은 보낸 값과 같은 데이터를 담은 PATH_RESPONSE를 받았을 때만 성공한다. PATH_CHALLENGE를 1200바이트보다 작은 데이터그램으로 보냈더라도 맞는 응답이 오면 상대의 주소는 검증된 것이고, 그때부터는 받은 양의 3배보다 많이 보낼 수 있다. 하지만 경로 MTU는 검증되지 않았으므로, 경로가 필요한 MTU를 지원하는지 확인하려면 1200바이트 이상으로 채운 PATH_CHALLENGE로 경로 검증을 한 번 더 시작해야 한다(MUST). PATH_CHALLENGE를 담은 패킷에 대한 ACK는 위조될 수 있어서 검증으로 치지 않는다(이상 8.2.3절).
이전 경로도 확인한다. 경로 밖에서 진짜 패킷을 엿보고 복사본을 먼저 보내는 공격자는 NAT 재바인딩처럼 보이는 가짜 이동을 일으켜 자신을 경로 위에 올려놓을 수 있다. 이를 막기 위해 이동으로 보이는 일이 생기면 직전까지 쓰던 경로도 PATH_CHALLENGE로 검증해야 한다(MUST, 9.3.3절). 원래 경로에서 더 큰 패킷 번호의 비탐색 패킷이 오면 그 경로로 되돌아간다.
실패하면 되돌아간다. 검증이 실패하는 것은 검증하던 쪽이 시도를 포기했을 때뿐이다. 포기는 타이머로 정하는 것이 원칙이고(SHOULD), RFC는 현재 PTO와 새 경로의 PTO 중 큰 값의 3배를 권장한다. 새 경로의 PTO는 RFC 9002의 초기 RTT 값(kInitialRtt)으로 계산한다(8.2.4절). 새 주소 검증이 실패하면 마지막으로 검증된 주소로 되돌아가야 한다(MUST). 그런 주소의 상태가 없다면 연결 상태를 모두 버리고 조용히 닫아야 한다(MUST, 9.3.2절).
혼잡 제어를 초기화한다. 새 경로의 용량은 이전 경로와 다를 수 있다. 상대가 새 주소를 가지고 있음을 확인하면 새 경로의 혼잡 제어기와 RTT 추정기를 즉시 초기값으로 되돌려야 한다(MUST). 예외는 포트 번호만 바뀐 경우다. 포트만 바뀌는 것은 대개 NAT 재바인딩이나 중간 장비 때문이므로 기존 상태를 유지해도 된다(MAY, 9.4절).
새 클라이언트 주소를 검증한 서버는 새 주소 검증 토큰(8장)을 클라이언트에 보내는 것이 원칙이다(SHOULD, 9.3절).
새 경로에는 새 connection ID
같은 connection ID를 여러 경로에서 쓰면 수동 관찰자가 두 경로의 트래픽이 같은 연결이라는 것을 알아챌 수 있다. 그래서 RFC 9000은 이렇게 정한다(9.5절).
- 여러 로컬 주소에서 보낼 때 같은 connection ID를 다시 쓰면 안 된다(MUST NOT). 이동할 때도, 새 경로를 탐색할 때도 마찬가지다.
- 여러 목적지 주소로 보낼 때도 같은 connection ID를 다시 쓰면 안 된다(MUST NOT).
- 예외가 하나 있다. 상대가 통제할 수 없는 네트워크 변화(예: NAT 재바인딩) 때문에 같은 Destination Connection ID를 단 패킷이 새 출발지 주소에서 오면, 같은 로컬 주소에서 보내는 한 지금의 connection ID를 새 원격 주소에 계속 써도 된다(MAY).
이 규칙이 실제로 문제가 되는 곳은 connection ID의 재고다. 위 그림에서 서버가 B로 보낼 때 쓴 c2는 클라이언트가 미리 발급해 둔 값이어야 한다. 쓸 수 있는 connection ID가 바닥난 끝점은 새 경로를 탐색하거나 이동을 시작할 수 없고, 상대의 탐색이나 이동에 응답할 수도 없다(9.5절). 받는 쪽에 상대가 준 미사용 connection ID가 없으면, 상대가 하나를 줄 때까지 새 경로로 아무것도 보낼 수 없다(9.3절).
재고를 관리하는 장치는 다음과 같다.
| 장치 | 하는 일 | 근거 |
|---|---|---|
active_connection_id_limit 전송 매개변수 |
상대로부터 받아 보관할 connection ID의 최대 개수. 2 이상이어야 하고(MUST), 없으면 2로 본다 | 18.2절 |
| NEW_CONNECTION_ID 프레임 | 새 connection ID와 순번, 상태 없는 리셋 토큰(16바이트)을 전달. Retire Prior To 값보다 작은 순번의 ID는 폐기하라고 요청 | 19.15절, 5.1.2절 |
| RETIRE_CONNECTION_ID 프레임 | 상대가 발급한 ID를 더는 쓰지 않겠다고 알림. 새 ID를 보내 달라는 요청도 겸함 | 19.16절, 5.1.2절 |
발급하는 쪽은 상대가 쓸 수 있는 미사용 connection ID를 충분히 갖도록 해야 하고(SHOULD), 상대의 한도보다 많이 줘서는 안 된다(MUST NOT). 상대가 하나를 폐기하면 새것을 보내는 것이 원칙이다(SHOULD, 5.1.1절). 이동을 시작하는 쪽도, 상대가 이동 시 새 connection ID를 쓸 수 있도록 상대의 재고를 챙겨야 한다(SHOULD). 상대가 재고를 다 썼을 수 있다면 새 경로로 보내는 모든 패킷에 NEW_CONNECTION_ID를 넣는 방법도 RFC가 제시한다(9.5절).
로드 밸런서 뒤의 서버는 어떻게 해야 하나
출발지와 목적지의 IP 주소와 포트만 보고 서버를 고르는 로드 밸런서라면, 클라이언트 주소가 바뀐 패킷은 다른 서버로 갈 수 있다. RFC 9000 5.2.3절은 이런 배치에서 연결을 이어 갈 방법으로 두 가지를 든다.
- connection ID를 기준으로 패킷을 올바른 서버로 넘겨 주는 별도의(out-of-band) 장치를 둔다.
- 처음 접속한 주소와 다른 전용 IP 주소나 포트를 쓸 수 있다면,
preferred_address전송 매개변수로 클라이언트에게 그 주소로 옮기라고 요청한다. 클라이언트는 이 주소를 쓰지 않기로 할 수 있다.
둘 다 마련하지 못한 배치의 서버는 disable_active_migration 전송 매개변수로 이동을 지원하지 않는다고 알려야 한다(SHOULD). 이 매개변수와 관련해 알아 둘 점이 있다.
- 이 매개변수가 금지하는 것은 클라이언트가 핸드셰이크 때 쓴 서버 주소로 새 로컬 주소에서 보내는 것, 곧 능동 이동이다(18.2절). NAT 재바인딩은 클라이언트가 새 로컬 주소를 쓰는 것이 아니므로 이 금지로 막을 수 있는 대상이 아니다. 주소 변화를 감지하면 경로 검증을 해야 한다는 9장의 규칙은 그대로 적용된다. 이 부분은 RFC 문구를 이 글이 해석한 것이다.
- 클라이언트가 이 규칙을 어기면 서버는 상태 없는 리셋(Stateless Reset)을 보내지 않고 그 경로의 패킷을 버리거나, 경로 검증을 진행해 이동을 받아들이거나 둘 중 하나를 해야 한다(MUST, 9장). 상태 없는 리셋을 보내거나 연결을 닫으면 제3자가 트래픽을 위조해 연결을 끊을 수 있게 되기 때문이다.
- 클라이언트가
preferred_address에 따라 옮긴 뒤에는 이 매개변수가 이동을 금지하지 않는다. - 단순한 주소 기반 부하 분산을 쓰는 서버 배치는 상태 없는 리셋 오라클(stateless reset oracle)을 만들지 않아야 한다(MUST, 5.2.3절, 21.11절).
서버 선호 주소(preferred address)
서버 주소를 바꾸는 유일한 방법이 9.6절의 선호 주소다. RFC는 여러 서버가 함께 쓰는 주소로 처음 접속받은 뒤 연결 안정성을 위해 유니캐스트 주소로 옮기고 싶은 경우에 특히 유용하다고 적는다.
- 서버는 핸드셰이크 중
preferred_address전송 매개변수로 IPv4와 IPv6 주소를 하나씩 알릴 수 있다. 한 주소 계열만 주려면 다른 쪽을0.0.0.0:0이나[::]:0으로 채운다(18.2절). - 이 매개변수에는 순번 1인 connection ID와 상태 없는 리셋 토큰이 들어 있어서, 클라이언트가 옮길 때 쓸 미사용 connection ID가 적어도 하나 보장된다. 길이 0 connection ID를 쓰는 서버는 선호 주소를 보내면 안 된다(MUST NOT).
- 클라이언트는 핸드셰이크가 확인된 뒤 두 주소 중 하나를 골라 경로 검증을 하는 것이 원칙이고(SHOULD), 옮기기 전에 반드시 검증해야 한다(MUST, 9.6.2절). 검증이 실패하면 원래 서버 주소로 계속 보내야 한다(MUST).
- 서버는 클라이언트가 선호 주소로 보낸 비탐색 패킷을 받고 새 경로 검증까지 마칠 때까지는 원래 주소에서 비탐색 패킷을 보내야 한다(MUST).
- 선호 주소는 그것을 알려 준 연결에서만 유효하다. 이 연결에서 재개(resume)한 연결을 포함해 다른 연결에 쓰면 안 된다(MUST NOT).
정리: 구현하거나 운영할 때 확인할 것
RFC 9000에서 바로 나오는 규칙과, 그것을 바탕으로 한 이 글의 제안을 나눠 적는다.
RFC 9000의 규칙
- 주소가 바뀐 패킷이 와도 connection ID가 맞으면 같은 연결로 처리하고, 새 주소는 경로 검증으로 확인한다.
- 검증 전에는 그 주소에서 받은 양의 3배까지만 보낸다.
- 이동이 보이면 이전 경로도 PATH_CHALLENGE로 검증하고, 새 주소 검증이 실패하면 마지막으로 검증된 주소로 돌아간다.
- 새 로컬 주소나 새 목적지 주소에는 쓰지 않은 connection ID를 쓴다. 상대에게 미사용 connection ID를 넉넉히 발급해 둔다.
- connection ID로 서버를 찾아 줄 수 없는 로드 밸런서 뒤라면
disable_active_migration을 보낸다.
이 글의 제안
active_connection_id_limit이 기본값 2라면 핸드셰이크 때 쓴 ID 말고 여유분이 하나뿐일 수 있다. 이동이나 NAT 재바인딩을 자주 겪는 클라이언트를 받는 서버라면 이 값과 실제 발급 개수를 확인해 둔다.- 연결 단위 로그에는 주소만이 아니라 connection ID의 순번(sequence number)과 경로 검증 결과를 함께 남긴다. 같은 연결의 주소가 바뀐 기록을 이동, NAT 재바인딩, 검증 실패로 나눠 볼 수 있다.
참고한 RFC
- RFC 9000 — QUIC: A UDP-Based Multiplexed and Secure Transport (2021년 5월): 5.1절, 5.2절, 8장, 9장, 17.2
17.3절, 18.2절, 19.1519.18절.
이 글은 RFC 9000의 규칙만 다룬다. 링크와 내용은 2026년 10월 19일에 확인했다.