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