[PATCH 3/4] Add support of wireguard-go as an Android Vpn Service
Aurélien Chabot
aurelien at chabot.fr
Tue Nov 7 05:38:14 CET 2017
Signed-off-by: Aurélien Chabot <aurelien at chabot.fr>
---
.gitignore | 5 +
app/build.gradle | 1 +
app/src/main/AndroidManifest.xml | 17 +-
.../java/com/wireguard/android/AddActivity.java | 5 +-
.../com/wireguard/android/AndroidVpnService.java | 147 +++++++
.../com/wireguard/android/BaseConfigActivity.java | 55 ++-
.../com/wireguard/android/BaseConfigFragment.java | 2 +-
.../wireguard/android/BootCompletedReceiver.java | 2 +-
.../java/com/wireguard/android/ConfigActivity.java | 4 +-
.../com/wireguard/android/ConfigEditFragment.java | 6 +-
.../com/wireguard/android/ConfigListFragment.java | 4 +-
.../wireguard/android/ConfigListPreference.java | 2 +-
.../java/com/wireguard/android/ConfigManager.java | 412 +++++++++++++++++++
.../com/wireguard/android/KernelVpnService.java | 115 ++++++
.../com/wireguard/android/QuickTileService.java | 30 +-
.../java/com/wireguard/android/VpnService.java | 446 +++------------------
app/src/main/java/com/wireguard/config/Config.java | 12 +-
.../main/java/com/wireguard/config/Interface.java | 12 +-
.../java/com/wireguard/config/IpcAttribute.java | 55 +++
.../java/com/wireguard/config/IpcSerializable.java | 9 +
app/src/main/java/com/wireguard/config/Peer.java | 21 +-
.../java/com/wireguard/crypto/KeyEncoding.java | 12 +
.../main/java/com/wireguard/crypto/Keypair.java | 6 +
app/src/main/res/layout/config_list_item.xml | 4 +-
app/src/main/res/values/strings.xml | 2 +
app/src/main/res/xml/preferences.xml | 5 +
settings.gradle | 2 +-
wireguardbinding/build.gradle | 2 +
wireguardbinding/src/wireguard | 1 +
.../src/wireguardbinding/tun_android.go | 61 +++
wireguardbinding/src/wireguardbinding/wireguard.go | 49 +++
31 files changed, 1053 insertions(+), 453 deletions(-)
create mode 100644 app/src/main/java/com/wireguard/android/AndroidVpnService.java
create mode 100644 app/src/main/java/com/wireguard/android/ConfigManager.java
create mode 100644 app/src/main/java/com/wireguard/android/KernelVpnService.java
create mode 100644 app/src/main/java/com/wireguard/config/IpcAttribute.java
create mode 100644 app/src/main/java/com/wireguard/config/IpcSerializable.java
create mode 100644 wireguardbinding/build.gradle
create mode 120000 wireguardbinding/src/wireguard
create mode 100644 wireguardbinding/src/wireguardbinding/tun_android.go
create mode 100644 wireguardbinding/src/wireguardbinding/wireguard.go
diff --git a/.gitignore b/.gitignore
index 32babdb..b9e505b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,7 +5,12 @@
.DS_Store
Thumbs.db
build/
+*.swp
*.apk
*.class
*.dex
*.iml
+*.a
+*.aar
+wireguardbinding/pkg
+wireguardbinding/src/golang.org
diff --git a/app/build.gradle b/app/build.gradle
index 5caf20b..384c09f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -23,4 +23,5 @@ android {
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
+ compile project(':wireguardbinding')
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1563d31..7685dc8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
android:installLocation="internalOnly">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+ <uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="false"
@@ -48,9 +49,23 @@
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
</service>
+
+ <service android:name=".ConfigManager" />
+
<service
- android:name=".VpnService"
+ android:name=".KernelVpnService"
android:exported="false" />
+
+ <service
+ android:name=".AndroidVpnService"
+ android:permission="android.permission.BIND_VPN_SERVICE" >
+ <intent-filter>
+ <action android:name="android.net.VpnService" />
+ </intent-filter>
+ </service>
+
+
+
</application>
</manifest>
diff --git a/app/src/main/java/com/wireguard/android/AddActivity.java b/app/src/main/java/com/wireguard/android/AddActivity.java
index 080eeca..2cb729d 100644
--- a/app/src/main/java/com/wireguard/android/AddActivity.java
+++ b/app/src/main/java/com/wireguard/android/AddActivity.java
@@ -30,8 +30,9 @@ public class AddActivity extends BaseConfigActivity {
}
@Override
- protected void onServiceAvailable() {
- super.onServiceAvailable();
+ protected void onConfigManagerAvailable() {
+ super.onConfigManagerAvailable();
+
final FragmentManager fm = getFragmentManager();
ConfigEditFragment fragment = (ConfigEditFragment) fm.findFragmentById(R.id.master_fragment);
if (fragment == null) {
diff --git a/app/src/main/java/com/wireguard/android/AndroidVpnService.java b/app/src/main/java/com/wireguard/android/AndroidVpnService.java
new file mode 100644
index 0000000..2a83827
--- /dev/null
+++ b/app/src/main/java/com/wireguard/android/AndroidVpnService.java
@@ -0,0 +1,147 @@
+package com.wireguard.android;
+
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import com.wireguard.config.Config;
+
+import wireguardbinding.Wireguardbinding;
+
+public class AndroidVpnService extends android.net.VpnService
+ implements com.wireguard.android.VpnService {
+ private static final String TAG = "AndroidVpnService";
+
+ private static AndroidVpnService instance;
+ public static AndroidVpnService getInstance() {
+ return instance;
+ }
+
+ private final IBinder binder = new Binder();
+ private String enabledConfig;
+
+ @Override
+ public void disable(final String name) {
+ final Config config = ConfigManager.getInstance().get(name);
+ if (config == null || !config.isEnabled())
+ return;
+ new ConfigDisabler(config).execute();
+ }
+
+ @Override
+ public void enable(final String name) {
+ if (enabledConfig != null) // One config at a time
+ return;
+
+ final Config config = ConfigManager.getInstance().get(name);
+ if (config == null || config.isEnabled())
+ return;
+
+ new ConfigEnabler(config).execute();
+ }
+
+ @Override
+ public IBinder onBind(final Intent intent) {
+ instance = this;
+ return binder;
+ }
+
+ @Override
+ public void onRevoke() {
+ if (enabledConfig != null)
+ disable(enabledConfig);
+ stopSelf();
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ // Ensure the service sticks around after being unbound. This only needs to happen once.
+ startService(new Intent(this, getClass()));
+ }
+
+ @Override
+ public int onStartCommand(final Intent intent, final int flags, final int startId) {
+ instance = this;
+ return START_STICKY;
+ }
+
+ 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) {
+ Wireguardbinding.stop();
+ return true;
+ }
+
+ @Override
+ protected void onPostExecute(final Boolean result) {
+ if (!result)
+ return;
+ ConfigManager.getInstance().setIsEnable(config.getName(), false);
+ enabledConfig = null;
+ }
+ }
+
+ 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) {
+ // Vpn service need to be already ready
+ if(prepare(getBaseContext()) != null)
+ return false;
+
+ Builder builder = new Builder();
+
+ builder.setSession(config.getName());
+ builder.addAddress(config.getInterface().getAddress(), 32);
+ if (config.getInterface().getDns() != null)
+ builder.addDnsServer(config.getInterface().getDns());
+ builder.addRoute("0.0.0.0", 0);
+ builder.setBlocking(true);
+ ParcelFileDescriptor tun = builder.establish();
+ if (tun == null) {
+ Log.d(TAG, "Unable to create tun device");
+ return false;
+ }
+
+ Wireguardbinding.start(tun.detachFd(), config.getName());
+ long socket = 0;
+ while((socket = Wireguardbinding.socket()) == 0) {
+ Log.d(TAG, "Wait for socket");
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ protect((int) socket);
+
+ Wireguardbinding.setConf(config.toIpcString());
+
+ return true;
+ }
+
+ @Override
+ protected void onPostExecute(final Boolean result) {
+ if (!result)
+ return;
+ ConfigManager.getInstance().setIsEnable(config.getName(), true);
+ enabledConfig = config.getName();
+ }
+ }
+}
diff --git a/app/src/main/java/com/wireguard/android/BaseConfigActivity.java b/app/src/main/java/com/wireguard/android/BaseConfigActivity.java
index 17ca3b6..4417063 100644
--- a/app/src/main/java/com/wireguard/android/BaseConfigActivity.java
+++ b/app/src/main/java/com/wireguard/android/BaseConfigActivity.java
@@ -4,7 +4,6 @@ import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
@@ -43,12 +42,19 @@ abstract class BaseConfigActivity extends Activity {
initialConfig = intent.getStringExtra(KEY_CURRENT_CONFIG);
wasEditing = intent.getBooleanExtra(KEY_IS_EDITING, false);
}
- // Trigger starting the service as early as possible
- if (VpnService.getInstance() != null)
- onServiceAvailable();
+
+ // Trigger starting the services as early as possible
+ if (ConfigManager.getInstance() != null)
+ onConfigManagerAvailable();
else
- bindService(new Intent(this, VpnService.class), new ServiceConnectionCallbacks(),
- Context.BIND_AUTO_CREATE);
+ new ConfigManagerConnectionCallbacks(this);
+
+ Intent intent = AndroidVpnService.prepare(this);
+ if (intent != null) {
+ startActivityForResult(intent, 0);
+ } else {
+ onActivityResult(0, RESULT_OK, null);
+ }
}
protected abstract void onCurrentConfigChanged(Config config);
@@ -63,13 +69,25 @@ abstract class BaseConfigActivity extends Activity {
outState.putBoolean(KEY_IS_EDITING, isEditing);
}
- protected void onServiceAvailable() {
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode == RESULT_OK) {
+ if (VpnService.Singleton.getInstance() != null)
+ onVpnServiceAvailable();
+ else
+ new VpnServiceConnectionCallbacks(this);
+ }
+ }
+
+ protected void onConfigManagerAvailable() {
// Make sure the subclass activity is initialized before setting its config.
if (initialConfig != null && currentConfig == null)
- setCurrentConfig(VpnService.getInstance().get(initialConfig));
+ setCurrentConfig(ConfigManager.getInstance().get(initialConfig));
setIsEditing(wasEditing);
}
+ protected void onVpnServiceAvailable() {
+ }
+
public void setCurrentConfig(final Config config) {
if (currentConfig == config)
return;
@@ -84,18 +102,25 @@ abstract class BaseConfigActivity extends Activity {
onEditingStateChanged(isEditing);
}
- private class ServiceConnectionCallbacks implements ServiceConnection {
+ private class VpnServiceConnectionCallbacks extends VpnService.Singleton.VpnServiceConnection {
+ public VpnServiceConnectionCallbacks(Context ctx) {
+ super(ctx);
+ }
+
@Override
public void onServiceConnected(final ComponentName component, final IBinder binder) {
- // We don't actually need a binding, only notification that the service is started.
- unbindService(this);
- onServiceAvailable();
+ onVpnServiceAvailable();
+ }
+ }
+
+ private class ConfigManagerConnectionCallbacks extends ConfigManager.ConfigManagerConnection {
+ public ConfigManagerConnectionCallbacks(Context ctx) {
+ super(ctx);
}
@Override
- public void onServiceDisconnected(final ComponentName component) {
- // This can never happen; the service runs in the same thread as the activity.
- throw new IllegalStateException();
+ public void onServiceConnected(final ComponentName component, final IBinder binder) {
+ onConfigManagerAvailable();
}
}
}
diff --git a/app/src/main/java/com/wireguard/android/BaseConfigFragment.java b/app/src/main/java/com/wireguard/android/BaseConfigFragment.java
index 4291264..19a45e6 100644
--- a/app/src/main/java/com/wireguard/android/BaseConfigFragment.java
+++ b/app/src/main/java/com/wireguard/android/BaseConfigFragment.java
@@ -30,7 +30,7 @@ abstract class BaseConfigFragment extends Fragment {
else if (getArguments() != null)
initialConfig = getArguments().getString(KEY_CURRENT_CONFIG);
if (initialConfig != null && currentConfig == null)
- setCurrentConfig(VpnService.getInstance().get(initialConfig));
+ setCurrentConfig(ConfigManager.getInstance().get(initialConfig));
}
@Override
diff --git a/app/src/main/java/com/wireguard/android/BootCompletedReceiver.java b/app/src/main/java/com/wireguard/android/BootCompletedReceiver.java
index 68cb5f1..49f6530 100644
--- a/app/src/main/java/com/wireguard/android/BootCompletedReceiver.java
+++ b/app/src/main/java/com/wireguard/android/BootCompletedReceiver.java
@@ -10,6 +10,6 @@ public class BootCompletedReceiver extends BroadcastReceiver {
public void onReceive(final Context context, final Intent intent) {
if (!intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED))
return;
- context.startService(new Intent(context, VpnService.class));
+ context.startService(new Intent(context, ConfigManager.class));
}
}
diff --git a/app/src/main/java/com/wireguard/android/ConfigActivity.java b/app/src/main/java/com/wireguard/android/ConfigActivity.java
index 61ccbc1..61f79ce 100644
--- a/app/src/main/java/com/wireguard/android/ConfigActivity.java
+++ b/app/src/main/java/com/wireguard/android/ConfigActivity.java
@@ -222,8 +222,8 @@ public class ConfigActivity extends BaseConfigActivity {
}
@Override
- protected void onServiceAvailable() {
- super.onServiceAvailable();
+ protected void onConfigManagerAvailable() {
+ super.onConfigManagerAvailable();
// Allow creating fragments.
isServiceAvailable = true;
moveToState(getCurrentConfig(), isEditing());
diff --git a/app/src/main/java/com/wireguard/android/ConfigEditFragment.java b/app/src/main/java/com/wireguard/android/ConfigEditFragment.java
index 1edae9c..60d2720 100644
--- a/app/src/main/java/com/wireguard/android/ConfigEditFragment.java
+++ b/app/src/main/java/com/wireguard/android/ConfigEditFragment.java
@@ -113,12 +113,12 @@ public class ConfigEditFragment extends BaseConfigFragment {
}
private void saveConfig() {
- final VpnService service = VpnService.getInstance();
+ final ConfigManager configManager = ConfigManager.getInstance();
try {
if (getCurrentConfig() != null)
- service.update(getCurrentConfig().getName(), localConfig);
+ configManager.update(getCurrentConfig().getName(), localConfig);
else
- service.add(localConfig);
+ configManager.add(localConfig);
} catch (final IllegalArgumentException | IllegalStateException e) {
Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
return;
diff --git a/app/src/main/java/com/wireguard/android/ConfigListFragment.java b/app/src/main/java/com/wireguard/android/ConfigListFragment.java
index 8586a74..c5af9a4 100644
--- a/app/src/main/java/com/wireguard/android/ConfigListFragment.java
+++ b/app/src/main/java/com/wireguard/android/ConfigListFragment.java
@@ -42,7 +42,7 @@ public class ConfigListFragment extends BaseConfigFragment {
final Bundle savedInstanceState) {
final ConfigListFragmentBinding binding =
ConfigListFragmentBinding.inflate(inflater, parent, false);
- binding.setConfigs(VpnService.getInstance().getConfigs());
+ binding.setConfigs(ConfigManager.getInstance().getConfigs());
listView = binding.configList;
listView.setMultiChoiceModeListener(new ConfigListModeListener());
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@@ -109,7 +109,7 @@ public class ConfigListFragment extends BaseConfigFragment {
if (configsToRemove.contains(getCurrentConfig()))
setCurrentConfig(null);
for (final Config config : configsToRemove)
- VpnService.getInstance().remove(config.getName());
+ ConfigManager.getInstance().remove(config.getName());
configsToRemove.clear();
mode.finish();
return true;
diff --git a/app/src/main/java/com/wireguard/android/ConfigListPreference.java b/app/src/main/java/com/wireguard/android/ConfigListPreference.java
index 3842161..3e0194c 100644
--- a/app/src/main/java/com/wireguard/android/ConfigListPreference.java
+++ b/app/src/main/java/com/wireguard/android/ConfigListPreference.java
@@ -14,7 +14,7 @@ public class ConfigListPreference extends ListPreference {
public ConfigListPreference(final Context context, final AttributeSet attrs,
final int defStyleAttr, final int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- final Set<String> entrySet = VpnService.getInstance().getConfigs().keySet();
+ final Set<String> entrySet = ConfigManager.getInstance().getConfigs().keySet();
final CharSequence[] entries = entrySet.toArray(new CharSequence[entrySet.size()]);
setEntries(entries);
setEntryValues(entries);
diff --git a/app/src/main/java/com/wireguard/android/ConfigManager.java b/app/src/main/java/com/wireguard/android/ConfigManager.java
new file mode 100644
index 0000000..04d666f
--- /dev/null
+++ b/app/src/main/java/com/wireguard/android/ConfigManager.java
@@ -0,0 +1,412 @@
+package com.wireguard.android;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.service.quicksettings.TileService;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+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.nio.charset.StandardCharsets;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+public class ConfigManager extends Service
+ implements SharedPreferences.OnSharedPreferenceChangeListener {
+
+ private static final String TAG = "ConfigManager";
+ 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 final IBinder binder = new Binder();
+ private final Set<String> enabledConfigs = new HashSet<>();
+ private String primaryName;
+ private SharedPreferences preferences;
+
+ private final ObservableTreeMap<String, Config> configurations = new ObservableTreeMap<>();
+
+ private static ConfigManager instance;
+ public static ConfigManager getInstance() {
+ return instance;
+ }
+
+ public static class ConfigManagerConnection implements ServiceConnection {
+
+ private Context ctx;
+
+ public ConfigManagerConnection(Context ctx) {
+ this.ctx = ctx;
+ ctx.bindService(new Intent(ctx, ConfigManager.class), this,
+ Context.BIND_AUTO_CREATE);
+ }
+
+ @Override
+ public void onServiceConnected(final ComponentName component, final IBinder binder) {
+ // We don't actually need a binding, only notification that the service is started.
+ ctx.unbindService(this);
+ }
+
+ @Override
+ public void onServiceDisconnected(final ComponentName component) {
+ // This can never happen; the service runs in the same thread as the activity.
+ throw new IllegalStateException();
+ }
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(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()));
+ preferences = PreferenceManager.getDefaultSharedPreferences(this);
+ preferences.registerOnSharedPreferenceChangeListener(this);
+
+ onSharedPreferenceChanged(preferences, VpnService.KEY_USE_KERNEL_MODULE);
+
+ new ConfigLoader().execute(getFilesDir().listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(final File dir, final String name) {
+ return name.endsWith(".conf");
+ }
+ }));
+ }
+
+ @Override
+ public void onDestroy() {
+ preferences.unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(final SharedPreferences preferences,
+ final String key) {
+ switch (key) {
+ case VpnService.KEY_USE_KERNEL_MODULE: {
+ Log.d(TAG, "Update impl config");
+ if (preferences.getBoolean(VpnService.KEY_USE_KERNEL_MODULE, false))
+ VpnService.Singleton.setImplementation(VpnService.Singleton.VpnImplementation.KERNEL);
+ else
+ VpnService.Singleton.setImplementation(VpnService.Singleton.VpnImplementation.ANDROID);
+
+ new VpnService.Singleton.VpnServiceConnection(ConfigManager.this);
+ } break;
+ case KEY_PRIMARY_CONFIG: {
+ boolean changed = false;
+ final String newName = preferences.getString(key, null);
+ if (primaryName != null && !primaryName.equals(newName)) {
+ final Config oldConfig = get(primaryName);
+ if (oldConfig != null)
+ oldConfig.setIsPrimary(false);
+ changed = true;
+ }
+ if (newName != null && !newName.equals(primaryName)) {
+ final Config newConfig = get(newName);
+ if (newConfig != null)
+ newConfig.setIsPrimary(true);
+ else
+ preferences.edit().remove(KEY_PRIMARY_CONFIG).apply();
+ changed = true;
+ }
+ primaryName = newName;
+ if (changed)
+ updateTile();
+ } break;
+ }
+ }
+
+ @Override
+ public int onStartCommand(final Intent intent, final int flags, final int startId) {
+ instance = this;
+ return START_STICKY;
+ }
+
+ 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));
+ }
+
+ /**
+ * 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).execute();
+ }
+
+ /**
+ * Remove a configuration from being managed by the service.
+ * 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;
+
+ configurations.remove(name);
+ new ConfigRemover(config).execute();
+ }
+
+ /**
+ * Update the attributes of the named configuration.
+ *
+ * @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;
+
+ new ConfigUpdater(oldConfig, config).execute();
+ }
+
+ public void setIsEnable(final String name, final boolean isEnabled) {
+ if (name == null)
+ return;
+
+ Config config = get(name);
+ if (config == null)
+ return;
+ config.setIsEnabled(isEnabled);
+
+ if (isEnabled) {
+ enabledConfigs.add(name);
+ preferences.edit().putStringSet(KEY_ENABLED_CONFIGS, enabledConfigs).apply();
+ } else {
+ enabledConfigs.remove(name);
+ preferences.edit().putStringSet(KEY_ENABLED_CONFIGS, enabledConfigs).apply();
+ }
+
+ if (name.equals(primaryName))
+ updateTile();
+ }
+
+ /**
+ * 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;
+ }
+
+ 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<>();
+ 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) {
+ 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);
+
+ // Run the handler to avoid duplicating the code here.
+ onSharedPreferenceChanged(preferences, KEY_PRIMARY_CONFIG);
+
+ if (VpnService.Singleton.getInstance() != null)
+ onVpnServiceAvailable();
+ else
+ new VpnServiceConnectionCallbacks(ConfigManager.this);
+ }
+ }
+
+ protected void onVpnServiceAvailable() {
+ 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())
+ VpnService.Singleton.getInstance().enable(name);
+ }
+ }
+ }
+ }
+
+ private class VpnServiceConnectionCallbacks extends VpnService.Singleton.VpnServiceConnection {
+ public VpnServiceConnectionCallbacks(Context ctx) {
+ super(ctx);
+ }
+
+ @Override
+ public void onServiceConnected(final ComponentName component, final IBinder binder) {
+ onVpnServiceAvailable();
+ }
+ }
+
+
+ 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 Config knownConfig;
+ private final Config newConfig;
+ private final String newName;
+ private final String oldName;
+
+ private ConfigUpdater(final Config knownConfig, final Config newConfig) {
+ this.knownConfig = knownConfig;
+ this.newConfig = newConfig.copy();
+ this.newConfig.setIsEnabled(this.knownConfig.isEnabled());
+ newName = newConfig.getName();
+ // When adding a config, "old file" and "new file" are the same thing.
+ oldName = knownConfig != null ? knownConfig.getName() : newName;
+ 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 must have a valid keypair");
+ 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.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();
+ }
+ }
+}
diff --git a/app/src/main/java/com/wireguard/android/KernelVpnService.java b/app/src/main/java/com/wireguard/android/KernelVpnService.java
new file mode 100644
index 0000000..66fe607
--- /dev/null
+++ b/app/src/main/java/com/wireguard/android/KernelVpnService.java
@@ -0,0 +1,115 @@
+package com.wireguard.android;
+
+import android.app.Service;
+import android.content.Intent;
+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;
+
+/**
+ * Service that handles config state coordination and all background processing for the application.
+ */
+
+public class KernelVpnService extends Service implements com.wireguard.android.VpnService {
+ private static final String TAG = "KernelVpnService";
+
+ private static KernelVpnService instance;
+ public static KernelVpnService getInstance() {
+ return instance;
+ }
+
+ private final IBinder binder = new Binder();
+ private RootShell rootShell;
+
+ @Override
+ public void disable(final String name) {
+ final Config config = ConfigManager.getInstance().get(name);
+ if (config == null || !config.isEnabled())
+ return;
+
+ new ConfigDisabler(config).execute();
+ }
+
+ @Override
+ public void enable(final String name) {
+ final Config config = ConfigManager.getInstance().get(name);
+ if (config == null || config.isEnabled())
+ return;
+
+ new ConfigEnabler(config).execute();
+ }
+
+ @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);
+ }
+
+ @Override
+ public void onDestroy() {
+ }
+
+ @Override
+ public int onStartCommand(final Intent intent, final int flags, final int startId) {
+ instance = this;
+ return START_STICKY;
+ }
+
+ 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;
+
+ ConfigManager.getInstance().setIsEnable(config.getName(), 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;
+
+ ConfigManager.getInstance().setIsEnable(config.getName(), true);
+ }
+ }
+}
diff --git a/app/src/main/java/com/wireguard/android/QuickTileService.java b/app/src/main/java/com/wireguard/android/QuickTileService.java
index ddadf29..c12a6df 100644
--- a/app/src/main/java/com/wireguard/android/QuickTileService.java
+++ b/app/src/main/java/com/wireguard/android/QuickTileService.java
@@ -3,8 +3,6 @@ package com.wireguard.android;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.graphics.drawable.Icon;
import android.os.Build;
@@ -18,11 +16,12 @@ import com.wireguard.config.Config;
@TargetApi(Build.VERSION_CODES.N)
public class QuickTileService extends TileService {
private Config config;
+ private ConfigManager configManager;
private SharedPreferences preferences;
- private VpnService service;
@Override
public void onClick() {
+ VpnService service = VpnService.Singleton.getInstance();
if (service != null && config != null) {
if (config.isEnabled())
service.disable(config.getName());
@@ -34,10 +33,9 @@ public class QuickTileService extends TileService {
@Override
public void onCreate() {
preferences = PreferenceManager.getDefaultSharedPreferences(this);
- service = VpnService.getInstance();
- if (service == null)
- bindService(new Intent(this, VpnService.class), new ServiceConnectionCallbacks(),
- Context.BIND_AUTO_CREATE);
+ configManager = ConfigManager.getInstance();
+ if (configManager == null)
+ new ConfigManagerCallbacks(this);
TileService.requestListeningState(this, new ComponentName(this, getClass()));
}
@@ -45,8 +43,8 @@ public class QuickTileService extends TileService {
public void onStartListening() {
// Since this is an active tile, this only gets called when we want to update the tile.
final Tile tile = getQsTile();
- final String configName = preferences.getString(VpnService.KEY_PRIMARY_CONFIG, null);
- config = configName != null && service != null ? service.get(configName) : null;
+ final String configName = preferences.getString(ConfigManager.KEY_PRIMARY_CONFIG, null);
+ config = configName != null && configManager != null ? configManager.get(configName) : null;
if (config != null) {
tile.setLabel(config.getName());
final int state = config.isEnabled() ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
@@ -65,18 +63,14 @@ public class QuickTileService extends TileService {
tile.updateTile();
}
- private class ServiceConnectionCallbacks implements ServiceConnection {
- @Override
- public void onServiceConnected(final ComponentName component, final IBinder binder) {
- // We don't actually need a binding, only notification that the service is started.
- unbindService(this);
- service = VpnService.getInstance();
+ private class ConfigManagerCallbacks extends ConfigManager.ConfigManagerConnection {
+ public ConfigManagerCallbacks(Context ctx) {
+ super(ctx);
}
@Override
- public void onServiceDisconnected(final ComponentName component) {
- // This can never happen; the service runs in the same thread as this service.
- throw new IllegalStateException();
+ public void onServiceConnected(final ComponentName component, final IBinder binder) {
+ configManager = ConfigManager.getInstance();
}
}
}
diff --git a/app/src/main/java/com/wireguard/android/VpnService.java b/app/src/main/java/com/wireguard/android/VpnService.java
index 0fd6713..f6e9651 100644
--- a/app/src/main/java/com/wireguard/android/VpnService.java
+++ b/app/src/main/java/com/wireguard/android/VpnService.java
@@ -1,65 +1,63 @@
package com.wireguard.android;
-import android.app.Service;
import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.AsyncTask;
-import android.os.Binder;
-import android.os.Build;
+import android.content.ServiceConnection;
import android.os.IBinder;
import android.preference.PreferenceManager;
-import android.service.quicksettings.TileService;
-import android.util.Log;
-import com.wireguard.config.Config;
-import com.wireguard.config.Peer;
+public interface VpnService {
+ String KEY_USE_KERNEL_MODULE = "use_kernel_module";
-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.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Set;
+ class Singleton {
+ enum VpnImplementation { KERNEL, ANDROID, UNKNOWN};
+ private static VpnImplementation implementation = VpnImplementation.UNKNOWN;
-/**
- * Service that handles config state coordination and all background processing for the application.
- */
+ public synchronized static void setImplementation(VpnImplementation implementation) {
+ Singleton.implementation = implementation;
+ }
-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 = "VpnService";
+ public synchronized static VpnService getInstance() {
+ switch (implementation) {
+ case ANDROID:
+ return AndroidVpnService.getInstance();
+ case KERNEL:
+ return KernelVpnService.getInstance();
+ }
+ return null;
+ }
- private static VpnService instance;
+ public static class VpnServiceConnection implements ServiceConnection {
- public static VpnService getInstance() {
- return instance;
- }
+ private Context ctx;
- 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 VpnServiceConnection(Context ctx) {
+ this.ctx = ctx;
- /**
- * 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();
+ if (PreferenceManager.getDefaultSharedPreferences(ctx).getBoolean(KEY_USE_KERNEL_MODULE, false))
+ ctx.bindService(new Intent(ctx, KernelVpnService.class), this,
+ Context.BIND_AUTO_CREATE);
+ else
+ ctx.bindService(new Intent(ctx, AndroidVpnService.class), this,
+ Context.BIND_AUTO_CREATE);
+ }
+
+ @Override
+ public void onServiceConnected(final ComponentName component, final IBinder binder) {
+ // We don't actually need a binding, only notification that the service is started.
+ ctx.unbindService(this);
+ }
+
+ @Override
+ public void onServiceDisconnected(final ComponentName component) {
+ // This can never happen; the service runs in the same thread as the activity.
+ throw new IllegalStateException();
+ }
+ }
}
+
/**
* 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
@@ -67,12 +65,7 @@ public class VpnService extends Service
*
* @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();
- }
+ void disable(final String name);
/**
* Attempt to set up and enable an interface for this configuration. The configuration's enabled
@@ -81,352 +74,5 @@ public class VpnService extends Service
*
* @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;
- }
-
- @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) {
- if (!result)
- return;
- config.setIsEnabled(false);
- 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, 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.setIsEnabled(true);
- enabledConfigs.add(config.getName());
- preferences.edit().putStringSet(KEY_ENABLED_CONFIGS, enabledConfigs).apply();
- if (config.getName().equals(primaryName))
- updateTile();
- }
- }
-
- 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.setIsEnabled(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);
- // 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 Config knownConfig;
- private final Config newConfig;
- private final String newName;
- private final String oldName;
- private final Boolean shouldConnect;
-
- 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 must have a valid keypair");
- 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();
- }
- }
+ void enable(final String name);
}
diff --git a/app/src/main/java/com/wireguard/config/Config.java b/app/src/main/java/com/wireguard/config/Config.java
index 2a282d0..c69b2f5 100644
--- a/app/src/main/java/com/wireguard/config/Config.java
+++ b/app/src/main/java/com/wireguard/config/Config.java
@@ -25,7 +25,7 @@ import java.util.regex.Pattern;
*/
public class Config extends BaseObservable
- implements Comparable<Config>, Copyable<Config>, Observable, Parcelable {
+ implements Comparable<Config>, Copyable<Config>, Observable, Parcelable, IpcSerializable {
public static final Parcelable.Creator<Config> CREATOR = new Parcelable.Creator<Config>() {
@Override
public Config createFromParcel(final Parcel in) {
@@ -186,4 +186,14 @@ public class Config extends BaseObservable
dest.writeString(name);
dest.writeTypedList(peers);
}
+
+ @Override
+ public String toIpcString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(iface.toIpcString());
+ sb.append("replace_peers=true\n");
+ for (final Peer peer : peers)
+ sb.append(peer.toIpcString());
+ return sb.toString();
+ }
}
diff --git a/app/src/main/java/com/wireguard/config/Interface.java b/app/src/main/java/com/wireguard/config/Interface.java
index b291844..0d8246c 100644
--- a/app/src/main/java/com/wireguard/config/Interface.java
+++ b/app/src/main/java/com/wireguard/config/Interface.java
@@ -15,7 +15,7 @@ import com.wireguard.crypto.Keypair;
*/
public class Interface extends BaseObservable
- implements Copyable<Interface>, Observable, Parcelable {
+ implements Copyable<Interface>, Observable, Parcelable, IpcSerializable {
public static final Parcelable.Creator<Interface> CREATOR
= new Parcelable.Creator<Interface>() {
@Override
@@ -200,4 +200,14 @@ public class Interface extends BaseObservable
dest.writeString(mtu);
dest.writeString(privateKey);
}
+
+ @Override
+ public String toIpcString() {
+ final StringBuilder sb = new StringBuilder();
+ if (listenPort != null)
+ sb.append(IpcAttribute.LISTEN_PORT.composeWith(listenPort));
+ if (privateKey != null)
+ sb.append(IpcAttribute.PRIVATE_KEY.composeWith(keypair.getPrivateKeyHex()));
+ return sb.toString();
+ }
}
diff --git a/app/src/main/java/com/wireguard/config/IpcAttribute.java b/app/src/main/java/com/wireguard/config/IpcAttribute.java
new file mode 100644
index 0000000..a4a030a
--- /dev/null
+++ b/app/src/main/java/com/wireguard/config/IpcAttribute.java
@@ -0,0 +1,55 @@
+package com.wireguard.config;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The set of valid attributes for an interface or peer over WireGuard IPC configuration channel.
+ */
+
+enum IpcAttribute {
+ ALLOWED_IPS("allowed_ip"),
+ ENDPOINT("endpoint"),
+ LISTEN_PORT("listen_port"),
+ PERSISTENT_KEEPALIVE("persistent_keepalive_interval"),
+ PRE_SHARED_KEY("preshared_key"),
+ PRIVATE_KEY("private_key"),
+ PUBLIC_KEY("public_key");
+
+ private static final Map<String, IpcAttribute> map;
+
+ static {
+ map = new HashMap<>(IpcAttribute.values().length);
+ for (final IpcAttribute key : IpcAttribute.values())
+ map.put(key.getToken(), key);
+ }
+
+ public static IpcAttribute match(final String line) {
+ return map.get(line.split("\\s|=")[0]);
+ }
+
+ private final String token;
+ private final Pattern pattern;
+
+ IpcAttribute(final String token) {
+ pattern = Pattern.compile(token + "\\s*=\\s*(\\S.*)");
+ this.token = token;
+ }
+
+ public String composeWith(final String value) {
+ return token + "=" + value + "\n";
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public String parseFrom(final String line) {
+ final Matcher matcher = pattern.matcher(line);
+ if (matcher.matches())
+ return matcher.group(1);
+ return null;
+ }
+}
diff --git a/app/src/main/java/com/wireguard/config/IpcSerializable.java b/app/src/main/java/com/wireguard/config/IpcSerializable.java
new file mode 100644
index 0000000..46dfea0
--- /dev/null
+++ b/app/src/main/java/com/wireguard/config/IpcSerializable.java
@@ -0,0 +1,9 @@
+package com.wireguard.config;
+
+/**
+ * Interface for classes that can perform a serialization for the ipc link
+ */
+
+public interface IpcSerializable {
+ String toIpcString();
+}
diff --git a/app/src/main/java/com/wireguard/config/Peer.java b/app/src/main/java/com/wireguard/config/Peer.java
index 718a5c3..ce55bb2 100644
--- a/app/src/main/java/com/wireguard/config/Peer.java
+++ b/app/src/main/java/com/wireguard/config/Peer.java
@@ -7,12 +7,13 @@ import android.os.Parcel;
import android.os.Parcelable;
import com.android.databinding.library.baseAdapters.BR;
+import com.wireguard.crypto.KeyEncoding;
/**
* Represents the configuration for a WireGuard peer (a [Peer] block).
*/
-public class Peer extends BaseObservable implements Copyable<Peer>, Observable, Parcelable {
+public class Peer extends BaseObservable implements Copyable<Peer>, Observable, Parcelable, IpcSerializable {
public static final Parcelable.Creator<Peer> CREATOR = new Parcelable.Creator<Peer>() {
@Override
public Peer createFromParcel(final Parcel in) {
@@ -175,4 +176,22 @@ public class Peer extends BaseObservable implements Copyable<Peer>, Observable,
dest.writeString(preSharedKey);
dest.writeString(publicKey);
}
+
+ @Override
+ public String toIpcString() {
+ final StringBuilder sb = new StringBuilder();
+ if (publicKey != null)
+ sb.append(IpcAttribute.PUBLIC_KEY.composeWith(KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(publicKey))));
+ if (endpoint != null)
+ sb.append(IpcAttribute.ENDPOINT.composeWith(endpoint));
+ if (persistentKeepalive != null)
+ sb.append(IpcAttribute.PERSISTENT_KEEPALIVE.composeWith(persistentKeepalive));
+ if (allowedIPs != null) {
+ sb.append("replace_allowed_ips=true\n");
+ sb.append(IpcAttribute.ALLOWED_IPS.composeWith(allowedIPs));
+ }
+ if (preSharedKey != null)
+ sb.append(IpcAttribute.PRE_SHARED_KEY.composeWith(preSharedKey));
+ return sb.toString();
+ }
}
diff --git a/app/src/main/java/com/wireguard/crypto/KeyEncoding.java b/app/src/main/java/com/wireguard/crypto/KeyEncoding.java
index f83fd0b..2e48efd 100644
--- a/app/src/main/java/com/wireguard/crypto/KeyEncoding.java
+++ b/app/src/main/java/com/wireguard/crypto/KeyEncoding.java
@@ -88,4 +88,16 @@ public class KeyEncoding {
output[KEY_LENGTH_BASE64 - 1] = '=';
return new String(output);
}
+
+ private final static char[] hexArray = "0123456789ABCDEF".toCharArray();
+
+ public static String keyToHex(final byte[] bytes) {
+ char[] hexChars = new char[bytes.length * 2];
+ for ( int j = 0; j < bytes.length; j++ ) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = hexArray[v >>> 4];
+ hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
}
diff --git a/app/src/main/java/com/wireguard/crypto/Keypair.java b/app/src/main/java/com/wireguard/crypto/Keypair.java
index e0d35d6..a8b1951 100644
--- a/app/src/main/java/com/wireguard/crypto/Keypair.java
+++ b/app/src/main/java/com/wireguard/crypto/Keypair.java
@@ -42,8 +42,14 @@ public class Keypair {
public String getPrivateKey() {
return KeyEncoding.keyToBase64(privateKey);
}
+ public String getPrivateKeyHex() {
+ return KeyEncoding.keyToHex(privateKey);
+ }
public String getPublicKey() {
return KeyEncoding.keyToBase64(publicKey);
}
+ public String getPublicKeyHex() {
+ return KeyEncoding.keyToHex(publicKey);
+ }
}
diff --git a/app/src/main/res/layout/config_list_item.xml b/app/src/main/res/layout/config_list_item.xml
index d15d48d..dcc40ad 100644
--- a/app/src/main/res/layout/config_list_item.xml
+++ b/app/src/main/res/layout/config_list_item.xml
@@ -2,9 +2,7 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
-
<import type="android.graphics.Typeface" />
-
<import type="com.wireguard.android.VpnService" />
<variable
@@ -38,7 +36,7 @@
android:layout_alignBaseline="@+id/config_name"
android:layout_alignParentEnd="true"
android:gravity="center_vertical"
- android:onClick="@{() -> item.enabled ? VpnService.instance.disable(item.name) : VpnService.instance.enable(item.name)}"
+ android:onClick="@{() -> item.enabled ? VpnService.Singleton.getInstance().disable(item.name) : VpnService.Singleton.getInstance().enable(item.name)}"
android:text="@string/toggle"
android:textColor="@{item.enabled ? @android:color/holo_green_dark : @android:color/holo_red_dark}" />
</RelativeLayout>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bc2ec7a..deb4eee 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -38,6 +38,8 @@
<string name="public_key_description">WireGuard public key</string>
<string name="restore_on_boot">Restore on boot</string>
<string name="restore_on_boot_summary">Restore previously enabled configurations on boot</string>
+ <string name="use_kernel_module">Use the WireGuard kernel implementation</string>
+ <string name="use_kernel_module_summary">Require a kernel with WireGuard support and root right</string>
<string name="save">Save</string>
<string name="settings">Settings</string>
<string name="status">Status</string>
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 95683a1..3d9dc96 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -9,4 +9,9 @@
android:key="restore_on_boot"
android:summary="@string/restore_on_boot_summary"
android:title="@string/restore_on_boot" />
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:key="use_kernel_module"
+ android:summary="@string/use_kernel_module_summary"
+ android:title="@string/use_kernel_module" />
</PreferenceScreen>
diff --git a/settings.gradle b/settings.gradle
index e7b4def..9f20b46 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':app'
+include ':app', ':wireguardbinding'
diff --git a/wireguardbinding/build.gradle b/wireguardbinding/build.gradle
new file mode 100644
index 0000000..52b2613
--- /dev/null
+++ b/wireguardbinding/build.gradle
@@ -0,0 +1,2 @@
+configurations.maybeCreate("default")
+artifacts.add("default", file('wireguardbinding.aar'))
diff --git a/wireguardbinding/src/wireguard b/wireguardbinding/src/wireguard
new file mode 120000
index 0000000..0090783
--- /dev/null
+++ b/wireguardbinding/src/wireguard
@@ -0,0 +1 @@
+../../wireguard-go/src/wireguard
\ No newline at end of file
diff --git a/wireguardbinding/src/wireguardbinding/tun_android.go b/wireguardbinding/src/wireguardbinding/tun_android.go
new file mode 100644
index 0000000..853d6da
--- /dev/null
+++ b/wireguardbinding/src/wireguardbinding/tun_android.go
@@ -0,0 +1,61 @@
+package wireguardbinding
+
+/* Implementation of the TUN device interface for android
+ */
+
+import (
+ "os"
+ "wireguard"
+)
+
+type NativeTun struct {
+ fd *os.File
+ name string
+ events chan wireguard.TUNEvent
+}
+
+func (tun *NativeTun) Name() string {
+ return tun.name
+}
+
+func (tun *NativeTun) setMTU(n int) error {
+ return nil
+}
+
+func (tun *NativeTun) MTU() (int, error) {
+ return wireguard.DefaultMTU, nil
+}
+
+func (tun *NativeTun) Write(d []byte) (int, error) {
+ return tun.fd.Write(d)
+}
+
+func (tun *NativeTun) Read(d []byte) (int, error) {
+ return tun.fd.Read(d)
+}
+
+func (tun *NativeTun) Events() chan wireguard.TUNEvent {
+ return tun.events
+}
+
+func (tun *NativeTun) Close() error {
+ return tun.fd.Close()
+}
+
+func CreateTUN(fdint int, name string) (wireguard.TUNDevice, error) {
+
+ fd := os.NewFile(uintptr(fdint), name)
+ if fd == nil {
+ return nil, nil
+ }
+
+ device := &NativeTun{
+ fd: fd,
+ name: name,
+ events: make(chan wireguard.TUNEvent, 5),
+ }
+
+ device.events <- wireguard.TUNEventUp
+
+ return device, nil
+}
diff --git a/wireguardbinding/src/wireguardbinding/wireguard.go b/wireguardbinding/src/wireguardbinding/wireguard.go
new file mode 100644
index 0000000..3103674
--- /dev/null
+++ b/wireguardbinding/src/wireguardbinding/wireguard.go
@@ -0,0 +1,49 @@
+package wireguardbinding
+
+import (
+ "log"
+ "wireguard"
+ "bufio"
+ "strings"
+)
+
+var device *wireguard.Device
+
+func Start(fd int, name string) {
+ log.Println("Start")
+
+ tun, err := CreateTUN(fd, name)
+ if err != nil {
+ log.Println("Failed to create tun device: ", err)
+ return
+ }
+
+ log.Println("NAME ", tun.Name())
+
+ device = wireguard.NewDevice(tun, wireguard.LogLevelDebug)
+}
+
+func Socket() int {
+ log.Println("Socket")
+ fd, _ := wireguard.GetUDPConn(device)
+ return int(fd)
+}
+
+func Stop() {
+ log.Println("Stop")
+ device.Close()
+ device = nil
+}
+
+func SetConf(conf string) {
+ log.Println("SetConf")
+ scanner := bufio.NewScanner(strings.NewReader(conf))
+ wireguard.SetOperation(device, scanner)
+}
+
+func GetConf() []string {
+ log.Println("GetConf")
+ var conf []string
+ wireguard.GetOperation(device, conf)
+ return conf
+}
--
2.15.0
More information about the WireGuard
mailing list