diff options
Diffstat (limited to 'app/src/main/java/com/wireguard/android/backends/VpnService.java')
-rw-r--r-- | app/src/main/java/com/wireguard/android/backends/VpnService.java | 559 |
1 files changed, 0 insertions, 559 deletions
diff --git a/app/src/main/java/com/wireguard/android/backends/VpnService.java b/app/src/main/java/com/wireguard/android/backends/VpnService.java deleted file mode 100644 index 5002a835..00000000 --- a/app/src/main/java/com/wireguard/android/backends/VpnService.java +++ /dev/null @@ -1,559 +0,0 @@ -package com.wireguard.android.backends; - -import android.app.Service; -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Binder; -import android.os.Build; -import android.os.IBinder; -import android.preference.PreferenceManager; -import android.provider.OpenableColumns; -import android.service.quicksettings.TileService; -import android.system.OsConstants; -import android.util.Log; -import android.widget.Toast; - -import com.wireguard.android.NotSupportedActivity; -import com.wireguard.android.QuickTileService; -import com.wireguard.android.R; -import com.wireguard.android.databinding.ObservableSortedMap; -import com.wireguard.android.databinding.ObservableTreeMap; -import com.wireguard.config.Config; -import com.wireguard.config.Peer; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.FilenameFilter; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -/** - * Service that handles config state coordination and all background processing for the application. - */ - -public class VpnService extends Service - implements SharedPreferences.OnSharedPreferenceChangeListener { - public static final String KEY_ENABLED_CONFIGS = "enabled_configs"; - public static final String KEY_PRIMARY_CONFIG = "primary_config"; - public static final String KEY_RESTORE_ON_BOOT = "restore_on_boot"; - private static final String TAG = "WireGuard/VpnService"; - - private static VpnService instance; - private final IBinder binder = new Binder(); - private final ObservableTreeMap<String, Config> configurations = new ObservableTreeMap<>(); - private final Set<String> enabledConfigs = new HashSet<>(); - private SharedPreferences preferences; - private String primaryName; - private RootShell rootShell; - - public static VpnService getInstance() { - return instance; - } - - /** - * 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 ObservableSortedMap<String, Config> getConfigs() { - return configurations; - } - - public void importFrom(final Uri... uris) { - new ConfigImporter().execute(uris); - } - - @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"); - } - })); - preferences = PreferenceManager.getDefaultSharedPreferences(this); - preferences.registerOnSharedPreferenceChangeListener(this); - } - - @Override - public void onDestroy() { - preferences.unregisterOnSharedPreferenceChangeListener(this); - } - - @Override - public void onSharedPreferenceChanged(final SharedPreferences preferences, - final String key) { - if (!KEY_PRIMARY_CONFIG.equals(key)) - return; - boolean changed = false; - final String newName = preferences.getString(key, null); - if (primaryName != null && !primaryName.equals(newName)) { - final Config oldConfig = configurations.get(primaryName); - if (oldConfig != null) - oldConfig.setIsPrimary(false); - changed = true; - } - if (newName != null && !newName.equals(primaryName)) { - final Config newConfig = configurations.get(newName); - if (newConfig != null) - newConfig.setIsPrimary(true); - else - preferences.edit().remove(KEY_PRIMARY_CONFIG).apply(); - changed = true; - } - primaryName = newName; - if (changed) - updateTile(); - } - - @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 void updateTile() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) - return; - Log.v(TAG, "Requesting quick tile update"); - TileService.requestListeningState(this, new ComponentName(this, QuickTileService.class)); - } - - 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) { - config.setIsEnabled(!result); - if (!result) { - Toast.makeText(getApplicationContext(), getString(R.string.error_down), - Toast.LENGTH_SHORT).show(); - return; - } - enabledConfigs.remove(config.getName()); - preferences.edit().putStringSet(KEY_ENABLED_CONFIGS, enabledConfigs).apply(); - if (config.getName().equals(primaryName)) - updateTile(); - } - } - - private class ConfigEnabler extends AsyncTask<Void, Void, Integer> { - private final Config config; - - private ConfigEnabler(final Config config) { - this.config = config; - } - - @Override - protected Integer doInBackground(final Void... voids) { - if (!new File("/sys/module/wireguard").exists()) - return -0xfff0001; - if (!existsInPath("su")) - return -0xfff0002; - Log.i(TAG, "Running wg-quick up for " + config.getName()); - final File configFile = new File(getFilesDir(), config.getName() + ".conf"); - final int ret = rootShell.run(null, "wg-quick up '" + configFile.getPath() + "'"); - if (ret == OsConstants.EACCES) - return -0xfff0002; - return ret; - } - - private boolean existsInPath(final String file) { - final String pathEnv = System.getenv("PATH"); - if (pathEnv == null) - return false; - final String[] paths = pathEnv.split(":"); - for (final String path : paths) - if (new File(path, file).exists()) - return true; - return false; - } - - @Override - protected void onPostExecute(final Integer ret) { - config.setIsEnabled(ret == 0); - if (ret != 0) { - if (ret == -0xfff0001) { - startActivity(new Intent(getApplicationContext(), NotSupportedActivity.class)); - } else if (ret == -0xfff0002) { - Toast.makeText(getApplicationContext(), getString(R.string.error_su), - Toast.LENGTH_LONG).show(); - } else { - Toast.makeText(getApplicationContext(), getString(R.string.error_up), - Toast.LENGTH_SHORT).show(); - } - return; - } - enabledConfigs.add(config.getName()); - preferences.edit().putStringSet(KEY_ENABLED_CONFIGS, enabledConfigs).apply(); - if (config.getName().equals(primaryName)) - updateTile(); - } - } - - private class ConfigImporter extends AsyncTask<Uri, String, List<File>> { - @Override - protected List<File> doInBackground(final Uri... uris) { - final ContentResolver contentResolver = getContentResolver(); - final List<File> files = new ArrayList<>(uris.length); - for (final Uri uri : uris) { - if (isCancelled()) - return null; - String name = null; - if ("file".equals(uri.getScheme())) { - name = uri.getLastPathSegment(); - } else { - final String[] columns = {OpenableColumns.DISPLAY_NAME}; - try (final Cursor cursor = - getContentResolver().query(uri, columns, null, null, null)) { - if (cursor != null && cursor.moveToFirst() && !cursor.isNull(0)) { - name = cursor.getString(0); - Log.v(getClass().getSimpleName(), "Got name via cursor"); - } - } - if (name == null) { - name = Uri.decode(uri.getLastPathSegment()); - if (name.indexOf('/') >= 0) - name = name.substring(name.lastIndexOf('/') + 1); - Log.v(getClass().getSimpleName(), "Got name from urlencoded path"); - } - } - if (!name.endsWith(".conf")) - name = name + ".conf"; - if (!Config.isNameValid(name.substring(0, name.length() - 5))) { - Log.v(getClass().getSimpleName(), "Detected name is not valid: " + name); - publishProgress(name + ": Invalid config filename"); - continue; - } - Log.d(getClass().getSimpleName(), "Mapped URI " + uri + " to file name " + name); - final File output = new File(getFilesDir(), name); - if (output.exists()) { - Log.w(getClass().getSimpleName(), "Config file " + name + " already exists"); - publishProgress(name + " already exists"); - continue; - } - try (final InputStream in = contentResolver.openInputStream(uri); - final OutputStream out = new FileOutputStream(output, false)) { - if (in == null) - throw new IOException("Failed to open input"); - // FIXME: This is a rather arbitrary size. - final byte[] buffer = new byte[4096]; - int bytes; - while ((bytes = in.read(buffer)) != -1) - out.write(buffer, 0, bytes); - files.add(output); - } catch (final IOException e) { - Log.w(getClass().getSimpleName(), "Failed to import config from " + uri, e); - publishProgress(name + ": " + e.getMessage()); - } - } - return files; - } - - @Override - protected void onProgressUpdate(final String... errors) { - Toast.makeText(getApplicationContext(), errors[0], Toast.LENGTH_SHORT).show(); - } - - @Override - protected void onPostExecute(final List<File> files) { - new ConfigLoader().execute(files.toArray(new File[files.size()])); - } - } - - private class ConfigLoader extends AsyncTask<File, String, 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.setIsEnabled(interfaces.contains(configName)); - config.setName(configName); - configs.add(config); - } catch (IllegalArgumentException | IOException e) { - if (!file.delete()) { - Log.e(TAG, "Could not delete configuration for config " + configName); - } - Log.w(TAG, "Failed to load config from " + fileName, e); - publishProgress(fileName + ": " + e.getMessage()); - } - } - return configs; - } - - @Override - protected void onProgressUpdate(final String... errors) { - Toast.makeText(getApplicationContext(), errors[0], Toast.LENGTH_SHORT).show(); - } - - @Override - protected void onPostExecute(final List<Config> configs) { - if (configs == null) - return; - for (final Config config : configs) - configurations.put(config.getName(), config); - // Run the handler to avoid duplicating the code here. - onSharedPreferenceChanged(preferences, KEY_PRIMARY_CONFIG); - if (preferences.getBoolean(KEY_RESTORE_ON_BOOT, false)) { - final Set<String> configsToEnable = - preferences.getStringSet(KEY_ENABLED_CONFIGS, null); - if (configsToEnable != null) { - for (final String name : configsToEnable) { - final Config config = configurations.get(name); - if (config != null && !config.isEnabled()) - new ConfigEnabler(config).execute(); - } - } - } - } - } - - 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()); - if (config.getName().equals(primaryName)) { - // This will get picked up by the preference change listener. - preferences.edit().remove(KEY_PRIMARY_CONFIG).apply(); - } - } - } - - private class ConfigUpdater extends AsyncTask<Void, Void, Boolean> { - private final Config newConfig; - private final String newName; - private final String oldName; - private final Boolean shouldConnect; - private Config knownConfig; - - private ConfigUpdater(final Config knownConfig, final Config newConfig, - final Boolean shouldConnect) { - this.knownConfig = knownConfig; - this.newConfig = newConfig.copy(); - newName = newConfig.getName(); - // When adding a config, "old file" and "new file" are the same thing. - oldName = knownConfig != null ? knownConfig.getName() : newName; - this.shouldConnect = shouldConnect; - if (newName == null || !Config.isNameValid(newName)) - throw new IllegalArgumentException("This configuration does not have a valid name"); - if (isAddOrRename() && configurations.containsKey(newName)) - throw new IllegalStateException("Configuration " + newName + " already exists"); - if (newConfig.getInterface().getPublicKey() == null) - throw new IllegalArgumentException("This configuration needs a valid private key"); - for (final Peer peer : newConfig.getPeers()) - if (peer.getPublicKey() == null || peer.getPublicKey().isEmpty()) - throw new IllegalArgumentException("Each peer must have a valid public key"); - } - - @Override - protected Boolean doInBackground(final Void... voids) { - Log.i(TAG, (knownConfig == null ? "Adding" : "Updating") + " config " + newName); - final File newFile = new File(getFilesDir(), newName + ".conf"); - final File oldFile = new File(getFilesDir(), oldName + ".conf"); - if (isAddOrRename() && 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 isAddOrRename() { - return knownConfig == null || !newName.equals(oldName); - } - - private boolean isRename() { - return knownConfig != null && !newName.equals(oldName); - } - - @Override - protected void onPostExecute(final Boolean result) { - if (!result) - return; - if (knownConfig != null) - configurations.remove(oldName); - if (knownConfig == null) - knownConfig = new Config(); - knownConfig.copyFrom(newConfig); - knownConfig.setIsEnabled(false); - knownConfig.setIsPrimary(oldName != null && oldName.equals(primaryName)); - configurations.put(newName, knownConfig); - if (isRename() && oldName != null && oldName.equals(primaryName)) - preferences.edit().putString(KEY_PRIMARY_CONFIG, newName).apply(); - if (shouldConnect) - new ConfigEnabler(knownConfig).execute(); - } - } -} |