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 | 4905185e6121471f8c15d4ac59623323284135e5 (patch) | |
tree | bbbaa47a67158eef6dabfb6a9ed59cafe4b94d40 /app/src/main/java | |
parent | 09904305136adec00fae06f84e299b2df9a580c9 (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.
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
Diffstat (limited to 'app/src/main/java')
19 files changed, 370 insertions, 260 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). |