Skip to content

Commit c91de70

Browse files
committed
Enable using different Tor identities per node.
1 parent b1c7e36 commit c91de70

2 files changed

Lines changed: 64 additions & 7 deletions

File tree

NBitcoin/Protocol/Connectors/DefaultEndpointConnector.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,25 @@ public class DefaultEndpointConnector : IEnpointConnector
1919
/// </summary>
2020
public bool AllowOnlyTorEndpoints { get; set; } = false;
2121

22+
23+
/// <summary>
24+
/// Use a new Tor stream for every connection.
25+
/// </summary>
26+
public bool ChangeTorIdentities { get; set; } = false;
27+
2228
public DefaultEndpointConnector()
2329
{
2430
}
2531

26-
public DefaultEndpointConnector(bool allowOnlyTorEndpoints)
32+
public DefaultEndpointConnector(bool allowOnlyTorEndpoints, bool changeTorIdentities)
2733
{
2834
AllowOnlyTorEndpoints = allowOnlyTorEndpoints;
35+
ChangeTorIdentities = changeTorIdentities;
2936
}
3037

3138
public IEnpointConnector Clone()
3239
{
33-
return new DefaultEndpointConnector(AllowOnlyTorEndpoints);
40+
return new DefaultEndpointConnector(AllowOnlyTorEndpoints, ChangeTorIdentities);
3441
}
3542

3643
public async Task ConnectSocket(Socket socket, EndPoint endpoint, NodeConnectionParameters nodeConnectionParameters, CancellationToken cancellationToken)
@@ -51,7 +58,7 @@ public async Task ConnectSocket(Socket socket, EndPoint endpoint, NodeConnection
5158
if (!socks)
5259
return;
5360

54-
await SocksHelper.Handshake(socket, endpoint, cancellationToken).ConfigureAwait(false);
61+
await SocksHelper.Handshake(socket, endpoint, ChangeTorIdentities, cancellationToken).ConfigureAwait(false);
5562
}
5663
}
5764
}

NBitcoin/Socks/SocksHelper.cs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#if !NOSOCKET
22
using System;
33
using System.Collections.Generic;
4+
using System.Linq;
45
using System.Net;
56
using System.Net.Sockets;
67
using System.Text;
@@ -11,7 +12,16 @@ namespace NBitcoin.Socks
1112
{
1213
public class SocksHelper
1314
{
14-
static readonly byte[] SelectionMessage = new byte[] { 5, 1, 0 };
15+
// https://gitweb.torproject.org/torspec.git/tree/socks-extensions.txt
16+
// The "NO AUTHENTICATION REQUIRED" (SOCKS5) authentication method[00] is
17+
// supported; and as of Tor 0.2.3.2-alpha, the "USERNAME/PASSWORD" (SOCKS5)
18+
// authentication method[02] is supported too, and used as a method to
19+
// implement stream isolation.As an extension to support some broken clients,
20+
// we allow clients to pass "USERNAME/PASSWORD" authentication to us even if
21+
// no authentication was selected.
22+
static readonly byte[] SelectionMessageNoAuthenticationRequired = new byte[] { 5, 1, 0 };
23+
static readonly byte[] SelectionMessageUsernamePassword = new byte[] { 5, 1, 2 };
24+
1525
internal static byte[] CreateConnectMessage(EndPoint endpoint)
1626
{
1727
if (endpoint == null)
@@ -66,17 +76,57 @@ internal static byte[] CreateConnectMessage(EndPoint endpoint)
6676
return sendBuffer;
6777
}
6878

69-
public static async Task Handshake(Socket socket, EndPoint endpoint, CancellationToken cancellationToken)
79+
public static async Task Handshake(Socket socket, EndPoint endpoint, bool changeTorIdentities, CancellationToken cancellationToken)
7080
{
7181
NetworkStream stream = new NetworkStream(socket, false);
72-
await stream.WriteAsync(SelectionMessage, 0, SelectionMessage.Length).WithCancellation(cancellationToken).ConfigureAwait(false);
82+
var selectionMessage = changeTorIdentities ? SelectionMessageUsernamePassword : SelectionMessageNoAuthenticationRequired;
83+
await stream.WriteAsync(selectionMessage, 0, selectionMessage.Length).WithCancellation(cancellationToken).ConfigureAwait(false);
7384
await stream.FlushAsync().WithCancellation(cancellationToken).ConfigureAwait(false);
7485

7586
var selectionResponse = new byte[2];
7687
await stream.ReadAsync(selectionResponse, 0, 2).WithCancellation(cancellationToken).ConfigureAwait(false);
7788
if (selectionResponse[0] != 5)
7889
throw new SocksException("Invalid version in selection reply");
79-
if (selectionResponse[1] != 0)
90+
if (selectionResponse[1] == 2)
91+
{
92+
// https://tools.ietf.org/html/rfc1929#section-2
93+
// Once the SOCKS V5 server has started, and the client has selected the
94+
// Username / Password Authentication protocol, the Username / Password
95+
// subnegotiation begins. This begins with the client producing a
96+
// Username / Password request:
97+
98+
// Create a random string 21 char string.
99+
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
100+
var identity = new string(Enumerable.Repeat(chars, 21)
101+
.Select(s => s[new Random().Next(s.Length)]).ToArray());
102+
103+
var username = identity;
104+
var password = identity;
105+
var uName = Encoding.UTF8.GetBytes(username);
106+
var passwd = Encoding.UTF8.GetBytes(password);
107+
var usernamePasswordRequest = ByteArrayExtensions.Concat(new byte[] { 1, (byte)uName.Length }, uName, new byte[] { (byte)passwd.Length }, passwd);
108+
109+
await stream.WriteAsync(usernamePasswordRequest, 0, usernamePasswordRequest.Length).WithCancellation(cancellationToken).ConfigureAwait(false);
110+
await stream.FlushAsync().WithCancellation(cancellationToken).ConfigureAwait(false);
111+
112+
var userNamePasswordResponse = new byte[2];
113+
await stream.ReadAsync(userNamePasswordResponse, 0, 2).WithCancellation(cancellationToken).ConfigureAwait(false);
114+
115+
if (userNamePasswordResponse[0] != 1)
116+
{
117+
throw new NotSupportedException($"Authentication version {userNamePasswordResponse[0]} is not supported. Only version {1} is supported.");
118+
}
119+
120+
if (userNamePasswordResponse[1] != 0) // Tor authentication is different, this will never happen;
121+
{
122+
// https://tools.ietf.org/html/rfc1929#section-2
123+
// A STATUS field of X'00' indicates success. If the server returns a
124+
// `failure' (STATUS value other than X'00') status, it MUST close the
125+
// connection.
126+
throw new SocksException("Wrong username and/or password.");
127+
}
128+
}
129+
else if (selectionResponse[1] != 0)
80130
throw new SocksException("Unsupported authentication method in selection reply");
81131

82132
var connectBytes = CreateConnectMessage(endpoint);

0 commit comments

Comments
 (0)