Implement `ReportCircuitClosedAsync`
MODIFY
WalletWasabi.Fluent/Global.cs
WalletWasabi.Fluent/Global.cs
@@ -237,8 +237,7 @@ public class Global
Logger.LogInfo($"{nameof(TorProcessManager)} is initialized.");
}
- Tor.Http.TorHttpClient torHttpClient = BackendHttpClientFactory.NewTorHttpClient(Mode.DefaultCircuit);
- HostedServices.Register<TorMonitor>(() => new TorMonitor(period: TimeSpan.FromSeconds(3), fallbackBackendUri: Config.GetFallbackBackendUri(), torHttpClient, TorManager), nameof(TorMonitor));
+ HostedServices.Register<TorMonitor>(() => new TorMonitor(period: TimeSpan.FromSeconds(3), Config.GetFallbackBackendUri(), TorManager, BackendHttpClientFactory), nameof(TorMonitor));
}
}

MODIFY
WalletWasabi.Tests/UnitTests/Tor/Control/Messages/Events/CircEventTests.cs
WalletWasabi.Tests/UnitTests/Tor/Control/Messages/Events/CircEventTests.cs
@@ -2,7 +2,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using WalletWasabi.Tor.Control.Messages;
using WalletWasabi.Tor.Control.Messages.CircuitStatus;
-using WalletWasabi.Tor.Control.Messages.Events.StatusEvents;
+using WalletWasabi.Tor.Control.Messages.Events;
using Xunit;
namespace WalletWasabi.Tests.UnitTests.Tor.Control.Messages.Events;

MODIFY
WalletWasabi/Tor/Control/Messages/Events/CircEvent.cs
WalletWasabi/Tor/Control/Messages/Events/CircEvent.cs
@@ -2,7 +2,7 @@ using WalletWasabi.Tor.Control.Exceptions;
using WalletWasabi.Tor.Control.Messages.CircuitStatus;
using WalletWasabi.Tor.Control.Utils;
-namespace WalletWasabi.Tor.Control.Messages.Events.StatusEvents;
+namespace WalletWasabi.Tor.Control.Messages.Events;
/// <summary>Circuit event as specified in <c>4.1.1. Circuit status changed</c> spec.</summary>
/// <seealso href="https://gitweb.torproject.org/torspec.git/tree/control-spec.txt"/>

MODIFY
WalletWasabi/Tor/Socks5/Pool/TorHttpPool.cs
WalletWasabi/Tor/Socks5/Pool/TorHttpPool.cs
@@ -321,6 +321,29 @@ public class TorHttpPool : IDisposable
}
}
+ /// <summary>Allows to report that a Tor circuit was closed.</summary>
+ /// <param name="circuitName">Name of the circuit. Example is: <c>IK1DG1HZCZFEQUTF86O86</c>.</param>
+ /// <remarks>
+ /// Useful to clean up <see cref="ConnectionPerHost"/> so that we do not exhaust <see cref="MaxConnectionsPerHost"/> limit.
+ /// <para>If client code forgets to dispose <see cref="PersonCircuit"/>, this should help us to recover eventually.</para>
+ /// </remarks>
+ public async Task ReportCircuitClosedAsync(string circuitName, CancellationToken cancellationToken)
+ {
+ using (await ObtainPoolConnectionLock.LockAsync(cancellationToken).ConfigureAwait(false))
+ {
+ foreach ((string host, List<TorTcpConnection> tcpConnections) in ConnectionPerHost)
+ {
+ foreach (TorTcpConnection tcpConnection in tcpConnections)
+ {
+ if (tcpConnection.Circuit.Name == circuitName)
+ {
+ tcpConnection.MarkAsToDispose(force: false);
+ }
+ }
+ }
+ }
+ }
+
private static string GetRequestHost(HttpRequestMessage request)
{
return Guard.NotNullOrEmptyOrWhitespace(nameof(request.RequestUri.DnsSafeHost), request.RequestUri!.DnsSafeHost, trim: true);
@@ -330,6 +353,7 @@ public class TorHttpPool : IDisposable
/// <param name="host">URI's host value.</param>
/// <param name="circuit">Tor circuit for which to get a TCP connection.</param>
/// <returns>Whether a connection can be added to <see cref="ConnectionPerHost"/> and reserved connection to use, if any.</returns>
+ /// <remarks>Guarded by <see cref="ObtainPoolConnectionLock"/>.</remarks>
private bool GetPoolConnectionNoLock(string host, ICircuit circuit, out TorTcpConnection? connection)
{
if (!ConnectionPerHost.ContainsKey(host))

MODIFY
WalletWasabi/Tor/Socks5/TorTcpConnection.cs
WalletWasabi/Tor/Socks5/TorTcpConnection.cs
@@ -110,14 +110,25 @@ public class TorTcpConnection : IDisposable
}
}
+ /// <param name="force">
+ /// <c>true</c> if we know that we have just finished using the TCP connection.
+ /// <c>false</c> if we got an external piece of information that the TCP connection should be closed.
+ /// </param>
/// <remarks>Connection transporting an HTTP request that was canceled or otherwise failed must be torn down.</remarks>
- public void MarkAsToDispose()
+ public void MarkAsToDispose(bool force = true)
{
lock (StateLock)
+ {
+ if (force)
{
Debug.Assert(State == TcpConnectionState.InUse, $"Unexpected state: '{State}'.");
State = TcpConnectionState.ToDispose;
}
+ else if (State == TcpConnectionState.FreeToUse)
+ {
+ State = TcpConnectionState.ToDispose;
+ }
+ }
}
/// <inheritdoc/>

MODIFY
WalletWasabi/Tor/TorMonitor.cs
WalletWasabi/Tor/TorMonitor.cs
@@ -16,6 +16,8 @@ using WalletWasabi.Tor.Http;
using WalletWasabi.Tor.Socks5.Exceptions;
using WalletWasabi.Tor.Socks5.Models.Fields.OctetFields;
using WalletWasabi.Tor.Socks5.Pool;
+using WalletWasabi.Tor.Socks5.Pool.Circuits;
+using WalletWasabi.WebClients.Wasabi;
namespace WalletWasabi.Tor;
@@ -24,11 +26,12 @@ public class TorMonitor : PeriodicRunner
{
public static readonly TimeSpan CheckIfRunningAfterTorMisbehavedFor = TimeSpan.FromSeconds(7);
- public TorMonitor(TimeSpan period, Uri fallbackBackendUri, TorHttpClient httpClient, TorProcessManager torProcessManager) : base(period)
+ public TorMonitor(TimeSpan period, Uri fallbackBackendUri, TorProcessManager torProcessManager, HttpClientFactory backendHttpClientFactory) : base(period)
{
FallbackBackendUri = fallbackBackendUri;
- HttpClient = httpClient;
TorProcessManager = torProcessManager;
+ TorHttpPool = backendHttpClientFactory.TorHttpPool!;
+ HttpClient = backendHttpClientFactory.NewTorHttpClient(Mode.DefaultCircuit);
}
private CancellationTokenSource LoopCts { get; } = new();
@@ -37,6 +40,7 @@ public class TorMonitor : PeriodicRunner
private Uri FallbackBackendUri { get; }
private TorHttpClient HttpClient { get; }
private TorProcessManager TorProcessManager { get; }
+ private TorHttpPool TorHttpPool { get; }
private Task? BootstrapTask { get; set; }
@@ -109,6 +113,12 @@ public class TorMonitor : PeriodicRunner
Logger.LogInfo("Tor circuit was established.");
circuitEstablished = true;
}
+
+ if (info.CircStatus == CircStatus.CLOSED && info.UserName is not null)
+ {
+ Logger.LogTrace($"Tor circuit #{info.CircuitID} ('{info.UserName}') was closed.");
+ await TorHttpPool.ReportCircuitClosedAsync(info.UserName, linkedCts.Token).ConfigureAwait(false);
+ }
}
}
}

MODIFY
WalletWasabi/WebClients/Wasabi/HttpClientFactory.cs
WalletWasabi/WebClients/Wasabi/HttpClientFactory.cs
@@ -66,7 +66,7 @@ public class HttpClientFactory : IWasabiHttpClientFactory, IDisposable
private HttpClient HttpClient { get; }
/// <summary>Available only when Tor is enabled in User settings.</summary>
- private TorHttpPool? TorHttpPool { get; }
+ public TorHttpPool? TorHttpPool { get; }
/// <summary>Backend HTTP client, shared instance.</summary>
private IHttpClient BackendHttpClient { get; }

Implement `ReportCircuitClosedAsync`