Simplify connectivity policy
MODIFY
WalletWasabi.Tests/UnitTests/BestEffortEndpointConnectorTests.cs
WalletWasabi.Tests/UnitTests/BestEffortEndpointConnectorTests.cs
@@ -31,49 +31,56 @@ namespace WalletWasabi.Tests.UnitTests
Exception ex;
// Try to connect to a non-onion address.
- ex = await Assert.ThrowsAsync<InvalidOperationException>(
+ ex = await Assert.ThrowsAsync<SocketException>(
async () => await ConnectAsync(new IPEndPoint(IPAddress.Loopback, 180)));
- Assert.Equal("The Endpoint connector is configured to allow only Tor endpoints and the '127.0.0.1:180' enpoint is not one", ex.Message);
- Assert.Equal(BestEffortEndpointConnector.ConnectionMode.OnionServiceOnly, connector.State.Mode);
+ Assert.Contains("refused", ex.Message);
+ Assert.False(connector.State.AllowOnlyTorEndpoints);
// Try to connect to an onion address (it has to fail because there is no real socks proxy listening).
ex = await Assert.ThrowsAnyAsync<SocketException>(
- async () => await ConnectAsync(new DnsEndPoint("nec4kn4ghql7p7an.onion", 80)));
+ async () => await ConnectAsync(new DnsEndPoint("nec4kn4ghql7p7an.onion", 180)));
Assert.Contains("refused", ex.Message);
- Assert.Equal(BestEffortEndpointConnector.ConnectionMode.OnionServiceOnly, connector.State.Mode);
+ Assert.False(connector.State.AllowOnlyTorEndpoints);
- // Timing out - Mode changes from OnionServiceOnly -> AllowGoingThroughTorExitNodes.
- connector.State.LastModeChangeTime = DateTimeOffset.UtcNow.AddMinutes(-1.5);
-
- ex = await Assert.ThrowsAnyAsync<SocketException>(
+ // Simulate we lost connection.
+ connector.State.ConnectedNodesCount = 10;
+ ex = await Assert.ThrowsAsync<SocketException>(
async () => await ConnectAsync(new IPEndPoint(IPAddress.Loopback, 180)));
Assert.Contains("refused", ex.Message);
- Assert.Equal(BestEffortEndpointConnector.ConnectionMode.AllowGoingThroughTorExitNodes, connector.State.Mode);
-
- // Simulate there is a connection made (so, it is feasible to have more).
- connector.UpdateConnectedNodesCounter(1);
- connector.State.LastModeChangeTime = DateTimeOffset.UtcNow.AddMinutes(-1.5);
+ Assert.True(connector.State.AllowOnlyTorEndpoints);
ex = await Assert.ThrowsAnyAsync<SocketException>(
- async () => await ConnectAsync(new IPEndPoint(IPAddress.Loopback, 180)));
+ async () => await ConnectAsync(new DnsEndPoint("nec4kn4ghql7p7an.onion", 180)));
Assert.Contains("refused", ex.Message);
- Assert.Equal(BestEffortEndpointConnector.ConnectionMode.AllowGoingThroughTorExitNodes, connector.State.Mode);
+ Assert.True(connector.State.AllowOnlyTorEndpoints);
// Simulate we lost connection.
connector.State.ConnectedNodesCount = 0;
+ ex = await Assert.ThrowsAsync<SocketException>(
+ async () => await ConnectAsync(new IPEndPoint(IPAddress.Loopback, 180)));
+ Assert.Contains("refused", ex.Message);
+ Assert.False(connector.State.AllowOnlyTorEndpoints);
+ // Try to connect to an onion address (it has to fail because there is no real socks proxy listening).
ex = await Assert.ThrowsAnyAsync<SocketException>(
async () => await ConnectAsync(new DnsEndPoint("nec4kn4ghql7p7an.onion", 180)));
Assert.Contains("refused", ex.Message);
- Assert.Equal(BestEffortEndpointConnector.ConnectionMode.OnionServiceOnly, connector.State.Mode);
+ Assert.False(connector.State.AllowOnlyTorEndpoints);
- // Simulate there are no connections made.
- connector.State.LastModeChangeTime = DateTimeOffset.UtcNow.AddHours(-1);
+ // Enough peers with recent connection.
+ connector.State.ConnectedNodesCount = 10;
+ connector.State.LastConnectionTime = DateTimeOffset.UtcNow;
+ ex = await Assert.ThrowsAnyAsync<SocketException>(
+ async () => await ConnectAsync(new DnsEndPoint("nec4kn4ghql7p7an.onion", 180)));
+ Assert.Contains("refused", ex.Message);
+ Assert.True(connector.State.AllowOnlyTorEndpoints);
+ // Enough peers with no recent connection.
+ connector.State.LastConnectionTime = DateTimeOffset.UtcNow.AddMinutes(-1);
ex = await Assert.ThrowsAnyAsync<SocketException>(
- async () => await ConnectAsync(new IPEndPoint(IPAddress.Loopback, 180)));
+ async () => await ConnectAsync(new DnsEndPoint("nec4kn4ghql7p7an.onion", 180)));
Assert.Contains("refused", ex.Message);
- Assert.Equal(BestEffortEndpointConnector.ConnectionMode.AllowGoingThroughTorExitNodes, connector.State.Mode);
+ Assert.False(connector.State.AllowOnlyTorEndpoints);
}
}
}

MODIFY
WalletWasabi/BestEffortEndpointConnector.cs
WalletWasabi/BestEffortEndpointConnector.cs
+using NBitcoin;
using NBitcoin.Protocol;
using NBitcoin.Protocol.Behaviors;
using NBitcoin.Protocol.Connectors;
+using NBitcoin.Socks;
using System;
+using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
@@ -13,28 +16,15 @@ namespace WalletWasabi
public class BestEffortEndpointConnector : IEnpointConnector
{
public BestEffortEndpointConnector()
- : this(
- new DefaultEndpointConnector(allowOnlyTorEndpoints: true),
- new EffortState(ConnectionMode.OnionServiceOnly, DateTimeOffset.UtcNow))
+ : this(new EffortState(DateTimeOffset.UtcNow))
{
}
- private BestEffortEndpointConnector(DefaultEndpointConnector connector, EffortState state)
+ private BestEffortEndpointConnector(EffortState state)
{
- Connector = connector;
State = state;
}
- public enum ConnectionMode
- {
- // Connect to nodes published as onion services only
- OnionServiceOnly,
-
- // Connect to nodes published on the clearnet through Tor exit nodes
- AllowGoingThroughTorExitNodes,
- }
-
- public DefaultEndpointConnector Connector { get; private set; }
public EffortState State { get; private set; }
public void UpdateConnectedNodesCounter(int connectedNodes)
@@ -44,23 +34,49 @@ namespace WalletWasabi
public IEnpointConnector Clone()
{
- return new BestEffortEndpointConnector(Connector, State);
+ return new BestEffortEndpointConnector(State);
}
- public virtual async Task ConnectSocket(Socket socket, EndPoint endPoint, NodeConnectionParameters nodeConnectionParameters, CancellationToken cancellationToken)
+ public virtual async Task ConnectSocket(Socket socket, EndPoint endpoint, NodeConnectionParameters nodeConnectionParameters, CancellationToken cancellationToken)
{
- if (State.CheckModeUpdate())
+ var isTor = endpoint.IsTor();
+
+ var socksSettings = nodeConnectionParameters.TemplateBehaviors.Find<SocksSettingsBehavior>();
+ var socketEndpoint = endpoint;
+ var useSocks = isTor || socksSettings?.OnlyForOnionHosts is false;
+ if (useSocks)
+ {
+ if (socksSettings?.SocksEndpoint == null)
{
- Connector.AllowOnlyTorEndpoints = State.Mode switch
+ throw new InvalidOperationException("SocksSettingsBehavior.SocksEndpoint is not set but the connection is expecting using socks proxy");
+ }
+ if (!isTor && State.AllowOnlyTorEndpoints)
{
- ConnectionMode.OnionServiceOnly => true,
- ConnectionMode.AllowGoingThroughTorExitNodes => false,
- _ => throw new InvalidOperationException($"Unknown ${typeof(ConnectionMode).Name} with value {State.Mode}.")
- };
+ throw new InvalidOperationException($"The Endpoint connector is configured to allow only Tor endpoints and the '{endpoint}' enpoint is not one");
+ }
+
+ socketEndpoint = socksSettings.SocksEndpoint;
+ }
+ if (socketEndpoint is IPEndPoint mappedv4 && mappedv4.Address.IsIPv4MappedToIPv6Ex())
+ {
+ socketEndpoint = new IPEndPoint(mappedv4.Address.MapToIPv4Ex(), mappedv4.Port);
+ }
+ await socket.ConnectAsync(socketEndpoint, cancellationToken).ConfigureAwait(false);
+
+ if (useSocks)
+ {
+ await SocksHelper.Handshake(socket, endpoint, GenerateCredentials(), cancellationToken).ConfigureAwait(false);
+
+ State.ResetConnectionTimeControl();
+ }
}
- await Connector.ConnectSocket(socket, endPoint, nodeConnectionParameters, cancellationToken).ConfigureAwait(false);
- State.ResetModeChangeTimeControl();
+ private NetworkCredential GenerateCredentials()
+ {
+ const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ var identity = new string(Enumerable.Repeat(Chars, 21)
+ .Select(s => s[(int)(RandomUtils.GetUInt32() % s.Length)]).ToArray());
+ return new NetworkCredential(identity, identity);
}
// A class to share state between all the BestEffortEndpointConnector connectors.
@@ -68,67 +84,52 @@ namespace WalletWasabi
// attempt using the original connector.
public class EffortState
{
- private long _connectedNodesCount;
- private DateTimeOffset _lastModeChangeTime;
- private bool _lostConnection;
+ private bool _allowAnyConnetionType;
- public EffortState(ConnectionMode mode, DateTimeOffset lastModeChangeTime)
+ public EffortState(DateTimeOffset lastConnectionTime)
{
- Mode = mode;
- LastModeChangeTime = lastModeChangeTime;
+ LastConnectionTime = lastConnectionTime;
}
public long ConnectedNodesCount
{
- get => _connectedNodesCount;
- set
- {
- _lostConnection = value == 0 && _connectedNodesCount > 0;
- _connectedNodesCount = value;
- }
+ get;
+ set;
}
- public ConnectionMode Mode { get; set; }
-
- public DateTimeOffset LastModeChangeTime
- {
- get => _lastModeChangeTime;
- set
+ public DateTimeOffset LastConnectionTime
{
- _lostConnection = false;
- _lastModeChangeTime = value;
- }
+ get;
+ set;
}
- public TimeSpan ElapsedTimeSinceLastModeChange => DateTimeOffset.UtcNow - LastModeChangeTime;
+ public TimeSpan ElapsedTimeSinceLastConnection => DateTimeOffset.UtcNow - LastConnectionTime;
- public void ResetModeChangeTimeControl()
+ public bool AllowOnlyTorEndpoints
{
- LastModeChangeTime = DateTimeOffset.UtcNow;
- }
-
- public bool CheckModeUpdate()
+ get
{
- var previousMode = Mode;
- if (_lostConnection)
+ var allowAnyConnetionType = ConnectedNodesCount <= 5 || ElapsedTimeSinceLastConnection > TimeSpan.FromSeconds(30);
+
+ if (_allowAnyConnetionType != allowAnyConnetionType)
{
- Mode = ConnectionMode.OnionServiceOnly;
+ _allowAnyConnetionType = allowAnyConnetionType;
+ Logger.LogDebug(ToString());
+ ResetConnectionTimeControl();
}
- else if (ElapsedTimeSinceLastModeChange > TimeSpan.FromMinutes(1))
- {
- if (Mode == ConnectionMode.OnionServiceOnly)
- {
- Mode = ConnectionMode.AllowGoingThroughTorExitNodes;
+
+ return !_allowAnyConnetionType;
}
}
- if (previousMode != Mode)
+ public void ResetConnectionTimeControl()
{
- Logger.LogInfo($"Update connection mode from {previousMode} to {Mode}.");
- ResetModeChangeTimeControl();
- return true;
+ LastConnectionTime = DateTimeOffset.UtcNow;
}
- return false;
+
+ public override string ToString()
+ {
+ return $"Connections: {ConnectedNodesCount}, Last connection time: {LastConnectionTime}, Currently allow only onions: {!_allowAnyConnetionType}.";
}
}
}

Simplify connectivity policy