package info.kghost.android.openvpn; import info.kghost.android.openvpn.VpnStatus.VpnState; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; import java.net.Inet4Address; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import java.security.KeyStore; import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.spongycastle.openssl.PEMWriter; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Intent; import android.net.VpnService; import android.os.AsyncTask; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; import android.security.KeyChain; import android.util.Base64; import android.util.Log; public class OpenVpnService extends VpnService { class Task extends AsyncTask<Object, VpnStatus.VpnState, String> { private Process process; private ManagementSocket sock; private OpenvpnProfile profile; private String username; private String password; private Charset charset = Charset.forName("UTF-8"); public Task(OpenvpnProfile profile, String username, String password) { this.profile = profile; this.username = username; this.password = password; } private String[] prepare(OpenvpnProfile profile) throws Exception { ArrayList<String> config = new ArrayList<String>(); config.add(new File(getCacheDir(), "openvpn").getAbsolutePath()); config.add("--client"); config.add("--tls-client"); config.add("--script-security"); config.add("0"); config.add("--management"); config.add(managementPath.getAbsolutePath()); config.add("unixseq"); config.add("--management-query-passwords"); config.add("--management-hold"); config.add("--management-signal"); config.add("--remap-usr1"); config.add("SIGTERM"); config.add("--route-noexec"); config.add("--ifconfig-noexec"); config.add("--verb"); config.add("3"); config.add("--dev"); config.add("[[ANDROID]]"); config.add("--dev-type"); config.add("tun"); if (profile.getUserAuth()) { config.add("--auth-user-pass"); } if (profile.getLocalAddr() != null) { config.add("--local"); config.add(profile.getLocalAddr()); } else { config.add("--nobind"); } config.add("--proto"); config.add(profile.getProto()); config.add("--remote"); config.add(profile.getServerName()); config.add(profile.getPort()); if (profile.getUseCompLzo()) config.add("--comp-lzo"); if (!"None".equals(profile.getNsCertType())) { config.add("--ns-cert-type"); config.add(profile.getNsCertType()); } if (profile.getRedirectGateway()) { config.add("--redirect-gateway"); } if (profile.getCipher() != null) { config.add("--cipher"); config.add(profile.getCipher()); } if (!profile.getKeySize().equals("0")) { config.add("--keysize"); config.add(profile.getKeySize()); } try { if (profile.getUserCertName() != null) { KeyStore pkcs12Store = KeyStore.getInstance("PKCS12"); pkcs12Store.load(null, null); PrivateKey pk = KeyChain.getPrivateKey(OpenVpnService.this, profile.getUserCertName()); X509Certificate[] chain = KeyChain.getCertificateChain( OpenVpnService.this, profile.getUserCertName()); pkcs12Store.setKeyEntry("key", pk, null, chain); if (profile.getCertName() != null) { KeyStore localTrustStore = KeyStore.getInstance("BKS"); localTrustStore .load(new ByteArrayInputStream(profile .getCertName()), null); Certificate root = localTrustStore.getCertificate("c"); if (root != null) pkcs12Store.setCertificateEntry("root", root); } ByteArrayOutputStream f = new ByteArrayOutputStream(); pkcs12Store.store(f, "".toCharArray()); config.add("--pkcs12"); config.add("[[INLINE]]"); config.add(Base64.encodeToString(f.toByteArray(), Base64.DEFAULT)); f.close(); } else if (profile.getCertName() != null) { KeyStore localTrustStore = KeyStore.getInstance("BKS"); localTrustStore.load( new ByteArrayInputStream(profile.getCertName()), null); Certificate root = localTrustStore.getCertificate("c"); if (root == null) throw new RuntimeException( "Certificate authority error"); StringWriter s = new StringWriter(); PEMWriter w = new PEMWriter(s); w.writeObject(root); w.flush(); config.add("--ca"); config.add("[[INLINE]]"); config.add("# CA cert below\n" + s.toString()); w.close(); } } catch (Exception e) { Log.w(OpenVpnService.class.getName(), "Error passing certifications", e); throw e; } if (profile.getUseTlsAuth()) { config.add("--tls-auth"); config.add(profile.getTlsAuthKey()); if (!"None".equals(profile.getTlsAuthKeyDirection())) config.add(profile.getTlsAuthKeyDirection()); } if (profile.getExtra() != null) for (String s : profile.getExtra().trim() .split(" +(?=([^\"]*\"[^\"]*\")*[^\"]*$)")) config.add(s); return config.toArray(new String[0]); } private boolean isProcessAlive(Process process) { try { process.exitValue(); return false; } catch (IllegalThreadStateException e) { return true; } } private ByteBuffer str_to_bb(String msg) throws CharacterCodingException { CharsetEncoder encoder = charset.newEncoder(); ByteBuffer buffer = ByteBuffer .allocateDirect(((int) (msg.length() * encoder .maxBytesPerChar())) + 1); CoderResult result = encoder.encode(CharBuffer.wrap(msg), buffer, true); if (!result.isUnderflow()) result.throwException(); result = encoder.flush(buffer); if (!result.isUnderflow()) result.throwException(); buffer.flip(); return buffer; } private String bb_to_str(ByteBuffer buffer) throws CharacterCodingException { CharsetDecoder decoder = charset.newDecoder(); return decoder.decode(buffer).toString(); } private int netmaskToPrefixLength(String netmask) throws UnknownHostException { byte[] mask = Inet4Address.getByName(netmask).getAddress(); if (mask.length != 4) { throw new IllegalArgumentException("Not an IPv4 address"); } int mask_int = ((mask[3] & 0xff) << 24) | ((mask[2] & 0xff) << 16) | ((mask[1] & 0xff) << 8) | (mask[0] & 0xff); return Integer.bitCount(mask_int); } private void doCommands() throws CharacterCodingException, UnknownHostException { ByteBuffer buffer = ByteBuffer.allocateDirect(2000); VpnService.Builder builder = null; while (true) { buffer.limit(0); FileDescriptorHolder fd = new FileDescriptorHolder(); try { int read = sock.read(buffer, fd); if (read <= 0) break; String lines[] = bb_to_str(buffer).split("\\r?\\n"); for (int i = 0; i < lines.length; ++i) { String cmd = lines[i]; if (cmd.startsWith(">LOG:")) { log.add(cmd.substring(">LOG:".length())); Log.i(getClass().getName(), cmd); } else if (cmd.startsWith(">INFO:")) { Log.i(getClass().getName(), cmd); } else if (cmd.equals(">HOLD:Waiting for hold release")) { sock.write(str_to_bb("echo on all\n" + "log on all\n" + "state on all\n" + "hold release\n")); } else if (cmd.startsWith(">ECHO:")) { String c = cmd.substring(cmd.indexOf(',') + 1); if (c.startsWith("tun-protect")) { protect(fd.get()); fd.close(); } else if (c.startsWith("tun-ip ")) { String[] ip = c.substring("tun-ip ".length()) .split(" "); builder.addAddress(ip[0], 32); } else if (c.startsWith("tun-mtu ")) { builder.setMtu(Integer.parseInt(c .substring("tun-mtu ".length()))); } else if (c.startsWith("tun-route ")) { String[] route = c.substring( "tun-route ".length()).split(" "); builder.addRoute(route[0], netmaskToPrefixLength(route[1])); } else if (c.startsWith("tun-redirect-gateway")) { builder.addRoute("0.0.0.0", 0); } else if (c.startsWith("tun-dns ")) { String dns = c.substring("tun-dns ".length()); builder.addDnsServer(dns); } else { Log.i(getClass().getName(), "Ignore ECHO: " + cmd); } } else if (cmd .equals(">NEED-TUN:Need 'TUN' confirmation")) { FileDescriptorHolder tun = new FileDescriptorHolder( builder.establish().detachFd()); sock.write(str_to_bb("tun TUN ok\n"), tun); tun.close(); } else if (cmd.startsWith(">STATE:")) { int start = cmd.indexOf(','); int end = cmd.indexOf(',', start + 1); String state = cmd.substring(start + 1, end); if (state.equals("GET_CONFIG")) { builder = new VpnService.Builder(); } else if (state.equals("CONNECTED")) { builder = null; publishProgress(VpnState.CONNECTED); } } else if (cmd.startsWith(">PASSWORD:")) { String c = cmd.substring(">PASSWORD:".length()); int first = c.indexOf('\''); int second = c.indexOf('\'', first + 1); final String authType = c.substring(first + 1, second); if (c.startsWith("Need")) { sock.write(str_to_bb("username '" + authType + "' \"" + username.replace("\"", "\\\"") .replace("\\", "\\\\") + "\"\n" + "password '" + authType + "' '" + password.replace("\"", "\\\"") .replace("\\", "\\\\") + "'\n")); } else { throw new RuntimeException("Password Error"); } } else { if (fd.valid()) Log.w(getClass().getName(), "Unknown Command: " + cmd + " (fd: " + fd.get() + ")"); else Log.w(getClass().getName(), "Unknown Command: " + cmd); } } } finally { if (fd.valid()) throw new RuntimeException("Unexpected fd"); } } } @Override protected String doInBackground(Object... params) { publishProgress(VpnState.PREPARING); try { process = Runtime.getRuntime().exec(prepare(profile)); for (int i = 0; i < 30 && isProcessAlive(process) && sock == null; ++i) try { // Wait openvpn to create management socket sock = new ManagementSocket(managementPath); } catch (Exception e) { Thread.sleep(1000); } if (sock == null) { InputStream stdout = process.getInputStream(); byte[] buffer = new byte[stdout.available()]; stdout.read(buffer); for (String s : new String(buffer, "UTF-8") .split("\\r?\\n")) { log.add(s); } if (isProcessAlive(process)) process.destroy(); return "Failed to start openvpn process"; } publishProgress(VpnState.CONNECTING); try { doCommands(); } finally { synchronized (this) { sock.shutdownAll(); sock.close(); sock = null; } } return null; } catch (Exception e) { publishProgress(VpnState.UNUSABLE); Log.wtf(getClass().getName(), e); return e.getLocalizedMessage(); } finally { publishProgress(VpnState.DISCONNECTING); try { if (process != null) process.waitFor(); } catch (InterruptedException e) { Log.wtf(getClass().getName(), e); return e.getLocalizedMessage(); } publishProgress(VpnState.IDLE); } } public synchronized void interrupt() { if (sock != null) try { sock.write(str_to_bb("exit\n")); } catch (CharacterCodingException e) { Log.wtf(getClass().getName(), "WTF", e); } } private void update(int resId) { // The intent to launch when the user clicks the expanded // notification Intent intent = new Intent(OpenVpnService.this, VpnSettings.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); PendingIntent pendIntent = PendingIntent.getActivity( OpenVpnService.this, 0, intent, 0); Notification notice = new Notification.Builder(OpenVpnService.this) .setSmallIcon(R.drawable.openvpn_icon).setTicker("OpenVPN") .setWhen(System.currentTimeMillis()) .setContentTitle(getString(resId)) .setContentText(getString(resId)) .setContentIntent(pendIntent).setOngoing(true) .getNotification(); startForeground(48998, notice); } @Override protected void onPreExecute() { log = new LogQueue(63); update(R.string.vpn_preparing); } @Override protected void onPostExecute(String result) { stopForeground(true); if (result == null) return; Intent intent = new Intent(OpenVpnService.this, VpnSettings.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); PendingIntent pendIntent = PendingIntent.getActivity( OpenVpnService.this, 0, intent, 0); Notification notice = new Notification.Builder(OpenVpnService.this) .setSmallIcon(R.drawable.openvpn_icon).setTicker("OpenVPN") .setWhen(System.currentTimeMillis()) .setContentTitle(getString(R.string.vpn_error)) .setContentText(result).setContentIntent(pendIntent) .getNotification(); ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) .notify(91684, notice); Util.showLongToastMessage(OpenVpnService.this, result); } @Override protected void onCancelled() { stopForeground(true); } @Override protected void onProgressUpdate(VpnStatus.VpnState... state) { mState = state[0]; Intent intent = new Intent( "info.kghost.android.openvpn.connectivity"); VpnStatus s = new VpnStatus(); if (profile != null) s.name = profile.getName(); s.state = mState; intent.putExtra("connection_state", s); sendBroadcast(intent); switch (mState) { case PREPARING: update(R.string.vpn_preparing); break; case CONNECTING: update(R.string.vpn_connecting); break; case DISCONNECTING: case CANCELLED: update(R.string.vpn_disconnecting); break; case CONNECTED: update(R.string.vpn_connected); break; case IDLE: update(R.string.vpn_disconnected); break; case UNUSABLE: update(R.string.vpn_unusable); break; case UNKNOWN: break; } } }; private String mName = null; private VpnStatus.VpnState mState = null; private ExecutorService executor; private Task mTask = null; private File managementPath = null; private LogQueue log = null; static { System.loadLibrary("jni_openvpn"); } @Override public IBinder onBind(Intent intent) { return mBinder; } private final IVpnService.Stub mBinder = new IVpnService.Stub() { @Override public boolean connect(OpenvpnProfile profile, String username, String password) throws RemoteException { if (profile == null) return false; if (mTask != null && mTask.getStatus() == AsyncTask.Status.FINISHED) mTask = null; if (mTask == null) mTask = new Task((OpenvpnProfile) profile, username, password); if (mTask.getStatus() == AsyncTask.Status.PENDING) { mName = profile.getName(); mTask.executeOnExecutor(executor); return true; } return false; } @Override public void disconnect() throws RemoteException { if (mTask != null) mTask.interrupt(); mName = null; } @Override public VpnStatus checkStatus() throws RemoteException { VpnStatus s = new VpnStatus(); s.name = mName; if (mState != null) s.state = mState; else s.state = VpnStatus.VpnState.IDLE; return s; } @Override public LogQueue getLog() throws RemoteException { return log; } }; private void restoreLog() { File logfile = new File(getCacheDir(), "log.2"); if (logfile.exists()) { int length; InputStream is = null; try { is = new FileInputStream(logfile); length = (int) logfile.length(); byte[] bytes = new byte[(int) length]; int offset = 0; int numRead = 0; while (offset < length && (numRead = is.read(bytes, offset, length - offset)) >= 0) { offset += numRead; } if (offset == length) { Parcel parcel = Parcel.obtain(); parcel.unmarshall(bytes, 0, length); parcel.setDataPosition(0); Parcelable o = parcel.readParcelable(LogQueue.class .getClassLoader()); if (o instanceof LogQueue) { log = (LogQueue) o; } } } catch (IOException e) { } finally { if (is != null) try { is.close(); } catch (IOException e) { } } } } private void saveLog() { if (log != null) { File logfile = new File(getCacheDir(), "log.2"); Parcel parcel = Parcel.obtain(); parcel.writeParcelable(log, 0); byte[] bytes = parcel.marshall(); FileOutputStream os = null; try { os = new FileOutputStream(logfile); os.write(bytes); } catch (IOException e) { } finally { if (os != null) try { os.close(); } catch (IOException e) { } } } } @Override public void onCreate() { super.onCreate(); restoreLog(); managementPath = new File(getCacheDir(), "manage"); executor = Executors.newSingleThreadExecutor(); } @Override public void onRevoke() { if (mTask != null) mTask.interrupt(); } @Override public void onDestroy() { if (mTask != null) mTask.interrupt(); executor = null; saveLog(); super.onDestroy(); } }