Home | History | Annotate | Download | only in toyvpn
      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 {
     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 
     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      */
     99     public void setConfigureIntent(PendingIntent intent) {
    100         mConfigureIntent = intent;
    101     }
    102 
    103     public void setOnEstablishListener(OnEstablishListener listener) {
    104         mOnEstablishListener = listener;
    105     }
    106 
    107     @Override
    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 
    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 
    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 
    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 
    325     private final String getTag() {
    326         return ToyVpnConnection.class.getSimpleName() + "[" + mConnectionId + "]";
    327     }
    328 }
    329