Signed-off-by: Samuel Holland <samuel@sholland.org>master
@@ -81,17 +81,15 @@ public class Application extends android.app.Application { | |||
@ApplicationScope | |||
@Provides | |||
public static Backend getBackend(final AsyncWorker asyncWorker, | |||
@ApplicationContext final Context context, | |||
public static Backend getBackend(@ApplicationContext final Context context, | |||
final RootShell rootShell) { | |||
return new WgQuickBackend(asyncWorker, context, rootShell); | |||
return new WgQuickBackend(context, rootShell); | |||
} | |||
@ApplicationScope | |||
@Provides | |||
public static ConfigStore getConfigStore(final AsyncWorker asyncWorker, | |||
@ApplicationContext final Context context) { | |||
return new FileConfigStore(asyncWorker, context); | |||
public static ConfigStore getConfigStore(@ApplicationContext final Context context) { | |||
return new FileConfigStore(context); | |||
} | |||
@@ -7,61 +7,53 @@ import com.wireguard.config.Config; | |||
import java.util.Set; | |||
import java9.util.concurrent.CompletionStage; | |||
/** | |||
* Interface for implementations of the WireGuard secure network tunnel. | |||
*/ | |||
public interface Backend { | |||
/** | |||
* Update the volatile configuration of a running tunnel, asynchronously, 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. | |||
* 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 A future completed when the configuration of the tunnel has been updated, and the new | |||
* volatile configuration has been determined. This future will always be completed on the main | |||
* thread. | |||
* @return The updated configuration of the tunnel. | |||
*/ | |||
CompletionStage<Config> applyConfig(Tunnel tunnel, Config config); | |||
Config applyConfig(Tunnel tunnel, Config config) throws Exception; | |||
/** | |||
* Enumerate the names of currently-running tunnels. | |||
* | |||
* @return A future completed when the set of running tunnel names is available. This future | |||
* will always be completed on the main thread. | |||
* @return The set of running tunnel names. | |||
*/ | |||
CompletionStage<Set<String>> enumerate(); | |||
Set<String> enumerate() throws Exception; | |||
/** | |||
* Get the actual state of a tunnel, asynchronously. | |||
* Get the actual state of a tunnel. | |||
* | |||
* @param tunnel The tunnel to examine the state of. | |||
* @return A future completed when the state of the tunnel has been determined. This future will | |||
* always be completed on the main thread. | |||
* @return The state of the tunnel. | |||
*/ | |||
CompletionStage<State> getState(Tunnel tunnel); | |||
State getState(Tunnel tunnel) throws Exception; | |||
/** | |||
* Get statistics about traffic and errors on this tunnel, asynchronously. If the tunnel is not | |||
* running, the statistics object will be filled with zero values. | |||
* Get statistics about traffic and errors on this tunnel. If the tunnel is not running, the | |||
* statistics object will be filled with zero values. | |||
* | |||
* @param tunnel The tunnel to retrieve statistics for. | |||
* @return A future completed when statistics for the tunnel are available. This future will | |||
* always be completed on the main thread. | |||
* @return The statistics for the tunnel. | |||
*/ | |||
CompletionStage<Statistics> getStatistics(Tunnel tunnel); | |||
Statistics getStatistics(Tunnel tunnel) throws Exception; | |||
/** | |||
* Set the state of a tunnel, asynchronously. | |||
* Set the state of a tunnel. | |||
* | |||
* @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}. | |||
* @return A future completed when the state of the tunnel has changed, containing the new state | |||
* of the tunnel. This future will always be completed on the main thread. | |||
* @return The updated state of the tunnel. | |||
*/ | |||
CompletionStage<State> setState(Tunnel tunnel, State state); | |||
State setState(Tunnel tunnel, State state) throws Exception; | |||
} |
@@ -6,7 +6,6 @@ import android.util.Log; | |||
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.AsyncWorker; | |||
import com.wireguard.android.util.RootShell; | |||
import com.wireguard.config.Config; | |||
@@ -17,25 +16,20 @@ import java.util.LinkedList; | |||
import java.util.List; | |||
import java.util.Set; | |||
import java9.util.concurrent.CompletableFuture; | |||
import java9.util.concurrent.CompletionStage; | |||
import java9.util.stream.Collectors; | |||
import java9.util.stream.Stream; | |||
/** | |||
* Created by samuel on 12/19/17. | |||
* WireGuard backend that uses {@code wg-quick} to implement tunnel configuration. | |||
*/ | |||
public final class WgQuickBackend implements Backend { | |||
private static final String TAG = WgQuickBackend.class.getSimpleName(); | |||
private final AsyncWorker asyncWorker; | |||
private final Context context; | |||
private final RootShell rootShell; | |||
public WgQuickBackend(final AsyncWorker asyncWorker, final Context context, | |||
final RootShell rootShell) { | |||
this.asyncWorker = asyncWorker; | |||
public WgQuickBackend(final Context context, final RootShell rootShell) { | |||
this.context = context; | |||
this.rootShell = rootShell; | |||
} | |||
@@ -49,47 +43,47 @@ public final class WgQuickBackend implements Backend { | |||
} | |||
@Override | |||
public CompletionStage<Config> applyConfig(final Tunnel tunnel, final Config config) { | |||
public Config applyConfig(final Tunnel tunnel, final Config config) { | |||
if (tunnel.getState() == State.UP) | |||
return CompletableFuture.failedFuture(new UnsupportedOperationException("stub")); | |||
return CompletableFuture.completedFuture(config); | |||
throw new UnsupportedOperationException("Not implemented"); | |||
return config; | |||
} | |||
@Override | |||
public CompletionStage<Set<String>> enumerate() { | |||
return asyncWorker.supplyAsync(() -> { | |||
final List<String> output = new LinkedList<>(); | |||
// Don't throw an exception here or nothing will show up in the UI. | |||
if (rootShell.run(output, "wg show interfaces") != 0 || output.isEmpty()) | |||
return Collections.emptySet(); | |||
// wg puts all interface names on the same line. Split them into separate elements. | |||
return Stream.of(output.get(0).split(" ")) | |||
.collect(Collectors.toUnmodifiableSet()); | |||
}); | |||
public Set<String> enumerate() { | |||
final List<String> output = new LinkedList<>(); | |||
// Don't throw an exception here or nothing will show up in the UI. | |||
if (rootShell.run(output, "wg show interfaces") != 0 || output.isEmpty()) | |||
return Collections.emptySet(); | |||
// wg puts all interface names on the same line. Split them into separate elements. | |||
return Stream.of(output.get(0).split(" ")).collect(Collectors.toUnmodifiableSet()); | |||
} | |||
@Override | |||
public CompletionStage<State> getState(final Tunnel tunnel) { | |||
public State getState(final Tunnel tunnel) { | |||
Log.v(TAG, "Requested state for tunnel " + tunnel.getName()); | |||
return enumerate().thenApply(set -> set.contains(tunnel.getName()) ? State.UP : State.DOWN); | |||
return enumerate().contains(tunnel.getName()) ? State.UP : State.DOWN; | |||
} | |||
@Override | |||
public CompletionStage<Statistics> getStatistics(final Tunnel tunnel) { | |||
return CompletableFuture.completedFuture(new Statistics()); | |||
public Statistics getStatistics(final Tunnel tunnel) { | |||
return new Statistics(); | |||
} | |||
@Override | |||
public CompletionStage<State> setState(final Tunnel tunnel, final State state) { | |||
public State setState(final Tunnel tunnel, final State state) throws IOException { | |||
Log.v(TAG, "Requested state change to " + state + " for tunnel " + tunnel.getName()); | |||
return tunnel.getStateAsync().thenCompose(currentState -> asyncWorker.supplyAsync(() -> { | |||
final String stateName = resolveState(currentState, state).name().toLowerCase(); | |||
final File file = new File(context.getFilesDir(), tunnel.getName() + ".conf"); | |||
final String path = file.getAbsolutePath(); | |||
final State originalState = getState(tunnel); | |||
final State resolvedState = resolveState(originalState, state); | |||
if (resolvedState == State.UP) { | |||
// FIXME: Assumes file layout from FileConfigStore. Use a temporary file. | |||
if (rootShell.run(null, String.format("wg-quick %s '%s'", stateName, path)) != 0) | |||
final File file = new File(context.getFilesDir(), tunnel.getName() + ".conf"); | |||
if (rootShell.run(null, String.format("wg-quick up '%s'", file.getAbsolutePath())) != 0) | |||
throw new IOException("wg-quick failed"); | |||
} else { | |||
if (rootShell.run(null, String.format("wg-quick down '%s'", tunnel.getName())) != 0) | |||
throw new IOException("wg-quick failed"); | |||
return tunnel; | |||
})).thenCompose(this::getState); | |||
} | |||
return getState(tunnel); | |||
} | |||
} |
@@ -4,8 +4,6 @@ import com.wireguard.config.Config; | |||
import java.util.Set; | |||
import java9.util.concurrent.CompletionStage; | |||
/** | |||
* Interface for persistent storage providers for WireGuard configurations. | |||
*/ | |||
@@ -17,39 +15,32 @@ public interface ConfigStore { | |||
* | |||
* @param name The name of the tunnel to create. | |||
* @param config Configuration for the new tunnel. | |||
* @return A future completed when the tunnel and its configuration have been saved to | |||
* persistent storage. This future encapsulates the configuration that was actually saved to | |||
* persistent storage. This future will always be completed on the main thread. | |||
* @return The configuration that was actually saved to persistent storage. | |||
*/ | |||
CompletionStage<Config> create(final String name, final Config config); | |||
Config create(final String name, final Config config) throws Exception; | |||
/** | |||
* Delete a persistent tunnel. | |||
* | |||
* @param name The name of the tunnel to delete. | |||
* @return A future completed when the tunnel and its configuration have been deleted. This | |||
* future will always be completed on the main thread. | |||
*/ | |||
CompletionStage<Void> delete(final String name); | |||
void delete(final String name) throws Exception; | |||
/** | |||
* Enumerate the names of tunnels present in persistent storage. | |||
* | |||
* @return A future completed when the set of present tunnel names is available. This future | |||
* will always be completed on the main thread. | |||
* @return The set of present tunnel names. | |||
*/ | |||
CompletionStage<Set<String>> enumerate(); | |||
Set<String> enumerate() throws Exception; | |||
/** | |||
* Load the configuration for the tunnel given by {@code name}. | |||
* | |||
* @param name The identifier for the configuration in persistent storage (i.e. the name of the | |||
* tunnel). | |||
* @return A future completed when an in-memory representation of the configuration is | |||
* available. This future encapsulates the configuration loaded from persistent storage. This | |||
* future will always be completed on the main thread. | |||
* @return An in-memory representation of the configuration loaded from persistent storage. | |||
*/ | |||
CompletionStage<Config> load(final String name); | |||
Config load(final String name) throws Exception; | |||
/** | |||
* Save the configuration for an existing tunnel given by {@code name}. | |||
@@ -57,9 +48,7 @@ public interface ConfigStore { | |||
* @param name The identifier for the configuration in persistent storage (i.e. the name of | |||
* the tunnel). | |||
* @param config An updated configuration object for the tunnel. | |||
* @return A future completed when the configuration has been saved to persistent storage. This | |||
* future encapsulates the configuration that was actually saved to persistent storage. This | |||
* future will always be completed on the main thread. | |||
* @return The configuration that was actually saved to persistent storage. | |||
*/ | |||
CompletionStage<Config> save(final String name, final Config config); | |||
Config save(final String name, final Config config) throws Exception; | |||
} |
@@ -4,7 +4,6 @@ import android.content.Context; | |||
import android.util.Log; | |||
import com.wireguard.android.Application.ApplicationContext; | |||
import com.wireguard.android.util.AsyncWorker; | |||
import com.wireguard.config.Config; | |||
import java.io.File; | |||
@@ -14,56 +13,48 @@ import java.io.IOException; | |||
import java.nio.charset.StandardCharsets; | |||
import java.util.Set; | |||
import java9.util.concurrent.CompletionStage; | |||
import java9.util.stream.Collectors; | |||
import java9.util.stream.Stream; | |||
/** | |||
* Created by samuel on 12/28/17. | |||
* Configuration store that uses a {@code wg-quick}-style file for each configured tunnel. | |||
*/ | |||
public final class FileConfigStore implements ConfigStore { | |||
private static final String TAG = FileConfigStore.class.getSimpleName(); | |||
private final AsyncWorker asyncWorker; | |||
private final Context context; | |||
public FileConfigStore(final AsyncWorker asyncWorker, | |||
@ApplicationContext final Context context) { | |||
this.asyncWorker = asyncWorker; | |||
public FileConfigStore(@ApplicationContext final Context context) { | |||
this.context = context; | |||
} | |||
@Override | |||
public CompletionStage<Config> create(final String name, final Config config) { | |||
return asyncWorker.supplyAsync(() -> { | |||
final File file = fileFor(name); | |||
if (!file.createNewFile()) { | |||
final String message = "Configuration file " + file.getName() + " already exists"; | |||
throw new IllegalStateException(message); | |||
} | |||
try (FileOutputStream stream = new FileOutputStream(file, false)) { | |||
stream.write(config.toString().getBytes(StandardCharsets.UTF_8)); | |||
return config; | |||
} | |||
}); | |||
public Config create(final String name, final Config config) throws IOException { | |||
final File file = fileFor(name); | |||
if (!file.createNewFile()) { | |||
final String message = "Configuration file " + file.getName() + " already exists"; | |||
throw new IllegalStateException(message); | |||
} | |||
try (FileOutputStream stream = new FileOutputStream(file, false)) { | |||
stream.write(config.toString().getBytes(StandardCharsets.UTF_8)); | |||
return config; | |||
} | |||
} | |||
@Override | |||
public CompletionStage<Void> delete(final String name) { | |||
return asyncWorker.runAsync(() -> { | |||
final File file = fileFor(name); | |||
if (!file.delete()) | |||
throw new IOException("Cannot delete configuration file " + file.getName()); | |||
}); | |||
public void delete(final String name) throws IOException { | |||
final File file = fileFor(name); | |||
if (!file.delete()) | |||
throw new IOException("Cannot delete configuration file " + file.getName()); | |||
} | |||
@Override | |||
public CompletionStage<Set<String>> enumerate() { | |||
return asyncWorker.supplyAsync(() -> Stream.of(context.fileList()) | |||
public Set<String> enumerate() { | |||
return Stream.of(context.fileList()) | |||
.filter(name -> name.endsWith(".conf")) | |||
.map(name -> name.substring(0, name.length() - ".conf".length())) | |||
.collect(Collectors.toUnmodifiableSet())); | |||
.collect(Collectors.toUnmodifiableSet()); | |||
} | |||
private File fileFor(final String name) { | |||
@@ -71,28 +62,23 @@ public final class FileConfigStore implements ConfigStore { | |||
} | |||
@Override | |||
public CompletionStage<Config> load(final String name) { | |||
return asyncWorker.supplyAsync(() -> { | |||
try (FileInputStream stream = new FileInputStream(fileFor(name))) { | |||
return Config.from(stream); | |||
} | |||
}); | |||
public Config load(final String name) throws IOException { | |||
try (FileInputStream stream = new FileInputStream(fileFor(name))) { | |||
return Config.from(stream); | |||
} | |||
} | |||
@Override | |||
public CompletionStage<Config> save(final String name, final Config config) { | |||
public Config save(final String name, final Config config) throws IOException { | |||
Log.d(TAG, "Requested save config for tunnel " + name); | |||
return asyncWorker.supplyAsync(() -> { | |||
final File file = fileFor(name); | |||
if (!file.isFile()) { | |||
final String message = "Configuration file " + file.getName() + " not found"; | |||
throw new IllegalStateException(message); | |||
} | |||
try (FileOutputStream stream = new FileOutputStream(file, false)) { | |||
Log.d(TAG, "Writing out config for tunnel " + name); | |||
stream.write(config.toString().getBytes(StandardCharsets.UTF_8)); | |||
return config; | |||
} | |||
}); | |||
final File file = fileFor(name); | |||
if (!file.isFile()) { | |||
final String message = "Configuration file " + file.getName() + " not found"; | |||
throw new IllegalStateException(message); | |||
} | |||
try (FileOutputStream stream = new FileOutputStream(file, false)) { | |||
stream.write(config.toString().getBytes(StandardCharsets.UTF_8)); | |||
return config; | |||
} | |||
} | |||
} |
@@ -1,12 +1,14 @@ | |||
package com.wireguard.android.model; | |||
import android.content.SharedPreferences; | |||
import android.support.annotation.NonNull; | |||
import com.wireguard.android.Application.ApplicationScope; | |||
import com.wireguard.android.backend.Backend; | |||
import com.wireguard.android.configStore.ConfigStore; | |||
import com.wireguard.android.model.Tunnel.State; | |||
import com.wireguard.android.model.Tunnel.Statistics; | |||
import com.wireguard.android.util.AsyncWorker; | |||
import com.wireguard.android.util.ExceptionLoggers; | |||
import com.wireguard.android.util.ObservableKeyedList; | |||
import com.wireguard.android.util.ObservableSortedKeyedArrayList; | |||
@@ -38,6 +40,7 @@ public final class TunnelManager { | |||
private static final String KEY_RUNNING_TUNNELS = "enabled_configs"; | |||
private static final String TAG = TunnelManager.class.getSimpleName(); | |||
private final AsyncWorker asyncWorker; | |||
private final Backend backend; | |||
private final ConfigStore configStore; | |||
private final SharedPreferences preferences; | |||
@@ -45,45 +48,55 @@ public final class TunnelManager { | |||
new ObservableSortedKeyedArrayList<>(COMPARATOR); | |||
@Inject | |||
public TunnelManager(final Backend backend, final ConfigStore configStore, | |||
final SharedPreferences preferences) { | |||
public TunnelManager(final AsyncWorker asyncWorker, final Backend backend, | |||
final ConfigStore configStore, final SharedPreferences preferences) { | |||
this.asyncWorker = asyncWorker; | |||
this.backend = backend; | |||
this.configStore = configStore; | |||
this.preferences = preferences; | |||
} | |||
private Tunnel add(final String name, final Config config, final State state) { | |||
private Tunnel addToList(final String name, final Config config, final State state) { | |||
final Tunnel tunnel = new Tunnel(this, name, config, state); | |||
tunnels.add(tunnel); | |||
return tunnel; | |||
} | |||
public CompletionStage<Tunnel> create(final String name, final Config config) { | |||
public CompletionStage<Tunnel> create(@NonNull final String name, final Config config) { | |||
if (!Tunnel.isNameValid(name)) | |||
return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid name")); | |||
if (tunnels.containsKey(name)) { | |||
final String message = "Tunnel " + name + " already exists"; | |||
return CompletableFuture.failedFuture(new IllegalArgumentException(message)); | |||
} | |||
return configStore.create(name, config).thenApply(cfg -> add(name, cfg, State.DOWN)); | |||
return asyncWorker.supplyAsync(() -> configStore.create(name, config)) | |||
.thenApply(savedConfig -> addToList(name, savedConfig, State.DOWN)); | |||
} | |||
CompletionStage<Void> delete(final Tunnel tunnel) { | |||
return setTunnelState(tunnel, State.DOWN) | |||
.thenCompose(x -> configStore.delete(tunnel.getName())) | |||
.thenAccept(x -> remove(tunnel)); | |||
return asyncWorker.runAsync(() -> { | |||
backend.setState(tunnel, State.DOWN); | |||
configStore.delete(tunnel.getName()); | |||
}).thenAccept(x -> { | |||
if (tunnel.getName().equals(preferences.getString(KEY_PRIMARY_TUNNEL, null))) | |||
preferences.edit().remove(KEY_PRIMARY_TUNNEL).apply(); | |||
tunnels.remove(tunnel); | |||
}); | |||
} | |||
CompletionStage<Config> getTunnelConfig(final Tunnel tunnel) { | |||
return configStore.load(tunnel.getName()).thenApply(tunnel::onConfigChanged); | |||
return asyncWorker.supplyAsync(() -> configStore.load(tunnel.getName())) | |||
.thenApply(tunnel::onConfigChanged); | |||
} | |||
CompletionStage<State> getTunnelState(final Tunnel tunnel) { | |||
return backend.getState(tunnel).thenApply(tunnel::onStateChanged); | |||
return asyncWorker.supplyAsync(() -> backend.getState(tunnel)) | |||
.thenApply(tunnel::onStateChanged); | |||
} | |||
CompletionStage<Statistics> getTunnelStatistics(final Tunnel tunnel) { | |||
return backend.getStatistics(tunnel).thenApply(tunnel::onStatisticsChanged); | |||
return asyncWorker.supplyAsync(() -> backend.getStatistics(tunnel)) | |||
.thenApply(tunnel::onStatisticsChanged); | |||
} | |||
public ObservableKeyedList<String, Tunnel> getTunnels() { | |||
@@ -91,16 +104,14 @@ public final class TunnelManager { | |||
} | |||
public void onCreate() { | |||
configStore.enumerate().thenAcceptBoth(backend.enumerate(), (names, running) -> { | |||
for (final String name : names) | |||
add(name, null, running.contains(name) ? State.UP : State.DOWN); | |||
}).whenComplete(ExceptionLoggers.E); | |||
asyncWorker.supplyAsync(configStore::enumerate) | |||
.thenAcceptBoth(asyncWorker.supplyAsync(backend::enumerate), this::onTunnelsLoaded) | |||
.whenComplete(ExceptionLoggers.E); | |||
} | |||
private void remove(final Tunnel tunnel) { | |||
if (tunnel.getName().equals(preferences.getString(KEY_PRIMARY_TUNNEL, null))) | |||
preferences.edit().remove(KEY_PRIMARY_TUNNEL).apply(); | |||
tunnels.remove(tunnel); | |||
private void onTunnelsLoaded(final Set<String> present, final Set<String> running) { | |||
for (final String name : present) | |||
addToList(name, null, running.contains(name) ? State.UP : State.DOWN); | |||
} | |||
public CompletionStage<Void> restoreState() { | |||
@@ -125,13 +136,14 @@ public final class TunnelManager { | |||
} | |||
CompletionStage<Config> setTunnelConfig(final Tunnel tunnel, final Config config) { | |||
return backend.applyConfig(tunnel, config) | |||
.thenCompose(cfg -> configStore.save(tunnel.getName(), cfg)) | |||
.thenApply(tunnel::onConfigChanged); | |||
return asyncWorker.supplyAsync(() -> { | |||
final Config appliedConfig = backend.applyConfig(tunnel, config); | |||
return configStore.save(tunnel.getName(), appliedConfig); | |||
}).thenApply(tunnel::onConfigChanged); | |||
} | |||
CompletionStage<State> setTunnelState(final Tunnel tunnel, final State state) { | |||
return backend.setState(tunnel, state) | |||
return asyncWorker.supplyAsync(() -> backend.setState(tunnel, state)) | |||
.thenApply(tunnel::onStateChanged); | |||
} | |||
} |