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:
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"/>
|
<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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user