이번 시간에는 지난 시간(wireguard windows version)에 이어 Wireguard Android 버젼에 관한 이야기를 이어가 보고자 한다. 😎
목차
1. WireGuard Android Project 소개
1. WireGuard Android Project 소개
2. WireGuard-Go 핵심 코드 분석
3. WireGuard Android 코드 분석#1 - Frontend UI
4. WireGuard Android 코드 분석#2 - Backend VpnService
5. WireGuard Auto Connect 기능과 연동하기
6. References
1. WireGuard Android Project 소개
Android는 2008년 9월에 1.0 버젼이 출시된 이후로, (2025년 현재 기준으로) 17년 정도의 시간이 흘러, 어느새 version 16(Baklava)까지 진화하였다. 한편, Android용 wireguard는 2018년에 alpha version이 나온 이후로, 7년 정도의 시간이 흘렀다. Time flies like an arrow~ 🚀
1.1 wireguard-android 프로젝트 개요
Android는 linux kernel을 채용하고 있기 때문에 wireguard android app을 만들 경우, wireguard linux kernel 버젼을 사용하는 것이 답이겠으나, (android version에 따라) 제조사에서 이를 지원하지 않는 문제가 있기 때문에, 실제로는 사용자 영역에서 동작하는 wireguard-go 버젼이 기본으로 활용되고 있다. 참고로 wireguard android app에는 wireguard linux kernel을 사용하는 기능도 이미 지원하고 있다.
먼저, Wireguard android app 코드는 아래 site에서 확인 가능하다.
한편, wireguard kernel 버젼을 android ROM에 통합하고자 할 경우에는 아래 site를 참고할 수 있다.
📌 Google에서는 Android 12 버젼 linux kernel 부터 wireguard를 포함시켰으나, 제조사별로 실제 linux kernel version에 차이가 있는 만큼, 정확한 지원여부는 제조사에서 제공하는 스펙 정보를 참조할 필요가 있다.
<Wireguard-android app의 특징>
- wireguard android app의 핵심은 사용자 영역에서 동작하는 wireguard-go 코드에 있다.
- wireguard app은 Kotlin UI(9 activities, 3 boradcast receivers, 1 content provider, 1 service 등으로 구성)와 Java로 구현한 VpnService로 구성되어 있다. 또한, VpnService는 JNI를 통해 wireguard-go 코드(libwg-go.so)와 연동된다. 즉, Java -> JNI -> C -> Go code 흐름을 따른다.
- wireguard-go code가 탑재된 VpnService는 library(andrid archive file) 형태로 별도로 배포/관리되고 있다. 이는 (개발자 입장에서 보면) wireguard app을 만들기 위해 UI 부분만 신경쓰면 된다는 얘기이기도 하다.
- VpnService용 library는 getVersion(), setState(), getState(), getStatistics() 등의 method를 제공하므로, UI에서는 이를 잘 활용하기만 하면 된다.
- wireguard-go code는 UI에서 사용하기 쉽도록 TurnOn, TurnOff, GetConfig 정도의 아주 간단한 API 만을 제공하도록 설계되어 있다. 따라서, 이렇게 구현된 wireguard-go code는 iOS, macOS app을 만드는 데도 그대로 활용된다.
___________________________________________________________
1.2 wireguard-android app build 하기
자, 그럼 지금부터는 Wireguard android app을 build하는 절차를 살펴 보기로 하자. 겉으로 보기에는 단순할 것 처럼 보이지만, Android의 특성 상 몇가지 번거로운 절차가 뒤따른다.
<사전 준비 사항>
-> 아래 내용은 Ubuntu 22.04 LTS 버젼을 기준으로 한다.
먼저, JDK 17 버젼을 설치하도록 한다.
$ sudo apt install openjdk-17-jdk
-> JDK 17 version을 설치하도록 한다.
$ java --version
openjdk 17.0.16 2025-07-15
OpenJDK Runtime Environment (build 17.0.16+8-Ubuntu-0ubuntu122.04.1)
OpenJDK 64-Bit Server VM (build 17.0.16+8-Ubuntu-0ubuntu122.04.1, mixed mode, sharing)
$ vi ~/.bashrc
-> .bashrc file에 아래 내용을 추가하도록 하자.
...
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
export PATH=$JAVA_HOME/bin/:$PATH
export CLASS_PATH=$JAVA_HOME/lib:$CLASS_PATH
~
$ source ~/.bashrc
$ echo $JAVA_HOME
/usr/lib/jvm/java-17-openjdk-amd64
다음으로, android SDK & NDK 등을 위해 android studio를 설치하도록 한다.
[그림 1.3] Android Studio 설치(1)
$ tar xvzf android-studio-2025.1.2.12-linux.tar.gz
$ cd android-studio/bin
$ ./studio.sh
Android SDK path를 지정하도록 하자.
$ export ANDROID_HOME=/home/chyi/Android/Sdk
$ export PATH=$ANDROID_HOME/platform-tools:$PATH
자, 기본 환경이 준비되었으니, wireguard code를 내려 받아 build를 해 보도록 하자.
<Wireguard Android build 하기>
$ git clone --recurse-submodules https://git.zx2c4.com/wireguard-android
$ cd wireguard-android
이 상태에서 android studio를 실행 한 후, wireguard-android code를 불러 오도록 하자.
$ ./studio
그래들(gradle)을 이용하여 build를 시도해 보도록 하자.
$ ./gradlew assembleRelease
Configuration on demand is an incubating feature.
[Incubating] Problems report is available at: file:///mnt/hdd/workspace/wireguard/wireguard-android/build/reports/problems/problems-report.html
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
For more on this, please refer to https://docs.gradle.org/8.14/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
BUILD SUCCESSFUL in 1m 25s
96 actionable tasks: 96 executed
-> OK, 정상적으로 build가 되었다.
📌 (2025년 8월 기준) 3개월 전에 wireguard android build 관련하여 수정 작업이 진행된 듯 보인다. 2년전 쯤에 wireguard android app을 build하기 위해 gradle 설정을 몇차례에 걸쳐 수정했던 아찔한 기억이 있는데, 한방에 build가 되니 다행스럽다. 😍
Build 결과물이 어디에 있는지 확인해 보면 다음과 같다.
$ find . -name "*.apk" -print
./ui/build/outputs/apk/release/ui-release-unsigned.apk
$ cd ui/build/outputs/apk/release
$ ls -la
합계 9840
drwxrwxr-x 3 chyi chyi 4096 8월 27 12:12 .
drwxrwxr-x 3 chyi chyi 4096 8월 27 12:12 ..
drwxrwxr-x 4 chyi chyi 4096 8월 27 12:12 baselineProfiles
-rw-rw-r-- 1 chyi chyi 744 8월 27 12:12 output-metadata.json
-rw-rw-r-- 1 chyi chyi 10058632 8월 27 12:12 ui-release-unsigned.apk
1.3 wireguard-android app 설치해 보기
자, 그렇다면 정상적으로 build가 되었는지, target device에서 실제로 돌려 보기로 하자. 💢
<Wireguard Android app 설치하기>
먼저, 자신의 android phone을 개발자 모드(USB 디버깅 허용 선택)로 변경한 후, adb로 장치가 인식되는지 확인한다.
📌 개발자 모드 변경 방법은 android phone별로 차이가 있는 관계로 여기에서는 별도로 설명하지는 않는다.
<Ubuntu에서 android device를 USB로 인식시키기 위한 설정>
$ sudo curl --create-dirs -L -o /etc/udev/rules.d/51-android.rules https://raw.githubusercontent.com/snowdream/51-android/master/51-android.rules
$ sudo chmod a+r /etc/udev/rules.d/51-android.rules
$ sudo service udev restart
이후, USB cable을 연결한 후, USB 허용 popup이 뜨면 허용을 선택해 준다.
$ adb devices
-> 아래와 같이 장치가 하나 보이면 정상적으로 인식된 것이다.
[그림 1.7] adb로 smart phone 장치 인식하기
📌 장치 인식이 잘 안될 경우, USB cable을 재 연결해 보는 것도 도움이 된다.
이 상태에서 adb install 명령으로 앞서 build하여 얻은 apk 파일을 설치해 본다.
Performing Streamed Install
adb: failed to install /mnt/hdd/workspace/wireguard/wireguard-android/ui/build/outputs/apk/release/ui-release-unsigned.apk: Failure [INSTALL_PARSE_F
AILED_NO_CERTIFICATES: Failed collecting certificates for /data/app/vmdl957896466.tmp/base.apk: Failed to collect certificates from /data/app/vmdl95
7896466.tmp/base.apk: Attempt to get length of null array]
(release 버젼이므로) 인증서가 없다는 이유로 설치에 실패한다.
adb: failed to install /mnt/hdd/workspace/wireguard/wireguard-android/ui/build/outputs/apk/release/ui-release-unsigned.apk: Failure [INSTALL_PARSE_F
AILED_NO_CERTIFICATES: Failed collecting certificates for /data/app/vmdl957896466.tmp/base.apk: Failed to collect certificates from /data/app/vmdl95
7896466.tmp/base.apk: Attempt to get length of null array]
(release 버젼이므로) 인증서가 없다는 이유로 설치에 실패한다.
그렇다면, debug mode로 다시 build하여 설치를 이어가 보도록 하자. 😋
$ ./gradlew assembleDebug
Android phone에 제대로 설치가 되었는지, wireguard app icon을 선택해 보면 다음과 같은 page가 뜬다.
위의 그림을 (좀 더 이해하기 쉽도록) 다른 각도에서 표현해 보면 다음과 같다.
vpnThread 자리에 wireguard-go code가 위치한 상태에서, 일반 android app으로 부터 패킷이 송/수신되는 경우에 대한 내용을 상세히 그려보면 다음과 같다. 동작 관련해서는 이미 2장에서 상세히 설명한 바 있다.
Configuration on demand is an incubating feature.
BUILD SUCCESSFUL in 22s
75 actionable tasks: 64 executed, 10 from cache, 1 up-to-date
$ find . -name "*.apk" -print
./ui/build/outputs/apk/release/ui-release-unsigned.apk
./ui/build/outputs/apk/debug/ui-debug.apk
$ ./adb install <YOUR_PATH/ui-release-unsigned.apk
-> OK, 이번에는 정상적으로 설치가 된다.
[그림 1.8] adb install로 apk 설치하기
-> WGTunnel project에 관하여
____________________________
Wireguard android app 중에는 third party에서 만든, WGTunnel이라는 project도 있다. 다양한 기능을 제공하고 있어, original wireguard-android project 보다 (github star 갯수로만 보면) 인기가 더 높은 듯도 보인다. 그러나 핵심 기능(wireguard handshaking code)은 여전히 원저자(Jason A. Donenfeld)의 코드를 그대로 활용하고 있음을 알 수 있다.
참고로, build 결과물인 wgtunnel-standalone-v3.9.5.apk 파일(zip file)을 풀어보면 다음과 같다.
-rw-rw-r-- 1 chyi chyi 26304 1월 1 1981 AndroidManifest.xml
-rw-rw-r-- 1 chyi chyi 1733 1월 1 1981 DebugProbesKt.bin
drwxrwxr-x 6 chyi chyi 12288 8월 28 15:14 META-INF
drwxrwxr-x 3 chyi chyi 4096 8월 28 15:14 assets
-rw-r--r-- 1 chyi chyi 42301580 1월 1 1981 classes.dex
-rw-r--r-- 1 chyi chyi 36216 1월 1 1981 classes10.dex
-rw-r--r-- 1 chyi chyi 149776 1월 1 1981 classes11.dex
-rw-r--r-- 1 chyi chyi 258900 1월 1 1981 classes12.dex
-rw-r--r-- 1 chyi chyi 291436 1월 1 1981 classes13.dex
-rw-r--r-- 1 chyi chyi 262564 1월 1 1981 classes14.dex
-rw-r--r-- 1 chyi chyi 194592 1월 1 1981 classes15.dex
-rw-r--r-- 1 chyi chyi 453176 1월 1 1981 classes16.dex
-rw-r--r-- 1 chyi chyi 428004 1월 1 1981 classes17.dex
-rw-r--r-- 1 chyi chyi 84624 1월 1 1981 classes18.dex
-rw-r--r-- 1 chyi chyi 10240 1월 1 1981 classes19.dex
-rw-r--r-- 1 chyi chyi 12828 1월 1 1981 classes2.dex
-rw-r--r-- 1 chyi chyi 273012 1월 1 1981 classes20.dex
-rw-r--r-- 1 chyi chyi 174328 1월 1 1981 classes21.dex
-rw-r--r-- 1 chyi chyi 150356 1월 1 1981 classes22.dex
-rw-r--r-- 1 chyi chyi 704 1월 1 1981 classes23.dex
-rw-r--r-- 1 chyi chyi 65112 1월 1 1981 classes24.dex
-rw-r--r-- 1 chyi chyi 15196728 1월 1 1981 classes25.dex
-rw-r--r-- 1 chyi chyi 9904964 1월 1 1981 classes26.dex
-rw-r--r-- 1 chyi chyi 12000064 1월 1 1981 classes27.dex
-rw-r--r-- 1 chyi chyi 1342952 1월 1 1981 classes28.dex
-rw-r--r-- 1 chyi chyi 28704 1월 1 1981 classes3.dex
-rw-r--r-- 1 chyi chyi 58396 1월 1 1981 classes4.dex
-rw-r--r-- 1 chyi chyi 9596 1월 1 1981 classes5.dex
-rw-r--r-- 1 chyi chyi 13308 1월 1 1981 classes6.dex
-rw-r--r-- 1 chyi chyi 565608 1월 1 1981 classes7.dex
-rw-r--r-- 1 chyi chyi 403328 1월 1 1981 classes8.dex
-rw-r--r-- 1 chyi chyi 252560 1월 1 1981 classes9.dex
drwxrwxr-x 9 chyi chyi 4096 8월 28 15:14 kotlin
drwxrwxr-x 6 chyi chyi 4096 8월 28 15:14 lib <===
drwxrwxr-x 3 chyi chyi 4096 8월 28 15:14 okhttp3
drwxrwxr-x 45 chyi chyi 4096 8월 28 15:14 res
-rw-rw-r-- 1 chyi chyi 1963932 1월 1 1981 resources.arsc
주황색으로 표시된 so file은 wireguard VpnService library에 포함되어 있는 것이며, 나머지는 신규로 추가된 것들이다.
-rw-r--r-- 1 chyi chyi 9366896 1월 1 1981 libam-go.so
-rw-r--r-- 1 chyi chyi 31992 1월 1 1981 libam-quick.so
-rw-r--r-- 1 chyi chyi 108560 1월 1 1981 libam.so
-rw-r--r-- 1 chyi chyi 10760 1월 1 1981 libandroidx.graphics.path.so
-rw-r--r-- 1 chyi chyi 6232 1월 1 1981 libdatastore_shared_counter.so
-rw-r--r-- 1 chyi chyi 318696 1월 1 1981 libhev-socks5-tunnel.so
-rw-r--r-- 1 chyi chyi 3607856 1월 1 1981 libwg-go.so
-rw-r--r-- 1 chyi chyi 31904 1월 1 1981 libwg-quick.so
-rw-r--r-- 1 chyi chyi 93760 1월 1 1981 libwg.so
_________________________________________________________________
지금까지 Wireguard-android code를 내려 받아 build한 후, 자신의 phone에 설치하는 과정을 간략히 살펴 보았다. 이어지는 장에서는 Wireguard-android app의 전체 s/w 아키텍쳐를 분석하기에 앞서, 핵심 엔진인 wireguard-go 코드의 동작 원리를 먼저 파헤쳐 보기로 하자.
2. WireGuard-Go 핵심 코드 분석
이번 장에서는 wireguard android app의 내부 엔진이라고 할 수 있는 wireguard-go 코드의 동작 원리를 먼저 분석해 보고자 한다.
참고로, 이번 장의 글을 읽기 전에 wireguard handshaking과 관련하여 이전 posting을 먼저 읽어 볼 것을 권한다.
<Jason A. Donenfeld의 great works 😍>
- linux kernel, windows kernel에서 동작하는 fantastic wireguard code를 구현한 일 => a work of art !
- kernel에서 구현이 불가한 환경을 대비하여 사용자 영역에서 동작하는 wireguard-go 코드를 구현한 일
- mobile 등의 환경에서 wireguard-go를 호출하여 사용하기 편리하도록 simple한 API를 제공하도록 구조를 설계한 일 => a work of art !
- NoiseIK 기반의 simple하고 slim한 wireguard protocol을 설계한 일. 아, 물론 암호 코드에 대한 탁월한 식견도 높이 살만하다. => a great hacker
- (물론 open source 진영의 여러 개발자들과 협업을 통한 산물이기는 하지만) 많은 부분(Linux, windows, android, iOS version)을 혼자 설계하고 구현한 일 => low level의 엔진과 UI를 두루 섭렵하기는 결코 쉽지 않은 일이다.
2.1 wireguard-go 프로젝트 해부
우선 아래 site에서 wireguard-go code를 download 받아 build 후 실행해 보도록 하자.
<How to download and build the wireguard-go>
$ git clone https://git.zx2c4.com/wireguard-go
$ cd wireguard-go
$ make
<How to run>
$ ./wireguard-go wg0$ sudo ip address add dev wg0 10.1.1.1/24$ sudo ip link set up dev wg0$ sudo wg set wg0 listen-port 51820 private-key ./privatekey peer dZopqlLFIFCSSxIQbI1+f6sCUWlrjj4X19VC7iA34Bs= allowed-ips 10.1.1.0/24 endpoint 172.32.1.254:51820
외관상으로 볼 때, wireguard-go는 tun device를 사용하는 여타의 vpn s/w(예: OpenVPN)의 구조와 크게 다를 바가 없다. 물론 handshaking 과정 등은 전혀 다르지만 말이다.
먼저, 아래 2개의 그림은 2개의 network interface 즉, br0(lan)와 eth0(wan)를 갖는 gateway(router) 상에서의 wireguard-go의 동작 방식을 표현한 것이다. wireguard-go는 tun0(or wg0)라는 가상의 network interface를 하나 만들고, 이를 기반으로 터널링 처리를 하게된다.
[그림 2.1] Gateway(Router) 상에서의 WireGuard-Go 패킷 처리 구조(1)
📌 wiregurd-go code는 tun0 interface와 eth0 interface 간에 패킷을 교환해주는 일종의 proxy로 이해하면 된다.
[그림 2.2] Gateway(Router) 상에서의 WireGuard-Go 패킷 처리 구조(2) - 2대의 wireguard-go gateway 간 통신
<좌측 Gateway의 routing table 예>
root@OpenWrt:~# netstat -nr
Kernel IP routing table
Destination Gateway Genmask Flags MSS Window irtt Iface
0.0.0.0 172.31.1.1 0.0.0.0 UG 0 0 0 eth0
192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 br-lan
Kernel IP routing table
Destination Gateway Genmask Flags MSS Window irtt Iface
0.0.0.0 172.31.1.1 0.0.0.0 UG 0 0 0 eth0
192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 br-lan
10.1.1.0 0.0.0.0 255.255.255.0 U 0 0 0 tun0
172.16.1.0 0.0.0.0 255.255.255.0 U 0 0 0 tun0 <=== this
172.31.1.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
__________________________________________________________________________________________
172.31.1.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
__________________________________________________________________________________________
(위의 그림과 routing table을 기준으로) wireguard-go의 동작 원리를 간략히 요약해 보면 다음과 같다.
<송신 패킷 처리 과정>
0) 먼저, wireguard-go process는 wireguard peer와 handshaking(NoiseIK) 과정을 통해 wireguard tunnel을 확립한다.
1) 이 상태에서 Gateway 내부망의 PC로 부터 패킷이 lan interface(br0)를 통해 유입된다.
2) PC에서 들어온 패킷의 목적지 주소(예: 172.16.1.1)를 보니, 위의 routing table에서 this로 표시한 항목에 matching된다(routing된다).
-> TUN driver의 특성: 자신에게 전달된 packet이 (더 이상 routing되지 않고) TUN driver 내부 buffer에 쌓인다(enqueue).
3) wireguard-go process(or daemon)는 /dev/net/tun device를 open한 상태에서 대기하고 있다가, 패킷(앞서 buffer에 쌓여 있는 패킷)이 들어올 경우 이를 read해 간다.
4) wireguard-go process는 TUN device에서 읽어온 패킷에 대해, 아래 그림과 같이 암호화(ChaCha20/Poly1305 AEAD) 및 터널링 처리(wireguard header, outter udp, ip header 추가 등)를 진행한 후, 물리 interface 즉, eth0를 통해 해당 패킷을 내보낸다(raw socket 사용).
[그림 2.3] WireGuard-Go ChaCha20/Poly1305 AEAD 암호화 및 UDP packet encapsulation 과정
<수신 패킷 처리 과정>
1) wireguard peer로 부터 vpn packet이 도착한다.
2) 해당 packet의 목적지 port(UDP 51820)를 확인해 보니, wireguard process의 것에 해당하므로, 해당 패킷은 tcp/ip stack을 거쳐 곧바로 wireguard process에게 전달된다.
3) wireguard process는 (아래 그림과 같이) 해당 패킷의 터널 헤더를 제거하고, 복호화(ChaCha20/Poly1305 AEAD)를 진행한다.
4) 이후, wireguard process는 터널 헤더가 제거된 ip packet을 TUN device로 write한다.
5) TUN device로 write된 패킷은 목적지 ip 주소(ex: 192.168.1.1)를 기반으로 routing하여 br0 interface를 통해 내부망의 PC로 전달된다.
-> routing이 원할히 진행되도록 사전에 ip forwarding 설정은 enable되어 있어야 한다.
________________________________________________________
한편, network interface가 1개만 있는 host 상에서의 wireguard-go의 구조는 아래 그림과 같이 표현할 수 있다. 이 경우 역시, wireguard-go는 wg0(= tun0) interface를 하나 생성하고, 이를 통해 터널링 처리를 진행하게 된다. 동작 원리는 앞서 설명한 내용과 다르지 않다.
[그림 2.5] Host 상에서의 WireGuard-Go 패킷 처리 구조
📌 application에서 내보낸 패킷이 wg0(= tun0) interface로 향하는 이유는 목적지 주소가 wg0 interface의 대역에 존재하기 때문이다. 이는 linux의 network interface & routing의 동작 원리와 관련된 내용이다.
자, 그럼 지금부터 wireguard-go code를 분석해 보도록 하자.
<wireguard-go main routine에서 하는 일>
<wireguard-go/main.go>
1) tun device 생성
=> tun device open, tun device read 함수 정의, tun 관련 event 처리 routine 정의
2) wireguard device 생성
=> 3개의 queue(handshaking, encryption, decryption) 생성, 3개의 go routine(handshaking, encryption, decryption worker) 호출
3) UAPI 처리 코드
=> wg tool을 통해 내려주는 설정을 받아주는 코드(unix domain socket 코드)
4) 기타 daemon화 및 로그 초기화 등 진행
wireguard-go는 위의 2) 단계에서 wireguard device를 생성하면서 아래와 같이 3개의 queue(go 채널)를 만드는데, 이 3개의 queue는 이어서 설명하는 패킷 처리 routine과 자연스럽게 연결된다.
- device.queue.handshake = newHandshakeQueue() //handshaking message용 queue
- device.queue.encryption = newOutboundQueue() //encryption을 하기 전에 대기하는 queue
- device.queue.decryption = newInboundQueue() //decryption을 하기 전에 대기하는 queue
<wireguard-go code의 패킷 처리>
a) device.RoutineHandshake( ) : peer와의 tunnel을 형성하기 위한 noise handshaking 과정
b) device.RoutineEncryption( ) : tun device로 부터 패킷 수신 후, tunneling 처리. 이어서 tunnel 처리된 패킷을 eth0 device로 전송하는 과정 (주의: 실제로 이 함수는 암호화 및 tunnel 처리만 담당)
c) device.RoutineDecryption( ) : eth0 device로 부터 tunnel packet 수신 후, tunnel 제거. 이어서 tunnel 제거된 패킷을 tun device로 전송하는 과정 (주의: 실제로 이 함수는 터널 제거 및 복호화 처리만 담당)
2.2 wireguard-go의 핵심 API 활용하기
지금까지 살펴 본 내용은 wireguard-go라는 binary(daemon)를 명령행에서 실행하는 경우에 대한 것이었다. 그런데 이러한 daemon 형태의 program을 android와 같은 mobile app 환경에서 직접 사용하는 것에는 많은 어려움이 있다. 가령, 일반 사용자가 wireguard-go daemon을 booting script에 추가하여 원하는 시점에 사용한다거나, 아니면 app에서 특정 시점에 원하는 daemon(background process)을 구동하여 사용하는 것은 생각 처럼 간단치가 않다. 그렇다면, 어찌해야 할까 ?
When in Rome, do as the Romans do.
재미있게도, wireguard를 만든 Jason A. Donenfeld는 이러한 경우를 대비하여, 몇가지 API 만을 사용하여 wireguard-go 코드의 핵심 기능이 구동될 수 있도록 하는 방법을 마련해 두었다. 💯
이 내용을 간략히 요약해 보면 다음과 같다.
<wireguard-go 핵심 기능 구동 절차 요약>
[1] tun, tnet, err := CreateUnmonitoredTUNFromFD(int(tunFd))
=> TUN device에 대한 fd(file descriptor)를 전달 받아 사용.
=> 경우에 따라서는 이 단계에서 TUN device를 직접 생성 & open하는 API를 호출할 수도 있음.
[2] dev := device.NewDevice(tun, ....)
=> 이 함수 내에서 3개의 queue(handshaking, encryption, decryption) 생성,
=> 3개의 go routine(handshaking, encryption, decryption worker) 호출
[3] dev.IpcSet(....)
=> UI로 부터 전달된 wireguard configuration 기반으로, wireguard 설정.
[4] uapiFile := ipc.UAPIOpen(), ..., ipc.UAPIListen(....), ... ipc.Accept(...)
device.IpcHandle(...)
=> UI와의 IPC를 위한 창구 open. IPC 명령 처리
[5] err = dev.Up()
=> wireguard interface link up
_____________________________
이렇게 5개의 함수만 호출해 주면, android, iOS, macOS app 등에서 wireguard를 쉽게 구동시킬 수 있다.
(3장에서 다시 소개하겠지만) wireguard-android/tunnel/tools/libwg-go/api-android.go 파일의 내용을 들여다 보면, 위의 내용이 무슨 얘기인지 쉽게 이해할 수 있을 것으로 보인다.
[그림 2.8] libwg-go/api-android.go 코드 내용에서 발췌
참고로, 위의 [3] 단계 과정을 좀 더 부연 설명하면 다음과 같다.
먼저, UI로 부터 전달된 wireguard 설정 정보(interface, peer 정보)는 dev.IpcSet(....) method로 전달되게 되고, 이를 토대로 wireguard engine 동작하게 된다. 이 때, UI로 부터 전달되는 wireguard 설정 정보는 2가지 type(get or set)으로 구분되며, 아래와 같은 모습을 하고 있다.
<UI <= wireguard engine: 설정 정보 획득>
get=1
{empty line}
<UI => wireguard engine: 설정 정보 지정>
set=1
key1=value1
key2=value2
key3=value3
key4=value4
key5=value5
...
{empty line}
가령, UI에서 wireguard 설정을 내려 보내고자 할 경우에는 아래와 같은 형식으로 string을 만들어 내려 보내면 된다.
<UI => wireguard engine으로 전달되는 명령>
set=1
private_key=e84b5a6d2717c1003a13b431570353dbaca9146cf150c5f8575680feba52027a
fwmark=0
listen_port=12912
replace_peers=true
public_key=b85996fecc9c7f1fc6d2572a76eda11d59bcd20be8e543b15ce4bd85a8e75a33
preshared_key=188515093e952f5f22e865cef3012e72f8b5f0b598ac0309d5dacce3b70fcf52
replace_allowed_ips=true
allowed_ip=192.168.4.4/32
endpoint=[abcd:23::33%2]:51820
public_key=58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376
replace_allowed_ips=true
allowed_ip=192.168.4.6/32
persistent_keepalive_interval=111
endpoint=182.122.22.19:3233
public_key=662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58
endpoint=5.152.198.39:51820
replace_allowed_ips=true
allowed_ip=192.168.4.10/32
allowed_ip=192.168.4.11/32
public_key=e818b58db5274087fcc1be5dc728cf53d3b5726b4cef6b9bab8f8f8c2452c25c
remove=true
{empty line}
반대로, wireguard engine으로 부터 설정 정보를 얻어 오고자 할 경우에는, 아래와 같은 형태의 string을 내려 보내고, 그 결과를 받아 parsing하여 사용하면 된다.
<UI => wireguard engine으로 전달되는 명령>
get=1
{empty line}
<결과 string>
private_key=e84b5a6d2717c1003a13b431570353dbaca9146cf150c5f8575680feba52027a
listen_port=12912
public_key=b85996fecc9c7f1fc6d2572a76eda11d59bcd20be8e543b15ce4bd85a8e75a33
preshared_key=188515093e952f5f22e865cef3012e72f8b5f0b598ac0309d5dacce3b70fcf52
allowed_ip=192.168.4.4/32
endpoint=[abcd:23::33%2]:51820
public_key=58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376
tx_bytes=38333
rx_bytes=2224
allowed_ip=192.168.4.6/32
persistent_keepalive_interval=111
endpoint=182.122.22.19:3233
public_key=662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58
endpoint=5.152.198.39:51820
allowed_ip=192.168.4.10/32
allowed_ip=192.168.4.11/32
tx_bytes=1212111
rx_bytes=1929999999
protocol_version=1
errno=0
{empty line}
이와 관련하여 자세한 사항은 wireguard homepage 내용을 참조하도록 하자.
______________________________________________
지금까지 (wireguard-android app의 핵심 엔진인) wireguard-go 코드의 동작 원리와 mobile 환경에서 쉽게 사용할 수 있는 API set 까지 확인해 보았다. 그럼 이제부터 본격적으로 Wireguard-android app의 전체 s/w 구조를 파헤쳐 보도록 하자. 💢
3. WireGuard Android 코드 분석#1 - Frontend UI
Wireguard android app은 어떤 형태로 구성되어 있을까 ? 이번 장과 다음 장에서는 이에 관하여 상세히 분석해 보고자 한다. 😎
3.1 Wireguard Android UI 분석
먼저 wireguard app의 UI page와 동작 과정을 살펴 보도록 하자. Kotlin code(특히 9개의 activity)를 제대로 이해하려면 화면 내용과 동작 방식을 숙지하고 있어야 한다. 😋
[그림 3.2] Wireguard android 첫 page - 하단의 + 버튼 선택
[그림 3.3] 터널 생성 하기 - 파일 불러오기, QR 코드 스캔, 직접만들기 중 하나 선택
📌 여기에서는 (좀 불편한 방법이기는 해도) 직접만들기 메뉴를 선택하도록 한다.
[그림 3.4] 직접만들기 메뉴 선택 - 수동 설정 page
📌 설정이 완료된 후에는 상단의 디스크 저장 버튼을 눌러준다.📌 Peer는 여러 개를 이어서 추가할 수가 있다.
한편, 위의 직접 만들기 메뉴 중간에 보면, "모든 앱"이라고 표시된 부분이 있는데, 이를 선택하면 다음과 같은 popup 창이 뜬다. 만일 wireguard 적용 대상에서 제외하고 싶은 app이 있다면, 이곳에서 설정해주면 된다(default 설정은 모든 app에 대해 wireguard를 적용하는 것이다).
[그림 3.5] 수동 설정 page 중 모든 앱 선택 화면
📌 wireguard android app은 application 별로 터널 적용 여부를 선택하는 기능이 있다.
터널 설정을 마친 후에는 (앞으로 4장에서 자세히 소개할) VpnService를 구동해 주어야 한다. (커널 기능을 사용하는) VpnService를 구동시키기 위해서는 사용자의 허락이 필요하다.
[그림 3.6] 터널 구동하기 - 우측 상단의 slide 버튼 우측으로 이동
📌 정상적으로 wireguard가 구동된 경우, 화면 상단에 작은 좌물쇠 icon이 표시된다(이 부분은 android version & 기기 별로 표시되는 방식에 차이가 있음).
[그림 3.7] 생성된 터널 목록 확인
📌 터널은 여러개를 만들 수가 있다. 물론, 보통은 1개만 설정해서 사용한다.
앞서 설정한 내용을 확인해 보고자 할 경우에는 위의 tunnel list 화면 목록에서 하나(예: wg0)를 선택하면 된다.
📌 이 상태에서 상단의 pen 버튼을 누르면, 수정 모드로 전환된다.
그럼, 지금까지 설정한 내용이 정상적으로 동작하는지를 확인해 보도록 하자. 이를 위해 당연히 peer 쪽에도 (아래와 같은) wireguard 설정이 되어 있어야 한다.
GooglePlay에서 적당한 ping app을 하나 받아 설치 후, peer vpn ip로 ping을 시도해 보자.
[그림 3.10] ping test하기
OK, 정상적으로 동작한다.
만일, 정상적으로 vpn이 동작하지 않는다면, 로그 내용을 확인하여 원인을 파악해 볼 수도 있다.
[그림 3.11] 로그 확인하기
📌 로그를 확인하는 부분이 wireguard 관련 내용만 보이는게 아니라서 그런지, 조금은 불편한 느낌이다. 😓
아니면, 앞서 설정한 내용(wg0.conf 파일)을 zip 파일로 내려 받아, PC에서 확인해 보는 것도 좋은 방법이다.
[그림 3.12] wireguard 설정 정보 내려 받기 - 터널 정보를 Zip 파일로 내보내기 메뉴 선택
위에서 내려 받은 파일은 Ubuntu PC에서 압축을 풀어 보니, wg0.conf 파일이 틔어 나온다.
[그림 3.13] wireguard 설정 정보(wg0.conf) 확인 모습
이상으로 wireguard-android app을 구성하는 UI를 살펴 보았다. 이 밖에도 몇가지 더 UI page가 있기는 하지만, 이 정도 선에서 마무리하고자 한다. 💢
3.2 Wireguard Android app의 구조 분석
Wireguard android app은 크게 보면 Kotlin 기반의 UI(Activity, Receiver, Content Provider, Service로 구성)와 Java 기반의 터널 처리 코드(VPN Service), 그리고 Golang으로 구현한 wireguard-go library로 구성되어 있다. wireguard-go library는 Java 기반의 VPN Service와 통합되는 것으로 이해하면 된다.
[그림 3.14] Wireguard android app의 구조(1)
📌 Wireguard android UI만 따로 살펴 보면 android 4대 component 즉, activity(9개), service(1개), broadcast receiver(3개), 그리고 content provider(1개)로 구성되어 있음을 알 수 있다.
[그림 3.15] Wireguard android app의 구조(2)
[그림 3.16] Wireguard android app의 구조(3)
지금까지 wireguard-android app의 frontend 부분을 살펴 보았으니, 다음 장에서는 backend 부분을 살펴 보기로 하자.
4. WireGuard Android 코드 분석#2 - Backend VpnService
Wireguard android app의 vpn 동작 방식을 이해하기 위해서는 Android가 기본적으로 제공하는 VpnService(android service)를 이해할 필요가 있다.
Android VpnService는 사실 특별한 내용은 아니고, (앞서 2장에서 충분히 설명한 것 처럼) TUN network device를 효과적으로 이용할 수 있도록 android 용(android framework)으로 만든 것이라고 말할 수 있다.
4.1 Android VpnService와 wireguard
자, 그럼 지금부터 Android VpnService 코드를 구현하기 위해 필요한 절차를 열거해 보기로 하자.
단계 1) Manifest 선언
먼저, 아래와 같이 AndroidManifest.xml 파일에 VpnService를 선언해 준다.
단계 2) Service 준비하기
VPN을 시작하기 전에, 먼저 VpnService.prepare(Context) method를 사용하여 service를 준비할 필요가 있다. 이 method는 결과값으로 intent를 하나 return해 주게 되는데, 이를 이용하여 사용자 접근 권한을 물어보는 system activity를 구동시키게 된다. 물론,이미 권한이 허용된 상황이라면 intent 대신 null이 리턴된다.
[그림 4.3] Android VpnService 구현 관련 Skeleton(2)
단계 3) MyVpnService 구현하기
VpnService를 상속(extend) 받은 후, service 관련 lifecycle method 들인 onCreate, onStartCommand, onDestory method를 재 정의(override)하여 사용하도록 한다. 특히, onStartCommand method 내에서는 VPN interface(= TUN interface)를 만들고, 트래픽 handling(send/recv, 암/복호화, encapsulation/decapsulation) 코드를 구현해야 한다.
[그림 4.4] Android VpnService 구현 관련 Skeleton(3)
단계 4) 패킷 처리
패킷 처리는 사용자가 직접 구현한 vpnThread 내에서 하게 된다. 먼저, (vpnThread 코드는) vpnInterface.fileDescriptor를 통해 IP packet을 읽은 후, 암호화 및 터널링 처리를 한다. 이후, 해당 패킷(터널링 패킷)을 vpn server로 전송(raw socket) 한다. 한편, vpn server로 부터 도착한 패킷은 tcp/ip stack을 타고 곧바로 vpnThread 코드로 전달(vpn protocol이 사용하는 tcp or udp port 정보 덕택에)되게 되는데, 해당 패킷을 복호화 및 decapsulation(터널 헤더 제거)한 후에는 다시 vpnInterface.fileDescriptor를 이용하여 network stack(정확하게는 TUN device)으로 write해 주어야 한다.
📌 vpnInterface.fileDescriptor는 /dev/tun file을 open한 후, 얻게되는 file descriptor로 이해하면 된다.
Network stack으로 내려간 패킷은 (일반 android application의) 목적지 port 정보를 이용하여 최종적으로 해당 application에게 전달된다.
____________________________________________________________
앞서 설명한 내용대로라면, Java code만 이용하더라도 충분히 vpn 기능을 구현해 낼 수가 있다. 하지만, 암복호화 및 UDP tunnel 등 복잡한 작업이 동반되어야 하는 만큼, 보통은 C/C++, Go, Rust 등의 저수준 language를 활용하여 구현하는 편이 효율적이라고 볼 수 있다. 💢
따라서, 앞서 설명한 vpnThread 자리에 wireguard-go code가 사용된다면, 아래와 같은 그림을 생각해 볼 수가 있겠다.
[그림 4.5] Android VpnService 기반의 WireGuard(1)
여기서, 한가지 중요한 사실은 vpnInterface = builder.establish() method 호출을 통해 알게된 vpnInterface.fileDescriptor 정보가 반드시 wireguard-go code에 전달되어야만 한다는 것이다. 그래야만 wireguard-go code에서 TUN device로 부터 패킷을 read하거나, TUN device로 패킷을 write할 수 있기 때문이다.
📌 참고로, TUN 기반으로 vpn을 구현할 경우에는 어쩔 수 없이 지연 시간(userspace 코드와 kernel 간의 context switching이 빈번히 발생, memory copy 자주 발생)이 늘어날 수 밖에 없다.
[그림 4.7] Wireguard android app의 아키텍쳐(1) - 패킷 송신 처리 과정
참고로, wireguard VpnService code는 아래 site(maven repository)에서 별도로 관리(aar file 및 source code)되고 있다.
[그림 4.9] maven central repository에 올라가 있는 wireguard tunnel library
이는 사용자로 하여금 wireguard 부분을 구현하는 것에 크게 신경쓸 필요 없이, UI 구현에만 집중하면 된다는 것을 의미한다. 😀
4.2 wireguard 관련 주요 코드 흐름 분석
마지막으로 살펴볼 내용은 몇가지 API 호출을 통한 주요 코드 흐름에 관한 것이다.
먼저, VpnService를 구현한 Backend code는 아래와 같은 interface(Backend.java)를 제공한다. 따라서 UI 코드에서는 Backend class를 알아낸 상태에서 아래 method를 호출하여 VpnService backend routine을 call해 주면 된다. 아래 method 중 눈여겨 보아야 할 것으로는 tunnel on/off와 관련된 setState( ) method와 터널 상태 정보(interface, peer info)를 위한 getStatistics( ) method이다.
<Backend interface>
-> UI에서 Backend와 통신하는데 사용하는 API
_________________________________________________
Set<String> getRunningTunnelNames();
-> 동작 중인 tunnel 명 얻기
Tunnel.State getState(Tunnel tunnel) throws Exception;
-> Tunnel on/off 상태 값 관리
Statistics getStatistics(Tunnel tunnel) throws Exception;
-> JNI wgGetConfig call과 연관됨.
String getVersion() throws Exception;
-> JNI wgVersion call과 연관됨.
Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception;
-> JNI wgTurnOn, wgTurnOff call과 연관됨.
_________________________________________________
한편, Backend routine에서 wireguard-go code를 호출하기 위한 JNI interface는 다음과 같다.
<JNI>
_________________________________________________
JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOn(JNIEnv *env, jclass c, jstring ifname, jint tun_fd, jstring settings)
-> tunnel 구동 시작
JNIEXPORT void JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOff(JNIEnv *env, jclass c, jint handle)
-> tunnel 구동 중지
JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetSocketV4(JNIEnv *env, jclass c, jint handle)
JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetSocketV6(JNIEnv *env, jclass c, jint handle)
JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetConfig(JNIEnv *env, jclass c, jint handle)
-> tunnel 설정 정보 및 handshaking tx/rx bytes 정보 등 획득
JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgVersion(JNIEnv *env, jclass c)
-> version 정보
_________________________________________________
API 호출 관계를 그림으로 그려 보면 다음과 같다.
[그림 4.10] 주요 API 흐름(1)
📌 VPN Service backend java code는 실제로 Gobackend(GoBackend.java)와 kernel backend(WgQuickBackend.java) code가 있지만, 이번 posting에서는 Gobackend만을 언급하기로 한다.[그림 4.11] 주요 API 흐름(2)
📌 앞서도 언급했듯이, Java VpnService에서 open한 tun device에 대한 file descriptor 정보는 JNI를 통해 libwg-go.so library로 전달되어야만 한다.
끝으로, 가장 중요한 3가지 API 흐름을 정리해 보면 다음과 같다.
<wgTurnOn code 흐름>
-> UI -> setState(UP) -> GoBackend -> JNI -> wgTurnOn( ) -> libwg-go.so(wireguard-go)
📌 UI code 내부의 흐름 분석이 조금 아쉽기는 하지만, 주요 API 흐름 관점에서 대략 정리해 보았다. 😂
<wgTurnOff code 흐름>
-> UI -> setState(DOWN) -> GoBackend -> JNI -> wgTurnOff( ) -> libwg-go.so(wireguard-go)
<GetConfig code 흐름>
-> UI -> getStatistics -> GoBackend -> JNI -> wgGetConfig( ) -> libwg-go.so(wireguard-go)
______________________________________________
이상으로, Wireguard android app의 UI 화면 구성 내용(3장)과 Backend 기능(VpnService)의 동작원리(4장)를 분석해 보았다. 늘 그렇지만 시작할 때 계획했던 것보다 부족한 부분이 항상 남는다. 아쉽지만 부족한 부분은 독자 여러분의 몫으로 남기며, 다음 장으로 넘어가 보자. 😋
5. WireGuard Auto Connect 기능과 연동하기
이번 장에서는 지난 시간과 마찬가지로 필자가 개발 중인 Wireguard Auto Connect 기능을 wireguard-android code에 적용하는 내용을 소개하고자 한다.
[그림 5.1] wireguard-android와 Auto Connect 기능 연동하기(1)
<TODO>
a) Auto Connect Start code를 Golang으로 구현한다.
b) Java VpnService code와 Auto Connect code를 JNI로 연결한다.
c) UI 화면(page)을 하나 만들어, Auto Connect를 Start 하도록 한다.
d) 이후, Auto Connect를 통해 얻은 peer 정보를 토대로 터널 설정을 자동으로 하도록 한다.
To be continued...
6. References
[1] https://github.com/WireGuard/wireguard-android
[2] https://developer.android.com/build/gradle-build-overview?hl=ko
[3] https://github.com/snowdream/51-android/blob/master/51-android.rules
[4] https://ryan-schachte.com/blog/userspace_wireguard_tunnels/
[5] https://slowbootkernelhacks.blogspot.com/2024/06/wireguard-awesome-open-source-project.html
[6] https://tailscale.com/blog/more-throughput
[7] https://developer.android.com/develop/connectivity/vpn?hl=ko
-> android vpn service 관련
[8] https://dev.to/ankushppie/building-a-secure-vpn-in-android-with-wireguard-a-complete-guide-n1
-> Jetpack Compose와 Kotline으로 android에 wireguard 통합하기
[9] https://medium.com/@satish.nada98/complete-guide-to-implementing-a-vpn-service-in-android-exploring-development-details-with-code-96683c834d8d
-> kotlin으로 android vpn service 구현하기
[10] https://medium.com/@mdazadhossain95/flutter-wireguard-vpn-one-codebase-android-and-ios-dedb9d4286ec
-> flutter로 wireguard app(Android, iOS) 만들기
[11] https://www.javatips.net/api/android.net.vpnservice
-> android.net.vpnservice를 사용하는 example codes
[12] https://central.sonatype.com/artifact/com.wireguard.android/tunnel
[13] https://javadoc.io/doc/com.wireguard.android/tunnel/latest/index.html
-> Embeddable tunnel library for WireGuard for Android
[14] And, Google and Gemini~
Slowboot