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

Commit 13f6adf7 authored by James Bottomley's avatar James Bottomley Committed by Steve Kondik
Browse files

framework: add openvpn to native vpn services



This requires a modified mtpd (so it knows how to start openvpn and
run as root, so must drop privs for pppd).

The change causes mtpd to start openvpn and create a management socket
(in /dev/sockets) which is then used to control the interaction.  To
save space on typing, a lot of options (like ifconfig and routes) are
expected to be pushed from the server.

Also updated keystore to allow AID_ROOT to get certificates (because
openvpn will be running as root until the VPN connection is
established and it can drop privileges).

Signed-off-by: default avatarJames Bottomley <James.Bottomley@suse.de>
parent f537d710
Loading
Loading
Loading
Loading
+386 −0
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.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.net.vpn.OpenvpnProfile;
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;
import java.util.ArrayList;

/**
 * The service that manages the openvpn VPN connection.
 */
class OpenvpnService extends VpnService<OpenvpnProfile> {
    private static final String OPENVPN_DAEMON = "openvpn";
    private static final String MTPD = "mtpd";
    private static final String USE_INLINE = "[[INLINE]]";
    private static final String USE_KEYSTORE = "[[ANDROID]]";
    private static final String TAG = OpenvpnService.class.getSimpleName();
    private static int count = 0;

    private final String socketName = OPENVPN_DAEMON + getCount();

    private transient OpenvpnThread thread = null;

    private transient String mPassword;
    private transient String mUsername;

    private synchronized static String getCount() {
	    return Integer.toString(count++);
    }

    @Override
    protected void connect(String serverIp, String username, String password)
            throws IOException {
        OpenvpnProfile p = getProfile();
	ArrayList<String> args = new ArrayList<String>();

	mUsername = username;
	mPassword = password;

	args.add(OPENVPN_DAEMON);
	args.add("--dev"); args.add("tun");
	args.add("--remote"); args.add(serverIp);
	args.add("--nobind");
	args.add("--proto"); args.add(p.getProto());
	args.add("--client");
	args.add("--rport"); args.add(p.getPort());
	args.add("--ca"); args.add(USE_INLINE); args.add(USE_KEYSTORE + p.getCAFile());
	args.add("--cert"); args.add(USE_INLINE); args.add(USE_KEYSTORE + p.getCertFile());
	args.add("--key"); args.add(USE_INLINE); args.add(USE_KEYSTORE + p.getKeyFile());
	args.add("--persist-tun");
	args.add("--persist-key");
	args.add("--management"); args.add("/dev/socket/" + socketName); args.add("unix");
	args.add("--management-hold");
	if (p.getUseCompLzo()) {
	    args.add("--comp-lzo");
	}
	if (p.getUserAuth()) {
	    args.add("--auth-user-pass");
	    args.add("--management-query-passwords");
	}
	if (p.getSupplyAddr()) {
	    args.add("--ifconfig"); args.add(p.getLocalAddr()); args.add(p.getRemoteAddr());
	}

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

    @Override
    protected void disconnect() {
	if (thread != null)
	    thread.disconnectAndWait();
    }

    @Override
    void waitUntilConnectedOrTimedout() throws IOException {
	thread = new OpenvpnThread();
	thread.openvpnStart();
	thread.waitConnect(60);
	setVpnStateUp(true);
    }

    @Override
    protected void stopPreviouslyRunDaemons() {
        stopDaemon(MtpdHelper.MTPD);
    }

    @Override
    protected void recover() {
	try {
	    thread = new OpenvpnThread();
	    thread.openvpnStart();
	} catch (IOException e) {
	    onError(e);
	}
   }

    void startConnectivityMonitor() {
	/* Openvpn is completely event driven, so we don't need
	 * a polling monitor at all, so do nothing here */
    }

    private class OpenvpnThread extends Thread {
	InputStream in;
	OutputStream out;
	LocalSocket mSocket;

	boolean finalDisconnect = false;
	boolean firstConnect = false;
	boolean disconnecting = false;
	boolean passwordError = false;

	boolean SFbool;
	volatile String SFreason;
	String vpnState = "WAIT"; // initial state

	OpenvpnThread() throws IOException {
	    openSocket();
	    in = mSocket.getInputStream();
	    out = mSocket.getOutputStream();
	}

	public void openvpnStart() throws IOException {
	    super.start();
	    send("state on");	// make state dynamic
	    send("log on");	// dynamically log over the socket
	    send("hold off");	// don't hold for subsequent reconnects
	    send("hold release");	// release from hold
	    send("bytecount 2");  // need this to update the monitor
	}

	public synchronized void disconnectAndWait() {
	    try {
		disconnecting = true;
		send("signal SIGTERM");
		while (!finalDisconnect)
		    this.wait();
	    } catch(Exception e) {
		    // we're done
	    }
	}

	public synchronized void waitConnect(long seconds) throws IOException {
	    long endTime = System.currentTimeMillis() + seconds * 1000;
	    long wait;
	    while (!isConnected() && (wait = (endTime -  System.currentTimeMillis())) > 0) {
		try {
		    this.wait(wait);
		} catch(InterruptedException e) {
		    // do nothing
		}
		if (passwordError)
		    throw new VpnConnectingError(VpnManager.VPN_ERROR_AUTH);
	    } 
	    if (!isConnected())
		throw new VpnConnectingError(VpnManager.VPN_ERROR_CONNECTION_FAILED);
	    firstConnect = true;
	}

	private boolean isConnected() {
	    return vpnState.equals("CONNECTED");
	}

	private void openSocket() throws IOException {
	    LocalSocket s = new LocalSocket();
	    LocalSocketAddress a = new LocalSocketAddress(socketName,
							  LocalSocketAddress.Namespace.RESERVED);
	    IOException excp = null;
	    for (int i = 0; i < 10; i++) {
		try {
		    s.connect(a);
		    mSocket = s;
		    return;
		} catch (IOException e) {
		    excp = e;
		    try {
			Thread.currentThread().sleep(500);
		    } catch (InterruptedException ex) {
			throw new RuntimeException(ex);
		    }
		}
	    }
	    throw excp;
	}

	private synchronized boolean waitForSuccessOrFail() {
	    SFreason = null;
	    try {
		while (SFreason == null) {
		    this.wait();
		}
		return SFbool;
	    } catch(InterruptedException e) {
		return false;
	    }
	}

	private synchronized void signalSuccessOrFail(boolean success, String reason) {
	    SFbool = success;
	    SFreason = reason;
	    this.notifyAll();
	}
			

	private synchronized void sendAsync(String str) throws IOException {
	    str += "\n";
	    out.write(str.getBytes());
	    out.flush();
	}

	private boolean send(String str) throws IOException {
	    sendAsync(str);
	    return waitForSuccessOrFail();
	}

	private synchronized void signalState(String s) {
	    // state strings come as <date in secs>, <state>, <other stuff>
	    int first = s.indexOf(',');
	    if (first == -1)
		return;
	    int second = s.indexOf(',', first + 1);
	    if (second == -1)
		return;
	    String state = s.substring(first + 1, second);

	    /*
	     * state can be:
	     *
	     *
	     * CONNECTING    -- OpenVPN's initial state.
	     * WAIT          -- (Client only) Waiting for initial response
	     *                  from server.
	     * AUTH          -- (Client only) Authenticating with server.
	     * GET_CONFIG    -- (Client only) Downloading configuration options
	     *                 from server.
	     * ASSIGN_IP     -- Assigning IP address to virtual network
	     *                 interface.
	     * ADD_ROUTES    -- Adding routes to system.
	     * CONNECTED     -- Initialization Sequence Completed.
	     * RECONNECTING  -- A restart has occurred.
	     * EXITING       -- A graceful exit is in progress.
	     *
	     * Really all we care about is connected or not
	     */
	    vpnState = state;
	    this.notifyAll();
	    if (state.equals("EXITING") && firstConnect && !disconnecting)
		onError(new IOException("Connection Closed"));
	}

	private synchronized void signalPassword(String s) throws IOException {
	    /* message should be Need '<auth type>' password
	     * but coult be Verification Failed: '<auth type'
	     */

	    int first = s.indexOf('\'');
	    int second = s.indexOf('\'', first + 1);
	    final String authType = s.substring(first + 1, second);

	    /* AuthType can be one of
	     *
	     * "Auth"	- regular client server authentication
	     * "Private Key" - password for private key (unimplemented)
	     */

	    if (s.startsWith("Need")) {
		/* we're in the processor thread, so we have to send
		 * these asynchronously to avoid a deadlock */
		sendAsync("username '" + authType +"' '" + mUsername + "'");
		sendAsync("password '" + authType +"' '" + mPassword + "'");
	    } else {
		// must be signalling authentication failure
		passwordError = true;
		this.notifyAll();
	    }
	}

	private void signalBytecount(String s) {
	    int index = s.indexOf(',');
	    if (index == -1)
		// no , in message, ignore it
		return;

	    String in = s.substring(0, index);
	    String out = s.substring(index+1);
	    vpnStateUpdate(Long.parseLong(in), Long.parseLong(out));
	}

	private void signalLog(String s) {
	    //log format is <date in secs>,<severity>,<message>
	    int first = s.indexOf(',');
	    if (first == -1)
		return;
	    int second = s.indexOf(',', first + 1);
	    if (second == -1)
		return;
	    String message = s.substring(second + 1);
	    Log.i("openvpn", message);
	}

	private void parseLine(String s) throws IOException {
	    int index = s.indexOf(':');
	    if (index == -1)
		// no : in message, ignore it
		return;

	    String token = s.substring(0, index);
	    String body = s.substring(index +1);

	    if (token.equals(">INFO")) {
		// This is the starting string, just skip it
	    } else if (token.equals("SUCCESS")) {
		signalSuccessOrFail(true, body);
	    } else if (token.equals("ERROR")) {
		signalSuccessOrFail(false, body);
	    } else if (token.equals(">STATE")) {
		signalState(body);
	    } else if (token.equals(">FATAL")) {
		signalState("EXITING," + body);
	    } else if (token.equals(">PASSWORD")) {
		signalPassword(body);
	    } else if (token.equals(">LOG")) {
		signalLog(body);
	    }else if (token.equals(">HOLD")) {
		// just warning us we're in a hold state, ignore
	    } else if (token.equals(">BYTECOUNT")) {
		signalBytecount(body);
	    } else {
		Log.w(TAG, "Unknown control token:\"" + token + "\"");
	    }
	}
	    
	public void run() {

	    System.out.println("THREAD " + this + " RUNNING");
	    
	    try {
		int c;
		StringBuffer s = new StringBuffer();
		while (true) {
		    c = in.read();
		    if (c == -1)
			throw new IOException("End of Stream");
		    if (c == '\n') {
			parseLine(s.toString());
			s = new StringBuffer();
			continue;
		    }
		    if (c == '\r')
			continue;
		    s.append((char)c);
		}
	    } catch(IOException e) {
		// terminate
	    } finally {
		synchronized(this) {
		    finalDisconnect = true;
		    this.notifyAll();
		}
		System.out.println("THREAD " + this + " TERMINATED");
	    }
	}
    }
}
+39 −7
Original line number Diff line number Diff line
@@ -84,6 +84,13 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
    protected abstract void connect(String serverIp, String username,
            String password) throws IOException;

    /**
     * Disconnects the vpn
     */
    protected void disconnect() {
            mDaemons.stopAll();
    }

    /**
     * Returns the daemons management class for this service object.
     */
@@ -110,13 +117,18 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
        recover(context);
    }

    // intended for override by subclasses
    protected void recover() {
	startConnectivityMonitor();
    }

    void recover(VpnServiceBinder context) {
        mContext = context;
        mNotification = new NotificationHelper();

        if (VpnState.CONNECTED.equals(mState)) {
            Log.i("VpnService", "     recovered: " + mProfile.getName());
            startConnectivityMonitor();
	    recover();
	}
    }

@@ -147,7 +159,7 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
            setState(VpnState.DISCONNECTING);
            mNotification.showDisconnect();

            mDaemons.stopAll();
	    disconnect();
        } catch (Throwable e) {
            Log.e(TAG, "onDisconnect()", e);
        } finally {
@@ -155,7 +167,7 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
        }
    }

    private void onError(Throwable error) {
    void onError(Throwable error) {
        // error may occur during or after connection setup
        // and it may be due to one or all services gone
        if (mError != null) {
@@ -167,7 +179,7 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
        onDisconnect();
    }

    private void onError(int errorCode) {
    void onError(int errorCode) {
        onError(new VpnConnectingError(errorCode));
    }

@@ -183,7 +195,7 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
        }
    }

    private void waitUntilConnectedOrTimedout() throws IOException {
    void waitUntilConnectedOrTimedout() throws IOException {
        sleep(2000); // 2 seconds
        for (int i = 0; i < 80; i++) {
            if (mState != VpnState.CONNECTING) {
@@ -207,6 +219,21 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
        }
    }

    void setVpnStateUp(boolean state) throws IOException {
	if (state) {
	    SystemProperties.set(VPN_STATUS, VPN_IS_UP);
	    onConnected();
	    mNotification.update();
	} else {
	    SystemProperties.set(VPN_STATUS, VPN_IS_DOWN);
	}
    }

    void vpnStateUpdate(long in, long out) {
	// currently don't show in and out bytes in status
	mNotification.update();
    }

    private synchronized void onConnected() throws IOException {
        if (DBG) Log.d(TAG, "onConnected()");

@@ -276,6 +303,11 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
    private void setVpnDns() {
        String vpnDns1 = SystemProperties.get(VPN_DNS1);
        String vpnDns2 = SystemProperties.get(VPN_DNS2);
	if (vpnDns1.length() == 0) {
	    Log.i(TAG, "No vpn dns supplied, not updating");
	    return;
	}

        SystemProperties.set(DNS1, vpnDns1);
        SystemProperties.set(DNS2, vpnDns2);
        Log.i(TAG, String.format("set vpn dns prop: %s, %s",
@@ -323,7 +355,7 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
        }
    }

    private void startConnectivityMonitor() {
    void startConnectivityMonitor() {
        new Thread(new Runnable() {
            public void run() {
                Log.i(TAG, "VPN connectivity monitor running");
+6 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.net.vpn.IVpnService;
import android.net.vpn.L2tpIpsecProfile;
import android.net.vpn.L2tpIpsecPskProfile;
import android.net.vpn.L2tpProfile;
import android.net.vpn.OpenvpnProfile;
import android.net.vpn.PptpProfile;
import android.net.vpn.VpnManager;
import android.net.vpn.VpnProfile;
@@ -153,6 +154,11 @@ public class VpnServiceBinder extends Service {
                l2tp.setContext(this, (L2tpProfile) p);
                return l2tp;

	    case OPENVPN:
		OpenvpnService ovpn = new OpenvpnService();
		ovpn.setContext(this, (OpenvpnProfile)p );
		return ovpn;

            case PPTP:
                PptpService pptp = new PptpService();
                pptp.setContext(this, (PptpProfile) p);
+174 −0
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 android.net.vpn;

import android.os.Parcel;
import android.security.CertTool;

/**
 * The profile for Openvpn type of VPN.
 * {@hide}
 */
public class OpenvpnProfile extends VpnProfile {
    private static final long serialVersionUID = 1L;
    private static final String PROTO_UDP = "udp";
    private static final String PROTO_TCP = "tcp";

    // Standard Settings
    private boolean mUserAuth = false;
    private String mCA;
    private String mCert;
    // Advanced Settings
    private int mPort = 1194;
    private String mProto = PROTO_UDP;
    private boolean mUseCompLzo = false;
    private boolean mSupplyAddr = false;
    private String mLocalAddr;
    private String mRemoteAddr;

    @Override
    public VpnType getType() {
        return VpnType.OPENVPN;
    }

    public void setPort(String port) {
	try {
	    mPort = Integer.parseInt(port);
	} catch (NumberFormatException e) {
	    // no update
	}
    }

    public String getPort() {
        return Integer.toString(mPort);
    }

    public String getProto() {
	return mProto;
    }

    public CharSequence[] getProtoList() {
	String[] s = new String[2];
	s[0] = PROTO_UDP;
	s[1] = PROTO_TCP;
	return s;
    }

    public void setProto(String p) {
	if (p.contains(PROTO_TCP))
	    mProto = PROTO_TCP;
	else if(p.contains(PROTO_UDP))
	    mProto = PROTO_UDP;
    }
	

    public boolean getUserAuth() {
	return mUserAuth;
    }

    public void setUserAuth(boolean auth) {
	mUserAuth = auth;
    }

    public String getCAFile() {
	return CertTool.getInstance().getCaCertificate(mCA) ;
    }

    public String getCAName() {
	return mCA;
    }

    public void setCAName(String name) {
	mCA = name;
    }

    public String getCertFile() {
        return CertTool.getInstance().getUserCertificate(mCert);
    }

    public String getCertName() {
	return mCert;
    }

    public void setCertName(String name) {
	mCert = name;
    }

    public String getKeyFile() {
        return CertTool.getInstance().getUserPrivateKey(mCert);
    }

    public void setUseCompLzo(boolean b) {
	mUseCompLzo = b;
    }

    public boolean getUseCompLzo() {
	return mUseCompLzo;
    }

    public void setSupplyAddr(boolean b) {
	mSupplyAddr = b;
    }

    public boolean getSupplyAddr() {
	return mSupplyAddr;
    }

    public void setLocalAddr(String addr) {
	mLocalAddr = addr;
    }

    public String getLocalAddr() {
	return mLocalAddr;
    }

    public void setRemoteAddr(String addr) {
	mRemoteAddr = addr;
    }

    public String getRemoteAddr() {
	return mRemoteAddr;
    }

    @Override
    protected void readFromParcel(Parcel in) {
        super.readFromParcel(in);
        mPort = in.readInt();
	mProto = in.readString();
	mUserAuth = in.readInt() == 1;
	mCA = in.readString();
	mCert = in.readString();
	mUseCompLzo = in.readInt() == 1;
	mSupplyAddr = in.readInt() == 1;
	mLocalAddr = in.readString();
	mRemoteAddr = in.readString();
    }

    @Override
    public void writeToParcel(Parcel parcel, int flags) {
        super.writeToParcel(parcel, flags);
        parcel.writeInt(mPort);
	parcel.writeString(mProto);
	parcel.writeInt(mUserAuth ? 1 : 0);
        parcel.writeString(mCA);
        parcel.writeString(mCert);
	parcel.writeInt(mUseCompLzo ? 1 : 0);
	parcel.writeInt(mSupplyAddr ? 1 : 0);
	parcel.writeString(mLocalAddr);
	parcel.writeString(mRemoteAddr);
    }
}
+3 −0
Original line number Diff line number Diff line
@@ -29,6 +29,9 @@ public enum VpnType {
            L2tpIpsecPskProfile.class),
    L2TP_IPSEC("L2TP/IPSec CRT", R.string.l2tp_ipsec_crt_vpn_description,
            L2tpIpsecProfile.class);
    L2TP_IPSEC("L2TP/IPSec CRT", "Certificate based L2TP/IPSec VPN",
	       L2tpIpsecProfile.class),
    OPENVPN("OpenVPN", "", OpenvpnProfile.class);

    private String mDisplayName;
    private int mDescriptionId;