/* * Copyright (C) 2017 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 static java.nio.charset.StandardCharsets.US_ASCII; import android.app.PendingIntent; import android.content.pm.PackageManager; import android.net.ProxyInfo; import android.net.VpnService; import android.os.ParcelFileDescriptor; import android.text.TextUtils; import android.util.Log; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.SocketException; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import java.util.Set; import java.util.concurrent.TimeUnit; public class ToyVpnConnection implements Runnable { /** * Callback interface to let the {@link ToyVpnService} know about new connections * and update the foreground notification with connection status. */ public interface OnEstablishListener { void onEstablish(ParcelFileDescriptor tunInterface); } /** Maximum packet size is constrained by the MTU, which is given as a signed short. */ private static final int MAX_PACKET_SIZE = Short.MAX_VALUE; /** Time to wait in between losing the connection and retrying. */ private static final long RECONNECT_WAIT_MS = TimeUnit.SECONDS.toMillis(3); /** Time between keepalives if there is no traffic at the moment. * * TODO: don't do this; it's much better to let the connection die and then reconnect when * necessary instead of keeping the network hardware up for hours on end in between. **/ private static final long KEEPALIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(15); /** Time to wait without receiving any response before assuming the server is gone. */ private static final long RECEIVE_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(20); /** * Time between polling the VPN interface for new traffic, since it's non-blocking. * * TODO: really don't do this; a blocking read on another thread is much cleaner. */ private static final long IDLE_INTERVAL_MS = TimeUnit.MILLISECONDS.toMillis(100); /** * Number of periods of length {@IDLE_INTERVAL_MS} to wait before declaring the handshake a * complete and abject failure. * * TODO: use a higher-level protocol; hand-rolling is a fun but pointless exercise. */ private static final int MAX_HANDSHAKE_ATTEMPTS = 50; private final VpnService mService; private final int mConnectionId; private final String mServerName; private final int mServerPort; private final byte[] mSharedSecret; private PendingIntent mConfigureIntent; private OnEstablishListener mOnEstablishListener; // Proxy settings private String mProxyHostName; private int mProxyHostPort; // Allowed/Disallowed packages for VPN usage private final boolean mAllow; private final Set mPackages; public 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 packages) { mService = service; mConnectionId = connectionId; mServerName = serverName; mServerPort= serverPort; mSharedSecret = sharedSecret; if (!TextUtils.isEmpty(proxyHostName)) { mProxyHostName = proxyHostName; } if (proxyHostPort > 0) { // The port value is always an integer due to the configured inputType. mProxyHostPort = proxyHostPort; } mAllow = allow; mPackages = packages; } /** * Optionally, set an intent to configure the VPN. This is {@code null} by default. */ public void setConfigureIntent(PendingIntent intent) { mConfigureIntent = intent; } public void setOnEstablishListener(OnEstablishListener listener) { mOnEstablishListener = listener; } @Override public void run() { try { Log.i(getTag(), "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. final SocketAddress serverAddress = new InetSocketAddress(mServerName, mServerPort); // We try to create the tunnel several times. // TODO: The better way is to work with ConnectivityManager, trying only when the // network is available. // Here we just use a counter to keep things simple. for (int attempt = 0; attempt < 10; ++attempt) { // Reset the counter if we were connected. if (run(serverAddress)) { attempt = 0; } // Sleep for a while. This also checks if we got interrupted. Thread.sleep(3000); } Log.i(getTag(), "Giving up"); } catch (IOException | InterruptedException | IllegalArgumentException e) { Log.e(getTag(), "Connection failed, exiting", e); } } private boolean run(SocketAddress server) throws IOException, InterruptedException, IllegalArgumentException { ParcelFileDescriptor iface = null; boolean connected = false; // Create a DatagramChannel as the VPN tunnel. try (DatagramChannel tunnel = DatagramChannel.open()) { // Protect the tunnel before connecting to avoid loopback. if (!mService.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. iface = handshake(tunnel); // Now we are connected. Set the flag. connected = true; // Packets to be sent are queued in this input stream. FileInputStream in = new FileInputStream(iface.getFileDescriptor()); // Packets received need to be written to this output stream. FileOutputStream out = new FileOutputStream(iface.getFileDescriptor()); // Allocate the buffer for a single packet. ByteBuffer packet = ByteBuffer.allocate(MAX_PACKET_SIZE); // Timeouts: // - when data has not been sent in a while, send empty keepalive messages. // - when data has not been received in a while, assume the connection is broken. long lastSendTime = System.currentTimeMillis(); long lastReceiveTime = System.currentTimeMillis(); // 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; lastReceiveTime = System.currentTimeMillis(); } // 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; lastSendTime = System.currentTimeMillis(); } // If we are idle or waiting for the network, sleep for a // fraction of time to avoid busy looping. if (idle) { Thread.sleep(IDLE_INTERVAL_MS); final long timeNow = System.currentTimeMillis(); if (lastSendTime + KEEPALIVE_INTERVAL_MS <= timeNow) { // We are receiving for a long time but not sending. // 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(); lastSendTime = timeNow; } else if (lastReceiveTime + RECEIVE_TIMEOUT_MS <= timeNow) { // We are sending for a long time but not receiving. throw new IllegalStateException("Timed out"); } } } } catch (SocketException e) { Log.e(getTag(), "Cannot use socket", e); } finally { if (iface != null) { try { iface.close(); } catch (IOException e) { Log.e(getTag(), "Unable to close interface", e); } } } return connected; } private ParcelFileDescriptor handshake(DatagramChannel tunnel) throws IOException, InterruptedException { // 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. We have a hardcoded maximum // handshake size of 1024 bytes, which should be enough for demo // purposes. 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 < MAX_HANDSHAKE_ATTEMPTS; ++i) { Thread.sleep(IDLE_INTERVAL_MS); // Normally we should not receive random packets. Check that the first // byte is 0 as expected. int length = tunnel.read(packet); if (length > 0 && packet.get(0) == 0) { return configure(new String(packet.array(), 1, length - 1, US_ASCII).trim()); } } throw new IOException("Timed out"); } private ParcelFileDescriptor configure(String parameters) throws IllegalArgumentException { // Configure a builder while parsing the parameters. VpnService.Builder builder = mService.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 (NumberFormatException e) { throw new IllegalArgumentException("Bad parameter: " + parameter); } } // Create a new interface using the builder and save the parameters. final ParcelFileDescriptor vpnInterface; for (String packageName : mPackages) { try { if (mAllow) { builder.addAllowedApplication(packageName); } else { builder.addDisallowedApplication(packageName); } } catch (PackageManager.NameNotFoundException e){ Log.w(getTag(), "Package not available: " + packageName, e); } } builder.setSession(mServerName).setConfigureIntent(mConfigureIntent); if (!TextUtils.isEmpty(mProxyHostName)) { builder.setHttpProxy(ProxyInfo.buildDirectProxy(mProxyHostName, mProxyHostPort)); } synchronized (mService) { vpnInterface = builder.establish(); if (mOnEstablishListener != null) { mOnEstablishListener.onEstablish(vpnInterface); } } Log.i(getTag(), "New interface: " + vpnInterface + " (" + parameters + ")"); return vpnInterface; } private final String getTag() { return ToyVpnConnection.class.getSimpleName() + "[" + mConnectionId + "]"; } }