Danh sách cho phép của Android VpnService: vì sao trang được cho phép vẫn treo
Danh sách cho phép của bạn có lẽ vẫn ổn. Trang bị treo vì câu trả lời DNS, bảng định tuyến, socket của trình phân giải hoặc MTU. Đây là thứ tự kiểm tra.
Bạn đã thêm một tên miền vào danh sách cho phép, truy vấn đến được ứng dụng của bạn, bộ so khớp trả về cho phép, và trang vẫn không bao giờ tải. Lúc đó phần lớn mọi người bắt đầu viết lại bộ so khớp và thêm so sánh ký tự đại diện. Theo kinh nghiệm của tôi, bộ so khớp thường đúng, và lỗi nằm ở một tầng sâu hơn: trong câu trả lời DNS bạn tự tạo, trong tuyến đường bạn cài đặt, hoặc trong socket bạn dùng để chuyển tiếp truy vấn. Danh sách là phần dễ; đường đi của gói tin mới là nơi danh sách cho phép vỡ.
Bộ so khớp hiếm khi là thủ phạm
Điều đầu tiên cần tách bạch là bị chặn và bị hỏng. Một tên bị chặn phải thất bại nhanh và thấy được, vì trình duyệt nhận câu trả lời rằng tên không tồn tại và dừng lại. Một tên bị hỏng thì treo, thử lại qua IPv6, chuyển sang vận chuyển khác, và để lại cho bạn một vòng quay. Nếu bạn thấy vòng quay, danh sách cho phép của bạn có lẽ đang trả lời thứ mà ứng dụng khách không thể xử lý.
Bản ghi A rỗng là lỗi kinh điển. Trả NOERROR với không câu trả lời nào trông như một câu trả lời hợp lệ, nên trình phân giải stub chấp nhận, trình duyệt thử lại, thử AAAA, thử trình phân giải khác và bỏ cuộc muộn hơn nhiều. Mã trả lời 3, tức NXDOMAIN, với phần câu hỏi được lặp lại và ID giao dịch được sao chép, kết thúc cuộc trò chuyện ngay lập tức. Hãy dùng nó cho mọi tên bạn chủ động từ chối.
IPv6 cũng xứng đáng nhận quyết định tương tự thay vì một giá trị mặc định. Nếu bạn trả NXDOMAIN cho A và để AAAA đi qua, ứng dụng khách có thể đến tên bị chặn qua IPv6 và bộ lọc bị rò. Một số cách hiện thực trả lời truy vấn AAAA bị chặn bằng địa chỉ loopback để kết nối thất bại ngay thay vì chuyển sang phương án dự phòng. Hãy chọn một chính sách, áp dụng cho cả hai loại bản ghi, và ghi log cả hai.
Tuyến đường bắt tất cả thì sở hữu tất cả
Thiết kế chỉ DNS không bao giờ thêm addRoute("0.0.0.0", 0), nên chỉ truy vấn vào giao diện TUN còn mọi thứ khác vẫn dùng mạng bên dưới. Đó là hình dạng an toàn, và cũng giải thích vì sao một số danh sách cho phép có vẻ chạy trên Wi-Fi lại hỏng trên mạng di động.
Khoảnh khắc có một tuyến mặc định, mọi luồng là của bạn. Một gói TCP đến giao diện mà không được chuyển tiếp cũng không được trả lời sẽ nằm trong hàng đợi cho đến khi ứng dụng khách hết thời gian, và không có lỗi nào để ghi vì không có gì hỏng. Tôi từng thấy có người dành cả buổi chiều cho một danh sách cho phép hỏng mà thực ra chỉ là thiếu bộ chuyển tiếp cho cổng 443. Hãy ghi log giao thức và đích của mọi gói bạn quyết định bỏ.
Socket của trình phân giải phải ra khỏi đường hầm
Chuyển tiếp là nơi đặt cái bẫy tiếp theo. Trình phân giải của nhà mạng thường nằm ở địa chỉ riêng mà đường hầm không tới được, nên truy vấn bạn chuyển tiếp đi vào hư không và ứng dụng báo hết thời gian đọc. Hãy gửi các truy vấn đã duyệt đến một trình phân giải mà bạn biết là tới được, và đưa socket đó cho VpnService.protect() trước. Một socket không được bảo vệ sẽ đẩy truy vấn trở lại đường hầm của chính bạn, nơi nó lại đến ứng dụng và quay vòng cho đến khi hết thời gian.
Lỗi trông giống hệt một sự cố mạng, và đó là lý do nó sống lâu như vậy trong báo cáo lỗi. Hãy giữ bộ đếm truy vấn đã gửi và trả lời đã nhận: nếu số đầu tăng còn số thứ hai vẫn bằng không, bạn đang nhìn socket chứ không phải danh sách.
Kích thước và checksum hỏng mà không có thông báo
Việc bỏ im lặng cũng đến từ kích thước. Một câu trả lời DNS với tập trả lời lớn, hoặc bắt tay TCP với tùy chọn rộng rãi, có thể vượt MTU đường đi mà bạn chưa bao giờ đặt rõ ràng; mảnh ghép biến mất ở phía trên và ứng dụng khách hoặc thử lại nhỏ hơn hoặc thôi thử. Đặt MTU giao diện thành 1280 là lựa chọn thận trọng sống sót trên phần lớn đường di động, và nói rõ tốt hơn là thừa hưởng một giá trị mặc định mà không ai đo.
Nếu bạn tự dựng gói tin, checksum là kẻ giết người im lặng thứ hai. Checksum UDP tính trên một pseudo-header bỏ sót độ dài UDP hoặc trường giao thức tạo ra gói trông đúng ở phía bạn và bị bên nhận loại bỏ không dấu vết. Hãy so pseudo-header của bạn với đặc tả từng trường, và in checksum đã tính cho một luồng đã biết trong khi gỡ lỗi.
Những gì VpnService không làm thay bạn
Có hai giới hạn đáng biết trước khi thiết kế quanh chúng. Không ứng dụng thông thường nào cài được proxy toàn hệ thống hay tệp PAC ngoài Wi-Fi, và phần đó không thay đổi. Đường đi theo từng ứng dụng thì đã thay đổi: trước API 29, VpnService.Builder hoàn toàn không mang được proxy, và từ API 29, setHttpProxy(ProxyInfo.buildPacProxy(uri)) trao cho nền tảng một script PAC để đánh giá cho mạng VPN. Hãy đánh giá quy tắc trong mã của bạn khi nhắm API cũ hơn hoặc khi mô hình PAC không diễn đạt được quy tắc. UDP cũng không miễn phí: trình duyệt chỉ gửi TCP qua proxy SOCKS, và phần lớn ứng dụng khách SOCKS5 không bao giờ hiện thực UDP ASSOCIATE, nên thiết kế dựa trên proxy bỏ hoặc rò UDP trong khi TCP trông khỏe mạnh.
Giới hạn thứ hai đó là lý do một giao thức dựa trên UDP có thể chạy trên mạng thường và hỏng ngay khi VPN của bạn kết nối. Nếu đường hầm được kỳ vọng mang UDP, bộ chuyển tiếp phải hiện thực đường chuyển tiếp cho nó, không chỉ đường TCP với kiểu socket khác.
Kiểm tra theo thứ tự này
Làm danh sách theo thứ tự và dừng ở giai đoạn đầu tiên hỏng, vì mọi kiểm tra sau đó chỉ là nhiễu cho đến khi cái trước vững.
- Liệt kê các tuyến đã cài và xác nhận gói không phải DNS có vào giao diện TUN hay không.
- Ghi log tên truy vấn, quyết định và mã trả lời cho một tên bị chặn và một tên đã duyệt.
- Xác nhận socket của trình phân giải được bảo vệ và trình phân giải đã chọn trả lời từ bên trong đường hầm.
- Xác nhận MTU giao diện và checksum của một câu trả lời bạn tự dựng.
- Lặp lại cùng trang trên Wi-Fi và mạng di động, vì trình phân giải nhà mạng và MTU đường đi khác nhau.
Nếu cả năm đều vững mà trang vẫn treo, nghi phạm tiếp theo là chính bộ chuyển tiếp: so một yêu cầu đã bắt với một câu trả lời đã bắt và tìm số byte thiếu hoặc socket nửa đóng. Tài liệu tham chiếu VpnService.Builder bao quát các hợp đồng tuyến, DNS và MTU, và protect() là một lời gọi VpnService riêng chứ không phải tùy chọn của builder; một giao diện đã kết nối chỉ có nghĩa là hợp đồng đầu tiên đã được đáp ứng.