집이나 사무실에서 VPN을 켜면 보통 모든 트래픽이 다 그쪽으로 몰리죠. 근데 저는 특정 사이트 하나만 골라서 우회하고 싶었습니다. 유튜브도, 카톡도, 다른 브라우저 탭도 평소처럼 쓰고, 딱 그 사이트만 서버를 거쳐서 나가게 하고 싶었어요.
찾아보니 “크롬 확장이 알아서 탭 단위로 VPN을 태워준다” 같은 건 없더라고요. 그래서 직접 만들어봤습니다. 이 글은 그 과정과 원리를 정리한 겁니다.
왜 크롬 확장이 WireGuard를 직접 못 쓰나
가장 먼저 부딪힌 벽이 이거였습니다. WireGuard는 커널 레벨에서 raw UDP 소켓을 열고 암호화 핸드셰이크를 하는 프로토콜인데, 크롬 확장은 샌드박스 안에서 돌아가서 이런 저수준 네트워크 접근 자체가 막혀 있습니다. 이건 확장을 얼마나 잘 만드느냐의 문제가 아니라 Chrome Extension API 자체의 구조적 한계입니다.
그래서 우회 경로를 잡았습니다. WireGuard 자체를 브라우저가 다루는 대신, 터널 반대편에 SOCKS5 프록시를 하나 띄워두고, 확장은 그 SOCKS5한테만 말을 거는 구조입니다.
전체 그림
브라우저 탭 요청
→ 확장의 PAC 스크립트가 도메인 판정
→ 등록된 도메인: SOCKS5(터널 IP:1080)로
→ WireGuard 터널 경유 (암호화)
→ 서버의 SOCKS5가 실제 목적지에 서버 공인 IP로 접속
→ 인터넷 (그 사이트 입장에선 서버가 접속한 것으로 보임)
→ 그 외 도메인: 직결
→ 평소 인터넷 회선 그대로
핵심은 탭 단위가 아니라 도메인 단위라는 점입니다. Chrome의 프록시 API는 브라우저 전체에 PAC 스크립트 하나를 적용하는 방식이라, 요청이 올 때마다 “이 호스트가 목록에 있나?”만 판단할 수 있습니다. 같은 사이트를 여러 탭에서 열면 전부 같은 규칙을 따릅니다.
그리고 WireGuard 설정에서 AllowedIPs를 0.0.0.0/0이 아니라 서버 터널 대역(예: 10.0.0.0/24)으로 좁혀둔 것도 중요합니다. 이렇게 해야 WireGuard가 상시 연결돼 있어도 평소 트래픽엔 영향이 없고, SOCKS5로 명시적으로 보낸 요청만 터널을 탑니다(이른바 split-tunnel).
1. 크롬 확장 — PAC 스크립트로 도메인별 분기
핵심 로직은 background.js에 있습니다. 저장된 도메인 목록으로 PAC 스크립트를 생성해서 chrome.proxy.settings에 적용하는 방식이에요.
function buildPacScript({ socksHost, socksPort, domains }) {
const activeDomains = domains.filter((d) => d.enabled);
const domainChecks = activeDomains
.map((d) =>
`dnsDomainIs(host, "${d.domain}") || dnsDomainIs(host, ".${d.domain}") || host == "${d.domain}"`
)
.join(" || ");
return `
function FindProxyForURL(url, host) {
if (${activeDomains.length === 0 ? "false" : domainChecks}) {
return "SOCKS5 ${socksHost}:${socksPort}; DIRECT";
}
return "DIRECT";
}
`;
}
dnsDomainIs를 두 가지 형태로 넣어서 루트 도메인과 서브도메인(www.example.com 등)을 둘 다 잡습니다. 도메인이 목록에 없으면 그냥 DIRECT.
여기서 한 가지 주의할 점 — "SOCKS5 ...; DIRECT"처럼 세미콜론 뒤에 DIRECT를 폴백으로 붙여놨는데, 이게 SOCKS5 연결이 실패하면 에러 없이 조용히 직결로 넘어간다는 뜻입니다. 편할 때도 있지만, 디버깅할 때는 “분명 VPN 탄 줄 알았는데 알고 보니 그냥 직결이었다” 같은 함정이 되기 쉽습니다.
팝업에서는 마스터 스위치(전체 on/off), 도메인별 개별 on/off, 도메인 추가/삭제를 관리하고, chrome.storage.onChanged로 변경이 생기면 PAC 스크립트를 즉시 재생성합니다. 툴바 아이콘도 실제로 라우팅 중인지(마스터 ON + 활성 도메인 1개 이상)에 따라 색을 다르게 표시하도록 chrome.action.setIcon()을 붙였습니다.
2. 서버 — SOCKS5 데몬 띄우기
기존에 WireGuard가 이미 서버에 떠 있다면, 새로 컨테이너 하나만 추가하면 됩니다. Docker Compose로 가볍게:
services:
socks5-vpn:
image: serjs/go-socks5-proxy
container_name: socks5-vpn
restart: unless-stopped
network_mode: host
environment:
- PROXY_PORT=1080
- REQUIRE_AUTH=false
network_mode: host로 띄우면 호스트의 모든 네트워크 인터페이스(WireGuard의 가상 인터페이스 포함)에 자동으로 붙습니다.
여기서 한 번 삽질했는데, 이 이미지가 최근 버전부터 REQUIRE_AUTH 기본값이 true로 바뀌어서, PROXY_USER/PROXY_PASSWORD를 안 넣으면 컨테이너가 뜨자마자 종료되고 재시작 루프에 빠집니다. REQUIRE_AUTH=false를 명시적으로 넣어야 합니다(어차피 방화벽으로 외부 노출은 막을 거라 인증 없이도 괜찮습니다).
3. 방화벽 — WireGuard로 연결된 사람만 접근 가능하게
SOCKS5를 아무 제한 없이 띄우면 전 세계 누구나 인증 없이 그 서버를 경유해서 인터넷에 나갈 수 있는 상태가 됩니다. 그래서 ufw로 딱 WireGuard 인터페이스에서 들어온 요청만 허용합니다.
sudo ufw allow in on wg0 to any port 1080 proto tcp
sudo ufw deny 1080/tcp
ufw는 규칙을 위에서부터 순서대로 매칭하기 때문에, wg 인터페이스로 들어온 요청은 먼저 걸려서 통과되고, 그 외 경로(공인 IP로 직접)로 들어온 요청은 이 규칙을 못 지나쳐서 아래 deny에 걸립니다.
참고로 클라우드 제공업체의 네트워크 시큐리티 그룹/시큐리티 리스트는 건드릴 필요가 없습니다. 1080 트래픽은 공인 인터페이스로 직접 오는 게 아니라 이미 WireGuard가 복호화해서 내부적으로 만들어낸 패킷이라, 클라우드 레벨 방화벽이 애초에 관여하지 않습니다. 열어야 하는 건 WireGuard 자체 포트(기본 51820/udp)뿐입니다.
4. 새 기기를 WireGuard 피어로 등록하기
이미 WireGuard 서버가 돌고 있다면, 새 PC용 키쌍만 만들어서 피어로 추가하면 됩니다.
# 서버에서 키 생성
wg genkey | tee new-peer-privatekey | wg pubkey > new-peer-publickey
# wg0.conf에 추가 (기존 피어 블록은 그대로 두고 아래에 추가)
# [Peer]
# PublicKey = <new-peer-publickey 내용>
# AllowedIPs = 10.0.0.3/32
# 재시작 없이 적용
sudo bash -c "wg syncconf wg0 <(wg-quick strip wg0)"
클라이언트(Windows/Mac 등) 쪽 설정 파일은 이렇게 구성합니다:
[Interface]
PrivateKey = <new-peer-privatekey 내용>
Address = 10.0.0.3/32
[Peer]
PublicKey = <서버 공개키>
Endpoint = <서버 공인 IP>:51820
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25
AllowedIPs = 10.0.0.0/24가 split-tunnel의 핵심입니다. 이 대역으로 가는 패킷만 WireGuard 터널을 타고, 나머지 모든 트래픽(다른 프로그램, 다른 탭)은 평소 라우팅 그대로 나갑니다. 그래서 WireGuard 클라이언트를 상시 켜둬도 다른 서비스엔 전혀 영향이 없습니다.
최종적으로 필요한 “3단 스위치”
정리하면 이 시스템에서 어떤 사이트가 실제로 VPN을 타려면 세 가지가 전부 켜져 있어야 합니다.
- WireGuard 클라이언트 — Activate 상태
- 확장 프로그램 마스터 토글 — ON
- 그 도메인의 개별 토글 — ON
셋 중 하나라도 꺼지면 해당 트래픽은 에러 없이 조용히 평소 회선으로 넘어갑니다. 처음 세팅할 때는 이 폴백 동작 때문에 “왜 안 되지” 하고 헤맸는데, 원인을 알고 나니 오히려 편했습니다 — WireGuard를 상시 연결해두고 확장 토글로만 사이트별 라우팅을 관리하면 되거든요.
검증할 때 쓴 명령어들
# 서버: SOCKS5 자체 동작 확인
curl --socks5 127.0.0.1:1080 https://ifconfig.me
# 클라이언트(WireGuard 연결 상태에서): 터널 경유 확인
curl --socks5 10.0.0.1:1080 https://ifconfig.me
# 서버: 피어/핸드셰이크 확인
sudo wg show
이 세 가지만 순서대로 찍어보면 어느 구간이 막혔는지 바로 감이 옵니다 — 첫 번째가 실패하면 SOCKS5 자체 문제, 두 번째만 실패하면 터널/방화벽 문제, 둘 다 되는데 브라우저에서 안 되면 확장 쪽(PAC 적용 여부, 토글 상태) 문제인 식으로요.