RootShell: multiplex commands
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
parent
ae2068dc16
commit
31ba7e6593
@ -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.
|
||||||
|
try {
|
||||||
if (rootShell.run(output, "wg show interfaces") != 0 || output.isEmpty())
|
if (rootShell.run(output, "wg show interfaces") != 0 || output.isEmpty())
|
||||||
return Collections.emptySet();
|
return Collections.emptySet();
|
||||||
|
} catch (Exception e) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) + ": "
|
||||||
|
@ -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=$?;");
|
||||||
|
builder.append(String.format("echo '%s' $ret;", end));
|
||||||
|
builder.append(String.format("echo '%s' $ret >&2;", end));
|
||||||
|
|
||||||
|
builder.append('\n');
|
||||||
|
|
||||||
|
Log.v(TAG, "executing: " + command);
|
||||||
|
stdin.write(builder.toString());
|
||||||
|
stdin.flush();
|
||||||
|
|
||||||
String line;
|
String line;
|
||||||
while ((line = stdoutReader.readLine()) != null) {
|
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;
|
||||||
|
}
|
||||||
|
if (line.startsWith(end) && line.length() > end.length()) {
|
||||||
|
errnoStdout = Integer.valueOf(line.substring(end.length() + 1));
|
||||||
|
++beginEnds;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (output != null)
|
if (output != null)
|
||||||
output.add(line);
|
output.add(line);
|
||||||
Log.v(TAG, "stdout: " + line);
|
Log.v(TAG, "stdout: " + line);
|
||||||
}
|
}
|
||||||
int linesOfStderr = 0;
|
first = true;
|
||||||
String stderrLast = null;
|
while ((line = stderr.readLine()) != null) {
|
||||||
while ((line = stderrReader.readLine()) != null) {
|
if (first) {
|
||||||
++linesOfStderr;
|
first = false;
|
||||||
stderrLast = line;
|
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);
|
Log.v(TAG, "stderr: " + line);
|
||||||
}
|
}
|
||||||
exitValue = process.waitFor();
|
if (errnoStderr != errnoStdout || beginEnds != 4)
|
||||||
process.destroy();
|
throw new ErrnoException("Incorrect errno reporting", OsConstants.EBADMSG);
|
||||||
if (exitValue == 1 && linesOfStderr == 1 && stderrLast.equals("Permission denied"))
|
|
||||||
exitValue = OsConstants.EACCES;
|
return errnoStdout;
|
||||||
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;
|
|
||||||
|
public class NoRootException extends Exception {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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])));
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
return rootShell.run(null, script.toString());
|
return rootShell.run(null, script.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return OsConstants.EACCES;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user