Skip to content

Commit f027453

Browse files
committed
Fix SPN using instance name instead of port when SSRP resolves port without tcp: prefix
When connecting to a named instance without specifying a protocol prefix (e.g. 'server\instance'), the DataSource.ResolvedProtocol is Protocol.None. The SPN generation in GetSqlServerSPNs only checked for Protocol.TCP when deciding to use the SSRP-resolved port, causing Protocol.None and Protocol.Admin connections to incorrectly use the instance name in the SPN (MSSQLSvc/host:instance) instead of the resolved port (MSSQLSvc/host:port). The fix inverts the condition to check for Protocol.NP (Named Pipes), which is the only protocol that should use instance name in the SPN. All other protocols (TCP, None, Admin) now correctly use the resolved port. Fixes #3566
1 parent 6db52a6 commit f027453

2 files changed

Lines changed: 127 additions & 5 deletions

File tree

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniProxy.netcore.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ internal static SniHandle CreateConnectionHandle(
116116
return sniHandle;
117117
}
118118

119-
private static ResolvedServerSpn GetSqlServerSPNs(DataSource dataSource, string serverSPN)
119+
internal static ResolvedServerSpn GetSqlServerSPNs(DataSource dataSource, string serverSPN)
120120
{
121121
Debug.Assert(!string.IsNullOrWhiteSpace(dataSource.ServerName));
122122
if (!string.IsNullOrWhiteSpace(serverSPN))
@@ -132,14 +132,19 @@ private static ResolvedServerSpn GetSqlServerSPNs(DataSource dataSource, string
132132
}
133133
else if (!string.IsNullOrWhiteSpace(dataSource.InstanceName))
134134
{
135-
postfix = dataSource.ResolvedProtocol == DataSource.Protocol.TCP ? dataSource.ResolvedPort.ToString() : dataSource.InstanceName;
135+
// Named Pipes use the instance name in the SPN (MSSQLSvc/host:instance).
136+
// All other protocols (TCP, None, Admin) use the port resolved by SSRP
137+
// (MSSQLSvc/host:port). Protocol.None is the default when no prefix is
138+
// specified in the data source (e.g. "server\instance"), and it is treated
139+
// as TCP for connection purposes. See GitHub issue #3566.
140+
postfix = dataSource.ResolvedProtocol == DataSource.Protocol.NP ? dataSource.InstanceName : dataSource.ResolvedPort.ToString();
136141
}
137142

138143
SqlClientEventSource.Log.TryTraceEvent("SNIProxy.GetSqlServerSPN | Info | ServerName {0}, InstanceName {1}, Port {2}, postfix {3}", dataSource?.ServerName, dataSource?.InstanceName, dataSource?.Port, postfix);
139144
return GetSqlServerSPNs(hostName, postfix, dataSource.ResolvedProtocol);
140145
}
141146

142-
private static ResolvedServerSpn GetSqlServerSPNs(string hostNameOrAddress, string portOrInstanceName, DataSource.Protocol protocol)
147+
internal static ResolvedServerSpn GetSqlServerSPNs(string hostNameOrAddress, string portOrInstanceName, DataSource.Protocol protocol)
143148
{
144149
Debug.Assert(!string.IsNullOrWhiteSpace(hostNameOrAddress));
145150
IPHostEntry hostEntry = null;
@@ -607,8 +612,8 @@ private bool InferNamedPipesInformation()
607612
// If the data source starts with "np:servername"
608613
if (!_dataSourceAfterTrimmingProtocol.Contains(PipeBeginning))
609614
{
610-
// Assuming that user did not change default NamedPipe name, if the datasource is in the format servername\instance,
611-
// separate servername and instance and prepend instance with MSSQL$ and append default pipe path
615+
// Assuming that user did not change default NamedPipe name, if the datasource is in the format servername\instance,
616+
// separate servername and instance and prepend instance with MSSQL$ and append default pipe path
612617
// https://learn.microsoft.com/en-us/sql/tools/configuration-manager/named-pipes-properties?view=sql-server-ver16
613618
if (_dataSourceAfterTrimmingProtocol.Contains(PathSeparator) && ResolvedProtocol == Protocol.NP)
614619
{
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
#if NET
6+
7+
using Microsoft.Data.SqlClient.ManagedSni;
8+
using Xunit;
9+
10+
namespace Microsoft.Data.SqlClient.UnitTests.ManagedSni
11+
{
12+
public class SniProxyGetSqlServerSPNsTest
13+
{
14+
/// <summary>
15+
/// Verifies that when connecting to a named instance without a protocol prefix
16+
/// (Protocol.None), the SPN uses the resolved port number from SSRP rather than
17+
/// the instance name. This is a regression test for GitHub issue #3566.
18+
/// </summary>
19+
[Fact]
20+
public void GetSqlServerSPNs_ProtocolNone_WithResolvedPort_UsesPortNotInstanceName()
21+
{
22+
// Arrange: parse "server\instance" which sets Protocol.None and IsSsrpRequired
23+
DataSource dataSource = DataSource.ParseServerName(@"server\instance");
24+
Assert.NotNull(dataSource);
25+
Assert.Equal(DataSource.Protocol.None, dataSource.ResolvedProtocol);
26+
Assert.Equal("instance", dataSource.InstanceName);
27+
Assert.Equal(-1, dataSource.Port);
28+
29+
// Simulate SSRP resolution setting the port (as CreateTcpHandle would do)
30+
dataSource.ResolvedPort = 12345;
31+
32+
// Act
33+
ResolvedServerSpn spn = SniProxy.GetSqlServerSPNs(dataSource, serverSPN: string.Empty);
34+
35+
// Assert: SPN should contain the resolved port, NOT the instance name
36+
Assert.Contains(":12345", spn.Primary);
37+
Assert.DoesNotContain("instance", spn.Primary, System.StringComparison.OrdinalIgnoreCase);
38+
}
39+
40+
/// <summary>
41+
/// Verifies that when connecting with an explicit tcp: prefix (Protocol.TCP),
42+
/// the SPN uses the resolved port number. This was the original fix for #2187.
43+
/// </summary>
44+
[Fact]
45+
public void GetSqlServerSPNs_ProtocolTcp_WithResolvedPort_UsesPort()
46+
{
47+
// Arrange: parse "tcp:server\instance" which sets Protocol.TCP
48+
DataSource dataSource = DataSource.ParseServerName(@"tcp:server\instance");
49+
Assert.NotNull(dataSource);
50+
Assert.Equal(DataSource.Protocol.TCP, dataSource.ResolvedProtocol);
51+
52+
dataSource.ResolvedPort = 54321;
53+
54+
// Act
55+
ResolvedServerSpn spn = SniProxy.GetSqlServerSPNs(dataSource, serverSPN: string.Empty);
56+
57+
// Assert
58+
Assert.Contains(":54321", spn.Primary);
59+
Assert.DoesNotContain("instance", spn.Primary, System.StringComparison.OrdinalIgnoreCase);
60+
}
61+
62+
/// <summary>
63+
/// Verifies that when connecting with Named Pipes protocol, the SPN uses
64+
/// the instance name rather than a port number.
65+
/// </summary>
66+
[Fact]
67+
public void GetSqlServerSPNs_ProtocolNp_WithInstanceName_UsesInstanceName()
68+
{
69+
// Named Pipes data sources go through a different parsing path
70+
// (InferNamedPipesInformation) that doesn't populate InstanceName,
71+
// so we test the lower-level overload directly.
72+
ResolvedServerSpn spn = SniProxy.GetSqlServerSPNs("server", "myinstance", DataSource.Protocol.NP);
73+
74+
Assert.Contains(":myinstance", spn.Primary);
75+
Assert.Null(spn.Secondary);
76+
}
77+
78+
/// <summary>
79+
/// Verifies that when a custom ServerSPN is provided in the connection string,
80+
/// it is used as-is regardless of protocol or instance name.
81+
/// </summary>
82+
[Fact]
83+
public void GetSqlServerSPNs_CustomSpnProvided_UsesCustomSpn()
84+
{
85+
DataSource dataSource = DataSource.ParseServerName(@"server\instance");
86+
Assert.NotNull(dataSource);
87+
dataSource.ResolvedPort = 12345;
88+
89+
string customSpn = "MSSQLSvc/myserver.domain.com:1433";
90+
ResolvedServerSpn spn = SniProxy.GetSqlServerSPNs(dataSource, serverSPN: customSpn);
91+
92+
Assert.Equal(customSpn, spn.Primary);
93+
Assert.Null(spn.Secondary);
94+
}
95+
96+
/// <summary>
97+
/// Verifies that when connecting with admin: prefix (DAC), the SPN uses
98+
/// the resolved port number (DAC also resolves via SSRP).
99+
/// </summary>
100+
[Fact]
101+
public void GetSqlServerSPNs_ProtocolAdmin_WithResolvedPort_UsesPort()
102+
{
103+
DataSource dataSource = DataSource.ParseServerName(@"admin:server\instance");
104+
Assert.NotNull(dataSource);
105+
Assert.Equal(DataSource.Protocol.Admin, dataSource.ResolvedProtocol);
106+
107+
dataSource.ResolvedPort = 11111;
108+
109+
ResolvedServerSpn spn = SniProxy.GetSqlServerSPNs(dataSource, serverSPN: string.Empty);
110+
111+
Assert.Contains(":11111", spn.Primary);
112+
Assert.DoesNotContain("instance", spn.Primary, System.StringComparison.OrdinalIgnoreCase);
113+
}
114+
}
115+
}
116+
117+
#endif

0 commit comments

Comments
 (0)