RootShell: multiplex commands

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Jason A. Donenfeld 2018-01-08 19:46:51 +01:00
parent ae2068dc16
commit 31ba7e6593
5 changed files with 150 additions and 47 deletions

View File

@ -52,8 +52,12 @@ public final class WgQuickBackend implements Backend {
public Set<String> enumerate() { public Set<String> enumerate() {
final List<String> output = new ArrayList<>(); final List<String> output = new ArrayList<>();
// Don't throw an exception here or nothing will show up in the UI. // Don't throw an exception here or nothing will show up in the UI.
if (rootShell.run(output, "wg show interfaces") != 0 || output.isEmpty()) try {
if (rootShell.run(output, "wg show interfaces") != 0 || output.isEmpty())
return Collections.emptySet();
} catch (Exception e) {
return Collections.emptySet(); return Collections.emptySet();
}
// wg puts all interface names on the same line. Split them into separate elements. // wg puts all interface names on the same line. Split them into separate elements.
return Stream.of(output.get(0).split(" ")).collect(Collectors.toUnmodifiableSet()); return Stream.of(output.get(0).split(" ")).collect(Collectors.toUnmodifiableSet());
} }
@ -86,11 +90,8 @@ public final class WgQuickBackend implements Backend {
} else { } else {
result = rootShell.run(null, String.format("wg-quick down '%s'", tunnel.getName())); result = rootShell.run(null, String.format("wg-quick down '%s'", tunnel.getName()));
} }
if (result != 0) { if (result != 0)
final String message = result == OsConstants.EACCES ? throw new Exception("wg-quick failed");
"Root access unavailable" : "wg-quick failed";
throw new Exception(message);
}
return getState(tunnel); return getState(tunnel);
} }
} }

View File

@ -19,6 +19,7 @@ import com.wireguard.android.databinding.TunnelListItemBinding;
import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State; import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.RootShell;
/** /**
* Helper method shared by TunnelListFragment and TunnelDetailFragment. * Helper method shared by TunnelListFragment and TunnelDetailFragment.
@ -60,6 +61,8 @@ public final class TunnelController {
// Make links work. // Make links work.
((TextView) dialog.findViewById(android.R.id.message)) ((TextView) dialog.findViewById(android.R.id.message))
.setMovementMethod(LinkMovementMethod.getInstance()); .setMovementMethod(LinkMovementMethod.getInstance());
} else if (throwable instanceof RootShell.NoRootException) {
Snackbar.make(view, R.string.error_rootshell, Snackbar.LENGTH_LONG).show();
} else { } else {
final String message = final String message =
context.getString(checked ? R.string.error_up : R.string.error_down) + ": " context.getString(checked ? R.string.error_up : R.string.error_down) + ": "

View File

@ -1,19 +1,22 @@
package com.wireguard.android.util; package com.wireguard.android.util;
import android.content.Context; import android.content.Context;
import android.system.ErrnoException;
import android.system.OsConstants; import android.system.OsConstants;
import android.util.Log; import android.util.Log;
import com.wireguard.android.Application.ApplicationContext; import com.wireguard.android.Application.ApplicationContext;
import com.wireguard.android.Application.ApplicationScope; import com.wireguard.android.Application.ApplicationScope;
import java.io.BufferedWriter;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.OutputStreamWriter;
import java.io.InputStreamReader;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -38,6 +41,11 @@ public class RootShell {
private final String preamble; private final String preamble;
private BufferedWriter stdin;
private BufferedReader stdout;
private BufferedReader stderr;
private Process process;
@Inject @Inject
public RootShell(@ApplicationContext final Context context) { public RootShell(@ApplicationContext final Context context) {
final String binDir = context.getCacheDir().getPath() + "/bin"; final String binDir = context.getCacheDir().getPath() + "/bin";
@ -54,6 +62,7 @@ public class RootShell {
builder.append(String.format("[ %s -ef %s ] || ln -sf %s %s || exit 31;", arg1, arg2, arg1, arg2)); builder.append(String.format("[ %s -ef %s ] || ln -sf %s %s || exit 31;", arg1, arg2, arg1, arg2));
} }
builder.append(String.format("export PATH=\"%s:$PATH\" TMPDIR=\"%s\";", binDir, tmpDir)); builder.append(String.format("export PATH=\"%s:$PATH\" TMPDIR=\"%s\";", binDir, tmpDir));
builder.append("id;\n");
preamble = builder.toString(); preamble = builder.toString();
} }
@ -68,6 +77,63 @@ public class RootShell {
return false; return false;
} }
private void ensureRoot() throws Exception {
try {
if (process != null) {
process.exitValue();
process = null;
}
} catch (IllegalThreadStateException e) {
return;
}
if (!isExecutable("su"))
throw new NoRootException();
try {
final ProcessBuilder builder = new ProcessBuilder();
builder.environment().put("LANG", "C");
builder.command("su");
process = builder.start();
stdin = new BufferedWriter(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));
Log.d(TAG, "New root shell, sending preamble: " + preamble);
stdin.write(preamble);
stdin.flush();
final String id = stdout.readLine();
try {
int errno = process.exitValue();
String line;
while ((line = stderr.readLine()) != null) {
if (line.contains("Permission denied"))
throw new NoRootException();
}
throw new ErrnoException("Unknown error when obtaining root access", errno);
} catch (IllegalThreadStateException e) {
// We're alive, so keep executing.
}
if (id == null || !id.contains("uid=0"))
throw new NoRootException();
} catch (Exception e) {
Log.w(TAG, "Session failed with exception", e);
process.destroy();
process = null;
final Matcher match = ERRNO_EXTRACTOR.matcher(e.toString());
if (match.find()) {
final int errno = Integer.valueOf(match.group(1));
if (errno == OsConstants.EACCES)
throw new NoRootException();
else
throw new ErrnoException("Unknown error when obtaining root access", errno);
}
throw e;
}
}
/** /**
* Run a command in a root shell. * Run a command in a root shell.
* *
@ -76,46 +142,74 @@ public class RootShell {
* @param command Command to run as root. * @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.
*/ */
public int run(final List<String> output, final String command) { public int run(final List<String> output, final String command) throws Exception {
int exitValue = -1; ensureRoot();
if (!isExecutable("su"))
return OsConstants.EACCES; StringBuilder builder = new StringBuilder();
try { final String marker = UUID.randomUUID().toString();
final ProcessBuilder builder = new ProcessBuilder(); final String begin = marker + " begin";
builder.environment().put("LANG", "C"); final String end = marker + " end";
builder.command("su", "-c", preamble + command);
final Process process = builder.start(); builder.append(String.format("echo '%s';", begin));
Log.d(TAG, "Running: " + command); builder.append(String.format("echo '%s' >&2;", begin));
final InputStream stdout = process.getInputStream();
final InputStream stderr = process.getErrorStream(); builder.append('(');
final BufferedReader stdoutReader = builder.append(command);
new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8)); builder.append(");");
final BufferedReader stderrReader =
new BufferedReader(new InputStreamReader(stderr, StandardCharsets.UTF_8)); builder.append("ret=$?;");
String line; builder.append(String.format("echo '%s' $ret;", end));
while ((line = stdoutReader.readLine()) != null) { builder.append(String.format("echo '%s' $ret >&2;", end));
if (output != null)
output.add(line); builder.append('\n');
Log.v(TAG, "stdout: " + line);
Log.v(TAG, "executing: " + command);
stdin.write(builder.toString());
stdin.flush();
String line;
boolean first = true;
int errnoStdout = -1, errnoStderr = -2;
int beginEnds = 0;
while ((line = stdout.readLine()) != null) {
if (first) {
first = false;
if (!line.startsWith(begin))
throw new ErrnoException("Could not find begin marker", OsConstants.EBADMSG);
++beginEnds;
continue;
} }
int linesOfStderr = 0; if (line.startsWith(end) && line.length() > end.length()) {
String stderrLast = null; errnoStdout = Integer.valueOf(line.substring(end.length() + 1));
while ((line = stderrReader.readLine()) != null) { ++beginEnds;
++linesOfStderr; break;
stderrLast = line;
Log.v(TAG, "stderr: " + line);
} }
exitValue = process.waitFor(); if (output != null)
process.destroy(); output.add(line);
if (exitValue == 1 && linesOfStderr == 1 && stderrLast.equals("Permission denied")) Log.v(TAG, "stdout: " + line);
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())
exitValue = Integer.valueOf(match.group(1));
} }
return exitValue; first = true;
while ((line = stderr.readLine()) != null) {
if (first) {
first = false;
if (!line.startsWith(begin))
throw new ErrnoException("Could not find begin marker", OsConstants.EBADMSG);
++beginEnds;
continue;
}
if (line.startsWith(end) && line.length() > end.length()) {
errnoStderr = Integer.valueOf(line.substring(end.length() + 1));
++beginEnds;
break;
}
Log.v(TAG, "stderr: " + line);
}
if (errnoStderr != errnoStdout || beginEnds != 4)
throw new ErrnoException("Incorrect errno reporting", OsConstants.EBADMSG);
return errnoStdout;
}
public class NoRootException extends Exception {
} }
} }

View File

@ -65,6 +65,10 @@ public final class ToolsInstaller {
new File(installDir, names[1]), new File(installDir, names[1]),
new File(installDir, names[1]))); new File(installDir, names[1])));
} }
return rootShell.run(null, script.toString()); try {
return rootShell.run(null, script.toString());
} catch (Exception e) {
return OsConstants.EACCES;
}
} }
} }

View File

@ -17,6 +17,7 @@
<string name="endpoint">Endpoint</string> <string name="endpoint">Endpoint</string>
<string name="error_down">Error bringing down WireGuard tunnel</string> <string name="error_down">Error bringing down WireGuard tunnel</string>
<string name="error_up">Error bringing up WireGuard tunnel</string> <string name="error_up">Error bringing up WireGuard tunnel</string>
<string name="error_rootshell">Please obtain root access and try again</string>
<string name="generate">Generate</string> <string name="generate">Generate</string>
<string name="hint_automatic">(auto)</string> <string name="hint_automatic">(auto)</string>
<string name="hint_generated">(generated)</string> <string name="hint_generated">(generated)</string>