summaryrefslogtreecommitdiffhomepage
path: root/tunnel/src/main/java/com/wireguard/android/util
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2020-03-09 19:00:14 +0530
committerHarsh Shandilya <me@msfjarvis.dev>2020-03-09 19:24:26 +0530
commitadc613d8011af7c508050badb1272e8326554c39 (patch)
tree4eadedc0767e1f4f482b7c22ec905329acab62a6 /tunnel/src/main/java/com/wireguard/android/util
parentfd573f6c1c37bcfcd09237dfcd55e08b1e0eaff9 (diff)
Migrate tunnel related classes to tunnel/ Gradle module
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Diffstat (limited to 'tunnel/src/main/java/com/wireguard/android/util')
-rw-r--r--tunnel/src/main/java/com/wireguard/android/util/AsyncWorker.java63
-rw-r--r--tunnel/src/main/java/com/wireguard/android/util/RootShell.java211
-rw-r--r--tunnel/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java94
-rw-r--r--tunnel/src/main/java/com/wireguard/android/util/ToolsInstaller.java196
4 files changed, 564 insertions, 0 deletions
diff --git a/tunnel/src/main/java/com/wireguard/android/util/AsyncWorker.java b/tunnel/src/main/java/com/wireguard/android/util/AsyncWorker.java
new file mode 100644
index 00000000..1d041851
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/util/AsyncWorker.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.util;
+
+import android.os.Handler;
+
+import java.util.concurrent.Executor;
+
+import java9.util.concurrent.CompletableFuture;
+import java9.util.concurrent.CompletionStage;
+
+/**
+ * Helper class for running asynchronous tasks and ensuring they are completed on the main thread.
+ */
+
+public class AsyncWorker {
+ private final Executor executor;
+ private final Handler handler;
+
+ public AsyncWorker(final Executor executor, final Handler handler) {
+ this.executor = executor;
+ this.handler = handler;
+ }
+
+ public CompletionStage<Void> runAsync(final AsyncRunnable<?> runnable) {
+ final CompletableFuture<Void> future = new CompletableFuture<>();
+ executor.execute(() -> {
+ try {
+ runnable.run();
+ handler.post(() -> future.complete(null));
+ } catch (final Throwable t) {
+ handler.post(() -> future.completeExceptionally(t));
+ }
+ });
+ return future;
+ }
+
+ public <T> CompletionStage<T> supplyAsync(final AsyncSupplier<T, ?> supplier) {
+ final CompletableFuture<T> future = new CompletableFuture<>();
+ executor.execute(() -> {
+ try {
+ final T result = supplier.get();
+ handler.post(() -> future.complete(result));
+ } catch (final Throwable t) {
+ handler.post(() -> future.completeExceptionally(t));
+ }
+ });
+ return future;
+ }
+
+ @FunctionalInterface
+ public interface AsyncRunnable<E extends Throwable> {
+ void run() throws E;
+ }
+
+ @FunctionalInterface
+ public interface AsyncSupplier<T, E extends Throwable> {
+ T get() throws E;
+ }
+}
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..1fc2c9f2
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/util/RootShell.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.util;
+
+import android.content.Context;
+import androidx.annotation.Nullable;
+import android.util.Log;
+
+import com.wireguard.android.util.RootShell.RootShellException.Reason;
+
+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;
+
+/**
+ * Helper class for running commands as root.
+ */
+
+public class RootShell {
+ private static final String SU = "su";
+ private static final String TAG = "WireGuard/" + RootShell.class.getSimpleName();
+
+ 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");
+ preamble = String.format("export CALLING_PACKAGE=%s PATH=\"%s:$PATH\" TMPDIR='%s'; id -u\n",
+ context.getPackageName(), localBinaryDir, localTemporaryDir);
+ }
+
+ 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 {
+ 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
+ }
+ private final Reason reason;
+ private final Object[] format;
+ public RootShellException(final Reason reason, final Object ...format) {
+ this.reason = reason;
+ this.format = format;
+ }
+ public boolean isIORelated() {
+ return reason != Reason.NO_ROOT_ACCESS;
+ }
+ public Reason getReason() {
+ return reason;
+ }
+ public Object[] getFormat() {
+ return format;
+ }
+ }
+}
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..93e44b64
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright © 2017-2019 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 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;
+
+public final class SharedLibraryLoader {
+ private static final String TAG = "WireGuard/" + SharedLibraryLoader.class.getSimpleName();
+
+ 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) {
+ final ZipFile zipFile;
+ try {
+ zipFile = new ZipFile(new File(apk), ZipFile.OPEN_READ);
+ } catch (final IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ final String mappedLibName = System.mapLibraryName(libName);
+ final byte[] buffer = new byte[1024 * 32];
+ 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;
+ while ((len = in.read(buffer)) != -1) {
+ out.write(buffer, 0, len);
+ }
+ out.getFD().sync();
+ }
+ zipFile.close();
+ 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..ac18cabf
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/util/ToolsInstaller.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.util;
+
+import android.content.Context;
+import androidx.annotation.Nullable;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.wireguard.android.util.RootShell.RootShellException;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Helper to install WireGuard tools to the system partition.
+ */
+
+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.class.getSimpleName();
+
+ private final Context context;
+ private final RootShell rootShell;
+ private final File localBinaryDir;
+ private final Object lock = new Object();
+ @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 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 /sbin/.magisk/img/wireguard' INT TERM EXIT; ");
+ script.append(String.format("rm -rf /sbin/.magisk/img/wireguard/; mkdir -p /sbin/.magisk/img/wireguard%s; ", INSTALL_DIR));
+ script.append("printf 'name=WireGuard Command Line Tools\nversion=1.0\nversionCode=1\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /sbin/.magisk/img/wireguard/module.prop; ");
+ script.append("touch /sbin/.magisk/img/wireguard/auto_mount; ");
+ for (final String name : EXECUTABLES) {
+ final File destination = new File("/sbin/.magisk/img/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;
+ }
+ }
+
+ 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;
+ }
+
+ private boolean willInstallAsMagiskModule() {
+ synchronized (lock) {
+ if (installAsMagiskModule == null) {
+ try {
+ installAsMagiskModule = rootShell.run(null, "[ -d /sbin/.magisk/mirror -a -d /sbin/.magisk/img -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
+ } catch (final Exception ignored) {
+ installAsMagiskModule = false;
+ }
+ }
+ return installAsMagiskModule;
+ }
+ }
+}