• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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