diff options
author | Parménides GV <parmegv@sdf.org> | 2015-01-31 01:19:49 +0100 |
---|---|---|
committer | Parménides GV <parmegv@sdf.org> | 2015-01-31 01:19:49 +0100 |
commit | c95a21a736fadb46685e051064b0ec1efdae667a (patch) | |
tree | 90c4af22d047b7297a66a9e623c86c5af63b12f6 /app/src | |
parent | 1a4643dc08a86dcd9650afa2255945df14445f2d (diff) |
Updated ics-openvpn to rev 1020.
Improved build.gradle script.
Diffstat (limited to 'app/src')
-rw-r--r-- | app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java | 1030 | ||||
-rw-r--r-- | app/src/main/java/de/blinkt/openvpn/core/NetworkSpace.java | 13 | ||||
-rw-r--r-- | app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java | 34 | ||||
-rw-r--r-- | app/src/main/res/drawable-hdpi/ic_check_white_24dp.png | bin | 0 -> 309 bytes | |||
-rw-r--r-- | app/src/main/res/drawable-mdpi/ic_check_white_24dp.png | bin | 0 -> 243 bytes | |||
-rw-r--r-- | app/src/main/res/drawable-xhdpi/ic_check_white_24dp.png | bin | 0 -> 363 bytes | |||
-rw-r--r-- | app/src/main/res/drawable-xxhdpi/ic_check_white_24dp.png | bin | 0 -> 460 bytes | |||
-rw-r--r-- | app/src/main/res/drawable-xxxhdpi/ic_check_white_24dp.png | bin | 0 -> 587 bytes | |||
-rw-r--r-- | app/src/main/res/layout/log_fragment.xml | 1 | ||||
-rw-r--r-- | app/src/main/res/values-v21/refs.xml | 3 | ||||
-rwxr-xr-x | app/src/main/res/values/strings-icsopenvpn.xml | 1 |
11 files changed, 546 insertions, 536 deletions
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 5dc96bbc..5f5d486c 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java +++ b/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java @@ -28,11 +28,11 @@ import de.blinkt.openvpn.VpnProfile; public class ConfigParser { - public static final String CONVERTED_PROFILE = "converted Profile"; - private HashMap<String, Vector<Vector<String>>> options = new HashMap<String, Vector<Vector<String>>>(); - private HashMap<String, Vector<String>> meta = new HashMap<String, Vector<String>>(); + public static final String CONVERTED_PROFILE = "converted Profile"; + private HashMap<String, Vector<Vector<String>>> options = new HashMap<String, Vector<Vector<String>>>(); + private HashMap<String, Vector<String>> meta = new HashMap<String, Vector<String>>(); - public void parseConfig(Reader reader) throws IOException, ConfigParseError { + public void parseConfig(Reader reader) throws IOException, ConfigParseError { BufferedReader br = new BufferedReader(reader); @@ -75,192 +75,176 @@ public class ConfigParser { } catch (java.lang.OutOfMemoryError memoryError) { throw new ConfigParseError("File too large to parse: " + memoryError.getLocalizedMessage()); } - } + } - private Vector<String> parsemeta(String line) { - String meta = line.split("#\\sOVPN_ACCESS_SERVER_", 2)[1]; - String[] parts = meta.split("=",2); - Vector<String> rval = new Vector<String>(); + private Vector<String> parsemeta(String line) { + String meta = line.split("#\\sOVPN_ACCESS_SERVER_", 2)[1]; + String[] parts = meta.split("=", 2); + Vector<String> rval = new Vector<String>(); Collections.addAll(rval, parts); - return rval; - - } - - private void checkinlinefile(Vector<String> args, BufferedReader br) throws IOException, ConfigParseError { - String arg0 = args.get(0).trim(); - // CHeck for <foo> - if(arg0.startsWith("<") && arg0.endsWith(">")) { - String argname = arg0.substring(1, arg0.length()-1); - String inlinefile = VpnProfile.INLINE_TAG; - - String endtag = String.format("</%s>",argname); - do { - String line = br.readLine(); - if(line==null){ - throw new ConfigParseError(String.format("No endtag </%s> for starttag <%s> found",argname,argname)); - } - if(line.trim().equals(endtag)) - break; - else { - inlinefile+=line; - inlinefile+= "\n"; - } - } while(true); - - args.clear(); - args.add(argname); - args.add(inlinefile); - } - - } - - 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: - return Character.isWhitespace(c) || c == '\0'; - - } - - public 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>(); - - if (line.length()==0) - return parameters; - - - linestate state = linestate.initial; - boolean backslash = false; - char out=0; - - int pos=0; - String currentarg=""; - - do { - // Emulate the c parsing ... - char in; - if(pos < line.length()) - in = line.charAt(pos); - else - in = '\0'; - - if (!backslash && in == '\\' && state != linestate.readin_single_quote) - { - backslash = true; - } - else - { - if (state == linestate.initial) - { - if (!space (in)) - { - if (in == ';' || in == '#') /* comment */ - break; - if (!backslash && in == '\"') - state = linestate.reading_quoted; - else if (!backslash && in == '\'') - state = linestate.readin_single_quote; - else - { - out = in; - state = linestate.reading_unquoted; - } - } - } - else if (state == linestate.reading_unquoted) - { - if (!backslash && space (in)) - state = linestate.done; - else - out = in; - } - else if (state == linestate.reading_quoted) - { - if (!backslash && in == '\"') - state = linestate.done; - else - out = in; - } - else if (state == linestate.readin_single_quote) - { - if (in == '\'') - state = linestate.done; - else - out = in; - } - - if (state == linestate.done) - { - /* ASSERT (parm_len > 0); */ - state = linestate.initial; - parameters.add(currentarg); - currentarg = ""; - out =0; - } - - if (backslash && out!=0) - { - if (!(out == '\\' || out == '\"' || space (out))) - { - throw new ConfigParseError("Options warning: Bad backslash ('\\') usage"); - } - } - backslash = false; - } + return rval; + + } + + private void checkinlinefile(Vector<String> args, BufferedReader br) throws IOException, ConfigParseError { + String arg0 = args.get(0).trim(); + // CHeck for <foo> + if (arg0.startsWith("<") && arg0.endsWith(">")) { + String argname = arg0.substring(1, arg0.length() - 1); + String inlinefile = VpnProfile.INLINE_TAG; + + String endtag = String.format("</%s>", argname); + do { + String line = br.readLine(); + if (line == null) { + throw new ConfigParseError(String.format("No endtag </%s> for starttag <%s> found", argname, argname)); + } + if (line.trim().equals(endtag)) + break; + else { + inlinefile += line; + inlinefile += "\n"; + } + } while (true); + + args.clear(); + args.add(argname); + args.add(inlinefile); + } + + } + + 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: + return Character.isWhitespace(c) || c == '\0'; + + } + + public 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>(); + + if (line.length() == 0) + return parameters; + + + linestate state = linestate.initial; + boolean backslash = false; + char out = 0; + + int pos = 0; + String currentarg = ""; + + do { + // Emulate the c parsing ... + char in; + if (pos < line.length()) + in = line.charAt(pos); + else + in = '\0'; + + if (!backslash && in == '\\' && state != linestate.readin_single_quote) { + backslash = true; + } else { + if (state == linestate.initial) { + if (!space(in)) { + if (in == ';' || in == '#') /* comment */ + break; + if (!backslash && in == '\"') + state = linestate.reading_quoted; + else if (!backslash && in == '\'') + state = linestate.readin_single_quote; + else { + out = in; + state = linestate.reading_unquoted; + } + } + } else if (state == linestate.reading_unquoted) { + if (!backslash && space(in)) + state = linestate.done; + else + out = in; + } else if (state == linestate.reading_quoted) { + if (!backslash && in == '\"') + state = linestate.done; + else + out = in; + } else if (state == linestate.readin_single_quote) { + if (in == '\'') + state = linestate.done; + else + out = in; + } + + if (state == linestate.done) { + /* ASSERT (parm_len > 0); */ + state = linestate.initial; + parameters.add(currentarg); + currentarg = ""; + out = 0; + } + + if (backslash && out != 0) { + if (!(out == '\\' || out == '\"' || space(out))) { + throw new ConfigParseError("Options warning: Bad backslash ('\\') usage"); + } + } + backslash = false; + } /* store parameter character */ - if (out!=0) - { - currentarg+=out; - } - } while (pos++ < line.length()); - - 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", - "dhcp-release", - "dhcp-renew", - "dh", + if (out != 0) { + currentarg += out; + } + } while (pos++ < line.length()); + + 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", + "dhcp-release", + "dhcp-renew", + "dh", "group", "ip-win32", "management-hold", "management", "management-client", "management-query-remote", - "management-query-passwords", + "management-query-passwords", "management-query-proxy", "management-external-key", "management-forget-disconnect", @@ -269,32 +253,32 @@ public class ConfigParser { "management-up-down", "management-client-user", "management-client-group", - "pause-exit", + "pause-exit", "plugin", "machine-readable-output", - "persist-key", - "register-dns", - "route-delay", - "route-gateway", - "route-metric", - "route-method", - "status", - "script-security", - "show-net-up", - "suppress-timestamps", - "tmp-dir", - "tun-ipv6", - "topology", + "persist-key", + "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"} - }; + { + {"setenv", "IV_GUI_VER"}, + {"setenv", "IV_OPENVPN_GUI_VERSION"}, + {"engine", "dynamic"} + }; final String[] connectionOptions = { "local", @@ -326,70 +310,67 @@ public class ConfigParser { // This method is far too long - @SuppressWarnings("ConstantConditions") + @SuppressWarnings("ConstantConditions") public VpnProfile convertProfile() throws ConfigParseError, IOException { - boolean noauthtypeset=true; - VpnProfile np = new VpnProfile(CONVERTED_PROFILE); - // Pull, client, tls-client - np.clearDefaults(); - - if(options.containsKey("client") || options.containsKey("pull")) { - np.mUsePull=true; - options.remove("pull"); - options.remove("client"); - } - - Vector<String> secret = getOption("secret", 1, 2); - if(secret!=null) - { - np.mAuthenticationType=VpnProfile.TYPE_STATICKEYS; - noauthtypeset=false; - np.mUseTLSAuth=true; - np.mTLSAuthFilename=secret.get(1); - if(secret.size()==3) - np.mTLSAuthDirection=secret.get(2); - - } - - Vector<Vector<String>> routes = getAllOption("route", 1, 4); - if(routes!=null) { - String routeopt = ""; + boolean noauthtypeset = true; + VpnProfile np = new VpnProfile(CONVERTED_PROFILE); + // Pull, client, tls-client + np.clearDefaults(); + + if (options.containsKey("client") || options.containsKey("pull")) { + np.mUsePull = true; + options.remove("pull"); + options.remove("client"); + } + + Vector<String> secret = getOption("secret", 1, 2); + if (secret != null) { + np.mAuthenticationType = VpnProfile.TYPE_STATICKEYS; + noauthtypeset = false; + np.mUseTLSAuth = true; + np.mTLSAuthFilename = secret.get(1); + if (secret.size() == 3) + np.mTLSAuthDirection = secret.get(2); + + } + + Vector<Vector<String>> routes = getAllOption("route", 1, 4); + if (routes != null) { + String routeopt = ""; String routeExcluded = ""; - for(Vector<String> route:routes){ - String netmask = "255.255.255.255"; + for (Vector<String> route : routes) { + String netmask = "255.255.255.255"; String gateway = "vpn_gateway"; - if(route.size() >= 3) - netmask = route.get(2); + if (route.size() >= 3) + netmask = route.get(2); if (route.size() >= 4) gateway = route.get(3); - String net = route.get(1); - try { - CIDRIP cidr = new CIDRIP(net, netmask); + String net = route.get(1); + try { + CIDRIP cidr = new CIDRIP(net, netmask); if (gateway.equals("net_gateway")) routeExcluded += cidr.toString() + " "; else - routeopt+=cidr.toString() + " "; - } catch (ArrayIndexOutOfBoundsException aioob) { - throw new ConfigParseError("Could not parse netmask of route " + netmask); - } catch (NumberFormatException ne) { - - + routeopt += cidr.toString() + " "; + } catch (ArrayIndexOutOfBoundsException aioob) { + throw new ConfigParseError("Could not parse netmask of route " + netmask); + } catch (NumberFormatException ne) { - throw new ConfigParseError("Could not parse netmask of route " + netmask); - } + throw new ConfigParseError("Could not parse netmask of route " + netmask); + } - } - np.mCustomRoutes=routeopt; - np.mExcludedRoutes=routeExcluded; - } + } + np.mCustomRoutes = routeopt; + np.mExcludedRoutes = routeExcluded; + } Vector<Vector<String>> routesV6 = getAllOption("route-ipv6", 1, 4); - if (routesV6!=null) { + if (routesV6 != null) { String customIPv6Routes = ""; - for (Vector<String> route:routesV6){ + for (Vector<String> route : routesV6) { customIPv6Routes += route.get(1) + " "; } @@ -397,39 +378,36 @@ public class ConfigParser { } // Also recognize tls-auth [inline] direction ... - Vector<Vector<String>> tlsauthoptions = getAllOption("tls-auth", 1, 2); - if(tlsauthoptions!=null) { - for(Vector<String> tlsauth:tlsauthoptions) { - if(tlsauth!=null) - { - if(!tlsauth.get(1).equals("[inline]")) { - np.mTLSAuthFilename=tlsauth.get(1); - np.mUseTLSAuth=true; - } - if(tlsauth.size()==3) - np.mTLSAuthDirection=tlsauth.get(2); - } - } - } - - Vector<String> direction = getOption("key-direction", 1, 1); - if(direction!=null) - np.mTLSAuthDirection=direction.get(1); + Vector<Vector<String>> tlsauthoptions = getAllOption("tls-auth", 1, 2); + if (tlsauthoptions != null) { + for (Vector<String> tlsauth : tlsauthoptions) { + if (tlsauth != null) { + if (!tlsauth.get(1).equals("[inline]")) { + np.mTLSAuthFilename = tlsauth.get(1); + np.mUseTLSAuth = true; + } + if (tlsauth.size() == 3) + np.mTLSAuthDirection = tlsauth.get(2); + } + } + } + + Vector<String> direction = getOption("key-direction", 1, 1); + if (direction != null) + np.mTLSAuthDirection = direction.get(1); Vector<Vector<String>> defgw = getAllOption("redirect-gateway", 0, 5); - if(defgw != null) - { - np.mUseDefaultRoute=true; + if (defgw != null) { + np.mUseDefaultRoute = true; checkRedirectParameters(np, defgw); } - Vector<Vector<String>> redirectPrivate = getAllOption("redirect-private",0,5); - if (redirectPrivate != null) - { - checkRedirectParameters(np,redirectPrivate); + Vector<Vector<String>> redirectPrivate = getAllOption("redirect-private", 0, 5); + if (redirectPrivate != null) { + checkRedirectParameters(np, redirectPrivate); } - Vector<String> dev =getOption("dev",1,1); - Vector<String> devtype =getOption("dev-type",1,1); + Vector<String> dev = getOption("dev", 1, 1); + Vector<String> devtype = getOption("dev-type", 1, 1); if ((devtype != null && devtype.get(1).equals("tun")) || (dev != null && dev.get(1).startsWith("tun")) || @@ -437,15 +415,15 @@ public class ConfigParser { //everything okay } else { throw new ConfigParseError("Sorry. Only tun mode is supported. See the FAQ for more detail"); - } + } - Vector<String> mssfix = getOption("mssfix",0,1); + Vector<String> mssfix = getOption("mssfix", 0, 1); - if (mssfix!=null) { - if (mssfix.size()>=2) { + if (mssfix != null) { + if (mssfix.size() >= 2) { try { - np.mMssFix=Integer.parseInt(mssfix.get(1)); - } catch(NumberFormatException e) { + np.mMssFix = Integer.parseInt(mssfix.get(1)); + } catch (NumberFormatException e) { throw new ConfigParseError("Argument to --mssfix has to be an integer"); } } else { @@ -454,172 +432,171 @@ public class ConfigParser { } - Vector<String> mode =getOption("mode",1,1); - if (mode != null){ - if(!mode.get(1).equals("p2p")) - throw new ConfigParseError("Invalid mode for --mode specified, need p2p"); - } - - - - Vector<Vector<String>> dhcpoptions = getAllOption("dhcp-option", 2, 2); - if(dhcpoptions!=null) { - for(Vector<String> dhcpoption:dhcpoptions) { - String type=dhcpoption.get(1); - String arg = dhcpoption.get(2); - if(type.equals("DOMAIN")) { - np.mSearchDomain=dhcpoption.get(2); - } else if(type.equals("DNS")) { - np.mOverrideDNS=true; - if(np.mDNS1.equals(VpnProfile.DEFAULT_DNS1)) - np.mDNS1=arg; - else - np.mDNS2=arg; - } - } - } - - Vector<String> ifconfig = getOption("ifconfig", 2, 2); - if(ifconfig!=null) { - try { - CIDRIP cidr = new CIDRIP(ifconfig.get(1), ifconfig.get(2)); - np.mIPv4Address=cidr.toString(); - } catch (NumberFormatException nfe) { - throw new ConfigParseError("Could not pase ifconfig IP address: " + nfe.getLocalizedMessage()); - } - - } - - if(getOption("remote-random-hostname", 0, 0)!=null) - np.mUseRandomHostname=true; - - if(getOption("float", 0, 0)!=null) - np.mUseFloat=true; - - if(getOption("comp-lzo", 0, 1)!=null) - np.mUseLzo=true; - - Vector<String> cipher = getOption("cipher", 1, 1); - if(cipher!=null) - np.mCipher= cipher.get(1); - - Vector<String> auth = getOption("auth", 1, 1); - if(auth!=null) - np.mAuth = auth.get(1); - - - Vector<String> ca = getOption("ca",1,1); - if(ca!=null){ - np.mCaFilename = ca.get(1); - } - - Vector<String> cert = getOption("cert",1,1); - if(cert!=null){ - np.mClientCertFilename = cert.get(1); - np.mAuthenticationType = VpnProfile.TYPE_CERTIFICATES; - noauthtypeset=false; - } - Vector<String> key= getOption("key",1,1); - if(key!=null) - np.mClientKeyFilename=key.get(1); - - Vector<String> pkcs12 = getOption("pkcs12",1,1); - if(pkcs12!=null) { - np.mPKCS12Filename = pkcs12.get(1); - np.mAuthenticationType = VpnProfile.TYPE_KEYSTORE; - noauthtypeset=false; - } - - Vector<String> cryptoapicert = getOption("cryptoapicert",1,1); - if(cryptoapicert!=null) { + Vector<String> mode = getOption("mode", 1, 1); + if (mode != null) { + if (!mode.get(1).equals("p2p")) + throw new ConfigParseError("Invalid mode for --mode specified, need p2p"); + } + + + Vector<Vector<String>> dhcpoptions = getAllOption("dhcp-option", 2, 2); + if (dhcpoptions != null) { + for (Vector<String> dhcpoption : dhcpoptions) { + String type = dhcpoption.get(1); + String arg = dhcpoption.get(2); + if (type.equals("DOMAIN")) { + np.mSearchDomain = dhcpoption.get(2); + } else if (type.equals("DNS")) { + np.mOverrideDNS = true; + if (np.mDNS1.equals(VpnProfile.DEFAULT_DNS1)) + np.mDNS1 = arg; + else + np.mDNS2 = arg; + } + } + } + + Vector<String> ifconfig = getOption("ifconfig", 2, 2); + if (ifconfig != null) { + try { + CIDRIP cidr = new CIDRIP(ifconfig.get(1), ifconfig.get(2)); + np.mIPv4Address = cidr.toString(); + } catch (NumberFormatException nfe) { + throw new ConfigParseError("Could not pase ifconfig IP address: " + nfe.getLocalizedMessage()); + } + + } + + if (getOption("remote-random-hostname", 0, 0) != null) + np.mUseRandomHostname = true; + + if (getOption("float", 0, 0) != null) + np.mUseFloat = true; + + if (getOption("comp-lzo", 0, 1) != null) + np.mUseLzo = true; + + Vector<String> cipher = getOption("cipher", 1, 1); + if (cipher != null) + np.mCipher = cipher.get(1); + + Vector<String> auth = getOption("auth", 1, 1); + if (auth != null) + np.mAuth = auth.get(1); + + + Vector<String> ca = getOption("ca", 1, 1); + if (ca != null) { + np.mCaFilename = ca.get(1); + } + + Vector<String> cert = getOption("cert", 1, 1); + if (cert != null) { + np.mClientCertFilename = cert.get(1); + np.mAuthenticationType = VpnProfile.TYPE_CERTIFICATES; + noauthtypeset = false; + } + Vector<String> key = getOption("key", 1, 1); + if (key != null) + np.mClientKeyFilename = key.get(1); + + Vector<String> pkcs12 = getOption("pkcs12", 1, 1); + if (pkcs12 != null) { + np.mPKCS12Filename = pkcs12.get(1); np.mAuthenticationType = VpnProfile.TYPE_KEYSTORE; - noauthtypeset=false; + noauthtypeset = false; } - Vector<String> compatnames = getOption("compat-names",1,2); - Vector<String> nonameremapping = getOption("no-name-remapping",1,1); - Vector<String> tlsremote = getOption("tls-remote",1,1); - if(tlsremote!=null){ - np.mRemoteCN = tlsremote.get(1); - np.mCheckRemoteCN=true; - np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE; + Vector<String> cryptoapicert = getOption("cryptoapicert", 1, 1); + if (cryptoapicert != null) { + np.mAuthenticationType = VpnProfile.TYPE_KEYSTORE; + noauthtypeset = false; + } - if((compatnames!=null && compatnames.size() > 2) || - (nonameremapping!=null)) - np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING; - } + Vector<String> compatnames = getOption("compat-names", 1, 2); + Vector<String> nonameremapping = getOption("no-name-remapping", 1, 1); + Vector<String> tlsremote = getOption("tls-remote", 1, 1); + if (tlsremote != null) { + np.mRemoteCN = tlsremote.get(1); + np.mCheckRemoteCN = true; + np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE; + + if ((compatnames != null && compatnames.size() > 2) || + (nonameremapping != null)) + np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING; + } - Vector<String> verifyx509name = getOption("verify-x509-name",1,2); - if(verifyx509name!=null){ - np.mRemoteCN = verifyx509name.get(1); - np.mCheckRemoteCN=true; - if(verifyx509name.size()>2) { - if (verifyx509name.get(2).equals("name")) - np.mX509AuthType=VpnProfile.X509_VERIFY_TLSREMOTE_RDN; - else if (verifyx509name.get(2).equals("name-prefix")) - np.mX509AuthType=VpnProfile.X509_VERIFY_TLSREMOTE_RDN_PREFIX; - else - throw new ConfigParseError("Unknown parameter to x509-verify-name: " + verifyx509name.get(2) ); - } else { - np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_DN; - } + Vector<String> verifyx509name = getOption("verify-x509-name", 1, 2); + if (verifyx509name != null) { + np.mRemoteCN = verifyx509name.get(1); + np.mCheckRemoteCN = true; + if (verifyx509name.size() > 2) { + if (verifyx509name.get(2).equals("name")) + np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_RDN; + else if (verifyx509name.get(2).equals("name-prefix")) + np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_RDN_PREFIX; + else + throw new ConfigParseError("Unknown parameter to x509-verify-name: " + verifyx509name.get(2)); + } else { + np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_DN; + } - } + } - Vector<String> verb = getOption("verb",1,1); - if(verb!=null){ - np.mVerb=verb.get(1); - } + Vector<String> verb = getOption("verb", 1, 1); + if (verb != null) { + np.mVerb = verb.get(1); + } - if(getOption("nobind", 0, 0) != null) - np.mNobind=true; + if (getOption("nobind", 0, 0) != null) + np.mNobind = true; - if(getOption("persist-tun", 0,0) != null) - np.mPersistTun=true; + if (getOption("persist-tun", 0, 0) != null) + np.mPersistTun = true; - Vector<String> connectretry = getOption("connect-retry", 1, 1); - if(connectretry!=null) - np.mConnectRetry =connectretry.get(1); + Vector<String> connectretry = getOption("connect-retry", 1, 1); + if (connectretry != null) + np.mConnectRetry = connectretry.get(1); - Vector<String> connectretrymax = getOption("connect-retry-max", 1, 1); - if(connectretrymax!=null) - np.mConnectRetryMax =connectretrymax.get(1); + Vector<String> connectretrymax = getOption("connect-retry-max", 1, 1); + if (connectretrymax != null) + np.mConnectRetryMax = connectretrymax.get(1); Vector<Vector<String>> remotetls = getAllOption("remote-cert-tls", 1, 1); - if(remotetls!=null) - if(remotetls.get(0).get(1).equals("server")) - np.mExpectTLSCert=true; - else - options.put("remotetls",remotetls); - - Vector<String> authuser = getOption("auth-user-pass",0,1); - if(authuser !=null){ - if(noauthtypeset) { - np.mAuthenticationType=VpnProfile.TYPE_USERPASS; - } else if(np.mAuthenticationType==VpnProfile.TYPE_CERTIFICATES) { - np.mAuthenticationType=VpnProfile.TYPE_USERPASS_CERTIFICATES; - } else if(np.mAuthenticationType==VpnProfile.TYPE_KEYSTORE) { - np.mAuthenticationType=VpnProfile.TYPE_USERPASS_KEYSTORE; - } - if(authuser.size()>1) { - // Set option value to password get to embed later. - np.mUsername=null; - useEmbbedUserAuth(np, authuser.get(1)); - } - } + if (remotetls != null) + if (remotetls.get(0).get(1).equals("server")) + np.mExpectTLSCert = true; + else + options.put("remotetls", remotetls); + + Vector<String> authuser = getOption("auth-user-pass", 0, 1); + if (authuser != null) { + if (noauthtypeset) { + np.mAuthenticationType = VpnProfile.TYPE_USERPASS; + } else if (np.mAuthenticationType == VpnProfile.TYPE_CERTIFICATES) { + np.mAuthenticationType = VpnProfile.TYPE_USERPASS_CERTIFICATES; + } else if (np.mAuthenticationType == VpnProfile.TYPE_KEYSTORE) { + np.mAuthenticationType = VpnProfile.TYPE_USERPASS_KEYSTORE; + } + if (authuser.size() > 1) { + // Set option value to password get to embed later. + np.mUsername = null; + useEmbbedUserAuth(np, authuser.get(1)); + } + } Pair<Connection, Connection[]> conns = parseConnectionOptions(null); - np.mConnections =conns.second; + np.mConnections = conns.second; Vector<Vector<String>> connectionBlocks = getAllOption("connection", 1, 1); - if (np.mConnections.length > 0 && connectionBlocks !=null ) { - throw new ConfigParseError("Using a <connection> block and --remote is not allowed."); + if (np.mConnections.length > 0 && connectionBlocks != null) { + throw new ConfigParseError("Using a <connection> block and --remote is not allowed."); } - if (connectionBlocks!=null) { + if (connectionBlocks != null) { np.mConnections = new Connection[connectionBlocks.size()]; int connIndex = 0; @@ -633,43 +610,43 @@ public class ConfigParser { connIndex++; } } - if(getOption("remote-random", 0, 0) != null) - np.mRemoteRandom=true; + if (getOption("remote-random", 0, 0) != null) + np.mRemoteRandom = true; Vector<String> protoforce = getOption("proto-force", 1, 1); - if(protoforce!=null) { + if (protoforce != null) { boolean disableUDP; String protoToDisable = protoforce.get(1); if (protoToDisable.equals("udp")) - disableUDP=true; + disableUDP = true; else if (protoToDisable.equals("tcp")) - disableUDP=false; + disableUDP = false; else throw new ConfigParseError(String.format("Unknown protocol %s in proto-force", protoToDisable)); - for (Connection conn:np.mConnections) - if(conn.mUseUdp==disableUDP) - conn.mEnabled=false; + for (Connection conn : np.mConnections) + if (conn.mUseUdp == disableUDP) + conn.mEnabled = false; } // Parse OpenVPN Access Server extra - Vector<String> friendlyname = meta.get("FRIENDLY_NAME"); - if(friendlyname !=null && friendlyname.size() > 1) - np.mName=friendlyname.get(1); + Vector<String> friendlyname = meta.get("FRIENDLY_NAME"); + if (friendlyname != null && friendlyname.size() > 1) + np.mName = friendlyname.get(1); - Vector<String> ocusername = meta.get("USERNAME"); - if(ocusername !=null && ocusername.size() > 1) - np.mUsername=ocusername.get(1); + Vector<String> ocusername = meta.get("USERNAME"); + if (ocusername != null && ocusername.size() > 1) + np.mUsername = ocusername.get(1); checkIgnoreAndInvalidOptions(np); - fixup(np); + fixup(np); - return np; - } + return np; + } private Pair<Connection, Connection[]> parseConnection(String connection, Connection defaultValues) throws IOException, ConfigParseError { - // Parse a connection Block as a new configuration file + // Parse a connection Block as a new configuration file ConfigParser connectionParser = new ConfigParser(); @@ -683,7 +660,7 @@ public class ConfigParser { private Pair<Connection, Connection[]> parseConnectionOptions(Connection connDefault) throws ConfigParseError { Connection conn; - if (connDefault!=null) + if (connDefault != null) try { conn = connDefault.clone(); } catch (CloneNotSupportedException e) { @@ -693,28 +670,28 @@ public class ConfigParser { else conn = new Connection(); - Vector<String> port = getOption("port", 1,1); - if(port!=null){ + Vector<String> port = getOption("port", 1, 1); + if (port != null) { conn.mServerPort = port.get(1); } - Vector<String> rport = getOption("rport", 1,1); - if(rport!=null){ + Vector<String> rport = getOption("rport", 1, 1); + if (rport != null) { conn.mServerPort = rport.get(1); } - Vector<String> proto = getOption("proto", 1,1); - if(proto!=null){ - conn.mUseUdp=isUdpProto(proto.get(1)); + Vector<String> proto = getOption("proto", 1, 1); + if (proto != null) { + conn.mUseUdp = isUdpProto(proto.get(1)); } // Parse remote config - Vector<Vector<String>> remotes = getAllOption("remote",1,3); + Vector<Vector<String>> remotes = getAllOption("remote", 1, 3); // Assume that we need custom options if connectionDefault are set - if(connDefault!=null) { + if (connDefault != null) { for (Vector<Vector<String>> option : options.values()) { conn.mCustomConfiguration += getOptionStrings(option); @@ -724,14 +701,14 @@ public class ConfigParser { conn.mUseCustomConfig = true; } // Make remotes empty to simplify code - if (remotes==null) + if (remotes == null) remotes = new Vector<Vector<String>>(); Connection[] connections = new Connection[remotes.size()]; - int i=0; - for (Vector<String> remote: remotes) { + int i = 0; + for (Vector<String> remote : remotes) { try { connections[i] = conn.clone(); } catch (CloneNotSupportedException e) { @@ -739,7 +716,7 @@ public class ConfigParser { } switch (remote.size()) { case 4: - connections[i].mUseUdp=isUdpProto(remote.get(3)); + connections[i].mUseUdp = isUdpProto(remote.get(3)); case 3: connections[i].mServerPort = remote.get(2); case 2: @@ -752,63 +729,60 @@ public class ConfigParser { } private void checkRedirectParameters(VpnProfile np, Vector<Vector<String>> defgw) { - for (Vector<String> redirect: defgw) - for (int i=1;i<redirect.size();i++){ + for (Vector<String> redirect : defgw) + for (int i = 1; i < redirect.size(); i++) { if (redirect.get(i).equals("block-local")) - np.mAllowLocalLAN=false; + np.mAllowLocalLAN = false; else if (redirect.get(i).equals("unblock-local")) - np.mAllowLocalLAN=true; + np.mAllowLocalLAN = true; } } - private boolean isUdpProto(String proto) throws ConfigParseError { - boolean isudp; - if(proto.equals("udp") || proto.equals("udp6")) - isudp=true; - else if (proto.equals("tcp-client") || - proto.equals("tcp") || - proto.equals("tcp6") || - proto.endsWith("tcp6-client")) - isudp =false; - else - throw new ConfigParseError("Unsupported option to --proto " + proto); - 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)) - throw new ConfigParseError(String.format("Unsupported Option %s encountered in config file. Aborting",option)); + private boolean isUdpProto(String proto) throws ConfigParseError { + boolean isudp; + if (proto.equals("udp") || proto.equals("udp6")) + isudp = true; + else if (proto.equals("tcp-client") || + proto.equals("tcp") || + proto.equals("tcp6") || + proto.endsWith("tcp6-client")) + isudp = false; + else + throw new ConfigParseError("Unsupported option to --proto " + proto); + return isudp; + } - for(String option:ignoreOptions) - // removing an item which is not in the map is no error - options.remove(option); + 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)) + throw new ConfigParseError(String.format("Unsupported Option %s encountered in config file. Aborting", option)); + for (String option : ignoreOptions) + // removing an item which is not in the map is no error + options.remove(option); - if(options.size()> 0) { - np.mCustomConfigOptions += "# These Options were found in the config file do not map to config settings:\n"; + if (options.size() > 0) { + np.mCustomConfigOptions += "# These Options were found in the config file do not map to config settings:\n"; - for(Vector<Vector<String>> option:options.values()) { + for (Vector<Vector<String>> option : options.values()) { - np.mCustomConfigOptions += getOptionStrings(option); + np.mCustomConfigOptions += getOptionStrings(option); - } - np.mUseCustomConfig=true; + } + np.mUseCustomConfig = true; - } - } + } + } boolean ignoreThisOption(Vector<String> option) { @@ -843,35 +817,35 @@ public class ConfigParser { private void fixup(VpnProfile np) { - if(np.mRemoteCN.equals(np.mServerName)) { - np.mRemoteCN=""; - } - } - - private Vector<String> getOption(String option, int minarg, int maxarg) throws ConfigParseError { - Vector<Vector<String>> alloptions = getAllOption(option, minarg, maxarg); - if(alloptions==null) - return null; - else - 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) - return null; - - for(Vector<String> optionline:args) - - if(optionline.size()< (minarg+1) || optionline.size() > maxarg+1) { - String err = String.format(Locale.getDefault(),"Option %s has %d parameters, expected between %d and %d", - option,optionline.size()-1,minarg,maxarg ); - throw new ConfigParseError(err); - } - options.remove(option); - return args; - } + if (np.mRemoteCN.equals(np.mServerName)) { + np.mRemoteCN = ""; + } + } + + private Vector<String> getOption(String option, int minarg, int maxarg) throws ConfigParseError { + Vector<Vector<String>> alloptions = getAllOption(option, minarg, maxarg); + if (alloptions == null) + return null; + else + 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) + return null; + + for (Vector<String> optionline : args) + + if (optionline.size() < (minarg + 1) || optionline.size() > maxarg + 1) { + String err = String.format(Locale.getDefault(), "Option %s has %d parameters, expected between %d and %d", + option, optionline.size() - 1, minarg, maxarg); + throw new ConfigParseError(err); + } + options.remove(option); + return args; + } } 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 26354689..c86f9e44 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/NetworkSpace.java +++ b/app/src/main/java/de/blinkt/openvpn/core/NetworkSpace.java @@ -172,8 +172,16 @@ public class NetworkSpace { } public boolean containsNet(ipAddress network) { - return getFirstAddress().compareTo(network.getFirstAddress()) != 1 && - getLastAddress().compareTo(network.getLastAddress()) != -1; + // this.first >= net.first && this.last <= net.last + BigInteger ourFirst = getFirstAddress(); + BigInteger ourLast = getLastAddress(); + BigInteger netFirst = network.getFirstAddress(); + BigInteger netLast = network.getLastAddress(); + + boolean a = ourFirst.compareTo(netFirst) != 1; + boolean b = ourLast.compareTo(netLast) != -1; + return a && b; + } } @@ -320,6 +328,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) { if(!calculatedIp.included && origIp.containsNet(calculatedIp)) { skipIp=true; 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 3cb5527b..3c1ec064 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java +++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java @@ -51,6 +51,8 @@ import static de.blinkt.openvpn.core.NetworkSpace.ipAddress; import static de.blinkt.openvpn.core.VpnStatus.ConnectionStatus.LEVEL_CONNECTED; import static de.blinkt.openvpn.core.VpnStatus.ConnectionStatus.LEVEL_CONNECTING_NO_SERVER_REPLY_YET; import static de.blinkt.openvpn.core.VpnStatus.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT; +import static de.blinkt.openvpn.core.VpnStatus.ConnectionStatus.LEVEL_NOTCONNECTED; +import static de.blinkt.openvpn.core.VpnStatus.ConnectionStatus.LEVEL_NONETWORK; import se.leap.bitmaskclient.Dashboard; @@ -174,7 +176,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac mNotificationManager.notify(OPENVPN_STATUS, notification); - startForeground(OPENVPN_STATUS, notification); + //startForeground(OPENVPN_STATUS, notification); } private int getIconByConnectionStatus(ConnectionStatus level) { @@ -552,9 +554,15 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac Collection<ipAddress> positiveIPv4Routes = mRoutes.getPositiveIPList(); Collection<ipAddress> positiveIPv6Routes = mRoutesv6.getPositiveIPList(); + ipAddress multicastRange = new ipAddress(new CIDRIP("224.0.0.0", 3), true); + for (NetworkSpace.ipAddress route : positiveIPv4Routes) { try { - builder.addRoute(route.getIPv4Address(), route.networkMask); + + if (multicastRange.containsNet(route)) + VpnStatus.logDebug(R.string.ignore_multicast_route, route.toString()); + else + builder.addRoute(route.getIPv4Address(), route.networkMask); } catch (IllegalArgumentException ia) { VpnStatus.logError(getString(R.string.route_rejected) + route + " " + ia.getLocalizedMessage()); } @@ -607,7 +615,10 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac try { //Debug.stopMethodTracing(); - return builder.establish(); + ParcelFileDescriptor tun = builder.establish(); + if (tun==null) + throw new NullPointerException("Android establish() method returned null (Really broken network configuration?)"); + return tun; } catch (Exception e) { VpnStatus.logError(R.string.tun_open_error); VpnStatus.logError(getString(R.string.error) + e.getLocalizedMessage()); @@ -843,6 +854,21 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac mDisplayBytecount = true; mConnecttime = System.currentTimeMillis(); lowpriority = true; + if(mProfile.mPersistTun) { + NotificationManager ns = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + ns.cancel(OPENVPN_STATUS); + return; + } + } else if (level == LEVEL_NONETWORK || level == LEVEL_NOTCONNECTED) { + NotificationManager ns = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + ns.cancel(OPENVPN_STATUS); + return; + } else if (level != LEVEL_NOTCONNECTED && mConnecttime > 0) { + mDisplayBytecount = false; + String msg = "Traffic is blocked until the VPN becomes active."; + String ticker = msg; + showNotification(msg, ticker, lowpriority , 0, level); + return; } else { mDisplayBytecount = false; } @@ -876,7 +902,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true)); boolean lowpriority = !mNotificationAlwaysVisible; - showNotification(netstat, null, lowpriority, mConnecttime, LEVEL_CONNECTED); + //showNotification(netstat, null, lowpriority, mConnecttime, LEVEL_CONNECTED); } } diff --git a/app/src/main/res/drawable-hdpi/ic_check_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_check_white_24dp.png Binary files differnew file mode 100644 index 00000000..f42a0e2d --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_check_white_24dp.png diff --git a/app/src/main/res/drawable-mdpi/ic_check_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_check_white_24dp.png Binary files differnew file mode 100644 index 00000000..e91f9048 --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_check_white_24dp.png diff --git a/app/src/main/res/drawable-xhdpi/ic_check_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_check_white_24dp.png Binary files differnew file mode 100644 index 00000000..e5024472 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_check_white_24dp.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_check_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_check_white_24dp.png Binary files differnew file mode 100644 index 00000000..6e03d54c --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_check_white_24dp.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_check_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_check_white_24dp.png Binary files differnew file mode 100644 index 00000000..87892840 --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/ic_check_white_24dp.png diff --git a/app/src/main/res/layout/log_fragment.xml b/app/src/main/res/layout/log_fragment.xml index 4fec942e..491882a9 100644 --- a/app/src/main/res/layout/log_fragment.xml +++ b/app/src/main/res/layout/log_fragment.xml @@ -13,6 +13,7 @@ <LinearLayout android:background="@drawable/white_rect" android:elevation="1dp" + android:orientation="vertical" android:layout_height="wrap_content" android:layout_width="match_parent"> diff --git a/app/src/main/res/values-v21/refs.xml b/app/src/main/res/values-v21/refs.xml index d29d04ed..a4a26bec 100644 --- a/app/src/main/res/values-v21/refs.xml +++ b/app/src/main/res/values-v21/refs.xml @@ -7,9 +7,8 @@ <resources> <drawable name="ic_menu_close_clear_cancel">@drawable/ic_close_white_24dp</drawable> <drawable name="ic_menu_share">@drawable/ic_share_white_24dp </drawable> + <drawable name="ic_menu_save">@drawable/ic_check_white_24dp</drawable> <drawable name="ic_menu_view">@drawable/ic_filter_list_white_24dp</drawable> <drawable name="ic_menu_delete">@drawable/ic_delete_white_24dp</drawable> <drawable name="ic_menu_delete_grey">@drawable/ic_delete_grey600_24dp</drawable> - - <drawable name="ic_menu_edit">@drawable/ic_edit_white_24dp</drawable> </resources> diff --git a/app/src/main/res/values/strings-icsopenvpn.xml b/app/src/main/res/values/strings-icsopenvpn.xml index 307d3a42..39ad1193 100755 --- a/app/src/main/res/values/strings-icsopenvpn.xml +++ b/app/src/main/res/values/strings-icsopenvpn.xml @@ -350,5 +350,6 @@ <string name="show_log">Show log</string> <string name="faq_android_clients">Multiple OpenVPN clients for Android exist. The most common ones are OpenVPN for Android (this client), OpenVPN Connect and OpenVPN Settings.<p>The clients can be grouped into two groups: OpenVPN for Android and OpenVPN Connect use the official VPNService API (Android 4.0+) and require no root and OpenVPN Settings which uses root.<p>OpenVPN for Android is an open source client and developed by Arne Schwabe. It is targeted at more advanced users and offers many settings and the ability to import profiles from files and to configure/change profiles inside the app. The client is based on the community version of OpenVPN. It is based on the OpenVPN 2.x source code. This client can be seen as the semi officially client of the community. <p>OpenVPN Connect is non open source client that is developed by OpenVPN Technologies, Inc. The client is indented to be general use client and moree targeted at the average user and allows the import of OpenVPN profiles. This client is based on the OpenVPN C++ reimplementation of the OpenVPN protocol (This was required to allow OpenVPN Technologies, Inc to publish an iOS OpenVPN app). This client is the official client of the OpenVPN technologies <p> OpenVPN Settings is the oldest of the clients and also a UI for the open source OpenVPN. In contrast to OpenVPN for Android it requires root and does not use the VPNService API. It does not depend on Android 4.0+</string> <string name="faq_androids_clients_title">Differences between the OpenVPN Android clients</string> + <string name="ignore_multicast_route">Ignoring multicast route: %s</string> </resources> |