두 개의 A 레코드, 하나의 주소, 하나의 데몬: 사용자가 어떤 이름을 입력하든 도착하는 패킷은 동일하고, 포트 1194에 묶인 프로세스에는 둘을 구분할 무엇도 없다. 이것은 수리를 기다리는 잘못된 설정이 아니다. 이름 조회가 표현할 수 있는 것의 한계다. 두 OpenVPN 인스턴스 사이에서 고르는 무엇이든 클라이언트가 연결에 넣고 프런트엔드가 되읽을 수 있는 값이어야 하며, 표준 OpenVPN 클라이언트는 그런 값을 평문으로 넣지 않는다. 이어지는 내용은 그것이 무엇을 뜻하는지, 그리고 어떤 구성이 실제로 트래픽을 나누는지다.

리졸버가 돌려주는 것에서 시작하라

클라이언트는 vpn-a.example을 묻고 주소를 받는다. 그 주소와 포트가 접속 대상의 전부다. 이름은 조회 과정에서 로컬로 소모되고 회선 위의 필드가 되지 않으므로, 반대편 리스너는 어떤 레이블이 쓰였는지 볼 수 없다. vpn-a.example과 vpn-b.example를 같은 호스트로 향하게 하면 두 조회 모두 같은 목적지를 돌려준다. 그래서 데몬은 차이를 기록하지 않은 채 두 연결을 하나의 인스턴스로 흡수한다. 주소 기반 프로필이 이름 기반 프로필과 똑같이 동작하는 것도 같은 이유다.

이것은 매혹적인 우회책의 계열도 닫는다. 추가 별칭, 한 소켓에서 이름마다 인증서를 고르는 일, 질의된 이름에 묶인 방화벽 규칙이다. 어느 것도 첫 패킷이 클라이언트를 떠나기 전에 이미 버려진 정보에 작용할 수 없다. 두 서버가 계속 분리되어야 한다면 분리는 연결 자체와 함께 이동해야 한다. 즉 주소, 포트, 또는 프로토콜이 정의하는 페이로드 필드다.

셈에 들어가는 유일한 선택자는 패킷 안쪽으로 지나간다

HTTP는 Host 헤더로, TLS는 ClientHello 안의 서버 이름 확장으로 이를 처리하며, 그래서 하나의 주소가 하나의 리스너 뒤에서 여러 사이트를 본다. 둘 다 회선 위 프로토콜이 특정 서비스에 대한 요청을 실어 나르기 때문에 동작한다. OpenVPN에 관한 핵심 사실은 중간 장비가 검사하는 계층에 그 두 필드가 모두 없다는 것이다.

표준 구성에서 제어 채널을 보호하는 TLS 핸드셰이크는 바깥 세션이 수립된 뒤에 일어나 그 안에서 운반된다. 따라서 그 세션을 종료하지 않은 리스너는 불투명한 스트림을 본다. 앞부분 바이트의 오프코드와 프레이밍이 식별 가능하므로 OpenVPN이라는 것까지는 알 수 있지만, 서버 이름이 있다면 암호화된 부분에 앉아 있다. 바깥 패킷에서 가상 호스트를 읽으려는 설계는 프로토콜이 애초에 거기에 쓰지 않는 필드를 읽는 것이다. 이것은 터널이 만들어진 방식에서 오는 것이지, 더 영리한 패턴 일치가 메울 수 있는 틈이 아니다.

프로토콜 분배기가 멈추는 곳

sslh 프로젝트가 문서화하는 종류의 멀티플렉서는 하나의 포트에서 듣고, 앞부분 바이트를 검사하며, 인식한 프로토콜을 쓰는 백엔드로 연결을 넘긴다. 하나의 포트로 SSH, HTTPS, OpenVPN을 함께 처리해야 할 때 이것은 실제 가치다.

sslh는 프로토콜마다 별도의 프로브를 두며, 그 TLS 프로브는 진짜 ClientHello에서 서버 이름이나 ALPN 값을 뽑아낼 수 있다. 기본 OpenVPN을 인식하는 것은 TLS 확장을 읽는 것과 다른 분류이고, 둘은 조합되지 않는다. 듣는 소켓에 ClientHello를 한 번도 보내지 않는 클라이언트는 프로브에 읽을 것을 남기지 않기 때문이다. 그래서 OpenVPN에 맞는 규칙은 “이것은 OpenVPN 클라이언트다”라고 답하고 더 나아가지 않는다. 그 규칙 뒤의 두 OpenVPN 백엔드는 구별되지 않고, 프런트엔드에는 선택의 근거가 없으며, 모든 세션이 같은 곳에 착지한다. 멀티플렉싱은 하나의 포트를 서로 다른 서비스 사이에 공유한다. 한 서비스 안에서 가상 호스팅을 하지는 않는다.

두 서버를 실제로 나누는 구성

가장 싸고 정직한 분리는 두 번째 공용 주소를 인스턴스마다 하나씩 두는 것이고, 기존 프로필은 서로 다른 주소를 가리키므로 필요한 것을 이미 갖추고 있다. 다음은 같은 주소의 다른 포트이며, 각 프로필의 숫자 하나 말고는 비용이 없다. TCP와 UDP는 포트 표를 따로 유지하므로 1194/tcp와 1194/udp는 무관한 소켓이고 각각 하나씩 같은 호스트에 공존할 수 있다. 이것은 이름 기반 라우팅이 아니라 전송 계층의 분리이며, 흔히 그것과 혼동된다. 두 선택지 모두 방화벽 검토를 통과한다, 규칙이 패킷이 실제로 담고 있는 것과 일치하기 때문이다.

필요한 경계가 두 구현 사이가 아니라 사용자 집단 사이에 있다면, 서로 다른 클라이언트 설정과 자체 인증서 처리, 명시적 접근 정책을 갖춘 하나의 인스턴스가 그것을 덮을 수 있다. 그것이 사는 것은 서버 내부의 정책이지 서버 사이의 격리가 아니며, 둘은 다른 요구다. 침해 상황에서도 버텨야 하는 격리는 여전히 주소나 방화벽 경계를 필요로 한다.

OpenVPN 앞에 둔 TLS 종단기는 유능한 프런트엔드에 실제 서버 이름을 노출할 수 있지만, 클라이언트 쪽도 그 추가 계층에 맞게 구성된 경우에만 그렇다. 기존 프로필은 같은 주소를 가리키는 것만으로 그 동작을 얻지 않는다.

만들기 전에 선택자를 증명하라

백엔드를 식별할 필드를 지목하고, 그다음 클라이언트가 정말 그것을 보내는지 확인하라. 답이 주소라면 계획은 유지되고, 듣는 쪽에 결코 닿지 않는 핸드셰이크 안의 서버 이름이라면 어떤 DNS 작업도 구하지 못한다. 세 개의 명령이 듣는 쪽을 빠르게 정리한다.

ss -lntup | grep -E ':443|:1194'
openssl s_client -connect vpn-a.example:443 -servername vpn-a.example </dev/null | head -n 20
journalctl -u openvpn-server@server-a -n 20 --no-pager

openssl 줄은 그 주소에서 무언가가 TLS를 종료하는 곳에서만 인증서를 낸다. 기본 OpenVPN 리스너 앞에서는 멈추거나 실패하며, 그것 자체가 바깥 계층이 무엇을 노출하는지에 대한 답이다. journal 줄은 각 백엔드에서 읽으면 어느 인스턴스가 세션을 받아들였는지 보여준다. 프런트엔드 로그는 분류된 것을 기록하고 의도된 것은 결코 기록하지 않으므로, 사용자가 어디에 착지했는지에 대한 권위는 백엔드에만 남는다.

서로 다른 서버에 닿도록 만든 두 프로필로 마무리하고, 하나씩 연결한 뒤 각각의 백엔드 로그를 기록하라. 두 세션이 같은 journal에 나타난다면 선택자가 설정 오류가 아니라 없는 것이며, 고칠 것은 클라이언트가 겨냥할 수 있는 주소나 포트다. 라우팅 규칙을 편집해도 도움이 되지 않는다, 규칙에는 라우팅할 재료가 한 번도 주어지지 않았기 때문이다. 분리가 실제가 되면 다음 사람이 두 번째 별칭을 더하고 그것이 일을 해 주리라 기대하지 않도록 프로필 옆에 적어 두라.

관련 글