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
17package com.example.android.toyvpn;
18
19import android.app.PendingIntent;
20import android.app.Service;
21import android.content.Intent;
22import android.net.VpnService;
23import android.os.Handler;
24import android.os.Message;
25import android.os.ParcelFileDescriptor;
26import android.util.Log;
27import android.widget.Toast;
28
29import java.io.FileInputStream;
30import java.io.FileOutputStream;
31import java.net.InetSocketAddress;
32import java.nio.ByteBuffer;
33import java.nio.channels.DatagramChannel;
34
35public 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
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
74    public void onDestroy() {
75        if (mThread != null) {
76            mThread.interrupt();
77        }
78    }
79
80    @Override
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
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
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
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
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