VPN

Android는 개발자가 가상 사설망 (VPN) 솔루션을 만들 수 있는 API를 제공합니다. 이 가이드를 읽고 나면 Android 지원 기기용 자체 VPN 클라이언트를 개발하고 테스트하는 방법을 알 수 있습니다.

개요

VPN을 사용하면 네트워크에 물리적으로 연결되지 않은 기기가 네트워크에 안전하게 액세스할 수 있습니다.

Android에는 레거시 VPN이라고도 하는 내장 VPN 클라이언트 (PPTP 및 L2TP/IPSec)가 포함되어 있습니다. Android 4.0 (API 수준 14)에서는 앱 개발자가 자체 VPN 솔루션을 제공할 수 있도록 API를 도입했습니다. VPN 솔루션을 사람들이 기기에 설치하는 앱에 패키징합니다. 개발자는 일반적으로 다음 이유 중 하나로 인해 VPN 앱을 빌드합니다.

  • 내장 클라이언트에서 지원하지 않는 VPN 프로토콜 제공
  • 복잡한 구성 없이 사용자가 VPN 서비스에 연결할 수 있도록 지원

이 가이드의 나머지 부분에서는 VPN 앱 (상시 사용 설정앱별 VPN 포함)을 개발하는 방법을 설명하며 내장 VPN 클라이언트는 다루지 않습니다.

사용자 환경

Android는 누군가가 VPN 솔루션을 구성, 시작, 중지할 수 있도록 사용자 인터페이스 (UI)를 제공합니다. 또한 시스템 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" 인텐트 필터로 서비스를 알립니다.

다음은 앱 manifest 파일에서 서비스를 선언하는 방법을 보여주는 예입니다.

<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 서비스가 될 수 있습니다. 앱이 마지막으로 메서드를 호출한 후 사용자가 다른 앱을 VPN 서비스로 설정했을 수 있으므로 항상 VpnService.prepare()를 호출합니다. 자세한 내용은 서비스 수명 주기 섹션을 참조하세요.

서비스 연결

서비스가 실행되면 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 메서드를 호출하여 서비스를 구성합니다.

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()
시스템에서 로컬 TUN 인터페이스 주소로 할당하는 서브넷 마스크와 함께 IPv4 또는 IPv6 주소를 하나 이상 추가합니다. 앱은 일반적으로 핸드셰이크 중에 VPN 게이트웨이에서 IP 주소와 서브넷 마스크를 수신합니다.
addRoute()
시스템이 VPN 인터페이스를 통해 트래픽을 전송하도록 하려면 경로를 하나 이상 추가합니다. 목적지 주소별로 필터를 라우팅합니다. 전체 트래픽을 허용하려면 0.0.0.0/0 또는 ::/0과 같은 열린 경로를 설정합니다.

establish() 메서드는 앱이 인터페이스 버퍼에서 패킷을 읽고 쓰는 데 사용하는 ParcelFileDescriptor 인스턴스를 반환합니다. 앱이 준비되지 않았거나 누군가 권한을 취소하면 establish() 메서드는 null를 반환합니다.

서비스 수명 주기

앱은 시스템에서 선택한 VPN 및 모든 활성 연결의 상태를 추적해야 합니다. 기기 사용자가 변경사항을 계속 인식하도록 앱의 사용자 인터페이스 (UI)를 업데이트합니다.

서비스 시작

VPN 서비스는 다음과 같은 방법으로 시작될 수 있습니다.

  • 일반적으로 사용자가 연결 버튼을 탭하여 앱이 서비스를 시작합니다.
  • 연결 유지 VPN이 켜지므로 시스템이 서비스를 시작합니다.

앱은 인텐트를 startService()에 전달하여 VPN 서비스를 시작합니다. 자세한 내용은 서비스 시작을 참조하세요.

시스템은 onStartCommand()를 호출하여 백그라운드에서 서비스를 시작합니다. 그러나 버전 8.0 (API 수준 26) 이상에서는 Android가 백그라운드 앱에 제한을 둡니다. 이러한 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에는 시스템이 VPN 서비스를 시작했는지 확인하는 API가 포함되어 있지 않습니다. 그러나 앱이 시작하는 서비스 인스턴스에 플래그를 지정하면 시스템이 상시 사용 설정 VPN의 플래그가 지정되지 않은 서비스를 시작했다고 가정할 수 있습니다. 예를 들면 다음과 같습니다.

  1. VPN 서비스를 시작하기 위한 Intent 인스턴스를 생성합니다.
  2. 인텐트에 엑스트라를 추가하여 VPN 서비스에 플래그를 지정합니다.
  3. 서비스의 onStartCommand() 메서드에서 intent 인수의 extras에 있는 플래그를 찾습니다.

차단된 연결

기기 사용자(또는 IT 관리자)는 전체 트래픽이 VPN을 사용하도록 할 수 있습니다. 시스템은 VPN을 사용하지 않는 네트워크 트래픽을 차단합니다. 기기 사용자는 설정의 VPN 옵션 패널에서 VPN 없는 연결 차단 스위치를 찾을 수 있습니다.

연결 유지 선택 해제

현재 앱에서 연결 유지 VPN을 지원할 수 없다면 SERVICE_META_DATA_SUPPORTS_ALWAYS_ON 서비스 메타데이터를 false로 설정하여 (Android 8.1 이상에서) VPN을 선택 해제할 수 있습니다. 다음 앱 매니페스트 예는 메타데이터 요소를 추가하는 방법을 보여줍니다.

<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 서비스를 설정하고 연결하는 방법을 보여줍니다.