Bạn không thể hỏi Android rằng VPN có đang kết nối hay không. VpnService không cung cấp getter trạng thái kết nối, và biểu tượng chìa khóa trên thanh trạng thái do hệ thống vẽ, không phải thứ mà ứng dụng của bạn đọc được. Điều bạn có thể làm là hỏi tầng kết nối một câu hẹp hơn: lưu lượng của ứng dụng này đang dùng mạng nào, và mạng đó có mang transport VPN không. Hướng dẫn này đi qua các bước kiểm tra theo thứ tự tránh dương tính giả, từ một lệnh shell trên thiết bị đến callback mà bạn để chạy trong ứng dụng.

Kiểm tra thiết bị từ shell trước khi chạm vào mã ứng dụng

Trước tiên hãy lấy một mốc chuẩn. adb shell dumpsys connectivity in ra mọi mạng mà hệ thống biết, và mỗi khối liệt kê các transport của nó, nên một mạng VPN xuất hiện với VPN trong danh sách đó. Đọc khối mạng mặc định trước mọi thứ khác, rồi tìm các mạng khác mang cùng transport.

Hai đầu ra trả lời hai câu hỏi khác nhau. Việc có một mạng VPN không giống với việc ứng dụng của bạn dùng nó, và trên thiết bị có hồ sơ công việc hoặc người dùng thứ hai, cả hai có thể cùng đúng một lúc trong khi tiến trình của bạn nói chuyện trực tiếp với internet. Dòng mạng đang hoạt động mới là thứ quyết định các socket của bạn làm gì.

Hỏi mạng mà ứng dụng đang dùng

Trong mã, bài kiểm tra khả năng gồm bốn dòng. Yêu cầu ACCESS_NETWORK_STATE trong manifest, rồi so sánh khả năng của mạng đang hoạt động với transport VPN:

ConnectivityManager cm = (ConnectivityManager)
    context.getSystemService(Context.CONNECTIVITY_SERVICE);
Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean vpnInUse = caps != null
    && caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);

Câu trả lời cố ý hẹp: mạng mặc định của ứng dụng có phải VPN ngay lúc này không. Nó không nói gì về việc là VPN nào, có phải của bạn không, hay đường hầm có tới đầu bên kia không. Hãy coi true là “lưu lượng đi ra qua một đường hầm nào đó” và giữ sổ sách của riêng bạn tách biệt. Một boolean lặng lẽ mang ba nghĩa cùng lúc chính là lý do màn hình cài đặt báo VPN đang tắt trong khi người dùng duyệt web qua nó.

Ngược lại, khi cần biết có mạng VPN nào không, hãy lặp qua cm.getAllNetworks() và kiểm tra từng tập khả năng. Hai câu trả lời khác nhau trên thiết bị chạy VPN theo từng ứng dụng, bao phủ các ứng dụng khác nhưng không bao phủ ứng dụng của bạn, và sự khác biệt đó là nguồn gốc của phần lớn báo cáo lỗi nói rằng ứng dụng không thấy VPN đang bật rõ ràng.

Theo dõi thay đổi thay vì thăm dò định kỳ

Thăm dò bằng bộ hẹn giờ là hình thức sai cho câu hỏi này, vì thay đổi trạng thái đến dưới dạng sự kiện. Đăng ký một yêu cầu khớp với transport VPN và để framework cho bạn biết khi một mạng khớp xuất hiện, biến mất hoặc thay đổi:

NetworkRequest request = new NetworkRequest.Builder()
    .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
    .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
    .build();
cm.registerNetworkCallback(request, callback);

Mạng VPN không bao giờ mang NET_CAPABILITY_NOT_VPN, nên việc loại bỏ tường minh ghi lại ý định. Không có nó yêu cầu vẫn khớp, nhưng người đọc mã tiếp theo phải tự tìm ra điều đó.

Đăng ký thuộc phạm vi cần quan sát, thường là một màn hình hoặc một dịch vụ nền trước. Hủy đăng ký trong onStop hoặc onDestroy, và đừng để callback gắn vào một activity đã biến mất. Ứng dụng không có thành phần hiển thị là chủ nhà tồi cho một quan sát viên thường trú, và những callback bạn quên chính là thứ sụp sau khi cấu hình thay đổi.

Giữ ba câu hỏi tách biệt

Phần lớn sự nhầm lẫn trong lĩnh vực này đến từ việc coi ba bước kiểm tra khác nhau là một. Chúng dùng API khác nhau, và mỗi cái có thể đúng trong khi các cái khác sai.

QuestionHow you answer itWhat a yes means
Did the user grant VPN consent to my app?VpnService.prepare() returns nullConsent exists; the tunnel may still be down
Is the device sending default traffic through a VPN?TRANSPORT_VPN on the active networkSomething is tunneling, not necessarily your app
Did this request arrive through the VPN?Source address seen by the serverProof for that connection, nothing wider

Dòng thứ ba là thứ mọi người cố trả lời ở phía client, và không thể trả lời ở đó. Hãy cấp cho client VPN một nhóm địa chỉ riêng và đọc địa chỉ nguồn trên máy chủ; đó là bằng chứng duy nhất tồn tại khi người dùng đổi đường hầm giữa phiên.

Thứ tự xác minh loại bỏ dương tính giả

Mã mẫu cũ vẫn còn lưu hành gọi getNetworkInfo(ConnectivityManager.TYPE_VPN). Lời gọi đó đã lỗi thời, và trên vài bản phát hành nó trả về null ngay cả khi VPN đang kết nối, đó là lý do nhiều đoạn mã sao chép trông hỏng trên thiết bị vẫn chạy. Các bài kiểm tra khả năng né bẫy đó và hành xử như nhau từ Android 5.0 trở đi. Nếu bạn duy trì mã nguồn vẫn gọi nó, hãy thay thế lời gọi thay vì bọc nó trong một phương án dự phòng, vì chính phương án dự phòng đã tạo ra câu trả lời sai ngay từ đầu.

Một lối tắt đáng được cảnh báo. So sánh địa chỉ công khai do một dịch vụ bên ngoài báo cáo với địa chỉ thoát của VPN có chứng minh điều gì đó về đường đi, nhưng nó thêm một yêu cầu bên thứ ba vào mọi lần kiểm tra, phụ thuộc vào việc dịch vụ đó còn hoạt động, và có thể bị lưu đệm hoặc chặn. Hãy giữ nó như chẩn đoán thủ công, không bao giờ là bước kiểm tra trạng thái bên trong ứng dụng.

Chạy các bước kiểm tra theo thứ tự này và dừng ở bước đầu tiên không khớp với báo cáo của người dùng: danh sách transport từ shell, khả năng mạng đang hoạt động, sự kiện callback khi bạn bật và tắt đường hầm, rồi đến địa chỉ nguồn phía máy chủ. Nếu cả bốn khớp nhau mà ứng dụng vẫn hành xử sai, lỗi nằm trong máy trạng thái của chính bạn, không phải trong cách Android nhìn mạng.

Bài viết liên quan