Merge "Make ToyVpn a little more realistic" am: 5387f7163d
am: e58f77f2f3
Change-Id: I70a0a3c98d307f4113b1bbc1e6c3fbd349693dbc
This commit is contained in:
34
samples/ToyVpn/res/drawable/ic_vpn.xml
Normal file
34
samples/ToyVpn/res/drawable/ic_vpn.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -18,49 +18,60 @@ package com.example.android.toyvpn;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user