App 內建 VPN/DNS service
我們偶爾會有需要使用到 VPN 服務,可能是要存取公司內部網路、模擬成外國IP用來切換帳號,或甚至是在強國需要翻牆的時候。 VPN 服務提供了一個中介層,把所有的網路請求透過這個中介層來做轉發
一來可以模擬一些資訊(國家/地區),二來也可以保護本地的真實資料不會暴露在網路上
不過這次的需求比較特殊,因為工作需要,我們的App可能會處於一個 DNS 被竄改的網路環境,所以我們App所使用的 domain 可能會被 hijack 而被導向我們不預期的IP。 所以這時候我們就透過了 Android 內建的 VPNService 來在我們的 App 中建立一個 VPN Service,一來是把App中所有的流量集中從這個 VPNService 出去,二來從中強迫指定 DNS Server 的規則以及真實 IP,以保護我們 App 中所有網路請求的 DNS 轉換都是可信任的。
具體作法的架構如下
建立一個 MyVPNService
, 然後與系統的網路層綁定,讓系統自動將 My Application 中所有 Network 的流量,都先通過配置好的 MyVPNService
出去。
其中這個自定義的 MyVPNService 就包含了我們指定的 DNS server,而不去用系統內建的。
具體實作步驟:
- 建立 MyVPNService 並繼承
android.net.VPNService
- 在 AndroidMenifest.xml 中註冊 MyVPNService
- 在我們 App 初始化的時候,啟動這個 Service,並且在 service init 的時候做相對應的配置跟綁定。
- 記得在 App 退出的時候,停掉這個 Service
MyVPNService.java
public class MyVPNService extends VpnService { private VpnService.Builder builder = new VpnService.Builder(); private ParcelFileDescriptor fileDescriptor; private Thread mThread; private boolean shouldRun = true; private DatagramChannel tunnel; public void stopThisService() { this.shouldRun = false; stopSelf(); } @Override public int onStartCommand(final Intent paramIntent, int p1, int p2) { mThread = new Thread(new Runnable() { public void run() { try { fileDescriptor = builder.setSession("MyVPNService") .addAddress("192.168.0.1", 24) .addDnsServer("8.8.8.8") .addDnsServer("8.8.4.4") .establish(); tunnel = DatagramChannel.open(); tunnel.connect(new InetSocketAddress("127.0.0.1", 8087)); protect(tunnel.socket()); while (shouldRun) Thread.sleep(100L); } catch (Exception exception) {} finally { if (fileDescriptor != null) { try { fileDescriptor.close(); fileDescriptor = null; } catch (IOException e) {} } } } }, "DNS Changer"); mThread.start(); return Service.START_NOT_STICKY; } @Override public IBinder onBind(Intent intent) { return mBinder; } private final IBinder mBinder = new LocalBinder(); public class LocalBinder extends Binder { public MyVPNService getService() { return MyVPNService.this; } } }
AndroidManifest.xml
宣告 Service 的同時,也要記得加上
BIND_VPN_SERVICE
的權限!
... <service android:name=".MyVPNService" android:permission="android.permission.BIND_VPN_SERVICE" /> ...
MainActivity.java
public class MainActivity extends Activity { final int REQUEST_CONNECT = 21; private MyVPNService mService = null; private Intent serviceIntent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); serviceIntent = new Intent(this, MyVPNService.class); startDNS(); } private void startDNS(){ Intent intent = VpnService.prepare(this); if (intent != null) startActivityForResult(intent, REQUEST_CONNECT); else onActivityResult(REQUEST_CONNECT, RESULT_OK, null); } private ServiceConnection connection = new ServiceConnection(){ @Override public void onServiceConnected(ComponentName name, IBinder service) { if(service != null && service instanceof MyVPNService.LocalBinder){ mService = ((MyVPNService.LocalBinder) service).getService(); } } @Override public void onServiceDisconnected(ComponentName name) {} }; protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK) { startService(serviceIntent); bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE); } super.onActivityResult(requestCode, resultCode, data); } @Override protected void onStop() { super.onStop(); if(mService != null) mService.stopThisService(); unbindService(connection); } }
解說
首先這個 Service 必需繼承 Android 的 android.net.VpnService
public class MyVPNService extends VpnService
然後在 Service 啟動時開啟一個 background thread 來做初始化
首先先建立這個 VPN Service 的描述檔
addAddress
:指定虛擬接口的 IP
addDnsServer
:指定接口的 DNS Server
... VpnService.Builder builder = new VpnService.Builder(); ParcelFileDescriptor fileDescriptor; ... fileDescriptor = builder.setSession("MyVPNService") .addAddress("192.168.0.1", 24) .addDnsServer("8.8.8.8") .addDnsServer("8.8.4.4") .establish();
當然還有像是
setMtu
addRoute
addSearchDomain
功能,但是目前的應用主要是以指定 DNS 的需求
接下來就是建立一個Socket,並指定到本地的 8087 port
... DatagramChannel tunnel; ... tunnel = DatagramChannel.open(); tunnel.connect(new InetSocketAddress("127.0.0.1", 8087));
最後,透過 VpnService 中的 protect
讓系統進行 Socket 的設置以及確保流量會透過我們建立的 Socket 出去
protect(tunnel.socket());
建立好 VPNService 後,還要在 App 開啟時(或是任何需要的時候)去啟動它
要啟動時,首先因為 VPN Service 的開啟,就像 Runtime permission 一樣,必須要明確的用戶確認後才能使用。 所以,要先確認用戶是否已經同意/開啟過
private void startDNS(){ Intent intent = VpnService.prepare(this); if (intent != null) startActivityForResult(intent, REQUEST_CONNECT); else onActivityResult(REQUEST_CONNECT, RESULT_OK, null); }
如果 VpnService.prepare(this)
有拿到 intent,表示用戶還沒有同意過,所以系統會給你一個對應的 intent 來取得用戶同意
開啟這個 intent 後,系統會跳出一個視窗來詢問使用者是否同意我們的 App 開啟一個背景 VPN
在用戶同意後,未來啟動的時候,VpnService.prepare(this)
就不會拿到 intent,這時候就可以直接啟動 Service
最後,在 App 裏開的時候,記得要停掉這個 VPNService,不然這個 Service 會一直在背景開啟,並且一直轉導用戶的網路流量(就變成真正的 VPN服務,或是感覺就像是監控間諜軟體 XD)
@Override protected void onStop() { super.onStop(); if(mService != null) mService.stopThisService(); unbindService(connection); }
參考資料
- VPNService 原理及使用介紹:https://blog.csdn.net/roland_sun/article/details/46337171
- Sample project : https://github.com/msayan/star-dns-changer
- Public DNS 服務:https://www.lifewire.com/free-and-public-dns-servers-2626062