--- title: Squid 학습 노트 — 프록시는 두 모드로 산다 (L0·L1) date: 2026-07-22 type: note domain: networking tags: [squid, forward-proxy, proxy, http-connect, acl, tunneling, access-log] --- > [!note] 원본은 실측 기반 학습 노트 HTML > 이 글의 본체는 학습 노트 전문이다. 배치도·모드 비교 시퀀스·first-match 계단 다이어그램 3종, 실측 출력 9건, 운영 함정 5건, 확인 질문 6문(정답 접기)을 포함한다. 모든 출력은 Ubuntu 24.04 + Squid 6.14 컨테이너 실측값. > > **[→ 전문 보기 (squid-study-l0-l1.html)](files/squid-study-l0-l1.html)** > > 이 페이지는 핵심 요약본. ## 한 문단 요약 프록시는 두 모드로 산다. 대상이 `http://`면 Squid는 absolute-form 요청 라인을 끝까지 파싱하는 **완전한 중간자**다 — URL 수준 ACL·캐시가 가능하고, origin-form으로 재작성하며 `Via`/`X-Forwarded-For`를 붙여 오리진이 받는 바이트 자체가 달라진다. 대상이 `https://`면 클라이언트가 TLS 시작 전에 `CONNECT host:443`을 보내고, Squid는 ACL 판정·DNS resolve까지만 개입한 뒤 **양방향 바이트 복사기**가 된다(TLS는 클라이언트↔오리진 end-to-end — 증거는 curl에 오리진 인증서 `CN=origin.lab`이 그대로 보인다는 것). "아무것도 못 본다"가 아니라 **개입이 시간 축으로 나뉜다**는 것이 정확한 표현이다. 정책 쪽(L1)의 전부는 `acl`(어휘)과 `http_access`(문장)의 분리, 그리고 값 OR → 줄 AND → 줄간 first-match라는 3층 평가에 숨은 4번째 규칙 — 무매치 시 **마지막 줄의 반대**가 암묵 기본값 — 이다. `deny all`을 항상 명시하는 이유가 여기 있다. ## 구성 | 레벨 | 주제 | 핵심 | |---|---|---| | L0 | forward proxy와 CONNECT의 실체 | request-target 3형태, TCP 두 개 독립·DNS는 Squid의 몫, 두 동작 모드, 실측 4실험 | | L1 | squid.conf 해부 — ACL과 first-match | 정의/사용 분리, 3층+암묵 기본값, dstdomain 선행 점·rDNS, 실습 5건 | | QR | 치트시트 | 운영 반사신경 7건 | ## L0 핵심 압축 - **왜 존재하는가** — 보안망은 직접 외부 연결을 금지하고 단일 관문만 연다. 관문이 하나로 모이면 정책(허용 도메인)·감사(접속 로그)·캐시가 따라온다. forward proxy = 클라이언트의 대리인, reverse proxy = 서버의 대리인 — nginx/Envoy로 익숙한 쪽은 후자, Squid의 본업은 전자. - **프록시 요청은 별도 프로토콜이 아니다** — 그냥 HTTP이고, 다른 것은 request-target 형태 하나(RFC 9112): | 형태 | 예시 | 언제 | |---|---|---| | origin-form | `GET / HTTP/1.1` | 서버에 직접 (평소) | | absolute-form | `GET http://origin.lab/ HTTP/1.1` | 프록시에 http:// 요청 | | authority-form | `CONNECT origin.lab:443 HTTP/1.1` | 프록시에 터널 요청 | - **`HTTP_PROXY`는 OS 기능이 아니라 라이브러리 관례** — curl·requests·Go는 읽고 Java는 기본 무시(`-Dhttp.proxyHost`). `https_proxy`는 "https:// URL에 쓸 프록시"라는 선택 기준이고, 프록시 자체와의 연결은 두 경우 다 보통 평문 http. - **배치: TCP 두 개, DNS는 Squid의 몫** — 클라이언트는 프록시 주소만 resolve한다. 목적지 이름은 요청 라인에 문자열로 실려 가고, resolve는 Squid가 한다. TCP #1(클라→프록시)과 #2(프록시→오리진)는 완전히 독립 — keep-alive 재사용도 타임아웃도 각자 간다. ### 실측 4실험이 증명한 것 | 실험 | 관찰 | 증명 | |---|---|---| | A | curl -v에 `GET http://origin.lab:8080/ HTTP/1.1` | 목적지가 요청 라인 안에 (absolute-form) | | B | `200 Connection established` 후 TLS 시작, 인증서 `CN=origin.lab` | TLS는 터널 "안에서", end-to-end | | C | tcpdump 오리진 도착분에 `Via`·`X-Forwarded-For` | 모드 1은 재작성 — 클라가 보낸 적 없는 헤더 추가 (내부 IP 노출) | | D | 터널 12초 유지 중 로그 0줄 → 종료 후 1줄, elapsed=11970ms | CONNECT 로그는 **종료 시점의 부검 기록**. 바이트 필드 = Squid가 클라 방향으로 보낸 총량 | ## L1 핵심 압축 - **정의와 사용의 분리** — `acl`은 조건에 이름을 붙이는 정의, `http_access`는 이름을 조합한 판정 규칙. 같은 ACL을 `ssl_bump`·`cache`·`cache_peer_access` 등 다른 판정 지시자도 재사용한다. acl 정의 줄끼리는 순서 무의미, `http_access` 줄들은 순서가 전부. - **평가 4규칙** — ① ACL 내부 값들은 OR ② 한 줄 위 ACL들은 AND(`!`=부정) ③ 줄들 사이는 first-match 즉시 종결 ④ 무매치면 **마지막 줄의 반대**가 암묵 기본값. 운영 설정이 전부 `http_access deny all`로 끝나는 이유 = ④에 보안을 맡기지 않기 위해서. - **dstdomain 세부** — `.origin.lab`(선행 점)은 자신+모든 서브도메인, `origin.lab`은 정확히 그 호스트만. URL에 IP가 오면 rDNS로 도메인화해 비교를 시도하는데, 실제 인터넷 PTR은 대개 클라우드/ISP 이름이라 매칭 실패 → 403. rDNS는 slow ACL이라 지연도 얹힌다. ### 실습 5건이 증명한 것 - **dead rule 실측** — 광범위 `allow` 아래의 `deny`는 죽어 있다(차단 대상이 200). deny를 위로 올리면 403. `TCP_DENIED` + `HIER_NONE`(오리진에 아예 안 나감) = **403의 발원지는 Squid 정책**이라는 판독 공식. - **암묵 기본값 실측** — allow가 하나도 없는 설정에서 200이 나온다. 마지막 줄이 deny라 기본값이 allow가 된 것 — deny 규칙으로만 채운 방화벽은 문이 열려 있는 상태. - **문법 검증기** — 정의 안 된 ACL 참조는 `FATAL: Bungled`. 이 설정으로 restart하면 Squid가 아예 안 뜬다(reload는 기존 설정으로 버팀). 절차는 항상 `squid -k parse` → reload. - **rDNS 도박 실측** — 같은 랩에서 `127.0.0.2`는 403인데 `127.0.0.1`은 200 — /etc/hosts 덕에 rDNS가 origin.lab을 돌려줬기 때문. dstdomain의 IP 매칭은 **내가 통제하지 않는 PTR 레코드에 의존하는 도박**. ## 운영 함정 5건 | ID | 함정 | 증상 / 통제 | |---|---|---| | 0.A | NO_PROXY 해석은 구현마다 다르다 | 같은 NO_PROXY인데 특정 언어 앱만 내부 트래픽이 Squid로 흘러 403/지연 | | 0.B | Via·X-Forwarded-For 정보 유출 | 내부 IP·프록시 호스트명이 외부 오리진에 전달. `via off`·`forwarded_for delete` | | 0.C | 장수 터널은 로그에 안 보인다 | 살아있는 CONNECT는 로그 0줄 — "로그에 없다 = 트래픽 없다"는 오판. 장애 시 로그 폭주도 같은 원리 | | 1.A | 죽은 규칙(dead rule) | "차단했는데 여전히 접속" = 위쪽 광범위 allow가 선점. 추가할 **위치**가 내용보다 어렵다 | | 1.B | 앱이 IP로 호출 | rDNS가 기대 도메인을 안 주는 한 403. access.log URL 필드에 IP가 찍혀 있으면 이 케이스 | ## 치트시트 | 반사신경 | 내용 | |---|---| | 평가 3층 | 값 OR → 줄 AND(`!`=부정) → 줄간 first-match. 무매치면 마지막 줄의 반대 | | 403 + HIER_NONE | 거부의 발원지는 오리진이 아니라 Squid 정책(ACL) | | CONNECT 로그 | 터널 **종료 시** 기록. elapsed=터널 수명, 바이트=클라 방향 전달 총량 | | dstdomain | `.d.com`=자신+서브도메인, `d.com`=정확히 그 호스트. IP 요청은 rDNS 의존 | | 설정 변경 절차 | `squid -k parse`(문법) → `squid -k reconfigure`(reload). FATAL 설정으로 restart 금지 | | 프록시 경유 테스트 | `curl -v -x http://proxy:3128 URL` — 요청 라인 형태(absolute/CONNECT)부터 본다 | | env 변수 | 라이브러리 관례일 뿐(Java는 무시). https_proxy=https:// URL용 프록시. no_proxy 파싱은 구현차 | ## 확인 질문 (인출 연습) 원문에 L0 3문 + L1 3문이 정답 접기와 함께 있다 — 특히 L1의 3-ACL conf 판정 문제(줄 번호까지 답하기)는 first-match 체화용. [원문에서 풀기](files/squid-study-l0-l1.html) ## 관련 문서 - [HTTP Tunneling — CONNECT 기초·Squid 실습](/docs/networking/http-tunneling-basics/) — L0의 CONNECT를 실습 중심으로 - [HTTPS_PROXY 통신 흐름 실측 가이드](/docs/networking/https-proxy-connect-flow-and-lab/) — CONNECT 이후 연결 재사용·5케이스 매트릭스 - [TLS 1.3 over Squid tunnel](/docs/networking/tls13-over-squid-tunnel/) — 터널 안 TLS의 실측 - [Outbound 집약 설계서 — Squid 경로 일원화](/docs/istio/egress/squid-consolidation-guide/) — 이 노트의 L0가 그 설계서 Part 01–02의 배경에 대응