summaryrefslogtreecommitdiffstats
path: root/samples/ToyVpn
diff options
context:
space:
mode:
authorChia-chi Yeh <chiachi@android.com>2011-11-23 17:09:17 -0800
committerFred Chung <fchung@google.com>2011-11-28 17:21:34 -0800
commit9ad3f40880fa998063e6d3bcb994e918b44272bc (patch)
tree9640a8ed361879195758c83be5e8648c105b6e4c /samples/ToyVpn
parentc4d810a43ce5d052812a453b488ef0cc7511dfda (diff)
downloadandroid_development-9ad3f40880fa998063e6d3bcb994e918b44272bc.tar.gz
android_development-9ad3f40880fa998063e6d3bcb994e918b44272bc.tar.bz2
android_development-9ad3f40880fa998063e6d3bcb994e918b44272bc.zip
Android VPN sample for ICS SDK
Change-Id: I84e568625c5c9cc9b48f338e2d6226a8e9f67017
Diffstat (limited to 'samples/ToyVpn')
-rw-r--r--samples/ToyVpn/Android.mk16
-rw-r--r--samples/ToyVpn/AndroidManifest.xml41
-rwxr-xr-xsamples/ToyVpn/_index.html7
-rw-r--r--samples/ToyVpn/res/layout/form.xml37
-rw-r--r--samples/ToyVpn/res/values/strings.xml29
-rw-r--r--samples/ToyVpn/res/values/styles.xml25
-rw-r--r--samples/ToyVpn/server/linux/Makefile18
-rw-r--r--samples/ToyVpn/server/linux/ToyVpnServer.cpp288
-rwxr-xr-xsamples/ToyVpn/server/linux/_index.html1
-rw-r--r--samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java66
-rw-r--r--samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java337
11 files changed, 865 insertions, 0 deletions
diff --git a/samples/ToyVpn/Android.mk b/samples/ToyVpn/Android.mk
new file mode 100644
index 000000000..8fe714cae
--- /dev/null
+++ b/samples/ToyVpn/Android.mk
@@ -0,0 +1,16 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := samples
+
+# Only compile source java files in this apk.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := ToyVpn
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
+
+# Use the following include to make our test apk.
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/samples/ToyVpn/AndroidManifest.xml b/samples/ToyVpn/AndroidManifest.xml
new file mode 100644
index 000000000..8366dd6bc
--- /dev/null
+++ b/samples/ToyVpn/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.toyvpn">
+
+ <uses-permission android:name="android.permission.INTERNET"/>
+
+ <uses-sdk android:minSdkVersion="14"/>
+
+ <application android:label="@string/app">
+
+ <activity android:name=".ToyVpnClient"
+ android:configChanges="orientation|keyboardHidden">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+
+ <service android:name=".ToyVpnService"
+ android:permission="android.permission.BIND_VPN_SERVICE">
+ <intent-filter>
+ <action android:name="android.net.VpnService"/>
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/samples/ToyVpn/_index.html b/samples/ToyVpn/_index.html
new file mode 100755
index 000000000..d896899c2
--- /dev/null
+++ b/samples/ToyVpn/_index.html
@@ -0,0 +1,7 @@
+<p>ToyVPN is a sample application that shows how to build a VPN client using the <a href="../../../reference/android/net/VpnService.html">VpnService</a> class introduced in API level 14.</p>
+
+<p>This application consists of an Android client and a sample implementation of a server. It performs IP over UDP and is capable of doing seamless handover between different networks as long as it receives the same VPN parameters.</p>
+
+<p>The sample code of the server-side implementation is Linux-specific and is available in the <code>server</code> directory. To run the server or port it to another platform, please see comments in the code for the details.</p>
+
+<img alt="" src="../images/vpn-confirmation.png" />
diff --git a/samples/ToyVpn/res/layout/form.xml b/samples/ToyVpn/res/layout/form.xml
new file mode 100644
index 000000000..7a325db54
--- /dev/null
+++ b/samples/ToyVpn/res/layout/form.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <LinearLayout android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="3mm">
+
+ <TextView style="@style/item" android:text="@string/address"/>
+ <EditText style="@style/item" android:id="@+id/address"/>
+
+ <TextView style="@style/item" android:text="@string/port"/>
+ <EditText style="@style/item" android:id="@+id/port"/>
+
+ <TextView style="@style/item" android:text="@string/secret"/>
+ <EditText style="@style/item" android:id="@+id/secret" android:password="true"/>
+
+ <Button style="@style/item" android:id="@+id/connect" android:text="@string/connect"/>
+
+ </LinearLayout>
+</ScrollView>
diff --git a/samples/ToyVpn/res/values/strings.xml b/samples/ToyVpn/res/values/strings.xml
new file mode 100644
index 000000000..2fe40d279
--- /dev/null
+++ b/samples/ToyVpn/res/values/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <string name="app">ToyVPN</string>
+
+ <string name="address">Server Address:</string>
+ <string name="port">Server Port:</string>
+ <string name="secret">Shared Secret:</string>
+ <string name="connect">Connect!</string>
+
+ <string name="connecting">ToyVPN is connecting...</string>
+ <string name="connected">ToyVPN is connected!</string>
+ <string name="disconnected">ToyVPN is disconnected!</string>
+</resources>
diff --git a/samples/ToyVpn/res/values/styles.xml b/samples/ToyVpn/res/values/styles.xml
new file mode 100644
index 000000000..409005331
--- /dev/null
+++ b/samples/ToyVpn/res/values/styles.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <style name="item">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textAppearance">?android:attr/textAppearanceMedium</item>
+ <item name="android:singleLine">true</item>
+ </style>
+</resources>
diff --git a/samples/ToyVpn/server/linux/Makefile b/samples/ToyVpn/server/linux/Makefile
new file mode 100644
index 000000000..8b1ef03db
--- /dev/null
+++ b/samples/ToyVpn/server/linux/Makefile
@@ -0,0 +1,18 @@
+#
+# Copyright (C) 2011 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+all:
+ g++ -Wall -o ToyVpnServer ToyVpnServer.cpp
diff --git a/samples/ToyVpn/server/linux/ToyVpnServer.cpp b/samples/ToyVpn/server/linux/ToyVpnServer.cpp
new file mode 100644
index 000000000..c3d07dfaf
--- /dev/null
+++ b/samples/ToyVpn/server/linux/ToyVpnServer.cpp
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <errno.h>
+#include <fcntl.h>
+
+#ifdef __linux__
+
+// There are several ways to play with this program. Here we just give an
+// example for the simplest scenario. Let us say that a Linux box has a
+// public IPv4 address on eth0. Please try the following steps and adjust
+// the parameters when necessary.
+//
+// # Enable IP forwarding
+// echo 1 > /proc/sys/net/ipv4/ip_forward
+//
+// # Pick a range of private addresses and perform NAT over eth0.
+// iptables -t nat -A POSTROUTING -s 10.0.0.0/8 -o eth0 -j MASQUERADE
+//
+// # Create a TUN interface.
+// ip tuntap add dev tun0 mode tun
+//
+// # Set the addresses and bring up the interface.
+// ifconfig tun0 10.0.0.1 dstaddr 10.0.0.2 up
+//
+// # Create a server on port 8000 with shared secret "test".
+// ./ToyVpnServer tun0 8000 test -m 1400 -a 10.0.0.2 32 -d 8.8.8.8 -r 0.0.0.0 0
+//
+// This program only handles a session at a time. To allow multiple sessions,
+// multiple servers can be created on the same port, but each of them requires
+// its own TUN interface. A short shell script will be sufficient. Since this
+// program is designed for demonstration purpose, it performs neither strong
+// authentication nor encryption. DO NOT USE IT IN PRODUCTION!
+
+#include <net/if.h>
+#include <linux/if_tun.h>
+
+static int get_interface(char *name)
+{
+ int interface = open("/dev/net/tun", O_RDWR | O_NONBLOCK);
+
+ ifreq ifr;
+ memset(&ifr, 0, sizeof(ifr));
+ ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
+ strncpy(ifr.ifr_name, name, sizeof(ifr.ifr_name));
+
+ if (ioctl(interface, TUNSETIFF, &ifr)) {
+ perror("Cannot get TUN interface");
+ exit(1);
+ }
+
+ return interface;
+}
+
+#else
+
+#error Sorry, you have to implement this part by yourself.
+
+#endif
+
+static int get_tunnel(char *port, char *secret)
+{
+ // We use an IPv6 socket to cover both IPv4 and IPv6.
+ int tunnel = socket(AF_INET6, SOCK_DGRAM, 0);
+ int flag = 1;
+ setsockopt(tunnel, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
+ flag = 0;
+ setsockopt(tunnel, IPPROTO_IPV6, IPV6_V6ONLY, &flag, sizeof(flag));
+
+ // Accept packets received on any local address.
+ sockaddr_in6 addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sin6_family = AF_INET6;
+ addr.sin6_port = htons(atoi(port));
+
+ // Call bind(2) in a loop since Linux does not have SO_REUSEPORT.
+ while (bind(tunnel, (sockaddr *)&addr, sizeof(addr))) {
+ if (errno != EADDRINUSE) {
+ return -1;
+ }
+ usleep(100000);
+ }
+
+ // Receive packets till the secret matches.
+ char packet[1024];
+ socklen_t addrlen;
+ do {
+ addrlen = sizeof(addr);
+ int n = recvfrom(tunnel, packet, sizeof(packet), 0,
+ (sockaddr *)&addr, &addrlen);
+ if (n <= 0) {
+ return -1;
+ }
+ packet[n] = 0;
+ } while (packet[0] != 0 || strcmp(secret, &packet[1]));
+
+ // Connect to the client as we only handle one client at a time.
+ connect(tunnel, (sockaddr *)&addr, addrlen);
+ return tunnel;
+}
+
+static void build_parameters(char *parameters, int size, int argc, char **argv)
+{
+ // Well, for simplicity, we just concatenate them (almost) blindly.
+ int offset = 0;
+ for (int i = 4; i < argc; ++i) {
+ char *parameter = argv[i];
+ int length = strlen(parameter);
+ char delimiter = ',';
+
+ // If it looks like an option, prepend a space instead of a comma.
+ if (length == 2 && parameter[0] == '-') {
+ ++parameter;
+ --length;
+ delimiter = ' ';
+ }
+
+ // This is just a demo app, really.
+ if (offset + length >= size) {
+ puts("Parameters are too large");
+ exit(1);
+ }
+
+ // Append the delimiter and the parameter.
+ parameters[offset] = delimiter;
+ memcpy(&parameters[offset + 1], parameter, length);
+ offset += 1 + length;
+ }
+
+ // Fill the rest of the space with spaces.
+ memset(&parameters[offset], ' ', size - offset);
+
+ // Control messages always start with zero.
+ parameters[0] = 0;
+}
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char **argv)
+{
+ if (argc < 5) {
+ printf("Usage: %s <tunN> <port> <secret> options...\n"
+ "\n"
+ "Options:\n"
+ " -m <MTU> for the maximum transmission unit\n"
+ " -a <address> <prefix-length> for the private address\n"
+ " -r <address> <prefix-length> for the forwarding route\n"
+ " -d <address> for the domain name server\n"
+ " -s <domain> for the search domain\n"
+ "\n"
+ "Note that TUN interface needs to be configured properly\n"
+ "BEFORE running this program. For more information, please\n"
+ "read the comments in the source code.\n\n", argv[0]);
+ exit(1);
+ }
+
+ // Parse the arguments and set the parameters.
+ char parameters[1024];
+ build_parameters(parameters, sizeof(parameters), argc, argv);
+
+ // Get TUN interface.
+ int interface = get_interface(argv[1]);
+
+ // Wait for a tunnel.
+ int tunnel;
+ while ((tunnel = get_tunnel(argv[2], argv[3])) != -1) {
+ printf("%s: Here comes a new tunnel\n", argv[1]);
+
+ // On UN*X, there are many ways to deal with multiple file
+ // descriptors, such as poll(2), select(2), epoll(7) on Linux,
+ // kqueue(2) on FreeBSD, pthread(3), or even fork(2). Here we
+ // mimic everything from the client, so their source code can
+ // be easily compared side by side.
+
+ // Put the tunnel into non-blocking mode.
+ fcntl(tunnel, F_SETFL, O_NONBLOCK);
+
+ // Send the parameters several times in case of packet loss.
+ for (int i = 0; i < 3; ++i) {
+ send(tunnel, parameters, sizeof(parameters), MSG_NOSIGNAL);
+ }
+
+ // Allocate the buffer for a single packet.
+ char packet[32767];
+
+ // We use a timer to determine the status of the tunnel. It
+ // works on both sides. A positive value means sending, and
+ // any other means receiving. We start with receiving.
+ int timer = 0;
+
+ // We keep forwarding packets till something goes wrong.
+ while (true) {
+ // Assume that we did not make any progress in this iteration.
+ bool idle = true;
+
+ // Read the outgoing packet from the input stream.
+ int length = read(interface, packet, sizeof(packet));
+ if (length > 0) {
+ // Write the outgoing packet to the tunnel.
+ send(tunnel, packet, length, MSG_NOSIGNAL);
+
+ // There might be more outgoing packets.
+ idle = false;
+
+ // If we were receiving, switch to sending.
+ if (timer < 1) {
+ timer = 1;
+ }
+ }
+
+ // Read the incoming packet from the tunnel.
+ length = recv(tunnel, packet, sizeof(packet), 0);
+ if (length == 0) {
+ break;
+ }
+ if (length > 0) {
+ // Ignore control messages, which start with zero.
+ if (packet[0] != 0) {
+ // Write the incoming packet to the output stream.
+ write(interface, packet, length);
+ }
+
+ // There might be more incoming packets.
+ idle = false;
+
+ // If we were sending, switch to receiving.
+ if (timer > 0) {
+ timer = 0;
+ }
+ }
+
+ // If we are idle or waiting for the network, sleep for a
+ // fraction of time to avoid busy looping.
+ if (idle) {
+ usleep(100000);
+
+ // Increase the timer. This is inaccurate but good enough,
+ // since everything is operated in non-blocking mode.
+ timer += (timer > 0) ? 100 : -100;
+
+ // We are receiving for a long time but not sending.
+ // Can you figure out why we use a different value? :)
+ if (timer < -16000) {
+ // Send empty control messages.
+ packet[0] = 0;
+ for (int i = 0; i < 3; ++i) {
+ send(tunnel, packet, 1, MSG_NOSIGNAL);
+ }
+
+ // Switch to sending.
+ timer = 1;
+ }
+
+ // We are sending for a long time but not receiving.
+ if (timer > 20000) {
+ break;
+ }
+ }
+ }
+ printf("%s: The tunnel is broken\n", argv[1]);
+ close(tunnel);
+ }
+ perror("Cannot create tunnels");
+ exit(1);
+}
diff --git a/samples/ToyVpn/server/linux/_index.html b/samples/ToyVpn/server/linux/_index.html
new file mode 100755
index 000000000..7919ccb3b
--- /dev/null
+++ b/samples/ToyVpn/server/linux/_index.html
@@ -0,0 +1 @@
+<p>Server code can be found in the ToyVPN sample distributed in the SDK.</p>
diff --git a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java
new file mode 100644
index 000000000..925179a3c
--- /dev/null
+++ b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.toyvpn;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.VpnService;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Button;
+
+public class ToyVpnClient extends Activity implements View.OnClickListener {
+ private TextView mServerAddress;
+ private TextView mServerPort;
+ private TextView mSharedSecret;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.form);
+
+ mServerAddress = (TextView) findViewById(R.id.address);
+ mServerPort = (TextView) findViewById(R.id.port);
+ mSharedSecret = (TextView) findViewById(R.id.secret);
+
+ findViewById(R.id.connect).setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ Intent intent = VpnService.prepare(this);
+ if (intent != null) {
+ startActivityForResult(intent, 0);
+ } else {
+ onActivityResult(0, RESULT_OK, null);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int request, int result, Intent data) {
+ if (result == RESULT_OK) {
+ String prefix = getPackageName();
+ Intent intent = new Intent(this, ToyVpnService.class)
+ .putExtra(prefix + ".ADDRESS", mServerAddress.getText().toString())
+ .putExtra(prefix + ".PORT", mServerPort.getText().toString())
+ .putExtra(prefix + ".SECRET", mSharedSecret.getText().toString());
+ startService(intent);
+ }
+ }
+}
diff --git a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java
new file mode 100644
index 000000000..41cf0e13b
--- /dev/null
+++ b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.toyvpn;
+
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.net.VpnService;
+import android.os.Handler;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.DatagramChannel;
+
+public class ToyVpnService extends VpnService implements Handler.Callback, Runnable {
+ private static final String TAG = "ToyVpnService";
+
+ private String mServerAddress;
+ private String mServerPort;
+ private byte[] mSharedSecret;
+ private PendingIntent mConfigureIntent;
+
+ private Handler mHandler;
+ private Thread mThread;
+
+ private ParcelFileDescriptor mInterface;
+ private String mParameters;
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ // The handler is only used to show messages.
+ if (mHandler == null) {
+ mHandler = new Handler(this);
+ }
+
+ // Stop the previous session by interrupting the thread.
+ if (mThread != null) {
+ mThread.interrupt();
+ }
+
+ // Extract information from the intent.
+ String prefix = getPackageName();
+ mServerAddress = intent.getStringExtra(prefix + ".ADDRESS");
+ mServerPort = intent.getStringExtra(prefix + ".PORT");
+ mSharedSecret = intent.getStringExtra(prefix + ".SECRET").getBytes();
+
+ // Start a new session by creating a new thread.
+ mThread = new Thread(this, "ToyVpnThread");
+ mThread.start();
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mThread != null) {
+ mThread.interrupt();
+ }
+ }
+
+ @Override
+ public boolean handleMessage(Message message) {
+ if (message != null) {
+ Toast.makeText(this, message.what, Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ }
+
+ @Override
+ public synchronized void run() {
+ try {
+ Log.i(TAG, "Starting");
+
+ // If anything needs to be obtained using the network, get it now.
+ // This greatly reduces the complexity of seamless handover, which
+ // tries to recreate the tunnel without shutting down everything.
+ // In this demo, all we need to know is the server address.
+ InetSocketAddress server = new InetSocketAddress(
+ mServerAddress, Integer.parseInt(mServerPort));
+
+ // We try to create the tunnel for several times. The better way
+ // is to work with ConnectivityManager, such as trying only when
+ // the network is avaiable. Here we just use a counter to keep
+ // things simple.
+ for (int attempt = 0; attempt < 10; ++attempt) {
+ mHandler.sendEmptyMessage(R.string.connecting);
+
+ // Reset the counter if we were connected.
+ if (run(server)) {
+ attempt = 0;
+ }
+
+ // Sleep for a while. This also checks if we got interrupted.
+ Thread.sleep(3000);
+ }
+ Log.i(TAG, "Giving up");
+ } catch (Exception e) {
+ Log.e(TAG, "Got " + e.toString());
+ } finally {
+ try {
+ mInterface.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ mInterface = null;
+ mParameters = null;
+
+ mHandler.sendEmptyMessage(R.string.disconnected);
+ Log.i(TAG, "Exiting");
+ }
+ }
+
+ private boolean run(InetSocketAddress server) throws Exception {
+ DatagramChannel tunnel = null;
+ boolean connected = false;
+ try {
+ // Create a DatagramChannel as the VPN tunnel.
+ tunnel = DatagramChannel.open();
+
+ // Protect the tunnel before connecting to avoid loopback.
+ if (!protect(tunnel.socket())) {
+ throw new IllegalStateException("Cannot protect the tunnel");
+ }
+
+ // Connect to the server.
+ tunnel.connect(server);
+
+ // For simplicity, we use the same thread for both reading and
+ // writing. Here we put the tunnel into non-blocking mode.
+ tunnel.configureBlocking(false);
+
+ // Authenticate and configure the virtual network interface.
+ handshake(tunnel);
+
+ // Now we are connected. Set the flag and show the message.
+ connected = true;
+ mHandler.sendEmptyMessage(R.string.connected);
+
+ // Packets to be sent are queued in this input stream.
+ FileInputStream in = new FileInputStream(mInterface.getFileDescriptor());
+
+ // Packets received need to be written to this output stream.
+ FileOutputStream out = new FileOutputStream(mInterface.getFileDescriptor());
+
+ // Allocate the buffer for a single packet.
+ ByteBuffer packet = ByteBuffer.allocate(32767);
+
+ // We use a timer to determine the status of the tunnel. It
+ // works on both sides. A positive value means sending, and
+ // any other means receiving. We start with receiving.
+ int timer = 0;
+
+ // We keep forwarding packets till something goes wrong.
+ while (true) {
+ // Assume that we did not make any progress in this iteration.
+ boolean idle = true;
+
+ // Read the outgoing packet from the input stream.
+ int length = in.read(packet.array());
+ if (length > 0) {
+ // Write the outgoing packet to the tunnel.
+ packet.limit(length);
+ tunnel.write(packet);
+ packet.clear();
+
+ // There might be more outgoing packets.
+ idle = false;
+
+ // If we were receiving, switch to sending.
+ if (timer < 1) {
+ timer = 1;
+ }
+ }
+
+ // Read the incoming packet from the tunnel.
+ length = tunnel.read(packet);
+ if (length > 0) {
+ // Ignore control messages, which start with zero.
+ if (packet.get(0) != 0) {
+ // Write the incoming packet to the output stream.
+ out.write(packet.array(), 0, length);
+ }
+ packet.clear();
+
+ // There might be more incoming packets.
+ idle = false;
+
+ // If we were sending, switch to receiving.
+ if (timer > 0) {
+ timer = 0;
+ }
+ }
+
+ // If we are idle or waiting for the network, sleep for a
+ // fraction of time to avoid busy looping.
+ if (idle) {
+ Thread.sleep(100);
+
+ // Increase the timer. This is inaccurate but good enough,
+ // since everything is operated in non-blocking mode.
+ timer += (timer > 0) ? 100 : -100;
+
+ // We are receiving for a long time but not sending.
+ if (timer < -15000) {
+ // Send empty control messages.
+ packet.put((byte) 0).limit(1);
+ for (int i = 0; i < 3; ++i) {
+ packet.position(0);
+ tunnel.write(packet);
+ }
+ packet.clear();
+
+ // Switch to sending.
+ timer = 1;
+ }
+
+ // We are sending for a long time but not receiving.
+ if (timer > 20000) {
+ throw new IllegalStateException("Timed out");
+ }
+ }
+ }
+ } catch (InterruptedException e) {
+ throw e;
+ } catch (Exception e) {
+ Log.e(TAG, "Got " + e.toString());
+ } finally {
+ try {
+ tunnel.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ return connected;
+ }
+
+ private void handshake(DatagramChannel tunnel) throws Exception {
+ // To build a secured tunnel, we should perform mutual authentication
+ // and exchange session keys for encryption. To keep things simple in
+ // this demo, we just send the shared secret in plaintext and wait
+ // for the server to send the parameters.
+
+ // Allocate the buffer for handshaking.
+ ByteBuffer packet = ByteBuffer.allocate(1024);
+
+ // Control messages always start with zero.
+ packet.put((byte) 0).put(mSharedSecret).flip();
+
+ // Send the secret several times in case of packet loss.
+ for (int i = 0; i < 3; ++i) {
+ packet.position(0);
+ tunnel.write(packet);
+ }
+ packet.clear();
+
+ // Wait for the parameters within a limited time.
+ for (int i = 0; i < 50; ++i) {
+ Thread.sleep(100);
+
+ // Normally we should not receive random packets.
+ int length = tunnel.read(packet);
+ if (length > 0 && packet.get(0) == 0) {
+ configure(new String(packet.array(), 1, length - 1).trim());
+ return;
+ }
+ }
+ throw new IllegalStateException("Timed out");
+ }
+
+ private void configure(String parameters) throws Exception {
+ // If the old interface has exactly the same parameters, use it!
+ if (mInterface != null && parameters.equals(mParameters)) {
+ Log.i(TAG, "Using the previous interface");
+ return;
+ }
+
+ // Configure a builder while parsing the parameters.
+ Builder builder = new Builder();
+ for (String parameter : parameters.split(" ")) {
+ String[] fields = parameter.split(",");
+ try {
+ switch (fields[0].charAt(0)) {
+ case 'm':
+ builder.setMtu(Short.parseShort(fields[1]));
+ break;
+ case 'a':
+ builder.addAddress(fields[1], Integer.parseInt(fields[2]));
+ break;
+ case 'r':
+ builder.addRoute(fields[1], Integer.parseInt(fields[2]));
+ break;
+ case 'd':
+ builder.addDnsServer(fields[1]);
+ break;
+ case 's':
+ builder.addSearchDomain(fields[1]);
+ break;
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Bad parameter: " + parameter);
+ }
+ }
+
+ // Close the old interface since the parameters have been changed.
+ try {
+ mInterface.close();
+ } catch (Exception e) {
+ // ignore
+ }
+
+ // Create a new interface using the builder and save the parameters.
+ mInterface = builder.setSession(mServerAddress)
+ .setConfigureIntent(mConfigureIntent)
+ .establish();
+ mParameters = parameters;
+ Log.i(TAG, "New interface: " + parameters);
+ }
+}