diff --git a/source/Halibut.Tests/Transport/SecureClientFixture.cs b/source/Halibut.Tests/Transport/SecureClientFixture.cs index 3a8a83673..80f7e8bed 100644 --- a/source/Halibut.Tests/Transport/SecureClientFixture.cs +++ b/source/Halibut.Tests/Transport/SecureClientFixture.cs @@ -80,7 +80,7 @@ public async Task SecureClientClearsPoolWhenAllConnectionsCorrupt() Params = new object[] { "Fred" } }; - var tcpConnectionFactory = new TcpConnectionFactory(Certificates.Octopus, halibutTimeoutsAndLimits, new StreamFactory()); + var tcpConnectionFactory = new TcpConnectionFactory(Certificates.Octopus, halibutTimeoutsAndLimits, new StreamFactory(), NoOpSecureConnectionObserver.Instance); var secureClient = new SecureListeningClient(GetProtocol, endpoint, Certificates.Octopus, log, connectionManager, tcpConnectionFactory); ResponseMessage response = null!; diff --git a/source/Halibut.Tests/Transport/SecureListenerFixture.cs b/source/Halibut.Tests/Transport/SecureListenerFixture.cs index 0914aad3b..69775b77e 100644 --- a/source/Halibut.Tests/Transport/SecureListenerFixture.cs +++ b/source/Halibut.Tests/Transport/SecureListenerFixture.cs @@ -73,7 +73,8 @@ public async Task SecureListenerDoesNotCreateHundredsOfIoEventsPerSecondOnWindow (_, _) => UnauthorizedClientConnectResponse.BlockConnection, timeoutsAndLimits, new StreamFactory(), - NoOpConnectionsObserver.Instance + NoOpConnectionsObserver.Instance, + NoOpSecureConnectionObserver.Instance ); var idleAverage = CollectCounterValues(opsPerSec) diff --git a/source/Halibut/HalibutRuntime.cs b/source/Halibut/HalibutRuntime.cs index 93d005c90..368899c1b 100644 --- a/source/Halibut/HalibutRuntime.cs +++ b/source/Halibut/HalibutRuntime.cs @@ -43,6 +43,7 @@ public class HalibutRuntime : IHalibutRuntime readonly IRpcObserver rpcObserver; readonly TcpConnectionFactory tcpConnectionFactory; readonly IConnectionsObserver connectionsObserver; + readonly ISecureConnectionObserver secureConnectionObserver; readonly IActiveTcpConnectionsLimiter activeTcpConnectionsLimiter; readonly IControlMessageObserver controlMessageObserver; @@ -59,7 +60,9 @@ internal HalibutRuntime( IStreamFactory streamFactory, IRpcObserver rpcObserver, IConnectionsObserver connectionsObserver, - IControlMessageObserver controlMessageObserver) + IControlMessageObserver controlMessageObserver, + ISecureConnectionObserver secureConnectionObserver + ) { this.serverCertificate = serverCertificate; this.trustProvider = trustProvider; @@ -73,10 +76,11 @@ internal HalibutRuntime( invoker = new ServiceInvoker(serviceFactory); TimeoutsAndLimits = halibutTimeoutsAndLimits; this.connectionsObserver = connectionsObserver; + this.secureConnectionObserver = secureConnectionObserver; this.controlMessageObserver = controlMessageObserver; connectionManager = new ConnectionManagerAsync(); - this.tcpConnectionFactory = new TcpConnectionFactory(serverCertificate, TimeoutsAndLimits, streamFactory); + this.tcpConnectionFactory = new TcpConnectionFactory(serverCertificate, TimeoutsAndLimits, streamFactory, secureConnectionObserver); activeTcpConnectionsLimiter = new ActiveTcpConnectionsLimiter(TimeoutsAndLimits); } @@ -130,7 +134,9 @@ public int Listen(IPEndPoint endpoint) HandleUnauthorizedClientConnect, TimeoutsAndLimits, streamFactory, - connectionsObserver); + connectionsObserver, + secureConnectionObserver + ); listeners.DoWithExclusiveAccess(l => { diff --git a/source/Halibut/HalibutRuntimeBuilder.cs b/source/Halibut/HalibutRuntimeBuilder.cs index 287fcab96..7869d38f2 100644 --- a/source/Halibut/HalibutRuntimeBuilder.cs +++ b/source/Halibut/HalibutRuntimeBuilder.cs @@ -28,6 +28,7 @@ public class HalibutRuntimeBuilder IStreamFactory? streamFactory; IRpcObserver? rpcObserver; IConnectionsObserver? connectionsObserver; + ISecureConnectionObserver? secureConnectionObserver; IControlMessageObserver? controlMessageObserver; MessageStreamWrappers queueMessageStreamWrappers = new(); @@ -43,6 +44,12 @@ public HalibutRuntimeBuilder WithConnectionsObserver(IConnectionsObserver connec return this; } + public HalibutRuntimeBuilder WithSecureConnectionObserver(ISecureConnectionObserver secureConnectionsObserver) + { + this.secureConnectionObserver = secureConnectionsObserver; + return this; + } + internal HalibutRuntimeBuilder WithStreamFactory(IStreamFactory streamFactory) { this.streamFactory = streamFactory; @@ -175,6 +182,7 @@ public HalibutRuntime Build() var streamFactory = this.streamFactory ?? new StreamFactory(); var connectionsObserver = this.connectionsObserver ?? NoOpConnectionsObserver.Instance; + var secureConnectionObserver = this.secureConnectionObserver ?? NoOpSecureConnectionObserver.Instance; var rpcObserver = this.rpcObserver ?? new NoRpcObserver(); var controlMessageObserver = this.controlMessageObserver ?? new NoOpControlMessageObserver(); @@ -191,7 +199,9 @@ public HalibutRuntime Build() streamFactory, rpcObserver, connectionsObserver, - controlMessageObserver); + controlMessageObserver, + secureConnectionObserver + ); if (onUnauthorizedClientConnect is not null) { diff --git a/source/Halibut/Transport/Observability/ConnectionDirection.cs b/source/Halibut/Transport/Observability/ConnectionDirection.cs new file mode 100644 index 000000000..7254e20ee --- /dev/null +++ b/source/Halibut/Transport/Observability/ConnectionDirection.cs @@ -0,0 +1,22 @@ +// Copyright 2012-2013 Octopus Deploy Pty. Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Halibut.Transport.Observability +{ + public enum ConnectionDirection + { + Incoming, + Outgoing + } +} \ No newline at end of file diff --git a/source/Halibut/Transport/Observability/ISecureConnectionObserver.cs b/source/Halibut/Transport/Observability/ISecureConnectionObserver.cs new file mode 100644 index 000000000..394a3a169 --- /dev/null +++ b/source/Halibut/Transport/Observability/ISecureConnectionObserver.cs @@ -0,0 +1,23 @@ +// Copyright 2012-2013 Octopus Deploy Pty. Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Security.Authentication; + +namespace Halibut.Transport.Observability +{ + public interface ISecureConnectionObserver + { + public void SecureConnectionEstablished(SecureConnectionInfo secureConnectionInfo); + } +} \ No newline at end of file diff --git a/source/Halibut/Transport/Observability/NoOpSecureConnectionObserver.cs b/source/Halibut/Transport/Observability/NoOpSecureConnectionObserver.cs new file mode 100644 index 000000000..90fb6a33c --- /dev/null +++ b/source/Halibut/Transport/Observability/NoOpSecureConnectionObserver.cs @@ -0,0 +1,25 @@ +// Copyright 2012-2013 Octopus Deploy Pty. Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Halibut.Transport.Observability +{ + public class NoOpSecureConnectionObserver : ISecureConnectionObserver + { + static NoOpSecureConnectionObserver? singleInstance; + public static NoOpSecureConnectionObserver Instance => singleInstance ??= new NoOpSecureConnectionObserver(); + public void SecureConnectionEstablished(SecureConnectionInfo secureConnectionInfo) + { + } + } +} \ No newline at end of file diff --git a/source/Halibut/Transport/Observability/SecureConnectionInfo.cs b/source/Halibut/Transport/Observability/SecureConnectionInfo.cs new file mode 100644 index 000000000..c1b323725 --- /dev/null +++ b/source/Halibut/Transport/Observability/SecureConnectionInfo.cs @@ -0,0 +1,52 @@ +// Copyright 2012-2013 Octopus Deploy Pty. Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Security.Authentication; + +namespace Halibut.Transport.Observability +{ + public struct SecureConnectionInfo + { + SecureConnectionInfo( + SslProtocols sslProtocols, + ConnectionDirection connectionDirection, + string thumbprint + ) + { + SslProtocols = sslProtocols; + ConnectionDirection = connectionDirection; + Thumbprint = thumbprint; + } + + public SslProtocols SslProtocols { get; } + public ConnectionDirection ConnectionDirection { get; } + public string Thumbprint { get; } + + public static SecureConnectionInfo CreateIncoming( + SslProtocols sslProtocols, + string thumbprint + ) + { + return new SecureConnectionInfo(sslProtocols, ConnectionDirection.Incoming, thumbprint); + } + + public static SecureConnectionInfo CreateOutgoing( + SslProtocols sslProtocols, + string thumbprint + ) + { + return new(sslProtocols, ConnectionDirection.Outgoing, thumbprint); + } + } +} \ No newline at end of file diff --git a/source/Halibut/Transport/SecureListener.cs b/source/Halibut/Transport/SecureListener.cs index 59f666f84..5132e1a4e 100644 --- a/source/Halibut/Transport/SecureListener.cs +++ b/source/Halibut/Transport/SecureListener.cs @@ -48,6 +48,7 @@ public class SecureListener : IAsyncDisposable readonly HalibutTimeoutsAndLimits halibutTimeoutsAndLimits; readonly IStreamFactory streamFactory; readonly IConnectionsObserver connectionsObserver; + readonly ISecureConnectionObserver secureConnectionObserver; ILog log; TcpListener listener; Thread? backgroundThread; @@ -67,7 +68,9 @@ public SecureListener( Func unauthorizedClientConnect, HalibutTimeoutsAndLimits halibutTimeoutsAndLimits, IStreamFactory streamFactory, - IConnectionsObserver connectionsObserver) + IConnectionsObserver connectionsObserver, + ISecureConnectionObserver secureConnectionObserver + ) { this.endPoint = endPoint; this.serverCertificate = serverCertificate; @@ -81,6 +84,7 @@ public SecureListener( this.halibutTimeoutsAndLimits = halibutTimeoutsAndLimits; this.streamFactory = streamFactory; this.connectionsObserver = connectionsObserver; + this.secureConnectionObserver = secureConnectionObserver; this.cts = new CancellationTokenSource(); this.cancellationToken = cts.Token; @@ -336,6 +340,7 @@ await ssl { connectionAuthorizedAndObserved = true; connectionsObserver.ConnectionAccepted(true); + secureConnectionObserver.SecureConnectionEstablished(SecureConnectionInfo.CreateIncoming(ssl.SslProtocol, thumbprint)); tcpClientManager.AddActiveClient(thumbprint, client); errorEventType = EventType.Error; await ExchangeMessages(ssl).ConfigureAwait(false); diff --git a/source/Halibut/Transport/SslConfiguration.cs b/source/Halibut/Transport/SslConfiguration.cs index d4e066df6..28259c759 100644 --- a/source/Halibut/Transport/SslConfiguration.cs +++ b/source/Halibut/Transport/SslConfiguration.cs @@ -1,16 +1,47 @@ +using System; using System.Security.Authentication; namespace Halibut.Transport { public static class SslConfiguration { - public static SslProtocols SupportedProtocols + static SslProtocols GetSupportedProtocols() { +#if NETFRAMEWORK + // Net48 tests has issues establishing a common algorithm when we allow system default + var supportedProtocolMode = "legacy"; +#else + var supportedProtocolMode = Environment.GetEnvironmentVariable("HALIBUT_SUPPORTED_SSL_PROTOCOLS")?.ToLowerInvariant(); +#endif + + + if (supportedProtocolMode == "legacy") + { #pragma warning disable SYSLIB0039 - // See https://learn.microsoft.com/en-us/dotnet/fundamentals/syslib-diagnostics/syslib0039 - // TLS 1.0 and 1.1 are obsolete from .NET 7 - get => SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13; + // See https://learn.microsoft.com/en-us/dotnet/fundamentals/syslib-diagnostics/syslib0039 + // TLS 1.0 and 1.1 are obsolete from .NET 7 + return SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13; #pragma warning restore SYSLIB0039 + } + + if (supportedProtocolMode == "system") + { + return SslProtocols.None; + } + + if (supportedProtocolMode == "modern") + { + return SslProtocols.Tls12 | SslProtocols.Tls13; + } + + if (supportedProtocolMode == "tls1.3") + { + return SslProtocols.Tls13; + } + + return SslProtocols.None; } + + public static SslProtocols SupportedProtocols { get; } = GetSupportedProtocols(); } } \ No newline at end of file diff --git a/source/Halibut/Transport/TcpConnectionFactory.cs b/source/Halibut/Transport/TcpConnectionFactory.cs index 2931297a5..ac637cd9b 100644 --- a/source/Halibut/Transport/TcpConnectionFactory.cs +++ b/source/Halibut/Transport/TcpConnectionFactory.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Halibut.Diagnostics; +using Halibut.Transport.Observability; using Halibut.Transport.Protocol; using Halibut.Transport.Proxy; using Halibut.Transport.Streams; @@ -20,12 +21,19 @@ public class TcpConnectionFactory : IConnectionFactory readonly X509Certificate2 clientCertificate; readonly HalibutTimeoutsAndLimits halibutTimeoutsAndLimits; readonly IStreamFactory streamFactory; + readonly ISecureConnectionObserver secureConnectionObserver; - public TcpConnectionFactory(X509Certificate2 clientCertificate, HalibutTimeoutsAndLimits halibutTimeoutsAndLimits, IStreamFactory streamFactory) + public TcpConnectionFactory( + X509Certificate2 clientCertificate, + HalibutTimeoutsAndLimits halibutTimeoutsAndLimits, + IStreamFactory streamFactory, + ISecureConnectionObserver secureConnectionObserver + ) { this.clientCertificate = clientCertificate; this.halibutTimeoutsAndLimits = halibutTimeoutsAndLimits; this.streamFactory = streamFactory; + this.secureConnectionObserver = secureConnectionObserver; } public async Task EstablishNewConnectionAsync(ExchangeProtocolBuilder exchangeProtocolBuilder, ServiceEndPoint serviceEndpoint, ILog log, CancellationToken cancellationToken) @@ -60,6 +68,7 @@ await ssl.AuthenticateAsClientAsync( await ssl.FlushAsync(cancellationToken); log.Write(EventType.Security, "Secure connection established. Server at {0} identified by thumbprint: {1}, using protocol {2}", client.Client.RemoteEndPoint, serviceEndpoint.RemoteThumbprint, ssl.SslProtocol.ToString()); + secureConnectionObserver.SecureConnectionEstablished(SecureConnectionInfo.CreateOutgoing(ssl.SslProtocol, serviceEndpoint.RemoteThumbprint ?? "Unknown")); return new SecureConnection(client, ssl, exchangeProtocolBuilder, halibutTimeoutsAndLimits, log); }