aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/tunnel
diff options
context:
space:
mode:
Diffstat (limited to 'tunnel')
-rw-r--r--tunnel/build.gradle.kts128
-rw-r--r--tunnel/src/main/AndroidManifest.xml18
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Backend.java67
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/BackendException.java61
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java439
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Statistics.java102
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java57
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java196
-rw-r--r--tunnel/src/main/java/com/wireguard/android/util/RootShell.java222
-rw-r--r--tunnel/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java95
-rw-r--r--tunnel/src/main/java/com/wireguard/android/util/ToolsInstaller.java202
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Attribute.java60
-rw-r--r--tunnel/src/main/java/com/wireguard/config/BadConfigException.java120
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Config.java223
-rw-r--r--tunnel/src/main/java/com/wireguard/config/InetAddresses.java86
-rw-r--r--tunnel/src/main/java/com/wireguard/config/InetEndpoint.java126
-rw-r--r--tunnel/src/main/java/com/wireguard/config/InetNetwork.java79
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Interface.java423
-rw-r--r--tunnel/src/main/java/com/wireguard/config/ParseException.java48
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Peer.java307
-rw-r--r--tunnel/src/main/java/com/wireguard/crypto/Curve25519.java500
-rw-r--r--tunnel/src/main/java/com/wireguard/crypto/Key.java290
-rw-r--r--tunnel/src/main/java/com/wireguard/crypto/KeyFormatException.java37
-rw-r--r--tunnel/src/main/java/com/wireguard/crypto/KeyPair.java54
-rw-r--r--tunnel/src/main/java/com/wireguard/util/NonNullForAll.java29
-rw-r--r--tunnel/src/test/java/com/wireguard/config/BadConfigExceptionTest.java173
-rw-r--r--tunnel/src/test/java/com/wireguard/config/ConfigTest.java49
-rw-r--r--tunnel/src/test/resources/broken.conf9
-rw-r--r--tunnel/src/test/resources/invalid-key.conf9
-rw-r--r--tunnel/src/test/resources/invalid-number.conf9
-rw-r--r--tunnel/src/test/resources/invalid-value.conf9
-rw-r--r--tunnel/src/test/resources/missing-attribute.conf8
-rw-r--r--tunnel/src/test/resources/missing-section.conf5
-rw-r--r--tunnel/src/test/resources/syntax-error.conf9
-rw-r--r--tunnel/src/test/resources/unknown-attribute.conf9
-rw-r--r--tunnel/src/test/resources/unknown-section.conf9
-rw-r--r--tunnel/src/test/resources/working.conf9
-rw-r--r--tunnel/tools/CMakeLists.txt44
m---------tunnel/tools/elf-cleaner0
-rw-r--r--tunnel/tools/libwg-go/.gitignore1
-rw-r--r--tunnel/tools/libwg-go/Makefile52
-rw-r--r--tunnel/tools/libwg-go/api-android.go227
-rw-r--r--tunnel/tools/libwg-go/go.mod14
-rw-r--r--tunnel/tools/libwg-go/go.sum13
-rw-r--r--tunnel/tools/libwg-go/goruntime-boottime-over-monotonic.diff171
-rw-r--r--tunnel/tools/libwg-go/jni.c71
-rw-r--r--tunnel/tools/ndk-compat/compat.c25
-rw-r--r--tunnel/tools/ndk-compat/compat.h10
m---------tunnel/tools/wireguard-tools0
49 files changed, 4904 insertions, 0 deletions
diff --git a/tunnel/build.gradle.kts b/tunnel/build.gradle.kts
new file mode 100644
index 00000000..589d72da
--- /dev/null
+++ b/tunnel/build.gradle.kts
@@ -0,0 +1,128 @@
+@file:Suppress("UnstableApiUsage")
+
+import org.gradle.api.tasks.testing.logging.TestLogEvent
+
+val pkg: String = providers.gradleProperty("wireguardPackageName").get()
+
+plugins {
+ alias(libs.plugins.android.library)
+ `maven-publish`
+ signing
+}
+
+android {
+ compileSdk = 34
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ namespace = "${pkg}.tunnel"
+ defaultConfig {
+ minSdk = 21
+ }
+ externalNativeBuild {
+ cmake {
+ path("tools/CMakeLists.txt")
+ }
+ }
+ testOptions.unitTests.all {
+ it.testLogging { events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) }
+ }
+ buildTypes {
+ all {
+ externalNativeBuild {
+ cmake {
+ targets("libwg-go.so", "libwg.so", "libwg-quick.so")
+ arguments("-DGRADLE_USER_HOME=${project.gradle.gradleUserHomeDir}")
+ }
+ }
+ }
+ release {
+ externalNativeBuild {
+ cmake {
+ arguments("-DANDROID_PACKAGE_NAME=${pkg}")
+ }
+ }
+ }
+ debug {
+ externalNativeBuild {
+ cmake {
+ arguments("-DANDROID_PACKAGE_NAME=${pkg}.debug")
+ }
+ }
+ }
+ }
+ lint {
+ disable += "LongLogTag"
+ disable += "NewApi"
+ }
+ publishing {
+ singleVariant("release") {
+ withJavadocJar()
+ withSourcesJar()
+ }
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.annotation)
+ implementation(libs.androidx.collection)
+ compileOnly(libs.jsr305)
+ testImplementation(libs.junit)
+}
+
+publishing {
+ publications {
+ register<MavenPublication>("release") {
+ groupId = pkg
+ artifactId = "tunnel"
+ version = providers.gradleProperty("wireguardVersionName").get()
+ afterEvaluate {
+ from(components["release"])
+ }
+ pom {
+ name.set("WireGuard Tunnel Library")
+ description.set("Embeddable tunnel library for WireGuard for Android")
+ url.set("https://www.wireguard.com/")
+
+ licenses {
+ license {
+ name.set("The Apache Software License, Version 2.0")
+ url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
+ distribution.set("repo")
+ }
+ }
+ scm {
+ connection.set("scm:git:https://git.zx2c4.com/wireguard-android")
+ developerConnection.set("scm:git:https://git.zx2c4.com/wireguard-android")
+ url.set("https://git.zx2c4.com/wireguard-android")
+ }
+ developers {
+ organization {
+ name.set("WireGuard")
+ url.set("https://www.wireguard.com/")
+ }
+ developer {
+ name.set("WireGuard")
+ email.set("team@wireguard.com")
+ }
+ }
+ }
+ }
+ }
+ repositories {
+ maven {
+ name = "sonatype"
+ url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
+ credentials {
+ username = providers.environmentVariable("SONATYPE_USER").orNull
+ password = providers.environmentVariable("SONATYPE_PASSWORD").orNull
+ }
+ }
+ }
+}
+
+signing {
+ useGpgCmd()
+ sign(publishing.publications)
+}
diff --git a/tunnel/src/main/AndroidManifest.xml b/tunnel/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..99509a20
--- /dev/null
+++ b/tunnel/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<!--
+ ~ Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application>
+ <service
+ android:name="com.wireguard.android.backend.GoBackend$VpnService"
+ android:permission="android.permission.BIND_VPN_SERVICE"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="android.net.VpnService" />
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Backend.java b/tunnel/src/main/java/com/wireguard/android/backend/Backend.java
new file mode 100644
index 00000000..edf98b9e
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/backend/Backend.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.backend;
+
+import com.wireguard.config.Config;
+import com.wireguard.util.NonNullForAll;
+
+import java.util.Set;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Interface for implementations of the WireGuard secure network tunnel.
+ */
+
+@NonNullForAll
+public interface Backend {
+ /**
+ * Enumerate names of currently-running tunnels.
+ *
+ * @return The set of running tunnel names.
+ */
+ Set<String> getRunningTunnelNames();
+
+ /**
+ * Get the state of a tunnel.
+ *
+ * @param tunnel The tunnel to examine the state of.
+ * @return The state of the tunnel.
+ * @throws Exception Exception raised when retrieving tunnel's state.
+ */
+ Tunnel.State getState(Tunnel tunnel) throws Exception;
+
+ /**
+ * Get statistics about traffic and errors on this tunnel. If the tunnel is not running, the
+ * statistics object will be filled with zero values.
+ *
+ * @param tunnel The tunnel to retrieve statistics for.
+ * @return The statistics for the tunnel.
+ * @throws Exception Exception raised when retrieving statistics.
+ */
+ Statistics getStatistics(Tunnel tunnel) throws Exception;
+
+ /**
+ * Determine version of underlying backend.
+ *
+ * @return The version of the backend.
+ * @throws Exception Exception raised while retrieving version.
+ */
+ String getVersion() throws Exception;
+
+ /**
+ * Set the state of a tunnel, updating it's configuration. If the tunnel is already up, config
+ * may update the running configuration; config may be null when setting the tunnel down.
+ *
+ * @param tunnel The tunnel to control the state of.
+ * @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
+ * {@code TOGGLE}.
+ * @param config The configuration for this tunnel, may be null if state is {@code DOWN}.
+ * @return The updated state of the tunnel.
+ * @throws Exception Exception raised while changing state.
+ */
+ Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception;
+}
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/BackendException.java b/tunnel/src/main/java/com/wireguard/android/backend/BackendException.java
new file mode 100644
index 00000000..af966ec1
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/backend/BackendException.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.backend;
+
+import com.wireguard.util.NonNullForAll;
+
+/**
+ * A subclass of {@link Exception} that encapsulates the reasons for a failure originating in
+ * implementations of {@link Backend}.
+ */
+@NonNullForAll
+public final class BackendException extends Exception {
+ private final Object[] format;
+ private final Reason reason;
+
+ /**
+ * Public constructor for BackendException.
+ *
+ * @param reason The {@link Reason} which caused this exception to be thrown
+ * @param format Format string values used when converting exceptions to user-facing strings.
+ */
+ public BackendException(final Reason reason, final Object... format) {
+ this.reason = reason;
+ this.format = format;
+ }
+
+ /**
+ * Get the format string values associated with the instance.
+ *
+ * @return Array of {@link Object} for string formatting purposes
+ */
+ public Object[] getFormat() {
+ return format;
+ }
+
+ /**
+ * Get the reason for this exception.
+ *
+ * @return Associated {@link Reason} for this exception.
+ */
+ public Reason getReason() {
+ return reason;
+ }
+
+ /**
+ * Enum class containing all known reasons for why a {@link BackendException} might be thrown.
+ */
+ public enum Reason {
+ UNKNOWN_KERNEL_MODULE_NAME,
+ WG_QUICK_CONFIG_ERROR_CODE,
+ TUNNEL_MISSING_CONFIG,
+ VPN_NOT_AUTHORIZED,
+ UNABLE_TO_START_VPN,
+ TUN_CREATION_ERROR,
+ GO_ACTIVATION_ERROR_CODE,
+ DNS_RESOLUTION_FAILURE,
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java
new file mode 100644
index 00000000..429cb1f1
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.backend;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.wireguard.android.backend.BackendException.Reason;
+import com.wireguard.android.backend.Tunnel.State;
+import com.wireguard.android.util.SharedLibraryLoader;
+import com.wireguard.config.Config;
+import com.wireguard.config.InetEndpoint;
+import com.wireguard.config.InetNetwork;
+import com.wireguard.config.Peer;
+import com.wireguard.crypto.Key;
+import com.wireguard.crypto.KeyFormatException;
+import com.wireguard.util.NonNullForAll;
+
+import java.net.InetAddress;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import androidx.annotation.Nullable;
+import androidx.collection.ArraySet;
+
+/**
+ * Implementation of {@link Backend} that uses the wireguard-go userspace implementation to provide
+ * WireGuard tunnels.
+ */
+@NonNullForAll
+public final class GoBackend implements Backend {
+ private static final int DNS_RESOLUTION_RETRIES = 10;
+ private static final String TAG = "WireGuard/GoBackend";
+ @Nullable private static AlwaysOnCallback alwaysOnCallback;
+ private static GhettoCompletableFuture<VpnService> vpnService = new GhettoCompletableFuture<>();
+ private final Context context;
+ @Nullable private Config currentConfig;
+ @Nullable private Tunnel currentTunnel;
+ private int currentTunnelHandle = -1;
+
+ /**
+ * Public constructor for GoBackend.
+ *
+ * @param context An Android {@link Context}
+ */
+ public GoBackend(final Context context) {
+ SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
+ this.context = context;
+ }
+
+ /**
+ * Set a {@link AlwaysOnCallback} to be invoked when {@link VpnService} is started by the
+ * system's Always-On VPN mode.
+ *
+ * @param cb Callback to be invoked
+ */
+ public static void setAlwaysOnCallback(final AlwaysOnCallback cb) {
+ alwaysOnCallback = cb;
+ }
+
+ @Nullable private static native String wgGetConfig(int handle);
+
+ private static native int wgGetSocketV4(int handle);
+
+ private static native int wgGetSocketV6(int handle);
+
+ private static native void wgTurnOff(int handle);
+
+ private static native int wgTurnOn(String ifName, int tunFd, String settings);
+
+ private static native String wgVersion();
+
+ /**
+ * Method to get the names of running tunnels.
+ *
+ * @return A set of string values denoting names of running tunnels.
+ */
+ @Override
+ public Set<String> getRunningTunnelNames() {
+ if (currentTunnel != null) {
+ final Set<String> runningTunnels = new ArraySet<>();
+ runningTunnels.add(currentTunnel.getName());
+ return runningTunnels;
+ }
+ return Collections.emptySet();
+ }
+
+ /**
+ * Get the associated {@link State} for a given {@link Tunnel}.
+ *
+ * @param tunnel The tunnel to examine the state of.
+ * @return {@link State} associated with the given tunnel.
+ */
+ @Override
+ public State getState(final Tunnel tunnel) {
+ return currentTunnel == tunnel ? State.UP : State.DOWN;
+ }
+
+ /**
+ * Get the associated {@link Statistics} for a given {@link Tunnel}.
+ *
+ * @param tunnel The tunnel to retrieve statistics for.
+ * @return {@link Statistics} associated with the given tunnel.
+ */
+ @Override
+ public Statistics getStatistics(final Tunnel tunnel) {
+ final Statistics stats = new Statistics();
+ if (tunnel != currentTunnel || currentTunnelHandle == -1)
+ return stats;
+ final String config = wgGetConfig(currentTunnelHandle);
+ if (config == null)
+ return stats;
+ Key key = null;
+ long rx = 0;
+ long tx = 0;
+ long latestHandshakeMSec = 0;
+ for (final String line : config.split("\\n")) {
+ if (line.startsWith("public_key=")) {
+ if (key != null)
+ stats.add(key, rx, tx, latestHandshakeMSec);
+ rx = 0;
+ tx = 0;
+ latestHandshakeMSec = 0;
+ try {
+ key = Key.fromHex(line.substring(11));
+ } catch (final KeyFormatException ignored) {
+ key = null;
+ }
+ } else if (line.startsWith("rx_bytes=")) {
+ if (key == null)
+ continue;
+ try {
+ rx = Long.parseLong(line.substring(9));
+ } catch (final NumberFormatException ignored) {
+ rx = 0;
+ }
+ } else if (line.startsWith("tx_bytes=")) {
+ if (key == null)
+ continue;
+ try {
+ tx = Long.parseLong(line.substring(9));
+ } catch (final NumberFormatException ignored) {
+ tx = 0;
+ }
+ } else if (line.startsWith("last_handshake_time_sec=")) {
+ if (key == null)
+ continue;
+ try {
+ latestHandshakeMSec += Long.parseLong(line.substring(24)) * 1000;
+ } catch (final NumberFormatException ignored) {
+ latestHandshakeMSec = 0;
+ }
+ } else if (line.startsWith("last_handshake_time_nsec=")) {
+ if (key == null)
+ continue;
+ try {
+ latestHandshakeMSec += Long.parseLong(line.substring(25)) / 1000000;
+ } catch (final NumberFormatException ignored) {
+ latestHandshakeMSec = 0;
+ }
+ }
+ }
+ if (key != null)
+ stats.add(key, rx, tx, latestHandshakeMSec);
+ return stats;
+ }
+
+ /**
+ * Get the version of the underlying wireguard-go library.
+ *
+ * @return {@link String} value of the version of the wireguard-go library.
+ */
+ @Override
+ public String getVersion() {
+ return wgVersion();
+ }
+
+ /**
+ * Change the state of a given {@link Tunnel}, optionally applying a given {@link Config}.
+ *
+ * @param tunnel The tunnel to control the state of.
+ * @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
+ * {@code TOGGLE}.
+ * @param config The configuration for this tunnel, may be null if state is {@code DOWN}.
+ * @return {@link State} of the tunnel after state changes are applied.
+ * @throws Exception Exception raised while changing tunnel state.
+ */
+ @Override
+ public State setState(final Tunnel tunnel, State state, @Nullable final Config config) throws Exception {
+ final State originalState = getState(tunnel);
+
+ if (state == State.TOGGLE)
+ state = originalState == State.UP ? State.DOWN : State.UP;
+ if (state == originalState && tunnel == currentTunnel && config == currentConfig)
+ return originalState;
+ if (state == State.UP) {
+ final Config originalConfig = currentConfig;
+ final Tunnel originalTunnel = currentTunnel;
+ if (currentTunnel != null)
+ setStateInternal(currentTunnel, null, State.DOWN);
+ try {
+ setStateInternal(tunnel, config, state);
+ } catch (final Exception e) {
+ if (originalTunnel != null)
+ setStateInternal(originalTunnel, originalConfig, State.UP);
+ throw e;
+ }
+ } else if (state == State.DOWN && tunnel == currentTunnel) {
+ setStateInternal(tunnel, null, State.DOWN);
+ }
+ return getState(tunnel);
+ }
+
+ private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state)
+ throws Exception {
+ Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state);
+
+ if (state == State.UP) {
+ if (config == null)
+ throw new BackendException(Reason.TUNNEL_MISSING_CONFIG);
+
+ if (VpnService.prepare(context) != null)
+ throw new BackendException(Reason.VPN_NOT_AUTHORIZED);
+
+ final VpnService service;
+ if (!vpnService.isDone()) {
+ Log.d(TAG, "Requesting to start VpnService");
+ context.startService(new Intent(context, VpnService.class));
+ }
+
+ try {
+ service = vpnService.get(2, TimeUnit.SECONDS);
+ } catch (final TimeoutException e) {
+ final Exception be = new BackendException(Reason.UNABLE_TO_START_VPN);
+ be.initCause(e);
+ throw be;
+ }
+ service.setOwner(this);
+
+ if (currentTunnelHandle != -1) {
+ Log.w(TAG, "Tunnel already up");
+ return;
+ }
+
+
+ dnsRetry: for (int i = 0; i < DNS_RESOLUTION_RETRIES; ++i) {
+ // Pre-resolve IPs so they're cached when building the userspace string
+ for (final Peer peer : config.getPeers()) {
+ final InetEndpoint ep = peer.getEndpoint().orElse(null);
+ if (ep == null)
+ continue;
+ if (ep.getResolved().orElse(null) == null) {
+ if (i < DNS_RESOLUTION_RETRIES - 1) {
+ Log.w(TAG, "DNS host \"" + ep.getHost() + "\" failed to resolve; trying again");
+ Thread.sleep(1000);
+ continue dnsRetry;
+ } else
+ throw new BackendException(Reason.DNS_RESOLUTION_FAILURE, ep.getHost());
+ }
+ }
+ break;
+ }
+
+ // Build config
+ final String goConfig = config.toWgUserspaceString();
+
+ // Create the vpn tunnel with android API
+ final VpnService.Builder builder = service.getBuilder();
+ builder.setSession(tunnel.getName());
+
+ for (final String excludedApplication : config.getInterface().getExcludedApplications())
+ builder.addDisallowedApplication(excludedApplication);
+
+ for (final String includedApplication : config.getInterface().getIncludedApplications())
+ builder.addAllowedApplication(includedApplication);
+
+ for (final InetNetwork addr : config.getInterface().getAddresses())
+ builder.addAddress(addr.getAddress(), addr.getMask());
+
+ for (final InetAddress addr : config.getInterface().getDnsServers())
+ builder.addDnsServer(addr.getHostAddress());
+
+ for (final String dnsSearchDomain : config.getInterface().getDnsSearchDomains())
+ builder.addSearchDomain(dnsSearchDomain);
+
+ boolean sawDefaultRoute = false;
+ for (final Peer peer : config.getPeers()) {
+ for (final InetNetwork addr : peer.getAllowedIps()) {
+ if (addr.getMask() == 0)
+ sawDefaultRoute = true;
+ builder.addRoute(addr.getAddress(), addr.getMask());
+ }
+ }
+
+ // "Kill-switch" semantics
+ if (!(sawDefaultRoute && config.getPeers().size() == 1)) {
+ builder.allowFamily(OsConstants.AF_INET);
+ builder.allowFamily(OsConstants.AF_INET6);
+ }
+
+ builder.setMtu(config.getInterface().getMtu().orElse(1280));
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
+ builder.setMetered(false);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
+ service.setUnderlyingNetworks(null);
+
+ builder.setBlocking(true);
+ try (final ParcelFileDescriptor tun = builder.establish()) {
+ if (tun == null)
+ throw new BackendException(Reason.TUN_CREATION_ERROR);
+ Log.d(TAG, "Go backend " + wgVersion());
+ currentTunnelHandle = wgTurnOn(tunnel.getName(), tun.detachFd(), goConfig);
+ }
+ if (currentTunnelHandle < 0)
+ throw new BackendException(Reason.GO_ACTIVATION_ERROR_CODE, currentTunnelHandle);
+
+ currentTunnel = tunnel;
+ currentConfig = config;
+
+ service.protect(wgGetSocketV4(currentTunnelHandle));
+ service.protect(wgGetSocketV6(currentTunnelHandle));
+ } else {
+ if (currentTunnelHandle == -1) {
+ Log.w(TAG, "Tunnel already down");
+ return;
+ }
+ int handleToClose = currentTunnelHandle;
+ currentTunnel = null;
+ currentTunnelHandle = -1;
+ currentConfig = null;
+ wgTurnOff(handleToClose);
+ try {
+ vpnService.get(0, TimeUnit.NANOSECONDS).stopSelf();
+ } catch (final TimeoutException ignored) { }
+ }
+
+ tunnel.onStateChange(state);
+ }
+
+ /**
+ * Callback for {@link GoBackend} that is invoked when {@link VpnService} is started by the
+ * system's Always-On VPN mode.
+ */
+ public interface AlwaysOnCallback {
+ void alwaysOnTriggered();
+ }
+
+ // TODO: When we finally drop API 21 and move to API 24, delete this and replace with the ordinary CompletableFuture.
+ private static final class GhettoCompletableFuture<V> {
+ private final LinkedBlockingQueue<V> completion = new LinkedBlockingQueue<>(1);
+ private final FutureTask<V> result = new FutureTask<>(completion::peek);
+
+ public boolean complete(final V value) {
+ final boolean offered = completion.offer(value);
+ if (offered)
+ result.run();
+ return offered;
+ }
+
+ public V get() throws ExecutionException, InterruptedException {
+ return result.get();
+ }
+
+ public V get(final long timeout, final TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException {
+ return result.get(timeout, unit);
+ }
+
+ public boolean isDone() {
+ return !completion.isEmpty();
+ }
+
+ public GhettoCompletableFuture<V> newIncompleteFuture() {
+ return new GhettoCompletableFuture<>();
+ }
+ }
+
+ /**
+ * {@link android.net.VpnService} implementation for {@link GoBackend}
+ */
+ public static class VpnService extends android.net.VpnService {
+ @Nullable private GoBackend owner;
+
+ public Builder getBuilder() {
+ return new Builder();
+ }
+
+ @Override
+ public void onCreate() {
+ vpnService.complete(this);
+ super.onCreate();
+ }
+
+ @Override
+ public void onDestroy() {
+ if (owner != null) {
+ final Tunnel tunnel = owner.currentTunnel;
+ if (tunnel != null) {
+ if (owner.currentTunnelHandle != -1)
+ wgTurnOff(owner.currentTunnelHandle);
+ owner.currentTunnel = null;
+ owner.currentTunnelHandle = -1;
+ owner.currentConfig = null;
+ tunnel.onStateChange(State.DOWN);
+ }
+ }
+ vpnService = vpnService.newIncompleteFuture();
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(@Nullable final Intent intent, final int flags, final int startId) {
+ vpnService.complete(this);
+ if (intent == null || intent.getComponent() == null || !intent.getComponent().getPackageName().equals(getPackageName())) {
+ Log.d(TAG, "Service started by Always-on VPN feature");
+ if (alwaysOnCallback != null)
+ alwaysOnCallback.alwaysOnTriggered();
+ }
+ return super.onStartCommand(intent, flags, startId);
+ }
+
+ public void setOwner(final GoBackend owner) {
+ this.owner = owner;
+ }
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java b/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java
new file mode 100644
index 00000000..08b84949
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.backend;
+
+import android.os.SystemClock;
+
+import com.wireguard.crypto.Key;
+import com.wireguard.util.NonNullForAll;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Class representing transfer statistics for a {@link Tunnel} instance.
+ */
+@NonNullForAll
+public class Statistics {
+ public record PeerStats(long rxBytes, long txBytes, long latestHandshakeEpochMillis) { }
+ private final Map<Key, PeerStats> stats = new HashMap<>();
+ private long lastTouched = SystemClock.elapsedRealtime();
+
+ Statistics() {
+ }
+
+ /**
+ * Add a peer and its current stats to the internal map.
+ *
+ * @param key A WireGuard public key bound to a particular peer
+ * @param rxBytes The received traffic for the {@link com.wireguard.config.Peer} referenced by
+ * the provided {@link Key}. This value is in bytes
+ * @param txBytes The transmitted traffic for the {@link com.wireguard.config.Peer} referenced by
+ * the provided {@link Key}. This value is in bytes.
+ * @param latestHandshake The timestamp of the latest handshake for the {@link com.wireguard.config.Peer}
+ * referenced by the provided {@link Key}. The value is in epoch milliseconds.
+ */
+ void add(final Key key, final long rxBytes, final long txBytes, final long latestHandshake) {
+ stats.put(key, new PeerStats(rxBytes, txBytes, latestHandshake));
+ lastTouched = SystemClock.elapsedRealtime();
+ }
+
+ /**
+ * Check if the statistics are stale, indicating the need for the {@link Backend} to update them.
+ *
+ * @return boolean indicating if the current statistics instance has stale values.
+ */
+ public boolean isStale() {
+ return SystemClock.elapsedRealtime() - lastTouched > 900;
+ }
+
+ /**
+ * Get the statistics for the {@link com.wireguard.config.Peer} referenced by the provided {@link Key}
+ *
+ * @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
+ * @return a {@link PeerStats} representing various statistics about this peer.
+ */
+ @Nullable
+ public PeerStats peer(final Key peer) {
+ return stats.get(peer);
+ }
+
+ /**
+ * Get the list of peers being tracked by this instance.
+ *
+ * @return An array of {@link Key} instances representing WireGuard
+ * {@link com.wireguard.config.Peer}s
+ */
+ public Key[] peers() {
+ return stats.keySet().toArray(new Key[0]);
+ }
+
+ /**
+ * Get the total received traffic by all the peers being tracked by this instance
+ *
+ * @return a long representing the number of bytes received by the peers being tracked.
+ */
+ public long totalRx() {
+ long rx = 0;
+ for (final PeerStats val : stats.values()) {
+ rx += val.rxBytes;
+ }
+ return rx;
+ }
+
+ /**
+ * Get the total transmitted traffic by all the peers being tracked by this instance
+ *
+ * @return a long representing the number of bytes transmitted by the peers being tracked.
+ */
+ public long totalTx() {
+ long tx = 0;
+ for (final PeerStats val : stats.values()) {
+ tx += val.txBytes;
+ }
+ return tx;
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java b/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java
new file mode 100644
index 00000000..766df443
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.backend;
+
+import com.wireguard.util.NonNullForAll;
+
+import java.util.regex.Pattern;
+
+/**
+ * Represents a WireGuard tunnel.
+ */
+
+@NonNullForAll
+public interface Tunnel {
+ int NAME_MAX_LENGTH = 15;
+ Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,15}");
+
+ static boolean isNameInvalid(final CharSequence name) {
+ return !NAME_PATTERN.matcher(name).matches();
+ }
+
+ /**
+ * Get the name of the tunnel, which should always pass the !isNameInvalid test.
+ *
+ * @return The name of the tunnel.
+ */
+ String getName();
+
+ /**
+ * React to a change in state of the tunnel. Should only be directly called by Backend.
+ *
+ * @param newState The new state of the tunnel.
+ */
+ void onStateChange(State newState);
+
+ /**
+ * Enum class to represent all possible states of a {@link Tunnel}.
+ */
+ enum State {
+ DOWN,
+ TOGGLE,
+ UP;
+
+ /**
+ * Get the state of a {@link Tunnel}
+ *
+ * @param running boolean indicating if the tunnel is running.
+ * @return State of the tunnel based on whether or not it is running.
+ */
+ public static State of(final boolean running) {
+ return running ? UP : DOWN;
+ }
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java b/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java
new file mode 100644
index 00000000..023743a8
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.backend;
+
+import android.content.Context;
+import android.util.Log;
+import android.util.Pair;
+
+import com.wireguard.android.backend.BackendException.Reason;
+import com.wireguard.android.backend.Tunnel.State;
+import com.wireguard.android.util.RootShell;
+import com.wireguard.android.util.ToolsInstaller;
+import com.wireguard.config.Config;
+import com.wireguard.crypto.Key;
+import com.wireguard.util.NonNullForAll;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Implementation of {@link Backend} that uses the kernel module and {@code wg-quick} to provide
+ * WireGuard tunnels.
+ */
+
+@NonNullForAll
+public final class WgQuickBackend implements Backend {
+ private static final String TAG = "WireGuard/WgQuickBackend";
+ private final File localTemporaryDir;
+ private final RootShell rootShell;
+ private final Map<Tunnel, Config> runningConfigs = new HashMap<>();
+ private final ToolsInstaller toolsInstaller;
+ private boolean multipleTunnels;
+
+ public WgQuickBackend(final Context context, final RootShell rootShell, final ToolsInstaller toolsInstaller) {
+ localTemporaryDir = new File(context.getCacheDir(), "tmp");
+ this.rootShell = rootShell;
+ this.toolsInstaller = toolsInstaller;
+ }
+
+ public static boolean hasKernelSupport() {
+ return new File("/sys/module/wireguard").exists();
+ }
+
+ @Override
+ public Set<String> getRunningTunnelNames() {
+ final List<String> output = new ArrayList<>();
+ // Don't throw an exception here or nothing will show up in the UI.
+ try {
+ toolsInstaller.ensureToolsAvailable();
+ if (rootShell.run(output, "wg show interfaces") != 0 || output.isEmpty())
+ return Collections.emptySet();
+ } catch (final Exception e) {
+ Log.w(TAG, "Unable to enumerate running tunnels", e);
+ return Collections.emptySet();
+ }
+ // wg puts all interface names on the same line. Split them into separate elements.
+ return Set.of(output.get(0).split(" "));
+ }
+
+ @Override
+ public State getState(final Tunnel tunnel) {
+ return getRunningTunnelNames().contains(tunnel.getName()) ? State.UP : State.DOWN;
+ }
+
+ @Override
+ public Statistics getStatistics(final Tunnel tunnel) {
+ final Statistics stats = new Statistics();
+ final Collection<String> output = new ArrayList<>();
+ try {
+ if (rootShell.run(output, String.format("wg show '%s' dump", tunnel.getName())) != 0)
+ return stats;
+ } catch (final Exception ignored) {
+ return stats;
+ }
+ for (final String line : output) {
+ final String[] parts = line.split("\\t");
+ if (parts.length != 8)
+ continue;
+ try {
+ stats.add(Key.fromBase64(parts[0]), Long.parseLong(parts[5]), Long.parseLong(parts[6]), Long.parseLong(parts[4]) * 1000);
+ } catch (final Exception ignored) {
+ }
+ }
+ return stats;
+ }
+
+ @Override
+ public String getVersion() throws Exception {
+ final List<String> output = new ArrayList<>();
+ if (rootShell.run(output, "cat /sys/module/wireguard/version") != 0 || output.isEmpty())
+ throw new BackendException(Reason.UNKNOWN_KERNEL_MODULE_NAME);
+ return output.get(0);
+ }
+
+ public void setMultipleTunnels(final boolean on) {
+ multipleTunnels = on;
+ }
+
+ @Override
+ public State setState(final Tunnel tunnel, State state, @Nullable final Config config) throws Exception {
+ final State originalState = getState(tunnel);
+ final Config originalConfig = runningConfigs.get(tunnel);
+ final Map<Tunnel, Config> runningConfigsSnapshot = new HashMap<>(runningConfigs);
+
+ if (state == State.TOGGLE)
+ state = originalState == State.UP ? State.DOWN : State.UP;
+ if ((state == State.UP && originalState == State.UP && originalConfig != null && originalConfig == config) ||
+ (state == State.DOWN && originalState == State.DOWN))
+ return originalState;
+ if (state == State.UP) {
+ toolsInstaller.ensureToolsAvailable();
+ if (!multipleTunnels && originalState == State.DOWN) {
+ final List<Pair<Tunnel, Config>> rewind = new LinkedList<>();
+ try {
+ for (final Map.Entry<Tunnel, Config> entry : runningConfigsSnapshot.entrySet()) {
+ setStateInternal(entry.getKey(), entry.getValue(), State.DOWN);
+ rewind.add(Pair.create(entry.getKey(), entry.getValue()));
+ }
+ } catch (final Exception e) {
+ try {
+ for (final Pair<Tunnel, Config> entry : rewind) {
+ setStateInternal(entry.first, entry.second, State.UP);
+ }
+ } catch (final Exception ignored) {
+ }
+ throw e;
+ }
+ }
+ if (originalState == State.UP)
+ setStateInternal(tunnel, originalConfig == null ? config : originalConfig, State.DOWN);
+ try {
+ setStateInternal(tunnel, config, State.UP);
+ } catch (final Exception e) {
+ try {
+ if (originalState == State.UP && originalConfig != null) {
+ setStateInternal(tunnel, originalConfig, State.UP);
+ }
+ if (!multipleTunnels && originalState == State.DOWN) {
+ for (final Map.Entry<Tunnel, Config> entry : runningConfigsSnapshot.entrySet()) {
+ setStateInternal(entry.getKey(), entry.getValue(), State.UP);
+ }
+ }
+ } catch (final Exception ignored) {
+ }
+ throw e;
+ }
+ } else if (state == State.DOWN) {
+ setStateInternal(tunnel, originalConfig == null ? config : originalConfig, State.DOWN);
+ }
+ return state;
+ }
+
+ private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception {
+ Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state);
+
+ Objects.requireNonNull(config, "Trying to set state up with a null config");
+
+ final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf");
+ try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) {
+ stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
+ }
+ String command = String.format("wg-quick %s '%s'",
+ state.toString().toLowerCase(Locale.ENGLISH), tempFile.getAbsolutePath());
+ if (state == State.UP)
+ command = "cat /sys/module/wireguard/version && " + command;
+ final int result = rootShell.run(null, command);
+ // noinspection ResultOfMethodCallIgnored
+ tempFile.delete();
+ if (result != 0)
+ throw new BackendException(Reason.WG_QUICK_CONFIG_ERROR_CODE, result);
+
+ if (state == State.UP)
+ runningConfigs.put(tunnel, config);
+ else
+ runningConfigs.remove(tunnel);
+
+ tunnel.onStateChange(state);
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/android/util/RootShell.java b/tunnel/src/main/java/com/wireguard/android/util/RootShell.java
new file mode 100644
index 00000000..67bff568
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/util/RootShell.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.util;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.wireguard.android.util.RootShell.RootShellException.Reason;
+import com.wireguard.util.NonNullForAll;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.UUID;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Helper class for running commands as root.
+ */
+
+@NonNullForAll
+public class RootShell {
+ private static final String SU = "su";
+ private static final String TAG = "WireGuard/RootShell";
+
+ private final File localBinaryDir;
+ private final File localTemporaryDir;
+ private final Object lock = new Object();
+ private final String preamble;
+ @Nullable private Process process;
+ @Nullable private BufferedReader stderr;
+ @Nullable private OutputStreamWriter stdin;
+ @Nullable private BufferedReader stdout;
+
+ public RootShell(final Context context) {
+ localBinaryDir = new File(context.getCodeCacheDir(), "bin");
+ localTemporaryDir = new File(context.getCacheDir(), "tmp");
+ final String packageName = context.getPackageName();
+ if (packageName.contains("'"))
+ throw new RuntimeException("Impossibly invalid package name contains a single quote");
+ preamble = String.format("export CALLING_PACKAGE='%s' PATH=\"%s:$PATH\" TMPDIR='%s'; magisk --sqlite \"UPDATE policies SET notification=0, logging=0 WHERE uid=%d\" >/dev/null 2>&1; id -u\n",
+ packageName, localBinaryDir, localTemporaryDir, android.os.Process.myUid());
+ }
+
+ private static boolean isExecutableInPath(final String name) {
+ final String path = System.getenv("PATH");
+ if (path == null)
+ return false;
+ for (final String dir : path.split(":"))
+ if (new File(dir, name).canExecute())
+ return true;
+ return false;
+ }
+
+ private boolean isRunning() {
+ synchronized (lock) {
+ try {
+ // Throws an exception if the process hasn't finished yet.
+ if (process != null)
+ process.exitValue();
+ return false;
+ } catch (final IllegalThreadStateException ignored) {
+ // The existing process is still running.
+ return true;
+ }
+ }
+ }
+
+ /**
+ * Run a command in a root shell.
+ *
+ * @param output Lines read from stdout are appended to this list. Pass null if the
+ * output from the shell is not important.
+ * @param command Command to run as root.
+ * @return The exit value of the command.
+ */
+ public int run(@Nullable final Collection<String> output, final String command)
+ throws IOException, RootShellException {
+ synchronized (lock) {
+ /* Start inside synchronized block to prevent a concurrent call to stop(). */
+ start();
+ final String marker = UUID.randomUUID().toString();
+ final String script = "echo " + marker + "; echo " + marker + " >&2; (" + command +
+ "); ret=$?; echo " + marker + " $ret; echo " + marker + " $ret >&2\n";
+ Log.v(TAG, "executing: " + command);
+ stdin.write(script);
+ stdin.flush();
+ String line;
+ int errnoStdout = Integer.MIN_VALUE;
+ int errnoStderr = Integer.MAX_VALUE;
+ int markersSeen = 0;
+ while ((line = stdout.readLine()) != null) {
+ if (line.startsWith(marker)) {
+ ++markersSeen;
+ if (line.length() > marker.length() + 1) {
+ errnoStdout = Integer.valueOf(line.substring(marker.length() + 1));
+ break;
+ }
+ } else if (markersSeen > 0) {
+ if (output != null)
+ output.add(line);
+ Log.v(TAG, "stdout: " + line);
+ }
+ }
+ while ((line = stderr.readLine()) != null) {
+ if (line.startsWith(marker)) {
+ ++markersSeen;
+ if (line.length() > marker.length() + 1) {
+ errnoStderr = Integer.valueOf(line.substring(marker.length() + 1));
+ break;
+ }
+ } else if (markersSeen > 2) {
+ Log.v(TAG, "stderr: " + line);
+ }
+ }
+ if (markersSeen != 4)
+ throw new RootShellException(Reason.SHELL_MARKER_COUNT_ERROR, markersSeen);
+ if (errnoStdout != errnoStderr)
+ throw new RootShellException(Reason.SHELL_EXIT_STATUS_READ_ERROR);
+ Log.v(TAG, "exit: " + errnoStdout);
+ return errnoStdout;
+ }
+ }
+
+ public void start() throws IOException, RootShellException {
+ if (!isExecutableInPath(SU))
+ throw new RootShellException(Reason.NO_ROOT_ACCESS);
+ synchronized (lock) {
+ if (isRunning())
+ return;
+ if (!localBinaryDir.isDirectory() && !localBinaryDir.mkdirs())
+ throw new RootShellException(Reason.CREATE_BIN_DIR_ERROR);
+ if (!localTemporaryDir.isDirectory() && !localTemporaryDir.mkdirs())
+ throw new RootShellException(Reason.CREATE_TEMP_DIR_ERROR);
+ try {
+ final ProcessBuilder builder = new ProcessBuilder().command(SU);
+ builder.environment().put("LC_ALL", "C");
+ try {
+ process = builder.start();
+ } catch (final IOException e) {
+ // A failure at this stage means the device isn't rooted.
+ final RootShellException rse = new RootShellException(Reason.NO_ROOT_ACCESS);
+ rse.initCause(e);
+ throw rse;
+ }
+ stdin = new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8);
+ stdout = new BufferedReader(new InputStreamReader(process.getInputStream(),
+ StandardCharsets.UTF_8));
+ stderr = new BufferedReader(new InputStreamReader(process.getErrorStream(),
+ StandardCharsets.UTF_8));
+ stdin.write(preamble);
+ stdin.flush();
+ // Check that the shell started successfully.
+ final String uid = stdout.readLine();
+ if (!"0".equals(uid)) {
+ Log.w(TAG, "Root check did not return correct UID: " + uid);
+ throw new RootShellException(Reason.NO_ROOT_ACCESS);
+ }
+ if (!isRunning()) {
+ String line;
+ while ((line = stderr.readLine()) != null) {
+ Log.w(TAG, "Root check returned an error: " + line);
+ if (line.contains("Permission denied"))
+ throw new RootShellException(Reason.NO_ROOT_ACCESS);
+ }
+ throw new RootShellException(Reason.SHELL_START_ERROR, process.exitValue());
+ }
+ } catch (final IOException | RootShellException e) {
+ stop();
+ throw e;
+ }
+ }
+ }
+
+ public void stop() {
+ synchronized (lock) {
+ if (process != null) {
+ process.destroy();
+ process = null;
+ }
+ }
+ }
+
+ public static class RootShellException extends Exception {
+ private final Object[] format;
+ private final Reason reason;
+
+ public RootShellException(final Reason reason, final Object... format) {
+ this.reason = reason;
+ this.format = format;
+ }
+
+ public Object[] getFormat() {
+ return format;
+ }
+
+ public Reason getReason() {
+ return reason;
+ }
+
+ public boolean isIORelated() {
+ return reason != Reason.NO_ROOT_ACCESS;
+ }
+
+ public enum Reason {
+ NO_ROOT_ACCESS,
+ SHELL_MARKER_COUNT_ERROR,
+ SHELL_EXIT_STATUS_READ_ERROR,
+ SHELL_START_ERROR,
+ CREATE_BIN_DIR_ERROR,
+ CREATE_TEMP_DIR_ERROR
+ }
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java b/tunnel/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java
new file mode 100644
index 00000000..15a00467
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.util;
+
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import com.wireguard.util.NonNullForAll;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+@NonNullForAll
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class SharedLibraryLoader {
+ private static final String TAG = "WireGuard/SharedLibraryLoader";
+
+ private SharedLibraryLoader() {
+ }
+
+ public static boolean extractLibrary(final Context context, final String libName, final File destination) throws IOException {
+ final Collection<String> apks = new HashSet<>();
+ if (context.getApplicationInfo().sourceDir != null)
+ apks.add(context.getApplicationInfo().sourceDir);
+ if (context.getApplicationInfo().splitSourceDirs != null)
+ apks.addAll(Arrays.asList(context.getApplicationInfo().splitSourceDirs));
+
+ for (final String abi : Build.SUPPORTED_ABIS) {
+ for (final String apk : apks) {
+ try (final ZipFile zipFile = new ZipFile(new File(apk), ZipFile.OPEN_READ)) {
+ final String mappedLibName = System.mapLibraryName(libName);
+ final String libZipPath = "lib" + File.separatorChar + abi + File.separatorChar + mappedLibName;
+ final ZipEntry zipEntry = zipFile.getEntry(libZipPath);
+ if (zipEntry == null)
+ continue;
+ Log.d(TAG, "Extracting apk:/" + libZipPath + " to " + destination.getAbsolutePath());
+ try (final FileOutputStream out = new FileOutputStream(destination);
+ final InputStream in = zipFile.getInputStream(zipEntry)) {
+ int len;
+ final byte[] buffer = new byte[1024 * 32];
+ while ((len = in.read(buffer)) != -1) {
+ out.write(buffer, 0, len);
+ }
+ out.getFD().sync();
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static void loadSharedLibrary(final Context context, final String libName) {
+ Throwable noAbiException;
+ try {
+ System.loadLibrary(libName);
+ return;
+ } catch (final UnsatisfiedLinkError e) {
+ Log.d(TAG, "Failed to load library normally, so attempting to extract from apk", e);
+ noAbiException = e;
+ }
+ File f = null;
+ try {
+ f = File.createTempFile("lib", ".so", context.getCodeCacheDir());
+ if (extractLibrary(context, libName, f)) {
+ System.load(f.getAbsolutePath());
+ return;
+ }
+ } catch (final Exception e) {
+ Log.d(TAG, "Failed to load library apk:/" + libName, e);
+ noAbiException = e;
+ } finally {
+ if (f != null)
+ // noinspection ResultOfMethodCallIgnored
+ f.delete();
+ }
+ if (noAbiException instanceof RuntimeException)
+ throw (RuntimeException) noAbiException;
+ throw new RuntimeException(noAbiException);
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/android/util/ToolsInstaller.java b/tunnel/src/main/java/com/wireguard/android/util/ToolsInstaller.java
new file mode 100644
index 00000000..65d23eab
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/util/ToolsInstaller.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.util;
+
+import android.content.Context;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.wireguard.android.util.RootShell.RootShellException;
+import com.wireguard.util.NonNullForAll;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * Helper to install WireGuard tools to the system partition.
+ */
+
+@NonNullForAll
+public final class ToolsInstaller {
+ public static final int ERROR = 0x0;
+ public static final int MAGISK = 0x4;
+ public static final int NO = 0x2;
+ public static final int SYSTEM = 0x8;
+ public static final int YES = 0x1;
+ private static final String[] EXECUTABLES = {"wg", "wg-quick"};
+ private static final File[] INSTALL_DIRS = {
+ new File("/system/xbin"),
+ new File("/system/bin"),
+ };
+ @Nullable private static final File INSTALL_DIR = getInstallDir();
+ private static final String TAG = "WireGuard/ToolsInstaller";
+
+ private final Context context;
+ private final File localBinaryDir;
+ private final Object lock = new Object();
+ private final RootShell rootShell;
+ @Nullable private Boolean areToolsAvailable;
+ @Nullable private Boolean installAsMagiskModule;
+
+ public ToolsInstaller(final Context context, final RootShell rootShell) {
+ localBinaryDir = new File(context.getCodeCacheDir(), "bin");
+ this.context = context;
+ this.rootShell = rootShell;
+ }
+
+ @Nullable
+ private static File getInstallDir() {
+ final String path = System.getenv("PATH");
+ if (path == null)
+ return INSTALL_DIRS[0];
+ final List<String> paths = Arrays.asList(path.split(":"));
+ for (final File dir : INSTALL_DIRS) {
+ if (paths.contains(dir.getPath()) && dir.isDirectory())
+ return dir;
+ }
+ return null;
+ }
+
+ public int areInstalled() throws RootShellException {
+ if (INSTALL_DIR == null)
+ return ERROR;
+ final StringBuilder script = new StringBuilder();
+ for (final String name : EXECUTABLES) {
+ script.append(String.format("cmp -s '%s' '%s' && ",
+ new File(localBinaryDir, name).getAbsolutePath(),
+ new File(INSTALL_DIR, name).getAbsolutePath()));
+ }
+ script.append("exit ").append(OsConstants.EALREADY).append(';');
+ try {
+ final int ret = rootShell.run(null, script.toString());
+ if (ret == OsConstants.EALREADY)
+ return willInstallAsMagiskModule() ? YES | MAGISK : YES | SYSTEM;
+ else
+ return willInstallAsMagiskModule() ? NO | MAGISK : NO | SYSTEM;
+ } catch (final IOException ignored) {
+ return ERROR;
+ } catch (final RootShellException e) {
+ if (e.isIORelated())
+ return ERROR;
+ throw e;
+ }
+ }
+
+ public void ensureToolsAvailable() throws FileNotFoundException {
+ synchronized (lock) {
+ if (areToolsAvailable == null) {
+ try {
+ Log.d(TAG, extract() ? "Tools are now extracted into our private binary dir" :
+ "Tools were already extracted into our private binary dir");
+ areToolsAvailable = true;
+ } catch (final IOException e) {
+ Log.e(TAG, "The wg and wg-quick tools are not available", e);
+ areToolsAvailable = false;
+ }
+ }
+ if (!areToolsAvailable)
+ throw new FileNotFoundException("Required tools unavailable");
+ }
+ }
+
+ public boolean extract() throws IOException {
+ localBinaryDir.mkdirs();
+ final File[] files = new File[EXECUTABLES.length];
+ final File[] tempFiles = new File[EXECUTABLES.length];
+ boolean allExist = true;
+ for (int i = 0; i < files.length; ++i) {
+ files[i] = new File(localBinaryDir, EXECUTABLES[i]);
+ tempFiles[i] = new File(localBinaryDir, EXECUTABLES[i] + ".tmp");
+ allExist &= files[i].exists();
+ }
+ if (allExist)
+ return false;
+ for (int i = 0; i < files.length; ++i) {
+ if (!SharedLibraryLoader.extractLibrary(context, EXECUTABLES[i], tempFiles[i]))
+ throw new FileNotFoundException("Unable to find " + EXECUTABLES[i]);
+ if (!tempFiles[i].setExecutable(true, false))
+ throw new IOException("Unable to mark " + tempFiles[i].getAbsolutePath() + " as executable");
+ if (!tempFiles[i].renameTo(files[i]))
+ throw new IOException("Unable to rename " + tempFiles[i].getAbsolutePath() + " to " + files[i].getAbsolutePath());
+ }
+ return true;
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public int install() throws RootShellException, IOException {
+ if (!context.getPackageName().startsWith("com.wireguard."))
+ throw new SecurityException("The tools may only be installed system-wide from the main WireGuard app.");
+ return willInstallAsMagiskModule() ? installMagisk() : installSystem();
+ }
+
+ private int installMagisk() throws RootShellException, IOException {
+ extract();
+ final StringBuilder script = new StringBuilder("set -ex; ");
+
+ script.append("trap 'rm -rf /data/adb/modules/wireguard' INT TERM EXIT; ");
+ script.append(String.format("rm -rf /data/adb/modules/wireguard/; mkdir -p /data/adb/modules/wireguard%s; ", INSTALL_DIR));
+ script.append("printf 'id=wireguard\nname=WireGuard Command Line Tools\nversion=1.0\nversionCode=1\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /data/adb/modules/wireguard/module.prop; ");
+ script.append("touch /data/adb/modules/wireguard/auto_mount; ");
+ for (final String name : EXECUTABLES) {
+ final File destination = new File("/data/adb/modules/wireguard" + INSTALL_DIR, name);
+ script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; chcon 'u:object_r:system_file:s0' '%s' || true; ",
+ new File(localBinaryDir, name), destination, destination, destination));
+ }
+ script.append("trap - INT TERM EXIT;");
+
+ try {
+ return rootShell.run(null, script.toString()) == 0 ? YES | MAGISK : ERROR;
+ } catch (final IOException ignored) {
+ return ERROR;
+ } catch (final RootShellException e) {
+ if (e.isIORelated())
+ return ERROR;
+ throw e;
+ }
+ }
+
+ private int installSystem() throws RootShellException, IOException {
+ if (INSTALL_DIR == null)
+ return OsConstants.ENOENT;
+ extract();
+ final StringBuilder script = new StringBuilder("set -ex; ");
+ script.append("trap 'mount -o ro,remount /system' EXIT; mount -o rw,remount /system; ");
+ for (final String name : EXECUTABLES) {
+ final File destination = new File(INSTALL_DIR, name);
+ script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; restorecon '%s' || true; ",
+ new File(localBinaryDir, name), destination, destination, destination));
+ }
+ try {
+ return rootShell.run(null, script.toString()) == 0 ? YES | SYSTEM : ERROR;
+ } catch (final IOException ignored) {
+ return ERROR;
+ } catch (final RootShellException e) {
+ if (e.isIORelated())
+ return ERROR;
+ throw e;
+ }
+ }
+
+ private boolean willInstallAsMagiskModule() {
+ synchronized (lock) {
+ if (installAsMagiskModule == null) {
+ try {
+ installAsMagiskModule = rootShell.run(null, "[ -d /data/adb/modules -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
+ } catch (final Exception ignored) {
+ installAsMagiskModule = false;
+ }
+ }
+ return installAsMagiskModule;
+ }
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/Attribute.java b/tunnel/src/main/java/com/wireguard/config/Attribute.java
new file mode 100644
index 00000000..1b7bc368
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/Attribute.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import com.wireguard.util.NonNullForAll;
+
+import java.util.Iterator;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@NonNullForAll
+public final class Attribute {
+ private static final Pattern LINE_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*([^\\s#][^#]*)");
+ private static final Pattern LIST_SEPARATOR = Pattern.compile("\\s*,\\s*");
+
+ private final String key;
+ private final String value;
+
+ private Attribute(final String key, final String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public static String join(final Iterable<?> values) {
+ final Iterator<?> it = values.iterator();
+ if (!it.hasNext()) {
+ return "";
+ }
+ final StringBuilder sb = new StringBuilder();
+ sb.append(it.next());
+ while (it.hasNext()) {
+ sb.append(", ");
+ sb.append(it.next());
+ }
+ return sb.toString();
+ }
+
+ public static Optional<Attribute> parse(final CharSequence line) {
+ final Matcher matcher = LINE_PATTERN.matcher(line);
+ if (!matcher.matches())
+ return Optional.empty();
+ return Optional.of(new Attribute(matcher.group(1), matcher.group(2)));
+ }
+
+ public static String[] split(final CharSequence value) {
+ return LIST_SEPARATOR.split(value);
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/BadConfigException.java b/tunnel/src/main/java/com/wireguard/config/BadConfigException.java
new file mode 100644
index 00000000..db022e14
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/BadConfigException.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import com.wireguard.crypto.KeyFormatException;
+import com.wireguard.util.NonNullForAll;
+
+import androidx.annotation.Nullable;
+
+@NonNullForAll
+public class BadConfigException extends Exception {
+ private final Location location;
+ private final Reason reason;
+ private final Section section;
+ @Nullable private final CharSequence text;
+
+ private BadConfigException(final Section section, final Location location,
+ final Reason reason, @Nullable final CharSequence text,
+ @Nullable final Throwable cause) {
+ super(cause);
+ this.section = section;
+ this.location = location;
+ this.reason = reason;
+ this.text = text;
+ }
+
+ public BadConfigException(final Section section, final Location location,
+ final Reason reason, @Nullable final CharSequence text) {
+ this(section, location, reason, text, null);
+ }
+
+ public BadConfigException(final Section section, final Location location,
+ final KeyFormatException cause) {
+ this(section, location, Reason.INVALID_KEY, null, cause);
+ }
+
+ public BadConfigException(final Section section, final Location location,
+ @Nullable final CharSequence text,
+ final NumberFormatException cause) {
+ this(section, location, Reason.INVALID_NUMBER, text, cause);
+ }
+
+ public BadConfigException(final Section section, final Location location,
+ final ParseException cause) {
+ this(section, location, Reason.INVALID_VALUE, cause.getText(), cause);
+ }
+
+ public Location getLocation() {
+ return location;
+ }
+
+ public Reason getReason() {
+ return reason;
+ }
+
+ public Section getSection() {
+ return section;
+ }
+
+ @Nullable
+ public CharSequence getText() {
+ return text;
+ }
+
+ public enum Location {
+ TOP_LEVEL(""),
+ ADDRESS("Address"),
+ ALLOWED_IPS("AllowedIPs"),
+ DNS("DNS"),
+ ENDPOINT("Endpoint"),
+ EXCLUDED_APPLICATIONS("ExcludedApplications"),
+ INCLUDED_APPLICATIONS("IncludedApplications"),
+ LISTEN_PORT("ListenPort"),
+ MTU("MTU"),
+ PERSISTENT_KEEPALIVE("PersistentKeepalive"),
+ PRE_SHARED_KEY("PresharedKey"),
+ PRIVATE_KEY("PrivateKey"),
+ PUBLIC_KEY("PublicKey");
+
+ private final String name;
+
+ Location(final String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+
+ public enum Reason {
+ INVALID_KEY,
+ INVALID_NUMBER,
+ INVALID_VALUE,
+ MISSING_ATTRIBUTE,
+ MISSING_SECTION,
+ SYNTAX_ERROR,
+ UNKNOWN_ATTRIBUTE,
+ UNKNOWN_SECTION
+ }
+
+ public enum Section {
+ CONFIG("Config"),
+ INTERFACE("Interface"),
+ PEER("Peer");
+
+ private final String name;
+
+ Section(final String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/Config.java b/tunnel/src/main/java/com/wireguard/config/Config.java
new file mode 100644
index 00000000..ee9cebce
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/Config.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import com.wireguard.config.BadConfigException.Location;
+import com.wireguard.config.BadConfigException.Reason;
+import com.wireguard.config.BadConfigException.Section;
+import com.wireguard.util.NonNullForAll;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Represents the contents of a wg-quick configuration file, made up of one or more "Interface"
+ * sections (combined together), and zero or more "Peer" sections (treated individually).
+ * <p>
+ * Instances of this class are immutable.
+ */
+@NonNullForAll
+public final class Config {
+ private final Interface interfaze;
+ private final List<Peer> peers;
+
+ private Config(final Builder builder) {
+ interfaze = Objects.requireNonNull(builder.interfaze, "An [Interface] section is required");
+ // Defensively copy to ensure immutability even if the Builder is reused.
+ peers = Collections.unmodifiableList(new ArrayList<>(builder.peers));
+ }
+
+ /**
+ * Parses an series of "Interface" and "Peer" sections into a {@code Config}. Throws
+ * {@link BadConfigException} if the input is not well-formed or contains data that cannot
+ * be parsed.
+ *
+ * @param stream a stream of UTF-8 text that is interpreted as a WireGuard configuration
+ * @return a {@code Config} instance representing the supplied configuration
+ */
+ public static Config parse(final InputStream stream)
+ throws IOException, BadConfigException {
+ return parse(new BufferedReader(new InputStreamReader(stream)));
+ }
+
+ /**
+ * Parses an series of "Interface" and "Peer" sections into a {@code Config}. Throws
+ * {@link BadConfigException} if the input is not well-formed or contains data that cannot
+ * be parsed.
+ *
+ * @param reader a BufferedReader of UTF-8 text that is interpreted as a WireGuard configuration
+ * @return a {@code Config} instance representing the supplied configuration
+ */
+ public static Config parse(final BufferedReader reader)
+ throws IOException, BadConfigException {
+ final Builder builder = new Builder();
+ final Collection<String> interfaceLines = new ArrayList<>();
+ final Collection<String> peerLines = new ArrayList<>();
+ boolean inInterfaceSection = false;
+ boolean inPeerSection = false;
+ boolean seenInterfaceSection = false;
+ @Nullable String line;
+ while ((line = reader.readLine()) != null) {
+ final int commentIndex = line.indexOf('#');
+ if (commentIndex != -1)
+ line = line.substring(0, commentIndex);
+ line = line.trim();
+ if (line.isEmpty())
+ continue;
+ if (line.startsWith("[")) {
+ // Consume all [Peer] lines read so far.
+ if (inPeerSection) {
+ builder.parsePeer(peerLines);
+ peerLines.clear();
+ }
+ if ("[Interface]".equalsIgnoreCase(line)) {
+ inInterfaceSection = true;
+ inPeerSection = false;
+ seenInterfaceSection = true;
+ } else if ("[Peer]".equalsIgnoreCase(line)) {
+ inInterfaceSection = false;
+ inPeerSection = true;
+ } else {
+ throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
+ Reason.UNKNOWN_SECTION, line);
+ }
+ } else if (inInterfaceSection) {
+ interfaceLines.add(line);
+ } else if (inPeerSection) {
+ peerLines.add(line);
+ } else {
+ throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
+ Reason.UNKNOWN_SECTION, line);
+ }
+ }
+ if (inPeerSection)
+ builder.parsePeer(peerLines);
+ if (!seenInterfaceSection)
+ throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
+ Reason.MISSING_SECTION, null);
+ // Combine all [Interface] sections in the file.
+ builder.parseInterface(interfaceLines);
+ return builder.build();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof Config))
+ return false;
+ final Config other = (Config) obj;
+ return interfaze.equals(other.interfaze) && peers.equals(other.peers);
+ }
+
+ /**
+ * Returns the interface section of the configuration.
+ *
+ * @return the interface configuration
+ */
+ public Interface getInterface() {
+ return interfaze;
+ }
+
+ /**
+ * Returns a list of the configuration's peer sections.
+ *
+ * @return a list of {@link Peer}s
+ */
+ public List<Peer> getPeers() {
+ return peers;
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * interfaze.hashCode() + peers.hashCode();
+ }
+
+ /**
+ * Converts the {@code Config} into a string suitable for debugging purposes. The {@code Config}
+ * is identified by its interface's public key and the number of peers it has.
+ *
+ * @return a concise single-line identifier for the {@code Config}
+ */
+ @Override
+ public String toString() {
+ return "(Config " + interfaze + " (" + peers.size() + " peers))";
+ }
+
+ /**
+ * Converts the {@code Config} into a string suitable for use as a {@code wg-quick}
+ * configuration file.
+ *
+ * @return the {@code Config} represented as one [Interface] and zero or more [Peer] sections
+ */
+ public String toWgQuickString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("[Interface]\n").append(interfaze.toWgQuickString());
+ for (final Peer peer : peers)
+ sb.append("\n[Peer]\n").append(peer.toWgQuickString());
+ return sb.toString();
+ }
+
+ /**
+ * Serializes the {@code Config} for use with the WireGuard cross-platform userspace API.
+ *
+ * @return the {@code Config} represented as a series of "key=value" lines
+ */
+ public String toWgUserspaceString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(interfaze.toWgUserspaceString());
+ sb.append("replace_peers=true\n");
+ for (final Peer peer : peers)
+ sb.append(peer.toWgUserspaceString());
+ return sb.toString();
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ public static final class Builder {
+ // Defaults to an empty set.
+ private final ArrayList<Peer> peers = new ArrayList<>();
+ // No default; must be provided before building.
+ @Nullable private Interface interfaze;
+
+ public Builder addPeer(final Peer peer) {
+ peers.add(peer);
+ return this;
+ }
+
+ public Builder addPeers(final Collection<Peer> peers) {
+ this.peers.addAll(peers);
+ return this;
+ }
+
+ public Config build() {
+ if (interfaze == null)
+ throw new IllegalArgumentException("An [Interface] section is required");
+ return new Config(this);
+ }
+
+ public Builder parseInterface(final Iterable<? extends CharSequence> lines)
+ throws BadConfigException {
+ return setInterface(Interface.parse(lines));
+ }
+
+ public Builder parsePeer(final Iterable<? extends CharSequence> lines)
+ throws BadConfigException {
+ return addPeer(Peer.parse(lines));
+ }
+
+ public Builder setInterface(final Interface interfaze) {
+ this.interfaze = interfaze;
+ return this;
+ }
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/InetAddresses.java b/tunnel/src/main/java/com/wireguard/config/InetAddresses.java
new file mode 100644
index 00000000..e4d697c5
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/InetAddresses.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import com.wireguard.util.NonNullForAll;
+
+import java.lang.reflect.Method;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.regex.Pattern;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Utility methods for creating instances of {@link InetAddress}.
+ */
+@NonNullForAll
+public final class InetAddresses {
+ @Nullable private static final Method PARSER_METHOD;
+ private static final Pattern WONT_TOUCH_RESOLVER = Pattern.compile("^(((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?)|((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$");
+ private static final Pattern VALID_HOSTNAME = Pattern.compile("^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\\.?$");
+
+ static {
+ Method m = null;
+ try {
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.Q)
+ // noinspection JavaReflectionMemberAccess
+ m = InetAddress.class.getMethod("parseNumericAddress", String.class);
+ } catch (final Exception ignored) {
+ }
+ PARSER_METHOD = m;
+ }
+
+ private InetAddresses() {
+ }
+
+ /**
+ * Determines whether input is a valid DNS hostname.
+ *
+ * @param maybeHostname a string that is possibly a DNS hostname
+ * @return whether or not maybeHostname is a valid DNS hostname
+ */
+ public static boolean isHostname(final CharSequence maybeHostname) {
+ return VALID_HOSTNAME.matcher(maybeHostname).matches();
+ }
+
+ /**
+ * Parses a numeric IPv4 or IPv6 address without performing any DNS lookups.
+ *
+ * @param address a string representing the IP address
+ * @return an instance of {@link Inet4Address} or {@link Inet6Address}, as appropriate
+ */
+ public static InetAddress parse(final String address) throws ParseException {
+ if (address.isEmpty())
+ throw new ParseException(InetAddress.class, address, "Empty address");
+ try {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q)
+ return android.net.InetAddresses.parseNumericAddress(address);
+ else if (PARSER_METHOD != null)
+ return (InetAddress) PARSER_METHOD.invoke(null, address);
+ else
+ throw new NoSuchMethodException("parseNumericAddress");
+ } catch (final IllegalArgumentException e) {
+ throw new ParseException(InetAddress.class, address, e);
+ } catch (final Exception e) {
+ final Throwable cause = e.getCause();
+ // Re-throw parsing exceptions with the original type, as callers might try to catch
+ // them. On the other hand, callers cannot be expected to handle reflection failures.
+ if (cause instanceof IllegalArgumentException)
+ throw new ParseException(InetAddress.class, address, cause);
+ try {
+ if (WONT_TOUCH_RESOLVER.matcher(address).matches())
+ return InetAddress.getByName(address);
+ else
+ throw new ParseException(InetAddress.class, address, "Not an IP address");
+ } catch (final UnknownHostException f) {
+ throw new ParseException(InetAddress.class, address, f);
+ }
+ }
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java b/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java
new file mode 100644
index 00000000..d1db432b
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import com.wireguard.util.NonNullForAll;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Optional;
+import java.util.regex.Pattern;
+
+import androidx.annotation.Nullable;
+
+
+/**
+ * An external endpoint (host and port) used to connect to a WireGuard {@link Peer}.
+ * <p>
+ * Instances of this class are externally immutable.
+ */
+@NonNullForAll
+public final class InetEndpoint {
+ private static final Pattern BARE_IPV6 = Pattern.compile("^[^\\[\\]]*:[^\\[\\]]*");
+ private static final Pattern FORBIDDEN_CHARACTERS = Pattern.compile("[/?#]");
+
+ private final String host;
+ private final boolean isResolved;
+ private final Object lock = new Object();
+ private final int port;
+ private Instant lastResolution = Instant.EPOCH;
+ @Nullable private InetEndpoint resolved;
+
+ private InetEndpoint(final String host, final boolean isResolved, final int port) {
+ this.host = host;
+ this.isResolved = isResolved;
+ this.port = port;
+ }
+
+ public static InetEndpoint parse(final String endpoint) throws ParseException {
+ if (FORBIDDEN_CHARACTERS.matcher(endpoint).find())
+ throw new ParseException(InetEndpoint.class, endpoint, "Forbidden characters");
+ final URI uri;
+ try {
+ uri = new URI("wg://" + endpoint);
+ } catch (final URISyntaxException e) {
+ throw new ParseException(InetEndpoint.class, endpoint, e);
+ }
+ if (uri.getPort() < 0 || uri.getPort() > 65535)
+ throw new ParseException(InetEndpoint.class, endpoint, "Missing/invalid port number");
+ try {
+ InetAddresses.parse(uri.getHost());
+ // Parsing ths host as a numeric address worked, so we don't need to do DNS lookups.
+ return new InetEndpoint(uri.getHost(), true, uri.getPort());
+ } catch (final ParseException ignored) {
+ // Failed to parse the host as a numeric address, so it must be a DNS hostname/FQDN.
+ return new InetEndpoint(uri.getHost(), false, uri.getPort());
+ }
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof InetEndpoint))
+ return false;
+ final InetEndpoint other = (InetEndpoint) obj;
+ return host.equals(other.host) && port == other.port;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ /**
+ * Generate an {@code InetEndpoint} instance with the same port and the host resolved using DNS
+ * to a numeric address. If the host is already numeric, the existing instance may be returned.
+ * Because this function may perform network I/O, it must not be called from the main thread.
+ *
+ * @return the resolved endpoint, or {@link Optional#empty()}
+ */
+ public Optional<InetEndpoint> getResolved() {
+ if (isResolved)
+ return Optional.of(this);
+ synchronized (lock) {
+ //TODO(zx2c4): Implement a real timeout mechanism using DNS TTL
+ if (Duration.between(lastResolution, Instant.now()).toMinutes() > 1) {
+ try {
+ // Prefer v4 endpoints over v6 to work around DNS64 and IPv6 NAT issues.
+ final InetAddress[] candidates = InetAddress.getAllByName(host);
+ InetAddress address = candidates[0];
+ for (final InetAddress candidate : candidates) {
+ if (candidate instanceof Inet4Address) {
+ address = candidate;
+ break;
+ }
+ }
+ resolved = new InetEndpoint(address.getHostAddress(), true, port);
+ lastResolution = Instant.now();
+ } catch (final UnknownHostException e) {
+ resolved = null;
+ }
+ }
+ return Optional.ofNullable(resolved);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return host.hashCode() ^ port;
+ }
+
+ @Override
+ public String toString() {
+ final boolean isBareIpv6 = isResolved && BARE_IPV6.matcher(host).matches();
+ return (isBareIpv6 ? '[' + host + ']' : host) + ':' + port;
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/InetNetwork.java b/tunnel/src/main/java/com/wireguard/config/InetNetwork.java
new file mode 100644
index 00000000..4a918044
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/InetNetwork.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import com.wireguard.util.NonNullForAll;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+
+/**
+ * An Internet network, denoted by its address and netmask
+ * <p>
+ * Instances of this class are immutable.
+ */
+@NonNullForAll
+public final class InetNetwork {
+ private final InetAddress address;
+ private final int mask;
+
+ private InetNetwork(final InetAddress address, final int mask) {
+ this.address = address;
+ this.mask = mask;
+ }
+
+ public static InetNetwork parse(final String network) throws ParseException {
+ final int slash = network.lastIndexOf('/');
+ final String maskString;
+ final int rawMask;
+ final String rawAddress;
+ if (slash >= 0) {
+ maskString = network.substring(slash + 1);
+ try {
+ rawMask = Integer.parseInt(maskString, 10);
+ } catch (final NumberFormatException ignored) {
+ throw new ParseException(Integer.class, maskString);
+ }
+ rawAddress = network.substring(0, slash);
+ } else {
+ maskString = "";
+ rawMask = -1;
+ rawAddress = network;
+ }
+ final InetAddress address = InetAddresses.parse(rawAddress);
+ final int maxMask = (address instanceof Inet4Address) ? 32 : 128;
+ if (rawMask > maxMask)
+ throw new ParseException(InetNetwork.class, maskString, "Invalid network mask");
+ final int mask = rawMask >= 0 ? rawMask : maxMask;
+ return new InetNetwork(address, mask);
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof InetNetwork))
+ return false;
+ final InetNetwork other = (InetNetwork) obj;
+ return address.equals(other.address) && mask == other.mask;
+ }
+
+ public InetAddress getAddress() {
+ return address;
+ }
+
+ public int getMask() {
+ return mask;
+ }
+
+ @Override
+ public int hashCode() {
+ return address.hashCode() ^ mask;
+ }
+
+ @Override
+ public String toString() {
+ return address.getHostAddress() + '/' + mask;
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/Interface.java b/tunnel/src/main/java/com/wireguard/config/Interface.java
new file mode 100644
index 00000000..bebca2e5
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/Interface.java
@@ -0,0 +1,423 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import com.wireguard.config.BadConfigException.Location;
+import com.wireguard.config.BadConfigException.Reason;
+import com.wireguard.config.BadConfigException.Section;
+import com.wireguard.crypto.Key;
+import com.wireguard.crypto.KeyFormatException;
+import com.wireguard.crypto.KeyPair;
+import com.wireguard.util.NonNullForAll;
+
+import java.net.InetAddress;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Represents the configuration for a WireGuard interface (an [Interface] block). Interfaces must
+ * have a private key (used to initialize a {@code KeyPair}), and may optionally have several other
+ * attributes.
+ * <p>
+ * Instances of this class are immutable.
+ */
+@NonNullForAll
+public final class Interface {
+ private static final int MAX_UDP_PORT = 65535;
+ private static final int MIN_UDP_PORT = 0;
+
+ private final Set<InetNetwork> addresses;
+ private final Set<InetAddress> dnsServers;
+ private final Set<String> dnsSearchDomains;
+ private final Set<String> excludedApplications;
+ private final Set<String> includedApplications;
+ private final KeyPair keyPair;
+ private final Optional<Integer> listenPort;
+ private final Optional<Integer> mtu;
+
+ private Interface(final Builder builder) {
+ // Defensively copy to ensure immutability even if the Builder is reused.
+ addresses = Collections.unmodifiableSet(new LinkedHashSet<>(builder.addresses));
+ dnsServers = Collections.unmodifiableSet(new LinkedHashSet<>(builder.dnsServers));
+ dnsSearchDomains = Collections.unmodifiableSet(new LinkedHashSet<>(builder.dnsSearchDomains));
+ excludedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.excludedApplications));
+ includedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.includedApplications));
+ keyPair = Objects.requireNonNull(builder.keyPair, "Interfaces must have a private key");
+ listenPort = builder.listenPort;
+ mtu = builder.mtu;
+ }
+
+ /**
+ * Parses an series of "KEY = VALUE" lines into an {@code Interface}. Throws
+ * {@link ParseException} if the input is not well-formed or contains unknown attributes.
+ *
+ * @param lines An iterable sequence of lines, containing at least a private key attribute
+ * @return An {@code Interface} with all of the attributes from {@code lines} set
+ */
+ public static Interface parse(final Iterable<? extends CharSequence> lines)
+ throws BadConfigException {
+ final Builder builder = new Builder();
+ for (final CharSequence line : lines) {
+ final Attribute attribute = Attribute.parse(line).orElseThrow(() ->
+ new BadConfigException(Section.INTERFACE, Location.TOP_LEVEL,
+ Reason.SYNTAX_ERROR, line));
+ switch (attribute.getKey().toLowerCase(Locale.ENGLISH)) {
+ case "address":
+ builder.parseAddresses(attribute.getValue());
+ break;
+ case "dns":
+ builder.parseDnsServers(attribute.getValue());
+ break;
+ case "excludedapplications":
+ builder.parseExcludedApplications(attribute.getValue());
+ break;
+ case "includedapplications":
+ builder.parseIncludedApplications(attribute.getValue());
+ break;
+ case "listenport":
+ builder.parseListenPort(attribute.getValue());
+ break;
+ case "mtu":
+ builder.parseMtu(attribute.getValue());
+ break;
+ case "privatekey":
+ builder.parsePrivateKey(attribute.getValue());
+ break;
+ default:
+ throw new BadConfigException(Section.INTERFACE, Location.TOP_LEVEL,
+ Reason.UNKNOWN_ATTRIBUTE, attribute.getKey());
+ }
+ }
+ return builder.build();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof Interface))
+ return false;
+ final Interface other = (Interface) obj;
+ return addresses.equals(other.addresses)
+ && dnsServers.equals(other.dnsServers)
+ && dnsSearchDomains.equals(other.dnsSearchDomains)
+ && excludedApplications.equals(other.excludedApplications)
+ && includedApplications.equals(other.includedApplications)
+ && keyPair.equals(other.keyPair)
+ && listenPort.equals(other.listenPort)
+ && mtu.equals(other.mtu);
+ }
+
+ /**
+ * Returns the set of IP addresses assigned to the interface.
+ *
+ * @return a set of {@link InetNetwork}s
+ */
+ public Set<InetNetwork> getAddresses() {
+ // The collection is already immutable.
+ return addresses;
+ }
+
+ /**
+ * Returns the set of DNS servers associated with the interface.
+ *
+ * @return a set of {@link InetAddress}es
+ */
+ public Set<InetAddress> getDnsServers() {
+ // The collection is already immutable.
+ return dnsServers;
+ }
+
+ /**
+ * Returns the set of DNS search domains associated with the interface.
+ *
+ * @return a set of strings
+ */
+ public Set<String> getDnsSearchDomains() {
+ // The collection is already immutable.
+ return dnsSearchDomains;
+ }
+
+ /**
+ * Returns the set of applications excluded from using the interface.
+ *
+ * @return a set of package names
+ */
+ public Set<String> getExcludedApplications() {
+ // The collection is already immutable.
+ return excludedApplications;
+ }
+
+ /**
+ * Returns the set of applications included exclusively for using the interface.
+ *
+ * @return a set of package names
+ */
+ public Set<String> getIncludedApplications() {
+ // The collection is already immutable.
+ return includedApplications;
+ }
+
+ /**
+ * Returns the public/private key pair used by the interface.
+ *
+ * @return a key pair
+ */
+ public KeyPair getKeyPair() {
+ return keyPair;
+ }
+
+ /**
+ * Returns the UDP port number that the WireGuard interface will listen on.
+ *
+ * @return a UDP port number, or {@code Optional.empty()} if none is configured
+ */
+ public Optional<Integer> getListenPort() {
+ return listenPort;
+ }
+
+ /**
+ * Returns the MTU used for the WireGuard interface.
+ *
+ * @return the MTU, or {@code Optional.empty()} if none is configured
+ */
+ public Optional<Integer> getMtu() {
+ return mtu;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 1;
+ hash = 31 * hash + addresses.hashCode();
+ hash = 31 * hash + dnsServers.hashCode();
+ hash = 31 * hash + excludedApplications.hashCode();
+ hash = 31 * hash + includedApplications.hashCode();
+ hash = 31 * hash + keyPair.hashCode();
+ hash = 31 * hash + listenPort.hashCode();
+ hash = 31 * hash + mtu.hashCode();
+ return hash;
+ }
+
+ /**
+ * Converts the {@code Interface} into a string suitable for debugging purposes. The {@code
+ * Interface} is identified by its public key and (if set) the port used for its UDP socket.
+ *
+ * @return A concise single-line identifier for the {@code Interface}
+ */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("(Interface ");
+ sb.append(keyPair.getPublicKey().toBase64());
+ listenPort.ifPresent(lp -> sb.append(" @").append(lp));
+ sb.append(')');
+ return sb.toString();
+ }
+
+ /**
+ * Converts the {@code Interface} into a string suitable for inclusion in a {@code wg-quick}
+ * configuration file.
+ *
+ * @return The {@code Interface} represented as a series of "Key = Value" lines
+ */
+ public String toWgQuickString() {
+ final StringBuilder sb = new StringBuilder();
+ if (!addresses.isEmpty())
+ sb.append("Address = ").append(Attribute.join(addresses)).append('\n');
+ if (!dnsServers.isEmpty()) {
+ final List<String> dnsServerStrings = dnsServers.stream().map(InetAddress::getHostAddress).collect(Collectors.toList());
+ dnsServerStrings.addAll(dnsSearchDomains);
+ sb.append("DNS = ").append(Attribute.join(dnsServerStrings)).append('\n');
+ }
+ if (!excludedApplications.isEmpty())
+ sb.append("ExcludedApplications = ").append(Attribute.join(excludedApplications)).append('\n');
+ if (!includedApplications.isEmpty())
+ sb.append("IncludedApplications = ").append(Attribute.join(includedApplications)).append('\n');
+ listenPort.ifPresent(lp -> sb.append("ListenPort = ").append(lp).append('\n'));
+ mtu.ifPresent(m -> sb.append("MTU = ").append(m).append('\n'));
+ sb.append("PrivateKey = ").append(keyPair.getPrivateKey().toBase64()).append('\n');
+ return sb.toString();
+ }
+
+ /**
+ * Serializes the {@code Interface} for use with the WireGuard cross-platform userspace API.
+ * Note that not all attributes are included in this representation.
+ *
+ * @return the {@code Interface} represented as a series of "KEY=VALUE" lines
+ */
+ public String toWgUserspaceString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("private_key=").append(keyPair.getPrivateKey().toHex()).append('\n');
+ listenPort.ifPresent(lp -> sb.append("listen_port=").append(lp).append('\n'));
+ return sb.toString();
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ public static final class Builder {
+ // Defaults to an empty set.
+ private final Set<InetNetwork> addresses = new LinkedHashSet<>();
+ // Defaults to an empty set.
+ private final Set<InetAddress> dnsServers = new LinkedHashSet<>();
+ // Defaults to an empty set.
+ private final Set<String> dnsSearchDomains = new LinkedHashSet<>();
+ // Defaults to an empty set.
+ private final Set<String> excludedApplications = new LinkedHashSet<>();
+ // Defaults to an empty set.
+ private final Set<String> includedApplications = new LinkedHashSet<>();
+ // No default; must be provided before building.
+ @Nullable private KeyPair keyPair;
+ // Defaults to not present.
+ private Optional<Integer> listenPort = Optional.empty();
+ // Defaults to not present.
+ private Optional<Integer> mtu = Optional.empty();
+
+ public Builder addAddress(final InetNetwork address) {
+ addresses.add(address);
+ return this;
+ }
+
+ public Builder addAddresses(final Collection<InetNetwork> addresses) {
+ this.addresses.addAll(addresses);
+ return this;
+ }
+
+ public Builder addDnsServer(final InetAddress dnsServer) {
+ dnsServers.add(dnsServer);
+ return this;
+ }
+
+ public Builder addDnsServers(final Collection<? extends InetAddress> dnsServers) {
+ this.dnsServers.addAll(dnsServers);
+ return this;
+ }
+
+ public Builder addDnsSearchDomain(final String dnsSearchDomain) {
+ dnsSearchDomains.add(dnsSearchDomain);
+ return this;
+ }
+
+ public Builder addDnsSearchDomains(final Collection<String> dnsSearchDomains) {
+ this.dnsSearchDomains.addAll(dnsSearchDomains);
+ return this;
+ }
+
+ public Interface build() throws BadConfigException {
+ if (keyPair == null)
+ throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY,
+ Reason.MISSING_ATTRIBUTE, null);
+ if (!includedApplications.isEmpty() && !excludedApplications.isEmpty())
+ throw new BadConfigException(Section.INTERFACE, Location.INCLUDED_APPLICATIONS,
+ Reason.INVALID_KEY, null);
+ return new Interface(this);
+ }
+
+ public Builder excludeApplication(final String application) {
+ excludedApplications.add(application);
+ return this;
+ }
+
+ public Builder excludeApplications(final Collection<String> applications) {
+ excludedApplications.addAll(applications);
+ return this;
+ }
+
+ public Builder includeApplication(final String application) {
+ includedApplications.add(application);
+ return this;
+ }
+
+ public Builder includeApplications(final Collection<String> applications) {
+ includedApplications.addAll(applications);
+ return this;
+ }
+
+ public Builder parseAddresses(final CharSequence addresses) throws BadConfigException {
+ try {
+ for (final String address : Attribute.split(addresses))
+ addAddress(InetNetwork.parse(address));
+ return this;
+ } catch (final ParseException e) {
+ throw new BadConfigException(Section.INTERFACE, Location.ADDRESS, e);
+ }
+ }
+
+ public Builder parseDnsServers(final CharSequence dnsServers) throws BadConfigException {
+ try {
+ for (final String dnsServer : Attribute.split(dnsServers)) {
+ try {
+ addDnsServer(InetAddresses.parse(dnsServer));
+ } catch (final ParseException e) {
+ if (e.getParsingClass() != InetAddress.class || !InetAddresses.isHostname(dnsServer))
+ throw e;
+ addDnsSearchDomain(dnsServer);
+ }
+ }
+ return this;
+ } catch (final ParseException e) {
+ throw new BadConfigException(Section.INTERFACE, Location.DNS, e);
+ }
+ }
+
+ public Builder parseExcludedApplications(final CharSequence apps) {
+ return excludeApplications(List.of(Attribute.split(apps)));
+ }
+
+ public Builder parseIncludedApplications(final CharSequence apps) {
+ return includeApplications(List.of(Attribute.split(apps)));
+ }
+
+ public Builder parseListenPort(final String listenPort) throws BadConfigException {
+ try {
+ return setListenPort(Integer.parseInt(listenPort));
+ } catch (final NumberFormatException e) {
+ throw new BadConfigException(Section.INTERFACE, Location.LISTEN_PORT, listenPort, e);
+ }
+ }
+
+ public Builder parseMtu(final String mtu) throws BadConfigException {
+ try {
+ return setMtu(Integer.parseInt(mtu));
+ } catch (final NumberFormatException e) {
+ throw new BadConfigException(Section.INTERFACE, Location.MTU, mtu, e);
+ }
+ }
+
+ public Builder parsePrivateKey(final String privateKey) throws BadConfigException {
+ try {
+ return setKeyPair(new KeyPair(Key.fromBase64(privateKey)));
+ } catch (final KeyFormatException e) {
+ throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY, e);
+ }
+ }
+
+ public Builder setKeyPair(final KeyPair keyPair) {
+ this.keyPair = keyPair;
+ return this;
+ }
+
+ public Builder setListenPort(final int listenPort) throws BadConfigException {
+ if (listenPort < MIN_UDP_PORT || listenPort > MAX_UDP_PORT)
+ throw new BadConfigException(Section.INTERFACE, Location.LISTEN_PORT,
+ Reason.INVALID_VALUE, String.valueOf(listenPort));
+ this.listenPort = listenPort == 0 ? Optional.empty() : Optional.of(listenPort);
+ return this;
+ }
+
+ public Builder setMtu(final int mtu) throws BadConfigException {
+ if (mtu < 0)
+ throw new BadConfigException(Section.INTERFACE, Location.LISTEN_PORT,
+ Reason.INVALID_VALUE, String.valueOf(mtu));
+ this.mtu = mtu == 0 ? Optional.empty() : Optional.of(mtu);
+ return this;
+ }
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/ParseException.java b/tunnel/src/main/java/com/wireguard/config/ParseException.java
new file mode 100644
index 00000000..e72ef3d7
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/ParseException.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import com.wireguard.util.NonNullForAll;
+
+import androidx.annotation.Nullable;
+
+/**
+ *
+ */
+@NonNullForAll
+public class ParseException extends Exception {
+ private final Class<?> parsingClass;
+ private final CharSequence text;
+
+ public ParseException(final Class<?> parsingClass, final CharSequence text,
+ @Nullable final String message, @Nullable final Throwable cause) {
+ super(message, cause);
+ this.parsingClass = parsingClass;
+ this.text = text;
+ }
+
+ public ParseException(final Class<?> parsingClass, final CharSequence text,
+ @Nullable final String message) {
+ this(parsingClass, text, message, null);
+ }
+
+ public ParseException(final Class<?> parsingClass, final CharSequence text,
+ @Nullable final Throwable cause) {
+ this(parsingClass, text, null, cause);
+ }
+
+ public ParseException(final Class<?> parsingClass, final CharSequence text) {
+ this(parsingClass, text, null, null);
+ }
+
+ public Class<?> getParsingClass() {
+ return parsingClass;
+ }
+
+ public CharSequence getText() {
+ return text;
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/Peer.java b/tunnel/src/main/java/com/wireguard/config/Peer.java
new file mode 100644
index 00000000..8a0fd763
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/Peer.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import com.wireguard.config.BadConfigException.Location;
+import com.wireguard.config.BadConfigException.Reason;
+import com.wireguard.config.BadConfigException.Section;
+import com.wireguard.crypto.Key;
+import com.wireguard.crypto.KeyFormatException;
+import com.wireguard.util.NonNullForAll;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Represents the configuration for a WireGuard peer (a [Peer] block). Peers must have a public key,
+ * and may optionally have several other attributes.
+ * <p>
+ * Instances of this class are immutable.
+ */
+@NonNullForAll
+public final class Peer {
+ private final Set<InetNetwork> allowedIps;
+ private final Optional<InetEndpoint> endpoint;
+ private final Optional<Integer> persistentKeepalive;
+ private final Optional<Key> preSharedKey;
+ private final Key publicKey;
+
+ private Peer(final Builder builder) {
+ // Defensively copy to ensure immutability even if the Builder is reused.
+ allowedIps = Collections.unmodifiableSet(new LinkedHashSet<>(builder.allowedIps));
+ endpoint = builder.endpoint;
+ persistentKeepalive = builder.persistentKeepalive;
+ preSharedKey = builder.preSharedKey;
+ publicKey = Objects.requireNonNull(builder.publicKey, "Peers must have a public key");
+ }
+
+ /**
+ * Parses an series of "KEY = VALUE" lines into a {@code Peer}. Throws {@link ParseException} if
+ * the input is not well-formed or contains unknown attributes.
+ *
+ * @param lines an iterable sequence of lines, containing at least a public key attribute
+ * @return a {@code Peer} with all of its attributes set from {@code lines}
+ */
+ public static Peer parse(final Iterable<? extends CharSequence> lines)
+ throws BadConfigException {
+ final Builder builder = new Builder();
+ for (final CharSequence line : lines) {
+ final Attribute attribute = Attribute.parse(line).orElseThrow(() ->
+ new BadConfigException(Section.PEER, Location.TOP_LEVEL,
+ Reason.SYNTAX_ERROR, line));
+ switch (attribute.getKey().toLowerCase(Locale.ENGLISH)) {
+ case "allowedips":
+ builder.parseAllowedIPs(attribute.getValue());
+ break;
+ case "endpoint":
+ builder.parseEndpoint(attribute.getValue());
+ break;
+ case "persistentkeepalive":
+ builder.parsePersistentKeepalive(attribute.getValue());
+ break;
+ case "presharedkey":
+ builder.parsePreSharedKey(attribute.getValue());
+ break;
+ case "publickey":
+ builder.parsePublicKey(attribute.getValue());
+ break;
+ default:
+ throw new BadConfigException(Section.PEER, Location.TOP_LEVEL,
+ Reason.UNKNOWN_ATTRIBUTE, attribute.getKey());
+ }
+ }
+ return builder.build();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof Peer))
+ return false;
+ final Peer other = (Peer) obj;
+ return allowedIps.equals(other.allowedIps)
+ && endpoint.equals(other.endpoint)
+ && persistentKeepalive.equals(other.persistentKeepalive)
+ && preSharedKey.equals(other.preSharedKey)
+ && publicKey.equals(other.publicKey);
+ }
+
+ /**
+ * Returns the peer's set of allowed IPs.
+ *
+ * @return the set of allowed IPs
+ */
+ public Set<InetNetwork> getAllowedIps() {
+ // The collection is already immutable.
+ return allowedIps;
+ }
+
+ /**
+ * Returns the peer's endpoint.
+ *
+ * @return the endpoint, or {@code Optional.empty()} if none is configured
+ */
+ public Optional<InetEndpoint> getEndpoint() {
+ return endpoint;
+ }
+
+ /**
+ * Returns the peer's persistent keepalive.
+ *
+ * @return the persistent keepalive, or {@code Optional.empty()} if none is configured
+ */
+ public Optional<Integer> getPersistentKeepalive() {
+ return persistentKeepalive;
+ }
+
+ /**
+ * Returns the peer's pre-shared key.
+ *
+ * @return the pre-shared key, or {@code Optional.empty()} if none is configured
+ */
+ public Optional<Key> getPreSharedKey() {
+ return preSharedKey;
+ }
+
+ /**
+ * Returns the peer's public key.
+ *
+ * @return the public key
+ */
+ public Key getPublicKey() {
+ return publicKey;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 1;
+ hash = 31 * hash + allowedIps.hashCode();
+ hash = 31 * hash + endpoint.hashCode();
+ hash = 31 * hash + persistentKeepalive.hashCode();
+ hash = 31 * hash + preSharedKey.hashCode();
+ hash = 31 * hash + publicKey.hashCode();
+ return hash;
+ }
+
+ /**
+ * Converts the {@code Peer} into a string suitable for debugging purposes. The {@code Peer} is
+ * identified by its public key and (if known) its endpoint.
+ *
+ * @return a concise single-line identifier for the {@code Peer}
+ */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("(Peer ");
+ sb.append(publicKey.toBase64());
+ endpoint.ifPresent(ep -> sb.append(" @").append(ep));
+ sb.append(')');
+ return sb.toString();
+ }
+
+ /**
+ * Converts the {@code Peer} into a string suitable for inclusion in a {@code wg-quick}
+ * configuration file.
+ *
+ * @return the {@code Peer} represented as a series of "Key = Value" lines
+ */
+ public String toWgQuickString() {
+ final StringBuilder sb = new StringBuilder();
+ if (!allowedIps.isEmpty())
+ sb.append("AllowedIPs = ").append(Attribute.join(allowedIps)).append('\n');
+ endpoint.ifPresent(ep -> sb.append("Endpoint = ").append(ep).append('\n'));
+ persistentKeepalive.ifPresent(pk -> sb.append("PersistentKeepalive = ").append(pk).append('\n'));
+ preSharedKey.ifPresent(psk -> sb.append("PreSharedKey = ").append(psk.toBase64()).append('\n'));
+ sb.append("PublicKey = ").append(publicKey.toBase64()).append('\n');
+ return sb.toString();
+ }
+
+ /**
+ * Serializes the {@code Peer} for use with the WireGuard cross-platform userspace API. Note
+ * that not all attributes are included in this representation.
+ *
+ * @return the {@code Peer} represented as a series of "key=value" lines
+ */
+ public String toWgUserspaceString() {
+ final StringBuilder sb = new StringBuilder();
+ // The order here is important: public_key signifies the beginning of a new peer.
+ sb.append("public_key=").append(publicKey.toHex()).append('\n');
+ for (final InetNetwork allowedIp : allowedIps)
+ sb.append("allowed_ip=").append(allowedIp).append('\n');
+ endpoint.flatMap(InetEndpoint::getResolved).ifPresent(ep -> sb.append("endpoint=").append(ep).append('\n'));
+ persistentKeepalive.ifPresent(pk -> sb.append("persistent_keepalive_interval=").append(pk).append('\n'));
+ preSharedKey.ifPresent(psk -> sb.append("preshared_key=").append(psk.toHex()).append('\n'));
+ return sb.toString();
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ public static final class Builder {
+ // See wg(8)
+ private static final int MAX_PERSISTENT_KEEPALIVE = 65535;
+
+ // Defaults to an empty set.
+ private final Set<InetNetwork> allowedIps = new LinkedHashSet<>();
+ // Defaults to not present.
+ private Optional<InetEndpoint> endpoint = Optional.empty();
+ // Defaults to not present.
+ private Optional<Integer> persistentKeepalive = Optional.empty();
+ // Defaults to not present.
+ private Optional<Key> preSharedKey = Optional.empty();
+ // No default; must be provided before building.
+ @Nullable private Key publicKey;
+
+ public Builder addAllowedIp(final InetNetwork allowedIp) {
+ allowedIps.add(allowedIp);
+ return this;
+ }
+
+ public Builder addAllowedIps(final Collection<InetNetwork> allowedIps) {
+ this.allowedIps.addAll(allowedIps);
+ return this;
+ }
+
+ public Peer build() throws BadConfigException {
+ if (publicKey == null)
+ throw new BadConfigException(Section.PEER, Location.PUBLIC_KEY,
+ Reason.MISSING_ATTRIBUTE, null);
+ return new Peer(this);
+ }
+
+ public Builder parseAllowedIPs(final CharSequence allowedIps) throws BadConfigException {
+ try {
+ for (final String allowedIp : Attribute.split(allowedIps))
+ addAllowedIp(InetNetwork.parse(allowedIp));
+ return this;
+ } catch (final ParseException e) {
+ throw new BadConfigException(Section.PEER, Location.ALLOWED_IPS, e);
+ }
+ }
+
+ public Builder parseEndpoint(final String endpoint) throws BadConfigException {
+ try {
+ return setEndpoint(InetEndpoint.parse(endpoint));
+ } catch (final ParseException e) {
+ throw new BadConfigException(Section.PEER, Location.ENDPOINT, e);
+ }
+ }
+
+ public Builder parsePersistentKeepalive(final String persistentKeepalive)
+ throws BadConfigException {
+ try {
+ return setPersistentKeepalive(Integer.parseInt(persistentKeepalive));
+ } catch (final NumberFormatException e) {
+ throw new BadConfigException(Section.PEER, Location.PERSISTENT_KEEPALIVE,
+ persistentKeepalive, e);
+ }
+ }
+
+ public Builder parsePreSharedKey(final String preSharedKey) throws BadConfigException {
+ try {
+ return setPreSharedKey(Key.fromBase64(preSharedKey));
+ } catch (final KeyFormatException e) {
+ throw new BadConfigException(Section.PEER, Location.PRE_SHARED_KEY, e);
+ }
+ }
+
+ public Builder parsePublicKey(final String publicKey) throws BadConfigException {
+ try {
+ return setPublicKey(Key.fromBase64(publicKey));
+ } catch (final KeyFormatException e) {
+ throw new BadConfigException(Section.PEER, Location.PUBLIC_KEY, e);
+ }
+ }
+
+ public Builder setEndpoint(final InetEndpoint endpoint) {
+ this.endpoint = Optional.of(endpoint);
+ return this;
+ }
+
+ public Builder setPersistentKeepalive(final int persistentKeepalive)
+ throws BadConfigException {
+ if (persistentKeepalive < 0 || persistentKeepalive > MAX_PERSISTENT_KEEPALIVE)
+ throw new BadConfigException(Section.PEER, Location.PERSISTENT_KEEPALIVE,
+ Reason.INVALID_VALUE, String.valueOf(persistentKeepalive));
+ this.persistentKeepalive = persistentKeepalive == 0 ?
+ Optional.empty() : Optional.of(persistentKeepalive);
+ return this;
+ }
+
+ public Builder setPreSharedKey(final Key preSharedKey) {
+ this.preSharedKey = Optional.of(preSharedKey);
+ return this;
+ }
+
+ public Builder setPublicKey(final Key publicKey) {
+ this.publicKey = publicKey;
+ return this;
+ }
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/crypto/Curve25519.java b/tunnel/src/main/java/com/wireguard/crypto/Curve25519.java
new file mode 100644
index 00000000..aa66d1ca
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/crypto/Curve25519.java
@@ -0,0 +1,500 @@
+/*
+ * Copyright © 2016 Southern Storm Software, Pty Ltd.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.crypto;
+
+import com.wireguard.util.NonNullForAll;
+
+import java.util.Arrays;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Implementation of Curve25519 ECDH.
+ * <p>
+ * This implementation was imported to WireGuard from noise-java:
+ * https://github.com/rweather/noise-java
+ * <p>
+ * This implementation is based on that from arduinolibs:
+ * https://github.com/rweather/arduinolibs
+ * <p>
+ * Differences in this version are due to using 26-bit limbs for the
+ * representation instead of the 8/16/32-bit limbs in the original.
+ * <p>
+ * References: http://cr.yp.to/ecdh.html, RFC 7748
+ */
+@SuppressWarnings({"MagicNumber", "NonConstantFieldWithUpperCaseName", "SuspiciousNameCombination"})
+@NonNullForAll
+public final class Curve25519 {
+ // Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.
+ private static final int NUM_LIMBS_255BIT = 10;
+ private static final int NUM_LIMBS_510BIT = 20;
+
+ private final int[] A;
+ private final int[] AA;
+ private final int[] B;
+ private final int[] BB;
+ private final int[] C;
+ private final int[] CB;
+ private final int[] D;
+ private final int[] DA;
+ private final int[] E;
+ private final long[] t1;
+ private final int[] t2;
+ private final int[] x_1;
+ private final int[] x_2;
+ private final int[] x_3;
+ private final int[] z_2;
+ private final int[] z_3;
+
+ /**
+ * Constructs the temporary state holder for Curve25519 evaluation.
+ */
+ private Curve25519() {
+ // Allocate memory for all of the temporary variables we will need.
+ x_1 = new int[NUM_LIMBS_255BIT];
+ x_2 = new int[NUM_LIMBS_255BIT];
+ x_3 = new int[NUM_LIMBS_255BIT];
+ z_2 = new int[NUM_LIMBS_255BIT];
+ z_3 = new int[NUM_LIMBS_255BIT];
+ A = new int[NUM_LIMBS_255BIT];
+ B = new int[NUM_LIMBS_255BIT];
+ C = new int[NUM_LIMBS_255BIT];
+ D = new int[NUM_LIMBS_255BIT];
+ E = new int[NUM_LIMBS_255BIT];
+ AA = new int[NUM_LIMBS_255BIT];
+ BB = new int[NUM_LIMBS_255BIT];
+ DA = new int[NUM_LIMBS_255BIT];
+ CB = new int[NUM_LIMBS_255BIT];
+ t1 = new long[NUM_LIMBS_510BIT];
+ t2 = new int[NUM_LIMBS_510BIT];
+ }
+
+ /**
+ * Conditional swap of two values.
+ *
+ * @param select Set to 1 to swap, 0 to leave as-is.
+ * @param x The first value.
+ * @param y The second value.
+ */
+ private static void cswap(int select, final int[] x, final int[] y) {
+ select = -select;
+ for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
+ final int dummy = select & (x[index] ^ y[index]);
+ x[index] ^= dummy;
+ y[index] ^= dummy;
+ }
+ }
+
+ /**
+ * Evaluates the Curve25519 curve.
+ *
+ * @param result Buffer to place the result of the evaluation into.
+ * @param offset Offset into the result buffer.
+ * @param privateKey The private key to use in the evaluation.
+ * @param publicKey The public key to use in the evaluation, or null
+ * if the base point of the curve should be used.
+ */
+ public static void eval(final byte[] result, final int offset,
+ final byte[] privateKey, @Nullable final byte[] publicKey) {
+ final Curve25519 state = new Curve25519();
+ try {
+ // Unpack the public key value. If null, use 9 as the base point.
+ Arrays.fill(state.x_1, 0);
+ if (publicKey != null) {
+ // Convert the input value from little-endian into 26-bit limbs.
+ for (int index = 0; index < 32; ++index) {
+ final int bit = (index * 8) % 26;
+ final int word = (index * 8) / 26;
+ final int value = publicKey[index] & 0xFF;
+ if (bit <= (26 - 8)) {
+ state.x_1[word] |= value << bit;
+ } else {
+ state.x_1[word] |= value << bit;
+ state.x_1[word] &= 0x03FFFFFF;
+ state.x_1[word + 1] |= value >> (26 - bit);
+ }
+ }
+
+ // Just in case, we reduce the number modulo 2^255 - 19 to
+ // make sure that it is in range of the field before we start.
+ // This eliminates values between 2^255 - 19 and 2^256 - 1.
+ state.reduceQuick(state.x_1);
+ state.reduceQuick(state.x_1);
+ } else {
+ state.x_1[0] = 9;
+ }
+
+ // Initialize the other temporary variables.
+ Arrays.fill(state.x_2, 0); // x_2 = 1
+ state.x_2[0] = 1;
+ Arrays.fill(state.z_2, 0); // z_2 = 0
+ System.arraycopy(state.x_1, 0, state.x_3, 0, state.x_1.length); // x_3 = x_1
+ Arrays.fill(state.z_3, 0); // z_3 = 1
+ state.z_3[0] = 1;
+
+ // Evaluate the curve for every bit of the private key.
+ state.evalCurve(privateKey);
+
+ // Compute x_2 * (z_2 ^ (p - 2)) where p = 2^255 - 19.
+ state.recip(state.z_3, state.z_2);
+ state.mul(state.x_2, state.x_2, state.z_3);
+
+ // Convert x_2 into little-endian in the result buffer.
+ for (int index = 0; index < 32; ++index) {
+ final int bit = (index * 8) % 26;
+ final int word = (index * 8) / 26;
+ if (bit <= (26 - 8))
+ result[offset + index] = (byte) (state.x_2[word] >> bit);
+ else
+ result[offset + index] = (byte) ((state.x_2[word] >> bit) | (state.x_2[word + 1] << (26 - bit)));
+ }
+ } finally {
+ // Clean up all temporary state before we exit.
+ state.destroy();
+ }
+ }
+
+ /**
+ * Subtracts two numbers modulo 2^255 - 19.
+ *
+ * @param result The result.
+ * @param x The first number to subtract.
+ * @param y The second number to subtract.
+ */
+ private static void sub(final int[] result, final int[] x, final int[] y) {
+ int index;
+ int borrow;
+
+ // Subtract y from x to generate the intermediate result.
+ borrow = 0;
+ for (index = 0; index < NUM_LIMBS_255BIT; ++index) {
+ borrow = x[index] - y[index] - ((borrow >> 26) & 0x01);
+ result[index] = borrow & 0x03FFFFFF;
+ }
+
+ // If we had a borrow, then the result has gone negative and we
+ // have to add 2^255 - 19 to the result to make it positive again.
+ // The top bits of "borrow" will be all 1's if there is a borrow
+ // or it will be all 0's if there was no borrow. Easiest is to
+ // conditionally subtract 19 and then mask off the high bits.
+ borrow = result[0] - ((-((borrow >> 26) & 0x01)) & 19);
+ result[0] = borrow & 0x03FFFFFF;
+ for (index = 1; index < NUM_LIMBS_255BIT; ++index) {
+ borrow = result[index] - ((borrow >> 26) & 0x01);
+ result[index] = borrow & 0x03FFFFFF;
+ }
+ result[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
+ }
+
+ /**
+ * Adds two numbers modulo 2^255 - 19.
+ *
+ * @param result The result.
+ * @param x The first number to add.
+ * @param y The second number to add.
+ */
+ private void add(final int[] result, final int[] x, final int[] y) {
+ int carry = x[0] + y[0];
+ result[0] = carry & 0x03FFFFFF;
+ for (int index = 1; index < NUM_LIMBS_255BIT; ++index) {
+ carry = (carry >> 26) + x[index] + y[index];
+ result[index] = carry & 0x03FFFFFF;
+ }
+ reduceQuick(result);
+ }
+
+ /**
+ * Destroy all sensitive data in this object.
+ */
+ private void destroy() {
+ // Destroy all temporary variables.
+ Arrays.fill(x_1, 0);
+ Arrays.fill(x_2, 0);
+ Arrays.fill(x_3, 0);
+ Arrays.fill(z_2, 0);
+ Arrays.fill(z_3, 0);
+ Arrays.fill(A, 0);
+ Arrays.fill(B, 0);
+ Arrays.fill(C, 0);
+ Arrays.fill(D, 0);
+ Arrays.fill(E, 0);
+ Arrays.fill(AA, 0);
+ Arrays.fill(BB, 0);
+ Arrays.fill(DA, 0);
+ Arrays.fill(CB, 0);
+ Arrays.fill(t1, 0L);
+ Arrays.fill(t2, 0);
+ }
+
+ /**
+ * Evaluates the curve for every bit in a secret key.
+ *
+ * @param s The 32-byte secret key.
+ */
+ private void evalCurve(final byte[] s) {
+ int sposn = 31;
+ int sbit = 6;
+ int svalue = s[sposn] | 0x40;
+ int swap = 0;
+
+ // Iterate over all 255 bits of "s" from the highest to the lowest.
+ // We ignore the high bit of the 256-bit representation of "s".
+ while (true) {
+ // Conditional swaps on entry to this bit but only if we
+ // didn't swap on the previous bit.
+ final int select = (svalue >> sbit) & 0x01;
+ swap ^= select;
+ cswap(swap, x_2, x_3);
+ cswap(swap, z_2, z_3);
+ swap = select;
+
+ // Evaluate the curve.
+ add(A, x_2, z_2); // A = x_2 + z_2
+ square(AA, A); // AA = A^2
+ sub(B, x_2, z_2); // B = x_2 - z_2
+ square(BB, B); // BB = B^2
+ sub(E, AA, BB); // E = AA - BB
+ add(C, x_3, z_3); // C = x_3 + z_3
+ sub(D, x_3, z_3); // D = x_3 - z_3
+ mul(DA, D, A); // DA = D * A
+ mul(CB, C, B); // CB = C * B
+ add(x_3, DA, CB); // x_3 = (DA + CB)^2
+ square(x_3, x_3);
+ sub(z_3, DA, CB); // z_3 = x_1 * (DA - CB)^2
+ square(z_3, z_3);
+ mul(z_3, z_3, x_1);
+ mul(x_2, AA, BB); // x_2 = AA * BB
+ mulA24(z_2, E); // z_2 = E * (AA + a24 * E)
+ add(z_2, z_2, AA);
+ mul(z_2, z_2, E);
+
+ // Move onto the next lower bit of "s".
+ if (sbit > 0) {
+ --sbit;
+ } else if (sposn == 0) {
+ break;
+ } else if (sposn == 1) {
+ --sposn;
+ svalue = s[sposn] & 0xF8;
+ sbit = 7;
+ } else {
+ --sposn;
+ svalue = s[sposn];
+ sbit = 7;
+ }
+ }
+
+ // Final conditional swaps.
+ cswap(swap, x_2, x_3);
+ cswap(swap, z_2, z_3);
+ }
+
+ /**
+ * Multiplies two numbers modulo 2^255 - 19.
+ *
+ * @param result The result.
+ * @param x The first number to multiply.
+ * @param y The second number to multiply.
+ */
+ private void mul(final int[] result, final int[] x, final int[] y) {
+ // Multiply the two numbers to create the intermediate result.
+ long v = x[0];
+ for (int i = 0; i < NUM_LIMBS_255BIT; ++i) {
+ t1[i] = v * y[i];
+ }
+ for (int i = 1; i < NUM_LIMBS_255BIT; ++i) {
+ v = x[i];
+ for (int j = 0; j < (NUM_LIMBS_255BIT - 1); ++j) {
+ t1[i + j] += v * y[j];
+ }
+ t1[i + NUM_LIMBS_255BIT - 1] = v * y[NUM_LIMBS_255BIT - 1];
+ }
+
+ // Propagate carries and convert back into 26-bit words.
+ v = t1[0];
+ t2[0] = ((int) v) & 0x03FFFFFF;
+ for (int i = 1; i < NUM_LIMBS_510BIT; ++i) {
+ v = (v >> 26) + t1[i];
+ t2[i] = ((int) v) & 0x03FFFFFF;
+ }
+
+ // Reduce the result modulo 2^255 - 19.
+ reduce(result, t2, NUM_LIMBS_255BIT);
+ }
+
+ /**
+ * Multiplies a number by the a24 constant, modulo 2^255 - 19.
+ *
+ * @param result The result.
+ * @param x The number to multiply by a24.
+ */
+ private void mulA24(final int[] result, final int[] x) {
+ final long a24 = 121665;
+ long carry = 0;
+ for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
+ carry += a24 * x[index];
+ t2[index] = ((int) carry) & 0x03FFFFFF;
+ carry >>= 26;
+ }
+ t2[NUM_LIMBS_255BIT] = ((int) carry) & 0x03FFFFFF;
+ reduce(result, t2, 1);
+ }
+
+ /**
+ * Raise x to the power of (2^250 - 1).
+ *
+ * @param result The result. Must not overlap with x.
+ * @param x The argument.
+ */
+ private void pow250(final int[] result, final int[] x) {
+ // The big-endian hexadecimal expansion of (2^250 - 1) is:
+ // 03FFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF
+ //
+ // The naive implementation needs to do 2 multiplications per 1 bit and
+ // 1 multiplication per 0 bit. We can improve upon this by creating a
+ // pattern 0000000001 ... 0000000001. If we square and multiply the
+ // pattern by itself we can turn the pattern into the partial results
+ // 0000000011 ... 0000000011, 0000000111 ... 0000000111, etc.
+ // This averages out to about 1.1 multiplications per 1 bit instead of 2.
+
+ // Build a pattern of 250 bits in length of repeated copies of 0000000001.
+ square(A, x);
+ for (int j = 0; j < 9; ++j)
+ square(A, A);
+ mul(result, A, x);
+ for (int i = 0; i < 23; ++i) {
+ for (int j = 0; j < 10; ++j)
+ square(A, A);
+ mul(result, result, A);
+ }
+
+ // Multiply bit-shifted versions of the 0000000001 pattern into
+ // the result to "fill in" the gaps in the pattern.
+ square(A, result);
+ mul(result, result, A);
+ for (int j = 0; j < 8; ++j) {
+ square(A, A);
+ mul(result, result, A);
+ }
+ }
+
+ /**
+ * Computes the reciprocal of a number modulo 2^255 - 19.
+ *
+ * @param result The result. Must not overlap with x.
+ * @param x The argument.
+ */
+ private void recip(final int[] result, final int[] x) {
+ // The reciprocal is the same as x ^ (p - 2) where p = 2^255 - 19.
+ // The big-endian hexadecimal expansion of (p - 2) is:
+ // 7FFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFEB
+ // Start with the 250 upper bits of the expansion of (p - 2).
+ pow250(result, x);
+
+ // Deal with the 5 lowest bits of (p - 2), 01011, from highest to lowest.
+ square(result, result);
+ square(result, result);
+ mul(result, result, x);
+ square(result, result);
+ square(result, result);
+ mul(result, result, x);
+ square(result, result);
+ mul(result, result, x);
+ }
+
+ /**
+ * Reduce a number modulo 2^255 - 19.
+ *
+ * @param result The result.
+ * @param x The value to be reduced. This array will be
+ * modified during the reduction.
+ * @param size The number of limbs in the high order half of x.
+ */
+ private void reduce(final int[] result, final int[] x, final int size) {
+ // Calculate (x mod 2^255) + ((x / 2^255) * 19) which will
+ // either produce the answer we want or it will produce a
+ // value of the form "answer + j * (2^255 - 19)". There are
+ // 5 left-over bits in the top-most limb of the bottom half.
+ int carry = 0;
+ int limb = x[NUM_LIMBS_255BIT - 1] >> 21;
+ x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
+ for (int index = 0; index < size; ++index) {
+ limb += x[NUM_LIMBS_255BIT + index] << 5;
+ carry += (limb & 0x03FFFFFF) * 19 + x[index];
+ x[index] = carry & 0x03FFFFFF;
+ limb >>= 26;
+ carry >>= 26;
+ }
+ if (size < NUM_LIMBS_255BIT) {
+ // The high order half of the number is short; e.g. for mulA24().
+ // Propagate the carry through the rest of the low order part.
+ for (int index = size; index < NUM_LIMBS_255BIT; ++index) {
+ carry += x[index];
+ x[index] = carry & 0x03FFFFFF;
+ carry >>= 26;
+ }
+ }
+
+ // The "j" value may still be too large due to the final carry-out.
+ // We must repeat the reduction. If we already have the answer,
+ // then this won't do any harm but we must still do the calculation
+ // to preserve the overall timing. The "j" value will be between
+ // 0 and 19, which means that the carry we care about is in the
+ // top 5 bits of the highest limb of the bottom half.
+ carry = (x[NUM_LIMBS_255BIT - 1] >> 21) * 19;
+ x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
+ for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
+ carry += x[index];
+ result[index] = carry & 0x03FFFFFF;
+ carry >>= 26;
+ }
+
+ // At this point "x" will either be the answer or it will be the
+ // answer plus (2^255 - 19). Perform a trial subtraction to
+ // complete the reduction process.
+ reduceQuick(result);
+ }
+
+ /**
+ * Reduces a number modulo 2^255 - 19 where it is known that the
+ * number can be reduced with only 1 trial subtraction.
+ *
+ * @param x The number to reduce, and the result.
+ */
+ private void reduceQuick(final int[] x) {
+ // Perform a trial subtraction of (2^255 - 19) from "x" which is
+ // equivalent to adding 19 and subtracting 2^255. We add 19 here;
+ // the subtraction of 2^255 occurs in the next step.
+ int carry = 19;
+ for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
+ carry += x[index];
+ t2[index] = carry & 0x03FFFFFF;
+ carry >>= 26;
+ }
+
+ // If there was a borrow, then the original "x" is the correct answer.
+ // If there was no borrow, then "t2" is the correct answer. Select the
+ // correct answer but do it in a way that instruction timing will not
+ // reveal which value was selected. Borrow will occur if bit 21 of
+ // "t2" is zero. Turn the bit into a selection mask.
+ final int mask = -((t2[NUM_LIMBS_255BIT - 1] >> 21) & 0x01);
+ final int nmask = ~mask;
+ t2[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
+ for (int index = 0; index < NUM_LIMBS_255BIT; ++index)
+ x[index] = (x[index] & nmask) | (t2[index] & mask);
+ }
+
+ /**
+ * Squares a number modulo 2^255 - 19.
+ *
+ * @param result The result.
+ * @param x The number to square.
+ */
+ private void square(final int[] result, final int[] x) {
+ mul(result, x, x);
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/crypto/Key.java b/tunnel/src/main/java/com/wireguard/crypto/Key.java
new file mode 100644
index 00000000..5fa93a46
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/crypto/Key.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.crypto;
+
+import com.wireguard.crypto.KeyFormatException.Type;
+import com.wireguard.util.NonNullForAll;
+
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+/**
+ * Represents a WireGuard public or private key. This class uses specialized constant-time base64
+ * and hexadecimal codec implementations that resist side-channel attacks.
+ * <p>
+ * Instances of this class are immutable.
+ */
+@SuppressWarnings("MagicNumber")
+@NonNullForAll
+public final class Key {
+ private final byte[] key;
+
+ /**
+ * Constructs an object encapsulating the supplied key.
+ *
+ * @param key an array of bytes containing a binary key. Callers of this constructor are
+ * responsible for ensuring that the array is of the correct length.
+ */
+ private Key(final byte[] key) {
+ // Defensively copy to ensure immutability.
+ this.key = Arrays.copyOf(key, key.length);
+ }
+
+ /**
+ * Decodes a single 4-character base64 chunk to an integer in constant time.
+ *
+ * @param src an array of at least 4 characters in base64 format
+ * @param srcOffset the offset of the beginning of the chunk in {@code src}
+ * @return the decoded 3-byte integer, or some arbitrary integer value if the input was not
+ * valid base64
+ */
+ private static int decodeBase64(final char[] src, final int srcOffset) {
+ int val = 0;
+ for (int i = 0; i < 4; ++i) {
+ final char c = src[i + srcOffset];
+ val |= (-1
+ + ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64))
+ + ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70))
+ + ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5))
+ + ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63)
+ + ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64)
+ ) << (18 - 6 * i);
+ }
+ return val;
+ }
+
+ /**
+ * Encodes a single 4-character base64 chunk from 3 consecutive bytes in constant time.
+ *
+ * @param src an array of at least 3 bytes
+ * @param srcOffset the offset of the beginning of the chunk in {@code src}
+ * @param dest an array of at least 4 characters
+ * @param destOffset the offset of the beginning of the chunk in {@code dest}
+ */
+ private static void encodeBase64(final byte[] src, final int srcOffset,
+ final char[] dest, final int destOffset) {
+ final byte[] input = {
+ (byte) ((src[srcOffset] >>> 2) & 63),
+ (byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63),
+ (byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63),
+ (byte) ((src[2 + srcOffset]) & 63),
+ };
+ for (int i = 0; i < 4; ++i) {
+ dest[i + destOffset] = (char) (input[i] + 'A'
+ + (((25 - input[i]) >>> 8) & 6)
+ - (((51 - input[i]) >>> 8) & 75)
+ - (((61 - input[i]) >>> 8) & 15)
+ + (((62 - input[i]) >>> 8) & 3));
+ }
+ }
+
+ /**
+ * Decodes a WireGuard public or private key from its base64 string representation. This
+ * function throws a {@link KeyFormatException} if the source string is not well-formed.
+ *
+ * @param str the base64 string representation of a WireGuard key
+ * @return the decoded key encapsulated in an immutable container
+ */
+ public static Key fromBase64(final String str) throws KeyFormatException {
+ final char[] input = str.toCharArray();
+ if (input.length != Format.BASE64.length || input[Format.BASE64.length - 1] != '=')
+ throw new KeyFormatException(Format.BASE64, Type.LENGTH);
+ final byte[] key = new byte[Format.BINARY.length];
+ int i;
+ int ret = 0;
+ for (i = 0; i < key.length / 3; ++i) {
+ final int val = decodeBase64(input, i * 4);
+ ret |= val >>> 31;
+ key[i * 3] = (byte) ((val >>> 16) & 0xff);
+ key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
+ key[i * 3 + 2] = (byte) (val & 0xff);
+ }
+ final char[] endSegment = {
+ input[i * 4],
+ input[i * 4 + 1],
+ input[i * 4 + 2],
+ 'A',
+ };
+ final int val = decodeBase64(endSegment, 0);
+ ret |= (val >>> 31) | (val & 0xff);
+ key[i * 3] = (byte) ((val >>> 16) & 0xff);
+ key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
+
+ if (ret != 0)
+ throw new KeyFormatException(Format.BASE64, Type.CONTENTS);
+ return new Key(key);
+ }
+
+ /**
+ * Wraps a WireGuard public or private key in an immutable container. This function throws a
+ * {@link KeyFormatException} if the source data is not the correct length.
+ *
+ * @param bytes an array of bytes containing a WireGuard key in binary format
+ * @return the key encapsulated in an immutable container
+ */
+ public static Key fromBytes(final byte[] bytes) throws KeyFormatException {
+ if (bytes.length != Format.BINARY.length)
+ throw new KeyFormatException(Format.BINARY, Type.LENGTH);
+ return new Key(bytes);
+ }
+
+ /**
+ * Decodes a WireGuard public or private key from its hexadecimal string representation. This
+ * function throws a {@link KeyFormatException} if the source string is not well-formed.
+ *
+ * @param str the hexadecimal string representation of a WireGuard key
+ * @return the decoded key encapsulated in an immutable container
+ */
+ public static Key fromHex(final String str) throws KeyFormatException {
+ final char[] input = str.toCharArray();
+ if (input.length != Format.HEX.length)
+ throw new KeyFormatException(Format.HEX, Type.LENGTH);
+ final byte[] key = new byte[Format.BINARY.length];
+ int ret = 0;
+ for (int i = 0; i < key.length; ++i) {
+ int c;
+ int cNum;
+ int cNum0;
+ int cAlpha;
+ int cAlpha0;
+ int cVal;
+ final int cAcc;
+
+ c = input[i * 2];
+ cNum = c ^ 48;
+ cNum0 = ((cNum - 10) >>> 8) & 0xff;
+ cAlpha = (c & ~32) - 55;
+ cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
+ ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
+ cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
+ cAcc = cVal * 16;
+
+ c = input[i * 2 + 1];
+ cNum = c ^ 48;
+ cNum0 = ((cNum - 10) >>> 8) & 0xff;
+ cAlpha = (c & ~32) - 55;
+ cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
+ ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
+ cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
+ key[i] = (byte) (cAcc | cVal);
+ }
+ if (ret != 0)
+ throw new KeyFormatException(Format.HEX, Type.CONTENTS);
+ return new Key(key);
+ }
+
+ /**
+ * Generates a private key using the system's {@link SecureRandom} number generator.
+ *
+ * @return a well-formed random private key
+ */
+ static Key generatePrivateKey() {
+ final SecureRandom secureRandom = new SecureRandom();
+ final byte[] privateKey = new byte[Format.BINARY.getLength()];
+ secureRandom.nextBytes(privateKey);
+ privateKey[0] &= 248;
+ privateKey[31] &= 127;
+ privateKey[31] |= 64;
+ return new Key(privateKey);
+ }
+
+ /**
+ * Generates a public key from an existing private key.
+ *
+ * @param privateKey a private key
+ * @return a well-formed public key that corresponds to the supplied private key
+ */
+ static Key generatePublicKey(final Key privateKey) {
+ final byte[] publicKey = new byte[Format.BINARY.getLength()];
+ Curve25519.eval(publicKey, 0, privateKey.getBytes(), null);
+ return new Key(publicKey);
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == this)
+ return true;
+ if (obj == null || obj.getClass() != getClass())
+ return false;
+ final Key other = (Key) obj;
+ return MessageDigest.isEqual(key, other.key);
+ }
+
+ /**
+ * Returns the key as an array of bytes.
+ *
+ * @return an array of bytes containing the raw binary key
+ */
+ public byte[] getBytes() {
+ // Defensively copy to ensure immutability.
+ return Arrays.copyOf(key, key.length);
+ }
+
+ @Override
+ public int hashCode() {
+ int ret = 0;
+ for (int i = 0; i < key.length / 4; ++i)
+ ret ^= (key[i * 4 + 0] >> 0) + (key[i * 4 + 1] >> 8) + (key[i * 4 + 2] >> 16) + (key[i * 4 + 3] >> 24);
+ return ret;
+ }
+
+ /**
+ * Encodes the key to base64.
+ *
+ * @return a string containing the encoded key
+ */
+ public String toBase64() {
+ final char[] output = new char[Format.BASE64.length];
+ int i;
+ for (i = 0; i < key.length / 3; ++i)
+ encodeBase64(key, i * 3, output, i * 4);
+ final byte[] endSegment = {
+ key[i * 3],
+ key[i * 3 + 1],
+ 0,
+ };
+ encodeBase64(endSegment, 0, output, i * 4);
+ output[Format.BASE64.length - 1] = '=';
+ return new String(output);
+ }
+
+ /**
+ * Encodes the key to hexadecimal ASCII characters.
+ *
+ * @return a string containing the encoded key
+ */
+ public String toHex() {
+ final char[] output = new char[Format.HEX.length];
+ for (int i = 0; i < key.length; ++i) {
+ output[i * 2] = (char) (87 + (key[i] >> 4 & 0xf)
+ + ((((key[i] >> 4 & 0xf) - 10) >> 8) & ~38));
+ output[i * 2 + 1] = (char) (87 + (key[i] & 0xf)
+ + ((((key[i] & 0xf) - 10) >> 8) & ~38));
+ }
+ return new String(output);
+ }
+
+ /**
+ * The supported formats for encoding a WireGuard key.
+ */
+ public enum Format {
+ BASE64(44),
+ BINARY(32),
+ HEX(64);
+
+ private final int length;
+
+ Format(final int length) {
+ this.length = length;
+ }
+
+ public int getLength() {
+ return length;
+ }
+ }
+
+}
diff --git a/tunnel/src/main/java/com/wireguard/crypto/KeyFormatException.java b/tunnel/src/main/java/com/wireguard/crypto/KeyFormatException.java
new file mode 100644
index 00000000..c64b3dc8
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/crypto/KeyFormatException.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.crypto;
+
+import com.wireguard.util.NonNullForAll;
+
+/**
+ * An exception thrown when attempting to parse an invalid key (too short, too long, or byte
+ * data inappropriate for the format). The format being parsed can be accessed with the
+ * {@link #getFormat} method.
+ */
+@NonNullForAll
+public final class KeyFormatException extends Exception {
+ private final Key.Format format;
+ private final Type type;
+
+ KeyFormatException(final Key.Format format, final Type type) {
+ this.format = format;
+ this.type = type;
+ }
+
+ public Key.Format getFormat() {
+ return format;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public enum Type {
+ CONTENTS,
+ LENGTH
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/crypto/KeyPair.java b/tunnel/src/main/java/com/wireguard/crypto/KeyPair.java
new file mode 100644
index 00000000..7a564689
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/crypto/KeyPair.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.crypto;
+
+import com.wireguard.util.NonNullForAll;
+
+/**
+ * Represents a Curve25519 key pair as used by WireGuard.
+ * <p>
+ * Instances of this class are immutable.
+ */
+@NonNullForAll
+public class KeyPair {
+ private final Key privateKey;
+ private final Key publicKey;
+
+ /**
+ * Creates a key pair using a newly-generated private key.
+ */
+ public KeyPair() {
+ this(Key.generatePrivateKey());
+ }
+
+ /**
+ * Creates a key pair using an existing private key.
+ *
+ * @param privateKey a private key, used to derive the public key
+ */
+ public KeyPair(final Key privateKey) {
+ this.privateKey = privateKey;
+ publicKey = Key.generatePublicKey(privateKey);
+ }
+
+ /**
+ * Returns the private key from the key pair.
+ *
+ * @return the private key
+ */
+ public Key getPrivateKey() {
+ return privateKey;
+ }
+
+ /**
+ * Returns the public key from the key pair.
+ *
+ * @return the public key
+ */
+ public Key getPublicKey() {
+ return publicKey;
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/util/NonNullForAll.java b/tunnel/src/main/java/com/wireguard/util/NonNullForAll.java
new file mode 100644
index 00000000..1c395621
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/util/NonNullForAll.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.annotation.Nonnull;
+import javax.annotation.meta.TypeQualifierDefault;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * This annotation can be applied to a package, class or method to indicate that all
+ * class fields and method parameters and return values in that element are nonnull
+ * by default unless overridden.
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+@Nonnull
+@TypeQualifierDefault({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+
+public @interface NonNullForAll {
+}
diff --git a/tunnel/src/test/java/com/wireguard/config/BadConfigExceptionTest.java b/tunnel/src/test/java/com/wireguard/config/BadConfigExceptionTest.java
new file mode 100644
index 00000000..a24e049e
--- /dev/null
+++ b/tunnel/src/test/java/com/wireguard/config/BadConfigExceptionTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import com.wireguard.config.BadConfigException.Location;
+import com.wireguard.config.BadConfigException.Reason;
+import com.wireguard.config.BadConfigException.Section;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class BadConfigExceptionTest {
+ private static final Map<String, InputStream> CONFIG_MAP = new HashMap<>();
+ private static final String[] CONFIG_NAMES = {
+ "invalid-key",
+ "invalid-number",
+ "invalid-value",
+ "missing-attribute",
+ "missing-section",
+ "syntax-error",
+ "unknown-attribute",
+ "unknown-section"
+ };
+
+ @AfterClass
+ public static void closeStreams() {
+ for (final InputStream inputStream : CONFIG_MAP.values()) {
+ try {
+ inputStream.close();
+ } catch (final IOException ignored) {
+ }
+ }
+ }
+
+ @BeforeClass
+ public static void readConfigs() {
+ for (final String config : CONFIG_NAMES) {
+ CONFIG_MAP.put(config, BadConfigExceptionTest.class.getClassLoader().getResourceAsStream(config + ".conf"));
+ }
+ }
+
+ @Test
+ public void throws_correctly_with_INVALID_KEY_reason() {
+ try {
+ Config.parse(CONFIG_MAP.get("invalid-key"));
+ fail("Config parsing must fail in this test");
+ } catch (final BadConfigException e) {
+ assertEquals(e.getReason(), Reason.INVALID_KEY);
+ assertEquals(e.getLocation(), Location.PUBLIC_KEY);
+ assertEquals(e.getSection(), Section.PEER);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ fail("IOException thrown during test");
+ }
+ }
+
+ @Test
+ public void throws_correctly_with_INVALID_NUMBER_reason() {
+ try {
+ Config.parse(CONFIG_MAP.get("invalid-number"));
+ fail("Config parsing must fail in this test");
+ } catch (final BadConfigException e) {
+ assertEquals(e.getReason(), Reason.INVALID_NUMBER);
+ assertEquals(e.getLocation(), Location.PERSISTENT_KEEPALIVE);
+ assertEquals(e.getSection(), Section.PEER);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ fail("IOException thrown during test");
+ }
+ }
+
+ @Test
+ public void throws_correctly_with_INVALID_VALUE_reason() {
+ try {
+ Config.parse(CONFIG_MAP.get("invalid-value"));
+ fail("Config parsing must fail in this test");
+ } catch (final BadConfigException e) {
+ assertEquals(e.getReason(), Reason.INVALID_VALUE);
+ assertEquals(e.getLocation(), Location.DNS);
+ assertEquals(e.getSection(), Section.INTERFACE);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ fail("IOException throwing during test");
+ }
+ }
+
+ @Test
+ public void throws_correctly_with_MISSING_ATTRIBUTE_reason() {
+ try {
+ Config.parse(CONFIG_MAP.get("missing-attribute"));
+ fail("Config parsing must fail in this test");
+ } catch (final BadConfigException e) {
+ assertEquals(e.getReason(), Reason.MISSING_ATTRIBUTE);
+ assertEquals(e.getLocation(), Location.PUBLIC_KEY);
+ assertEquals(e.getSection(), Section.PEER);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ fail("IOException throwing during test");
+ }
+ }
+
+ @Test
+ public void throws_correctly_with_MISSING_SECTION_reason() {
+ try {
+ Config.parse(CONFIG_MAP.get("missing-section"));
+ fail("Config parsing must fail in this test");
+ } catch (final BadConfigException e) {
+ assertEquals(e.getReason(), Reason.MISSING_SECTION);
+ assertEquals(e.getLocation(), Location.TOP_LEVEL);
+ assertEquals(e.getSection(), Section.CONFIG);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ fail("IOException throwing during test");
+ }
+ }
+
+ @Test
+ public void throws_correctly_with_SYNTAX_ERROR_reason() {
+ try {
+ Config.parse(CONFIG_MAP.get("syntax-error"));
+ fail("Config parsing must fail in this test");
+ } catch (final BadConfigException e) {
+ assertEquals(e.getReason(), Reason.SYNTAX_ERROR);
+ assertEquals(e.getLocation(), Location.TOP_LEVEL);
+ assertEquals(e.getSection(), Section.PEER);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ fail("IOException throwing during test");
+ }
+ }
+
+ @Test
+ public void throws_correctly_with_UNKNOWN_ATTRIBUTE_reason() {
+ try {
+ Config.parse(CONFIG_MAP.get("unknown-attribute"));
+ fail("Config parsing must fail in this test");
+ } catch (final BadConfigException e) {
+ assertEquals(e.getReason(), Reason.UNKNOWN_ATTRIBUTE);
+ assertEquals(e.getLocation(), Location.TOP_LEVEL);
+ assertEquals(e.getSection(), Section.PEER);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ fail("IOException throwing during test");
+ }
+ }
+
+ @Test
+ public void throws_correctly_with_UNKNOWN_SECTION_reason() {
+ try {
+ Config.parse(CONFIG_MAP.get("unknown-section"));
+ fail("Config parsing must fail in this test");
+ } catch (final BadConfigException e) {
+ assertEquals(e.getReason(), Reason.UNKNOWN_SECTION);
+ assertEquals(e.getLocation(), Location.TOP_LEVEL);
+ assertEquals(e.getSection(), Section.CONFIG);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ fail("IOException throwing during test");
+ }
+ }
+}
diff --git a/tunnel/src/test/java/com/wireguard/config/ConfigTest.java b/tunnel/src/test/java/com/wireguard/config/ConfigTest.java
new file mode 100644
index 00000000..92143e72
--- /dev/null
+++ b/tunnel/src/test/java/com/wireguard/config/ConfigTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Objects;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class ConfigTest {
+
+ @Test(expected = BadConfigException.class)
+ public void invalid_config_throws() throws IOException, BadConfigException {
+ try (final InputStream is = Objects.requireNonNull(getClass().getClassLoader()).getResourceAsStream("broken.conf")) {
+ Config.parse(is);
+ }
+ }
+
+ @Test
+ public void valid_config_parses_correctly() throws IOException, ParseException {
+ Config config = null;
+ final Collection<InetNetwork> expectedAllowedIps = new HashSet<>(Arrays.asList(InetNetwork.parse("0.0.0.0/0"), InetNetwork.parse("::0/0")));
+ try (final InputStream is = Objects.requireNonNull(getClass().getClassLoader()).getResourceAsStream("working.conf")) {
+ config = Config.parse(is);
+ } catch (final BadConfigException e) {
+ fail("'working.conf' should never fail to parse");
+ }
+ assertNotNull("config cannot be null after parsing", config);
+ assertTrue(
+ "No applications should be excluded by default",
+ config.getInterface().getExcludedApplications().isEmpty()
+ );
+ assertEquals("Test config has exactly one peer", 1, config.getPeers().size());
+ assertEquals("Test config's allowed IPs are 0.0.0.0/0 and ::0/0", config.getPeers().get(0).getAllowedIps(), expectedAllowedIps);
+ assertEquals("Test config has one DNS server", 1, config.getInterface().getDnsServers().size());
+ }
+}
diff --git a/tunnel/src/test/resources/broken.conf b/tunnel/src/test/resources/broken.conf
new file mode 100644
index 00000000..753c9717
--- /dev/null
+++ b/tunnel/src/test/resources/broken.conf
@@ -0,0 +1,9 @@
+[Interface]
+PrivateKey = l0lth1s1sd3f1n1t3lybr0k3n=
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0
+
+[Peer]
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
+AllowedIPs = 0.0.0.0/0,::0/0
+Endpoint = 192.0.2.1:51820
diff --git a/tunnel/src/test/resources/invalid-key.conf b/tunnel/src/test/resources/invalid-key.conf
new file mode 100644
index 00000000..215bec3b
--- /dev/null
+++ b/tunnel/src/test/resources/invalid-key.conf
@@ -0,0 +1,9 @@
+[Interface]
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0
+PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
+[Peer]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint = 192.0.2.1:51820
+PersistentKeepalive = 0
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6Og=
diff --git a/tunnel/src/test/resources/invalid-number.conf b/tunnel/src/test/resources/invalid-number.conf
new file mode 100644
index 00000000..f05fe32b
--- /dev/null
+++ b/tunnel/src/test/resources/invalid-number.conf
@@ -0,0 +1,9 @@
+[Interface]
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0
+PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
+[Peer]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint = 192.0.2.1:51820
+PersistentKeepalive = 0L
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
diff --git a/tunnel/src/test/resources/invalid-value.conf b/tunnel/src/test/resources/invalid-value.conf
new file mode 100644
index 00000000..6a1e3b62
--- /dev/null
+++ b/tunnel/src/test/resources/invalid-value.conf
@@ -0,0 +1,9 @@
+[Interface]
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0,invalid_value
+PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
+[Peer]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint = 192.0.2.1:51820
+PersistentKeepalive = 0
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
diff --git a/tunnel/src/test/resources/missing-attribute.conf b/tunnel/src/test/resources/missing-attribute.conf
new file mode 100644
index 00000000..ddf8cbb5
--- /dev/null
+++ b/tunnel/src/test/resources/missing-attribute.conf
@@ -0,0 +1,8 @@
+[Interface]
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0
+PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
+[Peer]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint = 192.0.2.1:51820
+PersistentKeepalive = 0
diff --git a/tunnel/src/test/resources/missing-section.conf b/tunnel/src/test/resources/missing-section.conf
new file mode 100644
index 00000000..676199ac
--- /dev/null
+++ b/tunnel/src/test/resources/missing-section.conf
@@ -0,0 +1,5 @@
+[Peer]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint = 192.0.2.1:51820
+PersistentKeepalive = 0
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
diff --git a/tunnel/src/test/resources/syntax-error.conf b/tunnel/src/test/resources/syntax-error.conf
new file mode 100644
index 00000000..38b8ec95
--- /dev/null
+++ b/tunnel/src/test/resources/syntax-error.conf
@@ -0,0 +1,9 @@
+[Interface]
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0
+PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
+[Peer]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint =
+PersistentKeepalive = 0
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
diff --git a/tunnel/src/test/resources/unknown-attribute.conf b/tunnel/src/test/resources/unknown-attribute.conf
new file mode 100644
index 00000000..f311161d
--- /dev/null
+++ b/tunnel/src/test/resources/unknown-attribute.conf
@@ -0,0 +1,9 @@
+[Interface]
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0
+PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
+[Peer]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint = 192.0.2.1:51820
+DontLetTheFeelingFade = 1
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
diff --git a/tunnel/src/test/resources/unknown-section.conf b/tunnel/src/test/resources/unknown-section.conf
new file mode 100644
index 00000000..579d9717
--- /dev/null
+++ b/tunnel/src/test/resources/unknown-section.conf
@@ -0,0 +1,9 @@
+[Interface]
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0
+PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
+[Peers]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint = 192.0.2.1:51820
+PersistentKeepalive = 0
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
diff --git a/tunnel/src/test/resources/working.conf b/tunnel/src/test/resources/working.conf
new file mode 100644
index 00000000..3f9665c3
--- /dev/null
+++ b/tunnel/src/test/resources/working.conf
@@ -0,0 +1,9 @@
+[Interface]
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0
+PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
+[Peer]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint = 192.0.2.1:51820
+PersistentKeepalive = 0
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
diff --git a/tunnel/tools/CMakeLists.txt b/tunnel/tools/CMakeLists.txt
new file mode 100644
index 00000000..490a1755
--- /dev/null
+++ b/tunnel/tools/CMakeLists.txt
@@ -0,0 +1,44 @@
+# SPDX-License-Identifier: Apache-2.0
+#
+# Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+
+cmake_minimum_required(VERSION 3.4.1)
+project("WireGuard")
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
+add_link_options(LINKER:--build-id=none)
+add_compile_options(-Wall -Werror)
+
+add_executable(libwg-quick.so wireguard-tools/src/wg-quick/android.c ndk-compat/compat.c)
+target_compile_options(libwg-quick.so PUBLIC -std=gnu11 -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DWG_PACKAGE_NAME=\"${ANDROID_PACKAGE_NAME}\")
+target_link_libraries(libwg-quick.so -ldl)
+
+file(GLOB WG_SOURCES wireguard-tools/src/*.c ndk-compat/compat.c)
+add_executable(libwg.so ${WG_SOURCES})
+target_include_directories(libwg.so PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/uapi/linux/" "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/")
+target_compile_options(libwg.so PUBLIC -std=gnu11 -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DRUNSTATEDIR=\"/data/data/${ANDROID_PACKAGE_NAME}/cache\")
+
+add_custom_target(libwg-go.so WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/libwg-go" COMMENT "Building wireguard-go" VERBATIM COMMAND "${ANDROID_HOST_PREBUILTS}/bin/make"
+ ANDROID_ARCH_NAME=${ANDROID_ARCH_NAME}
+ ANDROID_PACKAGE_NAME=${ANDROID_PACKAGE_NAME}
+ GRADLE_USER_HOME=${GRADLE_USER_HOME}
+ CC=${CMAKE_C_COMPILER}
+ CFLAGS=${CMAKE_C_FLAGS}
+ LDFLAGS=${CMAKE_SHARED_LINKER_FLAGS}
+ SYSROOT=${CMAKE_SYSROOT}
+ TARGET=${CMAKE_C_COMPILER_TARGET}
+ DESTDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
+ BUILDDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/../generated-src
+)
+
+# Strip unwanted ELF sections to prevent DT_FLAGS_1 warnings on old Android versions
+file(GLOB ELF_CLEANER_SOURCES elf-cleaner/*.c elf-cleaner/*.cpp)
+add_custom_target(elf-cleaner COMMENT "Building elf-cleaner" VERBATIM COMMAND cc
+ -O2 -DPACKAGE_NAME="elf-cleaner" -DPACKAGE_VERSION="" -DCOPYRIGHT=""
+ -o "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner" ${ELF_CLEANER_SOURCES}
+)
+add_custom_command(TARGET libwg.so POST_BUILD VERBATIM COMMAND "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner"
+ --api-level "${ANDROID_NATIVE_API_LEVEL}" "$<TARGET_FILE:libwg.so>")
+add_dependencies(libwg.so elf-cleaner)
+add_custom_command(TARGET libwg-quick.so POST_BUILD VERBATIM COMMAND "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner"
+ --api-level "${ANDROID_NATIVE_API_LEVEL}" "$<TARGET_FILE:libwg-quick.so>")
+add_dependencies(libwg-quick.so elf-cleaner)
diff --git a/tunnel/tools/elf-cleaner b/tunnel/tools/elf-cleaner
new file mode 160000
+Subproject 7efc05090675ec6161b7def862728086a26c3b1
diff --git a/tunnel/tools/libwg-go/.gitignore b/tunnel/tools/libwg-go/.gitignore
new file mode 100644
index 00000000..d1638636
--- /dev/null
+++ b/tunnel/tools/libwg-go/.gitignore
@@ -0,0 +1 @@
+build/ \ No newline at end of file
diff --git a/tunnel/tools/libwg-go/Makefile b/tunnel/tools/libwg-go/Makefile
new file mode 100644
index 00000000..427bfdc3
--- /dev/null
+++ b/tunnel/tools/libwg-go/Makefile
@@ -0,0 +1,52 @@
+# SPDX-License-Identifier: Apache-2.0
+#
+# Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+
+BUILDDIR ?= $(CURDIR)/build
+DESTDIR ?= $(CURDIR)/out
+
+NDK_GO_ARCH_MAP_x86 := 386
+NDK_GO_ARCH_MAP_x86_64 := amd64
+NDK_GO_ARCH_MAP_arm := arm
+NDK_GO_ARCH_MAP_arm64 := arm64
+NDK_GO_ARCH_MAP_mips := mipsx
+NDK_GO_ARCH_MAP_mips64 := mips64x
+
+comma := ,
+CLANG_FLAGS := --target=$(TARGET) --sysroot=$(SYSROOT)
+export CGO_CFLAGS := $(CLANG_FLAGS) $(subst -mthumb,-marm,$(CFLAGS))
+export CGO_LDFLAGS := $(CLANG_FLAGS) $(patsubst -Wl$(comma)--build-id=%,-Wl$(comma)--build-id=none,$(LDFLAGS)) -Wl,-soname=libwg-go.so
+export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
+export GOOS := android
+export CGO_ENABLED := 1
+
+GO_VERSION := 1.21.3
+GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m))
+GO_TARBALL := go$(GO_VERSION).$(GO_PLATFORM).tar.gz
+GO_HASH_darwin-amd64 := 27014fc69e301d7588a169ca239b3cc609f0aa1abf38528bf0d20d3b259211eb
+GO_HASH_darwin-arm64 := 65302a7a9f7a4834932b3a7a14cb8be51beddda757b567a2f9e0cbd0d7b5a6ab
+GO_HASH_linux-amd64 := 1241381b2843fae5a9707eec1f8fb2ef94d827990582c7c7c32f5bdfbfd420c8
+
+default: $(DESTDIR)/libwg-go.so
+
+$(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL):
+ mkdir -p "$(dir $@)"
+ flock "$@.lock" -c ' \
+ [ -f "$@" ] && exit 0; \
+ curl -o "$@.tmp" "https://dl.google.com/go/$(GO_TARBALL)" && \
+ echo "$(GO_HASH_$(GO_PLATFORM)) $@.tmp" | sha256sum -c && \
+ mv "$@.tmp" "$@"'
+
+$(BUILDDIR)/go-$(GO_VERSION)/.prepared: $(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL)
+ mkdir -p "$(dir $@)"
+ flock "$@.lock" -c ' \
+ [ -f "$@" ] && exit 0; \
+ tar -C "$(dir $@)" --strip-components=1 -xzf "$^" && \
+ patch -p1 -f -N -r- -d "$(dir $@)" < goruntime-boottime-over-monotonic.diff && \
+ touch "$@"'
+
+$(DESTDIR)/libwg-go.so: export PATH := $(BUILDDIR)/go-$(GO_VERSION)/bin/:$(PATH)
+$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(GO_VERSION)/.prepared go.mod
+ go build -tags linux -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard -buildid=" -v -trimpath -buildvcs=false -o "$@" -buildmode c-shared
+
+.DELETE_ON_ERROR:
diff --git a/tunnel/tools/libwg-go/api-android.go b/tunnel/tools/libwg-go/api-android.go
new file mode 100644
index 00000000..d47c5d76
--- /dev/null
+++ b/tunnel/tools/libwg-go/api-android.go
@@ -0,0 +1,227 @@
+/* SPDX-License-Identifier: Apache-2.0
+ *
+ * Copyright © 2017-2022 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+package main
+
+// #cgo LDFLAGS: -llog
+// #include <android/log.h>
+import "C"
+
+import (
+ "fmt"
+ "math"
+ "net"
+ "os"
+ "os/signal"
+ "runtime"
+ "runtime/debug"
+ "strings"
+ "unsafe"
+
+ "golang.org/x/sys/unix"
+ "golang.zx2c4.com/wireguard/conn"
+ "golang.zx2c4.com/wireguard/device"
+ "golang.zx2c4.com/wireguard/ipc"
+ "golang.zx2c4.com/wireguard/tun"
+)
+
+type AndroidLogger struct {
+ level C.int
+ tag *C.char
+}
+
+func cstring(s string) *C.char {
+ b, err := unix.BytePtrFromString(s)
+ if err != nil {
+ b := [1]C.char{}
+ return &b[0]
+ }
+ return (*C.char)(unsafe.Pointer(b))
+}
+
+func (l AndroidLogger) Printf(format string, args ...interface{}) {
+ C.__android_log_write(l.level, l.tag, cstring(fmt.Sprintf(format, args...)))
+}
+
+type TunnelHandle struct {
+ device *device.Device
+ uapi net.Listener
+}
+
+var tunnelHandles map[int32]TunnelHandle
+
+func init() {
+ tunnelHandles = make(map[int32]TunnelHandle)
+ signals := make(chan os.Signal)
+ signal.Notify(signals, unix.SIGUSR2)
+ go func() {
+ buf := make([]byte, os.Getpagesize())
+ for {
+ select {
+ case <-signals:
+ n := runtime.Stack(buf, true)
+ if n == len(buf) {
+ n--
+ }
+ buf[n] = 0
+ C.__android_log_write(C.ANDROID_LOG_ERROR, cstring("WireGuard/GoBackend/Stacktrace"), (*C.char)(unsafe.Pointer(&buf[0])))
+ }
+ }
+ }()
+}
+
+//export wgTurnOn
+func wgTurnOn(interfaceName string, tunFd int32, settings string) int32 {
+ tag := cstring("WireGuard/GoBackend/" + interfaceName)
+ logger := &device.Logger{
+ Verbosef: AndroidLogger{level: C.ANDROID_LOG_DEBUG, tag: tag}.Printf,
+ Errorf: AndroidLogger{level: C.ANDROID_LOG_ERROR, tag: tag}.Printf,
+ }
+
+ tun, name, err := tun.CreateUnmonitoredTUNFromFD(int(tunFd))
+ if err != nil {
+ unix.Close(int(tunFd))
+ logger.Errorf("CreateUnmonitoredTUNFromFD: %v", err)
+ return -1
+ }
+
+ logger.Verbosef("Attaching to interface %v", name)
+ device := device.NewDevice(tun, conn.NewStdNetBind(), logger)
+
+ err = device.IpcSet(settings)
+ if err != nil {
+ unix.Close(int(tunFd))
+ logger.Errorf("IpcSet: %v", err)
+ return -1
+ }
+ device.DisableSomeRoamingForBrokenMobileSemantics()
+
+ var uapi net.Listener
+
+ uapiFile, err := ipc.UAPIOpen(name)
+ if err != nil {
+ logger.Errorf("UAPIOpen: %v", err)
+ } else {
+ uapi, err = ipc.UAPIListen(name, uapiFile)
+ if err != nil {
+ uapiFile.Close()
+ logger.Errorf("UAPIListen: %v", err)
+ } else {
+ go func() {
+ for {
+ conn, err := uapi.Accept()
+ if err != nil {
+ return
+ }
+ go device.IpcHandle(conn)
+ }
+ }()
+ }
+ }
+
+ err = device.Up()
+ if err != nil {
+ logger.Errorf("Unable to bring up device: %v", err)
+ uapiFile.Close()
+ device.Close()
+ return -1
+ }
+ logger.Verbosef("Device started")
+
+ var i int32
+ for i = 0; i < math.MaxInt32; i++ {
+ if _, exists := tunnelHandles[i]; !exists {
+ break
+ }
+ }
+ if i == math.MaxInt32 {
+ logger.Errorf("Unable to find empty handle")
+ uapiFile.Close()
+ device.Close()
+ return -1
+ }
+ tunnelHandles[i] = TunnelHandle{device: device, uapi: uapi}
+ return i
+}
+
+//export wgTurnOff
+func wgTurnOff(tunnelHandle int32) {
+ handle, ok := tunnelHandles[tunnelHandle]
+ if !ok {
+ return
+ }
+ delete(tunnelHandles, tunnelHandle)
+ if handle.uapi != nil {
+ handle.uapi.Close()
+ }
+ handle.device.Close()
+}
+
+//export wgGetSocketV4
+func wgGetSocketV4(tunnelHandle int32) int32 {
+ handle, ok := tunnelHandles[tunnelHandle]
+ if !ok {
+ return -1
+ }
+ bind, _ := handle.device.Bind().(conn.PeekLookAtSocketFd)
+ if bind == nil {
+ return -1
+ }
+ fd, err := bind.PeekLookAtSocketFd4()
+ if err != nil {
+ return -1
+ }
+ return int32(fd)
+}
+
+//export wgGetSocketV6
+func wgGetSocketV6(tunnelHandle int32) int32 {
+ handle, ok := tunnelHandles[tunnelHandle]
+ if !ok {
+ return -1
+ }
+ bind, _ := handle.device.Bind().(conn.PeekLookAtSocketFd)
+ if bind == nil {
+ return -1
+ }
+ fd, err := bind.PeekLookAtSocketFd6()
+ if err != nil {
+ return -1
+ }
+ return int32(fd)
+}
+
+//export wgGetConfig
+func wgGetConfig(tunnelHandle int32) *C.char {
+ handle, ok := tunnelHandles[tunnelHandle]
+ if !ok {
+ return nil
+ }
+ settings, err := handle.device.IpcGet()
+ if err != nil {
+ return nil
+ }
+ return C.CString(settings)
+}
+
+//export wgVersion
+func wgVersion() *C.char {
+ info, ok := debug.ReadBuildInfo()
+ if !ok {
+ return C.CString("unknown")
+ }
+ for _, dep := range info.Deps {
+ if dep.Path == "golang.zx2c4.com/wireguard" {
+ parts := strings.Split(dep.Version, "-")
+ if len(parts) == 3 && len(parts[2]) == 12 {
+ return C.CString(parts[2][:7])
+ }
+ return C.CString(dep.Version)
+ }
+ }
+ return C.CString("unknown")
+}
+
+func main() {}
diff --git a/tunnel/tools/libwg-go/go.mod b/tunnel/tools/libwg-go/go.mod
new file mode 100644
index 00000000..9f9381ff
--- /dev/null
+++ b/tunnel/tools/libwg-go/go.mod
@@ -0,0 +1,14 @@
+module golang.zx2c4.com/wireguard/android
+
+go 1.20
+
+require (
+ golang.org/x/sys v0.13.0
+ golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb
+)
+
+require (
+ golang.org/x/crypto v0.14.0 // indirect
+ golang.org/x/net v0.17.0 // indirect
+ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
+)
diff --git a/tunnel/tools/libwg-go/go.sum b/tunnel/tools/libwg-go/go.sum
new file mode 100644
index 00000000..e5f8fc83
--- /dev/null
+++ b/tunnel/tools/libwg-go/go.sum
@@ -0,0 +1,13 @@
+github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
+golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
+golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
+golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
+golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb h1:c5tyN8sSp8jSDxdCCDXVOpJwYXXhmTkNMt+g0zTSOic=
+golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
+gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
diff --git a/tunnel/tools/libwg-go/goruntime-boottime-over-monotonic.diff b/tunnel/tools/libwg-go/goruntime-boottime-over-monotonic.diff
new file mode 100644
index 00000000..5d78242b
--- /dev/null
+++ b/tunnel/tools/libwg-go/goruntime-boottime-over-monotonic.diff
@@ -0,0 +1,171 @@
+From 61f3ae8298d1c503cbc31539e0f3a73446c7db9d Mon Sep 17 00:00:00 2001
+From: "Jason A. Donenfeld" <Jason@zx2c4.com>
+Date: Tue, 21 Mar 2023 15:33:56 +0100
+Subject: [PATCH] [release-branch.go1.20] runtime: use CLOCK_BOOTTIME in
+ nanotime on Linux
+
+This makes timers account for having expired while a computer was
+asleep, which is quite common on mobile devices. Note that BOOTTIME is
+identical to MONOTONIC, except that it takes into account time spent
+in suspend. In Linux 4.17, the kernel will actually make MONOTONIC act
+like BOOTTIME anyway, so this switch will additionally unify the
+timer behavior across kernels.
+
+BOOTTIME was introduced into Linux 2.6.39-rc1 with 70a08cca1227d in
+2011.
+
+Fixes #24595
+
+Change-Id: I7b2a6ca0c5bc5fce57ec0eeafe7b68270b429321
+---
+ src/runtime/sys_linux_386.s | 4 ++--
+ src/runtime/sys_linux_amd64.s | 2 +-
+ src/runtime/sys_linux_arm.s | 4 ++--
+ src/runtime/sys_linux_arm64.s | 4 ++--
+ src/runtime/sys_linux_mips64x.s | 4 ++--
+ src/runtime/sys_linux_mipsx.s | 2 +-
+ src/runtime/sys_linux_ppc64x.s | 2 +-
+ src/runtime/sys_linux_s390x.s | 2 +-
+ 8 files changed, 12 insertions(+), 12 deletions(-)
+
+diff --git a/src/runtime/sys_linux_386.s b/src/runtime/sys_linux_386.s
+index 12a294153d..17e3524b40 100644
+--- a/src/runtime/sys_linux_386.s
++++ b/src/runtime/sys_linux_386.s
+@@ -352,13 +352,13 @@ noswitch:
+
+ LEAL 8(SP), BX // &ts (struct timespec)
+ MOVL BX, 4(SP)
+- MOVL $1, 0(SP) // CLOCK_MONOTONIC
++ MOVL $7, 0(SP) // CLOCK_BOOTTIME
+ CALL AX
+ JMP finish
+
+ fallback:
+ MOVL $SYS_clock_gettime, AX
+- MOVL $1, BX // CLOCK_MONOTONIC
++ MOVL $7, BX // CLOCK_BOOTTIME
+ LEAL 8(SP), CX
+ INVOKE_SYSCALL
+
+diff --git a/src/runtime/sys_linux_amd64.s b/src/runtime/sys_linux_amd64.s
+index c7a89ba536..01f0a6a26e 100644
+--- a/src/runtime/sys_linux_amd64.s
++++ b/src/runtime/sys_linux_amd64.s
+@@ -255,7 +255,7 @@ noswitch:
+ SUBQ $16, SP // Space for results
+ ANDQ $~15, SP // Align for C code
+
+- MOVL $1, DI // CLOCK_MONOTONIC
++ MOVL $7, DI // CLOCK_BOOTTIME
+ LEAQ 0(SP), SI
+ MOVQ runtime·vdsoClockgettimeSym(SB), AX
+ CMPQ AX, $0
+diff --git a/src/runtime/sys_linux_arm.s b/src/runtime/sys_linux_arm.s
+index 7b8c4f0e04..9798a1334e 100644
+--- a/src/runtime/sys_linux_arm.s
++++ b/src/runtime/sys_linux_arm.s
+@@ -11,7 +11,7 @@
+ #include "textflag.h"
+
+ #define CLOCK_REALTIME 0
+-#define CLOCK_MONOTONIC 1
++#define CLOCK_BOOTTIME 7
+
+ // for EABI, as we don't support OABI
+ #define SYS_BASE 0x0
+@@ -374,7 +374,7 @@ finish:
+
+ // func nanotime1() int64
+ TEXT runtime·nanotime1(SB),NOSPLIT,$12-8
+- MOVW $CLOCK_MONOTONIC, R0
++ MOVW $CLOCK_BOOTTIME, R0
+ MOVW $spec-12(SP), R1 // timespec
+
+ MOVW runtime·vdsoClockgettimeSym(SB), R4
+diff --git a/src/runtime/sys_linux_arm64.s b/src/runtime/sys_linux_arm64.s
+index 38ff6ac330..6b819c5441 100644
+--- a/src/runtime/sys_linux_arm64.s
++++ b/src/runtime/sys_linux_arm64.s
+@@ -14,7 +14,7 @@
+ #define AT_FDCWD -100
+
+ #define CLOCK_REALTIME 0
+-#define CLOCK_MONOTONIC 1
++#define CLOCK_BOOTTIME 7
+
+ #define SYS_exit 93
+ #define SYS_read 63
+@@ -338,7 +338,7 @@ noswitch:
+ BIC $15, R1
+ MOVD R1, RSP
+
+- MOVW $CLOCK_MONOTONIC, R0
++ MOVW $CLOCK_BOOTTIME, R0
+ MOVD runtime·vdsoClockgettimeSym(SB), R2
+ CBZ R2, fallback
+
+diff --git a/src/runtime/sys_linux_mips64x.s b/src/runtime/sys_linux_mips64x.s
+index 47f2da524d..a8b387f193 100644
+--- a/src/runtime/sys_linux_mips64x.s
++++ b/src/runtime/sys_linux_mips64x.s
+@@ -326,7 +326,7 @@ noswitch:
+ AND $~15, R1 // Align for C code
+ MOVV R1, R29
+
+- MOVW $1, R4 // CLOCK_MONOTONIC
++ MOVW $7, R4 // CLOCK_BOOTTIME
+ MOVV $0(R29), R5
+
+ MOVV runtime·vdsoClockgettimeSym(SB), R25
+@@ -336,7 +336,7 @@ noswitch:
+ // see walltime for detail
+ BEQ R2, R0, finish
+ MOVV R0, runtime·vdsoClockgettimeSym(SB)
+- MOVW $1, R4 // CLOCK_MONOTONIC
++ MOVW $7, R4 // CLOCK_BOOTTIME
+ MOVV $0(R29), R5
+ JMP fallback
+
+diff --git a/src/runtime/sys_linux_mipsx.s b/src/runtime/sys_linux_mipsx.s
+index 5e6b6c1504..7f5fd2a80e 100644
+--- a/src/runtime/sys_linux_mipsx.s
++++ b/src/runtime/sys_linux_mipsx.s
+@@ -243,7 +243,7 @@ TEXT runtime·walltime(SB),NOSPLIT,$8-12
+ RET
+
+ TEXT runtime·nanotime1(SB),NOSPLIT,$8-8
+- MOVW $1, R4 // CLOCK_MONOTONIC
++ MOVW $7, R4 // CLOCK_BOOTTIME
+ MOVW $4(R29), R5
+ MOVW $SYS_clock_gettime, R2
+ SYSCALL
+diff --git a/src/runtime/sys_linux_ppc64x.s b/src/runtime/sys_linux_ppc64x.s
+index d0427a4807..05ee9fede9 100644
+--- a/src/runtime/sys_linux_ppc64x.s
++++ b/src/runtime/sys_linux_ppc64x.s
+@@ -298,7 +298,7 @@ fallback:
+ JMP return
+
+ TEXT runtime·nanotime1(SB),NOSPLIT,$16-8
+- MOVD $1, R3 // CLOCK_MONOTONIC
++ MOVD $7, R3 // CLOCK_BOOTTIME
+
+ MOVD R1, R15 // R15 is unchanged by C code
+ MOVD g_m(g), R21 // R21 = m
+diff --git a/src/runtime/sys_linux_s390x.s b/src/runtime/sys_linux_s390x.s
+index 1448670b91..7d2ee3231c 100644
+--- a/src/runtime/sys_linux_s390x.s
++++ b/src/runtime/sys_linux_s390x.s
+@@ -296,7 +296,7 @@ fallback:
+ RET
+
+ TEXT runtime·nanotime1(SB),NOSPLIT,$32-8
+- MOVW $1, R2 // CLOCK_MONOTONIC
++ MOVW $7, R2 // CLOCK_BOOTTIME
+
+ MOVD R15, R7 // Backup stack pointer
+
+--
+2.17.1
+
diff --git a/tunnel/tools/libwg-go/jni.c b/tunnel/tools/libwg-go/jni.c
new file mode 100644
index 00000000..7ad94d35
--- /dev/null
+++ b/tunnel/tools/libwg-go/jni.c
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: Apache-2.0
+ *
+ * Copyright © 2017-2021 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+#include <jni.h>
+#include <stdlib.h>
+#include <string.h>
+
+struct go_string { const char *str; long n; };
+extern int wgTurnOn(struct go_string ifname, int tun_fd, struct go_string settings);
+extern void wgTurnOff(int handle);
+extern int wgGetSocketV4(int handle);
+extern int wgGetSocketV6(int handle);
+extern char *wgGetConfig(int handle);
+extern char *wgVersion();
+
+JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOn(JNIEnv *env, jclass c, jstring ifname, jint tun_fd, jstring settings)
+{
+ const char *ifname_str = (*env)->GetStringUTFChars(env, ifname, 0);
+ size_t ifname_len = (*env)->GetStringUTFLength(env, ifname);
+ const char *settings_str = (*env)->GetStringUTFChars(env, settings, 0);
+ size_t settings_len = (*env)->GetStringUTFLength(env, settings);
+ int ret = wgTurnOn((struct go_string){
+ .str = ifname_str,
+ .n = ifname_len
+ }, tun_fd, (struct go_string){
+ .str = settings_str,
+ .n = settings_len
+ });
+ (*env)->ReleaseStringUTFChars(env, ifname, ifname_str);
+ (*env)->ReleaseStringUTFChars(env, settings, settings_str);
+ return ret;
+}
+
+JNIEXPORT void JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOff(JNIEnv *env, jclass c, jint handle)
+{
+ wgTurnOff(handle);
+}
+
+JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetSocketV4(JNIEnv *env, jclass c, jint handle)
+{
+ return wgGetSocketV4(handle);
+}
+
+JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetSocketV6(JNIEnv *env, jclass c, jint handle)
+{
+ return wgGetSocketV6(handle);
+}
+
+JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetConfig(JNIEnv *env, jclass c, jint handle)
+{
+ jstring ret;
+ char *config = wgGetConfig(handle);
+ if (!config)
+ return NULL;
+ ret = (*env)->NewStringUTF(env, config);
+ free(config);
+ return ret;
+}
+
+JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgVersion(JNIEnv *env, jclass c)
+{
+ jstring ret;
+ char *version = wgVersion();
+ if (!version)
+ return NULL;
+ ret = (*env)->NewStringUTF(env, version);
+ free(version);
+ return ret;
+}
diff --git a/tunnel/tools/ndk-compat/compat.c b/tunnel/tools/ndk-compat/compat.c
new file mode 100644
index 00000000..c5ce85ec
--- /dev/null
+++ b/tunnel/tools/ndk-compat/compat.c
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: BSD
+ *
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ *
+ */
+
+#define FILE_IS_EMPTY
+
+#if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 24
+#undef FILE_IS_EMPTY
+#include <string.h>
+
+char *strchrnul(const char *s, int c)
+{
+ char *x = strchr(s, c);
+ if (!x)
+ return (char *)s + strlen(s);
+ return x;
+}
+#endif
+
+#ifdef FILE_IS_EMPTY
+#undef FILE_IS_EMPTY
+static char ____x __attribute__((unused));
+#endif
diff --git a/tunnel/tools/ndk-compat/compat.h b/tunnel/tools/ndk-compat/compat.h
new file mode 100644
index 00000000..7dfd8e14
--- /dev/null
+++ b/tunnel/tools/ndk-compat/compat.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: BSD
+ *
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ *
+ */
+
+#if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 24
+char *strchrnul(const char *s, int c);
+#endif
+
diff --git a/tunnel/tools/wireguard-tools b/tunnel/tools/wireguard-tools
new file mode 160000
+Subproject b4f6b4f229d291daf7c35c6f1e7f4841cc6d69b