From 73362a056a63865e9fa30b556d763d38336c511f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 9 Nov 2026 12:30:51 -0500 Subject: [PATCH] fix(questdb): disable Npgsql GSS encryption probe unless the connection string sets it (lmxopcua#758) QuestDB speaks plain PG-wire; Npgsql's default GSS Encryption Mode=Prefer made Linux central boots log 'Cannot load library libgssapi_krb5.so.2'. An explicit 'GSS Encryption Mode' in the connection string is kept. Not re-verified on a Linux host here; the Postgres Sql driver is left alone (Kerberos may be wanted). Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV --- .../QuestDbConnection.cs | 23 +++++++++++++++---- .../QuestDbConnectionTests.cs | 18 +++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb/QuestDbConnection.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb/QuestDbConnection.cs index 54d430209..f04d4e682 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb/QuestDbConnection.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb/QuestDbConnection.cs @@ -42,6 +42,13 @@ internal sealed class QuestDbConnection : IAsyncDisposable, IDisposable var explicitPool = HasExplicitMaxPoolSize(options.ConnectionString); if (!explicitPool) csb.MaxPoolSize = DefaultMaxPoolSize; EffectiveMaxPoolSize = csb.MaxPoolSize; + // #758: QuestDB speaks plain PG-wire with no GSS support. Npgsql's default GSS Encryption Mode=Prefer probes + // for GSSAPI on every new physical connection, which on Linux logs "Cannot load library libgssapi_krb5.so.2" + // when the Kerberos client library is not installed. The probe falls back cleanly, so this only silences a + // misleading line; an explicit "GSS Encryption Mode" in the connection string is always kept. + if (!HasExplicitKey(options.ConnectionString, "GSSEncryptionMode")) + csb.GssEncryptionMode = GssEncryptionMode.Disable; + EffectiveGssEncryptionMode = csb.GssEncryptionMode; (log ?? NullLogger.Instance).LogInformation( "QuestDB historian connection pool: Maximum Pool Size = {MaxPoolSize} ({Source}).", EffectiveMaxPoolSize, explicitPool ? "from the connection string" : "default cap"); @@ -51,17 +58,23 @@ internal sealed class QuestDbConnection : IAsyncDisposable, IDisposable /// The Npgsql Maximum Pool Size in effect (the connection string's own value, else ). internal int EffectiveMaxPoolSize { get; } + /// The Npgsql GSS Encryption Mode in effect (the connection string's own value, else Disable, #758). + internal GssEncryptionMode EffectiveGssEncryptionMode { get; } + // Npgsql accepts "Maximum Pool Size" and "MaxPoolSize" (case-insensitive); a generic parse keeps the caller's own // keywords so "was it set" is answered by the string itself, not by the builder's default. - private static bool HasExplicitMaxPoolSize(string connectionString) + private static bool HasExplicitMaxPoolSize(string connectionString) => + HasExplicitKey(connectionString, "MaxPoolSize", "MaximumPoolSize"); + + // Spaces are ignored in keywords ("GSS Encryption Mode" == "GSSEncryptionMode"); matching is case-insensitive. + private static bool HasExplicitKey(string connectionString, params string[] normalizedKeys) { var generic = new System.Data.Common.DbConnectionStringBuilder { ConnectionString = connectionString }; foreach (string key in generic.Keys) { - var k = key.Replace(" ", "", StringComparison.Ordinal); - if (string.Equals(k, "MaxPoolSize", StringComparison.OrdinalIgnoreCase) - || string.Equals(k, "MaximumPoolSize", StringComparison.OrdinalIgnoreCase)) - return true; + var k = key.Replace(" ", "", StringComparison.Ordinal).Replace("_", "", StringComparison.Ordinal); + foreach (var wanted in normalizedKeys) + if (string.Equals(k, wanted, StringComparison.OrdinalIgnoreCase)) return true; } return false; } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.Tests/QuestDbConnectionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.Tests/QuestDbConnectionTests.cs index afc09155e..0ed41bf55 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.Tests/QuestDbConnectionTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.Tests/QuestDbConnectionTests.cs @@ -146,6 +146,24 @@ public sealed class QuestDbConnectionTests return c.EffectiveMaxPoolSize; } + // ---- GSS probe (#758) ------------------------------------------------------------------------------------------- + + private static Npgsql.GssEncryptionMode GssOf(string cs) + { + using var c = new QuestDbConnection(new QuestDbHistorianOptions { ConnectionString = cs }); + return c.EffectiveGssEncryptionMode; + } + + [Fact] + public void An_unset_GSS_Encryption_Mode_is_disabled() => + GssOf("Host=127.0.0.1;Port=1").ShouldBe(Npgsql.GssEncryptionMode.Disable); + + [Theory] + [InlineData("Host=127.0.0.1;Port=1;GSS Encryption Mode=Prefer", Npgsql.GssEncryptionMode.Prefer)] + [InlineData("Host=127.0.0.1;Port=1;GssEncryptionMode=Require", Npgsql.GssEncryptionMode.Require)] + public void An_explicit_GSS_Encryption_Mode_is_kept(string cs, Npgsql.GssEncryptionMode expected) => + GssOf(cs).ShouldBe(expected); + [Fact] public void An_unset_Maximum_Pool_Size_is_capped_at_24() => PoolOf("Host=127.0.0.1;Port=1").ShouldBe(24);