| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.example.android.toyvpn; |
| |
| import static java.nio.charset.StandardCharsets.US_ASCII; |
| |
| import android.app.PendingIntent; |
| import android.content.pm.PackageManager; |
| import android.net.ProxyInfo; |
| import android.net.VpnService; |
| import android.os.ParcelFileDescriptor; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.net.InetSocketAddress; |
| import java.net.SocketAddress; |
| import java.net.SocketException; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.DatagramChannel; |
| import java.util.Set; |
| import java.util.concurrent.TimeUnit; |
| |
| public class ToyVpnConnection implements Runnable { |
| /** |
| * Callback interface to let the {@link ToyVpnService} know about new connections |
| * and update the foreground notification with connection status. |
| */ |
| public interface OnEstablishListener { |
| void onEstablish(ParcelFileDescriptor tunInterface); |
| } |
| |
| /** Maximum packet size is constrained by the MTU, which is given as a signed short. */ |
| private static final int MAX_PACKET_SIZE = Short.MAX_VALUE; |
| |
| /** Time to wait in between losing the connection and retrying. */ |
| private static final long RECONNECT_WAIT_MS = TimeUnit.SECONDS.toMillis(3); |
| |
| /** Time between keepalives if there is no traffic at the moment. |
| * |
| * TODO: don't do this; it's much better to let the connection die and then reconnect when |
| * necessary instead of keeping the network hardware up for hours on end in between. |
| **/ |
| private static final long KEEPALIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(15); |
| |
| /** Time to wait without receiving any response before assuming the server is gone. */ |
| private static final long RECEIVE_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(20); |
| |
| /** |
| * Time between polling the VPN interface for new traffic, since it's non-blocking. |
| * |
| * TODO: really don't do this; a blocking read on another thread is much cleaner. |
| */ |
| private static final long IDLE_INTERVAL_MS = TimeUnit.MILLISECONDS.toMillis(100); |
| |
| /** |
| * Number of periods of length {@IDLE_INTERVAL_MS} to wait before declaring the handshake a |
| * complete and abject failure. |
| * |
| * TODO: use a higher-level protocol; hand-rolling is a fun but pointless exercise. |
| */ |
| private static final int MAX_HANDSHAKE_ATTEMPTS = 50; |
| |
| private final VpnService mService; |
| private final int mConnectionId; |
| |
| private final String mServerName; |
| private final int mServerPort; |
| private final byte[] mSharedSecret; |
| |
| private PendingIntent mConfigureIntent; |
| private OnEstablishListener mOnEstablishListener; |
| |
| // Proxy settings |
| private String mProxyHostName; |
| private int mProxyHostPort; |
| |
| // Allowed/Disallowed packages for VPN usage |
| private final boolean mAllow; |
| private final Set<String> mPackages; |
| |
| public 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) { |
| mService = service; |
| mConnectionId = connectionId; |
| |
| mServerName = serverName; |
| mServerPort= serverPort; |
| mSharedSecret = sharedSecret; |
| |
| if (!TextUtils.isEmpty(proxyHostName)) { |
| mProxyHostName = proxyHostName; |
| } |
| if (proxyHostPort > 0) { |
| // The port value is always an integer due to the configured inputType. |
| mProxyHostPort = proxyHostPort; |
| } |
| mAllow = allow; |
| mPackages = packages; |
| } |
| |
| /** |
| * Optionally, set an intent to configure the VPN. This is {@code null} by default. |
| */ |
| public void setConfigureIntent(PendingIntent intent) { |
| mConfigureIntent = intent; |
| } |
| |
| public void setOnEstablishListener(OnEstablishListener listener) { |
| mOnEstablishListener = listener; |
| } |
| |
| @Override |
| public void run() { |
| try { |
| Log.i(getTag(), "Starting"); |
| |
| // If anything needs to be obtained using the network, get it now. |
| // This greatly reduces the complexity of seamless handover, which |
| // tries to recreate the tunnel without shutting down everything. |
| // In this demo, all we need to know is the server address. |
| final SocketAddress serverAddress = new InetSocketAddress(mServerName, mServerPort); |
| |
| // We try to create the tunnel several times. |
| // TODO: The better way is to work with ConnectivityManager, trying only when the |
| // network is available. |
| // Here we just use a counter to keep things simple. |
| for (int attempt = 0; attempt < 10; ++attempt) { |
| // Reset the counter if we were connected. |
| if (run(serverAddress)) { |
| attempt = 0; |
| } |
| |
| // Sleep for a while. This also checks if we got interrupted. |
| Thread.sleep(3000); |
| } |
| Log.i(getTag(), "Giving up"); |
| } catch (IOException | InterruptedException | IllegalArgumentException e) { |
| Log.e(getTag(), "Connection failed, exiting", e); |
| } |
| } |
| |
| private boolean run(SocketAddress server) |
| throws IOException, InterruptedException, IllegalArgumentException { |
| ParcelFileDescriptor iface = null; |
| boolean connected = false; |
| // Create a DatagramChannel as the VPN tunnel. |
| try (DatagramChannel tunnel = DatagramChannel.open()) { |
| |
| // Protect the tunnel before connecting to avoid loopback. |
| if (!mService.protect(tunnel.socket())) { |
| throw new IllegalStateException("Cannot protect the tunnel"); |
| } |
| |
| // Connect to the server. |
| tunnel.connect(server); |
| |
| // For simplicity, we use the same thread for both reading and |
| // writing. Here we put the tunnel into non-blocking mode. |
| tunnel.configureBlocking(false); |
| |
| // Authenticate and configure the virtual network interface. |
| iface = handshake(tunnel); |
| |
| // Now we are connected. Set the flag. |
| connected = true; |
| |
| // Packets to be sent are queued in this input stream. |
| FileInputStream in = new FileInputStream(iface.getFileDescriptor()); |
| |
| // Packets received need to be written to this output stream. |
| FileOutputStream out = new FileOutputStream(iface.getFileDescriptor()); |
| |
| // Allocate the buffer for a single packet. |
| ByteBuffer packet = ByteBuffer.allocate(MAX_PACKET_SIZE); |
| |
| // Timeouts: |
| // - when data has not been sent in a while, send empty keepalive messages. |
| // - when data has not been received in a while, assume the connection is broken. |
| long lastSendTime = System.currentTimeMillis(); |
| long lastReceiveTime = System.currentTimeMillis(); |
| |
| // We keep forwarding packets till something goes wrong. |
| while (true) { |
| // Assume that we did not make any progress in this iteration. |
| boolean idle = true; |
| |
| // Read the outgoing packet from the input stream. |
| int length = in.read(packet.array()); |
| if (length > 0) { |
| // Write the outgoing packet to the tunnel. |
| packet.limit(length); |
| tunnel.write(packet); |
| packet.clear(); |
| |
| // There might be more outgoing packets. |
| idle = false; |
| lastReceiveTime = System.currentTimeMillis(); |
| } |
| |
| // Read the incoming packet from the tunnel. |
| length = tunnel.read(packet); |
| if (length > 0) { |
| // Ignore control messages, which start with zero. |
| if (packet.get(0) != 0) { |
| // Write the incoming packet to the output stream. |
| out.write(packet.array(), 0, length); |
| } |
| packet.clear(); |
| |
| // There might be more incoming packets. |
| idle = false; |
| lastSendTime = System.currentTimeMillis(); |
| } |
| |
| // If we are idle or waiting for the network, sleep for a |
| // fraction of time to avoid busy looping. |
| if (idle) { |
| Thread.sleep(IDLE_INTERVAL_MS); |
| final long timeNow = System.currentTimeMillis(); |
| |
| if (lastSendTime + KEEPALIVE_INTERVAL_MS <= timeNow) { |
| // We are receiving for a long time but not sending. |
| // Send empty control messages. |
| packet.put((byte) 0).limit(1); |
| for (int i = 0; i < 3; ++i) { |
| packet.position(0); |
| tunnel.write(packet); |
| } |
| packet.clear(); |
| lastSendTime = timeNow; |
| } else if (lastReceiveTime + RECEIVE_TIMEOUT_MS <= timeNow) { |
| // We are sending for a long time but not receiving. |
| throw new IllegalStateException("Timed out"); |
| } |
| } |
| } |
| } catch (SocketException e) { |
| Log.e(getTag(), "Cannot use socket", e); |
| } finally { |
| if (iface != null) { |
| try { |
| iface.close(); |
| } catch (IOException e) { |
| Log.e(getTag(), "Unable to close interface", e); |
| } |
| } |
| } |
| return connected; |
| } |
| |
| private ParcelFileDescriptor handshake(DatagramChannel tunnel) |
| throws IOException, InterruptedException { |
| // To build a secured tunnel, we should perform mutual authentication |
| // and exchange session keys for encryption. To keep things simple in |
| // this demo, we just send the shared secret in plaintext and wait |
| // for the server to send the parameters. |
| |
| // Allocate the buffer for handshaking. We have a hardcoded maximum |
| // handshake size of 1024 bytes, which should be enough for demo |
| // purposes. |
| ByteBuffer packet = ByteBuffer.allocate(1024); |
| |
| // Control messages always start with zero. |
| packet.put((byte) 0).put(mSharedSecret).flip(); |
| |
| // Send the secret several times in case of packet loss. |
| for (int i = 0; i < 3; ++i) { |
| packet.position(0); |
| tunnel.write(packet); |
| } |
| packet.clear(); |
| |
| // Wait for the parameters within a limited time. |
| for (int i = 0; i < MAX_HANDSHAKE_ATTEMPTS; ++i) { |
| Thread.sleep(IDLE_INTERVAL_MS); |
| |
| // Normally we should not receive random packets. Check that the first |
| // byte is 0 as expected. |
| int length = tunnel.read(packet); |
| if (length > 0 && packet.get(0) == 0) { |
| return configure(new String(packet.array(), 1, length - 1, US_ASCII).trim()); |
| } |
| } |
| throw new IOException("Timed out"); |
| } |
| |
| private ParcelFileDescriptor configure(String parameters) throws IllegalArgumentException { |
| // Configure a builder while parsing the parameters. |
| VpnService.Builder builder = mService.new Builder(); |
| for (String parameter : parameters.split(" ")) { |
| String[] fields = parameter.split(","); |
| try { |
| switch (fields[0].charAt(0)) { |
| case 'm': |
| builder.setMtu(Short.parseShort(fields[1])); |
| break; |
| case 'a': |
| builder.addAddress(fields[1], Integer.parseInt(fields[2])); |
| break; |
| case 'r': |
| builder.addRoute(fields[1], Integer.parseInt(fields[2])); |
| break; |
| case 'd': |
| builder.addDnsServer(fields[1]); |
| break; |
| case 's': |
| builder.addSearchDomain(fields[1]); |
| break; |
| } |
| } catch (NumberFormatException e) { |
| throw new IllegalArgumentException("Bad parameter: " + parameter); |
| } |
| } |
| |
| // Create a new interface using the builder and save the parameters. |
| final ParcelFileDescriptor vpnInterface; |
| for (String packageName : mPackages) { |
| try { |
| if (mAllow) { |
| builder.addAllowedApplication(packageName); |
| } else { |
| builder.addDisallowedApplication(packageName); |
| } |
| } catch (PackageManager.NameNotFoundException e){ |
| Log.w(getTag(), "Package not available: " + packageName, e); |
| } |
| } |
| builder.setSession(mServerName).setConfigureIntent(mConfigureIntent); |
| if (!TextUtils.isEmpty(mProxyHostName)) { |
| builder.setHttpProxy(ProxyInfo.buildDirectProxy(mProxyHostName, mProxyHostPort)); |
| } |
| synchronized (mService) { |
| vpnInterface = builder.establish(); |
| if (mOnEstablishListener != null) { |
| mOnEstablishListener.onEstablish(vpnInterface); |
| } |
| } |
| Log.i(getTag(), "New interface: " + vpnInterface + " (" + parameters + ")"); |
| return vpnInterface; |
| } |
| |
| private final String getTag() { |
| return ToyVpnConnection.class.getSimpleName() + "[" + mConnectionId + "]"; |
| } |
| } |