From 6b661c06f534a13f1f06e24d6837180f558c175f Mon Sep 17 00:00:00 2001 From: Luke Butters Date: Fri, 27 Mar 2026 10:59:06 +1100 Subject: [PATCH 1/4] Add SNI support --- source/Halibut/ServiceEndPoint.cs | 8 ++++++++ source/Halibut/Transport/TcpConnectionFactory.cs | 8 +++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/source/Halibut/ServiceEndPoint.cs b/source/Halibut/ServiceEndPoint.cs index 7da00d51d..e49abec09 100644 --- a/source/Halibut/ServiceEndPoint.cs +++ b/source/Halibut/ServiceEndPoint.cs @@ -76,6 +76,14 @@ public ServiceEndPoint(Uri baseUri, string? remoteThumbprint, ProxyDetails? prox public ProxyDetails? Proxy { get; } + /// + /// When set, TCP connections will be made to this host and port instead of the host and port + /// in , while the original host is still used for + /// TLS SNI. This is equivalent to curl's --resolve flag and is useful when routing through a + /// local proxy (e.g. Toxiproxy) while preserving the correct TLS handshake. + /// + public (string Host, int Port)? ForceResolveAddress { get; set; } + public bool IsWebSocketEndpoint => IsWebSocketAddress(BaseUri); public override string ToString() => baseUriString; diff --git a/source/Halibut/Transport/TcpConnectionFactory.cs b/source/Halibut/Transport/TcpConnectionFactory.cs index 0e9c3ffda..10a8f1c51 100644 --- a/source/Halibut/Transport/TcpConnectionFactory.cs +++ b/source/Halibut/Transport/TcpConnectionFactory.cs @@ -79,19 +79,21 @@ await ssl.AuthenticateAsClientAsync( internal static async Task CreateConnectedTcpClientAsync(ServiceEndPoint endPoint, HalibutTimeoutsAndLimits halibutTimeoutsAndLimits, IStreamFactory streamFactory, ILog log, CancellationToken cancellationToken) { TcpClient client; + var connectHost = endPoint.ForceResolveAddress?.Host ?? endPoint.BaseUri.Host; + var connectPort = endPoint.ForceResolveAddress?.Port ?? endPoint.BaseUri.Port; if (endPoint.Proxy is null) { client = CreateTcpClientAsync(halibutTimeoutsAndLimits); - await client.ConnectWithTimeoutAsync(endPoint.BaseUri, endPoint.TcpClientConnectTimeout, cancellationToken); + await client.ConnectWithTimeoutAsync(connectHost, connectPort, endPoint.TcpClientConnectTimeout, cancellationToken); } else { log.Write(EventType.Diagnostic, "Creating a proxy client"); - + client = await new ProxyClientFactory(streamFactory) .CreateProxyClient(log, endPoint.Proxy) .WithTcpClientFactory(() => CreateTcpClientAsync(halibutTimeoutsAndLimits)) - .CreateConnectionAsync(endPoint.BaseUri.Host, endPoint.BaseUri.Port, endPoint.TcpClientConnectTimeout, cancellationToken); + .CreateConnectionAsync(connectHost, connectPort, endPoint.TcpClientConnectTimeout, cancellationToken); } return client; } From 6daea8aa00c5c279c7e442dd396635f61d464f29 Mon Sep 17 00:00:00 2001 From: Luke Butters Date: Fri, 27 Mar 2026 13:43:46 +1100 Subject: [PATCH 2/4] . --- source/Halibut/ServiceEndPoint.cs | 10 +++++----- source/Halibut/Transport/TcpConnectionFactory.cs | 7 +++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/source/Halibut/ServiceEndPoint.cs b/source/Halibut/ServiceEndPoint.cs index e49abec09..9bb5fd83e 100644 --- a/source/Halibut/ServiceEndPoint.cs +++ b/source/Halibut/ServiceEndPoint.cs @@ -77,12 +77,12 @@ public ServiceEndPoint(Uri baseUri, string? remoteThumbprint, ProxyDetails? prox public ProxyDetails? Proxy { get; } /// - /// When set, TCP connections will be made to this host and port instead of the host and port - /// in , while the original host is still used for - /// TLS SNI. This is equivalent to curl's --resolve flag and is useful when routing through a - /// local proxy (e.g. Toxiproxy) while preserving the correct TLS handshake. + /// When set, TCP connections will be made to this host instead of the host in , + /// while the original host is still used for TLS SNI. This is equivalent to + /// curl's --resolve flag and is useful when routing through a local proxy (e.g. Toxiproxy) while + /// preserving the correct TLS handshake. /// - public (string Host, int Port)? ForceResolveAddress { get; set; } + public string? ForceResolveHost { get; set; } public bool IsWebSocketEndpoint => IsWebSocketAddress(BaseUri); diff --git a/source/Halibut/Transport/TcpConnectionFactory.cs b/source/Halibut/Transport/TcpConnectionFactory.cs index 10a8f1c51..3d5dbd66b 100644 --- a/source/Halibut/Transport/TcpConnectionFactory.cs +++ b/source/Halibut/Transport/TcpConnectionFactory.cs @@ -79,12 +79,11 @@ await ssl.AuthenticateAsClientAsync( internal static async Task CreateConnectedTcpClientAsync(ServiceEndPoint endPoint, HalibutTimeoutsAndLimits halibutTimeoutsAndLimits, IStreamFactory streamFactory, ILog log, CancellationToken cancellationToken) { TcpClient client; - var connectHost = endPoint.ForceResolveAddress?.Host ?? endPoint.BaseUri.Host; - var connectPort = endPoint.ForceResolveAddress?.Port ?? endPoint.BaseUri.Port; + var connectHost = endPoint.ForceResolveHost ?? endPoint.BaseUri.Host; if (endPoint.Proxy is null) { client = CreateTcpClientAsync(halibutTimeoutsAndLimits); - await client.ConnectWithTimeoutAsync(connectHost, connectPort, endPoint.TcpClientConnectTimeout, cancellationToken); + await client.ConnectWithTimeoutAsync(connectHost, endPoint.BaseUri.Port, endPoint.TcpClientConnectTimeout, cancellationToken); } else { @@ -93,7 +92,7 @@ internal static async Task CreateConnectedTcpClientAsync(ServiceEndPo client = await new ProxyClientFactory(streamFactory) .CreateProxyClient(log, endPoint.Proxy) .WithTcpClientFactory(() => CreateTcpClientAsync(halibutTimeoutsAndLimits)) - .CreateConnectionAsync(connectHost, connectPort, endPoint.TcpClientConnectTimeout, cancellationToken); + .CreateConnectionAsync(connectHost, endPoint.BaseUri.Port, endPoint.TcpClientConnectTimeout, cancellationToken); } return client; } From 768048691022cf3203646fc9ce92034d07aa39d1 Mon Sep 17 00:00:00 2001 From: Luke Butters Date: Mon, 30 Mar 2026 09:40:16 +1100 Subject: [PATCH 3/4] . --- .../Queue/Redis/MessageStorage/IRehydrateDataStream.cs | 4 ++-- source/Halibut/ServiceEndPoint.cs | 10 +++++----- source/Halibut/Transport/TcpConnectionFactory.cs | 7 ++++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/source/Halibut/Queue/Redis/MessageStorage/IRehydrateDataStream.cs b/source/Halibut/Queue/Redis/MessageStorage/IRehydrateDataStream.cs index 3f8440418..600d0bd14 100644 --- a/source/Halibut/Queue/Redis/MessageStorage/IRehydrateDataStream.cs +++ b/source/Halibut/Queue/Redis/MessageStorage/IRehydrateDataStream.cs @@ -80,12 +80,12 @@ public async Task SaveToAsync(string filePath, CancellationToken cancellationTok } } - public async Task ReadAsync(Func readerAsync, CancellationToken cancellationToken) + /*public async Task ReadAsync(Func readerAsync, CancellationToken cancellationToken) { await using var dataStreamRehydrationData = DataStreamRehydrationDataSupplier(); await readerAsync(dataStreamRehydrationData.Data, cancellationToken); - } + }*/ } } \ No newline at end of file diff --git a/source/Halibut/ServiceEndPoint.cs b/source/Halibut/ServiceEndPoint.cs index 9bb5fd83e..4d160aa01 100644 --- a/source/Halibut/ServiceEndPoint.cs +++ b/source/Halibut/ServiceEndPoint.cs @@ -77,12 +77,12 @@ public ServiceEndPoint(Uri baseUri, string? remoteThumbprint, ProxyDetails? prox public ProxyDetails? Proxy { get; } /// - /// When set, TCP connections will be made to this host instead of the host in , - /// while the original host is still used for TLS SNI. This is equivalent to - /// curl's --resolve flag and is useful when routing through a local proxy (e.g. Toxiproxy) while - /// preserving the correct TLS handshake. + /// When set, TCP connections will be made to the host and port of this URI instead of those in + /// , while the original host is still used for TLS SNI. + /// This is equivalent to curl's --resolve flag and is useful when routing through a local proxy + /// (e.g. Toxiproxy) while preserving the correct TLS handshake. /// - public string? ForceResolveHost { get; set; } + public Uri? ForceResolveHost { get; set; } public bool IsWebSocketEndpoint => IsWebSocketAddress(BaseUri); diff --git a/source/Halibut/Transport/TcpConnectionFactory.cs b/source/Halibut/Transport/TcpConnectionFactory.cs index 3d5dbd66b..b61e190e5 100644 --- a/source/Halibut/Transport/TcpConnectionFactory.cs +++ b/source/Halibut/Transport/TcpConnectionFactory.cs @@ -79,11 +79,12 @@ await ssl.AuthenticateAsClientAsync( internal static async Task CreateConnectedTcpClientAsync(ServiceEndPoint endPoint, HalibutTimeoutsAndLimits halibutTimeoutsAndLimits, IStreamFactory streamFactory, ILog log, CancellationToken cancellationToken) { TcpClient client; - var connectHost = endPoint.ForceResolveHost ?? endPoint.BaseUri.Host; + var connectHost = endPoint.ForceResolveHost?.Host ?? endPoint.BaseUri.Host; + var connectPort = endPoint.ForceResolveHost?.Port ?? endPoint.BaseUri.Port; if (endPoint.Proxy is null) { client = CreateTcpClientAsync(halibutTimeoutsAndLimits); - await client.ConnectWithTimeoutAsync(connectHost, endPoint.BaseUri.Port, endPoint.TcpClientConnectTimeout, cancellationToken); + await client.ConnectWithTimeoutAsync(connectHost, connectPort, endPoint.TcpClientConnectTimeout, cancellationToken); } else { @@ -92,7 +93,7 @@ internal static async Task CreateConnectedTcpClientAsync(ServiceEndPo client = await new ProxyClientFactory(streamFactory) .CreateProxyClient(log, endPoint.Proxy) .WithTcpClientFactory(() => CreateTcpClientAsync(halibutTimeoutsAndLimits)) - .CreateConnectionAsync(connectHost, endPoint.BaseUri.Port, endPoint.TcpClientConnectTimeout, cancellationToken); + .CreateConnectionAsync(connectHost, connectPort, endPoint.TcpClientConnectTimeout, cancellationToken); } return client; } From 152fdc1c0dbb558755e06af69d4a0d07f6c1340b Mon Sep 17 00:00:00 2001 From: Luke Butters Date: Mon, 30 Mar 2026 09:46:40 +1100 Subject: [PATCH 4/4] . --- .../Queue/Redis/MessageStorage/IRehydrateDataStream.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/Halibut/Queue/Redis/MessageStorage/IRehydrateDataStream.cs b/source/Halibut/Queue/Redis/MessageStorage/IRehydrateDataStream.cs index 600d0bd14..3f8440418 100644 --- a/source/Halibut/Queue/Redis/MessageStorage/IRehydrateDataStream.cs +++ b/source/Halibut/Queue/Redis/MessageStorage/IRehydrateDataStream.cs @@ -80,12 +80,12 @@ public async Task SaveToAsync(string filePath, CancellationToken cancellationTok } } - /*public async Task ReadAsync(Func readerAsync, CancellationToken cancellationToken) + public async Task ReadAsync(Func readerAsync, CancellationToken cancellationToken) { await using var dataStreamRehydrationData = DataStreamRehydrationDataSupplier(); await readerAsync(dataStreamRehydrationData.Data, cancellationToken); - }*/ + } } } \ No newline at end of file