diff options
Diffstat (limited to 'app/src/main/java')
17 files changed, 1479 insertions, 535 deletions
diff --git a/app/src/main/java/de/blinkt/openvpn/LaunchVPN.java b/app/src/main/java/de/blinkt/openvpn/LaunchVPN.java index e83cffa0..5bf178ae 100644 --- a/app/src/main/java/de/blinkt/openvpn/LaunchVPN.java +++ b/app/src/main/java/de/blinkt/openvpn/LaunchVPN.java @@ -7,6 +7,8 @@ package de.blinkt.openvpn; import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.R; + import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; @@ -231,4 +233,4 @@ public class LaunchVPN extends Activity { VpnStatus.logException("SU command", e); } } -} +}
\ No newline at end of file diff --git a/app/src/main/java/de/blinkt/openvpn/VpnProfile.java b/app/src/main/java/de/blinkt/openvpn/VpnProfile.java index 4becdc32..bd28ae16 100644 --- a/app/src/main/java/de/blinkt/openvpn/VpnProfile.java +++ b/app/src/main/java/de/blinkt/openvpn/VpnProfile.java @@ -6,7 +6,6 @@ package de.blinkt.openvpn; import se.leap.bitmaskclient.R; - import se.leap.bitmaskclient.BuildConfig; import android.annotation.SuppressLint; @@ -16,10 +15,12 @@ import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; +import android.os.RemoteException; import android.preference.PreferenceManager; import android.security.KeyChain; import android.security.KeyChainException; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Base64; @@ -62,14 +63,9 @@ public class VpnProfile implements Serializable, Cloneable { public static final String EXTRA_PROFILEUUID = "de.blinkt.openvpn.profileUUID"; public static final String INLINE_TAG = "[[INLINE]]"; public static final String DISPLAYNAME_TAG = "[[NAME]]"; - - private static final long serialVersionUID = 7085688938959334563L; public static final int MAXLOGLEVEL = 4; - public static final int CURRENT_PROFILE_VERSION = 6; + public static final int CURRENT_PROFILE_VERSION = 7; public static final int DEFAULT_MSSFIX_SIZE = 1280; - public static String DEFAULT_DNS1 = "8.8.8.8"; - public static String DEFAULT_DNS2 = "8.8.4.4"; - public static final int TYPE_CERTIFICATES = 0; public static final int TYPE_PKCS12 = 1; public static final int TYPE_KEYSTORE = 2; @@ -78,17 +74,20 @@ public class VpnProfile implements Serializable, Cloneable { public static final int TYPE_USERPASS_CERTIFICATES = 5; public static final int TYPE_USERPASS_PKCS12 = 6; public static final int TYPE_USERPASS_KEYSTORE = 7; + public static final int TYPE_EXTERNAL_APP = 8; public static final int X509_VERIFY_TLSREMOTE = 0; public static final int X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING = 1; public static final int X509_VERIFY_TLSREMOTE_DN = 2; public static final int X509_VERIFY_TLSREMOTE_RDN = 3; public static final int X509_VERIFY_TLSREMOTE_RDN_PREFIX = 4; - - public static final int AUTH_RETRY_NONE_FORGET = 0; - private static final int AUTH_RETRY_NONE_KEEP = 1; public static final int AUTH_RETRY_NOINTERACT = 2; + public static final boolean mIsOpenVPN22 = false; + private static final long serialVersionUID = 7085688938959334563L; + private static final int AUTH_RETRY_NONE_KEEP = 1; private static final int AUTH_RETRY_INTERACT = 3; + public static String DEFAULT_DNS1 = "8.8.8.8"; + public static String DEFAULT_DNS2 = "8.8.4.4"; // variable named wrong and should haven beeen transient // but needs to keep wrong name to guarante loading of old // profiles @@ -105,7 +104,6 @@ public class VpnProfile implements Serializable, Cloneable { public String mPKCS12Filename; public String mPKCS12Password; public boolean mUseTLSAuth = false; - public String mDNS1 = DEFAULT_DNS1; public String mDNS2 = DEFAULT_DNS2; public String mIPv4Address; @@ -127,7 +125,7 @@ public class VpnProfile implements Serializable, Cloneable { public String mCustomConfigOptions = ""; public String mVerb = "1"; //ignored public String mCipher = ""; - public boolean mNobind = false; + public boolean mNobind = true; public boolean mUseDefaultRoutev6 = true; public String mCustomRoutesv6 = ""; public String mKeyPassword = ""; @@ -139,13 +137,7 @@ public class VpnProfile implements Serializable, Cloneable { public String mAuth = ""; public int mX509AuthType = X509_VERIFY_TLSREMOTE_RDN; public String mx509UsernameField = null; - - private transient PrivateKey mPrivateKey; - // Public attributes, since I got mad with getter/setter - // set members to default values - private UUID mUuid; public boolean mAllowLocalLAN; - private int mProfileVersion; public String mExcludedRoutes; public String mExcludedRoutesv6; public int mMssFix = 0; // -1 is default, @@ -153,26 +145,26 @@ public class VpnProfile implements Serializable, Cloneable { public boolean mRemoteRandom = false; public HashSet<String> mAllowedAppsVpn = new HashSet<>(); public boolean mAllowedAppsVpnAreDisallowed = true; - public String mCrlFilename; public String mProfileCreator; - + public String mExternalAuthenticator; public int mAuthRetry = AUTH_RETRY_NONE_FORGET; public int mTunMtu; - - public boolean mPushPeerInfo = false; - public static final boolean mIsOpenVPN22 = false; - public int mVersion = 0; - // timestamp when the profile was last used public long mLastUsed; - + public String importedProfileHash; /* Options no longer used in new profiles */ public String mServerName = "openvpn.example.com"; public String mServerPort = "1194"; public boolean mUseUdp = true; + public boolean mTemporaryProfile = false; + private transient PrivateKey mPrivateKey; + // Public attributes, since I got mad with getter/setter + // set members to default values + private UUID mUuid; + private int mProfileVersion; public VpnProfile(String name) { @@ -200,6 +192,48 @@ public class VpnProfile implements Serializable, Cloneable { return '"' + escapedString + '"'; } + public static boolean doUseOpenVPN3(Context c) { + SharedPreferences prefs = Preferences.getDefaultSharedPreferences(c); + boolean useOpenVPN3 = prefs.getBoolean("ovpn3", false); + if (!BuildConfig.openvpn3) + useOpenVPN3 = false; + return useOpenVPN3; + } + + //! Put inline data inline and other data as normal escaped filename + public static String insertFileData(String cfgentry, String filedata) { + if (filedata == null) { + return String.format("%s %s\n", cfgentry, "file missing in config profile"); + } else if (isEmbedded(filedata)) { + String dataWithOutHeader = getEmbeddedContent(filedata); + return String.format(Locale.ENGLISH, "<%s>\n%s\n</%s>\n", cfgentry, dataWithOutHeader, cfgentry); + } else { + return String.format(Locale.ENGLISH, "%s %s\n", cfgentry, openVpnEscape(filedata)); + } + } + + public static String getDisplayName(String embeddedFile) { + int start = DISPLAYNAME_TAG.length(); + int end = embeddedFile.indexOf(INLINE_TAG); + return embeddedFile.substring(start, end); + } + + public static String getEmbeddedContent(String data) { + if (!data.contains(INLINE_TAG)) + return data; + + int start = data.indexOf(INLINE_TAG) + INLINE_TAG.length(); + return data.substring(start); + } + + public static boolean isEmbedded(String data) { + if (data == null) + return false; + if (data.startsWith(INLINE_TAG) || data.startsWith(DISPLAYNAME_TAG)) + return true; + else + return false; + } @Override public boolean equals(Object obj) { @@ -223,6 +257,7 @@ public class VpnProfile implements Serializable, Cloneable { mAllowLocalLAN = true; mPushPeerInfo = false; mMssFix = 0; + mNobind = false; } public UUID getUUID() { @@ -230,6 +265,11 @@ public class VpnProfile implements Serializable, Cloneable { } + // Only used for the special case of managed profiles + public void setUUID(UUID uuid) { + mUuid = uuid; + } + public String getName() { if (TextUtils.isEmpty(mName)) return "No profile name"; @@ -248,6 +288,7 @@ public class VpnProfile implements Serializable, Cloneable { } if (mAllowedAppsVpn == null) mAllowedAppsVpn = new HashSet<>(); + if (mConnections == null) mConnections = new Connection[0]; @@ -255,7 +296,11 @@ public class VpnProfile implements Serializable, Cloneable { if (TextUtils.isEmpty(mProfileCreator)) mUserEditable = true; } - + if (mProfileVersion < 7) { + for (Connection c : mConnections) + if (c.mProxyType == null) + c.mProxyType = Connection.ProxyType.NONE; + } mProfileVersion = CURRENT_PROFILE_VERSION; @@ -274,68 +319,61 @@ public class VpnProfile implements Serializable, Cloneable { } - - public static boolean doUseOpenVPN3(Context c) { - SharedPreferences prefs = Preferences.getDefaultSharedPreferences(c); - boolean useOpenVPN3 = prefs.getBoolean("ovpn3", false); - if (!BuildConfig.openvpn3) - useOpenVPN3 = false; - return useOpenVPN3; - } - public String getConfigFile(Context context, boolean configForOvpn3) { File cacheDir = context.getCacheDir(); - String cfg = ""; + StringBuilder cfg = new StringBuilder(); if (!configForOvpn3) { // Enable management interface - cfg += "# Config for OpenVPN 2.x\n"; - cfg += "# Enables connection to GUI\n"; - cfg += "management "; + cfg.append("# Config for OpenVPN 2.x\n"); + cfg.append("# Enables connection to GUI\n"); + cfg.append("management "); - cfg += cacheDir.getAbsolutePath() + "/" + "mgmtsocket"; - cfg += " unix\n"; - cfg += "management-client\n"; + cfg.append(cacheDir.getAbsolutePath()).append("/").append("mgmtsocket"); + cfg.append(" unix\n"); + cfg.append("management-client\n"); // Not needed, see updated man page in 2.3 //cfg += "management-signal\n"; - cfg += "management-query-passwords\n"; - cfg += "management-hold\n\n"; + cfg.append("management-query-passwords\n"); + cfg.append("management-hold\n\n"); - cfg += String.format("setenv IV_GUI_VER %s \n", openVpnEscape(getVersionEnvString(context))); + cfg.append(String.format("setenv IV_GUI_VER %s \n", openVpnEscape(getVersionEnvString(context)))); String versionString = getPlatformVersionEnvString(); - cfg += String.format("setenv IV_PLAT_VER %s\n", openVpnEscape(versionString)); + cfg.append(String.format("setenv IV_PLAT_VER %s\n", openVpnEscape(versionString))); } else { - cfg += "# Config for OpeNVPN 3 C++\n"; + cfg.append("# Config for OpenVPN 3 C++\n"); } - cfg += "machine-readable-output\n"; - cfg += "allow-recursive-routing\n"; - - // Users are confused by warnings that are misleading... - cfg += "ifconfig-nowarn\n"; + if (!configForOvpn3) { + cfg.append("machine-readable-output\n"); + if (!mIsOpenVPN22) + cfg.append("allow-recursive-routing\n"); + // Users are confused by warnings that are misleading... + cfg.append("ifconfig-nowarn\n"); + } boolean useTLSClient = (mAuthenticationType != TYPE_STATICKEYS); if (useTLSClient && mUsePull) - cfg += "client\n"; + cfg.append("client\n"); else if (mUsePull) - cfg += "pull\n"; + cfg.append("pull\n"); else if (useTLSClient) - cfg += "tls-client\n"; + cfg.append("tls-client\n"); //cfg += "verb " + mVerb + "\n"; - cfg += "verb " + MAXLOGLEVEL + "\n"; + cfg.append("verb " + MAXLOGLEVEL + "\n"); if (mConnectRetryMax == null) { mConnectRetryMax = "-1"; } if (!mConnectRetryMax.equals("-1")) - cfg += "connect-retry-max " + mConnectRetryMax + "\n"; + cfg.append("connect-retry-max ").append(mConnectRetryMax).append("\n"); if (TextUtils.isEmpty(mConnectRetry)) mConnectRetry = "2"; @@ -345,34 +383,34 @@ public class VpnProfile implements Serializable, Cloneable { if (!mIsOpenVPN22) - cfg += "connect-retry " + mConnectRetry + " " + mConnectRetryMaxTime + "\n"; - else if (mIsOpenVPN22 && mUseUdp) - cfg += "connect-retry " + mConnectRetry + "\n"; + cfg.append("connect-retry ").append(mConnectRetry).append(" ").append(mConnectRetryMaxTime).append("\n"); + else if (mIsOpenVPN22 && !mUseUdp) + cfg.append("connect-retry ").append(mConnectRetry).append("\n"); - cfg += "resolv-retry 60\n"; + cfg.append("resolv-retry 60\n"); // We cannot use anything else than tun - cfg += "dev tun\n"; + cfg.append("dev tun\n"); boolean canUsePlainRemotes = true; if (mConnections.length == 1) { - cfg += mConnections[0].getConnectionBlock(); + cfg.append(mConnections[0].getConnectionBlock(configForOvpn3)); } else { for (Connection conn : mConnections) { canUsePlainRemotes = canUsePlainRemotes && conn.isOnlyRemote(); } if (mRemoteRandom) - cfg += "remote-random\n"; + cfg.append("remote-random\n"); if (canUsePlainRemotes) { for (Connection conn : mConnections) { if (conn.mEnabled) { - cfg += conn.getConnectionBlock(); + cfg.append(conn.getConnectionBlock(configForOvpn3)); } } } @@ -381,91 +419,101 @@ public class VpnProfile implements Serializable, Cloneable { switch (mAuthenticationType) { case VpnProfile.TYPE_USERPASS_CERTIFICATES: - cfg += "auth-user-pass\n"; + cfg.append("auth-user-pass\n"); case VpnProfile.TYPE_CERTIFICATES: // Ca - cfg += insertFileData("ca", mCaFilename); + cfg.append(insertFileData("ca", mCaFilename)); // Client Cert + Key - cfg += insertFileData("key", mClientKeyFilename); - cfg += insertFileData("cert", mClientCertFilename); + cfg.append(insertFileData("key", mClientKeyFilename)); + cfg.append(insertFileData("cert", mClientCertFilename)); break; case VpnProfile.TYPE_USERPASS_PKCS12: - cfg += "auth-user-pass\n"; + cfg.append("auth-user-pass\n"); case VpnProfile.TYPE_PKCS12: - cfg += insertFileData("pkcs12", mPKCS12Filename); + cfg.append(insertFileData("pkcs12", mPKCS12Filename)); + + if (!TextUtils.isEmpty(mCaFilename)) + { + cfg.append(insertFileData("ca", mCaFilename)); + } break; case VpnProfile.TYPE_USERPASS_KEYSTORE: - cfg += "auth-user-pass\n"; + cfg.append("auth-user-pass\n"); case VpnProfile.TYPE_KEYSTORE: + case VpnProfile.TYPE_EXTERNAL_APP: if (!configForOvpn3) { - String[] ks = getKeyStoreCertificates(context); - cfg += "### From Keystore ####\n"; + String[] ks = getExternalCertificates(context); + cfg.append("### From Keystore/ext auth app ####\n"); if (ks != null) { - cfg += "<ca>\n" + ks[0] + "\n</ca>\n"; - if (ks[1] != null) - cfg += "<extra-certs>\n" + ks[1] + "\n</extra-certs>\n"; - cfg += "<cert>\n" + ks[2] + "\n</cert>\n"; - cfg += "management-external-key\n"; + cfg.append("<ca>\n").append(ks[0]).append("\n</ca>\n"); + if (!TextUtils.isEmpty(ks[1])) + cfg.append("<extra-certs>\n").append(ks[1]).append("\n</extra-certs>\n"); + cfg.append("<cert>\n").append(ks[2]).append("\n</cert>\n"); + cfg.append("management-external-key nopadding\n"); } else { - cfg += context.getString(R.string.keychain_access) + "\n"; + cfg.append(context.getString(R.string.keychain_access)).append("\n"); if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) if (!mAlias.matches("^[a-zA-Z0-9]$")) - cfg += context.getString(R.string.jelly_keystore_alphanumeric_bug) + "\n"; + cfg.append(context.getString(R.string.jelly_keystore_alphanumeric_bug)).append("\n"); } } break; case VpnProfile.TYPE_USERPASS: - cfg += "auth-user-pass\n"; - cfg += insertFileData("ca", mCaFilename); + cfg.append("auth-user-pass\n"); + cfg.append(insertFileData("ca", mCaFilename)); + if (configForOvpn3) { + // OpenVPN 3 needs to be told that a client certificate is not required + cfg.append("client-cert-not-required\n"); + } } if (isUserPWAuth()) { - if (mAuthenticationType == AUTH_RETRY_NOINTERACT) - cfg += "auth-retry nointeract"; + if (mAuthRetry == AUTH_RETRY_NOINTERACT) + cfg.append("auth-retry nointeract\n"); } if (!TextUtils.isEmpty(mCrlFilename)) - cfg += insertFileData("crl-verify", mCrlFilename); + cfg.append(insertFileData("crl-verify", mCrlFilename)); if (mUseLzo) { - cfg += "comp-lzo\n"; + cfg.append("comp-lzo\n"); } if (mUseTLSAuth) { boolean useTlsCrypt = mTLSAuthDirection.equals("tls-crypt"); if (mAuthenticationType == TYPE_STATICKEYS) - cfg += insertFileData("secret", mTLSAuthFilename); + cfg.append(insertFileData("secret", mTLSAuthFilename)); else if (useTlsCrypt) - cfg += insertFileData("tls-crypt", mTLSAuthFilename); + cfg.append(insertFileData("tls-crypt", mTLSAuthFilename)); else - cfg += insertFileData("tls-auth", mTLSAuthFilename); + cfg.append(insertFileData("tls-auth", mTLSAuthFilename)); if (!TextUtils.isEmpty(mTLSAuthDirection) && !useTlsCrypt) { - cfg += "key-direction "; - cfg += mTLSAuthDirection; - cfg += "\n"; + cfg.append("key-direction "); + cfg.append(mTLSAuthDirection); + cfg.append("\n"); } } if (!mUsePull) { if (!TextUtils.isEmpty(mIPv4Address)) - cfg += "ifconfig " + cidrToIPAndNetmask(mIPv4Address) + "\n"; + cfg.append("ifconfig ").append(cidrToIPAndNetmask(mIPv4Address)).append("\n"); if (!TextUtils.isEmpty(mIPv6Address)) { // Use our own ip as gateway since we ignore it anyway String fakegw = mIPv6Address.split("/", 2)[0]; - cfg += "ifconfig-ipv6 " + mIPv6Address + " " + fakegw + "\n"; + cfg.append("ifconfig-ipv6 ").append(mIPv6Address).append(" ").append(fakegw).append("\n"); } } if (mUsePull && mRoutenopull) - cfg += "route-nopull\n"; + cfg.append("route-nopull\n"); String routes = ""; @@ -483,128 +531,129 @@ public class VpnProfile implements Serializable, Cloneable { if (mUseDefaultRoutev6) - cfg += "route-ipv6 ::/0\n"; + cfg.append("route-ipv6 ::/0\n"); else for (String route : getCustomRoutesv6(mCustomRoutesv6)) { routes += "route-ipv6 " + route + "\n"; } - cfg += routes; + cfg.append(routes); if (mOverrideDNS || !mUsePull) { if (!TextUtils.isEmpty(mDNS1)) { - cfg += "dhcp-option DNS " + mDNS1 + "\n"; + cfg.append("dhcp-option DNS ").append(mDNS1).append("\n"); } if (!TextUtils.isEmpty(mDNS2)) { - cfg += "dhcp-option DNS " + mDNS2 + "\n"; + cfg.append("dhcp-option DNS ").append(mDNS2).append("\n"); } if (!TextUtils.isEmpty(mSearchDomain)) - cfg += "dhcp-option DOMAIN " + mSearchDomain + "\n"; + cfg.append("dhcp-option DOMAIN ").append(mSearchDomain).append("\n"); } if (mMssFix != 0) { if (mMssFix != 1450) { - cfg += String.format(Locale.US, "mssfix %d\n", mMssFix); + cfg.append(String.format(Locale.US, "mssfix %d\n", mMssFix)); } else - cfg += "mssfix\n"; + cfg.append("mssfix\n"); } if (mTunMtu >= 48 && mTunMtu != 1500) { - cfg += String.format(Locale.US, "tun-mtu %d\n", mTunMtu); + cfg.append(String.format(Locale.US, "tun-mtu %d\n", mTunMtu)); } if (mNobind) - cfg += "nobind\n"; + cfg.append("nobind\n"); // Authentication if (mAuthenticationType != TYPE_STATICKEYS) { if (mCheckRemoteCN) { if (mRemoteCN == null || mRemoteCN.equals("")) - cfg += "verify-x509-name " + openVpnEscape(mConnections[0].mServerName) + " name\n"; + cfg.append("verify-x509-name ").append(openVpnEscape(mConnections[0].mServerName)).append(" name\n"); else switch (mX509AuthType) { // 2.2 style x509 checks case X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING: - cfg += "compat-names no-remapping\n"; + cfg.append("compat-names no-remapping\n"); case X509_VERIFY_TLSREMOTE: - cfg += "tls-remote " + openVpnEscape(mRemoteCN) + "\n"; + cfg.append("tls-remote ").append(openVpnEscape(mRemoteCN)).append("\n"); break; case X509_VERIFY_TLSREMOTE_RDN: - cfg += "verify-x509-name " + openVpnEscape(mRemoteCN) + " name\n"; + cfg.append("verify-x509-name ").append(openVpnEscape(mRemoteCN)).append(" name\n"); break; case X509_VERIFY_TLSREMOTE_RDN_PREFIX: - cfg += "verify-x509-name " + openVpnEscape(mRemoteCN) + " name-prefix\n"; + cfg.append("verify-x509-name ").append(openVpnEscape(mRemoteCN)).append(" name-prefix\n"); break; case X509_VERIFY_TLSREMOTE_DN: - cfg += "verify-x509-name " + openVpnEscape(mRemoteCN) + "\n"; + cfg.append("verify-x509-name ").append(openVpnEscape(mRemoteCN)).append("\n"); break; } if (!TextUtils.isEmpty(mx509UsernameField)) - cfg += "x509-username-field " + openVpnEscape(mx509UsernameField) + "\n"; + cfg.append("x509-username-field ").append(openVpnEscape(mx509UsernameField)).append("\n"); } if (mExpectTLSCert) - cfg += "remote-cert-tls server\n"; + cfg.append("remote-cert-tls server\n"); } if (!TextUtils.isEmpty(mCipher)) { - cfg += "cipher " + mCipher + "\n"; + cfg.append("cipher ").append(mCipher).append("\n"); } if (!TextUtils.isEmpty(mAuth)) { - cfg += "auth " + mAuth + "\n"; + cfg.append("auth ").append(mAuth).append("\n"); } // Obscure Settings dialog if (mUseRandomHostname) - cfg += "#my favorite options :)\nremote-random-hostname\n"; + cfg.append("#my favorite options :)\nremote-random-hostname\n"); if (mUseFloat) - cfg += "float\n"; + cfg.append("float\n"); if (mPersistTun) { - cfg += "persist-tun\n"; - cfg += "# persist-tun also enables pre resolving to avoid DNS resolve problem\n"; - cfg += "preresolve\n"; + cfg.append("persist-tun\n"); + cfg.append("# persist-tun also enables pre resolving to avoid DNS resolve problem\n"); + if (!mIsOpenVPN22) + cfg.append("preresolve\n"); } if (mPushPeerInfo) - cfg += "push-peer-info\n"; + cfg.append("push-peer-info\n"); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean usesystemproxy = prefs.getBoolean("usesystemproxy", true); - if (usesystemproxy && !mIsOpenVPN22 && !configForOvpn3) { - cfg += "# Use system proxy setting\n"; - cfg += "management-query-proxy\n"; + if (usesystemproxy && !mIsOpenVPN22 && !configForOvpn3 && !usesExtraProxyOptions()) { + cfg.append("# Use system proxy setting\n"); + cfg.append("management-query-proxy\n"); } if (mUseCustomConfig) { - cfg += "# Custom configuration options\n"; - cfg += "# You are on your on own here :)\n"; - cfg += mCustomConfigOptions; - cfg += "\n"; + cfg.append("# Custom configuration options\n"); + cfg.append("# You are on your on own here :)\n"); + cfg.append(mCustomConfigOptions); + cfg.append("\n"); } if (!canUsePlainRemotes) { - cfg += "# Connection Options are at the end to allow global options (and global custom options) to influence connection blocks\n"; + cfg.append("# Connection Options are at the end to allow global options (and global custom options) to influence connection blocks\n"); for (Connection conn : mConnections) { if (conn.mEnabled) { - cfg += "<connection>\n"; - cfg += conn.getConnectionBlock(); - cfg += "</connection>\n"; + cfg.append("<connection>\n"); + cfg.append(conn.getConnectionBlock(configForOvpn3)); + cfg.append("</connection>\n"); } } } - return cfg; + return cfg.toString(); } public String getPlatformVersionEnvString() { @@ -624,18 +673,6 @@ public class VpnProfile implements Serializable, Cloneable { } - //! Put inline data inline and other data as normal escaped filename - public static String insertFileData(String cfgentry, String filedata) { - if (filedata == null) { - return String.format("%s %s\n", cfgentry, "file missing in config profile"); - } else if (isEmbedded(filedata)) { - String dataWithOutHeader = getEmbeddedContent(filedata); - return String.format(Locale.ENGLISH, "<%s>\n%s\n</%s>\n", cfgentry, dataWithOutHeader, cfgentry); - } else { - return String.format(Locale.ENGLISH, "%s %s\n", cfgentry, openVpnEscape(filedata)); - } - } - @NonNull private Collection<String> getCustomRoutes(String routes) { Vector<String> cidrRoutes = new Vector<>(); @@ -697,7 +734,6 @@ public class VpnProfile implements Serializable, Cloneable { return parts[0] + " " + netmask; } - public Intent prepareStartService(Context context) { Intent intent = getStartServiceIntent(context); @@ -727,33 +763,6 @@ public class VpnProfile implements Serializable, Cloneable { return intent; } - public String[] getKeyStoreCertificates(Context context) { - return getKeyStoreCertificates(context, 5); - } - - public static String getDisplayName(String embeddedFile) { - int start = DISPLAYNAME_TAG.length(); - int end = embeddedFile.indexOf(INLINE_TAG); - return embeddedFile.substring(start, end); - } - - public static String getEmbeddedContent(String data) { - if (!data.contains(INLINE_TAG)) - return data; - - int start = data.indexOf(INLINE_TAG) + INLINE_TAG.length(); - return data.substring(start); - } - - public static boolean isEmbedded(String data) { - if (data == null) - return false; - if (data.startsWith(INLINE_TAG) || data.startsWith(DISPLAYNAME_TAG)) - return true; - else - return false; - } - public void checkForRestart(final Context context) { /* This method is called when OpenVPNService is restarted */ @@ -762,7 +771,7 @@ public class VpnProfile implements Serializable, Cloneable { new Thread(new Runnable() { @Override public void run() { - getKeyStoreCertificates(context); + getExternalCertificates(context); } }).start(); @@ -798,26 +807,40 @@ public class VpnProfile implements Serializable, Cloneable { } + private X509Certificate[] getKeyStoreCertificates(Context context) throws KeyChainException, InterruptedException { + PrivateKey privateKey = KeyChain.getPrivateKey(context, mAlias); + mPrivateKey = privateKey; - class NoCertReturnedException extends Exception { - public NoCertReturnedException(String msg) { - super(msg); - } + + X509Certificate[] caChain = KeyChain.getCertificateChain(context, mAlias); + return caChain; + } + + private X509Certificate[] getExtAppCertificates(Context context) throws KeyChainException { + if (mExternalAuthenticator == null || mAlias == null) + throw new KeyChainException("Alias or external auth provider name not set"); + return ExtAuthHelper.getCertificateChain(context, mExternalAuthenticator, mAlias); + } + + public String[] getExternalCertificates(Context context) { + return getExternalCertificates(context, 5); } - synchronized String[] getKeyStoreCertificates(Context context, int tries) { + + synchronized String[] getExternalCertificates(Context context, int tries) { // Force application context- KeyChain methods will block long enough that by the time they // are finished and try to unbind, the original activity context might have been destroyed. context = context.getApplicationContext(); try { - PrivateKey privateKey = KeyChain.getPrivateKey(context, mAlias); - mPrivateKey = privateKey; - String keystoreChain = null; - - X509Certificate[] caChain = KeyChain.getCertificateChain(context, mAlias); + X509Certificate caChain[]; + if (mAuthenticationType == TYPE_EXTERNAL_APP) { + caChain = getExtAppCertificates(context); + } else { + caChain = getKeyStoreCertificates(context); + } if (caChain == null) throw new NoCertReturnedException("No certificate returned from Keystore"); @@ -900,14 +923,18 @@ public class VpnProfile implements Serializable, Cloneable { } catch (InterruptedException e1) { VpnStatus.logException(e1); } - return getKeyStoreCertificates(context, tries - 1); + return getExternalCertificates(context, tries - 1); } } + public int checkProfile(Context c) { + return checkProfile(c, doUseOpenVPN3(c)); + } + //! Return an error if something is wrong - public int checkProfile(Context context) { - if (mAuthenticationType == TYPE_KEYSTORE || mAuthenticationType == TYPE_USERPASS_KEYSTORE) { + public int checkProfile(Context context, boolean useOpenVPN3) { + if (mAuthenticationType == TYPE_KEYSTORE || mAuthenticationType == TYPE_USERPASS_KEYSTORE || mAuthenticationType == TYPE_EXTERNAL_APP) { if (mAlias == null) return R.string.no_keystore_cert_selected; } else if (mAuthenticationType == TYPE_CERTIFICATES || mAuthenticationType == TYPE_USERPASS_CERTIFICATES) { @@ -944,21 +971,35 @@ public class VpnProfile implements Serializable, Cloneable { boolean noRemoteEnabled = true; - for (Connection c : mConnections) + for (Connection c : mConnections) { if (c.mEnabled) noRemoteEnabled = false; + } if (noRemoteEnabled) return R.string.remote_no_server_selected; - if (doUseOpenVPN3(context)) { + if (useOpenVPN3) { if (mAuthenticationType == TYPE_STATICKEYS) { return R.string.openvpn3_nostatickeys; } if (mAuthenticationType == TYPE_PKCS12 || mAuthenticationType == TYPE_USERPASS_PKCS12) { return R.string.openvpn3_pkcs12; } + for (Connection conn : mConnections) { + if (conn.mProxyType == Connection.ProxyType.ORBOT || conn.mProxyType == Connection.ProxyType.SOCKS5) + return R.string.openvpn3_socksproxy; + } } + for (Connection c : mConnections) { + if (c.mProxyType == Connection.ProxyType.ORBOT) { + if (usesExtraProxyOptions()) + return R.string.error_orbot_and_proxy_options; + if (!OrbotHelper.checkTorReceier(context)) + return R.string.no_orbotfound; + } + } + // Everything okay return R.string.no_error_found; @@ -1073,18 +1114,42 @@ public class VpnProfile implements Serializable, Cloneable { } public String getUUIDString() { - return mUuid.toString(); + return mUuid.toString().toLowerCase(Locale.ENGLISH); } public PrivateKey getKeystoreKey() { return mPrivateKey; } - public String getSignedData(String b64data) { - PrivateKey privkey = getKeystoreKey(); - + @Nullable + public String getSignedData(Context c, String b64data, boolean pkcs1padding) { byte[] data = Base64.decode(b64data, Base64.DEFAULT); + byte[] signed_bytes; + if (mAuthenticationType == TYPE_EXTERNAL_APP) + signed_bytes = getExtAppSignedData(c, data); + else + signed_bytes = getKeyChainSignedData(data, pkcs1padding); + if (signed_bytes != null) + return Base64.encodeToString(signed_bytes, Base64.NO_WRAP); + else + return null; + } + + private byte[] getExtAppSignedData(Context c, byte[] data) { + if (TextUtils.isEmpty(mExternalAuthenticator)) + return null; + try { + return ExtAuthHelper.signData(c, mExternalAuthenticator, mAlias, data); + } catch (KeyChainException | InterruptedException e) { + VpnStatus.logError(R.string.error_extapp_sign, mExternalAuthenticator, e.getClass().toString(), e.getLocalizedMessage()); + return null; + } + } + + private byte[] getKeyChainSignedData(byte[] data, boolean pkcs1padding) { + + PrivateKey privkey = getKeystoreKey(); // The Jelly Bean *evil* Hack // 4.2 implements the RSA/ECB/PKCS1PADDING in the OpenSSLprovider if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) { @@ -1109,15 +1174,17 @@ public class VpnProfile implements Serializable, Cloneable { the public/private part in the TLS exchange */ Cipher signer; - signer = Cipher.getInstance("RSA/ECB/PKCS1PADDING"); + if (pkcs1padding) + signer = Cipher.getInstance("RSA/ECB/PKCS1PADDING"); + else + signer = Cipher.getInstance("RSA/ECB/NoPadding"); signer.init(Cipher.ENCRYPT_MODE, privkey); signed_bytes = signer.doFinal(data); } - return Base64.encodeToString(signed_bytes, Base64.NO_WRAP); - + return signed_bytes; } catch (NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException | SignatureException e) { VpnStatus.logError(R.string.error_rsa_sign, e.getClass().toString(), e.getLocalizedMessage()); @@ -1125,7 +1192,7 @@ public class VpnProfile implements Serializable, Cloneable { } } - private String processSignJellyBeans(PrivateKey privkey, byte[] data) { + private byte[] processSignJellyBeans(PrivateKey privkey, byte[] data) { try { Method getKey = privkey.getClass().getSuperclass().getDeclaredMethod("getOpenSSLKey"); getKey.setAccessible(true); @@ -1143,8 +1210,7 @@ public class VpnProfile implements Serializable, Cloneable { getPkeyContext.setAccessible(false); // 112 with TLS 1.2 (172 back with 4.3), 36 with TLS 1.0 - byte[] signed_bytes = NativeUtils.rsasign(data, pkey); - return Base64.encodeToString(signed_bytes, Base64.NO_WRAP); + return NativeUtils.rsasign(data, pkey); } catch (NoSuchMethodException | InvalidKeyException | InvocationTargetException | IllegalAccessException | IllegalArgumentException e) { VpnStatus.logError(R.string.error_rsa_sign, e.getClass().toString(), e.getLocalizedMessage()); @@ -1152,6 +1218,22 @@ public class VpnProfile implements Serializable, Cloneable { } } + private boolean usesExtraProxyOptions() { + if (mUseCustomConfig && mCustomConfigOptions != null && mCustomConfigOptions.contains("http-proxy-option ")) + return true; + for (Connection c : mConnections) + if (c.usesExtraProxyOptions()) + return true; + + return false; + } + + class NoCertReturnedException extends Exception { + public NoCertReturnedException(String msg) { + super(msg); + } + } + } diff --git a/app/src/main/java/de/blinkt/openvpn/core/CIDRIP.java b/app/src/main/java/de/blinkt/openvpn/core/CIDRIP.java index 799c68c9..ca3d1161 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/CIDRIP.java +++ b/app/src/main/java/de/blinkt/openvpn/core/CIDRIP.java @@ -14,6 +14,11 @@ class CIDRIP { public CIDRIP(String ip, String mask) { mIp = ip; + len = calculateLenFromMask(mask); + + } + + public static int calculateLenFromMask(String mask) { long netmask = getInt(mask); // Add 33. bit to ensure the loop terminates @@ -24,6 +29,7 @@ class CIDRIP { lenZeros++; netmask = netmask >> 1; } + int len; // Check if rest of netmask is only 1s if (netmask != (0x1ffffffffl >> lenZeros)) { // Asume no CIDR, set /32 @@ -31,7 +37,7 @@ class CIDRIP { } else { len = 32 - lenZeros; } - + return len; } public CIDRIP(String address, int prefix_length) { diff --git a/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java b/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java index 9889754d..0148bfb7 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java +++ b/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java @@ -5,17 +5,15 @@ package de.blinkt.openvpn.core; -import android.text.TextUtils; +import android.os.Build; import android.support.v4.util.Pair; +import android.text.TextUtils; import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; import java.io.StringReader; -import java.util.Collections; -import java.util.HashMap; -import java.util.Locale; -import java.util.Vector; +import java.util.*; import de.blinkt.openvpn.VpnProfile; @@ -29,10 +27,127 @@ public class ConfigParser { public static final String CONVERTED_PROFILE = "converted Profile"; - private HashMap<String, Vector<Vector<String>>> options = new HashMap<String, Vector<Vector<String>>>(); + final String[] unsupportedOptions = {"config", + "tls-server" + + }; + // Ignore all scripts + // in most cases these won't work and user who wish to execute scripts will + // figure out themselves + private final String[] ignoreOptions = {"tls-client", + "allow-recursive-routing", + "askpass", + "auth-nocache", + "up", + "down", + "route-up", + "ipchange", + "route-pre-down", + "auth-user-pass-verify", + "block-outside-dns", + "client-cert-not-required", + "dhcp-release", + "dhcp-renew", + "dh", + "group", + "ip-win32", + "ifconfig-nowarn", + "management-hold", + "management", + "management-client", + "management-query-remote", + "management-query-passwords", + "management-query-proxy", + "management-external-key", + "management-forget-disconnect", + "management-signal", + "management-log-cache", + "management-up-down", + "management-client-user", + "management-client-group", + "pause-exit", + "preresolve", + "plugin", + "machine-readable-output", + "persist-key", + "push", + "register-dns", + "route-delay", + "route-gateway", + "route-metric", + "route-method", + "status", + "script-security", + "show-net-up", + "suppress-timestamps", + "tap-sleep", + "tmp-dir", + "tun-ipv6", + "topology", + "user", + "win-sys", + }; + private final String[][] ignoreOptionsWithArg = + { + {"setenv", "IV_GUI_VER"}, + {"setenv", "IV_PLAT_VER"}, + {"setenv", "IV_OPENVPN_GUI_VERSION"}, + {"engine", "dynamic"}, + {"setenv", "CLIENT_CERT"}, + {"resolv-retry", "60"} + }; + private final String[] connectionOptions = { + "local", + "remote", + "float", + "port", + "connect-retry", + "connect-timeout", + "connect-retry-max", + "link-mtu", + "tun-mtu", + "tun-mtu-extra", + "fragment", + "mtu-disc", + "local-port", + "remote-port", + "bind", + "nobind", + "proto", + "http-proxy", + "http-proxy-retry", + "http-proxy-timeout", + "http-proxy-option", + "socks-proxy", + "socks-proxy-retry", + "http-proxy-user-pass", + "explicit-exit-notify", + }; + private HashSet<String> connectionOptionsSet = new HashSet<>(Arrays.asList(connectionOptions)); + + private HashMap<String, Vector<Vector<String>>> options = new HashMap<>(); private HashMap<String, Vector<String>> meta = new HashMap<String, Vector<String>>(); private String auth_user_pass_file; + static public void useEmbbedUserAuth(VpnProfile np, String inlinedata) { + String data = VpnProfile.getEmbeddedContent(inlinedata); + String[] parts = data.split("\n"); + if (parts.length >= 2) { + np.mUsername = parts[0]; + np.mPassword = parts[1]; + } + } + + static public void useEmbbedHttpAuth(Connection c, String inlinedata) { + String data = VpnProfile.getEmbeddedContent(inlinedata); + String[] parts = data.split("\n"); + if (parts.length >= 2) { + c.mProxyAuthUser = parts[0]; + c.mProxyAuthPassword = parts[1]; + c.mUseProxyAuth = true; + } + } + public void parseConfig(Reader reader) throws IOException, ConfigParseError { HashMap<String, String> optionAliases = new HashMap<>(); @@ -76,7 +191,7 @@ public class ConfigParser { checkinlinefile(args, br); String optionname = args.get(0); - if (optionAliases.get(optionname)!=null) + if (optionAliases.get(optionname) != null) optionname = optionAliases.get(optionname); if (!options.containsKey(optionname)) { @@ -119,8 +234,8 @@ public class ConfigParser { } } while (true); - if(inlinefile.endsWith("\n")) - inlinefile = inlinefile.substring(0, inlinefile.length()-1); + if (inlinefile.endsWith("\n")) + inlinefile = inlinefile.substring(0, inlinefile.length() - 1); args.clear(); args.add(argname); @@ -133,11 +248,6 @@ public class ConfigParser { return auth_user_pass_file; } - enum linestate { - initial, - readin_single_quote, reading_quoted, reading_unquoted, done - } - private boolean space(char c) { // I really hope nobody is using zero bytes inside his/her config file // to sperate parameter but here we go: @@ -145,15 +255,6 @@ public class ConfigParser { } - public static class ConfigParseError extends Exception { - private static final long serialVersionUID = -60L; - - public ConfigParseError(String msg) { - super(msg); - } - } - - // adapted openvpn's parse function to java private Vector<String> parseline(String line) throws ConfigParseError { Vector<String> parameters = new Vector<String>(); @@ -226,7 +327,7 @@ public class ConfigParser { backslash = false; } - /* store parameter character */ + /* store parameter character */ if (out != 0) { currentarg += out; } @@ -235,105 +336,6 @@ public class ConfigParser { return parameters; } - - final String[] unsupportedOptions = {"config", - "tls-server" - - }; - - // Ignore all scripts - // in most cases these won't work and user who wish to execute scripts will - // figure out themselves - final String[] ignoreOptions = {"tls-client", - "askpass", - "auth-nocache", - "up", - "down", - "route-up", - "ipchange", - "route-up", - "route-pre-down", - "auth-user-pass-verify", - "block-outside-dns", - "dhcp-release", - "dhcp-renew", - "dh", - "group", - "allow-recursive-routing", - "ip-win32", - "ifconfig-nowarn", - "management-hold", - "management", - "management-client", - "management-query-remote", - "management-query-passwords", - "management-query-proxy", - "management-external-key", - "management-forget-disconnect", - "management-signal", - "management-log-cache", - "management-up-down", - "management-client-user", - "management-client-group", - "pause-exit", - "preresolve", - "plugin", - "machine-readable-output", - "persist-key", - "push", - "register-dns", - "route-delay", - "route-gateway", - "route-metric", - "route-method", - "status", - "script-security", - "show-net-up", - "suppress-timestamps", - "tmp-dir", - "tun-ipv6", - "topology", - "user", - "win-sys", - }; - - final String[][] ignoreOptionsWithArg = - { - {"setenv", "IV_GUI_VER"}, - {"setenv", "IV_OPENVPN_GUI_VERSION"}, - {"engine", "dynamic"}, - {"setenv", "CLIENT_CERT"}, - {"resolve-retry","60"} - }; - - final String[] connectionOptions = { - "local", - "remote", - "float", - "port", - "connect-retry", - "connect-timeout", - "connect-retry-max", - "link-mtu", - "tun-mtu", - "tun-mtu-extra", - "fragment", - "mtu-disc", - "local-port", - "remote-port", - "bind", - "nobind", - "proto", - "http-proxy", - "http-proxy-retry", - "http-proxy-timeout", - "http-proxy-option", - "socks-proxy", - "socks-proxy-retry", - "explicit-exit-notify", - }; - - // This method is far too long @SuppressWarnings("ConstantConditions") public VpnProfile convertProfile() throws ConfigParseError, IOException { @@ -403,8 +405,8 @@ public class ConfigParser { } Vector<String> routeNoPull = getOption("route-nopull", 0, 0); - if (routeNoPull!=null) - np.mRoutenopull=true; + if (routeNoPull != null) + np.mRoutenopull = true; // Also recognize tls-auth [inline] direction ... Vector<Vector<String>> tlsauthoptions = getAllOption("tls-auth", 1, 2); @@ -426,7 +428,7 @@ public class ConfigParser { np.mTLSAuthDirection = direction.get(1); Vector<String> tlscrypt = getOption("tls-crypt", 1, 1); - if (tlscrypt!=null) { + if (tlscrypt != null) { np.mUseTLSAuth = true; np.mTLSAuthFilename = tlscrypt.get(1); np.mTLSAuthDirection = "tls-crypt"; @@ -467,7 +469,7 @@ public class ConfigParser { } - Vector<String> tunmtu = getOption("mtu", 1, 1); + Vector<String> tunmtu = getOption("tun-mtu", 1, 1); if (tunmtu != null) { try { @@ -478,7 +480,6 @@ public class ConfigParser { } - Vector<String> mode = getOption("mode", 1, 1); if (mode != null) { if (!mode.get(1).equals("p2p")) @@ -592,9 +593,9 @@ public class ConfigParser { } - Vector<String> x509usernamefield = getOption("x509-username-field", 1,1); - if (x509usernamefield!=null) { - np.mx509UsernameField = x509usernamefield.get(1); + Vector<String> x509usernamefield = getOption("x509-username-field", 1, 1); + if (x509usernamefield != null) { + np.mx509UsernameField = x509usernamefield.get(1); } @@ -666,7 +667,7 @@ public class ConfigParser { if (crlfile != null) { // If the 'dir' parameter is present just add it as custom option .. if (crlfile.size() == 3 && crlfile.get(2).equals("dir")) - np.mCustomConfigOptions += TextUtils.join(" ", crlfile) + "\n"; + np.mCustomConfigOptions += join(" ", crlfile) + "\n"; else // Save the filename for the config converter to add later np.mCrlFilename = crlfile.get(1); @@ -732,6 +733,13 @@ public class ConfigParser { return np; } + private String join(String s, Vector<String> str) { + if (Build.VERSION.SDK_INT > 26) + return String.join(s, str); + else + return TextUtils.join(s, str); + } + private Pair<Connection, Connection[]> parseConnection(String connection, Connection defaultValues) throws IOException, ConfigParseError { // Parse a connection Block as a new configuration file @@ -783,20 +791,48 @@ public class ConfigParser { } } - // Parse remote config - Vector<Vector<String>> remotes = getAllOption("remote", 1, 3); + Vector<String> proxy = getOption("socks-proxy", 1, 2); + if (proxy == null) + proxy = getOption("http-proxy", 2, 2); + + if (proxy != null) { + if (proxy.get(0).equals("socks-proxy")) { + conn.mProxyType = Connection.ProxyType.SOCKS5; + // socks defaults to 1080, http always sets port + conn.mProxyPort = "1080"; + } else { + conn.mProxyType = Connection.ProxyType.HTTP; + } + + conn.mProxyName = proxy.get(1); + if (proxy.size() >= 3) + conn.mProxyPort = proxy.get(2); + } + Vector<String> httpproxyauthhttp = getOption("http-proxy-user-pass", 1, 1); + if (httpproxyauthhttp != null) + useEmbbedHttpAuth(conn, httpproxyauthhttp.get(1)); - // Assume that we need custom options if connectionDefault are set - if (connDefault != null) { - for (Vector<Vector<String>> option : options.values()) { - conn.mCustomConfiguration += getOptionStrings(option); + // Parse remote config + Vector<Vector<String>> remotes = getAllOption("remote", 1, 3); + + + Vector <String> optionsToRemove = new Vector<>(); + // Assume that we need custom options if connectionDefault are set or in the connection specific set + for (Map.Entry<String, Vector<Vector<String>>> option : options.entrySet()) { + if (connDefault != null || connectionOptionsSet.contains(option.getKey())) { + conn.mCustomConfiguration += getOptionStrings(option.getValue()); + optionsToRemove.add(option.getKey()); } - if (!TextUtils.isEmpty(conn.mCustomConfiguration)) - conn.mUseCustomConfig = true; } + for (String o: optionsToRemove) + options.remove(o); + + if (!(conn.mCustomConfiguration == null || "".equals(conn.mCustomConfiguration.trim()))) + conn.mUseCustomConfig = true; + // Make remotes empty to simplify code if (remotes == null) remotes = new Vector<Vector<String>>(); @@ -821,6 +857,7 @@ public class ConfigParser { } i++; } + return Pair.create(conn, connections); } @@ -830,19 +867,19 @@ public class ConfigParser { boolean noIpv4 = false; if (defaultRoute) - for (Vector<String> redirect : defgw) - for (int i = 1; i < redirect.size(); i++) { - if (redirect.get(i).equals("block-local")) - np.mAllowLocalLAN = false; - else if (redirect.get(i).equals("unblock-local")) - np.mAllowLocalLAN = true; - else if (redirect.get(i).equals("!ipv4")) - noIpv4=true; - else if (redirect.get(i).equals("ipv6")) - np.mUseDefaultRoutev6=true; - } + for (Vector<String> redirect : defgw) + for (int i = 1; i < redirect.size(); i++) { + if (redirect.get(i).equals("block-local")) + np.mAllowLocalLAN = false; + else if (redirect.get(i).equals("unblock-local")) + np.mAllowLocalLAN = true; + else if (redirect.get(i).equals("!ipv4")) + noIpv4 = true; + else if (redirect.get(i).equals("ipv6")) + np.mUseDefaultRoutev6 = true; + } if (defaultRoute && !noIpv4) - np.mUseDefaultRoute=true; + np.mUseDefaultRoute = true; } private boolean isUdpProto(String proto) throws ConfigParseError { @@ -861,15 +898,6 @@ public class ConfigParser { return isudp; } - static public void useEmbbedUserAuth(VpnProfile np, String inlinedata) { - String data = VpnProfile.getEmbeddedContent(inlinedata); - String[] parts = data.split("\n"); - if (parts.length >= 2) { - np.mUsername = parts[0]; - np.mPassword = parts[1]; - } - } - private void checkIgnoreAndInvalidOptions(VpnProfile np) throws ConfigParseError { for (String option : unsupportedOptions) if (options.containsKey(option)) @@ -880,7 +908,16 @@ public class ConfigParser { options.remove(option); - if (options.size() > 0) { + boolean customOptions=false; + for (Vector<Vector<String>> option: options.values()) + { + for (Vector<String> optionsline : option) { + if (!ignoreThisOption(optionsline)) { + customOptions = true; + } + } + } + if (customOptions) { np.mCustomConfigOptions = "# These options found in the config file do not map to config settings:\n" + np.mCustomConfigOptions; @@ -894,7 +931,6 @@ public class ConfigParser { } } - boolean ignoreThisOption(Vector<String> option) { for (String[] ignoreOption : ignoreOptionsWithArg) { @@ -920,7 +956,7 @@ public class ConfigParser { if (!ignoreThisOption(optionsline)) { // Check if option had been inlined and inline again if (optionsline.size() == 2 && - ("extra-certs".equals(optionsline.get(0)) || "http-proxy-user-pass".equals(optionsline.get(0)))) { + "extra-certs".equals(optionsline.get(0))) { custom += VpnProfile.insertFileData(optionsline.get(0), optionsline.get(1)); } else { for (String arg : optionsline) @@ -932,7 +968,6 @@ public class ConfigParser { return custom; } - private void fixup(VpnProfile np) { if (np.mRemoteCN.equals(np.mServerName)) { np.mRemoteCN = ""; @@ -947,7 +982,6 @@ public class ConfigParser { return alloptions.lastElement(); } - private Vector<Vector<String>> getAllOption(String option, int minarg, int maxarg) throws ConfigParseError { Vector<Vector<String>> args = options.get(option); if (args == null) @@ -964,6 +998,19 @@ public class ConfigParser { return args; } + enum linestate { + initial, + readin_single_quote, reading_quoted, reading_unquoted, done + } + + public static class ConfigParseError extends Exception { + private static final long serialVersionUID = -60L; + + public ConfigParseError(String msg) { + super(msg); + } + } + } diff --git a/app/src/main/java/de/blinkt/openvpn/core/Connection.java b/app/src/main/java/de/blinkt/openvpn/core/Connection.java index ff15daec..df071894 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/Connection.java +++ b/app/src/main/java/de/blinkt/openvpn/core/Connection.java @@ -19,11 +19,25 @@ public class Connection implements Serializable, Cloneable { public boolean mEnabled = true; public int mConnectTimeout = 0; public static final int CONNECTION_DEFAULT_TIMEOUT = 120; + public ProxyType mProxyType = ProxyType.NONE; + public String mProxyName = "proxy.example.com"; + public String mProxyPort = "8080"; + + public boolean mUseProxyAuth; + public String mProxyAuthUser = null; + public String mProxyAuthPassword = null; + + public enum ProxyType { + NONE, + HTTP, + SOCKS5, + ORBOT + } private static final long serialVersionUID = 92031902903829089L; - public String getConnectionBlock() { + public String getConnectionBlock(boolean isOpenVPN3) { String cfg = ""; // Server Address @@ -39,14 +53,31 @@ public class Connection implements Serializable, Cloneable { if (mConnectTimeout != 0) cfg += String.format(Locale.US, " connect-timeout %d\n", mConnectTimeout); + // OpenVPN 2.x manages proxy connection via management interface + if ((isOpenVPN3 || usesExtraProxyOptions()) && mProxyType == ProxyType.HTTP) + { + cfg+=String.format(Locale.US,"http-proxy %s %s\n", mProxyName, mProxyPort); + if (mUseProxyAuth) + cfg+=String.format(Locale.US, "<http-proxy-user-pass>\n%s\n%s\n</http-proxy-user-pass>\n", mProxyAuthUser, mProxyAuthPassword); + } + if (usesExtraProxyOptions() && mProxyType == ProxyType.SOCKS5) { + cfg+=String.format(Locale.US,"socks-proxy %s %s\n", mProxyName, mProxyPort); + } if (!TextUtils.isEmpty(mCustomConfiguration) && mUseCustomConfig) { cfg += mCustomConfiguration; cfg += "\n"; } + + return cfg; } + public boolean usesExtraProxyOptions() { + return (mUseCustomConfig && mCustomConfiguration.contains("http-proxy-option ")); + } + + @Override public Connection clone() throws CloneNotSupportedException { return (Connection) super.clone(); diff --git a/app/src/main/java/de/blinkt/openvpn/core/ExtAuthHelper.java b/app/src/main/java/de/blinkt/openvpn/core/ExtAuthHelper.java new file mode 100644 index 00000000..166bce12 --- /dev/null +++ b/app/src/main/java/de/blinkt/openvpn/core/ExtAuthHelper.java @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2012-2018 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.*; +import android.security.KeyChainException; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.SpinnerAdapter; +import de.blinkt.openvpn.api.ExternalCertificateProvider; + +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.UnsupportedEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +public class ExtAuthHelper { + + public static final String ACTION_CERT_CONFIGURATION = "de.blinkt.openvpn.api.ExternalCertificateConfiguration"; + public static final String ACTION_CERT_PROVIDER = "de.blinkt.openvpn.api.ExternalCertificateProvider"; + + public static final String EXTRA_ALIAS = "de.blinkt.openvpn.api.KEY_ALIAS"; + public static final String EXTRA_DESCRIPTION = "de.blinkt.openvpn.api.KEY_DESCRIPTION"; + + + public static void setExternalAuthProviderSpinnerList(Spinner spinner, String selectedApp) { + Context c = spinner.getContext(); + final PackageManager pm = c.getPackageManager(); + ArrayList<ExternalAuthProvider> extProviders = getExternalAuthProviderList(c); + + int selectedPos = -1; + + if (extProviders.size() ==0) + { + selectedApp = ""; + ExternalAuthProvider noauthprovider = new ExternalAuthProvider(); + noauthprovider.label = "No external auth provider found"; + noauthprovider.packageName = selectedApp; + noauthprovider.configurable = false; + extProviders.add(noauthprovider); + } + + + for (int i = 0; i < extProviders.size(); i++) { + if (extProviders.get(i).packageName.equals(selectedApp)) + selectedPos = i; + } + SpinnerAdapter extAppAdapter = new ArrayAdapter<ExternalAuthProvider>(c, android.R.layout.simple_spinner_item, android.R.id.text1, extProviders); + spinner.setAdapter(extAppAdapter); + if (selectedPos != -1) + spinner.setSelection(selectedPos); + } + + static ArrayList<ExternalAuthProvider> getExternalAuthProviderList(Context c) { + Intent configureExtAuth = new Intent(ACTION_CERT_CONFIGURATION); + + final PackageManager packageManager = c.getPackageManager(); + List<ResolveInfo> configureList = + packageManager.queryIntentActivities(configureExtAuth, 0); + + Intent serviceExtAuth = new Intent(ACTION_CERT_PROVIDER); + + List<ResolveInfo> serviceList = + packageManager.queryIntentServices(serviceExtAuth, 0); + + + // For now only list those who appear in both lists + + ArrayList<ExternalAuthProvider> providers = new ArrayList<ExternalAuthProvider>(); + + for (ResolveInfo service : serviceList) { + ExternalAuthProvider ext = new ExternalAuthProvider(); + ext.packageName = service.serviceInfo.packageName; + + ext.label = (String) service.serviceInfo.applicationInfo.loadLabel(packageManager); + + for (ResolveInfo activity : configureList) { + if (service.serviceInfo.packageName.equals(activity.activityInfo.packageName)) { + ext.configurable = true; + } + } + providers.add(ext); + + } + return providers; + + } + + @Nullable + @WorkerThread + public static byte[] signData(@NonNull Context context, + @NonNull String extAuthPackageName, + @NonNull String alias, + @NonNull byte[] data + ) throws KeyChainException, InterruptedException + + { + + + try (ExternalAuthProviderConnection authProviderConnection = bindToExtAuthProvider(context.getApplicationContext(), extAuthPackageName)) { + ExternalCertificateProvider externalAuthProvider = authProviderConnection.getService(); + return externalAuthProvider.getSignedData(alias, data); + + } catch (RemoteException e) { + throw new KeyChainException(e); + } + } + + @Nullable + @WorkerThread + public static X509Certificate[] getCertificateChain(@NonNull Context context, + @NonNull String extAuthPackageName, + @NonNull String alias) throws KeyChainException { + + final byte[] certificateBytes; + try (ExternalAuthProviderConnection authProviderConnection = bindToExtAuthProvider(context.getApplicationContext(), extAuthPackageName)) { + ExternalCertificateProvider externalAuthProvider = authProviderConnection.getService(); + certificateBytes = externalAuthProvider.getCertificateChain(alias); + if (certificateBytes == null) { + return null; + } + Collection<X509Certificate> chain = toCertificates(certificateBytes); + return chain.toArray(new X509Certificate[chain.size()]); + + } catch (RemoteException | RuntimeException | InterruptedException e) { + throw new KeyChainException(e); + } + } + + public static Bundle getCertificateMetaData(@NonNull Context context, + @NonNull String extAuthPackageName, + String alias) throws KeyChainException + { + try (ExternalAuthProviderConnection authProviderConnection = bindToExtAuthProvider(context.getApplicationContext(), extAuthPackageName)) { + ExternalCertificateProvider externalAuthProvider = authProviderConnection.getService(); + return externalAuthProvider.getCertificateMetaData(alias); + + } catch (RemoteException | RuntimeException | InterruptedException e) { + throw new KeyChainException(e); + } + } + + public static Collection<X509Certificate> toCertificates(@NonNull byte[] bytes) { + final String BEGINCERT = "-----BEGIN CERTIFICATE-----"; + try { + Vector<X509Certificate> retCerts = new Vector<>(); + // Java library is broken, although the javadoc says it will extract all certificates from a byte array + // it only extracts the first one + String allcerts = new String(bytes, "iso8859-1"); + String[] certstrings = allcerts.split(BEGINCERT); + for (String certstring: certstrings) { + certstring = BEGINCERT + certstring; + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + retCerts.addAll((Collection<? extends X509Certificate>) certFactory.generateCertificates( + new ByteArrayInputStream((certstring.getBytes("iso8859-1"))))); + + } + return retCerts; + + } catch (CertificateException e) { + throw new AssertionError(e); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } + + // adapted form Keychain + @WorkerThread + public static ExternalAuthProviderConnection bindToExtAuthProvider(@NonNull Context context, String packagename) throws KeyChainException, InterruptedException { + ensureNotOnMainThread(context); + final BlockingQueue<ExternalCertificateProvider> q = new LinkedBlockingQueue<>(1); + ServiceConnection extAuthServiceConnection = new ServiceConnection() { + volatile boolean mConnectedAtLeastOnce = false; + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + if (!mConnectedAtLeastOnce) { + mConnectedAtLeastOnce = true; + try { + q.put(ExternalCertificateProvider.Stub.asInterface(service)); + } catch (InterruptedException e) { + // will never happen, since the queue starts with one available slot + } + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + } + }; + Intent intent = new Intent(ACTION_CERT_PROVIDER); + intent.setPackage(packagename); + + if (!context.bindService(intent, extAuthServiceConnection, Context.BIND_AUTO_CREATE)) { + throw new KeyChainException("could not bind to external authticator app: " + packagename); + } + return new ExternalAuthProviderConnection(context, extAuthServiceConnection, q.take()); + } + + private static void ensureNotOnMainThread(@NonNull Context context) { + Looper looper = Looper.myLooper(); + if (looper != null && looper == context.getMainLooper()) { + throw new IllegalStateException( + "calling this from your main thread can lead to deadlock"); + } + } + + public static class ExternalAuthProvider { + + public String packageName; + public boolean configurable = false; + private String label; + + @Override + public String toString() { + return label; + } + } + + public static class ExternalAuthProviderConnection implements Closeable { + private final Context context; + private final ServiceConnection serviceConnection; + private final ExternalCertificateProvider service; + + protected ExternalAuthProviderConnection(Context context, + ServiceConnection serviceConnection, + ExternalCertificateProvider service) { + this.context = context; + this.serviceConnection = serviceConnection; + this.service = service; + } + + @Override + public void close() { + context.unbindService(serviceConnection); + } + + public ExternalCertificateProvider getService() { + return service; + } + } +} diff --git a/app/src/main/java/de/blinkt/openvpn/core/ICSOpenVPNApplication.java b/app/src/main/java/de/blinkt/openvpn/core/ICSOpenVPNApplication.java index 38f51807..5b1307b7 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/ICSOpenVPNApplication.java +++ b/app/src/main/java/de/blinkt/openvpn/core/ICSOpenVPNApplication.java @@ -65,4 +65,4 @@ public class ICSOpenVPNApplication extends Application { mChannel.setLightColor(Color.BLUE); mNotificationManager.createNotificationChannel(mChannel); } -} +}
\ No newline at end of file diff --git a/app/src/main/java/de/blinkt/openvpn/core/LogFileHandler.java b/app/src/main/java/de/blinkt/openvpn/core/LogFileHandler.java index 57d1fb22..fca633b6 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/LogFileHandler.java +++ b/app/src/main/java/de/blinkt/openvpn/core/LogFileHandler.java @@ -134,7 +134,9 @@ class LogFileHandler extends Handler { if (!logfile.exists() || !logfile.canRead()) return; - readCacheContents(new FileInputStream(logfile)); + FileInputStream log = new FileInputStream(logfile); + readCacheContents(log); + log.close(); } catch (java.io.IOException | java.lang.RuntimeException e) { VpnStatus.logError("Reading cached logfile failed"); diff --git a/app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java b/app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java index 70c7455a..6b633c34 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java +++ b/app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java @@ -16,7 +16,15 @@ public class NativeUtils { static native void jniclose(int fdint); - public static native String getNativeAPI(); + public static String getNativeAPI() + { + if (isRoboUnitTest()) + return "ROBO"; + else + return getJNIAPI(); + } + + private static native String getJNIAPI(); public final static int[] openSSLlengths = { @@ -26,8 +34,14 @@ public class NativeUtils { public static native double[] getOpenSSLSpeed(String algorithm, int testnum); static { - System.loadLibrary("opvpnutil"); - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) - System.loadLibrary("jbcrypto"); + if (!isRoboUnitTest()) { + System.loadLibrary("opvpnutil"); + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) + System.loadLibrary("jbcrypto"); + } + } + + public static boolean isRoboUnitTest() { + return "robolectric".equals(Build.FINGERPRINT); } } diff --git a/app/src/main/java/de/blinkt/openvpn/core/NetworkSpace.java b/app/src/main/java/de/blinkt/openvpn/core/NetworkSpace.java index 37689b3f..0c54b050 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/NetworkSpace.java +++ b/app/src/main/java/de/blinkt/openvpn/core/NetworkSpace.java @@ -7,9 +7,6 @@ package de.blinkt.openvpn.core; import android.os.Build; import android.support.annotation.NonNull; -import android.text.TextUtils; - -import junit.framework.Assert; import java.math.BigInteger; import java.net.Inet6Address; @@ -21,9 +18,17 @@ import java.util.Vector; import se.leap.bitmaskclient.BuildConfig; + + public class NetworkSpace { - static class ipAddress implements Comparable<ipAddress> { + static void assertTrue(boolean f) + { + if (!f) + throw new IllegalStateException(); + } + + static class IpAddress implements Comparable<IpAddress> { private BigInteger netAddress; public int networkMask; private boolean included; @@ -38,7 +43,7 @@ public class NetworkSpace { * 2. smaller networks are returned as smaller */ @Override - public int compareTo(@NonNull ipAddress another) { + public int compareTo(@NonNull IpAddress another) { int comp = getFirstAddress().compareTo(another.getFirstAddress()); if (comp != 0) return comp; @@ -59,22 +64,22 @@ public class NetworkSpace { */ @Override public boolean equals(Object o) { - if (!(o instanceof ipAddress)) + if (!(o instanceof IpAddress)) return super.equals(o); - ipAddress on = (ipAddress) o; + IpAddress on = (IpAddress) o; return (networkMask == on.networkMask) && on.getFirstAddress().equals(getFirstAddress()); } - public ipAddress(CIDRIP ip, boolean include) { + public IpAddress(CIDRIP ip, boolean include) { included = include; netAddress = BigInteger.valueOf(ip.getInt()); networkMask = ip.len; isV4 = true; } - public ipAddress(Inet6Address address, int mask, boolean include) { + public IpAddress(Inet6Address address, int mask, boolean include) { networkMask = mask; included = include; @@ -130,7 +135,7 @@ public class NetworkSpace { return String.format(Locale.US, "%s/%d", getIPv6Address(), networkMask); } - ipAddress(BigInteger baseAddress, int mask, boolean included, boolean isV4) { + IpAddress(BigInteger baseAddress, int mask, boolean included, boolean isV4) { this.netAddress = baseAddress; this.networkMask = mask; this.included = included; @@ -138,26 +143,26 @@ public class NetworkSpace { } - public ipAddress[] split() { - ipAddress firstHalf = new ipAddress(getFirstAddress(), networkMask + 1, included, isV4); - ipAddress secondHalf = new ipAddress(firstHalf.getLastAddress().add(BigInteger.ONE), networkMask + 1, included, isV4); + public IpAddress[] split() { + IpAddress firstHalf = new IpAddress(getFirstAddress(), networkMask + 1, included, isV4); + IpAddress secondHalf = new IpAddress(firstHalf.getLastAddress().add(BigInteger.ONE), networkMask + 1, included, isV4); if (BuildConfig.DEBUG) - Assert.assertTrue(secondHalf.getLastAddress().equals(getLastAddress())); - return new ipAddress[]{firstHalf, secondHalf}; + assertTrue(secondHalf.getLastAddress().equals(getLastAddress())); + return new IpAddress[]{firstHalf, secondHalf}; } String getIPv4Address() { if (BuildConfig.DEBUG) { - Assert.assertTrue(isV4); - Assert.assertTrue(netAddress.longValue() <= 0xffffffffl); - Assert.assertTrue(netAddress.longValue() >= 0); + assertTrue(isV4); + assertTrue(netAddress.longValue() <= 0xffffffffl); + assertTrue(netAddress.longValue() >= 0); } long ip = netAddress.longValue(); return String.format(Locale.US, "%d.%d.%d.%d", (ip >> 24) % 256, (ip >> 16) % 256, (ip >> 8) % 256, ip % 256); } String getIPv6Address() { - if (BuildConfig.DEBUG) Assert.assertTrue(!isV4); + if (BuildConfig.DEBUG) assertTrue(!isV4); BigInteger r = netAddress; String ipv6str = null; @@ -186,7 +191,7 @@ public class NetworkSpace { return ipv6str; } - public boolean containsNet(ipAddress network) { + public boolean containsNet(IpAddress network) { // this.first >= net.first && this.last <= net.last BigInteger ourFirst = getFirstAddress(); BigInteger ourLast = getLastAddress(); @@ -201,12 +206,12 @@ public class NetworkSpace { } - TreeSet<ipAddress> mIpAddresses = new TreeSet<ipAddress>(); + TreeSet<IpAddress> mIpAddresses = new TreeSet<IpAddress>(); - public Collection<ipAddress> getNetworks(boolean included) { - Vector<ipAddress> ips = new Vector<ipAddress>(); - for (ipAddress ip : mIpAddresses) { + public Collection<IpAddress> getNetworks(boolean included) { + Vector<IpAddress> ips = new Vector<IpAddress>(); + for (IpAddress ip : mIpAddresses) { if (ip.included == included) ips.add(ip); } @@ -220,35 +225,35 @@ public class NetworkSpace { void addIP(CIDRIP cidrIp, boolean include) { - mIpAddresses.add(new ipAddress(cidrIp, include)); + mIpAddresses.add(new IpAddress(cidrIp, include)); } public void addIPSplit(CIDRIP cidrIp, boolean include) { - ipAddress newIP = new ipAddress(cidrIp, include); - ipAddress[] splitIps = newIP.split(); - for (ipAddress split : splitIps) + IpAddress newIP = new IpAddress(cidrIp, include); + IpAddress[] splitIps = newIP.split(); + for (IpAddress split : splitIps) mIpAddresses.add(split); } void addIPv6(Inet6Address address, int mask, boolean included) { - mIpAddresses.add(new ipAddress(address, mask, included)); + mIpAddresses.add(new IpAddress(address, mask, included)); } - TreeSet<ipAddress> generateIPList() { + TreeSet<IpAddress> generateIPList() { - PriorityQueue<ipAddress> networks = new PriorityQueue<ipAddress>(mIpAddresses); + PriorityQueue<IpAddress> networks = new PriorityQueue<IpAddress>(mIpAddresses); - TreeSet<ipAddress> ipsDone = new TreeSet<ipAddress>(); + TreeSet<IpAddress> ipsDone = new TreeSet<IpAddress>(); - ipAddress currentNet = networks.poll(); + IpAddress currentNet = networks.poll(); if (currentNet == null) return ipsDone; while (currentNet != null) { // Check if it and the next of it are compatible - ipAddress nextNet = networks.poll(); + IpAddress nextNet = networks.poll(); - if (BuildConfig.DEBUG) Assert.assertNotNull(currentNet); + if (BuildConfig.DEBUG) assertTrue(currentNet!=null); if (nextNet == null || currentNet.getLastAddress().compareTo(nextNet.getFirstAddress()) == -1) { // Everything good, no overlapping nothing to do ipsDone.add(currentNet); @@ -263,7 +268,7 @@ public class NetworkSpace { currentNet = nextNet; } else { // our currentNet is included in next and types differ. Need to split the next network - ipAddress[] newNets = nextNet.split(); + IpAddress[] newNets = nextNet.split(); // TODO: The contains method of the Priority is stupid linear search @@ -274,7 +279,7 @@ public class NetworkSpace { if (newNets[0].getLastAddress().equals(currentNet.getLastAddress())) { if (BuildConfig.DEBUG) - Assert.assertEquals(newNets[0].networkMask, currentNet.networkMask); + assertTrue(newNets[0].networkMask == currentNet.networkMask); // Don't add the lower half that would conflict with currentNet } else { if (!networks.contains(newNets[0])) @@ -284,9 +289,9 @@ public class NetworkSpace { } } else { if (BuildConfig.DEBUG) { - Assert.assertTrue(currentNet.networkMask < nextNet.networkMask); - Assert.assertTrue(nextNet.getFirstAddress().compareTo(currentNet.getFirstAddress()) == 1); - Assert.assertTrue(currentNet.getLastAddress().compareTo(nextNet.getLastAddress()) != -1); + assertTrue(currentNet.networkMask < nextNet.networkMask); + assertTrue(nextNet.getFirstAddress().compareTo(currentNet.getFirstAddress()) == 1); + assertTrue(currentNet.getLastAddress().compareTo(nextNet.getLastAddress()) != -1); } // This network is bigger than the next and last ip of current >= next @@ -296,13 +301,13 @@ public class NetworkSpace { // simply ignore the next and move on } else { // We need to split our network - ipAddress[] newNets = currentNet.split(); + IpAddress[] newNets = currentNet.split(); if (newNets[1].networkMask == nextNet.networkMask) { if (BuildConfig.DEBUG) { - Assert.assertTrue(newNets[1].getFirstAddress().equals(nextNet.getFirstAddress())); - Assert.assertTrue(newNets[1].getLastAddress().equals(currentNet.getLastAddress())); + assertTrue(newNets[1].getFirstAddress().equals(nextNet.getFirstAddress())); + assertTrue(newNets[1].getLastAddress().equals(currentNet.getLastAddress())); // split second equal the next network, do not add it } networks.add(nextNet); @@ -322,11 +327,11 @@ public class NetworkSpace { return ipsDone; } - Collection<ipAddress> getPositiveIPList() { - TreeSet<ipAddress> ipsSorted = generateIPList(); + Collection<IpAddress> getPositiveIPList() { + TreeSet<IpAddress> ipsSorted = generateIPList(); - Vector<ipAddress> ips = new Vector<ipAddress>(); - for (ipAddress ia : ipsSorted) { + Vector<IpAddress> ips = new Vector<IpAddress>(); + for (IpAddress ia : ipsSorted) { if (ia.included) ips.add(ia); } @@ -334,7 +339,7 @@ public class NetworkSpace { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { // Include postive routes from the original set under < 4.4 since these might overrule the local // network but only if no smaller negative route exists - for (ipAddress origIp : mIpAddresses) { + for (IpAddress origIp : mIpAddresses) { if (!origIp.included) continue; @@ -345,7 +350,7 @@ public class NetworkSpace { boolean skipIp = false; // If there is any smaller net that is excluded we may not add the positive route back - for (ipAddress calculatedIp : ipsSorted) { + for (IpAddress calculatedIp : ipsSorted) { if (!calculatedIp.included && origIp.containsNet(calculatedIp)) { skipIp = true; break; diff --git a/app/src/main/java/de/blinkt/openvpn/core/NetworkUtils.java b/app/src/main/java/de/blinkt/openvpn/core/NetworkUtils.java new file mode 100644 index 00000000..40449118 --- /dev/null +++ b/app/src/main/java/de/blinkt/openvpn/core/NetworkUtils.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2012-2018 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.content.Context; +import android.net.*; +import android.os.Build; +import android.text.TextUtils; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.util.Vector; + +public class NetworkUtils { + + public static Vector<String> getLocalNetworks(Context c, boolean ipv6) { + Vector<String> nets = new Vector<>(); + ConnectivityManager conn = (ConnectivityManager) c.getSystemService(Context.CONNECTIVITY_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Network[] networks = conn.getAllNetworks(); + for (Network network : networks) { + NetworkInfo ni = conn.getNetworkInfo(network); + LinkProperties li = conn.getLinkProperties(network); + + NetworkCapabilities nc = conn.getNetworkCapabilities(network); + + // Skip VPN networks like ourselves + if (nc.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) + continue; + + // Also skip mobile networks + if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) + continue; + + + for (LinkAddress la : li.getLinkAddresses()) { + if ((la.getAddress() instanceof Inet4Address && !ipv6) || + (la.getAddress() instanceof Inet6Address && ipv6)) + nets.add(la.toString()); + } + } + } else { + // Old Android Version, use native utils via ifconfig instead + // Add local network interfaces + if (ipv6) + return nets; + + String[] localRoutes = NativeUtils.getIfconfig(); + + // The format of mLocalRoutes is kind of broken because I don't really like JNI + for (int i = 0; i < localRoutes.length; i += 3) { + String intf = localRoutes[i]; + String ipAddr = localRoutes[i + 1]; + String netMask = localRoutes[i + 2]; + + if (intf == null || intf.equals("lo") || + intf.startsWith("tun") || intf.startsWith("rmnet")) + continue; + + if (ipAddr == null || netMask == null) { + VpnStatus.logError("Local routes are broken?! (Report to author) " + TextUtils.join("|", localRoutes)); + continue; + } + nets.add(ipAddr + "/" + CIDRIP.calculateLenFromMask(netMask)); + + } + + } + return nets; + } + +}
\ No newline at end of file diff --git a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java index f701b7aa..11bc4da3 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java +++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java @@ -7,8 +7,12 @@ package de.blinkt.openvpn.core; import android.Manifest.permission; import android.annotation.TargetApi; +import android.app.Activity; import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.app.UiModeManager; +import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; @@ -16,14 +20,17 @@ import android.content.pm.ShortcutManager; import android.content.res.Configuration; import android.content.res.Resources; import android.net.ConnectivityManager; +import android.net.Uri; import android.net.VpnService; import android.os.Build; +import android.os.Bundle; import android.os.Handler; import android.os.Handler.Callback; import android.os.IBinder; import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.RemoteException; +import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import android.system.OsConstants; import android.text.TextUtils; @@ -32,6 +39,7 @@ import android.widget.Toast; import java.io.IOException; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; @@ -39,6 +47,8 @@ import java.util.Collection; import java.util.Locale; import java.util.Vector; +import de.blinkt.openvpn.LaunchVPN; +import se.leap.bitmaskclient.R; import de.blinkt.openvpn.VpnProfile; import de.blinkt.openvpn.core.VpnStatus.ByteCountListener; import de.blinkt.openvpn.core.VpnStatus.StateListener; @@ -47,7 +57,7 @@ import se.leap.bitmaskclient.VpnNotificationManager; import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_CONNECTED; import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT; -import static de.blinkt.openvpn.core.NetworkSpace.ipAddress; +import static de.blinkt.openvpn.core.NetworkSpace.IpAddress; public class OpenVPNService extends VpnService implements StateListener, Callback, ByteCountListener, IOpenVPNServiceInternal, VpnNotificationManager.VpnServiceCallback { @@ -55,12 +65,12 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac 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 = "se.leap.bitmaskclient.RESUME_VPN"; public static final String NOTIFICATION_CHANNEL_BG_ID = "openvpn_bg"; public static final String NOTIFICATION_CHANNEL_NEWSTATUS_ID = "openvpn_newstat"; public static final String VPNSERVICE_TUN = "vpnservice-tun"; - + public final static String ORBOT_PACKAGE_NAME = "org.torproject.android"; + private static final String PAUSE_VPN = "de.blinkt.openvpn.PAUSE_VPN"; + private static final String RESUME_VPN = "se.leap.bitmaskclient.RESUME_VPN"; private static boolean mNotificationAlwaysVisible = false; private final Vector<String> mDnslist = new Vector<>(); private final NetworkSpace mRoutes = new NetworkSpace(); @@ -407,8 +417,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } Runnable processThread; - if (useOpenVPN3) - { + if (useOpenVPN3) { OpenVPNManagement mOpenVPN3 = instantiateOpenVPN3Core(); processThread = (Runnable) mOpenVPN3; mManagement = mOpenVPN3; @@ -545,7 +554,9 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } if (mLocalIP != null) { - addLocalNetworksToRoutes(); + // OpenVPN3 manages excluded local networks by callback + if (!VpnProfile.doUseOpenVPN3(this)) + addLocalNetworksToRoutes(); try { builder.addAddress(mLocalIP.mIp, mLocalIP.len); } catch (IllegalArgumentException iae) { @@ -584,15 +595,15 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac builder.setMtu(mMtu); } - Collection<ipAddress> positiveIPv4Routes = mRoutes.getPositiveIPList(); - Collection<ipAddress> positiveIPv6Routes = mRoutesv6.getPositiveIPList(); + Collection<IpAddress> positiveIPv4Routes = mRoutes.getPositiveIPList(); + Collection<IpAddress> positiveIPv6Routes = mRoutesv6.getPositiveIPList(); if ("samsung".equals(Build.BRAND) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mDnslist.size() >= 1) { // Check if the first DNS Server is in the VPN range try { - ipAddress dnsServer = new ipAddress(new CIDRIP(mDnslist.get(0), 32), true); + IpAddress dnsServer = new IpAddress(new CIDRIP(mDnslist.get(0), 32), true); boolean dnsIncluded = false; - for (ipAddress net : positiveIPv4Routes) { + for (IpAddress net : positiveIPv4Routes) { if (net.containsNet(dnsServer)) { dnsIncluded = true; } @@ -609,9 +620,9 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } } - ipAddress multicastRange = new ipAddress(new CIDRIP("224.0.0.0", 3), true); + IpAddress multicastRange = new IpAddress(new CIDRIP("224.0.0.0", 3), true); - for (NetworkSpace.ipAddress route : positiveIPv4Routes) { + for (IpAddress route : positiveIPv4Routes) { try { if (multicastRange.containsNet(route)) @@ -623,7 +634,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } } - for (NetworkSpace.ipAddress route6 : positiveIPv6Routes) { + for (IpAddress route6 : positiveIPv6Routes) { try { builder.addRoute(route6.getIPv6Address(), route6.networkMask); } catch (IllegalArgumentException ia) { @@ -635,7 +646,16 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac if (mDomain != null) builder.addSearchDomain(mDomain); - VpnStatus.logInfo(R.string.local_ip_info, mLocalIP.mIp, mLocalIP.len, mLocalIPv6, mMtu); + String ipv4info; + int ipv4len; + if (mLocalIP!=null) { + ipv4len=mLocalIP.len; + ipv4info=mLocalIP.mIp; + } else { + ipv4len = -1; + ipv4info="(not set)"; + } + VpnStatus.logInfo(R.string.local_ip_info, ipv4info, ipv4len, mLocalIPv6, mMtu); VpnStatus.logInfo(R.string.dns_server_info, TextUtils.join(", ", mDnslist), mDomain); VpnStatus.logInfo(R.string.routes_info_incl, TextUtils.join(", ", mRoutes.getNetworks(true)), TextUtils.join(", ", mRoutesv6.getNetworks(true))); VpnStatus.logInfo(R.string.routes_info_excl, TextUtils.join(", ", mRoutes.getNetworks(false)), TextUtils.join(", ", mRoutesv6.getNetworks(false))); @@ -643,6 +663,10 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setAllowedVpnPackages(builder); } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + // VPN always uses the default network + builder.setUnderlyingNetworks(null); + } String session = mProfile.mName; @@ -650,6 +674,8 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac session = getString(R.string.session_ipv6string, session, mLocalIP, mLocalIPv6); else if (mLocalIP != null) session = getString(R.string.session_ipv4string, session, mLocalIP); + else + session = getString(R.string.session_ipv4string, session, mLocalIPv6); builder.setSession(session); @@ -691,25 +717,11 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } private void addLocalNetworksToRoutes() { - - // Add local network interfaces - String[] localRoutes = NativeUtils.getIfconfig(); - - // The format of mLocalRoutes is kind of broken because I don't really like JNI - for (int i = 0; i < localRoutes.length; i += 3) { - String intf = localRoutes[i]; - String ipAddr = localRoutes[i + 1]; - String netMask = localRoutes[i + 2]; - - if (intf == null || intf.equals("lo") || - intf.startsWith("tun") || intf.startsWith("rmnet")) - continue; - - if (ipAddr == null || netMask == null) { - VpnStatus.logError("Local routes are broken?! (Report to author) " + TextUtils.join("|", localRoutes)); - continue; - } - + for (String net: NetworkUtils.getLocalNetworks(this, false)) + { + String[] netparts = net.split("/"); + String ipAddr = netparts[0]; + int netMask = Integer.parseInt(netparts[1]); if (ipAddr.equals(mLocalIP.mIp)) continue; @@ -719,19 +731,50 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && mProfile.mAllowLocalLAN) mRoutes.addIP(new CIDRIP(ipAddr, netMask), false); } + + // IPv6 is Lollipop+ only so we can skip the lower than KITKAT case + if (mProfile.mAllowLocalLAN) { + for (String net : NetworkUtils.getLocalNetworks(this, true)) { + addRoutev6(net, false);; + } + } + + } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void setAllowedVpnPackages(Builder builder) { + boolean profileUsesOrBot = false; + + for (Connection c : mProfile.mConnections) { + if (c.mProxyType == Connection.ProxyType.ORBOT) + profileUsesOrBot = true; + } + + if (profileUsesOrBot) + VpnStatus.logDebug("VPN Profile uses at least one server entry with Orbot. Setting up VPN so that OrBot is not redirected over VPN."); + + boolean atLeastOneAllowedApp = false; + + if (mProfile.mAllowedAppsVpnAreDisallowed && profileUsesOrBot) { + try { + builder.addDisallowedApplication(ORBOT_PACKAGE_NAME); + } catch (PackageManager.NameNotFoundException e) { + VpnStatus.logDebug("Orbot not installed?"); + } + } + for (String pkg : mProfile.mAllowedAppsVpn) { try { if (mProfile.mAllowedAppsVpnAreDisallowed) { builder.addDisallowedApplication(pkg); } else { - builder.addAllowedApplication(pkg); - atLeastOneAllowedApp = true; + if (!(profileUsesOrBot && pkg.equals(ORBOT_PACKAGE_NAME))) { + builder.addAllowedApplication(pkg); + atLeastOneAllowedApp = true; + } } } catch (PackageManager.NameNotFoundException e) { mProfile.mAllowedAppsVpn.remove(pkg); @@ -776,13 +819,13 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac CIDRIP route = new CIDRIP(dest, mask); boolean include = isAndroidTunDevice(device); - NetworkSpace.ipAddress gatewayIP = new NetworkSpace.ipAddress(new CIDRIP(gateway, 32), false); + IpAddress gatewayIP = new IpAddress(new CIDRIP(gateway, 32), false); if (mLocalIP == null) { VpnStatus.logError("Local IP address unset and received. Neither pushed server config nor local config specifies an IP addresses. Opening tun device is most likely going to fail."); return; } - NetworkSpace.ipAddress localNet = new NetworkSpace.ipAddress(mLocalIP, true); + IpAddress localNet = new IpAddress(mLocalIP, true); if (localNet.containsNet(gatewayIP)) include = true; @@ -802,10 +845,13 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } public void addRoutev6(String network, String device) { - String[] v6parts = network.split("/"); + // Tun is opened after ROUTE6, no device name may be present boolean included = isAndroidTunDevice(device); + addRoutev6(network, included); + } - // Tun is opened after ROUTE6, no device name may be present + public void addRoutev6(String network, boolean included) { + String[] v6parts = network.split("/"); try { Inet6Address ip = (Inet6Address) InetAddress.getAllByName(v6parts[0])[0]; @@ -870,7 +916,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac if (mLocalIP.len <= 31 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { CIDRIP interfaceRoute = new CIDRIP(mLocalIP.mIp, mLocalIP.len); interfaceRoute.normalise(); - addRoute(interfaceRoute ,true); + addRoute(interfaceRoute, true); } @@ -895,11 +941,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac // 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) { + if (level == LEVEL_CONNECTED) { mDisplayBytecount = true; mConnecttime = System.currentTimeMillis(); if (!runningOnAndroidTV()) @@ -1005,4 +1047,51 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac public void onNotificationStop() { stopForeground(true); } + + public void trigger_url_open(String info) { + /* + String channel = NOTIFICATION_CHANNEL_USERREQ_ID; + + + String url = info.split(":",2)[1]; + + NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + Notification.Builder nbuilder = new Notification.Builder(this); + nbuilder.setContentTitle(getString(R.string.openurl_requested)); + + nbuilder.setContentText(url); + nbuilder.setAutoCancel(true); + + int icon = android.R.drawable.ic_dialog_info; + + nbuilder.setSmallIcon(icon); + + Intent openUrlIntent = new Intent(Intent.ACTION_VIEW); + openUrlIntent.setData(Uri.parse(url)); + openUrlIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + nbuilder.setContentIntent(PendingIntent.getActivity(this,0, openUrlIntent, 0)); + + + // Try to set the priority available since API 16 (Jellybean) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + jbNotificationExtras(PRIORITY_MAX, nbuilder); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + lpNotificationExtras(nbuilder, Notification.CATEGORY_STATUS); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + //noinspection NewApi + nbuilder.setChannelId(channel); + } + + @SuppressWarnings("deprecation") + Notification notification = nbuilder.getNotification(); + + int notificationId = channel.hashCode(); + + mNotificationManager.notify(notificationId, notification); + */ + } }
\ No newline at end of file diff --git a/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java b/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java index 2b6df9af..4f7a5bda 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java +++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java @@ -6,16 +6,20 @@ package de.blinkt.openvpn.core; import android.content.Context; +import android.content.Intent; import android.net.LocalServerSocket; import android.net.LocalSocket; import android.net.LocalSocketAddress; +import android.os.Build; import android.os.Handler; import android.os.ParcelFileDescriptor; import android.support.annotation.NonNull; -import android.text.TextUtils; +import android.support.annotation.RequiresApi; +import android.system.ErrnoException; +import android.system.Os; import android.util.Log; - -import junit.framework.Assert; +import se.leap.bitmaskclient.R; +import de.blinkt.openvpn.VpnProfile; import java.io.FileDescriptor; import java.io.IOException; @@ -24,19 +28,13 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.InetSocketAddress; import java.net.SocketAddress; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedList; -import java.util.Locale; -import java.util.Vector; - -import se.leap.bitmaskclient.BuildConfig; -import se.leap.bitmaskclient.R; -import de.blinkt.openvpn.VpnProfile; +import java.util.*; public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { + public static final int ORBOT_TIMEOUT_MS = 20 * 1000; private static final String TAG = "openvpn"; + private static final Vector<OpenVpnManagementThread> active = new Vector<>(); private final Handler mResumeHandler; private LocalSocket mSocket; private VpnProfile mProfile; @@ -45,13 +43,55 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { private LocalServerSocket mServerSocket; private boolean mWaitingForRelease = false; private long mLastHoldRelease = 0; - - private static final Vector<OpenVpnManagementThread> active = new Vector<>(); private LocalSocket mServerSocketLocal; private pauseReason lastPauseReason = pauseReason.noNetwork; private PausedStateCallback mPauseCallback; private boolean mShuttingDown; + private Runnable mResumeHoldRunnable = () -> { + if (shouldBeRunning()) { + releaseHoldCmd(); + } + }; + private Runnable orbotStatusTimeOutRunnable = new Runnable() { + @Override + public void run() { + sendProxyCMD(Connection.ProxyType.SOCKS5, "127.0.0.1", Integer.toString(OrbotHelper.SOCKS_PROXY_PORT_DEFAULT), false); + OrbotHelper.get(mOpenVPNService).removeStatusCallback(statusCallback); + + } + }; + private OrbotHelper.StatusCallback statusCallback = new OrbotHelper.StatusCallback() { + + @Override + public void onStatus(Intent statusIntent) { + StringBuilder extras = new StringBuilder(); + for (String key : statusIntent.getExtras().keySet()) { + Object val = statusIntent.getExtras().get(key); + + extras.append(String.format(Locale.ENGLISH, "%s - '%s'", key, val == null ? "null" : val.toString())); + } + VpnStatus.logDebug("Got Orbot status: " + extras); + } + + @Override + public void onNotYetInstalled() { + VpnStatus.logDebug("Orbot not yet installed"); + } + + @Override + public void onOrbotReady(Intent intent, String socksHost, int socksPort) { + mResumeHandler.removeCallbacks(orbotStatusTimeOutRunnable); + sendProxyCMD(Connection.ProxyType.SOCKS5, socksHost, Integer.toString(socksPort), false); + OrbotHelper.get(mOpenVPNService).removeStatusCallback(this); + } + + @Override + public void onDisabled(Intent intent) { + VpnStatus.logWarning("Orbot integration for external applications is disabled. Waiting %ds before connecting to the default port. Enable external app integration in Orbot or use Socks v5 config instead of Orbot to avoid this delay."); + } + }; + private transient Connection mCurrentProxyConnection; public OpenVpnManagementThread(VpnProfile profile, OpenVPNService openVpnService) { mProfile = profile; @@ -60,14 +100,21 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { } - private Runnable mResumeHoldRunnable = new Runnable() { - @Override - public void run() { - if (shouldBeRunning()) { - releaseHoldCmd(); + private static boolean stopOpenVPN() { + synchronized (active) { + boolean sendCMD = false; + for (OpenVpnManagementThread mt : active) { + sendCMD = mt.managmentCommand("signal SIGINT\n"); + try { + if (mt.mSocket != null) + mt.mSocket.close(); + } catch (IOException e) { + // Ignore close error on already closed socket + } } + return sendCMD; } - }; + } public boolean openManagementInterface(@NonNull Context c) { // Could take a while to open connection @@ -122,7 +169,6 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { return false; } - @Override public void run() { byte[] buffer = new byte[2048]; @@ -198,9 +244,13 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { //ParcelFileDescriptor pfd = ParcelFileDescriptor.fromFd(fdint); //pfd.close(); - NativeUtils.jniclose(fdint); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + fdCloseLollipop(fd); + } else { + NativeUtils.jniclose(fdint); + } return; - } catch (NoSuchMethodException | IllegalArgumentException | InvocationTargetException | IllegalAccessException | NullPointerException e) { + } catch ( NoSuchMethodException | IllegalArgumentException | InvocationTargetException | IllegalAccessException | NullPointerException e) { VpnStatus.logException("Failed to retrieve fd from socket (" + fd + ")", e); } @@ -208,6 +258,15 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { } + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private void fdCloseLollipop(FileDescriptor fd) { + try { + Os.close(fd); + } catch (Exception e) { + VpnStatus.logException("Failed to close fd (" + fd + ")", e); + } + } + private String processInput(String pendingInput) { @@ -223,11 +282,9 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { return pendingInput; } - private void processCommand(String command) { //Log.i(TAG, "Line from managment" + command); - if (command.startsWith(">") && command.contains(":")) { String[] parts = command.split(":", 2); String cmd = parts[0].substring(1); @@ -263,6 +320,9 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { case "PK_SIGN": processSignCommand(argument); break; + case "INFOMSG": + processInfoMessage(argument); + break; default: VpnStatus.logWarning("MGMT: Got unrecognized command" + command); Log.i(TAG, "Got unrecognized command" + command); @@ -281,6 +341,14 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { } } + private void processInfoMessage(String info) + { + if (info.startsWith("OPEN_URL:")) + { + mOpenVPNService.trigger_url_open(info); + } + } + private void processLogMessage(String argument) { String[] args = argument.split(",", 4); // 0 unix time stamp @@ -367,7 +435,6 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { //managmentCommand("log on all\n"); } - public void releaseHold() { if (mWaitingForRelease) releaseHoldCmd(); @@ -375,27 +442,81 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { private void processProxyCMD(String argument) { String[] args = argument.split(",", 3); - SocketAddress proxyaddr = ProxyDetection.detectProxy(mProfile); + Connection.ProxyType proxyType = Connection.ProxyType.NONE; + + int connectionEntryNumber = Integer.parseInt(args[0]) - 1; + String proxyport = null; + String proxyname = null; + boolean proxyUseAuth = false; + + if (mProfile.mConnections.length > connectionEntryNumber) { + Connection connection = mProfile.mConnections[connectionEntryNumber]; + proxyType = connection.mProxyType; + proxyname = connection.mProxyName; + proxyport = connection.mProxyPort; + proxyUseAuth = connection.mUseProxyAuth; + + // Use transient variable to remember http user/password + mCurrentProxyConnection = connection; + + } else { + VpnStatus.logError(String.format(Locale.ENGLISH, "OpenVPN is asking for a proxy of an unknown connection entry (%d)", connectionEntryNumber)); + } - if (args.length >= 2) { + // atuo detection of proxy + if (proxyType == Connection.ProxyType.NONE) { + SocketAddress proxyaddr = ProxyDetection.detectProxy(mProfile); + if (proxyaddr instanceof InetSocketAddress) { + InetSocketAddress isa = (InetSocketAddress) proxyaddr; + proxyType = Connection.ProxyType.HTTP; + proxyname = isa.getHostName(); + proxyport = String.valueOf(isa.getPort()); + proxyUseAuth = false; + + } + } + + + if (args.length >= 2 && proxyType == Connection.ProxyType.HTTP) { String proto = args[1]; if (proto.equals("UDP")) { - proxyaddr = null; + proxyname = null; + VpnStatus.logInfo("Not using an HTTP proxy since the connection uses UDP"); } } - if (proxyaddr instanceof InetSocketAddress) { - InetSocketAddress isa = (InetSocketAddress) proxyaddr; - VpnStatus.logInfo(R.string.using_proxy, isa.getHostName(), isa.getPort()); + if (proxyType == Connection.ProxyType.ORBOT) { + VpnStatus.updateStateString("WAIT_ORBOT", "Waiting for Orbot to start", R.string.state_waitorbot, ConnectionStatus.LEVEL_CONNECTING_NO_SERVER_REPLY_YET); + OrbotHelper orbotHelper = OrbotHelper.get(mOpenVPNService); + if (!orbotHelper.checkTorReceier(mOpenVPNService)) + VpnStatus.logError("Orbot does not seem to be installed!"); + + mResumeHandler.postDelayed(orbotStatusTimeOutRunnable, ORBOT_TIMEOUT_MS); + orbotHelper.addStatusCallback(mOpenVPNService, statusCallback); + + orbotHelper.sendOrbotStartAndStatusBroadcast(); + + } else { + sendProxyCMD(proxyType, proxyname, proxyport, proxyUseAuth); + } + } + + private void sendProxyCMD(Connection.ProxyType proxyType, String proxyname, String proxyport, boolean usePwAuth) { + if (proxyType != Connection.ProxyType.NONE && proxyname != null) { + + VpnStatus.logInfo(R.string.using_proxy, proxyname, proxyname); - String proxycmd = String.format(Locale.ENGLISH, "proxy HTTP %s %d\n", isa.getHostName(), isa.getPort()); + String pwstr = usePwAuth ? " auto" : ""; + + String proxycmd = String.format(Locale.ENGLISH, "proxy %s %s %s%s\n", + proxyType == Connection.ProxyType.HTTP ? "HTTP" : "SOCKS", + proxyname, proxyport, pwstr); managmentCommand(proxycmd); } else { managmentCommand("proxy NONE\n"); } - } private void processState(String argument) { @@ -408,7 +529,6 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { VpnStatus.updateStateString(currentstate, args[2]); } - private void processByteCount(String argument) { // >BYTECOUNT:{BYTES_IN},{BYTES_OUT} int comma = argument.indexOf(','); @@ -419,7 +539,6 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { } - private void processNeedCommand(String argument) { int p1 = argument.indexOf('\''); int p2 = argument.indexOf('\'', p1 + 1); @@ -452,7 +571,8 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { */ if (routeparts.length == 5) { - if (BuildConfig.DEBUG) Assert.assertEquals("dev", routeparts[3]); + //if (BuildConfig.DEBUG) + // assertEquals("dev", routeparts[3]); mOpenVPNService.addRoute(routeparts[0], routeparts[1], routeparts[2], routeparts[4]); } else if (routeparts.length >= 3) { mOpenVPNService.addRoute(routeparts[0], routeparts[1], routeparts[2], null); @@ -473,8 +593,10 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { mOpenVPNService.setLocalIP(ifconfigparts[0], ifconfigparts[1], mtu, ifconfigparts[3]); break; case "IFCONFIG6": - mOpenVPNService.setLocalIPv6(extra); - + String[] ifconfig6parts = extra.split(" "); + mtu = Integer.parseInt(ifconfig6parts[1]); + mOpenVPNService.setMtu(mtu); + mOpenVPNService.setLocalIPv6(ifconfig6parts[0]); break; case "PERSIST_TUN_ACTION": // check if tun cfg stayed the same @@ -546,6 +668,10 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { try { + // Ignore Auth token message, already managed by openvpn itself + if (argument.startsWith("Auth-Token:")) { + return; + } int p1 = argument.indexOf('\''); int p2 = argument.indexOf('\'', p1 + 1); @@ -560,17 +686,26 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { } String pw = null; + String username = null; if (needed.equals("Private Key")) { pw = mProfile.getPasswordPrivateKey(); } else if (needed.equals("Auth")) { pw = mProfile.getPasswordAuth(); + username = mProfile.mUsername; - String usercmd = String.format("username '%s' %s\n", - needed, VpnProfile.openVpnEscape(mProfile.mUsername)); - managmentCommand(usercmd); + } else if (needed.equals("HTTP Proxy")) { + if( mCurrentProxyConnection != null) { + pw = mCurrentProxyConnection.mProxyAuthPassword; + username = mCurrentProxyConnection.mProxyAuthUser; + } } if (pw != null) { + if (username !=null) { + String usercmd = String.format("username '%s' %s\n", + needed, VpnProfile.openVpnEscape(username)); + managmentCommand(usercmd); + } String cmd = String.format("password '%s' %s\n", needed, VpnProfile.openVpnEscape(pw)); managmentCommand(cmd); } else { @@ -580,28 +715,10 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { } - private void proccessPWFailed(String needed, String args) { VpnStatus.updateStateString("AUTH_FAILED", needed + args, R.string.state_auth_failed, ConnectionStatus.LEVEL_AUTH_FAILED); } - - private static boolean stopOpenVPN() { - synchronized (active) { - boolean sendCMD = false; - for (OpenVpnManagementThread mt : active) { - sendCMD = mt.managmentCommand("signal SIGINT\n"); - try { - if (mt.mSocket != null) - mt.mSocket.close(); - } catch (IOException e) { - // Ignore close error on already closed socket - } - } - return sendCMD; - } - } - @Override public void networkChange(boolean samenetwork) { if (mWaitingForRelease) @@ -634,7 +751,7 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { private void processSignCommand(String b64data) { - String signed_string = mProfile.getSignedData(b64data); + String signed_string = mProfile.getSignedData(mOpenVPNService, b64data, false); if (signed_string == null) { managmentCommand("pk-sig\n"); diff --git a/app/src/main/java/de/blinkt/openvpn/core/OrbotHelper.java b/app/src/main/java/de/blinkt/openvpn/core/OrbotHelper.java new file mode 100644 index 00000000..68f5835f --- /dev/null +++ b/app/src/main/java/de/blinkt/openvpn/core/OrbotHelper.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2012-2018 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +/* + * Portions Copyright 2014-2016 Hans-Christoph Steiner + * Portions Copyright 2012-2016 Nathan Freitas + * Portions Copyright (c) 2016 CommonsWare, LLC + * + * 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 de.blinkt.openvpn.core; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.text.TextUtils; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static de.blinkt.openvpn.core.OpenVPNService.ORBOT_PACKAGE_NAME; + +public class OrbotHelper { + //! Based on the class from NetCipher but stripped down and modified for icsopenvpn + + /** + * {@link Intent} send by Orbot with {@code ON/OFF/STARTING/STOPPING} status + * included as an {@link #EXTRA_STATUS} {@code String}. Your app should + * always receive {@code ACTION_STATUS Intent}s since any other app could + * start Orbot. Also, user-triggered starts and stops will also cause + * {@code ACTION_STATUS Intent}s to be broadcast. + */ + public final static String ACTION_STATUS = "org.torproject.android.intent.action.STATUS"; + public final static String STATUS_ON = "ON"; + public final static String STATUS_STARTS_DISABLED = "STARTS_DISABLED"; + + public final static String STATUS_STARTING = "STARTING"; + public final static String STATUS_STOPPING = "STOPPING"; + public final static String EXTRA_STATUS = "org.torproject.android.intent.extra.STATUS"; + /** + * A request to Orbot to transparently start Tor services + */ + public final static String ACTION_START = "org.torproject.android.intent.action.START"; + public final static String EXTRA_PACKAGE_NAME = "org.torproject.android.intent.extra.PACKAGE_NAME"; + public static final int SOCKS_PROXY_PORT_DEFAULT = 9050; + private static OrbotHelper mInstance; + + String EXTRA_SOCKS_PROXY_HOST = "org.torproject.android.intent.extra.SOCKS_PROXY_HOST"; + String EXTRA_SOCKS_PROXY_PORT = "org.torproject.android.intent.extra.SOCKS_PROXY_PORT"; + private Context mContext; + private Set<StatusCallback> statusCallbacks = new HashSet<>(); + private BroadcastReceiver orbotStatusReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context c, Intent intent) { + if (TextUtils.equals(intent.getAction(), + OrbotHelper.ACTION_STATUS)) { + for (StatusCallback cb : statusCallbacks) { + cb.onStatus(intent); + } + + String status = intent.getStringExtra(EXTRA_STATUS); + if (TextUtils.equals(status, STATUS_ON)) { + int socksPort = intent.getIntExtra(EXTRA_SOCKS_PROXY_PORT, SOCKS_PROXY_PORT_DEFAULT); + String socksHost = intent.getStringExtra(EXTRA_SOCKS_PROXY_HOST); + if (TextUtils.isEmpty(socksHost)) + socksHost = "127.0.0.1"; + for (StatusCallback cb : statusCallbacks) { + cb.onOrbotReady(intent, socksHost, socksPort); + } + } else if (TextUtils.equals(status, STATUS_STARTS_DISABLED)) { + for (StatusCallback cb : statusCallbacks) + cb.onDisabled(intent); + } + + } + } + }; + + private OrbotHelper() { + + } + + public static OrbotHelper get(OpenVPNService mOpenVPNService) { + if (mInstance == null) + mInstance = new OrbotHelper(); + return mInstance; + } + + /** + * Gets an {@link Intent} for starting Orbot. Orbot will reply with the + * current status to the {@code packageName} of the app in the provided + * {@link Context} (i.e. {@link Context#getPackageName()}. + */ + public static Intent getOrbotStartIntent(Context context) { + Intent intent = new Intent(ACTION_START); + intent.setPackage(ORBOT_PACKAGE_NAME); + intent.putExtra(EXTRA_PACKAGE_NAME, context.getPackageName()); + return intent; + } + + public static boolean checkTorReceier(Context c) { + Intent startOrbot = getOrbotStartIntent(c); + PackageManager pm = c.getPackageManager(); + Intent result = null; + List<ResolveInfo> receivers = + pm.queryBroadcastReceivers(startOrbot, 0); + + return receivers != null && receivers.size() > 0; + } + + /** + * Adds a StatusCallback to be called when we find out that + * Orbot is ready. If Orbot is ready for use, your callback + * will be called with onEnabled() immediately, before this + * method returns. + * + * @param cb a callback + * @return the singleton, for chaining + */ + public synchronized OrbotHelper addStatusCallback(Context c, StatusCallback cb) { + if (statusCallbacks.size() == 0) { + c.getApplicationContext().registerReceiver(orbotStatusReceiver, + new IntentFilter(OrbotHelper.ACTION_STATUS)); + mContext = c.getApplicationContext(); + } + if (!checkTorReceier(c)) + cb.onNotYetInstalled(); + statusCallbacks.add(cb); + return (this); + } + + /** + * Removes an existing registered StatusCallback. + * + * @param cb the callback to remove + * @return the singleton, for chaining + */ + public synchronized void removeStatusCallback(StatusCallback cb) { + statusCallbacks.remove(cb); + if (statusCallbacks.size() == 0) + mContext.unregisterReceiver(orbotStatusReceiver); + } + + public void sendOrbotStartAndStatusBroadcast() { + mContext.sendBroadcast(getOrbotStartIntent(mContext)); + } + + private void startOrbotService(String action) { + Intent clearVPNMode = new Intent(); + clearVPNMode.setComponent(new ComponentName(ORBOT_PACKAGE_NAME, ".service.TorService")); + clearVPNMode.setAction(action); + mContext.startService(clearVPNMode); + } + + public interface StatusCallback { + /** + * Called when Orbot is operational + * + * @param statusIntent an Intent containing information about + * Orbot, including proxy ports + */ + void onStatus(Intent statusIntent); + + + /** + * Called if Orbot is not yet installed. Usually, you handle + * this by checking the return value from init() on OrbotInitializer + * or calling isInstalled() on OrbotInitializer. However, if + * you have need for it, if a callback is registered before + * an init() call determines that Orbot is not installed, your + * callback will be called with onNotYetInstalled(). + */ + void onNotYetInstalled(); + + void onOrbotReady(Intent intent, String socksHost, int socksPort); + + /** + * Called if Orbot background control is disabled. + * @param intent the intent delivered + */ + void onDisabled(Intent intent); + } +} diff --git a/app/src/main/java/de/blinkt/openvpn/core/ProfileManager.java b/app/src/main/java/de/blinkt/openvpn/core/ProfileManager.java index f776fc2e..b9edc4b2 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/ProfileManager.java +++ b/app/src/main/java/de/blinkt/openvpn/core/ProfileManager.java @@ -128,6 +128,7 @@ public class ProfileManager { } public static void setTemporaryProfile(Context c, VpnProfile tmp) { + tmp.mTemporaryProfile = true; ProfileManager.tmpprofile = tmp; saveProfile(c, tmp, true, true); } @@ -174,8 +175,9 @@ public class ProfileManager { vlist.add(TEMPORARY_PROFILE_FILENAME); for (String vpnentry : vlist) { + ObjectInputStream vpnfile=null; try { - ObjectInputStream vpnfile = new ObjectInputStream(context.openFileInput(vpnentry + ".vp")); + vpnfile = new ObjectInputStream(context.openFileInput(vpnentry + ".vp")); VpnProfile vp = ((VpnProfile) vpnfile.readObject()); // Sanity check @@ -189,9 +191,18 @@ public class ProfileManager { profiles.put(vp.getUUID().toString(), vp); } + } catch (IOException | ClassNotFoundException e) { if (!vpnentry.equals(TEMPORARY_PROFILE_FILENAME)) VpnStatus.logException("Loading VPN List", e); + } finally { + if (vpnfile!=null) { + try { + vpnfile.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } } } } diff --git a/app/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java b/app/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java index 97a73964..810974df 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java +++ b/app/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java @@ -48,7 +48,7 @@ public class VPNLaunchHelper { } } - return null; + throw new RuntimeException("Cannot find any execulte for this device's ABIs " + abis.toString()); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) diff --git a/app/src/main/java/de/blinkt/openvpn/core/VpnStatus.java b/app/src/main/java/de/blinkt/openvpn/core/VpnStatus.java index a9cc4f18..5fbb440b 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/VpnStatus.java +++ b/app/src/main/java/de/blinkt/openvpn/core/VpnStatus.java @@ -300,6 +300,8 @@ public class VpnStatus { return R.string.state_resolve; case "TCP_CONNECT": return R.string.state_tcp_connect; + case "AUTH_PENDING": + return R.string.state_auth_pending; default: return R.string.unknown_state; } @@ -323,7 +325,7 @@ public class VpnStatus { private static ConnectionStatus getLevel(String state) { String[] noreplyet = {"CONNECTING", "WAIT", "RECONNECTING", "RESOLVE", "TCP_CONNECT"}; - String[] reply = {"AUTH", "GET_CONFIG", "ASSIGN_IP", "ADD_ROUTES"}; + String[] reply = {"AUTH", "GET_CONFIG", "ASSIGN_IP", "ADD_ROUTES", "AUTH_PENDING"}; String[] connected = {"CONNECTED"}; String[] notconnected = {"DISCONNECTED", "EXITING"}; @@ -385,7 +387,7 @@ public class VpnStatus { for (StateListener sl : stateListener) { sl.updateState(state, msg, resid, level); } - newLogItem(new LogItem((LogLevel.DEBUG), String.format("New OpenVPN Status (%s->%s): %s",state,level.toString(),msg))); + //newLogItem(new LogItem((LogLevel.DEBUG), String.format("New OpenVPN Status (%s->%s): %s",state,level.toString(),msg))); } public static void logInfo(String message) { |