From fe9609d14c7ce713e5f55527c7b0732544071011 Mon Sep 17 00:00:00 2001 From: cyBerta <cyberta@riseup.net> Date: Tue, 25 Mar 2025 21:42:39 +0100 Subject: [PATCH 1/8] improve state handling of obfsvpn; try to restart obfsvpn in on different proxy port in case the default one is already boudn --- .../blinkt/openvpn/core/OpenVPNService.java | 76 ++++++----- .../core/connection/Obfs4Connection.java | 2 +- .../bitmaskclient/eip/VpnConfigGenerator.java | 2 +- .../pluggableTransports/ObfsvpnClient.java | 123 +++++++++++++++--- 4 files changed, 146 insertions(+), 57 deletions(-) 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 c8ac965f1..1ed7bc394 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,17 +375,42 @@ 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); + // 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; + + // stop old running obfsvpn client + if (!stopObfsvpn()) { + VpnStatus.logError("Failed to stop already running obfsvpn client"); + endVpnService(); + return; + } + + // 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(); + 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) { @@ -387,6 +418,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac endVpnService(); return; } + String nativeLibraryDirectory = getApplicationInfo().nativeLibraryDir; String tmpDir; try { @@ -399,25 +431,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 +484,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 0fe6bff23..e2c596ace 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 0288ab258..76e32349e 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 625bbfd84..0691c826b 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,19 @@ 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 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 +45,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("[obfs4-client] error: " + s); } @Override public void log(String state, String message) { VpnStatus.logDebug("[obfs4-client] " + state + ": " + message); + CountDownLatch startCallback = this.startCallback; + if (startCallback != null && "RUNNING".equals(state)) { + startCallback.countDown(); + this.startCallback = null; + } } } -- GitLab From 764b27ce455d75c2c894ee4aaeb8a94c5e6da216 Mon Sep 17 00:00:00 2001 From: cyBerta <cyberta@riseup.net> Date: Tue, 25 Mar 2025 21:43:48 +0100 Subject: [PATCH 2/8] update bitmask-core-android, include obfsvpn updates for better state handling --- bitmask-core-android | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitmask-core-android b/bitmask-core-android index 3f45af788..72863e67f 160000 --- a/bitmask-core-android +++ b/bitmask-core-android @@ -1 +1 @@ -Subproject commit 3f45af788cf7e36d9246c0238a999d8d0c983c99 +Subproject commit 72863e67fab9b037a4aad417c23d3d617425063d -- GitLab From d604e0e78796fda87bdea761b11239caf55d6e7f Mon Sep 17 00:00:00 2001 From: cyBerta <cyberta@riseup.net> Date: Tue, 25 Mar 2025 22:23:02 +0100 Subject: [PATCH 3/8] obfsvpn client: use constant for running state --- .../leap/bitmaskclient/pluggableTransports/ObfsvpnClient.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 0691c826b..dd16ffea9 100644 --- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsvpnClient.java +++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsvpnClient.java @@ -26,6 +26,7 @@ public class ObfsvpnClient implements EventLogger { 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; @@ -171,7 +172,7 @@ public class ObfsvpnClient implements EventLogger { public void log(String state, String message) { VpnStatus.logDebug("[obfs4-client] " + state + ": " + message); CountDownLatch startCallback = this.startCallback; - if (startCallback != null && "RUNNING".equals(state)) { + if (startCallback != null && STATE_RUNNING.equals(state)) { startCallback.countDown(); this.startCallback = null; } -- GitLab From f6d9b959fab359fbb9847608237f7bb97dab0b5c Mon Sep 17 00:00:00 2001 From: cyBerta <cyberta@riseup.net> Date: Tue, 25 Mar 2025 22:23:53 +0100 Subject: [PATCH 4/8] rename log prefix from 'obfs4-client' to 'obfsvpn-client' --- .../leap/bitmaskclient/pluggableTransports/ObfsvpnClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 dd16ffea9..2e216015a 100644 --- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsvpnClient.java +++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsvpnClient.java @@ -165,12 +165,12 @@ public class ObfsvpnClient implements EventLogger { @Override public void error(String s) { - VpnStatus.logError("[obfs4-client] error: " + 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(); -- GitLab From 96ed81af00650f64006f66d976018cf23251c5f0 Mon Sep 17 00:00:00 2001 From: cyBerta <cyberta@riseup.net> Date: Fri, 11 Apr 2025 16:10:14 +0200 Subject: [PATCH 5/8] update bitmask-core-android, obfsvpn and bitmask-core improvements --- bitmask-core-android | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitmask-core-android b/bitmask-core-android index 72863e67f..64fab2f17 160000 --- a/bitmask-core-android +++ b/bitmask-core-android @@ -1 +1 @@ -Subproject commit 72863e67fab9b037a4aad417c23d3d617425063d +Subproject commit 64fab2f1727d147d473d33761eeb7be94a354eaa -- GitLab From fcb3b143d24327432430809f1ea8e64777f62da9 Mon Sep 17 00:00:00 2001 From: cyBerta <cyberta@riseup.net> Date: Fri, 11 Apr 2025 22:03:44 +0200 Subject: [PATCH 6/8] fix mock test implementation of BitmaskMobileCore interface --- .../testutils/TestSetupHelper.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 974202030..752a53362 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) { -- GitLab From e2d7b1a2cf9f9f67e3c835b8b97ca97fde94c5c0 Mon Sep 17 00:00:00 2001 From: cyBerta <cyberta@riseup.net> Date: Sat, 12 Apr 2025 11:54:58 +0200 Subject: [PATCH 7/8] always close obfspvn client first, then stop running openvpn process --- .../de/blinkt/openvpn/core/OpenVPNService.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 1ed7bc394..1cfb2c277 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java +++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java @@ -378,13 +378,6 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac Connection connection = mProfile.mConnections[0]; VpnStatus.setCurrentlyConnectingProfile(mProfile); - // 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; - // stop old running obfsvpn client if (!stopObfsvpn()) { VpnStatus.logError("Failed to stop already running obfsvpn client"); @@ -392,6 +385,13 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac 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()) { -- GitLab From 3e2850f8339b8dd9b9acc01e9662adbdc3f05234 Mon Sep 17 00:00:00 2001 From: cyBerta <cyberta@riseup.net> Date: Sat, 12 Apr 2025 11:56:41 +0200 Subject: [PATCH 8/8] update VPN state to 'NOPROCESS' (not running) if starting obfsvpn fails while trying to establish an obfuscated connection --- app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java | 3 +++ 1 file changed, 3 insertions(+) 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 1cfb2c277..a82a87d9c 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java +++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java @@ -382,6 +382,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac 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; } @@ -404,6 +405,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac e.printStackTrace(); VpnStatus.logException(e); endVpnService(); + VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED); return; } } @@ -416,6 +418,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } catch (IOException e) { VpnStatus.logException("Error writing config file", e); endVpnService(); + VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED); return; } -- GitLab