TorProcessManager: Terminate Tor gracefully when TerminateTorOnExit = true.
MODIFY
WalletWasabi/Tor/Control/TorControlClient.cs
WalletWasabi/Tor/Control/TorControlClient.cs
@@ -49,6 +49,29 @@ namespace WalletWasabi.Tor.Control
return ProtocolInfoReply.FromReply(reply);
}
+ /// <summary>
+ /// Instructs Tor to shut down when this control connection is closed.
+ /// </summary>
+ /// <seealso href="https://gitweb.torproject.org/torspec.git/tree/control-spec.txt">See 3.23. TAKEOWNERSHIP.</seealso>
+ public Task<TorControlReply> TakeOwnershipAsync(CancellationToken cancellationToken = default)
+ => SendCommandAsync("TAKEOWNERSHIP\r\n", cancellationToken);
+
+ /// <summary>
+ /// Requests controlled shutdown.
+ /// </summary>
+ /// <remarks>If server is a client (OP), exit immediately. If it's a relay (OR), close listeners and exit after <c>ShutdownWaitLength</c> seconds.</remarks>
+ /// <seealso href="https://gitweb.torproject.org/torspec.git/tree/control-spec.txt">See 3.7. SIGNAL.</seealso>
+ public Task<TorControlReply> SignalShutdownAsync(CancellationToken cancellationToken = default)
+ => SendCommandAsync("SIGNAL SHUTDOWN\r\n", cancellationToken);
+
+ /// <summary>
+ /// Causes Tor to stop polling for the existence of a process with its owning controller's PID.
+ /// </summary>
+ /// <remarks>If TAKEOWNERSHIP command is sent, Tor will still exit when the control connection ends.</remarks>
+ /// <seealso href="https://gitweb.torproject.org/torspec.git/tree/control-spec.txt">See 3.2. RESETCONF and 3.23. TAKEOWNERSHIP.</seealso>
+ public Task<TorControlReply> ResetOwningControllerProcessConfAsync(CancellationToken cancellationToken = default)
+ => SendCommandAsync("RESETCONF __OwningControllerProcess\r\n", cancellationToken);
+
/// <summary>
/// Sends a command to Tor.
/// </summary>

MODIFY
WalletWasabi/Tor/TorProcessManager.cs
WalletWasabi/Tor/TorProcessManager.cs
@@ -7,6 +7,9 @@ using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Logging;
using WalletWasabi.Microservices;
+using WalletWasabi.Tor.Control;
+using WalletWasabi.Tor.Control.Exceptions;
+using WalletWasabi.Tor.Control.Messages;
using WalletWasabi.Tor.Socks5;
namespace WalletWasabi.Tor
@@ -21,17 +24,19 @@ namespace WalletWasabi.Tor
{
TorSocks5EndPoint = torSocks5EndPoint;
TorProcess = null;
+ TorControlClient = null;
Settings = settings;
TcpConnectionFactory = new(torSocks5EndPoint);
}
private EndPoint TorSocks5EndPoint { get; }
-
private ProcessAsync? TorProcess { get; set; }
-
private TorSettings Settings { get; }
private TorTcpConnectionFactory TcpConnectionFactory { get; }
+
+ private TorControlClient? TorControlClient { get; set; }
+
private bool _disposed = false;
/// <summary>
@@ -53,12 +58,14 @@ namespace WalletWasabi.Tor
? $"Tor is already running on {endpoint.Address}:{endpoint.Port}."
: "Tor is already running.";
Logger.LogInfo(msg);
+
+ TorControlClient = await InitTorControlOrThrowAsync(token).ConfigureAwait(false);
return true;
}
string torArguments = Settings.GetCmdArguments(TorSocks5EndPoint);
- var startInfo = new ProcessStartInfo
+ ProcessStartInfo startInfo = new()
{
FileName = Settings.TorBinaryFilePath,
Arguments = torArguments,
@@ -116,6 +123,8 @@ namespace WalletWasabi.Tor
}
Logger.LogInfo("Tor is running.");
+ TorControlClient = await InitTorControlOrThrowAsync(token).ConfigureAwait(false);
+
return true;
}
catch (OperationCanceledException ex)
@@ -131,30 +140,64 @@ namespace WalletWasabi.Tor
return false;
}
- public async Task StopAsync()
+ /// <summary>
+ /// Connects to Tor control using a TCP client or throws <see cref="TorControlException"/>.
+ /// </summary>
+ /// <exception cref="TorControlException">When authentication fails for some reason.</exception>
+ /// <seealso href="https://gitweb.torproject.org/torspec.git/tree/control-spec.txt">This method follows instructions in 3.23. TAKEOWNERSHIP.</seealso>
+ private async Task<TorControlClient> InitTorControlOrThrowAsync(CancellationToken token = default)
{
- if (TorProcess is { } && Settings.TerminateOnExit)
+ // Get cookie.
+ string cookieString = ByteHelpers.ToHex(File.ReadAllBytes(Settings.CookieAuthFilePath));
+
+ // Authenticate.
+ TorControlClientFactory factory = new();
+ TorControlClient client = await factory.ConnectAndAuthenticateAsync(Settings.ControlEndpoint, cookieString, token).ConfigureAwait(false);
+
+ if (Settings.TerminateOnExit)
{
- Logger.LogInfo($"Killing Tor process.");
+ // This is necessary for the scenario when Tor was started by a previous WW instance with TerminateTorOnExit=false configuration option.
+ TorControlReply takeReply = await client.TakeOwnershipAsync(token).ConfigureAwait(false);
- try
+ if (!takeReply)
+ {
+ throw new TorControlException($"Failed to take ownership of the Tor instance. Reply: '{takeReply}'.");
+ }
+
+ TorControlReply resetReply = await client.ResetOwningControllerProcessConfAsync(token).ConfigureAwait(false);
+
+ if (!resetReply)
{
- TorProcess.Kill();
- using CancellationTokenSource cts = new(TimeSpan.FromMinutes(1));
- await TorProcess.WaitForExitAsync(cts.Token, killOnCancel: true).ConfigureAwait(false);
+ throw new TorControlException($"Failed to reset __OwningControllerProcess. Reply: '{resetReply}'.");
+ }
+ }
- Logger.LogInfo($"Tor process killed successfully.");
+ return client;
}
- catch (Exception ex)
+
+ public async Task StopAsync()
{
- Logger.LogError($"Could not kill Tor process: {ex.Message}.");
+ _disposed = true;
+
+ if (TorControlClient is TorControlClient torControlClient)
+ {
+ // Even though terminating the TCP connection with Tor would shut down Tor,
+ // the spec is quite clear:
+ // > As of Tor 0.2.5.2-alpha, Tor does not wait a while for circuits to
+ // > close when shutting down because of an exiting controller. If you
+ // > want to ensure a clean shutdown--and you should!--then send "SIGNAL
+ // > SHUTDOWN" and wait for the Tor process to close.)
+ if (Settings.TerminateOnExit)
+ {
+ await torControlClient.SignalShutdownAsync().ConfigureAwait(false);
}
+
+ // Leads to Tor termination because we sent TAKEOWNERSHIP command.
+ torControlClient.Dispose();
}
// Dispose Tor process resources (does not stop/kill Tor process).
TorProcess?.Dispose();
-
- _disposed = true;
}
private void ThrowIfDisposed()

TorProcessManager: Terminate Tor gracefully when TerminateTorOnExit = true.