feat(lmxproxy): phase 1 — v2 protocol types and domain model

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-21 23:41:56 -04:00
parent 08d2a07d8b
commit 0d63fb1105
87 changed files with 3389 additions and 956 deletions

View File

@@ -0,0 +1,184 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using Grpc.Net.Client;
using Microsoft.Extensions.Logging;
namespace ZB.MOM.WW.LmxProxy.Client.Security;
internal static class GrpcChannelFactory
{
private const string Http2UnencryptedSwitch = "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport";
static GrpcChannelFactory()
{
AppContext.SetSwitch(Http2UnencryptedSwitch, true);
}
/// <summary>
/// Creates a gRPC channel with optional TLS configuration.
/// </summary>
/// <param name="address">The server address.</param>
/// <param name="tlsConfiguration">Optional TLS configuration.</param>
/// <param name="logger">The logger.</param>
/// <returns>A configured gRPC channel.</returns>
public static GrpcChannel CreateChannel(Uri address, ClientTlsConfiguration? tlsConfiguration, ILogger logger)
{
var options = new GrpcChannelOptions
{
HttpHandler = CreateHttpHandler(tlsConfiguration, logger)
};
return GrpcChannel.ForAddress(address, options);
}
private static HttpMessageHandler CreateHttpHandler(ClientTlsConfiguration? tlsConfiguration, ILogger logger)
{
var handler = new SocketsHttpHandler
{
AutomaticDecompression = DecompressionMethods.None,
AllowAutoRedirect = false,
EnableMultipleHttp2Connections = true
};
if (tlsConfiguration?.UseTls == true)
{
ConfigureTls(handler, tlsConfiguration, logger);
}
return handler;
}
private static void ConfigureTls(SocketsHttpHandler handler, ClientTlsConfiguration tlsConfiguration, ILogger logger)
{
SslClientAuthenticationOptions sslOptions = handler.SslOptions;
sslOptions.EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13;
if (!string.IsNullOrWhiteSpace(tlsConfiguration.ServerNameOverride))
{
sslOptions.TargetHost = tlsConfiguration.ServerNameOverride;
}
if (!string.IsNullOrWhiteSpace(tlsConfiguration.ClientCertificatePath) &&
!string.IsNullOrWhiteSpace(tlsConfiguration.ClientKeyPath))
{
try
{
var clientCertificate = X509Certificate2.CreateFromPemFile(
tlsConfiguration.ClientCertificatePath,
tlsConfiguration.ClientKeyPath);
clientCertificate = new X509Certificate2(clientCertificate.Export(X509ContentType.Pfx));
sslOptions.ClientCertificates ??= new X509CertificateCollection();
sslOptions.ClientCertificates.Add(clientCertificate);
logger.LogInformation("Configured client certificate for mutual TLS ({CertificatePath})", tlsConfiguration.ClientCertificatePath);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to load client certificate from {CertificatePath}", tlsConfiguration.ClientCertificatePath);
}
}
sslOptions.RemoteCertificateValidationCallback = (_, certificate, chain, sslPolicyErrors) =>
ValidateServerCertificate(tlsConfiguration, logger, certificate, chain, sslPolicyErrors);
}
private static bool ValidateServerCertificate(
ClientTlsConfiguration tlsConfiguration,
ILogger logger,
X509Certificate? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors)
{
if (tlsConfiguration.IgnoreAllCertificateErrors)
{
logger.LogWarning("SECURITY WARNING: Ignoring all certificate validation errors for LmxProxy gRPC connection.");
return true;
}
if (certificate is null)
{
logger.LogWarning("Server certificate was null.");
return false;
}
if (!tlsConfiguration.ValidateServerCertificate)
{
logger.LogWarning("SECURITY WARNING: Server certificate validation disabled for LmxProxy gRPC connection.");
return true;
}
X509Certificate2 certificate2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
if (!string.IsNullOrWhiteSpace(tlsConfiguration.ServerNameOverride))
{
string dnsName = certificate2.GetNameInfo(X509NameType.DnsName, forIssuer: false);
if (!string.Equals(dnsName, tlsConfiguration.ServerNameOverride, StringComparison.OrdinalIgnoreCase))
{
logger.LogWarning("Server certificate subject '{Subject}' does not match expected host '{ExpectedHost}'",
dnsName, tlsConfiguration.ServerNameOverride);
return false;
}
}
using X509Chain validationChain = chain ?? new X509Chain();
validationChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
validationChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
if (!string.IsNullOrWhiteSpace(tlsConfiguration.ServerCaCertificatePath) &&
File.Exists(tlsConfiguration.ServerCaCertificatePath))
{
try
{
X509Certificate2 ca = LoadCertificate(tlsConfiguration.ServerCaCertificatePath);
validationChain.ChainPolicy.CustomTrustStore.Add(ca);
validationChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to load CA certificate from {Path}", tlsConfiguration.ServerCaCertificatePath);
}
}
if (tlsConfiguration.AllowSelfSignedCertificates)
{
validationChain.ChainPolicy.VerificationFlags |= X509VerificationFlags.AllowUnknownCertificateAuthority;
}
bool isValid = validationChain.Build(certificate2);
if (isValid)
{
return true;
}
if (tlsConfiguration.AllowSelfSignedCertificates &&
validationChain.ChainStatus.All(status =>
status.Status == X509ChainStatusFlags.UntrustedRoot ||
status.Status == X509ChainStatusFlags.PartialChain))
{
logger.LogWarning("Accepting self-signed certificate for {Subject}", certificate2.Subject);
return true;
}
string statusMessage = string.Join(", ", validationChain.ChainStatus.Select(s => s.Status));
logger.LogWarning("Server certificate validation failed: {Status}", statusMessage);
return false;
}
private static X509Certificate2 LoadCertificate(string path)
{
try
{
return X509Certificate2.CreateFromPemFile(path);
}
catch
{
return new X509Certificate2(File.ReadAllBytes(path));
}
}
}