VPN

Android 為開發人員提供 API,方便他們建立虛擬私人網路 (VPN) 解決方案。閱讀本指南後,您將會瞭解如何為 Android 裝置開發及測試自己的 VPN 用戶端。

總覽

VPN 可讓未實際連上網路的裝置安全地存取網路。

Android 內建 (PPTP 和 L2TP/IPSec) VPN 用戶端,有時也稱為「舊版 VPN」。Android 4.0 (API 級別 14) 推出了 API,讓應用程式開發人員可以自行提供 VPN 解決方案。只要將 VPN 解決方案封裝至使用者安裝在裝置上的應用程式中。一般來說,開發人員建構 VPN 應用程式的原因可能是:

  • 為了提供內建用戶端不支援的 VPN 通訊協定。
  • 為了協助其他使用者不必進行繁複設定程序,就能連線至 VPN 服務。

本指南的其餘部分將說明如何開發 VPN 應用程式 (包括一律開啟個別應用程式 VPN),且不支援內建 VPN 用戶端。

使用者體驗

Android 提供使用者介面 (UI),可協助他人設定、啟動及停止 VPN 解決方案。系統 UI 也會讓裝置使用者知道有效的 VPN 連線。Android 會顯示下列 VPN 連線的 UI 元件:

  • 首次啟用 VPN 應用程式之前,系統會顯示連線要求對話方塊。對話方塊會提示裝置使用者,確認他們信任 VPN 並接受要求。
  • VPN 設定畫面 (「設定」>「網路和網際網路」>「VPN」) 會顯示使用者接受連線要求的 VPN 應用程式。我們有個按鈕可以設定系統選項 或是清除 VPN
  • 連線時,「快速設定」匣會顯示資訊面板。輕觸標籤,即可顯示含有更多資訊的對話方塊和「設定」的連結。
  • 狀態列內含 VPN (金鑰) 圖示,表示有效連線。

您的應用程式也需要提供 UI,讓裝置使用者能夠設定您的服務選項。舉例來說,您的解決方案可能需要擷取帳戶驗證設定。應用程式應顯示下列 UI:

  • 手動開始及停止連線的控制項。永久連線的 VPN 可視需要連線,但允許使用者在首次使用 VPN 時設定連線。
  • 服務啟用時無法關閉的通知。通知可能會顯示連線狀態或提供更多資訊,例如網路統計資料。只要輕觸通知,應用程式就會移至前景。在服務停用後移除通知。

VPN 服務

應用程式會將使用者的系統網路 (或工作資料夾) 連線至 VPN 閘道。每位使用者 (或工作資料夾) 都可執行不同的 VPN 應用程式。您可以建立 VPN 服務,讓系統用來啟動和停止 VPN,並追蹤連線狀態。您的 VPN 服務繼承自 VpnService

這項服務也可做為 VPN 閘道連線及其本機裝置介面的容器。您的服務執行個體會呼叫 VpnService.Builder 方法來建立新的本機介面。

圖 1.VpnService 如何將 Android 網路連結至 VPN 閘道
區塊架構圖表,顯示 VpnService 如何在系統網路中建立本機 TUN 介面。

您的應用程式會轉移以下資料,將裝置連線至 VPN 閘道:

  • 從本機介面的檔案描述元讀取傳出 IP 封包、加密這些封包,並將其傳送至 VPN 閘道。
  • 將傳入封包 (從 VPN 閘道接收及解密) 寫入本機介面的檔案描述元。

每個使用者或設定檔只能使用一項服務。啟動新服務,會自動停止現有服務。

新增服務

若要在應用程式中新增 VPN 服務,請建立沿用 VpnService 的 Android 服務。使用以下內容,在應用程式資訊清單檔案中宣告 VPN 服務

  • 請使用 BIND_VPN_SERVICE 權限保護服務,確保只有系統能夠繫結至您的服務。
  • 使用 "android.net.VpnService" 意圖篩選器通告服務,這樣系統才能找到您的服務。

以下範例說明如何在應用程式資訊清單檔案中宣告服務:

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

現在應用程式宣告了服務,系統可以視需要自動啟動及停止應用程式的 VPN 服務。例如,系統會在執行永久連線的 VPN 時控管您的服務。

準備服務

如要準備應用程式成為使用者目前的 VPN 服務,請呼叫 VpnService.prepare()。如果使用裝置的使用者尚未授予應用程式權限,這個方法會傳回活動意圖。您可以使用這個意圖啟動要求權限的系統活動。系統會顯示與其他權限對話方塊類似的對話方塊,例如相機或聯絡人存取權。如果您的應用程式已準備就緒,這個方法會傳回 null

只有一個應用程式可以是目前準備的 VPN 服務。務必呼叫 VpnService.prepare(),因為自應用程式上次呼叫該方法後,使用者可能將不同的應用程式設為 VPN 服務。詳情請參閱服務生命週期一節。

連結服務

服務執行後,您可以建立連線至 VPN 閘道的新本機介面。如要要求權限,並將服務連線至 VPN 閘道,您必須依序完成下列步驟:

  1. 視需要呼叫 VpnService.prepare() 要求權限。
  2. 呼叫 VpnService.protect(),將應用程式的通道通訊端保持在系統 VPN 外,避免產生循環連線。
  3. 呼叫 DatagramSocket.connect() 將應用程式的通道通訊端連線至 VPN 閘道。
  4. 呼叫 VpnService.Builder 方法,在裝置上設定新的本機 TUN 介面,以便處理 VPN 流量。
  5. 呼叫 VpnService.Builder.establish(),讓系統建立本機 TUN 介面,並開始透過介面轉送流量。

VPN 閘道通常會在搖動期間建議本機 TUN 介面的設定。應用程式會呼叫 VpnService.Builder 方法來設定服務,如以下範例所示:

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();

「個別應用程式 VPN」一節中的範例顯示 IPv6 設定和更多選項。您必須先新增下列 VpnService.Builder 值,才能建立新介面:

addAddress()
新增至少一個 IPv4 或 IPv6 位址,以及系統指派為本機 TUN 介面位址的子網路遮罩。您的應用程式通常會在騰動期間從 VPN 閘道收到 IP 位址和子網路遮罩。
addRoute()
如果您希望系統透過 VPN 介面傳送流量,請至少新增一個路徑。路徑會依目的地地址篩選。如要接受所有流量,請設定開放式路線,例如 0.0.0.0/0::/0

establish() 方法會傳回 ParcelFileDescriptor 執行個體,應用程式用來在介面緩衝區中讀取和寫入封包。如果應用程式未準備就緒,或有人撤銷權限,establish() 方法會傳回 null

服務生命週期

應用程式應追蹤系統所選 VPN 的狀態和任何有效連線。更新應用程式的使用者介面 (UI),讓裝置使用者知道任何變更。

啟動服務

您可以用以下方式啟動 VPN 服務:

  • 您的應用程式會啟動服務,這通常是因為使用者輕觸了連結按鈕。
  • 永久連線的 VPN 已開啟,因此系統會啟動服務。

應用程式會將意圖傳送至 startService(),藉此啟動 VPN 服務。詳情請參閱「啟動服務」。

系統會呼叫 onStartCommand(),在背景啟動您的服務。不過,Android 會在 8.0 (API 級別 26) 以上版本中,對背景應用程式設下限制。如果您支援這些 API 級別,就必須呼叫 Service.startForeground(),將服務轉換至前景。詳情請參閱「在前景執行服務」。

停止服務

裝置使用者可以透過應用程式的 UI 停止服務。請停止服務,而非只關閉連線。當使用裝置的使用者在「設定」應用程式的 VPN 畫面中執行下列操作時,系統也會停止有效的連線:

  • 中斷連線或清除 VPN 應用程式
  • 關閉有效連線的永久連線 VPN

系統會呼叫服務的 onRevoke() 方法,但這個呼叫可能不會發生在主執行緒上。當系統呼叫此方法時,替代網路介面已轉送流量,您可以安全地處理下列資源:

永久連線的 VPN

Android 可在裝置啟動時啟動 VPN 服務,並在裝置開啟時持續運作。這項功能稱為「永久連線的 VPN」,適用於 Android 7.0 (API 級別 24) 以上版本。雖然 Android 會保留服務生命週期,但這是負責 VPN 閘道連線的 VPN 服務。永久連線的 VPN 也可以封鎖未使用 VPN 的連線。

使用者體驗

在 Android 8.0 以上版本中,系統會顯示下列對話方塊,讓裝置使用者知道永久連線的 VPN:

  • 當永久連線的 VPN 連線中斷或無法連線時,使用者會看到無法關閉的通知。輕觸通知即可顯示說明對話方塊。當 VPN 重新連線,或有人關閉永久連線的 VPN 選項時,通知就會消失。
  • 永久連線 VPN 可讓使用裝置的使用者封鎖任何未使用 VPN 的網路連線。啟用此選項後,「設定」應用程式會警告使用者 VPN 連線前未連上網際網路。「設定」應用程式會提示裝置使用者繼續操作或取消。

由於系統 (而非使用者) 啟動及停止永久連線,因此您必須調整應用程式的行為和使用者介面:

  1. 停用所有會中斷連線的 UI,因為系統和「設定」應用程式會控制連線。
  2. 儲存每個應用程式啟動程序之間的任何設定,並設定具有最新設定的連線。由於系統隨選啟動應用程式,因此使用裝置的使用者不一定想設定連線。

您也可以使用代管設定來設定連線。IT 管理員可運用受管理的設定從遠端設定 VPN。

偵測一律開啟

Android 並未包含 API,可確認系統是否已啟動 VPN 服務。但是,當應用程式標記任何已啟動的服務執行個體時,您可以假設系統已針對永久連線的 VPN 啟動未標記的服務。範例如下:

  1. 建立 Intent 執行個體來啟動 VPN 服務。
  2. 插入額外項目來標記 VPN 服務。
  3. 在服務的 onStartCommand() 方法中,尋找 intent 引數額外項目中的標記。

已封鎖的連線

使用裝置 (或 IT 管理員) 可以強制所有流量使用 VPN。系統會封鎖所有未使用 VPN 的網路流量。使用裝置的使用者可以在「設定」的「VPN 選項」面板中找到「封鎖沒有 VPN 的連線」切換鈕。

選擇停用一律開啟功能

如果您的應用程式目前不支援永久連線的 VPN,您可以將 SERVICE_META_DATA_SUPPORTS_ALWAYS_ON 服務中繼資料設為 false,藉此選擇停用 (在 Android 8.1 以上版本中)。以下應用程式資訊清單範例說明如何新增中繼資料元素:

<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>

當應用程式停用永久連線的 VPN 時,系統會停用「設定」中的選項 UI 控制項。

個別應用程式 VPN

VPN 應用程式可以篩選哪些已安裝的應用程式可透過 VPN 連線傳送流量。您可以建立允許清單或不允許清單,但不能同時建立兩者。如果您不建立允許或禁止的清單,系統會透過 VPN 傳送所有網路流量。

您的 VPN 應用程式必須在連線建立前設定清單。如果您需要變更清單,請建立新的 VPN 連線。當您將應用程式加入清單時,必須安裝應用程式。

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();

允許的應用程式

如要將應用程式新增至許可清單,請呼叫 VpnService.Builder.addAllowedApplication()。如果清單包含一或多個應用程式,則只有清單中的應用程式會使用 VPN。所有其他應用程式 (不在清單中) 都會假設 VPN 並未執行而使用系統網路。如果許可清單沒有任何內容,所有應用程式都會使用 VPN。

不允許的應用程式

如要將應用程式新增至不允許的清單,請呼叫 VpnService.Builder.addDisallowedApplication()。不允許的應用程式會假設 VPN 並未執行而使用系統網路,其他所有應用程式都會使用 VPN。

略過 VPN

你的 VPN 可讓應用程式略過 VPN,並選取自己的網路。如要略過 VPN,請在建立 VPN 介面時呼叫 VpnService.Builder.allowBypass()。VPN 服務啟動後即無法變更此值。如果應用程式未將程序或通訊端繫結至特定網路,則應用程式的網路流量會透過 VPN 繼續。

繫結至特定網路的應用程式若無人封鎖不會經過 VPN 的流量,就不會產生連線。如要透過特定網路傳送流量,應用程式在連線通訊端前會呼叫 ConnectivityManager.bindProcessToNetwork()Network.bindSocket() 等方法。

程式碼範例

Android 開放原始碼計畫提供名為 ToyVPN 的範例應用程式。這個應用程式會說明如何設定並連線至 VPN 服務。