diff options
5 files changed, 608 insertions, 7 deletions
diff --git a/packages/VpnServices/src/com/android/server/vpn/OpenvpnService.java b/packages/VpnServices/src/com/android/server/vpn/OpenvpnService.java new file mode 100644 index 0000000..5143199 --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/OpenvpnService.java @@ -0,0 +1,386 @@ +/* + * 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"); + } + } + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/VpnService.java b/packages/VpnServices/src/com/android/server/vpn/VpnService.java index 63b87b1..6953a32 100644 --- a/packages/VpnServices/src/com/android/server/vpn/VpnService.java +++ b/packages/VpnServices/src/com/android/server/vpn/VpnService.java @@ -85,6 +85,13 @@ abstract class VpnService<E extends VpnProfile> implements Serializable { String password) throws IOException; /** + * Disconnects the vpn + */ + protected void disconnect() { + mDaemons.stopAll(); + } + + /** * Returns the daemons management class for this service object. */ protected VpnDaemons getDaemons() { @@ -110,14 +117,19 @@ 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(); + } } VpnState getState() { @@ -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"); diff --git a/packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java b/packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java index 5672a01..64abf87 100644 --- a/packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java +++ b/packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java @@ -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; @@ -159,6 +160,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); diff --git a/vpn/java/android/net/vpn/OpenvpnProfile.java b/vpn/java/android/net/vpn/OpenvpnProfile.java new file mode 100644 index 0000000..6a106db --- /dev/null +++ b/vpn/java/android/net/vpn/OpenvpnProfile.java @@ -0,0 +1,174 @@ +/* + * 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); + } +} diff --git a/vpn/java/android/net/vpn/VpnType.java b/vpn/java/android/net/vpn/VpnType.java index 356f8b1..53ef5fe 100644 --- a/vpn/java/android/net/vpn/VpnType.java +++ b/vpn/java/android/net/vpn/VpnType.java @@ -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; |