homelab89 Docs Logs Legacy Files ☰ TOC 🌓
noteistio 2026-07-09istioegressdnsgslbservice-entrytls-passthroughenvoyresolution

SE resolution(STRICT/LOGICAL) × TLS passthrough(L4) — in-flight 요청의 운명

ABSTRACT

ServiceEntry.resolution(DNS→STRICT_DNS vs DNS_ROUND_ROBIN→LOGICAL_DNS)과 트래픽 경로(L7 TLS Origination vs L4 TLS Passthrough)를 조합하면 2×2 매트릭스가 나온다. GSLB가 flip을 걸었을 때 이미 진행 중(in-flight)인 요청은 네 칸 모두에서 완주한다 — 이건 이번 실측으로 확정됐다. 이 문서가 답하는 진짜 질문은 “안 끊긴다"가 아니라 **“안 끊기는 이유가 칸마다 다른가, 그리고 요청이 끝난 다음엔 무슨 일이 벌어지는가”**다. 네 칸의 실측 근거와 그 사이의 메커니즘 차이를 정리한다.


1. 질문

SE의 resolution 설정(DNS=STRICT_DNS vs DNS_ROUND_ROBIN=LOGICAL_DNS)에 따라, TLS passthrough(L4) 경로에서 GSLB flip이 일어날 때 진행 중인(in-flight) 요청은 어떻게 처리되는가?

이 질문은 두 축의 조합이다 — resolution(STRICT/LOGICAL)과 경로(L7 origination/L4 passthrough). 답은 이 문서 전체에 걸쳐 2×2 매트릭스 하나로 완결한다(§3). “L7이 L4보다 안전한가” 같은 넓은 보호-경계론은 배경(§2)으로만 다루고 본론으로 확장하지 않는다 — 그 주제의 정본은 이미 TLS passthrough(L4) 대조 리포트에 있다. dead-IP 유실 회복(D-1/D-3의 retry 이야기)도 “L4에는 무엇이 없는지"를 보여주는 배경으로만 짧게 인용한다(§4-4).

실측/추론 표기 규칙: 이 문서의 모든 주장은 클러스터 실측(홈랩, Istio 1.30.0)이거나 그 실측에서 논리적으로 따라오는 메커니즘 추론이다. 실측하지 못한 부분은 문장 끝에 **(메커니즘 추론 — 미실측)**을 명시한다. 표기가 없으면 실측 근거가 있는 문장이다.


2. 배경 — sidecar egress의 두 경로

resolution이 Envoy cluster type을 STRICT_DNS/LOGICAL_DNS 중 무엇으로 바꾸는지의 변환 규칙 자체는 이 문서의 범위가 아니다 — ServiceEntry resolution 정본 §3이 이미 정리했다. 여기서 필요한 건 그 위에 얹히는 두 번째 축, 경로다.

client가 sidecar에 평문 HTTP로 요청을 보내고 sidecar가 TLS를 origination하면(40/41 SE + 42 VS + 43 DR 조합), Envoy는 HTTP 요청/응답의 경계를 안다. 그래서 RDS route가 붙고, 그 route 위에 connection pool이 얹히고, pool 안에서 keepalive 연결이 여러 요청에 재사용된다. 반대로 client가 처음부터 TLS를 직접 맺고 sidecar는 SNI만 보고 통과시키면(48/50 SE, protocol: TLS, VS/DR 없음), Envoy에게는 그냥 바이트 스트림이다 — SNI 매치로 filter chain을 고른 다음 바로 tcp_proxy가 raw 소켓을 1:1로 잇는다. route도, pool도 없다.

L7 — TLS Origination(평문 HTTP 진입, 40/41 SE+42 VS+43 DR) L4 — TLS Passthrough(SNI, 48/50 SE만) Client(app) — 평문 HTTP Envoy: HTTP filter chain RDS route 매치(VS42) Connection pool(keepalive) 요청/응답 경계 O Upstream(A 또는 B, pool이 아는 host) Client(app) — 직접 TLS ClientHello Envoy: SNI 매치 → tcp_proxy Route 없음 raw TCP 소켓 1:1 (pool 없음) Upstream(연결 시점 IP에 고정)
그림 1. sidecar egress의 두 경로. L7은 HTTP filter chain → RDS route → connection pool을 거쳐 "요청 경계"가 있고, L4는 SNI 매치 직후 곧바로 tcp_proxy가 raw 소켓을 1:1로 잇는다 — 중간에 pool 단계 자체가 없다.

“요청 경계가 있다/없다"는 이 문서 전체를 관통하는 유일한 구조적 차이다. pool이 있으면 그 위에 drain 유예·재사용·stale pin 같은 커넥션 수명 정책이 얹힐 자리가 생긴다. pool이 없으면 그 자리 자체가 없다 — 있고 없고의 문제가 아니라 개념이 성립하지 않는다. §4가 이 차이를 4칸에 대입해서 푼다.


3. 실측 매트릭스

L7 origination(HTTP pool) L4 passthrough(raw socket) STRICT_DNS (resolution: DNS) LOGICAL_DNS (resolution: DNS_ROUND_ROBIN) 완주 · membership 즉시 반영 destroy는 완료 후에만(설계된 drain) → 다음 새 연결은 새 IP 실측 · part2 §1 완주 · membership 지연 반영 소켓은 애초에 불간섭(destroy=자연 종료) pool 없음 → stale-pin 개념 자체가 희석(추론) 실측 · D report §5 완주 · membership Δ0 연결 pool 잔존 → stale pin 지속 가능 실측 · part2 §2 완주 · membership 개념 자체 없음 flip을 이벤트로도 안 봄 — 가장 무관심 실측 · F-0 §3~4

네 칸 모두 curl 200 · SIZE 409610B(전량) 완주 — 차이는 “요청이 끝난 다음” 연결의 운명

그림 2. resolution × 경로 2×2 매트릭스 요약. 색은 "완료 후 다음 연결의 예측 가능성"을 뜻한다 — 기본(회색 테두리)은 다음 IP가 구조적으로 결정되고, 호박색(LOGICAL×L7)은 옛 IP로 재사용될 위험(stale pin)이 실측으로 확인된 칸이다.

네 칸의 근거를 표로 펼치면 다음과 같다. 모두 40초 in-flight 스트림(/slow, payload 409610바이트) + flip(+5s) + 4점 스냅샷(BEFORE/MID1 +12s/MID2 +30s/AFTER +43s) 하니스로 측정했다.

L7 origination(HTTP, pool) L4 passthrough(SNI, raw socket)
STRICT_DNS
resolution: DNS
완주(200·409610B·body=backend-a). membership_change는 flip 반영과 함께 +1(MID1=+12s 시점에 이미 host A→B 교체 완료). upstream_cx_destroy_local요청 완료 직후에만 +1 — host가 이미 목록에서 빠졌어도 in-flight 연결은 능동적으로 drain 유예된다. 다음 신규 연결은 반드시 새 IP(backend-b)로 붙는다.
(2026-07-02_112113_dns-inflight-part2.md §1)
완주(200·409610B·최초 접속 pod 유지). membership_change지연 반영(MID1=+12s엔 아직 pod-A만 보임, MID2=+30s에야 pod-B로 교체·membership_change +1). destroy도 요청 자연 종료 시에만 +1 — 단 이건 “유예"가 아니라 raw 소켓이 애초에 CDS/EDS 갱신과 무관하게 살아있었을 뿐이다.
(2026-07-09_134250_dns-passthrough-tcp.md §5, D-2)
LOGICAL_DNS
resolution: DNS_ROUND_ROBIN
완주(200·409610B·body=backend-a, 2/2 결정론적). membership_change는 BEFORE→AFTER 전 구간 Δ0(논리 host 1개 유지, flip을 이벤트로 취급 안 함). upstream_cx_active=1이 AFTER(+43s)에도 그대로 남는다 — destroy 자체가 관측 창 안에서 발생하지 않는다(연결이 pool에 잔존, 재사용 대기 → stale pin 지속 가능).
(2026-07-02_112113_dns-inflight-part2.md §2)
완주(200·409610B·최초 접속 pod 유지, 2/2 결정론적). membership_change는 BEFORE→AFTER 전 구간 11→11 고정 — flip이 반영돼도 “논리적으로는 여전히 host 1개"라 membership 갱신 개념 자체가 없다. admin의 endpoint 표시 IP는 MID1(+12s)에 이미 새 IP로 갱신되지만, 이건 다음 연결이 참조할 해석 캐시가 바뀐 것이지 membership 이벤트가 아니다.
(2026-07-09_144251_dns-passthrough-logical-inflight.md §3~4, F-0)

4. 칸 사이 차이의 메커니즘

flip 이슈(A→B, t=+5s)

STRICT_DNS × L7 origination (HTTP pool, 40/41+42+43) membership +1 destroy_local +1(완료 후)

LOGICAL_DNS × L7 origination (HTTP pool, 40/41+42+43) membership_change Δ0(전 구간) destroy 없음 → pool 잔존(재사용 대기)

STRICT_DNS × L4 passthrough (raw socket, 48 SE만) membership +1(지연, +12~+30s) destroy +1(자연 종료)

LOGICAL_DNS × L4 passthrough (raw socket, 50 SE만) membership_change Δ0(개념 없음) destroy +1(자연 종료)

0 +5s flip +12s MID1 +30s MID2 +43s AFTER
그림 3. 40초 in-flight 스트림 동안의 membership/destroy 이벤트 타임라인. 점(●)은 실제로 카운터가 틱한 지점이다 — L7 STRICT만 flip 직후(+12s 이내)에 membership이 갱신되고, 나머지 세 레인은 그보다 늦거나(L4 STRICT) 아예 갱신되지 않는다(LOGICAL 둘 다). destroy는 네 레인 중 셋에서 정확히 스트림이 끝나는 시점(+43s)에만 틱하고, LOGICAL×L7 레인만 관측 창 안에서 전혀 틱하지 않은 채 연결이 이어진다.

4.1 네 칸 모두 생존하지만, 보증 수준은 다르다

in-flight 생존은 네 칸 전부에서 성립한다(§3). 하지만 그 생존이 무엇에 의한 것인지가 다르다.

  • L7 origination(둘 다): Envoy가 요청 경계를 안다. connection pool이 “이 연결 위에 아직 활성 요청이 있다"를 추적하고, host가 제거돼도 그 요청이 끝날 때까지 destroy를 미룬다. 이건 설계된 동작이다 — HTTP1.1 keepalive·HTTP2 GOAWAY류의 graceful shutdown과 같은 계열.
  • L4 passthrough(둘 다): tcp_proxy는 한 번 맺은 커널 소켓을 그냥 릴레이할 뿐이다. CDS/EDS가 나중에 그 host를 지워도 이미 진행 중인 연결과는 애초에 무관하다 — 정리할 지점 자체가 없다. D report는 이 차이를 정확히 짚었다: “우연히 안전한 것이지 설계로 안전한 게 아니다” (2026-07-09_134250_dns-passthrough-tcp.md §5.3).

결과의 유사성(둘 다 완주)에 속아 “L4도 L7만큼 안전하다"고 일반화하면 안 된다. 완료까지 살아남는 것완료 후에도 신뢰할 수 있는 장애 복구 체계를 갖추는 것은 별개 질문이고, 후자는 §4.4에서 갈린다.

4.2 L4엔 재사용 pool이 없다 — LOGICAL의 stale pin이 구조적으로 희석된다

L7×LOGICAL 칸의 진짜 위험은 “이번 요청이 안 끊긴다"가 아니라 “다음 요청도 같은(옛) 커넥션을 재사용해서 계속 backend-a로 간다“는 stale pin이다 — 이건 connection pool이 있기 때문에 생기는 문제다. D report가 명시적으로 확인한 사실은 “TCP passthrough엔 connection pool 자체가 없다” (2026-07-09_134250_dns-passthrough-tcp.md §5.3)는 것이다. tcp_proxy는 다운스트림 연결마다 새 업스트림 소켓을 1:1로 여는 구조라, HTTP처럼 “여러 요청이 하나의 커넥션을 keepalive로 나눠 쓰는” 개념 자체가 없다.

이 사실에서 따라오는 결론 — L4에서는 매 신규 연결마다 그 시점의 DNS/EDS 해석을 새로 타므로, L7×LOGICAL에서 본 stale pin 문제가 구조적으로 희석된다. 이 결론의 골격은 후속 실측 (2026-07-09, repo docs/test-reports/2026-07-09_204354_dns-passthrough-keepalive-pin.md)으로 확정됐다 — 단, pin이 사라지는 게 아니라 위치가 옮겨간다는 형태로: 앱이 하나의 TLS 연결을 keepalive로 유지하면 flip 후에도 후속 요청이 계속 옛 backend로 가고(앱 연결 레벨의 pin, 2런 동일·upstream_cx_total 교차 증명), 연결을 닫고 새로 맺는 순간 새 backend로 간다. 즉 Envoy pool의 pin은 구조적으로 없지만 앱 커넥션 재사용이 같은 효과를 만든다. flip 이후 새로 여는 L4 연결이 정확히 몇 초 만에 새 IP를 따라가는지(반영 지연의 시간 분해능)는 여전히 실측 범위 밖이다.

4.3 이 매트릭스가 단일 IP flip만 쓴 이유 — 당시의 NACK 우려는 실측으로 오류 판명

네 칸 모두 flip은 “단일 IP → 단일 IP"로만 걸었다. 당시엔 LOGICAL_DNS cluster가 lb_endpoint 1개여야 한다는 제약 때문에, A record가 다중 IP가 되는 순간 이 SE를 받는 프록시가 CDS push를 통째로 NACK할 수 있다고 우려해(같은 push의 listener까지 드롭 — DNS/GSLB 재현 랩 §7, 당시 관찰) 일부러 회피한 설계였다.

정정(2026-07-10) — 2026-07-09 실측(repo docs/test-reports/2026-07-09_211314-dns-logical-multia.md)으로 이 우려는 오류로 판명됐다. 뒤섞여 있던 것은 사실 서로 다른 두 메커니즘이었다:

  • 런타임 DNS가 실제로 복수 A레코드를 응답하는 경우(이 매트릭스가 다루는 GSLB 시나리오 그 자체) — LOGICAL_DNS는 NACK 없이 정상 동작했다(cds/lds update_rejected 델타 0, istiod 로그에 nack/reject 매치 없음). 다만 pilot-agent request GET clusters에 잡히는 cluster 멤버는 dig 응답의 첫 IP 하나뿐이었고, curl 15회 전부 그 IP로만 갔다 — 나머지 IP는 cluster 멤버 목록에 아예 나타나지 않은 채 조용히 무시됐을 뿐, 통신은 안 깨진다.
  • SE.spec.endpoints에 여러 개를 명시적으로 선언하는 경우(이 매트릭스와는 다른 설정 패턴) — 이때만 kubectl apply --dry-run=server 단계에서 admission webhook이 즉시 거부한다(The ServiceEntry ... is invalid: ... DNS_ROUND_ROBIN mode cannot have multiple endpoints). CDS/xDS까지 가지도 않으므로 NACK이 날 지점 자체가 없다.

즉 이 매트릭스 자체(단일 IP → 단일 IP flip 실측)는 여전히 유효하다 — 다만 “GSLB가 실제로 멀티 IP A record를 돌려주는 환경이라면 LOGICAL_DNS를 쓸 수 없다"는 전제가 틀렸다. LOGICAL은 그런 환경에서도 쓸 수 있지만(admission 거부는 없음), 첫 IP만 쓰므로 GSLB가 의도한 분산·페일오버를 실현하지 못한다는 것이 정확한 결론이다 — 상세는 결정 브리프 §3 근거 2 참조.

4.4 dead-IP 구제(L7 기본 retry)는 L4에 없다 — 배경으로 짧게

이 문서의 중심은 in-flight(§3~4.3)이지만, “요청이 끝난 뒤"의 이야기를 완전히 빼면 왜 이 차이가 프로덕션에서 중요한지 감이 안 온다. D-1(같은 40초 창, 같은 dead-IP 조건)에서 L7 경로는 connect-fail 자동 재시도로 46/46 성공(유실 0%)했지만, L4 passthrough는 route 자체가 없어 이 재시도가 걸릴 자리가 없어 18/41(43.9%)이 client에게 그대로 실패로 전달됐다(2026-07-09_134250_dns-passthrough-tcp.md §3). outlier detection을 걸면 43.9%→0.21%까지 떨어지지만 정확히 0엔 안 닿는다(D-3, 같은 문서 §6). in-flight는 4칸 모두 보호되지만, 연결이 끊어진 뒤 새로 맺으려는 시도는 L4에서 훨씬 더 무방비다 — 자세한 수치와 원인은 원본 리포트를 참조.

4.5 공식 문서와의 대조 — drain 의미론 확인

§3~4.4가 실측으로 확정한 “STRICT는 host가 빠져도 in-flight는 안 끊고 완료 후에만 폐쇄한다”, “LOGICAL은 아예 개입하지 않는다"는 결과가 우연이 아니라 Envoy의 service discovery 아키텍처 문서에 그대로 명문화돼 있다는 것을 이 절에서 확인한다.

Strict DNS — “Envoy will continuously and asynchronously resolve the specified DNS targets. Each returned IP address in the DNS result will be considered an explicit host in the upstream cluster. … If a host is removed from the result Envoy assumes it no longer exists and will drain traffic from any existing connection pools. … Note that Envoy never synchronously resolves DNS in the forwarding path.”

→ DNS 대상을 지속적·비동기적으로 해석한다. 반환된 각 IP는 cluster의 명시적 host로 간주된다. host가 결과에서 사라지면 더 이상 존재하지 않는 것으로 간주하고 기존 connection pool로부터 트래픽을 drain한다. Envoy는 포워딩 경로에서 절대 동기적으로 DNS를 해석하지 않는다.

Logical DNS — “…a logical DNS cluster only uses the first IP address returned when a new connection needs to be initiated. Thus, a single logical connection pool may contain physical connections to a variety of different upstream hosts. Connections are never drained, including on a successful DNS resolution that returns 0 hosts. With logical DNS, connections stay alive until they get cycled.”

새 연결을 시작해야 하는 시점에 반환돼 있는 첫 번째 IP만 사용한다. 그래서 하나의 논리적 connection pool 안에 서로 다른 실제 host로의 물리 연결들이 공존할 수 있다. 연결은 절대 drain되지 않는다 — DNS가 host 0개를 반환한 경우조차. 연결은 순환(교체)될 때까지 살아있다.

동사가 “close"가 아니라 “drain"이다. §3의 STRICT×L7 칸에서 destroy_local이 flip 시점이 아니라 요청 자연 종료 직후에만 틱한 것(§14-4/part2 §1 원표)이 바로 이 “drain” 동작 그 자체다 — 새 트래픽 배정은 즉시 멈추고(membership에서 제거), 이미 진행 중인 사용은 끝날 때까지 기다렸다가 닫는다. 문서 문장 하나에 실제로는 시점이 다른 두 사건이 들어 있는 셈이다: ① host 목록(라우팅 후보) 갱신은 즉시, ② 기존 연결의 처분(drain)은 정중하게. 이 구분을 놓치면 “host 제거 = 그 자리에서 연결 강제 종료"로 오독하기 쉽다.

close_connections_on_host_set_change가 opt-in이라는 증거: 기본 동작이 “host 변경 시 즉시 강제 정리"였다면 이 필드가 존재할 이유가 없다. cluster.proto의 CommonLbConfig는 이렇게 정의한다:

close_connections_on_host_set_change — “If set to true, the cluster manager will drain all existing connections to upstream hosts whenever hosts are added or removed from the cluster.” (기본값 false)

true로 설정하면, host가 cluster에 추가되거나 제거될 때마다 cluster manager가 기존 연결을 전부 drain한다. (기본값은 false.)

이 랩의 SE(40/41/48/50)는 전부 이 필드를 건드리지 않았다 — 즉 §3의 모든 관측은 기본값(false)에서 나온 결과다. 기본값이 이미 “즉시 강제 정리"라면 true로 바꿀 때 새로 생기는 동작이 없어야 하는데, 필드 설명 자체가 “drain all existing connections"를 true일 때의 효과로 명시한다 — 이는 기본값에서는 이 즉시-drain이 일어나지 않는다는 뜻이고, §3에서 실측한 “완료까지 유예"와 정확히 들어맞는다.

“질의(query) vs 소비(consume)” 구분 — §4.2의 근거: 공식 문서는 STRICT/LOGICAL 모두 “continuously and asynchronously resolve"한다고 말한다 — 질의 주기는 두 타입이 같다. 갈리는 지점은 그 결과를 언제 반영하는가다.

  • STRICT: 백그라운드 질의 결과가 도착하는 즉시 host 목록 자체를 교체한다 → §3에서 관측한 membership_change가 이 반영이다.
  • LOGICAL: 결과를 캐시해 두고, “새 연결을 시작해야 하는 시점"에만 그 캐시의 첫 IP를 참조한다 — 연결을 맺는 순간 DNS를 동기적으로 다시 묻는 게 아니다(“Envoy never synchronously resolves DNS in the forwarding path"는 STRICT/LOGICAL 공통 제약이다). §3의 LOGICAL×L7/L4 두 칸에서 membership_change가 각각 Δ0·11→11 고정으로 나온 것은 “질의를 안 해서"가 아니라 “질의 결과를 소비할 새 연결이 이 관측 창 안에서 발생하지 않아서” 다 — 이미 맺어진 연결은 질의 결과와 무관하게 그대로 유지된다.

“cycled” 해석: “connections stay alive until they get cycled"의 cycled는 Envoy가 능동적으로 갈아타는 이벤트가 아니라, 연결의 자연스러운 수명 종료(원격 keepalive timeout, idle timeout, 클라이언트 종료, 연결당 요청 수 상한 등)로 죽은 다음에 다음 새 연결이 그 시점의 최신 캐시를 참조하는 것을 말한다. §3-LOGICAL×L7 런 사이에서 관측한 “run1이 pool에 남긴 backend-a 연결을 8분 뒤 원격 nginx keepalive timeout이 닫은” 사례(part2 §2 “런 간 노이즈”)가 바로 이 cycling이다 — Envoy가 GSLB 신호를 보고 갈아탄 게 아니라, 연결이 다른 이유로 죽자 그 다음 연결이 마침 새 IP를 물었을 뿐이다.


5. 실측 함정 콜아웃 — ClusterIP 섀도잉

DNS가 Service ClusterIP를 가리키면 이 매트릭스 전체가 조용히 무효화된다

L4 passthrough SE(48/50)의 host를 backend Service의 ClusterIP로 두면 curl은 200을 반환하는데 passthrough cluster의 카운터(upstream_cx_total)는 전혀 늘지 않는다 — Istio sidecar가 in-mesh Service ClusterIP마다 만들어 둔 전용 리스너SO_ORIGINAL_DST로 목적지를 가로채, 0.0.0.0:443의 SNI 캐치올보다 항상 먼저 매치되기 때문이다. §3~4의 모든 L4 측정은 DNS를 backend pod IP로 돌려서 이 함정을 피한 뒤에 나온 값이다. 겉으로는 전혀 티가 안 나는 함정이라(curl 200), GSLB/외부 도메인을 흉내내는 랩·테스트 환경 전반에서 재발할 수 있다 — 상세 메커니즘과 해결책은 TLS passthrough 대조 리포트 §2, DNS/GSLB 재현 랩 §8-3에 있다.


6. 프로덕션 시사점

조합 조심할 것
STRICT × L7 기본값처럼 다루기 좋다 — flip 시 in-flight는 보호되고, 완료 후엔 반드시 새 IP로 넘어간다. GSLB 추종이 빠르고 예측 가능해야 하는 경로에 적합.
LOGICAL × L7 in-flight는 안전하지만, 완료된 연결이 pool에 남아 다음 요청도 옛 IP로 갈 수 있다(stale pin). GSLB가 장애 failover 목적이면 이 지연이 치명적일 수 있다 — pinned backend가 죽어야만 재연결이 강제된다(재현 랩 mode3 참조).
STRICT × L4 in-flight는 안전하지만 membership 반영 자체가 client 쪽 DNS 갱신보다 느리다(+12s에도 아직) — 운영 중 “flip이 반영 안 됐다"고 오판하기 쉽다. 신뢰할 진단은 client dig가 아니라 cluster 레벨 proxy-config endpoints/membership_change.
LOGICAL × L4 in-flight는 안전하지만 membership이라는 신호 자체가 없다 — flip을 관측하려면 endpoint 표시(해석 캐시)를 봐야 하고, “membership이 안 바뀌었으니 flip이 안 됐다"는 판단은 틀렸다.
L4 passthrough 전반(둘 다) dead-IP 재시도가 sidecar 레벨에 없다(§4.4) — outlier detection + 클라이언트 자체 재시도를 반드시 별도로 갖춰야 한다. “TLS passthrough니까 사이드카가 알아서 해줄 것"이라는 가정은 이번에도 틀렸다.

참조

아카이브 내부

원시 리포트(repo)

  • docs/test-reports/2026-07-02_112113_dns-inflight-part2.md — L7 STRICT/LOGICAL in-flight 4점 스냅샷(§3 매트릭스 좌측 열의 출처)
  • docs/test-reports/2026-07-09_134250_dns-passthrough-tcp.md — L4 STRICT 대조(D-1/D-2/D-3, §3 매트릭스 우측 상단 및 §4.4의 출처)
  • docs/test-reports/2026-07-09_144251_dns-passthrough-logical-inflight.md — L4 LOGICAL in-flight(F-0, §3 매트릭스 우측 하단의 출처, 매트릭스 마지막 칸)
  • docs/test-reports/2026-07-01_dns-resolution.md §14 — 종합 리포트(위 세 리포트를 흡수해 재정리)

다운로드 가능 사본(fileshare)

Files