• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 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 android.app.PendingIntent;
20 import android.app.Service;
21 import android.content.Intent;
22 import android.net.VpnService;
23 import android.os.Handler;
24 import android.os.Message;
25 import android.os.ParcelFileDescriptor;
26 import android.util.Log;
27 import android.widget.Toast;
28 
29 import java.io.FileInputStream;
30 import java.io.FileOutputStream;
31 import java.net.InetSocketAddress;
32 import java.nio.ByteBuffer;
33 import java.nio.channels.DatagramChannel;
34 
35 public class ToyVpnService extends VpnService implements Handler.Callback, Runnable {
36     private static final String TAG = "ToyVpnService";
37 
38     private String mServerAddress;
39     private String mServerPort;
40     private byte[] mSharedSecret;
41     private PendingIntent mConfigureIntent;
42 
43     private Handler mHandler;
44     private Thread mThread;
45 
46     private ParcelFileDescriptor mInterface;
47     private String mParameters;
48 
49     @Override
onStartCommand(Intent intent, int flags, int startId)50     public int onStartCommand(Intent intent, int flags, int startId) {
51         // The handler is only used to show messages.
52         if (mHandler == null) {
53             mHandler = new Handler(this);
54         }
55 
56         // Stop the previous session by interrupting the thread.
57         if (mThread != null) {
58             mThread.interrupt();
59         }
60 
61         // Extract information from the intent.
62         String prefix = getPackageName();
63         mServerAddress = intent.getStringExtra(prefix + ".ADDRESS");
64         mServerPort = intent.getStringExtra(prefix + ".PORT");
65         mSharedSecret = intent.getStringExtra(prefix + ".SECRET").getBytes();
66 
67         // Start a new session by creating a new thread.
68         mThread = new Thread(this, "ToyVpnThread");
69         mThread.start();
70         return START_STICKY;
71     }
72 
73     @Override
onDestroy()74     public void onDestroy() {
75         if (mThread != null) {
76             mThread.interrupt();
77         }
78     }
79 
80     @Override
handleMessage(Message message)81     public boolean handleMessage(Message message) {
82         if (message != null) {
83             Toast.makeText(this, message.what, Toast.LENGTH_SHORT).show();
84         }
85         return true;
86     }
87 
88     @Override
run()89     public synchronized void run() {
90         try {
91             Log.i(TAG, "Starting");
92 
93             // If anything needs to be obtained using the network, get it now.
94             // This greatly reduces the complexity of seamless handover, which
95             // tries to recreate the tunnel without shutting down everything.
96             // In this demo, all we need to know is the server address.
97             InetSocketAddress server = new InetSocketAddress(
98                     mServerAddress, Integer.parseInt(mServerPort));
99 
100             // We try to create the tunnel for several times. The better way
101             // is to work with ConnectivityManager, such as trying only when
102             // the network is avaiable. Here we just use a counter to keep
103             // things simple.
104             for (int attempt = 0; attempt < 10; ++attempt) {
105                 mHandler.sendEmptyMessage(R.string.connecting);
106 
107                 // Reset the counter if we were connected.
108                 if (run(server)) {
109                     attempt = 0;
110                 }
111 
112                 // Sleep for a while. This also checks if we got interrupted.
113                 Thread.sleep(3000);
114             }
115             Log.i(TAG, "Giving up");
116         } catch (Exception e) {
117             Log.e(TAG, "Got " + e.toString());
118         } finally {
119             try {
120                 mInterface.close();
121             } catch (Exception e) {
122                 // ignore
123             }
124             mInterface = null;
125             mParameters = null;
126 
127             mHandler.sendEmptyMessage(R.string.disconnected);
128             Log.i(TAG, "Exiting");
129         }
130     }
131 
run(InetSocketAddress server)132     private boolean run(InetSocketAddress server) throws Exception {
133         DatagramChannel tunnel = null;
134         boolean connected = false;
135         try {
136             // Create a DatagramChannel as the VPN tunnel.
137             tunnel = DatagramChannel.open();
138 
139             // Protect the tunnel before connecting to avoid loopback.
140             if (!protect(tunnel.socket())) {
141                 throw new IllegalStateException("Cannot protect the tunnel");
142             }
143 
144             // Connect to the server.
145             tunnel.connect(server);
146 
147             // For simplicity, we use the same thread for both reading and
148             // writing. Here we put the tunnel into non-blocking mode.
149             tunnel.configureBlocking(false);
150 
151             // Authenticate and configure the virtual network interface.
152             handshake(tunnel);
153 
154             // Now we are connected. Set the flag and show the message.
155             connected = true;
156             mHandler.sendEmptyMessage(R.string.connected);
157 
158             // Packets to be sent are queued in this input stream.
159             FileInputStream in = new FileInputStream(mInterface.getFileDescriptor());
160 
161             // Packets received need to be written to this output stream.
162             FileOutputStream out = new FileOutputStream(mInterface.getFileDescriptor());
163 
164             // Allocate the buffer for a single packet.
165             ByteBuffer packet = ByteBuffer.allocate(32767);
166 
167             // We use a timer to determine the status of the tunnel. It
168             // works on both sides. A positive value means sending, and
169             // any other means receiving. We start with receiving.
170             int timer = 0;
171 
172             // We keep forwarding packets till something goes wrong.
173             while (true) {
174                 // Assume that we did not make any progress in this iteration.
175                 boolean idle = true;
176 
177                 // Read the outgoing packet from the input stream.
178                 int length = in.read(packet.array());
179                 if (length > 0) {
180                     // Write the outgoing packet to the tunnel.
181                     packet.limit(length);
182                     tunnel.write(packet);
183                     packet.clear();
184 
185                     // There might be more outgoing packets.
186                     idle = false;
187 
188                     // If we were receiving, switch to sending.
189                     if (timer < 1) {
190                         timer = 1;
191                     }
192                 }
193 
194                 // Read the incoming packet from the tunnel.
195                 length = tunnel.read(packet);
196                 if (length > 0) {
197                     // Ignore control messages, which start with zero.
198                     if (packet.get(0) != 0) {
199                         // Write the incoming packet to the output stream.
200                         out.write(packet.array(), 0, length);
201                     }
202                     packet.clear();
203 
204                     // There might be more incoming packets.
205                     idle = false;
206 
207                     // If we were sending, switch to receiving.
208                     if (timer > 0) {
209                         timer = 0;
210                     }
211                 }
212 
213                 // If we are idle or waiting for the network, sleep for a
214                 // fraction of time to avoid busy looping.
215                 if (idle) {
216                     Thread.sleep(100);
217 
218                     // Increase the timer. This is inaccurate but good enough,
219                     // since everything is operated in non-blocking mode.
220                     timer += (timer > 0) ? 100 : -100;
221 
222                     // We are receiving for a long time but not sending.
223                     if (timer < -15000) {
224                         // Send empty control messages.
225                         packet.put((byte) 0).limit(1);
226                         for (int i = 0; i < 3; ++i) {
227                             packet.position(0);
228                             tunnel.write(packet);
229                         }
230                         packet.clear();
231 
232                         // Switch to sending.
233                         timer = 1;
234                     }
235 
236                     // We are sending for a long time but not receiving.
237                     if (timer > 20000) {
238                         throw new IllegalStateException("Timed out");
239                     }
240                 }
241             }
242         } catch (InterruptedException e) {
243             throw e;
244         } catch (Exception e) {
245             Log.e(TAG, "Got " + e.toString());
246         } finally {
247             try {
248                 tunnel.close();
249             } catch (Exception e) {
250                 // ignore
251             }
252         }
253         return connected;
254     }
255 
handshake(DatagramChannel tunnel)256     private void handshake(DatagramChannel tunnel) throws Exception {
257         // To build a secured tunnel, we should perform mutual authentication
258         // and exchange session keys for encryption. To keep things simple in
259         // this demo, we just send the shared secret in plaintext and wait
260         // for the server to send the parameters.
261 
262         // Allocate the buffer for handshaking.
263         ByteBuffer packet = ByteBuffer.allocate(1024);
264 
265         // Control messages always start with zero.
266         packet.put((byte) 0).put(mSharedSecret).flip();
267 
268         // Send the secret several times in case of packet loss.
269         for (int i = 0; i < 3; ++i) {
270             packet.position(0);
271             tunnel.write(packet);
272         }
273         packet.clear();
274 
275         // Wait for the parameters within a limited time.
276         for (int i = 0; i < 50; ++i) {
277             Thread.sleep(100);
278 
279             // Normally we should not receive random packets.
280             int length = tunnel.read(packet);
281             if (length > 0 && packet.get(0) == 0) {
282                 configure(new String(packet.array(), 1, length - 1).trim());
283                 return;
284             }
285         }
286         throw new IllegalStateException("Timed out");
287     }
288 
configure(String parameters)289     private void configure(String parameters) throws Exception {
290         // If the old interface has exactly the same parameters, use it!
291         if (mInterface != null && parameters.equals(mParameters)) {
292             Log.i(TAG, "Using the previous interface");
293             return;
294         }
295 
296         // Configure a builder while parsing the parameters.
297         Builder builder = new Builder();
298         for (String parameter : parameters.split(" ")) {
299             String[] fields = parameter.split(",");
300             try {
301                 switch (fields[0].charAt(0)) {
302                     case 'm':
303                         builder.setMtu(Short.parseShort(fields[1]));
304                         break;
305                     case 'a':
306                         builder.addAddress(fields[1], Integer.parseInt(fields[2]));
307                         break;
308                     case 'r':
309                         builder.addRoute(fields[1], Integer.parseInt(fields[2]));
310                         break;
311                     case 'd':
312                         builder.addDnsServer(fields[1]);
313                         break;
314                     case 's':
315                         builder.addSearchDomain(fields[1]);
316                         break;
317                 }
318             } catch (Exception e) {
319                 throw new IllegalArgumentException("Bad parameter: " + parameter);
320             }
321         }
322 
323         // Close the old interface since the parameters have been changed.
324         try {
325             mInterface.close();
326         } catch (Exception e) {
327             // ignore
328         }
329 
330         // Create a new interface using the builder and save the parameters.
331         mInterface = builder.setSession(mServerAddress)
332                 .setConfigureIntent(mConfigureIntent)
333                 .establish();
334         mParameters = parameters;
335         Log.i(TAG, "New interface: " + parameters);
336     }
337 }
338