mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 18:11:36 +01:00
Use NBitcoin's socks implementation
This commit is contained in:
parent
e5a26cfca8
commit
73d5415ea9
@ -56,7 +56,6 @@ using BTCPayServer.Configuration;
|
|||||||
using System.Security;
|
using System.Security;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using BTCPayServer.Tor;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
@ -148,28 +147,6 @@ namespace BTCPayServer.Tests
|
|||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
[Trait("Fast", "Fast")]
|
|
||||||
public void CanParseEndpoint()
|
|
||||||
{
|
|
||||||
Assert.False(EndPointParser.TryParse("126.2.2.2", out var endpoint));
|
|
||||||
Assert.True(EndPointParser.TryParse("126.2.2.2:20", out endpoint));
|
|
||||||
var ipEndpoint = Assert.IsType<IPEndPoint>(endpoint);
|
|
||||||
Assert.Equal("126.2.2.2", ipEndpoint.Address.ToString());
|
|
||||||
Assert.Equal(20, ipEndpoint.Port);
|
|
||||||
Assert.True(EndPointParser.TryParse("toto.com:20", out endpoint));
|
|
||||||
var dnsEndpoint = Assert.IsType<DnsEndPoint>(endpoint);
|
|
||||||
Assert.IsNotType<OnionEndpoint>(endpoint);
|
|
||||||
Assert.Equal("toto.com", dnsEndpoint.Host.ToString());
|
|
||||||
Assert.Equal(20, dnsEndpoint.Port);
|
|
||||||
Assert.False(EndPointParser.TryParse("toto invalid hostname:2029", out endpoint));
|
|
||||||
Assert.True(EndPointParser.TryParse("toto.onion:20", out endpoint));
|
|
||||||
var onionEndpoint = Assert.IsType<OnionEndpoint>(endpoint);
|
|
||||||
Assert.Equal("toto.onion", onionEndpoint.Host.ToString());
|
|
||||||
Assert.Equal(20, onionEndpoint.Port);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Fast", "Fast")]
|
[Trait("Fast", "Fast")]
|
||||||
public void CanParseTorrc()
|
public void CanParseTorrc()
|
||||||
|
@ -146,9 +146,15 @@ namespace BTCPayServer.Configuration
|
|||||||
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
|
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
|
||||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||||
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
|
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
|
||||||
SocksEndpoint = conf.GetOrDefault<EndPoint>("socksendpoint", null);
|
|
||||||
if (SocksEndpoint is Tor.OnionEndpoint)
|
var socksEndpointString = conf.GetOrDefault<string>("socksendpoint", null);
|
||||||
throw new ConfigException($"socksendpoint should not be a tor endpoint");
|
if(!string.IsNullOrEmpty(socksEndpointString))
|
||||||
|
{
|
||||||
|
if (!Utils.TryParseEndpoint(socksEndpointString, 9050, out var endpoint))
|
||||||
|
throw new ConfigException("Invalid value for socksendpoint");
|
||||||
|
SocksEndpoint = endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var sshSettings = ParseSSHConfiguration(conf);
|
var sshSettings = ParseSSHConfiguration(conf);
|
||||||
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
|
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
|
||||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using NBitcoin;
|
||||||
|
|
||||||
namespace BTCPayServer.Configuration
|
namespace BTCPayServer.Configuration
|
||||||
{
|
{
|
||||||
@ -39,12 +40,6 @@ namespace BTCPayServer.Configuration
|
|||||||
return (T)(object)str;
|
return (T)(object)str;
|
||||||
else if (typeof(T) == typeof(IPAddress))
|
else if (typeof(T) == typeof(IPAddress))
|
||||||
return (T)(object)IPAddress.Parse(str);
|
return (T)(object)IPAddress.Parse(str);
|
||||||
else if (typeof(T) == typeof(EndPoint))
|
|
||||||
{
|
|
||||||
if (EndPointParser.TryParse(str, out var endpoint))
|
|
||||||
return (T)(object)endpoint;
|
|
||||||
throw new FormatException("Invalid endpoint");
|
|
||||||
}
|
|
||||||
else if (typeof(T) == typeof(IPEndPoint))
|
else if (typeof(T) == typeof(IPEndPoint))
|
||||||
{
|
{
|
||||||
var separator = str.LastIndexOf(":", StringComparison.InvariantCulture);
|
var separator = str.LastIndexOf(":", StringComparison.InvariantCulture);
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using BTCPayServer.Tor;
|
|
||||||
|
|
||||||
namespace BTCPayServer
|
|
||||||
{
|
|
||||||
public static class EndPointParser
|
|
||||||
{
|
|
||||||
public static bool TryParse(string hostPort, out EndPoint endpoint)
|
|
||||||
{
|
|
||||||
if (hostPort == null)
|
|
||||||
throw new ArgumentNullException(nameof(hostPort));
|
|
||||||
endpoint = null;
|
|
||||||
var index = hostPort.LastIndexOf(':');
|
|
||||||
if (index == -1)
|
|
||||||
return false;
|
|
||||||
var portStr = hostPort.Substring(index + 1);
|
|
||||||
if (!ushort.TryParse(portStr, out var port))
|
|
||||||
return false;
|
|
||||||
return TryParse(hostPort.Substring(0, index), port, out endpoint);
|
|
||||||
}
|
|
||||||
public static bool TryParse(string host, int port, out EndPoint endpoint)
|
|
||||||
{
|
|
||||||
if (host == null)
|
|
||||||
throw new ArgumentNullException(nameof(host));
|
|
||||||
endpoint = null;
|
|
||||||
if (IPAddress.TryParse(host, out var address))
|
|
||||||
endpoint = new IPEndPoint(address, port);
|
|
||||||
else if (host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase))
|
|
||||||
endpoint = new OnionEndpoint(host, port);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (Uri.CheckHostName(host) != UriHostNameType.Dns)
|
|
||||||
return false;
|
|
||||||
endpoint = new DnsEndPoint(host, port);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,8 +9,8 @@ using BTCPayServer.Data;
|
|||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Lightning;
|
using BTCPayServer.Lightning;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Tor;
|
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
|
using NBitcoin;
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning
|
namespace BTCPayServer.Payments.Lightning
|
||||||
{
|
{
|
||||||
@ -110,10 +110,10 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!EndPointParser.TryParse(nodeInfo.Host, nodeInfo.Port, out var endpoint))
|
if (!Utils.TryParseEndpoint(nodeInfo.Host, nodeInfo.Port, out var endpoint))
|
||||||
throw new PaymentMethodUnavailableException($"Could not parse the endpoint {nodeInfo.Host}");
|
throw new PaymentMethodUnavailableException($"Could not parse the endpoint {nodeInfo.Host}");
|
||||||
|
|
||||||
using (var tcp = await _socketFactory.ConnectAsync(endpoint, SocketType.Stream, ProtocolType.Tcp, cancellation))
|
using (var tcp = await _socketFactory.ConnectAsync(endpoint, cancellation))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ using System.Text;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Configuration;
|
using BTCPayServer.Configuration;
|
||||||
using BTCPayServer.Tor;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Services
|
namespace BTCPayServer.Services
|
||||||
{
|
{
|
||||||
@ -19,7 +18,7 @@ namespace BTCPayServer.Services
|
|||||||
{
|
{
|
||||||
_options = options;
|
_options = options;
|
||||||
}
|
}
|
||||||
public async Task<Socket> ConnectAsync(EndPoint endPoint, SocketType socketType, ProtocolType protocolType, CancellationToken cancellationToken)
|
public async Task<Socket> ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Socket socket = null;
|
Socket socket = null;
|
||||||
try
|
try
|
||||||
@ -29,11 +28,22 @@ namespace BTCPayServer.Services
|
|||||||
socket = new Socket(ipEndpoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
socket = new Socket(ipEndpoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||||
await socket.ConnectAsync(ipEndpoint).WithCancellation(cancellationToken);
|
await socket.ConnectAsync(ipEndpoint).WithCancellation(cancellationToken);
|
||||||
}
|
}
|
||||||
else if (endPoint is OnionEndpoint onionEndpoint)
|
else if (IsTor(endPoint))
|
||||||
{
|
{
|
||||||
if (_options.SocksEndpoint == null)
|
if (_options.SocksEndpoint == null)
|
||||||
throw new NotSupportedException("It is impossible to connect to an onion address without btcpay's -socksendpoint configured");
|
throw new NotSupportedException("It is impossible to connect to an onion address without btcpay's -socksendpoint configured");
|
||||||
socket = await Socks5Connect.ConnectSocksAsync(_options.SocksEndpoint, onionEndpoint, cancellationToken);
|
if (_options.SocksEndpoint.AddressFamily != AddressFamily.Unspecified)
|
||||||
|
{
|
||||||
|
socket = new Socket(_options.SocksEndpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If the socket is a DnsEndpoint, we allow either ipv6 or ipv4
|
||||||
|
socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
|
||||||
|
socket.DualMode = true;
|
||||||
|
}
|
||||||
|
await socket.ConnectAsync(_options.SocksEndpoint).WithCancellation(cancellationToken);
|
||||||
|
await NBitcoin.Socks.SocksHelper.Handshake(socket, endPoint, cancellationToken);
|
||||||
}
|
}
|
||||||
else if (endPoint is DnsEndPoint dnsEndPoint)
|
else if (endPoint is DnsEndPoint dnsEndPoint)
|
||||||
{
|
{
|
||||||
@ -52,6 +62,15 @@ namespace BTCPayServer.Services
|
|||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsTor(EndPoint endPoint)
|
||||||
|
{
|
||||||
|
if (endPoint is IPEndPoint)
|
||||||
|
return endPoint.AsOnionDNSEndpoint() != null;
|
||||||
|
if (endPoint is DnsEndPoint dns)
|
||||||
|
return dns.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void CloseSocket(ref Socket s)
|
private void CloseSocket(ref Socket s)
|
||||||
{
|
{
|
||||||
if (s == null)
|
if (s == null)
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Tor
|
|
||||||
{
|
|
||||||
public class OnionEndpoint : DnsEndPoint
|
|
||||||
{
|
|
||||||
public OnionEndpoint(string host, int port): base(host, port)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,187 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NBitcoin;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Tor
|
|
||||||
{
|
|
||||||
public enum SocksErrorCode
|
|
||||||
{
|
|
||||||
Success = 0,
|
|
||||||
GeneralServerFailure = 1,
|
|
||||||
ConnectionNotAllowed = 2,
|
|
||||||
NetworkUnreachable = 3,
|
|
||||||
HostUnreachable = 4,
|
|
||||||
ConnectionRefused = 5,
|
|
||||||
TTLExpired = 6,
|
|
||||||
CommandNotSupported = 7,
|
|
||||||
AddressTypeNotSupported = 8,
|
|
||||||
}
|
|
||||||
public class SocksException : Exception
|
|
||||||
{
|
|
||||||
public SocksException(SocksErrorCode errorCode) : base(GetMessageForCode((int)errorCode))
|
|
||||||
{
|
|
||||||
SocksErrorCode = errorCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SocksErrorCode SocksErrorCode
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetMessageForCode(int errorCode)
|
|
||||||
{
|
|
||||||
switch (errorCode)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
return "Success";
|
|
||||||
case 1:
|
|
||||||
return "general SOCKS server failure";
|
|
||||||
case 2:
|
|
||||||
return "connection not allowed by ruleset";
|
|
||||||
case 3:
|
|
||||||
return "Network unreachable";
|
|
||||||
case 4:
|
|
||||||
return "Host unreachable";
|
|
||||||
case 5:
|
|
||||||
return "Connection refused";
|
|
||||||
case 6:
|
|
||||||
return "TTL expired";
|
|
||||||
case 7:
|
|
||||||
return "Command not supported";
|
|
||||||
case 8:
|
|
||||||
return "Address type not supported";
|
|
||||||
default:
|
|
||||||
return "Unknown code";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public SocksException(string message) : base(message)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Socks5Connect
|
|
||||||
{
|
|
||||||
static readonly byte[] SelectionMessage = new byte[] { 5, 1, 0 };
|
|
||||||
public static async Task<Socket> ConnectSocksAsync(EndPoint socksEndpoint, DnsEndPoint endpoint, CancellationToken cancellation)
|
|
||||||
{
|
|
||||||
Socket s = null;
|
|
||||||
int maxTries = 3;
|
|
||||||
int retry = 0;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
||||||
await s.ConnectAsync(socksEndpoint).WithCancellation(cancellation).ConfigureAwait(false);
|
|
||||||
NetworkStream stream = new NetworkStream(s, false);
|
|
||||||
|
|
||||||
await stream.WriteAsync(SelectionMessage, 0, SelectionMessage.Length, cancellation).ConfigureAwait(false);
|
|
||||||
await stream.FlushAsync(cancellation).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var selectionResponse = new byte[2];
|
|
||||||
await stream.ReadAsync(selectionResponse, 0, 2, cancellation);
|
|
||||||
if (selectionResponse[0] != 5)
|
|
||||||
throw new SocksException("Invalid version in selection reply");
|
|
||||||
if (selectionResponse[1] != 0)
|
|
||||||
throw new SocksException("Unsupported authentication method in selection reply");
|
|
||||||
|
|
||||||
var connectBytes = CreateConnectMessage(endpoint.Host, endpoint.Port);
|
|
||||||
await stream.WriteAsync(connectBytes, 0, connectBytes.Length, cancellation).ConfigureAwait(false);
|
|
||||||
await stream.FlushAsync(cancellation).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var connectResponse = new byte[10];
|
|
||||||
await stream.ReadAsync(connectResponse, 0, 10, cancellation);
|
|
||||||
if (connectResponse[0] != 5)
|
|
||||||
throw new SocksException("Invalid version in connect reply");
|
|
||||||
if (connectResponse[1] != 0)
|
|
||||||
{
|
|
||||||
var code = (SocksErrorCode)connectResponse[1];
|
|
||||||
if (!IsTransient(code) || retry++ >= maxTries)
|
|
||||||
throw new SocksException(code);
|
|
||||||
CloseSocket(ref s);
|
|
||||||
await Task.Delay(1000, cancellation).ConfigureAwait(false);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (connectResponse[2] != 0)
|
|
||||||
throw new SocksException("Invalid RSV in connect reply");
|
|
||||||
if (connectResponse[3] != 1)
|
|
||||||
throw new SocksException("Invalid ATYP in connect reply");
|
|
||||||
for (int i = 4; i < 4 + 4; i++)
|
|
||||||
{
|
|
||||||
if (connectResponse[i] != 0)
|
|
||||||
throw new SocksException("Invalid BIND address in connect reply");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connectResponse[8] != 0 || connectResponse[9] != 0)
|
|
||||||
throw new SocksException("Invalid PORT address connect reply");
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
CloseSocket(ref s);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void CloseSocket(ref Socket s)
|
|
||||||
{
|
|
||||||
if (s == null)
|
|
||||||
return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
s.Shutdown(SocketShutdown.Both);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
s.Dispose();
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
s = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsTransient(SocksErrorCode code)
|
|
||||||
{
|
|
||||||
return code == SocksErrorCode.GeneralServerFailure ||
|
|
||||||
code == SocksErrorCode.TTLExpired;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static byte[] CreateConnectMessage(string host, int port)
|
|
||||||
{
|
|
||||||
byte[] sendBuffer;
|
|
||||||
byte[] nameBytes = Encoding.ASCII.GetBytes(host);
|
|
||||||
|
|
||||||
var addressBytes =
|
|
||||||
Enumerable.Empty<byte>()
|
|
||||||
.Concat(new[] { (byte)nameBytes.Length })
|
|
||||||
.Concat(nameBytes).ToArray();
|
|
||||||
|
|
||||||
sendBuffer =
|
|
||||||
Enumerable.Empty<byte>()
|
|
||||||
.Concat(
|
|
||||||
new byte[]
|
|
||||||
{
|
|
||||||
(byte)5, (byte) 0x01, (byte) 0x00, (byte)0x03
|
|
||||||
})
|
|
||||||
.Concat(addressBytes)
|
|
||||||
.Concat(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)port))).ToArray();
|
|
||||||
return sendBuffer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user