cc-gateway는 Claude Code와 Anthropic API 사이에 끼어 앉아, 각 기기가 본사로 보내는 디바이스 ID·이메일·환경 정보·텔레메트리를 가로채 하나의 통일된 신원으로 다시 써서 전달하는 리버스 프록시다. 여러 대의 컴퓨터에서 같은 계정을 써도, 본사 입장에서는 "단 한 대의 기기"로만 보이게 만든다.
Claude Code는 데스크톱·노트북·서버마다 다른 디바이스 ID와 환경 정보를 본사에 보낸다. 이걸 그대로 두면 "한 계정이 다섯 기기에서 동시에 쓰인다"는 신호가 된다. cc-gateway는 모든 기기의 요청을 가로채 하나의 통일된 디바이스 ID와 환경 정보로 다시 포장한 뒤 Anthropic 서버로 보낸다.
핵심은 인터셉터 체인이다. 요청 한 건이 들어오면 인증 → 본문 수집 → 본문 리라이트 → 헤더 리라이트 → OAuth 토큰 주입 → 업스트림 포워딩의 6단계를 차례로 통과한다. 그리고 코드 자체가 학습 교본이다 — Node.js 빌트인 모듈만으로 프로덕션급 프록시를 만드는 방법이 전부 들어 있다.
요즘 AI 서비스는 "한 사람당 하나의 계정"을 강제한다. 하지만 사람들은 데스크톱·노트북·회사 PC·서버에서 같은 계정을 쓰고 싶어 한다. Claude Code는 각 기기마다 다른 디바이스 ID와 텔레메트리를 본사에 보내기 때문에 — 본사 입장에서는 "한 계정이 다섯 기기에서 동시에"라는 신호가 된다. cc-gateway는 이 신호 자체를 지운다.
많은 사람이 VPN으로 IP만 숨기면 멀티디바이스 문제가 해결된다고 생각한다. 틀렸다. IP는 핑거프린팅의 한 조각일 뿐이다. OS, 메모리 크기, 터미널 종류 같은 40개 이상의 차원이 추가로 수집되는데 이건 VPN으로 못 가린다. IP가 같아도 환경 정보가 다 다르면 다른 기기로 식별된다.
IP만 가리는 게 아니라 device_id, email, 세션 메타데이터, 메모리 사이즈, OS 정보 등 40개 이상의 차원을 모두 통일된 값으로 다시 쓴다. 모든 기기(관리자 포함)가 게이트웨이를 거치게 하면 본사는 전부 같은 한 대로 본다. 게다가 OAuth 토큰을 중앙에서 관리해 클라이언트가 토큰을 안 보내도 게이트웨이가 끼워 넣는다.
| 관점 | 왜 배울 가치가 있나 |
|---|---|
| 의존성 최소화 | 외부 의존성 단 2개. Express/Fastify/Axios 뒤에 숨은 HTTP의 진짜 동작이 그대로 드러난다 |
| 실전 프록시 | 인증 → 본문/헤더 리라이트 → 업스트림 포워딩 — 모든 리버스 프록시의 공통 골격을 작은 코드로 본다 |
| 스트리밍 | SSE 응답을 pipe() 한 줄로 무손실 통과시키는 법 — "가공하지 않는 안전함"을 배운다 |
| 역공학 | 관찰만으로 CCH 빌링 해시 알고리즘을 복원 — 리버스 엔지니어링의 사고법을 엿본다 |
| 기술 | 역할 | 상세 |
|---|---|---|
| TypeScript 5.7 | 전체 소스 | tsc로 dist/ 빌드, tsx로 개발 중 직접 실행 |
| Node.js 22+ | 런타임 | 빌트인 http/https/crypto/fs/url 모듈이 코드의 토대 |
| YAML | 설정 포맷 | config.example.yaml / clash-rules.yaml |
| Shell | 설치/배포 | scripts/*.sh — quick-setup, add-client, admin-setup |
요즘 npm 프로젝트의 의존성은 보통 50~200개다. 이건 "가구 하나를 만들려고 백 가지 공구를 다 사오는 목공"과 비슷하다.
cc-gateway의 저자는 정반대를 택했다 — 도끼와 끌만으로 가구를 짠 장인이다. 보안 면에서도 유리하다: 의존성이 적을수록 공급망 공격(supply chain attack)에 노출되는 표면이 작아진다.
| 기술 | 역할 | 상세 |
|---|---|---|
| Dockerfile | 컨테이너화 | 빌드/런타임 분리 멀티 스테이지 패턴(추정) — 최종 이미지 경량화 |
| docker-compose | 오케스트레이션 | docker compose restart로 프로덕션 운영 |
| GitHub Actions | CI | CI 워크플로 없음 — .github/에 로고·메타 파일(logo-dark.svg, logo-light.svg, repo-meta.yml)만 존재 |
| tests | 검증 | tests/rewriter.test.ts — 16개 통과 테스트(README 기준) |
이 프로젝트에서 가장 흥미로운 코드는 rewriter.ts다. 클라이언트의 요청 한 건이 들어오면 아래 6단계를 순서대로 거친다.
1) 클라이언트 인증. 게이트웨이를 쓸 권한이 있는지 확인. x-api-key나 Bearer 토큰 검증.
2) 본문 수집. HTTP 요청은 청크 단위로 도착한다. 다 모아 하나의 Buffer로 만든다.
3) 본문 리라이트. 경로별로 다르게 처리. /v1/messages면 메시지 본문을, /event_logging이면 텔레메트리를 변환한다.
4) 헤더 리라이트. 빌링 헤더 제거, User-Agent 교체, 기타 식별 헤더 정리.
5) OAuth 토큰 주입. 클라이언트가 자기 토큰을 안 보내도, 게이트웨이가 중앙 OAuth 토큰을 끼워 보낸다.
6) 업스트림 포워딩. 정리된 요청을 진짜 Anthropic API 서버로 전달.
절차가 까다로운 보안 우체국을 상상해보자. 신분증 확인하고, 봉투 내용을 검사하고, 보낸 사람 주소를 회사 공통 주소로 바꿔 적고, 회사 우표를 붙인 뒤, 마지막에 진짜 우체국으로 전달한다.
cc-gateway가 하는 일이 딱 그렇다. 각 클라이언트는 자기 편지를 보냈다고 생각하지만, 본사가 받아본 봉투에는 모두 같은 발신인이 적혀 있다.
요청은 통째로 바꾸지만, 응답은 한 글자도 안 건드린다. 코드로 보면 이 한 줄이다 — proxyRes.pipe(res). 업스트림 응답을 클라이언트 응답에 통째로 흘려보낸다.
처음 프록시를 만들면 응답도 변환하고 싶어진다. 그런데 SSE 스트리밍 응답에 잘못 손대면 실시간 타이핑이 멈추거나, 중간이 잘리거나, 인코딩이 깨진다. Claude 사용 경험을 망치는 가장 빠른 길이다.
Node.js 스트림은 데이터를 메모리에 다 쌓지 않고 도착하는 즉시 다음 단계로 흘려보낸다. proxyRes.pipe(res) 한 줄이면 본사에서 도착한 한 글자가 바로 클라이언트로 흐른다. 가공이 필요 없으면 가공하지 않는다 — 단순함이 최고의 안전장치다.
가장 까다로운 부분은 x-anthropic-billing-header 처리다. Claude Code가 보내는 이 빌링 헤더에는 메시지 본문 기반 검증값이 포함돼 있는데, 게이트웨이가 본문을 리라이트하면 불일치가 발생할 수 있다. v0.2.0에서는 이 헤더를 완전히 제거(strip)하는 방식으로 전략이 바뀌었다 — Anthropic 공식 환경변수 CLAUDE_CODE_ATTRIBUTION_HEADER=false와 동일한 효과이며, 프롬프트 캐시 공유의 부산물이기도 하다.
1) 헤더 완전 제거: 게이트웨이가 요청에서 x-anthropic-billing-header를 strip한다.
2) 재계산 없이 통과 — 헤더 자체가 없으면 불일치 검증도 발생하지 않는다.
3) 공식 CLAUDE_CODE_ATTRIBUTION_HEADER=false 옵션과 동일한 동작이다.
※ 구버전(v0.1.x)에는 SHA-256 기반 해시 재계산 fallback 코드가 있었으나 v0.2.0에서 기본 비활성화됨.
택배 박스에 원래 내용물 바코드 스티커가 붙어 있다고 하자. 박스 안을 바꿨는데 바코드는 그대로면 검수에서 걸린다.
v0.2.0의 해법은 "바코드를 다시 인쇄"하는 대신 바코드 스티커를 아예 떼어 보내는 것이다. 스티커가 없으면 바코드 불일치 검수 자체가 생략된다.
현실에서 같은 컴퓨터의 메모리 사용량도 시시각각 변한다. 그런데 "모든 요청에서 정확히 348572104바이트"라면? 본사 분석 시스템은 즉시 "고정값 봇/위조"라고 판단한다. 고정값은 그 자체가 핑거프린트가 된다.
메모리(RSS·힙), 가동 시간처럼 자연히 변하는 값은 비현실적이지 않은 범위 안에서 매 요청 랜덤으로 생성한다. OS·아키텍처처럼 절대 안 바뀌는 값은 고정. 이 분류가 신뢰성을 만든다.
구조를 읽는 법: proxy.ts가 들어오는 연결을 받고 → rewriter.ts가 요청을 변환하고 → oauth.ts가 신원을 주입하고 → 다시 proxy.ts가 업스트림으로 포워딩한다. 파일 8개만 읽으면 전체 흐름이 손에 잡히는 크기라서 입문용 리딩 대상으로 이상적이다.
배울 것: http.createServer()로 서버를 만들고, https.request()로 업스트림을 호출하고, pipe()로 스트림을 잇는 — 모든 Node.js 웹 서버의 토대. Express/Fastify가 마법처럼 느껴지는 이유는 이 기초를 안 봤기 때문이다.
배울 것: 인증 → 본문/헤더 변환 → 포워딩의 파이프라인 설계. 경로별 분기(/v1/messages vs /event_logging)로 같은 진입점에서 서로 다른 변환을 적용하는 라우팅 감각.
배울 것: 요청은 버퍼링해 변환하되, 응답은 버퍼링 없이 즉시 흘려보내야 하는 이유. 백프레셔(backpressure)를 자동 처리하는 pipe()의 가치.
배울 것: 외부 OAuth 라이브러리 없이 토큰 캐싱·만료 추적·setTimeout 사전 갱신을 직접 구현하는 법. 만료 5분 전 자동 갱신으로 서비스 중단을 막는다.
배울 것: crypto 빌트인의 SHA-256 사용법과, 관찰 → 가설 → 시뮬레이션 → 복원으로 이어지는 리버스 엔지니어링 절차. "입력 한 글자가 바뀌면 출력이 완전히 달라진다"는 해시의 성질을 실전에서 다룬다.
배울 것: 의존성 2개라는 극단적 선택이 주는 보안·유지보수 이점. 빌트인으로 충분한 일을 굳이 패키지로 끌어오지 않는 판단 기준.
배울 것: tsx watch로 개발하고 tsc로 dist/를 만든 뒤 컨테이너에 담는 흐름. 빌드 환경과 런타임 환경을 분리해 최종 이미지를 줄이는 패턴은 Python·Go·Rust에도 그대로 적용된다.
| 항목 | 최소 | 권장 |
|---|---|---|
| Node.js | 22+ (README 명시) | 22 LTS 최신 |
| OS | Linux / macOS / Windows (Node 동작 환경) | Linux 서버 (상시 구동) |
| RAM | 256MB~512MB | 1GB+ (여유) |
| 네트워크 | 인터넷 연결 필수 (업스트림 API) | 안정적 회선 / 고정 IP |
| Docker | 선택 (로컬은 npm만으로 가능) | 프로덕션 권장 (compose) |
| 계정 | 유효한 Anthropic / Claude 자격증명 | 중앙 OAuth 토큰 발급 완료 |
| 실행 방법 | 명령 | 용도 |
|---|---|---|
| 로컬 개발 | npm install → bash scripts/quick-setup.sh → npm run dev | 코드 읽고 고치며 실험 |
| 프로덕션 | bash scripts/admin-setup.sh → docker compose restart | 상시 구동 게이트웨이 |
| 빌드 | npm run build (tsc) → npm start | 컨테이너 없이 직접 구동 |
| 테스트 | npm test (tsx tests/rewriter.test.ts) | 리라이터 회귀 검증(16개) |
Express 없이 순수 http.createServer()만으로 — 들어오는 요청을 다른 URL로 그대로 전달하는 프록시를 만들어본다. 헤더 로깅과 SSE 스트리밍 통과 테스트까지.
// echo-proxy.mjs
import http from 'http';
import https from 'https';
http.createServer((req, res) => {
// 업스트림으로 그대로 포워딩
const up = https.request(
{ host: 'httpbin.org', path: req.url, method: req.method, headers: req.headers },
(proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res); // ← SSE도 이 한 줄로 무손실 통과
});
req.pipe(up);
}).listen(8787);
JSON 본문에서 user.email을 마스킹하고(j***@gmail.com 형태), user.ip를 SHA-256 해시로 바꾸는 미니 미들웨어를 만든다. 인터셉터 체인의 핵심을 익힌다.
req.on('data')로 모아 Buffer 합치기 → JSON.parse → 필드 교체 → crypto.createHash('sha256') → 다시 직렬화해 업스트림에 write.oauth.ts를 참고해 독립적인 토큰 관리 모듈을 짠다. 캐싱, 만료 추적, setTimeout 사전 갱신 스케줄링, 갱신 실패 시 재시도까지. 이걸 만들면 어떤 API와도 안정적으로 통신할 수 있다.
expires_at을 기록 → 만료 5분 전 시점에 setTimeout 예약 → 갱신 성공 시 캐시 교체, 실패 시 지수 백오프로 재시도.이 레포의 Dockerfile을 읽고 빌드 환경과 런타임 환경을 분리하는 패턴을 익힌다. TypeScript를 빌드 스테이지에서 컴파일하고, 런타임 스테이지에는 dist/와 프로덕션 의존성만 복사해 최종 이미지를 1/3 이하로 줄인다.
FROM node:22 AS build (npm ci + tsc) → FROM node:22-slim (dist + node_modules 프로덕션만 COPY). 같은 패턴이 Python·Go·Rust에도 적용된다.mitmproxy를 띄우고 자기가 쓰는 CLI 도구의 트래픽을 캡처한다. Claude Code뿐 아니라 git, npm, gh도 어딘가로 데이터를 보낸다. 이걸 직접 보는 경험이 네트워크 프로그래밍 시야를 넓힌다.
mitmproxy 실행 → HTTPS_PROXY=http://127.0.0.1:8080 환경변수로 대상 도구 실행 → 어떤 엔드포인트에 어떤 본문이 가는지 관찰 → cc-gateway가 무엇을 리라이트하는지 역으로 이해.| 요일 | 학습 내용 | 실습 |
|---|---|---|
| 월 | http/https 빌트인 모듈 | createServer로 hello 서버 |
| 화 | 스트림과 pipe | 요청→업스트림 에코 프록시(과제 1) |
| 수 | 요청 본문 수집 패턴 | 청크 모아 Buffer 합치기 |
| 목 | 헤더 다루기 | User-Agent·식별 헤더 교체 실험 |
| 금 | SSE 이해 | 스트리밍 응답을 무손실 통과시키기 |
| 요일 | 학습 내용 | 실습 |
|---|---|---|
| 월 | 리버스 프록시 개념 | Nginx와 cc-gateway 비교 |
| 화 | 인터셉터 체인 설계 | rewriter.ts 흐름 따라 읽기 |
| 수 | 경로별 분기 | /messages vs /event_logging 처리 분리 |
| 목 | 본문 변환 미들웨어 | email 마스킹·ip 해시(과제 2) |
| 금 | 핑거프린팅 차원 분류 | 고정값 vs 랜덤값 분류 설계 |
| 요일 | 학습 내용 | 실습 |
|---|---|---|
| 월 | OAuth2 기본 흐름 | 토큰 발급/만료 개념 정리 |
| 화 | 사전 갱신 패턴 | setTimeout 갱신 모듈(과제 3) |
| 수 | crypto 빌트인 | SHA-256 해시 계산 실습 |
| 목 | 해시 검증 메커니즘 | CCH 재현 원리 따라가기 |
| 금 | 역공학 사고법 | 관찰→가설→시뮬레이션 연습 |
| 요일 | 학습 내용 | 실습 |
|---|---|---|
| 월 | TypeScript 빌드 파이프라인 | tsx watch / tsc 빌드 비교 |
| 화 | Docker 멀티 스테이지 | 이미지 경량화(과제 4) |
| 수 | docker-compose 운영 | compose로 상시 구동·재시작 |
| 목 | 외부 프록시 연동 | https-proxy-agent로 Clash 경유 |
| 금 | 트래픽 관찰 종합 | mitmproxy 캡처(과제 5) |
| 키워드 | 설명 |
|---|---|
| 리버스 프록시 | 진짜 서버 앞에 앉아 요청을 가로채는 중간 서버. 클라이언트는 이걸 진짜 서버로 믿는다 |
| 인터셉터 체인 | 요청이 거치는 변환 단계의 순서(인증→본문→헤더→OAuth→포워딩). rewriter.ts의 골격 |
| 디바이스 핑거프린팅 | OS·메모리·터미널 등 수십 차원을 조합해 기기를 식별하는 기술. cc-gateway가 무력화 대상으로 삼는다 |
| 텔레메트리 | 소프트웨어가 사용 패턴·환경 정보를 본사로 보내는 데이터. "원격 측정"의 뜻 |
| SSE | Server-Sent Events. 서버가 데이터를 한 글자씩 실시간으로 흘려보내는 HTTP 기술 |
| pipe() | Node.js 스트림을 잇는 메서드. 데이터를 쌓지 않고 도착 즉시 흘려보내 백프레셔를 자동 처리 |
| 업스트림 (Upstream) | 프록시가 요청을 최종 전달하는 진짜 서버. 여기서는 Anthropic API |
| OAuth 토큰 | API 접근 권한을 증명하는 시간 제한 자격증명. 만료 전 갱신해야 끊김이 없다 |
| 사전 갱신 | Proactive Renewal. 토큰 만료 5분 전에 미리 갱신해 서비스 중단을 막는 패턴 |
| CCH | Claude Code Hash. 첫 메시지 기반 빌링 검증 해시. 본문을 바꾸면 재계산이 필요하다 |
| SHA-256 | 암호학적 해시 함수. 임의 길이 입력을 256비트 고정 출력으로. 한 글자만 달라도 결과가 완전히 바뀐다 |
| 소금값 (Salt) | 해시 입력에 덧붙이는 고정/비밀 문자열. 같은 입력이라도 결과를 예측·재현하기 어렵게 만든다 |
| 고정값 함정 | 변동해야 할 지표를 상수로 박으면 오히려 위조 신호가 되는 문제. 합리적 범위 랜덤으로 회피 |
| 공급망 공격 | 의존성(써드파티 패키지)을 통해 침투하는 공격. 의존성이 적을수록 노출 표면이 작다 |
| 멀티 스테이지 빌드 | Docker에서 빌드 환경과 런타임 환경을 분리해 최종 이미지를 줄이는 패턴 |
| https-proxy-agent | 외부 프록시(Clash·V2Ray) 환경에서 Node가 업스트림에 나갈 수 있게 해주는 의존성 |