흔한 루프 보호는 호스트 경로다: VPN 서버의 공인 주소로 가는 트래픽을 업링크로 내보내 암호화된 세션이 자기 터널로 들어가지 않게 한다. 이 방식은 같은 서버의 웹 서비스에도 터널을 통해 도달하고 싶은 순간까지 작동하는데, 예외가 목적지에 묶여 있고 목적지가 동일하기 때문이다. 출구는 목적지로 분류하는 것을 멈추고 어느 소켓이 패킷을 보냈는지로 분류하는 것이다.

분리하려는 두 흐름에서 시작하자

한 흐름은 암호화된 OpenVPN 전송 자체로, 업링크로 나가야 하며 그렇지 않으면 터널이 자기 외부 연결을 실어 나르게 된다. 다른 하나는 같은 공인 주소로 가는 평범한 HTTPS 요청으로, 서비스에 터널 출발지로 도달하도록 터널 안에 두려는 것이다. 목적지는 같고 많은 경우 포트 번호도 같지만 필요한 경로는 다르다.

이 설명에서는 10.90.0.10의 클라이언트가 tun1을 통해 있고 서버 공인 주소는 198.51.100.25이며, 기본 경로가 일반 업링크를 가리키는 손대지 않은 메인 테이블을 가정한다. 서버 쪽은 안쪽 경로에서 웹 서비스를 받아들이고 트래픽을 터널 출발지로 돌려보내야 하는데, 공인 주소로 패킷을 보내는 것이 서버의 리스너나 방화벽 정책을 무효화하지는 않는다.

무엇이든 바꾸기 전에 호스트의 현재 규칙을 기록하고, 아무도 쓰지 않는 값 곧 표시, 테이블 번호, 세 개의 규칙 우선순위를 고르자. 이 연결을 거치지 않는 복귀 경로를 확보해 두어야 하는데, 여기서의 실수는 브라우저 탭을 깨는 것이 아니라 자신의 원격 접근을 없앤다.

봉투를 실어 나르는 소켓을 표시하자

클라이언트 구성은 암호화된 발신 패킷에 표시를 얻고 스스로 경로를 설치하는 것을 멈추므로, 경로를 결정하는 것은 대체 정책뿐이다:

mark 42
route-noexec
disable-dco

십진수 42는 십육진수 0x2a이며, 라우팅 규칙이 일치시킬 것은 바로 이 십육진수 형식이다. link-options 참조는 mark가 Linux에서 암호화된 발신 패킷에 적용된다고 설명하므로, 바로 경로를 제어해야 하는 소켓이다. 데이터 채널 오프로드를 끄면 이 예시는 사용자 공간 경로에 머물고, 설명된 표시 동작이 그대로 적용된다.

route-noexec는 OpenVPN의 자동 경로 추가와 제거를 억제한다. 그 자리에 아무것도 설치하지 않으므로, 이 순간부터 경로 소유자가 터널에 필요한 모든 경로를 유지해야 한다. 변경 후 사설 목적지가 사라진다면 그것은 빠진 대체 정책이지 표시의 결함이 아니다.

이 작업을 하는 동안 198.51.100.25에 대한 호스트 경로를 메인 테이블에 남기지 말자. 그런 경로는 설계가 좁히려는 예외를 그대로 되살리고, 아래 정책 중 어느 것도 참조되기 전에 HTTPS 흐름에서 이긴다.

나머지는 모두 터널 테이블로 보내자

터널 인터페이스가 존재하고 주소를 가지면 정책은 이렇게 된다:

ip -4 route add default dev tun1 src 10.90.0.10 table 47100
ip -4 rule add priority 20000 lookup main suppress_prefixlength 0
ip -4 rule add priority 20001 not fwmark 0x2a lookup 47100
ip -4 rule add priority 20002 not fwmark 0x2a unreachable

첫 규칙은 승인된 로컬 네트워크를 포함한 호스트 고유 경로를 보이게 유지하면서, 이 조회에서 메인 테이블의 기본 경로를 보이지 않게 만든다. 이 구분이 중요하다. 기본 경로를 억제하는 것은 테이블을 무시하는 것과 다르고, 아직 필요한 로컬 예외가 살아남는 것은 바로 이 때문이다.

표시되지 않은 트래픽은 터널 테이블로 떨어져 터널 출발지를 얻고 VPN으로 나간다. 표시된 트래픽은 다음 두 규칙의 fwmark 검사에서 실패하고, 업링크 기본 경로가 아직 있는 목록 더 아래의 일반 메인 조회로 계속 간다. 마지막 unreachable 규칙은 터널 경로가 없을 때 표시되지 않은 흐름이 조용히 업링크로 되돌아가는 것을 막는다. 선택자는 처음부터 끝까지 표시이며, 목적지 주소는 결정에 들어오지 않는다.

그 가드를 연결 해제를 넘겨 유지하고, 손으로가 아니라 정책의 나머지와 함께 설치하자. 인터페이스와 함께 사라지는 규칙은 정책이 아니라 우연이다. 터널이 떨어질 때 정책을 지우면 실패 모드가 '차단'에서 '누출'로 바뀌는데, 나중에 만료되는 오래된 터널 경로가 잘못된 인터페이스로 나간 트래픽보다 훨씬 설명하기 쉽다.

복귀 경로도 같은 표시를 보게 하자

발신 표시는 메커니즘의 절반일 뿐이며, 여기가 대부분의 재현이 빠뜨리는 부분이다. 암호화된 세션에 대한 응답은 업링크로 도착하고, 출발지가 온 길로 되돌아가는지 묻는 복귀 경로 검증을 받는다. 도움이 없으면 그 조회는 표시되지 않은 터널 정책을 적용하고 패킷이 잘못된 인터페이스에 있다고 결론짓는다.

수정은 방화벽 소유자의 몫이다: 발신 연결에 표시를 저장하고, 일치하는 수신 패킷에 복원하고, 해당 경로에 출발지 검증 표시 처리를 활성화하자. OpenVPN이 소켓 표시를 설정하는 것은 이 세 가지 중 어느 것도 하지 않으며, 클라이언트 옵션만 나열한 레시피는 불완전하다. 그 통합을 서면으로 요청하자, 설계에서 가장 눈에 띄지 않고 재구축에서 가장 잃기 쉬운 부분이기 때문이다.

느슨한 복귀 경로 검사도 대체가 아니다. 느슨한 모드도 출발지에 대한 어떤 경로가 있어야 하고, 추가한 unreachable 가드는 표시되지 않은 조회에 대해 그것을 의도적으로 제거한다. 이 설계에 의존하기 전에 연결 표시 통합을 완료하고 검토받자, 그리고 호스트 필터는 별개의 질문으로 다루자: 표시는 라우팅 신호이지 허가가 아니다. 통합이 빠져 있으면, 증상은 세션이 수립된 뒤 응답이 도착하기 시작하자마자 조용해지는 것이다.

두 번의 경로 조회로 증명하자

커널이 두 문맥에서 무엇을 결정하는지 비교하고, 종료 상태가 아니라 dev와 src 필드를 읽자:

ip -4 route get 198.51.100.25
ip -4 route get 198.51.100.25 mark 0x2a

첫 조회는 터널 출발지를 가진 tun1로, 두 번째는 업링크로 해석되어야 한다. 합성 조회이므로 정책을 증명할 뿐 실행 중인 프로세스는 증명하지 않는데, 실행 중인 클라이언트의 실제 전송 경로를 따로 확인한 다음 인증서 검증을 켠 채 실제 HTTPS 요청을 하자.

완료라고 선언하기 전에 설계가 다루지 않는 것을 적어 두자. DNS, IPv6, 규칙의 수명 주기는 각각 별도의 결정이 필요하며, 이것은 IPv4 라우팅 예시이지 완전한 프라이버시 구성이 아니다. 그 가치는 선택자에 있다: 봉투는 업링크 예외를 얻고, 같은 주소의 평범한 서비스는 그것을 물려받지 않아도 된다.

관련 글