diff --git a/app/src/main/java/de/blinkt/openvpn/VpnProfile.java b/app/src/main/java/de/blinkt/openvpn/VpnProfile.java
index 83cde85e7ccd7dba24ca8f546d0f0ce0894a8883..9baac1955db24a5bf128a9ba635de0c9733d8196 100644
--- a/app/src/main/java/de/blinkt/openvpn/VpnProfile.java
+++ b/app/src/main/java/de/blinkt/openvpn/VpnProfile.java
@@ -5,6 +5,9 @@
 
 package de.blinkt.openvpn;
 
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PROFILE;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.stringEqual;
+
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.Intent;
@@ -34,8 +37,6 @@ import java.io.FileWriter;
 import java.io.IOException;
 import java.io.Serializable;
 import java.io.StringWriter;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.security.PrivateKey;
@@ -69,12 +70,6 @@ import de.blinkt.openvpn.core.connection.ConnectionAdapter;
 import se.leap.bitmaskclient.BuildConfig;
 import se.leap.bitmaskclient.R;
 
-import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
-import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_KCP;
-import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN;
-import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PROFILE;
-import static se.leap.bitmaskclient.base.utils.ConfigHelper.stringEqual;
-
 public class VpnProfile implements Serializable, Cloneable {
     // Note that this class cannot be moved to core where it belongs since
     // the profile loading depends on it being here
@@ -184,6 +179,7 @@ public class VpnProfile implements Serializable, Cloneable {
     /* Options no longer used in new profiles */
     public String mServerName = "openvpn.example.com";
     public String mServerPort = "1194";
+    @Deprecated
     public boolean mUseUdp = true;
     public boolean mTemporaryProfile = false;
     private transient PrivateKey mPrivateKey;
@@ -193,8 +189,7 @@ public class VpnProfile implements Serializable, Cloneable {
     private int mProfileVersion;
     public boolean mBlockUnusedAddressFamilies = true;
     public String mGatewayIp;
-    private boolean mUseObfs4;
-    private boolean mUseObfs4Kcp;
+    private final int mTransportType;
 
     public VpnProfile(String name, Connection.TransportType transportType) {
         mUuid = UUID.randomUUID();
@@ -203,8 +198,7 @@ public class VpnProfile implements Serializable, Cloneable {
 
         mConnections = new Connection[1];
         mLastUsed = System.currentTimeMillis();
-        mUseObfs4 = transportType == OBFS4;
-        mUseObfs4Kcp = transportType == OBFS4_KCP;
+        mTransportType = transportType.toInt();
     }
 
     public static String openVpnEscape(String unescaped) {
@@ -270,8 +264,7 @@ public class VpnProfile implements Serializable, Cloneable {
         if (obj instanceof VpnProfile) {
             VpnProfile vp = (VpnProfile) obj;
             return stringEqual(vp.mGatewayIp, mGatewayIp) &&
-                    vp.mUseObfs4 == mUseObfs4 &&
-                    vp.mUseObfs4Kcp == mUseObfs4Kcp;
+                    vp.mTransportType == mTransportType;
         }
         return false;
     }
@@ -302,17 +295,12 @@ public class VpnProfile implements Serializable, Cloneable {
     }
 
     public boolean usePluggableTransports() {
-        return mUseObfs4Kcp || mUseObfs4;
+        Connection.TransportType type = Connection.TransportType.fromInt(mTransportType);
+        return type != null && type.isPluggableTransport();
     }
 
     public Connection.TransportType getTransportType() {
-        if (mUseObfs4) {
-            return OBFS4;
-        } else if (mUseObfs4Kcp) {
-            return OBFS4_KCP;
-        } else {
-            return OPENVPN;
-        }
+        return Connection.TransportType.fromInt(mTransportType);
     }
 
     public String getName() {
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 6063ea538a91e6541636ec4ee29f5b04ab6b6d0f..e8d333e34155a356b0b3581e661969ced4ac42fa 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java
@@ -27,6 +27,7 @@ import java.util.Vector;
 import de.blinkt.openvpn.VpnProfile;
 import de.blinkt.openvpn.core.connection.Connection;
 import de.blinkt.openvpn.core.connection.Obfs4Connection;
+import de.blinkt.openvpn.core.connection.Obfs4HopConnection;
 import de.blinkt.openvpn.core.connection.OpenvpnConnection;
 import se.leap.bitmaskclient.pluggableTransports.Obfs4Options;
 
@@ -807,8 +808,23 @@ public class ConfigParser {
                 e.printStackTrace();
                 return null;
             }
-        else
-            conn = transportType.getMetaType() == PT ? new Obfs4Connection(obfs4Options) : new OpenvpnConnection();
+        else {
+            switch (transportType) {
+                case OBFS4:
+                    conn = new Obfs4Connection(obfs4Options);
+                    break;
+                case OBFS4_HOP:
+                    conn = new Obfs4HopConnection(obfs4Options);
+                    break;
+                case OPENVPN:
+                    conn = new OpenvpnConnection();
+                    break;
+                default:
+                    throw new ConfigParseError("Unexpected transport type: " + transportType);
+
+            }
+
+        }
 
         Vector<String> port = getOption("port", 1, 1);
         if (port != null) {
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 0ae7639e40d08df1f66dfe857afb5ec628b33f42..6adffda1a1b72bc6153393c6036743f2245a9099 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java
@@ -12,7 +12,6 @@ import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PROFILE;
 import static se.leap.bitmaskclient.base.utils.ConfigHelper.ObfsVpnHelper.useObfsVpn;
 
 import android.Manifest.permission;
-import android.annotation.TargetApi;
 import android.app.Notification;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -41,7 +40,6 @@ import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.util.Collection;
-import java.util.Locale;
 import java.util.Vector;
 
 import de.blinkt.openvpn.VpnProfile;
@@ -53,7 +51,8 @@ import se.leap.bitmaskclient.R;
 import se.leap.bitmaskclient.eip.EipStatus;
 import se.leap.bitmaskclient.eip.VpnNotificationManager;
 import se.leap.bitmaskclient.firewall.FirewallManager;
-import se.leap.bitmaskclient.pluggableTransports.ObfsVpnClient;
+import se.leap.bitmaskclient.pluggableTransports.PtClientBuilder;
+import se.leap.bitmaskclient.pluggableTransports.PtClientInterface;
 import se.leap.bitmaskclient.pluggableTransports.ShapeshifterClient;
 
 
@@ -91,7 +90,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
     private Runnable mOpenVPNThread;
     private VpnNotificationManager notificationManager;
     private ShapeshifterClient shapeshifter;
-    private ObfsVpnClient obfsVpnClient;
+    private PtClientInterface obfsVpnClient;
     private FirewallManager firewallManager;
 
     private final IBinder mBinder = new IOpenVPNServiceInternal.Stub() {
@@ -415,18 +414,19 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
         stopOldOpenVPNProcess();
         // An old running VPN should now be exited
         mStarting = false;
-
-        if (mProfile.usePluggableTransports() && connection instanceof Obfs4Connection) {
-            Obfs4Connection obfs4Connection = (Obfs4Connection) connection;
+        Connection.TransportType transportType = connection.getTransportType();
+        if (mProfile.usePluggableTransports() && transportType.isPluggableTransport()) {
             if (useObfsVpn()) {
                 if (obfsVpnClient != null && obfsVpnClient.isStarted()) {
                     obfsVpnClient.stop();
                 }
-                obfsVpnClient = new ObfsVpnClient(obfs4Connection.getDispatcherOptions());
+                obfsVpnClient = PtClientBuilder.getPtClient(connection);
                 int runningSocksPort = obfsVpnClient.start();
-                connection.setProxyPort(String.valueOf(runningSocksPort));
+                if (connection.getTransportType() == Connection.TransportType.OBFS4) {
+                    connection.setProxyPort(String.valueOf(runningSocksPort));
+                }
             } else if (shapeshifter == null) {
-                shapeshifter = new ShapeshifterClient(obfs4Connection.getDispatcherOptions());
+                shapeshifter = new ShapeshifterClient(((Obfs4Connection) connection).getObfs4Options());
                 shapeshifter.start();
             }
         }
diff --git a/app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java b/app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java
index f60e7333f3564f6b816c15c886ffcbb9bb3bb626..0b28cbca8c50a82906b1fe989c040f5bbe8f174c 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java
@@ -5,6 +5,8 @@
 
 package de.blinkt.openvpn.core.connection;
 
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.*;
+
 import android.text.TextUtils;
 
 import com.google.gson.annotations.JsonAdapter;
@@ -37,14 +39,33 @@ public abstract class Connection implements Serializable, Cloneable {
         ORBOT
     }
 
+    public enum TransportProtocol {
+        UDP("udp"),
+        TCP("tcp"),
+        KCP("kcp");
+
+        final String protocol;
+
+        TransportProtocol(String transportProtocol) {
+            this.protocol = transportProtocol;
+        }
+
+        @Override
+        public String toString() {
+            return protocol;
+        }
+    }
+
+    // !! Never use valueOf() to instantiate this enum, use fromString() !!
     public enum TransportType {
         OBFS4("obfs4"),
+        // dashes are not allowed in enums, so obfs4-hop becomes obfs4Hop -.-
+        OBFS4_HOP("obfs4Hop"),
         OPENVPN("openvpn"),
-        OBFS4_KCP("obfs4-1"),
 
         PT("metaTransport");
 
-        String transport;
+        final String transport;
 
         TransportType(String transportType) {
             this.transport = transportType;
@@ -52,15 +73,63 @@ public abstract class Connection implements Serializable, Cloneable {
 
         @Override
         public String toString() {
+            if (this == OBFS4_HOP) {
+                return "obfs4-hop";
+            }
             return transport;
         }
 
+        public int toInt() {
+            switch (this) {
+                case PT:
+                    return 0;
+                case OPENVPN:
+                    return 1;
+                case OBFS4:
+                    return 2;
+                case OBFS4_HOP:
+                    return 3;
+                default:
+                    return -1;
+            }
+        }
+
+        public static TransportType fromString(String value) {
+            switch (value) {
+                case "obfs4":
+                    return OBFS4;
+                case "obfs4-hop":
+                    return OBFS4_HOP;
+                case "metaTransport":
+                    return PT;
+                case "openvpn":
+                    return OPENVPN;
+                default:
+                    throw new IllegalArgumentException(value + " is not a valid value for TransportType.");
+            }
+        }
+
+        public static TransportType fromInt(int value) {
+            switch (value) {
+                case 0:
+                    return PT;
+                case 1:
+                    return OPENVPN;
+                case 2:
+                    return OBFS4;
+                case 3:
+                    return OBFS4_HOP;
+                default:
+                    return null;
+            }
+        }
+
         public boolean isPluggableTransport() {
-            return this == OBFS4 || this == OBFS4_KCP || this == PT;
+            return this == OBFS4 || this == OBFS4_HOP || this == PT;
         }
 
         public TransportType getMetaType() {
-            if (this == OBFS4 || this == OBFS4_KCP || this == PT) {
+            if (this == OBFS4 || this == OBFS4_HOP || this == PT) {
                 return PT;
             }
             return OPENVPN;
diff --git a/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java b/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java
index c77c23fd15ca8247a0d00c39f1dcc676522aa4d9..d152031aeec44f2e3b2e4a282a18aecfe810442c 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java
@@ -4,6 +4,7 @@ import static se.leap.bitmaskclient.base.utils.ConfigHelper.ObfsVpnHelper.useObf
 import static se.leap.bitmaskclient.pluggableTransports.ShapeshifterClient.DISPATCHER_IP;
 import static se.leap.bitmaskclient.pluggableTransports.ShapeshifterClient.DISPATCHER_PORT;
 
+import se.leap.bitmaskclient.pluggableTransports.HoppingObfsVpnClient;
 import se.leap.bitmaskclient.pluggableTransports.Obfs4Options;
 import se.leap.bitmaskclient.pluggableTransports.ObfsVpnClient;
 
@@ -19,21 +20,32 @@ public class Obfs4Connection extends Connection {
 
     public Obfs4Connection(Obfs4Options options) {
         if (useObfsVpn()) {
-            setServerName(options.remoteIP);
-            setServerPort(options.remotePort);
+            setServerName(options.gatewayIP);
+            setServerPort(options.transport.getPorts()[0]);
             setProxyName(ObfsVpnClient.SOCKS_IP);
-            setProxyPort(String.valueOf(ObfsVpnClient.SOCKS_PORT.get()));
             setProxyType(ProxyType.SOCKS5);
+            switch (options.transport.getTransportType()) {
+                case OBFS4:
+                    setUseUdp(false);
+                    setProxyPort(String.valueOf(ObfsVpnClient.SOCKS_PORT.get()));
+                    break;
+                case OBFS4_HOP:
+                    setUseUdp(true);
+                    setProxyPort(String.valueOf(HoppingObfsVpnClient.PORT));
+                    break;
+                default:break;
+            }
         } else {
             setServerName(DISPATCHER_IP);
             setServerPort(DISPATCHER_PORT);
             setProxyName("");
             setProxyPort("");
             setProxyType(ProxyType.NONE);
+
+            // while udp/kcp might be used on the wire,
+            // we don't use udp for openvpn in case of a obfs4 connection
+            setUseUdp(false);
         }
-        // while udp/kcp might be used on the wire,
-        // we don't use udp for openvpn in case of a obfs4 connection
-        setUseUdp(false);
         setProxyAuthUser(null);
         setProxyAuthPassword(null);
         setUseProxyAuth(false);
@@ -53,7 +65,7 @@ public class Obfs4Connection extends Connection {
     }
 
 
-    public Obfs4Options getDispatcherOptions() {
+    public Obfs4Options getObfs4Options() {
         return options;
     }
 
diff --git a/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4HopConnection.java b/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4HopConnection.java
new file mode 100644
index 0000000000000000000000000000000000000000..f983ae20c53f720b2eda72785a2cdfa74fc1017e
--- /dev/null
+++ b/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4HopConnection.java
@@ -0,0 +1,48 @@
+package de.blinkt.openvpn.core.connection;
+
+import se.leap.bitmaskclient.pluggableTransports.HoppingObfsVpnClient;
+import se.leap.bitmaskclient.pluggableTransports.Obfs4Options;
+
+
+/**
+ * Created by cyberta on 08.03.19.
+ */
+
+public class Obfs4HopConnection extends Connection {
+
+    private static final String TAG = Obfs4HopConnection.class.getName();
+    private Obfs4Options options;
+
+    public Obfs4HopConnection(Obfs4Options options) {
+        setServerName(HoppingObfsVpnClient.IP);
+        setServerPort(String.valueOf(HoppingObfsVpnClient.PORT));
+        setProxyName("");
+        setProxyPort("");
+        setProxyType(ProxyType.NONE);
+
+
+        setUseUdp(true);
+        setProxyAuthUser(null);
+        setProxyAuthPassword(null);
+        setUseProxyAuth(false);
+        this.options = options;
+    }
+
+    @Override
+    public Connection clone() throws CloneNotSupportedException {
+        Obfs4HopConnection connection = (Obfs4HopConnection) super.clone();
+        connection.options = this.options;
+        return connection;
+    }
+
+    @Override
+    public TransportType getTransportType() {
+        return TransportType.OBFS4_HOP;
+    }
+
+
+    public Obfs4Options getObfs4Options() {
+        return options;
+    }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
index ee5bd2a79aa1c3519ea6f965176b5f217c12ed96..5746797472934b2bcbbd247f5d985d8ed028759f 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
@@ -179,10 +179,19 @@ public interface Constants {
     String PORTS = "ports";
     String PROTOCOLS = "protocols";
     String UDP = "udp";
+    String TCP = "tcp";
+    String KCP = "kcp";
     String CAPABILITIES = "capabilities";
     String TRANSPORT = "transport";
     String TYPE = "type";
     String OPTIONS = "options";
+    String IAT_MODE = "iatMode";
+    String CERT = "cert";
+    String CERTS = "certs";
+    String ENDPOINTS = "endpoints";
+    String PORT_SEED = "port_seed";
+    String PORT_COUNT = "port_count";
+    String EXPERIMENTAL = "experimental";
     String VERSION = "version";
     String NAME = "name";
     String TIMEZONE = "timezone";
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
index 53f864cf74cf54ec99493a9ef955d650ea96020c..08e13cf6089e05b3ffb5bb6cc78d4d759e901955 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
@@ -16,11 +16,14 @@
  */
 package se.leap.bitmaskclient.base.models;
 
+import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.KCP;
+import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.TCP;
 import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
-import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_KCP;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_HOP;
 import static se.leap.bitmaskclient.base.models.Constants.CAPABILITIES;
 import static se.leap.bitmaskclient.base.models.Constants.GATEWAYS;
 import static se.leap.bitmaskclient.base.models.Constants.LOCATIONS;
+import static se.leap.bitmaskclient.base.models.Constants.PROTOCOLS;
 import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOWED_REGISTERED;
 import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOW_ANONYMOUS;
 import static se.leap.bitmaskclient.base.models.Constants.TRANSPORT;
@@ -46,6 +49,7 @@ import java.util.HashSet;
 import java.util.Locale;
 import java.util.Set;
 
+import de.blinkt.openvpn.core.connection.Connection.TransportProtocol;
 import de.blinkt.openvpn.core.connection.Connection.TransportType;
 import motd.IStringCollection;
 import motd.Motd;
@@ -122,8 +126,10 @@ public final class Provider implements Parcelable {
         setGeoipUrl(geoipUrl);
     }
 
-    public Provider(String mainUrl, String providerIp, String providerApiIp) {
-        this(mainUrl, null, null, providerIp, providerApiIp);
+    public static Provider createCustomProvider(String mainUrl, String domain) {
+        Provider p = new Provider(mainUrl);
+        p.domain = domain;
+        return p;
     }
 
     public Provider(String mainUrl, String geoipUrl, String motdUrl, String providerIp, String providerApiIp) {
@@ -181,16 +187,16 @@ public final class Provider implements Parcelable {
 
     public boolean supportsPluggableTransports() {
         if (useObfsVpn()) {
-            return supportsTransports(new TransportType[]{OBFS4, OBFS4_KCP});
+            return supportsTransports(new Pair[]{new Pair<>(OBFS4, TCP), new Pair<>(OBFS4, KCP), new Pair<>(OBFS4_HOP, TCP), new Pair<>(OBFS4_HOP, KCP)});
         }
-        return supportsTransports(new TransportType[]{OBFS4});
+        return supportsTransports(new Pair[]{new Pair<>(OBFS4, TCP)});
     }
 
     public boolean supportsExperimentalPluggableTransports() {
-        return supportsTransports(new TransportType[]{OBFS4_KCP});
+        return supportsTransports(new Pair[]{new Pair<>(OBFS4, KCP),  new Pair<>(OBFS4_HOP, TCP), new Pair<>(OBFS4_HOP, KCP)});
     }
 
-    private boolean supportsTransports(TransportType[] transportTypes) {
+    private boolean supportsTransports(Pair<TransportType, TransportProtocol>[] transportTypes) {
         try {
             JSONArray gatewayJsons = eipServiceJson.getJSONArray(GATEWAYS);
             for (int i = 0; i < gatewayJsons.length(); i++) {
@@ -199,9 +205,13 @@ public final class Provider implements Parcelable {
                         getJSONArray(TRANSPORT);
                 for (int j = 0; j < transports.length(); j++) {
                     String supportedTransportType = transports.getJSONObject(j).getString(TYPE);
-                    for (TransportType transportType : transportTypes) {
-                        if (transportType.toString().equals(supportedTransportType)) {
-                            return true;
+                    JSONArray transportProtocols = transports.getJSONObject(j).getJSONArray(PROTOCOLS);
+                    for (Pair<TransportType, TransportProtocol> transportPair : transportTypes) {
+                        for (int k = 0; k < transportProtocols.length(); k++) {
+                            if (transportPair.first.toString().equals(supportedTransportType) &&
+                                transportPair.second.toString().equals(transportProtocols.getString(k))) {
+                                return true;
+                            }
                         }
                     }
                 }
@@ -512,6 +522,7 @@ public final class Provider implements Parcelable {
         JSONObject json = new JSONObject();
         try {
             json.put(Provider.MAIN_URL, mainUrl);
+            json.put(Provider.DOMAIN, domain);
         } catch (JSONException e) {
             e.printStackTrace();
         }
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java
index 90a033ddb85f9fa538c7fe2909da40f46e57bb64..33fbbf7af31b3aae3fcb23a3401134c08d89a6e3 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java
@@ -1,21 +1,57 @@
 package se.leap.bitmaskclient.base.models;
 
+import androidx.annotation.Nullable;
+
+import com.google.gson.FieldNamingPolicy;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
+import com.google.gson.annotations.SerializedName;
 
 import org.json.JSONObject;
 
-public class Transport {
+import java.io.Serializable;
+
+import de.blinkt.openvpn.core.connection.Connection;
+
+public class Transport implements Serializable {
     private String type;
     private String[] protocols;
+    @Nullable
     private String[] ports;
+    @Nullable
     private Options options;
 
     public Transport(String type, String[] protocols, String[] ports, String cert) {
+        this(type, protocols, ports, new Options(cert, "0"));
+    }
+
+    public Transport(String type, String[] protocols, String[] ports, Options options) {
         this.type = type;
         this.protocols = protocols;
         this.ports = ports;
-        this.options = new Options(cert);
+        this.options = options;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public Connection.TransportType getTransportType() {
+        return Connection.TransportType.fromString(type);
+    }
+
+    public String[] getProtocols() {
+        return protocols;
+    }
+
+    @Nullable
+    public String[] getPorts() {
+        return ports;
+    }
+
+    @Nullable
+    public Options getOptions() {
+        return options;
     }
 
     @Override
@@ -25,16 +61,71 @@ public class Transport {
 
     public static Transport fromJson(JSONObject json) {
         GsonBuilder builder = new GsonBuilder();
-        return builder.create().fromJson(json.toString(), Transport.class);
+        return builder.
+                setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).
+                create().
+                fromJson(json.toString(), Transport.class);
     }
 
-    public static class Options {
+    public static class Options implements Serializable {
+        @Nullable
         private String cert;
+        @SerializedName("iatMode")
         private String iatMode;
 
-        public Options(String cert) {
+        @Nullable
+        private Endpoint[] endpoints;
+
+        private boolean experimental;
+
+        private int portSeed;
+
+        private int portCount;
+
+
+        public Options(String cert, String iatMode) {
             this.cert = cert;
-            this.iatMode = "0";
+            this.iatMode = iatMode;
+        }
+
+        public Options(String iatMode, Endpoint[] endpoints, int portSeed, int portCount, boolean experimental) {
+            this(iatMode, endpoints, null, portSeed, portCount, experimental);
+        }
+
+        public Options(String iatMode, Endpoint[] endpoints, String cert, int portSeed, int portCount, boolean experimental) {
+            this.iatMode = iatMode;
+            this.endpoints = endpoints;
+            this.portSeed = portSeed;
+            this.portCount = portCount;
+            this.experimental = experimental;
+            this.cert = cert;
+        }
+
+
+        @Nullable
+        public String getCert() {
+            return cert;
+        }
+
+        public String getIatMode() {
+            return iatMode;
+        }
+
+        @Nullable
+        public Endpoint[] getEndpoints() {
+            return endpoints;
+        }
+
+        public boolean isExperimental() {
+            return experimental;
+        }
+
+        public int getPortSeed() {
+            return portSeed;
+        }
+
+        public int getPortCount() {
+            return portCount;
         }
 
         @Override
@@ -44,6 +135,29 @@ public class Transport {
     }
 
 
+    public static class Endpoint implements Serializable {
+        private String ip;
+        private String cert;
+
+        public Endpoint(String ip, String cert) {
+            this.ip = ip;
+            this.cert = cert;
+        }
+
+        @Override
+        public String toString() {
+            return new Gson().toJson(this);
+        }
+
+        public String getIp() {
+            return ip;
+        }
+
+        public String getCert() {
+            return cert;
+        }
+    }
+
 }
 
 
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/InputStreamHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/InputStreamHelper.java
index 8a5264997a81542b9050cc78d4e5fff0edc6b6a0..8e6273a7400bbb5171bd33c0a4bfa02407e41af2 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/utils/InputStreamHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/InputStreamHelper.java
@@ -23,22 +23,12 @@ public class InputStreamHelper {
         return s.hasNext() ? s.next() : "";
     }
 
-    public static String extractKeyFromInputStream(InputStream inputStream, String key) {
-        String value = "";
-
-        JSONObject fileContents = inputStreamToJson(inputStream);
-        if (fileContents != null)
-            value = fileContents.optString(key);
-        return value;
-    }
-
     public static JSONObject inputStreamToJson(InputStream inputStream) {
-        JSONObject json = null;
+        JSONObject json = new JSONObject();
         try {
             byte[] bytes = new byte[inputStream.available()];
             if (inputStream.read(bytes) > 0)
                 json = new JSONObject(new String(bytes));
-            inputStream.reset();
         } catch (IOException | JSONException e) {
             e.printStackTrace();
         }
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
index 88cdc715b3a6c42c7286337e37629910a66dacb9..5b082448d5853662cd72b3abef5c61280e395e27 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
@@ -91,6 +91,7 @@ import se.leap.bitmaskclient.base.models.Provider;
 import se.leap.bitmaskclient.base.models.Pair;
 import se.leap.bitmaskclient.base.models.ProviderObservable;
 import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.eip.GatewaysManager.GatewayOptions;
 
 /**
  * EIP is the abstract base class for interacting with and managing the Encrypted
@@ -255,8 +256,8 @@ public final class EIP extends JobIntentService implements Observer {
             return;
         }
 
-        Pair<Gateway, Connection.TransportType> gatewayTransportTypePair = gatewaysManager.select(nClosestGateway);
-        launchActiveGateway(gatewayTransportTypePair, nClosestGateway, result);
+        GatewayOptions gatewayOptions = gatewaysManager.select(nClosestGateway);
+        launchActiveGateway(gatewayOptions, nClosestGateway, result);
         if (result.containsKey(BROADCAST_RESULT_KEY) && !result.getBoolean(BROADCAST_RESULT_KEY)) {
             tellToReceiverOrBroadcast(this, EIP_ACTION_START, RESULT_CANCELED, result);
         } else {
@@ -270,7 +271,7 @@ public final class EIP extends JobIntentService implements Observer {
      */
     private void startEIPAlwaysOnVpn() {
         GatewaysManager gatewaysManager = new GatewaysManager(getApplicationContext());
-        Pair<Gateway, Connection.TransportType> gatewayTransportTypePair = gatewaysManager.select(0);
+        GatewayOptions gatewayOptions = gatewaysManager.select(0);
         Bundle result = new Bundle();
 
         if (shouldUpdateVPNCertificate()) {
@@ -279,7 +280,7 @@ public final class EIP extends JobIntentService implements Observer {
             ProviderObservable.getInstance().updateProvider(p);
         }
 
-        launchActiveGateway(gatewayTransportTypePair, 0, result);
+        launchActiveGateway(gatewayOptions, 0, result);
         if (result.containsKey(BROADCAST_RESULT_KEY) && !result.getBoolean(BROADCAST_RESULT_KEY)){
             VpnStatus.logWarning("ALWAYS-ON VPN: " + getString(R.string.no_vpn_profiles_defined));
         }
@@ -323,13 +324,13 @@ public final class EIP extends JobIntentService implements Observer {
     /**
      * starts the VPN and connects to the given gateway
      *
-     * @param gatewayTransportTypePair Pair of Gateway and associated transport used to connect
+     * @param gatewayOptions GatewayOptions model containing a Gateway and the associated transport used to connect
      */
-    private void launchActiveGateway(@Nullable Pair<Gateway, Connection.TransportType> gatewayTransportTypePair, int nClosestGateway, Bundle result) {
+    private void launchActiveGateway(@Nullable GatewayOptions gatewayOptions, int nClosestGateway, Bundle result) {
         VpnProfile profile;
 
-        if (gatewayTransportTypePair == null || gatewayTransportTypePair.first == null ||
-                (profile = gatewayTransportTypePair.first.getProfile(gatewayTransportTypePair.second)) == null) {
+        if (gatewayOptions == null || gatewayOptions.gateway == null ||
+                (profile = gatewayOptions.gateway.getProfile(gatewayOptions.transportType)) == null) {
             String preferredLocation = getPreferredCity(getApplicationContext());
             if (preferredLocation != null) {
                 setErrorResult(result, NO_MORE_GATEWAYS.toString(), getStringResourceForNoMoreGateways(), getString(R.string.app_name), preferredLocation);
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java b/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java
index 929935ebe77452f7320bc89650980253ecf44195..719b960e978529b70c5db0445e19ac18d0ee8c56 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java
@@ -16,6 +16,7 @@
  */
 package se.leap.bitmaskclient.eip;
 
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.PT;
 import static se.leap.bitmaskclient.base.models.Constants.FULLNESS;
 import static se.leap.bitmaskclient.base.models.Constants.HOST;
 import static se.leap.bitmaskclient.base.models.Constants.IP_ADDRESS;
@@ -52,7 +53,6 @@ import java.util.HashSet;
 import de.blinkt.openvpn.VpnProfile;
 import de.blinkt.openvpn.core.ConfigParser;
 import de.blinkt.openvpn.core.connection.Connection;
-import se.leap.bitmaskclient.R;
 import se.leap.bitmaskclient.base.utils.ConfigHelper;
 
 /**
@@ -77,6 +77,9 @@ public class Gateway {
     private String name;
     private int timezone;
     private int apiVersion;
+    /** FIXME: We expect here that not more than one obfs4 transport is offered by a gateway, however
+     * it's possible to setup gateways that have obfs4 over kcp and tcp which result in different VpnProfiles each
+     */
     private HashMap<Connection.TransportType, VpnProfile> vpnProfiles;
 
     /**
@@ -209,7 +212,7 @@ public class Gateway {
     }
 
     public boolean supportsTransport(Connection.TransportType transportType) {
-        if (transportType == Connection.TransportType.PT) {
+        if (transportType == PT) {
             return supportsPluggableTransports();
         }
         return vpnProfiles.get(transportType) != null;
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java
index 52030ce395b7150bd9fd81c4112601c82175831f..ad95c82367925d7a03a050e96ba0dac9c4fde4fb 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java
@@ -55,6 +55,7 @@ public class GatewaySelector {
         return offsets.isEmpty() ? null : offsets.firstEntry().getValue().iterator().next();
     }
 
+    // calculateOffsets randomizes the order of Gateways with the same distance, e.g. from the same location
     private TreeMap<Integer, Set<Gateway>> calculateOffsets() {
         TreeMap<Integer, Set<Gateway>> offsets = new TreeMap<Integer, Set<Gateway>>();
         int localOffset = getCurrentTimezone();
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java
index 521d095ec6f10ecc54cca9fad67fe7fe462e7dd1..d114665b87006da79adf6ad619c0bdc6cdc548be 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java
@@ -17,7 +17,7 @@
 package se.leap.bitmaskclient.eip;
 
 import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
-import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_KCP;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_HOP;
 import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN;
 import static de.blinkt.openvpn.core.connection.Connection.TransportType.PT;
 import static se.leap.bitmaskclient.base.models.Constants.GATEWAYS;
@@ -61,7 +61,6 @@ import se.leap.bitmaskclient.BuildConfig;
 import se.leap.bitmaskclient.R;
 import se.leap.bitmaskclient.base.models.GatewayJson;
 import se.leap.bitmaskclient.base.models.Location;
-import se.leap.bitmaskclient.base.models.Pair;
 import se.leap.bitmaskclient.base.models.Provider;
 import se.leap.bitmaskclient.base.models.ProviderObservable;
 import se.leap.bitmaskclient.base.models.Transport;
@@ -103,6 +102,16 @@ public class GatewaysManager {
         }
     }
 
+    public static class GatewayOptions {
+        public Gateway gateway;
+        public TransportType transportType;
+
+        public GatewayOptions(Gateway gateway, TransportType transportType) {
+            this.gateway = gateway;
+            this.transportType = transportType;
+        }
+    }
+
     private static final String TAG = GatewaysManager.class.getSimpleName();
     public static final String PINNED_OBFUSCATION_PROXY = "pinned.obfuscation.proxy";
 
@@ -113,6 +122,8 @@ public class GatewaysManager {
     private ArrayList<Location> locations = new ArrayList<>();
     private TransportType selectedTransport;
 
+    GatewaySelector gatewaySelector;
+
     public GatewaysManager(Context context) {
         this.context = context;
         configureFromCurrentProvider();
@@ -122,7 +133,7 @@ public class GatewaysManager {
      * select closest Gateway
       * @return the n closest Gateway
      */
-    public Pair<Gateway, TransportType> select(int nClosest) {
+    public GatewayOptions select(int nClosest) {
         if (PreferenceHelper.useObfuscationPinning(context)) {
             if (nClosest > 2) {
                 // no need to try again the pinned proxy, probably configuration error
@@ -132,14 +143,14 @@ public class GatewaysManager {
             if (gateway == null) {
                 return null;
             }
-            return new Pair<>(gateway, getObfuscationPinningKCP(context) ? OBFS4_KCP : OBFS4);
+            return new GatewayOptions(gateway, OBFS4);
         }
         String selectedCity = getPreferredCity(context);
         return select(nClosest, selectedCity);
     }
 
-    public Pair<Gateway, TransportType> select(int nClosest, String city) {
-        TransportType[] transportTypes = getUseBridges(context) ? new TransportType[]{OBFS4, OBFS4_KCP} : new TransportType[]{OPENVPN};
+    public GatewayOptions select(int nClosest, String city) {
+        TransportType[] transportTypes = getUseBridges(context) ? new TransportType[]{OBFS4, OBFS4_HOP} : new TransportType[]{OPENVPN};
         if (presortedList.size() > 0) {
             return getGatewayFromPresortedList(nClosest, transportTypes, city);
         }
@@ -266,9 +277,11 @@ public class GatewaysManager {
         return Load.getLoadByValue(location.getAverageLoad(transportType));
     }
 
-    private Pair<Gateway, TransportType> getGatewayFromTimezoneCalculation(int nClosest, TransportType[] transportTypes, @Nullable String city) {
+    private GatewayOptions getGatewayFromTimezoneCalculation(int nClosest, TransportType[] transportTypes, @Nullable String city) {
         List<Gateway> list = new ArrayList<>(gateways.values());
-        GatewaySelector gatewaySelector = new GatewaySelector(list);
+        if (gatewaySelector == null) {
+            gatewaySelector = new GatewaySelector(list);
+        }
         Gateway gateway;
         int found  = 0;
         int i = 0;
@@ -277,7 +290,7 @@ public class GatewaysManager {
                 if ((city == null && gateway.supportsTransport(transportType)) ||
                         (gateway.getName().equals(city) && gateway.supportsTransport(transportType))) {
                     if (found == nClosest) {
-                        return new Pair<>(gateway, transportType);
+                        return new GatewayOptions(gateway, transportType);
                     }
                     found++;
                 }
@@ -287,14 +300,14 @@ public class GatewaysManager {
         return null;
     }
 
-    private Pair<Gateway, TransportType> getGatewayFromPresortedList(int nClosest, TransportType[] transportTypes, @Nullable String city) {
+    private GatewayOptions getGatewayFromPresortedList(int nClosest, TransportType[] transportTypes, @Nullable String city) {
         int found = 0;
         for (Gateway gateway : presortedList) {
             for (TransportType transportType : transportTypes) {
                 if ((city == null && gateway.supportsTransport(transportType)) ||
                         (gateway.getName().equals(city) && gateway.supportsTransport(transportType))) {
                     if (found == nClosest) {
-                        return new Pair<>(gateway, transportType);
+                        return new GatewayOptions(gateway, transportType);
                     }
                     found++;
                 }
@@ -333,7 +346,9 @@ public class GatewaysManager {
 
     private int getPositionFromTimezoneCalculatedList(VpnProfile profile) {
         TransportType transportType = profile.getTransportType();
-        GatewaySelector gatewaySelector = new GatewaySelector(new ArrayList<>(gateways.values()));
+        if (gatewaySelector == null) {
+            gatewaySelector = new GatewaySelector(new ArrayList<>(gateways.values()));
+        }
         Gateway gateway;
         int nClosest = 0;
         int i = 0;
@@ -387,10 +402,9 @@ public class GatewaysManager {
 
              if (PreferenceHelper.useObfuscationPinning(context)) {
                  try {
-                     TransportType transportType = getObfuscationPinningKCP(context) ? OBFS4_KCP : OBFS4;
                      Transport[] transports = new Transport[]{
-                             new Transport(transportType.toString(),
-                                     new String[]{"tcp"},
+                             new Transport(OBFS4.toString(),
+                                     new String[]{getObfuscationPinningKCP(context) ? "kcp" : "tcp"},
                                      new String[]{getObfuscationPinningPort(context)},
                                      getObfuscationPinningCert(context))};
                      GatewayJson.Capabilities capabilities = new GatewayJson.Capabilities(false, false, false, transports, false);
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java b/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java
index 72a0d80ad2e7bcc9902bb2a9c2a63ac375f97cb4..2c22d4f7aadfe2a4935d8b653694a68ea6b54151 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java
@@ -17,20 +17,19 @@
 package se.leap.bitmaskclient.eip;
 
 import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
-import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_KCP;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_HOP;
 import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN;
-import static de.blinkt.openvpn.core.connection.Connection.TransportType.PT;
 import static se.leap.bitmaskclient.base.models.Constants.CAPABILITIES;
 import static se.leap.bitmaskclient.base.models.Constants.IP_ADDRESS;
 import static se.leap.bitmaskclient.base.models.Constants.IP_ADDRESS6;
-import static se.leap.bitmaskclient.base.models.Constants.OPTIONS;
+import static se.leap.bitmaskclient.base.models.Constants.KCP;
 import static se.leap.bitmaskclient.base.models.Constants.PORTS;
 import static se.leap.bitmaskclient.base.models.Constants.PROTOCOLS;
 import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PRIVATE_KEY;
 import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE;
 import static se.leap.bitmaskclient.base.models.Constants.REMOTE;
+import static se.leap.bitmaskclient.base.models.Constants.TCP;
 import static se.leap.bitmaskclient.base.models.Constants.TRANSPORT;
-import static se.leap.bitmaskclient.base.models.Constants.TYPE;
 import static se.leap.bitmaskclient.base.models.Constants.UDP;
 import static se.leap.bitmaskclient.base.utils.ConfigHelper.ObfsVpnHelper.useObfsVpn;
 import static se.leap.bitmaskclient.pluggableTransports.ShapeshifterClient.DISPATCHER_IP;
@@ -55,15 +54,16 @@ import de.blinkt.openvpn.core.VpnStatus;
 import de.blinkt.openvpn.core.connection.Connection;
 import de.blinkt.openvpn.core.connection.Connection.TransportType;
 import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.models.Transport;
 import se.leap.bitmaskclient.base.utils.ConfigHelper;
+import se.leap.bitmaskclient.pluggableTransports.HoppingObfsVpnClient;
 import se.leap.bitmaskclient.pluggableTransports.Obfs4Options;
 
 public class VpnConfigGenerator {
     private final JSONObject generalConfiguration;
     private final JSONObject gateway;
     private final JSONObject secrets;
-    private JSONObject obfs4Transport;
-    private JSONObject obfs4TKcpTransport;
+    HashMap<TransportType, Transport> transports = new HashMap<>();
     private final int apiVersion;
     private final boolean preferUDP;
     private final boolean experimentalTransports;
@@ -115,23 +115,14 @@ public class VpnConfigGenerator {
 
     public void checkCapabilities() throws ConfigParser.ConfigParseError {
         try {
-
             if (apiVersion >= 3) {
                 JSONArray supportedTransports = gateway.getJSONObject(CAPABILITIES).getJSONArray(TRANSPORT);
                 for (int i = 0; i < supportedTransports.length(); i++) {
-                    JSONObject transport = supportedTransports.getJSONObject(i);
-                    if (transport.getString(TYPE).equals(OBFS4.toString())) {
-                        obfs4Transport = transport;
-                        if (!experimentalTransports && !obfuscationPinningKCP) {
-                            break;
-                        }
-                    } else if ((experimentalTransports || obfuscationPinningKCP) && transport.getString(TYPE).equals(OBFS4_KCP.toString())) {
-                        obfs4TKcpTransport = transport;
-                    }
+                    Transport transport = Transport.fromJson(supportedTransports.getJSONObject(i));
+                    transports.put(transport.getTransportType(), transport);
                 }
             }
-
-        } catch (JSONException e) {
+        } catch (Exception e) {
             throw new ConfigParser.ConfigParseError("Api version ("+ apiVersion +") did not match required JSON fields");
         }
     }
@@ -147,18 +138,20 @@ public class VpnConfigGenerator {
                 e.printStackTrace();
             }
         }
-        if (supportsObfs4()) {
-            try {
-                profiles.put(OBFS4, createProfile(OBFS4));
-            } catch (ConfigParser.ConfigParseError | NumberFormatException | JSONException | IOException e) {
-                e.printStackTrace();
-            }
-        }
-        if (supportsObfs4Kcp()) {
-            try {
-                profiles.put(OBFS4_KCP, createProfile(OBFS4_KCP));
-            } catch (ConfigParser.ConfigParseError | NumberFormatException | JSONException | IOException e) {
-                e.printStackTrace();
+        if (apiVersion >= 3) {
+            for (TransportType transportType : transports.keySet()) {
+                Transport transport = transports.get(transportType);
+                if (transportType.isPluggableTransport()) {
+                    Transport.Options transportOptions = transport.getOptions();
+                    if (!experimentalTransports && transportOptions != null && transportOptions.isExperimental()) {
+                        continue;
+                    }
+                    try {
+                        profiles.put(transportType, createProfile(transportType));
+                    } catch (ConfigParser.ConfigParseError | NumberFormatException | JSONException | IOException e) {
+                        e.printStackTrace();
+                    }
+                }
             }
         }
         if (profiles.isEmpty()) {
@@ -168,14 +161,9 @@ public class VpnConfigGenerator {
     }
 
     private boolean supportsOpenvpn() {
-        return !useObfuscationPinning && !gatewayConfiguration(OPENVPN).isEmpty();
-    }
-    private boolean supportsObfs4(){
-        return obfs4Transport != null && !(useObfuscationPinning && obfuscationPinningKCP);
-    }
-
-    private boolean supportsObfs4Kcp() {
-        return obfs4TKcpTransport != null && !(useObfuscationPinning && !obfuscationPinningKCP);
+        return !useObfuscationPinning &&
+                ((apiVersion >= 3 && transports.containsKey(OPENVPN)) ||
+                        (apiVersion < 3 && !gatewayConfiguration(OPENVPN).isEmpty()));
     }
 
     private String getConfigurationString(TransportType transportType) {
@@ -193,10 +181,8 @@ public class VpnConfigGenerator {
         String configuration = getConfigurationString(transportType);
         ConfigParser icsOpenvpnConfigParser = new ConfigParser();
         icsOpenvpnConfigParser.parseConfig(new StringReader(configuration));
-        if (transportType == OBFS4) {
-            icsOpenvpnConfigParser.setObfs4Options(getObfs4Options(obfs4Transport, false));
-        } else if (transportType == OBFS4_KCP) {
-            icsOpenvpnConfigParser.setObfs4Options(getObfs4Options(obfs4TKcpTransport, true));
+        if (transportType == OBFS4 || transportType == OBFS4_HOP) {
+            icsOpenvpnConfigParser.setObfs4Options(getObfs4Options(transportType));
         }
 
         VpnProfile profile = icsOpenvpnConfigParser.convertProfile(transportType);
@@ -208,22 +194,19 @@ public class VpnConfigGenerator {
         return profile;
     }
 
-    // TODO: whad does
-    private Obfs4Options getObfs4Options(JSONObject transportJson, boolean useUdp) throws JSONException {
-        JSONObject transportOptions = transportJson.getJSONObject(OPTIONS);
-        String iatMode = transportOptions.getString("iatMode");
-        String cert = transportOptions.getString("cert");
-        String port = transportJson.getJSONArray(PORTS).getString(0);
+    private Obfs4Options getObfs4Options(TransportType transportType) throws JSONException {
         String ip = gateway.getString(IP_ADDRESS);
-        boolean udp = useUdp;
-
+        Transport transport;
         if (useObfuscationPinning) {
-            cert = obfuscationPinningCert;
-            port = obfuscationPinningPort;
+            transport = new Transport(OBFS4.toString(),
+                    new String[]{obfuscationPinningKCP ? KCP : TCP},
+                    new String[]{obfuscationPinningPort},
+                    obfuscationPinningCert);
             ip = obfuscationPinningIP;
-            udp = obfuscationPinningKCP;
+        } else {
+            transport = transports.get(transportType);
         }
-        return new Obfs4Options(ip, port, cert, iatMode, udp);
+        return new Obfs4Options(ip, transport);
     }
 
     private String generalConfiguration() {
@@ -250,7 +233,7 @@ public class VpnConfigGenerator {
     }
 
     private String gatewayConfiguration(TransportType transportType) {
-        String remotes = "";
+        String configs = "";
 
         StringBuilder stringBuilder = new StringBuilder();
         try {
@@ -271,8 +254,7 @@ public class VpnConfigGenerator {
                             new String[]{ipAddress} :
                             new String[]{ipAddress6, ipAddress};
 
-                    JSONArray transports = capabilities.getJSONArray(TRANSPORT);
-                    gatewayConfigMinApiv3(transportType, stringBuilder, ipAddresses, transports);
+                    gatewayConfigMinApiv3(transportType, stringBuilder, ipAddresses);
                     break;
             }
         } catch (JSONException e) {
@@ -280,19 +262,19 @@ public class VpnConfigGenerator {
             e.printStackTrace();
         }
 
-        remotes = stringBuilder.toString();
-        if (remotes.endsWith(newLine)) {
-            remotes = remotes.substring(0, remotes.lastIndexOf(newLine));
+        configs = stringBuilder.toString();
+        if (configs.endsWith(newLine)) {
+            configs = configs.substring(0, configs.lastIndexOf(newLine));
         }
 
-        return remotes;
+        return configs;
     }
 
-    private void gatewayConfigMinApiv3(TransportType transportType, StringBuilder stringBuilder, String[] ipAddresses, JSONArray transports) throws JSONException {
-        if (transportType.getMetaType() == PT) {
-            ptGatewayConfigMinApiv3(stringBuilder, ipAddresses, transportType, transports);
+    private void gatewayConfigMinApiv3(TransportType transportType, StringBuilder stringBuilder, String[] ipAddresses) throws JSONException {
+        if (transportType.isPluggableTransport()) {
+            ptGatewayConfigMinApiv3(stringBuilder, ipAddresses, transports.get(transportType));
         } else {
-            ovpnGatewayConfigMinApi3(stringBuilder, ipAddresses, transports);
+            ovpnGatewayConfigMinApi3(stringBuilder, ipAddresses, transports.get(OPENVPN));
         }
     }
 
@@ -311,19 +293,16 @@ public class VpnConfigGenerator {
         }
     }
 
-    private void ovpnGatewayConfigMinApi3(StringBuilder stringBuilder, String[] ipAddresses, JSONArray transports) throws JSONException {
-        String port;
-        String protocol;
-        JSONObject openvpnTransport = getTransport(transports, OPENVPN);
-        JSONArray ports = openvpnTransport.getJSONArray(PORTS);
-        JSONArray protocols = openvpnTransport.getJSONArray(PROTOCOLS);
+    private void ovpnGatewayConfigMinApi3(StringBuilder stringBuilder, String[] ipAddresses, Transport transport) {
+        if (transport.getProtocols() == null || transport.getPorts() == null) {
+            VpnStatus.logError("Misconfigured provider: missing details for transport openvpn on gateway " + ipAddresses[0]);
+            return;
+        }
         if (preferUDP) {
             StringBuilder udpRemotes = new StringBuilder();
             StringBuilder tcpRemotes = new StringBuilder();
-            for (int i = 0; i < protocols.length(); i++) {
-                protocol = protocols.optString(i);
-                for (int j = 0; j < ports.length(); j++) {
-                    port = ports.optString(j);
+            for (String protocol : transport.getProtocols()) {
+                for (String port : transport.getPorts()) {
                     for (String ipAddress : ipAddresses) {
                         String newRemote = REMOTE + " " + ipAddress + " " + port + " " + protocol + newLine;
                         if (UDP.equals(protocol)) {
@@ -337,10 +316,8 @@ public class VpnConfigGenerator {
             stringBuilder.append(udpRemotes.toString());
             stringBuilder.append(tcpRemotes.toString());
         } else {
-            for (int j = 0; j < ports.length(); j++) {
-                port = ports.getString(j);
-                for (int k = 0; k < protocols.length(); k++) {
-                    protocol = protocols.optString(k);
+            for (String protocol : transport.getProtocols()) {
+                for (String port : transport.getPorts()) {
                     for (String ipAddress : ipAddresses) {
                         String newRemote = REMOTE + " " + ipAddress + " " + port + " " + protocol + newLine;
                         stringBuilder.append(newRemote);
@@ -350,32 +327,18 @@ public class VpnConfigGenerator {
         }
     }
 
-    private JSONObject getTransport(JSONArray transports, TransportType transportType) throws JSONException {
-        JSONObject selectedTransport = new JSONObject();
-        for (int i = 0; i < transports.length(); i++) {
-            JSONObject transport = transports.getJSONObject(i);
-            if (transport.getString(TYPE).equals(transportType.toString())) {
-                selectedTransport = transport;
-                break;
-            }
-        }
-        return selectedTransport;
-    }
-
     private boolean isAllowedProtocol(TransportType transportType, String protocol) {
         switch (transportType) {
             case OPENVPN:
-                return "tcp".equals(protocol) || "udp".equals(protocol);
+                return TCP.equals(protocol) || UDP.equals(protocol);
+            case OBFS4_HOP:
             case OBFS4:
-            case OBFS4_KCP:
-                return "tcp".equals(protocol);
+                return TCP.equals(protocol) || KCP.equals(protocol);
         }
         return false;
     }
 
-    private void ptGatewayConfigMinApiv3(StringBuilder stringBuilder, String[] ipAddresses, TransportType transportType, JSONArray transports) throws JSONException {
-        JSONObject ptTransport = getTransport(transports, transportType);
-        JSONArray ptProtocols = ptTransport.getJSONArray(PROTOCOLS);
+    private void ptGatewayConfigMinApiv3(StringBuilder stringBuilder, String[] ipAddresses, Transport transport) {
 
         //for now only use ipv4 gateway the syntax route remote_host 255.255.255.255 net_gateway is not yet working
         // https://community.openvpn.net/openvpn/ticket/1161
@@ -399,63 +362,123 @@ public class VpnConfigGenerator {
         }
 
         if (ipAddress == null) {
-            VpnStatus.logError("No matching IPv4 address found to configure obfs4.");
+            VpnStatus.logError("Misconfigured provider: No matching IPv4 address found to configure obfs4.");
             return;
         }
 
-        if (!useObfuscationPinning) {
-            // check if at least one openvpn protocol is TCP, openvpn in UDP is currently not supported for obfs4,
-            // however on the wire UDP might be used
-            boolean hasOpenvpnTcp = false;
-            JSONObject openvpnTransport = getTransport(transports, OPENVPN);
-            JSONArray gatewayProtocols = openvpnTransport.getJSONArray(PROTOCOLS);
-            for (int i = 0; i < gatewayProtocols.length(); i++) {
-                String protocol = gatewayProtocols.getString(i);
-                if (protocol.contains("tcp")) {
-                    hasOpenvpnTcp = true;
-                    break;
-                }
-            }
-            if (!hasOpenvpnTcp) {
-                VpnStatus.logError("obfs4 currently only allows openvpn in TCP mode! Skipping obfs4 config for ip " + ipAddress);
-                return;
-            }
-        }
-
-        boolean hasAllowedPTProtocol = false;
-        for (int i = 0; i < ptProtocols.length(); i++) {
-            String protocol = ptProtocols.getString(i);
-            if (isAllowedProtocol(transportType, protocol)) {
-                hasAllowedPTProtocol = true;
-                break;
-            }
+        if (!openvpnModeSupportsPt(transport, ipAddress) || !hasPTAllowedProtocol(transport, ipAddress)) {
+            return;
         }
 
-        if (!hasAllowedPTProtocol) {
-            VpnStatus.logError("Misconfigured provider: wrong protocol defined in  " + transportType.toString()+ " transport JSON.");
+        TransportType transportType = transport.getTransportType();
+        if (transportType == OBFS4 && (transport.getPorts() == null || transport.getPorts().length == 0)) {
+            VpnStatus.logError("Misconfigured provider: no ports defined in " + transport.getType() + " transport JSON for gateway " + ipAddress);
             return;
         }
 
-        JSONArray ports = ptTransport.getJSONArray(PORTS);
-        if (ports.isNull(0)){
-            VpnStatus.logError("Misconfigured provider: no ports defined in " + transportType.toString()+ " transport JSON.");
+        if (transportType == OBFS4_HOP &&
+                (transport.getOptions() == null ||
+                        (transport.getOptions().getEndpoints() == null && transport.getOptions().getCert() == null) ||
+                        transport.getOptions().getPortCount() == 0)) {
+            VpnStatus.logError("Misconfigured provider: missing properties for transport " + transport.getType() + " on gateway " + ipAddress);
             return;
         }
 
-        String route = "route " + ipAddress + " 255.255.255.255 net_gateway" + newLine;
-        String remote;
+        stringBuilder.append(getRouteString(ipAddress, transport));
+        stringBuilder.append(getRemoteString(ipAddress, transport));
+        stringBuilder.append(getExtraOptions(transport));
+    }
+
+    public String getRemoteString(String ipAddress, Transport transport) {
         if (useObfsVpn()) {
             if (useObfuscationPinning) {
-                remote = REMOTE + " " + obfuscationPinningIP + " " + obfuscationPinningPort + newLine;
-                route = "route " + obfuscationPinningIP + " 255.255.255.255 net_gateway" + newLine;
-            } else {
-                remote = REMOTE + " " + ipAddress + " " + ports.getString(0) + newLine;
+                return REMOTE + " " + obfuscationPinningIP + " " + obfuscationPinningPort + " tcp" + newLine;
+            }
+            switch (transport.getTransportType()) {
+                case OBFS4:
+                    return REMOTE + " " + ipAddress + " " + transport.getPorts()[0] + " tcp" + newLine;
+                case OBFS4_HOP:
+                    return REMOTE + " " + HoppingObfsVpnClient.IP + " " + HoppingObfsVpnClient.PORT + " udp" + newLine;
+                default:
+                    VpnStatus.logError("Unexpected pluggable transport type " + transport.getType() + " for gateway " + ipAddress);
+                    return "";
+            }
+        }
+        return REMOTE + " " + DISPATCHER_IP + " " + DISPATCHER_PORT + " tcp" + newLine;
+    }
+
+    public String getExtraOptions(Transport transport) {
+        if (transport.getTransportType() == OBFS4_HOP) {
+            return "replay-window 65535" + newLine +
+                    "ping-restart 300" + newLine +
+                    "tun-mtu 48000" + newLine;
+        }
+        return "";
+    }
+
+    public String getRouteString(String ipAddress, Transport transport) {
+        if (useObfuscationPinning) {
+            return "route " + obfuscationPinningIP + " 255.255.255.255 net_gateway" + newLine;
+        }
+        switch (transport.getTransportType()) {
+            case OBFS4:
+                return "route " + ipAddress + " 255.255.255.255 net_gateway" + newLine;
+            case OBFS4_HOP:
+                if (transport.getOptions().getEndpoints() != null)  {
+                    StringBuilder routes = new StringBuilder();
+                    for (Transport.Endpoint endpoint : transport.getOptions().getEndpoints()) {
+                        routes.append("route " + endpoint.getIp() + " 255.255.255.255 net_gateway" + newLine);
+                    }
+                    return routes.toString();
+                } else {
+                    return "route " + ipAddress + " 255.255.255.255 net_gateway" + newLine;
+                }
+        }
+
+        return "";
+    }
+
+    // While openvpn in TCP mode is required for obfs4, openvpn in UDP mode is required for obfs4-hop
+    private boolean openvpnModeSupportsPt(Transport transport, String ipAddress) {
+        if (useObfuscationPinning) {
+            // we don't know if the manually pinned bridge points to a openvpn gateway with the right
+            // configuration, so we assume yes
+            return true;
+        }
+        Transport openvpnTransport = transports.get(OPENVPN);
+        if (openvpnTransport == null) {
+            // the bridge seems to be to be decoupled from the gateway, we can't say if the openvpn gateway
+            // will support this PT and hope the admins configured the gateway correctly
+            return true;
+        }
+
+        String[] protocols = openvpnTransport.getProtocols();
+        if (protocols == null) {
+            VpnStatus.logError("Misconfigured provider: Protocol array is missing for openvpn gateway " + ipAddress);
+            return false;
+        }
+
+        String requiredProtocol = transport.getTransportType() == OBFS4_HOP ? UDP : TCP;
+        for (String protocol : protocols) {
+            if (protocol.equals(requiredProtocol)) {
+                return true;
             }
-        } else {
-            remote = REMOTE + " " + DISPATCHER_IP + " " + DISPATCHER_PORT + " tcp" + newLine;
         }
-        stringBuilder.append(route);
-        stringBuilder.append(remote);
+
+        VpnStatus.logError("Misconfigured provider: " + transport.getTransportType().toString() + " currently only allows openvpn in " + requiredProtocol + " mode! Skipping config for ip " + ipAddress);
+        return false;
+    }
+
+    private boolean hasPTAllowedProtocol(Transport transport, String ipAddress) {
+        String[] ptProtocols = transport.getProtocols();
+        for (String protocol : ptProtocols) {
+            if (isAllowedProtocol(transport.getTransportType(), protocol)) {
+                return true;
+            }
+        }
+
+        VpnStatus.logError("Misconfigured provider: wrong protocol defined in  " + transport.getType() + " transport JSON for gateway " + ipAddress);
+        return false;
     }
 
     private String secretsConfiguration() {
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/HoppingConfig.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/HoppingConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..3780b7dc006286f37717f9ec48abba7fe07d741e
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/HoppingConfig.java
@@ -0,0 +1,57 @@
+package se.leap.bitmaskclient.pluggableTransports;
+
+import androidx.annotation.NonNull;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import se.leap.bitmaskclient.base.models.Transport;
+
+public class HoppingConfig {
+    final boolean kcp;
+    final String proxyAddr;
+    final String[] remotes;
+    final String[] certs;
+    final int portSeed;
+    final int portCount;
+    final int minHopSeconds;
+    final int hopJitter;
+
+    public HoppingConfig(boolean kcp,
+                         String proxyAddr,
+                         Obfs4Options options,
+                         int minHopSeconds,
+                         int hopJitter) {
+        this.kcp = kcp;
+        this.proxyAddr = proxyAddr;
+        Transport transport = options.transport;
+        Transport.Endpoint[] endpoints = transport.getOptions().getEndpoints();
+        if (endpoints == null) {
+            // only port hopping, we assume the gateway IP as hopping PT's IP
+            this.remotes = new String[]{ options.gatewayIP };
+            this.certs = new String[] { transport.getOptions().getCert() };
+        } else {
+            // port+ip hopping
+            this.remotes = new String[endpoints.length];
+            this.certs = new String[endpoints.length];
+            for (int i = 0; i < remotes.length; i++) {
+                remotes[i] = endpoints[i].getIp();
+                certs[i] = endpoints[i].getCert();
+            }
+        }
+        this.portSeed = transport.getOptions().getPortSeed();
+        this.portCount = transport.getOptions().getPortCount();
+        this.minHopSeconds = minHopSeconds;
+        this.hopJitter = hopJitter;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        Gson gson = new GsonBuilder()
+                .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+                .create();
+       return gson.toJson(this);
+    }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/HoppingObfsVpnClient.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/HoppingObfsVpnClient.java
new file mode 100644
index 0000000000000000000000000000000000000000..751208ba91e1b3cd4b1d43054ae57a7522616ef2
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/HoppingObfsVpnClient.java
@@ -0,0 +1,66 @@
+package se.leap.bitmaskclient.pluggableTransports;
+
+import client.Client;
+import client.HopClient;
+import de.blinkt.openvpn.core.VpnStatus;
+import se.leap.bitmaskclient.base.models.Constants;
+
+public class HoppingObfsVpnClient implements PtClientInterface {
+
+    public static final int PORT = 8080;
+    public static final String IP = "127.0.0.1";
+
+    public final HopClient client;
+
+    public HoppingObfsVpnClient(Obfs4Options options) throws IllegalStateException {
+
+        //FIXME: use a different strategy here
+        //Basically we would want to track if the more performant transport protocol (KCP?/TCP?) usage was successful
+        //if so, we stick to it, otherwise we flip the flag
+        boolean kcp = Constants.KCP.equals(options.transport.getProtocols()[0]);
+
+        HoppingConfig hoppingConfig = new HoppingConfig(kcp,IP+":"+PORT, options, 10, 10);
+        try {
+            client = Client.newFFIHopClient(hoppingConfig.toString());
+        } catch (Exception e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    @Override
+    public int start() {
+        try {
+            client.setEventLogger(this);
+            return client.start() ? PORT : 0;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0;
+        }
+    }
+
+    @Override
+    public void stop() {
+        try {
+            client.stop();
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            client.setEventLogger(null);
+        }
+    }
+
+    @Override
+    public boolean isStarted() {
+        return client.isStarted();
+    }
+
+    @Override
+    public void error(String s) {
+        VpnStatus.logError("[hopping-obfs4] " + s);
+    }
+
+    @Override
+    public void log(String state, String message) {
+        VpnStatus.logDebug("[hopping-obfs4] " + state + ": " + message);
+    }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Obfs4Options.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Obfs4Options.java
index b96f88ca12f6a340bd5b653cb1b2a4d1ea8f5af1..0dd81eb86feae194a15e4de4e5ff7cf2e53e4cc1 100644
--- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Obfs4Options.java
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Obfs4Options.java
@@ -2,20 +2,15 @@ package se.leap.bitmaskclient.pluggableTransports;
 
 import java.io.Serializable;
 
+import se.leap.bitmaskclient.base.models.Transport;
+
 public class Obfs4Options implements Serializable {
-    public String cert;
-    public String iatMode;
-    public String remoteIP;
-    public String remotePort;
-    // openvpn is still using tcp, obfs4 is wrapped in kcp, if udp == true
-    public boolean udp;
+    public String gatewayIP;
+    public Transport transport;
 
-    public Obfs4Options(String remoteIP, String remotePort, String cert, String iatMode, boolean udp) {
-        this.cert = cert;
-        this.iatMode = iatMode;
-        this.remoteIP = remoteIP;
-        this.remotePort = remotePort;
-        this.udp = udp;
+    public Obfs4Options(String gatewayIP,
+                        Transport transport) {
+        this.gatewayIP = gatewayIP;
+        this.transport = transport;
     }
-
 }
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsVpnClient.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsVpnClient.java
index f6c8837eabc2b4c9844a8bd96e3449577ca57118..9d5ddcf9ae6041a4b2ffb9dc1ff630843cb97067 100644
--- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsVpnClient.java
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsVpnClient.java
@@ -1,5 +1,7 @@
 package se.leap.bitmaskclient.pluggableTransports;
 
+import static se.leap.bitmaskclient.base.models.Constants.KCP;
+
 import android.util.Log;
 
 import java.util.Observable;
@@ -7,12 +9,12 @@ import java.util.Observer;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import client.Client_;
+import client.Client;
 import de.blinkt.openvpn.core.ConnectionStatus;
 import de.blinkt.openvpn.core.VpnStatus;
 import se.leap.bitmaskclient.eip.EipStatus;
 
-public class ObfsVpnClient implements Observer, client.EventLogger {
+public class ObfsVpnClient implements Observer, PtClientInterface {
 
     public static final AtomicInteger SOCKS_PORT = new AtomicInteger(4430);
     public static final String SOCKS_IP = "127.0.0.1";
@@ -27,9 +29,17 @@ public class ObfsVpnClient implements Observer, client.EventLogger {
     private final client.Client_ obfsVpnClient;
     private final Object LOCK = new Object();
 
-    public ObfsVpnClient(Obfs4Options options) {
-        obfsVpnClient = new Client_(options.udp, SOCKS_IP+":"+SOCKS_PORT.get(), options.cert);
-        obfsVpnClient.setEventLogger(this);
+    public ObfsVpnClient(Obfs4Options options) throws IllegalStateException{
+        //FIXME: use a different strategy here
+        //Basically we would want to track if the more performant transport protocol (KCP?/TCP?) usage was successful
+        //if so, we stick to it, otherwise we flip the flag
+        boolean kcp = KCP.equals(options.transport.getProtocols()[0]);
+
+        if (options.transport.getOptions().getCert() == null) {
+            throw new IllegalStateException("No cert found to establish a obfs4 connection");
+        }
+
+        obfsVpnClient = Client.newClient(kcp, SOCKS_IP+":"+SOCKS_PORT.get(), options.transport.getOptions().getCert());
     }
 
     /**
@@ -38,6 +48,7 @@ public class ObfsVpnClient implements Observer, client.EventLogger {
      */
     public int start() {
         synchronized (LOCK) {
+            obfsVpnClient.setEventLogger(this);
             Log.d(TAG, "aquired LOCK");
             new Thread(this::startSync).start();
             waitUntilStarted();
@@ -46,6 +57,7 @@ public class ObfsVpnClient implements Observer, client.EventLogger {
         return SOCKS_PORT.get();
     }
 
+    // We're waiting here until the obfsvpn client has found a unbound port and started
     private void waitUntilStarted() {
         int count = -1;
         try {
@@ -88,6 +100,8 @@ public class ObfsVpnClient implements Observer, client.EventLogger {
             } catch (Exception e) {
                 e.printStackTrace();
                 VpnStatus.logError("[obfsvpn] " + e.getLocalizedMessage());
+            } finally {
+                obfsVpnClient.setEventLogger(null);
             }
             pendingNetworkErrorHandling.set(false);
             Log.d(TAG, "stopping obfsVpnClient releasing LOCK ...");
@@ -98,6 +112,7 @@ public class ObfsVpnClient implements Observer, client.EventLogger {
         return obfsVpnClient.isStarted();
     }
 
+    // TODO: register observer!
     @Override
     public void update(Observable observable, Object arg) {
         if (observable instanceof EipStatus) {
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/PtClientBuilder.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/PtClientBuilder.java
new file mode 100644
index 0000000000000000000000000000000000000000..945e3d7aa38a10f39697abc405c565ed53e97998
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/PtClientBuilder.java
@@ -0,0 +1,18 @@
+package se.leap.bitmaskclient.pluggableTransports;
+
+import de.blinkt.openvpn.core.connection.Connection;
+import de.blinkt.openvpn.core.connection.Obfs4Connection;
+import de.blinkt.openvpn.core.connection.Obfs4HopConnection;
+
+public class PtClientBuilder {
+    public static PtClientInterface getPtClient(Connection connection) throws IllegalStateException {
+        switch (connection.getTransportType()) {
+            case OBFS4:
+                return new ObfsVpnClient(((Obfs4Connection) connection).getObfs4Options());
+            case OBFS4_HOP:
+                return new HoppingObfsVpnClient(((Obfs4HopConnection) connection).getObfs4Options());
+            default:
+                throw new IllegalStateException("Unexpected pluggable transport " + connection.getTransportType());
+        }
+    }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/PtClientInterface.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/PtClientInterface.java
new file mode 100644
index 0000000000000000000000000000000000000000..28d19a9746256fe29ce589992cbc4034a2d2e3b3
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/PtClientInterface.java
@@ -0,0 +1,9 @@
+package se.leap.bitmaskclient.pluggableTransports;
+
+import client.EventLogger;
+
+public interface PtClientInterface extends EventLogger {
+    int start();
+    void stop();
+    boolean isStarted();
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ShapeshifterClient.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ShapeshifterClient.java
index f1eb0f1bddc17b4a65550b1368f2cadae2c406cb..102dcf35e20a6203c841be99c90bd0db2a628cee 100644
--- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ShapeshifterClient.java
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ShapeshifterClient.java
@@ -42,6 +42,7 @@ public class ShapeshifterClient implements Observer {
     private int retry = 0;
     private final Handler reconnectHandler;
 
+    @Deprecated
     public class ShapeshifterLogger implements shapeshifter.Logger {
         @Override
         public void log(String s) {
@@ -71,8 +72,8 @@ public class ShapeshifterClient implements Observer {
 
     private void setup(Obfs4Options options) {
         shapeShifter.setSocksAddr(DISPATCHER_IP+":"+DISPATCHER_PORT);
-        shapeShifter.setTarget(options.remoteIP+":"+options.remotePort);
-        shapeShifter.setCert(options.cert);
+        shapeShifter.setTarget(options.gatewayIP +":"+options.transport.getPorts()[0]);
+        shapeShifter.setCert(options.transport.getOptions().getCert());
     }
 
     public void setOptions(Obfs4Options options) {
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java
index 1ae2a0337eb47b65584a73a6aa359d979b2c5787..775e174ad8b65c8e241c39fd65d83aeda9d4a316 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java
@@ -1,11 +1,28 @@
 package se.leap.bitmaskclient.providersetup;
 
+import static se.leap.bitmaskclient.base.models.Constants.EXT_JSON;
+import static se.leap.bitmaskclient.base.models.Constants.EXT_PEM;
+import static se.leap.bitmaskclient.base.models.Constants.URLS;
+import static se.leap.bitmaskclient.base.models.Provider.DOMAIN;
+import static se.leap.bitmaskclient.base.models.Provider.GEOIP_URL;
+import static se.leap.bitmaskclient.base.models.Provider.MAIN_URL;
+import static se.leap.bitmaskclient.base.models.Provider.MOTD_URL;
+import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_API_IP;
+import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_IP;
+import static se.leap.bitmaskclient.base.utils.FileHelper.createFile;
+import static se.leap.bitmaskclient.base.utils.FileHelper.persistFile;
+import static se.leap.bitmaskclient.base.utils.InputStreamHelper.getInputStreamFrom;
+import static se.leap.bitmaskclient.base.utils.InputStreamHelper.inputStreamToJson;
+import static se.leap.bitmaskclient.base.utils.InputStreamHelper.loadInputStreamAsString;
+
 import android.content.res.AssetManager;
 
 import androidx.annotation.VisibleForTesting;
 
 import com.pedrogomez.renderers.AdapteeCollection;
 
+import org.json.JSONObject;
+
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -19,20 +36,6 @@ import java.util.Set;
 
 import se.leap.bitmaskclient.base.models.Provider;
 
-import static se.leap.bitmaskclient.base.models.Constants.EXT_JSON;
-import static se.leap.bitmaskclient.base.models.Constants.EXT_PEM;
-import static se.leap.bitmaskclient.base.models.Constants.URLS;
-import static se.leap.bitmaskclient.base.models.Provider.GEOIP_URL;
-import static se.leap.bitmaskclient.base.models.Provider.MAIN_URL;
-import static se.leap.bitmaskclient.base.models.Provider.MOTD_URL;
-import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_API_IP;
-import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_IP;
-import static se.leap.bitmaskclient.base.utils.FileHelper.createFile;
-import static se.leap.bitmaskclient.base.utils.FileHelper.persistFile;
-import static se.leap.bitmaskclient.base.utils.InputStreamHelper.extractKeyFromInputStream;
-import static se.leap.bitmaskclient.base.utils.InputStreamHelper.getInputStreamFrom;
-import static se.leap.bitmaskclient.base.utils.InputStreamHelper.loadInputStreamAsString;
-
 /**
  * Created by parmegv on 4/12/14.
  */
@@ -96,11 +99,14 @@ public class ProviderManager implements AdapteeCollection<Provider> {
                 try {
                     String provider = file.substring(0, file.length() - ".url".length());
                     InputStream providerFile = assetsManager.open(directory + "/" + file);
-                    mainUrl = extractKeyFromInputStream(providerFile, MAIN_URL);
-                    providerIp = extractKeyFromInputStream(providerFile, PROVIDER_IP);
-                    providerApiIp = extractKeyFromInputStream(providerFile, PROVIDER_API_IP);
-                    geoipUrl =  extractKeyFromInputStream(providerFile, GEOIP_URL);
-                    motdUrl = extractKeyFromInputStream(providerFile, MOTD_URL);
+                    JSONObject providerConfig = inputStreamToJson(providerFile);
+                    if (providerConfig != null) {
+                        mainUrl = providerConfig.optString(MAIN_URL);
+                        providerIp = providerConfig.optString(PROVIDER_IP);
+                        providerApiIp = providerConfig.optString(PROVIDER_API_IP);
+                        geoipUrl =  providerConfig.optString(GEOIP_URL);
+                        motdUrl = providerConfig.optString(MOTD_URL);
+                    }
                     certificate = loadInputStreamAsString(assetsManager.open(provider + EXT_PEM));
                     providerDefinition = loadInputStreamAsString(assetsManager.open(provider + EXT_JSON));
                 } catch (IOException e) {
@@ -116,20 +122,20 @@ public class ProviderManager implements AdapteeCollection<Provider> {
     private void addCustomProviders(File externalFilesDir) {
         this.externalFilesDir = externalFilesDir;
         customProviders = externalFilesDir != null && externalFilesDir.isDirectory() ?
-                providersFromFiles(externalFilesDir.list()) :
+                customProvidersFromFiles(externalFilesDir.list()) :
                 new HashSet<>();
         customProviderURLs = getProviderUrlSetFromProviderSet(customProviders);
     }
 
-    private Set<Provider> providersFromFiles(String[] files) {
+    private Set<Provider> customProvidersFromFiles(String[] files) {
         Set<Provider> providers = new HashSet<>();
         try {
             for (String file : files) {
                 InputStream inputStream = getInputStreamFrom(externalFilesDir.getAbsolutePath() + "/" + file);
-                String mainUrl = extractKeyFromInputStream(inputStream, MAIN_URL);
-                String providerIp = extractKeyFromInputStream(inputStream, PROVIDER_IP);
-                String providerApiIp = extractKeyFromInputStream(inputStream, PROVIDER_API_IP);
-                providers.add(new Provider(mainUrl, providerIp, providerApiIp));
+                JSONObject providerConfig = inputStreamToJson(inputStream);
+                String mainUrl = providerConfig.optString(MAIN_URL);
+                String domain = providerConfig.optString(DOMAIN);
+                providers.add(Provider.createCustomProvider(mainUrl, domain));
             }
         } catch (FileNotFoundException | NullPointerException e) {
             e.printStackTrace();
@@ -238,7 +244,7 @@ public class ProviderManager implements AdapteeCollection<Provider> {
      */
     private void deleteLegacyCustomProviders() throws IOException, SecurityException {
         Set<Provider> persistedCustomProviders = externalFilesDir != null && externalFilesDir.isDirectory() ?
-                providersFromFiles(externalFilesDir.list()) : new HashSet<Provider>();
+                customProvidersFromFiles(externalFilesDir.list()) : new HashSet<Provider>();
             persistedCustomProviders.removeAll(customProviders);
         for (Provider providerToDelete : persistedCustomProviders) {
             File providerFile = createFile(externalFilesDir, providerToDelete.getName() + EXT_JSON);
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java
index 90ebfb4d9bf504faaf3df06e2ed4aebaf08aae5f..eb9898b84caec35e626df14ecd754ea92f286996 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java
@@ -114,7 +114,7 @@ public abstract class ProviderListBaseActivity extends ProviderSetupBaseActivity
     }
 
     public void showAndSelectProvider(String newURL) {
-        provider = new Provider(newURL, null, null);
+        provider = new Provider(newURL);
         autoSelectProvider();
     }
 
diff --git a/app/src/test/java/de/blinkt/openvpn/VpnProfileTest.java b/app/src/test/java/de/blinkt/openvpn/VpnProfileTest.java
index 9675c877966de7663936af6ee1e69fbdee56b1d8..a3ac3f9252f9f31432ae9c8ce84d1dd654c685f5 100644
--- a/app/src/test/java/de/blinkt/openvpn/VpnProfileTest.java
+++ b/app/src/test/java/de/blinkt/openvpn/VpnProfileTest.java
@@ -3,12 +3,12 @@ package de.blinkt.openvpn;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
 import static org.powermock.api.mockito.PowerMockito.mockStatic;
 import static org.powermock.api.mockito.PowerMockito.when;
 import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
-import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_KCP;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_HOP;
 import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN;
+import static se.leap.bitmaskclient.base.models.Constants.UDP;
 
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -18,10 +18,12 @@ import org.junit.runner.RunWith;
 import org.powermock.core.classloader.annotations.PrepareForTest;
 import org.powermock.modules.junit4.PowerMockRunner;
 
+import java.util.Arrays;
 import java.util.UUID;
 
 import de.blinkt.openvpn.core.connection.Obfs4Connection;
 import de.blinkt.openvpn.core.connection.OpenvpnConnection;
+import se.leap.bitmaskclient.base.models.Transport;
 import se.leap.bitmaskclient.base.utils.ConfigHelper;
 import se.leap.bitmaskclient.pluggableTransports.Obfs4Options;
 
@@ -29,10 +31,16 @@ import se.leap.bitmaskclient.pluggableTransports.Obfs4Options;
 @PrepareForTest({UUID.class, ConfigHelper.ObfsVpnHelper.class})
 public class VpnProfileTest {
 
-    private static final String OPENVPNCONNECTION_PROFILE   = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mUseObfs4\":false,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mUseObfs4Kcp\":false,\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mUseUdp\":false,\"mServerName\":\"openvpn.example.com\",\"mProxyType\":\"NONE\",\"mProxyPort\":\"8080\",\"mUseCustomConfig\":false,\"mConnectTimeout\":0,\"mProxyName\":\"proxy.example.com\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.OpenvpnConnection\",\"mServerPort\":\"1194\",\"mEnabled\":true}],\"mUseLzo\":false,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}";
-    private static final String OBFS4CONNECTION_PROFILE = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mUseObfs4\":true,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mUseObfs4Kcp\":false,\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"127.0.0.1\",\"mProxyType\":\"NONE\",\"mConnectTimeout\":0,\"mServerPort\":\"4430\",\"mUseUdp\":false,\"mProxyPort\":\"\",\"mUseCustomConfig\":false,\"options\":{\"udp\":false,\"remoteIP\":\"192.168.0.1\",\"iatMode\":\"1\",\"remotePort\":\"1234\",\"cert\":\"CERT\"},\"mProxyName\":\"\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}";
-    private static final String OBFS4CONNECTION_PROFILE_OBFSVPN = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mUseObfs4\":true,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mUseObfs4Kcp\":false,\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"192.168.0.1\",\"mProxyType\":\"SOCKS5\",\"mConnectTimeout\":0,\"mServerPort\":\"1234\",\"mUseUdp\":false,\"mProxyPort\":\"4430\",\"mUseCustomConfig\":false,\"options\":{\"udp\":false,\"remoteIP\":\"192.168.0.1\",\"iatMode\":\"1\",\"remotePort\":\"1234\",\"cert\":\"CERT\"},\"mProxyName\":\"127.0.0.1\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}";
-    private static final String OBFS4CONNECTION_PROFILE_OBFSVPN_KCP = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mUseObfs4\":false,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mUseObfs4Kcp\":true,\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"192.168.0.1\",\"mProxyType\":\"SOCKS5\",\"mConnectTimeout\":0,\"mServerPort\":\"1234\",\"mUseUdp\":false,\"mProxyPort\":\"4430\",\"mUseCustomConfig\":false,\"options\":{\"udp\":true,\"remoteIP\":\"192.168.0.1\",\"iatMode\":\"1\",\"remotePort\":\"1234\",\"cert\":\"CERT\"},\"mProxyName\":\"127.0.0.1\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}\n";
+    private static final String OPENVPNCONNECTION_PROFILE   = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mUseUdp\":false,\"mServerName\":\"openvpn.example.com\",\"mProxyType\":\"NONE\",\"mProxyPort\":\"8080\",\"mUseCustomConfig\":false,\"mConnectTimeout\":0,\"mProxyName\":\"proxy.example.com\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.OpenvpnConnection\",\"mServerPort\":\"1194\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":1,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}";
+    private static final String OBFS4CONNECTION_PROFILE = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"127.0.0.1\",\"mProxyType\":\"NONE\",\"mConnectTimeout\":0,\"mServerPort\":\"4430\",\"mUseUdp\":false,\"mProxyPort\":\"\",\"mUseCustomConfig\":false,\"options\":{\"gatewayIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"portCount\":0,\"iatMode\":\"0\",\"cert\":\"CERT\",\"experimental\":false,\"portSeed\":0},\"type\":\"obfs4\",\"protocols\":[\"tcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":2,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}";
+    private static final String OBFS4CONNECTION_PROFILE_OBFSVPN = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"192.168.0.1\",\"mProxyType\":\"SOCKS5\",\"mConnectTimeout\":0,\"mServerPort\":\"1234\",\"mUseUdp\":false,\"mProxyPort\":\"4430\",\"mUseCustomConfig\":false,\"options\":{\"gatewayIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"portCount\":0,\"iatMode\":\"1\",\"cert\":\"CERT\",\"experimental\":false,\"portSeed\":0},\"type\":\"obfs4\",\"protocols\":[\"tcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"127.0.0.1\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":2,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}";
+    private static final String OBFS4CONNECTION_PROFILE_OBFSVPN_KCP = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"192.168.0.1\",\"mProxyType\":\"SOCKS5\",\"mConnectTimeout\":0,\"mServerPort\":\"1234\",\"mUseUdp\":false,\"mProxyPort\":\"4430\",\"mUseCustomConfig\":false,\"options\":{\"gatewayIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"portCount\":0,\"iatMode\":\"1\",\"cert\":\"CERT\",\"experimental\":false,\"portSeed\":0},\"type\":\"obfs4\",\"protocols\":[\"kcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"127.0.0.1\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":2,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}\n";
+
+    private static final String OBFS4CONNECTION_PROFILE_OBFSVPN_HOP = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"192.168.0.1\",\"mProxyType\":\"SOCKS5\",\"mConnectTimeout\":0,\"mServerPort\":\"1234\",\"mUseUdp\":false,\"mProxyPort\":\"4430\",\"mUseCustomConfig\":false,\"options\":{\"gatewayIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"portCount\":100,\"endpoints\":[{\"ip\":\"1.1.1.1\",\"cert\":\"CERT1\"},{\"ip\":\"2.2.2.2\",\"cert\":\"CERT2\"}],\"iatMode\":\"1\",\"experimental\":true,\"portSeed\":200},\"type\":\"obfs4\",\"protocols\":[\"tcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"127.0.0.1\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":3,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}\n";
+
+    private static final String OBFS4CONNECTION_PROFILE_OBFSVPN_HOP_KCP = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"192.168.0.1\",\"mProxyType\":\"SOCKS5\",\"mConnectTimeout\":0,\"mServerPort\":\"1234\",\"mUseUdp\":false,\"mProxyPort\":\"4430\",\"mUseCustomConfig\":false,\"options\":{\"gatewayIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"portCount\":100,\"endpoints\":[{\"ip\":\"1.1.1.1\",\"cert\":\"CERT1\"},{\"ip\":\"2.2.2.2\",\"cert\":\"CERT2\"}],\"iatMode\":\"1\",\"experimental\":true,\"portSeed\":2500},\"type\":\"obfs4\",\"protocols\":[\"kcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"127.0.0.1\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":3,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}\n";
+
+    private static final String OBFS4CONNECTION_PROFILE_OBFSVPN_PORTHOPPING = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"192.168.0.1\",\"mProxyType\":\"SOCKS5\",\"mConnectTimeout\":0,\"mServerPort\":\"1234\",\"mUseUdp\":true,\"mProxyPort\":\"8080\",\"mUseCustomConfig\":false,\"options\":{\"gatewayIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"portCount\":100,\"iatMode\":\"1\",\"cert\":\"CERT\",\"experimental\":true,\"portSeed\":200},\"type\":\"obfs4-hop\",\"protocols\":[\"tcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"127.0.0.1\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":3,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}\n";
 
     @Before
     public void setup() {
@@ -72,7 +80,9 @@ public class VpnProfileTest {
         when(ConfigHelper.ObfsVpnHelper.useObfsVpn()).thenReturn(false);
 
         VpnProfile mockVpnProfile = new VpnProfile("mockProfile", OBFS4);
-        mockVpnProfile.mConnections[0] = new Obfs4Connection(new Obfs4Options("192.168.0.1", "1234", "CERT", "1", false));
+
+        Transport transport = new Transport(OBFS4.toString(), new String[]{"tcp"}, new String[]{"1234"}, "CERT");
+        mockVpnProfile.mConnections[0] = new Obfs4Connection(new Obfs4Options("192.168.0.1", transport));
         mockVpnProfile.mLastUsed = 0;
         String s = mockVpnProfile.toJson();
         System.out.println(s);
@@ -89,7 +99,9 @@ public class VpnProfileTest {
     public void toJson_obfs4_obfsvpn() throws JSONException {
         when(ConfigHelper.ObfsVpnHelper.useObfsVpn()).thenReturn(true);
         VpnProfile mockVpnProfile = new VpnProfile("mockProfile", OBFS4);
-        mockVpnProfile.mConnections[0] = new Obfs4Connection(new Obfs4Options("192.168.0.1", "1234", "CERT", "1", false));
+        Transport.Options options = new Transport.Options("CERT", "1");
+        Transport transport = new Transport(OBFS4.toString(), new String[]{"tcp"}, new String[]{"1234"}, options);
+        mockVpnProfile.mConnections[0] = new Obfs4Connection(new Obfs4Options("192.168.0.1", transport));
         mockVpnProfile.mLastUsed = 0;
         String s = mockVpnProfile.toJson();
         System.out.println(s);
@@ -106,8 +118,10 @@ public class VpnProfileTest {
     public void toJson_obfs4_obfsvpn_kcp() throws JSONException {
         when(ConfigHelper.ObfsVpnHelper.useObfsVpn()).thenReturn(true);
 
-        VpnProfile mockVpnProfile = new VpnProfile("mockProfile", OBFS4_KCP);
-        mockVpnProfile.mConnections[0] = new Obfs4Connection(new Obfs4Options("192.168.0.1", "1234", "CERT", "1", true));
+        VpnProfile mockVpnProfile = new VpnProfile("mockProfile", OBFS4);
+        Transport.Options options = new Transport.Options("CERT", "1");
+        Transport transport = new Transport(OBFS4.toString(), new String[]{"kcp"}, new String[]{"1234"}, options);
+        mockVpnProfile.mConnections[0] = new Obfs4Connection(new Obfs4Options("192.168.0.1", transport));
         mockVpnProfile.mLastUsed = 0;
         String s = mockVpnProfile.toJson();
         System.out.println(s);
@@ -120,6 +134,71 @@ public class VpnProfileTest {
         assertEquals(expectation.toString(),actual.toString());
     }
 
+    @Test
+    public void toJson_obfs4hop_kcp() throws JSONException {
+        when(ConfigHelper.ObfsVpnHelper.useObfsVpn()).thenReturn(true);
+
+        VpnProfile mockVpnProfile = new VpnProfile("mockProfile", OBFS4_HOP);
+
+        Transport.Options options = new Transport.Options("1", new Transport.Endpoint[]{new Transport.Endpoint("1.1.1.1", "CERT1"), new Transport.Endpoint("2.2.2.2", "CERT2")}, 2500, 100, true);
+        Transport transport = new Transport(OBFS4.toString(), new String[]{"kcp"}, new String[]{"1234"}, options);
+        mockVpnProfile.mConnections[0] = new Obfs4Connection(new Obfs4Options("192.168.0.1", transport));
+
+        mockVpnProfile.mLastUsed = 0;
+        String s = mockVpnProfile.toJson();
+        System.out.println(s);
+
+        //ignore UUID in comparison -> set it to fixed value
+        JSONObject actual = new JSONObject(s);
+        actual.put("mUuid", "9d295ca2-3789-48dd-996e-f731dbf50fdc");
+        JSONObject expectation = new JSONObject(OBFS4CONNECTION_PROFILE_OBFSVPN_HOP_KCP);
+
+        assertEquals(expectation.toString(),actual.toString());
+    }
+
+    @Test
+    public void toJson_obfs4hop_portHopping() throws JSONException {
+        when(ConfigHelper.ObfsVpnHelper.useObfsVpn()).thenReturn(true);
+
+        VpnProfile mockVpnProfile = new VpnProfile("mockProfile", OBFS4_HOP);
+
+        Transport.Options options = new Transport.Options("1", null, "CERT",200, 100, true);
+        Transport transport = new Transport(OBFS4_HOP.toString(), new String[]{"tcp"}, new String[]{"1234"}, options);
+        mockVpnProfile.mConnections[0] = new Obfs4Connection(new Obfs4Options("192.168.0.1", transport));
+
+        mockVpnProfile.mLastUsed = 0;
+        String s = mockVpnProfile.toJson();
+        System.out.println(s);
+
+        //ignore UUID in comparison -> set it to fixed value
+        JSONObject actual = new JSONObject(s);
+        actual.put("mUuid", "9d295ca2-3789-48dd-996e-f731dbf50fdc");
+        JSONObject expectation = new JSONObject(OBFS4CONNECTION_PROFILE_OBFSVPN_PORTHOPPING);
+
+        assertEquals(expectation.toString(),actual.toString());
+    }
+
+    @Test
+    public void toJson_obfs4hop() throws JSONException {
+        when(ConfigHelper.ObfsVpnHelper.useObfsVpn()).thenReturn(true);
+
+        VpnProfile mockVpnProfile = new VpnProfile("mockProfile", OBFS4_HOP);
+        Transport.Options options = new Transport.Options("1", new Transport.Endpoint[]{new Transport.Endpoint("1.1.1.1", "CERT1"), new Transport.Endpoint("2.2.2.2", "CERT2")}, 200, 100, true);
+        Transport transport = new Transport(OBFS4.toString(), new String[]{"tcp"}, new String[]{"1234"}, options);
+        mockVpnProfile.mConnections[0] = new Obfs4Connection(new Obfs4Options("192.168.0.1", transport));
+
+        mockVpnProfile.mLastUsed = 0;
+        String s = mockVpnProfile.toJson();
+        System.out.println(s);
+
+        //ignore UUID in comparison -> set it to fixed value
+        JSONObject actual = new JSONObject(s);
+        actual.put("mUuid", "9d295ca2-3789-48dd-996e-f731dbf50fdc");
+        JSONObject expectation = new JSONObject(OBFS4CONNECTION_PROFILE_OBFSVPN_HOP);
+
+        assertEquals(expectation.toString(),actual.toString());
+    }
+
     @Test
     public void fromJson_obfs4() {
         when(ConfigHelper.ObfsVpnHelper.useObfsVpn()).thenReturn(false);
@@ -131,11 +210,12 @@ public class VpnProfileTest {
         assertFalse(mockVpnProfile.mConnections[0].isUseUdp());
         Obfs4Connection obfs4Connection = (Obfs4Connection) mockVpnProfile.mConnections[0];
         assertEquals(OBFS4, obfs4Connection.getTransportType());
-        assertFalse(obfs4Connection.getDispatcherOptions().udp);
-        assertEquals("CERT", obfs4Connection.getDispatcherOptions().cert);
-        assertEquals("1", obfs4Connection.getDispatcherOptions().iatMode);
-        assertEquals("192.168.0.1", obfs4Connection.getDispatcherOptions().remoteIP);
-        assertEquals("1234", obfs4Connection.getDispatcherOptions().remotePort);
+        assertFalse(Arrays.asList(obfs4Connection.getObfs4Options().transport.getProtocols()).contains(UDP));
+        assertEquals("CERT", obfs4Connection.getObfs4Options().transport.getOptions().getCert());
+        assertEquals("0", obfs4Connection.getObfs4Options().transport.getOptions().getIatMode());
+        assertEquals("192.168.0.1", obfs4Connection.getObfs4Options().gatewayIP);
+        assertEquals("1234", obfs4Connection.getObfs4Options().transport.getPorts()[0]);
+        assertEquals(1, obfs4Connection.getObfs4Options().transport.getPorts().length);
     }
 
     @Test
@@ -149,11 +229,12 @@ public class VpnProfileTest {
         assertFalse(mockVpnProfile.mConnections[0].isUseUdp());
         Obfs4Connection obfs4Connection = (Obfs4Connection) mockVpnProfile.mConnections[0];
         assertEquals(OBFS4, obfs4Connection.getTransportType());
-        assertFalse(obfs4Connection.getDispatcherOptions().udp);
-        assertEquals("CERT", obfs4Connection.getDispatcherOptions().cert);
-        assertEquals("1", obfs4Connection.getDispatcherOptions().iatMode);
-        assertEquals("192.168.0.1", obfs4Connection.getDispatcherOptions().remoteIP);
-        assertEquals("1234", obfs4Connection.getDispatcherOptions().remotePort);
+        assertEquals("tcp", obfs4Connection.getObfs4Options().transport.getProtocols()[0]);
+        assertEquals("CERT", obfs4Connection.getObfs4Options().transport.getOptions().getCert());
+        assertEquals("1", obfs4Connection.getObfs4Options().transport.getOptions().getIatMode());
+        assertEquals("192.168.0.1", obfs4Connection.getObfs4Options().gatewayIP);
+        assertEquals("1234", obfs4Connection.getObfs4Options().transport.getPorts()[0]);
+        assertEquals(1, obfs4Connection.getObfs4Options().transport.getPorts().length);
     }
 
     @Test
@@ -167,10 +248,10 @@ public class VpnProfileTest {
         assertFalse(mockVpnProfile.mConnections[0].isUseUdp());
         Obfs4Connection obfs4Connection = (Obfs4Connection) mockVpnProfile.mConnections[0];
         assertEquals(OBFS4, obfs4Connection.getTransportType());
-        assertTrue(obfs4Connection.getDispatcherOptions().udp);
-        assertEquals("CERT", obfs4Connection.getDispatcherOptions().cert);
-        assertEquals("1", obfs4Connection.getDispatcherOptions().iatMode);
-        assertEquals("192.168.0.1", obfs4Connection.getDispatcherOptions().remoteIP);
-        assertEquals("1234", obfs4Connection.getDispatcherOptions().remotePort);
+        assertEquals("kcp", obfs4Connection.getObfs4Options().transport.getProtocols()[0]);
+        assertEquals("CERT", obfs4Connection.getObfs4Options().transport.getOptions().getCert());
+        assertEquals("1", obfs4Connection.getObfs4Options().transport.getOptions().getIatMode());
+        assertEquals("192.168.0.1", obfs4Connection.getObfs4Options().gatewayIP);
+        assertEquals("1234", obfs4Connection.getObfs4Options().transport.getPorts()[0]);
     }
 }
\ No newline at end of file
diff --git a/app/src/test/java/de/blinkt/openvpn/core/connection/ConnectionTest.java b/app/src/test/java/de/blinkt/openvpn/core/connection/ConnectionTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..6313bc82cff63f2c0c01ef7190995679f961357c
--- /dev/null
+++ b/app/src/test/java/de/blinkt/openvpn/core/connection/ConnectionTest.java
@@ -0,0 +1,33 @@
+package de.blinkt.openvpn.core.connection;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_HOP;
+
+import org.junit.Test;
+
+public class ConnectionTest {
+
+    @Test
+    public void TransportTypeTest_fromString() {
+        Connection.TransportType transportType = Connection.TransportType.fromString("obfs4-hop");
+        assertEquals(OBFS4_HOP, transportType);
+    }
+
+    @Test
+    public void TransportTypeTest_toString() {
+        assertEquals("obfs4-hop", OBFS4_HOP.toString());
+    }
+
+    @Test
+    public void TransportTypeTest_valueOf() {
+        Exception exception = null;
+        try {
+            Connection.TransportType.valueOf("obfs4-hop");
+        } catch (IllegalArgumentException e) {
+            exception = e;
+        }
+        assertNotNull(exception);
+    }
+
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/base/models/GatewayJsonTest.java b/app/src/test/java/se/leap/bitmaskclient/base/models/GatewayJsonTest.java
index 612753781d5e17048579d0ad269a64e5db7bf9c3..801f98ad8198894f986f284c634dd532d65ca23a 100644
--- a/app/src/test/java/se/leap/bitmaskclient/base/models/GatewayJsonTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/base/models/GatewayJsonTest.java
@@ -12,7 +12,7 @@ public class GatewayJsonTest {
 
     @Test
     public void testToString() {
-        String gatewayJSON = "{\"location\":\"Unknown Location\",\"ip_address\":\"1.2.3.4\",\"host\":\"pinned.obfuscation.proxy\",\"capabilities\":{\"adblock\":false,\"filter_dns\":false,\"limited\":false,\"transport\":[{\"type\":\"obfs4\",\"protocols\":[\"tcp\"],\"ports\":[\"1194\"],\"options\":{\"cert\":\"xxxxxxx\",\"iatMode\":\"0\"}}],\"user_ips\":false}}";
+        String gatewayJSON = "{\"location\":\"Unknown Location\",\"ip_address\":\"1.2.3.4\",\"host\":\"pinned.obfuscation.proxy\",\"capabilities\":{\"adblock\":false,\"filter_dns\":false,\"limited\":false,\"transport\":[{\"type\":\"obfs4\",\"protocols\":[\"tcp\"],\"ports\":[\"1194\"],\"options\":{\"cert\":\"xxxxxxx\",\"iatMode\":\"0\",\"experimental\":false,\"portSeed\":0,\"portCount\":0}}],\"user_ips\":false}}";
 
         Connection.TransportType transportType = OBFS4;
         Transport[] transports = new Transport[]{
diff --git a/app/src/test/java/se/leap/bitmaskclient/base/models/TransportTest.java b/app/src/test/java/se/leap/bitmaskclient/base/models/TransportTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..37dfc16130dbe6f357fbb850b182684c8a829a2d
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/base/models/TransportTest.java
@@ -0,0 +1,68 @@
+package se.leap.bitmaskclient.base.models;
+
+import static se.leap.bitmaskclient.base.models.Constants.CAPABILITIES;
+import static se.leap.bitmaskclient.base.models.Constants.TRANSPORT;
+
+import junit.framework.TestCase;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+
+import de.blinkt.openvpn.core.connection.Connection;
+import se.leap.bitmaskclient.testutils.TestSetupHelper;
+
+public class TransportTest extends TestCase {
+
+    private JSONObject gateway;
+
+    public void test_obfs4_fromJson() throws IOException, JSONException {
+        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_obfs4hop_tcp_gateways.json"))).getJSONArray("gateways").getJSONObject(2);
+        JSONObject obfs4Transport = gateway.getJSONObject(CAPABILITIES).getJSONArray(TRANSPORT).getJSONObject(1);
+        Transport transport = Transport.fromJson(obfs4Transport);
+        assertEquals("obfs4", transport.getType());
+        assertEquals("0", transport.getOptions().getIatMode());
+        assertEquals("kcp", transport.getProtocols()[0]);
+        assertEquals(1, transport.getProtocols().length);
+        assertEquals("23050", transport.getPorts()[0]);
+        assertEquals(1, transport.getPorts().length);
+        assertEquals("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1", transport.getOptions().getCert());
+        assertNull(transport.getOptions().getEndpoints());
+        assertEquals(0, transport.getOptions().getPortCount());
+        assertEquals(0, transport.getOptions().getPortSeed());
+        assertFalse(transport.getOptions().isExperimental());
+    }
+
+    public void test_obfs4hop_fromJson() throws IOException, JSONException {
+        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_obfs4hop_tcp_gateways.json"))).getJSONArray("gateways").getJSONObject(2);
+        JSONObject obfs4Transport = gateway.getJSONObject(CAPABILITIES).getJSONArray(TRANSPORT).getJSONObject(2);
+        Transport transport = Transport.fromJson(obfs4Transport);
+        assertEquals("obfs4-hop", transport.getType());
+        assertEquals(Connection.TransportType.OBFS4_HOP, transport.getTransportType());
+        assertEquals("tcp", transport.getProtocols()[0]);
+        assertEquals(1, transport.getProtocols().length);
+        assertNull(transport.getPorts());
+        assertNull(transport.getOptions().getCert());
+        assertNotNull(transport.getOptions().getEndpoints());
+        assertEquals(2, transport.getOptions().getEndpoints().length);
+        assertEquals("CERT1", transport.getOptions().getEndpoints()[0].getCert());
+        assertEquals("CERT2", transport.getOptions().getEndpoints()[1].getCert());
+        assertEquals("1.1.1.1", transport.getOptions().getEndpoints()[0].getIp());
+        assertEquals("2.2.2.2", transport.getOptions().getEndpoints()[1].getIp());
+        assertTrue(transport.getOptions().isExperimental());
+    }
+
+    public void test_openvpn_fromJson() throws IOException, JSONException {
+        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_obfs4hop_tcp_gateways.json"))).getJSONArray("gateways").getJSONObject(2);
+        JSONObject obfs4Transport = gateway.getJSONObject(CAPABILITIES).getJSONArray(TRANSPORT).getJSONObject(0);
+        Transport transport = Transport.fromJson(obfs4Transport);
+        assertEquals("openvpn", transport.getType());
+        assertEquals(2, transport.getProtocols().length);
+        assertEquals("tcp", transport.getProtocols()[0]);
+        assertEquals("udp", transport.getProtocols()[1]);
+        assertEquals(1, transport.getPorts().length);
+        assertEquals("1195", transport.getPorts()[0]);
+        assertNull(transport.getOptions());
+    }
+}
\ No newline at end of file
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
index 3b0d5552727705cfdb10db2c9de5a1ba9238fd82..ee6ccce5ce8f8cac52db01e104dccd06c64c19d0 100644
--- a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
@@ -16,6 +16,7 @@ import org.powermock.core.classloader.annotations.PrepareForTest;
 import org.powermock.modules.junit4.PowerMockRunner;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 
 import de.blinkt.openvpn.VpnProfile;
@@ -32,7 +33,6 @@ import se.leap.bitmaskclient.testutils.MockSharedPreferences;
 import se.leap.bitmaskclient.testutils.TestSetupHelper;
 
 import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
-import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_KCP;
 import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN;
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertNull;
@@ -93,14 +93,6 @@ public class GatewaysManagerTest {
         assertEquals(0, gatewaysManager.size());
     }
 
-    @Test
-    public void testGatewayManagerFromCurrentProvider_misconfiguredProvider_noGateways() throws IOException, NullPointerException {
-        Provider provider = getProvider(null, null, null, null, null, null, "ptdemo_misconfigured_gateway.json", null);
-        MockHelper.mockProviderObservable(provider);
-        GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
-        assertEquals(0, gatewaysManager.size());
-    }
-
     @Test
     public void testGatewayManagerFromCurrentProvider_threeGateways() {
         Provider provider = getProvider(null, null, null, null,null, null, "ptdemo_three_mixed_gateways.json", null);
@@ -220,7 +212,89 @@ public class GatewaysManagerTest {
         when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(true);
         GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
 
-        assertEquals("37.12.247.10", gatewaysManager.select(0).first.getRemoteIP());
+        assertEquals("37.12.247.10", gatewaysManager.select(0).gateway.getRemoteIP());
+    }
+
+    @Test
+    public void TestSelectN_select_includeExperimentalTransport_DecoupledPortHoppingGW() {
+        Provider provider = getProvider(null, null, null, null, null, null, "decoupled_pt_portHopping.eip-service.json", null);
+
+        MockHelper.mockProviderObservable(provider);
+        mockStatic(PreferenceHelper.class);
+        when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(true);
+        when(PreferenceHelper.allowExperimentalTransports(any(Context.class))).thenReturn(true);
+        GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
+        ArrayList<String> hosts = new ArrayList<>();
+        hosts.add(gatewaysManager.select(0).gateway.getHost());
+        hosts.add(gatewaysManager.select(1).gateway.getHost());
+
+        assertTrue(hosts.contains("bridge-nyc1-02.bitmask-dev.leapvpn.net"));
+        assertTrue(hosts.contains("bridge-nyc1-01.bitmask-dev.leapvpn.net"));
+
+    }
+
+    @Test
+    public void TestSelectN_select_includeExperimentalTransport_DecoupledPortAndIPHoppingGW() {
+        Provider provider = getProvider(null, null, null, null, null, null, "decoupled_pt.eip-service.json", null);
+
+        MockHelper.mockProviderObservable(provider);
+        mockStatic(PreferenceHelper.class);
+        when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(true);
+        when(PreferenceHelper.allowExperimentalTransports(any(Context.class))).thenReturn(true);
+        GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
+
+        ArrayList<String> hosts = new ArrayList<>();
+        hosts.add(gatewaysManager.select(0).gateway.getHost());
+        hosts.add(gatewaysManager.select(1).gateway.getHost());
+        assertTrue(hosts.contains("bridge-nyc1-02.bitmask-dev.leapvpn.net"));
+        assertTrue(hosts.contains("bridge-nyc1-01.bitmask-dev.leapvpn.net"));
+    }
+
+    @Test
+    public void TestSelectN_select_excludeExperimentalTransport_DecoupledPortHoppingGW() {
+        Provider provider = getProvider(null, null, null, null, null, null, "decoupled_pt_portHopping.eip-service.json", null);
+
+        MockHelper.mockProviderObservable(provider);
+        mockStatic(PreferenceHelper.class);
+        when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(true);
+        when(PreferenceHelper.allowExperimentalTransports(any(Context.class))).thenReturn(false);
+
+        for (int i = 0; i < 1000; i++) {
+            GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
+            assertEquals("bridge-nyc1-01.bitmask-dev.leapvpn.net", gatewaysManager.select(0).gateway.getHost());
+        }
+    }
+
+    @Test
+    public void TestSelectN_select_excludeExperimentalTransport_DecoupledPortAndIPHoppingGW() {
+        Provider provider = getProvider(null, null, null, null, null, null, "decoupled_pt.eip-service.json", null);
+
+        MockHelper.mockProviderObservable(provider);
+        mockStatic(PreferenceHelper.class);
+        when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(true);
+        when(PreferenceHelper.allowExperimentalTransports(any(Context.class))).thenReturn(false);
+        GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
+
+        assertEquals("bridge-nyc1-01.bitmask-dev.leapvpn.net", gatewaysManager.select(0).gateway.getHost());
+        assertNull(gatewaysManager.select(1));
+    }
+
+    @Test
+    public void TestSelectN_select_excludeExperimentalTransport_InGatewayHoppingPTBridge() {
+        Provider provider = getProvider(null, null, null, null, null, null, "ptdemo_obfs4hop_tcp_gateways.json", null);
+
+        MockHelper.mockProviderObservable(provider);
+        mockStatic(PreferenceHelper.class);
+        when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(true);
+        when(PreferenceHelper.allowExperimentalTransports(any(Context.class))).thenReturn(false);
+        GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
+
+        ArrayList<String> hosts = new ArrayList<>();
+        hosts.add(gatewaysManager.select(0).gateway.getHost());
+        hosts.add(gatewaysManager.select(1).gateway.getHost());
+
+        assertTrue(hosts.contains("pt.demo.bitmask.net"));
+        assertTrue(hosts.contains("manila.bitmask.net"));
     }
 
     @Test
@@ -233,9 +307,9 @@ public class GatewaysManagerTest {
         when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(false);
         GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
 
-        assertEquals("manila.bitmask.net", gatewaysManager.select(0).first.getHost());
-        assertEquals("moscow.bitmask.net", gatewaysManager.select(1).first.getHost());
-        assertEquals("pt.demo.bitmask.net", gatewaysManager.select(2).first.getHost());
+        assertEquals("manila.bitmask.net", gatewaysManager.select(0).gateway.getHost());
+        assertEquals("moscow.bitmask.net", gatewaysManager.select(1).gateway.getHost());
+        assertEquals("pt.demo.bitmask.net", gatewaysManager.select(2).gateway.getHost());
     }
 
     @Test
@@ -248,8 +322,8 @@ public class GatewaysManagerTest {
         when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(true);
         GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
 
-        assertEquals("moscow.bitmask.net", gatewaysManager.select(0).first.getHost());
-        assertEquals("pt.demo.bitmask.net", gatewaysManager.select(1).first.getHost());
+        assertEquals("moscow.bitmask.net", gatewaysManager.select(0).gateway.getHost());
+        assertEquals("pt.demo.bitmask.net", gatewaysManager.select(1).gateway.getHost());
         assertNull(gatewaysManager.select(2));
     }
 
@@ -265,9 +339,9 @@ public class GatewaysManagerTest {
         when(PreferenceHelper.getPreferredCity(any(Context.class))).thenReturn("Paris");
         GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
 
-        assertEquals("mouette.riseup.net", gatewaysManager.select(0).first.getHost());
-        assertEquals("hoatzin.riseup.net", gatewaysManager.select(1).first.getHost());
-        assertEquals("zarapito.riseup.net", gatewaysManager.select(2).first.getHost());
+        assertEquals("mouette.riseup.net", gatewaysManager.select(0).gateway.getHost());
+        assertEquals("hoatzin.riseup.net", gatewaysManager.select(1).gateway.getHost());
+        assertEquals("zarapito.riseup.net", gatewaysManager.select(2).gateway.getHost());
     }
 
     @Test
@@ -281,9 +355,9 @@ public class GatewaysManagerTest {
         when(PreferenceHelper.getPreferredCity(any(Context.class))).thenReturn("Paris");
         GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
 
-        assertEquals("mouette.riseup.net", gatewaysManager.select(0).first.getHost());
-        assertEquals("hoatzin.riseup.net", gatewaysManager.select(1).first.getHost());
-        assertEquals("zarapito.riseup.net", gatewaysManager.select(2).first.getHost());
+        assertEquals("mouette.riseup.net", gatewaysManager.select(0).gateway.getHost());
+        assertEquals("hoatzin.riseup.net", gatewaysManager.select(1).gateway.getHost());
+        assertEquals("zarapito.riseup.net", gatewaysManager.select(2).gateway.getHost());
     }
 
     @Test
@@ -298,9 +372,9 @@ public class GatewaysManagerTest {
         when(PreferenceHelper.getPreferredCity(any(Context.class))).thenReturn("Paris");
         GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
 
-        assertEquals("Paris", gatewaysManager.select(0).first.getName());
-        assertEquals("Paris", gatewaysManager.select(1).first.getName());
-        assertEquals("Paris", gatewaysManager.select(2).first.getName());
+        assertEquals("Paris", gatewaysManager.select(0).gateway.getName());
+        assertEquals("Paris", gatewaysManager.select(1).gateway.getName());
+        assertEquals("Paris", gatewaysManager.select(2).gateway.getName());
         assertEquals(null, gatewaysManager.select(3));
     }
 
@@ -314,9 +388,9 @@ public class GatewaysManagerTest {
         when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(false);
         GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
 
-        assertEquals("mouette.riseup.net", gatewaysManager.select(0, "Paris").first.getHost());
-        assertEquals("hoatzin.riseup.net", gatewaysManager.select(1, "Paris").first.getHost());
-        assertEquals("zarapito.riseup.net", gatewaysManager.select(2, "Paris").first.getHost());
+        assertEquals("mouette.riseup.net", gatewaysManager.select(0, "Paris").gateway.getHost());
+        assertEquals("hoatzin.riseup.net", gatewaysManager.select(1, "Paris").gateway.getHost());
+        assertEquals("zarapito.riseup.net", gatewaysManager.select(2, "Paris").gateway.getHost());
     }
 
     @Test
@@ -329,9 +403,9 @@ public class GatewaysManagerTest {
         when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(false);
         GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
 
-        assertEquals("mouette.riseup.net", gatewaysManager.select(0, "Paris").first.getHost());
-        assertEquals("hoatzin.riseup.net", gatewaysManager.select(1, "Paris").first.getHost());
-        assertEquals("zarapito.riseup.net", gatewaysManager.select(2, "Paris").first.getHost());
+        assertEquals("mouette.riseup.net", gatewaysManager.select(0, "Paris").gateway.getHost());
+        assertEquals("hoatzin.riseup.net", gatewaysManager.select(1, "Paris").gateway.getHost());
+        assertEquals("zarapito.riseup.net", gatewaysManager.select(2, "Paris").gateway.getHost());
     }
 
     @Test
@@ -345,9 +419,9 @@ public class GatewaysManagerTest {
         when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(false);
         GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
 
-        assertEquals("Paris", gatewaysManager.select(0, "Paris").first.getName());
-        assertEquals("Paris", gatewaysManager.select(1, "Paris").first.getName());
-        assertEquals("Paris", gatewaysManager.select(2, "Paris").first.getName());
+        assertEquals("Paris", gatewaysManager.select(0, "Paris").gateway.getName());
+        assertEquals("Paris", gatewaysManager.select(1, "Paris").gateway.getName());
+        assertEquals("Paris", gatewaysManager.select(2, "Paris").gateway.getName());
         assertEquals(null, gatewaysManager.select(3, "Paris"));
     }
 
@@ -534,7 +608,7 @@ public class GatewaysManagerTest {
         mockStatic(PreferenceHelper.class);
         when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(false);
         GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
-        List<Location> locations = gatewaysManager.getSortedGatewayLocations(OBFS4_KCP);
+        List<Location> locations = gatewaysManager.getSortedGatewayLocations(OBFS4);
 
         assertEquals(3, locations.size());
     }
@@ -548,13 +622,10 @@ public class GatewaysManagerTest {
         when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(false);
         GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
 
-        assertEquals(0.3, gatewaysManager.getLocation("Amsterdam").getAverageLoad(OBFS4_KCP));
         assertEquals(0.3, gatewaysManager.getLocation("Amsterdam").getAverageLoad(OBFS4));
         assertEquals(0.3, gatewaysManager.getLocation("Amsterdam").getAverageLoad(OPENVPN));
     }
 
-
-
     @Test
     public void testGetLoadForLocation_() {
         MockHelper.mockProviderObservable(null);
@@ -562,6 +633,24 @@ public class GatewaysManagerTest {
         assertEquals(GatewaysManager.Load.UNKNOWN, gatewaysManager.getLoadForLocation("unknown city", OPENVPN));
     }
 
+    @Test
+    public void testGatewayManagerFromCurrentProvider_decoupledBridges_twoGateways() throws IOException, NullPointerException {
+        Provider provider = getProvider(null, null, null, null, null, null, "decoupled_pt.eip-service.json", null);
+        MockHelper.mockProviderObservable(provider);
+        GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
+        assertEquals(2, gatewaysManager.size());
+    }
+
+    @Test
+    public void testGatewayManagerFromCurrentProvider_decoupledBridgesIncludingExperimental_threeGateways() throws IOException, NullPointerException {
+        Provider provider = getProvider(null, null, null, null, null, null, "decoupled_pt.eip-service.json", null);
+        MockHelper.mockProviderObservable(provider);
+        mockStatic(PreferenceHelper.class);
+        when(PreferenceHelper.allowExperimentalTransports(any(Context.class))).thenReturn(true);
+        GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
+        assertEquals(3, gatewaysManager.size());
+    }
+
     private String getJsonStringFor(String filename) throws IOException {
         return TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream(filename));
     }
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java
index 4fb178fce1aabc48110603aa0d4842b0355de750..932310559b47f88c8b26035725f1fba6c8f79b3c 100644
--- a/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java
@@ -2,6 +2,7 @@ package se.leap.bitmaskclient.eip;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
@@ -9,7 +10,7 @@ import static org.mockito.Mockito.mock;
 import static org.powermock.api.mockito.PowerMockito.mockStatic;
 import static org.powermock.api.mockito.PowerMockito.when;
 import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
-import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_KCP;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_HOP;
 import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN;
 import static se.leap.bitmaskclient.base.models.Constants.OPENVPN_CONFIGURATION;
 import static se.leap.bitmaskclient.testutils.MockHelper.mockTextUtils;
@@ -33,7 +34,10 @@ import java.io.File;
 import java.util.HashMap;
 
 import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ConfigParser;
 import de.blinkt.openvpn.core.connection.Connection;
+import de.blinkt.openvpn.core.connection.Obfs4Connection;
+import de.blinkt.openvpn.core.connection.Obfs4HopConnection;
 import se.leap.bitmaskclient.base.utils.ConfigHelper;
 import se.leap.bitmaskclient.testutils.MockHelper;
 import se.leap.bitmaskclient.testutils.TestSetupHelper;
@@ -1418,6 +1422,266 @@ public class VpnConfigGeneratorTest {
             "rcvbuf 0 \n" +
             "tls-version-min 1.2 \n";
 
+    String expectedVPNConfig_hopping_pt_portHopping = "# Config for OpenVPN 2.x\n" +
+            "# Enables connection to GUI\n" +
+            "management /data/data/se.leap.bitmask/mgmtsocket unix\n" +
+            "management-client\n" +
+            "management-query-passwords\n" +
+            "management-hold\n" +
+            "\n" +
+            "setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
+            "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+            "machine-readable-output\n" +
+            "allow-recursive-routing\n" +
+            "ifconfig-nowarn\n" +
+            "client\n" +
+            "verb 4\n" +
+            "connect-retry 2 300\n" +
+            "resolv-retry 60\n" +
+            "dev tun\n" +
+            "remote 127.0.0.1 8080 udp\n" +
+            "<ca>\n" +
+            "-----BEGIN CERTIFICATE-----\n" +
+            "MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt\n" +
+            "YXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v\n" +
+            "Yml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw\n" +
+            "FgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV\n" +
+            "BAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw\n" +
+            "ggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai\n" +
+            "dHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB\n" +
+            "7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r/bEHUSevmR09BRp86syHZerdNGpXYhcQ84\n" +
+            "CA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+\n" +
+            "znCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe/RGk4\n" +
+            "MEqGFuOzrtsgEhPIX0hplhb0Tgz/rtug+yTT7oJjBa3u20AAOQ38/M99EfdeJvc4\n" +
+            "lPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu/qt3DfvLfhboq+0\n" +
+            "bQvLUPXrVDr70onv5UDjpmEA/cLmaIqqrduuTkFZOym65/PfAPvpGnt7crQj/Ibl\n" +
+            "DEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB\n" +
+            "lfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy\n" +
+            "YMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw\n" +
+            "XjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH/BAQDAgIE\n" +
+            "MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w\n" +
+            "DQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl\n" +
+            "cXYyB6hY5hv/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY\n" +
+            "k/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj\n" +
+            "RnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG\n" +
+            "htD/JKwt6xBmNwktH0GI/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX\n" +
+            "EIQ0ZR56bFuJA/CwValBqV/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J\n" +
+            "aF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l\n" +
+            "mlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK\n" +
+            "G6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co\n" +
+            "Ja8zlx64jmMZPg/t3wWqkZgXZ14qnbyG5/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d\n" +
+            "69db12/g4f6phldhxiWuGC/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e\n" +
+            "yV8e\n" +
+            "-----END CERTIFICATE-----\n" +
+            "\n" +
+            "</ca>\n" +
+            "<key>\n" +
+            "-----BEGIN RSA PRIVATE KEY-----\n" +
+            "MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDUTYWeGgsHS+fjijmziniNqw6h\n" +
+            "MBpyK4S/cM6PxV28C33VuOWPTMcIYesctjZANWFCggfFTQSjV5Qaxq9UK4i27tayLbCdlVS6hpbl\n" +
+            "Vf4DuI3Gj1Pv1rtITBShtvCf3T7yBnjW4wVpOpsUAAOViKUSvUU3kPPMFWhiGQw8yHYr82ts6XMo\n" +
+            "jwMoonW5Ml4e7C7Cr22QesC63q7emNcpUd0pZGT9C33RgDAHZDMrlyjo4HEp1JbUfB0gbmXElJbE\n" +
+            "1TNdZ62HhgmMjzTUN1GGrQ1t91AEoEQwaK65o4YSj+yFv6KXZZz5OWaz94tKiN9v26EXtBFmRlyb\n" +
+            "6+D9ynSd9LghAgMBAAECggEBANPHLRXkhsHVj1EkzqBx7gXr8CEMmiTvknFh9zvltrZhhDoRQjWr\n" +
+            "chPDkcRHY2Cznvy4N0YyqQDD2ULIlZdSAgPxxothFoBruWSD47yMBmLx08ORsDpcqt/YvPAATJI8\n" +
+            "IpFNsXcyaXBp/M57oRemgnxp/8UJPJmFdWX99H4hvffh/jdj7POgYiWUaAl37XTYZKZ4nzKU2wpL\n" +
+            "EDLj9RKPz9gG7CYp2zrLC9LaAsrXVrKwPBw6g+XwbClaqFj97db3mrY4lr6mTo89qmus1AU+fBDH\n" +
+            "3Xlpmc8JwB+30TvhRNKrpLx9cEjuEj7K1gm8Y4dWCjPi+lNbtAyUBcgPJFa/81ECgYEA7pLoBU/Y\n" +
+            "ZYjyHFca8FvDBcBh6haHfqJr9doXWtgjDrbi3o2n5wHqfKhFWOH6vPEQozkOVeX1ze6HOiRmGBpW\n" +
+            "r+r7x8TD25L7I6HJw3M351RWOAfkF0w/RTVdetcTgduQtfN1u6BDhYSVceXMjyQYx7MhfETWI8Gh\n" +
+            "KSYm8OEDYiUCgYEA489fmbrCcUnXzpTsbswJ5NmSoEXbcX8cLxnQuzE0z9GHhQdrMjOpXR76reTW\n" +
+            "6jcuudarNcwRUYSWWhjCDKHhpx4HhasWPaHgr7jIzcRw8yZSJRSxKr8sl1qh6g7s47JcmfXOMWLt\n" +
+            "yuyE933XrT19Th4ODZHY40Uv35mPjMi9d00CgYEAyRNAQtndBRa7GG/B4Ls2T+6pl+aNJIo4e+no\n" +
+            "rURlp800wWabEPRocdBRQmyULBLxduBr2LIMzhgwGSz8b2wji/l9ZA3PFY135bxClVzSzUIjuO3N\n" +
+            "rGUzHl2wAAyuAFDSUshzfkPBJRNt8aVBF5PQ3t93ZYmPAmv8LPZe875yX5ECgYEAsUEcwK/ZNW7g\n" +
+            "dQPZR4iJNkC4Xu6cBZ6Cnn92swBheEYvLSoNlX0vDZ7aLE3/jzQqrjzC8NP8sbH5jtbuvgeDXZX3\n" +
+            "AmGRp5j6C6A61ihAPmEVz3ZfN8SSfJ3vl//PAIg6lyz0J+cy4Q7RkwSeuVQ72Hl4M8TEvmmKC3Af\n" +
+            "ispy6Y0CgYEAgl1o2lo+ACyk+oVQPaaPqK3d7WOBFp4eR2nXFor/vsx9igQOlZUgzRDQsR8jo1o9\n" +
+            "efOSBf87igrZGgssys89pWa2dnXnz5PMmzkKr6bw4D9Ez6u6Puc9UZhGw/8wDYg6fSosdB9utspm\n" +
+            "M698ycef7jBNMDgmhpSvfw5GctoNQ4s=\n" +
+            "-----END RSA PRIVATE KEY-----\n" +
+            "</key>\n" +
+            "<cert>\n" +
+            "-----BEGIN CERTIFICATE-----\n" +
+            "MIIEjDCCAnSgAwIBAgIQG6MBp/cd9DlY+7cdvp3R3jANBgkqhkiG9w0BAQsFADBmMRAwDgYDVQQK\n" +
+            "DAdCaXRtYXNrMRwwGgYDVQQLDBNodHRwczovL2JpdG1hc2submV0MTQwMgYDVQQDDCtCaXRtYXNr\n" +
+            "IFJvb3QgQ0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMB4XDTE0MTIwNTAwMDAwMFoXDTE1\n" +
+            "MDMwNTAwMDAwMFowLTErMCkGA1UEAwwiVU5MSU1JVEVEZDBwZDdkMzE4eTNtOHNkeXllaTFqYmZl\n" +
+            "eDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANRNhZ4aCwdL5+OKObOKeI2rDqEwGnIr\n" +
+            "hL9wzo/FXbwLfdW45Y9Mxwhh6xy2NkA1YUKCB8VNBKNXlBrGr1QriLbu1rItsJ2VVLqGluVV/gO4\n" +
+            "jcaPU+/Wu0hMFKG28J/dPvIGeNbjBWk6mxQAA5WIpRK9RTeQ88wVaGIZDDzIdivza2zpcyiPAyii\n" +
+            "dbkyXh7sLsKvbZB6wLrert6Y1ylR3SlkZP0LfdGAMAdkMyuXKOjgcSnUltR8HSBuZcSUlsTVM11n\n" +
+            "rYeGCYyPNNQ3UYatDW33UASgRDBorrmjhhKP7IW/opdlnPk5ZrP3i0qI32/boRe0EWZGXJvr4P3K\n" +
+            "dJ30uCECAwEAAaNvMG0wHQYDVR0OBBYEFK8bMVAM4GBB5sHptoIOAaIvlYueMAsGA1UdDwQEAwIH\n" +
+            "gDATBgNVHSUEDDAKBggrBgEFBQcDAjAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFId+E7bsWFsUWah9\n" +
+            "vZuPvZ7O+aJsMA0GCSqGSIb3DQEBCwUAA4ICAQAQOX81csVhvP422NKkZH7+g3npBpl+sEHedaGR\n" +
+            "xYPOu4HrA4TVF9h44sljRoRJyenGNdBZCXcLKHg889eePTf8Z5K3lTojp6hvwyA6tgxOMHT1kESW\n" +
+            "PfqnRw8mHfHJuE3g+4YNUMwggzwc/VZATdV/7M33sarVN9AUOHou9n9BizgCC+UnYlS+F2POumE3\n" +
+            "FbOhKo5uubI02MwBYlN2JVO2TBt1Q20w8wc6cU07Xi5Epp+1mkgFiOShkNtPcJmEyBWJhxDtSDOW\n" +
+            "2doqWYNqH2kq7B5R/kyyfcpFJqAnBTV7xs+C5rTS1mW7LpxfdCUMbYuLCpyxpO3A/DhAm8n47tUH\n" +
+            "lBtmo8Avdb8VdFpYiGBpB0o9kTFcsWFb2GkWFBduGfSEB8jUI7QtqhgZqocAKK/cweSRV8FwyUcn\n" +
+            "R0prRm3QEi9fbXqEddzjSY9y/lqWYzT7u+IOAQpKroeZ4wzgYperDNOUFuYk1rP7yuvjP2pV5rcN\n" +
+            "yPoBP60TPVWMRM4WJm6nTogAz2qBrFsf/XwT/ajzbsjT6HNB7QbRE+wkFkqspoXG5Agp7KQ8lW3L\n" +
+            "SKCDGOQJz7VIE85pD0tg7QEXBEw8oaRZtMjQ0Gvs25mxXAKka4wGasaWfYH6d0E+iKYcWn86V1rH\n" +
+            "K2ZoknT+Nno5jgjFuUR3fZseNizEfx7BteooKQ==\n" +
+            "-----END CERTIFICATE-----\n" +
+            "</cert>\n" +
+            "# crl-verify file missing in config profile\n" +
+            "route 192.81.208.164  255.255.255.255 net_gateway\n"+
+            "tun-mtu 48000\n"+
+            "nobind\n"+
+            "remote-cert-tls server\n" +
+            "data-ciphers AES-256-GCM\n" +
+            "cipher AES-256-GCM\n" +
+            "auth SHA512\n" +
+            "float\n"+
+            "persist-tun\n" +
+            "# persist-tun also enables pre resolving to avoid DNS resolve problem\n" +
+            "preresolve\n" +
+            "# Use system proxy setting\n" +
+            "management-query-proxy\n" +
+            "# Custom configuration options\n" +
+            "# You are on your on own here :)\n" +
+            "# These options found in the config file do not map to config settings:\n" +
+            "keepalive 10 30 \n" +
+            "replay-window 65535 \n" +
+            "sndbuf 0 \n" +
+            "rcvbuf 0 \n" +
+            "tls-version-min 1.2 \n" +
+            "ping-restart 300 \n" +
+            "tls-cipher TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384 \n";
+
+    String expectedVPNConfig_hopping_pt_portAndIpHopping = "# Config for OpenVPN 2.x\n" +
+            "# Enables connection to GUI\n" +
+            "management /data/data/se.leap.bitmask/mgmtsocket unix\n" +
+            "management-client\n" +
+            "management-query-passwords\n" +
+            "management-hold\n" +
+            "\n" +
+            "setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
+            "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+            "machine-readable-output\n" +
+            "allow-recursive-routing\n" +
+            "ifconfig-nowarn\n" +
+            "client\n" +
+            "verb 4\n" +
+            "connect-retry 2 300\n" +
+            "resolv-retry 60\n" +
+            "dev tun\n" +
+            "remote 127.0.0.1 8080 udp\n" +
+            "<ca>\n" +
+            "-----BEGIN CERTIFICATE-----\n" +
+            "MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt\n" +
+            "YXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v\n" +
+            "Yml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw\n" +
+            "FgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV\n" +
+            "BAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw\n" +
+            "ggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai\n" +
+            "dHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB\n" +
+            "7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r/bEHUSevmR09BRp86syHZerdNGpXYhcQ84\n" +
+            "CA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+\n" +
+            "znCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe/RGk4\n" +
+            "MEqGFuOzrtsgEhPIX0hplhb0Tgz/rtug+yTT7oJjBa3u20AAOQ38/M99EfdeJvc4\n" +
+            "lPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu/qt3DfvLfhboq+0\n" +
+            "bQvLUPXrVDr70onv5UDjpmEA/cLmaIqqrduuTkFZOym65/PfAPvpGnt7crQj/Ibl\n" +
+            "DEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB\n" +
+            "lfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy\n" +
+            "YMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw\n" +
+            "XjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH/BAQDAgIE\n" +
+            "MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w\n" +
+            "DQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl\n" +
+            "cXYyB6hY5hv/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY\n" +
+            "k/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj\n" +
+            "RnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG\n" +
+            "htD/JKwt6xBmNwktH0GI/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX\n" +
+            "EIQ0ZR56bFuJA/CwValBqV/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J\n" +
+            "aF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l\n" +
+            "mlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK\n" +
+            "G6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co\n" +
+            "Ja8zlx64jmMZPg/t3wWqkZgXZ14qnbyG5/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d\n" +
+            "69db12/g4f6phldhxiWuGC/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e\n" +
+            "yV8e\n" +
+            "-----END CERTIFICATE-----\n" +
+            "\n" +
+            "</ca>\n" +
+            "<key>\n" +
+            "-----BEGIN RSA PRIVATE KEY-----\n" +
+            "MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDUTYWeGgsHS+fjijmziniNqw6h\n" +
+            "MBpyK4S/cM6PxV28C33VuOWPTMcIYesctjZANWFCggfFTQSjV5Qaxq9UK4i27tayLbCdlVS6hpbl\n" +
+            "Vf4DuI3Gj1Pv1rtITBShtvCf3T7yBnjW4wVpOpsUAAOViKUSvUU3kPPMFWhiGQw8yHYr82ts6XMo\n" +
+            "jwMoonW5Ml4e7C7Cr22QesC63q7emNcpUd0pZGT9C33RgDAHZDMrlyjo4HEp1JbUfB0gbmXElJbE\n" +
+            "1TNdZ62HhgmMjzTUN1GGrQ1t91AEoEQwaK65o4YSj+yFv6KXZZz5OWaz94tKiN9v26EXtBFmRlyb\n" +
+            "6+D9ynSd9LghAgMBAAECggEBANPHLRXkhsHVj1EkzqBx7gXr8CEMmiTvknFh9zvltrZhhDoRQjWr\n" +
+            "chPDkcRHY2Cznvy4N0YyqQDD2ULIlZdSAgPxxothFoBruWSD47yMBmLx08ORsDpcqt/YvPAATJI8\n" +
+            "IpFNsXcyaXBp/M57oRemgnxp/8UJPJmFdWX99H4hvffh/jdj7POgYiWUaAl37XTYZKZ4nzKU2wpL\n" +
+            "EDLj9RKPz9gG7CYp2zrLC9LaAsrXVrKwPBw6g+XwbClaqFj97db3mrY4lr6mTo89qmus1AU+fBDH\n" +
+            "3Xlpmc8JwB+30TvhRNKrpLx9cEjuEj7K1gm8Y4dWCjPi+lNbtAyUBcgPJFa/81ECgYEA7pLoBU/Y\n" +
+            "ZYjyHFca8FvDBcBh6haHfqJr9doXWtgjDrbi3o2n5wHqfKhFWOH6vPEQozkOVeX1ze6HOiRmGBpW\n" +
+            "r+r7x8TD25L7I6HJw3M351RWOAfkF0w/RTVdetcTgduQtfN1u6BDhYSVceXMjyQYx7MhfETWI8Gh\n" +
+            "KSYm8OEDYiUCgYEA489fmbrCcUnXzpTsbswJ5NmSoEXbcX8cLxnQuzE0z9GHhQdrMjOpXR76reTW\n" +
+            "6jcuudarNcwRUYSWWhjCDKHhpx4HhasWPaHgr7jIzcRw8yZSJRSxKr8sl1qh6g7s47JcmfXOMWLt\n" +
+            "yuyE933XrT19Th4ODZHY40Uv35mPjMi9d00CgYEAyRNAQtndBRa7GG/B4Ls2T+6pl+aNJIo4e+no\n" +
+            "rURlp800wWabEPRocdBRQmyULBLxduBr2LIMzhgwGSz8b2wji/l9ZA3PFY135bxClVzSzUIjuO3N\n" +
+            "rGUzHl2wAAyuAFDSUshzfkPBJRNt8aVBF5PQ3t93ZYmPAmv8LPZe875yX5ECgYEAsUEcwK/ZNW7g\n" +
+            "dQPZR4iJNkC4Xu6cBZ6Cnn92swBheEYvLSoNlX0vDZ7aLE3/jzQqrjzC8NP8sbH5jtbuvgeDXZX3\n" +
+            "AmGRp5j6C6A61ihAPmEVz3ZfN8SSfJ3vl//PAIg6lyz0J+cy4Q7RkwSeuVQ72Hl4M8TEvmmKC3Af\n" +
+            "ispy6Y0CgYEAgl1o2lo+ACyk+oVQPaaPqK3d7WOBFp4eR2nXFor/vsx9igQOlZUgzRDQsR8jo1o9\n" +
+            "efOSBf87igrZGgssys89pWa2dnXnz5PMmzkKr6bw4D9Ez6u6Puc9UZhGw/8wDYg6fSosdB9utspm\n" +
+            "M698ycef7jBNMDgmhpSvfw5GctoNQ4s=\n" +
+            "-----END RSA PRIVATE KEY-----\n" +
+            "</key>\n" +
+            "<cert>\n" +
+            "-----BEGIN CERTIFICATE-----\n" +
+            "MIIEjDCCAnSgAwIBAgIQG6MBp/cd9DlY+7cdvp3R3jANBgkqhkiG9w0BAQsFADBmMRAwDgYDVQQK\n" +
+            "DAdCaXRtYXNrMRwwGgYDVQQLDBNodHRwczovL2JpdG1hc2submV0MTQwMgYDVQQDDCtCaXRtYXNr\n" +
+            "IFJvb3QgQ0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMB4XDTE0MTIwNTAwMDAwMFoXDTE1\n" +
+            "MDMwNTAwMDAwMFowLTErMCkGA1UEAwwiVU5MSU1JVEVEZDBwZDdkMzE4eTNtOHNkeXllaTFqYmZl\n" +
+            "eDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANRNhZ4aCwdL5+OKObOKeI2rDqEwGnIr\n" +
+            "hL9wzo/FXbwLfdW45Y9Mxwhh6xy2NkA1YUKCB8VNBKNXlBrGr1QriLbu1rItsJ2VVLqGluVV/gO4\n" +
+            "jcaPU+/Wu0hMFKG28J/dPvIGeNbjBWk6mxQAA5WIpRK9RTeQ88wVaGIZDDzIdivza2zpcyiPAyii\n" +
+            "dbkyXh7sLsKvbZB6wLrert6Y1ylR3SlkZP0LfdGAMAdkMyuXKOjgcSnUltR8HSBuZcSUlsTVM11n\n" +
+            "rYeGCYyPNNQ3UYatDW33UASgRDBorrmjhhKP7IW/opdlnPk5ZrP3i0qI32/boRe0EWZGXJvr4P3K\n" +
+            "dJ30uCECAwEAAaNvMG0wHQYDVR0OBBYEFK8bMVAM4GBB5sHptoIOAaIvlYueMAsGA1UdDwQEAwIH\n" +
+            "gDATBgNVHSUEDDAKBggrBgEFBQcDAjAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFId+E7bsWFsUWah9\n" +
+            "vZuPvZ7O+aJsMA0GCSqGSIb3DQEBCwUAA4ICAQAQOX81csVhvP422NKkZH7+g3npBpl+sEHedaGR\n" +
+            "xYPOu4HrA4TVF9h44sljRoRJyenGNdBZCXcLKHg889eePTf8Z5K3lTojp6hvwyA6tgxOMHT1kESW\n" +
+            "PfqnRw8mHfHJuE3g+4YNUMwggzwc/VZATdV/7M33sarVN9AUOHou9n9BizgCC+UnYlS+F2POumE3\n" +
+            "FbOhKo5uubI02MwBYlN2JVO2TBt1Q20w8wc6cU07Xi5Epp+1mkgFiOShkNtPcJmEyBWJhxDtSDOW\n" +
+            "2doqWYNqH2kq7B5R/kyyfcpFJqAnBTV7xs+C5rTS1mW7LpxfdCUMbYuLCpyxpO3A/DhAm8n47tUH\n" +
+            "lBtmo8Avdb8VdFpYiGBpB0o9kTFcsWFb2GkWFBduGfSEB8jUI7QtqhgZqocAKK/cweSRV8FwyUcn\n" +
+            "R0prRm3QEi9fbXqEddzjSY9y/lqWYzT7u+IOAQpKroeZ4wzgYperDNOUFuYk1rP7yuvjP2pV5rcN\n" +
+            "yPoBP60TPVWMRM4WJm6nTogAz2qBrFsf/XwT/ajzbsjT6HNB7QbRE+wkFkqspoXG5Agp7KQ8lW3L\n" +
+            "SKCDGOQJz7VIE85pD0tg7QEXBEw8oaRZtMjQ0Gvs25mxXAKka4wGasaWfYH6d0E+iKYcWn86V1rH\n" +
+            "K2ZoknT+Nno5jgjFuUR3fZseNizEfx7BteooKQ==\n" +
+            "-----END CERTIFICATE-----\n" +
+            "</cert>\n" +
+            "# crl-verify file missing in config profile\n" +
+            "route 192.81.208.164  255.255.255.255 net_gateway\n"+
+            "route 192.81.208.165  255.255.255.255 net_gateway\n"+
+            "route 192.81.208.166  255.255.255.255 net_gateway\n"+
+            "tun-mtu 48000\n"+
+            "nobind\n"+
+            "remote-cert-tls server\n" +
+            "data-ciphers AES-256-GCM\n" +
+            "cipher AES-256-GCM\n" +
+            "auth SHA512\n" +
+            "float\n"+
+            "persist-tun\n" +
+            "# persist-tun also enables pre resolving to avoid DNS resolve problem\n" +
+            "preresolve\n" +
+            "# Use system proxy setting\n" +
+            "management-query-proxy\n" +
+            "# Custom configuration options\n" +
+            "# You are on your on own here :)\n" +
+            "# These options found in the config file do not map to config settings:\n" +
+            "keepalive 10 30 \n" +
+            "replay-window 65535 \n" +
+            "sndbuf 0 \n" +
+            "rcvbuf 0 \n" +
+            "tls-version-min 1.2 \n" +
+            "ping-restart 300 \n" +
+            "tls-cipher TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384 \n";
+
 
     @Before
     public void setUp() throws  Exception {
@@ -1688,7 +1952,7 @@ public class VpnConfigGeneratorTest {
 
     @Test
     public void testGenerateVpnProfileExperimentalTransportsEnabled () throws Exception {
-        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_kcp_gateways.json"))).getJSONArray("gateways").getJSONObject(0);
+        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_kcp_gateways.json"))).getJSONArray("gateways").getJSONObject(2);
         generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_kcp_gateways.json"))).getJSONObject(OPENVPN_CONFIGURATION);
         VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
         configuration.apiVersion = 3;
@@ -1696,39 +1960,41 @@ public class VpnConfigGeneratorTest {
         configuration.preferUDP = true;
         vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
         HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
-        assertTrue(vpnProfiles.containsKey(OBFS4));
-        assertTrue(vpnProfiles.containsKey(OBFS4_KCP));
+        assertTrue(vpnProfiles.containsKey(OBFS4) && ((Obfs4Connection)vpnProfiles.get(OBFS4).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("kcp"));
         assertTrue(vpnProfiles.containsKey(OPENVPN));
-
     }
 
     @Test
     public void testGenerateVpnProfile_experimentalTransportsEnabled_KCPMisconfiguredWithUDP_SkippingObfsKCP () throws Exception {
-        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_kcp_gateways.json"))).getJSONArray("gateways").getJSONObject(0);
+        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_kcp_gateways.json"))).getJSONArray("gateways").getJSONObject(2);
         generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_kcp_gateways.json"))).getJSONObject(OPENVPN_CONFIGURATION);
         VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
         configuration.apiVersion = 3;
         configuration.experimentalTransports = true;
         vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
         HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
-        assertTrue(vpnProfiles.containsKey(OBFS4));
-        assertFalse(vpnProfiles.containsKey(OBFS4_KCP));
+        assertFalse(vpnProfiles.containsKey(OBFS4));
         assertTrue(vpnProfiles.containsKey(OPENVPN));
     }
 
     @Test
-    public void testGenerateVpnProfile_ObfuscationPinningEnabled_obfs4AndOpenvpnProfile () throws Exception {
-        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_kcp_gateways.json"))).getJSONArray("gateways").getJSONObject(0);
+    public void testGenerateVpnProfile_ObfuscationPinningNotEnabled_obfs4AndOpenvpnProfile () throws Exception {
+        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_kcp_gateways.json"))).getJSONArray("gateways").getJSONObject(1);
         generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_kcp_gateways.json"))).getJSONObject(OPENVPN_CONFIGURATION);
         VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
         configuration.apiVersion = 3;
-        configuration.useObfuscationPinning = true;
+        configuration.useObfuscationPinning = false;
+        configuration.obfuscationProxyPort = "443";
+        configuration.obfuscationProxyIP = "5.6.7.8";
+        configuration.obfuscationProxyCert = "asdfasdf";
+        configuration.obfuscationProxyKCP = true;
         configuration.remoteGatewayIP = "1.2.3.4";
         vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
         HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
-        assertFalse("has no openvpn profile", vpnProfiles.containsKey(OPENVPN));
+        assertTrue("has openvpn profile", vpnProfiles.containsKey(OPENVPN));
         assertTrue("has obfs4 profile", vpnProfiles.containsKey(OBFS4));
-        assertFalse("has no obfs4 kcp profile", vpnProfiles.containsKey(OBFS4_KCP));
+        assertTrue("bridge is running KCP", vpnProfiles.get(OBFS4).mGatewayIp.equals("1.2.3.4"));
+
     }
 
     @Test
@@ -1738,16 +2004,17 @@ public class VpnConfigGeneratorTest {
         VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
         configuration.apiVersion = 3;
         configuration.useObfuscationPinning = true;
-        configuration.obfuscationProxyKCP = true;
+        configuration.obfuscationProxyKCP = false;
         configuration.obfuscationProxyPort = "443";
         configuration.obfuscationProxyIP = "5.6.7.8";
         configuration.obfuscationProxyCert = "asdfasdf";
         configuration.remoteGatewayIP = "1.2.3.4";
         vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
         HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
-        assertTrue("has obfs4_kcp profile", vpnProfiles.containsKey(OBFS4_KCP));
         assertFalse("has openvpn profile", vpnProfiles.containsKey(OPENVPN));
-        assertFalse("has no obfs4 profile", vpnProfiles.containsKey(OBFS4));
+        assertTrue("has obfs4 profile", vpnProfiles.containsKey(OBFS4));
+        assertTrue("bridge is pinned one", vpnProfiles.get(OBFS4).getTransportType() == OBFS4 && !vpnProfiles.get(OBFS4).mConnections[0].isUseUdp() );
+        assertTrue("bridge is running TCP", ((Obfs4Connection) vpnProfiles.get(OBFS4).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("tcp"));
     }
 
     @Test
@@ -1765,9 +2032,126 @@ public class VpnConfigGeneratorTest {
 
         vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
         HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
-        assertTrue("has obfs4_kcp profile", vpnProfiles.containsKey(OBFS4_KCP));
         assertFalse("has openvpn profile", vpnProfiles.containsKey(OPENVPN));
-        assertFalse("has no obfs4 profile", vpnProfiles.containsKey(OBFS4));
+        assertTrue("has no obfs4 profile", vpnProfiles.containsKey(OBFS4));
+        assertTrue("bridge is pinned one", vpnProfiles.get(OBFS4).getTransportType() == OBFS4 && vpnProfiles.get(OBFS4).mGatewayIp.equals("1.2.3.4"));
+        assertTrue("bridge is running KCP", ((Obfs4Connection) vpnProfiles.get(OBFS4).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("kcp"));
+    }
+    @Test
+    public void testGenerateVpnProfile_obfs4hop_tcp () throws Exception {
+        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_obfs4hop_tcp_gateways.json"))).getJSONArray("gateways").getJSONObject(2);
+        generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_obfs4hop_tcp_gateways.json"))).getJSONObject(OPENVPN_CONFIGURATION);
+        VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
+        configuration.apiVersion = 3;
+        configuration.experimentalTransports = true;
+        vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
+        HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+        assertTrue(vpnProfiles.containsKey(OBFS4_HOP) && ((Obfs4HopConnection)vpnProfiles.get(OBFS4_HOP).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("tcp"));
+        assertTrue(vpnProfiles.containsKey(OPENVPN));
+    }
+
+    @Test
+    public void testGenerateVpnProfile_obfs4hop_kcp () throws Exception {
+        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_obfs4hop_kcp_gateways.json"))).getJSONArray("gateways").getJSONObject(2);
+        generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_obfs4hop_kcp_gateways.json"))).getJSONObject(OPENVPN_CONFIGURATION);
+        VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
+        configuration.apiVersion = 3;
+        configuration.experimentalTransports = true;
+        vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
+        HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+        assertTrue(vpnProfiles.containsKey(OBFS4_HOP) && ((Obfs4HopConnection)vpnProfiles.get(OBFS4_HOP).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("kcp"));
+        assertTrue(vpnProfiles.containsKey(OPENVPN));
+    }
+
+    @Test
+    public void testGetConfigFile_testHoppingPtPortHopping_decoupled() throws Exception {
+        when(ConfigHelper.ObfsVpnHelper.useObfsVpn()).thenReturn(true);
+        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt_portHopping.eip-service.json"))).getJSONArray("gateways").getJSONObject(2);
+        generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt_portHopping.eip-service.json"))).getJSONObject(OPENVPN_CONFIGURATION);
+        VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
+        configuration.apiVersion = 3;
+        configuration.experimentalTransports = true;
+        vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
+        HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+        System.out.println(vpnProfiles.get(OBFS4_HOP).getConfigFile(context, false));
+        assertEquals(expectedVPNConfig_hopping_pt_portHopping.trim(), vpnProfiles.get(OBFS4_HOP).getConfigFile(context, false).trim());
+    }
+
+    @Test
+    public void testGetConfigFile_testHoppingPtPortAndIPHopping_decoupled() throws Exception {
+        when(ConfigHelper.ObfsVpnHelper.useObfsVpn()).thenReturn(true);
+        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt_portHopping.eip-service.json"))).getJSONArray("gateways").getJSONObject(2);
+        generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt_portHopping.eip-service.json"))).getJSONObject(OPENVPN_CONFIGURATION);
+        VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
+        configuration.apiVersion = 3;
+        configuration.experimentalTransports = true;
+        vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
+        HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+        System.out.println(vpnProfiles.get(OBFS4_HOP).getConfigFile(context, false));
+        assertEquals(expectedVPNConfig_hopping_pt_portHopping.trim(), vpnProfiles.get(OBFS4_HOP).getConfigFile(context, false).trim());
+    }
+    @Test
+    public void testGenerateVpnProfile_obfs4_decoupled() throws Exception {
+        when(ConfigHelper.ObfsVpnHelper.useObfsVpn()).thenReturn(true);
+        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt.eip-service.json"))).getJSONArray("gateways").getJSONObject(1);
+        generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt.eip-service.json"))).getJSONObject(OPENVPN_CONFIGURATION);
+        VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
+        configuration.apiVersion = 3;
+        configuration.experimentalTransports = true;
+        vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
+        HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+        assertTrue(vpnProfiles.containsKey(OBFS4));
+        assertTrue(((Obfs4Connection)vpnProfiles.get(OBFS4).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("tcp"));
+        assertFalse(vpnProfiles.containsKey(OPENVPN));
+    }
+
+    @Test
+    public void testGenerateVpnProfile_obfs4hop_decoupled() throws Exception {
+        when(ConfigHelper.ObfsVpnHelper.useObfsVpn()).thenReturn(true);
+        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt.eip-service.json"))).getJSONArray("gateways").getJSONObject(2);
+        generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt.eip-service.json"))).getJSONObject(OPENVPN_CONFIGURATION);
+        VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
+        configuration.apiVersion = 3;
+        configuration.experimentalTransports = true;
+        vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
+        HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+        assertTrue(vpnProfiles.containsKey(OBFS4_HOP));
+        assertTrue(((Obfs4HopConnection)vpnProfiles.get(OBFS4_HOP).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("tcp"));
+        assertFalse(vpnProfiles.containsKey(OPENVPN));
+        assertFalse(vpnProfiles.containsKey(OBFS4));
+    }
+
+    @Test
+    public void testGenerateVpnProfile_noExperimental_skipObfs4Hop() throws Exception {
+        when(ConfigHelper.ObfsVpnHelper.useObfsVpn()).thenReturn(true);
+        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt.eip-service.json"))).getJSONArray("gateways").getJSONObject(2);
+        generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt.eip-service.json"))).getJSONObject(OPENVPN_CONFIGURATION);
+        VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
+        configuration.apiVersion = 3;
+        configuration.experimentalTransports = false;
+        vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
+        Exception exception = null;
+        try {
+            vpnConfigGenerator.generateVpnProfiles();
+        } catch (ConfigParser.ConfigParseError e) {
+            exception = e;
+        }
+        assertNotNull(exception);
     }
 
+    @Test
+    public void testGenerateVpnProfile_obfs4hop_onlyPortHopping_decoupled() throws Exception {
+        when(ConfigHelper.ObfsVpnHelper.useObfsVpn()).thenReturn(true);
+        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt_portHopping.eip-service.json"))).getJSONArray("gateways").getJSONObject(2);
+        generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt_portHopping.eip-service.json"))).getJSONObject(OPENVPN_CONFIGURATION);
+        VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
+        configuration.apiVersion = 3;
+        configuration.experimentalTransports = true;
+        vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
+        HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+        assertTrue(vpnProfiles.containsKey(OBFS4_HOP));
+        assertTrue(((Obfs4HopConnection)vpnProfiles.get(OBFS4_HOP).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("tcp"));
+        assertFalse(vpnProfiles.containsKey(OPENVPN));
+        assertFalse(vpnProfiles.containsKey(OBFS4));
+    }
 }
\ No newline at end of file
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java b/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java
index 651aa345ad3c8b980870fcfb5219fd4cb976e925..d30e8b7ed6ecc3b93720b347a26b8bb6c33b1517 100644
--- a/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java
@@ -408,7 +408,6 @@ public class MockHelper {
                 return getClass().getClassLoader().getResourceAsStream(filename);
             }
         });
-        when(InputStreamHelper.extractKeyFromInputStream(any(InputStream.class), anyString())).thenCallRealMethod();
         when(InputStreamHelper.inputStreamToJson(any(InputStream.class))).thenCallRealMethod();
 
     }
diff --git a/app/src/test/resources/decoupled_pt.eip-service.json b/app/src/test/resources/decoupled_pt.eip-service.json
new file mode 100644
index 0000000000000000000000000000000000000000..e2ad50a1b0d928e7491df09e28edc6809136eedc
--- /dev/null
+++ b/app/src/test/resources/decoupled_pt.eip-service.json
@@ -0,0 +1,120 @@
+{
+  "gateways":[
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "transport":[
+          {
+            "ports":[
+              "53",
+              "80",
+              "1194"
+            ],
+            "protocols":[
+              "tcp",
+              "udp"
+            ],
+            "type":"openvpn"
+          }
+        ]
+      },
+      "host":"gateway-nyc1-01.bitmask-dev.leapvpn.net",
+      "ip_address":"67.205.131.206",
+      "location":"NYC"
+    },
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "transport":[
+          {
+            "options":{
+              "cert":"pIp+etK+ud3aKAa3Uujbc3q3kwQOBQgW1UUIXXkeQ+i24WFnSzTT1Q69F0x8EXpcq5jyGg",
+              "iatMode":"0"
+            },
+            "ports":[
+              "443"
+            ],
+            "protocols":[
+              "tcp"
+            ],
+            "type":"obfs4"
+          }
+        ]
+      },
+      "host":"bridge-nyc1-01.bitmask-dev.leapvpn.net",
+      "ip_address":"104.248.232.240",
+      "location":"NYC"
+    },
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "transport":[
+          {
+            "options":{
+              "endpoints": [
+                {
+                  "cert" : "CERT",
+                  "ip": "192.81.208.164"
+                },
+                {
+                  "cert" : "CERT2",
+                  "ip": "192.81.208.165"
+                },
+                {
+                  "cert" : "CERT3",
+                  "ip": "192.81.208.166"
+                }
+              ],
+              "experimental":true,
+              "iatMode":"0",
+              "port_count":100,
+              "port_seed":0
+            },
+            "ports":[
+
+            ],
+            "protocols":[
+              "tcp"
+            ],
+            "type":"obfs4-hop"
+          }
+        ]
+      },
+      "host":"bridge-nyc1-02.bitmask-dev.leapvpn.net",
+      "ip_address":"192.81.208.164",
+      "location":"NYC"
+    }
+  ],
+  "locations":{
+    "NYC":{
+      "country_code":"US",
+      "hemisphere":"N",
+      "name":"New York City",
+      "timezone":"-4"
+    }
+  },
+  "openvpn_configuration":{
+    "auth":"SHA512",
+    "cipher":"AES-256-GCM",
+    "data-ciphers":"AES-256-GCM",
+    "dev":"tun",
+    "float":"",
+    "keepalive":"10 30",
+    "key-direction":"1",
+    "nobind":true,
+    "persist-key":true,
+    "rcvbuf":"0",
+    "sndbuf":"0",
+    "tls-cipher":"TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384",
+    "tls-version-min":"1.2",
+    "verb":"3"
+  },
+  "serial":3,
+  "version":3
+}
\ No newline at end of file
diff --git a/app/src/test/resources/decoupled_pt_portHopping.eip-service.json b/app/src/test/resources/decoupled_pt_portHopping.eip-service.json
new file mode 100644
index 0000000000000000000000000000000000000000..a95044548b6b104f48e4f9424c5d3cb9865f8463
--- /dev/null
+++ b/app/src/test/resources/decoupled_pt_portHopping.eip-service.json
@@ -0,0 +1,107 @@
+{
+  "gateways":[
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "transport":[
+          {
+            "ports":[
+              "53",
+              "80",
+              "1194"
+            ],
+            "protocols":[
+              "tcp",
+              "udp"
+            ],
+            "type":"openvpn"
+          }
+        ]
+      },
+      "host":"gateway-nyc1-01.bitmask-dev.leapvpn.net",
+      "ip_address":"67.205.131.206",
+      "location":"NYC"
+    },
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "transport":[
+          {
+            "options":{
+              "cert":"CERT",
+              "iatMode":"0"
+            },
+            "ports":[
+              "443"
+            ],
+            "protocols":[
+              "tcp"
+            ],
+            "type":"obfs4"
+          }
+        ]
+      },
+      "host":"bridge-nyc1-01.bitmask-dev.leapvpn.net",
+      "ip_address":"104.248.232.240",
+      "location":"NYC"
+    },
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "transport":[
+          {
+            "options":{
+              "cert":"CERT",
+              "experimental":true,
+              "iatMode":"0",
+              "port_count":100,
+              "port_seed":0
+            },
+            "ports":[
+
+            ],
+            "protocols":[
+              "tcp"
+            ],
+            "type":"obfs4-hop"
+          }
+        ]
+      },
+      "host":"bridge-nyc1-02.bitmask-dev.leapvpn.net",
+      "ip_address":"192.81.208.164",
+      "location":"NYC"
+    }
+  ],
+  "locations":{
+    "NYC":{
+      "country_code":"US",
+      "hemisphere":"N",
+      "name":"New York City",
+      "timezone":"-4"
+    }
+  },
+  "openvpn_configuration":{
+    "auth":"SHA512",
+    "cipher":"AES-256-GCM",
+    "data-ciphers":"AES-256-GCM",
+    "dev":"tun",
+    "float":"",
+    "keepalive":"10 30",
+    "key-direction":"1",
+    "nobind":true,
+    "persist-key":true,
+    "rcvbuf":"0",
+    "sndbuf":"0",
+    "tls-cipher":"TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384",
+    "tls-version-min":"1.2",
+    "verb":"3"
+  },
+  "serial":3,
+  "version":3
+}
\ No newline at end of file
diff --git a/app/src/test/resources/ptdemo_kcp_gateways.json b/app/src/test/resources/ptdemo_kcp_gateways.json
index 332c8c11e2b471d05baaa59a6e77a7285a8c31f1..8c2c9d32b6b97c11c0bf66a43a8ea3ec1058ee26 100644
--- a/app/src/test/resources/ptdemo_kcp_gateways.json
+++ b/app/src/test/resources/ptdemo_kcp_gateways.json
@@ -19,19 +19,6 @@
               "iatMode": "0"
             }
           },
-          {
-            "type":"obfs4-1",
-            "protocols":[
-              "tcp"
-            ],
-            "ports":[
-              "23050"
-            ],
-            "options": {
-              "cert": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1",
-              "iatMode": "0"
-            }
-          },
           {
             "type":"openvpn",
             "protocols":[
@@ -101,9 +88,9 @@
             ]
           },
           {
-            "type":"obfs4-1",
+            "type":"obfs4",
             "protocols":[
-              "tcp"
+              "kcp"
             ],
             "ports":[
               "23050"
diff --git a/app/src/test/resources/ptdemo_misconfigured_gateway.json b/app/src/test/resources/ptdemo_misconfigured_gateway.json
deleted file mode 100644
index 380088800bc5362fe2f5e21e0841b79d693014e1..0000000000000000000000000000000000000000
--- a/app/src/test/resources/ptdemo_misconfigured_gateway.json
+++ /dev/null
@@ -1,54 +0,0 @@
-{
-  "gateways":[
-    {
-      "capabilities":{
-        "adblock":false,
-        "filter_dns":false,
-        "limited":false,
-        "transport":[
-          {
-            "type":"obfs4",
-            "protocols":[
-              "tcp"
-            ],
-            "ports":[
-              "443"
-            ],
-            "options": {
-              "cert": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX2",
-              "iat-mode": "0"
-            }
-          }
-        ],
-        "user_ips":false
-      },
-      "host":"moscow.bitmask.net",
-      "ip_address":"3.21.247.89",
-      "location":"moscow"
-    }
-  ],
-  "locations":{
-    "Amsterdam":{
-      "country_code":"NL",
-      "hemisphere":"N",
-      "name":"Amsterdam",
-      "timezone":"-1"
-    }
-  },
-  "openvpn_configuration":{
-    "auth":"SHA1",
-    "cipher":"AES-256-CBC",
-    "keepalive":"10 30",
-    "tls-cipher":"DHE-RSA-AES128-SHA",
-    "tun-ipv6":true,
-    "dev" : "tun",
-    "sndbuf" : "0",
-    "rcvbuf" : "0",
-    "nobind" : true,
-    "persist-key" : true,
-    "key-direction" : "1",
-    "verb" : "3"
-  },
-  "serial":2,
-  "version":3
-}
\ No newline at end of file
diff --git a/app/src/test/resources/ptdemo_misconfigured_kcp_gateways.json b/app/src/test/resources/ptdemo_misconfigured_kcp_gateways.json
index 94a1eafd1bc1b79b228652f9af7cee674ddbf5df..28a6a2011916b90b5051d86544272aa812fb68b1 100644
--- a/app/src/test/resources/ptdemo_misconfigured_kcp_gateways.json
+++ b/app/src/test/resources/ptdemo_misconfigured_kcp_gateways.json
@@ -20,7 +20,7 @@
             }
           },
           {
-            "type":"obfs4-1",
+            "type":"obfs4",
             "protocols":[
               "udp"
             ],
@@ -101,7 +101,7 @@
             ]
           },
           {
-            "type":"obfs4-1",
+            "type":"obfs4",
             "protocols":[
               "udp"
             ],
diff --git a/app/src/test/resources/ptdemo_obfs4hop_kcp_gateways.json b/app/src/test/resources/ptdemo_obfs4hop_kcp_gateways.json
new file mode 100644
index 0000000000000000000000000000000000000000..a56bc571b9635802cbe679a596f107625ee97c3a
--- /dev/null
+++ b/app/src/test/resources/ptdemo_obfs4hop_kcp_gateways.json
@@ -0,0 +1,178 @@
+{
+  "gateways":[
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "transport":[
+          {
+            "type":"obfs4",
+            "protocols":[
+              "tcp"
+            ],
+            "ports":[
+              "23049"
+            ],
+            "options": {
+              "cert": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1",
+              "iatMode": "0"
+            }
+          },
+          {
+            "type":"openvpn",
+            "protocols":[
+              "tcp"
+            ],
+            "ports":[
+              "1195"
+            ]
+          }
+        ],
+        "user_ips":false
+      },
+      "host":"pt.demo.bitmask.net",
+      "ip_address":"37.218.247.60",
+      "location":"Amsterdam"
+    },
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "transport":[
+          {
+            "type":"obfs4-hop",
+            "protocols":[
+              "kcp"
+            ],
+            "options": {
+              "iatMode": "0",
+              "endpoints": [
+                {
+                  "ip": "1.1.1.1",
+                  "cert": "/ntRNI6JYP7R6kGKldibKWj0aCsv96Hdu/jSGncPy+rcverCLI7Emod+vRkz61hM7F/udA"
+                },
+                {
+                  "ip": "2.2.2.2",
+                  "cert": "/ntRNI6JYP7R6kGKldibKWj0aCsv96Hdu/jSGncPy+rcverCLI7Emod+vRkz61hM7F/udA"
+                }
+              ],
+              "port_seed": 94,
+              "port_count": 100,
+              "experimental": true
+            }
+          },
+          {
+            "type":"openvpn",
+            "protocols":[
+              "tcp"
+            ],
+            "ports":[
+              "1195"
+            ]
+          }
+        ],
+        "user_ips":false
+      },
+      "host":"moscow.bitmask.net",
+      "ip_address":"3.21.247.89",
+      "location":"moscow"
+    },
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "transport":[
+          {
+            "type":"openvpn",
+            "protocols":[
+              "tcp",
+              "udp"
+            ],
+
+            "ports":[
+              "1195"
+            ]
+          },
+          {
+            "type":"obfs4",
+            "protocols":[
+              "kcp"
+            ],
+            "ports":[
+              "23050"
+            ],
+            "options": {
+              "cert": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1",
+              "iatMode": "0"
+            }
+          },
+          {
+            "type":"obfs4-hop",
+            "protocols":[
+              "kcp"
+            ],
+            "options": {
+              "iatMode": "0",
+              "endpoints": [
+                {
+                  "ip": "1.1.1.1",
+                  "cert": "/ntRNI6JYP7R6kGKldibKWj0aCsv96Hdu/jSGncPy+rcverCLI7Emod+vRkz61hM7F/udA"
+                },
+                {
+                  "ip": "2.2.2.2",
+                  "cert": "/ntRNI6JYP7R6kGKldibKWj0aCsv96Hdu/jSGncPy+rcverCLI7Emod+vRkz61hM7F/udA"
+                }
+              ],
+              "port_seed": 94,
+              "port_count": 100,
+              "experimental": true
+            }
+          }
+        ],
+        "user_ips":false
+      },
+      "host":"manila.bitmask.net",
+      "ip_address":"37.12.247.10",
+      "location":"manila"
+    }
+  ],
+  "locations":{
+    "Amsterdam":{
+      "country_code":"NL",
+      "hemisphere":"N",
+      "name":"Amsterdam",
+      "timezone":"-1"
+    },
+    "moscow": {
+      "country_code": "RU",
+      "hemisphere": "N",
+      "name": "Moscow",
+      "timezone": "+3"
+    },
+    "manila": {
+      "country_code": "PH",
+      "hemisphere": "N",
+      "name": "Manila",
+      "timezone": "+8"
+    }
+  },
+  "openvpn_configuration":{
+    "auth":"SHA1",
+    "cipher":"AES-256-CBC",
+    "keepalive":"10 30",
+    "tls-cipher":"DHE-RSA-AES128-SHA",
+    "tun-ipv6":true,
+    "dev" : "tun",
+    "sndbuf" : "0",
+    "rcvbuf" : "0",
+    "nobind" : true,
+    "persist-key" : true,
+    "key-direction" : "1",
+    "verb" : "3"
+  },
+  "serial":2,
+  "version":3
+}
\ No newline at end of file
diff --git a/app/src/test/resources/ptdemo_obfs4hop_misconfigured_udp_gateway.json b/app/src/test/resources/ptdemo_obfs4hop_misconfigured_udp_gateway.json
new file mode 100644
index 0000000000000000000000000000000000000000..a56bc571b9635802cbe679a596f107625ee97c3a
--- /dev/null
+++ b/app/src/test/resources/ptdemo_obfs4hop_misconfigured_udp_gateway.json
@@ -0,0 +1,178 @@
+{
+  "gateways":[
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "transport":[
+          {
+            "type":"obfs4",
+            "protocols":[
+              "tcp"
+            ],
+            "ports":[
+              "23049"
+            ],
+            "options": {
+              "cert": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1",
+              "iatMode": "0"
+            }
+          },
+          {
+            "type":"openvpn",
+            "protocols":[
+              "tcp"
+            ],
+            "ports":[
+              "1195"
+            ]
+          }
+        ],
+        "user_ips":false
+      },
+      "host":"pt.demo.bitmask.net",
+      "ip_address":"37.218.247.60",
+      "location":"Amsterdam"
+    },
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "transport":[
+          {
+            "type":"obfs4-hop",
+            "protocols":[
+              "kcp"
+            ],
+            "options": {
+              "iatMode": "0",
+              "endpoints": [
+                {
+                  "ip": "1.1.1.1",
+                  "cert": "/ntRNI6JYP7R6kGKldibKWj0aCsv96Hdu/jSGncPy+rcverCLI7Emod+vRkz61hM7F/udA"
+                },
+                {
+                  "ip": "2.2.2.2",
+                  "cert": "/ntRNI6JYP7R6kGKldibKWj0aCsv96Hdu/jSGncPy+rcverCLI7Emod+vRkz61hM7F/udA"
+                }
+              ],
+              "port_seed": 94,
+              "port_count": 100,
+              "experimental": true
+            }
+          },
+          {
+            "type":"openvpn",
+            "protocols":[
+              "tcp"
+            ],
+            "ports":[
+              "1195"
+            ]
+          }
+        ],
+        "user_ips":false
+      },
+      "host":"moscow.bitmask.net",
+      "ip_address":"3.21.247.89",
+      "location":"moscow"
+    },
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "transport":[
+          {
+            "type":"openvpn",
+            "protocols":[
+              "tcp",
+              "udp"
+            ],
+
+            "ports":[
+              "1195"
+            ]
+          },
+          {
+            "type":"obfs4",
+            "protocols":[
+              "kcp"
+            ],
+            "ports":[
+              "23050"
+            ],
+            "options": {
+              "cert": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1",
+              "iatMode": "0"
+            }
+          },
+          {
+            "type":"obfs4-hop",
+            "protocols":[
+              "kcp"
+            ],
+            "options": {
+              "iatMode": "0",
+              "endpoints": [
+                {
+                  "ip": "1.1.1.1",
+                  "cert": "/ntRNI6JYP7R6kGKldibKWj0aCsv96Hdu/jSGncPy+rcverCLI7Emod+vRkz61hM7F/udA"
+                },
+                {
+                  "ip": "2.2.2.2",
+                  "cert": "/ntRNI6JYP7R6kGKldibKWj0aCsv96Hdu/jSGncPy+rcverCLI7Emod+vRkz61hM7F/udA"
+                }
+              ],
+              "port_seed": 94,
+              "port_count": 100,
+              "experimental": true
+            }
+          }
+        ],
+        "user_ips":false
+      },
+      "host":"manila.bitmask.net",
+      "ip_address":"37.12.247.10",
+      "location":"manila"
+    }
+  ],
+  "locations":{
+    "Amsterdam":{
+      "country_code":"NL",
+      "hemisphere":"N",
+      "name":"Amsterdam",
+      "timezone":"-1"
+    },
+    "moscow": {
+      "country_code": "RU",
+      "hemisphere": "N",
+      "name": "Moscow",
+      "timezone": "+3"
+    },
+    "manila": {
+      "country_code": "PH",
+      "hemisphere": "N",
+      "name": "Manila",
+      "timezone": "+8"
+    }
+  },
+  "openvpn_configuration":{
+    "auth":"SHA1",
+    "cipher":"AES-256-CBC",
+    "keepalive":"10 30",
+    "tls-cipher":"DHE-RSA-AES128-SHA",
+    "tun-ipv6":true,
+    "dev" : "tun",
+    "sndbuf" : "0",
+    "rcvbuf" : "0",
+    "nobind" : true,
+    "persist-key" : true,
+    "key-direction" : "1",
+    "verb" : "3"
+  },
+  "serial":2,
+  "version":3
+}
\ No newline at end of file
diff --git a/app/src/test/resources/ptdemo_obfs4hop_tcp_gateways.json b/app/src/test/resources/ptdemo_obfs4hop_tcp_gateways.json
new file mode 100644
index 0000000000000000000000000000000000000000..34bcecb3f203dc1c659c1ab91a8279ddf12ae8b7
--- /dev/null
+++ b/app/src/test/resources/ptdemo_obfs4hop_tcp_gateways.json
@@ -0,0 +1,179 @@
+{
+  "gateways":[
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "transport":[
+          {
+            "type":"obfs4",
+            "protocols":[
+              "tcp"
+            ],
+            "ports":[
+              "23049"
+            ],
+            "options": {
+              "cert": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1",
+              "iatMode": "0"
+            }
+          },
+          {
+            "type":"openvpn",
+            "protocols":[
+              "tcp"
+            ],
+            "ports":[
+              "1195"
+            ]
+          }
+        ],
+        "user_ips":false
+      },
+      "host":"pt.demo.bitmask.net",
+      "ip_address":"37.218.247.60",
+      "location":"Amsterdam"
+    },
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "transport":[
+          {
+            "type":"obfs4-hop",
+            "protocols":[
+              "tcp"
+            ],
+            "options": {
+              "iatMode": "0",
+              "endpoints": [
+                {
+                  "ip": "1.1.1.1",
+                  "cert": "/ntRNI6JYP7R6kGKldibKWj0aCsv96Hdu/jSGncPy+rcverCLI7Emod+vRkz61hM7F/udA"
+                },
+                {
+                  "ip": "2.2.2.2",
+                  "cert": "/ntRNI6JYP7R6kGKldibKWj0aCsv96Hdu/jSGncPy+rcverCLI7Emod+vRkz61hM7F/udA"
+                }
+              ],
+              "port_seed": 94,
+              "port_count": 100,
+              "experimental": true
+            }
+          },
+          {
+            "type":"openvpn",
+            "protocols":[
+              "tcp",
+              "udp"
+            ],
+            "ports":[
+              "1195"
+            ]
+          }
+        ],
+        "user_ips":false
+      },
+      "host":"moscow.bitmask.net",
+      "ip_address":"3.21.247.89",
+      "location":"moscow"
+    },
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "transport":[
+          {
+            "type":"openvpn",
+            "protocols":[
+              "tcp",
+              "udp"
+            ],
+
+            "ports":[
+              "1195"
+            ]
+          },
+          {
+            "type":"obfs4",
+            "protocols":[
+              "kcp"
+            ],
+            "ports":[
+              "23050"
+            ],
+            "options": {
+              "cert": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1",
+              "iatMode": "0"
+            }
+          },
+          {
+            "type":"obfs4-hop",
+            "protocols":[
+              "tcp"
+            ],
+            "options": {
+              "iatMode": "0",
+              "endpoints": [
+                {
+                  "ip": "1.1.1.1",
+                  "cert": "CERT1"
+                },
+                {
+                  "ip": "2.2.2.2",
+                  "cert": "CERT2"
+                }
+              ],
+              "port_seed": 94,
+              "port_count": 100,
+              "experimental": true
+            }
+          }
+        ],
+        "user_ips":false
+      },
+      "host":"manila.bitmask.net",
+      "ip_address":"37.12.247.10",
+      "location":"manila"
+    }
+  ],
+  "locations":{
+    "Amsterdam":{
+      "country_code":"NL",
+      "hemisphere":"N",
+      "name":"Amsterdam",
+      "timezone":"-1"
+    },
+    "moscow": {
+      "country_code": "RU",
+      "hemisphere": "N",
+      "name": "Moscow",
+      "timezone": "+3"
+    },
+    "manila": {
+      "country_code": "PH",
+      "hemisphere": "N",
+      "name": "Manila",
+      "timezone": "+8"
+    }
+  },
+  "openvpn_configuration":{
+    "auth":"SHA1",
+    "cipher":"AES-256-CBC",
+    "keepalive":"10 30",
+    "tls-cipher":"DHE-RSA-AES128-SHA",
+    "tun-ipv6":true,
+    "dev" : "tun",
+    "sndbuf" : "0",
+    "rcvbuf" : "0",
+    "nobind" : true,
+    "persist-key" : true,
+    "key-direction" : "1",
+    "verb" : "3"
+  },
+  "serial":2,
+  "version":3
+}
\ No newline at end of file
diff --git a/app/src/test/resources/ptdemo_only_experimental_transports_gateways.json b/app/src/test/resources/ptdemo_only_experimental_transports_gateways.json
index 31c919e722b23cfe95c4f3993da193dcbb98fa33..8f98d1702d71201d5f88b69b08461fd8c4ab1356 100644
--- a/app/src/test/resources/ptdemo_only_experimental_transports_gateways.json
+++ b/app/src/test/resources/ptdemo_only_experimental_transports_gateways.json
@@ -7,9 +7,9 @@
         "limited":false,
         "transport":[
           {
-            "type":"obfs4-1",
+            "type":"obfs4",
             "protocols":[
-              "tcp"
+              "kcp"
             ],
             "ports":[
               "23050"
@@ -75,9 +75,9 @@
             ]
           },
           {
-            "type":"obfs4-1",
+            "type":"obfs4",
             "protocols":[
-              "tcp"
+              "kcp"
             ],
             "ports":[
               "23050"
diff --git a/bitmaskcore b/bitmaskcore
index 1040f75b4fcb9db89ad9c17120f11bab2c99085d..4e4d928fd10d237b96439b1cf667aecaf090c326 160000
--- a/bitmaskcore
+++ b/bitmaskcore
@@ -1 +1 @@
-Subproject commit 1040f75b4fcb9db89ad9c17120f11bab2c99085d
+Subproject commit 4e4d928fd10d237b96439b1cf667aecaf090c326