1 /* 2 * Copyright (C) 2017 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 static java.nio.charset.StandardCharsets.US_ASCII; 20 21 import android.app.PendingIntent; 22 import android.net.VpnService; 23 import android.os.ParcelFileDescriptor; 24 import android.util.Log; 25 26 import java.io.FileInputStream; 27 import java.io.FileOutputStream; 28 import java.io.IOException; 29 import java.net.InetSocketAddress; 30 import java.net.SocketAddress; 31 import java.net.SocketException; 32 import java.nio.ByteBuffer; 33 import java.nio.channels.DatagramChannel; 34 import java.util.concurrent.TimeUnit; 35 36 public class ToyVpnConnection implements Runnable { 37 /** 38 * Callback interface to let the {@link ToyVpnService} know about new connections 39 * and update the foreground notification with connection status. 40 */ 41 public interface OnEstablishListener { onEstablish(ParcelFileDescriptor tunInterface)42 void onEstablish(ParcelFileDescriptor tunInterface); 43 } 44 45 /** Maximum packet size is constrained by the MTU, which is given as a signed short. */ 46 private static final int MAX_PACKET_SIZE = Short.MAX_VALUE; 47 48 /** Time to wait in between losing the connection and retrying. */ 49 private static final long RECONNECT_WAIT_MS = TimeUnit.SECONDS.toMillis(3); 50 51 /** Time between keepalives if there is no traffic at the moment. 52 * 53 * TODO: don't do this; it's much better to let the connection die and then reconnect when 54 * necessary instead of keeping the network hardware up for hours on end in between. 55 **/ 56 private static final long KEEPALIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(15); 57 58 /** Time to wait without receiving any response before assuming the server is gone. */ 59 private static final long RECEIVE_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(20); 60 61 /** 62 * Time between polling the VPN interface for new traffic, since it's non-blocking. 63 * 64 * TODO: really don't do this; a blocking read on another thread is much cleaner. 65 */ 66 private static final long IDLE_INTERVAL_MS = TimeUnit.MILLISECONDS.toMillis(100); 67 68 /** 69 * Number of periods of length {@IDLE_INTERVAL_MS} to wait before declaring the handshake a 70 * complete and abject failure. 71 * 72 * TODO: use a higher-level protocol; hand-rolling is a fun but pointless exercise. 73 */ 74 private static final int MAX_HANDSHAKE_ATTEMPTS = 50; 75 76 private final VpnService mService; 77 private final int mConnectionId; 78 79 private final String mServerName; 80 private final int mServerPort; 81 private final byte[] mSharedSecret; 82 83 private PendingIntent mConfigureIntent; 84 private OnEstablishListener mOnEstablishListener; 85 ToyVpnConnection(final VpnService service, final int connectionId, final String serverName, final int serverPort, final byte[] sharedSecret)86 public ToyVpnConnection(final VpnService service, final int connectionId, 87 final String serverName, final int serverPort, final byte[] sharedSecret) { 88 mService = service; 89 mConnectionId = connectionId; 90 91 mServerName = serverName; 92 mServerPort= serverPort; 93 mSharedSecret = sharedSecret; 94 } 95 96 /** 97 * Optionally, set an intent to configure the VPN. This is {@code null} by default. 98 */ setConfigureIntent(PendingIntent intent)99 public void setConfigureIntent(PendingIntent intent) { 100 mConfigureIntent = intent; 101 } 102 setOnEstablishListener(OnEstablishListener listener)103 public void setOnEstablishListener(OnEstablishListener listener) { 104 mOnEstablishListener = listener; 105 } 106 107 @Override run()108 public void run() { 109 try { 110 Log.i(getTag(), "Starting"); 111 112 // If anything needs to be obtained using the network, get it now. 113 // This greatly reduces the complexity of seamless handover, which 114 // tries to recreate the tunnel without shutting down everything. 115 // In this demo, all we need to know is the server address. 116 final SocketAddress serverAddress = new InetSocketAddress(mServerName, mServerPort); 117 118 // We try to create the tunnel several times. 119 // TODO: The better way is to work with ConnectivityManager, trying only when the 120 // network is available. 121 // Here we just use a counter to keep things simple. 122 for (int attempt = 0; attempt < 10; ++attempt) { 123 // Reset the counter if we were connected. 124 if (run(serverAddress)) { 125 attempt = 0; 126 } 127 128 // Sleep for a while. This also checks if we got interrupted. 129 Thread.sleep(3000); 130 } 131 Log.i(getTag(), "Giving up"); 132 } catch (IOException | InterruptedException | IllegalArgumentException e) { 133 Log.e(getTag(), "Connection failed, exiting", e); 134 } 135 } 136 run(SocketAddress server)137 private boolean run(SocketAddress server) 138 throws IOException, InterruptedException, IllegalArgumentException { 139 ParcelFileDescriptor iface = null; 140 boolean connected = false; 141 // Create a DatagramChannel as the VPN tunnel. 142 try (DatagramChannel tunnel = DatagramChannel.open()) { 143 144 // Protect the tunnel before connecting to avoid loopback. 145 if (!mService.protect(tunnel.socket())) { 146 throw new IllegalStateException("Cannot protect the tunnel"); 147 } 148 149 // Connect to the server. 150 tunnel.connect(server); 151 152 // For simplicity, we use the same thread for both reading and 153 // writing. Here we put the tunnel into non-blocking mode. 154 tunnel.configureBlocking(false); 155 156 // Authenticate and configure the virtual network interface. 157 iface = handshake(tunnel); 158 159 // Now we are connected. Set the flag. 160 connected = true; 161 162 // Packets to be sent are queued in this input stream. 163 FileInputStream in = new FileInputStream(iface.getFileDescriptor()); 164 165 // Packets received need to be written to this output stream. 166 FileOutputStream out = new FileOutputStream(iface.getFileDescriptor()); 167 168 // Allocate the buffer for a single packet. 169 ByteBuffer packet = ByteBuffer.allocate(MAX_PACKET_SIZE); 170 171 // Timeouts: 172 // - when data has not been sent in a while, send empty keepalive messages. 173 // - when data has not been received in a while, assume the connection is broken. 174 long lastSendTime = System.currentTimeMillis(); 175 long lastReceiveTime = System.currentTimeMillis(); 176 177 // We keep forwarding packets till something goes wrong. 178 while (true) { 179 // Assume that we did not make any progress in this iteration. 180 boolean idle = true; 181 182 // Read the outgoing packet from the input stream. 183 int length = in.read(packet.array()); 184 if (length > 0) { 185 // Write the outgoing packet to the tunnel. 186 packet.limit(length); 187 tunnel.write(packet); 188 packet.clear(); 189 190 // There might be more outgoing packets. 191 idle = false; 192 lastReceiveTime = System.currentTimeMillis(); 193 } 194 195 // Read the incoming packet from the tunnel. 196 length = tunnel.read(packet); 197 if (length > 0) { 198 // Ignore control messages, which start with zero. 199 if (packet.get(0) != 0) { 200 // Write the incoming packet to the output stream. 201 out.write(packet.array(), 0, length); 202 } 203 packet.clear(); 204 205 // There might be more incoming packets. 206 idle = false; 207 lastSendTime = System.currentTimeMillis(); 208 } 209 210 // If we are idle or waiting for the network, sleep for a 211 // fraction of time to avoid busy looping. 212 if (idle) { 213 Thread.sleep(IDLE_INTERVAL_MS); 214 final long timeNow = System.currentTimeMillis(); 215 216 if (lastSendTime + KEEPALIVE_INTERVAL_MS <= timeNow) { 217 // We are receiving for a long time but not sending. 218 // Send empty control messages. 219 packet.put((byte) 0).limit(1); 220 for (int i = 0; i < 3; ++i) { 221 packet.position(0); 222 tunnel.write(packet); 223 } 224 packet.clear(); 225 lastSendTime = timeNow; 226 } else if (lastReceiveTime + RECEIVE_TIMEOUT_MS <= timeNow) { 227 // We are sending for a long time but not receiving. 228 throw new IllegalStateException("Timed out"); 229 } 230 } 231 } 232 } catch (SocketException e) { 233 Log.e(getTag(), "Cannot use socket", e); 234 } finally { 235 if (iface != null) { 236 try { 237 iface.close(); 238 } catch (IOException e) { 239 Log.e(getTag(), "Unable to close interface", e); 240 } 241 } 242 } 243 return connected; 244 } 245 handshake(DatagramChannel tunnel)246 private ParcelFileDescriptor handshake(DatagramChannel tunnel) 247 throws IOException, InterruptedException { 248 // To build a secured tunnel, we should perform mutual authentication 249 // and exchange session keys for encryption. To keep things simple in 250 // this demo, we just send the shared secret in plaintext and wait 251 // for the server to send the parameters. 252 253 // Allocate the buffer for handshaking. We have a hardcoded maximum 254 // handshake size of 1024 bytes, which should be enough for demo 255 // purposes. 256 ByteBuffer packet = ByteBuffer.allocate(1024); 257 258 // Control messages always start with zero. 259 packet.put((byte) 0).put(mSharedSecret).flip(); 260 261 // Send the secret several times in case of packet loss. 262 for (int i = 0; i < 3; ++i) { 263 packet.position(0); 264 tunnel.write(packet); 265 } 266 packet.clear(); 267 268 // Wait for the parameters within a limited time. 269 for (int i = 0; i < MAX_HANDSHAKE_ATTEMPTS; ++i) { 270 Thread.sleep(IDLE_INTERVAL_MS); 271 272 // Normally we should not receive random packets. Check that the first 273 // byte is 0 as expected. 274 int length = tunnel.read(packet); 275 if (length > 0 && packet.get(0) == 0) { 276 return configure(new String(packet.array(), 1, length - 1, US_ASCII).trim()); 277 } 278 } 279 throw new IOException("Timed out"); 280 } 281 configure(String parameters)282 private ParcelFileDescriptor configure(String parameters) throws IllegalArgumentException { 283 // Configure a builder while parsing the parameters. 284 VpnService.Builder builder = mService.new Builder(); 285 for (String parameter : parameters.split(" ")) { 286 String[] fields = parameter.split(","); 287 try { 288 switch (fields[0].charAt(0)) { 289 case 'm': 290 builder.setMtu(Short.parseShort(fields[1])); 291 break; 292 case 'a': 293 builder.addAddress(fields[1], Integer.parseInt(fields[2])); 294 break; 295 case 'r': 296 builder.addRoute(fields[1], Integer.parseInt(fields[2])); 297 break; 298 case 'd': 299 builder.addDnsServer(fields[1]); 300 break; 301 case 's': 302 builder.addSearchDomain(fields[1]); 303 break; 304 } 305 } catch (NumberFormatException e) { 306 throw new IllegalArgumentException("Bad parameter: " + parameter); 307 } 308 } 309 310 // Create a new interface using the builder and save the parameters. 311 final ParcelFileDescriptor vpnInterface; 312 synchronized (mService) { 313 vpnInterface = builder 314 .setSession(mServerName) 315 .setConfigureIntent(mConfigureIntent) 316 .establish(); 317 if (mOnEstablishListener != null) { 318 mOnEstablishListener.onEstablish(vpnInterface); 319 } 320 } 321 Log.i(getTag(), "New interface: " + vpnInterface + " (" + parameters + ")"); 322 return vpnInterface; 323 } 324 getTag()325 private final String getTag() { 326 return ToyVpnConnection.class.getSimpleName() + "[" + mConnectionId + "]"; 327 } 328 } 329