Shut down the Tor process without hacks.

Tor has a controller command, TAKEOWNERSHIP, and a configuration option,
__OwningControllerProcess, that work together to ensure Tor shuts down
when the controlling process dies and/or disconnects from the control
port. By using them we can avoid creating runaway Tor processes that
have to be killed with hacks.
This commit is contained in:
akwizgran
2014-07-08 22:35:37 +01:00
parent 7d9ce4c973
commit d406853f68
5 changed files with 19 additions and 123 deletions

View File

@@ -1,7 +1,6 @@
ControlPort 59051 ControlPort 59051
CookieAuthentication 1 CookieAuthentication 1
DisableNetwork 1 DisableNetwork 1
PidFile pid
RunAsDaemon 1 RunAsDaemon 1
SafeSocks 1 SafeSocks 1
SocksPort 59050 SocksPort 59050

View File

@@ -30,8 +30,8 @@ public class SplashScreenActivity extends RoboSplashActivity {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(SplashScreenActivity.class.getName()); Logger.getLogger(SplashScreenActivity.class.getName());
// This build expires on 17 May 2014 // This build expires on 12 July 2014
private static final long EXPIRY_DATE = 1400284800 * 1000L; private static final long EXPIRY_DATE = 1405123200 * 1000L;
private long now = System.currentTimeMillis(); private long now = System.currentTimeMillis();
@@ -61,6 +61,7 @@ public class SplashScreenActivity extends RoboSplashActivity {
setContentView(layout); setContentView(layout);
} }
@Override
protected void startNextActivity() { protected void startNextActivity() {
long duration = System.currentTimeMillis() - now; long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO)) if(LOG.isLoggable(INFO))

View File

@@ -7,7 +7,6 @@ import java.util.concurrent.Executor;
import org.briarproject.api.android.AndroidExecutor; import org.briarproject.api.android.AndroidExecutor;
import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.lifecycle.ShutdownManager;
import org.briarproject.api.plugins.PluginExecutor; import org.briarproject.api.plugins.PluginExecutor;
import org.briarproject.api.plugins.duplex.DuplexPluginConfig; import org.briarproject.api.plugins.duplex.DuplexPluginConfig;
import org.briarproject.api.plugins.duplex.DuplexPluginFactory; import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
@@ -26,6 +25,7 @@ import com.google.inject.Provides;
public class AndroidPluginsModule extends AbstractModule { public class AndroidPluginsModule extends AbstractModule {
@Override
protected void configure() {} protected void configure() {}
@Provides @Provides
@@ -41,14 +41,13 @@ public class AndroidPluginsModule extends AbstractModule {
DuplexPluginConfig getDuplexPluginConfig( DuplexPluginConfig getDuplexPluginConfig(
@PluginExecutor Executor pluginExecutor, @PluginExecutor Executor pluginExecutor,
AndroidExecutor androidExecutor, Application app, AndroidExecutor androidExecutor, Application app,
CryptoComponent crypto, LocationUtils locationUtils, CryptoComponent crypto, LocationUtils locationUtils) {
ShutdownManager shutdownManager) {
Context appContext = app.getApplicationContext(); Context appContext = app.getApplicationContext();
DuplexPluginFactory bluetooth = new DroidtoothPluginFactory( DuplexPluginFactory bluetooth = new DroidtoothPluginFactory(
pluginExecutor, androidExecutor, appContext, pluginExecutor, androidExecutor, appContext,
crypto.getSecureRandom()); crypto.getSecureRandom());
DuplexPluginFactory tor = new TorPluginFactory(pluginExecutor, DuplexPluginFactory tor = new TorPluginFactory(pluginExecutor,
appContext, locationUtils, shutdownManager); appContext, locationUtils);
DuplexPluginFactory lan = new AndroidLanTcpPluginFactory( DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(
pluginExecutor, appContext); pluginExecutor, appContext);
final Collection<DuplexPluginFactory> factories = final Collection<DuplexPluginFactory> factories =

View File

@@ -38,7 +38,6 @@ import org.briarproject.api.TransportConfig;
import org.briarproject.api.TransportId; import org.briarproject.api.TransportId;
import org.briarproject.api.TransportProperties; import org.briarproject.api.TransportProperties;
import org.briarproject.api.crypto.PseudoRandom; import org.briarproject.api.crypto.PseudoRandom;
import org.briarproject.api.lifecycle.ShutdownManager;
import org.briarproject.api.plugins.duplex.DuplexPlugin; import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback; import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexTransportConnection; import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
@@ -64,6 +63,7 @@ class TorPlugin implements DuplexPlugin, EventHandler {
private static final String[] EVENTS = { private static final String[] EVENTS = {
"CIRC", "ORCONN", "NOTICE", "WARN", "ERR" "CIRC", "ORCONN", "NOTICE", "WARN", "ERR"
}; };
private static final String OWNER = "__OwningControllerProcess";
private static final int SOCKS_PORT = 59050, CONTROL_PORT = 59051; private static final int SOCKS_PORT = 59050, CONTROL_PORT = 59051;
private static final int COOKIE_TIMEOUT = 3000; // Milliseconds private static final int COOKIE_TIMEOUT = 3000; // Milliseconds
private static final int HOSTNAME_TIMEOUT = 30 * 1000; // Milliseconds private static final int HOSTNAME_TIMEOUT = 30 * 1000; // Milliseconds
@@ -75,31 +75,26 @@ class TorPlugin implements DuplexPlugin, EventHandler {
private final Executor pluginExecutor; private final Executor pluginExecutor;
private final Context appContext; private final Context appContext;
private final LocationUtils locationUtils; private final LocationUtils locationUtils;
private final ShutdownManager shutdownManager;
private final DuplexPluginCallback callback; private final DuplexPluginCallback callback;
private final int maxFrameLength; private final int maxFrameLength;
private final long maxLatency, pollingInterval; private final long maxLatency, pollingInterval;
private final File torDirectory, torFile, geoIpFile, configFile, doneFile; private final File torDirectory, torFile, geoIpFile, configFile, doneFile;
private final File cookieFile, pidFile, hostnameFile; private final File cookieFile, hostnameFile;
private final AtomicBoolean circuitBuilt; private final AtomicBoolean circuitBuilt;
private volatile boolean running = false, networkEnabled = false; private volatile boolean running = false, networkEnabled = false;
private volatile boolean bootstrapped = false; private volatile boolean bootstrapped = false;
private volatile Process tor = null;
private volatile int pid = -1;
private volatile ServerSocket socket = null; private volatile ServerSocket socket = null;
private volatile Socket controlSocket = null; private volatile Socket controlSocket = null;
private volatile TorControlConnection controlConnection = null; private volatile TorControlConnection controlConnection = null;
private volatile BroadcastReceiver networkStateReceiver = null; private volatile BroadcastReceiver networkStateReceiver = null;
TorPlugin(Executor pluginExecutor, Context appContext, TorPlugin(Executor pluginExecutor, Context appContext,
LocationUtils locationUtils, ShutdownManager shutdownManager, LocationUtils locationUtils, DuplexPluginCallback callback,
DuplexPluginCallback callback, int maxFrameLength, long maxLatency, int maxFrameLength, long maxLatency, long pollingInterval) {
long pollingInterval) {
this.pluginExecutor = pluginExecutor; this.pluginExecutor = pluginExecutor;
this.appContext = appContext; this.appContext = appContext;
this.locationUtils = locationUtils; this.locationUtils = locationUtils;
this.shutdownManager = shutdownManager;
this.callback = callback; this.callback = callback;
this.maxFrameLength = maxFrameLength; this.maxFrameLength = maxFrameLength;
this.maxLatency = maxLatency; this.maxLatency = maxLatency;
@@ -110,7 +105,6 @@ class TorPlugin implements DuplexPlugin, EventHandler {
configFile = new File(torDirectory, "torrc"); configFile = new File(torDirectory, "torrc");
doneFile = new File(torDirectory, "done"); doneFile = new File(torDirectory, "done");
cookieFile = new File(torDirectory, ".tor/control_auth_cookie"); cookieFile = new File(torDirectory, ".tor/control_auth_cookie");
pidFile = new File(torDirectory, ".tor/pid");
hostnameFile = new File(torDirectory, "hostname"); hostnameFile = new File(torDirectory, "hostname");
circuitBuilt = new AtomicBoolean(false); circuitBuilt = new AtomicBoolean(false);
} }
@@ -133,17 +127,9 @@ class TorPlugin implements DuplexPlugin, EventHandler {
try { try {
controlSocket = new Socket("127.0.0.1", CONTROL_PORT); controlSocket = new Socket("127.0.0.1", CONTROL_PORT);
LOG.info("Tor is already running"); LOG.info("Tor is already running");
if(readPidFile() == -1) {
LOG.info("Could not read PID of Tor process");
controlSocket.close();
killZombieProcess();
startProcess = true;
}
} catch(IOException e) { } catch(IOException e) {
LOG.info("Tor is not running"); LOG.info("Tor is not running");
startProcess = true; startProcess = true;
}
if(startProcess) {
// Install the binary, possibly overwriting an older version // Install the binary, possibly overwriting an older version
if(!installBinary()) { if(!installBinary()) {
LOG.warning("Could not install Tor binary"); LOG.warning("Could not install Tor binary");
@@ -164,23 +150,25 @@ class TorPlugin implements DuplexPlugin, EventHandler {
// Start a new Tor process // Start a new Tor process
String torPath = torFile.getAbsolutePath(); String torPath = torFile.getAbsolutePath();
String configPath = configFile.getAbsolutePath(); String configPath = configFile.getAbsolutePath();
String[] cmd = { torPath, "-f", configPath }; String pid = String.valueOf(android.os.Process.myPid());
String[] cmd = { torPath, "-f", configPath, OWNER, pid };
String[] env = { "HOME=" + torDirectory.getAbsolutePath() }; String[] env = { "HOME=" + torDirectory.getAbsolutePath() };
Process torProcess;
try { try {
tor = Runtime.getRuntime().exec(cmd, env, torDirectory); torProcess = Runtime.getRuntime().exec(cmd, env, torDirectory);
} catch(SecurityException e1) { } catch(SecurityException e1) {
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e1.toString(), e1); if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e1.toString(), e1);
return false; return false;
} }
// Log the process's standard output until it detaches // Log the process's standard output until it detaches
if(LOG.isLoggable(INFO)) { if(LOG.isLoggable(INFO)) {
Scanner stdout = new Scanner(tor.getInputStream()); Scanner stdout = new Scanner(torProcess.getInputStream());
while(stdout.hasNextLine()) LOG.info(stdout.nextLine()); while(stdout.hasNextLine()) LOG.info(stdout.nextLine());
stdout.close(); stdout.close();
} }
try { try {
// Wait for the process to detach or exit // Wait for the process to detach or exit
int exit = tor.waitFor(); int exit = torProcess.waitFor();
if(exit != 0) { if(exit != 0) {
if(LOG.isLoggable(WARNING)) if(LOG.isLoggable(WARNING))
LOG.warning("Tor exited with value " + exit); LOG.warning("Tor exited with value " + exit);
@@ -201,20 +189,12 @@ class TorPlugin implements DuplexPlugin, EventHandler {
controlSocket = new Socket("127.0.0.1", CONTROL_PORT); controlSocket = new Socket("127.0.0.1", CONTROL_PORT);
} }
running = true; running = true;
// Read the PID of the Tor process so we can kill it if necessary
pid = readPidFile();
// Create a shutdown hook to ensure the Tor process is killed
shutdownManager.addShutdownHook(new Runnable() {
public void run() {
killTorProcess();
killZombieProcess();
}
});
// Open a control connection and authenticate using the cookie file // Open a control connection and authenticate using the cookie file
controlConnection = new TorControlConnection(controlSocket); controlConnection = new TorControlConnection(controlSocket);
controlConnection.authenticate(read(cookieFile)); controlConnection.authenticate(read(cookieFile));
// Tell Tor to exit when the control connection is closed // Tell Tor to exit when the control connection is closed
controlConnection.takeOwnership(); controlConnection.takeOwnership();
controlConnection.resetConf(Arrays.asList(OWNER));
// Register to receive events from the Tor process // Register to receive events from the Tor process
controlConnection.setEventHandler(this); controlConnection.setEventHandler(this);
controlConnection.setEvents(Arrays.asList(EVENTS)); controlConnection.setEvents(Arrays.asList(EVENTS));
@@ -370,83 +350,6 @@ class TorPlugin implements DuplexPlugin, EventHandler {
} }
} }
private int readPidFile() {
// Read the PID of the Tor process so we can kill it if necessary
try {
return Integer.parseInt(new String(read(pidFile), "UTF-8").trim());
} catch(IOException e) {
LOG.warning("Could not read PID file");
} catch(NumberFormatException e) {
LOG.warning("Could not parse PID file");
}
return -1;
}
/*
* If the app crashes, leaving a Tor process running, and the user clears
* the app's data, removing the PID file and auth cookie file, it's no
* longer possible to communicate with the zombie process and it must be
* killed. ActivityManager.killBackgroundProcesses() doesn't seem to work
* in this case, so we must parse the output of ps to get the PID.
* <p>
* On all devices we've tested, the output consists of a header line
* followed by one line per process. The second column is the PID and the
* last column is the process name, which includes the app's package name.
* On some devices tested by the Guardian Project, the first column is the
* PID.
*/
private void killZombieProcess() {
String packageName = "/" + appContext.getPackageName() + "/";
try {
// Parse the output of ps
Process ps = Runtime.getRuntime().exec("ps");
Scanner scanner = new Scanner(ps.getInputStream());
// Discard the header line
if(scanner.hasNextLine()) scanner.nextLine();
// Look for any Tor processes with our package name
boolean found = false;
while(scanner.hasNextLine()) {
String[] columns = scanner.nextLine().split("\\s+");
if(columns.length < 3) continue;
int pid;
try {
pid = Integer.parseInt(columns[1]);
} catch(NumberFormatException e) {
try {
pid = Integer.parseInt(columns[0]);
} catch(NumberFormatException e1) {
continue;
}
}
String name = columns[columns.length - 1];
if(name.contains(packageName) && name.endsWith("/tor")) {
if(LOG.isLoggable(INFO))
LOG.info("Killing zombie process " + pid);
android.os.Process.killProcess(pid);
found = true;
}
}
if(!found) LOG.info("No zombies found");
scanner.close();
} catch(IOException e) {
LOG.warning("Could not parse ps output");
} catch(SecurityException e) {
LOG.warning("Could not execute ps");
}
}
private void killTorProcess() {
if(tor != null) {
LOG.info("Killing Tor via destroy()");
tor.destroy();
}
if(pid != -1) {
if(LOG.isLoggable(INFO))
LOG.info("Killing Tor via killProcess(" + pid + ")");
android.os.Process.killProcess(pid);
}
}
private void bind() { private void bind() {
pluginExecutor.execute(new Runnable() { pluginExecutor.execute(new Runnable() {
public void run() { public void run() {
@@ -581,8 +484,6 @@ class TorPlugin implements DuplexPlugin, EventHandler {
controlSocket.close(); controlSocket.close();
} catch(IOException e) { } catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
killTorProcess();
killZombieProcess();
} }
} }

View File

@@ -4,7 +4,6 @@ import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.briarproject.api.TransportId; import org.briarproject.api.TransportId;
import org.briarproject.api.lifecycle.ShutdownManager;
import org.briarproject.api.plugins.duplex.DuplexPlugin; import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback; import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexPluginFactory; import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
@@ -25,14 +24,12 @@ public class TorPluginFactory implements DuplexPluginFactory {
private final Executor pluginExecutor; private final Executor pluginExecutor;
private final Context appContext; private final Context appContext;
private final LocationUtils locationUtils; private final LocationUtils locationUtils;
private final ShutdownManager shutdownManager;
public TorPluginFactory(Executor pluginExecutor, Context appContext, public TorPluginFactory(Executor pluginExecutor, Context appContext,
LocationUtils locationUtils, ShutdownManager shutdownManager) { LocationUtils locationUtils) {
this.pluginExecutor = pluginExecutor; this.pluginExecutor = pluginExecutor;
this.appContext = appContext; this.appContext = appContext;
this.locationUtils = locationUtils; this.locationUtils = locationUtils;
this.shutdownManager = shutdownManager;
} }
public TransportId getId() { public TransportId getId() {
@@ -46,7 +43,6 @@ public class TorPluginFactory implements DuplexPluginFactory {
return null; return null;
} }
return new TorPlugin(pluginExecutor,appContext, locationUtils, return new TorPlugin(pluginExecutor,appContext, locationUtils,
shutdownManager, callback, MAX_FRAME_LENGTH, MAX_LATENCY, callback, MAX_FRAME_LENGTH, MAX_LATENCY, POLLING_INTERVAL);
POLLING_INTERVAL);
} }
} }