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>master
@@ -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; | |||
@@ -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; | |||
@@ -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()); | |||
} | |||
@@ -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) { | |||
@@ -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(); | |||
} | |||
} |
@@ -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) -> { | |||
@@ -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); | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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(); | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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; | |||
@@ -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(); | |||
@@ -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) { | |||
@@ -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) | |||
@@ -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; | |||
} | |||
} | |||
} |
@@ -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); | |||
@@ -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( | |||
@@ -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). | |||
@@ -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" | |||
@@ -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 | |||
@@ -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" | |||
@@ -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> | |||
@@ -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> | |||
@@ -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> | |||
@@ -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> | |||
@@ -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> | |||
@@ -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> | |||