Skip to content

Commit 04db1a4

Browse files
Feature | Add support server SPN option (#1607)
1 parent 748fd36 commit 04db1a4

20 files changed

+291
-56
lines changed

doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml

+2
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,7 @@ End Module
540540
|Encrypt|'true' in 4.0 and above<br/><br/>'false' in 3.x and below|Recognized values are:<br/>versions 1 - 4: `true`/`yes` and `false`/`no`<br/>versions 5+: `true`/`yes`/`mandatory`, `false`/`no`/`optional` and `strict`. When `true`, TLS encryption is used for all data sent between the client and server if the server has a certificate installed. When `strict`, TDS 8.0 TLS encryption is used and the `TrustServerCertificate` setting is ignored and treated as false. For more information, see [Connection String Syntax](/sql/connect/ado-net/connection-string-syntax).<br /><br /> When `Encrypt` is true or strict and `TrustServerCertificate` is false, the server name (or IP address) in a server's certificate must exactly match the server name (or IP address) specified in the connection string. Otherwise, the connection attempt will fail. For information about support for certificates whose subject starts with a wildcard character (*), see [Accepted wildcards used by server certificates for server authentication](https://support.microsoft.com/kb/258858).|
541541
|Enlist|'true'|`true` indicates that the SQL Server connection pooler automatically enlists the connection in the creation thread's current transaction context.|
542542
|Failover Partner|N/A|The name of the failover partner server where database mirroring is configured.<br /><br /> If the value of this key is "", then **Initial Catalog** must be present, and its value must not be "".<br /><br /> The server name can be 128 characters or less.<br /><br /> If you specify a failover partner but the failover partner server is not configured for database mirroring and the primary server (specified with the Server keyword) is not available, then the connection will fail.<br /><br /> If you specify a failover partner and the primary server is not configured for database mirroring, the connection to the primary server (specified with the Server keyword) will succeed if the primary server is available.|
543+
|Failover Partner SPN<br /><br /> -or-<br /><br /> FailoverPartnerSPN|N/A|The SPN for the failover partner. The default value is an empty string, which causes SqlClient to use the default, driver-generated SPN.<br /><br /> (Only available in v5.0+)|
543544
|Host Name In Certificate<br /><br /> -or-<br /><br />HostNameInCertificate|N/A|Available starting in version 5.0.<br/><br/>The host name to use when validating the server certificate. When not specified, the server name from the Data Source is used for certificate validation.|
544545
|Initial Catalog<br /><br /> -or-<br /><br /> Database|N/A|The name of the database.<br /><br /> The database name can be 128 characters or less.|
545546
|Integrated Security<br /><br /> -or-<br /><br /> Trusted_Connection|'false'|When `false`, User ID and Password are specified in the connection. When `true`, the current Windows account credentials are used for authentication.<br /><br /> Recognized values are `true`, `false`, `yes`, `no`, and `sspi` (strongly recommended), which is equivalent to `true`.<br /><br /> If User ID and Password are specified and Integrated Security is set to true, the User ID and Password will be ignored and Integrated Security will be used.<br /><br /> <xref:Microsoft.Data.SqlClient.SqlCredential> is a more secure way to specify credentials for a connection that uses SQL Server Authentication (`Integrated Security=false`).|
@@ -556,6 +557,7 @@ End Module
556557
|Pool Blocking Period<br /><br /> -or-<br /><br />PoolBlockingPeriod|Auto|Sets the blocking period behavior for a connection pool. See <xref:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.PoolBlockingPeriod> property for details.|
557558
|Pooling|'true'|When the value of this key is set to true, any newly created connection will be added to the pool when closed by the application. In a next attempt to open the same connection, that connection will be drawn from the pool.<br /><br /> Connections are considered the same if they have the same connection string. Different connections have different connection strings.<br /><br /> The value of this key can be "true", "false", "yes", or "no".|
558559
|Replication|'false'|`true` if replication is supported using the connection.|
560+
|Server SPN<br /><br /> -or-<br /><br /> ServerSPN|N/A|The SPN for the data source. The default value is an empty string, which causes SqlClient to use the default, driver-generated SPN.<br /><br /> (Only available in v5.0+)|
559561
|Transaction Binding|Implicit Unbind|Controls connection association with an enlisted `System.Transactions` transaction.<br /><br /> Possible values are:<br /><br /> `Transaction Binding=Implicit Unbind;`<br /><br /> `Transaction Binding=Explicit Unbind;`<br /><br /> Implicit Unbind causes the connection to detach from the transaction when it ends. After detaching, additional requests on the connection are performed in autocommit mode. The `System.Transactions.Transaction.Current` property is not checked when executing requests while the transaction is active. After the transaction has ended, additional requests are performed in autocommit mode.<br /><br /> If the system ends the transaction (in the scope of a using block) before the last command completes, it will throw <xref:System.InvalidOperationException>.<br /><br /> Explicit Unbind causes the connection to remain attached to the transaction until the connection is closed or an explicit `SqlConnection.TransactionEnlist(null)` is called. Beginning in .NET Framework 4.0, changes to Implicit Unbind make Explicit Unbind obsolete. An `InvalidOperationException` is thrown if `Transaction.Current` is not the enlisted transaction or if the enlisted transaction is not active.|
560562
|Transparent Network IP Resolution<br /><br /> -or-<br /><br />TransparentNetworkIPResolution|See description.|When the value of this key is set to `true`, the application is required to retrieve all IP addresses for a particular DNS entry and attempt to connect with the first one in the list. If the connection is not established within 0.5 seconds, the application will try to connect to all others in parallel. When the first answers, the application will establish the connection with the respondent IP address.<br /><br /> If the `MultiSubnetFailover` key is set to `true`, `TransparentNetworkIPResolution` is ignored.<br /><br /> If the `Failover Partner` key is set, `TransparentNetworkIPResolution` is ignored.<br /><br /> The value of this key must be `true`, `false`, `yes`, or `no`.<br /><br /> A value of `yes` is treated the same as a value of `true`.<br /><br /> A value of `no` is treated the same as a value of `false`.<br /><br /> The default values are as follows:<br /><br /> <ul><li>`false` when:<br /><br /> <ul><li>Connecting to Azure SQL Database where the data source ends with:<br /><br /> <ul><li>.database.chinacloudapi.cn</li><li>.database.usgovcloudapi.net</li><li>.database.cloudapi.de</li><li>.database.windows.net</li></ul></li><li>`Authentication` is 'Active Directory Password' or 'Active Directory Integrated'</li></ul></li><li>`true` in all other cases.</li></ul>|
561563
|Trust Server Certificate<br /><br /> -or-<br /><br />TrustServerCertificate|'false'|When set to `true`, TLS is used to encrypt the channel when bypassing walking the certificate chain to validate trust. If TrustServerCertificate is set to `true` and Encrypt is set to `false`, the channel is not encrypted. Recognized values are `true`, `false`, `yes`, and `no`. For more information, see [Connection String Syntax](/sql/connect/ado-net/connection-string-syntax).|

doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml

+38
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,25 @@ If you specify a failover partner and the primary server is not configured for d
459459
]]></format>
460460
</remarks>
461461
</FailoverPartner>
462+
<FailoverPartnerSPN>
463+
<summary>Gets or sets the service principal name (SPN) of the failover partner for the connection.</summary>
464+
<value>
465+
The value of the <see cref="P:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.FailoverPartnerSPN" /> property, or <see langword="String.Empty" /> if none has been supplied.
466+
</value>
467+
<remarks>
468+
<format type="text/markdown">
469+
<![CDATA[
470+
471+
## Remarks
472+
This property corresponds to the "FailoverPartnerSPN" and "Failover Partner SPN" keys within the connection string.
473+
474+
> [!NOTE]
475+
> This property only applies when using Integrated Security mode, otherwise it is ignored.
476+
477+
]]>
478+
</format>
479+
</remarks>
480+
</FailoverPartnerSPN>
462481
<GetProperties>
463482
<param name="propertyDescriptors">To be added.</param>
464483
<summary>To be added.</summary>
@@ -820,6 +839,25 @@ Database = AdventureWorks
820839
]]></format>
821840
</remarks>
822841
</Replication>
842+
<ServerSPN>
843+
<summary>Gets or sets the service principal name (SPN) of the data source.</summary>
844+
<value>
845+
The value of the <see cref="P:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.ServerSPN" /> property, or <see langword="String.Empty" /> if none has been supplied.
846+
</value>
847+
<remarks>
848+
<format type="text/markdown">
849+
<![CDATA[
850+
851+
## Remarks
852+
This property corresponds to the "ServerSPN" and "Server SPN" keys within the connection string.
853+
854+
> [!NOTE]
855+
> This property only applies when using Integrated Security mode, otherwise it is ignored.
856+
857+
]]>
858+
</format>
859+
</remarks>
860+
</ServerSPN>
823861
<ShouldSerialize>
824862
<param name="keyword">The key to locate in the <see cref="T:Microsoft.Data.SqlClient.SqlConnectionStringBuilder" />.</param>
825863
<summary>Indicates whether the specified key exists in this <see cref="T:Microsoft.Data.SqlClient.SqlConnectionStringBuilder" /> instance.</summary>

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

+8
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,10 @@ public SqlConnectionStringBuilder(string connectionString) { }
10371037
[System.ComponentModel.DisplayNameAttribute("Failover Partner")]
10381038
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
10391039
public string FailoverPartner { get { throw null; } set { } }
1040+
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/FailoverPartnerSPN/*'/>
1041+
[System.ComponentModel.DisplayNameAttribute("Failover Partner SPN")]
1042+
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
1043+
public string FailoverPartnerSPN { get { throw null; } set { } }
10401044
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/InitialCatalog/*'/>
10411045
[System.ComponentModel.DisplayNameAttribute("Initial Catalog")]
10421046
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
@@ -1095,6 +1099,10 @@ public SqlConnectionStringBuilder(string connectionString) { }
10951099
[System.ComponentModel.DisplayNameAttribute("Replication")]
10961100
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
10971101
public bool Replication { get { throw null; } set { } }
1102+
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/ServerSPN/*'/>
1103+
[System.ComponentModel.DisplayNameAttribute("Server SPN")]
1104+
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
1105+
public string ServerSPN { get { throw null; } set { } }
10981106
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/TransactionBinding/*'/>
10991107
[System.ComponentModel.DisplayNameAttribute("Transaction Binding")]
11001108
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]

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

+11-6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using System.Net.Security;
1010
using System.Net.Sockets;
1111
using System.Text;
12-
using System.Text.RegularExpressions;
1312

1413
namespace Microsoft.Data.SqlClient.SNI
1514
{
@@ -69,7 +68,7 @@ internal static void GenSspiClientContext(SspiClientContextStatus sspiClientCont
6968
string[] serverSPNs = new string[serverName.Length];
7069
for (int i = 0; i < serverName.Length; i++)
7170
{
72-
serverSPNs[i] = Encoding.UTF8.GetString(serverName[i]);
71+
serverSPNs[i] = Encoding.Unicode.GetString(serverName[i]);
7372
}
7473
SecurityStatusPal statusCode = NegotiateStreamPal.InitializeSecurityContext(
7574
credentialsHandle,
@@ -135,6 +134,7 @@ private static bool IsErrorStatus(SecurityStatusPalErrorCode errorCode)
135134
/// <param name="timerExpire">Timer expiration</param>
136135
/// <param name="instanceName">Instance name</param>
137136
/// <param name="spnBuffer">SPN</param>
137+
/// <param name="serverSPN">pre-defined SPN</param>
138138
/// <param name="flushCache">Flush packet cache</param>
139139
/// <param name="async">Asynchronous connection</param>
140140
/// <param name="parallel">Attempt parallel connects</param>
@@ -151,6 +151,7 @@ internal static SNIHandle CreateConnectionHandle(
151151
long timerExpire,
152152
out byte[] instanceName,
153153
ref byte[][] spnBuffer,
154+
string serverSPN,
154155
bool flushCache,
155156
bool async,
156157
bool parallel,
@@ -200,7 +201,7 @@ internal static SNIHandle CreateConnectionHandle(
200201
{
201202
try
202203
{
203-
spnBuffer = GetSqlServerSPNs(details);
204+
spnBuffer = GetSqlServerSPNs(details, serverSPN);
204205
}
205206
catch (Exception e)
206207
{
@@ -212,9 +213,13 @@ internal static SNIHandle CreateConnectionHandle(
212213
return sniHandle;
213214
}
214215

215-
private static byte[][] GetSqlServerSPNs(DataSource dataSource)
216+
private static byte[][] GetSqlServerSPNs(DataSource dataSource, string serverSPN)
216217
{
217218
Debug.Assert(!string.IsNullOrWhiteSpace(dataSource.ServerName));
219+
if (!string.IsNullOrWhiteSpace(serverSPN))
220+
{
221+
return new byte[1][] { Encoding.Unicode.GetBytes(serverSPN) };
222+
}
218223

219224
string hostName = dataSource.ServerName;
220225
string postfix = null;
@@ -262,12 +267,12 @@ private static byte[][] GetSqlServerSPNs(string hostNameOrAddress, string portOr
262267
string serverSpnWithDefaultPort = serverSpn + $":{DefaultSqlServerPort}";
263268
// Set both SPNs with and without Port as Port is optional for default instance
264269
SqlClientEventSource.Log.TryAdvancedTraceEvent("SNIProxy.GetSqlServerSPN | Info | ServerSPNs {0} and {1}", serverSpn, serverSpnWithDefaultPort);
265-
return new byte[][] { Encoding.UTF8.GetBytes(serverSpn), Encoding.UTF8.GetBytes(serverSpnWithDefaultPort) };
270+
return new byte[][] { Encoding.Unicode.GetBytes(serverSpn), Encoding.Unicode.GetBytes(serverSpnWithDefaultPort) };
266271
}
267272
// else Named Pipes do not need to valid port
268273

269274
SqlClientEventSource.Log.TryAdvancedTraceEvent("SNIProxy.GetSqlServerSPN | Info | ServerSPN {0}", serverSpn);
270-
return new byte[][] { Encoding.UTF8.GetBytes(serverSpn) };
275+
return new byte[][] { Encoding.Unicode.GetBytes(serverSpn) };
271276
}
272277

273278
/// <summary>

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

+15-11
Original file line numberDiff line numberDiff line change
@@ -1552,7 +1552,7 @@ private void LoginNoFailover(ServerInfo serverInfo,
15521552
throw SQL.ROR_TimeoutAfterRoutingInfo(this);
15531553
}
15541554

1555-
serverInfo = new ServerInfo(ConnectionOptions, RoutingInfo, serverInfo.ResolvedServerName);
1555+
serverInfo = new ServerInfo(ConnectionOptions, RoutingInfo, serverInfo.ResolvedServerName, serverInfo.ServerSPN);
15561556
_timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.RoutingDestination);
15571557
_originalClientConnectionId = _clientConnectionId;
15581558
_routingDestination = serverInfo.UserServerName;
@@ -1696,7 +1696,7 @@ TimeoutTimer timeout
16961696
int sleepInterval = 100; //milliseconds to sleep (back off) between attempts.
16971697
long timeoutUnitInterval;
16981698

1699-
ServerInfo failoverServerInfo = new ServerInfo(connectionOptions, failoverHost);
1699+
ServerInfo failoverServerInfo = new ServerInfo(connectionOptions, failoverHost, connectionOptions.FailoverPartnerSPN);
17001700

17011701
ResolveExtendedServerName(primaryServerInfo, !redirectedUserInstance, connectionOptions);
17021702
if (null == ServerProvidedFailOverPartner)
@@ -1910,12 +1910,8 @@ private void AttemptOneLogin(
19101910
this,
19111911
ignoreSniOpenTimeout,
19121912
timeout.LegacyTimerExpire,
1913-
ConnectionOptions.Encrypt,
1914-
ConnectionOptions.TrustServerCertificate,
1915-
ConnectionOptions.IntegratedSecurity,
1916-
withFailover,
1917-
ConnectionOptions.Authentication,
1918-
ConnectionOptions.HostNameInCertificate);
1913+
ConnectionOptions,
1914+
withFailover);
19191915

19201916
_timeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.ConsumePreLoginHandshake);
19211917
_timeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.LoginBegin);
@@ -2797,6 +2793,7 @@ internal sealed class ServerInfo
27972793
internal string ResolvedServerName { get; private set; } // the resolved servername only
27982794
internal string ResolvedDatabaseName { get; private set; } // name of target database after resolution
27992795
internal string UserProtocol { get; private set; } // the user specified protocol
2796+
internal string ServerSPN { get; private set; } // the server SPN
28002797

28012798
// The original user-supplied server name from the connection string.
28022799
// If connection string has no Data Source, the value is set to string.Empty.
@@ -2817,10 +2814,16 @@ private set
28172814
internal readonly string PreRoutingServerName;
28182815

28192816
// Initialize server info from connection options,
2820-
internal ServerInfo(SqlConnectionString userOptions) : this(userOptions, userOptions.DataSource) { }
2817+
internal ServerInfo(SqlConnectionString userOptions) : this(userOptions, userOptions.DataSource, userOptions.ServerSPN) { }
2818+
2819+
// Initialize server info from connection options, but override DataSource and ServerSPN with given server name and server SPN
2820+
internal ServerInfo(SqlConnectionString userOptions, string serverName, string serverSPN) : this(userOptions, serverName)
2821+
{
2822+
ServerSPN = serverSPN;
2823+
}
28212824

28222825
// Initialize server info from connection options, but override DataSource with given server name
2823-
internal ServerInfo(SqlConnectionString userOptions, string serverName)
2826+
private ServerInfo(SqlConnectionString userOptions, string serverName)
28242827
{
28252828
//-----------------
28262829
// Preconditions
@@ -2839,7 +2842,7 @@ internal ServerInfo(SqlConnectionString userOptions, string serverName)
28392842

28402843

28412844
// Initialize server info from connection options, but override DataSource with given server name
2842-
internal ServerInfo(SqlConnectionString userOptions, RoutingInfo routing, string preRoutingServerName)
2845+
internal ServerInfo(SqlConnectionString userOptions, RoutingInfo routing, string preRoutingServerName, string serverSPN)
28432846
{
28442847
//-----------------
28452848
// Preconditions
@@ -2860,6 +2863,7 @@ internal ServerInfo(SqlConnectionString userOptions, RoutingInfo routing, string
28602863
UserProtocol = TdsEnums.TCP;
28612864
SetDerivedNames(UserProtocol, UserServerName);
28622865
ResolvedDatabaseName = userOptions.InitialCatalog;
2866+
ServerSPN = serverSPN;
28632867
}
28642868

28652869
internal void SetDerivedNames(string protocol, string serverName)

0 commit comments

Comments
 (0)