Skip to content

Commit ce3a5c3

Browse files
committed
per discussion
1 parent 151ee2c commit ce3a5c3

File tree

10 files changed

+133
-30
lines changed

10 files changed

+133
-30
lines changed

src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,7 @@
730730
</Compile>
731731
<Compile Include="Microsoft\Data\Common\DbConnectionOptions.cs" />
732732
<Compile Include="Microsoft\Data\SqlClient\SNI\ConcurrentQueueSemaphore.cs" />
733+
<Compile Include="Microsoft\Data\SqlClient\SNI\ResolvedServerSpn.cs" />
733734
<Compile Include="Microsoft\Data\SqlClient\SNI\SNIError.cs" />
734735
<Compile Include="Microsoft\Data\SqlClient\SNI\SNICommon.cs" />
735736
<Compile Include="Microsoft\Data\SqlClient\SNI\SNIHandle.cs" />
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
#nullable enable
6+
7+
namespace Microsoft.Data.SqlClient.SNI
8+
{
9+
/// <summary>
10+
/// This is used to hold the ServerSpn for a given connection. Most connection types have a single format, although TCP connections may allow
11+
/// with and without a port. Depending on how the SPN is registered on the server, either one may be the correct name.
12+
/// </summary>
13+
/// <see href="https://learn.microsoft.com/sql/database-engine/configure-windows/register-a-service-principal-name-for-kerberos-connections?view=sql-server-ver17#spn-formats"/>
14+
/// <param name="primary"></param>
15+
/// <param name="secondary"></param>
16+
/// <remarks>
17+
/// <para>SQL Server SPN format follows these patterns:</para>
18+
/// <list type="bullet">
19+
/// <item>
20+
/// <term>Default instance, no port (primary):</term>
21+
/// <description>MSSQLSvc/fully-qualified-domain-name</description>
22+
/// </item>
23+
/// <item>
24+
/// <term>Default instance, default port (secondary):</term>
25+
/// <description>MSSQLSvc/fully-qualified-domain-name:1433</description>
26+
/// </item>
27+
/// <item>
28+
/// <term>Named instance or custom port:</term>
29+
/// <description>MSSQLSvc/fully-qualified-domain-name:port_or_instance_name</description>
30+
/// </item>
31+
/// </list>
32+
/// <para>For TCP connections to named instances, the port number is used in SPN.</para>
33+
/// <para>For Named Pipe connections to named instances, the instance name is used in SPN.</para>
34+
/// <para>When hostname resolution fails, the user-provided hostname is used instead of FQDN.</para>
35+
/// <para>For default instances with TCP protocol, both forms (with and without port) may be returned.</para>
36+
/// </remarks>
37+
internal readonly struct ResolvedServerSpn(string primary, string? secondary = null)
38+
{
39+
public string Primary => primary;
40+
41+
public string? Secondary => secondary;
42+
}
43+
}

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIProxy.cs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6-
using System.Buffers;
7-
using System.Collections.Generic;
86
using System.Diagnostics;
97
using System.IO;
108
using System.Net;
11-
using System.Net.Security;
129
using System.Net.Sockets;
1310
using System.Text;
1411
using Microsoft.Data.ProviderBase;
@@ -51,7 +48,7 @@ internal static SNIHandle CreateConnectionHandle(
5148
string fullServerName,
5249
TimeoutTimer timeout,
5350
out byte[] instanceName,
54-
out string resolvedSpn,
51+
out ResolvedServerSpn resolvedSpn,
5552
string serverSPN,
5653
bool flushCache,
5754
bool async,
@@ -116,12 +113,12 @@ internal static SNIHandle CreateConnectionHandle(
116113
return sniHandle;
117114
}
118115

119-
private static string GetSqlServerSPNs(DataSource dataSource, string serverSPN)
116+
private static ResolvedServerSpn GetSqlServerSPNs(DataSource dataSource, string serverSPN)
120117
{
121118
Debug.Assert(!string.IsNullOrWhiteSpace(dataSource.ServerName));
122119
if (!string.IsNullOrWhiteSpace(serverSPN))
123120
{
124-
return serverSPN;
121+
return new(serverSPN);
125122
}
126123

127124
string hostName = dataSource.ServerName;
@@ -139,7 +136,7 @@ private static string GetSqlServerSPNs(DataSource dataSource, string serverSPN)
139136
return GetSqlServerSPNs(hostName, postfix, dataSource.ResolvedProtocol);
140137
}
141138

142-
private static string GetSqlServerSPNs(string hostNameOrAddress, string portOrInstanceName, DataSource.Protocol protocol)
139+
private static ResolvedServerSpn GetSqlServerSPNs(string hostNameOrAddress, string portOrInstanceName, DataSource.Protocol protocol)
143140
{
144141
Debug.Assert(!string.IsNullOrWhiteSpace(hostNameOrAddress));
145142
IPHostEntry hostEntry = null;
@@ -170,12 +167,12 @@ private static string GetSqlServerSPNs(string hostNameOrAddress, string portOrIn
170167
string serverSpnWithDefaultPort = serverSpn + $":{DefaultSqlServerPort}";
171168
// Set both SPNs with and without Port as Port is optional for default instance
172169
SqlClientEventSource.Log.TryAdvancedTraceEvent("SNIProxy.GetSqlServerSPN | Info | ServerSPNs {0} and {1}", serverSpn, serverSpnWithDefaultPort);
173-
return serverSpnWithDefaultPort;
170+
return new(serverSpn, serverSpnWithDefaultPort);
174171
}
175172
// else Named Pipes do not need to valid port
176173

177174
SqlClientEventSource.Log.TryAdvancedTraceEvent("SNIProxy.GetSqlServerSPN | Info | ServerSPN {0}", serverSpn);
178-
return serverSpn;
175+
return new(serverSpn);
179176
}
180177

181178
/// <summary>

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ internal void Connect(ServerInfo serverInfo,
442442
serverInfo.ExtendedServerName,
443443
timeout,
444444
out instanceName,
445-
out var serverSpn,
445+
out var resolvedServerSpn,
446446
false,
447447
true,
448448
fParallel,
@@ -540,7 +540,7 @@ internal void Connect(ServerInfo serverInfo,
540540
serverInfo.ExtendedServerName,
541541
timeout,
542542
out instanceName,
543-
out serverSpn,
543+
out resolvedServerSpn,
544544
true,
545545
true,
546546
fParallel,
@@ -591,9 +591,9 @@ internal void Connect(ServerInfo serverInfo,
591591
}
592592
SqlClientEventSource.Log.TryTraceEvent("<sc.TdsParser.Connect|SEC> Prelogin handshake successful");
593593

594-
if (_authenticationProvider is { } && serverSpn is { })
594+
if (_authenticationProvider is { })
595595
{
596-
_authenticationProvider.Initialize(serverInfo, _physicalStateObj, this, serverSpn);
596+
_authenticationProvider.Initialize(serverInfo, _physicalStateObj, this, resolvedServerSpn.Primary, resolvedServerSpn.Secondary);
597597
}
598598

599599
if (_fMARS && marsCapable)

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using System.Threading.Tasks;
1212
using Microsoft.Data.Common;
1313
using Microsoft.Data.ProviderBase;
14+
using Microsoft.Data.SqlClient.SNI;
1415

1516
namespace Microsoft.Data.SqlClient
1617
{
@@ -71,7 +72,7 @@ internal abstract void CreatePhysicalSNIHandle(
7172
string serverName,
7273
TimeoutTimer timeout,
7374
out byte[] instanceName,
74-
out string resolvedSpn,
75+
out ResolvedServerSpn resolvedSpn,
7576
bool flushCache,
7677
bool async,
7778
bool fParallel,

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ internal override void CreatePhysicalSNIHandle(
8181
string serverName,
8282
TimeoutTimer timeout,
8383
out byte[] instanceName,
84-
out string resolvedSpn,
84+
out ResolvedServerSpn resolvedSpn,
8585
bool flushCache,
8686
bool async,
8787
bool parallel,

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Interop.Windows.Sni;
1414
using Microsoft.Data.Common;
1515
using Microsoft.Data.ProviderBase;
16+
using Microsoft.Data.SqlClient.SNI;
1617

1718
namespace Microsoft.Data.SqlClient
1819
{
@@ -144,7 +145,7 @@ internal override void CreatePhysicalSNIHandle(
144145
string serverName,
145146
TimeoutTimer timeout,
146147
out byte[] instanceName,
147-
out string resolvedSpn,
148+
out ResolvedServerSpn resolvedSpn,
148149
bool flushCache,
149150
bool async,
150151
bool fParallel,
@@ -178,7 +179,7 @@ internal override void CreatePhysicalSNIHandle(
178179

179180
_sessionHandle = new SNIHandle(myInfo, serverName, ref serverSPN, timeout.MillisecondsRemainingInt, out instanceName,
180181
flushCache, !async, fParallel, ipPreference, cachedDNSInfo, hostNameInCertificate);
181-
resolvedSpn = serverSPN.TrimEnd();
182+
resolvedSpn = new(serverSPN.TrimEnd());
182183
}
183184

184185
protected override uint SniPacketGetData(PacketHandle packet, byte[] _inBuff, ref uint dataSize)

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSspiContextProvider.cs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99

1010
namespace Microsoft.Data.SqlClient
1111
{
12-
internal sealed class NegotiateSspiContextProvider : SspiContextProvider
12+
internal sealed class NegotiateSspiContextProvider : SspiContextProvider, IDisposable
1313
{
1414
private NegotiateAuthentication? _negotiateAuth;
1515

1616
protected override bool GenerateSspiClientContext(ReadOnlySpan<byte> incomingBlob, IBufferWriter<byte> outgoingBlobWriter, SspiAuthenticationParameters authParams)
1717
{
1818
NegotiateAuthenticationStatusCode statusCode = NegotiateAuthenticationStatusCode.UnknownCredentials;
1919

20-
_negotiateAuth ??= new(new NegotiateAuthenticationClientOptions { Package = "Negotiate", TargetName = authParams.Resource });
20+
_negotiateAuth = GetNegotiateAuthenticationForParams(authParams);
2121

2222
var sendBuff = _negotiateAuth.GetOutgoingBlob(incomingBlob, out statusCode)!;
2323

@@ -33,6 +33,27 @@ protected override bool GenerateSspiClientContext(ReadOnlySpan<byte> incomingBlo
3333

3434
return false;
3535
}
36+
37+
public void Dispose()
38+
{
39+
_negotiateAuth?.Dispose();
40+
}
41+
42+
private NegotiateAuthentication GetNegotiateAuthenticationForParams(SspiAuthenticationParameters authParams)
43+
{
44+
if (_negotiateAuth is { })
45+
{
46+
if (string.Equals(_negotiateAuth.TargetName, authParams.Resource, StringComparison.Ordinal))
47+
{
48+
return _negotiateAuth;
49+
}
50+
51+
// Dispose of it since we're not going to use it now
52+
_negotiateAuth?.Dispose();
53+
}
54+
55+
return _negotiateAuth = new(new NegotiateAuthenticationClientOptions { Package = "Negotiate", TargetName = authParams.Resource });
56+
}
3657
}
3758
}
3859
#endif

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,22 @@ internal abstract class SspiContextProvider
1111
private TdsParser _parser = null!;
1212
private ServerInfo _serverInfo = null!;
1313

14-
private SspiAuthenticationParameters? _authParam;
14+
private SspiAuthenticationParameters? _primaryAuthParams;
15+
private SspiAuthenticationParameters? _secondaryAuthParams;
1516

1617
private protected TdsParserStateObject _physicalStateObj = null!;
1718

19+
#if NET
20+
/// <remarks>
21+
/// <see cref="SNI.ResolvedServerSpn"/> for details as to what <paramref name="primaryServerSpn"/> and <paramref name="secondaryServerSpn"/> means and why there are two.
22+
/// </remarks>
23+
#endif
1824
internal void Initialize(
1925
ServerInfo serverInfo,
2026
TdsParserStateObject physicalStateObj,
2127
TdsParser parser,
22-
string serverSpn
28+
string primaryServerSpn,
29+
string? secondaryServerSpn = null
2330
)
2431
{
2532
_parser = parser;
@@ -28,16 +35,23 @@ string serverSpn
2835

2936
var options = parser.Connection.ConnectionOptions;
3037

31-
SqlClientEventSource.Log.StateDumpEvent("<SspiContextProvider> Initializing provider {0} with SPN={1}", GetType().FullName, serverSpn);
38+
SqlClientEventSource.Log.StateDumpEvent("<SspiContextProvider> Initializing provider {0} with SPN={1} and alternate={2}", GetType().FullName, primaryServerSpn, secondaryServerSpn);
3239

33-
_authParam = new SspiAuthenticationParameters(options.DataSource, serverSpn)
40+
_primaryAuthParams = CreateAuthParams(options, primaryServerSpn);
41+
42+
if (secondaryServerSpn is { })
3443
{
35-
DatabaseName = options.InitialCatalog,
36-
UserId = options.UserID,
37-
Password = options.Password,
38-
};
44+
_secondaryAuthParams = CreateAuthParams(options, secondaryServerSpn);
45+
}
3946

4047
Initialize();
48+
49+
static SspiAuthenticationParameters CreateAuthParams(SqlConnectionString connString, string serverSpn) => new(connString.DataSource, serverSpn)
50+
{
51+
DatabaseName = connString.InitialCatalog,
52+
UserId = connString.UserID,
53+
Password = connString.Password,
54+
};
4155
}
4256

4357
private protected virtual void Initialize()
@@ -50,11 +64,30 @@ internal void WriteSSPIContext(ReadOnlySpan<byte> receivedBuff, IBufferWriter<by
5064
{
5165
using var _ = TrySNIEventScope.Create(nameof(SspiContextProvider));
5266

53-
if (!(_authParam is { } && RunGenerateSspiClientContext(receivedBuff, outgoingBlobWriter, _authParam)))
67+
if (_primaryAuthParams is { })
5468
{
55-
// If we've hit here, the SSPI context provider implementation failed to generate the SSPI context.
56-
SSPIError(SQLMessage.SSPIGenerateError(), TdsEnums.GEN_CLIENT_CONTEXT);
69+
if (RunGenerateSspiClientContext(receivedBuff, outgoingBlobWriter, _primaryAuthParams))
70+
{
71+
return;
72+
}
73+
74+
// remove _primaryAuth from future attempts as it failed
75+
_primaryAuthParams = null;
76+
}
77+
78+
if (_secondaryAuthParams is { })
79+
{
80+
if (RunGenerateSspiClientContext(receivedBuff, outgoingBlobWriter, _secondaryAuthParams))
81+
{
82+
return;
83+
}
84+
85+
// remove _secondaryAuthParams from future attempts as it failed
86+
_secondaryAuthParams = null;
5787
}
88+
89+
// If we've hit here, the SSPI context provider implementation failed to generate the SSPI context.
90+
SSPIError(SQLMessage.SSPIGenerateError(), TdsEnums.GEN_CLIENT_CONTEXT);
5891
}
5992

6093
private bool RunGenerateSspiClientContext(ReadOnlySpan<byte> incomingBlob, IBufferWriter<byte> outgoingBlobWriter, SspiAuthenticationParameters authParams)

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEventSource.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,12 @@ internal void StateDumpEvent<T0, T1>(string message, T0 args0, T1 args1)
893893
{
894894
StateDump(string.Format(message, args0?.ToString() ?? NullStr, args1?.ToString() ?? NullStr));
895895
}
896+
897+
[NonEvent]
898+
internal void StateDumpEvent<T0, T1, T2>(string message, T0 args0, T1 args1, T2 args2)
899+
{
900+
StateDump(string.Format(message, args0?.ToString() ?? NullStr, args1?.ToString() ?? NullStr, args2?.ToString()));
901+
}
896902
#endregion
897903

898904
#region SNI Trace

0 commit comments

Comments
 (0)