diff options
Diffstat (limited to 'app/src/main/java/com/wireguard/android/VpnService.java')
-rw-r--r-- | app/src/main/java/com/wireguard/android/VpnService.java | 339 |
1 files changed, 339 insertions, 0 deletions
diff --git a/app/src/main/java/com/wireguard/android/VpnService.java b/app/src/main/java/com/wireguard/android/VpnService.java new file mode 100644 index 00000000..2f3d97c8 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/VpnService.java @@ -0,0 +1,339 @@ +package com.wireguard.android; + +import android.app.Service; +import android.content.Intent; +import android.databinding.ObservableArrayMap; +import android.os.AsyncTask; +import android.os.Binder; +import android.os.IBinder; +import android.util.Log; + +import com.wireguard.config.Config; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * Service that handles config state coordination and all background processing for the application. + */ + +public class VpnService extends Service { + private static final String TAG = "VpnService"; + + private static VpnService instance; + + public static VpnService getInstance() { + return instance; + } + + private final IBinder binder = new Binder(); + private final ObservableArrayMap<String, Config> configurations = new ObservableArrayMap<>(); + private RootShell rootShell; + + /** + * Add a new configuration to the set of known configurations. The configuration will initially + * be disabled. The configuration's name must be unique within the set of known configurations. + * + * @param config The configuration to add. + */ + public void add(final Config config) { + new ConfigUpdater(null, config, false).execute(); + } + + /** + * Attempt to disable and tear down an interface for this configuration. The configuration's + * enabled state will be updated the operation is successful. If this configuration is already + * disconnected, or it is not a known configuration, no changes will be made. + * + * @param name The name of the configuration (in the set of known configurations) to disable. + */ + public void disable(final String name) { + final Config config = configurations.get(name); + if (config == null || !config.isEnabled()) + return; + new ConfigDisabler(config).execute(); + } + + /** + * Attempt to set up and enable an interface for this configuration. The configuration's enabled + * state will be updated if the operation is successful. If this configuration is already + * enabled, or it is not a known configuration, no changes will be made. + * + * @param name The name of the configuration (in the set of known configurations) to enable. + */ + public void enable(final String name) { + final Config config = configurations.get(name); + if (config == null || config.isEnabled()) + return; + new ConfigEnabler(config).execute(); + } + + /** + * Retrieve a configuration known and managed by this service. The returned object must not be + * modified directly. + * + * @param name The name of the configuration (in the set of known configurations) to retrieve. + * @return An object representing the configuration. This object must not be modified. + */ + public Config get(final String name) { + return configurations.get(name); + } + + /** + * Retrieve the set of configurations known and managed by the service. Configurations in this + * set must not be modified directly. If a configuration is to be updated, first create a copy + * of it by calling getCopy(). + * + * @return The set of known configurations. + */ + public ObservableArrayMap<String, Config> getConfigs() { + return configurations; + } + + @Override + public IBinder onBind(final Intent intent) { + instance = this; + return binder; + } + + @Override + public void onCreate() { + // Ensure the service sticks around after being unbound. This only needs to happen once. + startService(new Intent(this, getClass())); + rootShell = new RootShell(this); + new ConfigLoader().execute(getFilesDir().listFiles(new FilenameFilter() { + @Override + public boolean accept(final File dir, final String name) { + return name.endsWith(".conf"); + } + })); + } + + @Override + public int onStartCommand(final Intent intent, final int flags, final int startId) { + instance = this; + return START_STICKY; + } + + /** + * Remove a configuration from being managed by the service. If it is currently enabled, the + * the configuration will be disabled before removal. If successful, the configuration will be + * removed from persistent storage. If the configuration is not known to the service, no changes + * will be made. + * + * @param name The name of the configuration (in the set of known configurations) to remove. + */ + public void remove(final String name) { + final Config config = configurations.get(name); + if (config == null) + return; + if (config.isEnabled()) + new ConfigDisabler(config).execute(); + new ConfigRemover(config).execute(); + } + + /** + * Update the attributes of the named configuration. If the configuration is currently enabled, + * it will be disabled before the update, and the service will attempt to re-enable it + * afterward. If successful, the updated configuration will be saved to persistent storage. + * + * @param name The name of an existing configuration to update. + * @param config A copy of the configuration, with updated attributes. + */ + public void update(final String name, final Config config) { + if (name == null) + return; + if (configurations.containsValue(config)) + throw new IllegalArgumentException("Config " + config.getName() + " modified directly"); + final Config oldConfig = configurations.get(name); + if (oldConfig == null) + return; + final boolean wasEnabled = oldConfig.isEnabled(); + if (wasEnabled) + new ConfigDisabler(oldConfig).execute(); + new ConfigUpdater(oldConfig, config, wasEnabled).execute(); + } + + private class ConfigDisabler extends AsyncTask<Void, Void, Boolean> { + private final Config config; + + private ConfigDisabler(final Config config) { + this.config = config; + } + + @Override + protected Boolean doInBackground(final Void... voids) { + Log.i(TAG, "Running wg-quick down for " + config.getName()); + final File configFile = new File(getFilesDir(), config.getName() + ".conf"); + return rootShell.run(null, "wg-quick down '" + configFile.getPath() + "'") == 0; + } + + @Override + protected void onPostExecute(final Boolean result) { + if (!result) + return; + config.setEnabled(false); + } + } + + private class ConfigEnabler extends AsyncTask<Void, Void, Boolean> { + private final Config config; + + private ConfigEnabler(final Config config) { + this.config = config; + } + + @Override + protected Boolean doInBackground(final Void... voids) { + Log.i(TAG, "Running wg-quick up for " + config.getName()); + final File configFile = new File(getFilesDir(), config.getName() + ".conf"); + return rootShell.run(null, "wg-quick up '" + configFile.getPath() + "'") == 0; + } + + @Override + protected void onPostExecute(final Boolean result) { + if (!result) + return; + config.setEnabled(true); + } + } + + private class ConfigLoader extends AsyncTask<File, Void, List<Config>> { + @Override + protected List<Config> doInBackground(final File... files) { + final List<Config> configs = new LinkedList<>(); + final List<String> interfaces = new LinkedList<>(); + final String command = "wg show interfaces"; + if (rootShell.run(interfaces, command) == 0 && interfaces.size() == 1) { + // wg puts all interface names on the same line. Split them into separate elements. + final String nameList = interfaces.get(0); + Collections.addAll(interfaces, nameList.split(" ")); + interfaces.remove(0); + } else { + interfaces.clear(); + Log.w(TAG, "No existing WireGuard interfaces found. Maybe they are all disabled?"); + } + for (final File file : files) { + if (isCancelled()) + return null; + final String fileName = file.getName(); + final String configName = fileName.substring(0, fileName.length() - 5); + Log.v(TAG, "Attempting to load config " + configName); + try { + final Config config = new Config(); + config.parseFrom(openFileInput(fileName)); + config.setEnabled(interfaces.contains(configName)); + config.setName(configName); + configs.add(config); + } catch (IllegalArgumentException | IOException e) { + Log.w(TAG, "Failed to load config from " + fileName, e); + } + } + return configs; + } + + @Override + protected void onPostExecute(final List<Config> configs) { + if (configs == null) + return; + for (final Config config : configs) + configurations.put(config.getName(), config); + } + } + + private class ConfigRemover extends AsyncTask<Void, Void, Boolean> { + private final Config config; + + private ConfigRemover(final Config config) { + this.config = config; + } + + @Override + protected Boolean doInBackground(final Void... voids) { + Log.i(TAG, "Removing config " + config.getName()); + final File configFile = new File(getFilesDir(), config.getName() + ".conf"); + if (configFile.delete()) { + return true; + } else { + Log.e(TAG, "Could not delete configuration for config " + config.getName()); + return false; + } + } + + @Override + protected void onPostExecute(final Boolean result) { + if (!result) + return; + configurations.remove(config.getName()); + } + } + + private class ConfigUpdater extends AsyncTask<Void, Void, Boolean> { + private Config newConfig; + private final String newName; + private final Config oldConfig; + private final String oldName; + private final Boolean shouldConnect; + + private ConfigUpdater(final Config oldConfig, final Config newConfig, + final Boolean shouldConnect) { + super(); + this.newConfig = newConfig; + this.oldConfig = oldConfig; + this.shouldConnect = shouldConnect; + newName = newConfig.getName(); + oldName = oldConfig.getName(); + if (isRename() && configurations.containsKey(newName)) + throw new IllegalStateException("Config " + newName + " already exists"); + } + + @Override + protected Boolean doInBackground(final Void... voids) { + Log.i(TAG, (oldConfig == null ? "Adding" : "Updating") + " config " + newName); + final File newFile = new File(getFilesDir(), newName + ".conf"); + final File oldFile = new File(getFilesDir(), oldName + ".conf"); + if (isRename() && newFile.exists()) { + Log.w(TAG, "Refusing to overwrite existing config configuration"); + return false; + } + try { + final FileOutputStream stream = openFileOutput(oldFile.getName(), MODE_PRIVATE); + stream.write(newConfig.toString().getBytes(StandardCharsets.UTF_8)); + stream.close(); + } catch (final IOException e) { + Log.e(TAG, "Could not save configuration for config " + oldName, e); + return false; + } + if (isRename() && !oldFile.renameTo(newFile)) { + Log.e(TAG, "Could not rename " + oldFile.getName() + " to " + newFile.getName()); + return false; + } + return true; + } + + private boolean isRename() { + return oldConfig != null && !newConfig.getName().equals(oldConfig.getName()); + } + + @Override + protected void onPostExecute(final Boolean result) { + if (!result) + return; + if (oldConfig != null) { + configurations.remove(oldName); + oldConfig.copyFrom(newConfig); + newConfig = oldConfig; + } + newConfig.setEnabled(false); + configurations.put(newName, newConfig); + if (shouldConnect) + new ConfigEnabler(newConfig).execute(); + } + } +} |