Android VpnService의 분할 터널링: 실제로 라우팅할 수 있는 범위
VpnService는 builder가 자신의 디스크립터로 넘긴 패킷만 전달합니다. 분할 터널링을 탓하기 전에 프리픽스, 프록시 인증, DNS, 앱 목록을 확인하세요.
VpnService 앱의 분할 터널링은 몇몇 구체적인 지점에서 깨지고, 고치는 방법은 지점마다 다릅니다. 클라이언트는 실패한 목적지 하나만 보여 주므로, 먼저 어느 경계에 부딪히는지 가려내야 합니다. builder가 받은 경로, 인증할 수 없는 프록시, 선언만 한 리졸버, 아니면 플랫폼이 사용자별로 적용하는 앱 목록입니다. 그래서 저는 네 단계 점검을 둡니다. 아래 순서로 진행하고 첫 실패에서 멈춥니다.
builder가 플랫폼에 무엇을 넘겼는지 확인하세요
VpnService.Builder는 선언 목록입니다. establish()는 가상 인터페이스용 ParcelFileDescriptor를 반환하고, 플랫폼은 addAddress와 addRoute의 프리픽스에 맞는 트래픽만 그 디스크립터로 보냅니다. addDnsServer는 어떤 리졸버에 질의할지 클라이언트에 알려 줍니다. 추가한 모든 프리픽스 밖의 목적지는 인터페이스가 무엇을 약속하든 앱에 도달하지 않습니다. 이 한 가지 사실이 코드를 바꾸기 전에 접수되는 분할 터널링 장애 보고의 대부분을 설명합니다.
builder가 조립한 주소, 경로, DNS 목록을 로그로 남기고 실패하는 목적지와 비교하세요. 관리 호스트용 /32 하나가 빠진 경우가 터널이 깨진 경우보다 훨씬 흔합니다. 목록이 맞으면 다음으로 넘어가고, 틀리면 나머지 점검은 헛수고입니다.
excludeRoute는 API 33부터만 존재합니다
목적지 기반 제외는 가장 자주 지원 스레드가 되는 요청입니다. builder.excludeRoute(IpPrefix)는 API 33에서 나왔습니다. 그 전에는 제외할 프리픽스만 빼고 모든 것에 경로를 추가하는 것이 유일하게 지원되는 근사였고, 이는 보완 프리픽스를 직접 계산해야 한다는 뜻입니다. 두 호출은 거울상이 아닙니다. 하나는 인터페이스 경로 집합에서 빼고, 다른 하나는 집합을 처음부터 만듭니다. 구형 기기는 모르는 호출을 예외로 던지거나 조용히 무시합니다. Build.VERSION.SDK_INT로 분기하고 실제 하드웨어에서 두 경로를 모두 시험하세요.
보완을 계산할 때는 프리픽스 정렬을 보세요. /16 경로 안의 /24 제외에는 나머지를 실제로 설명하는 덮개 프리픽스가 필요하며, 잘못된 /8 하나로는 안 됩니다. 최종 목록을 로그로 남기고 제외한 호스트가 여전히 터널 밖 경로로 해석되는지 확인하세요.
프록시 자격 증명은 표현할 수 없습니다
setHttpProxy(ProxyInfo)는 인터페이스를 HTTP 또는 SOCKS 프록시로 향하게 하고, PAC URL은 목적지별로 둘 사이를 고를 수 있습니다. ProxyInfo도 PAC 형식도 사용자 이름과 비밀번호를 담지 않으므로 인증이 필요한 프록시는 플랫폼 API 밖에 있습니다. 이것이 필요한 앱은 보통 사용자 공간 스택, tun2socks 또는 비슷한 엔진을 내장하고 407 과제에 스스로 응답합니다.
그 선택에는 결과가 따릅니다. 사용자 공간 스택은 자신에게 라우팅한 트래픽만 보므로 같은 디스크립터 경계 안에서 살아야 하고, 자기 업링크 소켓에도 같은 protect() 요구를 물려받습니다. protect()를 빼면 터널이 자기 전송을 붙잡고, 이는 첫 패킷 뒤에 멈추는 핸드셰이크로 나타납니다. 자격 증명을 요구하는 프록시로 시험하세요. 열린 프록시는 플랫폼 경로에서 통과할 뿐 아무것도 증명하지 않습니다.
앱별 목록과 DNS도 선언입니다
Builder에는 addAllowedApplication과 addDisallowedApplication이 모두 있고 서로 배타적입니다. 둘 다 호출하면 예외가 납니다. 어느 쪽이든 목록은 인터페이스 설정 시 사용자별로 적용되므로 직장 프로필 앱은 당신의 터널이 아니라 직장 프로필 터널의 지배를 받습니다. 어떤 API도 주지 않는 것은 다른 앱의 결정에 대한 통제입니다. 밖에서 패킷을 당신의 터널로 밀어 넣을 수 없고, 이미 활성화된 VPN을 닫거나 다시 구성할 수도 없습니다.
DNS도 같은 패턴입니다. addDnsServer는 리졸버 주소를 알릴 뿐, 앱이 질의에 응답하게 하지는 않습니다. 이름을 직접 해석하려면 리졸버를 돌리고 그 주소를 터널로 라우팅하세요. 고정 호스트 이름에 DoT나 DoH를 쓰는 앱은 알린 주소를 우회할 수 있으므로, 질의 경로까지 가로채지 않는 한 이름 해석은 최선의 노력으로 다루세요.
매니페스트, always-on, 네 단계 테스트
자격은 매니페스트에 선언합니다. BIND_VPN_SERVICE, android.net.VpnService 인텐트 필터, 그리고 터널이 백그라운드에서 살아남아야 한다면 포그라운드 서비스입니다. Always-on VPN은 추가로 깨끗한 onRevoke를 요구합니다. 사용자가 설정을 바꾸거나 다른 앱이 제어를 넘겨받을 때 시스템이 이를 호출하기 때문입니다.
점검을 순서대로 실행하고 첫 실패에서 멈추세요. 첫째, builder에 넘긴 주소, 경로, DNS 목록을 출력합니다. 둘째, establish()가 디스크립터를 반환했고 전송 소켓이 보호되는지 확인합니다. 셋째, 포함된 프리픽스 안의 목적지 하나와 제외된 프리픽스 안의 목적지 하나를 요청합니다. 넷째, 당신의 리졸버만 해석할 수 있는 호스트 이름을 질의해 어느 리졸버가 실제로 응답했는지 확인합니다.
완료된 핸드셰이크는 이후 단계를 하나도 증명하지 않습니다. 각 점검은 한 계층을 분리하므로, 둘째 단계의 성공은 넷째 단계에 대해 아무것도 말해 주지 않습니다.
VpnService.Builder 참조는 builder의 전체 표면을 나열합니다. 그 밖에 필요한 것은 다른 API, 다른 앱, 또는 당신의 사용자 공간 스택이며, 그것은 분할 터널 버그가 아니라 설계 결정입니다.