Skip to content

Commit 561b535

Browse files
authored
Add IP address preference support for TCP connection (#1015)
1 parent fee499f commit 561b535

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+894
-107
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<docs>
2+
<members name="SqlConnectionIPAddressPreference">
3+
<SqlConnectionIPAddressPreferenceNetfx>
4+
<summary>
5+
Specifies a value for IP address preference during a TCP connection.
6+
</summary>
7+
<remarks>
8+
<format type="text/markdown"><![CDATA[
9+
10+
## Remarks
11+
If `Multi Subnet Failover` or "Transparent Network IP Resolution" is set to `true`, this setting has no effect.
12+
]]></format>
13+
</remarks>
14+
</SqlConnectionIPAddressPreferenceNetfx>
15+
<SqlConnectionIPAddressPreference>
16+
<summary>
17+
Specifies a value for IP address preference during a TCP connection.
18+
</summary>
19+
<remarks>
20+
<format type="text/markdown"><![CDATA[
21+
22+
## Remarks
23+
If `Multi Subnet Failover` is set to `true`, this setting has no effect.
24+
]]></format>
25+
</remarks>
26+
</SqlConnectionIPAddressPreference>
27+
<IPv4First>
28+
<summary>Connects using IPv4 address(es) first. If the connection fails, try IPv6 address(es), if provided. This is the default value.</summary>
29+
<value>0</value>
30+
</IPv4First>
31+
<IPv6First>
32+
<summary>Connect using IPv6 address(es) first. If the connection fails, try IPv4 address(es), if available.</summary>
33+
<value>1</value>
34+
</IPv6First>
35+
<UsePlatformDefault>
36+
<summary>Connects with IP addresses in the order the underlying platform or operating system provides them.</summary>
37+
<value>2</value>
38+
</UsePlatformDefault>
39+
</members>
40+
</docs>

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,17 @@ False
346346
</remarks>
347347
<exception cref="T:System.ArgumentNullException">To set the value to null, use <see cref="F:System.DBNull.Value" />.</exception>
348348
</DataSource>
349+
<IPAddressPreference>
350+
<summary>Gets or sets the value of IP address preference.</summary>
351+
<returns>Returns IP address preference.</returns>
352+
<remarks>
353+
<format type="text/markdown"><![CDATA[
354+
355+
## Remarks
356+
If `Multi Subnet Failover` is set to `true`, this setting has no effect.
357+
]]></format>
358+
</remarks>
359+
</IPAddressPreference>
349360
<EnclaveAttestationUrl>
350361
<summary>Gets or sets the enclave attestation Url to be used with enclave based Always Encrypted.</summary>
351362
<value>The enclave attestation Url.</value>

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,18 @@ public enum SqlConnectionAttestationProtocol
398398
HGS = 3
399399
}
400400
#endif
401+
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionIPAddressPreference.xml' path='docs/members[@name="SqlConnectionIPAddressPreference"]/SqlConnectionIPAddressPreference/*' />
402+
public enum SqlConnectionIPAddressPreference
403+
{
404+
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionIPAddressPreference.xml' path='docs/members[@name="SqlConnectionIPAddressPreference"]/IPv4First/*' />
405+
IPv4First = 0, // default
406+
407+
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionIPAddressPreference.xml' path='docs/members[@name="SqlConnectionIPAddressPreference"]/IPv6First/*' />
408+
IPv6First = 1,
409+
410+
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionIPAddressPreference.xml' path='docs/members[@name="SqlConnectionIPAddressPreference"]/UsePlatformDefault/*' />
411+
UsePlatformDefault = 2
412+
}
401413
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlColumnEncryptionCertificateStoreProvider.xml' path='docs/members[@name="SqlColumnEncryptionCertificateStoreProvider"]/SqlColumnEncryptionCertificateStoreProvider/*'/>
402414
public partial class SqlColumnEncryptionCertificateStoreProvider : Microsoft.Data.SqlClient.SqlColumnEncryptionKeyStoreProvider
403415
{
@@ -883,6 +895,10 @@ public SqlConnectionStringBuilder(string connectionString) { }
883895
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
884896
public string EnclaveAttestationUrl { get { throw null; } set { } }
885897
#endif
898+
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/IPAddressPreference/*'/>
899+
[System.ComponentModel.DisplayNameAttribute("IP Address Preference")]
900+
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
901+
public Microsoft.Data.SqlClient.SqlConnectionIPAddressPreference IPAddressPreference { get { throw null; } set { } }
886902
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/Encrypt/*'/>
887903
[System.ComponentModel.DisplayNameAttribute("Encrypt")]
888904
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]

src/Microsoft.Data.SqlClient/netcore/src/Interop/SNINativeMethodWrapper.Windows.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ private unsafe struct SNI_CLIENT_CONSUMER_INFO
165165
public TransparentNetworkResolutionMode transparentNetworkResolution;
166166
public int totalTimeout;
167167
public bool isAzureSqlServerEndpoint;
168+
public SqlConnectionIPAddressPreference ipAddressPreference;
168169
public SNI_DNSCache_Info DNSCacheInfo;
169170
}
170171

@@ -275,6 +276,7 @@ private static extern uint SNIOpenWrapper(
275276
[In] SNIHandle pConn,
276277
out IntPtr ppConn,
277278
[MarshalAs(UnmanagedType.Bool)] bool fSync,
279+
SqlConnectionIPAddressPreference ipPreference,
278280
[In] ref SNI_DNSCache_Info pDNSCachedInfo);
279281

280282
[DllImport(SNI, CallingConvention = CallingConvention.Cdecl)]
@@ -341,7 +343,7 @@ internal static uint SNIInitialize()
341343
return SNIInitialize(IntPtr.Zero);
342344
}
343345

344-
internal static unsafe uint SNIOpenMarsSession(ConsumerInfo consumerInfo, SNIHandle parent, ref IntPtr pConn, bool fSync, SQLDNSInfo cachedDNSInfo)
346+
internal static unsafe uint SNIOpenMarsSession(ConsumerInfo consumerInfo, SNIHandle parent, ref IntPtr pConn, bool fSync, SqlConnectionIPAddressPreference ipPreference, SQLDNSInfo cachedDNSInfo)
345347
{
346348
// initialize consumer info for MARS
347349
Sni_Consumer_Info native_consumerInfo = new Sni_Consumer_Info();
@@ -353,10 +355,11 @@ internal static unsafe uint SNIOpenMarsSession(ConsumerInfo consumerInfo, SNIHan
353355
native_cachedDNSInfo.wszCachedTcpIPv6 = cachedDNSInfo?.AddrIPv6;
354356
native_cachedDNSInfo.wszCachedTcpPort = cachedDNSInfo?.Port;
355357

356-
return SNIOpenWrapper(ref native_consumerInfo, "session:", parent, out pConn, fSync, ref native_cachedDNSInfo);
358+
return SNIOpenWrapper(ref native_consumerInfo, "session:", parent, out pConn, fSync, ipPreference, ref native_cachedDNSInfo);
357359
}
358360

359-
internal static unsafe uint SNIOpenSyncEx(ConsumerInfo consumerInfo, string constring, ref IntPtr pConn, byte[] spnBuffer, byte[] instanceName, bool fOverrideCache, bool fSync, int timeout, bool fParallel, SQLDNSInfo cachedDNSInfo)
361+
internal static unsafe uint SNIOpenSyncEx(ConsumerInfo consumerInfo, string constring, ref IntPtr pConn, byte[] spnBuffer, byte[] instanceName, bool fOverrideCache,
362+
bool fSync, int timeout, bool fParallel, SqlConnectionIPAddressPreference ipPreference, SQLDNSInfo cachedDNSInfo)
360363
{
361364
fixed (byte* pin_instanceName = &instanceName[0])
362365
{
@@ -379,6 +382,7 @@ internal static unsafe uint SNIOpenSyncEx(ConsumerInfo consumerInfo, string cons
379382
clientConsumerInfo.totalTimeout = SniOpenTimeOut;
380383
clientConsumerInfo.isAzureSqlServerEndpoint = ADP.IsAzureSqlServerEndpoint(constring);
381384

385+
clientConsumerInfo.ipAddressPreference = ipPreference;
382386
clientConsumerInfo.DNSCacheInfo.wszCachedFQDN = cachedDNSInfo?.FQDN;
383387
clientConsumerInfo.DNSCacheInfo.wszCachedTcpIPv4 = cachedDNSInfo?.AddrIPv4;
384388
clientConsumerInfo.DNSCacheInfo.wszCachedTcpIPv6 = cachedDNSInfo?.AddrIPv6;

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs

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

55
using System;
6+
using System.Collections.Generic;
67
using System.Diagnostics;
78
using System.Globalization;
89
using System.Reflection;
@@ -400,6 +401,110 @@ internal static SqlConnectionAttestationProtocol ConvertToAttestationProtocol(st
400401

401402
#endregion
402403

404+
#region <<IPAddressPreference Utility>>
405+
/// <summary>
406+
/// IP Address Preference.
407+
/// </summary>
408+
private readonly static Dictionary<string, SqlConnectionIPAddressPreference> s_preferenceNames = new(StringComparer.InvariantCultureIgnoreCase);
409+
410+
static DbConnectionStringBuilderUtil()
411+
{
412+
foreach (SqlConnectionIPAddressPreference item in Enum.GetValues(typeof(SqlConnectionIPAddressPreference)))
413+
{
414+
s_preferenceNames.Add(item.ToString(), item);
415+
}
416+
}
417+
418+
/// <summary>
419+
/// Convert a string value to the corresponding IPAddressPreference.
420+
/// </summary>
421+
/// <param name="value">The string representation of the enumeration name to convert.</param>
422+
/// <param name="result">When this method returns, `result` contains an object of type `SqlConnectionIPAddressPreference` whose value is represented by `value` if the operation succeeds.
423+
/// If the parse operation fails, `result` contains the default value of the `SqlConnectionIPAddressPreference` type.</param>
424+
/// <returns>`true` if the value parameter was converted successfully; otherwise, `false`.</returns>
425+
internal static bool TryConvertToIPAddressPreference(string value, out SqlConnectionIPAddressPreference result)
426+
{
427+
if (!s_preferenceNames.TryGetValue(value, out result))
428+
{
429+
result = DbConnectionStringDefaults.IPAddressPreference;
430+
return false;
431+
}
432+
return true;
433+
}
434+
435+
/// <summary>
436+
/// Verifies if the `value` is defined in the expected Enum.
437+
/// </summary>
438+
internal static bool IsValidIPAddressPreference(SqlConnectionIPAddressPreference value)
439+
=> value == SqlConnectionIPAddressPreference.IPv4First
440+
|| value == SqlConnectionIPAddressPreference.IPv6First
441+
|| value == SqlConnectionIPAddressPreference.UsePlatformDefault;
442+
443+
internal static string IPAddressPreferenceToString(SqlConnectionIPAddressPreference value)
444+
=> Enum.GetName(typeof(SqlConnectionIPAddressPreference), value);
445+
446+
internal static SqlConnectionIPAddressPreference ConvertToIPAddressPreference(string keyword, object value)
447+
{
448+
if (value is null)
449+
{
450+
return DbConnectionStringDefaults.IPAddressPreference; // IPv4First
451+
}
452+
453+
if (value is string sValue)
454+
{
455+
// try again after remove leading & trailing whitespaces.
456+
sValue = sValue.Trim();
457+
if (TryConvertToIPAddressPreference(sValue, out SqlConnectionIPAddressPreference result))
458+
{
459+
return result;
460+
}
461+
462+
// string values must be valid
463+
throw ADP.InvalidConnectionOptionValue(keyword);
464+
}
465+
else
466+
{
467+
// the value is not string, try other options
468+
SqlConnectionIPAddressPreference eValue;
469+
470+
if (value is SqlConnectionIPAddressPreference preference)
471+
{
472+
eValue = preference;
473+
}
474+
else if (value.GetType().IsEnum)
475+
{
476+
// explicitly block scenarios in which user tries to use wrong enum types, like:
477+
// builder["SqlConnectionIPAddressPreference"] = EnvironmentVariableTarget.Process;
478+
// workaround: explicitly cast non-SqlConnectionIPAddressPreference enums to int
479+
throw ADP.ConvertFailed(value.GetType(), typeof(SqlConnectionIPAddressPreference), null);
480+
}
481+
else
482+
{
483+
try
484+
{
485+
// Enum.ToObject allows only integral and enum values (enums are blocked above), raising ArgumentException for the rest
486+
eValue = (SqlConnectionIPAddressPreference)Enum.ToObject(typeof(SqlConnectionIPAddressPreference), value);
487+
}
488+
catch (ArgumentException e)
489+
{
490+
// to be consistent with the messages we send in case of wrong type usage, replace
491+
// the error with our exception, and keep the original one as inner one for troubleshooting
492+
throw ADP.ConvertFailed(value.GetType(), typeof(SqlConnectionIPAddressPreference), e);
493+
}
494+
}
495+
496+
if (IsValidIPAddressPreference(eValue))
497+
{
498+
return eValue;
499+
}
500+
else
501+
{
502+
throw ADP.InvalidEnumerationValue(typeof(SqlConnectionIPAddressPreference), (int)eValue);
503+
}
504+
}
505+
}
506+
#endregion
507+
403508
internal static bool IsValidApplicationIntentValue(ApplicationIntent value)
404509
{
405510
Debug.Assert(Enum.GetNames(typeof(ApplicationIntent)).Length == 2, "ApplicationIntent enum has changed, update needed");
@@ -728,6 +833,7 @@ internal static partial class DbConnectionStringDefaults
728833
internal const SqlConnectionColumnEncryptionSetting ColumnEncryptionSetting = SqlConnectionColumnEncryptionSetting.Disabled;
729834
internal const string EnclaveAttestationUrl = _emptyString;
730835
internal const SqlConnectionAttestationProtocol AttestationProtocol = SqlConnectionAttestationProtocol.NotSpecified;
836+
internal const SqlConnectionIPAddressPreference IPAddressPreference = SqlConnectionIPAddressPreference.IPv4First;
731837
}
732838

733839

@@ -765,6 +871,7 @@ internal static partial class DbConnectionStringKeywords
765871
internal const string ColumnEncryptionSetting = "Column Encryption Setting";
766872
internal const string EnclaveAttestationUrl = "Enclave Attestation Url";
767873
internal const string AttestationProtocol = "Attestation Protocol";
874+
internal const string IPAddressPreference = "IP Address Preference";
768875

769876
// common keywords (OleDb, OracleClient, SqlClient)
770877
internal const string DataSource = "Data Source";
@@ -793,6 +900,9 @@ internal static class DbConnectionStringSynonyms
793900
//internal const string ApplicationName = APP;
794901
internal const string APP = "app";
795902

903+
// internal const string IPAddressPreference = IPADDRESSPREFERENCE;
904+
internal const string IPADDRESSPREFERENCE = "IPAddressPreference";
905+
796906
//internal const string ApplicationIntent = APPLICATIONINTENT;
797907
internal const string APPLICATIONINTENT = "ApplicationIntent";
798908

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -254,10 +254,12 @@ internal uint WritePacket(SNIHandle handle, SNIPacket packet, bool sync)
254254
/// <param name="async">Asynchronous connection</param>
255255
/// <param name="parallel">Attempt parallel connects</param>
256256
/// <param name="isIntegratedSecurity"></param>
257+
/// <param name="ipPreference">IP address preference</param>
257258
/// <param name="cachedFQDN">Used for DNS Cache</param>
258-
/// <param name="pendingDNSInfo">Used for DNS Cache</param>
259+
/// <param name="pendingDNSInfo">Used for DNS Cache</param>
259260
/// <returns>SNI handle</returns>
260-
internal SNIHandle CreateConnectionHandle(string fullServerName, bool ignoreSniOpenTimeout, long timerExpire, out byte[] instanceName, ref byte[][] spnBuffer, bool flushCache, bool async, bool parallel, bool isIntegratedSecurity, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo)
261+
internal SNIHandle CreateConnectionHandle(string fullServerName, bool ignoreSniOpenTimeout, long timerExpire, out byte[] instanceName, ref byte[][] spnBuffer,
262+
bool flushCache, bool async, bool parallel, bool isIntegratedSecurity, SqlConnectionIPAddressPreference ipPreference, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo)
261263
{
262264
instanceName = new byte[1];
263265

@@ -284,7 +286,7 @@ internal SNIHandle CreateConnectionHandle(string fullServerName, bool ignoreSniO
284286
case DataSource.Protocol.Admin:
285287
case DataSource.Protocol.None: // default to using tcp if no protocol is provided
286288
case DataSource.Protocol.TCP:
287-
sniHandle = CreateTcpHandle(details, timerExpire, parallel, cachedFQDN, ref pendingDNSInfo);
289+
sniHandle = CreateTcpHandle(details, timerExpire, parallel, ipPreference, cachedFQDN, ref pendingDNSInfo);
288290
break;
289291
case DataSource.Protocol.NP:
290292
sniHandle = CreateNpHandle(details, timerExpire, parallel);
@@ -374,10 +376,11 @@ private static byte[][] GetSqlServerSPNs(string hostNameOrAddress, string portOr
374376
/// <param name="details">Data source</param>
375377
/// <param name="timerExpire">Timer expiration</param>
376378
/// <param name="parallel">Should MultiSubnetFailover be used</param>
379+
/// <param name="ipPreference">IP address preference</param>
377380
/// <param name="cachedFQDN">Key for DNS Cache</param>
378-
/// <param name="pendingDNSInfo">Used for DNS Cache</param>
381+
/// <param name="pendingDNSInfo">Used for DNS Cache</param>
379382
/// <returns>SNITCPHandle</returns>
380-
private SNITCPHandle CreateTcpHandle(DataSource details, long timerExpire, bool parallel, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo)
383+
private SNITCPHandle CreateTcpHandle(DataSource details, long timerExpire, bool parallel, SqlConnectionIPAddressPreference ipPreference, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo)
381384
{
382385
// TCP Format:
383386
// tcp:<host name>\<instance name>
@@ -415,7 +418,7 @@ private SNITCPHandle CreateTcpHandle(DataSource details, long timerExpire, bool
415418
port = isAdminConnection ? DefaultSqlServerDacPort : DefaultSqlServerPort;
416419
}
417420

418-
return new SNITCPHandle(hostName, port, timerExpire, parallel, cachedFQDN, ref pendingDNSInfo);
421+
return new SNITCPHandle(hostName, port, timerExpire, parallel, ipPreference, cachedFQDN, ref pendingDNSInfo);
419422
}
420423

421424

0 commit comments

Comments
 (0)