Ứng dụng của bạn gọi VpnService.prepare, nhận null, dựng một đường hầm và descriptor có vẻ hợp lệ, nhưng không có gì đến được máy chủ. Hoặc prepare trả về một Intent, người dùng chấp thuận và establish vẫn thất bại. Cả hai kết quả thường có cùng ý nghĩa: một ứng dụng khác đã sở hữu giao diện VPN của người dùng này, và các lệnh gọi protect của bạn đang đánh dấu các socket mà đường hầm của bạn không dùng được. Hãy đi qua các kiểm tra dưới đây theo thứ tự và dừng ở kiểm tra đầu tiên thất bại, vì bước sau không thể sửa bước trước.

Kiểm tra chỗ VPN trước khi chạm vào protect

Android cấp cho mỗi người dùng đúng một giao diện VPN đang hoạt động. Đây là quy tắc của nền tảng, không phải quyền mà ứng dụng của bạn có thể yêu cầu, nên hai ứng dụng khách đều muốn trở thành đường hầm không thể cùng tồn tại.

VPN luôn bật được đặt cho ứng dụng khác là trường hợp khó nhất, vì hệ thống giữ ứng dụng đó kết nối và từ chối việc chiếm quyền. Một bộ lọc lưu lượng như AdGuard, ứng dụng khách doanh nghiệp hoặc VPN tiêu dùng có thể giữ chỗ trong khi người dùng đã quên rằng nó được cài đặt. Hãy bảo người dùng mở Cài đặt và đọc mục được liệt kê dưới VPN; tên ở đó chính là chủ sở hữu mà bạn đang cạnh tranh. Cho đến khi mục đó bị ngắt kết nối hoặc cài đặt luôn bật bị xóa, VpnService của bạn không thể trở thành giao diện.

Bước 1: đọc những gì prepare trả về

VpnService.prepare trả lời một câu hỏi duy nhất: ứng dụng này có được trở thành VPN không? Nó trả về null khi đã có sự đồng ý, và một Intent khi người dùng vẫn phải chấp thuận, mà bạn phải khởi chạy bằng startActivityForResult. Hãy coi việc trả về null là quyền được tiếp tục, không phải bằng chứng rằng establish sẽ thành công.

Intent intent = VpnService.prepare(context);
if (intent != null) {
    startActivityForResult(intent, REQUEST_VPN);
}

Nếu đường Intent quay lại và người dùng đã từ chối, prepare sẽ tiếp tục trả về Intent ở lần thử sau. Hãy ghi mã kết quả thay vì thử lại trong vòng lặp, vì sự đồng ý bị từ chối là quyết định của người dùng, không phải lỗi tạm thời.

Bước 2: xác nhận giao diện thuộc về ứng dụng của bạn

Sau establish, ParcelFileDescriptor bạn nhận được là bằng chứng sở hữu, và đáng để kiểm tra trước mọi công việc với socket. Một descriptor null hoặc đã đóng nghĩa là giao diện chưa bao giờ được tạo, kể cả khi không có ngoại lệ nào xuất hiện. So sánh tên giao diện từ builder với tên mà hệ thống báo cáo cho VPN đang hoạt động, dùng dumpsys trên thiết bị thử nghiệm. Nếu chúng khác nhau, đường hầm bạn dựng không phải đường hầm đang mang lưu lượng.

adb shell dumpsys connectivity

Cùng sự không khớp đó giải thích biểu tượng đã kết nối nhưng lưu lượng chết. Hệ thống hiển thị đường hầm của ứng dụng kia, trong khi mã của bạn giữ descriptor của một giao diện mà nó không sở hữu.

Bước 3: gọi protect trước connect

Khi giao diện tồn tại, các socket mà ứng dụng của bạn mở được định tuyến vào đường hầm theo mặc định. Lớp vận chuyển bên ngoài không thể theo quy tắc đó, nếu không gói được đóng gói sẽ quay lại giao diện, nên VpnService protect đánh dấu một socket để bỏ qua VPN và dùng mạng bên dưới. Lệnh gọi trả về một boolean, và bỏ qua nó là cách bạn kết thúc với một socket trông khỏe mạnh mà không mang gì.

Gọi protect trước connect hoặc bind, không bao giờ sau. Sự bảo vệ phải có trước khi socket được kết nối hoặc gắn, vì socket đã kết nối đã chọn tuyến của nó và một protect đến sau không di chuyển nó một cách đáng tin cậy. Nếu protect trả về false, hãy dừng lại và kiểm tra lại quyền sở hữu: nó thất bại khi giao diện không thuộc ứng dụng của bạn, đúng tình trạng mà một VPN khác để lại. Việc trả về false là tín hiệu quay lại Bước 1, không phải thử lại cùng socket đó.

DatagramSocket socket = new DatagramSocket();
if (!vpnService.protect(socket)) {
    return;
}
socket.connect(serverAddress, serverPort);

Bước 4: gắn một tiến trình khi socket khó xử

Một số lớp vận chuyển không phải một socket: một thư viện có thể mở nhiều socket, hoặc bạn muốn mọi kết nối trong một tiến trình phụ dùng cùng lối ra. ConnectivityManager.bindProcessToNetwork gắn tiến trình vào một mạng đã chọn, và setProcessDefaultNetwork cũ hơn cũng làm điều tương tự trước khi bị ngừng dùng. Yêu cầu kết nối bên dưới bằng NET_CAPABILITY_NOT_VPN để việc gắn chọn mạng bên dưới đường hầm của bạn chứ không phải chính đường hầm.

NetworkRequest request = new NetworkRequest.Builder()
    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
    .build();

Gắn một tiến trình không phải VPN thứ hai. Nó chọn lối ra cho tiến trình đó, hữu ích cho lớp vận chuyển của bạn và vô dụng cho việc chạy hai đường hầm cùng lúc.

Sửa thiết bị của người dùng, không phải đường đi của mã

Không có API nào cho phép hai ứng dụng chia sẻ chỗ VPN, nên cách khắc phục thuộc về trạng thái thiết bị. Hãy đưa ra cho người dùng hai con đường cụ thể: ngắt kết nối hoặc xóa cài đặt luôn bật của ứng dụng kia để prepare của bạn có thể tiếp quản, hoặc mở cài đặt theo từng ứng dụng của ứng dụng đó và loại trừ ứng dụng của bạn khỏi đường hầm của nó. Việc loại trừ giữ bộ lọc chạy cho mọi thứ khác và cho phép ứng dụng của bạn dùng kết nối thông thường, thường là kết quả mà người dùng thực sự muốn.

Viết cả hai con đường vào tài liệu hỗ trợ của bạn, vì người dùng hiếm khi biết ứng dụng nào sở hữu VPN. Đề nghị họ gửi tên VPN đang hoạt động từ Cài đặt và kết quả của prepare, establish và protect từ nhật ký của bạn, và bạn có thể nói trong một phút bước nào đã thất bại. Sau đó xác minh bằng lưu lượng thật: tra cứu tên, một kết nối TCP và một yêu cầu qua đường hầm, không chỉ một biểu tượng đã kết nối. Một descriptor hợp lệ và một chỉ báo xanh chứng minh quyền sở hữu là cùng lắm, không bao giờ chứng minh rằng lớp vận chuyển đã sống sót qua các quy tắc định tuyến.

Nếu cả bốn kiểm tra đều qua và lưu lượng vẫn chết, quyền sở hữu không còn là vấn đề của bạn. Hãy chuyển sang định tuyến và khả năng tiếp cận máy chủ, và thử cùng thông tin đăng nhập từ một thiết bị không cài ứng dụng VPN.

Bài viết liên quan