Android VPN: bộ mô tả VpnService so với một thiết bị tun root
Ứng dụng Android không thể tự tạo tun0: establish() trả về gì, Binder dừng ở đâu và JNI bắt đầu ở đâu, và khi nào một thiết bị do root tạo ra thắng.
Hai con đường có thể đặt một giao diện IP trước một ứng dụng Android, và cả hai đều kết thúc ở cùng một loại bộ mô tả tệp với những chủ sở hữu rất khác nhau. Một đường đi qua VpnService và builder của nó, đường kia qua một root shell và /dev/net/tun. Mỗi lần đọc trên bộ mô tả đó là một gói đi ra và mỗi lần ghi là một gói đi vào. Điều phân biệt hai con đường là ai giữ chính sách, cái gì sống sót qua một lần nâng cấp hệ điều hành, và bạn được chạm vào bao nhiêu phần của bảng định tuyến.
Hãy chọn con đường nền tảng trừ khi bạn có thể gọi tên đúng thứ mà nó từ chối làm. Sự từ chối đó thường là TAP, việc bắt gói ngoài tập ứng dụng của chính bạn, hoặc một quy tắc định tuyến mà framework không bao giờ phơi ra.
Trên con đường nền tảng, bạn khai báo ý định thay vì câu lệnh. Bạn cung cấp ít nhất một địa chỉ, các tuyến bạn muốn, máy chủ DNS và một danh sách ứng dụng tùy chọn, rồi dịch vụ hệ thống của Android thực hiện việc thiết lập đặc quyền dưới danh tính của chính nó. Bạn không bao giờ biết trước tên giao diện và không bao giờ phát ra ioctl tạo ra nó, và đó chính là điểm mấu chốt.
Hai cách để có một giao diện IP
Con đường root là công thức cũ hơn. Bạn mở /dev/net/tun, gọi TUNSETIFF với IFF_TUN, đưa giao diện lên bằng ip link, và thêm địa chỉ cùng tuyến bằng tay. Mỗi bước là của bạn để làm đúng, kể cả những bước mà framework lẽ ra đã xử lý âm thầm. Một tuyến thiếu tạo ra báo cáo cho người dùng giống hệt một máy chủ đã chết.
Việc gỡ lỗi khác biệt đúng như việc thiết lập. Trên con đường root, giao diện hoặc tồn tại hoặc ioctl của bạn trả về một lỗi mà bạn đọc được trực tiếp. Trên con đường nền tảng, giao diện chỉ tồn tại sau khi establish() thành công, nên một thất bại đến dưới dạng ngoại lệ hoặc bộ mô tả null thay vì một nút thiết bị để bạn chọc vào.
establish() thực sự trả về gì
Builder.establish() có ba kết cục và chúng cần ba phản ứng khác nhau. Một bộ mô tả nghĩa là giao diện đã được cấu hình và thuộc về bạn để dùng, null nghĩa là yêu cầu không đi qua, và một ngoại lệ bị ném nghĩa là dịch vụ hệ thống đã từ chối nó. Gộp null và thành công vào một boolean là cách phổ biến nhất để một ứng dụng báo cáo một đường hầm đang chạy mà không bao giờ mang gói nào. Tài liệu tham chiếu Builder nêu yêu cầu về địa chỉ, và đáng đọc trước khi bạn đổ lỗi cho máy chủ.
Hướng được cố định bởi bộ mô tả, không phải bởi giao thức của bạn. Việc đọc cho bạn các gói đi ra đã được chọn cho VPN, và việc ghi tiêm các gói đi vào trở lại giao diện. Transport của chính bạn mã hóa, xác thực và đóng khung các gói đó, vì nền tảng dừng ở ranh giới gói.
Binder là một bước nhảy, JNI là một trục khác
Một yêu cầu của builder đi vào hệ thống qua Binder, và ở đâu đó bên dưới nền tảng gọi mã native để cấu hình thiết bị. Đó là hai cơ chế khác nhau xếp chồng trong một lời gọi. Binder là giao tiếp giữa các tiến trình giữa ứng dụng của bạn và một dịch vụ hệ thống, còn JNI là cầu nối từ Java sang native bên trong một tiến trình. Đọc một cây AOSP của Android 9 làm ngăn xếp trông theo nghĩa đen, và đáng nhớ rằng tên lớp nội bộ và đường dẫn tệp thay đổi giữa các bản phát hành.
Điều không thay đổi là hợp đồng mà bạn có thể dựa vào: builder, bộ mô tả mà nó trả về, và vòng đời dịch vụ quanh nó. Không ứng dụng nào nên gọi một giao dịch dịch vụ nội bộ hay tạo lại một nút thiết bị để có giao diện. Nếu mã của bạn phụ thuộc vào một tên lớp AOSP, nó sẽ hỏng trên một bản phát hành bạn chưa kiểm thử.
TUN mang IP, TAP mang Ethernet
TUN là một giao diện IP. TAP là một giao diện Ethernet, và sự khác biệt đó quyết định việc bắc cầu có khả thi hay không. Một thiết bị TUN không có địa chỉ MAC và không có tiêu đề Ethernet để chuyển tiếp, nên một máy khách TUN không thể tham gia một cầu lớp 2, dù các tuyến được viết thế nào.
VpnService cho bạn biến thể IP, đó là thứ một VPN mức ứng dụng cần và có thể hỗ trợ mà không cần một ngăn xếp Ethernet thô. Nếu thiết kế của bạn yêu cầu bắc cầu, con đường nền tảng sẽ không đưa bạn tới đó, và con đường root chỉ làm được nếu kernel và quyền của thiết bị hợp tác.
Nơi con đường root vẫn thắng
Ba công việc đẩy người ta ra khỏi con đường nền tảng. Bắc cầu lớp 2 cần TAP. Bắt lưu lượng từ các ứng dụng ngoài phạm vi VPN của bạn cần khả năng nhìn mà framework không cấp. Định tuyến theo chính sách mà builder không bao giờ phơi ra, như một bảng riêng cho một đích, cần các quy tắc bạn tự cài.
Chi phí đến sau: một bootloader đã mở khóa hoặc một ảnh đã root, một chính sách SELinux dịch chuyển dưới chân bạn, và một thiết bị thôi hành xử đúng sau một bản OTA. Một công thức đã root cũng không thể phát hành qua kênh phân phối ứng dụng thông thường, nên hầu hết các nhóm giữ nó như một con đường chẩn đoán nội bộ thay vì một sản phẩm.
Chọn giữa hai cách
Hãy bắt đầu từ giao diện bạn cần, không phải từ mã bạn có. Nếu câu trả lời là một thiết bị TUN với các tuyến giới hạn theo ứng dụng, con đường nền tảng ngắn hơn và sống sót qua các lần nâng cấp. Nếu câu trả lời liên quan đến khung Ethernet, việc bắt các ứng dụng khác, hoặc các quy tắc định tuyến mà builder từ chối, bạn đang ở lãnh thổ root và nên dự trù nó như một chi phí vận hành.
Một phép kiểm tra giải quyết hầu hết các tranh luận: trước tiên hãy xây con đường nền tảng, ghi lại kết quả của establish() tách khỏi trạng thái phiên từ xa, và xác nhận một gói theo mỗi hướng. Chỉ sau đó mới quyết định liệu khả năng còn thiếu có biện minh cho việc rời bỏ con đường được hỗ trợ hay không.