SOCKS만 말하는 애플리케이션은 일반 VPN에서 대화할 상대가 없는데, 터널은 패킷을 나르고 앱은 프록시 종단점을 원하기 때문이다. 접근 소유자가 허용하면 OpenSSH가 그 종단점을 제공할 수 있는데, 동적 포워딩이 SSH 세션 하나를 로컬 프록시로 바꾸기 때문이다. 그다음에는 SSH 호스트에 도달하는 터널과 그 호스트가 이어서 여는 연결이라는 두 홉을 구분해 두어야 한다. 둘을 분리하면 이 구성이 왜 동작하는지와 결코 무엇을 하지 않을지가 함께 설명된다.

SSH 리스너가 되는 것과 되지 않는 것

동적 포워딩은 SSH 클라이언트가 로컬에서 수신하고 연결해 오는 모든 것과 SOCKS4 또는 SOCKS5로 대화하게 만든다. 각 요청은 목적지를 지정하고 SSH 서버가 이어지는 연결을 열기 때문에, 목적지 조회가 반대편에서 일어날 수 있다. 이는 애플리케이션 수준의 TCP 포워딩이며 기기 전체를 위한 VPN이 아니다. UDP 연관도, ICMP도, 프록시 설정을 완전히 무시하는 앱을 위한 것도 나르지 않는다. SSH 서버에서 앱까지의 홉 역시 터널 안에 있지 않은데, 목적지가 알려진 네트워크에서만 트래픽을 받을 때 이 점이 중요하다.

전제 조건은 실제로 존재한다. SSH 호스트로 가는 경로, 승인된 계정, 검증된 서버 호스트 키, 포워딩 권한이다. 어느 것도 프록시가 만들지 않으며, 마지막 항목이 보통 가장 먼저 빠진다. 다른 무엇을 디버깅하기 전에 네 가지를 모두 확인하라. 어느 것이 빠져도 클라이언트 쪽에서 보이는 실패는 같다.

루프백에서 명시적 포트로 시작하라

사용하지 않는 로컬 포트를 골라 주소를 생략하는 대신 루프백에 명시적으로 바인딩하라. 아래 세 가지 옵션은 각각 한 가지 이유로 존재한다.

ssh -D 127.0.0.1:1080 -NT -o ControlPath=none -o ExitOnForwardFailure=yes \
  [email protected]

-N 원격 명령을 건너뛰고 -T 의사 터미널을 건너뛰므로, 세션은 포워딩만을 위해 존재한다. ExitOnForwardFailure 리스너를 만들 수 없을 때 클라이언트가 종료하게 하며, 요청한 포워딩 없이 계속 실행되지 않는다. ControlPath=none 세션을 자체 포워딩을 추가할 수 있는 기존 다중화 연결 밖에 둔다. 루프백은 리스너를 LAN 밖에 두지만, 다른 로컬 프로세스도 접근할 수 있으므로 공유 머신에서 인가 경계는 아니다. OpenSSH 클라이언트 참조가 이 모든 플래그를 문서화하며, 루프백 경고는 수신 소켓이 무엇인지에서 나온다.

이름을 프록시를 우회하지 말고 그 안으로 보내라

SOCKS5 클라이언트는 목적지를 스스로 해석하거나 프록시에 요청할 수 있고, 그 선택이 어느 리졸버가 답할지를 정한다. socks5h 에서 curl은 호스트 이름을 프록시를 통해 보내므로 조회가 SSH 서버에서 일어난다. 평범한 socks5 에서는 노트북이 먼저 해석하고 연결만 이동하는데, 이름이 내부용이면 이것이 조용히 목적을 무너뜨린다. 아래 요청은 프록시 제외 목록도 비우므로, 상속된 NO_PROXY 값이 프록시를 우회하게 할 수 없다.

curl -sS --noproxy '' --proxy socks5h://127.0.0.1:1080 https://app.corp.example/health

인증서 검증과 앱 자체의 로그인은 여전히 적용된다. 프록시는 둘 다 대체하지 않는다. 두 번의 조회가 남는다. 노트북은 허용된 VPN과 DNS 경로로 SSH 호스트에 도달해야 하고, SSH 서버는 앱 이름을 직접 해석해야 한다. 두 번째는 어떤 트래픽이 프록시를 지나기 전에 단독으로 시험할 수 있다.

ssh [email protected] getent hosts app.corp.example

허용된 목적지 하나를 증명하고, 그다음 세션을 종료하라

정책이 허용하는 목적지와 거부해야 하는 목적지를 모두 시험하라. 동작하는 포워딩은 리스너가 동작한다는 것만 증명하고 그 이상은 아니다. 앱은 SSH 서버가 고르는 출발지 주소를 보는데, 그것은 노트북의 터널 주소가 아니다. 먼 쪽이 연결을 출발지로 기록한다면, 요청이 거부될 때 가장 먼저 볼 것이 바로 그 차이다.

세션을 소유한 프로세스를 통해 멈춰라. 무관한 셸을 죽이는 것이 아니다. 그다음 리스너가 사라졌는지 확인하라. 아직 응답하는 포트는 실제로 닫지 않은 세션이다. 이 연습이 도입한 프록시 설정만 제거하고 앱 구성의 나머지는 그대로 두라. 포워딩 규칙을 들고 있는 무인 터미널은 서비스가 아니며, 상시 프록시에는 승인된 수명 주기가 필요하고 잊힌 창이 아니다.

ss -ltn | grep 1080

이 구성으로 고칠 수 없는 장애

일부 증상은 위의 한계를 곧바로 가리키며, 이를 일찍 알아보면 프록시 디버깅에 반나절을 쓰지 않는다. 표는 보이는 것을 그것이 보통 뜻하는 것과 연결한다. 마지막 열의 어떤 것이든 설계의 질문이며 구성 오류가 아니다.

What you seeWhat it usually meansWhere to look
The app connects and nothing followsIt ignores proxy settings entirelyIts own network settings, not the SSH session
A name resolution error for the destinationThe SSH server cannot resolve itServer resolver and search domains
A UDP-only application failsDynamic forwarding carries TCP onlyThis arrangement cannot serve it
curl works, the browser does notThe browser is not using the socks5h endpointBrowser proxy configuration
The listener answers after the session endedAnother process owns port 1080The ss output, then the owning process

처음 세 줄은 사람들이 잘못 진단하는 것들인데, 각각이 프록시 자체에 대한 오류가 아니라 연결 시도를 만들어 내기 때문이다. 노트북에서 해석되는 이름은 서버의 리졸버에 대해 아무것도 말해 주지 않으며, 실패는 대신 인증서나 시간 초과 문제로 읽힌다. UDP만 쓰는 앱은 설계 안에서 망가진 것이 아니라 설계 밖에 있으며, 어떤 추가 ssh 옵션도 그것을 바꾸지 않는다. 브라우저 줄은 프록시 설정이 앱마다 다르다는 흔한 알림이다. 마지막 줄이 종료 확인이 존재하는 이유다. 아직 수신 중인 포트는 이제 다른 무언가가 그것을 소유한다는 뜻이다.

관련 글