diff --git a/build/sdk.atree b/build/sdk.atree
index 44430f627..148899b52 100644
--- a/build/sdk.atree
+++ b/build/sdk.atree
@@ -184,6 +184,7 @@ development/samples/SpinnerTest samples/${PLATFORM_NAME}/SpinnerT
development/samples/TicTacToeLib samples/${PLATFORM_NAME}/TicTacToeLib
development/samples/TicTacToeMain samples/${PLATFORM_NAME}/TicTacToeMain
development/samples/TtsEngine samples/${PLATFORM_NAME}/TtsEngine
+development/samples/ToyVpn samples/${PLATFORM_NAME}/ToyVpn
development/samples/USB/MissileLauncher samples/${PLATFORM_NAME}/USB/MissileLauncher
development/samples/USB/AdbTest samples/${PLATFORM_NAME}/USB/AdbTest
development/samples/VoiceRecognitionService samples/${PLATFORM_NAME}/VoiceRecognitionService
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 @@
+
+
+
+
ToyVPN is a sample application that shows how to build a VPN client using the VpnService class introduced in API level 14.
+ +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.
+ +The sample code of the server-side implementation is Linux-specific and is available in the server directory. To run the server or port it to another platform, please see comments in the code for the details.
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 @@
+
+
+
+Server code can be found in the ToyVPN sample distributed in the SDK.
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); + } +}