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 c8ac965f1b54bdc1eb77aff54b2f0747c7dc1617..a82a87d9ce4ba30e16ce020a03b2f2cce70d8716 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java +++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java @@ -182,6 +182,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac synchronized (mProcessLock) { mProcessThread = null; } + stopObfsvpn(); VpnStatus.removeByteCountListener(this); unregisterDeviceStateReceiver(mDeviceStateReceiver); mDeviceStateReceiver = null; @@ -230,15 +231,20 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac mDeviceStateReceiver.userPause(shouldBePaused); } + private boolean stopObfsvpn() { + if (obfsVpnClient == null || !obfsVpnClient.isStarted()) { + return true; + } + boolean success = obfsVpnClient.stop(); + obfsVpnClient = null; + return success; + } @Override public boolean stopVPN(boolean replaceConnection) { + stopObfsvpn(); if(isVpnRunning()) { if (getManagement() != null && getManagement().stopVPN(replaceConnection)) { if (!replaceConnection) { - if (obfsVpnClient != null && obfsVpnClient.isStarted()) { - obfsVpnClient.stop(); - obfsVpnClient = null; - } VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED); } return true; @@ -369,24 +375,53 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } private void startOpenVPN() { - //TODO: investigate how connections[n] with n>0 get called during vpn setup (on connection refused?) - // Do we need to check if there's any obfs4 connection in mProfile.mConnections and start - // the dispatcher here? Can we start the dispatcher at a later point of execution, e.g. when - // connections[n], n>0 gets choosen? - Connection connection = mProfile.mConnections[0]; VpnStatus.setCurrentlyConnectingProfile(mProfile); + // stop old running obfsvpn client + if (!stopObfsvpn()) { + VpnStatus.logError("Failed to stop already running obfsvpn client"); + endVpnService(); + VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED); + return; + } + + // Set a flag that we are starting a new VPN + mStarting = true; + // Stop the previous session by interrupting the thread. + stopOldOpenVPNProcess(); + // An old running VPN should now be exited + mStarting = false; + + // optionally start start obfsvpn and adapt openvpn config to the port obfsvpn is listening to + Connection.TransportType transportType = connection.getTransportType(); + if (mProfile.usePluggableTransports() && transportType.isPluggableTransport()) { + try { + obfsVpnClient = new ObfsvpnClient(((Obfs4Connection) connection).getObfs4Options()); + obfsVpnClient.start(); + int port = obfsVpnClient.getPort(); + connection.setServerPort(String.valueOf(port)); + } catch (RuntimeException e) { + e.printStackTrace(); + VpnStatus.logException(e); + endVpnService(); + VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED); + return; + } + } + + // write openvpn config VpnStatus.logInfo(R.string.building_configration); VpnStatus.updateStateString("VPN_GENERATE_CONFIG", "", R.string.building_configration, ConnectionStatus.LEVEL_START); - try { mProfile.writeConfigFile(this); } catch (IOException e) { VpnStatus.logException("Error writing config file", e); endVpnService(); + VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED); return; } + String nativeLibraryDirectory = getApplicationInfo().nativeLibraryDir; String tmpDir; try { @@ -399,25 +434,6 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac // Write OpenVPN binary String[] argv = VPNLaunchHelper.buildOpenvpnArgv(this); - - // Set a flag that we are starting a new VPN - mStarting = true; - // Stop the previous session by interrupting the thread. - - stopOldOpenVPNProcess(); - // An old running VPN should now be exited - mStarting = false; - Connection.TransportType transportType = connection.getTransportType(); - if (mProfile.usePluggableTransports() && transportType.isPluggableTransport()) { - if (obfsVpnClient != null && obfsVpnClient.isStarted()) { - obfsVpnClient.stop(); - } - obfsVpnClient = new ObfsvpnClient(((Obfs4Connection) connection).getObfs4Options()); - obfsVpnClient.start(); - Log.d(TAG, "obfsvpn client started"); - } - - // Start a new session by creating a new thread. boolean useOpenVPN3 = VpnProfile.doUseOpenVPN3(this); @@ -471,11 +487,6 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac if (mOpenVPNThread != null) ((OpenVPNThread) mOpenVPNThread).setReplaceConnection(); if (mManagement.stopVPN(true)) { - if (obfsVpnClient != null && obfsVpnClient.isStarted()) { - Log.d(TAG, "-> stop obfsvpnClient"); - obfsVpnClient.stop(); - obfsVpnClient = null; - } try { Thread.sleep(1000); } catch (InterruptedException e) { 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 0fe6bff239de219de7a6cbfb9e937ff6583794c6..e2c596aceee7dfbb5a6ec0abf2a17f760cdf439c 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 @@ -15,7 +15,7 @@ public class Obfs4Connection extends Connection { public Obfs4Connection(Obfs4Options options) { setServerName(ObfsvpnClient.IP); - setServerPort(String.valueOf(ObfsvpnClient.PORT)); + setServerPort(String.valueOf(ObfsvpnClient.DEFAULT_PORT)); setUseUdp(true); setProxyType(ProxyType.NONE); setProxyName(""); 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 0288ab2582468524695a62f073cad9548c36eefa..76e32349e2aff4b14cb21d7473b419d0e2237b51 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java @@ -379,7 +379,7 @@ public class VpnConfigGenerator { } stringBuilder.append(getRouteString(ipAddress, transport)); - String transparentProxyRemote = REMOTE + " " + ObfsvpnClient.IP + " " + ObfsvpnClient.PORT + " udp" + newLine; + String transparentProxyRemote = REMOTE + " " + ObfsvpnClient.IP + " " + ObfsvpnClient.DEFAULT_PORT + " udp" + newLine; stringBuilder.append(transparentProxyRemote); } 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 625bbfd84926c97bb8c6dff2e7ee9615ed9a8ab8..2e216015a5d4a79a3026235505ec6e7507bff0e3 100644 --- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsvpnClient.java +++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsvpnClient.java @@ -5,12 +5,16 @@ import static se.leap.bitmaskclient.base.models.Constants.QUIC; import android.util.Log; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + import client.Client; import client.Client_; import client.EventLogger; import de.blinkt.openvpn.core.VpnStatus; import de.blinkt.openvpn.core.connection.Connection; -import se.leap.bitmaskclient.base.models.Constants; import se.leap.bitmaskclient.pluggableTransports.models.HoppingConfig; import se.leap.bitmaskclient.pluggableTransports.models.KcpConfig; import se.leap.bitmaskclient.pluggableTransports.models.Obfs4Options; @@ -19,20 +23,20 @@ import se.leap.bitmaskclient.pluggableTransports.models.QuicConfig; public class ObfsvpnClient implements EventLogger { - public static final int PORT = 8080; + public static final int DEFAULT_PORT = 8080; public static final String IP = "127.0.0.1"; + private static final String ERROR_BIND = "bind: address already in use"; + private static final String STATE_RUNNING = "RUNNING"; private final Object LOCK = new Object(); - + private final AtomicInteger currentPort = new AtomicInteger(DEFAULT_PORT); + private CountDownLatch startCallback = null; private static final String TAG = ObfsvpnClient.class.getSimpleName(); public final Client_ client; 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 + // each obfuscation transport has only 1 protocol String protocol = options.transport.getProtocols()[0]; boolean kcpEnabled = KCP.equals(protocol); boolean quicEnabled = QUIC.equals(protocol); @@ -42,57 +46,135 @@ public class ObfsvpnClient implements EventLogger { } KcpConfig kcpConfig = new KcpConfig(kcpEnabled); QuicConfig quicConfig = new QuicConfig(quicEnabled); - HoppingConfig hoppingConfig = new HoppingConfig(hoppingEnabled,IP+":"+PORT, options); - ObfsvpnConfig obfsvpnConfig = new ObfsvpnConfig(IP+":"+PORT, hoppingConfig, kcpConfig, quicConfig, options.bridgeIP, options.transport.getPorts()[0], options.transport.getOptions().getCert() ); + HoppingConfig hoppingConfig = new HoppingConfig(hoppingEnabled,IP+":"+ DEFAULT_PORT, options); + ObfsvpnConfig obfsvpnConfig = new ObfsvpnConfig(IP+":"+ DEFAULT_PORT, hoppingConfig, kcpConfig, quicConfig, options.bridgeIP, options.transport.getPorts()[0], options.transport.getOptions().getCert() ); try { - Log.d(TAG, obfsvpnConfig.toString()); + Log.d(TAG, "create new obfsvpn client: " + obfsvpnConfig); client = Client.newFFIClient(obfsvpnConfig.toString()); } catch (Exception e) { throw new IllegalStateException(e); } } - public int start() { + public void start() throws RuntimeException { synchronized (LOCK) { + client.setEventLogger(this); + + // this CountDownLatch stops blocking if: + // a) obfsvpn changed its state to RUNNING + // b) an unrecoverable error happened + final CountDownLatch callback = new CountDownLatch(1); + this.startCallback = callback; + AtomicReference<Exception> err = new AtomicReference<>(); new Thread(() -> { try { - if (client.isStarted()) { - return; - } - client.setEventLogger(this); - client.start(); - } catch (Exception e) { + start(0); + } catch (RuntimeException e) { + // save exception and stop blocking e.printStackTrace(); + err.set(e); + callback.countDown(); } }).start(); - return PORT; + + try { + boolean completedBeforeTimeout = callback.await(35, TimeUnit.SECONDS); + Exception startException = err.get(); + this.startCallback = null; + if (!completedBeforeTimeout) { + client.setEventLogger(null); + throw new RuntimeException("failed to start obfsvpn: timeout error"); + } else if (startException != null) { + client.setEventLogger(null); + throw new RuntimeException("failed to start obfsvpn: ", startException); + } + } catch (InterruptedException e) { + this.startCallback = null; + client.setEventLogger(null); + throw new RuntimeException("failed to start obfsvpn: ", e); + } + } + } + + private void start(int portOffset) throws RuntimeException { + currentPort.set(DEFAULT_PORT + portOffset); + Log.d(TAG, "listen to 127.0.0.1:"+ (currentPort.get())); + final CountDownLatch errOnStartCDL = new CountDownLatch(1); + AtomicReference<Exception> err = new AtomicReference<>(); + new Thread(() -> { + try { + client.setProxyAddr(IP + ":" + (DEFAULT_PORT+portOffset)); + client.start(); + } catch (Exception e) { + err.set(e); + errOnStartCDL.countDown(); + } + }).start(); + + try { + // wait for 250 ms, in case there is an immediate error due to misconfiguration + // or bound ports the CountDownLatch is set to 0 and thus the return value of await is true + boolean receivedErr = errOnStartCDL.await(250, TimeUnit.MILLISECONDS); + if (receivedErr) { + Exception e = err.get(); + // attempt to restart the client with a different local proxy port in case + // there's a port binding error + if (e != null && + e.getMessage() != null && + e.getMessage().contains(ERROR_BIND) && + portOffset < 10) { + start(portOffset + 1); + return; + } else { + resetAndThrow(new RuntimeException("Failed to start obfsvpn: " + e)); + } + } + } catch (InterruptedException e) { + resetAndThrow(new RuntimeException(e)); } } - public void stop() { + private void resetAndThrow(RuntimeException e) throws RuntimeException{ + startCallback.countDown(); + startCallback = null; + client.setEventLogger(null); + throw e; + } + + public boolean stop() { synchronized (LOCK) { try { client.stop(); } catch (Exception e) { e.printStackTrace(); + return false; } finally { client.setEventLogger(null); } + return true; } } + public int getPort() { + return currentPort.get(); + } + public boolean isStarted() { return client.isStarted(); } @Override public void error(String s) { - VpnStatus.logError("[obfs4-client] " + s); - + VpnStatus.logError("[obfsvpn-client] error: " + s); } @Override public void log(String state, String message) { - VpnStatus.logDebug("[obfs4-client] " + state + ": " + message); + VpnStatus.logDebug("[obfsvpn-client] " + state + ": " + message); + CountDownLatch startCallback = this.startCallback; + if (startCallback != null && STATE_RUNNING.equals(state)) { + startCallback.countDown(); + this.startCallback = null; + } } } diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java b/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java index 974202030d69a5aa68cecbd1d00371cfeffe731d..752a53362f610caa148149a026730bf5b1ce1549 100644 --- a/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java +++ b/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java @@ -118,16 +118,6 @@ public class TestSetupHelper { return null; } - @Override - public String getBestBridge() throws Exception { - return null; - } - - @Override - public String getBestGateway() throws Exception { - return null; - } - @Override public String getGeolocation() throws Exception { return null; @@ -158,6 +148,11 @@ public class TestSetupHelper { } + @Override + public void setCountryCodeLookupURL(String s) throws Exception { + + } + @Override public void setDebug(boolean b) { @@ -178,6 +173,11 @@ public class TestSetupHelper { } + @Override + public void setStunServers(String s) throws Exception { + + } + @Override public void setUseTls(boolean b) { diff --git a/bitmask-core-android b/bitmask-core-android index 3f45af788cf7e36d9246c0238a999d8d0c983c99..64fab2f1727d147d473d33761eeb7be94a354eaa 160000 --- a/bitmask-core-android +++ b/bitmask-core-android @@ -1 +1 @@ -Subproject commit 3f45af788cf7e36d9246c0238a999d8d0c983c99 +Subproject commit 64fab2f1727d147d473d33761eeb7be94a354eaa