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