summaryrefslogtreecommitdiffstats
path: root/packages/VpnServices/src/com/android/server/vpn/AndroidServiceProxy.java
blob: e4c070ff8b296b8e3dd4913c5d671b59c42d39e3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
/*
 * 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.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.net.vpn.VpnManager;
import android.os.SystemProperties;
import android.util.Log;

import java.io.IOException;
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.
 */
public class AndroidServiceProxy extends ProcessProxy {
    private static final int WAITING_TIME = 15; // sec

    private static final String SVC_STATE_CMD_PREFIX = "init.svc.";
    private static final String SVC_START_CMD = "ctl.start";
    private static final String SVC_STOP_CMD = "ctl.stop";
    private static final String SVC_STATE_RUNNING = "running";
    private static final String SVC_STATE_STOPPED = "stopped";

    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 mTag;

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

    @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);
    }

    /**
     * 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);
        if (!blockUntil(SVC_STATE_STOPPED, 5)) {
            throw new IOException("cannot start service anew: " + svc
                    + ", it is still running");
        }

        Log.d(mTag, "+++++  Start: " + svc);
        SystemProperties.set(SVC_START_CMD, svc);

        boolean success = blockUntil(SVC_STATE_RUNNING, WAITING_TIME);

        if (success) {
            Log.d(mTag, "-----  Running: " + svc + ", create keepalive socket");
            LocalSocket s = mKeepaliveSocket = createServiceSocket();
            setState(ProcessState.RUNNING);

            if (s == null) {
                // no socket connection, stop hosting the service
                stop();
                return;
            }
            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;
                    }
                }
                Log.d(mTag, "control connection closed");
            } catch (IOException e) {
                if (e instanceof VpnConnectingError) {
                    throw e;
                } else {
                    Log.d(mTag, "control socket broken: " + e.getMessage());
                }
            }

            // Wait 5 seconds for the service to exit
            success = blockUntil(SVC_STATE_STOPPED, 5);
            Log.d(mTag, "stopping " + svc + ", success? " + success);
        } else {
            setState(ProcessState.STOPPED);
            throw new IOException("cannot start service: " + svc);
        }
    }

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

        // try a few times in case the service has not listen()ed
        IOException excp = null;
        for (int i = 0; i < 10; i++) {
            try {
                s.connect(a);
                return s;
            } catch (IOException e) {
                Log.d(mTag, "service not yet listen()ing; try again");
                excp = e;
                sleep(500);
            }
        }
        throw excp;
    }

    private OutputStream getControlSocketOutput() throws IOException {
        if (mKeepaliveSocket != null) {
            mControlSocketInUse = true;
            mSocketResult = null;
            return mKeepaliveSocket.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;
        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 "
                        + (i * sleepTime) + " msec");
                break;
            }
            sleep(sleepTime);
        }
        return expectedState.equals(SystemProperties.get(cmd));
    }

    private void outputString(OutputStream out, String s) throws IOException {
        byte[] bytes = s.getBytes();
        out.write(bytes.length);
        out.write(bytes);
        out.flush();
    }
}