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 服務。詳情請參閱「Service 生命週期」一節。

連結服務

服務開始運作後,您可以建立連至 VPN 閘道的新本機介面。如要要求權限並將服務連線至 VPN 閘道,請按照下列順序完成步驟:

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

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

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

「Per-app 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 服務就會持續運作。這項功能稱為「全天候運作 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 連線。將應用程式加入清單時,該應用程式必須已安裝在裝置上。

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

允許的應用程式

如要將應用程式加入許可清單,請呼叫 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 服務。