RootShell: rewrite
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
parent
6d1117a94c
commit
b7a6b44ec1
@ -1,13 +1,14 @@
|
|||||||
package com.wireguard.android.backends;
|
package com.wireguard.android.backends;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.system.OsConstants;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
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
|
* 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.
|
* 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 = "WireGuard/RootShell";
|
||||||
private static final String TAG = "RootShell";
|
|
||||||
private static final Pattern ERRNO_EXTRACTOR = Pattern.compile("error=(\\d+)");
|
private static final Pattern ERRNO_EXTRACTOR = Pattern.compile("error=(\\d+)");
|
||||||
|
|
||||||
private final byte[] setupCommands;
|
private static final String[][] libraryNamedExecutables = {
|
||||||
private final String shell;
|
{ "libwg.so", "wg" },
|
||||||
|
{ "libwg-quick.so", "wg-quick" }
|
||||||
|
};
|
||||||
|
|
||||||
|
private final String preamble;
|
||||||
|
|
||||||
RootShell(final Context context) {
|
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) {
|
new File(binDir).mkdirs();
|
||||||
final String tmpdir = context.getCacheDir().getPath();
|
new File(tmpDir).mkdirs();
|
||||||
final String fakelibdir = context.getApplicationInfo().nativeLibraryDir;
|
|
||||||
setupCommands = String.format(SETUP_TEMPLATE, tmpdir, tmpdir, tmpdir, tmpdir, fakelibdir, tmpdir, fakelibdir, tmpdir).getBytes(StandardCharsets.UTF_8);
|
preamble = String.format("export PATH=\"%s:$PATH\" TMPDIR=\"%s\";", binDir, tmpDir);
|
||||||
this.shell = shell;
|
|
||||||
|
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
|
* Run a command in a root shell.
|
||||||
* process, so they can be considered a shell script.
|
|
||||||
*
|
*
|
||||||
* @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.
|
* 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.
|
* @return The exit value of the last command run, or -1 if there was an internal error.
|
||||||
*/
|
*/
|
||||||
int run(final List<String> output, final String... commands) {
|
int run(final List<String> output, final String command) {
|
||||||
if (commands.length < 1)
|
|
||||||
throw new IndexOutOfBoundsException("At least one command must be supplied");
|
|
||||||
int exitValue = -1;
|
int exitValue = -1;
|
||||||
try {
|
try {
|
||||||
final ProcessBuilder builder = new ProcessBuilder().redirectErrorStream(true);
|
final ProcessBuilder builder = new ProcessBuilder();
|
||||||
final Process process = builder.command(shell).start();
|
builder.environment().put("LANG", "C");
|
||||||
final OutputStream stdin = process.getOutputStream();
|
builder.command("su", "-c", preamble + command);
|
||||||
stdin.write(setupCommands);
|
final Process process = builder.start();
|
||||||
for (final String command : commands)
|
Log.d(TAG, "Running: " + command);
|
||||||
stdin.write(command.concat("\n").getBytes(StandardCharsets.UTF_8));
|
|
||||||
stdin.close();
|
|
||||||
Log.d(TAG, "Sent " + commands.length + " command(s), now reading output");
|
|
||||||
final InputStream stdout = process.getInputStream();
|
final InputStream stdout = process.getInputStream();
|
||||||
|
final InputStream stderr = process.getErrorStream();
|
||||||
final BufferedReader stdoutReader =
|
final BufferedReader stdoutReader =
|
||||||
new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8));
|
new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8));
|
||||||
|
final BufferedReader stderrReader =
|
||||||
|
new BufferedReader(new InputStreamReader(stderr, StandardCharsets.UTF_8));
|
||||||
String line;
|
String line;
|
||||||
String lastLine = null;
|
|
||||||
while ((line = stdoutReader.readLine()) != null) {
|
while ((line = stdoutReader.readLine()) != null) {
|
||||||
Log.v(TAG, line);
|
|
||||||
lastLine = line;
|
|
||||||
if (output != null)
|
if (output != null)
|
||||||
output.add(line);
|
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();
|
process.destroy();
|
||||||
if (lastLine != null) {
|
if (exitValue == 1 && linesOfStderr == 1 && stderrLast.equals("Permission denied"))
|
||||||
// Remove the exit value line from the output
|
exitValue = OsConstants.EACCES;
|
||||||
if (output != null)
|
Log.d(TAG, "Exit status: " + exitValue);
|
||||||
output.remove(output.size() - 1);
|
} catch (IOException | InterruptedException e) {
|
||||||
exitValue = Integer.parseInt(lastLine);
|
|
||||||
}
|
|
||||||
Log.d(TAG, "Session completed with exit value " + exitValue);
|
|
||||||
} catch (IOException | InterruptedException | NumberFormatException e) {
|
|
||||||
Log.w(TAG, "Session failed with exception", e);
|
Log.w(TAG, "Session failed with exception", e);
|
||||||
final Matcher match = ERRNO_EXTRACTOR.matcher(e.toString());
|
final Matcher match = ERRNO_EXTRACTOR.matcher(e.toString());
|
||||||
if (match.find())
|
if (match.find())
|
||||||
|
@ -14,6 +14,8 @@ import android.os.IBinder;
|
|||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.provider.OpenableColumns;
|
import android.provider.OpenableColumns;
|
||||||
import android.service.quicksettings.TileService;
|
import android.service.quicksettings.TileService;
|
||||||
|
import android.system.ErrnoException;
|
||||||
|
import android.system.OsConstants;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.widget.Toast;
|
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_ENABLED_CONFIGS = "enabled_configs";
|
||||||
public static final String KEY_PRIMARY_CONFIG = "primary_config";
|
public static final String KEY_PRIMARY_CONFIG = "primary_config";
|
||||||
public static final String KEY_RESTORE_ON_BOOT = "restore_on_boot";
|
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 static VpnService instance;
|
||||||
private final IBinder binder = new Binder();
|
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());
|
Log.i(TAG, "Running wg-quick up for " + config.getName());
|
||||||
final File configFile = new File(getFilesDir(), config.getName() + ".conf");
|
final File configFile = new File(getFilesDir(), config.getName() + ".conf");
|
||||||
final int ret = rootShell.run(null, "wg-quick up '" + configFile.getPath() + "'");
|
final int ret = rootShell.run(null, "wg-quick up '" + configFile.getPath() + "'");
|
||||||
if (ret == 13 /* EPERM */)
|
if (ret == OsConstants.EACCES)
|
||||||
return -0xfff0002;
|
return -0xfff0002;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user