package de.blinkt.openvpn.core; import android.Manifest.permission; import android.annotation.TargetApi; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.net.ConnectivityManager; import android.net.VpnService; import android.os.*; import android.os.Handler.Callback; import android.preference.PreferenceManager; import android.text.TextUtils; import de.blinkt.openvpn.DisconnectVPN; import de.blinkt.openvpn.LogWindow; import de.blinkt.openvpn.R; import de.blinkt.openvpn.VpnProfile; import de.blinkt.openvpn.core.VpnStatus.ByteCountListener; import de.blinkt.openvpn.core.VpnStatus.ConnectionStatus; import de.blinkt.openvpn.core.VpnStatus.StateListener; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.MessageDigest; import java.util.HashMap; import java.util.Locale; import java.util.Vector; import static de.blinkt.openvpn.core.VpnStatus.ConnectionStatus.*; public class OpenVpnService extends VpnService implements StateListener, Callback, ByteCountListener { public static final String START_SERVICE = "de.blinkt.openvpn.START_SERVICE"; public static final String START_SERVICE_STICKY = "de.blinkt.openvpn.START_SERVICE_STICKY"; public static final String ALWAYS_SHOW_NOTIFICATION = "de.blinkt.openvpn.NOTIFICATION_ALWAYS_VISIBLE"; public static final String DISCONNECT_VPN = "de.blinkt.openvpn.DISCONNECT_VPN"; private static final String PAUSE_VPN = "de.blinkt.openvpn.PAUSE_VPN"; private static final String RESUME_VPN = "de.blinkt.openvpn.RESUME_VPN"; private static final int OPENVPN_STATUS = 1; private static boolean mNotificationAlwaysVisible = false; private final Vector mDnslist = new Vector(); private final Vector mRoutes = new Vector(); private final Vector mRoutesv6 = new Vector(); private final IBinder mBinder = new LocalBinder(); private Thread mProcessThread = null; private VpnProfile mProfile; private String mDomain = null; private CIDRIP mLocalIP = null; private int mMtu; private String mLocalIPv6 = null; private DeviceStateReceiver mDeviceStateReceiver; private boolean mDisplayBytecount = false; private boolean mStarting = false; private long mConnecttime; private boolean mOvpn3 = false; private OpenVPNManagement mManagement; private String mLastTunCfg; // From: http://stackoverflow.com/questions/3758606/how-to-convert-byte-size-into-human-readable-format-in-java public static String humanReadableByteCount(long bytes, boolean mbit) { if (mbit) bytes = bytes * 8; int unit = mbit ? 1000 : 1024; if (bytes < unit) return bytes + (mbit ? " bit" : " B"); int exp = (int) (Math.log(bytes) / Math.log(unit)); String pre = (mbit ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (mbit ? "" : ""); if (mbit) return String.format(Locale.getDefault(), "%.1f %sbit", bytes / Math.pow(unit, exp), pre); else return String.format(Locale.getDefault(), "%.1f %sB", bytes / Math.pow(unit, exp), pre); } @Override public IBinder onBind(Intent intent) { String action = intent.getAction(); if (action != null && action.equals(START_SERVICE)) return mBinder; else return super.onBind(intent); } @Override public void onRevoke() { mManagement.stopVPN(); endVpnService(); } // Similar to revoke but do not try to stop process public void processDied() { endVpnService(); } private void endVpnService() { mProcessThread = null; VpnStatus.removeByteCountListener(this); unregisterDeviceStateReceiver(); ProfileManager.setConntectedVpnProfileDisconnected(this); if (!mStarting) { stopForeground(!mNotificationAlwaysVisible); if (!mNotificationAlwaysVisible) { stopSelf(); VpnStatus.removeStateListener(this); } } } private void showNotification(String msg, String tickerText, boolean lowpriority, long when, ConnectionStatus status) { String ns = Context.NOTIFICATION_SERVICE; NotificationManager mNotificationManager = (NotificationManager) getSystemService(ns); int icon = getIconByConnectionStatus(status); android.app.Notification.Builder nbuilder = new Notification.Builder(this); if (mProfile != null) nbuilder.setContentTitle(getString(R.string.notifcation_title, mProfile.mName)); else nbuilder.setContentTitle(getString(R.string.notifcation_title_notconnect)); nbuilder.setContentText(msg); nbuilder.setOnlyAlertOnce(true); nbuilder.setOngoing(true); nbuilder.setContentIntent(getLogPendingIntent()); nbuilder.setSmallIcon(icon); if (when != 0) nbuilder.setWhen(when); // Try to set the priority available since API 16 (Jellybean) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) jbNotificationExtras(lowpriority, nbuilder); if (tickerText != null && !tickerText.equals("")) nbuilder.setTicker(tickerText); @SuppressWarnings("deprecation") Notification notification = nbuilder.getNotification(); mNotificationManager.notify(OPENVPN_STATUS, notification); startForeground(OPENVPN_STATUS, notification); } private int getIconByConnectionStatus(ConnectionStatus level) { switch (level) { case LEVEL_CONNECTED: return R.drawable.ic_stat_vpn; case LEVEL_AUTH_FAILED: case LEVEL_NONETWORK: case LEVEL_NOTCONNECTED: return R.drawable.ic_stat_vpn_offline; case LEVEL_CONNECTING_NO_SERVER_REPLY_YET: case LEVEL_WAITING_FOR_USER_INPUT: return R.drawable.ic_stat_vpn_outline; case LEVEL_CONNECTING_SERVER_REPLIED: return R.drawable.ic_stat_vpn_empty_halo; case LEVEL_VPNPAUSED: return android.R.drawable.ic_media_pause; case UNKNOWN_LEVEL: default: return R.drawable.ic_stat_vpn; } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private void jbNotificationExtras(boolean lowpriority, android.app.Notification.Builder nbuilder) { try { if (lowpriority) { Method setpriority = nbuilder.getClass().getMethod("setPriority", int.class); // PRIORITY_MIN == -2 setpriority.invoke(nbuilder, -2); Method setUsesChronometer = nbuilder.getClass().getMethod("setUsesChronometer", boolean.class); setUsesChronometer.invoke(nbuilder, true); } Intent disconnectVPN = new Intent(this, DisconnectVPN.class); disconnectVPN.setAction(DISCONNECT_VPN); PendingIntent disconnectPendingIntent = PendingIntent.getActivity(this, 0, disconnectVPN, 0); nbuilder.addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.cancel_connection), disconnectPendingIntent); Intent pauseVPN = new Intent(this, OpenVpnService.class); if (mDeviceStateReceiver == null || !mDeviceStateReceiver.isUserPaused()) { pauseVPN.setAction(PAUSE_VPN); PendingIntent pauseVPNPending = PendingIntent.getService(this, 0, pauseVPN, 0); nbuilder.addAction(android.R.drawable.ic_media_pause, getString(R.string.pauseVPN), pauseVPNPending); } else { pauseVPN.setAction(RESUME_VPN); PendingIntent resumeVPNPending = PendingIntent.getService(this, 0, pauseVPN, 0); nbuilder.addAction(android.R.drawable.ic_media_play, getString(R.string.resumevpn), resumeVPNPending); } //ignore exception } catch (NoSuchMethodException nsm) { nsm.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } PendingIntent getLogPendingIntent() { // Let the configure Button show the Log Intent intent = new Intent(getBaseContext(), LogWindow.class); intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); PendingIntent startLW = PendingIntent.getActivity(this, 0, intent, 0); intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); return startLW; } synchronized void registerDeviceStateReceiver(OpenVPNManagement magnagement) { // Registers BroadcastReceiver to track network connection changes. IntentFilter filter = new IntentFilter(); filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_SCREEN_ON); mDeviceStateReceiver = new DeviceStateReceiver(magnagement); registerReceiver(mDeviceStateReceiver, filter); VpnStatus.addByteCountListener(mDeviceStateReceiver); } synchronized void unregisterDeviceStateReceiver() { if (mDeviceStateReceiver != null) try { VpnStatus.removeByteCountListener(mDeviceStateReceiver); this.unregisterReceiver(mDeviceStateReceiver); } catch (IllegalArgumentException iae) { // I don't know why this happens: // java.lang.IllegalArgumentException: Receiver not registered: de.blinkt.openvpn.NetworkSateReceiver@41a61a10 // Ignore for now ... iae.printStackTrace(); } mDeviceStateReceiver = null; } public void userPause (boolean shouldBePaused) { if (mDeviceStateReceiver != null) mDeviceStateReceiver.userPause(shouldBePaused); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null && intent.getBooleanExtra(ALWAYS_SHOW_NOTIFICATION, false)) mNotificationAlwaysVisible = true; VpnStatus.addStateListener(this); VpnStatus.addByteCountListener(this); if (intent != null && PAUSE_VPN.equals(intent.getAction())) { if (mDeviceStateReceiver != null) mDeviceStateReceiver.userPause(true); return START_NOT_STICKY; } if (intent != null && RESUME_VPN.equals(intent.getAction())) { if (mDeviceStateReceiver != null) mDeviceStateReceiver.userPause(false); return START_NOT_STICKY; } if (intent != null && START_SERVICE.equals(intent.getAction())) return START_NOT_STICKY; if (intent != null && START_SERVICE_STICKY.equals(intent.getAction())) { return START_REDELIVER_INTENT; } assert (intent != null); // Extract information from the intent. String prefix = getPackageName(); String[] argv = intent.getStringArrayExtra(prefix + ".ARGV"); String nativelibdir = intent.getStringExtra(prefix + ".nativelib"); String profileUUID = intent.getStringExtra(prefix + ".profileUUID"); mProfile = ProfileManager.get(this, profileUUID); String startTitle = getString(R.string.start_vpn_title, mProfile.mName); String startTicker = getString(R.string.start_vpn_ticker, mProfile.mName); showNotification(startTitle, startTicker, false, 0, LEVEL_CONNECTING_NO_SERVER_REPLY_YET); // Set a flag that we are starting a new VPN mStarting = true; // Stop the previous session by interrupting the thread. if (mManagement != null && mManagement.stopVPN()) // an old was asked to exit, wait 1s try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } if (mProcessThread != null) { mProcessThread.interrupt(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } // An old running VPN should now be exited mStarting = false; // Open the Management Interface if (!mOvpn3) { // start a Thread that handles incoming messages of the managment socket OpenVpnManagementThread ovpnManagementThread = new OpenVpnManagementThread(mProfile, this); if (ovpnManagementThread.openManagementInterface(this)) { Thread mSocketManagerThread = new Thread(ovpnManagementThread, "OpenVPNManagementThread"); mSocketManagerThread.start(); mManagement = ovpnManagementThread; VpnStatus.logInfo("started Socket Thread"); } } // Start a new session by creating a new thread. SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); mOvpn3 = prefs.getBoolean("ovpn3", false); mOvpn3 = false; Runnable processThread; if (mOvpn3) { OpenVPNManagement mOpenVPN3 = instantiateOpenVPN3Core(); processThread = (Runnable) mOpenVPN3; mManagement = mOpenVPN3; } else { HashMap env = new HashMap(); processThread = new OpenVPNThread(this, argv, env, nativelibdir); } mProcessThread = new Thread(processThread, "OpenVPNProcessThread"); mProcessThread.start(); if (mDeviceStateReceiver != null) unregisterDeviceStateReceiver(); registerDeviceStateReceiver(mManagement); ProfileManager.setConnectedVpnProfile(this, mProfile); return START_NOT_STICKY; } private OpenVPNManagement instantiateOpenVPN3Core() { return null; } @Override public void onDestroy() { if (mProcessThread != null) { mManagement.stopVPN(); mProcessThread.interrupt(); } if (mDeviceStateReceiver != null) { this.unregisterReceiver(mDeviceStateReceiver); } // Just in case unregister for state VpnStatus.removeStateListener(this); } private String getTunConfigString() { // The format of the string is not important, only that // two identical configurations produce the same result String cfg="TUNCFG UNQIUE STRING ips:"; if (mLocalIP!=null) cfg+=mLocalIP.toString(); if (mLocalIPv6!=null) cfg+=mLocalIPv6.toString(); cfg+= "routes: " + TextUtils.join("|",mRoutes) + TextUtils.join("|",mRoutesv6); cfg+= "dns: " + TextUtils.join("|",mDnslist); cfg+= "domain: " + mDomain; cfg+= "mtu: " + mMtu; return cfg; } public ParcelFileDescriptor openTun() { Builder builder = new Builder(); if (mLocalIP == null && mLocalIPv6 == null) { VpnStatus.logError(getString(R.string.opentun_no_ipaddr)); return null; } if (mLocalIP != null) { try { builder.addAddress(mLocalIP.mIp, mLocalIP.len); } catch (IllegalArgumentException iae) { VpnStatus.logError(R.string.dns_add_error, mLocalIP, iae.getLocalizedMessage()); return null; } } if (mLocalIPv6 != null) { String[] ipv6parts = mLocalIPv6.split("/"); try { builder.addAddress(ipv6parts[0], Integer.parseInt(ipv6parts[1])); } catch (IllegalArgumentException iae) { VpnStatus.logError(R.string.ip_add_error, mLocalIPv6, iae.getLocalizedMessage()); return null; } } for (String dns : mDnslist) { try { builder.addDnsServer(dns); } catch (IllegalArgumentException iae) { VpnStatus.logError(R.string.dns_add_error, dns, iae.getLocalizedMessage()); } } builder.setMtu(mMtu); for (CIDRIP route : mRoutes) { try { builder.addRoute(route.mIp, route.len); } catch (IllegalArgumentException ia) { VpnStatus.logError(getString(R.string.route_rejected) + route + " " + ia.getLocalizedMessage()); } } for (String v6route : mRoutesv6) { try { String[] v6parts = v6route.split("/"); builder.addRoute(v6parts[0], Integer.parseInt(v6parts[1])); } catch (IllegalArgumentException ia) { VpnStatus.logError(getString(R.string.route_rejected) + v6route + " " + ia.getLocalizedMessage()); } } if (mDomain != null) builder.addSearchDomain(mDomain); VpnStatus.logInfo(R.string.last_openvpn_tun_config); VpnStatus.logInfo(R.string.local_ip_info, mLocalIP.mIp, mLocalIP.len, mLocalIPv6, mMtu); VpnStatus.logInfo(R.string.dns_server_info, joinString(mDnslist), mDomain); VpnStatus.logInfo(R.string.routes_info, joinString(mRoutes)); VpnStatus.logInfo(R.string.routes_info6, joinString(mRoutesv6)); String session = mProfile.mName; if (mLocalIP != null && mLocalIPv6 != null) session = getString(R.string.session_ipv6string, session, mLocalIP, mLocalIPv6); else if (mLocalIP != null) session = getString(R.string.session_ipv4string, session, mLocalIP); builder.setSession(session); // No DNS Server, log a warning if (mDnslist.size() == 0) VpnStatus.logInfo(R.string.warn_no_dns); mLastTunCfg = getTunConfigString(); // Reset information mDnslist.clear(); mRoutes.clear(); mRoutesv6.clear(); mLocalIP = null; mLocalIPv6 = null; mDomain = null; builder.setConfigureIntent(getLogPendingIntent()); try { return builder.establish(); } catch (Exception e) { VpnStatus.logError(R.string.tun_open_error); VpnStatus.logError(getString(R.string.error) + e.getLocalizedMessage()); if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) { VpnStatus.logError(R.string.tun_error_helpful); } return null; } } // Ugly, but java has no such method private String joinString(Vector vec) { String ret = ""; if (vec.size() > 0) { ret = vec.get(0).toString(); for (int i = 1; i < vec.size(); i++) { ret = ret + ", " + vec.get(i).toString(); } } return ret; } public void addDNS(String dns) { mDnslist.add(dns); } public void setDomain(String domain) { if (mDomain == null) { mDomain = domain; } } public void addRoute(String dest, String mask) { CIDRIP route = new CIDRIP(dest, mask); if (route.len == 32 && !mask.equals("255.255.255.255")) { VpnStatus.logWarning(R.string.route_not_cidr, dest, mask); } if (route.normalise()) VpnStatus.logWarning(R.string.route_not_netip, dest, route.len, route.mIp); mRoutes.add(route); } public void addRoutev6(String extra) { mRoutesv6.add(extra); } public void setMtu(int mtu) { mMtu = mtu; } public void setLocalIP(CIDRIP cdrip) { mLocalIP = cdrip; } public void setLocalIP(String local, String netmask, int mtu, String mode) { mLocalIP = new CIDRIP(local, netmask); mMtu = mtu; if (mLocalIP.len == 32 && !netmask.equals("255.255.255.255")) { // get the netmask as IP long netint = CIDRIP.getInt(netmask); if (Math.abs(netint - mLocalIP.getInt()) == 1) { if ("net30".equals(mode)) mLocalIP.len = 30; else mLocalIP.len = 31; } else { VpnStatus.logWarning(R.string.ip_not_cidr, local, netmask, mode); } } } public void setLocalIPv6(String ipv6addr) { mLocalIPv6 = ipv6addr; } @Override public void updateState(String state, String logmessage, int resid, ConnectionStatus level) { // If the process is not running, ignore any state, // Notification should be invisible in this state doSendBroadcast(state, level); if (mProcessThread == null && !mNotificationAlwaysVisible) return; // Display byte count only after being connected { if (level == LEVEL_WAITING_FOR_USER_INPUT) { // The user is presented a dialog of some kind, no need to inform the user // with a notifcation return; } else if (level == LEVEL_CONNECTED) { mDisplayBytecount = true; mConnecttime = System.currentTimeMillis(); } else { mDisplayBytecount = false; } // Other notifications are shown, // This also mean we are no longer connected, ignore bytecount messages until next // CONNECTED String ticker = getString(resid); showNotification(getString(resid) + " " + logmessage, ticker, false, 0, level); } } private void doSendBroadcast(String state, ConnectionStatus level) { Intent vpnstatus = new Intent(); vpnstatus.setAction("de.blinkt.openvpn.VPN_STATUS"); vpnstatus.putExtra("status", level.toString()); vpnstatus.putExtra("detailstatus", state); sendBroadcast(vpnstatus, permission.ACCESS_NETWORK_STATE); } @Override public void updateByteCount(long in, long out, long diffIn, long diffOut) { if (mDisplayBytecount) { String netstat = String.format(getString(R.string.statusline_bytecount), humanReadableByteCount(in, false), humanReadableByteCount(diffIn / OpenVPNManagement.mBytecountInterval, true), humanReadableByteCount(out, false), humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true)); boolean lowpriority = !mNotificationAlwaysVisible; showNotification(netstat, null, lowpriority, mConnecttime, LEVEL_CONNECTED); } } @Override public boolean handleMessage(Message msg) { Runnable r = msg.getCallback(); if (r != null) { r.run(); return true; } else { return false; } } public OpenVPNManagement getManagement() { return mManagement; } public String getTunReopenStatus() { String currentConfiguration = getTunConfigString(); if(currentConfiguration.equals(mLastTunCfg)) return "NOACTION"; else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) return "OPEN_AFTER_CLOSE"; else return "OPEN_BEFORE_CLOSE"; } public class LocalBinder extends Binder { public OpenVpnService getService() { // Return this instance of LocalService so clients can call public methods return OpenVpnService.this; } } }