From b7a6b44ec1df88ac83955d2ae7a01a7b705b9550 Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Sat, 16 Dec 2017 06:17:27 +0100 Subject: [PATCH] RootShell: rewrite Signed-off-by: Jason A. Donenfeld --- .../wireguard/android/backends/RootShell.java | 88 +++++++++++-------- .../android/backends/VpnService.java | 6 +- 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/wireguard/android/backends/RootShell.java b/app/src/main/java/com/wireguard/android/backends/RootShell.java index 31b10f98..44b9dd8c 100644 --- a/app/src/main/java/com/wireguard/android/backends/RootShell.java +++ b/app/src/main/java/com/wireguard/android/backends/RootShell.java @@ -1,13 +1,14 @@ package com.wireguard.android.backends; import android.content.Context; +import android.system.OsConstants; import android.util.Log; import java.io.BufferedReader; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.regex.Pattern; @@ -22,67 +23,76 @@ class RootShell { * Setup commands that are run at the beginning of each root shell. The trap command ensures * access to the return value of the last command, since su itself always exits with 0. */ - private static final String SETUP_TEMPLATE = "export PATH=\"%s/bin:$PATH\"; export TMPDIR=\"%s/temp\"; trap 'echo $?' EXIT; mkdir -p \"%s/bin\" \"%s/temp\"; ln -fs \"%s/libwg.so\" \"%s/bin/wg\" || exit 99; ln -fs \"%s/libwg-quick.so\" \"%s/bin/wg-quick\" || exit 99;"; - private static final String TAG = "RootShell"; + private static final String TAG = "WireGuard/RootShell"; private static final Pattern ERRNO_EXTRACTOR = Pattern.compile("error=(\\d+)"); - private final byte[] setupCommands; - private final String shell; + private static final String[][] libraryNamedExecutables = { + { "libwg.so", "wg" }, + { "libwg-quick.so", "wg-quick" } + }; + + private final String preamble; RootShell(final Context context) { - this(context, "su"); - } + final String binDir = context.getCacheDir().getPath() + "/bin"; + final String tmpDir = context.getCacheDir().getPath() + "/tmp"; - RootShell(final Context context, final String shell) { - final String tmpdir = context.getCacheDir().getPath(); - final String fakelibdir = context.getApplicationInfo().nativeLibraryDir; - setupCommands = String.format(SETUP_TEMPLATE, tmpdir, tmpdir, tmpdir, tmpdir, fakelibdir, tmpdir, fakelibdir, tmpdir).getBytes(StandardCharsets.UTF_8); - this.shell = shell; + new File(binDir).mkdirs(); + new File(tmpDir).mkdirs(); + + preamble = String.format("export PATH=\"%s:$PATH\" TMPDIR=\"%s\";", binDir, tmpDir); + + final String libDir = context.getApplicationInfo().nativeLibraryDir; + String symlinkCommand = "set -ex;"; + for (final String[] libraryNamedExecutable : libraryNamedExecutables) { + final String args = "'" + libDir + "/" + libraryNamedExecutable[0] + "' '" + binDir + "/" + libraryNamedExecutable[1] + "'"; + symlinkCommand += "ln -f " + args + " || ln -sf " + args + ";"; + } + if (run(null, symlinkCommand) != 0) + Log.e(TAG, "Unable to establish symlinks for important executables."); } /** - * Run a series of commands in a root shell. These commands are all sent to the same shell - * process, so they can be considered a shell script. + * Run a command in a root shell. * - * @param output Lines read from stdout and stderr are appended to this list. Pass null if the + * @param output Lines read from stdout are appended to this list. Pass null if the * output from the shell is not important. - * @param commands One or more commands to run as root (each element is a separate line). + * @param command Command to run as root. * @return The exit value of the last command run, or -1 if there was an internal error. */ - int run(final List output, final String... commands) { - if (commands.length < 1) - throw new IndexOutOfBoundsException("At least one command must be supplied"); + int run(final List output, final String command) { int exitValue = -1; try { - final ProcessBuilder builder = new ProcessBuilder().redirectErrorStream(true); - final Process process = builder.command(shell).start(); - final OutputStream stdin = process.getOutputStream(); - stdin.write(setupCommands); - for (final String command : commands) - stdin.write(command.concat("\n").getBytes(StandardCharsets.UTF_8)); - stdin.close(); - Log.d(TAG, "Sent " + commands.length + " command(s), now reading output"); + final ProcessBuilder builder = new ProcessBuilder(); + builder.environment().put("LANG", "C"); + builder.command("su", "-c", preamble + command); + final Process process = builder.start(); + Log.d(TAG, "Running: " + command); final InputStream stdout = process.getInputStream(); + final InputStream stderr = process.getErrorStream(); final BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8)); + final BufferedReader stderrReader = + new BufferedReader(new InputStreamReader(stderr, StandardCharsets.UTF_8)); String line; - String lastLine = null; while ((line = stdoutReader.readLine()) != null) { - Log.v(TAG, line); - lastLine = line; if (output != null) output.add(line); + Log.v(TAG, "stdout: " + line); } - process.waitFor(); + int linesOfStderr = 0; + String stderrLast = null; + while ((line = stderrReader.readLine()) != null) { + ++linesOfStderr; + stderrLast = line; + Log.v(TAG, "stderr: " + line); + } + exitValue = process.waitFor(); process.destroy(); - if (lastLine != null) { - // Remove the exit value line from the output - if (output != null) - output.remove(output.size() - 1); - exitValue = Integer.parseInt(lastLine); - } - Log.d(TAG, "Session completed with exit value " + exitValue); - } catch (IOException | InterruptedException | NumberFormatException e) { + if (exitValue == 1 && linesOfStderr == 1 && stderrLast.equals("Permission denied")) + exitValue = OsConstants.EACCES; + Log.d(TAG, "Exit status: " + exitValue); + } catch (IOException | InterruptedException e) { Log.w(TAG, "Session failed with exception", e); final Matcher match = ERRNO_EXTRACTOR.matcher(e.toString()); if (match.find()) diff --git a/app/src/main/java/com/wireguard/android/backends/VpnService.java b/app/src/main/java/com/wireguard/android/backends/VpnService.java index 5e254d2e..ad16fb36 100644 --- a/app/src/main/java/com/wireguard/android/backends/VpnService.java +++ b/app/src/main/java/com/wireguard/android/backends/VpnService.java @@ -14,6 +14,8 @@ import android.os.IBinder; import android.preference.PreferenceManager; import android.provider.OpenableColumns; import android.service.quicksettings.TileService; +import android.system.ErrnoException; +import android.system.OsConstants; import android.util.Log; import android.widget.Toast; @@ -48,7 +50,7 @@ public class VpnService extends Service public static final String KEY_ENABLED_CONFIGS = "enabled_configs"; public static final String KEY_PRIMARY_CONFIG = "primary_config"; public static final String KEY_RESTORE_ON_BOOT = "restore_on_boot"; - private static final String TAG = "VpnService"; + private static final String TAG = "WireGuard/VpnService"; private static VpnService instance; private final IBinder binder = new Binder(); @@ -275,7 +277,7 @@ public class VpnService extends Service Log.i(TAG, "Running wg-quick up for " + config.getName()); final File configFile = new File(getFilesDir(), config.getName() + ".conf"); final int ret = rootShell.run(null, "wg-quick up '" + configFile.getPath() + "'"); - if (ret == 13 /* EPERM */) + if (ret == OsConstants.EACCES) return -0xfff0002; return ret; }