Make ToyVpn a little more realistic
Now it shows how to:
- start as an always-on VPN.
- take over the last connection using protect().
Bug: 35802839
Test: manual connection
Change-Id: I4699afbcf4bd0933dbeb3bf77a2d91f49d6ede1d
diff --git a/samples/ToyVpn/res/drawable/ic_vpn.xml b/samples/ToyVpn/res/drawable/ic_vpn.xml
new file mode 100644
index 0000000..bc41788
--- /dev/null
+++ b/samples/ToyVpn/res/drawable/ic_vpn.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:width="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="#000000"
+ android:pathData="M19.35,10.04 C18.67,6.59,15.64,4,12,4 C9.11,4,6.6,5.64,5.35,8.04
+C2.34,8.36,0,10.91,0,14 C0,17.31,2.69,20,6,20 L19,20 C21.76,20,24,17.76,24,15
+C24,12.36,21.95,10.22,19.35,10.04 Z M19,18 L6,18 C3.79,18,2,16.21,2,14
+S3.79,10,6,10 L6.71,10 C7.37,7.69,9.48,6,12,6 C15.04,6,17.5,8.46,17.5,11.5
+L17.5,12 L19,12 C20.66,12,22,13.34,22,15 S20.66,18,19,18 Z" />
+ <path
+ android:strokeColor="#000000"
+ android:strokeWidth="2"
+ android:pathData="M6.58994,13.1803 C6.58994,13.1803,8.59173,15.8724,12.011,15.8726
+C15.2788,15.8728,17.3696,13.2502,17.3696,13.2502" />
+</vector>
diff --git a/samples/ToyVpn/res/layout/form.xml b/samples/ToyVpn/res/layout/form.xml
index 7a325db..0f62e17 100644
--- a/samples/ToyVpn/res/layout/form.xml
+++ b/samples/ToyVpn/res/layout/form.xml
@@ -26,12 +26,13 @@
<EditText style="@style/item" android:id="@+id/address"/>
<TextView style="@style/item" android:text="@string/port"/>
- <EditText style="@style/item" android:id="@+id/port"/>
+ <EditText style="@style/item" android:id="@+id/port" android:inputType="number"/>
<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"/>
+ <Button style="@style/item" android:id="@+id/disconnect" android:text="@string/disconnect"/>
</LinearLayout>
</ScrollView>
diff --git a/samples/ToyVpn/res/values/strings.xml b/samples/ToyVpn/res/values/strings.xml
index 2fe40d2..d5e06ba 100644
--- a/samples/ToyVpn/res/values/strings.xml
+++ b/samples/ToyVpn/res/values/strings.xml
@@ -22,6 +22,7 @@
<string name="port">Server Port:</string>
<string name="secret">Shared Secret:</string>
<string name="connect">Connect!</string>
+ <string name="disconnect">Disconnect!</string>
<string name="connecting">ToyVPN is connecting...</string>
<string name="connected">ToyVPN is connected!</string>
diff --git a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java
index 925179a..c6a72e9 100644
--- a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java
+++ b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java
@@ -18,49 +18,60 @@
import android.app.Activity;
import android.content.Intent;
+import android.content.SharedPreferences;
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;
+public class ToyVpnClient extends Activity {
+ public interface Prefs {
+ String NAME = "connection";
+ String SERVER_ADDRESS = "server.address";
+ String SERVER_PORT = "server.port";
+ String SHARED_SECRET = "shared.secret";
+ }
@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);
+ final TextView serverAddress = (TextView) findViewById(R.id.address);
+ final TextView serverPort = (TextView) findViewById(R.id.port);
+ final TextView sharedSecret = (TextView) findViewById(R.id.secret);
- findViewById(R.id.connect).setOnClickListener(this);
- }
+ final SharedPreferences prefs = getSharedPreferences(Prefs.NAME, MODE_PRIVATE);
+ serverAddress.setText(prefs.getString(Prefs.SERVER_ADDRESS, ""));
+ serverPort.setText(prefs.getString(Prefs.SERVER_PORT, ""));
+ sharedSecret.setText(prefs.getString(Prefs.SHARED_SECRET, ""));
- @Override
- public void onClick(View v) {
- Intent intent = VpnService.prepare(this);
- if (intent != null) {
- startActivityForResult(intent, 0);
- } else {
- onActivityResult(0, RESULT_OK, null);
- }
+ findViewById(R.id.connect).setOnClickListener(v -> {
+ prefs.edit()
+ .putString(Prefs.SERVER_ADDRESS, serverAddress.getText().toString())
+ .putString(Prefs.SERVER_PORT, serverPort.getText().toString())
+ .putString(Prefs.SHARED_SECRET, sharedSecret.getText().toString())
+ .commit();
+
+ Intent intent = VpnService.prepare(ToyVpnClient.this);
+ if (intent != null) {
+ startActivityForResult(intent, 0);
+ } else {
+ onActivityResult(0, RESULT_OK, null);
+ }
+ });
+ findViewById(R.id.disconnect).setOnClickListener(v -> {
+ startService(getServiceIntent().setAction(ToyVpnService.ACTION_DISCONNECT));
+ });
}
@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);
+ startService(getServiceIntent().setAction(ToyVpnService.ACTION_CONNECT));
}
}
+
+ private Intent getServiceIntent() {
+ return new Intent(this, ToyVpnService.class);
+ }
}
diff --git a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnConnection.java b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnConnection.java
new file mode 100644
index 0000000..6644ecd
--- /dev/null
+++ b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnConnection.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2017 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 static java.nio.charset.StandardCharsets.US_ASCII;
+
+import android.app.PendingIntent;
+import android.net.VpnService;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.channels.DatagramChannel;
+import java.util.concurrent.TimeUnit;
+
+public class ToyVpnConnection implements Runnable {
+ /**
+ * Callback interface to let the {@link ToyVpnService} know about new connections
+ * and update the foreground notification with connection status.
+ */
+ public interface OnEstablishListener {
+ void onEstablish(ParcelFileDescriptor tunInterface);
+ }
+
+ /** Maximum packet size is constrained by the MTU, which is given as a signed short. */
+ private static final int MAX_PACKET_SIZE = Short.MAX_VALUE;
+
+ /** Time to wait in between losing the connection and retrying. */
+ private static final long RECONNECT_WAIT_MS = TimeUnit.SECONDS.toMillis(3);
+
+ /** Time between keepalives if there is no traffic at the moment.
+ *
+ * TODO: don't do this; it's much better to let the connection die and then reconnect when
+ * necessary instead of keeping the network hardware up for hours on end in between.
+ **/
+ private static final long KEEPALIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(15);
+
+ /** Time to wait without receiving any response before assuming the server is gone. */
+ private static final long RECEIVE_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(20);
+
+ /**
+ * Time between polling the VPN interface for new traffic, since it's non-blocking.
+ *
+ * TODO: really don't do this; a blocking read on another thread is much cleaner.
+ */
+ private static final long IDLE_INTERVAL_MS = TimeUnit.MILLISECONDS.toMillis(100);
+
+ /**
+ * Number of periods of length {@IDLE_INTERVAL_MS} to wait before declaring the handshake a
+ * complete and abject failure.
+ *
+ * TODO: use a higher-level protocol; hand-rolling is a fun but pointless exercise.
+ */
+ private static final int MAX_HANDSHAKE_ATTEMPTS = 50;
+
+ private final VpnService mService;
+ private final int mConnectionId;
+
+ private final String mServerName;
+ private final int mServerPort;
+ private final byte[] mSharedSecret;
+
+ private PendingIntent mConfigureIntent;
+ private OnEstablishListener mOnEstablishListener;
+
+ public ToyVpnConnection(final VpnService service, final int connectionId,
+ final String serverName, final int serverPort, final byte[] sharedSecret) {
+ mService = service;
+ mConnectionId = connectionId;
+
+ mServerName = serverName;
+ mServerPort= serverPort;
+ mSharedSecret = sharedSecret;
+ }
+
+ /**
+ * Optionally, set an intent to configure the VPN. This is {@code null} by default.
+ */
+ public void setConfigureIntent(PendingIntent intent) {
+ mConfigureIntent = intent;
+ }
+
+ public void setOnEstablishListener(OnEstablishListener listener) {
+ mOnEstablishListener = listener;
+ }
+
+ @Override
+ public void run() {
+ try {
+ Log.i(getTag(), "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.
+ final SocketAddress serverAddress = new InetSocketAddress(mServerName, mServerPort);
+
+ // We try to create the tunnel several times.
+ // TODO: The better way is to work with ConnectivityManager, trying only when the
+ // network is available.
+ // Here we just use a counter to keep things simple.
+ for (int attempt = 0; attempt < 10; ++attempt) {
+ // Reset the counter if we were connected.
+ if (run(serverAddress)) {
+ attempt = 0;
+ }
+
+ // Sleep for a while. This also checks if we got interrupted.
+ Thread.sleep(3000);
+ }
+ Log.i(getTag(), "Giving up");
+ } catch (IOException | InterruptedException | IllegalArgumentException e) {
+ Log.e(getTag(), "Connection failed, exiting", e);
+ }
+ }
+
+ private boolean run(SocketAddress server)
+ throws IOException, InterruptedException, IllegalArgumentException {
+ ParcelFileDescriptor iface = null;
+ boolean connected = false;
+ // Create a DatagramChannel as the VPN tunnel.
+ try (DatagramChannel tunnel = DatagramChannel.open()) {
+
+ // Protect the tunnel before connecting to avoid loopback.
+ if (!mService.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.
+ iface = handshake(tunnel);
+
+ // Now we are connected. Set the flag.
+ connected = true;
+
+ // Packets to be sent are queued in this input stream.
+ FileInputStream in = new FileInputStream(iface.getFileDescriptor());
+
+ // Packets received need to be written to this output stream.
+ FileOutputStream out = new FileOutputStream(iface.getFileDescriptor());
+
+ // Allocate the buffer for a single packet.
+ ByteBuffer packet = ByteBuffer.allocate(MAX_PACKET_SIZE);
+
+ // Timeouts:
+ // - when data has not been sent in a while, send empty keepalive messages.
+ // - when data has not been received in a while, assume the connection is broken.
+ long lastSendTime = System.currentTimeMillis();
+ long lastReceiveTime = System.currentTimeMillis();
+
+ // 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;
+ lastReceiveTime = System.currentTimeMillis();
+ }
+
+ // 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;
+ lastSendTime = System.currentTimeMillis();
+ }
+
+ // If we are idle or waiting for the network, sleep for a
+ // fraction of time to avoid busy looping.
+ if (idle) {
+ Thread.sleep(IDLE_INTERVAL_MS);
+ final long timeNow = System.currentTimeMillis();
+
+ if (lastSendTime + KEEPALIVE_INTERVAL_MS <= timeNow) {
+ // We are receiving for a long time but not sending.
+ // 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();
+ lastSendTime = timeNow;
+ } else if (lastReceiveTime + RECEIVE_TIMEOUT_MS <= timeNow) {
+ // We are sending for a long time but not receiving.
+ throw new IllegalStateException("Timed out");
+ }
+ }
+ }
+ } catch (SocketException e) {
+ Log.e(getTag(), "Cannot use socket", e);
+ } finally {
+ if (iface != null) {
+ try {
+ iface.close();
+ } catch (IOException e) {
+ Log.e(getTag(), "Unable to close interface", e);
+ }
+ }
+ }
+ return connected;
+ }
+
+ private ParcelFileDescriptor handshake(DatagramChannel tunnel)
+ throws IOException, InterruptedException {
+ // 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. We have a hardcoded maximum
+ // handshake size of 1024 bytes, which should be enough for demo
+ // purposes.
+ 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 < MAX_HANDSHAKE_ATTEMPTS; ++i) {
+ Thread.sleep(IDLE_INTERVAL_MS);
+
+ // Normally we should not receive random packets. Check that the first
+ // byte is 0 as expected.
+ int length = tunnel.read(packet);
+ if (length > 0 && packet.get(0) == 0) {
+ return configure(new String(packet.array(), 1, length - 1, US_ASCII).trim());
+ }
+ }
+ throw new IOException("Timed out");
+ }
+
+ private ParcelFileDescriptor configure(String parameters) throws IllegalArgumentException {
+ // Configure a builder while parsing the parameters.
+ VpnService.Builder builder = mService.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 (NumberFormatException e) {
+ throw new IllegalArgumentException("Bad parameter: " + parameter);
+ }
+ }
+
+ // Create a new interface using the builder and save the parameters.
+ final ParcelFileDescriptor vpnInterface;
+ synchronized (mService) {
+ vpnInterface = builder
+ .setSession(mServerName)
+ .setConfigureIntent(mConfigureIntent)
+ .establish();
+ if (mOnEstablishListener != null) {
+ mOnEstablishListener.onEstablish(vpnInterface);
+ }
+ }
+ Log.i(getTag(), "New interface: " + vpnInterface + " (" + parameters + ")");
+ return vpnInterface;
+ }
+
+ private final String getTag() {
+ return ToyVpnConnection.class.getSimpleName() + "[" + mConnectionId + "]";
+ }
+}
diff --git a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java
index 41cf0e1..5e42d9d 100644
--- a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java
+++ b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java
@@ -16,322 +16,153 @@
package com.example.android.toyvpn;
+import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.net.VpnService;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.util.Log;
+import android.util.Pair;
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;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
-public class ToyVpnService extends VpnService implements Handler.Callback, Runnable {
- private static final String TAG = "ToyVpnService";
+public class ToyVpnService extends VpnService implements Handler.Callback {
+ private static final String TAG = ToyVpnService.class.getSimpleName();
- private String mServerAddress;
- private String mServerPort;
- private byte[] mSharedSecret;
- private PendingIntent mConfigureIntent;
+ public static final String ACTION_CONNECT = "com.example.android.toyvpn.START";
+ public static final String ACTION_DISCONNECT = "com.example.android.toyvpn.STOP";
private Handler mHandler;
- private Thread mThread;
- private ParcelFileDescriptor mInterface;
- private String mParameters;
+ private static class Connection extends Pair<Thread, ParcelFileDescriptor> {
+ public Connection(Thread thread, ParcelFileDescriptor pfd) {
+ super(thread, pfd);
+ }
+ }
+
+ private final AtomicReference<Thread> mConnectingThread = new AtomicReference<>();
+ private final AtomicReference<Connection> mConnection = new AtomicReference<>();
+
+ private AtomicInteger mNextConnectionId = new AtomicInteger(1);
+
+ private PendingIntent mConfigureIntent;
@Override
- public int onStartCommand(Intent intent, int flags, int startId) {
+ public void onCreate() {
// 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();
+ // Create the intent to "configure" the connection (just start ToyVpnClient).
+ mConfigureIntent = PendingIntent.getActivity(this, 0, new Intent(this, ToyVpnClient.class),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent != null && ACTION_DISCONNECT.equals(intent.getAction())) {
+ disconnect();
+ return START_NOT_STICKY;
+ } else {
+ connect();
+ return START_STICKY;
}
-
- // 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();
- }
+ disconnect();
}
@Override
public boolean handleMessage(Message message) {
- if (message != null) {
- Toast.makeText(this, message.what, Toast.LENGTH_SHORT).show();
+ Toast.makeText(this, message.what, Toast.LENGTH_SHORT).show();
+ if (message.what != R.string.disconnected) {
+ updateForegroundNotification(message.what);
}
return true;
}
- @Override
- public synchronized void run() {
+ private void connect() {
+ // Become a foreground service. Background services can be VPN services too, but they can
+ // be killed by background check before getting a chance to receive onRevoke().
+ updateForegroundNotification(R.string.connecting);
+ mHandler.sendEmptyMessage(R.string.connecting);
+
+ // Extract information from the shared preferences.
+ final SharedPreferences prefs = getSharedPreferences(ToyVpnClient.Prefs.NAME, MODE_PRIVATE);
+ final String server = prefs.getString(ToyVpnClient.Prefs.SERVER_ADDRESS, "");
+ final byte[] secret = prefs.getString(ToyVpnClient.Prefs.SHARED_SECRET, "").getBytes();
+ final int port;
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");
+ port = Integer.parseInt(prefs.getString(ToyVpnClient.Prefs.SERVER_PORT, ""));
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Bad port: " + prefs.getString(ToyVpnClient.Prefs.SERVER_PORT, null), e);
return;
}
- // Configure a builder while parsing the parameters.
- Builder builder = new Builder();
- for (String parameter : parameters.split(" ")) {
- String[] fields = parameter.split(",");
+ // Kick off a connection.
+ startConnection(new ToyVpnConnection(
+ this, mNextConnectionId.getAndIncrement(), server, port, secret));
+ }
+
+ private void startConnection(final ToyVpnConnection connection) {
+ // Replace any existing connecting thread with the new one.
+ final Thread thread = new Thread(connection, "ToyVpnThread");
+ setConnectingThread(thread);
+
+ // Handler to mark as connected once onEstablish is called.
+ connection.setConfigureIntent(mConfigureIntent);
+ connection.setOnEstablishListener(new ToyVpnConnection.OnEstablishListener() {
+ public void onEstablish(ParcelFileDescriptor tunInterface) {
+ mHandler.sendEmptyMessage(R.string.connected);
+
+ mConnectingThread.compareAndSet(thread, null);
+ setConnection(new Connection(thread, tunInterface));
+ }
+ });
+ thread.start();
+ }
+
+ private void setConnectingThread(final Thread thread) {
+ final Thread oldThread = mConnectingThread.getAndSet(thread);
+ if (oldThread != null) {
+ oldThread.interrupt();
+ }
+ }
+
+ private void setConnection(final Connection connection) {
+ final Connection oldConnection = mConnection.getAndSet(connection);
+ if (oldConnection != null) {
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);
+ oldConnection.first.interrupt();
+ oldConnection.second.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Closing VPN interface", e);
}
}
+ }
- // Close the old interface since the parameters have been changed.
- try {
- mInterface.close();
- } catch (Exception e) {
- // ignore
- }
+ private void disconnect() {
+ mHandler.sendEmptyMessage(R.string.disconnected);
+ setConnectingThread(null);
+ setConnection(null);
+ stopForeground(true);
+ }
- // 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);
+ private void updateForegroundNotification(final int message) {
+ startForeground(1, new Notification.Builder(this)
+ .setSmallIcon(R.drawable.ic_vpn)
+ .setContentText(getString(message))
+ .setContentIntent(mConfigureIntent)
+ .build());
}
}