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>
This commit is contained in:
Jason A. Donenfeld 2020-03-08 16:46:07 +08:00
parent 0990430513
commit 4905185e61
28 changed files with 378 additions and 274 deletions

View File

@ -5,6 +5,7 @@
package com.wireguard.android; package com.wireguard.android;
import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -19,12 +20,14 @@ import androidx.preference.PreferenceManager;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.app.AppCompatDelegate;
import com.wireguard.android.activity.MainActivity;
import com.wireguard.android.backend.Backend; import com.wireguard.android.backend.Backend;
import com.wireguard.android.backend.GoBackend; import com.wireguard.android.backend.GoBackend;
import com.wireguard.android.backend.WgQuickBackend; import com.wireguard.android.backend.WgQuickBackend;
import com.wireguard.android.configStore.FileConfigStore; import com.wireguard.android.configStore.FileConfigStore;
import com.wireguard.android.model.TunnelManager; import com.wireguard.android.model.TunnelManager;
import com.wireguard.android.util.AsyncWorker; import com.wireguard.android.util.AsyncWorker;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.ModuleLoader; import com.wireguard.android.util.ModuleLoader;
import com.wireguard.android.util.RootShell; import com.wireguard.android.util.RootShell;
import com.wireguard.android.util.ToolsInstaller; import com.wireguard.android.util.ToolsInstaller;
@ -89,8 +92,16 @@ public class Application extends android.app.Application {
} catch (final Exception ignored) { } catch (final Exception ignored) {
} }
} }
if (backend == null) if (backend == null) {
backend = new GoBackend(app.getApplicationContext()); 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; app.backend = backend;
} }
return app.backend; return app.backend;

View File

@ -21,8 +21,8 @@ import android.util.Log;
import com.wireguard.android.activity.MainActivity; import com.wireguard.android.activity.MainActivity;
import com.wireguard.android.activity.TunnelToggleActivity; import com.wireguard.android.activity.TunnelToggleActivity;
import com.wireguard.android.model.Tunnel; import com.wireguard.android.backend.Tunnel.State;
import com.wireguard.android.model.Tunnel.State; import com.wireguard.android.model.ObservableTunnel;
import com.wireguard.android.widget.SlashDrawable; import com.wireguard.android.widget.SlashDrawable;
import java.util.Objects; import java.util.Objects;
@ -41,7 +41,7 @@ public class QuickTileService extends TileService {
private final OnTunnelChangedCallback onTunnelChangedCallback = new OnTunnelChangedCallback(); private final OnTunnelChangedCallback onTunnelChangedCallback = new OnTunnelChangedCallback();
@Nullable private Icon iconOff; @Nullable private Icon iconOff;
@Nullable private Icon iconOn; @Nullable private Icon iconOn;
@Nullable private Tunnel tunnel; @Nullable private ObservableTunnel tunnel;
/* This works around an annoying unsolved frameworks bug some people are hitting. */ /* This works around an annoying unsolved frameworks bug some people are hitting. */
@Override @Override
@ -121,7 +121,7 @@ public class QuickTileService extends TileService {
private void updateTile() { private void updateTile() {
// Update the tunnel. // Update the tunnel.
final Tunnel newTunnel = Application.getTunnelManager().getLastUsedTunnel(); final ObservableTunnel newTunnel = Application.getTunnelManager().getLastUsedTunnel();
if (newTunnel != tunnel) { if (newTunnel != tunnel) {
if (tunnel != null) if (tunnel != null)
tunnel.removeOnPropertyChangedCallback(onStateChangedCallback); tunnel.removeOnPropertyChangedCallback(onStateChangedCallback);
@ -135,7 +135,7 @@ public class QuickTileService extends TileService {
final Tile tile = getQsTile(); final Tile tile = getQsTile();
if (tunnel != null) { if (tunnel != null) {
label = tunnel.getName(); 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 { } else {
label = getString(R.string.app_name); label = getString(R.string.app_name);
state = Tile.STATE_INACTIVE; state = Tile.STATE_INACTIVE;

View File

@ -11,7 +11,7 @@ import android.os.Bundle;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.wireguard.android.Application; import com.wireguard.android.Application;
import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.ObservableTunnel;
import java.util.Objects; import java.util.Objects;
@ -23,14 +23,14 @@ public abstract class BaseActivity extends ThemeChangeAwareActivity {
private static final String KEY_SELECTED_TUNNEL = "selected_tunnel"; private static final String KEY_SELECTED_TUNNEL = "selected_tunnel";
private final SelectionChangeRegistry selectionChangeRegistry = new SelectionChangeRegistry(); private final SelectionChangeRegistry selectionChangeRegistry = new SelectionChangeRegistry();
@Nullable private Tunnel selectedTunnel; @Nullable private ObservableTunnel selectedTunnel;
public void addOnSelectedTunnelChangedListener(final OnSelectedTunnelChangedListener listener) { public void addOnSelectedTunnelChangedListener(final OnSelectedTunnelChangedListener listener) {
selectionChangeRegistry.add(listener); selectionChangeRegistry.add(listener);
} }
@Nullable @Nullable
public Tunnel getSelectedTunnel() { public ObservableTunnel getSelectedTunnel() {
return selectedTunnel; return selectedTunnel;
} }
@ -60,15 +60,15 @@ public abstract class BaseActivity extends ThemeChangeAwareActivity {
super.onSaveInstanceState(outState); 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( public void removeOnSelectedTunnelChangedListener(
final OnSelectedTunnelChangedListener listener) { final OnSelectedTunnelChangedListener listener) {
selectionChangeRegistry.remove(listener); selectionChangeRegistry.remove(listener);
} }
public void setSelectedTunnel(@Nullable final Tunnel tunnel) { public void setSelectedTunnel(@Nullable final ObservableTunnel tunnel) {
final Tunnel oldTunnel = selectedTunnel; final ObservableTunnel oldTunnel = selectedTunnel;
if (Objects.equals(oldTunnel, tunnel)) if (Objects.equals(oldTunnel, tunnel))
return; return;
selectedTunnel = tunnel; selectedTunnel = tunnel;
@ -77,21 +77,21 @@ public abstract class BaseActivity extends ThemeChangeAwareActivity {
} }
public interface OnSelectedTunnelChangedListener { public interface OnSelectedTunnelChangedListener {
void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel); void onSelectedTunnelChanged(@Nullable ObservableTunnel oldTunnel, @Nullable ObservableTunnel newTunnel);
} }
private static final class SelectionChangeNotifier private static final class SelectionChangeNotifier
extends NotifierCallback<OnSelectedTunnelChangedListener, Tunnel, Tunnel> { extends NotifierCallback<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel> {
@Override @Override
public void onNotifyCallback(final OnSelectedTunnelChangedListener listener, public void onNotifyCallback(final OnSelectedTunnelChangedListener listener,
final Tunnel oldTunnel, final int ignored, final ObservableTunnel oldTunnel, final int ignored,
final Tunnel newTunnel) { final ObservableTunnel newTunnel) {
listener.onSelectedTunnelChanged(oldTunnel, newTunnel); listener.onSelectedTunnelChanged(oldTunnel, newTunnel);
} }
} }
private static final class SelectionChangeRegistry private static final class SelectionChangeRegistry
extends CallbackRegistry<OnSelectedTunnelChangedListener, Tunnel, Tunnel> { extends CallbackRegistry<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel> {
private SelectionChangeRegistry() { private SelectionChangeRegistry() {
super(new SelectionChangeNotifier()); super(new SelectionChangeNotifier());
} }

View File

@ -21,7 +21,7 @@ import android.widget.LinearLayout;
import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.android.fragment.TunnelDetailFragment; import com.wireguard.android.fragment.TunnelDetailFragment;
import com.wireguard.android.fragment.TunnelEditorFragment; import com.wireguard.android.fragment.TunnelEditorFragment;
import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.ObservableTunnel;
import java.util.List; import java.util.List;
@ -117,8 +117,8 @@ public class MainActivity extends BaseActivity
} }
@Override @Override
protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, protected void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel,
@Nullable final Tunnel newTunnel) { @Nullable final ObservableTunnel newTunnel) {
final FragmentManager fragmentManager = getSupportFragmentManager(); final FragmentManager fragmentManager = getSupportFragmentManager();
final int backStackEntries = fragmentManager.getBackStackEntryCount(); final int backStackEntries = fragmentManager.getBackStackEntryCount();
if (newTunnel == null) { if (newTunnel == null) {

View File

@ -9,7 +9,7 @@ import android.os.Bundle;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.wireguard.android.fragment.TunnelEditorFragment; import com.wireguard.android.fragment.TunnelEditorFragment;
import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.ObservableTunnel;
/** /**
* Standalone activity for creating tunnels. * Standalone activity for creating tunnels.
@ -28,7 +28,7 @@ public class TunnelCreatorActivity extends BaseActivity {
} }
@Override @Override
protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) { protected void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel, @Nullable final ObservableTunnel newTunnel) {
finish(); finish();
} }
} }

View File

@ -19,8 +19,8 @@ import android.widget.Toast;
import com.wireguard.android.Application; import com.wireguard.android.Application;
import com.wireguard.android.QuickTileService; import com.wireguard.android.QuickTileService;
import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.ObservableTunnel;
import com.wireguard.android.model.Tunnel.State; import com.wireguard.android.backend.Tunnel.State;
import com.wireguard.android.util.ErrorMessages; import com.wireguard.android.util.ErrorMessages;
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
@ -30,7 +30,7 @@ public class TunnelToggleActivity extends AppCompatActivity {
@Override @Override
protected void onCreate(@Nullable final Bundle savedInstanceState) { protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
final Tunnel tunnel = Application.getTunnelManager().getLastUsedTunnel(); final ObservableTunnel tunnel = Application.getTunnelManager().getLastUsedTunnel();
if (tunnel == null) if (tunnel == null)
return; return;
tunnel.setState(State.TOGGLE).whenComplete((v, t) -> { tunnel.setState(State.TOGGLE).whenComplete((v, t) -> {

View File

@ -5,43 +5,32 @@
package com.wireguard.android.backend; 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 com.wireguard.config.Config;
import java.util.Collection;
import java.util.Set; import java.util.Set;
import androidx.annotation.Nullable;
/** /**
* Interface for implementations of the WireGuard secure network tunnel. * Interface for implementations of the WireGuard secure network tunnel.
*/ */
public interface Backend { public interface Backend {
/** /**
* Update the volatile configuration of a running tunnel and return the resulting configuration. * Enumerate names of currently-running tunnels.
* 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.
* *
* @return The set of running tunnel names. * @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. * @param tunnel The tunnel to examine the state of.
* @return The state of the tunnel. * @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 * 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; 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 tunnel The tunnel to control the state of.
* @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or * @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
* {@code TOGGLE}. * {@code TOGGLE}.
* @param config The configuration for this tunnel, may be null if state is {@code DOWN}.
* @return The updated state of the tunnel. * @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);
} }

View File

@ -16,10 +16,7 @@ import android.util.Log;
import com.wireguard.android.Application; import com.wireguard.android.Application;
import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.android.activity.MainActivity; import com.wireguard.android.backend.Tunnel.State;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.SharedLibraryLoader; import com.wireguard.android.util.SharedLibraryLoader;
import com.wireguard.config.Config; import com.wireguard.config.Config;
@ -30,6 +27,7 @@ import com.wireguard.crypto.KeyFormatException;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -40,14 +38,26 @@ import java9.util.concurrent.CompletableFuture;
public final class GoBackend implements Backend { public final class GoBackend implements Backend {
private static final String TAG = "WireGuard/" + GoBackend.class.getSimpleName(); private static final String TAG = "WireGuard/" + GoBackend.class.getSimpleName();
private static CompletableFuture<VpnService> vpnService = new CompletableFuture<>(); 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 Context context;
private final PendingIntent configurationIntent;
@Nullable private Tunnel currentTunnel; @Nullable private Tunnel currentTunnel;
@Nullable private Config currentConfig;
private int currentTunnelHandle = -1; 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"); SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
this.context = context; this.context = context;
this.configurationIntent = configurationIntent;
} }
private static native String wgGetConfig(int handle); private static native String wgGetConfig(int handle);
@ -63,23 +73,7 @@ public final class GoBackend implements Backend {
private static native String wgVersion(); private static native String wgVersion();
@Override @Override
public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception { public Set<String> getRunningTunnelNames() {
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() {
if (currentTunnel != null) { if (currentTunnel != null) {
final Set<String> runningTunnels = new ArraySet<>(); final Set<String> runningTunnels = new ArraySet<>();
runningTunnels.add(currentTunnel.getName()); runningTunnels.add(currentTunnel.getName());
@ -147,25 +141,36 @@ public final class GoBackend implements Backend {
} }
@Override @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 State originalState = getState(tunnel);
if (state == State.TOGGLE) if (state == State.TOGGLE)
state = originalState == State.UP ? State.DOWN : State.UP; state = originalState == State.UP ? State.DOWN : State.UP;
if (state == originalState) if (state == originalState && tunnel == currentTunnel && config == currentConfig)
return originalState; return originalState;
if (state == State.UP && currentTunnel != null) if (state == State.UP) {
throw new IllegalStateException(context.getString(R.string.multiple_tunnels_error)); final Config originalConfig = currentConfig;
Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state); final Tunnel originalTunnel = currentTunnel;
setStateInternal(tunnel, tunnel.getConfig(), state); 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); return getState(tunnel);
} }
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state)
throws Exception { throws Exception {
Log.i(TAG, "Bringing tunnel " + tunnel.getName() + " " + state);
if (state == State.UP) { if (state == State.UP) {
Log.i(TAG, "Bringing tunnel up");
Objects.requireNonNull(config, context.getString(R.string.no_config_error)); Objects.requireNonNull(config, context.getString(R.string.no_config_error));
if (VpnService.prepare(context) != null) if (VpnService.prepare(context) != null)
@ -180,6 +185,7 @@ public final class GoBackend implements Backend {
} catch (final TimeoutException e) { } catch (final TimeoutException e) {
throw new Exception(context.getString(R.string.vpn_start_error), e); throw new Exception(context.getString(R.string.vpn_start_error), e);
} }
service.setOwner(this);
if (currentTunnelHandle != -1) { if (currentTunnelHandle != -1) {
Log.w(TAG, "Tunnel already up"); Log.w(TAG, "Tunnel already up");
@ -193,9 +199,7 @@ public final class GoBackend implements Backend {
final VpnService.Builder builder = service.getBuilder(); final VpnService.Builder builder = service.getBuilder();
builder.setSession(tunnel.getName()); builder.setSession(tunnel.getName());
final Intent configureIntent = new Intent(context, MainActivity.class); builder.setConfigureIntent(configurationIntent);
configureIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
builder.setConfigureIntent(PendingIntent.getActivity(context, 0, configureIntent, 0));
for (final String excludedApplication : config.getInterface().getExcludedApplications()) for (final String excludedApplication : config.getInterface().getExcludedApplications())
builder.addDisallowedApplication(excludedApplication); builder.addDisallowedApplication(excludedApplication);
@ -229,12 +233,11 @@ public final class GoBackend implements Backend {
throw new Exception(context.getString(R.string.tunnel_on_error, currentTunnelHandle)); throw new Exception(context.getString(R.string.tunnel_on_error, currentTunnelHandle));
currentTunnel = tunnel; currentTunnel = tunnel;
currentConfig = config;
service.protect(wgGetSocketV4(currentTunnelHandle)); service.protect(wgGetSocketV4(currentTunnelHandle));
service.protect(wgGetSocketV6(currentTunnelHandle)); service.protect(wgGetSocketV6(currentTunnelHandle));
} else { } else {
Log.i(TAG, "Bringing tunnel down");
if (currentTunnelHandle == -1) { if (currentTunnelHandle == -1) {
Log.w(TAG, "Tunnel already down"); Log.w(TAG, "Tunnel already down");
return; return;
@ -243,7 +246,11 @@ public final class GoBackend implements Backend {
wgTurnOff(currentTunnelHandle); wgTurnOff(currentTunnelHandle);
currentTunnel = null; currentTunnel = null;
currentTunnelHandle = -1; currentTunnelHandle = -1;
currentConfig = null;
} }
for (final TunnelStateChangeNotificationReceiver notifier : notifiers)
notifier.tunnelStateChange(tunnel, state);
} }
private void startVpnService() { private void startVpnService() {
@ -251,7 +258,23 @@ public final class GoBackend implements Backend {
context.startService(new Intent(context, VpnService.class)); 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 { public static class VpnService extends android.net.VpnService {
@Nullable private GoBackend owner;
public void setOwner(final GoBackend owner) {
this.owner = owner;
}
public Builder getBuilder() { public Builder getBuilder() {
return new Builder(); return new Builder();
} }
@ -264,13 +287,18 @@ public final class GoBackend implements Backend {
@Override @Override
public void onDestroy() { public void onDestroy() {
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> { if (owner != null) {
for (final Tunnel tunnel : tunnels) { final Tunnel tunnel = owner.currentTunnel;
if (tunnel != null && tunnel.getState() != State.DOWN) if (tunnel != null) {
tunnel.setState(State.DOWN); 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(); vpnService = vpnService.newIncompleteFuture();
super.onDestroy(); super.onDestroy();
} }
@ -280,10 +308,10 @@ public final class GoBackend implements Backend {
vpnService.complete(this); vpnService.complete(this);
if (intent == null || intent.getComponent() == null || !intent.getComponent().getPackageName().equals(getPackageName())) { if (intent == null || intent.getComponent() == null || !intent.getComponent().getPackageName().equals(getPackageName())) {
Log.d(TAG, "Service started by Always-on VPN feature"); 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); return super.onStartCommand(intent, flags, startId);
} }
} }
} }

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -11,9 +11,7 @@ import android.util.Log;
import com.wireguard.android.Application; import com.wireguard.android.Application;
import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.android.model.Tunnel; import com.wireguard.android.backend.Tunnel.State;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.config.Config; import com.wireguard.config.Config;
import com.wireguard.crypto.Key; import com.wireguard.crypto.Key;
@ -23,10 +21,13 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.HashMap;
import java9.util.stream.Collectors; import java9.util.stream.Collectors;
import java9.util.stream.Stream; import java9.util.stream.Stream;
@ -40,6 +41,8 @@ public final class WgQuickBackend implements Backend {
private final File localTemporaryDir; private final File localTemporaryDir;
private final Context context; private final Context context;
private final Map<Tunnel, Config> runningConfigs = new HashMap<>();
private final Set<TunnelStateChangeNotificationReceiver> notifiers = new HashSet<>();
public WgQuickBackend(final Context context) { public WgQuickBackend(final Context context) {
localTemporaryDir = new File(context.getCacheDir(), "tmp"); localTemporaryDir = new File(context.getCacheDir(), "tmp");
@ -47,23 +50,7 @@ public final class WgQuickBackend implements Backend {
} }
@Override @Override
public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception { public Set<String> getRunningTunnelNames() {
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() {
final List<String> output = new ArrayList<>(); final List<String> output = new ArrayList<>();
// Don't throw an exception here or nothing will show up in the UI. // Don't throw an exception here or nothing will show up in the UI.
try { try {
@ -80,7 +67,7 @@ public final class WgQuickBackend implements Backend {
@Override @Override
public State getState(final Tunnel tunnel) { 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 @Override
@ -120,20 +107,36 @@ public final class WgQuickBackend implements Backend {
} }
@Override @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 State originalState = getState(tunnel);
final Config originalConfig = runningConfigs.get(tunnel);
if (state == State.TOGGLE) if (state == State.TOGGLE)
state = originalState == State.UP ? State.DOWN : State.UP; 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; return originalState;
Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state); if (state == State.UP) {
Application.getToolsInstaller().ensureToolsAvailable(); Application.getToolsInstaller().ensureToolsAvailable();
setStateInternal(tunnel, tunnel.getConfig(), state); if (originalState == State.UP)
return getState(tunnel); 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 { 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"); final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf");
try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) { try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) {
@ -148,5 +151,23 @@ public final class WgQuickBackend implements Backend {
tempFile.delete(); tempFile.delete();
if (result != 0) if (result != 0)
throw new Exception(context.getString(R.string.tunnel_config_error, result)); 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);
} }
} }

View File

@ -23,8 +23,8 @@ import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListen
import com.wireguard.android.backend.GoBackend; import com.wireguard.android.backend.GoBackend;
import com.wireguard.android.databinding.TunnelDetailFragmentBinding; import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
import com.wireguard.android.databinding.TunnelListItemBinding; import com.wireguard.android.databinding.TunnelListItemBinding;
import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.ObservableTunnel;
import com.wireguard.android.model.Tunnel.State; import com.wireguard.android.backend.Tunnel.State;
import com.wireguard.android.util.ErrorMessages; 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 int REQUEST_CODE_VPN_PERMISSION = 23491;
private static final String TAG = "WireGuard/" + BaseFragment.class.getSimpleName(); private static final String TAG = "WireGuard/" + BaseFragment.class.getSimpleName();
@Nullable private BaseActivity activity; @Nullable private BaseActivity activity;
@Nullable private Tunnel pendingTunnel; @Nullable private ObservableTunnel pendingTunnel;
@Nullable private Boolean pendingTunnelUp; @Nullable private Boolean pendingTunnelUp;
@Nullable @Nullable
protected Tunnel getSelectedTunnel() { protected ObservableTunnel getSelectedTunnel() {
return activity != null ? activity.getSelectedTunnel() : null; return activity != null ? activity.getSelectedTunnel() : null;
} }
@ -75,14 +75,14 @@ public abstract class BaseFragment extends Fragment implements OnSelectedTunnelC
super.onDetach(); super.onDetach();
} }
protected void setSelectedTunnel(@Nullable final Tunnel tunnel) { protected void setSelectedTunnel(@Nullable final ObservableTunnel tunnel) {
if (activity != null) if (activity != null)
activity.setSelectedTunnel(tunnel); activity.setSelectedTunnel(tunnel);
} }
public void setTunnelState(final View view, final boolean checked) { public void setTunnelState(final View view, final boolean checked) {
final ViewDataBinding binding = DataBindingUtil.findBinding(view); final ViewDataBinding binding = DataBindingUtil.findBinding(view);
final Tunnel tunnel; final ObservableTunnel tunnel;
if (binding instanceof TunnelDetailFragmentBinding) if (binding instanceof TunnelDetailFragmentBinding)
tunnel = ((TunnelDetailFragmentBinding) binding).getTunnel(); tunnel = ((TunnelDetailFragmentBinding) binding).getTunnel();
else if (binding instanceof TunnelListItemBinding) 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) -> { tunnel.setState(State.of(checked)).whenComplete((state, throwable) -> {
if (throwable == null) if (throwable == null)
return; return;

View File

@ -18,8 +18,8 @@ import android.view.ViewGroup;
import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.android.databinding.TunnelDetailFragmentBinding; import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
import com.wireguard.android.databinding.TunnelDetailPeerBinding; import com.wireguard.android.databinding.TunnelDetailPeerBinding;
import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.ObservableTunnel;
import com.wireguard.android.model.Tunnel.State; import com.wireguard.android.backend.Tunnel.State;
import com.wireguard.android.ui.EdgeToEdge; import com.wireguard.android.ui.EdgeToEdge;
import com.wireguard.crypto.Key; import com.wireguard.crypto.Key;
@ -85,7 +85,7 @@ public class TunnelDetailFragment extends BaseFragment {
} }
@Override @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) if (binding == null)
return; return;
binding.setTunnel(newTunnel); binding.setTunnel(newTunnel);
@ -123,7 +123,7 @@ public class TunnelDetailFragment extends BaseFragment {
private void updateStats() { private void updateStats() {
if (binding == null || !isResumed()) if (binding == null || !isResumed())
return; return;
final Tunnel tunnel = binding.getTunnel(); final ObservableTunnel tunnel = binding.getTunnel();
if (tunnel == null) if (tunnel == null)
return; return;
final State state = tunnel.getState(); final State state = tunnel.getState();

View File

@ -11,7 +11,7 @@ import androidx.databinding.ObservableList;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.FragmentManager;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
@ -26,7 +26,7 @@ import com.wireguard.android.Application;
import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.android.databinding.TunnelEditorFragmentBinding; import com.wireguard.android.databinding.TunnelEditorFragmentBinding;
import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener; 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.model.TunnelManager;
import com.wireguard.android.ui.EdgeToEdge; import com.wireguard.android.ui.EdgeToEdge;
import com.wireguard.android.util.ErrorMessages; 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(); private static final String TAG = "WireGuard/" + TunnelEditorFragment.class.getSimpleName();
@Nullable private TunnelEditorFragmentBinding binding; @Nullable private TunnelEditorFragmentBinding binding;
@Nullable private Tunnel tunnel; @Nullable private ObservableTunnel tunnel;
private void onConfigLoaded(final Config config) { private void onConfigLoaded(final Config config) {
if (binding != null) { 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) { @Nullable final Throwable throwable) {
final String message; final String message;
if (throwable == null) { if (throwable == null) {
@ -126,7 +126,7 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
getActivity().runOnUiThread(() -> { getActivity().runOnUiThread(() -> {
// TODO(smaeul): Remove this hack when fixing the Config ViewModel // TODO(smaeul): Remove this hack when fixing the Config ViewModel
// The selected tunnel has to actually change, but we have to remember this one. // 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()) if (savedTunnel == getSelectedTunnel())
setSelectedTunnel(null); setSelectedTunnel(null);
setSelectedTunnel(savedTunnel); setSelectedTunnel(savedTunnel);
@ -187,8 +187,8 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
} }
@Override @Override
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, public void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel,
@Nullable final Tunnel newTunnel) { @Nullable final ObservableTunnel newTunnel) {
tunnel = newTunnel; tunnel = newTunnel;
if (binding == null) if (binding == null)
return; 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; final String message;
if (throwable == null) { if (throwable == null) {
tunnel = newTunnel; 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) { @Nullable final Throwable throwable) {
final String message; final String message;
if (throwable == null) { if (throwable == null) {

View File

@ -17,7 +17,7 @@ import android.provider.OpenableColumns;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.FragmentManager;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode; import androidx.appcompat.view.ActionMode;
import androidx.recyclerview.widget.RecyclerView; 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.ObservableKeyedRecyclerViewAdapter;
import com.wireguard.android.databinding.TunnelListFragmentBinding; import com.wireguard.android.databinding.TunnelListFragmentBinding;
import com.wireguard.android.databinding.TunnelListItemBinding; 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.ui.EdgeToEdge;
import com.wireguard.android.util.ErrorMessages; import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.widget.MultiselectableRelativeLayout; import com.wireguard.android.widget.MultiselectableRelativeLayout;
@ -91,7 +91,7 @@ public class TunnelListFragment extends BaseFragment {
return; return;
final ContentResolver contentResolver = activity.getContentResolver(); 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<>(); final List<Throwable> throwables = new ArrayList<>();
Application.getAsyncWorker().supplyAsync(() -> { Application.getAsyncWorker().supplyAsync(() -> {
final String[] columns = {OpenableColumns.DISPLAY_NAME}; final String[] columns = {OpenableColumns.DISPLAY_NAME};
@ -161,9 +161,9 @@ public class TunnelListFragment extends BaseFragment {
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception)); onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception));
} else { } else {
future.whenComplete((ignored1, ignored2) -> { future.whenComplete((ignored1, ignored2) -> {
final List<Tunnel> tunnels = new ArrayList<>(futureTunnels.size()); final List<ObservableTunnel> tunnels = new ArrayList<>(futureTunnels.size());
for (final CompletableFuture<Tunnel> futureTunnel : futureTunnels) { for (final CompletableFuture<ObservableTunnel> futureTunnel : futureTunnels) {
Tunnel tunnel = null; ObservableTunnel tunnel = null;
try { try {
tunnel = futureTunnel.getNow(null); tunnel = futureTunnel.getNow(null);
} catch (final Exception e) { } catch (final Exception e) {
@ -250,7 +250,7 @@ public class TunnelListFragment extends BaseFragment {
} }
@Override @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) if (binding == null)
return; return;
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> { Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
@ -281,7 +281,7 @@ public class TunnelListFragment extends BaseFragment {
showSnackbar(message); 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; String message = null;
for (final Throwable throwable : throwables) { for (final Throwable throwable : throwables) {
@ -315,7 +315,7 @@ public class TunnelListFragment extends BaseFragment {
binding.setFragment(this); binding.setFragment(this);
Application.getTunnelManager().getTunnels().thenAccept(binding::setTunnels); 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.setFragment(this);
binding.getRoot().setOnClickListener(clicked -> { binding.getRoot().setOnClickListener(clicked -> {
if (actionMode == null) { 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; return (MultiselectableRelativeLayout) binding.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel)).itemView;
} }
@ -355,12 +355,12 @@ public class TunnelListFragment extends BaseFragment {
case R.id.menu_action_delete: case R.id.menu_action_delete:
final Iterable<Integer> copyCheckedItems = new HashSet<>(checkedItems); final Iterable<Integer> copyCheckedItems = new HashSet<>(checkedItems);
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> { Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
final Collection<Tunnel> tunnelsToDelete = new ArrayList<>(); final Collection<ObservableTunnel> tunnelsToDelete = new ArrayList<>();
for (final Integer position : copyCheckedItems) for (final Integer position : copyCheckedItems)
tunnelsToDelete.add(tunnels.get(position)); tunnelsToDelete.add(tunnels.get(position));
final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete) final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete)
.map(Tunnel::delete) .map(ObservableTunnel::delete)
.toArray(CompletableFuture[]::new); .toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futures) CompletableFuture.allOf(futures)
.thenApply(x -> futures.length) .thenApply(x -> futures.length)

View File

@ -5,23 +5,17 @@
package com.wireguard.android.model; package com.wireguard.android.model;
import android.os.SystemClock;
import android.util.Pair;
import androidx.databinding.BaseObservable; import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable; import androidx.databinding.Bindable;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.wireguard.android.BR; 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.android.util.ExceptionLoggers;
import com.wireguard.config.Config; import com.wireguard.config.Config;
import com.wireguard.crypto.Key;
import com.wireguard.util.Keyed; 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.CompletableFuture;
import java9.util.concurrent.CompletionStage; import java9.util.concurrent.CompletionStage;
@ -29,28 +23,21 @@ import java9.util.concurrent.CompletionStage;
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel. * Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
*/ */
public class Tunnel extends BaseObservable implements Keyed<String> { public class ObservableTunnel extends BaseObservable implements Keyed<String>, Tunnel {
public static final int NAME_MAX_LENGTH = 15;
private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,15}");
private final TunnelManager manager; private final TunnelManager manager;
@Nullable private Config config; @Nullable private Config config;
private String name;
private State state; private State state;
private String name;
@Nullable private Statistics statistics; @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) { @Nullable final Config config, final State state) {
this.manager = manager;
this.name = name; this.name = name;
this.manager = manager;
this.config = config; this.config = config;
this.state = state; this.state = state;
} }
public static boolean isNameInvalid(final CharSequence name) {
return !NAME_PATTERN.matcher(name).matches();
}
public CompletionStage<Void> delete() { public CompletionStage<Void> delete() {
return manager.delete(this); return manager.delete(this);
} }
@ -74,6 +61,7 @@ public class Tunnel extends BaseObservable implements Keyed<String> {
return name; return name;
} }
@Override
@Bindable @Bindable
public String getName() { public String getName() {
return name; return name;
@ -146,60 +134,4 @@ public class Tunnel extends BaseObservable implements Keyed<String> {
return manager.setTunnelState(this, state); return manager.setTunnelState(this, state);
return CompletableFuture.completedFuture(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;
}
}
} }

View File

@ -15,9 +15,11 @@ import androidx.annotation.Nullable;
import com.wireguard.android.Application; import com.wireguard.android.Application;
import com.wireguard.android.BR; import com.wireguard.android.BR;
import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.android.backend.Backend.TunnelStateChangeNotificationReceiver;
import com.wireguard.android.configStore.ConfigStore; import com.wireguard.android.configStore.ConfigStore;
import com.wireguard.android.model.Tunnel.State; import com.wireguard.android.backend.Tunnel;
import com.wireguard.android.model.Tunnel.Statistics; import com.wireguard.android.backend.Tunnel.State;
import com.wireguard.android.backend.Statistics;
import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.ObservableSortedKeyedArrayList; import com.wireguard.android.util.ObservableSortedKeyedArrayList;
import com.wireguard.android.util.ObservableSortedKeyedList; 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, * 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( private static final Comparator<String> COMPARATOR = Comparators.<String>thenComparing(
String.CASE_INSENSITIVE_ORDER, Comparators.naturalOrder()); String.CASE_INSENSITIVE_ORDER, Comparators.naturalOrder());
private static final String KEY_LAST_USED_TUNNEL = "last_used_tunnel"; 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_RESTORE_ON_BOOT = "restore_on_boot";
private static final String KEY_RUNNING_TUNNELS = "enabled_configs"; 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 ConfigStore configStore;
private final Context context = Application.get(); private final Context context = Application.get();
private final ArrayList<CompletableFuture<Void>> delayedLoadRestoreTunnels = new ArrayList<>(); 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; private boolean haveLoaded;
@Nullable private Tunnel lastUsedTunnel; @Nullable private ObservableTunnel lastUsedTunnel;
public TunnelManager(final ConfigStore configStore) { public TunnelManager(final ConfigStore configStore) {
this.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)) return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getState(tunnel))
.thenApply(tunnel::onStateChanged); .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)) return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getStatistics(tunnel))
.thenApply(tunnel::onStatisticsChanged); .thenApply(tunnel::onStatisticsChanged);
} }
private Tunnel addToList(final String name, @Nullable final Config config, final State state) { private ObservableTunnel addToList(final String name, @Nullable final Config config, final State state) {
final Tunnel tunnel = new Tunnel(this, name, config, state); final ObservableTunnel tunnel = new ObservableTunnel(this, name, config, state);
tunnels.add(tunnel); tunnels.add(tunnel);
return 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)) if (Tunnel.isNameInvalid(name))
return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name))); return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
if (tunnels.containsKey(name)) { if (tunnels.containsKey(name)) {
@ -84,7 +93,7 @@ public final class TunnelManager extends BaseObservable {
.thenApply(savedConfig -> addToList(name, savedConfig, State.DOWN)); .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 State originalState = tunnel.getState();
final boolean wasLastUsed = tunnel == lastUsedTunnel; final boolean wasLastUsed = tunnel == lastUsedTunnel;
// Make sure nothing touches the tunnel. // Make sure nothing touches the tunnel.
@ -93,12 +102,12 @@ public final class TunnelManager extends BaseObservable {
tunnels.remove(tunnel); tunnels.remove(tunnel);
return Application.getAsyncWorker().runAsync(() -> { return Application.getAsyncWorker().runAsync(() -> {
if (originalState == State.UP) if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.DOWN); Application.getBackend().setState(tunnel, State.DOWN, null);
try { try {
configStore.delete(tunnel.getName()); configStore.delete(tunnel.getName());
} catch (final Exception e) { } catch (final Exception e) {
if (originalState == State.UP) 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. // Re-throw the exception to fail the completion.
throw e; throw e;
} }
@ -114,22 +123,22 @@ public final class TunnelManager extends BaseObservable {
@Bindable @Bindable
@Nullable @Nullable
public Tunnel getLastUsedTunnel() { public ObservableTunnel getLastUsedTunnel() {
return lastUsedTunnel; return lastUsedTunnel;
} }
CompletionStage<Config> getTunnelConfig(final Tunnel tunnel) { CompletionStage<Config> getTunnelConfig(final ObservableTunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> configStore.load(tunnel.getName())) return Application.getAsyncWorker().supplyAsync(() -> configStore.load(tunnel.getName()))
.thenApply(tunnel::onConfigChanged); .thenApply(tunnel::onConfigChanged);
} }
public CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> getTunnels() { public CompletableFuture<ObservableSortedKeyedList<String, ObservableTunnel>> getTunnels() {
return completableTunnels; return completableTunnels;
} }
public void onCreate() { public void onCreate() {
Application.getAsyncWorker().supplyAsync(configStore::enumerate) 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); .whenComplete(ExceptionLoggers.E);
} }
@ -159,9 +168,9 @@ public final class TunnelManager extends BaseObservable {
} }
public void refreshTunnelStates() { public void refreshTunnelStates() {
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().enumerate()) Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getRunningTunnelNames())
.thenAccept(running -> { .thenAccept(running -> {
for (final Tunnel tunnel : tunnels) for (final ObservableTunnel tunnel : tunnels)
tunnel.onStateChanged(running.contains(tunnel.getName()) ? State.UP : State.DOWN); tunnel.onStateChanged(running.contains(tunnel.getName()) ? State.UP : State.DOWN);
}) })
.whenComplete(ExceptionLoggers.E); .whenComplete(ExceptionLoggers.E);
@ -189,12 +198,12 @@ public final class TunnelManager extends BaseObservable {
public void saveState() { public void saveState() {
final Set<String> runningTunnels = StreamSupport.stream(tunnels) final Set<String> runningTunnels = StreamSupport.stream(tunnels)
.filter(tunnel -> tunnel.getState() == State.UP) .filter(tunnel -> tunnel.getState() == State.UP)
.map(Tunnel::getName) .map(ObservableTunnel::getName)
.collect(Collectors.toUnmodifiableSet()); .collect(Collectors.toUnmodifiableSet());
Application.getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, runningTunnels).apply(); 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) if (tunnel == lastUsedTunnel)
return; return;
lastUsedTunnel = tunnel; lastUsedTunnel = tunnel;
@ -205,14 +214,14 @@ public final class TunnelManager extends BaseObservable {
Application.getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).apply(); 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(() -> { return Application.getAsyncWorker().supplyAsync(() -> {
final Config appliedConfig = Application.getBackend().applyConfig(tunnel, config); Application.getBackend().setState(tunnel, tunnel.getState(), config);
return configStore.save(tunnel.getName(), appliedConfig); return configStore.save(tunnel.getName(), config);
}).thenApply(tunnel::onConfigChanged); }).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)) if (Tunnel.isNameInvalid(name))
return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name))); return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
if (tunnels.containsKey(name)) { if (tunnels.containsKey(name)) {
@ -227,11 +236,11 @@ public final class TunnelManager extends BaseObservable {
tunnels.remove(tunnel); tunnels.remove(tunnel);
return Application.getAsyncWorker().supplyAsync(() -> { return Application.getAsyncWorker().supplyAsync(() -> {
if (originalState == State.UP) if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.DOWN); Application.getBackend().setState(tunnel, State.DOWN, null);
configStore.rename(tunnel.getName(), name); configStore.rename(tunnel.getName(), name);
final String newName = tunnel.onNameChanged(name); final String newName = tunnel.onNameChanged(name);
if (originalState == State.UP) if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.UP); Application.getBackend().setState(tunnel, State.UP, tunnel.getConfig());
return newName; return newName;
}).whenComplete((newName, e) -> { }).whenComplete((newName, e) -> {
// On failure, we don't know what state the tunnel might be in. Fix that. // 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. // Ensure the configuration is loaded before trying to use it.
return tunnel.getConfigAsync().thenCompose(x -> return tunnel.getConfigAsync().thenCompose(config ->
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().setState(tunnel, state)) Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().setState(tunnel, state, config))
).whenComplete((newState, e) -> { ).whenComplete((newState, e) -> {
// Ensure onStateChanged is always called (failure or not), and with the correct state. // Ensure onStateChanged is always called (failure or not), and with the correct state.
tunnel.onStateChanged(e == null ? newState : tunnel.getState()); 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 { public static final class IntentReceiver extends BroadcastReceiver {
@Override @Override
public void onReceive(final Context context, @Nullable final Intent intent) { public void onReceive(final Context context, @Nullable final Intent intent) {
@ -290,7 +304,7 @@ public final class TunnelManager extends BaseObservable {
if (tunnelName == null) if (tunnelName == null)
return; return;
manager.getTunnels().thenAccept(tunnels -> { manager.getTunnels().thenAccept(tunnels -> {
final Tunnel tunnel = tunnels.get(tunnelName); final ObservableTunnel tunnel = tunnels.get(tunnelName);
if (tunnel == null) if (tunnel == null)
return; return;
manager.setTunnelState(tunnel, state); manager.setTunnelState(tunnel, state);

View File

@ -16,7 +16,7 @@ import android.util.Log;
import com.wireguard.android.Application; import com.wireguard.android.Application;
import com.wireguard.android.R; 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;
import com.wireguard.android.util.DownloadsFileSaver.DownloadsFile; import com.wireguard.android.util.DownloadsFileSaver.DownloadsFile;
import com.wireguard.android.util.ErrorMessages; import com.wireguard.android.util.ErrorMessages;
@ -48,9 +48,9 @@ public class ZipExporterPreference extends Preference {
Application.getTunnelManager().getTunnels().thenAccept(this::exportZip); 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()); final List<CompletableFuture<Config>> futureConfigs = new ArrayList<>(tunnels.size());
for (final Tunnel tunnel : tunnels) for (final ObservableTunnel tunnel : tunnels)
futureConfigs.add(tunnel.getConfigAsync().toCompletableFuture()); futureConfigs.add(tunnel.getConfigAsync().toCompletableFuture());
if (futureConfigs.isEmpty()) { if (futureConfigs.isEmpty()) {
exportZipComplete(null, new IllegalArgumentException( exportZipComplete(null, new IllegalArgumentException(

View File

@ -10,7 +10,7 @@ import android.text.InputFilter;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; 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). * InputFilter for entering WireGuard configuration names (Linux interface names).

View File

@ -5,7 +5,7 @@
<data> <data>
<import type="com.wireguard.android.model.Tunnel.State" /> <import type="com.wireguard.android.backend.Tunnel.State" />
<import type="com.wireguard.android.util.ClipboardUtils" /> <import type="com.wireguard.android.util.ClipboardUtils" />
@ -15,7 +15,7 @@
<variable <variable
name="tunnel" name="tunnel"
type="com.wireguard.android.model.Tunnel" /> type="com.wireguard.android.model.ObservableTunnel" />
<variable <variable
name="config" name="config"

View File

@ -5,7 +5,7 @@
<data> <data>
<import type="com.wireguard.android.model.Tunnel" /> <import type="com.wireguard.android.model.ObservableTunnel" />
<variable <variable
name="fragment" name="fragment"
@ -17,7 +17,7 @@
<variable <variable
name="tunnels" name="tunnels"
type="com.wireguard.android.util.ObservableKeyedList&lt;String, Tunnel&gt;" /> type="com.wireguard.android.util.ObservableKeyedList&lt;String, ObservableTunnel&gt;" />
</data> </data>
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout

View File

@ -5,13 +5,13 @@
<data> <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 <variable
name="collection" name="collection"
type="com.wireguard.android.util.ObservableKeyedList&lt;String, Tunnel&gt;" /> type="com.wireguard.android.util.ObservableKeyedList&lt;String, ObservableTunnel&gt;" />
<variable <variable
name="key" name="key"
@ -19,7 +19,7 @@
<variable <variable
name="item" name="item"
type="com.wireguard.android.model.Tunnel" /> type="com.wireguard.android.model.ObservableTunnel" />
<variable <variable
name="fragment" name="fragment"

View File

@ -104,7 +104,6 @@
<string name="module_installer_working">डाउनलोड कर रहा है और स्थापित कर रहा है…</string> <string name="module_installer_working">डाउनलोड कर रहा है और स्थापित कर रहा है…</string>
<string name="module_installer_error">कुछ गलत हो गया। कृपया पुन: प्रयास करें</string> <string name="module_installer_error">कुछ गलत हो गया। कृपया पुन: प्रयास करें</string>
<string name="mtu">MTU</string> <string name="mtu">MTU</string>
<string name="multiple_tunnels_error">एक समय में केवल एक यूजरस्पेस टनल ही चल सकता है</string>
<string name="name">नाम</string> <string name="name">नाम</string>
<string name="no_config_error">बिना किसी कॉन्फ़िगरेशन के एक टनल को लाने की कोशिश करना</string> <string name="no_config_error">बिना किसी कॉन्फ़िगरेशन के एक टनल को लाने की कोशिश करना</string>
<string name="no_configs_error">कोई कॉन्फ़िगरेशन नहीं मिला</string> <string name="no_configs_error">कोई कॉन्फ़िगरेशन नहीं मिला</string>

View File

@ -104,7 +104,6 @@
<string name="module_installer_working">Scaricamento e installazione…</string> <string name="module_installer_working">Scaricamento e installazione…</string>
<string name="module_installer_error">Qualcosa è andato storto. Riprova</string> <string name="module_installer_error">Qualcosa è andato storto. Riprova</string>
<string name="mtu">MTU</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="name">Nome</string>
<string name="no_config_error">Tentativo di attivare un tunnel senza configurazione</string> <string name="no_config_error">Tentativo di attivare un tunnel senza configurazione</string>
<string name="no_configs_error">Nessuna configurazione trovata</string> <string name="no_configs_error">Nessuna configurazione trovata</string>

View File

@ -98,7 +98,6 @@
<string name="module_installer_working">ダウンロードしてインストールしています…</string> <string name="module_installer_working">ダウンロードしてインストールしています…</string>
<string name="module_installer_error">失敗しました. 再度実行してみてください</string> <string name="module_installer_error">失敗しました. 再度実行してみてください</string>
<string name="mtu">MTU</string> <string name="mtu">MTU</string>
<string name="multiple_tunnels_error">同時に実行できるユーザースペーストンネルは1つだけです</string>
<string name="name">名前</string> <string name="name">名前</string>
<string name="no_config_error">未設定のままトンネルを起動しようとしています</string> <string name="no_config_error">未設定のままトンネルを起動しようとしています</string>
<string name="no_configs_error">設定が見つかりません</string> <string name="no_configs_error">設定が見つかりません</string>

View File

@ -104,7 +104,6 @@
<string name="module_installer_working">Скачивание и установка…</string> <string name="module_installer_working">Скачивание и установка…</string>
<string name="module_installer_error">Что-то пошло не так. Пожалуйста, попробуйте еще раз</string> <string name="module_installer_error">Что-то пошло не так. Пожалуйста, попробуйте еще раз</string>
<string name="mtu">MTU</string> <string name="mtu">MTU</string>
<string name="multiple_tunnels_error">Только один пользовательский туннель может работать одновременно</string>
<string name="name">Имя</string> <string name="name">Имя</string>
<string name="no_config_error">Попытка поднять туннель без конфигурации</string> <string name="no_config_error">Попытка поднять туннель без конфигурации</string>
<string name="no_configs_error">Конфигурации не найдены</string> <string name="no_configs_error">Конфигурации не найдены</string>

View File

@ -98,7 +98,6 @@
<string name="module_installer_working">正在下载安装...</string> <string name="module_installer_working">正在下载安装...</string>
<string name="module_installer_error">发生错误,请重试</string> <string name="module_installer_error">发生错误,请重试</string>
<string name="mtu">MTU</string> <string name="mtu">MTU</string>
<string name="multiple_tunnels_error">用户空间内一次只能建立一个连接</string>
<string name="name">名称</string> <string name="name">名称</string>
<string name="no_config_error">尝试在无配置情况下建立连接</string> <string name="no_config_error">尝试在无配置情况下建立连接</string>
<string name="no_configs_error">未找到配置</string> <string name="no_configs_error">未找到配置</string>

View File

@ -104,7 +104,6 @@
<string name="module_installer_working">Downloading and installing…</string> <string name="module_installer_working">Downloading and installing…</string>
<string name="module_installer_error">Something went wrong. Please try again</string> <string name="module_installer_error">Something went wrong. Please try again</string>
<string name="mtu">MTU</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="name">Name</string>
<string name="no_config_error">Trying to bring up a tunnel with no config</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> <string name="no_configs_error">No configurations found</string>