From 73d5415ea9b221b34f0bbd4da47bb252e98f7aad Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sun, 31 Mar 2019 13:16:05 +0900 Subject: [PATCH] Use NBitcoin's socks implementation --- BTCPayServer.Tests/UnitTest1.cs | 23 --- .../Configuration/BTCPayServerOptions.cs | 12 +- .../Configuration/ConfigurationExtensions.cs | 7 +- BTCPayServer/EndpointParser.cs | 43 ---- .../Lightning/LightningLikePaymentHandler.cs | 6 +- BTCPayServer/Services/SocketFactory.cs | 27 ++- BTCPayServer/Tor/OnionEndpoint.cs | 16 -- BTCPayServer/Tor/Socks5Connect.cs | 187 ------------------ 8 files changed, 36 insertions(+), 285 deletions(-) delete mode 100644 BTCPayServer/EndpointParser.cs delete mode 100644 BTCPayServer/Tor/OnionEndpoint.cs delete mode 100644 BTCPayServer/Tor/Socks5Connect.cs diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 04b94d9c5..b7d34491d 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -56,7 +56,6 @@ using BTCPayServer.Configuration; using System.Security; using System.Runtime.CompilerServices; using System.Net; -using BTCPayServer.Tor; namespace BTCPayServer.Tests { @@ -148,28 +147,6 @@ namespace BTCPayServer.Tests #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(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(endpoint); - Assert.IsNotType(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(endpoint); - Assert.Equal("toto.onion", onionEndpoint.Host.ToString()); - Assert.Equal(20, onionEndpoint.Port); - } - [Fact] [Trait("Fast", "Fast")] public void CanParseTorrc() diff --git a/BTCPayServer/Configuration/BTCPayServerOptions.cs b/BTCPayServer/Configuration/BTCPayServerOptions.cs index 5761858c6..7069f959d 100644 --- a/BTCPayServer/Configuration/BTCPayServerOptions.cs +++ b/BTCPayServer/Configuration/BTCPayServerOptions.cs @@ -146,9 +146,15 @@ namespace BTCPayServer.Configuration MySQLConnectionString = conf.GetOrDefault("mysql", null); BundleJsCss = conf.GetOrDefault("bundlejscss", true); TorrcFile = conf.GetOrDefault("torrcfile", null); - SocksEndpoint = conf.GetOrDefault("socksendpoint", null); - if (SocksEndpoint is Tor.OnionEndpoint) - throw new ConfigException($"socksendpoint should not be a tor endpoint"); + + var socksEndpointString = conf.GetOrDefault("socksendpoint", null); + 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); if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server)) diff --git a/BTCPayServer/Configuration/ConfigurationExtensions.cs b/BTCPayServer/Configuration/ConfigurationExtensions.cs index 10750e8c3..4788bc0c3 100644 --- a/BTCPayServer/Configuration/ConfigurationExtensions.cs +++ b/BTCPayServer/Configuration/ConfigurationExtensions.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net; using System.Threading.Tasks; using Microsoft.Extensions.Primitives; +using NBitcoin; namespace BTCPayServer.Configuration { @@ -39,12 +40,6 @@ namespace BTCPayServer.Configuration return (T)(object)str; else if (typeof(T) == typeof(IPAddress)) 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)) { var separator = str.LastIndexOf(":", StringComparison.InvariantCulture); diff --git a/BTCPayServer/EndpointParser.cs b/BTCPayServer/EndpointParser.cs deleted file mode 100644 index 28c1c2426..000000000 --- a/BTCPayServer/EndpointParser.cs +++ /dev/null @@ -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; - } - } -} diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index 21c0ddb99..098a46829 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -9,8 +9,8 @@ using BTCPayServer.Data; using BTCPayServer.HostedServices; using BTCPayServer.Lightning; using BTCPayServer.Services.Invoices; -using BTCPayServer.Tor; using BTCPayServer.Services; +using NBitcoin; namespace BTCPayServer.Payments.Lightning { @@ -110,10 +110,10 @@ namespace BTCPayServer.Payments.Lightning { 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}"); - using (var tcp = await _socketFactory.ConnectAsync(endpoint, SocketType.Stream, ProtocolType.Tcp, cancellation)) + using (var tcp = await _socketFactory.ConnectAsync(endpoint, cancellation)) { } } diff --git a/BTCPayServer/Services/SocketFactory.cs b/BTCPayServer/Services/SocketFactory.cs index 1530ac33b..38aa46488 100644 --- a/BTCPayServer/Services/SocketFactory.cs +++ b/BTCPayServer/Services/SocketFactory.cs @@ -8,7 +8,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Configuration; -using BTCPayServer.Tor; namespace BTCPayServer.Services { @@ -19,7 +18,7 @@ namespace BTCPayServer.Services { _options = options; } - public async Task ConnectAsync(EndPoint endPoint, SocketType socketType, ProtocolType protocolType, CancellationToken cancellationToken) + public async Task ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken) { Socket socket = null; try @@ -29,11 +28,22 @@ namespace BTCPayServer.Services socket = new Socket(ipEndpoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); await socket.ConnectAsync(ipEndpoint).WithCancellation(cancellationToken); } - else if (endPoint is OnionEndpoint onionEndpoint) + else if (IsTor(endPoint)) { if (_options.SocksEndpoint == null) 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) { @@ -52,6 +62,15 @@ namespace BTCPayServer.Services 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) { if (s == null) diff --git a/BTCPayServer/Tor/OnionEndpoint.cs b/BTCPayServer/Tor/OnionEndpoint.cs deleted file mode 100644 index 2b6d7371e..000000000 --- a/BTCPayServer/Tor/OnionEndpoint.cs +++ /dev/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) - { - - } - } -} diff --git a/BTCPayServer/Tor/Socks5Connect.cs b/BTCPayServer/Tor/Socks5Connect.cs deleted file mode 100644 index 040f1ac9a..000000000 --- a/BTCPayServer/Tor/Socks5Connect.cs +++ /dev/null @@ -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 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() - .Concat(new[] { (byte)nameBytes.Length }) - .Concat(nameBytes).ToArray(); - - sendBuffer = - Enumerable.Empty() - .Concat( - new byte[] - { - (byte)5, (byte) 0x01, (byte) 0x00, (byte)0x03 - }) - .Concat(addressBytes) - .Concat(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)port))).ToArray(); - return sendBuffer; - } - } -}