vgrok — ngrok과 다른 두 가지
내 컴퓨터에서 서버를 띄워도 밖에서는 못 들어온다. 전화기는 있는데 번호가 없는 것과 같다 — 내가 거는 건 되는데 남이 나에게 걸 수는 없다(NAT·방화벽 뒤라서).
터널링의 요령은 이 비대칭을 뒤집는 것이다. 내가 먼저 밖으로 걸어놓고 끊지 않는다. 번호가 공개된 쪽이 그 통화를 붙들고 있다가, 자기한테 온 요청을 그 통화에 대고 흘려보낸다.
외부 요청 ──HTTP──▶ [공개 머신] ──┐
│ WebSocket (내 컴퓨터가 먼저 걸어둔 것)
내 컴퓨터 ◀──HTTP──┘
여기까지는 ngrok도 같다. vgrok이 다른 건 둘이다.
하나 — 공개 머신을 빌린다
ngrok은 그 “번호가 공개된 쪽”을 자기 인프라로 굴린다. vgrok은 잠깐 빌리는 컴퓨터(Vercel Sandbox)를 그 자리에 끼워넣는다. 마침 필요한 두 가지가 다 딸려 오기 때문이다 — 공개 주소가 그냥 나오고(도메인도 인증서도 설정 0), 코드를 올려 돌릴 수 있다.
그러니 터널 머신은 특별한 물건이 아니다. “공개 주소가 나오고 코드가 돌면” 무엇이든 된다. Sandbox는 그걸 가장 싸게 얻는 한 가지 방법일 뿐이다.
핵심 코드가 220줄쯤인 것이 그 증거다 — 네트워킹·보안·HTTPS·DNS를 빌린 쪽이 다 떠안아서 남는 게 이만큼이다.
대가는 수명이다. 빌린 거라 오래 못 쓴다 — Hobby는 한 세션에 45분.1 다만 세션 단위라 멈췄다 이어붙이면 총 수명은 늘릴 수 있다.
둘 — 바이트가 아니라 메시지를 나른다
ngrok은 TCP 터널도 준다. 바이트를 그대로 흘려보내니 SSH·데이터베이스·MQTT·게임 서버까지 나른다.2
vgrok은 그렇지 않다. HTTP 요청을 뜯어서 method·url·headers·body로 만들고, 반대편에서 다시 조립한다. WebSocket은 끊기지 않는 통로 역할만 하고, 그 위로 흐르는 건 직렬화된 HTTP 메시지다.
이 한 가지 선택에서 나머지가 전부 따라 나온다.
| 그래서 | 왜냐면 |
|---|---|
| 번호표를 붙인다 | 통로 하나에 여러 요청·응답을 섞어 싣기 때문. 바이트 터널이면 연결마다 흐름이 따로라 필요 없다 |
| 바이너리를 글자로 바꾼다 | 메시지를 JSON으로 만드니까. 이미지를 그대로 못 싣는다 |
| HTTP만 된다 | HTTP를 뜯어서 나르니까. 뜯을 수 없는 프로토콜은 못 나른다 |
| 다 모아야 보낸다 | 메시지 한 덩어리로 부치니까. 양쪽 다 on('data')로 모았다가 on('end')에서 한 번에 넘긴다3 |
이미지·CSS 같은 static 리소스도 특별 취급이 없다. 똑같이 바이트를 글자로 바꿔 실었다 되돌린다. 되긴 되는데 하나씩 통째로 통과한다 — 점진 렌더가 안 되고, 큰 파일은 메모리에 통째로 올라가며, base64라 전송량이 3분의 1쯤 늘어난다. 끝나지 않는 응답(SSE 같은 것)은 아예 못 다룬다.
그 밖에 클라이언트는 하나만 붙는다.
Footnotes
-
최대 세션 길이는 Hobby 45분 · Pro/Enterprise 24시간. “The maximum duration applies to a single session, not to the sandbox itself. The limit resets every time a sandbox stops and resumes, so the total lifetime of a persistent sandbox is effectively unbounded.” (Vercel Sandbox — pricing and limits) ↩
-
“TCP endpoints enable you to deliver any network service with a TCP-based protocol” — SSH·VNC·RDP, MySQL·Postgres, MQTT, 게임 서버 등. (ngrok — TCP endpoints) ↩
-
server.ts는req.on('data', chunk => chunks.push(chunk))로 모아req.on('end')에서Buffer.concat(chunks).toString('base64url')로 부치고, 받는 쪽은res.end(Buffer.from(body, 'base64url'))로 한 번에 쓴다.client.ts도 응답을 같은 방식으로 모은다. (vgrok — styfle) ↩
핵심 한 줄: 원격 호스팅 DevTools 프론트엔드(appspot.com)를 로컬 CDP에 붙이되, 중간에 로컬 프록시(9223)를 끼워 Chrome의 cross-origin WebSocket 거부를 우회한다.
연결 경로:
chrome_devtools_remote(unix socket) →adb forward로localhost:9222에 노출localhost:9222에서/json으로 탭 목록,/devtools/page/{id}로 페이지별 CDP WS 제공localhost:9223aiohttp 프록시가 프론트엔드 WS를 받아 9222로 양방향 중계- DevTools 프론트엔드는
termux-open-url로 브라우저에서 실행,?ws=localhost:9223/{id}로 백엔드 지정
트릭의 본질 — 왜 프록시가 필요한가:
- 프론트엔드 origin은
https://chrome-devtools-frontend.appspot.com→ 9222 직결 시 Chrome CDP가 외부 Origin WS를 거부 (DNS rebinding 방어) - 프록시가 프론트엔드 WS를 종단하고 파이썬 프로세스가 새 WS로 9222에 재접속 → 브라우저 Origin이 없으니 통과 (“Origin laundering”).
cors_middleware가 허용 헤더를 덧붙인다.
기억해둘 것:
ws://localhost이 HTTPS 페이지에서 mixed-content로 안 막히는 이유: localhost는 secure context로 취급 → IP/도메인이었으면wss강제로 깨졌을 것.- 더 깔끔한 대안: 프록시를 안 쓰고 프론트엔드를
localhost에서 self-host하면 Origin 문제 자체가 사라진다. ?@f84901f7…= DevTools 프론트엔드 빌드 리비전 핀.