From 08f9700058b07b79908110baf4cef353d7eba5f4 Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Sun, 16 Nov 2014 19:21:14 +0100 Subject: Implement support for multiple connection/remote entries (closes issue #2) --HG-- extra : rebase_source : 22f70cc3750ec5342e1c8b69f8978b92237710c6 --- .../main/java/de/blinkt/openvpn/VpnProfile.java | 54 +++-- .../blinkt/openvpn/activities/VPNPreferences.java | 3 +- .../java/de/blinkt/openvpn/core/ConfigParser.java | 217 ++++++++++++++------- .../java/de/blinkt/openvpn/core/Connection.java | 45 +++++ .../openvpn/fragments/ConnectionsAdapter.java | 176 +++++++++++++++++ .../blinkt/openvpn/fragments/Settings_Basic.java | 14 -- .../openvpn/fragments/Settings_Connections.java | 107 ++++++++++ .../java/de/blinkt/openvpn/fragments/Utils.java | 1 - .../blinkt/openvpn/fragments/VPNProfileList.java | 4 - main/src/main/res/anim/fab_anim.xml | 23 +++ main/src/main/res/drawable/oval_ripple.xml | 13 ++ main/src/main/res/drawable/white_rect.xml | 11 ++ main/src/main/res/layout-v21/connection_fab.xml | 23 +++ main/src/main/res/layout/basic_settings.xml | 45 ----- main/src/main/res/layout/connection_fab.xml | 11 ++ main/src/main/res/layout/connections.xml | 52 +++++ main/src/main/res/layout/server_card.xml | 195 ++++++++++++++++++ main/src/main/res/layout/server_layout | 172 ++++++++++++++++ main/src/main/res/layout/vpn_profile_list.xml | 116 ++++++----- main/src/main/res/menu/connections.xml | 15 ++ main/src/main/res/values/dimens.xml | 7 + main/src/main/res/xml/vpn_headers.xml | 3 +- 22 files changed, 1112 insertions(+), 195 deletions(-) create mode 100644 main/src/main/java/de/blinkt/openvpn/core/Connection.java create mode 100644 main/src/main/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java create mode 100644 main/src/main/java/de/blinkt/openvpn/fragments/Settings_Connections.java create mode 100644 main/src/main/res/anim/fab_anim.xml create mode 100644 main/src/main/res/drawable/oval_ripple.xml create mode 100644 main/src/main/res/drawable/white_rect.xml create mode 100644 main/src/main/res/layout-v21/connection_fab.xml create mode 100644 main/src/main/res/layout/connection_fab.xml create mode 100644 main/src/main/res/layout/connections.xml create mode 100644 main/src/main/res/layout/server_card.xml create mode 100644 main/src/main/res/layout/server_layout create mode 100644 main/src/main/res/menu/connections.xml diff --git a/main/src/main/java/de/blinkt/openvpn/VpnProfile.java b/main/src/main/java/de/blinkt/openvpn/VpnProfile.java index 6f21a847..aff2a362 100644 --- a/main/src/main/java/de/blinkt/openvpn/VpnProfile.java +++ b/main/src/main/java/de/blinkt/openvpn/VpnProfile.java @@ -47,6 +47,7 @@ import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; +import de.blinkt.openvpn.core.Connection; import de.blinkt.openvpn.core.NativeUtils; import de.blinkt.openvpn.core.OpenVPNService; import de.blinkt.openvpn.core.VPNLaunchHelper; @@ -67,7 +68,7 @@ public class VpnProfile implements Serializable { private static final long serialVersionUID = 7085688938959334563L; public static final int MAXLOGLEVEL = 4; - public static final int CURRENT_PROFILE_VERSION = 2; + public static final int CURRENT_PROFILE_VERSION = 4; public static final int DEFAULT_MSSFIX_SIZE = 1450; public static String DEFAULT_DNS1 = "8.8.8.8"; public static String DEFAULT_DNS2 = "8.8.4.4"; @@ -102,12 +103,10 @@ public class VpnProfile implements Serializable { public String mClientKeyFilename; public String mCaFilename; public boolean mUseLzo = true; - public String mServerPort = "1194"; - public boolean mUseUdp = true; public String mPKCS12Filename; public String mPKCS12Password; public boolean mUseTLSAuth = false; - public String mServerName = "openvpn.blinkt.de"; + public String mDNS1 = DEFAULT_DNS1; public String mDNS2 = DEFAULT_DNS2; public String mIPv4Address; @@ -148,6 +147,13 @@ public class VpnProfile implements Serializable { public String mExcludedRoutes; public String mExcludedRoutesv6; public int mMssFix =0; // -1 is default, + public Connection[] mConnections; + public boolean mRemoteRandom=false; + + /* Options no long used in new profiles */ + public String mServerName = "openvpn.blinkt.de"; + public String mServerPort = "1194"; + public boolean mUseUdp = true; @@ -202,7 +208,25 @@ public class VpnProfile implements Serializable { mAllowLocalLAN = Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT; } + + if (mProfileVersion < 4) + moveOptionsToConnection(); + mProfileVersion= CURRENT_PROFILE_VERSION; + + } + + private void moveOptionsToConnection() { + mConnections = new Connection[1]; + Connection conn = new Connection(); + + conn.mServerName = mServerName; + conn.mServerPort = mServerPort; + conn.mUseUdp = mUseUdp; + conn.mCustomConfiguration = ""; + + mConnections[0] = conn; + } public String getConfigFile(Context context, boolean configForOvpn3) { @@ -263,15 +287,19 @@ public class VpnProfile implements Serializable { // We cannot use anything else than tun cfg += "dev tun\n"; - // Server Address - cfg += "remote "; - cfg += mServerName; - cfg += " "; - cfg += mServerPort; - if (mUseUdp) - cfg += " udp\n"; - else - cfg += " tcp-client\n"; + if (mConnections.length==1) { + cfg += mConnections[0].getConnectionBlock(); + } else { + if (mRemoteRandom) + cfg+="remote-random\n"; + for (Connection conn : mConnections) { + if (conn.mEnabled) { + cfg += "\n"; + cfg += conn.getConnectionBlock(); + cfg += "\n"; + } + } + } switch (mAuthenticationType) { diff --git a/main/src/main/java/de/blinkt/openvpn/activities/VPNPreferences.java b/main/src/main/java/de/blinkt/openvpn/activities/VPNPreferences.java index 1f022545..1641a51b 100644 --- a/main/src/main/java/de/blinkt/openvpn/activities/VPNPreferences.java +++ b/main/src/main/java/de/blinkt/openvpn/activities/VPNPreferences.java @@ -25,6 +25,7 @@ import de.blinkt.openvpn.core.ProfileManager; import de.blinkt.openvpn.fragments.Settings_Authentication; import de.blinkt.openvpn.fragments.Settings_Basic; import de.blinkt.openvpn.fragments.Settings_Behaviour; +import de.blinkt.openvpn.fragments.Settings_Connections; import de.blinkt.openvpn.fragments.Settings_IP; import de.blinkt.openvpn.fragments.Settings_Obscure; import de.blinkt.openvpn.fragments.Settings_Routing; @@ -37,7 +38,7 @@ public class VPNPreferences extends PreferenceActivity { static final Class validFragments[] = new Class[] { Settings_Authentication.class, Settings_Basic.class, Settings_IP.class, Settings_Obscure.class, Settings_Routing.class, ShowConfigFragment.class, - Settings_Behaviour.class + Settings_Behaviour.class, Settings_Connections.class }; private String mProfileUUID; diff --git a/main/src/main/java/de/blinkt/openvpn/core/ConfigParser.java b/main/src/main/java/de/blinkt/openvpn/core/ConfigParser.java index 9e99ba55..3015b6fe 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/ConfigParser.java +++ b/main/src/main/java/de/blinkt/openvpn/core/ConfigParser.java @@ -5,9 +5,15 @@ package de.blinkt.openvpn.core; +import android.text.TextUtils; +import android.util.Pair; + import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.io.Reader; +import java.io.StringReader; import java.util.Collections; import java.util.HashMap; import java.util.Locale; @@ -28,9 +34,6 @@ public class ConfigParser { private HashMap>> options = new HashMap>>(); private HashMap> meta = new HashMap>(); - - private boolean extraRemotesAsCustom=false; - public void parseConfig(Reader reader) throws IOException, ConfigParseError { @@ -53,7 +56,7 @@ public class ConfigParser { meta.put(metaarg.get(0),metaarg); continue; } - Vector args = parseline(line); + Vector args = parseline(line); if(args.size() ==0) continue; @@ -98,7 +101,7 @@ public class ConfigParser { break; else { inlinefile+=line; - inlinefile+= "\n"; + inlinefile+= "\n"; } } while(true); @@ -132,7 +135,7 @@ public class ConfigParser { // adapted openvpn's parse function to java private Vector parseline(String line) throws ConfigParseError { - Vector parameters = new Vector(); + Vector parameters = new Vector(); if (line.length()==0) return parameters; @@ -145,12 +148,12 @@ public class ConfigParser { int pos=0; String currentarg=""; - do { + do { // Emulate the c parsing ... char in; if(pos < line.length()) in = line.charAt(pos); - else + else in = '\0'; if (!backslash && in == '\\' && state != linestate.readin_single_quote) @@ -228,10 +231,7 @@ public class ConfigParser { } - final String[] unsupportedOptions = { "config", - "connection", - "proto-force", - "remote-random", + final String[] unsupportedOptions = { "config", "tls-server" }; @@ -299,7 +299,7 @@ public class ConfigParser { "remote", "float", "port", -// "connect-retry", + "connect-retry", "connect-timeout", "connect-retry-max", "link-mtu", @@ -325,7 +325,7 @@ public class ConfigParser { // This method is far too long @SuppressWarnings("ConstantConditions") - public VpnProfile convertProfile() throws ConfigParseError{ + public VpnProfile convertProfile() throws ConfigParseError, IOException { boolean noauthtypeset=true; VpnProfile np = new VpnProfile(CONVERTED_PROFILE); // Pull, client, tls-client @@ -338,7 +338,7 @@ public class ConfigParser { } Vector secret = getOption("secret", 1, 2); - if(secret!=null) + if(secret!=null) { np.mAuthenticationType=VpnProfile.TYPE_STATICKEYS; noauthtypeset=false; @@ -362,7 +362,7 @@ public class ConfigParser { if (route.size() >= 4) gateway = route.get(3); - String net = route.get(1); + String net = route.get(1); try { CIDRIP cidr = new CIDRIP(net, netmask); if (gateway.equals("net_gateway")) @@ -398,7 +398,7 @@ public class ConfigParser { Vector> tlsauthoptions = getAllOption("tls-auth", 1, 2); if(tlsauthoptions!=null) { for(Vector tlsauth:tlsauthoptions) { - if(tlsauth!=null) + if(tlsauth!=null) { if(!tlsauth.get(1).equals("[inline]")) { np.mTLSAuthFilename=tlsauth.get(1); @@ -458,36 +458,6 @@ public class ConfigParser { throw new ConfigParseError("Invalid mode for --mode specified, need p2p"); } - Vector port = getOption("port", 1,1); - if(port!=null){ - np.mServerPort = port.get(1); - } - - Vector rport = getOption("rport", 1,1); - if(rport!=null){ - np.mServerPort = rport.get(1); - } - - Vector proto = getOption("proto", 1,1); - if(proto!=null){ - np.mUseUdp=isUdpProto(proto.get(1)); - } - - // Parse remote config - Vector> remotes = getAllOption("remote",1,3); - - if(remotes!=null && remotes.size()>=1 ) { - Vector remote = remotes.get(0); - switch (remote.size()) { - case 4: - np.mUseUdp=isUdpProto(remote.get(3)); - case 3: - np.mServerPort = remote.get(2); - case 2: - np.mServerName = remote.get(1); - } - } - Vector> dhcpoptions = getAllOption("dhcp-option", 2, 2); @@ -581,18 +551,18 @@ public class ConfigParser { if(verifyx509name!=null){ np.mRemoteCN = verifyx509name.get(1); np.mCheckRemoteCN=true; - if(verifyx509name.size()>2) { + 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 + else throw new ConfigParseError("Unknown parameter to x509-verify-name: " + verifyx509name.get(2) ); } else { np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_DN; } - } + } Vector verb = getOption("verb",1,1); @@ -615,7 +585,7 @@ public class ConfigParser { if(connectretrymax!=null) np.mConnectRetryMax =connectretrymax.get(1); - Vector> remotetls = getAllOption("remote-cert-tls", 1, 1); + Vector> remotetls = getAllOption("remote-cert-tls", 1, 1); if(remotetls!=null) if(remotetls.get(0).get(1).equals("server")) np.mExpectTLSCert=true; @@ -639,7 +609,49 @@ public class ConfigParser { } } - // Parse OpenVPN Access Server extra + Pair conns = parseConnectionOptions(null); + np.mConnections =conns.second; + + Vector> connectionBlocks = getAllOption("connection", 1, 1); + + if (np.mConnections.length > 0 && connectionBlocks !=null ) { + throw new ConfigParseError("Using a block and --remote is not allowed."); + } + + if (connectionBlocks!=null) { + np.mConnections = new Connection[connectionBlocks.size()]; + + int connIndex = 0; + for (Vector conn : connectionBlocks) { + Pair connectionBlockConnection = + parseConnection(conn.get(1), conns.first); + + if (connectionBlockConnection.second.length != 1) + throw new ConfigParseError("A block must have exactly one remote"); + np.mConnections[connIndex] = connectionBlockConnection.second[0]; + connIndex++; + } + } + if(getOption("remote-random", 0, 0) != null) + np.mRemoteRandom=true; + + Vector protoforce = getOption("proto-force", 1, 1); + if(protoforce!=null) { + boolean disableUDP; + String protoToDisable = protoforce.get(1); + if (protoToDisable.equals("udp")) + disableUDP=true; + else if (protoToDisable.equals("tcp")) + 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; + } + + // Parse OpenVPN Access Server extra Vector friendlyname = meta.get("FRIENDLY_NAME"); if(friendlyname !=null && friendlyname.size() > 1) np.mName=friendlyname.get(1); @@ -649,20 +661,95 @@ public class ConfigParser { if(ocusername !=null && ocusername.size() > 1) np.mUsername=ocusername.get(1); - // Check the other options - if(remotes !=null && remotes.size()>1 && extraRemotesAsCustom) { - // first is already added - remotes.remove(0); - np.mCustomConfigOptions += getOptionStrings(remotes); - np.mUseCustomConfig=true; - - } - checkIgnoreAndInvalidOptions(np); + checkIgnoreAndInvalidOptions(np); fixup(np); return np; } + private Pair parseConnection(String connection, Connection defaultValues) throws IOException, ConfigParseError { + // Parse a connection Block as a new configuration file + + + ConfigParser connectionParser = new ConfigParser(); + StringReader reader = new StringReader(connection.substring(VpnProfile.INLINE_TAG.length())); + connectionParser.parseConfig(reader); + + Pair conn = connectionParser.parseConnectionOptions(defaultValues); + + return conn; + } + + private Pair parseConnectionOptions(Connection connDefault) throws ConfigParseError { + Connection conn; + if (connDefault!=null) + try { + conn = connDefault.clone(); + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + return null; + } + else + conn = new Connection(); + + Vector port = getOption("port", 1,1); + if(port!=null){ + conn.mServerPort = port.get(1); + } + + Vector rport = getOption("rport", 1,1); + if(rport!=null){ + conn.mServerPort = rport.get(1); + } + + Vector proto = getOption("proto", 1,1); + if(proto!=null){ + conn.mUseUdp=isUdpProto(proto.get(1)); + } + + + // Parse remote config + Vector> remotes = getAllOption("remote",1,3); + + + // Assume that we need custom options if connectionDefault are set + if(connDefault!=null) { + for (Vector> option : options.values()) { + + conn.mCustomConfiguration += getOptionStrings(option); + + } + if (!TextUtils.isEmpty(conn.mCustomConfiguration)) + conn.mUseCustomConfig = true; + } + // Make remotes empty to simplify code + if (remotes==null) + remotes = new Vector>(); + + Connection[] connections = new Connection[remotes.size()]; + + + int i=0; + for (Vector remote: remotes) { + try { + connections[i] = conn.clone(); + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + } + switch (remote.size()) { + case 4: + connections[i].mUseUdp=isUdpProto(remote.get(3)); + case 3: + connections[i].mServerPort = remote.get(2); + case 2: + connections[i].mServerName = remote.get(1); + } + i++; + } + return Pair.create(conn, connections); + + } + private void checkRedirectParameters(VpnProfile np, Vector> defgw) { for (Vector redirect: defgw) for (int i=1;i parent, View v, int position, long id) { + Toast.makeText(getActivity(), "" + position, Toast.LENGTH_SHORT).show(); + } + }); + + mConnectionsAdapter = new ConnectionsAdapter(getActivity(), this, mProfile); + gridview.setAdapter(mConnectionsAdapter); + + ImageButton fab_button = (ImageButton) v.findViewById(R.id.add_new_remote); + if(fab_button!=null) + fab_button.setOnClickListener(this); + + mUseRandomRemote = (CheckBox) v.findViewById(R.id.remote_random); + mUseRandomRemote.setChecked(mProfile.mRemoteRandom); + + mWarning = (TextView) v.findViewById(R.id.noserver_active_warning); + return v; + } + + + + @Override + public void onClick(View v) { + if (v.getId() == R.id.add_new_remote) { + mConnectionsAdapter.addRemote(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId()==R.id.add_new_remote) + mConnectionsAdapter.addRemote(); + return super.onOptionsItemSelected(item); + } + + @Override + public void onPause() { + super.onPause(); + mConnectionsAdapter.saveProfile(); + mProfile.mRemoteRandom = mUseRandomRemote.isChecked(); + } + + public void setWarningVisible(int showWarning) { + mWarning.setVisibility(showWarning); + } +} diff --git a/main/src/main/java/de/blinkt/openvpn/fragments/Utils.java b/main/src/main/java/de/blinkt/openvpn/fragments/Utils.java index 2408956c..8676ccaf 100644 --- a/main/src/main/java/de/blinkt/openvpn/fragments/Utils.java +++ b/main/src/main/java/de/blinkt/openvpn/fragments/Utils.java @@ -109,7 +109,6 @@ public class Utils { // DocumentsContract.EXTRA_SHOW_ADVANCED is hidden i.putExtra("android.content.extra.SHOW_ADVANCED", true); - /* Samsung has decided to do something strange, on stock Android GET_CONTENT opens the document UI */ /* fist try with documentsui */ i.setPackage("com.android.documentsui"); diff --git a/main/src/main/java/de/blinkt/openvpn/fragments/VPNProfileList.java b/main/src/main/java/de/blinkt/openvpn/fragments/VPNProfileList.java index e4911a41..d13d1ca6 100644 --- a/main/src/main/java/de/blinkt/openvpn/fragments/VPNProfileList.java +++ b/main/src/main/java/de/blinkt/openvpn/fragments/VPNProfileList.java @@ -135,12 +135,8 @@ public class VPNProfileList extends ListFragment { TextView newvpntext = (TextView) v.findViewById(R.id.add_new_vpn_hint); TextView importvpntext = (TextView) v.findViewById(R.id.import_vpn_hint); - - newvpntext.setText(Html.fromHtml(getString(R.string.add_new_vpn_hint),new MiniImageGetter(),null)); importvpntext.setText(Html.fromHtml(getString(R.string.vpn_import_hint),new MiniImageGetter(),null)); - - return v; diff --git a/main/src/main/res/anim/fab_anim.xml b/main/src/main/res/anim/fab_anim.xml new file mode 100644 index 00000000..40d72b9a --- /dev/null +++ b/main/src/main/res/anim/fab_anim.xml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/main/src/main/res/drawable/oval_ripple.xml b/main/src/main/res/drawable/oval_ripple.xml new file mode 100644 index 00000000..65f754d2 --- /dev/null +++ b/main/src/main/res/drawable/oval_ripple.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/main/src/main/res/drawable/white_rect.xml b/main/src/main/res/drawable/white_rect.xml new file mode 100644 index 00000000..bb5a9fba --- /dev/null +++ b/main/src/main/res/drawable/white_rect.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/layout-v21/connection_fab.xml b/main/src/main/res/layout-v21/connection_fab.xml new file mode 100644 index 00000000..571a2a17 --- /dev/null +++ b/main/src/main/res/layout-v21/connection_fab.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/main/src/main/res/layout/basic_settings.xml b/main/src/main/res/layout/basic_settings.xml index 0a27a3e5..f8546a1b 100644 --- a/main/src/main/res/layout/basic_settings.xml +++ b/main/src/main/res/layout/basic_settings.xml @@ -27,52 +27,7 @@ style="@style/item" android:inputType="text" /> - - - - - - - - - - - - - - + + + + + \ No newline at end of file diff --git a/main/src/main/res/layout/connections.xml b/main/src/main/res/layout/connections.xml new file mode 100644 index 00000000..30494bef --- /dev/null +++ b/main/src/main/res/layout/connections.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/layout/server_card.xml b/main/src/main/res/layout/server_card.xml new file mode 100644 index 00000000..d9051b29 --- /dev/null +++ b/main/src/main/res/layout/server_card.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/layout/server_layout b/main/src/main/res/layout/server_layout new file mode 100644 index 00000000..c6d11fe6 --- /dev/null +++ b/main/src/main/res/layout/server_layout @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/layout/vpn_profile_list.xml b/main/src/main/res/layout/vpn_profile_list.xml index 18184e5d..79272f6c 100644 --- a/main/src/main/res/layout/vpn_profile_list.xml +++ b/main/src/main/res/layout/vpn_profile_list.xml @@ -1,71 +1,89 @@ - - - - - - + android:layout_height="match_parent"> + android:orientation="vertical" + android:paddingLeft="@dimen/stdpadding" + android:paddingRight="@dimen/stdpadding"> - + android:layout_height="fill_parent" + android:descendantFocusability="afterDescendants" /> - + android:layout_height="match_parent" + android:gravity="center" + android:orientation="vertical"> + - - + - + - + - - + + + + + + + + + - \ No newline at end of file + + diff --git a/main/src/main/res/menu/connections.xml b/main/src/main/res/menu/connections.xml new file mode 100644 index 00000000..7446746f --- /dev/null +++ b/main/src/main/res/menu/connections.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/main/src/main/res/values/dimens.xml b/main/src/main/res/values/dimens.xml index 9b43bd12..d46cfa98 100644 --- a/main/src/main/res/values/dimens.xml +++ b/main/src/main/res/values/dimens.xml @@ -9,4 +9,11 @@ 8dp false + 48dp + 1dp + 4dp + 16dp + 56dp + + \ No newline at end of file diff --git a/main/src/main/res/xml/vpn_headers.xml b/main/src/main/res/xml/vpn_headers.xml index 65f5e825..576aea8b 100644 --- a/main/src/main/res/xml/vpn_headers.xml +++ b/main/src/main/res/xml/vpn_headers.xml @@ -6,11 +6,12 @@ -
+