• 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.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