VPN

Android cung cấp các API cho nhà phát triển để tạo giải pháp mạng riêng ảo (VPN). Sau khi đọc hướng dẫn này, bạn sẽ biết cách phát triển và kiểm thử ứng dụng VPN của riêng mình cho các thiết bị chạy Android.

Tổng quan

VPN cho phép các thiết bị không kết nối mạng một cách an toàn để truy cập vào mạng.

Android có một ứng dụng VPN (PPTP và L2TP/IPSec) tích hợp sẵn, đôi khi được gọi là VPN cũ. Android 4.0 (API cấp 14) đã ra mắt các API để nhà phát triển ứng dụng có thể cung cấp giải pháp VPN của riêng họ. Bạn đóng gói giải pháp VPN vào một ứng dụng mà mọi người cài đặt trên thiết bị. Các nhà phát triển thường xây dựng ứng dụng VPN vì một trong những lý do sau:

  • Để cung cấp các giao thức VPN mà ứng dụng tích hợp sẵn không hỗ trợ.
  • Giúp mọi người kết nối với dịch vụ VPN mà không cần cấu hình phức tạp.

Phần còn lại của hướng dẫn này giải thích cách phát triển ứng dụng VPN (bao gồm cả VPN luôn bật và VPN mỗi ứng dụng) và không đề cập đến ứng dụng VPN tích hợp sẵn.

Trải nghiệm người dùng

Android cung cấp giao diện người dùng (UI) để giúp mọi người định cấu hình, bắt đầu và dừng giải pháp VPN của bạn. Giao diện người dùng hệ thống cũng giúp người sử dụng thiết bị nhận biết được kết nối VPN đang hoạt động. Android hiển thị các thành phần giao diện người dùng sau cho kết nối VPN:

  • Trước khi một ứng dụng VPN có thể hoạt động lần đầu tiên, hệ thống sẽ hiển thị hộp thoại yêu cầu kết nối. Hộp thoại này sẽ nhắc người dùng thiết bị xác nhận rằng họ tin tưởng VPN và chấp nhận yêu cầu.
  • Màn hình cài đặt VPN (Cài đặt > Mạng và Internet > VPN) cho thấy các ứng dụng VPN mà một người đã chấp nhận yêu cầu kết nối. Có một nút để định cấu hình các tuỳ chọn hệ thống hoặc quên VPN.
  • Khay Cài đặt nhanh sẽ hiển thị một bảng thông tin khi có kết nối. Khi nhấn vào nhãn, bạn sẽ thấy một hộp thoại có thêm thông tin và đường liên kết đến phần Cài đặt.
  • Thanh trạng thái có một biểu tượng VPN (khoá) để cho biết có một kết nối đang hoạt động.

Ứng dụng của bạn cũng cần cung cấp giao diện người dùng để người sử dụng thiết bị có thể định cấu hình các tuỳ chọn của dịch vụ. Ví dụ: có thể giải pháp của bạn cần thu thập chế độ cài đặt xác thực tài khoản. Ứng dụng phải hiển thị giao diện người dùng sau đây:

  • Các chế độ kiểm soát dùng để bắt đầu và dừng kết nối theo cách thủ công. VPN luôn bật có thể kết nối khi cần, nhưng cho phép mọi người định cấu hình kết nối trong lần đầu tiên họ dùng VPN.
  • Một thông báo không đóng được khi dịch vụ đang hoạt động. Thông báo có thể cho thấy trạng thái kết nối hoặc cung cấp thêm thông tin – chẳng hạn như số liệu thống kê về mạng. Nhấn vào thông báo đó sẽ đưa ứng dụng của bạn lên nền trước. Xoá thông báo sau khi dịch vụ không hoạt động.

Dịch vụ VPN

Ứng dụng của bạn kết nối mạng hệ thống của người dùng (hoặc hồ sơ công việc) với cổng VPN. Mỗi người dùng (hoặc hồ sơ công việc) có thể chạy một ứng dụng VPN khác nhau. Bạn tạo một dịch vụ VPN mà hệ thống dùng để khởi động và dừng VPN của bạn, đồng thời theo dõi trạng thái kết nối. Dịch vụ VPN của bạn sẽ kế thừa từ VpnService.

Dịch vụ này cũng đóng vai trò là vùng chứa cho các kết nối cổng VPN và giao diện thiết bị cục bộ của bạn. Thực thể dịch vụ của bạn gọi các phương thức VpnService.Builder để thiết lập giao diện cục bộ mới.

Hình 1. Cách VpnService kết nối mạng Android với cổng VPN
Sơ đồ cấu trúc khối cho thấy cách VpnService tạo giao diện TUN cục bộ trong kết nối mạng hệ thống.

Ứng dụng của bạn chuyển dữ liệu sau đây để kết nối thiết bị với cổng VPN:

  • Đọc các gói IP gửi đi từ chỉ số mô tả tệp của giao diện cục bộ, mã hoá các gói đó rồi gửi đến cổng VPN.
  • Ghi các gói đến (đã nhận và giải mã từ cổng VPN) vào chỉ số mô tả tệp của giao diện cục bộ.

Mỗi người dùng hoặc hồ sơ chỉ có một dịch vụ đang hoạt động. Khi bắt đầu một dịch vụ mới, hệ thống sẽ tự động dừng một dịch vụ hiện có.

Thêm dịch vụ

Để thêm dịch vụ VPN vào ứng dụng, hãy tạo một dịch vụ Android kế thừa từ VpnService. Khai báo dịch vụ VPN trong tệp kê khai ứng dụng với các bổ sung sau:

  • Bảo vệ dịch vụ bằng quyền BIND_VPN_SERVICE để chỉ hệ thống mới có thể liên kết với dịch vụ của bạn.
  • Quảng cáo dịch vụ bằng bộ lọc ý định "android.net.VpnService" để hệ thống có thể tìm thấy dịch vụ của bạn.

Ví dụ này cho thấy cách bạn có thể khai báo dịch vụ trong tệp kê khai ứng dụng:

<service android:name=".MyVpnService"
         android:permission="android.permission.BIND_VPN_SERVICE">
     <intent-filter>
         <action android:name="android.net.VpnService"/>
     </intent-filter>
</service>

Giờ đây, ứng dụng của bạn khai báo dịch vụ, hệ thống có thể tự động khởi động và dừng dịch vụ VPN của ứng dụng khi cần. Ví dụ: hệ thống sẽ kiểm soát dịch vụ của bạn khi chạy VPN luôn bật.

Chuẩn bị dịch vụ

Để chuẩn bị ứng dụng trở thành dịch vụ VPN hiện tại của người dùng, hãy gọi VpnService.prepare(). Nếu người sử dụng thiết bị chưa cấp quyền cho ứng dụng của bạn, thì phương thức này sẽ trả về một ý định hoạt động. Bạn dùng ý định này để bắt đầu một hoạt động trên hệ thống có yêu cầu cấp quyền. Hệ thống sẽ hiện một hộp thoại tương tự như hộp thoại cấp quyền khác, chẳng hạn như quyền truy cập vào máy ảnh hoặc danh bạ. Nếu ứng dụng của bạn đã được chuẩn bị, phương thức này sẽ trả về null.

Chỉ một ứng dụng có thể là dịch vụ VPN hiện được chuẩn bị. Luôn gọi VpnService.prepare() vì một người có thể đã đặt một ứng dụng khác làm dịch vụ VPN kể từ lần gần đây nhất ứng dụng của bạn gọi phương thức này. Để tìm hiểu thêm, hãy xem phần Vòng đời dịch vụ.

Kết nối dịch vụ

Sau khi dịch vụ chạy, bạn có thể thiết lập một giao diện cục bộ mới được kết nối với cổng VPN. Để yêu cầu quyền và kết nối với dịch vụ của bạn với cổng VPN, bạn cần hoàn tất các bước theo thứ tự sau:

  1. Gọi VpnService.prepare() để yêu cầu cấp quyền (khi cần).
  2. Gọi VpnService.protect() để giữ cổng đường hầm của ứng dụng bên ngoài VPN hệ thống và tránh kết nối vòng tròn.
  3. Gọi DatagramSocket.connect() để kết nối ổ cắm đường hầm của ứng dụng với cổng VPN.
  4. Gọi các phương thức VpnService.Builder để định cấu hình giao diện TUN cục bộ mới trên thiết bị cho lưu lượng truy cập VPN.
  5. Gọi VpnService.Builder.establish() để hệ thống thiết lập giao diện TUN cục bộ và bắt đầu định tuyến lưu lượng truy cập thông qua giao diện này.

Cổng VPN thường đề xuất các chế độ cài đặt cho giao diện TUN cục bộ trong quá trình bắt tay. Ứng dụng của bạn gọi các phương thức VpnService.Builder để định cấu hình một dịch vụ như trong mẫu sau:

Kotlin

// Configure a new interface from our VpnService instance. This must be done
// from inside a VpnService.
val builder = Builder()

// Create a local TUN interface using predetermined addresses. In your app,
// you typically use values returned from the VPN gateway during handshaking.
val localTunnel = builder
        .addAddress("192.168.2.2", 24)
        .addRoute("0.0.0.0", 0)
        .addDnsServer("192.168.1.1")
        .establish()

Java

// Configure a new interface from our VpnService instance. This must be done
// from inside a VpnService.
VpnService.Builder builder = new VpnService.Builder();

// Create a local TUN interface using predetermined addresses. In your app,
// you typically use values returned from the VPN gateway during handshaking.
ParcelFileDescriptor localTunnel = builder
    .addAddress("192.168.2.2", 24)
    .addRoute("0.0.0.0", 0)
    .addDnsServer("192.168.1.1")
    .establish();

Ví dụ trong phần VPN cho mỗi ứng dụng cho thấy một cấu hình IPv6 kèm theo các lựa chọn khác. Bạn cần thêm các giá trị VpnService.Builder sau đây trước khi có thể thiết lập giao diện mới:

addAddress()
Thêm ít nhất một địa chỉ IPv4 hoặc IPv6 cùng với mặt nạ mạng con mà hệ thống chỉ định làm địa chỉ giao diện TUN cục bộ. Ứng dụng của bạn thường nhận được địa chỉ IP và mặt nạ mạng con từ cổng VPN trong quá trình bắt tay.
addRoute()
Thêm ít nhất một tuyến nếu bạn muốn hệ thống gửi lưu lượng truy cập thông qua giao diện VPN. Các tuyến đường được lọc theo địa chỉ đích. Để chấp nhận tất cả lưu lượng truy cập, hãy đặt một tuyến mở như 0.0.0.0/0 hoặc ::/0.

Phương thức establish() sẽ trả về một thực thể ParcelFileDescriptor mà ứng dụng của bạn dùng để đọc và ghi các gói vào và đi từ vùng đệm của giao diện. Phương thức establish() sẽ trả về null nếu ứng dụng của bạn chưa được chuẩn bị hoặc ai đó thu hồi quyền.

Vòng đời dịch vụ

Ứng dụng của bạn phải theo dõi trạng thái của VPN đã chọn của hệ thống và mọi kết nối đang hoạt động. Cập nhật giao diện người dùng (UI) của ứng dụng để giúp người sử dụng thiết bị biết được mọi thay đổi.

Bắt đầu dịch vụ

Bạn có thể bắt đầu dịch vụ VPN theo những cách sau:

  • Ứng dụng của bạn sẽ khởi động dịch vụ – thường là do một người đã nhấn vào nút kết nối.
  • Hệ thống sẽ khởi động dịch vụ vì VPN luôn bật đang bật.

Ứng dụng khởi động dịch vụ VPN bằng cách truyền một ý định đến startService(). Để tìm hiểu thêm, hãy đọc bài viết Bắt đầu sử dụng một dịch vụ.

Hệ thống sẽ bắt đầu dịch vụ của bạn ở chế độ nền bằng cách gọi onStartCommand(). Tuy nhiên, Android đặt ra các hạn chế đối với ứng dụng chạy ở chế độ nền trong phiên bản 8.0 (API cấp 26) trở lên. Nếu hỗ trợ các cấp độ API này, bạn cần chuyển đổi dịch vụ lên nền trước bằng cách gọi Service.startForeground(). Để tìm hiểu thêm, hãy đọc phần Chạy một dịch vụ ở nền trước.

Dừng một dịch vụ

Người đang sử dụng thiết bị có thể dừng dịch vụ của bạn bằng cách sử dụng giao diện người dùng trên ứng dụng của bạn. Dừng dịch vụ thay vì chỉ đóng kết nối. Hệ thống cũng dừng một kết nối đang hoạt động khi người dùng thiết bị thực hiện thao tác sau trên màn hình VPN của ứng dụng Cài đặt:

  • ngắt kết nối hoặc xoá ứng dụng VPN
  • tắt VPN luôn bật đối với kết nối đang hoạt động

Hệ thống gọi phương thức onRevoke() của dịch vụ nhưng lệnh gọi này có thể không xảy ra trên luồng chính. Khi hệ thống gọi phương thức này, giao diện mạng thay thế đã định tuyến lưu lượng truy cập. Bạn có thể vứt bỏ các tài nguyên sau một cách an toàn:

  • Đóng cổng đường hầm được bảo vệ với cổng VPN bằng cách gọi DatagramSocket.close().
  • Đóng chỉ số mô tả tệp của lô đất (bạn không cần phải loại bỏ nó) bằng cách gọi ParcelFileDescriptor.close().

VPN luôn bật

Android có thể bắt đầu một dịch vụ VPN khi thiết bị khởi động và tiếp tục chạy trong khi thiết bị đang bật. Tính năng này được gọi là VPN luôn bật và có trong Android 7.0 (API cấp 24) trở lên. Mặc dù Android duy trì vòng đời của dịch vụ, nhưng dịch vụ VPN lại chịu trách nhiệm về kết nối cổng VPN. VPN luôn bật cũng có thể chặn các kết nối không dùng VPN.

Trải nghiệm người dùng

Trên Android 8.0 trở lên, hệ thống sẽ hiển thị các hộp thoại sau để người dùng thiết bị biết về VPN luôn bật:

  • Khi kết nối VPN luôn bật ngắt kết nối hoặc không thể kết nối, mọi người sẽ thấy một thông báo không thể loại bỏ. Khi nhấn vào thông báo, một hộp thoại sẽ giải thích thêm. Thông báo sẽ biến mất khi VPN kết nối lại hoặc ai đó tắt tuỳ chọn VPN luôn bật.
  • VPN luôn bật cho phép người dùng sử dụng thiết bị chặn mọi kết nối mạng không dùng VPN. Khi bật tuỳ chọn này, ứng dụng Cài đặt sẽ cảnh báo mọi người rằng họ không có kết nối Internet trước khi VPN kết nối. Ứng dụng Cài đặt sẽ nhắc người sử dụng thiết bị tiếp tục hoặc huỷ.

Vì hệ thống (chứ không phải một người) khởi động và dừng kết nối luôn bật, nên bạn cần điều chỉnh hành vi và giao diện người dùng của ứng dụng:

  1. Tắt mọi giao diện người dùng ngắt kết nối vì hệ thống và ứng dụng Cài đặt kiểm soát kết nối.
  2. Lưu cấu hình bất kỳ giữa mỗi lần khởi động ứng dụng và định cấu hình kết nối với chế độ cài đặt mới nhất. Vì hệ thống khởi động ứng dụng của bạn theo yêu cầu nên người sử dụng thiết bị có thể không phải lúc nào cũng muốn định cấu hình kết nối.

Bạn cũng có thể sử dụng cấu hình được quản lý để định cấu hình kết nối. Cấu hình được quản lý giúp quản trị viên CNTT định cấu hình VPN của bạn từ xa.

Phát hiện trạng thái luôn bật

Android không có các API để xác nhận xem hệ thống đã khởi động dịch vụ VPN hay chưa. Tuy nhiên, khi ứng dụng gắn cờ bất kỳ thực thể dịch vụ nào khởi động, bạn có thể giả định rằng hệ thống đã khởi động các dịch vụ không bị gắn cờ cho VPN luôn bật. Sau đây là ví dụ:

  1. Tạo một thực thể Intent để bắt đầu dịch vụ VPN.
  2. Gắn cờ dịch vụ VPN bằng cách đặt thêm một giá trị vào ý định.
  3. Trong phương thức onStartCommand() của dịch vụ, hãy tìm cờ trong phần bổ sung của đối số intent.

Kết nối bị chặn

Người dùng thiết bị (hoặc quản trị viên CNTT) có thể buộc tất cả lưu lượng truy cập sử dụng VPN. Hệ thống sẽ chặn mọi lưu lượng truy cập mạng không sử dụng VPN. Người dùng thiết bị có thể thấy nút chuyển Chặn kết nối mà không cần VPN trong bảng tuỳ chọn VPN ở phần Cài đặt.

Chọn không sử dụng chế độ luôn bật

Nếu ứng dụng của bạn hiện không thể hỗ trợ VPN luôn bật, thì bạn có thể chọn không sử dụng (trong Android 8.1 trở lên) bằng cách đặt siêu dữ liệu dịch vụ SERVICE_META_DATA_SUPPORTS_ALWAYS_ON thành false. Ví dụ sau đây về tệp kê khai ứng dụng cho biết cách thêm phần tử siêu dữ liệu:

<service android:name=".MyVpnService"
         android:permission="android.permission.BIND_VPN_SERVICE">
     <intent-filter>
         <action android:name="android.net.VpnService"/>
     </intent-filter>
     <meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
             android:value=false/>
</service>

Khi ứng dụng của bạn chọn không sử dụng VPN luôn bật, hệ thống sẽ tắt các tuỳ chọn điều khiển trên giao diện người dùng trong phần Cài đặt.

VPN cho mỗi ứng dụng

Các ứng dụng VPN có thể lọc xem những ứng dụng đã cài đặt nào được phép gửi lưu lượng truy cập thông qua kết nối VPN. Bạn có thể tạo một danh sách được phép hoặc một danh sách không được phép, nhưng không thể tạo cả hai. Nếu bạn không tạo danh sách được cho phép hoặc không được phép, hệ thống sẽ gửi tất cả lưu lượng truy cập mạng thông qua VPN.

Ứng dụng VPN của bạn phải thiết lập các danh sách trước khi thiết lập kết nối. Nếu bạn cần thay đổi danh sách, hãy thiết lập kết nối VPN mới. Ứng dụng phải được cài đặt trên thiết bị khi bạn thêm ứng dụng đó vào danh sách.

Kotlin

// The apps that will have access to the VPN.
val appPackages = arrayOf(
        "com.android.chrome",
        "com.google.android.youtube",
        "com.example.a.missing.app")

// Loop through the app packages in the array and confirm that the app is
// installed before adding the app to the allowed list.
val builder = Builder()
for (appPackage in appPackages) {
    try {
        packageManager.getPackageInfo(appPackage, 0)
        builder.addAllowedApplication(appPackage)
    } catch (e: PackageManager.NameNotFoundException) {
        // The app isn't installed.
    }
}

// Complete the VPN interface config.
val localTunnel = builder
        .addAddress("2001:db8::1", 64)
        .addRoute("::", 0)
        .establish()

Java

// The apps that will have access to the VPN.
String[] appPackages = {
    "com.android.chrome",
    "com.google.android.youtube",
    "com.example.a.missing.app"};

// Loop through the app packages in the array and confirm that the app is
// installed before adding the app to the allowed list.
VpnService.Builder builder = new VpnService.Builder();
PackageManager packageManager = getPackageManager();
for (String appPackage: appPackages) {
  try {
    packageManager.getPackageInfo(appPackage, 0);
    builder.addAllowedApplication(appPackage);
  } catch (PackageManager.NameNotFoundException e) {
    // The app isn't installed.
  }
}

// Complete the VPN interface config.
ParcelFileDescriptor localTunnel = builder
    .addAddress("2001:db8::1", 64)
    .addRoute("::", 0)
    .establish();

Ứng dụng được phép

Để thêm một ứng dụng vào danh sách cho phép, hãy gọi VpnService.Builder.addAllowedApplication(). Nếu danh sách bao gồm một hoặc nhiều ứng dụng, thì chỉ các ứng dụng trong danh sách mới dùng VPN. Tất cả các ứng dụng khác (không có trong danh sách) đều sử dụng mạng hệ thống như thể VPN không chạy. Khi danh sách được phép trống, tất cả ứng dụng đều dùng VPN.

Ứng dụng không được phép

Để thêm một ứng dụng vào danh sách không được phép, hãy gọi VpnService.Builder.addDisallowedApplication(). Các ứng dụng không được phép sử dụng kết nối mạng hệ thống như thể VPN không chạy – mọi ứng dụng khác đều dùng VPN.

Bỏ qua VPN

VPN có thể cho phép các ứng dụng bỏ qua VPN và chọn mạng riêng của ứng dụng đó. Để bỏ qua VPN, hãy gọi VpnService.Builder.allowBypass() khi thiết lập giao diện VPN. Bạn không thể thay đổi giá trị này sau khi khởi động dịch vụ VPN. Nếu một ứng dụng không liên kết quy trình của ứng dụng hoặc ổ cắm với một mạng cụ thể, thì lưu lượng truy cập mạng của ứng dụng đó sẽ tiếp tục thông qua VPN.

Các ứng dụng liên kết với một mạng cụ thể sẽ không có kết nối khi có người chặn lưu lượng truy cập không đi qua VPN. Để gửi lưu lượng truy cập thông qua một mạng cụ thể, hãy gọi các phương thức của ứng dụng, chẳng hạn như ConnectivityManager.bindProcessToNetwork() hoặc Network.bindSocket() trước khi kết nối ổ cắm.

Mã mẫu

Dự án nguồn mở Android có một ứng dụng mẫu có tên là ToyVPN. Ứng dụng này cho biết cách thiết lập và kết nối một dịch vụ VPN.