다른 VPN이 이미 활성화되어 있으면 VpnService 보호가 실패합니다
Android는 사용자당 VPN 인터페이스 하나만 허용합니다. prepare, establish, protect, 소유권 검사를 순서대로 실행한 뒤 해지 또는 앱별 제외를 선택하세요.
앱이 VpnService.prepare를 호출해 null을 받고 터널을 만들며 디스크립터도 유효해 보이지만 서버에는 아무것도 도달하지 않습니다. 또는 prepare가 Intent를 반환하고 사용자가 승인해도 establish가 계속 실패합니다. 두 결과는 대개 같은 뜻으로, 다른 앱이 이미 이 사용자의 VPN 인터페이스를 소유하고 있고 당신의 protect 호출이 터널이 쓸 수 없는 소켓을 표시하고 있는 것입니다. 뒤 단계가 앞 단계를 고칠 수 없으므로 아래 검사를 순서대로 진행하고 처음 실패하는 곳에서 멈추세요.
protect를 건드리기 전에 VPN 슬롯을 확인하세요
Android는 각 사용자에게 정확히 하나의 활성 VPN 인터페이스만 줍니다. 플랫폼 규칙이지 앱이 요청할 수 있는 권한이 아니므로, 둘 다 터널이 되려는 두 클라이언트는 공존할 수 없습니다.
다른 앱에 설정된 상시 VPN이 가장 어려운 경우로, 시스템이 그 앱을 연결 상태로 유지하고 인수를 거부합니다. AdGuard 같은 트래픽 필터, 기업용 클라이언트, 소비자용 VPN이 설치된 줄 모르는 사이에 슬롯을 차지할 수 있습니다. 사용자에게 설정을 열어 VPN 아래 항목을 읽게 하고, 그곳의 이름이 경쟁하는 소유자임을 알려 주세요. 그 항목이 연결 해제되거나 상시 VPN 설정이 지워질 때까지 당신의 VpnService는 인터페이스가 될 수 없습니다.
1단계: prepare가 무엇을 반환하는지 읽기
VpnService.prepare는 이 앱이 VPN이 될 수 있는가라는 한 가지 질문에 답합니다. 동의가 이미 있으면 null을, 사용자가 아직 승인해야 하면 Intent를 반환하며, 후자는 startActivityForResult로 실행해야 합니다. null 반환을 establish 성공의 증거가 아니라 계속 진행해도 된다는 허가로 보세요.
Intent intent = VpnService.prepare(context);
if (intent != null) {
startActivityForResult(intent, REQUEST_VPN);
}Intent 경로가 돌아왔는데 사용자가 거부했다면 prepare는 다음 시도에서도 계속 Intent를 반환합니다. 거부된 동의는 사용자의 결정이지 일시적 오류가 아니므로 루프에서 재시도하는 대신 결과 코드를 기록하세요.
2단계: 인터페이스가 당신의 앱 소유인지 확인하기
establish 후 받은 ParcelFileDescriptor가 소유권의 증거이며, 어떤 소켓 작업보다 먼저 확인할 가치가 있습니다. null이거나 이미 닫힌 디스크립터는 예외가 드러나지 않았더라도 인터페이스가 만들어지지 않았다는 뜻입니다. builder의 인터페이스 이름을 테스트 기기에서 dumpsys를 써서 시스템이 활성 VPN에 대해 보고하는 이름과 비교하세요. 둘이 다르다면 당신이 만든 터널은 트래픽을 실어 나르는 터널이 아닙니다.
adb shell dumpsys connectivity같은 불일치가 연결 아이콘은 있는데 트래픽은 죽은 상태를 설명합니다. 시스템은 다른 앱의 터널을 보여주고, 당신의 코드는 소유하지 않은 인터페이스의 디스크립터를 들고 있습니다.
3단계: connect 전에 protect 호출하기
인터페이스가 존재하면 앱이 여는 소켓은 기본적으로 터널로 라우팅됩니다. 외부 전송은 그 규칙을 따를 수 없어 캡슐화된 패킷이 인터페이스로 되돌아가므로 VpnService protect가 소켓을 표시해 VPN을 우회하고 기저 네트워크를 쓰게 합니다. 호출은 boolean을 반환하며, 이를 무시하는 것이 건강해 보이지만 아무것도 나르지 않는 소켓에 도달하는 길입니다.
protect는 connect나 bind 전에 호출하고 절대 그 뒤에 호출하지 마세요. 보호는 소켓이 연결되거나 바인딩되기 전에 자리 잡아야 하며, 연결된 소켓은 이미 경로를 골랐으므로 나중의 protect가 그것을 안정적으로 옮기지 않습니다. protect가 false를 반환하면, 즉 인터페이스가 당신의 앱 소유가 아닐 때는 멈추고 소유권을 다시 확인하세요. false 반환은 1단계로 돌아가라는 신호이지 같은 소켓을 다시 시도하라는 신호가 아닙니다.
DatagramSocket socket = new DatagramSocket();
if (!vpnService.protect(socket)) {
return;
}
socket.connect(serverAddress, serverPort);4단계: 소켓이 까다로울 때 프로세스를 바인딩하기
일부 전송은 단일 소켓이 아니어서 라이브러리가 여러 개를 열 수 있고, 헬퍼 프로세스의 모든 연결이 같은 출구를 쓰기를 원할 수도 있습니다. ConnectivityManager.bindProcessToNetwork는 프로세스를 선택한 네트워크에 바인딩하며, 더 오래된 setProcessDefaultNetwork도 지원 중단되기 전에 같은 일을 했습니다. 기저 연결을 NET_CAPABILITY_NOT_VPN으로 요청해 바인딩이 터널 자체가 아니라 터널 아래의 네트워크를 고르게 하세요.
NetworkRequest request = new NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();프로세스를 바인딩하는 것은 두 번째 VPN이 아닙니다. 그 프로세스의 출구를 고르는 것으로, 당신의 전송에는 유용하고 두 터널을 동시에 돌리는 데는 쓸모없습니다.
코드 경로가 아니라 사용자의 기기를 고치세요
두 앱이 VPN 슬롯을 공유하게 하는 API는 없으므로 해결책은 기기 상태에 있습니다. 사용자에게 다른 앱의 연결을 끊거나 상시 VPN을 지워서 당신의 prepare가 인수하게 하는 길과, 그 앱의 앱별 설정을 열어 당신의 앱을 그 터널에서 제외하는 길을 제시하세요. 제외는 나머지 모든 것에 대해 필터를 계속 돌리고 당신의 앱이 일반 연결을 쓰게 해 주며, 흔히 사용자가 실제로 원하는 결과입니다.
사용자는 어느 앱이 VPN을 소유하는지 거의 모르므로 두 길을 지원 문서에 적어 두세요. 설정의 활성 VPN 이름과 로그의 prepare, establish, protect 결과를 보내 달라고 하면 일 분 안에 어느 단계가 실패했는지 알 수 있습니다. 그런 다음 실제 트래픽으로 이름 조회, TCP 연결, 터널을 통한 요청 하나까지 확인하고 연결 아이콘만 보지 마세요. 유효한 디스크립터와 초록 표시는 기껏해야 소유권을 증명하며, 전송이 라우팅 규칙을 살아남았다는 것은 결코 증명하지 않습니다.
네 검사가 모두 통과했는데도 트래픽이 죽는다면 소유권은 더 이상 당신의 문제가 아닙니다. 라우팅과 서버 도달 가능성으로 넘어가고, VPN 앱이 설치되지 않은 기기에서 같은 자격 증명을 시험하세요.