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
This commit is contained in:
Robin Lee
2017-03-07 21:55:41 +00:00
parent 1345214bfc
commit 72f57c9946
6 changed files with 506 additions and 300 deletions

View File

@@ -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>

View File

@@ -26,12 +26,13 @@
<EditText style="@style/item" android:id="@+id/address"/> <EditText style="@style/item" android:id="@+id/address"/>
<TextView style="@style/item" android:text="@string/port"/> <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"/> <TextView style="@style/item" android:text="@string/secret"/>
<EditText style="@style/item" android:id="@+id/secret" android:password="true"/> <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/connect" android:text="@string/connect"/>
<Button style="@style/item" android:id="@+id/disconnect" android:text="@string/disconnect"/>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

@@ -22,6 +22,7 @@
<string name="port">Server Port:</string> <string name="port">Server Port:</string>
<string name="secret">Shared Secret:</string> <string name="secret">Shared Secret:</string>
<string name="connect">Connect!</string> <string name="connect">Connect!</string>
<string name="disconnect">Disconnect!</string>
<string name="connecting">ToyVPN is connecting...</string> <string name="connecting">ToyVPN is connecting...</string>
<string name="connected">ToyVPN is connected!</string> <string name="connected">ToyVPN is connected!</string>

View File

@@ -18,49 +18,60 @@ package com.example.android.toyvpn;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.net.VpnService; import android.net.VpnService;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Button;
public class ToyVpnClient extends Activity implements View.OnClickListener { public class ToyVpnClient extends Activity {
private TextView mServerAddress; public interface Prefs {
private TextView mServerPort; String NAME = "connection";
private TextView mSharedSecret; String SERVER_ADDRESS = "server.address";
String SERVER_PORT = "server.port";
String SHARED_SECRET = "shared.secret";
}
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.form); setContentView(R.layout.form);
mServerAddress = (TextView) findViewById(R.id.address); final TextView serverAddress = (TextView) findViewById(R.id.address);
mServerPort = (TextView) findViewById(R.id.port); final TextView serverPort = (TextView) findViewById(R.id.port);
mSharedSecret = (TextView) findViewById(R.id.secret); 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 findViewById(R.id.connect).setOnClickListener(v -> {
public void onClick(View v) { prefs.edit()
Intent intent = VpnService.prepare(this); .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) { if (intent != null) {
startActivityForResult(intent, 0); startActivityForResult(intent, 0);
} else { } else {
onActivityResult(0, RESULT_OK, null); onActivityResult(0, RESULT_OK, null);
} }
});
findViewById(R.id.disconnect).setOnClickListener(v -> {
startService(getServiceIntent().setAction(ToyVpnService.ACTION_DISCONNECT));
});
} }
@Override @Override
protected void onActivityResult(int request, int result, Intent data) { protected void onActivityResult(int request, int result, Intent data) {
if (result == RESULT_OK) { if (result == RESULT_OK) {
String prefix = getPackageName(); startService(getServiceIntent().setAction(ToyVpnService.ACTION_CONNECT));
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);
} }
} }
private Intent getServiceIntent() {
return new Intent(this, ToyVpnService.class);
}
} }

View File

@@ -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 + "]";
}
}

View File

@@ -16,322 +16,153 @@
package com.example.android.toyvpn; package com.example.android.toyvpn;
import android.app.Notification;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.net.VpnService; import android.net.VpnService;
import android.os.Handler; import android.os.Handler;
import android.os.Message; import android.os.Message;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import android.widget.Toast; import android.widget.Toast;
import java.io.FileInputStream; import java.io.IOException;
import java.io.FileOutputStream; import java.util.concurrent.atomic.AtomicInteger;
import java.net.InetSocketAddress; import java.util.concurrent.atomic.AtomicReference;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
public class ToyVpnService extends VpnService implements Handler.Callback, Runnable { public class ToyVpnService extends VpnService implements Handler.Callback {
private static final String TAG = "ToyVpnService"; private static final String TAG = ToyVpnService.class.getSimpleName();
private String mServerAddress; public static final String ACTION_CONNECT = "com.example.android.toyvpn.START";
private String mServerPort; public static final String ACTION_DISCONNECT = "com.example.android.toyvpn.STOP";
private byte[] mSharedSecret;
private PendingIntent mConfigureIntent;
private Handler mHandler; private Handler mHandler;
private Thread mThread;
private ParcelFileDescriptor mInterface; private static class Connection extends Pair<Thread, ParcelFileDescriptor> {
private String mParameters; 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 @Override
public int onStartCommand(Intent intent, int flags, int startId) { public void onCreate() {
// The handler is only used to show messages. // The handler is only used to show messages.
if (mHandler == null) { if (mHandler == null) {
mHandler = new Handler(this); mHandler = new Handler(this);
} }
// Stop the previous session by interrupting the thread. // Create the intent to "configure" the connection (just start ToyVpnClient).
if (mThread != null) { mConfigureIntent = PendingIntent.getActivity(this, 0, new Intent(this, ToyVpnClient.class),
mThread.interrupt(); PendingIntent.FLAG_UPDATE_CURRENT);
} }
// Extract information from the intent. @Override
String prefix = getPackageName(); public int onStartCommand(Intent intent, int flags, int startId) {
mServerAddress = intent.getStringExtra(prefix + ".ADDRESS"); if (intent != null && ACTION_DISCONNECT.equals(intent.getAction())) {
mServerPort = intent.getStringExtra(prefix + ".PORT"); disconnect();
mSharedSecret = intent.getStringExtra(prefix + ".SECRET").getBytes(); return START_NOT_STICKY;
} else {
// Start a new session by creating a new thread. connect();
mThread = new Thread(this, "ToyVpnThread");
mThread.start();
return START_STICKY; return START_STICKY;
} }
}
@Override @Override
public void onDestroy() { public void onDestroy() {
if (mThread != null) { disconnect();
mThread.interrupt();
}
} }
@Override @Override
public boolean handleMessage(Message message) { 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; return true;
} }
@Override private void connect() {
public synchronized void run() { // Become a foreground service. Background services can be VPN services too, but they can
try { // be killed by background check before getting a chance to receive onRevoke().
Log.i(TAG, "Starting"); updateForegroundNotification(R.string.connecting);
// 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); mHandler.sendEmptyMessage(R.string.connecting);
// Reset the counter if we were connected. // Extract information from the shared preferences.
if (run(server)) { final SharedPreferences prefs = getSharedPreferences(ToyVpnClient.Prefs.NAME, MODE_PRIVATE);
attempt = 0; final String server = prefs.getString(ToyVpnClient.Prefs.SERVER_ADDRESS, "");
} final byte[] secret = prefs.getString(ToyVpnClient.Prefs.SHARED_SECRET, "").getBytes();
final int port;
// 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 { try {
mInterface.close(); port = Integer.parseInt(prefs.getString(ToyVpnClient.Prefs.SERVER_PORT, ""));
} catch (Exception e) { } catch (NumberFormatException e) {
// ignore Log.e(TAG, "Bad port: " + prefs.getString(ToyVpnClient.Prefs.SERVER_PORT, null), e);
} return;
mInterface = null;
mParameters = null;
mHandler.sendEmptyMessage(R.string.disconnected);
Log.i(TAG, "Exiting");
}
} }
private boolean run(InetSocketAddress server) throws Exception { // Kick off a connection.
DatagramChannel tunnel = null; startConnection(new ToyVpnConnection(
boolean connected = false; this, mNextConnectionId.getAndIncrement(), server, port, secret));
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. private void startConnection(final ToyVpnConnection connection) {
tunnel.connect(server); // Replace any existing connecting thread with the new one.
final Thread thread = new Thread(connection, "ToyVpnThread");
setConnectingThread(thread);
// For simplicity, we use the same thread for both reading and // Handler to mark as connected once onEstablish is called.
// writing. Here we put the tunnel into non-blocking mode. connection.setConfigureIntent(mConfigureIntent);
tunnel.configureBlocking(false); connection.setOnEstablishListener(new ToyVpnConnection.OnEstablishListener() {
public void onEstablish(ParcelFileDescriptor tunInterface) {
// 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); mHandler.sendEmptyMessage(R.string.connected);
// Packets to be sent are queued in this input stream. mConnectingThread.compareAndSet(thread, null);
FileInputStream in = new FileInputStream(mInterface.getFileDescriptor()); setConnection(new Connection(thread, tunInterface));
}
});
thread.start();
}
// Packets received need to be written to this output stream. private void setConnectingThread(final Thread thread) {
FileOutputStream out = new FileOutputStream(mInterface.getFileDescriptor()); final Thread oldThread = mConnectingThread.getAndSet(thread);
if (oldThread != null) {
// Allocate the buffer for a single packet. oldThread.interrupt();
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. private void setConnection(final Connection connection) {
length = tunnel.read(packet); final Connection oldConnection = mConnection.getAndSet(connection);
if (length > 0) { if (oldConnection != null) {
// 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 { try {
tunnel.close(); oldConnection.first.interrupt();
} catch (Exception e) { oldConnection.second.close();
// ignore } catch (IOException e) {
Log.e(TAG, "Closing VPN interface", e);
} }
} }
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. private void disconnect() {
try { mHandler.sendEmptyMessage(R.string.disconnected);
mInterface.close(); setConnectingThread(null);
} catch (Exception e) { setConnection(null);
// ignore stopForeground(true);
} }
// Create a new interface using the builder and save the parameters. private void updateForegroundNotification(final int message) {
mInterface = builder.setSession(mServerAddress) startForeground(1, new Notification.Builder(this)
.setConfigureIntent(mConfigureIntent) .setSmallIcon(R.drawable.ic_vpn)
.establish(); .setContentText(getString(message))
mParameters = parameters; .setContentIntent(mConfigureIntent)
Log.i(TAG, "New interface: " + parameters); .build());
} }
} }