Android VpnService가 일부 트래픽만 캡처하는 이유
경로, DNS 항목, 앱별 범위가 어떤 패킷이 tun 장치에 도달하는지, 로컬 장치가 사라지는 이유, 빌더가 거부하는 것을 결정합니다.
VpnService 자체가 트래픽을 가로채지는 않습니다. 시스템에 tun 인터페이스를 요청하고, 그 인터페이스에 주소와 경로의 집합을 넘기면, 그 순간부터 커널은 그 경로와 일치하는 모든 패킷을 앱이 읽을 수 있는 파일 디스크립터로 복사합니다. 앱 코드는 경로가 선택하지 않은 패킷을 결코 보지 못하므로, 터널의 범위는 프로그래밍 질문이 되기 전에 라우팅 테이블 질문입니다. 그래서 같은 서비스 구현이 한 휴대폰에서는 모든 흐름을 삼키고 다른 휴대폰에서는 일부만 삼키며, 경로를 추가하면 끊어진 것처럼 보이던 터널이 고쳐지기도 합니다. 얻는 것은 선언한 것입니다, 빌더가 조용히 거부한 것을 빼고.
어떤 패킷이 도착하는지를 경로가 결정한다
Builder.addRoute(prefix, prefixLength)는 VPN 네트워크의 라우팅 테이블에 프리픽스를 설치합니다. 시스템은 그 네트워크를 범위 내 앱의 기본으로 취급하므로, 그 항목은 물리 네트워크의 기본 경로보다 먼저 조회되고 일치하는 프리픽스만으로도 흐름을 우회시킬 수 있습니다. 따라서 0.0.0.0/0에 대한 경로는 IPv4를 전부 가져가며 프린터, cast 대상, 라우터의 관리 페이지까지 포함합니다.
물리 기본 항목을 지우지 않고 그 범위를 원하는 클라이언트는 대신 0.0.0.0/1와 128.0.0.0/1 쌍을 사용합니다. 더 구체적인 두 경로가 기본 경로를 이기고, 함께 전체 주소 공간을 덮으며, 휴대폰 자체의 기본 항목은 여전히 물리 네트워크에 묶인 소켓을 위해 테이블에 남습니다.
주소와 DNS 항목은 같은 빌더에서 오며 다른 질문에 답합니다. addAddress는 인터페이스에 프리픽스 길이를 가진 주소를 주고, addDnsServer는 범위 내 앱이 인터페이스를 통해 질의할 리졸버를 설치합니다. 두 계열 모두 한 인터페이스에 있을 수 있으므로 IPv6 주소 옆에 IPv4 전용 경로 집합을 두는 구성은 올바르게 읽히고도 누출됩니다. 인터페이스에 주소가 없는 계열을 라우팅하면 커널은 그 패킷의 출발지 주소를 갖지 못하고 패킷은 디스크립터에 결코 나타나지 않습니다.
establish()가 돌려주는 것
establish()는 ParcelFileDescriptor를 반환하거나, 앱이 승인되지 않았을 때 null을 반환합니다. null은 네트워크 오류가 아니라 동의 상태이므로, VpnService.prepare()가 이미 null을 반환했거나 사용자가 호출 전에 시스템 대화상자를 승인했어야 합니다. 동의는 서비스가 실행되는 동안 철회될 수 있고, 인터페이스는 읽기 루프에 대한 콜백 없이 발밑에서 닫힙니다.
디스크립터는 달리 말하지 않는 한 비차단이므로, 읽기는 스레드를 세우는 대신 데이터 없이 즉시 반환합니다. 차단 소켓용으로 작성된 루프는 코어 하나를 태우고, 빈 읽기를 치명적으로 보는 루프는 잠깐의 유휴 시간에 터널을 찢습니다. Builder.setBlocking(true)가 그 동작을 바꾸며, 대가는 읽기 내부에서 기다리는 스레드입니다.
경로와 주소 인수는 LinkAddress 값으로 검증되고, 224.0.0.0/4 같은 멀티캐스트 대상은 즉시 거부됩니다. 점대점 tun 인터페이스에는 브로드캐스트 도메인도 없으므로, 그 너머의 멀티캐스트 검색에는 경로를 하나 더 두는 것이 아니라 다른 설계가 필요합니다. 확인해야 할 목록은 VpnService.Builder이며, 호출이 무시되었다고 결론내리기 전에 살펴보십시오.
앱별 범위는 집합을 좁히지 터널을 좁히지 않는다
API 21부터 빌더는 addAllowedApplication와 addDisallowedApplication를 받아들이고, 두 목록은 한 빌더 안에서 상호 배타적입니다. 허용 목록은 그 패킷을 위해 터널을 존재하게 하고 나머지는 물리 네트워크를 유지하며, 금지 목록은 지정된 패킷 외의 모든 것을 잡고, 클라이언트가 분할 터널링으로 제공하는 것이 바로 이것입니다. API 21 이전에는 플랫폼 메커니즘이 아예 없었고, 앱별 동작은 자체 포워더 안에서 UID로 필터링하는 서드파티 클라이언트에서 왔습니다. 이 구분은 디버깅할 때 중요한데, 패킷 하나짜리 허용 목록을 가진 앱별 터널은 트래픽을 거의 만들지 않으며 그것은 올바른 동작이지 끊어진 경로가 아닙니다.
범위가 무엇이든, 앱이 VPN 서버로 여는 소켓은 자신이 만드는 터널을 통과해서는 안 됩니다. VpnService.protect(socket)는 그 소켓을 하위 네트워크에 묶어 패킷이 tun 장치를 건너뛰게 합니다. 그 호출을 빼먹으면 핸드셰이크는 그것을 띄워야 할 인터페이스에 삼켜집니다.
VpnService.setUnderlyingNetworks는 어느 물리 네트워크가 터널을 나르는지 시스템에 알려 고리를 닫습니다. 그 값은 VPN이 다른 앱에 연결성을 어떻게 보고하는지를 좌우하며, null이거나 오래된 값은 휴대폰의 나머지가 오프라인으로 취급하는 연결된 터널의 흔한 원인입니다. 하위 네트워크가 바뀔 때 갱신하고, 시작할 때만 하지 마십시오.
체크섬과 MTU: 올바른 경로도 실패하는 지점
패킷이 도착한 뒤, 패킷 수준의 두 가지 세부가 무엇이든 끝에서 끝까지 동작하는지를 결정합니다. 첫째는 출발지 주소로, 인터페이스 주소가 여러 개면 커널이 첫 번째를 실은 패킷을 내보낼 수 있으므로, 헤더를 다시 쓰는 포워더는 주소와 그를 덮는 체크섬을 고쳐야 합니다.
둘째는 체크섬의 범위입니다. 앱이 TCP나 UDP 패킷을 직접 만들 때 체크섬은 출발지 주소, 목적지 주소, 프로토콜 번호, 전송 길이를 담은 의사 헤더를 덮으며, 그중 하나라도 빼면 도착한 뒤 버려지는 패킷이 됩니다. 그 실패는 결코 오지 않는 응답 뒤의 시간 초과처럼 보이며, 이는 대부분의 사람을 경로로 되돌려 보냅니다. setMtu는 인터페이스의 MTU를 고정하고, 각 읽기는 정확히 하나의 완전한 IP 패킷을 돌려주므로, 바이트 스트림용으로 작성한 버퍼는 패킷이 아니라 파싱을 조각냅니다.
계층을 분리하는 검증 순회
범위가 어디서 어긋났는지 찾으려면 계층을 순서대로 시험하십시오. VPN 네트워크가 실제로 어떤 경로를 보유하는지 장치에 물은 다음, 알려진 주소로 가는 흐름이 디스크립터의 읽기로 나타나는지 확인하고, 다음으로 포워더가 내보내는 패킷이 잘 형성되었는지 확인하십시오. 경로 문제는 디스크립터에 아무것도 보이지 않게 하고, 체크섬 문제는 도착한 뒤 사라지는 패킷을 보이게 합니다.
디스크립터에 아무것도 도달하지 않으면 의심할 것은 읽는 쪽이 아니라 경로 집합이므로, 프리픽스를 확인하고, 같은 계열의 주소가 있는지 확인하고, 앱이 설정한 범위 안에 있는지 확인하십시오. 패킷은 디스크립터에 도달하지만 아무것도 돌아오지 않으면, 다시 경로를 건드리기 전에 protect()와 하위 네트워크 값을 살펴보십시오. 먼 쪽이 잘못된 패킷을 보고하면 체크섬과 MTU가 볼 곳입니다. 계층마다 한 번의 시험이 반나절의 추측을 아껴 줍니다.