VPN을 구성할 때, 특정 도메인에 대해서만 VPN을 활성화하고 싶은 경우가 있을 수 있다. 해당 기능에 대해 TailScale과 Netbird는 아래와 같은 방식으로 제공한다.
TailScale에서 제공하는 방식 (문서)
- DNS 쿼리를 로컬 TailScale 서버로 날리도록 한다.
- 로컬 데몬에서는 8.8.8.8과 같은 도메인 서버에 다시 요청을 날려서, 도메인 쿼리 결과를 기록해놓은 후 결과를 그대로 돌려준다.
- 기록한 IP에 대해 커넥터로 지정된 호스트에 라우팅하도록 한다.
- private 네트워크에 성공적으로 통신할 수 있다.
Netbird에서 제공하는 방식
- VPN 클라이언트에서 도메인 쿼리 결과를 주기적(기본 60s)으로 불러와 기록해놓는다.
- 저장된 IP 목록에 해당하는 요청이 발생하면 Netbird의 TURN 서버로 통신하도록 한다.
- private 네트워크에 성공적으로 통신할 수 있다.
직접 시도해본 방식
Netbird의 도메인 라우팅이 안드로이드, iOS 클라이언트에서 비정상적으로 동작하는 문제가 있어 아래와 같은 방법을 시도해보았다.
- 모든 도메인 쿼리 결과에 대해 자신의 public IP를 반환하는 DNS 서버를 구성한다. (linux bind와 같은 툴 사용)
- Netbird로 클라이언트의 DNS 쿼리 요청이 커스텀 DNS 서버를 향하도록 설정한다.
- 커스텀 DNS 서버가 항상 public IP를 반환하므로, 트래픽이 DNS 서버 호스트로 들어올 것이다.
호스트의 443 포트를 열고 요청이 들어온 도메인의 실제 IP로 포워딩하여 결과를 돌려준다. Nginx와 같은 Reverse Proxy를 사용할 수 있다. - private 네트워크에 성공적으로 통신할 수 있다.
Nginx로 설정했을 때 사용한 구성 파일은 다음과 같다.
여기서 Reverse Proxy로 Nginx를 사용했을 때 문제가 발생했다. 왜냐하면…
- 대상 서버의 변경 없이 요청을 프록시하기 위해 요청의 ssl을 terminate 시키지 않도록 설정했다.
- ssl을 terminate 시키지 않으면서, 즉 HTTPS 요청의 암호화가 해독되지 않은 상태에서 요청 도메인을 읽기 위해선 Nginx의 ssl preread 기능으로 SNI 정보를 읽어 사용해야한다.
- 다중 커넥션을 사용하는 HTTP/1.x와 달리 HTTP/2.0에서는 하나의 커넥션에 여러 요청을 보낸다. 모든 도메인이 같은 IP로 향하므로, 클라이언트에선
a.example.com
과b.example.com
을 같은 커넥션으로 전송한다. - 하지만 Nginx stream에서는 HTTP/2.0을 지원하지 않는다! 그래서 하나의 커넥션에 여러 요청이 들어왔을 때 요청들을 구분하지 못한다. 따라서
a.example.com
와b.example.com
가 같은 커넥션으로 들어오면, ssl 커넥션시 사용했던 도메인으로 모든 요청을 보내버린다. 따라서 트래픽이 의도한 도메인으로 전달되지 않는 경우가 생긴다!!
이 곳에서 이와 관련된 Nginx 사용자들의 논의를 볼 수 있다.
오픈소스 프로젝트인 dlundquist/sniproxy
도 비슷한 이슈 때문에 http2.0을 지원하지 못한다고 한다.
특정 도메인에 대한 요청을 프록싱하기 위해선 목적지 IP를 바꾸지 않는 방식으로 구현해야한다는 것을 몸소 느낄 수 있었다.
참고