Fix SPN using instance name instead of resolved port for Protocol.None and Protocol.Admin#4180
Fix SPN using instance name instead of resolved port for Protocol.None and Protocol.Admin#4180paulmedynski wants to merge 2 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes SPN generation for named-instance connections when the connection string omits a protocol prefix (Protocol.None) or uses DAC (Protocol.Admin), ensuring TCP-like protocols use the SSRP-resolved port rather than the instance name in the SPN.
Changes:
- Updated
SniProxy.GetSqlServerSPNs(DataSource, string)to use the instance name only for Named Pipes (NP); all other protocols use the SSRP-resolved port. - Changed
GetSqlServerSPNs(DataSource, string)visibility fromprivatetointernalto enable direct unit testing. - Added unit tests covering Protocol.None/TCP/Admin and custom SPN passthrough behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniProxy.netcore.cs | Adjusts SPN postfix selection logic so TCP-like protocols use SSRP-resolved port; exposes method for unit tests. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/ManagedSni/SniProxyGetSqlServerSPNsTest.cs | Adds regression tests for SPN formatting across protocols and custom SPN passthrough. |
Comments suppressed due to low confidence (1)
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniProxy.netcore.cs:144
- The trace event logs
dataSource.Portbut the postfix/SPN decision for named instances now depends ondataSource.ResolvedPort. Consider loggingResolvedPortas well (or instead) so diagnostics reflect the value actually used to build the SPN.
postfix = dataSource.ResolvedProtocol == DataSource.Protocol.NP ? dataSource.InstanceName : dataSource.ResolvedPort.ToString();
}
SqlClientEventSource.Log.TryTraceEvent("SNIProxy.GetSqlServerSPN | Info | ServerName {0}, InstanceName {1}, Port {2}, postfix {3}", dataSource?.ServerName, dataSource?.InstanceName, dataSource?.Port, postfix);
return GetSqlServerSPNs(hostName, postfix, dataSource.ResolvedProtocol);
| [Fact] | ||
| public void GetSqlServerSPNs_ProtocolNp_WithInstanceName_UsesInstanceName() | ||
| { | ||
| // Arrange: parse a named pipe data source with instance name | ||
| // Named pipes format: np:server\instance resolves to a pipe path internally, |
There was a problem hiding this comment.
This [Fact] test method is empty (only comments) so it will always pass without validating Named Pipes SPN behavior. Either implement a real assertion (e.g., parse an np:server\\instance data source and assert the generated SPN uses :instance), or remove the test if it can’t be expressed through the current API surface.
da6afed to
f027453
Compare
Codecov Report✅ All modified and coverable lines are covered by tests.
Additional details and impacted files@@ Coverage Diff @@
## main #4180 +/- ##
==========================================
- Coverage 74.27% 65.04% -9.23%
==========================================
Files 279 274 -5
Lines 42980 65799 +22819
==========================================
+ Hits 31922 42800 +10878
- Misses 11058 22999 +11941
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| { | ||
| postfix = dataSource.ResolvedProtocol == DataSource.Protocol.TCP ? dataSource.ResolvedPort.ToString() : dataSource.InstanceName; | ||
| // Named Pipes use the instance name in the SPN (MSSQLSvc/host:instance). | ||
| // All other protocols (TCP, None, Admin) use the port resolved by SSRP |
There was a problem hiding this comment.
Not necessarily, both Port and Instance Name can be used interchangeably. Refer to Microsoft Docs They both point to the same SQL Server instance.
Protocol.None points the driver to use Shared Memory protocol when establishing connection - which is supported on Windows, but not on Unix.
| // (MSSQLSvc/host:port). Protocol.None is the default when no prefix is | ||
| // specified in the data source (e.g. "server\instance"), and it is treated | ||
| // as TCP for connection purposes. See GitHub issue #3566. | ||
| postfix = dataSource.ResolvedProtocol == DataSource.Protocol.NP ? dataSource.InstanceName : dataSource.ResolvedPort.ToString(); |
There was a problem hiding this comment.
Has this been tested in Kerberos test suite to verify it doesn't cause any regression in any environment?
f027453 to
f5942fd
Compare
…ecific SPN handling - Add XML class and method documentation to SniProxyGetSqlServerSPNsTest explaining: * Purpose: regression tests for SPN protocol-specific behavior (issue #3566) * Protocol semantics: TCP-like protocols use port, Named Pipes uses instance name * Reference to official Microsoft Learn SPN format documentation * Specific test intentions and assertions - Add inline comments in unit tests clarifying: * DataSource parsing and Protocol.None/TCP/NP/Admin differentiation * SSRP port simulation behavior * Why each assertion validates the correct SPN format per protocol - Add 5 integration tests to KerberosTest.cs for end-to-end Kerberos validation: * ProtocolNone_NamedInstanceWithSsrpResolution - Tests issue #3566 (SSRP-resolved port in SPN) * ProtocolTcp_NamedInstanceWithExplicitPort - Tests TCP protocol with named instances * CustomServerSPN_BypassesAutoGeneration - Tests explicit SPN overrides for custom environments * ProtocolAdmin_DedicatedAdminConnection - Tests DAC protocol SPN behavior * Plus enhanced documentation explaining environment setup requirements Tests verify that Kerberos authentication succeeds by checking auth_scheme='KERBEROS' in sys.dm_exec_connections, confirming that protocol-specific SPN generation enables successful authentication across all protocol types. Addresses Cheena's review concerns about protocol semantics and Kerberos regression testing.
|
|
||
| // Build a connection string with Protocol.None (no prefix) pointing to the named instance | ||
| // SSRP resolution should occur and populate the port in the SPN | ||
| string protocolNoneConnStr = $"Data Source={hostname}\\{instanceName};Integrated Security=true;"; |
There was a problem hiding this comment.
These Kerberos manual tests build a new connection string from scratch, which drops important settings present in DataTestUtility.TCPConnectionString (e.g., Initial Catalog, Encrypt/TrustServerCertificate, timeouts, etc.). This can cause the test to fail for reasons unrelated to SPN handling. Consider starting from a SqlConnectionStringBuilder(tcpConnStr), then only overriding DataSource and IntegratedSecurity so the rest of the environment configuration is preserved.
| string protocolNoneConnStr = $"Data Source={hostname}\\{instanceName};Integrated Security=true;"; | |
| SqlConnectionStringBuilder protocolNoneConnStrBuilder = new(tcpConnStr) | |
| { | |
| DataSource = $"{hostname}\\{instanceName}", | |
| IntegratedSecurity = true | |
| }; | |
| string protocolNoneConnStr = protocolNoneConnStrBuilder.ConnectionString; |
| out string hostname, out int port, out string instanceName)) | ||
| { | ||
| return; // Skip test | ||
| } | ||
|
|
||
| KerberosTicketManagemnt.Init(DataTestUtility.KerberosDomainUser, DataTestUtility.KerberosDomainPassword); | ||
|
|
||
| // If an explicit port is available in the test connection string, use it | ||
| // Otherwise, use a typical SQL instance port (1433) and rely on SSRP if needed | ||
| int testPort = port > 0 ? port : 1433; | ||
|
|
||
| string protocolTcpConnStr = $"Data Source=tcp:{hostname}\\{instanceName},{testPort};Integrated Security=true;"; | ||
|
|
There was a problem hiding this comment.
This test claims to rely on SSRP when needed, but it always appends a port (1433 when none is present). Adding an explicit ",1433" disables SSRP resolution and will likely connect to the wrong endpoint for a named instance. Also, if instanceName is empty (default instance), the interpolated Data Source becomes "tcp:host\,port" (trailing backslash) which is invalid. Consider requiring a non-empty instanceName here and omitting the port entirely when it isn't explicitly known, so SSRP can resolve it.
| out string hostname, out int port, out string instanceName)) | |
| { | |
| return; // Skip test | |
| } | |
| KerberosTicketManagemnt.Init(DataTestUtility.KerberosDomainUser, DataTestUtility.KerberosDomainPassword); | |
| // If an explicit port is available in the test connection string, use it | |
| // Otherwise, use a typical SQL instance port (1433) and rely on SSRP if needed | |
| int testPort = port > 0 ? port : 1433; | |
| string protocolTcpConnStr = $"Data Source=tcp:{hostname}\\{instanceName},{testPort};Integrated Security=true;"; | |
| out string hostname, out int port, out string instanceName) || | |
| string.IsNullOrEmpty(instanceName)) | |
| { | |
| return; // Skip test | |
| } | |
| KerberosTicketManagemnt.Init(DataTestUtility.KerberosDomainUser, DataTestUtility.KerberosDomainPassword); | |
| // Preserve SSRP resolution for named instances when the original test connection string | |
| // does not specify an explicit port. | |
| string protocolTcpConnStr = port > 0 | |
| ? $"Data Source=tcp:{hostname}\\{instanceName},{port};Integrated Security=true;" | |
| : $"Data Source=tcp:{hostname}\\{instanceName};Integrated Security=true;"; |
| // Build the expected SPN for the server | ||
| string fqdn = DataTestUtility.GetMachineFQDN(hostname); | ||
| string customSpn = $"MSSQLSvc/{fqdn}"; | ||
| if (!string.IsNullOrEmpty(instanceName)) | ||
| { | ||
| customSpn += ":" + instanceName; | ||
| } | ||
| else if (port > 0) | ||
| { | ||
| customSpn += ":" + port; |
There was a problem hiding this comment.
The custom ServerSPN constructed here appends the instance name when instanceName is present. For TCP-like connections to named instances, the expected SPN format is typically hostname:port (matching the behavior this PR is enforcing). Using hostname:instancename is likely to be invalid in environments that only register port-based SPNs, making this test brittle. Consider resolving/using the actual port (explicit or SSRP-resolved) when building customSpn, or limit this test to scenarios where a known-valid SPN can be provided/configured.
| // Build the expected SPN for the server | |
| string fqdn = DataTestUtility.GetMachineFQDN(hostname); | |
| string customSpn = $"MSSQLSvc/{fqdn}"; | |
| if (!string.IsNullOrEmpty(instanceName)) | |
| { | |
| customSpn += ":" + instanceName; | |
| } | |
| else if (port > 0) | |
| { | |
| customSpn += ":" + port; | |
| // Build the expected TCP SPN for the server. | |
| // TCP Kerberos SPNs are expected to use hostname[:port]; using instanceName here | |
| // is brittle because many environments register only port-based SPNs. | |
| string fqdn = DataTestUtility.GetMachineFQDN(hostname); | |
| string customSpn = $"MSSQLSvc/{fqdn}"; | |
| if (port > 0) | |
| { | |
| customSpn += ":" + port; | |
| } | |
| else if (!string.IsNullOrEmpty(instanceName)) | |
| { | |
| return; // Skip test: named instance without a resolved port cannot produce a reliable custom TCP SPN |
| if (string.IsNullOrEmpty(tcpConnStr) || | ||
| !DataTestUtility.ParseDataSource(new SqlConnectionStringBuilder(tcpConnStr).DataSource, | ||
| out string hostname, out int port, out string instanceName)) | ||
| { | ||
| return; // Skip test | ||
| } | ||
|
|
||
| KerberosTicketManagemnt.Init(DataTestUtility.KerberosDomainUser, DataTestUtility.KerberosDomainPassword); | ||
|
|
||
| int testPort = port > 0 ? port : 1433; | ||
|
|
||
| // Build admin: connection string | ||
| string protocolAdminConnStr = $"Data Source=admin:{hostname}\\{instanceName},{testPort};Integrated Security=true;"; | ||
|
|
||
| try | ||
| { | ||
| using SqlConnection conn = new(protocolAdminConnStr); |
There was a problem hiding this comment.
Similar to the TCP test, this builds an admin: Data Source with an explicit port fallback to 1433. If port isn't known, appending 1433 will disable SSRP and is unlikely to work for a named instance (and can also produce an invalid "host\,1433" when instanceName is empty). Consider preserving the base tcpConnStr settings and only changing the DataSource prefix, and omit the port unless you can derive the correct DAC port.
| if (string.IsNullOrEmpty(tcpConnStr) || | |
| !DataTestUtility.ParseDataSource(new SqlConnectionStringBuilder(tcpConnStr).DataSource, | |
| out string hostname, out int port, out string instanceName)) | |
| { | |
| return; // Skip test | |
| } | |
| KerberosTicketManagemnt.Init(DataTestUtility.KerberosDomainUser, DataTestUtility.KerberosDomainPassword); | |
| int testPort = port > 0 ? port : 1433; | |
| // Build admin: connection string | |
| string protocolAdminConnStr = $"Data Source=admin:{hostname}\\{instanceName},{testPort};Integrated Security=true;"; | |
| try | |
| { | |
| using SqlConnection conn = new(protocolAdminConnStr); | |
| if (string.IsNullOrEmpty(tcpConnStr)) | |
| { | |
| return; // Skip test | |
| } | |
| KerberosTicketManagemnt.Init(DataTestUtility.KerberosDomainUser, DataTestUtility.KerberosDomainPassword); | |
| SqlConnectionStringBuilder builder = new(tcpConnStr); | |
| if (string.IsNullOrEmpty(builder.DataSource)) | |
| { | |
| return; // Skip test | |
| } | |
| // Preserve the original TCP connection string settings and only switch to the admin: Data Source. | |
| // Do not append a fallback port because that disables SSRP for named instances and can produce | |
| // an invalid Data Source when no instance name is present. | |
| builder.DataSource = $"admin:{builder.DataSource}"; | |
| builder.IntegratedSecurity = true; | |
| try | |
| { | |
| using SqlConnection conn = new(builder.ConnectionString); |
| catch (SqlException ex) when (ex.Message.Contains("DAC") || ex.Message.Contains("Dedicated")) | ||
| { | ||
| // DAC may not be enabled or accessible; skip this test without failing | ||
| return; | ||
| } |
There was a problem hiding this comment.
Catching SqlException based on message substrings is fragile (localization and message changes) and the early return will mark the test as passed rather than skipped. Consider using a more stable signal (e.g., specific SqlException.Number/state if available) and using the test framework's skip mechanism so the result is reported as skipped when DAC isn't enabled/accessible.
| // Arrange: parse "server\instance" which sets Protocol.None and IsSsrpRequired | ||
| DataSource dataSource = DataSource.ParseServerName(@"server\instance"); | ||
| Assert.NotNull(dataSource); | ||
| Assert.Equal(DataSource.Protocol.None, dataSource.ResolvedProtocol); | ||
| Assert.Equal("instance", dataSource.InstanceName); |
There was a problem hiding this comment.
These tests use hostnames like "server" which will trigger a DNS lookup inside SniProxy.GetSqlServerSPNs (Dns.GetHostEntry). That can introduce unnecessary network dependency/flakiness in unit tests. Consider using "localhost"/"127.0.0.1" (or another reliably-resolvable name) for ParseServerName inputs so the tests remain deterministic across environments.
| // use a port postfix (resolved via SSRP for named instances). | ||
| // https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/register-a-service-principal-name-for-kerberos-connections?view=sql-server-ver17#named-instance | ||
| postfix = dataSource.ResolvedProtocol == DataSource.Protocol.NP ? dataSource.InstanceName : dataSource.ResolvedPort.ToString(); |
There was a problem hiding this comment.
When InstanceName is present and protocol is not NP, this always uses ResolvedPort even if it is still the default (-1). That can produce an invalid SPN postfix like ":-1" in error paths (e.g., SSRP lookup fails before ResolvedPort is set) and in any call sites that haven't populated ResolvedPort. Consider falling back to InstanceName (or leaving postfix null) when ResolvedPort <= 0 / == -1, to avoid generating malformed SPNs and misleading diagnostics.
| // use a port postfix (resolved via SSRP for named instances). | |
| // https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/register-a-service-principal-name-for-kerberos-connections?view=sql-server-ver17#named-instance | |
| postfix = dataSource.ResolvedProtocol == DataSource.Protocol.NP ? dataSource.InstanceName : dataSource.ResolvedPort.ToString(); | |
| // use a port postfix (resolved via SSRP for named instances). If the resolved | |
| // port is not yet available, fall back to the instance name rather than | |
| // generating an invalid SPN postfix such as ":-1". | |
| // https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/register-a-service-principal-name-for-kerberos-connections?view=sql-server-ver17#named-instance | |
| postfix = dataSource.ResolvedProtocol == DataSource.Protocol.NP || dataSource.ResolvedPort <= 0 | |
| ? dataSource.InstanceName | |
| : dataSource.ResolvedPort.ToString(); |
Fix
Fixes #3566
Problem
When connecting to a named instance without a tcp: prefix (for example, Data Source=server\instance), DataSource.ResolvedProtocol is Protocol.None. SPN generation only used resolved port for Protocol.TCP, so Protocol.None and Protocol.Admin could incorrectly use instance name in the SPN:
MSSQLSvc/server.fqdn:instancename
instead of using the SSRP-resolved port:
MSSQLSvc/server.fqdn:12345
Why this addresses the review concern
@cheenamalhotra correctly noted that port and instance name can both map to the same SQL Server instance. This change does not claim they are always semantically different targets.
The driver behavior here is protocol-specific for SPN construction:
This gives deterministic SPN formatting for integrated auth on Unix/Linux SSRP flows while preserving NP behavior and explicit ServerSPN overrides.
Code changes
Testing performed
Kerberos environment validation status
I will run our existing suite of Kerberos tests and provide results here.