Platform Engineering · Protocol Deep Dive

HTTPS_PROXY 통신 흐름 실측 가이드
CONNECT 이후 · 연결 재사용 · 재현 랩

CONNECT 응답 200 이후 앱과 서버 사이에 정확히 무엇이 오가는지, 그리고 "연결을 재사용한다"는 말이 어느 층의 이야기인지를 실제 Squid로 측정해 정리한 문서입니다. 모든 출력은 재현 가능한 랩에서 캡처한 실측값이며, 마지막 파트에 그 랩을 직접 구성하는 방법을 담았습니다.

측정 환경 Squid 6.14 · curl 8.5 · TLS 1.3 모든 수치 실측 선행 문서 Outbound 집약 설계서
색 규약 — amber · 프록시가 읽을 수 있는 구간 teal · 프록시에게 불투명한 구간
Part 00

세 줄 요약

  1. CONNECT 이후에도 소켓은 새로 열리지 않는다. 프록시에 연결했던 그 TCP 하나 위에서 앱↔프록시의 평문 대화(전반)와 앱↔origin의 암호화 대화(후반)가 시간순으로 겹쳐 흐른다. Squid는 후반부에서 화자가 아니라 전선이다.
  2. "HTTPS 연결"은 요청 단위가 아니라 TLS 세션 단위다. TCP·터널·TLS 세션은 한 몸으로 태어나 요청 N개를 실어 나른다. 재사용이란 TLS를 생략하는 게 아니라 이미 성립한 세션 위에 다음 요청을 얹는 것이다.
  3. 터널 수를 정하는 건 Squid가 아니라 앱과 origin이다. keep-alive면 1터널, Connection: close면 요청마다 새 터널·새 ACL 심사·새 로그. HTTP/1.1의 동시 요청은 반드시 터널을 늘리고, HTTP/2는 스트림으로 접어 넣는다.
왜 이게 중요한가

A안(앱이 CONNECT 유지)에서 egress gateway의 Envoy는 TCP 프록시이므로 downstream 연결 1개당 upstream 연결 1개를 1:1로 맺습니다. 따라서 앱의 커넥션 풀 설정이 gateway의 동시 연결 수와 ephemeral port 소모량을 그대로 결정합니다. 용량 산정의 기준 단위가 "요청 수"가 아니라 "동시 터널 수"여야 하는 이유입니다.

Part 01

CONNECT 이후 — 소켓 하나, 대화 둘

흔한 오해는 "CONNECT로 터널이 열리면 앱이 그 터널이라는 새 통로에 다시 연결한다"는 그림입니다. 실제로는 새로 열리는 것이 없습니다. 앱은 프록시에 연결했던 같은 file descriptor에 계속 쓰고 읽을 뿐이고, 200 응답을 기점으로 거기에 쓰는 내용의 성격만 바뀝니다.

App · fd=3 Squid :3128 Origin :443 평문 — squid가 읽고 판단 CONNECT api.demo.test:443 200 Connection established 같은 fd=3 — squid는 바이트만 복사 0x16 ClientHello — SNI만 평문 인증서 CN=api.demo.test 0x17 GET /v1/pay — 암호화됨 0x17 응답 — 암호화됨 close_notify + FIN 종료 시점에 로그 1줄 — TCP_TUNNEL/200, 총 바이트
그림 1 · 200을 기점으로 같은 소켓 위에서 화자가 바뀐다
핵심

ACL 심사는 터널을 열 때 딱 한 번(그림의 평문 국면)이고, 그 뒤로 터널에 무엇이 몇 개나 흐르든 Squid는 관여하지 않습니다. 이 성질이 Part 03~04의 모든 결과를 설명합니다.

Part 02

바이트로 확인하기

아래는 curl -v가 아니라 raw socket + ssl.MemoryBIO소켓에 실제로 나가는 바이트를 찍은 결과입니다(스크립트는 Part 05). 대상은 실제 Squid(:3128)와 실제 TLS origin(api.demo.test:443, CN=api.demo.test).

[1] TCP 연결   fd=3   ('127.0.0.1', 45484) -> ('127.0.0.1', 3128)
[2] fd=3 에 평문 63B 씀:
       CONNECT api.demo.test:443 HTTP/1.1
       Host: api.demo.test:443
[3] 프록시 응답: HTTP/1.1 200 Connection established        (fd=3 그대로)

[4] 같은 fd=3 위에서 TLS 핸드셰이크 — 여기서부터 squid 에게 불투명
    →   517B  0x16 handshake              SNI 평문 포함!
           hex: 16 03 01 02 00 01 00 01 fc 03 03 1e 8c 0d 2d 44 ...
    ←  1387B  0x16 handshake
           hex: 16 03 03 00 7a 02 00 00 76 03 03 07 49 11 fe 0b ...
    →    80B  0x14 change_cipher_spec
    협상 결과: TLSv1.3 / TLS_AES_256_GCM_SHA384
    origin 인증서 subject: CN=api.demo.test   <- 앱이 터널 너머 origin 을 직접 검증

[5] 앱이 쓴 평문 GET 72B  ->  와이어에는 94B  0x17 application_data
           hex: 17 03 03 00 59 09 95 36 8f 59 b4 e4 1b 2b 06 c9 ...
           와이어에서 '/v1/pay' 문자열이 보이나?   False
           와이어에서 'api.demo.test' 가 보이나?  False
    ←   510B  0x17 application_data       응답도 암호화된 레코드
[6] 복호화한 응답 첫 줄: HTTP/1.1 404 File not found
[7] 이 전체 대화가 오간 fd = 3  (처음 프록시에 연결한 그 소켓)

2.1 읽는 법

단계관측의미
[3]→[4] 경계fd 번호가 그대로, 곧장 16 03 01새 연결 없음. 0x16=handshake 레코드 타입. 이 순간부터 Squid에겐 해석 불가능한 바이트 스트림
[4] ClientHello517B 안에 api.demo.test 문자열 존재암호 협상 이라 SNI는 가려지지 않음. Squid의 peek 모드와 egress gateway의 SNI 제어가 붙잡는 게 정확히 이 바이트
[4] 인증서subject가 origin의 것검증 주체는 앱. issuer가 사내 CA면 경로 어딘가에서 bump(MITM) 중이라는 신호
[5] 요청평문 72B → 와이어 94B, 경로 문자열 소멸차이 22B = 레코드 헤더 5B + AEAD 태그 16B + 콘텐츠 타입 1B. "Squid는 경로를 모른다"의 실증

2.2 origin이 받은 것

TLS origin 의 액세스 로그:
"GET /v1/pay?user=123 HTTP/1.1"     ← origin-form. 프록시의 흔적이 없음

터널 안쪽은 프록시가 존재하지 않는 세계입니다. origin 입장에서 이 요청은 평범한 직접 연결과 구분되지 않습니다. (대조: 평문 http:// 요청은 Squid가 absolute-form을 파싱해 origin-form으로 재작성해 보냅니다.)

2.3 Squid가 남긴 기록 — 한 줄

1784243300.392   22   127.0.0.1   TCP_TUNNEL/200   2500   CONNECT api.demo.test:443   -   HIER_DIRECT/192.0.2.2   -
                 └22ms            └터널로 처리     └총 2500B (핸드셰이크+요청+응답 전부 합산)     └squid가 해석한 IP

2.4 실패 케이스 — 로그는 깨끗한데 앱은 실패

같은 요청을 인증서를 신뢰하지 않는 상태로(-k 없이) 보내면:

앱:   < HTTP/1.1 200 Connection established
      * SSL certificate problem: self-signed certificate
      curl: (60) SSL certificate problem

squid access.log:  TCP_TUNNEL/200  1384  CONNECT api.demo.test:443  HIER_DIRECT/192.0.2.2   ← 정상 200
운영 함정

Squid는 200을 주고 정상 터널로 기록했는데 앱만 실패했습니다. TLS 검증은 앱과 origin 사이의 일이라 프록시 로그에 흔적이 남지 않습니다. "Squid 로그는 문제없다는데 앱은 안 된다"의 대표 원인 — 사내 CA 미배포, 인증서 만료, SAN 불일치, TLS 버전 미스매치가 전부 이 패턴입니다. 진단은 프록시 로그가 아니라 앱 쪽 curl -v의 subject/issuer 줄에서 해야 합니다.

Part 03

연결 재사용과 네 개의 층

"연결을 재사용한다"가 헷갈리는 이유는 "연결"이라는 한 단어가 서로 다른 네 개의 층을 가리키기 때문입니다. 각 요청이 HTTPS라고 해서 요청마다 새 연결이 필요한 게 아닙니다 — "HTTPS 연결"은 요청 단위가 아니라 TLS 세션 단위이고, TLS 세션 하나가 요청 여러 개를 실어 나릅니다. 재사용이란 TLS를 생략하는 게 아니라, 이미 성립한 TLS 세션 위에 다음 요청을 얹는 것입니다.

무엇의 단위인가실측에서 개수
TCP 연결 (소켓 fd)앱↔Squid 물리 배관1
CONNECT 터널Squid의 ACL 심사 · access.log 1줄1
TLS 세션핸드셰이크 · 인증서 검증1
HTTP 요청GET /a, GET /b, GET /c3

위 세 층은 함께 태어나 함께 죽습니다(터널이 곧 TCP이고, 그 위에 TLS 세션 하나). 맨 아래 층만 N개입니다.

TCP 연결 1개 · fd=3 · app ↔ squid 평문 — CONNECT host:443 → 200 · ACL 심사는 여기서 딱 1회 TLS 세션 1개 · app ↔ origin 핸드셰이크 · 인증서 검증도 1회 GET /a 요청 1 GET /b 요청 2 GET /c 요청 3
그림 2 · 층위 — 위 세 겹은 1개, 요청만 N개

3.1 배경 ① keep-alive — 요청은 연결의 단위가 아니다

HTTP/1.0 시절엔 요청 하나마다 TCP를 열고 닫았습니다("요청 = 연결"이 진짜였던 시대). HTTP/1.1부터 persistent connection이 기본이 되어, 응답을 받아도 연결이 살아있고 다음 요청이 그 위로 흐릅니다. Connection: close가 있어야 닫힙니다.

이건 프록시와 무관한 HTTP 자체의 성질인데, 프록시가 끼면 그 성질이 터널에 그대로 상속됩니다. 앱이 연결을 유지하면 터널도 유지되고, 앱이 닫으면 터널도 닫힙니다. 터널의 수명은 Squid가 아니라 앱과 origin이 정합니다.

3.2 배경 ② connection pool의 키

앱의 HTTP 라이브러리는 쓰고 난 연결을 버리지 않고 풀(pool)에 담아둡니다. 다음 요청이 오면 (scheme, host, port, proxy) 조합을 키로 재사용 가능한 연결을 찾습니다. 프록시를 쓸 때 이 키가 중요한 이유는 목적지가 다르면 터널을 공유할 수 없기 때문입니다 — 같은 Squid를 지나도 api.a.com용 터널과 api.b.com용 터널은 별개입니다. CONNECT가 열어준 파이프의 반대편은 이미 특정 origin에 고정돼 있으니까요.

3.3 배경 ③ HTTP/1.1은 동시에 하나, HTTP/2는 다중화

HTTP/1.1은 한 연결에서 요청 하나가 끝나야 다음 요청을 보낼 수 있습니다. 따라서 재사용은 순차적일 때만 가능하고, 동시 요청 N개는 반드시 연결 N개 = 터널 N개가 됩니다. HTTP/2는 하나의 연결 안에 여러 스트림을 다중화하므로 터널 하나로 접힙니다.

Part 04

재사용 매트릭스 — 5케이스 실측

같은 3요청을 조건만 바꿔가며 측정했습니다. 측정 원리는 터널 1개 = access.log 1줄입니다.

케이스조건요청터널관측
A순차 · keep-alive · 같은 호스트31137ms, 7221B 합산
B순차 · Connection: close333265B, 1135B, 3891B
C병렬 · HTTP/1.1 · 같은 호스트33멀티플렉싱 불가
D순차 · 서로 다른 호스트22풀 키가 다름
E병렬 · HTTP/231149ms, 5803B

A와 B의 차이 — 질문의 직접적인 답

요청은 똑같이 3개인데, 앱이 연결을 유지하느냐 닫느냐에 따라 TLS 핸드셰이크가 1번이냐 3번이냐가 갈립니다. B에서는 매번 새 CONNECT → 새 ACL 심사 → 새 TLS 핸드셰이크 → 새 로그 줄입니다. "각 요청이 별도의 HTTPS 연결"인 상황이 정확히 B입니다.

C와 E의 대비 — HTTP/2 멀티플렉싱

E 케이스의 클라이언트 트레이스:
> CONNECT example.com:443 HTTP/1.1        ← 터널은 딱 하나
* ALPN: server accepted h2
* [HTTP/2] [1] [:path: /a]
* Re-using existing connection with proxy squid.test
* [HTTP/2] [3] [:path: /b]                ← stream 3
* Re-using existing connection with proxy squid.test
* [HTTP/2] [5] [:path: /c]                ← stream 5

squid access.log:  149ms  TCP_TUNNEL/200  5803B  CONNECT example.com:443    ← 1줄

터널 하나 안에서 stream 1·3·5가 동시에 흐르고 Squid는 로그 한 줄만 남깁니다. 프록시 구간은 HTTP/1.1인데 터널 내부는 h2라는 사실이 여기서 실질적 의미를 가집니다 — 터널이 순수 TCP라 안에서 무엇을 하든 상관없기 때문입니다.

4.1 설계 함의

앱의 동시 터널 수 = gateway 의 동시 downstream = gateway→Squid 동시 연결 수

A안에서 egress gateway의 Envoy는 TCP 프록시입니다. TCP 스트림은 풀링이 불가능하므로 (바이트 순서가 그 커넥션에 묶여 있으니) downstream 연결 1개당 upstream 연결 1개를 1:1로 맺습니다. 따라서 앱의 커넥션 풀 설정이 gateway의 ephemeral port 소모량을 그대로 결정합니다. Connection: close를 쓰거나 풀이 작은 앱이 많으면 터널이 계속 새로 뚫리고, h2를 쓰는 앱은 요청 수천 개가 터널 하나로 접혀 들어갑니다.

놓치기 쉬운 것

Squid 로그로 API 호출 횟수를 세면 틀립니다. 로그 줄 수는 터널 수이지 요청 수가 아닙니다 — A에서 3요청이 1줄, h2 앱이면 수천 요청이 1줄. 감사·과금·이상탐지를 로그 줄 수 기반으로 하고 있다면 이미 어긋나 있을 수 있습니다.

TLS session resumption ≠ 터널 재사용. 새 터널이 필요한 상황에서도 session ticket으로 핸드셰이크 왕복은 줄일 수 있지만, CONNECT·ACL 심사·로그 줄은 그대로 새로 발생합니다. 다른 층의 최적화입니다.

풀 기본값은 런타임마다 다릅니다. Go의 MaxIdleConnsPerHost 기본값은 2라서, 동시성이 높은 Go 앱은 유휴 연결을 버리고 계속 새 터널을 뚫는 패턴이 나오기 쉽습니다. 전환 후 gateway 커넥션 수가 예상보다 많다면 앱의 풀 설정부터 보는 게 순서입니다.

Part 05

재현 랩 구성

설계 원칙

사내 Squid로는 이 실험을 할 수 없습니다. 목적지가 남의 서버면 "origin이 실제로 무엇을 받았는지"를 볼 수 없고, ACL을 바꿔볼 수도, 실패를 일부러 만들 수도 없습니다. 랩의 조건은 클라이언트·프록시·origin 세 지점을 전부 관측 가능하게 만드는 것입니다.

curl / python 관측 ① 앱이 쓴 것 Squid :3128 관측 ② access.log 가짜 프록시 :3129 받기만 함 — 원문 바이트 TLS origin :443 관측 ③ origin 이 받은 것 평문 origin :8080 absolute-form 재작성 확인 가짜 프록시(붉은 점선)는 verify-01 전용 — 진짜 squid 는 요청을 처리해버려 원문을 볼 수 없다
그림 3 · 랩의 부품과 세 개의 관측 지점

5.1 lab-setup.sh

멱등(idempotent)하게 작성 — 몇 번을 실행해도 안전합니다.

LAB=/opt/proxylab
apt-get install -y squid openssl curl

# 이름 등록 — 컨테이너가 /etc/hosts 를 리셋할 수 있어 매번 재확인
grep -q ' squid.test$'     /etc/hosts || echo "127.0.0.1 squid.test"      >> /etc/hosts
grep -q ' api.demo.test$'  /etc/hosts || echo "$ORIGIN_IP api.demo.test"  >> /etc/hosts
grep -q ' api2.demo.test$' /etc/hosts || echo "$ORIGIN_IP api2.demo.test" >> /etc/hosts

# ACL — deny 를 반드시 'allow localhost' 앞에 (first-match)
sed -i "s|^http_access allow localhost$|acl blocked_domains dstdomain .badsite.test\nhttp_access deny blocked_domains\nhttp_access allow localhost|" /etc/squid/squid.conf

# origin 인증서 — SAN 에 두 이름 다 넣어야 D 케이스(다른 호스트)가 성립
openssl req -x509 -newkey rsa:2048 -nodes -days 30 \
  -keyout $LAB/demo.key -out $LAB/demo.crt \
  -subj "/CN=api.demo.test" \
  -addext "subjectAltName=DNS:api.demo.test,DNS:api2.demo.test"

start_bg() {                      # $1=확인할 포트, 나머지=명령
  local port="$1"; shift          # pgrep 대신 포트 확인이 확실함
  (timeout 1 bash -c "</dev/tcp/127.0.0.1/$port") 2>/dev/null && return 0
  nohup setsid "$@" </dev/null >>"$LAB/bg.log" 2>&1 &
}
start_bg 8080 python3 -u -m http.server 8080
start_bg 443  python3 -u "$LAB/origin_tls.py" "$LAB/demo.crt" "$LAB/demo.key"
pgrep -x squid >/dev/null && squid -k reconfigure || squid
sleep 5                           # squid 는 바인딩까지 3~5초

origin_tls.py — 두 줄이 결정적

import http.server, ssl, sys
h = http.server.SimpleHTTPRequestHandler
h.protocol_version = "HTTP/1.1"        # 없으면 keep-alive 불가 = 재사용 실험 자체가 성립 안 함
srv = http.server.ThreadingHTTPServer(("0.0.0.0", 443), h)   # 단일 스레드면 교착 (함정 ③)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(sys.argv[1], sys.argv[2])
srv.socket = ctx.wrap_socket(srv.socket, server_side=True)
srv.serve_forever()

5.2 검증 3종 — 무엇을 증명하는가

스크립트방법증명하는 것
verify-01-forms.sh가짜 프록시로 요청 라인 원문 캡처request-target 세 형태(origin / absolute / authority)는 "읽는 상대"가 결정한다
verify-02-tunnel.pyraw socket + ssl.MemoryBIOTLS는 앱↔origin end-to-end이고 Squid는 바이트의 의미를 모른다
verify-03-reuse.sh조건별로 access.log 줄 수 카운트터널 수를 정하는 건 앱과 origin이다 (5케이스 매트릭스)

verify-01 — 가짜 프록시 트릭

진짜 Squid는 요청을 받아 즉시 처리해버리므로 "앱이 무엇을 보냈나"를 원문으로 보려면 받기만 하고 아무것도 안 하는 서버가 필요합니다.

import socket, sys
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", int(sys.argv[1]))); srv.listen(1)
conn, _ = srv.accept(); conn.settimeout(2)
sys.stdout.write(conn.recv(4096).decode(errors="replace"))   # 원문 그대로 출력
conn.close(); srv.close()
실행 결과 — 같은 URL, 스킴만 다르게:
===== 대상이 http://  → absolute-form =====
GET http://api.demo.test:8080/v1/pay?user=123 HTTP/1.1
Host: api.demo.test:8080

===== 대상이 https:// → authority-form (CONNECT) =====
CONNECT api.demo.test:443 HTTP/1.1
Host: api.demo.test:443

===== 비교: 프록시 없이 직접 → origin-form =====
> GET /v1/pay?user=123 HTTP/1.1
> Host: api.demo.test:8080

verify-02 — MemoryBIO 트릭

curl -v해석된 결과를 보여줍니다. 보통 ssl.wrap_socket은 TLS 엔진을 소켓에 직접 붙여서 암호문을 볼 수 없는데, MemoryBIO로 중간에 버퍼를 끼우면 와이어에 나가는 바이트가 손에 잡힙니다.

inb, outb = ssl.MemoryBIO(), ssl.MemoryBIO()
tls = ctx.wrap_bio(inb, outb, server_hostname=HOST)   # TLS 엔진을 소켓이 아니라 메모리에 붙임

# 핸드셰이크: 소켓과 BIO 사이를 손으로 펌프질
while True:
    try:
        tls.do_handshake(); break
    except ssl.SSLWantReadError:
        out = outb.read()
        if out: sock.sendall(out)          # 나갈 바이트 = 여기서 눈으로 볼 수 있음
        inb.write(sock.recv(16384))

# 요청: 평문을 넣고 암호문을 꺼낸다
tls.write(b"GET /v1/pay?user=123 HTTP/1.1\r\nHost: ...\r\n\r\n")
wire = outb.read()                          # ← 0x17 application_data 레코드
print(b"/v1/pay" in wire)                   # False — 경로가 사라졌음을 증명

verify-03 — 측정 원리

b=$(wc -l < /var/log/squid/access.log)
curl -sk -x $PROXY "$U1" "$U2" "$U3" -o /dev/null -o /dev/null -o /dev/null
sleep 1.5                                    # 로그는 '닫힐 때' 쓰이므로 필수
echo "터널 $(( $(wc -l < /var/log/squid/access.log) - b ))개"

케이스별 커맨드:

A  curl -sk -x $PROXY  $U1 $U2 $U3                          # → 1터널
B  curl -sk -x $PROXY -H "Connection: close" $U1 $U2 $U3    # → 3터널
C  curl -sk -x $PROXY -Z --parallel-immediate --http1.1 ...  # → 3터널
D  curl -sk -x $PROXY  https://api.demo.test/  https://api2.demo.test/   # → 2터널
E  curl -s  -x $PROXY -Z --http2  https://example.com/{a,b,c}            # → 1터널
주의

A 케이스는 실제로 존재하는 파일을 요청해 200을 받아야 합니다. 404 응답에는 Connection: close가 붙어(파이썬 send_error의 동작) 재사용이 깨지고 항상 3터널로 나옵니다 — 함정 ④.

Part 06

랩에서 밟은 함정 6개

구성 과정에서 실제로 겪은 것들이며, 각각이 이 주제의 메커니즘을 그대로 반영합니다.

① ACL을 넣었는데 안 먹힘

Ubuntu 패키지의 include /etc/squid/conf.d/*.conf는 1621행인데 http_access allow localhost는 1604행입니다. conf.d에 deny를 넣으면 이미 allow에서 결정이 끝난 뒤라 규칙이 통째로 무시됩니다.

교훈

first-match는 "설정 파일 어디에 쓰느냐"까지 포함하는 규칙입니다. 운영에서 "ACL 반영 안 돼요"의 다수가 이것입니다.

② /etc/hosts를 고쳤는데 계속 503 (HIER_NONE)

Squid가 자체 DNS 캐시(ipcache)를 갖고 있어, 한 번 실패한 이름은 계속 실패합니다. squid -k reconfigure로 플러시해야 합니다.

교훈

Squid가 이름을 해석하는 주체라는 사실이 운영 절차에 그대로 드러나는 지점입니다 — 사내에서 DNS 레코드를 바꿨는데 Squid만 옛 IP로 가는 상황이 정확히 같은 원리입니다.

③ 모든 터널의 지속시간이 정확히 25초

origin이 단일 스레드였던 것이 원인. keep-alive 연결이 다음 요청을 기다리며 스레드를 붙잡으니, 두 번째 연결이 첫 연결의 idle timeout까지 막혀 교착됐습니다. ThreadingHTTPServer로 바꾸니 53ms/103ms로 정상화.

교훈

"연결을 유지한다"는 것은 서버 자원을 붙잡는다는 뜻입니다. 프록시·gateway 계층에서도 동일하게 성립하며, Part 04의 용량 논의와 직결됩니다.

④ 재사용 실험이 항상 3터널

404 응답에 Connection: close가 붙어서였습니다. 실제 존재하는 파일을 요청해 200을 받아야 keep-alive가 유지됩니다. 터널 수명은 응답 하나하나가 좌우한다는 증거.

⑤ h2인데 터널이 3개

curl -Z --parallel-immediate가 범인. h2 협상 결과를 기다리지 않고 연결 3개를 즉시 열어버립니다. 플래그를 빼면 curl이 첫 연결의 ALPN 결과를 보고 멀티플렉싱해 1터널이 됩니다.

교훈

"h2를 쓰면 자동으로 1터널"이 아니라 클라이언트의 연결 개시 전략이 정합니다. 앱 프레임워크의 커넥션 매니저 설정이 gateway 부하를 좌우한다는 뜻이기도 합니다.

⑥ 자잘한 것들

Part 07

사내 환경 진단 치트시트

랩에서 익힌 것 중 실제 Squid를 건드리지 않고 파드에서 바로 쓸 수 있는 읽기 전용 진단들입니다.

# 1) 이 요청이 프록시를 타는가, 직행하는가 (NO_PROXY 진단)
curl -v https://api.target.com/ 2>&1 | grep -E 'Connected to|CONNECT'
#   "Connected to squid..."        → 프록시 경유
#   "Connected to api.target.com"  → NO_PROXY 매치로 직행

# 2) 경로상에 TLS 개입(bump)이 있는가
curl -v https://api.target.com/ 2>&1 | grep -E 'subject:|issuer:'
#   issuer 가 공인 CA 가 아니면 어딘가에서 가로채는 중

# 3) 내 앱이 터널을 몇 개 쓰는가 (커넥션 풀 진단 — gateway 용량과 직결)
ss -tn state established '( dport = :3128 )' | wc -l

# 4) 라이브러리가 env 를 읽기는 하는가
#   curl 성공 + 앱 timeout → JVM/Node 의 env 미지원 의심

7.1 실패 구간 판별표

증상실패 구간다음 행동
:3128 connection timeout프록시까지 못 감네트워크/방화벽/NO_PROXY 확인
403 + X-Squid-Error정책 차단 — 정상 동작Squid 운영팀에 도메인 허용 요청
503Squid → origin 실패 (DNS 포함)Squid access.log 의 HIER_ 필드
SSL certificate problem앱 ↔ origin 의 TLS — 프록시 로그엔 안 남음앱 쪽 curl -v 의 subject/issuer
약 1시간 주기 간헐 끊김idle timeout (프록시 또는 Envoy 기본값)타임아웃 층 정렬 (설계서 7.2)

7.2 용어 정리

request-target 세 형태
origin-form(GET /path) · absolute-form(GET http://host/path) · authority-form(CONNECT host:443). 요청 라인을 읽는 상대가 서버냐 프록시냐, 자원 요청이냐 배관 요청이냐가 결정한다.
TLS 레코드 타입
0x16 handshake · 0x14 change_cipher_spec · 0x17 application_data · 0x15 alert. 와이어 첫 바이트로 구분된다.
TCP_TUNNEL / HIER_DIRECT
Squid access.log의 결과 코드와 계층 필드. 터널로 처리했음과, Squid가 스스로 해석해 직접 연결한 IP를 뜻한다.
keep-alive (persistent connection)
HTTP/1.1 기본값. 응답 후에도 연결을 유지해 다음 요청을 같은 연결로 보낸다. 프록시 환경에서는 터널의 수명이 된다.
connection pool key
(scheme, host, port, proxy). 목적지가 다르면 같은 프록시라도 터널을 공유할 수 없다.
멀티플렉싱
HTTP/2가 한 연결 안에 여러 스트림을 동시에 흘리는 것. HTTP/1.1에는 없으므로 동시 요청이 곧 연결 수가 된다.
MemoryBIO
TLS 엔진을 소켓 대신 메모리 버퍼에 연결하는 방식. 암호화된 와이어 바이트를 직접 관찰할 때 쓴다.