Android VpnService: đọc TUN bị treo so với lỗi ghi ENOBUFS
Một lần đọc không bao giờ trở lại và một lần ghi thất bại với ENOBUFS có nguyên nhân trái ngược. Đo cửa sổ được công bố và độ dài gói để chọn cách sửa.
Hai triệu chứng xuất hiện trong cùng một vòng lặp VpnService và cả hai đều bị đổ lỗi cho cùng một thứ: một trình đọc không bao giờ trở lại, và một lần ghi thất bại với ENOBUFS. Chúng không phải là biến thể của một lỗi. Đọc bị treo là bất đồng về điều khiển luồng với peer; ghi thất bại là một gói mà giao diện sẽ không chấp nhận. Nếu coi chúng là một vấn đề, bạn sẽ mất một ngày tinh chỉnh các bộ đệm chưa từng liên quan. Cách phân chia dưới đây là cách tôi quyết định đo nửa nào của vòng lặp trước.
Một vòng lặp, hai điểm hỏng độc lập
Dịch vụ của bạn đọc một gói IP từ bộ mô tả tệp TUN, kiểm tra nó và ghi phản hồi trở lại cùng bộ mô tả đó. Mọi thứ hỏng trên đường đó đều lộ ra ở một trong hai lời gọi, vì vậy cả hai đều được báo là đường hầm hỏng. Hai lời gọi có hợp đồng khác nhau. Đọc chờ hạt nhân trao cho bạn một gói, còn ghi trao cho hạt nhân một gói mà nó có quyền từ chối.
Đọc là nửa dễ lý luận. Một lần đọc không trả về gì hoặc đang chờ lưu lượng không bao giờ đến hoặc chờ vì peer đã ngừng gửi.
Đọc bị treo là bất đồng về điều khiển luồng
Đọc một thiết bị TUN sẽ chặn cho đến khi có gói. Đó là hành vi được thiết kế, và đó là lý do một luồng đỗ trong read trông bình thường trên một liên kết rảnh. Nó trở thành treo khi ngăn xếp TCP của chính bạn tiếp tục nhận dữ liệu từ ứng dụng cục bộ nhưng không bao giờ công bố đủ cửa sổ để peer từ xa gửi thêm. Peer lấp đầy cửa sổ bạn đã công bố, chờ ACK và cập nhật cửa sổ, rồi dừng lại. Khi đó trình đọc của bạn không còn gì để trả về, và ứng dụng thấy một kết nối mở nhưng im lặng.
Nguyên nhân phổ biến là ngăn xếp ghi vào socket mà không theo dõi cửa sổ đã công bố, và một ngăn xếp khác quên gửi cập nhật cửa sổ sau khi xả bộ đệm. Từ bên ngoài chúng trông giống hệt nhau: byte đi ra, không có gì quay lại, không có lỗi nào được ném. Chặn luồng đọc trên phân tích cú pháp, phân giải DNS hoặc I/O đĩa tạo ra cùng một dạng triệu chứng vì lý do không liên quan, nên hãy xác nhận luồng thực sự ở trong read trước khi đổ lỗi cho điều khiển luồng.
Kiểm tra hàng đợi gửi của peer và cửa sổ nó thấy lần cuối. Nếu peer giữ dữ liệu vì cửa sổ của bạn bằng không, cách sửa nằm trên đường ACK của bạn, không phải trong lần đọc TUN.
ENOBUFS là vấn đề kích thước, không phải thiếu bộ đệm
Một lần ghi trả về ENOBUFS trên giao diện TUN thường từ chối độ dài gói. Giao diện có MTU, gói lớn hơn không thể xếp hàng, và thử lại cùng gói đó thất bại giống hệt mỗi lần. Tính tất định đó là dấu hiệu. Thiếu bộ đệm thực sự sẽ hết khi áp lực giảm, còn từ chối do kích thước không quan tâm bạn chờ bao lâu.
TCP có một cơ chế dành riêng cho việc này, và đó là bước mọi người bỏ qua: kẹp MSS. Khi tổng hợp các phân đoạn trong không gian người dùng, bạn chọn kích thước tải trọng, và lấy nó từ MTU của giao diện cục bộ thay vì MTU của đường hầm sẽ phát ra các phân đoạn mà đường hầm không thể mang. Kẹp về giá trị nhỏ hơn trong hai. Kẹp chỉ bao phủ TCP, nên một datagram UDP quá lớn phải được phân mảnh hoặc bỏ bởi mã của bạn.
Ghi nhật ký độ dài của mọi gói bạn ghi trong khi gỡ lỗi. Độ dài thất bại là câu trả lời, và nó sẽ không thay đổi giữa các lần thử.
Ba phép đo phân tách các trường hợp
Bạn không cần trình gỡ lỗi cho cả hai lỗi. Ba quan sát là đủ để dẫn bạn đến phần đúng, và chúng mất ít thời gian hơn một lần build lại.
| What you see | Where to look |
|---|---|
| Reader parked, peer send queue empty, last advertised window zero | Window updates in your ACK path |
| Write fails on one length and succeeds on a smaller one | MSS clamp and tunnel MTU |
| Both directions look fine, app still sees no data | Protected socket and routing loop |
Nếu hàng thứ ba khớp, bản thân vòng lặp vẫn ổn và vấn đề nằm ở nơi đường hầm không thể thấy: một socket vận chuyển không được bảo vệ mang lưu lượng VPN của chính bạn trở lại đường hầm, thường xuất hiện như một kết nối thành công rồi không giao gì.
Sửa đường ống mà cả hai đường phụ thuộc
Không triệu chứng nào đáng theo đuổi cho đến khi giao diện được xây đúng. Các lời gọi builder quan trọng là địa chỉ, tuyến, MTU và sự đồng ý, theo thứ tự đó:
Builder()
.addAddress(10.8.0.2, 32)
.addRoute(0.0.0.0, 0)
.setMtu(1400)
.establish()Đặt MTU thành giá trị thực của đường hầm thay vì để nguyên giá trị mà ví dụ đã dùng. Mọi thứ bạn ghi sau đó bị giới hạn bởi con số đó, và sự lệch nhau giữa MTU bạn đặt và MSS bạn kẹp về là cách phổ biến nhất để đưa lỗi ghi trở lại sau một bản sửa. Sau đó bảo vệ socket mang lưu lượng đến chính máy chủ VPN. Không có protect, những gói đó bị định tuyến trở lại đường hầm của bạn và kết nối không bao giờ hoàn tất, trông như đọc bị treo nhưng thực ra là vòng lặp định tuyến.
Sự đồng ý không thể bị bỏ qua. Một dịch vụ khởi động mà prepare không trả về null sẽ không có giao diện, và establish trả về null thay vì ném lỗi, nên hãy kiểm tra bộ mô tả trước khi bắt đầu vòng lặp. Tài liệu tham chiếu VpnService là nơi xác nhận hợp đồng builder thay vì sao chép một ví dụ.
Xác minh từng hướng một
Gửi một tiếng vọng UDP qua đường hầm và quan sát độ dài gói trước khi chạm vào TCP. Sau đó mở một bắt tay và xác nhận các phân đoạn vẫn dưới MSS bạn đã tính. Đếm gói thay vì kết nối, để một lần treo hiện ra như một khoảng trống trong bộ đếm thay vì một socket bị treo. Nếu việc đọc dừng sau một loạt ghi, cửa sổ là nghi phạm; nếu việc ghi thất bại ngay lập tức, đó là độ dài.
Sau đó khởi động lại thiết bị và lặp lại một lần, vì một VpnService chỉ hoạt động đến lần thay đổi mạng đầu tiên là vấn đề định tuyến chứ không phải điều khiển luồng.