Skip to content

Commit c2710f0

Browse files
committed
Address Copilot review feedback: SPN safety, DNS isolation, and test robustness
SniProxy.netcore.cs: - Guard against ResolvedPort <= 0 (e.g. -1 before SSRP resolves) to avoid producing malformed SPNs like ':-1' in error paths. Fall back to instance name when port is not yet available. SniProxyGetSqlServerSPNsTest.cs: - Replace generic 'server' hostnames with 'localhost' in all ParseServerName calls and the NP direct-call test. This avoids real DNS lookups that could cause flakiness in restricted-DNS environments. KerberosTest.cs: - Protocol.None test: build connection string from SqlConnectionStringBuilder(tcpConnStr) instead of from scratch to preserve Encrypt, TrustServerCertificate, etc. - TCP test: skip if no named instance; omit port fallback to 1433 (which disables SSRP and can produce invalid 'host\,port' strings). - Custom SPN test: require explicit port and use TCP-format SPN (host:port) rather than instance name, which is wrong for port-based SPN registrations. - Admin test: build from tcpConnStr base; require named instance; remove fragile catch-by-message-substring that could hide failures. DAC unavailability now correctly surfaces as a test failure instead of silent pass.
1 parent b87bc7b commit c2710f0

3 files changed

Lines changed: 83 additions & 54 deletions

File tree

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,12 @@ internal static ResolvedServerSpn GetSqlServerSPNs(DataSource dataSource, string
137137
// MSSQLSvc/<FQDN>:<instancename> for named instances. For our managed SNI path,
138138
// NP uses instance-name postfix and TCP-like protocols (TCP, None, Admin)
139139
// use a port postfix (resolved via SSRP for named instances).
140+
// If SSRP resolution hasn't populated ResolvedPort yet (value is -1), fall back
141+
// to the instance name to avoid producing a malformed SPN like ":-1".
140142
// 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
141-
postfix = dataSource.ResolvedProtocol == DataSource.Protocol.NP ? dataSource.InstanceName : dataSource.ResolvedPort.ToString();
143+
postfix = (dataSource.ResolvedProtocol == DataSource.Protocol.NP || dataSource.ResolvedPort <= 0)
144+
? dataSource.InstanceName
145+
: dataSource.ResolvedPort.ToString();
142146
}
143147

144148
SqlClientEventSource.Log.TryTraceEvent("SNIProxy.GetSqlServerSPN | Info | ServerName {0}, InstanceName {1}, Port {2}, ResolvedPort {3}, ResolvedProtocol {4}, postfix {5}", dataSource?.ServerName, dataSource?.InstanceName, dataSource?.Port, dataSource?.ResolvedPort, dataSource?.ResolvedProtocol, postfix);

src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/KerberosTests/KerberosTest.cs

Lines changed: 62 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,16 @@ public void KerberosTest_ProtocolNone_NamedInstanceWithSsrpResolution()
6868

6969
KerberosTicketManagemnt.Init(DataTestUtility.KerberosDomainUser, DataTestUtility.KerberosDomainPassword);
7070

71-
// Build a connection string with Protocol.None (no prefix) pointing to the named instance
72-
// SSRP resolution should occur and populate the port in the SPN
73-
string protocolNoneConnStr = $"Data Source={hostname}\\{instanceName};Integrated Security=true;";
71+
// Build from the base connection string to preserve environment settings (Encrypt,
72+
// TrustServerCertificate, timeouts, etc.), overriding only DataSource and IntegratedSecurity.
73+
// SSRP resolution should occur and populate the port in the SPN.
74+
SqlConnectionStringBuilder protocolNoneBuilder = new(tcpConnStr)
75+
{
76+
DataSource = $"{hostname}\\{instanceName}",
77+
IntegratedSecurity = true
78+
};
7479

75-
using SqlConnection conn = new(protocolNoneConnStr);
80+
using SqlConnection conn = new(protocolNoneBuilder.ConnectionString);
7681
conn.Open(); // Connection should succeed with Kerberos using the SSRP-resolved port in SPN
7782

7883
// Verify authentication occurred with KERBEROS auth_scheme
@@ -97,20 +102,30 @@ public void KerberosTest_ProtocolTcp_NamedInstanceWithExplicitPort()
97102
string tcpConnStr = DataTestUtility.TCPConnectionString;
98103
if (string.IsNullOrEmpty(tcpConnStr) ||
99104
!DataTestUtility.ParseDataSource(new SqlConnectionStringBuilder(tcpConnStr).DataSource,
100-
out string hostname, out int port, out string instanceName))
105+
out string hostname, out int port, out string instanceName) ||
106+
string.IsNullOrEmpty(instanceName))
101107
{
102-
return; // Skip test
108+
return; // Skip test; no named instance available
103109
}
104110

105111
KerberosTicketManagemnt.Init(DataTestUtility.KerberosDomainUser, DataTestUtility.KerberosDomainPassword);
106112

107-
// If an explicit port is available in the test connection string, use it
108-
// Otherwise, use a typical SQL instance port (1433) and rely on SSRP if needed
109-
int testPort = port > 0 ? port : 1433;
113+
// Build the tcp: data source. Include an explicit port only when the test connection string
114+
// already has one; otherwise leave SSRP to resolve the port for the named instance.
115+
// Do NOT fall back to port 1433: that disables SSRP and is unlikely to be correct for
116+
// named instances, and produces an invalid "host\,1433" when instanceName is empty.
117+
string newDataSource = port > 0
118+
? $"tcp:{hostname}\\{instanceName},{port}"
119+
: $"tcp:{hostname}\\{instanceName}";
110120

111-
string protocolTcpConnStr = $"Data Source=tcp:{hostname}\\{instanceName},{testPort};Integrated Security=true;";
121+
// Preserve the base connection string settings (Encrypt, TrustServerCertificate, etc.)
122+
SqlConnectionStringBuilder builder = new(tcpConnStr)
123+
{
124+
DataSource = newDataSource,
125+
IntegratedSecurity = true
126+
};
112127

113-
using SqlConnection conn = new(protocolTcpConnStr);
128+
using SqlConnection conn = new(builder.ConnectionString);
114129
conn.Open();
115130

116131
using SqlCommand command = new("SELECT auth_scheme from sys.dm_exec_connections where session_id = @@spid", conn);
@@ -142,19 +157,20 @@ public void KerberosTest_CustomServerSPN_BypassesAutoGeneration()
142157
return; // Skip test
143158
}
144159

160+
// For a reliable custom SPN test, we need to know the exact port so we can construct
161+
// the TCP-format SPN the server expects: MSSQLSvc/fqdn:port.
162+
// Using the instance name here is wrong for TCP environments that register only port-based SPNs.
163+
if (port <= 0)
164+
{
165+
return; // Skip test; cannot construct a valid port-based custom SPN without an explicit port
166+
}
167+
145168
KerberosTicketManagemnt.Init(DataTestUtility.KerberosDomainUser, DataTestUtility.KerberosDomainPassword);
146169

147-
// Build the expected SPN for the server
170+
// Build the TCP-format SPN that matches what the driver would auto-generate.
171+
// TCP Kerberos connections use MSSQLSvc/fqdn:port regardless of instance name.
148172
string fqdn = DataTestUtility.GetMachineFQDN(hostname);
149-
string customSpn = $"MSSQLSvc/{fqdn}";
150-
if (!string.IsNullOrEmpty(instanceName))
151-
{
152-
customSpn += ":" + instanceName;
153-
}
154-
else if (port > 0)
155-
{
156-
customSpn += ":" + port;
157-
}
173+
string customSpn = $"MSSQLSvc/{fqdn}:{port}";
158174

159175
SqlConnectionStringBuilder builder = new(tcpConnStr);
160176
builder.IntegratedSecurity = true;
@@ -185,35 +201,38 @@ public void KerberosTest_ProtocolAdmin_DedicatedAdminConnection()
185201
string tcpConnStr = DataTestUtility.TCPConnectionString;
186202
if (string.IsNullOrEmpty(tcpConnStr) ||
187203
!DataTestUtility.ParseDataSource(new SqlConnectionStringBuilder(tcpConnStr).DataSource,
188-
out string hostname, out int port, out string instanceName))
204+
out string hostname, out int port, out string instanceName) ||
205+
string.IsNullOrEmpty(instanceName))
189206
{
190-
return; // Skip test
207+
return; // Skip test; no named instance available
191208
}
192209

193210
KerberosTicketManagemnt.Init(DataTestUtility.KerberosDomainUser, DataTestUtility.KerberosDomainPassword);
194211

195-
int testPort = port > 0 ? port : 1433;
212+
// Build the admin: data source from the base connection string to preserve environment
213+
// settings. Do NOT fall back to port 1433 — the DAC port is separate from the regular
214+
// SQL Server port and must be discovered via SSRP if not explicitly known.
215+
string newDataSource = port > 0
216+
? $"admin:{hostname}\\{instanceName},{port}"
217+
: $"admin:{hostname}\\{instanceName}";
196218

197-
// Build admin: connection string
198-
string protocolAdminConnStr = $"Data Source=admin:{hostname}\\{instanceName},{testPort};Integrated Security=true;";
199-
200-
try
201-
{
202-
using SqlConnection conn = new(protocolAdminConnStr);
203-
conn.Open();
204-
205-
using SqlCommand command = new("SELECT auth_scheme from sys.dm_exec_connections where session_id = @@spid", conn);
206-
using SqlDataReader reader = command.ExecuteReader();
207-
if (reader.Read())
208-
{
209-
Assert.Equal("KERBEROS", reader.GetString(0));
210-
}
211-
}
212-
catch (SqlException ex) when (ex.Message.Contains("DAC") || ex.Message.Contains("Dedicated"))
219+
SqlConnectionStringBuilder adminBuilder = new(tcpConnStr)
213220
{
214-
// DAC may not be enabled or accessible; skip this test without failing
215-
return;
216-
}
221+
DataSource = newDataSource,
222+
IntegratedSecurity = true
223+
};
224+
225+
// Note: this test requires DAC to be enabled on the target instance
226+
// (sp_configure 'remote admin connections', 1). If DAC is not enabled,
227+
// the connection will fail with a SqlException and the test will report as failed,
228+
// which is the desired behavior — the test environment should be fixed.
229+
using SqlConnection conn = new(adminBuilder.ConnectionString);
230+
conn.Open();
231+
232+
using SqlCommand command = new("SELECT auth_scheme from sys.dm_exec_connections where session_id = @@spid", conn);
233+
using SqlDataReader reader = command.ExecuteReader();
234+
Assert.True(reader.Read(), "Expected to receive one row data");
235+
Assert.Equal("KERBEROS", reader.GetString(0));
217236
}
218237
}
219238

src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/ManagedSni/SniProxyGetSqlServerSPNsTest.cs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ public class SniProxyGetSqlServerSPNsTest
3535
[Fact]
3636
public void GetSqlServerSPNs_ProtocolNone_WithResolvedPort_UsesPortNotInstanceName()
3737
{
38-
// Arrange: parse "server\instance" which sets Protocol.None and IsSsrpRequired
39-
DataSource dataSource = DataSource.ParseServerName(@"server\instance");
38+
// Arrange: parse "localhost\instance" which sets Protocol.None and IsSsrpRequired.
39+
// Using "localhost" instead of an arbitrary hostname avoids real DNS lookups
40+
// that would make the test flaky in environments with restricted DNS resolution.
41+
DataSource dataSource = DataSource.ParseServerName(@"localhost\instance");
4042
Assert.NotNull(dataSource);
4143
Assert.Equal(DataSource.Protocol.None, dataSource.ResolvedProtocol);
4244
Assert.Equal("instance", dataSource.InstanceName);
@@ -62,8 +64,9 @@ public void GetSqlServerSPNs_ProtocolNone_WithResolvedPort_UsesPortNotInstanceNa
6264
[Fact]
6365
public void GetSqlServerSPNs_ProtocolTcp_WithResolvedPort_UsesPort()
6466
{
65-
// Arrange: parse "tcp:server\instance" which sets Protocol.TCP
66-
DataSource dataSource = DataSource.ParseServerName(@"tcp:server\instance");
67+
// Arrange: parse "tcp:localhost\instance" which sets Protocol.TCP.
68+
// Using "localhost" avoids real DNS lookups that would make the test flaky.
69+
DataSource dataSource = DataSource.ParseServerName(@"tcp:localhost\instance");
6770
Assert.NotNull(dataSource);
6871
Assert.Equal(DataSource.Protocol.TCP, dataSource.ResolvedProtocol);
6972

@@ -88,10 +91,11 @@ public void GetSqlServerSPNs_ProtocolTcp_WithResolvedPort_UsesPort()
8891
[Fact]
8992
public void GetSqlServerSPNs_ProtocolNp_WithInstanceName_UsesInstanceName()
9093
{
91-
// Arrange & Act: test the lower-level overload directly with NP protocol
94+
// Arrange & Act: test the lower-level overload directly with NP protocol.
9295
// Named Pipes data sources go through a different parsing path that doesn't
9396
// populate InstanceName in the same way, so we call the helper directly.
94-
ResolvedServerSpn spn = SniProxy.GetSqlServerSPNs("server", "myinstance", DataSource.Protocol.NP);
97+
// Using "localhost" avoids real DNS lookups that would make the test flaky.
98+
ResolvedServerSpn spn = SniProxy.GetSqlServerSPNs("localhost", "myinstance", DataSource.Protocol.NP);
9599

96100
// Assert: SPN should use the instance name, not a port
97101
Assert.Contains(":myinstance", spn.Primary);
@@ -108,8 +112,9 @@ public void GetSqlServerSPNs_ProtocolNp_WithInstanceName_UsesInstanceName()
108112
[Fact]
109113
public void GetSqlServerSPNs_CustomSpnProvided_UsesCustomSpn()
110114
{
111-
// Arrange: parse a named instance, but provide a custom SPN override
112-
DataSource dataSource = DataSource.ParseServerName(@"server\instance");
115+
// Arrange: parse a named instance, but provide a custom SPN override.
116+
// Using "localhost" avoids real DNS lookups that would make the test flaky.
117+
DataSource dataSource = DataSource.ParseServerName(@"localhost\instance");
113118
Assert.NotNull(dataSource);
114119
dataSource.ResolvedPort = 12345;
115120

@@ -133,8 +138,9 @@ public void GetSqlServerSPNs_CustomSpnProvided_UsesCustomSpn()
133138
[Fact]
134139
public void GetSqlServerSPNs_ProtocolAdmin_WithResolvedPort_UsesPort()
135140
{
136-
// Arrange: parse "admin:server\instance" which sets Protocol.Admin
137-
DataSource dataSource = DataSource.ParseServerName(@"admin:server\instance");
141+
// Arrange: parse "admin:localhost\instance" which sets Protocol.Admin.
142+
// Using "localhost" avoids real DNS lookups that would make the test flaky.
143+
DataSource dataSource = DataSource.ParseServerName(@"admin:localhost\instance");
138144
Assert.NotNull(dataSource);
139145
Assert.Equal(DataSource.Protocol.Admin, dataSource.ResolvedProtocol);
140146

0 commit comments

Comments
 (0)