diff options
author | Jason A. Donenfeld <Jason@zx2c4.com> | 2020-03-08 16:46:07 +0800 |
---|---|---|
committer | Jason A. Donenfeld <Jason@zx2c4.com> | 2020-03-09 00:19:42 +0800 |
commit | 7c60a0c764fcebb115de61cd1c8e35d34245e972 (patch) | |
tree | bbbaa47a67158eef6dabfb6a9ed59cafe4b94d40 /app | |
parent | c6c3e4356d9b2f7715fa05f1638c20cd21ffa8f1 (diff) |
backend: do not depend on anything except config
This is likely broken but should make for a good starting point.
It also should hopefully handle stopping tunnels before starting new
ones, in the case of the GoBackend. Again, untested.
Diffstat (limited to 'app')
28 files changed, 378 insertions, 274 deletions
diff --git a/app/src/main/java/com/wireguard/android/Application.java b/app/src/main/java/com/wireguard/android/Application.java index 9b40fcd6..0fd00874 100644 --- a/app/src/main/java/com/wireguard/android/Application.java +++ b/app/src/main/java/com/wireguard/android/Application.java @@ -5,6 +5,7 @@ package com.wireguard.android; +import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -19,12 +20,14 @@ import androidx.preference.PreferenceManager; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatDelegate; +import com.wireguard.android.activity.MainActivity; import com.wireguard.android.backend.Backend; import com.wireguard.android.backend.GoBackend; import com.wireguard.android.backend.WgQuickBackend; import com.wireguard.android.configStore.FileConfigStore; import com.wireguard.android.model.TunnelManager; import com.wireguard.android.util.AsyncWorker; +import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.android.util.ModuleLoader; import com.wireguard.android.util.RootShell; import com.wireguard.android.util.ToolsInstaller; @@ -89,8 +92,16 @@ public class Application extends android.app.Application { } catch (final Exception ignored) { } } - if (backend == null) - backend = new GoBackend(app.getApplicationContext()); + if (backend == null) { + final Context context = app.getApplicationContext(); + final Intent configureIntent = new Intent(context, MainActivity.class); + configureIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final PendingIntent pendingConfigureIntent = PendingIntent.getActivity(context, 0, configureIntent, 0); + backend = new GoBackend(context, pendingConfigureIntent); + GoBackend.setAlwaysOnCallback(() -> { + get().tunnelManager.restoreState(true).whenComplete(ExceptionLoggers.D); + }); + } app.backend = backend; } return app.backend; diff --git a/app/src/main/java/com/wireguard/android/QuickTileService.java b/app/src/main/java/com/wireguard/android/QuickTileService.java index 8909beec..66aecec3 100644 --- a/app/src/main/java/com/wireguard/android/QuickTileService.java +++ b/app/src/main/java/com/wireguard/android/QuickTileService.java @@ -21,8 +21,8 @@ import android.util.Log; import com.wireguard.android.activity.MainActivity; import com.wireguard.android.activity.TunnelToggleActivity; -import com.wireguard.android.model.Tunnel; -import com.wireguard.android.model.Tunnel.State; +import com.wireguard.android.backend.Tunnel.State; +import com.wireguard.android.model.ObservableTunnel; import com.wireguard.android.widget.SlashDrawable; import java.util.Objects; @@ -41,7 +41,7 @@ public class QuickTileService extends TileService { private final OnTunnelChangedCallback onTunnelChangedCallback = new OnTunnelChangedCallback(); @Nullable private Icon iconOff; @Nullable private Icon iconOn; - @Nullable private Tunnel tunnel; + @Nullable private ObservableTunnel tunnel; /* This works around an annoying unsolved frameworks bug some people are hitting. */ @Override @@ -121,7 +121,7 @@ public class QuickTileService extends TileService { private void updateTile() { // Update the tunnel. - final Tunnel newTunnel = Application.getTunnelManager().getLastUsedTunnel(); + final ObservableTunnel newTunnel = Application.getTunnelManager().getLastUsedTunnel(); if (newTunnel != tunnel) { if (tunnel != null) tunnel.removeOnPropertyChangedCallback(onStateChangedCallback); @@ -135,7 +135,7 @@ public class QuickTileService extends TileService { final Tile tile = getQsTile(); if (tunnel != null) { label = tunnel.getName(); - state = tunnel.getState() == Tunnel.State.UP ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; + state = tunnel.getState() == State.UP ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; } else { label = getString(R.string.app_name); state = Tile.STATE_INACTIVE; diff --git a/app/src/main/java/com/wireguard/android/activity/BaseActivity.java b/app/src/main/java/com/wireguard/android/activity/BaseActivity.java index b27ca6dd..8ec58ee8 100644 --- a/app/src/main/java/com/wireguard/android/activity/BaseActivity.java +++ b/app/src/main/java/com/wireguard/android/activity/BaseActivity.java @@ -11,7 +11,7 @@ import android.os.Bundle; import androidx.annotation.Nullable; import com.wireguard.android.Application; -import com.wireguard.android.model.Tunnel; +import com.wireguard.android.model.ObservableTunnel; import java.util.Objects; @@ -23,14 +23,14 @@ public abstract class BaseActivity extends ThemeChangeAwareActivity { private static final String KEY_SELECTED_TUNNEL = "selected_tunnel"; private final SelectionChangeRegistry selectionChangeRegistry = new SelectionChangeRegistry(); - @Nullable private Tunnel selectedTunnel; + @Nullable private ObservableTunnel selectedTunnel; public void addOnSelectedTunnelChangedListener(final OnSelectedTunnelChangedListener listener) { selectionChangeRegistry.add(listener); } @Nullable - public Tunnel getSelectedTunnel() { + public ObservableTunnel getSelectedTunnel() { return selectedTunnel; } @@ -60,15 +60,15 @@ public abstract class BaseActivity extends ThemeChangeAwareActivity { super.onSaveInstanceState(outState); } - protected abstract void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel); + protected abstract void onSelectedTunnelChanged(@Nullable ObservableTunnel oldTunnel, @Nullable ObservableTunnel newTunnel); public void removeOnSelectedTunnelChangedListener( final OnSelectedTunnelChangedListener listener) { selectionChangeRegistry.remove(listener); } - public void setSelectedTunnel(@Nullable final Tunnel tunnel) { - final Tunnel oldTunnel = selectedTunnel; + public void setSelectedTunnel(@Nullable final ObservableTunnel tunnel) { + final ObservableTunnel oldTunnel = selectedTunnel; if (Objects.equals(oldTunnel, tunnel)) return; selectedTunnel = tunnel; @@ -77,21 +77,21 @@ public abstract class BaseActivity extends ThemeChangeAwareActivity { } public interface OnSelectedTunnelChangedListener { - void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel); + void onSelectedTunnelChanged(@Nullable ObservableTunnel oldTunnel, @Nullable ObservableTunnel newTunnel); } private static final class SelectionChangeNotifier - extends NotifierCallback<OnSelectedTunnelChangedListener, Tunnel, Tunnel> { + extends NotifierCallback<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel> { @Override public void onNotifyCallback(final OnSelectedTunnelChangedListener listener, - final Tunnel oldTunnel, final int ignored, - final Tunnel newTunnel) { + final ObservableTunnel oldTunnel, final int ignored, + final ObservableTunnel newTunnel) { listener.onSelectedTunnelChanged(oldTunnel, newTunnel); } } private static final class SelectionChangeRegistry - extends CallbackRegistry<OnSelectedTunnelChangedListener, Tunnel, Tunnel> { + extends CallbackRegistry<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel> { private SelectionChangeRegistry() { super(new SelectionChangeNotifier()); } diff --git a/app/src/main/java/com/wireguard/android/activity/MainActivity.java b/app/src/main/java/com/wireguard/android/activity/MainActivity.java index c86669ba..5c9084d0 100644 --- a/app/src/main/java/com/wireguard/android/activity/MainActivity.java +++ b/app/src/main/java/com/wireguard/android/activity/MainActivity.java @@ -21,7 +21,7 @@ import android.widget.LinearLayout; import com.wireguard.android.R; import com.wireguard.android.fragment.TunnelDetailFragment; import com.wireguard.android.fragment.TunnelEditorFragment; -import com.wireguard.android.model.Tunnel; +import com.wireguard.android.model.ObservableTunnel; import java.util.List; @@ -117,8 +117,8 @@ public class MainActivity extends BaseActivity } @Override - protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, - @Nullable final Tunnel newTunnel) { + protected void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel, + @Nullable final ObservableTunnel newTunnel) { final FragmentManager fragmentManager = getSupportFragmentManager(); final int backStackEntries = fragmentManager.getBackStackEntryCount(); if (newTunnel == null) { diff --git a/app/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.java b/app/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.java index 08ea8e7e..c87ec537 100644 --- a/app/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.java +++ b/app/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.java @@ -9,7 +9,7 @@ import android.os.Bundle; import androidx.annotation.Nullable; import com.wireguard.android.fragment.TunnelEditorFragment; -import com.wireguard.android.model.Tunnel; +import com.wireguard.android.model.ObservableTunnel; /** * Standalone activity for creating tunnels. @@ -28,7 +28,7 @@ public class TunnelCreatorActivity extends BaseActivity { } @Override - protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) { + protected void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel, @Nullable final ObservableTunnel newTunnel) { finish(); } } diff --git a/app/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.java b/app/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.java index 69c995fe..09a34bf7 100644 --- a/app/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.java +++ b/app/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.java @@ -19,8 +19,8 @@ import android.widget.Toast; import com.wireguard.android.Application; import com.wireguard.android.QuickTileService; import com.wireguard.android.R; -import com.wireguard.android.model.Tunnel; -import com.wireguard.android.model.Tunnel.State; +import com.wireguard.android.model.ObservableTunnel; +import com.wireguard.android.backend.Tunnel.State; import com.wireguard.android.util.ErrorMessages; @RequiresApi(Build.VERSION_CODES.N) @@ -30,7 +30,7 @@ public class TunnelToggleActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - final Tunnel tunnel = Application.getTunnelManager().getLastUsedTunnel(); + final ObservableTunnel tunnel = Application.getTunnelManager().getLastUsedTunnel(); if (tunnel == null) return; tunnel.setState(State.TOGGLE).whenComplete((v, t) -> { diff --git a/app/src/main/java/com/wireguard/android/backend/Backend.java b/app/src/main/java/com/wireguard/android/backend/Backend.java index a25bfd04..7569b5fa 100644 --- a/app/src/main/java/com/wireguard/android/backend/Backend.java +++ b/app/src/main/java/com/wireguard/android/backend/Backend.java @@ -5,43 +5,32 @@ package com.wireguard.android.backend; -import com.wireguard.android.model.Tunnel; -import com.wireguard.android.model.Tunnel.State; -import com.wireguard.android.model.Tunnel.Statistics; import com.wireguard.config.Config; +import java.util.Collection; import java.util.Set; +import androidx.annotation.Nullable; + /** * Interface for implementations of the WireGuard secure network tunnel. */ public interface Backend { /** - * Update the volatile configuration of a running tunnel and return the resulting configuration. - * If the tunnel is not up, return the configuration that would result (if known), or else - * simply return the given configuration. - * - * @param tunnel The tunnel to apply the configuration to. - * @param config The new configuration for this tunnel. - * @return The updated configuration of the tunnel. - */ - Config applyConfig(Tunnel tunnel, Config config) throws Exception; - - /** - * Enumerate the names of currently-running tunnels. + * Enumerate names of currently-running tunnels. * * @return The set of running tunnel names. */ - Set<String> enumerate(); + Set<String> getRunningTunnelNames(); /** - * Get the actual state of a tunnel. + * Get the state of a tunnel. * * @param tunnel The tunnel to examine the state of. * @return The state of the tunnel. */ - State getState(Tunnel tunnel) throws Exception; + Tunnel.State getState(Tunnel tunnel) throws Exception; /** * Get statistics about traffic and errors on this tunnel. If the tunnel is not running, the @@ -68,12 +57,32 @@ public interface Backend { String getVersion() throws Exception; /** - * Set the state of a tunnel. + * Set the state of a tunnel, updating it's configuration. If the tunnel is already up, config + * may update the running configuration; config may be null when setting the tunnel down. * * @param tunnel The tunnel to control the state of. * @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or * {@code TOGGLE}. + * @param config The configuration for this tunnel, may be null if state is {@code DOWN}. * @return The updated state of the tunnel. */ - State setState(Tunnel tunnel, State state) throws Exception; + Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception; + + interface TunnelStateChangeNotificationReceiver { + void tunnelStateChange(Tunnel tunnel, Tunnel.State state); + } + + /** + * Register a state change notification callback. + * + * @param receiver The receiver object to receive the notification. + */ + void registerStateChangeNotification(TunnelStateChangeNotificationReceiver receiver); + + /** + * Unregister a state change notification callback. + * + * @param receiver The receiver object to no longer receive the notification. + */ + void unregisterStateChangeNotification(TunnelStateChangeNotificationReceiver receiver); } diff --git a/app/src/main/java/com/wireguard/android/backend/GoBackend.java b/app/src/main/java/com/wireguard/android/backend/GoBackend.java index ad45c933..6bbf5a3b 100644 --- a/app/src/main/java/com/wireguard/android/backend/GoBackend.java +++ b/app/src/main/java/com/wireguard/android/backend/GoBackend.java @@ -16,10 +16,7 @@ import android.util.Log; import com.wireguard.android.Application; import com.wireguard.android.R; -import com.wireguard.android.activity.MainActivity; -import com.wireguard.android.model.Tunnel; -import com.wireguard.android.model.Tunnel.State; -import com.wireguard.android.model.Tunnel.Statistics; +import com.wireguard.android.backend.Tunnel.State; import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.android.util.SharedLibraryLoader; import com.wireguard.config.Config; @@ -30,6 +27,7 @@ import com.wireguard.crypto.KeyFormatException; import java.net.InetAddress; import java.util.Collections; +import java.util.HashSet; import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -40,14 +38,26 @@ import java9.util.concurrent.CompletableFuture; public final class GoBackend implements Backend { private static final String TAG = "WireGuard/" + GoBackend.class.getSimpleName(); private static CompletableFuture<VpnService> vpnService = new CompletableFuture<>(); + public interface AlwaysOnCallback { + void alwaysOnTriggered(); + } + @Nullable private static AlwaysOnCallback alwaysOnCallback; + public static void setAlwaysOnCallback(AlwaysOnCallback cb) { + alwaysOnCallback = cb; + } private final Context context; + private final PendingIntent configurationIntent; @Nullable private Tunnel currentTunnel; + @Nullable private Config currentConfig; private int currentTunnelHandle = -1; - public GoBackend(final Context context) { + private final Set<TunnelStateChangeNotificationReceiver> notifiers = new HashSet<>(); + + public GoBackend(final Context context, final PendingIntent configurationIntent) { SharedLibraryLoader.loadSharedLibrary(context, "wg-go"); this.context = context; + this.configurationIntent = configurationIntent; } private static native String wgGetConfig(int handle); @@ -63,23 +73,7 @@ public final class GoBackend implements Backend { private static native String wgVersion(); @Override - public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception { - if (tunnel.getState() == State.UP) { - // Restart the tunnel to apply the new config. - setStateInternal(tunnel, tunnel.getConfig(), State.DOWN); - try { - setStateInternal(tunnel, config, State.UP); - } catch (final Exception e) { - // The new configuration didn't work, so try to go back to the old one. - setStateInternal(tunnel, tunnel.getConfig(), State.UP); - throw e; - } - } - return config; - } - - @Override - public Set<String> enumerate() { + public Set<String> getRunningTunnelNames() { if (currentTunnel != null) { final Set<String> runningTunnels = new ArraySet<>(); runningTunnels.add(currentTunnel.getName()); @@ -147,25 +141,36 @@ public final class GoBackend implements Backend { } @Override - public State setState(final Tunnel tunnel, State state) throws Exception { + public State setState(final Tunnel tunnel, State state, @Nullable final Config config) throws Exception { final State originalState = getState(tunnel); + if (state == State.TOGGLE) state = originalState == State.UP ? State.DOWN : State.UP; - if (state == originalState) + if (state == originalState && tunnel == currentTunnel && config == currentConfig) return originalState; - if (state == State.UP && currentTunnel != null) - throw new IllegalStateException(context.getString(R.string.multiple_tunnels_error)); - Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state); - setStateInternal(tunnel, tunnel.getConfig(), state); + if (state == State.UP) { + final Config originalConfig = currentConfig; + final Tunnel originalTunnel = currentTunnel; + if (currentTunnel != null) + setStateInternal(currentTunnel, null, State.DOWN); + try { + setStateInternal(tunnel, config, state); + } catch(final Exception e) { + if (originalTunnel != null) + setStateInternal(originalTunnel, originalConfig, State.UP); + throw e; + } + } else if (state == State.DOWN && tunnel == currentTunnel) { + setStateInternal(tunnel, null, State.DOWN); + } return getState(tunnel); } private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception { + Log.i(TAG, "Bringing tunnel " + tunnel.getName() + " " + state); if (state == State.UP) { - Log.i(TAG, "Bringing tunnel up"); - Objects.requireNonNull(config, context.getString(R.string.no_config_error)); if (VpnService.prepare(context) != null) @@ -180,6 +185,7 @@ public final class GoBackend implements Backend { } catch (final TimeoutException e) { throw new Exception(context.getString(R.string.vpn_start_error), e); } + service.setOwner(this); if (currentTunnelHandle != -1) { Log.w(TAG, "Tunnel already up"); @@ -193,9 +199,7 @@ public final class GoBackend implements Backend { final VpnService.Builder builder = service.getBuilder(); builder.setSession(tunnel.getName()); - final Intent configureIntent = new Intent(context, MainActivity.class); - configureIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - builder.setConfigureIntent(PendingIntent.getActivity(context, 0, configureIntent, 0)); + builder.setConfigureIntent(configurationIntent); for (final String excludedApplication : config.getInterface().getExcludedApplications()) builder.addDisallowedApplication(excludedApplication); @@ -229,12 +233,11 @@ public final class GoBackend implements Backend { throw new Exception(context.getString(R.string.tunnel_on_error, currentTunnelHandle)); currentTunnel = tunnel; + currentConfig = config; service.protect(wgGetSocketV4(currentTunnelHandle)); service.protect(wgGetSocketV6(currentTunnelHandle)); } else { - Log.i(TAG, "Bringing tunnel down"); - if (currentTunnelHandle == -1) { Log.w(TAG, "Tunnel already down"); return; @@ -243,7 +246,11 @@ public final class GoBackend implements Backend { wgTurnOff(currentTunnelHandle); currentTunnel = null; currentTunnelHandle = -1; + currentConfig = null; } + + for (final TunnelStateChangeNotificationReceiver notifier : notifiers) + notifier.tunnelStateChange(tunnel, state); } private void startVpnService() { @@ -251,7 +258,23 @@ public final class GoBackend implements Backend { context.startService(new Intent(context, VpnService.class)); } + @Override + public void registerStateChangeNotification(final TunnelStateChangeNotificationReceiver receiver) { + notifiers.add(receiver); + } + + @Override + public void unregisterStateChangeNotification(final TunnelStateChangeNotificationReceiver receiver) { + notifiers.remove(receiver); + } + public static class VpnService extends android.net.VpnService { + @Nullable private GoBackend owner; + + public void setOwner(final GoBackend owner) { + this.owner = owner; + } + public Builder getBuilder() { return new Builder(); } @@ -264,13 +287,18 @@ public final class GoBackend implements Backend { @Override public void onDestroy() { - Application.getTunnelManager().getTunnels().thenAccept(tunnels -> { - for (final Tunnel tunnel : tunnels) { - if (tunnel != null && tunnel.getState() != State.DOWN) - tunnel.setState(State.DOWN); + if (owner != null) { + final Tunnel tunnel = owner.currentTunnel; + if (tunnel != null) { + if (owner.currentTunnelHandle != -1) + wgTurnOff(owner.currentTunnelHandle); + owner.currentTunnel = null; + owner.currentTunnelHandle = -1; + owner.currentConfig = null; + for (final TunnelStateChangeNotificationReceiver notifier : owner.notifiers) + notifier.tunnelStateChange(tunnel, State.DOWN); } - }); - + } vpnService = vpnService.newIncompleteFuture(); super.onDestroy(); } @@ -280,10 +308,10 @@ public final class GoBackend implements Backend { vpnService.complete(this); if (intent == null || intent.getComponent() == null || !intent.getComponent().getPackageName().equals(getPackageName())) { Log.d(TAG, "Service started by Always-on VPN feature"); - Application.getTunnelManager().restoreState(true).whenComplete(ExceptionLoggers.D); + if (alwaysOnCallback != null) + alwaysOnCallback.alwaysOnTriggered(); } return super.onStartCommand(intent, flags, startId); } - } } diff --git a/app/src/main/java/com/wireguard/android/backend/Statistics.java b/app/src/main/java/com/wireguard/android/backend/Statistics.java new file mode 100644 index 00000000..2ca87d23 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/backend/Statistics.java @@ -0,0 +1,62 @@ +/* + * Copyright © 2020 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.backend; + +import android.os.SystemClock; +import android.util.Pair; + +import com.wireguard.crypto.Key; + +import java.util.HashMap; +import java.util.Map; + +public class Statistics { + private long lastTouched = SystemClock.elapsedRealtime(); + private final Map<Key, Pair<Long, Long>> peerBytes = new HashMap<>(); + + Statistics() { } + + void add(final Key key, final long rx, final long tx) { + peerBytes.put(key, Pair.create(rx, tx)); + lastTouched = SystemClock.elapsedRealtime(); + } + + public boolean isStale() { + return SystemClock.elapsedRealtime() - lastTouched > 900; + } + + public Key[] peers() { + return peerBytes.keySet().toArray(new Key[0]); + } + + public long peerRx(final Key peer) { + if (!peerBytes.containsKey(peer)) + return 0; + return peerBytes.get(peer).first; + } + + public long peerTx(final Key peer) { + if (!peerBytes.containsKey(peer)) + return 0; + return peerBytes.get(peer).second; + } + + public long totalRx() { + long rx = 0; + for (final Pair<Long, Long> val : peerBytes.values()) { + rx += val.first; + } + return rx; + } + + public long totalTx() { + long tx = 0; + for (final Pair<Long, Long> val : peerBytes.values()) { + tx += val.second; + } + return tx; + } +} diff --git a/app/src/main/java/com/wireguard/android/backend/Tunnel.java b/app/src/main/java/com/wireguard/android/backend/Tunnel.java new file mode 100644 index 00000000..86661355 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/backend/Tunnel.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2020 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.backend; + +import java.util.regex.Pattern; + +/** + * Represents a WireGuard tunnel. + */ + +public interface Tunnel { + enum State { + DOWN, + TOGGLE, + UP; + + public static State of(final boolean running) { + return running ? UP : DOWN; + } + } + + int NAME_MAX_LENGTH = 15; + Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,15}"); + + static boolean isNameInvalid(final CharSequence name) { + return !NAME_PATTERN.matcher(name).matches(); + } + + String getName(); +} diff --git a/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java b/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java index 99b90af2..e04e6658 100644 --- a/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java +++ b/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java @@ -11,9 +11,7 @@ import android.util.Log; import com.wireguard.android.Application; import com.wireguard.android.R; -import com.wireguard.android.model.Tunnel; -import com.wireguard.android.model.Tunnel.State; -import com.wireguard.android.model.Tunnel.Statistics; +import com.wireguard.android.backend.Tunnel.State; import com.wireguard.config.Config; import com.wireguard.crypto.Key; @@ -23,10 +21,13 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.HashMap; import java9.util.stream.Collectors; import java9.util.stream.Stream; @@ -40,6 +41,8 @@ public final class WgQuickBackend implements Backend { private final File localTemporaryDir; private final Context context; + private final Map<Tunnel, Config> runningConfigs = new HashMap<>(); + private final Set<TunnelStateChangeNotificationReceiver> notifiers = new HashSet<>(); public WgQuickBackend(final Context context) { localTemporaryDir = new File(context.getCacheDir(), "tmp"); @@ -47,23 +50,7 @@ public final class WgQuickBackend implements Backend { } @Override - public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception { - if (tunnel.getState() == State.UP) { - // Restart the tunnel to apply the new config. - setStateInternal(tunnel, tunnel.getConfig(), State.DOWN); - try { - setStateInternal(tunnel, config, State.UP); - } catch (final Exception e) { - // The new configuration didn't work, so try to go back to the old one. - setStateInternal(tunnel, tunnel.getConfig(), State.UP); - throw e; - } - } - return config; - } - - @Override - public Set<String> enumerate() { + public Set<String> getRunningTunnelNames() { final List<String> output = new ArrayList<>(); // Don't throw an exception here or nothing will show up in the UI. try { @@ -80,7 +67,7 @@ public final class WgQuickBackend implements Backend { @Override public State getState(final Tunnel tunnel) { - return enumerate().contains(tunnel.getName()) ? State.UP : State.DOWN; + return getRunningTunnelNames().contains(tunnel.getName()) ? State.UP : State.DOWN; } @Override @@ -120,20 +107,36 @@ public final class WgQuickBackend implements Backend { } @Override - public State setState(final Tunnel tunnel, State state) throws Exception { + public State setState(final Tunnel tunnel, State state, @Nullable final Config config) throws Exception { final State originalState = getState(tunnel); + final Config originalConfig = runningConfigs.get(tunnel); + if (state == State.TOGGLE) state = originalState == State.UP ? State.DOWN : State.UP; - if (state == originalState) + if ((state == State.UP && originalState == State.UP && originalConfig != null && originalConfig == config) || + (state == State.DOWN && originalState == State.DOWN)) return originalState; - Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state); - Application.getToolsInstaller().ensureToolsAvailable(); - setStateInternal(tunnel, tunnel.getConfig(), state); - return getState(tunnel); + if (state == State.UP) { + Application.getToolsInstaller().ensureToolsAvailable(); + if (originalState == State.UP) + setStateInternal(tunnel, originalConfig == null ? config : originalConfig, State.DOWN); + try { + setStateInternal(tunnel, config, State.UP); + } catch(final Exception e) { + if (originalState == State.UP && originalConfig != null) + setStateInternal(tunnel, originalConfig, State.UP); + throw e; + } + } else if (state == State.DOWN) { + setStateInternal(tunnel, originalConfig == null ? config : originalConfig, State.DOWN); + } + return state; } private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception { - Objects.requireNonNull(config, "Trying to set state with a null config"); + Log.i(TAG, "Bringing tunnel " + tunnel.getName() + " " + state); + + Objects.requireNonNull(config, "Trying to set state up with a null config"); final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf"); try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) { @@ -148,5 +151,23 @@ public final class WgQuickBackend implements Backend { tempFile.delete(); if (result != 0) throw new Exception(context.getString(R.string.tunnel_config_error, result)); + + if (state == State.UP) + runningConfigs.put(tunnel, config); + else + runningConfigs.remove(tunnel); + + for (final TunnelStateChangeNotificationReceiver notifier : notifiers) + notifier.tunnelStateChange(tunnel, state); + } + + @Override + public void registerStateChangeNotification(final TunnelStateChangeNotificationReceiver receiver) { + notifiers.add(receiver); + } + + @Override + public void unregisterStateChangeNotification(final TunnelStateChangeNotificationReceiver receiver) { + notifiers.remove(receiver); } } diff --git a/app/src/main/java/com/wireguard/android/fragment/BaseFragment.java b/app/src/main/java/com/wireguard/android/fragment/BaseFragment.java index 00e26348..11af26e4 100644 --- a/app/src/main/java/com/wireguard/android/fragment/BaseFragment.java +++ b/app/src/main/java/com/wireguard/android/fragment/BaseFragment.java @@ -23,8 +23,8 @@ import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListen import com.wireguard.android.backend.GoBackend; import com.wireguard.android.databinding.TunnelDetailFragmentBinding; import com.wireguard.android.databinding.TunnelListItemBinding; -import com.wireguard.android.model.Tunnel; -import com.wireguard.android.model.Tunnel.State; +import com.wireguard.android.model.ObservableTunnel; +import com.wireguard.android.backend.Tunnel.State; import com.wireguard.android.util.ErrorMessages; /** @@ -36,11 +36,11 @@ public abstract class BaseFragment extends Fragment implements OnSelectedTunnelC private static final int REQUEST_CODE_VPN_PERMISSION = 23491; private static final String TAG = "WireGuard/" + BaseFragment.class.getSimpleName(); @Nullable private BaseActivity activity; - @Nullable private Tunnel pendingTunnel; + @Nullable private ObservableTunnel pendingTunnel; @Nullable private Boolean pendingTunnelUp; @Nullable - protected Tunnel getSelectedTunnel() { + protected ObservableTunnel getSelectedTunnel() { return activity != null ? activity.getSelectedTunnel() : null; } @@ -75,14 +75,14 @@ public abstract class BaseFragment extends Fragment implements OnSelectedTunnelC super.onDetach(); } - protected void setSelectedTunnel(@Nullable final Tunnel tunnel) { + protected void setSelectedTunnel(@Nullable final ObservableTunnel tunnel) { if (activity != null) activity.setSelectedTunnel(tunnel); } public void setTunnelState(final View view, final boolean checked) { final ViewDataBinding binding = DataBindingUtil.findBinding(view); - final Tunnel tunnel; + final ObservableTunnel tunnel; if (binding instanceof TunnelDetailFragmentBinding) tunnel = ((TunnelDetailFragmentBinding) binding).getTunnel(); else if (binding instanceof TunnelListItemBinding) @@ -107,7 +107,7 @@ public abstract class BaseFragment extends Fragment implements OnSelectedTunnelC }); } - private void setTunnelStateWithPermissionsResult(final Tunnel tunnel, final boolean checked) { + private void setTunnelStateWithPermissionsResult(final ObservableTunnel tunnel, final boolean checked) { tunnel.setState(State.of(checked)).whenComplete((state, throwable) -> { if (throwable == null) return; diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java index 57e0d8ea..89fe6c77 100644 --- a/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java +++ b/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java @@ -18,8 +18,8 @@ import android.view.ViewGroup; import com.wireguard.android.R; import com.wireguard.android.databinding.TunnelDetailFragmentBinding; import com.wireguard.android.databinding.TunnelDetailPeerBinding; -import com.wireguard.android.model.Tunnel; -import com.wireguard.android.model.Tunnel.State; +import com.wireguard.android.model.ObservableTunnel; +import com.wireguard.android.backend.Tunnel.State; import com.wireguard.android.ui.EdgeToEdge; import com.wireguard.crypto.Key; @@ -85,7 +85,7 @@ public class TunnelDetailFragment extends BaseFragment { } @Override - public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) { + public void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel, @Nullable final ObservableTunnel newTunnel) { if (binding == null) return; binding.setTunnel(newTunnel); @@ -123,7 +123,7 @@ public class TunnelDetailFragment extends BaseFragment { private void updateStats() { if (binding == null || !isResumed()) return; - final Tunnel tunnel = binding.getTunnel(); + final ObservableTunnel tunnel = binding.getTunnel(); if (tunnel == null) return; final State state = tunnel.getState(); diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java index a93fabf6..b342d5a8 100644 --- a/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java +++ b/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java @@ -11,7 +11,7 @@ import androidx.databinding.ObservableList; import android.os.Bundle; import androidx.annotation.Nullable; import com.google.android.material.snackbar.Snackbar; -import androidx.fragment.app.FragmentManager; + import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -26,7 +26,7 @@ import com.wireguard.android.Application; import com.wireguard.android.R; import com.wireguard.android.databinding.TunnelEditorFragmentBinding; import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener; -import com.wireguard.android.model.Tunnel; +import com.wireguard.android.model.ObservableTunnel; import com.wireguard.android.model.TunnelManager; import com.wireguard.android.ui.EdgeToEdge; import com.wireguard.android.util.ErrorMessages; @@ -47,7 +47,7 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi private static final String TAG = "WireGuard/" + TunnelEditorFragment.class.getSimpleName(); @Nullable private TunnelEditorFragmentBinding binding; - @Nullable private Tunnel tunnel; + @Nullable private ObservableTunnel tunnel; private void onConfigLoaded(final Config config) { if (binding != null) { @@ -55,7 +55,7 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi } } - private void onConfigSaved(final Tunnel savedTunnel, + private void onConfigSaved(final ObservableTunnel savedTunnel, @Nullable final Throwable throwable) { final String message; if (throwable == null) { @@ -126,7 +126,7 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi getActivity().runOnUiThread(() -> { // TODO(smaeul): Remove this hack when fixing the Config ViewModel // The selected tunnel has to actually change, but we have to remember this one. - final Tunnel savedTunnel = tunnel; + final ObservableTunnel savedTunnel = tunnel; if (savedTunnel == getSelectedTunnel()) setSelectedTunnel(null); setSelectedTunnel(savedTunnel); @@ -187,8 +187,8 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi } @Override - public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, - @Nullable final Tunnel newTunnel) { + public void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel, + @Nullable final ObservableTunnel newTunnel) { tunnel = newTunnel; if (binding == null) return; @@ -201,7 +201,7 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi } } - private void onTunnelCreated(final Tunnel newTunnel, @Nullable final Throwable throwable) { + private void onTunnelCreated(final ObservableTunnel newTunnel, @Nullable final Throwable throwable) { final String message; if (throwable == null) { tunnel = newTunnel; @@ -219,7 +219,7 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi } } - private void onTunnelRenamed(final Tunnel renamedTunnel, final Config newConfig, + private void onTunnelRenamed(final ObservableTunnel renamedTunnel, final Config newConfig, @Nullable final Throwable throwable) { final String message; if (throwable == null) { diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java index fe8a3da1..21618e60 100644 --- a/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java +++ b/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java @@ -17,7 +17,7 @@ import android.provider.OpenableColumns; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.material.snackbar.Snackbar; -import androidx.fragment.app.FragmentManager; + import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.view.ActionMode; import androidx.recyclerview.widget.RecyclerView; @@ -36,7 +36,7 @@ import com.wireguard.android.activity.TunnelCreatorActivity; import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter; import com.wireguard.android.databinding.TunnelListFragmentBinding; import com.wireguard.android.databinding.TunnelListItemBinding; -import com.wireguard.android.model.Tunnel; +import com.wireguard.android.model.ObservableTunnel; import com.wireguard.android.ui.EdgeToEdge; import com.wireguard.android.util.ErrorMessages; import com.wireguard.android.widget.MultiselectableRelativeLayout; @@ -91,7 +91,7 @@ public class TunnelListFragment extends BaseFragment { return; final ContentResolver contentResolver = activity.getContentResolver(); - final Collection<CompletableFuture<Tunnel>> futureTunnels = new ArrayList<>(); + final Collection<CompletableFuture<ObservableTunnel>> futureTunnels = new ArrayList<>(); final List<Throwable> throwables = new ArrayList<>(); Application.getAsyncWorker().supplyAsync(() -> { final String[] columns = {OpenableColumns.DISPLAY_NAME}; @@ -161,9 +161,9 @@ public class TunnelListFragment extends BaseFragment { onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception)); } else { future.whenComplete((ignored1, ignored2) -> { - final List<Tunnel> tunnels = new ArrayList<>(futureTunnels.size()); - for (final CompletableFuture<Tunnel> futureTunnel : futureTunnels) { - Tunnel tunnel = null; + final List<ObservableTunnel> tunnels = new ArrayList<>(futureTunnels.size()); + for (final CompletableFuture<ObservableTunnel> futureTunnel : futureTunnels) { + ObservableTunnel tunnel = null; try { tunnel = futureTunnel.getNow(null); } catch (final Exception e) { @@ -250,7 +250,7 @@ public class TunnelListFragment extends BaseFragment { } @Override - public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) { + public void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel, @Nullable final ObservableTunnel newTunnel) { if (binding == null) return; Application.getTunnelManager().getTunnels().thenAccept(tunnels -> { @@ -281,7 +281,7 @@ public class TunnelListFragment extends BaseFragment { showSnackbar(message); } - private void onTunnelImportFinished(final List<Tunnel> tunnels, final Collection<Throwable> throwables) { + private void onTunnelImportFinished(final List<ObservableTunnel> tunnels, final Collection<Throwable> throwables) { String message = null; for (final Throwable throwable : throwables) { @@ -315,7 +315,7 @@ public class TunnelListFragment extends BaseFragment { binding.setFragment(this); Application.getTunnelManager().getTunnels().thenAccept(binding::setTunnels); - binding.setRowConfigurationHandler((ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TunnelListItemBinding, Tunnel>) (binding, tunnel, position) -> { + binding.setRowConfigurationHandler((ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TunnelListItemBinding, ObservableTunnel>) (binding, tunnel, position) -> { binding.setFragment(this); binding.getRoot().setOnClickListener(clicked -> { if (actionMode == null) { @@ -336,7 +336,7 @@ public class TunnelListFragment extends BaseFragment { }); } - private MultiselectableRelativeLayout viewForTunnel(final Tunnel tunnel, final List tunnels) { + private MultiselectableRelativeLayout viewForTunnel(final ObservableTunnel tunnel, final List tunnels) { return (MultiselectableRelativeLayout) binding.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel)).itemView; } @@ -355,12 +355,12 @@ public class TunnelListFragment extends BaseFragment { case R.id.menu_action_delete: final Iterable<Integer> copyCheckedItems = new HashSet<>(checkedItems); Application.getTunnelManager().getTunnels().thenAccept(tunnels -> { - final Collection<Tunnel> tunnelsToDelete = new ArrayList<>(); + final Collection<ObservableTunnel> tunnelsToDelete = new ArrayList<>(); for (final Integer position : copyCheckedItems) tunnelsToDelete.add(tunnels.get(position)); final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete) - .map(Tunnel::delete) + .map(ObservableTunnel::delete) .toArray(CompletableFuture[]::new); CompletableFuture.allOf(futures) .thenApply(x -> futures.length) diff --git a/app/src/main/java/com/wireguard/android/model/Tunnel.java b/app/src/main/java/com/wireguard/android/model/ObservableTunnel.java index 87b607d0..826a6a2a 100644 --- a/app/src/main/java/com/wireguard/android/model/Tunnel.java +++ b/app/src/main/java/com/wireguard/android/model/ObservableTunnel.java @@ -5,23 +5,17 @@ package com.wireguard.android.model; -import android.os.SystemClock; -import android.util.Pair; - import androidx.databinding.BaseObservable; import androidx.databinding.Bindable; import androidx.annotation.Nullable; import com.wireguard.android.BR; +import com.wireguard.android.backend.Statistics; +import com.wireguard.android.backend.Tunnel; import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.config.Config; -import com.wireguard.crypto.Key; import com.wireguard.util.Keyed; -import java.util.HashMap; -import java.util.Map; -import java.util.regex.Pattern; - import java9.util.concurrent.CompletableFuture; import java9.util.concurrent.CompletionStage; @@ -29,28 +23,21 @@ import java9.util.concurrent.CompletionStage; * Encapsulates the volatile and nonvolatile state of a WireGuard tunnel. */ -public class Tunnel extends BaseObservable implements Keyed<String> { - public static final int NAME_MAX_LENGTH = 15; - private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,15}"); - +public class ObservableTunnel extends BaseObservable implements Keyed<String>, Tunnel { private final TunnelManager manager; @Nullable private Config config; - private String name; private State state; + private String name; @Nullable private Statistics statistics; - Tunnel(final TunnelManager manager, final String name, + ObservableTunnel(final TunnelManager manager, final String name, @Nullable final Config config, final State state) { - this.manager = manager; this.name = name; + this.manager = manager; this.config = config; this.state = state; } - public static boolean isNameInvalid(final CharSequence name) { - return !NAME_PATTERN.matcher(name).matches(); - } - public CompletionStage<Void> delete() { return manager.delete(this); } @@ -74,6 +61,7 @@ public class Tunnel extends BaseObservable implements Keyed<String> { return name; } + @Override @Bindable public String getName() { return name; @@ -146,60 +134,4 @@ public class Tunnel extends BaseObservable implements Keyed<String> { return manager.setTunnelState(this, state); return CompletableFuture.completedFuture(this.state); } - - public enum State { - DOWN, - TOGGLE, - UP; - - public static State of(final boolean running) { - return running ? UP : DOWN; - } - } - - public static class Statistics extends BaseObservable { - private long lastTouched = SystemClock.elapsedRealtime(); - private final Map<Key, Pair<Long, Long>> peerBytes = new HashMap<>(); - - public void add(final Key key, final long rx, final long tx) { - peerBytes.put(key, Pair.create(rx, tx)); - lastTouched = SystemClock.elapsedRealtime(); - } - - private boolean isStale() { - return SystemClock.elapsedRealtime() - lastTouched > 900; - } - - public Key[] peers() { - return peerBytes.keySet().toArray(new Key[0]); - } - - public long peerRx(final Key peer) { - if (!peerBytes.containsKey(peer)) - return 0; - return peerBytes.get(peer).first; - } - - public long peerTx(final Key peer) { - if (!peerBytes.containsKey(peer)) - return 0; - return peerBytes.get(peer).second; - } - - public long totalRx() { - long rx = 0; - for (final Pair<Long, Long> val : peerBytes.values()) { - rx += val.first; - } - return rx; - } - - public long totalTx() { - long tx = 0; - for (final Pair<Long, Long> val : peerBytes.values()) { - tx += val.second; - } - return tx; - } - } } diff --git a/app/src/main/java/com/wireguard/android/model/TunnelManager.java b/app/src/main/java/com/wireguard/android/model/TunnelManager.java index 2deea6df..e7bb9cd5 100644 --- a/app/src/main/java/com/wireguard/android/model/TunnelManager.java +++ b/app/src/main/java/com/wireguard/android/model/TunnelManager.java @@ -15,9 +15,11 @@ import androidx.annotation.Nullable; import com.wireguard.android.Application; import com.wireguard.android.BR; import com.wireguard.android.R; +import com.wireguard.android.backend.Backend.TunnelStateChangeNotificationReceiver; import com.wireguard.android.configStore.ConfigStore; -import com.wireguard.android.model.Tunnel.State; -import com.wireguard.android.model.Tunnel.Statistics; +import com.wireguard.android.backend.Tunnel; +import com.wireguard.android.backend.Tunnel.State; +import com.wireguard.android.backend.Statistics; import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.android.util.ObservableSortedKeyedArrayList; import com.wireguard.android.util.ObservableSortedKeyedList; @@ -38,42 +40,49 @@ import java9.util.stream.StreamSupport; * Maintains and mediates changes to the set of available WireGuard tunnels, */ -public final class TunnelManager extends BaseObservable { +public final class TunnelManager extends BaseObservable implements TunnelStateChangeNotificationReceiver { private static final Comparator<String> COMPARATOR = Comparators.<String>thenComparing( String.CASE_INSENSITIVE_ORDER, Comparators.naturalOrder()); private static final String KEY_LAST_USED_TUNNEL = "last_used_tunnel"; private static final String KEY_RESTORE_ON_BOOT = "restore_on_boot"; private static final String KEY_RUNNING_TUNNELS = "enabled_configs"; - private final CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> completableTunnels = new CompletableFuture<>(); + private final CompletableFuture<ObservableSortedKeyedList<String, ObservableTunnel>> completableTunnels = new CompletableFuture<>(); private final ConfigStore configStore; private final Context context = Application.get(); private final ArrayList<CompletableFuture<Void>> delayedLoadRestoreTunnels = new ArrayList<>(); - private final ObservableSortedKeyedList<String, Tunnel> tunnels = new ObservableSortedKeyedArrayList<>(COMPARATOR); + private final ObservableSortedKeyedList<String, ObservableTunnel> tunnels = new ObservableSortedKeyedArrayList<>(COMPARATOR); private boolean haveLoaded; - @Nullable private Tunnel lastUsedTunnel; + @Nullable private ObservableTunnel lastUsedTunnel; public TunnelManager(final ConfigStore configStore) { this.configStore = configStore; + Application.getBackendAsync().thenAccept(backend -> backend.registerStateChangeNotification(this)); } - static CompletionStage<State> getTunnelState(final Tunnel tunnel) { + @Override + protected void finalize() throws Throwable { + Application.getBackendAsync().thenAccept(backend -> backend.unregisterStateChangeNotification(this)); + super.finalize(); + } + + static CompletionStage<State> getTunnelState(final ObservableTunnel tunnel) { return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getState(tunnel)) .thenApply(tunnel::onStateChanged); } - static CompletionStage<Statistics> getTunnelStatistics(final Tunnel tunnel) { + static CompletionStage<Statistics> getTunnelStatistics(final ObservableTunnel tunnel) { return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getStatistics(tunnel)) .thenApply(tunnel::onStatisticsChanged); } - private Tunnel addToList(final String name, @Nullable final Config config, final State state) { - final Tunnel tunnel = new Tunnel(this, name, config, state); + private ObservableTunnel addToList(final String name, @Nullable final Config config, final State state) { + final ObservableTunnel tunnel = new ObservableTunnel(this, name, config, state); tunnels.add(tunnel); return tunnel; } - public CompletionStage<Tunnel> create(final String name, @Nullable final Config config) { + public CompletionStage<ObservableTunnel> create(final String name, @Nullable final Config config) { if (Tunnel.isNameInvalid(name)) return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name))); if (tunnels.containsKey(name)) { @@ -84,7 +93,7 @@ public final class TunnelManager extends BaseObservable { .thenApply(savedConfig -> addToList(name, savedConfig, State.DOWN)); } - CompletionStage<Void> delete(final Tunnel tunnel) { + CompletionStage<Void> delete(final ObservableTunnel tunnel) { final State originalState = tunnel.getState(); final boolean wasLastUsed = tunnel == lastUsedTunnel; // Make sure nothing touches the tunnel. @@ -93,12 +102,12 @@ public final class TunnelManager extends BaseObservable { tunnels.remove(tunnel); return Application.getAsyncWorker().runAsync(() -> { if (originalState == State.UP) - Application.getBackend().setState(tunnel, State.DOWN); + Application.getBackend().setState(tunnel, State.DOWN, null); try { configStore.delete(tunnel.getName()); } catch (final Exception e) { if (originalState == State.UP) - Application.getBackend().setState(tunnel, State.UP); + Application.getBackend().setState(tunnel, State.UP, tunnel.getConfig()); // Re-throw the exception to fail the completion. throw e; } @@ -114,22 +123,22 @@ public final class TunnelManager extends BaseObservable { @Bindable @Nullable - public Tunnel getLastUsedTunnel() { + public ObservableTunnel getLastUsedTunnel() { return lastUsedTunnel; } - CompletionStage<Config> getTunnelConfig(final Tunnel tunnel) { + CompletionStage<Config> getTunnelConfig(final ObservableTunnel tunnel) { return Application.getAsyncWorker().supplyAsync(() -> configStore.load(tunnel.getName())) .thenApply(tunnel::onConfigChanged); } - public CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> getTunnels() { + public CompletableFuture<ObservableSortedKeyedList<String, ObservableTunnel>> getTunnels() { return completableTunnels; } public void onCreate() { Application.getAsyncWorker().supplyAsync(configStore::enumerate) - .thenAcceptBoth(Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().enumerate()), this::onTunnelsLoaded) + .thenAcceptBoth(Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getRunningTunnelNames()), this::onTunnelsLoaded) .whenComplete(ExceptionLoggers.E); } @@ -159,9 +168,9 @@ public final class TunnelManager extends BaseObservable { } public void refreshTunnelStates() { - Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().enumerate()) + Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getRunningTunnelNames()) .thenAccept(running -> { - for (final Tunnel tunnel : tunnels) + for (final ObservableTunnel tunnel : tunnels) tunnel.onStateChanged(running.contains(tunnel.getName()) ? State.UP : State.DOWN); }) .whenComplete(ExceptionLoggers.E); @@ -189,12 +198,12 @@ public final class TunnelManager extends BaseObservable { public void saveState() { final Set<String> runningTunnels = StreamSupport.stream(tunnels) .filter(tunnel -> tunnel.getState() == State.UP) - .map(Tunnel::getName) + .map(ObservableTunnel::getName) .collect(Collectors.toUnmodifiableSet()); Application.getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, runningTunnels).apply(); } - private void setLastUsedTunnel(@Nullable final Tunnel tunnel) { + private void setLastUsedTunnel(@Nullable final ObservableTunnel tunnel) { if (tunnel == lastUsedTunnel) return; lastUsedTunnel = tunnel; @@ -205,14 +214,14 @@ public final class TunnelManager extends BaseObservable { Application.getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).apply(); } - CompletionStage<Config> setTunnelConfig(final Tunnel tunnel, final Config config) { + CompletionStage<Config> setTunnelConfig(final ObservableTunnel tunnel, final Config config) { return Application.getAsyncWorker().supplyAsync(() -> { - final Config appliedConfig = Application.getBackend().applyConfig(tunnel, config); - return configStore.save(tunnel.getName(), appliedConfig); + Application.getBackend().setState(tunnel, tunnel.getState(), config); + return configStore.save(tunnel.getName(), config); }).thenApply(tunnel::onConfigChanged); } - CompletionStage<String> setTunnelName(final Tunnel tunnel, final String name) { + CompletionStage<String> setTunnelName(final ObservableTunnel tunnel, final String name) { if (Tunnel.isNameInvalid(name)) return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name))); if (tunnels.containsKey(name)) { @@ -227,11 +236,11 @@ public final class TunnelManager extends BaseObservable { tunnels.remove(tunnel); return Application.getAsyncWorker().supplyAsync(() -> { if (originalState == State.UP) - Application.getBackend().setState(tunnel, State.DOWN); + Application.getBackend().setState(tunnel, State.DOWN, null); configStore.rename(tunnel.getName(), name); final String newName = tunnel.onNameChanged(name); if (originalState == State.UP) - Application.getBackend().setState(tunnel, State.UP); + Application.getBackend().setState(tunnel, State.UP, tunnel.getConfig()); return newName; }).whenComplete((newName, e) -> { // On failure, we don't know what state the tunnel might be in. Fix that. @@ -244,10 +253,10 @@ public final class TunnelManager extends BaseObservable { }); } - CompletionStage<State> setTunnelState(final Tunnel tunnel, final State state) { + CompletionStage<State> setTunnelState(final ObservableTunnel tunnel, final State state) { // Ensure the configuration is loaded before trying to use it. - return tunnel.getConfigAsync().thenCompose(x -> - Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().setState(tunnel, state)) + return tunnel.getConfigAsync().thenCompose(config -> + Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().setState(tunnel, state, config)) ).whenComplete((newState, e) -> { // Ensure onStateChanged is always called (failure or not), and with the correct state. tunnel.onStateChanged(e == null ? newState : tunnel.getState()); @@ -257,6 +266,11 @@ public final class TunnelManager extends BaseObservable { }); } + @Override + public void tunnelStateChange(final Tunnel tunnel, final State state) { + ((ObservableTunnel)tunnel).onStateChanged(state); + } + public static final class IntentReceiver extends BroadcastReceiver { @Override public void onReceive(final Context context, @Nullable final Intent intent) { @@ -290,7 +304,7 @@ public final class TunnelManager extends BaseObservable { if (tunnelName == null) return; manager.getTunnels().thenAccept(tunnels -> { - final Tunnel tunnel = tunnels.get(tunnelName); + final ObservableTunnel tunnel = tunnels.get(tunnelName); if (tunnel == null) return; manager.setTunnelState(tunnel, state); diff --git a/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java b/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java index efda91bb..3af412a5 100644 --- a/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java +++ b/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java @@ -16,7 +16,7 @@ import android.util.Log; import com.wireguard.android.Application; import com.wireguard.android.R; -import com.wireguard.android.model.Tunnel; +import com.wireguard.android.model.ObservableTunnel; import com.wireguard.android.util.DownloadsFileSaver; import com.wireguard.android.util.DownloadsFileSaver.DownloadsFile; import com.wireguard.android.util.ErrorMessages; @@ -48,9 +48,9 @@ public class ZipExporterPreference extends Preference { Application.getTunnelManager().getTunnels().thenAccept(this::exportZip); } - private void exportZip(final List<Tunnel> tunnels) { + private void exportZip(final List<ObservableTunnel> tunnels) { final List<CompletableFuture<Config>> futureConfigs = new ArrayList<>(tunnels.size()); - for (final Tunnel tunnel : tunnels) + for (final ObservableTunnel tunnel : tunnels) futureConfigs.add(tunnel.getConfigAsync().toCompletableFuture()); if (futureConfigs.isEmpty()) { exportZipComplete(null, new IllegalArgumentException( diff --git a/app/src/main/java/com/wireguard/android/widget/NameInputFilter.java b/app/src/main/java/com/wireguard/android/widget/NameInputFilter.java index 07759a18..030be25a 100644 --- a/app/src/main/java/com/wireguard/android/widget/NameInputFilter.java +++ b/app/src/main/java/com/wireguard/android/widget/NameInputFilter.java @@ -10,7 +10,7 @@ import android.text.InputFilter; import android.text.SpannableStringBuilder; import android.text.Spanned; -import com.wireguard.android.model.Tunnel; +import com.wireguard.android.backend.Tunnel; /** * InputFilter for entering WireGuard configuration names (Linux interface names). diff --git a/app/src/main/res/layout/tunnel_detail_fragment.xml b/app/src/main/res/layout/tunnel_detail_fragment.xml index 40a79eb7..463f8b80 100644 --- a/app/src/main/res/layout/tunnel_detail_fragment.xml +++ b/app/src/main/res/layout/tunnel_detail_fragment.xml @@ -5,7 +5,7 @@ <data> - <import type="com.wireguard.android.model.Tunnel.State" /> + <import type="com.wireguard.android.backend.Tunnel.State" /> <import type="com.wireguard.android.util.ClipboardUtils" /> @@ -15,7 +15,7 @@ <variable name="tunnel" - type="com.wireguard.android.model.Tunnel" /> + type="com.wireguard.android.model.ObservableTunnel" /> <variable name="config" diff --git a/app/src/main/res/layout/tunnel_list_fragment.xml b/app/src/main/res/layout/tunnel_list_fragment.xml index c4247019..c8144dbb 100644 --- a/app/src/main/res/layout/tunnel_list_fragment.xml +++ b/app/src/main/res/layout/tunnel_list_fragment.xml @@ -5,7 +5,7 @@ <data> - <import type="com.wireguard.android.model.Tunnel" /> + <import type="com.wireguard.android.model.ObservableTunnel" /> <variable name="fragment" @@ -17,7 +17,7 @@ <variable name="tunnels" - type="com.wireguard.android.util.ObservableKeyedList<String, Tunnel>" /> + type="com.wireguard.android.util.ObservableKeyedList<String, ObservableTunnel>" /> </data> <androidx.coordinatorlayout.widget.CoordinatorLayout diff --git a/app/src/main/res/layout/tunnel_list_item.xml b/app/src/main/res/layout/tunnel_list_item.xml index 1cca9453..04c0f51e 100644 --- a/app/src/main/res/layout/tunnel_list_item.xml +++ b/app/src/main/res/layout/tunnel_list_item.xml @@ -5,13 +5,13 @@ <data> - <import type="com.wireguard.android.model.Tunnel" /> + <import type="com.wireguard.android.model.ObservableTunnel" /> - <import type="com.wireguard.android.model.Tunnel.State" /> + <import type="com.wireguard.android.backend.Tunnel.State" /> <variable name="collection" - type="com.wireguard.android.util.ObservableKeyedList<String, Tunnel>" /> + type="com.wireguard.android.util.ObservableKeyedList<String, ObservableTunnel>" /> <variable name="key" @@ -19,7 +19,7 @@ <variable name="item" - type="com.wireguard.android.model.Tunnel" /> + type="com.wireguard.android.model.ObservableTunnel" /> <variable name="fragment" diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 46785bc1..a9f01861 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -104,7 +104,6 @@ <string name="module_installer_working">डाउनलोड कर रहा है और स्थापित कर रहा है…</string> <string name="module_installer_error">कुछ गलत हो गया। कृपया पुन: प्रयास करें</string> <string name="mtu">MTU</string> - <string name="multiple_tunnels_error">एक समय में केवल एक यूजरस्पेस टनल ही चल सकता है</string> <string name="name">नाम</string> <string name="no_config_error">बिना किसी कॉन्फ़िगरेशन के एक टनल को लाने की कोशिश करना</string> <string name="no_configs_error">कोई कॉन्फ़िगरेशन नहीं मिला</string> diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index a7bd29cb..b76c39cd 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -104,7 +104,6 @@ <string name="module_installer_working">Scaricamento e installazione…</string> <string name="module_installer_error">Qualcosa è andato storto. Riprova</string> <string name="mtu">MTU</string> - <string name="multiple_tunnels_error">Può essere attivo solo un tunnel su spazio utente alla volta</string> <string name="name">Nome</string> <string name="no_config_error">Tentativo di attivare un tunnel senza configurazione</string> <string name="no_configs_error">Nessuna configurazione trovata</string> diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 16cd165e..22d1b72f 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -98,7 +98,6 @@ <string name="module_installer_working">ダウンロードしてインストールしています…</string> <string name="module_installer_error">失敗しました. 再度実行してみてください</string> <string name="mtu">MTU</string> - <string name="multiple_tunnels_error">同時に実行できるユーザースペーストンネルは1つだけです</string> <string name="name">名前</string> <string name="no_config_error">未設定のままトンネルを起動しようとしています</string> <string name="no_configs_error">設定が見つかりません</string> diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 0add655c..c8f1b8aa 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -104,7 +104,6 @@ <string name="module_installer_working">Скачивание и установка…</string> <string name="module_installer_error">Что-то пошло не так. Пожалуйста, попробуйте еще раз</string> <string name="mtu">MTU</string> - <string name="multiple_tunnels_error">Только один пользовательский туннель может работать одновременно</string> <string name="name">Имя</string> <string name="no_config_error">Попытка поднять туннель без конфигурации</string> <string name="no_configs_error">Конфигурации не найдены</string> diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 26ca0232..8f7ab987 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -98,7 +98,6 @@ <string name="module_installer_working">正在下载安装...</string> <string name="module_installer_error">发生错误,请重试</string> <string name="mtu">MTU</string> - <string name="multiple_tunnels_error">用户空间内一次只能建立一个连接</string> <string name="name">名称</string> <string name="no_config_error">尝试在无配置情况下建立连接</string> <string name="no_configs_error">未找到配置</string> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 91bbf0f1..45964eec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -104,7 +104,6 @@ <string name="module_installer_working">Downloading and installing…</string> <string name="module_installer_error">Something went wrong. Please try again</string> <string name="mtu">MTU</string> - <string name="multiple_tunnels_error">Only one userspace tunnel can run at a time</string> <string name="name">Name</string> <string name="no_config_error">Trying to bring up a tunnel with no config</string> <string name="no_configs_error">No configurations found</string> |