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

Commit 21bd4af8 authored by Hung-ying Tyan's avatar Hung-ying Tyan
Browse files

Simplify the VPN service implementation.

+ Remove NormalProcessProxy and ProcessProxy as they are not used
  anymore.
+ Rename AndroidServiceProxy to DaemonProxy and simplify its
  implementation as it does not extend to ProcessProxy anymore.
+ Execute connect() in VpnService in one thread, which simplifies socket
  and error handling.
+ Modify service subclasses accordingly.
+ Execute connect() and disconnect() in VpnServiceBinder so that the
  operations do not block the UI thread. Mark service as foreground only upon
  connecting.
parent 11b6a29d
Loading
Loading
Loading
Loading
+199 −0
Original line number Diff line number Diff line
@@ -27,18 +27,13 @@ import java.io.InputStream;
import java.io.OutputStream;

/**
 * Proxy to start, stop and interact with an Android service defined in init.rc.
 * The android service is expected to accept connection through Unix domain
 * socket. When the proxy successfully starts the service, it will establish a
 * socket connection with the service. The socket serves two purposes: (1) send
 * commands to the service; (2) for the proxy to know whether the service is
 * alive.
 *
 * After the service receives commands from the proxy, it should return either
 * 0 if the service will close the socket (and the proxy will re-establish
 * another connection immediately after), or 1 if the socket is remained alive.
 * Proxy to start, stop and interact with a VPN daemon.
 * The daemon is expected to accept connection through Unix domain socket.
 * When the proxy successfully starts the daemon, it will establish a socket
 * connection with the daemon, to both send commands to the daemon and receive
 * response and connecting error code from the daemon.
 */
public class AndroidServiceProxy extends ProcessProxy {
class DaemonProxy {
    private static final int WAITING_TIME = 15; // sec

    private static final String SVC_STATE_CMD_PREFIX = "init.svc.";
@@ -49,65 +44,27 @@ public class AndroidServiceProxy extends ProcessProxy {

    private static final int END_OF_ARGUMENTS = 255;

    private static final int STOP_SERVICE = -1;
    private static final int AUTH_ERROR_CODE = 51;

    private String mServiceName;
    private String mSocketName;
    private LocalSocket mKeepaliveSocket;
    private boolean mControlSocketInUse;
    private Integer mSocketResult = null;
    private String mName;
    private LocalSocket mControlSocket;
    private String mTag;

    /**
     * Creates a proxy with the service name.
     * @param serviceName the service name
     * Creates a proxy of the specified daemon.
     * @param daemonName name of the daemon
     */
    public AndroidServiceProxy(String serviceName) {
        mServiceName = serviceName;
        mSocketName = serviceName;
        mTag = "SProxy_" + serviceName;
    DaemonProxy(String daemonName) {
        mName = daemonName;
        mTag = "SProxy_" + daemonName;
    }

    @Override
    public String getName() {
        return "Service " + mServiceName;
    }

    @Override
    public synchronized void stop() {
        if (isRunning()) {
            try {
                setResultAndCloseControlSocket(STOP_SERVICE);
            } catch (IOException e) {
                // should not occur
                throw new RuntimeException(e);
            }
        }
        Log.d(mTag, "-----  Stop: " + mServiceName);
        SystemProperties.set(SVC_STOP_CMD, mServiceName);
    String getName() {
        return mName;
    }

    /**
     * Sends a command with arguments to the service through the control socket.
     */
    public synchronized void sendCommand(String ...args) throws IOException {
        OutputStream out = getControlSocketOutput();
        for (String arg : args) outputString(out, arg);
        out.write(END_OF_ARGUMENTS);
        out.flush();
        checkSocketResult();
    }

    /**
     * {@inheritDoc}
     * The method returns when the service exits.
     */
    @Override
    protected void performTask() throws IOException {
        String svc = mServiceName;
        Log.d(mTag, "-----  Stop the daemon just in case: " + mServiceName);
        SystemProperties.set(SVC_STOP_CMD, mServiceName);
    void start() throws IOException {
        String svc = mName;
        Log.d(mTag, "-----  Stop the daemon just in case: " + mName);
        SystemProperties.set(SVC_STOP_CMD, mName);
        if (!blockUntil(SVC_STATE_STOPPED, 5)) {
            throw new IOException("cannot start service anew: " + svc
                    + ", it is still running");
@@ -116,53 +73,70 @@ public class AndroidServiceProxy extends ProcessProxy {
        Log.d(mTag, "+++++  Start: " + svc);
        SystemProperties.set(SVC_START_CMD, svc);

        boolean success = blockUntil(SVC_STATE_RUNNING, WAITING_TIME);
        if (!blockUntil(SVC_STATE_RUNNING, WAITING_TIME)) {
            throw new IOException("cannot start service: " + svc);
        } else {
            mControlSocket = createServiceSocket();
        }
    }

        if (success) {
            Log.d(mTag, "-----  Running: " + svc + ", create keepalive socket");
            LocalSocket s = mKeepaliveSocket = createServiceSocket();
            setState(ProcessState.RUNNING);
    void sendCommand(String ...args) throws IOException {
        OutputStream out = getControlSocketOutput();
        for (String arg : args) outputString(out, arg);
        out.write(END_OF_ARGUMENTS);
        out.flush();

            if (s == null) {
                // no socket connection, stop hosting the service
                stop();
                return;
        int result = getResultFromSocket(true);
        if (result != args.length) {
            throw new IOException("socket error, result from service: "
                    + result);
        }
            try {
                for (;;) {
                    InputStream in = s.getInputStream();
                    int data = in.read();
                    if (data >= 0) {
                        Log.d(mTag, "got data from control socket: " + data);

                        setSocketResult(data);
                    } else {
                        // service is gone
                        if (mControlSocketInUse) setSocketResult(-1);
                        break;
    }

    // returns 0 if nothing is in the receive buffer
    int getResultFromSocket() throws IOException {
        return getResultFromSocket(false);
    }
                Log.d(mTag, "control connection closed");

    void closeControlSocket() {
        if (mControlSocket == null) return;
        try {
            mControlSocket.close();
        } catch (IOException e) {
                if (e instanceof VpnConnectingError) {
                    throw e;
                } else {
                    Log.d(mTag, "control socket broken: " + e.getMessage());
            Log.e(mTag, "close control socket", e);
        } finally {
            mControlSocket = null;
        }
    }

            // Wait 5 seconds for the service to exit
            success = blockUntil(SVC_STATE_STOPPED, 5);
    void stop() {
        String svc = mName;
        Log.d(mTag, "-----  Stop: " + svc);
        SystemProperties.set(SVC_STOP_CMD, svc);
        boolean success = blockUntil(SVC_STATE_STOPPED, 5);
        Log.d(mTag, "stopping " + svc + ", success? " + success);
        } else {
            setState(ProcessState.STOPPED);
            throw new IOException("cannot start service: " + svc);
    }

    boolean isStopped() {
        String cmd = SVC_STATE_CMD_PREFIX + mName;
        return SVC_STATE_STOPPED.equals(SystemProperties.get(cmd));
    }

    private int getResultFromSocket(boolean blocking) throws IOException {
        LocalSocket s = mControlSocket;
        if (s == null) return 0;
        InputStream in = s.getInputStream();
        if (!blocking && in.available() == 0) return 0;

        int data = in.read();
        Log.d(mTag, "got data from control socket: " + data);

        return data;
    }

    private LocalSocket createServiceSocket() throws IOException {
        LocalSocket s = new LocalSocket();
        LocalSocketAddress a = new LocalSocketAddress(mSocketName,
        LocalSocketAddress a = new LocalSocketAddress(mName,
                LocalSocketAddress.Namespace.RESERVED);

        // try a few times in case the service has not listen()ed
@@ -181,69 +155,25 @@ public class AndroidServiceProxy extends ProcessProxy {
    }

    private OutputStream getControlSocketOutput() throws IOException {
        if (mKeepaliveSocket != null) {
            mControlSocketInUse = true;
            mSocketResult = null;
            return mKeepaliveSocket.getOutputStream();
        if (mControlSocket != null) {
            return mControlSocket.getOutputStream();
        } else {
            throw new IOException("no control socket available");
        }
    }

    private void checkSocketResult() throws IOException {
        try {
            // will be notified when the result comes back from service
            if (mSocketResult == null) wait();
        } catch (InterruptedException e) {
            Log.d(mTag, "checkSocketResult(): " + e);
        } finally {
            mControlSocketInUse = false;
            if ((mSocketResult == null) || (mSocketResult < 0)) {
                throw new IOException("socket error, result from service: "
                        + mSocketResult);
            }
        }
    }

    private synchronized void setSocketResult(int result)
            throws VpnConnectingError {
        if (mControlSocketInUse) {
            mSocketResult = result;
            notifyAll();
        } else if (result > 0) {
            // error from daemon
            throw new VpnConnectingError((result == AUTH_ERROR_CODE)
                    ? VpnManager.VPN_ERROR_AUTH
                    : VpnManager.VPN_ERROR_CONNECTION_FAILED);
        }
    }

    private void setResultAndCloseControlSocket(int result)
            throws VpnConnectingError {
        setSocketResult(result);
        try {
            mKeepaliveSocket.shutdownInput();
            mKeepaliveSocket.shutdownOutput();
            mKeepaliveSocket.close();
        } catch (IOException e) {
            Log.e(mTag, "close keepalive socket", e);
        } finally {
            mKeepaliveSocket = null;
        }
    }

    /**
     * Waits for the process to be in the expected state. The method returns
     * false if after the specified duration (in seconds), the process is still
     * not in the expected state.
     */
    private boolean blockUntil(String expectedState, int waitTime) {
        String cmd = SVC_STATE_CMD_PREFIX + mServiceName;
        String cmd = SVC_STATE_CMD_PREFIX + mName;
        int sleepTime = 200; // ms
        int n = waitTime * 1000 / sleepTime;
        for (int i = 0; i < n; i++) {
            if (expectedState.equals(SystemProperties.get(cmd))) {
                Log.d(mTag, mServiceName + " is " + expectedState + " after "
                Log.d(mTag, mName + " is " + expectedState + " after "
                        + (i * sleepTime) + " msec");
                break;
            }
@@ -258,4 +188,12 @@ public class AndroidServiceProxy extends ProcessProxy {
        out.write(bytes);
        out.flush();
    }

    private void sleep(int msec) {
        try {
            Thread.currentThread().sleep(msec);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
+4 −4
Original line number Diff line number Diff line
@@ -25,7 +25,7 @@ import java.io.IOException;
 * connection.
 */
class L2tpIpsecPskService extends VpnService<L2tpIpsecPskProfile> {
    private static final String IPSEC_DAEMON = "racoon";
    private static final String IPSEC = "racoon";

    @Override
    protected void connect(String serverIp, String username, String password)
@@ -33,9 +33,9 @@ class L2tpIpsecPskService extends VpnService<L2tpIpsecPskProfile> {
        L2tpIpsecPskProfile p = getProfile();

        // IPSEC
        AndroidServiceProxy ipsecService = startService(IPSEC_DAEMON);
        ipsecService.sendCommand(serverIp, L2tpService.L2TP_PORT,
                p.getPresharedKey());
        DaemonProxy ipsec = startDaemon(IPSEC);
        ipsec.sendCommand(serverIp, L2tpService.L2TP_PORT, p.getPresharedKey());
        ipsec.closeControlSocket();

        sleep(2000); // 2 seconds

+4 −3
Original line number Diff line number Diff line
@@ -25,15 +25,16 @@ import java.io.IOException;
 * The service that manages the certificate based L2TP-over-IPSec VPN connection.
 */
class L2tpIpsecService extends VpnService<L2tpIpsecProfile> {
    private static final String IPSEC_DAEMON = "racoon";
    private static final String IPSEC = "racoon";

    @Override
    protected void connect(String serverIp, String username, String password)
            throws IOException {
        // IPSEC
        AndroidServiceProxy ipsecService = startService(IPSEC_DAEMON);
        ipsecService.sendCommand(serverIp, L2tpService.L2TP_PORT,
        DaemonProxy ipsec = startDaemon(IPSEC);
        ipsec.sendCommand(serverIp, L2tpService.L2TP_PORT,
                getUserkeyPath(), getUserCertPath(), getCaCertPath());
        ipsec.closeControlSocket();

        sleep(2000); // 2 seconds

+2 −2
Original line number Diff line number Diff line
@@ -24,7 +24,7 @@ import java.util.Arrays;
 * A helper class for sending commands to the MTP daemon (mtpd).
 */
class MtpdHelper {
    private static final String MTPD_SERVICE = "mtpd";
    private static final String MTPD = "mtpd";
    private static final String VPN_LINKNAME = "vpn";
    private static final String PPP_ARGS_SEPARATOR = "";

@@ -37,7 +37,7 @@ class MtpdHelper {
        args.add(PPP_ARGS_SEPARATOR);
        addPppArguments(vpnService, args, serverIp, username, password);

        AndroidServiceProxy mtpd = vpnService.startService(MTPD_SERVICE);
        DaemonProxy mtpd = vpnService.startDaemon(MTPD);
        mtpd.sendCommand(args.toArray(new String[args.size()]));
    }

+0 −85
Original line number Diff line number Diff line
/*
 * Copyright (C) 2009, 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.android.server.vpn;

import android.util.Log;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;

/**
 * Proxy to perform a command with arguments.
 */
public class NormalProcessProxy extends ProcessProxy {
    private Process mProcess;
    private String[] mArgs;
    private String mTag;

    /**
     * Creates a proxy with the arguments.
     * @param args the argument list with the first one being the command
     */
    public NormalProcessProxy(String ...args) {
        if ((args == null) || (args.length == 0)) {
            throw new IllegalArgumentException();
        }
        mArgs = args;
        mTag = "PProxy_" + getName();
    }

    @Override
    public String getName() {
        return mArgs[0];
    }

    @Override
    public synchronized void stop() {
        if (isStopped()) return;
        getHostThread().interrupt();
        // TODO: not sure how to reliably kill a process
        mProcess.destroy();
        setState(ProcessState.STOPPING);
    }

    @Override
    protected void performTask() throws IOException, InterruptedException {
        String[] args = mArgs;
        Log.d(mTag, "+++++  Execute: " + getEntireCommand());
        ProcessBuilder pb = new ProcessBuilder(args);
        setState(ProcessState.RUNNING);
        Process p = mProcess = pb.start();
        BufferedReader reader = new BufferedReader(new InputStreamReader(
                p.getInputStream()));
        while (true) {
            String line = reader.readLine();
            if ((line == null) || isStopping()) break;
            Log.d(mTag, line);
        }

        Log.d(mTag, "-----  p.waitFor(): " + getName());
        p.waitFor();
        Log.d(mTag, "-----  Done: " + getName());
    }

    private CharSequence getEntireCommand() {
        String[] args = mArgs;
        StringBuilder sb = new StringBuilder(args[0]);
        for (int i = 1; i < args.length; i++) sb.append(' ').append(args[i]);
        return sb;
    }
}
Loading