터널 자체의 패킷을 터널 밖에 두는 방법은 두 가지다. 전송 소켓을 보호하거나, 앱 전체를 VPN에서 빼는 것이다. 둘은 서로 다른 문제를 풀며, 배선하기 쉽다는 이유로 더 넓은 쪽을 고르는 것이 앱이 알아채지 못한 채 로컬 네트워크 접근과 분할 DNS를 잃는 길이다. 터널 밖에 남아야 하는 것부터 시작하고, 그것을 달성하는 가장 작은 범위를 고르라.

protect()가 터널에서 제거하는 것

VpnService.protect()는 소켓 단위 호출이다. 터널을 실어 나르는 소켓이나 디스크립터를 넘기면, Android가 그 소켓을 표시해 트래픽이 VPN 인터페이스 대신 하위 네트워크를 쓰도록 한다. 앱이 보내는 나머지는 계속 일반 VPN 경로를 따른다.

바로 그 좁은 범위가 핵심이다. 보호된 소켓은 HTTP 클라이언트가 이름을 해석하는 방식도, 분석 호출이 쓰는 인터페이스도, 나중에 열리는 두 번째 연결의 처리도 바꾸지 않는다.

범위를 결정하는 세 가지 조건

첫째 조건은 터널 전송만 빠져나가야 하는지다. 앱의 다른 트래픽이 계속 VPN을 거쳐야 한다면 소켓 보호가 올바른 범위다. 둘째 조건은 소켓 생성을 직접 제어하는지다. 내부 깊은 곳에서 자체 소켓을 여는 라이브러리는 훅을 노출하거나 미리 만든 소켓을 받지 않는 한protect()를 호출할 자리를 남기지 않는다. 셋째 조건은 앱이 로컬 네트워크나 captive portal에 직접 닿아야 하는지이며, 이는 터널을 살려 두는 것과 다른 요구다.

앱을addDisallowedApplication()로 제외하는 것은 무딘 도구다. 앱이 만드는 모든 연결을 VPN에서 빼버리며, 검사하려던 연결까지 포함된다. 일부 기기에서는 로컬 검색 동작도 바뀐다.

세 번째 선택지는 전송을 선택한 네트워크에 묶는 것이다.bindProcessToNetwork()또는 소켓 수준 네트워크 핸들을 쓴다. 기기에 Wi-Fi와 셀룰러가 동시에 있고 터널을 특정 업링크로 보내고 싶을 때 유용하다.

전송이 만들어지는 동안 호출하라

호출하라protect()전송을 만드는 같은 블록에서,connect()또는send()보다 먼저. 호출이 false를 돌려주면 소켓을 닫고 설정을 실패시켜라. 계속하면 패킷이 자기 인터페이스로 다시 들어올 수 있는 전송을 안고 가게 된다.

형태는 한 곳에 두기에 충분히 작다.

DatagramSocket transport = new DatagramSocket();
if (!protect(transport)) {
    transport.close();
    throw new IOException("transport not protected");
}
transport.connect(serverAddress);

여기서 세 가지 세부가 물린다. 첫째,불리언이 얻을 수 있는 유일한 신호다, 그러니 철회된 동의와 거부된 호출을 구별할 만큼의 문맥과 함께 실패를 기록하라. 둘째,protect()는 디스크립터 번호가 아니라 소켓을 덮는다: 소켓을 닫고 나중에 같은 정수를 다른 소켓에 재사용하면 새 소켓은 보호되지 않는다. 셋째, 네이티브 라이브러리가 전송을 열 때는 네이티브 코드가 연결하기 전에 디스크립터를 보호하라. 그렇지 않으면 경로가 이미 선택되었을 수 있다.VpnService 레퍼런스는 대상 API 수준이 실제로 어떤 오버로드를 제공하는지 확인하는 곳이다.

행복한 경로만이 아니라 차단된 경로를 시험하라

터널을 올린 상태에서 검증하라. 두 경로가 달라질 수 있는 유일한 상태이기 때문이다. 보호된 전송으로 요청 하나를 보내고,protect()를 한 번도 호출하지 않은 코드 경로에서 또 하나를 보낸 뒤, 먼 쪽이 보는 것을 비교하라: 보호된 연결은 하위 네트워크를 출발지로 도착해야 하고, 보호되지 않은 연결은 터널 자체의 주소를 보이며 터널의 바이트 카운터를 움직인다. 기기에서는ss나netstat가 무언가가 라디오를 떠나기 전에 같은 분리를 보여준다.

철회는 따로 시험하라. 동의 변경은 소켓 코드를 건드리지 않고 인터페이스를 무너뜨린다. VPN 동의를 철회하거나 서비스를 멈추고, 전송을 다시 만들고,protect()가 여전히 true를 돌려주며 동의가 다시 주어지면 전송이 다시 연결되는지 확인하라.

보호된 요청이 그래도 터널을 따라간다면, 보호 호출이 소켓이 이미 라우팅된 뒤에 일어나고 있을 가능성이 크다. 더 앞으로 옮기고 다시 시험하라.

결정을 전송 설정 옆에 적어 두라. 어떤 소켓이 보호되고, 어떤 앱 트래픽이 제외되며, 보호가 실패할 때 앱이 무엇을 하는지. 바로 그 한 줄이 테스트 하나가 실패했다고 해서 나중의 리팩터가 제외를 앱 전체로 넓히는 것을 막는다. 다음 사람에게 매니페스트를 역으로 뜯어보지 않고 의도를 확인할 방법도 준다.

관련 글