Home | History | Annotate | Download | only in toyvpn
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.android.toyvpn;
     18 
     19 import android.app.PendingIntent;
     20 import android.app.Service;
     21 import android.content.Intent;
     22 import android.net.VpnService;
     23 import android.os.Handler;
     24 import android.os.Message;
     25 import android.os.ParcelFileDescriptor;
     26 import android.util.Log;
     27 import android.widget.Toast;
     28 
     29 import java.io.FileInputStream;
     30 import java.io.FileOutputStream;
     31 import java.net.InetSocketAddress;
     32 import java.nio.ByteBuffer;
     33 import java.nio.channels.DatagramChannel;
     34 
     35 public class ToyVpnService extends VpnService implements Handler.Callback, Runnable {
     36     private static final String TAG = "ToyVpnService";
     37 
     38     private String mServerAddress;
     39     private String mServerPort;
     40     private byte[] mSharedSecret;
     41     private PendingIntent mConfigureIntent;
     42 
     43     private Handler mHandler;
     44     private Thread mThread;
     45 
     46     private ParcelFileDescriptor mInterface;
     47     private String mParameters;
     48 
     49     @Override
     50     public int onStartCommand(Intent intent, int flags, int startId) {
     51         // The handler is only used to show messages.
     52         if (mHandler == null) {
     53             mHandler = new Handler(this);
     54         }
     55 
     56         // Stop the previous session by interrupting the thread.
     57         if (mThread != null) {
     58             mThread.interrupt();
     59         }
     60 
     61         // Extract information from the intent.
     62         String prefix = getPackageName();
     63         mServerAddress = intent.getStringExtra(prefix + ".ADDRESS");
     64         mServerPort = intent.getStringExtra(prefix + ".PORT");
     65         mSharedSecret = intent.getStringExtra(prefix + ".SECRET").getBytes();
     66 
     67         // Start a new session by creating a new thread.
     68         mThread = new Thread(this, "ToyVpnThread");
     69         mThread.start();
     70         return START_STICKY;
     71     }
     72 
     73     @Override
     74     public void onDestroy() {
     75         if (mThread != null) {
     76             mThread.interrupt();
     77         }
     78     }
     79 
     80     @Override
     81     public boolean handleMessage(Message message) {
     82         if (message != null) {
     83             Toast.makeText(this, message.what, Toast.LENGTH_SHORT).show();
     84         }
     85         return true;
     86     }
     87 
     88     @Override
     89     public synchronized void run() {
     90         try {
     91             Log.i(TAG, "Starting");
     92 
     93             // If anything needs to be obtained using the network, get it now.
     94             // This greatly reduces the complexity of seamless handover, which
     95             // tries to recreate the tunnel without shutting down everything.
     96             // In this demo, all we need to know is the server address.
     97             InetSocketAddress server = new InetSocketAddress(
     98                     mServerAddress, Integer.parseInt(mServerPort));
     99 
    100             // We try to create the tunnel for several times. The better way
    101             // is to work with ConnectivityManager, such as trying only when
    102             // the network is avaiable. Here we just use a counter to keep
    103             // things simple.
    104             for (int attempt = 0; attempt < 10; ++attempt) {
    105                 mHandler.sendEmptyMessage(R.string.connecting);
    106 
    107                 // Reset the counter if we were connected.
    108                 if (run(server)) {
    109                     attempt = 0;
    110                 }
    111 
    112                 // Sleep for a while. This also checks if we got interrupted.
    113                 Thread.sleep(3000);
    114             }
    115             Log.i(TAG, "Giving up");
    116         } catch (Exception e) {
    117             Log.e(TAG, "Got " + e.toString());
    118         } finally {
    119             try {
    120                 mInterface.close();
    121             } catch (Exception e) {
    122                 // ignore
    123             }
    124             mInterface = null;
    125             mParameters = null;
    126 
    127             mHandler.sendEmptyMessage(R.string.disconnected);
    128             Log.i(TAG, "Exiting");
    129         }
    130     }
    131 
    132     private boolean run(InetSocketAddress server) throws Exception {
    133         DatagramChannel tunnel = null;
    134         boolean connected = false;
    135         try {
    136             // Create a DatagramChannel as the VPN tunnel.
    137             tunnel = DatagramChannel.open();
    138 
    139             // Protect the tunnel before connecting to avoid loopback.
    140             if (!protect(tunnel.socket())) {
    141                 throw new IllegalStateException("Cannot protect the tunnel");
    142             }
    143 
    144             // Connect to the server.
    145             tunnel.connect(server);
    146 
    147             // For simplicity, we use the same thread for both reading and
    148             // writing. Here we put the tunnel into non-blocking mode.
    149             tunnel.configureBlocking(false);
    150 
    151             // Authenticate and configure the virtual network interface.
    152             handshake(tunnel);
    153 
    154             // Now we are connected. Set the flag and show the message.
    155             connected = true;
    156             mHandler.sendEmptyMessage(R.string.connected);
    157 
    158             // Packets to be sent are queued in this input stream.
    159             FileInputStream in = new FileInputStream(mInterface.getFileDescriptor());
    160 
    161             // Packets received need to be written to this output stream.
    162             FileOutputStream out = new FileOutputStream(mInterface.getFileDescriptor());
    163 
    164             // Allocate the buffer for a single packet.
    165             ByteBuffer packet = ByteBuffer.allocate(32767);
    166 
    167             // We use a timer to determine the status of the tunnel. It
    168             // works on both sides. A positive value means sending, and
    169             // any other means receiving. We start with receiving.
    170             int timer = 0;
    171 
    172             // We keep forwarding packets till something goes wrong.
    173             while (true) {
    174                 // Assume that we did not make any progress in this iteration.
    175                 boolean idle = true;
    176 
    177                 // Read the outgoing packet from the input stream.
    178                 int length = in.read(packet.array());
    179                 if (length > 0) {
    180                     // Write the outgoing packet to the tunnel.
    181                     packet.limit(length);
    182                     tunnel.write(packet);
    183                     packet.clear();
    184 
    185                     // There might be more outgoing packets.
    186                     idle = false;
    187 
    188                     // If we were receiving, switch to sending.
    189                     if (timer < 1) {
    190                         timer = 1;
    191                     }
    192                 }
    193 
    194                 // Read the incoming packet from the tunnel.
    195                 length = tunnel.read(packet);
    196                 if (length > 0) {
    197                     // Ignore control messages, which start with zero.
    198                     if (packet.get(0) != 0) {
    199                         // Write the incoming packet to the output stream.
    200                         out.write(packet.array(), 0, length);
    201                     }
    202                     packet.clear();
    203 
    204                     // There might be more incoming packets.
    205                     idle = false;
    206 
    207                     // If we were sending, switch to receiving.
    208                     if (timer > 0) {
    209                         timer = 0;
    210                     }
    211                 }
    212 
    213                 // If we are idle or waiting for the network, sleep for a
    214                 // fraction of time to avoid busy looping.
    215                 if (idle) {
    216                     Thread.sleep(100);
    217 
    218                     // Increase the timer. This is inaccurate but good enough,
    219                     // since everything is operated in non-blocking mode.
    220                     timer += (timer > 0) ? 100 : -100;
    221 
    222                     // We are receiving for a long time but not sending.
    223                     if (timer < -15000) {
    224                         // Send empty control messages.
    225                         packet.put((byte) 0).limit(1);
    226                         for (int i = 0; i < 3; ++i) {
    227                             packet.position(0);
    228                             tunnel.write(packet);
    229                         }
    230                         packet.clear();
    231 
    232                         // Switch to sending.
    233                         timer = 1;
    234                     }
    235 
    236                     // We are sending for a long time but not receiving.
    237                     if (timer > 20000) {
    238                         throw new IllegalStateException("Timed out");
    239                     }
    240                 }
    241             }
    242         } catch (InterruptedException e) {
    243             throw e;
    244         } catch (Exception e) {
    245             Log.e(TAG, "Got " + e.toString());
    246         } finally {
    247             try {
    248                 tunnel.close();
    249             } catch (Exception e) {
    250                 // ignore
    251             }
    252         }
    253         return connected;
    254     }
    255 
    256     private void handshake(DatagramChannel tunnel) throws Exception {
    257         // To build a secured tunnel, we should perform mutual authentication
    258         // and exchange session keys for encryption. To keep things simple in
    259         // this demo, we just send the shared secret in plaintext and wait
    260         // for the server to send the parameters.
    261 
    262         // Allocate the buffer for handshaking.
    263         ByteBuffer packet = ByteBuffer.allocate(1024);
    264 
    265         // Control messages always start with zero.
    266         packet.put((byte) 0).put(mSharedSecret).flip();
    267 
    268         // Send the secret several times in case of packet loss.
    269         for (int i = 0; i < 3; ++i) {
    270             packet.position(0);
    271             tunnel.write(packet);
    272         }
    273         packet.clear();
    274 
    275         // Wait for the parameters within a limited time.
    276         for (int i = 0; i < 50; ++i) {
    277             Thread.sleep(100);
    278 
    279             // Normally we should not receive random packets.
    280             int length = tunnel.read(packet);
    281             if (length > 0 && packet.get(0) == 0) {
    282                 configure(new String(packet.array(), 1, length - 1).trim());
    283                 return;
    284             }
    285         }
    286         throw new IllegalStateException("Timed out");
    287     }
    288 
    289     private void configure(String parameters) throws Exception {
    290         // If the old interface has exactly the same parameters, use it!
    291         if (mInterface != null && parameters.equals(mParameters)) {
    292             Log.i(TAG, "Using the previous interface");
    293             return;
    294         }
    295 
    296         // Configure a builder while parsing the parameters.
    297         Builder builder = new Builder();
    298         for (String parameter : parameters.split(" ")) {
    299             String[] fields = parameter.split(",");
    300             try {
    301                 switch (fields[0].charAt(0)) {
    302                     case 'm':
    303                         builder.setMtu(Short.parseShort(fields[1]));
    304                         break;
    305                     case 'a':
    306                         builder.addAddress(fields[1], Integer.parseInt(fields[2]));
    307                         break;
    308                     case 'r':
    309                         builder.addRoute(fields[1], Integer.parseInt(fields[2]));
    310                         break;
    311                     case 'd':
    312                         builder.addDnsServer(fields[1]);
    313                         break;
    314                     case 's':
    315                         builder.addSearchDomain(fields[1]);
    316                         break;
    317                 }
    318             } catch (Exception e) {
    319                 throw new IllegalArgumentException("Bad parameter: " + parameter);
    320             }
    321         }
    322 
    323         // Close the old interface since the parameters have been changed.
    324         try {
    325             mInterface.close();
    326         } catch (Exception e) {
    327             // ignore
    328         }
    329 
    330         // Create a new interface using the builder and save the parameters.
    331         mInterface = builder.setSession(mServerAddress)
    332                 .setConfigureIntent(mConfigureIntent)
    333                 .establish();
    334         mParameters = parameters;
    335         Log.i(TAG, "New interface: " + parameters);
    336     }
    337 }
    338