Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5231b0af authored by Chia-chi Yeh's avatar Chia-chi Yeh Committed by Android (Google) Code Review
Browse files

Merge "VPN: add a method to handle requests of legacy VPNs."

parents 7a1e5769 85a7ce02
Loading
Loading
Loading
Loading
+202 −2
Original line number Diff line number Diff line
@@ -27,15 +27,23 @@ import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.net.INetworkManagementEventObserver;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.os.Binder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.Process;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.util.Log;

import com.android.internal.R;
import com.android.internal.net.VpnConfig;
import com.android.server.ConnectivityService.VpnCallback;

import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charsets;

/**
 * @hide
 */
@@ -49,7 +57,8 @@ public class Vpn extends INetworkManagementEventObserver.Stub {

    private String mPackageName;
    private String mInterfaceName;
    private String mDnsPropertyPrefix;

    private LegacyVpnRunner mLegacyVpnRunner;

    public Vpn(Context context, VpnCallback callback) {
        mContext = context;
@@ -249,4 +258,195 @@ public class Vpn extends INetworkManagementEventObserver.Stub {
    private native void nativeReset(String name);
    private native int nativeCheck(String name);
    private native void nativeProtect(int fd, String name);

    /**
     * Handle legacy VPN requests. This method stops the services and restart
     * them if their arguments are not null. Heavy things are offloaded to
     * another thread, so callers will not be blocked too long.
     *
     * @param raoocn The arguments to be passed to racoon.
     * @param mtpd The arguments to be passed to mtpd.
     */
    public synchronized void doLegacyVpn(String[] racoon, String[] mtpd) {
        // Currently only system user is allowed.
        if (Binder.getCallingUid() != Process.SYSTEM_UID) {
            throw new SecurityException("Unauthorized Caller");
        }

        // If the previous runner is still alive, interrupt it.
        if (mLegacyVpnRunner != null && mLegacyVpnRunner.isAlive()) {
            mLegacyVpnRunner.interrupt();
        }

        // Start a new runner and we are done!
        mLegacyVpnRunner = new LegacyVpnRunner(
                new String[] {"racoon", "mtpd"}, racoon, mtpd);
        mLegacyVpnRunner.start();
    }

    /**
     * Bringing up a VPN connection takes time, and that is all this thread
     * does. Here we have plenty of time. The only thing we need to take
     * care of is responding to interruptions as soon as possible. Otherwise
     * requests will be piled up. This can be done in a Handler as a state
     * machine, but it is much easier to read in the current form.
     */
    private class LegacyVpnRunner extends Thread {
        private static final String TAG = "LegacyVpnRunner";

        private static final String NONE = "--";

        private final String[] mServices;
        private final String[][] mArguments;
        private long mTimer = -1;

        public LegacyVpnRunner(String[] services, String[]... arguments) {
            super(TAG);
            mServices = services;
            mArguments = arguments;
        }

        @Override
        public void run() {
            // Wait for the previous thread since it has been interrupted.
            Log.v(TAG, "wait");
            synchronized (TAG) {
                Log.v(TAG, "run");
                execute();
                Log.v(TAG, "exit");
            }
        }

        private void checkpoint(boolean yield) throws InterruptedException {
            long now = SystemClock.elapsedRealtime();
            if (mTimer == -1) {
                mTimer = now;
                Thread.sleep(1);
            } else if (now - mTimer <= 30000) {
                Thread.sleep(yield ? 200 : 1);
            } else {
                throw new InterruptedException("timeout");
            }
        }

        private void execute() {
            // Catch all exceptions so we can clean up few things.
            try {
                // Initialize the timer.
                checkpoint(false);

                // First stop the services.
                for (String service : mServices) {
                    SystemProperties.set("ctl.stop", service);
                }

                // Wait for the services to stop.
                for (String service : mServices) {
                    String key = "init.svc." + service;
                    while (!"stopped".equals(SystemProperties.get(key))) {
                        checkpoint(true);
                    }
                }

                // Reset the properties.
                SystemProperties.set("vpn.dns", NONE);
                SystemProperties.set("vpn.via", NONE);
                while (!NONE.equals(SystemProperties.get("vpn.dns")) ||
                        !NONE.equals(SystemProperties.get("vpn.via"))) {
                    checkpoint(true);
                }

                // Check if we need to restart some services.
                boolean restart = false;
                for (String[] arguments : mArguments) {
                    restart = restart || (arguments != null);
                }
                if (!restart) {
                    return;
                }

                // Start the service with arguments.
                for (int i = 0; i < mServices.length; ++i) {
                    String[] arguments = mArguments[i];
                    if (arguments == null) {
                        continue;
                    }

                    // Start the service.
                    String service = mServices[i];
                    SystemProperties.set("ctl.start", service);

                    // Wait for the service to start.
                    String key = "init.svc." + service;
                    while (!"running".equals(SystemProperties.get(key))) {
                        checkpoint(true);
                    }

                    // Create the control socket.
                    LocalSocket socket = new LocalSocket();
                    LocalSocketAddress address = new LocalSocketAddress(
                            service, LocalSocketAddress.Namespace.RESERVED);

                    // Wait for the socket to connect.
                    while (true) {
                        try {
                            socket.connect(address);
                            break;
                        } catch (Exception e) {
                            // ignore
                        }
                        checkpoint(true);
                    }
                    socket.setSoTimeout(500);

                    // Send over the arguments.
                    OutputStream output = socket.getOutputStream();
                    for (String argument : arguments) {
                        byte[] bytes = argument.getBytes(Charsets.UTF_8);
                        if (bytes.length >= 0xFFFF) {
                            throw new IllegalArgumentException("argument too large");
                        }
                        output.write(bytes.length >> 8);
                        output.write(bytes.length);
                        output.write(bytes);
                        checkpoint(false);
                    }

                    // Send End-Of-Arguments.
                    output.write(0xFF);
                    output.write(0xFF);
                    output.flush();
                    socket.close();
                }

                // Now here is the beast from the old days. We check few
                // properties to figure out the current status. Ideally we
                // can read things back from the sockets and get rid of the
                // properties, but we have no time...
                while (NONE.equals(SystemProperties.get("vpn.dns")) ||
                        NONE.equals(SystemProperties.get("vpn.via"))) {

                    // Check if a running service is dead.
                    for (int i = 0; i < mServices.length; ++i) {
                        String service = mServices[i];
                        if (mArguments[i] != null && !"running".equals(
                                SystemProperties.get("init.svc." + service))) {
                            throw new IllegalArgumentException(service + " is dead");
                        }
                    }
                    checkpoint(true);
                }

                // Great! Now we are connected!
                Log.i(TAG, "connected!");
                // TODO:

            } catch (Exception e) {
                Log.i(TAG, e.getMessage());
                for (String service : mServices) {
                    SystemProperties.set("ctl.stop", service);
                }
            }
        }
    }
}