Có hai cách để giữ các gói của chính tunnel ở ngoài tunnel: bảo vệ socket vận chuyển, hoặc đưa toàn bộ ứng dụng ra khỏi VPN. Chúng giải quyết những vấn đề khác nhau, và việc chọn cái rộng hơn chỉ vì dễ nối dây hơn chính là cách các ứng dụng mất quyền truy cập mạng cục bộ và DNS tách rời mà không hề hay biết. Hãy bắt đầu từ thứ phải nằm ngoài tunnel, rồi chọn phạm vi nhỏ nhất đạt được điều đó.

protect() loại bỏ gì khỏi tunnel

VpnService.protect() là một lệnh gọi cho từng socket. Bạn đưa cho nó socket hoặc descriptor mang tunnel, và Android đánh dấu socket đó để lưu lượng của nó dùng mạng bên dưới thay cho giao diện VPN. Mọi thứ khác ứng dụng gửi vẫn đi theo tuyến VPN thông thường.

Chính phạm vi hẹp đó là mục đích. Một socket được bảo vệ không thay đổi cách ứng dụng khách HTTP của bạn phân giải tên, giao diện mà các lệnh gọi phân tích của bạn dùng, hay điều gì xảy ra với kết nối thứ hai được mở sau đó.

Ba điều kiện quyết định phạm vi

Điều kiện thứ nhất là chỉ phần vận chuyển của tunnel có cần thoát ra hay không. Nếu lưu lượng khác của ứng dụng vẫn phải đi qua VPN thì bảo vệ socket là phạm vi đúng. Điều kiện thứ hai là bạn có kiểm soát việc tạo socket hay không. Một thư viện tự mở socket ở sâu bên trong sẽ không cho bạn chỗ nào để gọi protect() trừ khi nó phơi ra một móc nối hoặc chấp nhận một socket đã dựng sẵn. Điều kiện thứ ba là ứng dụng có phải tiếp cận trực tiếp mạng cục bộ hoặc cổng đăng nhập hay không, đây là một yêu cầu khác với việc giữ tunnel sống.

Loại trừ ứng dụng bằng addDisallowedApplication() là công cụ thô. Nó gỡ mọi kết nối ứng dụng tạo ra khỏi VPN, kể cả những kết nối bạn muốn kiểm tra, và trên một số thiết bị nó còn thay đổi cách hoạt động của việc khám phá cục bộ.

Lựa chọn thứ ba là ràng buộc phần vận chuyển vào một mạng đã chọn bằng bindProcessToNetwork() hoặc một tay nắm mạng ở mức socket. Điều này hữu ích khi thiết bị có cả Wi-Fi và mạng di động cùng lúc và bạn muốn tunnel đi qua một đường lên cụ thể.

Gọi nó trong lúc phần vận chuyển đang được tạo

Gọi protect() trong cùng khối tạo ra phần vận chuyển, trước connect() hoặc send(). Nếu lệnh gọi trả về false, hãy đóng socket và để việc thiết lập thất bại; tiếp tục sẽ để bạn với một phần vận chuyển mà các gói của nó có thể quay lại giao diện của chính bạn.

Hình dạng đủ nhỏ để giữ ở một chỗ:

DatagramSocket transport = new DatagramSocket();
if (!protect(transport)) {
    transport.close();
    throw new IOException("transport not protected");
}
transport.connect(serverAddress);

Ba chi tiết cắn ở đây. Thứ nhất, giá trị boolean là tín hiệu duy nhất bạn nhận được, vì vậy hãy ghi lỗi với đủ ngữ cảnh để phân biệt một sự đồng ý bị thu hồi với một lệnh gọi bị từ chối. Thứ hai, protect() bao trùm socket, không phải số descriptor: nếu bạn đóng socket và sau đó dùng lại chính số nguyên đó cho một socket khác, socket mới không được bảo vệ. Thứ ba, khi một thư viện gốc mở phần vận chuyển, hãy bảo vệ descriptor trước khi mã gốc kết nối, nếu không tuyến có thể đã được chọn. Tham chiếu VpnService là nơi để xác nhận những nạp chồng mà mức API đích của bạn thực sự cung cấp.

Kiểm thử đường bị chặn, không chỉ đường thuận lợi

Hãy kiểm chứng khi tunnel đang bật, vì đó là trạng thái duy nhất mà hai tuyến có thể khác nhau. Gửi một yêu cầu qua phần vận chuyển được bảo vệ và một yêu cầu từ một đường mã chưa từng gọi protect(), rồi so sánh điều đầu xa nhìn thấy: kết nối được bảo vệ phải đến với mạng bên dưới làm nguồn, trong khi kết nối không được bảo vệ hiển thị địa chỉ của chính tunnel và làm nhích các bộ đếm byte của tunnel. Trên thiết bị, ss hoặc netstat hiển thị đúng sự phân tách đó trước khi bất cứ thứ gì rời khỏi sóng vô tuyến.

Hãy thử việc thu hồi riêng, vì một thay đổi sự đồng ý sẽ phá giao diện mà không chạm vào mã socket của bạn. Thu hồi sự đồng ý VPN hoặc dừng dịch vụ, dựng lại phần vận chuyển, và xác nhận rằng protect() vẫn trả về true và phần vận chuyển kết nối lại khi sự đồng ý được cấp lại.

Nếu yêu cầu được bảo vệ vẫn đi theo tunnel, lệnh gọi bảo vệ có lẽ đang xảy ra sau khi socket đã được định tuyến. Hãy chuyển nó lên sớm hơn và thử lại.

Hãy ghi lại quyết định ngay cạnh phần thiết lập vận chuyển: socket nào được bảo vệ, lưu lượng ứng dụng nào bị loại trừ, và ứng dụng làm gì khi việc bảo vệ thất bại. Chính dòng đó ngăn một lần tái cấu trúc sau này mở rộng việc loại trừ ra toàn bộ ứng dụng chỉ vì một bài kiểm tra duy nhất đang hỏng. Nó cũng cho người tiếp theo cách kiểm tra ý định mà không phải dịch ngược tệp manifest.

Bài viết liên quan