diff options
author | Chia-chi Yeh <chiachi@android.com> | 2011-11-23 17:09:17 -0800 |
---|---|---|
committer | Fred Chung <fchung@google.com> | 2011-11-28 17:21:34 -0800 |
commit | 9ad3f40880fa998063e6d3bcb994e918b44272bc (patch) | |
tree | 9640a8ed361879195758c83be5e8648c105b6e4c /samples/ToyVpn | |
parent | c4d810a43ce5d052812a453b488ef0cc7511dfda (diff) | |
download | android_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.mk | 16 | ||||
-rw-r--r-- | samples/ToyVpn/AndroidManifest.xml | 41 | ||||
-rwxr-xr-x | samples/ToyVpn/_index.html | 7 | ||||
-rw-r--r-- | samples/ToyVpn/res/layout/form.xml | 37 | ||||
-rw-r--r-- | samples/ToyVpn/res/values/strings.xml | 29 | ||||
-rw-r--r-- | samples/ToyVpn/res/values/styles.xml | 25 | ||||
-rw-r--r-- | samples/ToyVpn/server/linux/Makefile | 18 | ||||
-rw-r--r-- | samples/ToyVpn/server/linux/ToyVpnServer.cpp | 288 | ||||
-rwxr-xr-x | samples/ToyVpn/server/linux/_index.html | 1 | ||||
-rw-r--r-- | samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java | 66 | ||||
-rw-r--r-- | samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java | 337 |
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(¶meters[offset + 1], parameter, length); + offset += 1 + length; + } + + // Fill the rest of the space with spaces. + memset(¶meters[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); + } +} |