338 lines
12 KiB
Java
338 lines
12 KiB
Java
/*
|
|
* Copyright (C) 2011 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.example.android.toyvpn;
|
|
|
|
import android.app.PendingIntent;
|
|
import android.app.Service;
|
|
import android.content.Intent;
|
|
import android.net.VpnService;
|
|
import android.os.Handler;
|
|
import android.os.Message;
|
|
import android.os.ParcelFileDescriptor;
|
|
import android.util.Log;
|
|
import android.widget.Toast;
|
|
|
|
import java.io.FileInputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.net.InetSocketAddress;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.channels.DatagramChannel;
|
|
|
|
public class ToyVpnService extends VpnService implements Handler.Callback, Runnable {
|
|
private static final String TAG = "ToyVpnService";
|
|
|
|
private String mServerAddress;
|
|
private String mServerPort;
|
|
private byte[] mSharedSecret;
|
|
private PendingIntent mConfigureIntent;
|
|
|
|
private Handler mHandler;
|
|
private Thread mThread;
|
|
|
|
private ParcelFileDescriptor mInterface;
|
|
private String mParameters;
|
|
|
|
@Override
|
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
// The handler is only used to show messages.
|
|
if (mHandler == null) {
|
|
mHandler = new Handler(this);
|
|
}
|
|
|
|
// Stop the previous session by interrupting the thread.
|
|
if (mThread != null) {
|
|
mThread.interrupt();
|
|
}
|
|
|
|
// Extract information from the intent.
|
|
String prefix = getPackageName();
|
|
mServerAddress = intent.getStringExtra(prefix + ".ADDRESS");
|
|
mServerPort = intent.getStringExtra(prefix + ".PORT");
|
|
mSharedSecret = intent.getStringExtra(prefix + ".SECRET").getBytes();
|
|
|
|
// Start a new session by creating a new thread.
|
|
mThread = new Thread(this, "ToyVpnThread");
|
|
mThread.start();
|
|
return START_STICKY;
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
if (mThread != null) {
|
|
mThread.interrupt();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean handleMessage(Message message) {
|
|
if (message != null) {
|
|
Toast.makeText(this, message.what, Toast.LENGTH_SHORT).show();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public synchronized void run() {
|
|
try {
|
|
Log.i(TAG, "Starting");
|
|
|
|
// If anything needs to be obtained using the network, get it now.
|
|
// This greatly reduces the complexity of seamless handover, which
|
|
// tries to recreate the tunnel without shutting down everything.
|
|
// In this demo, all we need to know is the server address.
|
|
InetSocketAddress server = new InetSocketAddress(
|
|
mServerAddress, Integer.parseInt(mServerPort));
|
|
|
|
// We try to create the tunnel for several times. The better way
|
|
// is to work with ConnectivityManager, such as trying only when
|
|
// the network is avaiable. Here we just use a counter to keep
|
|
// things simple.
|
|
for (int attempt = 0; attempt < 10; ++attempt) {
|
|
mHandler.sendEmptyMessage(R.string.connecting);
|
|
|
|
// Reset the counter if we were connected.
|
|
if (run(server)) {
|
|
attempt = 0;
|
|
}
|
|
|
|
// Sleep for a while. This also checks if we got interrupted.
|
|
Thread.sleep(3000);
|
|
}
|
|
Log.i(TAG, "Giving up");
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Got " + e.toString());
|
|
} finally {
|
|
try {
|
|
mInterface.close();
|
|
} catch (Exception e) {
|
|
// ignore
|
|
}
|
|
mInterface = null;
|
|
mParameters = null;
|
|
|
|
mHandler.sendEmptyMessage(R.string.disconnected);
|
|
Log.i(TAG, "Exiting");
|
|
}
|
|
}
|
|
|
|
private boolean run(InetSocketAddress server) throws Exception {
|
|
DatagramChannel tunnel = null;
|
|
boolean connected = false;
|
|
try {
|
|
// Create a DatagramChannel as the VPN tunnel.
|
|
tunnel = DatagramChannel.open();
|
|
|
|
// Protect the tunnel before connecting to avoid loopback.
|
|
if (!protect(tunnel.socket())) {
|
|
throw new IllegalStateException("Cannot protect the tunnel");
|
|
}
|
|
|
|
// Connect to the server.
|
|
tunnel.connect(server);
|
|
|
|
// For simplicity, we use the same thread for both reading and
|
|
// writing. Here we put the tunnel into non-blocking mode.
|
|
tunnel.configureBlocking(false);
|
|
|
|
// Authenticate and configure the virtual network interface.
|
|
handshake(tunnel);
|
|
|
|
// Now we are connected. Set the flag and show the message.
|
|
connected = true;
|
|
mHandler.sendEmptyMessage(R.string.connected);
|
|
|
|
// Packets to be sent are queued in this input stream.
|
|
FileInputStream in = new FileInputStream(mInterface.getFileDescriptor());
|
|
|
|
// Packets received need to be written to this output stream.
|
|
FileOutputStream out = new FileOutputStream(mInterface.getFileDescriptor());
|
|
|
|
// Allocate the buffer for a single packet.
|
|
ByteBuffer packet = ByteBuffer.allocate(32767);
|
|
|
|
// We use a timer to determine the status of the tunnel. It
|
|
// works on both sides. A positive value means sending, and
|
|
// any other means receiving. We start with receiving.
|
|
int timer = 0;
|
|
|
|
// We keep forwarding packets till something goes wrong.
|
|
while (true) {
|
|
// Assume that we did not make any progress in this iteration.
|
|
boolean idle = true;
|
|
|
|
// Read the outgoing packet from the input stream.
|
|
int length = in.read(packet.array());
|
|
if (length > 0) {
|
|
// Write the outgoing packet to the tunnel.
|
|
packet.limit(length);
|
|
tunnel.write(packet);
|
|
packet.clear();
|
|
|
|
// There might be more outgoing packets.
|
|
idle = false;
|
|
|
|
// If we were receiving, switch to sending.
|
|
if (timer < 1) {
|
|
timer = 1;
|
|
}
|
|
}
|
|
|
|
// Read the incoming packet from the tunnel.
|
|
length = tunnel.read(packet);
|
|
if (length > 0) {
|
|
// Ignore control messages, which start with zero.
|
|
if (packet.get(0) != 0) {
|
|
// Write the incoming packet to the output stream.
|
|
out.write(packet.array(), 0, length);
|
|
}
|
|
packet.clear();
|
|
|
|
// There might be more incoming packets.
|
|
idle = false;
|
|
|
|
// If we were sending, switch to receiving.
|
|
if (timer > 0) {
|
|
timer = 0;
|
|
}
|
|
}
|
|
|
|
// If we are idle or waiting for the network, sleep for a
|
|
// fraction of time to avoid busy looping.
|
|
if (idle) {
|
|
Thread.sleep(100);
|
|
|
|
// Increase the timer. This is inaccurate but good enough,
|
|
// since everything is operated in non-blocking mode.
|
|
timer += (timer > 0) ? 100 : -100;
|
|
|
|
// We are receiving for a long time but not sending.
|
|
if (timer < -15000) {
|
|
// Send empty control messages.
|
|
packet.put((byte) 0).limit(1);
|
|
for (int i = 0; i < 3; ++i) {
|
|
packet.position(0);
|
|
tunnel.write(packet);
|
|
}
|
|
packet.clear();
|
|
|
|
// Switch to sending.
|
|
timer = 1;
|
|
}
|
|
|
|
// We are sending for a long time but not receiving.
|
|
if (timer > 20000) {
|
|
throw new IllegalStateException("Timed out");
|
|
}
|
|
}
|
|
}
|
|
} catch (InterruptedException e) {
|
|
throw e;
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Got " + e.toString());
|
|
} finally {
|
|
try {
|
|
tunnel.close();
|
|
} catch (Exception e) {
|
|
// ignore
|
|
}
|
|
}
|
|
return connected;
|
|
}
|
|
|
|
private void handshake(DatagramChannel tunnel) throws Exception {
|
|
// To build a secured tunnel, we should perform mutual authentication
|
|
// and exchange session keys for encryption. To keep things simple in
|
|
// this demo, we just send the shared secret in plaintext and wait
|
|
// for the server to send the parameters.
|
|
|
|
// Allocate the buffer for handshaking.
|
|
ByteBuffer packet = ByteBuffer.allocate(1024);
|
|
|
|
// Control messages always start with zero.
|
|
packet.put((byte) 0).put(mSharedSecret).flip();
|
|
|
|
// Send the secret several times in case of packet loss.
|
|
for (int i = 0; i < 3; ++i) {
|
|
packet.position(0);
|
|
tunnel.write(packet);
|
|
}
|
|
packet.clear();
|
|
|
|
// Wait for the parameters within a limited time.
|
|
for (int i = 0; i < 50; ++i) {
|
|
Thread.sleep(100);
|
|
|
|
// Normally we should not receive random packets.
|
|
int length = tunnel.read(packet);
|
|
if (length > 0 && packet.get(0) == 0) {
|
|
configure(new String(packet.array(), 1, length - 1).trim());
|
|
return;
|
|
}
|
|
}
|
|
throw new IllegalStateException("Timed out");
|
|
}
|
|
|
|
private void configure(String parameters) throws Exception {
|
|
// If the old interface has exactly the same parameters, use it!
|
|
if (mInterface != null && parameters.equals(mParameters)) {
|
|
Log.i(TAG, "Using the previous interface");
|
|
return;
|
|
}
|
|
|
|
// Configure a builder while parsing the parameters.
|
|
Builder builder = new Builder();
|
|
for (String parameter : parameters.split(" ")) {
|
|
String[] fields = parameter.split(",");
|
|
try {
|
|
switch (fields[0].charAt(0)) {
|
|
case 'm':
|
|
builder.setMtu(Short.parseShort(fields[1]));
|
|
break;
|
|
case 'a':
|
|
builder.addAddress(fields[1], Integer.parseInt(fields[2]));
|
|
break;
|
|
case 'r':
|
|
builder.addRoute(fields[1], Integer.parseInt(fields[2]));
|
|
break;
|
|
case 'd':
|
|
builder.addDnsServer(fields[1]);
|
|
break;
|
|
case 's':
|
|
builder.addSearchDomain(fields[1]);
|
|
break;
|
|
}
|
|
} catch (Exception e) {
|
|
throw new IllegalArgumentException("Bad parameter: " + parameter);
|
|
}
|
|
}
|
|
|
|
// Close the old interface since the parameters have been changed.
|
|
try {
|
|
mInterface.close();
|
|
} catch (Exception e) {
|
|
// ignore
|
|
}
|
|
|
|
// Create a new interface using the builder and save the parameters.
|
|
mInterface = builder.setSession(mServerAddress)
|
|
.setConfigureIntent(mConfigureIntent)
|
|
.establish();
|
|
mParameters = parameters;
|
|
Log.i(TAG, "New interface: " + parameters);
|
|
}
|
|
}
|