diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionIPAddressPreference.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionIPAddressPreference.xml new file mode 100644 index 0000000000..e713cb776b --- /dev/null +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionIPAddressPreference.xml @@ -0,0 +1,40 @@ + + + + + Specifies a value for IP address preference during a TCP connection. + + + + + + + + Specifies a value for IP address preference during a TCP connection. + + + + + + + Connects using IPv4 address(es) first. If the connection fails, try IPv6 address(es), if provided. This is the default value. + 0 + + + Connect using IPv6 address(es) first. If the connection fails, try IPv4 address(es), if available. + 1 + + + Connects with IP addresses in the order the underlying platform or operating system provides them. + 2 + + + diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml index 46e9ee6877..619ea2d690 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml @@ -346,6 +346,17 @@ False To set the value to null, use . + + Gets or sets the value of IP address preference. + Returns IP address preference. + + + + Gets or sets the enclave attestation Url to be used with enclave based Always Encrypted. The enclave attestation Url. diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs index e630a03acb..17bc6c59c4 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs @@ -398,6 +398,18 @@ public enum SqlConnectionAttestationProtocol HGS = 3 } #endif + /// + public enum SqlConnectionIPAddressPreference + { + /// + IPv4First = 0, // default + + /// + IPv6First = 1, + + /// + UsePlatformDefault = 2 + } /// public partial class SqlColumnEncryptionCertificateStoreProvider : Microsoft.Data.SqlClient.SqlColumnEncryptionKeyStoreProvider { @@ -883,6 +895,10 @@ public SqlConnectionStringBuilder(string connectionString) { } [System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)] public string EnclaveAttestationUrl { get { throw null; } set { } } #endif + /// + [System.ComponentModel.DisplayNameAttribute("IP Address Preference")] + [System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)] + public Microsoft.Data.SqlClient.SqlConnectionIPAddressPreference IPAddressPreference { get { throw null; } set { } } /// [System.ComponentModel.DisplayNameAttribute("Encrypt")] [System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)] diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Interop/SNINativeMethodWrapper.Windows.cs b/src/Microsoft.Data.SqlClient/netcore/src/Interop/SNINativeMethodWrapper.Windows.cs index 8201ec41aa..20159ca382 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Interop/SNINativeMethodWrapper.Windows.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Interop/SNINativeMethodWrapper.Windows.cs @@ -165,6 +165,7 @@ private unsafe struct SNI_CLIENT_CONSUMER_INFO public TransparentNetworkResolutionMode transparentNetworkResolution; public int totalTimeout; public bool isAzureSqlServerEndpoint; + public SqlConnectionIPAddressPreference ipAddressPreference; public SNI_DNSCache_Info DNSCacheInfo; } @@ -275,6 +276,7 @@ private static extern uint SNIOpenWrapper( [In] SNIHandle pConn, out IntPtr ppConn, [MarshalAs(UnmanagedType.Bool)] bool fSync, + SqlConnectionIPAddressPreference ipPreference, [In] ref SNI_DNSCache_Info pDNSCachedInfo); [DllImport(SNI, CallingConvention = CallingConvention.Cdecl)] @@ -341,7 +343,7 @@ internal static uint SNIInitialize() return SNIInitialize(IntPtr.Zero); } - internal static unsafe uint SNIOpenMarsSession(ConsumerInfo consumerInfo, SNIHandle parent, ref IntPtr pConn, bool fSync, SQLDNSInfo cachedDNSInfo) + internal static unsafe uint SNIOpenMarsSession(ConsumerInfo consumerInfo, SNIHandle parent, ref IntPtr pConn, bool fSync, SqlConnectionIPAddressPreference ipPreference, SQLDNSInfo cachedDNSInfo) { // initialize consumer info for MARS Sni_Consumer_Info native_consumerInfo = new Sni_Consumer_Info(); @@ -353,10 +355,11 @@ internal static unsafe uint SNIOpenMarsSession(ConsumerInfo consumerInfo, SNIHan native_cachedDNSInfo.wszCachedTcpIPv6 = cachedDNSInfo?.AddrIPv6; native_cachedDNSInfo.wszCachedTcpPort = cachedDNSInfo?.Port; - return SNIOpenWrapper(ref native_consumerInfo, "session:", parent, out pConn, fSync, ref native_cachedDNSInfo); + return SNIOpenWrapper(ref native_consumerInfo, "session:", parent, out pConn, fSync, ipPreference, ref native_cachedDNSInfo); } - 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) + internal static unsafe uint SNIOpenSyncEx(ConsumerInfo consumerInfo, string constring, ref IntPtr pConn, byte[] spnBuffer, byte[] instanceName, bool fOverrideCache, + bool fSync, int timeout, bool fParallel, SqlConnectionIPAddressPreference ipPreference, SQLDNSInfo cachedDNSInfo) { fixed (byte* pin_instanceName = &instanceName[0]) { @@ -379,6 +382,7 @@ internal static unsafe uint SNIOpenSyncEx(ConsumerInfo consumerInfo, string cons clientConsumerInfo.totalTimeout = SniOpenTimeOut; clientConsumerInfo.isAzureSqlServerEndpoint = ADP.IsAzureSqlServerEndpoint(constring); + clientConsumerInfo.ipAddressPreference = ipPreference; clientConsumerInfo.DNSCacheInfo.wszCachedFQDN = cachedDNSInfo?.FQDN; clientConsumerInfo.DNSCacheInfo.wszCachedTcpIPv4 = cachedDNSInfo?.AddrIPv4; clientConsumerInfo.DNSCacheInfo.wszCachedTcpIPv6 = cachedDNSInfo?.AddrIPv6; diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs index eb23fcdfef..3c22c4ecd8 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Reflection; @@ -400,6 +401,110 @@ internal static SqlConnectionAttestationProtocol ConvertToAttestationProtocol(st #endregion + #region <> + /// + /// IP Address Preference. + /// + private readonly static Dictionary s_preferenceNames = new(StringComparer.InvariantCultureIgnoreCase); + + static DbConnectionStringBuilderUtil() + { + foreach (SqlConnectionIPAddressPreference item in Enum.GetValues(typeof(SqlConnectionIPAddressPreference))) + { + s_preferenceNames.Add(item.ToString(), item); + } + } + + /// + /// Convert a string value to the corresponding IPAddressPreference. + /// + /// The string representation of the enumeration name to convert. + /// When this method returns, `result` contains an object of type `SqlConnectionIPAddressPreference` whose value is represented by `value` if the operation succeeds. + /// If the parse operation fails, `result` contains the default value of the `SqlConnectionIPAddressPreference` type. + /// `true` if the value parameter was converted successfully; otherwise, `false`. + internal static bool TryConvertToIPAddressPreference(string value, out SqlConnectionIPAddressPreference result) + { + if (!s_preferenceNames.TryGetValue(value, out result)) + { + result = DbConnectionStringDefaults.IPAddressPreference; + return false; + } + return true; + } + + /// + /// Verifies if the `value` is defined in the expected Enum. + /// + internal static bool IsValidIPAddressPreference(SqlConnectionIPAddressPreference value) + => value == SqlConnectionIPAddressPreference.IPv4First + || value == SqlConnectionIPAddressPreference.IPv6First + || value == SqlConnectionIPAddressPreference.UsePlatformDefault; + + internal static string IPAddressPreferenceToString(SqlConnectionIPAddressPreference value) + => Enum.GetName(typeof(SqlConnectionIPAddressPreference), value); + + internal static SqlConnectionIPAddressPreference ConvertToIPAddressPreference(string keyword, object value) + { + if (value is null) + { + return DbConnectionStringDefaults.IPAddressPreference; // IPv4First + } + + if (value is string sValue) + { + // try again after remove leading & trailing whitespaces. + sValue = sValue.Trim(); + if (TryConvertToIPAddressPreference(sValue, out SqlConnectionIPAddressPreference result)) + { + return result; + } + + // string values must be valid + throw ADP.InvalidConnectionOptionValue(keyword); + } + else + { + // the value is not string, try other options + SqlConnectionIPAddressPreference eValue; + + if (value is SqlConnectionIPAddressPreference preference) + { + eValue = preference; + } + else if (value.GetType().IsEnum) + { + // explicitly block scenarios in which user tries to use wrong enum types, like: + // builder["SqlConnectionIPAddressPreference"] = EnvironmentVariableTarget.Process; + // workaround: explicitly cast non-SqlConnectionIPAddressPreference enums to int + throw ADP.ConvertFailed(value.GetType(), typeof(SqlConnectionIPAddressPreference), null); + } + else + { + try + { + // Enum.ToObject allows only integral and enum values (enums are blocked above), raising ArgumentException for the rest + eValue = (SqlConnectionIPAddressPreference)Enum.ToObject(typeof(SqlConnectionIPAddressPreference), value); + } + catch (ArgumentException e) + { + // to be consistent with the messages we send in case of wrong type usage, replace + // the error with our exception, and keep the original one as inner one for troubleshooting + throw ADP.ConvertFailed(value.GetType(), typeof(SqlConnectionIPAddressPreference), e); + } + } + + if (IsValidIPAddressPreference(eValue)) + { + return eValue; + } + else + { + throw ADP.InvalidEnumerationValue(typeof(SqlConnectionIPAddressPreference), (int)eValue); + } + } + } + #endregion + internal static bool IsValidApplicationIntentValue(ApplicationIntent value) { Debug.Assert(Enum.GetNames(typeof(ApplicationIntent)).Length == 2, "ApplicationIntent enum has changed, update needed"); @@ -728,6 +833,7 @@ internal static partial class DbConnectionStringDefaults internal const SqlConnectionColumnEncryptionSetting ColumnEncryptionSetting = SqlConnectionColumnEncryptionSetting.Disabled; internal const string EnclaveAttestationUrl = _emptyString; internal const SqlConnectionAttestationProtocol AttestationProtocol = SqlConnectionAttestationProtocol.NotSpecified; + internal const SqlConnectionIPAddressPreference IPAddressPreference = SqlConnectionIPAddressPreference.IPv4First; } @@ -765,6 +871,7 @@ internal static partial class DbConnectionStringKeywords internal const string ColumnEncryptionSetting = "Column Encryption Setting"; internal const string EnclaveAttestationUrl = "Enclave Attestation Url"; internal const string AttestationProtocol = "Attestation Protocol"; + internal const string IPAddressPreference = "IP Address Preference"; // common keywords (OleDb, OracleClient, SqlClient) internal const string DataSource = "Data Source"; @@ -793,6 +900,9 @@ internal static class DbConnectionStringSynonyms //internal const string ApplicationName = APP; internal const string APP = "app"; + // internal const string IPAddressPreference = IPADDRESSPREFERENCE; + internal const string IPADDRESSPREFERENCE = "IPAddressPreference"; + //internal const string ApplicationIntent = APPLICATIONINTENT; internal const string APPLICATIONINTENT = "ApplicationIntent"; diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIProxy.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIProxy.cs index e05b7498f8..5823e7f44c 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIProxy.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIProxy.cs @@ -254,10 +254,12 @@ internal uint WritePacket(SNIHandle handle, SNIPacket packet, bool sync) /// Asynchronous connection /// Attempt parallel connects /// + /// IP address preference /// Used for DNS Cache - /// Used for DNS Cache + /// Used for DNS Cache /// SNI handle - 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) + internal SNIHandle CreateConnectionHandle(string fullServerName, bool ignoreSniOpenTimeout, long timerExpire, out byte[] instanceName, ref byte[][] spnBuffer, + bool flushCache, bool async, bool parallel, bool isIntegratedSecurity, SqlConnectionIPAddressPreference ipPreference, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo) { instanceName = new byte[1]; @@ -284,7 +286,7 @@ internal SNIHandle CreateConnectionHandle(string fullServerName, bool ignoreSniO case DataSource.Protocol.Admin: case DataSource.Protocol.None: // default to using tcp if no protocol is provided case DataSource.Protocol.TCP: - sniHandle = CreateTcpHandle(details, timerExpire, parallel, cachedFQDN, ref pendingDNSInfo); + sniHandle = CreateTcpHandle(details, timerExpire, parallel, ipPreference, cachedFQDN, ref pendingDNSInfo); break; case DataSource.Protocol.NP: sniHandle = CreateNpHandle(details, timerExpire, parallel); @@ -374,10 +376,11 @@ private static byte[][] GetSqlServerSPNs(string hostNameOrAddress, string portOr /// Data source /// Timer expiration /// Should MultiSubnetFailover be used + /// IP address preference /// Key for DNS Cache - /// Used for DNS Cache + /// Used for DNS Cache /// SNITCPHandle - private SNITCPHandle CreateTcpHandle(DataSource details, long timerExpire, bool parallel, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo) + private SNITCPHandle CreateTcpHandle(DataSource details, long timerExpire, bool parallel, SqlConnectionIPAddressPreference ipPreference, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo) { // TCP Format: // tcp:\ @@ -415,7 +418,7 @@ private SNITCPHandle CreateTcpHandle(DataSource details, long timerExpire, bool port = isAdminConnection ? DefaultSqlServerDacPort : DefaultSqlServerPort; } - return new SNITCPHandle(hostName, port, timerExpire, parallel, cachedFQDN, ref pendingDNSInfo); + return new SNITCPHandle(hostName, port, timerExpire, parallel, ipPreference, cachedFQDN, ref pendingDNSInfo); } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNITcpHandle.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNITcpHandle.cs index 4ca0631c51..d2a8341c0f 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNITcpHandle.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNITcpHandle.cs @@ -116,9 +116,10 @@ public override int ProtocolVersion /// TCP port number /// Connection timer expiration /// Parallel executions + /// IP address preference /// Key for DNS Cache - /// Used for DNS Cache - public SNITCPHandle(string serverName, int port, long timerExpire, bool parallel, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo) + /// Used for DNS Cache + public SNITCPHandle(string serverName, int port, long timerExpire, bool parallel, SqlConnectionIPAddressPreference ipPreference, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo) { long scopeID = SqlClientEventSource.Log.TrySNIScopeEnterEvent(s_className); SqlClientEventSource.Log.TrySNITraceEvent(s_className, EventType.INFO, "Connection Id {0}, Setting server name = {1}", args0: _connectionId, args1: serverName); @@ -147,8 +148,8 @@ public SNITCPHandle(string serverName, int port, long timerExpire, bool parallel SqlClientEventSource.Log.TrySNITraceEvent(s_className, EventType.INFO, "Connection Id {0}, Connecting to serverName {1} and port {2}", args0: _connectionId, args1: serverName, args2: port); // We will always first try to connect with serverName as before and let the DNS server to resolve the serverName. - // If the DSN resolution fails, we will try with IPs in the DNS cache if existed. We try with IPv4 first and followed by IPv6 if - // IPv4 fails. The exceptions will be throw to upper level and be handled as before. + // If the DSN resolution fails, we will try with IPs in the DNS cache if existed. We try with cached IPs based on IPAddressPreference. + // The exceptions will be throw to upper level and be handled as before. try { if (parallel) @@ -157,7 +158,7 @@ public SNITCPHandle(string serverName, int port, long timerExpire, bool parallel } else { - _socket = Connect(serverName, port, ts, isInfiniteTimeOut, cachedFQDN, ref pendingDNSInfo); + _socket = Connect(serverName, port, ts, isInfiniteTimeOut, ipPreference, cachedFQDN, ref pendingDNSInfo); } } catch (Exception ex) @@ -175,15 +176,26 @@ public SNITCPHandle(string serverName, int port, long timerExpire, bool parallel int portRetry = string.IsNullOrEmpty(cachedDNSInfo.Port) ? port : int.Parse(cachedDNSInfo.Port); SqlClientEventSource.Log.TrySNITraceEvent(s_className, EventType.INFO, "Connection Id {0}, Retrying with cached DNS IP Address {1} and port {2}", args0: _connectionId, args1: cachedDNSInfo.AddrIPv4, args2: cachedDNSInfo.Port); + string firstCachedIP; + string secondCachedIP; + + if (SqlConnectionIPAddressPreference.IPv6First == ipPreference) { + firstCachedIP = cachedDNSInfo.AddrIPv6; + secondCachedIP = cachedDNSInfo.AddrIPv4; + } else { + firstCachedIP = cachedDNSInfo.AddrIPv4; + secondCachedIP = cachedDNSInfo.AddrIPv6; + } + try { if (parallel) { - _socket = TryConnectParallel(cachedDNSInfo.AddrIPv4, portRetry, ts, isInfiniteTimeOut, ref reportError, cachedFQDN, ref pendingDNSInfo); + _socket = TryConnectParallel(firstCachedIP, portRetry, ts, isInfiniteTimeOut, ref reportError, cachedFQDN, ref pendingDNSInfo); } else { - _socket = Connect(cachedDNSInfo.AddrIPv4, portRetry, ts, isInfiniteTimeOut, cachedFQDN, ref pendingDNSInfo); + _socket = Connect(firstCachedIP, portRetry, ts, isInfiniteTimeOut, ipPreference, cachedFQDN, ref pendingDNSInfo); } } catch (Exception exRetry) @@ -194,11 +206,11 @@ public SNITCPHandle(string serverName, int port, long timerExpire, bool parallel SqlClientEventSource.Log.TrySNITraceEvent(s_className, EventType.INFO, "Connection Id {0}, Retrying exception {1}", args0: _connectionId, args1: exRetry?.Message); if (parallel) { - _socket = TryConnectParallel(cachedDNSInfo.AddrIPv6, portRetry, ts, isInfiniteTimeOut, ref reportError, cachedFQDN, ref pendingDNSInfo); + _socket = TryConnectParallel(secondCachedIP, portRetry, ts, isInfiniteTimeOut, ref reportError, cachedFQDN, ref pendingDNSInfo); } else { - _socket = Connect(cachedDNSInfo.AddrIPv6, portRetry, ts, isInfiniteTimeOut, cachedFQDN, ref pendingDNSInfo); + _socket = Connect(secondCachedIP, portRetry, ts, isInfiniteTimeOut, ipPreference, cachedFQDN, ref pendingDNSInfo); } } else @@ -320,42 +332,37 @@ private Socket TryConnectParallel(string hostName, int port, TimeSpan ts, bool i // Connect to server with hostName and port. // The IP information will be collected temporarily as the pendingDNSInfo but is not stored in the DNS cache at this point. // Only write to the DNS cache when we receive IsSupported flag as true in the Feature Ext Ack from server. - private static Socket Connect(string serverName, int port, TimeSpan timeout, bool isInfiniteTimeout, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo) + private static Socket Connect(string serverName, int port, TimeSpan timeout, bool isInfiniteTimeout, SqlConnectionIPAddressPreference ipPreference, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo) { + SqlClientEventSource.Log.TrySNITraceEvent(s_className, EventType.INFO, "IP preference : {0}", Enum.GetName(typeof(SqlConnectionIPAddressPreference), ipPreference)); + IPAddress[] ipAddresses = Dns.GetHostAddresses(serverName); string IPv4String = null; - string IPv6String = null; - + string IPv6String = null; + // Returning null socket is handled by the caller function. - if(ipAddresses == null || ipAddresses.Length == 0) + if (ipAddresses == null || ipAddresses.Length == 0) { return null; } Socket[] sockets = new Socket[ipAddresses.Length]; - AddressFamily[] preferedIPFamilies = new AddressFamily[] { AddressFamily.InterNetwork, AddressFamily.InterNetworkV6 }; - - CancellationTokenSource cts = null; + AddressFamily[] preferedIPFamilies = new AddressFamily[2]; - void Cancel() + if (ipPreference == SqlConnectionIPAddressPreference.IPv4First) { - for (int i = 0; i < sockets.Length; ++i) - { - try - { - if (sockets[i] != null && !sockets[i].Connected) - { - sockets[i].Dispose(); - sockets[i] = null; - } - } - catch (Exception e) - { - SqlClientEventSource.Log.TrySNITraceEvent(s_className, EventType.ERR, "THIS EXCEPTION IS BEING SWALLOWED: {0}", args0: e?.Message); - } - } + preferedIPFamilies[0] = AddressFamily.InterNetwork; + preferedIPFamilies[1] = AddressFamily.InterNetworkV6; } + else if (ipPreference == SqlConnectionIPAddressPreference.IPv6First) + { + preferedIPFamilies[0] = AddressFamily.InterNetworkV6; + preferedIPFamilies[1] = AddressFamily.InterNetwork; + } + // else -> UsePlatformDefault + + CancellationTokenSource cts = null; if (!isInfiniteTimeout) { @@ -366,32 +373,39 @@ void Cancel() Socket availableSocket = null; try { - int n = 0; // Socket index - // We go through the IP list twice. // In the first traversal, we only try to connect with the preferedIPFamilies[0]. // In the second traversal, we only try to connect with the preferedIPFamilies[1]. + // For UsePlatformDefault preference, we do traversal once. for (int i = 0; i < preferedIPFamilies.Length; ++i) { - foreach (IPAddress ipAddress in ipAddresses) + for (int n = 0; n < ipAddresses.Length; n++) { + IPAddress ipAddress = ipAddresses[n]; try { - if (ipAddress != null && ipAddress.AddressFamily == preferedIPFamilies[i]) + if (ipAddress != null) { + if (ipAddress.AddressFamily != preferedIPFamilies[i] && ipPreference != SqlConnectionIPAddressPreference.UsePlatformDefault) + { + continue; + } + sockets[n] = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // enable keep-alive on socket SetKeepAliveValues(ref sockets[n]); - SqlClientEventSource.Log.TrySNITraceEvent(s_className, EventType.INFO, "Connecting to IP address {0} and port {1}", args0: ipAddress, args1: port); + SqlClientEventSource.Log.TrySNITraceEvent(s_className, EventType.INFO, "Connecting to IP address {0} and port {1} using {2} address family.", + args0: ipAddress, + args1: port, + args2: ipAddress.AddressFamily); sockets[n].Connect(ipAddress, port); - if (sockets[n] != null) // sockets[i] can be null if cancel callback is executed during connect() + if (sockets[n] != null) // sockets[n] can be null if cancel callback is executed during connect() { if (sockets[n].Connected) { availableSocket = sockets[n]; - if (ipAddress.AddressFamily == AddressFamily.InterNetwork) { IPv4String = ipAddress.ToString(); @@ -409,20 +423,21 @@ void Cancel() sockets[n] = null; } } - n++; } } catch (Exception e) { SqlClientEventSource.Log.TrySNITraceEvent(s_className, EventType.ERR, "THIS EXCEPTION IS BEING SWALLOWED: {0}", args0: e?.Message); + SqlClientEventSource.Log.TryAdvancedTraceEvent($"{s_className}.{System.Reflection.MethodBase.GetCurrentMethod().Name}{EventType.ERR}THIS EXCEPTION IS BEING SWALLOWED: {e}"); } } - // If we have already got an valid Socket, we won't do the second traversal. - if (availableSocket != null) + // If we have already got a valid Socket, or the platform default was prefered + // we won't do the second traversal. + if (availableSocket != null || ipPreference == SqlConnectionIPAddressPreference.UsePlatformDefault) { break; - } + } } } finally @@ -437,6 +452,25 @@ void Cancel() } return availableSocket; + + void Cancel() + { + for (int i = 0; i < sockets.Length; ++i) + { + try + { + if (sockets[i] != null && !sockets[i].Connected) + { + sockets[i].Dispose(); + sockets[i] = null; + } + } + catch (Exception e) + { + SqlClientEventSource.Log.TrySNITraceEvent(s_className, EventType.ERR, "THIS EXCEPTION IS BEING SWALLOWED: {0}", args0: e?.Message); + } + } + } } private static Task ParallelConnectAsync(IPAddress[] serverAddresses, int port) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs index 7cdd3b56d5..d89f841b48 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -391,6 +391,14 @@ internal SqlConnectionAttestationProtocol AttestationProtocol } } + /// + /// Get IP address preference + /// + internal SqlConnectionIPAddressPreference iPAddressPreference + { + get => ((SqlConnectionString)ConnectionOptions).IPAddressPreference; + } + // This method will be called once connection string is set or changed. private void CacheConnectionStringProperties() { diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionString.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionString.cs index aa9023139c..d2d2cf7891 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionString.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionString.cs @@ -53,6 +53,7 @@ internal static partial class DEFAULT internal const SqlConnectionColumnEncryptionSetting ColumnEncryptionSetting = SqlConnectionColumnEncryptionSetting.Disabled; internal const string EnclaveAttestationUrl = _emptyString; internal static readonly SqlConnectionAttestationProtocol AttestationProtocol = SqlConnectionAttestationProtocol.NotSpecified; + internal static readonly SqlConnectionIPAddressPreference s_IPAddressPreference = SqlConnectionIPAddressPreference.IPv4First; } // SqlConnection ConnectionString Options @@ -69,6 +70,7 @@ internal static class KEY internal const string ColumnEncryptionSetting = "column encryption setting"; internal const string EnclaveAttestationUrl = "enclave attestation url"; internal const string AttestationProtocol = "attestation protocol"; + internal const string IPAddressPreference = "ip address preference"; internal const string Command_Timeout = "command timeout"; internal const string Connect_Timeout = "connect timeout"; @@ -106,6 +108,8 @@ internal static class KEY // Constant for the number of duplicate options in the connection string private static class SYNONYM { + // ip address preference + internal const string IPADDRESSPREFERENCE = "ipaddresspreference"; //application intent internal const string APPLICATIONINTENT = "applicationintent"; // application name @@ -160,9 +164,9 @@ private static class SYNONYM } #if NETCOREAPP - internal const int SynonymCount = 25; + internal const int SynonymCount = 26; #else - internal const int SynonymCount = 24; + internal const int SynonymCount = 25; #endif internal const int DeprecatedSynonymCount = 3; @@ -213,6 +217,7 @@ internal static class TRANSACTIONBINDING private readonly SqlConnectionColumnEncryptionSetting _columnEncryptionSetting; private readonly string _enclaveAttestationUrl; private readonly SqlConnectionAttestationProtocol _attestationProtocol; + private readonly SqlConnectionIPAddressPreference _ipAddressPreference; private readonly int _commandTimeout; private readonly int _connectTimeout; @@ -293,6 +298,7 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G _columnEncryptionSetting = ConvertValueToColumnEncryptionSetting(); _enclaveAttestationUrl = ConvertValueToString(KEY.EnclaveAttestationUrl, DEFAULT.EnclaveAttestationUrl); _attestationProtocol = ConvertValueToAttestationProtocol(); + _ipAddressPreference = ConvertValueToIPAddressPreference(); // Temporary string - this value is stored internally as an enum. string typeSystemVersionString = ConvertValueToString(KEY.Type_System_Version, null); @@ -559,6 +565,7 @@ internal SqlConnectionString(SqlConnectionString connectionOptions, string dataS internal SqlConnectionColumnEncryptionSetting ColumnEncryptionSetting { get { return _columnEncryptionSetting; } } internal string EnclaveAttestationUrl { get { return _enclaveAttestationUrl; } } internal SqlConnectionAttestationProtocol AttestationProtocol { get { return _attestationProtocol; } } + internal SqlConnectionIPAddressPreference IPAddressPreference => _ipAddressPreference; internal bool PersistSecurityInfo { get { return _persistSecurityInfo; } } internal bool Pooling { get { return _pooling; } } internal bool Replication { get { return _replication; } } @@ -687,6 +694,7 @@ internal static Dictionary GetParseSynonyms() { KEY.Connect_Retry_Count, KEY.Connect_Retry_Count }, { KEY.Connect_Retry_Interval, KEY.Connect_Retry_Interval }, { KEY.Authentication, KEY.Authentication }, + { KEY.IPAddressPreference, KEY.IPAddressPreference }, { SYNONYM.APP, KEY.Application_Name }, { SYNONYM.APPLICATIONINTENT, KEY.ApplicationIntent }, @@ -717,7 +725,8 @@ internal static Dictionary GetParseSynonyms() { SYNONYM.TRUSTSERVERCERTIFICATE, KEY.TrustServerCertificate }, { SYNONYM.UID, KEY.User_ID }, { SYNONYM.User, KEY.User_ID }, - { SYNONYM.WSID, KEY.Workstation_Id } + { SYNONYM.WSID, KEY.Workstation_Id }, + { SYNONYM.IPADDRESSPREFERENCE, KEY.IPAddressPreference } }; Debug.Assert(synonyms.Count == count, "incorrect initial ParseSynonyms size"); Interlocked.CompareExchange(ref s_sqlClientSynonyms, synonyms, null); @@ -898,5 +907,30 @@ internal SqlConnectionAttestationProtocol ConvertValueToAttestationProtocol() throw ADP.InvalidConnectionOptionValue(KEY.AttestationProtocol, e); } } + + /// + /// Convert the value to SqlConnectionIPAddressPreference + /// + /// + internal SqlConnectionIPAddressPreference ConvertValueToIPAddressPreference() + { + if (!TryGetParsetableValue(KEY.IPAddressPreference, out string value)) + { + return DEFAULT.s_IPAddressPreference; + } + + try + { + return DbConnectionStringBuilderUtil.ConvertToIPAddressPreference(KEY.IPAddressPreference, value); + } + catch (FormatException e) + { + throw ADP.InvalidConnectionOptionValue(KEY.IPAddressPreference, e); + } + catch (OverflowException e) + { + throw ADP.InvalidConnectionOptionValue(KEY.IPAddressPreference, e); + } + } } } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs index 0a7a06659a..9102b07a4a 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs @@ -59,6 +59,7 @@ private enum Keywords AttestationProtocol, CommandTimeout, + IPAddressPreference, // keep the count value last KeywordsCount @@ -107,6 +108,7 @@ private enum Keywords private SqlConnectionColumnEncryptionSetting _columnEncryptionSetting = DbConnectionStringDefaults.ColumnEncryptionSetting; private string _enclaveAttestationUrl = DbConnectionStringDefaults.EnclaveAttestationUrl; private SqlConnectionAttestationProtocol _attestationProtocol = DbConnectionStringDefaults.AttestationProtocol; + private SqlConnectionIPAddressPreference _ipAddressPreference = DbConnectionStringDefaults.IPAddressPreference; private static string[] CreateValidKeywords() { @@ -149,6 +151,7 @@ private static string[] CreateValidKeywords() validKeywords[(int)Keywords.ColumnEncryptionSetting] = DbConnectionStringKeywords.ColumnEncryptionSetting; validKeywords[(int)Keywords.EnclaveAttestationUrl] = DbConnectionStringKeywords.EnclaveAttestationUrl; validKeywords[(int)Keywords.AttestationProtocol] = DbConnectionStringKeywords.AttestationProtocol; + validKeywords[(int)Keywords.IPAddressPreference] = DbConnectionStringKeywords.IPAddressPreference; return validKeywords; } @@ -193,7 +196,9 @@ private static Dictionary CreateKeywordsDictionary() hash.Add(DbConnectionStringKeywords.ColumnEncryptionSetting, Keywords.ColumnEncryptionSetting); hash.Add(DbConnectionStringKeywords.EnclaveAttestationUrl, Keywords.EnclaveAttestationUrl); hash.Add(DbConnectionStringKeywords.AttestationProtocol, Keywords.AttestationProtocol); + hash.Add(DbConnectionStringKeywords.IPAddressPreference, Keywords.IPAddressPreference); + hash.Add(DbConnectionStringSynonyms.IPADDRESSPREFERENCE, Keywords.IPAddressPreference); hash.Add(DbConnectionStringSynonyms.APP, Keywords.ApplicationName); hash.Add(DbConnectionStringSynonyms.APPLICATIONINTENT, Keywords.ApplicationIntent); hash.Add(DbConnectionStringSynonyms.EXTENDEDPROPERTIES, Keywords.AttachDBFilename); @@ -326,6 +331,9 @@ public override object this[string keyword] case Keywords.AttestationProtocol: AttestationProtocol = ConvertToAttestationProtocol(keyword, value); break; + case Keywords.IPAddressPreference: + IPAddressPreference = ConvertToIPAddressPreference(keyword, value); + break; #if NETCOREAPP case Keywords.PoolBlockingPeriod: PoolBlockingPeriod = ConvertToPoolBlockingPeriod(keyword, value); break; #endif @@ -519,6 +527,22 @@ public SqlConnectionAttestationProtocol AttestationProtocol } } + /// + public SqlConnectionIPAddressPreference IPAddressPreference + { + get => _ipAddressPreference; + set + { + if (!DbConnectionStringBuilderUtil.IsValidIPAddressPreference(value)) + { + throw ADP.InvalidEnumerationValue(typeof(SqlConnectionIPAddressPreference), (int)value); + } + + SetIPAddressPreferenceValue(value); + _ipAddressPreference = value; + } + } + /// public bool TrustServerCertificate { @@ -904,6 +928,14 @@ private static SqlConnectionAttestationProtocol ConvertToAttestationProtocol(str return DbConnectionStringBuilderUtil.ConvertToAttestationProtocol(keyword, value); } + /// + /// Convert to SqlConnectionIPAddressPreference + /// + /// + /// + private static SqlConnectionIPAddressPreference ConvertToIPAddressPreference(string keyword, object value) + => DbConnectionStringBuilderUtil.ConvertToIPAddressPreference(keyword, value); + private object GetAt(Keywords index) { switch (index) @@ -980,6 +1012,8 @@ private object GetAt(Keywords index) return EnclaveAttestationUrl; case Keywords.AttestationProtocol: return AttestationProtocol; + case Keywords.IPAddressPreference: + return IPAddressPreference; default: Debug.Fail("unexpected keyword"); throw UnsupportedKeyword(s_validKeywords[(int)index]); @@ -1127,6 +1161,9 @@ private void Reset(Keywords index) case Keywords.AttestationProtocol: _attestationProtocol = DbConnectionStringDefaults.AttestationProtocol; break; + case Keywords.IPAddressPreference: + _ipAddressPreference = DbConnectionStringDefaults.IPAddressPreference; + break; default: Debug.Fail("unexpected keyword"); throw UnsupportedKeyword(s_validKeywords[(int)index]); @@ -1163,6 +1200,12 @@ private void SetAttestationProtocolValue(SqlConnectionAttestationProtocol value) base[DbConnectionStringKeywords.AttestationProtocol] = DbConnectionStringBuilderUtil.AttestationProtocolToString(value); } + private void SetIPAddressPreferenceValue(SqlConnectionIPAddressPreference value) + { + Debug.Assert(DbConnectionStringBuilderUtil.IsValidIPAddressPreference(value), "Invalid value for SqlConnectionIPAddressPreference"); + base[DbConnectionStringKeywords.IPAddressPreference] = DbConnectionStringBuilderUtil.IPAddressPreferenceToString(value); + } + private void SetAuthenticationValue(SqlAuthenticationMethod value) { Debug.Assert(DbConnectionStringBuilderUtil.IsValidAuthenticationTypeValue(value), "Invalid value for AuthenticationType"); @@ -1306,4 +1349,3 @@ public override StandardValuesCollection GetStandardValues(ITypeDescriptorContex } } } - diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 5dcda47e0f..3935ed01e4 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -21,7 +21,7 @@ namespace Microsoft.Data.SqlClient { - internal class SessionStateRecord + internal sealed class SessionStateRecord { internal bool _recoverable; internal uint _version; @@ -29,7 +29,7 @@ internal class SessionStateRecord internal byte[] _data; } - internal class SessionData + internal sealed class SessionData { internal const int _maxNumberOfSessionStates = 256; internal uint _tdsVersion; @@ -101,7 +101,7 @@ public void AssertUnrecoverableStateCountIsCorrect() } } - sealed internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable + internal sealed class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { // CONNECTION AND STATE VARIABLES private readonly SqlConnectionPoolGroupProviderInfo _poolGroupProviderInfo; // will only be null when called for ChangePassword, or creating SSE User Instance @@ -2481,12 +2481,9 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) internal void OnFeatureExtAck(int featureId, byte[] data) { - if (RoutingInfo != null) + if (RoutingInfo != null && TdsEnums.FEATUREEXT_SQLDNSCACHING != featureId) { - if (TdsEnums.FEATUREEXT_SQLDNSCACHING != featureId) - { - return; - } + return; } switch (featureId) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs index 416ec86fd0..9099f5882c 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -1077,6 +1077,19 @@ public enum SqlConnectionAttestationProtocol HGS = 3 } + /// + public enum SqlConnectionIPAddressPreference + { + /// + IPv4First = 0, // default + + /// + IPv6First = 1, + + /// + UsePlatformDefault = 2 + } + /// public enum SqlConnectionColumnEncryptionSetting { diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index e0ebfdf669..9e26aa375f 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -412,8 +412,8 @@ internal void Connect( _connHandler.pendingSQLDNSObject = null; // AD Integrated behaves like Windows integrated when connecting to a non-fedAuth server - _physicalStateObj.CreatePhysicalSNIHandle(serverInfo.ExtendedServerName, ignoreSniOpenTimeout, timerExpire, - out instanceName, ref _sniSpnBuffer, false, true, fParallel, FQDNforDNSCahce, ref _connHandler.pendingSQLDNSObject, integratedSecurity || authType == SqlAuthenticationMethod.ActiveDirectoryIntegrated); + _physicalStateObj.CreatePhysicalSNIHandle(serverInfo.ExtendedServerName, ignoreSniOpenTimeout, timerExpire, out instanceName, ref _sniSpnBuffer, false, true, fParallel, + _connHandler.ConnectionOptions.IPAddressPreference, FQDNforDNSCahce, ref _connHandler.pendingSQLDNSObject, integratedSecurity || authType == SqlAuthenticationMethod.ActiveDirectoryIntegrated); if (TdsEnums.SNI_SUCCESS != _physicalStateObj.Status) { @@ -477,7 +477,8 @@ internal void Connect( // On Instance failure re-connect and flush SNI named instance cache. _physicalStateObj.SniContext = SniContext.Snix_Connect; - _physicalStateObj.CreatePhysicalSNIHandle(serverInfo.ExtendedServerName, ignoreSniOpenTimeout, timerExpire, out instanceName, ref _sniSpnBuffer, true, true, fParallel, FQDNforDNSCahce, ref _connHandler.pendingSQLDNSObject, integratedSecurity); + _physicalStateObj.CreatePhysicalSNIHandle(serverInfo.ExtendedServerName, ignoreSniOpenTimeout, timerExpire, out instanceName, ref _sniSpnBuffer, true, true, fParallel, + _connHandler.ConnectionOptions.IPAddressPreference, FQDNforDNSCahce, ref _connHandler.pendingSQLDNSObject, integratedSecurity); if (TdsEnums.SNI_SUCCESS != _physicalStateObj.Status) { diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserSafeHandles.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserSafeHandles.cs index 921d72a385..62411969ff 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserSafeHandles.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserSafeHandles.cs @@ -144,6 +144,7 @@ internal SNIHandle( bool flushCache, bool fSync, bool fParallel, + SqlConnectionIPAddressPreference ipPreference, SQLDNSInfo cachedDNSInfo) : base(IntPtr.Zero, true) { @@ -159,18 +160,18 @@ internal SNIHandle( } _status = SNINativeMethodWrapper.SNIOpenSyncEx(myInfo, serverName, ref base.handle, - spnBuffer, instanceName, flushCache, fSync, timeout, fParallel, cachedDNSInfo); + spnBuffer, instanceName, flushCache, fSync, timeout, fParallel, ipPreference, cachedDNSInfo); } } // constructs SNI Handle for MARS session - internal SNIHandle(SNINativeMethodWrapper.ConsumerInfo myInfo, SNIHandle parent, SQLDNSInfo cachedDNSInfo) : base(IntPtr.Zero, true) + internal SNIHandle(SNINativeMethodWrapper.ConsumerInfo myInfo, SNIHandle parent, SqlConnectionIPAddressPreference ipPreference, SQLDNSInfo cachedDNSInfo) : base(IntPtr.Zero, true) { try { } finally { - _status = SNINativeMethodWrapper.SNIOpenMarsSession(myInfo, parent, ref base.handle, parent._fSync, cachedDNSInfo); + _status = SNINativeMethodWrapper.SNIOpenMarsSession(myInfo, parent, ref base.handle, parent._fSync, ipPreference, cachedDNSInfo); } } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs index f3299816ed..05bba67a5a 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs @@ -792,7 +792,8 @@ private void ResetCancelAndProcessAttention() } } - internal abstract void CreatePhysicalSNIHandle(string serverName, bool ignoreSniOpenTimeout, long timerExpire, out byte[] instanceName, ref byte[][] spnBuffer, bool flushCache, bool async, bool fParallel, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo, bool isIntegratedSecurity = false); + internal abstract void CreatePhysicalSNIHandle(string serverName, bool ignoreSniOpenTimeout, long timerExpire, out byte[] instanceName, ref byte[][] spnBuffer, bool flushCache, bool async, bool fParallel, + SqlConnectionIPAddressPreference iPAddressPreference, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo, bool isIntegratedSecurity = false); internal abstract void AssignPendingDNSInfo(string userProtocol, string DNSCacheKey, ref SQLDNSInfo pendingDNSInfo); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs index 24b0c960d8..6bf08b0336 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs @@ -50,9 +50,11 @@ internal SNIMarsHandle CreateMarsSession(object callbackObject, bool async) protected override uint SNIPacketGetData(PacketHandle packet, byte[] _inBuff, ref uint dataSize) => SNIProxy.GetInstance().PacketGetData(packet.ManagedPacket, _inBuff, ref dataSize); - internal override void CreatePhysicalSNIHandle(string serverName, bool ignoreSniOpenTimeout, long timerExpire, out byte[] instanceName, ref byte[][] spnBuffer, bool flushCache, bool async, bool parallel, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo, bool isIntegratedSecurity) + internal override void CreatePhysicalSNIHandle(string serverName, bool ignoreSniOpenTimeout, long timerExpire, out byte[] instanceName, ref byte[][] spnBuffer, bool flushCache, bool async, bool parallel, + SqlConnectionIPAddressPreference iPAddressPreference, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo, bool isIntegratedSecurity) { - _sessionHandle = SNIProxy.GetInstance().CreateConnectionHandle(serverName, ignoreSniOpenTimeout, timerExpire, out instanceName, ref spnBuffer, flushCache, async, parallel, isIntegratedSecurity, cachedFQDN, ref pendingDNSInfo); + _sessionHandle = SNIProxy.GetInstance().CreateConnectionHandle(serverName, ignoreSniOpenTimeout, timerExpire, out instanceName, ref spnBuffer, flushCache, async, parallel, isIntegratedSecurity, + iPAddressPreference, cachedFQDN, ref pendingDNSInfo); if (_sessionHandle == null) { _parser.ProcessSNIError(this); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs index 34c7910dde..ecb6e0bb43 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs @@ -66,7 +66,7 @@ protected override void CreateSessionHandle(TdsParserStateObject physicalConnect SQLDNSInfo cachedDNSInfo; bool ret = SQLFallbackDNSCache.Instance.GetDNSInfo(_parser.FQDNforDNSCahce, out cachedDNSInfo); - _sessionHandle = new SNIHandle(myInfo, nativeSNIObject.Handle, cachedDNSInfo); + _sessionHandle = new SNIHandle(myInfo, nativeSNIObject.Handle, _parser.Connection.ConnectionOptions.IPAddressPreference, cachedDNSInfo); } internal override void AssignPendingDNSInfo(string userProtocol, string DNSCacheKey, ref SQLDNSInfo pendingDNSInfo) @@ -137,7 +137,8 @@ private SNINativeMethodWrapper.ConsumerInfo CreateConsumerInfo(bool async) return myInfo; } - internal override void CreatePhysicalSNIHandle(string serverName, bool ignoreSniOpenTimeout, long timerExpire, out byte[] instanceName, ref byte[][] spnBuffer, bool flushCache, bool async, bool fParallel, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo, bool isIntegratedSecurity) + internal override void CreatePhysicalSNIHandle(string serverName, bool ignoreSniOpenTimeout, long timerExpire, out byte[] instanceName, ref byte[][] spnBuffer, bool flushCache, bool async, bool fParallel, + SqlConnectionIPAddressPreference ipPreference, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo, bool isIntegratedSecurity) { // We assume that the loadSSPILibrary has been called already. now allocate proper length of buffer spnBuffer = new byte[1][]; @@ -171,7 +172,7 @@ internal override void CreatePhysicalSNIHandle(string serverName, bool ignoreSni SQLDNSInfo cachedDNSInfo; bool ret = SQLFallbackDNSCache.Instance.GetDNSInfo(cachedFQDN, out cachedDNSInfo); - _sessionHandle = new SNIHandle(myInfo, serverName, spnBuffer[0], ignoreSniOpenTimeout, checked((int)timeout), out instanceName, flushCache, !async, fParallel, cachedDNSInfo); + _sessionHandle = new SNIHandle(myInfo, serverName, spnBuffer[0], ignoreSniOpenTimeout, checked((int)timeout), out instanceName, flushCache, !async, fParallel, ipPreference, cachedDNSInfo); } protected override uint SNIPacketGetData(PacketHandle packet, byte[] _inBuff, ref uint dataSize) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.Designer.cs index 8a95a52351..9318d7eb40 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.Designer.cs @@ -4442,6 +4442,12 @@ internal static string TCE_DbConnectionString_AttestationProtocol { return ResourceManager.GetString("TCE_DbConnectionString_AttestationProtocol", resourceCulture); } } + + /// + /// Looks up a localized string similar to Specifies an IP address preference when connecting to SQL instances. + /// + internal static string TCE_DbConnectionString_IPAddressPreference + => ResourceManager.GetString("TCE_DbConnectionString_IPAddressPreference", resourceCulture); /// /// Looks up a localized string similar to Decryption failed. The last 10 bytes of the encrypted column encryption key are: '{0}'. The first 10 bytes of ciphertext are: '{1}'.. diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.resx index 5dd36f38ac..335803c097 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.resx @@ -1851,6 +1851,9 @@ Specifies an attestation protocol for its corresponding enclave attestation service. + + Specifies an IP address preference when connecting to SQL instances. + The enclave type '{0}' returned from the server is not supported. diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs index 23c88ff5b5..7e35b64c04 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs @@ -888,6 +888,19 @@ public enum SqlConnectionAttestationProtocol HGS = 3 } + /// + public enum SqlConnectionIPAddressPreference + { + /// + IPv4First = 0, // default + + /// + IPv6First = 1, + + /// + UsePlatformDefault = 2 + } + /// public enum SqlConnectionOverrides { @@ -974,6 +987,10 @@ public SqlConnectionStringBuilder(string connectionString) { } [System.ComponentModel.DisplayNameAttribute("Attestation Protocol")] [System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)] public Microsoft.Data.SqlClient.SqlConnectionAttestationProtocol AttestationProtocol { get { throw null; } set { } } + /// + [System.ComponentModel.DisplayNameAttribute("IP Address Preference")] + [System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)] + public Microsoft.Data.SqlClient.SqlConnectionIPAddressPreference IPAddressPreference { get { throw null; } set { } } /// [System.ComponentModel.DisplayNameAttribute("Encrypt")] [System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)] diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/DbConnectionStringCommon.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/DbConnectionStringCommon.cs index 71ab0deea3..ec0bd3a558 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/DbConnectionStringCommon.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/DbConnectionStringCommon.cs @@ -998,6 +998,110 @@ internal static SqlConnectionAttestationProtocol ConvertToAttestationProtocol(st #endregion + #region <> + /// + /// IP Address Preference. + /// + private readonly static Dictionary s_preferenceNames = new(StringComparer.InvariantCultureIgnoreCase); + + static DbConnectionStringBuilderUtil() + { + foreach (SqlConnectionIPAddressPreference item in Enum.GetValues(typeof(SqlConnectionIPAddressPreference))) + { + s_preferenceNames.Add(item.ToString(), item); + } + } + + /// + /// Convert a string value to the corresponding IPAddressPreference. + /// + /// The string representation of the enumeration name to convert. + /// When this method returns, `result` contains an object of type `SqlConnectionIPAddressPreference` whose value is represented by `value` if the operation succeeds. + /// If the parse operation fails, `result` contains the default value of the `SqlConnectionIPAddressPreference` type. + /// `true` if the value parameter was converted successfully; otherwise, `false`. + internal static bool TryConvertToIPAddressPreference(string value, out SqlConnectionIPAddressPreference result) + { + if (!s_preferenceNames.TryGetValue(value, out result)) + { + result = DbConnectionStringDefaults.IPAddressPreference; + return false; + } + return true; + } + + /// + /// Verifies if the `value` is defined in the expected Enum. + /// + internal static bool IsValidIPAddressPreference(SqlConnectionIPAddressPreference value) + => value == SqlConnectionIPAddressPreference.IPv4First + || value == SqlConnectionIPAddressPreference.IPv6First + || value == SqlConnectionIPAddressPreference.UsePlatformDefault; + + internal static string IPAddressPreferenceToString(SqlConnectionIPAddressPreference value) + => Enum.GetName(typeof(SqlConnectionIPAddressPreference), value); + + internal static SqlConnectionIPAddressPreference ConvertToIPAddressPreference(string keyword, object value) + { + if (value is null) + { + return DbConnectionStringDefaults.IPAddressPreference; // IPv4First + } + + if (value is string sValue) + { + // try again after remove leading & trailing whitespaces. + sValue = sValue.Trim(); + if (TryConvertToIPAddressPreference(sValue, out SqlConnectionIPAddressPreference result)) + { + return result; + } + + // string values must be valid + throw ADP.InvalidConnectionOptionValue(keyword); + } + else + { + // the value is not string, try other options + SqlConnectionIPAddressPreference eValue; + + if (value is SqlConnectionIPAddressPreference preference) + { + eValue = preference; + } + else if (value.GetType().IsEnum) + { + // explicitly block scenarios in which user tries to use wrong enum types, like: + // builder["SqlConnectionIPAddressPreference"] = EnvironmentVariableTarget.Process; + // workaround: explicitly cast non-SqlConnectionIPAddressPreference enums to int + throw ADP.ConvertFailed(value.GetType(), typeof(SqlConnectionIPAddressPreference), null); + } + else + { + try + { + // Enum.ToObject allows only integral and enum values (enums are blocked above), raising ArgumentException for the rest + eValue = (SqlConnectionIPAddressPreference)Enum.ToObject(typeof(SqlConnectionIPAddressPreference), value); + } + catch (ArgumentException e) + { + // to be consistent with the messages we send in case of wrong type usage, replace + // the error with our exception, and keep the original one as inner one for troubleshooting + throw ADP.ConvertFailed(value.GetType(), typeof(SqlConnectionIPAddressPreference), e); + } + } + + if (IsValidIPAddressPreference(eValue)) + { + return eValue; + } + else + { + throw ADP.InvalidEnumerationValue(typeof(SqlConnectionIPAddressPreference), (int)eValue); + } + } + } + #endregion + internal static bool IsValidCertificateValue(string value) { return string.IsNullOrEmpty(value) @@ -1065,6 +1169,7 @@ internal static class DbConnectionStringDefaults internal static readonly SqlConnectionColumnEncryptionSetting ColumnEncryptionSetting = SqlConnectionColumnEncryptionSetting.Disabled; internal const string EnclaveAttestationUrl = _emptyString; internal const SqlConnectionAttestationProtocol AttestationProtocol = SqlConnectionAttestationProtocol.NotSpecified; + internal const SqlConnectionIPAddressPreference IPAddressPreference = SqlConnectionIPAddressPreference.IPv4First; internal const string Certificate = _emptyString; internal const PoolBlockingPeriod PoolBlockingPeriod = SqlClient.PoolBlockingPeriod.Auto; } @@ -1139,6 +1244,7 @@ internal static class DbConnectionStringKeywords internal const string ColumnEncryptionSetting = "Column Encryption Setting"; internal const string EnclaveAttestationUrl = "Enclave Attestation Url"; internal const string AttestationProtocol = "Attestation Protocol"; + internal const string IPAddressPreference = "IP Address Preference"; internal const string PoolBlockingPeriod = "Pool Blocking Period"; // common keywords (OleDb, OracleClient, SqlClient) @@ -1164,6 +1270,9 @@ internal static class DbConnectionStringSynonyms //internal const string ApplicationName = APP; internal const string APP = "app"; + // internal const string IPAddressPreference = IPADDRESSPREFERENCE; + internal const string IPADDRESSPREFERENCE = "ipaddresspreference"; + //internal const string ApplicationIntent = APPLICATIONINTENT; internal const string APPLICATIONINTENT = "applicationintent"; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeManagedWrapperX64.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeManagedWrapperX64.cs index 0cddc32dc1..b28c736977 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeManagedWrapperX64.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeManagedWrapperX64.cs @@ -101,6 +101,7 @@ internal static extern uint SNIOpenWrapper( [In] SNIHandle pConn, out IntPtr ppConn, [MarshalAs(UnmanagedType.Bool)] bool fSync, + SqlConnectionIPAddressPreference ipPreference, [In] ref SNI_DNSCache_Info pDNSCachedInfo); [DllImport(SNI, CallingConvention = CallingConvention.Cdecl)] diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeManagedWrapperX86.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeManagedWrapperX86.cs index 398ecc4872..2dc215ad36 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeManagedWrapperX86.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeManagedWrapperX86.cs @@ -101,6 +101,7 @@ internal static extern uint SNIOpenWrapper( [In] SNIHandle pConn, out IntPtr ppConn, [MarshalAs(UnmanagedType.Bool)] bool fSync, + SqlConnectionIPAddressPreference ipPreference, [In] ref SNI_DNSCache_Info pDNSCachedInfo); [DllImport(SNI, CallingConvention = CallingConvention.Cdecl)] diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeMethodWrapper.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeMethodWrapper.cs index 0ac874b8b6..19dd12587a 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeMethodWrapper.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeMethodWrapper.cs @@ -354,6 +354,7 @@ internal unsafe struct SNI_CLIENT_CONSUMER_INFO public TransparentNetworkResolutionMode transparentNetworkResolution; public int totalTimeout; public bool isAzureSqlServerEndpoint; + public SqlConnectionIPAddressPreference ipAddressPreference; public SNI_DNSCache_Info DNSCacheInfo; } @@ -604,11 +605,12 @@ private static uint SNIOpenWrapper( [In] SNIHandle pConn, out IntPtr ppConn, [MarshalAs(UnmanagedType.Bool)] bool fSync, + SqlConnectionIPAddressPreference ipPreference, [In] ref SNI_DNSCache_Info pDNSCachedInfo) { return s_is64bitProcess ? - SNINativeManagedWrapperX64.SNIOpenWrapper(ref pConsumerInfo, szConnect, pConn, out ppConn, fSync, ref pDNSCachedInfo) : - SNINativeManagedWrapperX86.SNIOpenWrapper(ref pConsumerInfo, szConnect, pConn, out ppConn, fSync, ref pDNSCachedInfo); + SNINativeManagedWrapperX64.SNIOpenWrapper(ref pConsumerInfo, szConnect, pConn, out ppConn, fSync, ipPreference, ref pDNSCachedInfo) : + SNINativeManagedWrapperX86.SNIOpenWrapper(ref pConsumerInfo, szConnect, pConn, out ppConn, fSync, ipPreference, ref pDNSCachedInfo); } private static IntPtr SNIPacketAllocateWrapper([In] SafeHandle pConn, IOType IOType) @@ -758,7 +760,7 @@ internal static uint SNIInitialize() return SNIInitialize(IntPtr.Zero); } - internal static unsafe uint SNIOpenMarsSession(ConsumerInfo consumerInfo, SNIHandle parent, ref IntPtr pConn, bool fSync, SQLDNSInfo cachedDNSInfo) + internal static unsafe uint SNIOpenMarsSession(ConsumerInfo consumerInfo, SNIHandle parent, ref IntPtr pConn, bool fSync, SqlConnectionIPAddressPreference ipPreference, SQLDNSInfo cachedDNSInfo) { // initialize consumer info for MARS Sni_Consumer_Info native_consumerInfo = new Sni_Consumer_Info(); @@ -770,10 +772,11 @@ internal static unsafe uint SNIOpenMarsSession(ConsumerInfo consumerInfo, SNIHan native_cachedDNSInfo.wszCachedTcpIPv6 = cachedDNSInfo?.AddrIPv6; native_cachedDNSInfo.wszCachedTcpPort = cachedDNSInfo?.Port; - return SNIOpenWrapper(ref native_consumerInfo, "session:", parent, out pConn, fSync, ref native_cachedDNSInfo); + return SNIOpenWrapper(ref native_consumerInfo, "session:", parent, out pConn, fSync, ipPreference, ref native_cachedDNSInfo); } - internal static unsafe uint SNIOpenSyncEx(ConsumerInfo consumerInfo, string constring, ref IntPtr pConn, byte[] spnBuffer, byte[] instanceName, bool fOverrideCache, bool fSync, int timeout, bool fParallel, Int32 transparentNetworkResolutionStateNo, Int32 totalTimeout, Boolean isAzureSqlServerEndpoint, SQLDNSInfo cachedDNSInfo) + internal static unsafe uint SNIOpenSyncEx(ConsumerInfo consumerInfo, string constring, ref IntPtr pConn, byte[] spnBuffer, byte[] instanceName, bool fOverrideCache, bool fSync, int timeout, bool fParallel, + Int32 transparentNetworkResolutionStateNo, Int32 totalTimeout, Boolean isAzureSqlServerEndpoint, SqlConnectionIPAddressPreference ipPreference, SQLDNSInfo cachedDNSInfo) { fixed (byte* pin_instanceName = &instanceName[0]) { @@ -808,6 +811,7 @@ internal static unsafe uint SNIOpenSyncEx(ConsumerInfo consumerInfo, string cons }; clientConsumerInfo.totalTimeout = totalTimeout; + clientConsumerInfo.ipAddressPreference = ipPreference; clientConsumerInfo.DNSCacheInfo.wszCachedFQDN = cachedDNSInfo?.FQDN; clientConsumerInfo.DNSCacheInfo.wszCachedTcpIPv4 = cachedDNSInfo?.AddrIPv4; clientConsumerInfo.DNSCacheInfo.wszCachedTcpIPv6 = cachedDNSInfo?.AddrIPv6; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs index f734ced9f8..3c9a6a4a1a 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -588,6 +588,14 @@ internal SqlConnectionAttestationProtocol AttestationProtocol } } + /// + /// Get IP address preference + /// + internal SqlConnectionIPAddressPreference iPAddressPreference + { + get => ((SqlConnectionString)ConnectionOptions).IPAddressPreference; + } + // Is this connection is a Context Connection? private bool UsesContextConnection(SqlConnectionString opt) { diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionString.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionString.cs index 2b3ffa9d70..761ab74751 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionString.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionString.cs @@ -58,6 +58,7 @@ internal static class DEFAULT internal static readonly SqlConnectionColumnEncryptionSetting ColumnEncryptionSetting = SqlConnectionColumnEncryptionSetting.Disabled; internal const string EnclaveAttestationUrl = _emptyString; internal static readonly SqlConnectionAttestationProtocol AttestationProtocol = SqlConnectionAttestationProtocol.NotSpecified; + internal static readonly SqlConnectionIPAddressPreference s_IPAddressPreference = SqlConnectionIPAddressPreference.IPv4First; #if ADONET_CERT_AUTH internal const string Certificate = _emptyString; @@ -76,6 +77,7 @@ internal static class KEY internal const string ColumnEncryptionSetting = "column encryption setting"; internal const string EnclaveAttestationUrl = "enclave attestation url"; internal const string AttestationProtocol = "attestation protocol"; + internal const string IPAddressPreference = "ip address preference"; internal const string Connect_Timeout = "connect timeout"; internal const string Command_Timeout = "command timeout"; internal const string Connection_Reset = "connection reset"; @@ -118,6 +120,8 @@ internal static class KEY private static class SYNONYM { + // ip address preference + internal const string IPADDRESSPREFERENCE = "ipaddresspreference"; // application intent internal const string APPLICATIONINTENT = "applicationintent"; // application name @@ -172,7 +176,7 @@ private static class SYNONYM // make sure to update SynonymCount value below when adding or removing synonyms } - internal const int SynonymCount = 29; + internal const int SynonymCount = 30; // the following are all inserted as keys into the _netlibMapping hash internal static class NETLIB @@ -239,6 +243,7 @@ internal static class TRANSACIONBINDING private readonly SqlConnectionColumnEncryptionSetting _columnEncryptionSetting; private readonly string _enclaveAttestationUrl; private readonly SqlConnectionAttestationProtocol _attestationProtocol; + private readonly SqlConnectionIPAddressPreference _ipAddressPreference; private readonly int _commandTimeout; private readonly int _connectTimeout; @@ -325,6 +330,7 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G _columnEncryptionSetting = ConvertValueToColumnEncryptionSetting(); _enclaveAttestationUrl = ConvertValueToString(KEY.EnclaveAttestationUrl, DEFAULT.EnclaveAttestationUrl); _attestationProtocol = ConvertValueToAttestationProtocol(); + _ipAddressPreference = ConvertValueToIPAddressPreference(); #if ADONET_CERT_AUTH _certificate = ConvertValueToString(KEY.Certificate, DEFAULT.Certificate); @@ -682,6 +688,7 @@ internal SqlConnectionString(SqlConnectionString connectionOptions, string dataS internal SqlConnectionColumnEncryptionSetting ColumnEncryptionSetting { get { return _columnEncryptionSetting; } } internal string EnclaveAttestationUrl { get { return _enclaveAttestationUrl; } } internal SqlConnectionAttestationProtocol AttestationProtocol { get { return _attestationProtocol; } } + internal SqlConnectionIPAddressPreference IPAddressPreference => _ipAddressPreference; #if ADONET_CERT_AUTH internal string Certificate { get { return _certificate; } } internal bool UsesCertificate { get { return _authType == SqlClient.SqlAuthenticationMethod.SqlCertificate; } } @@ -822,6 +829,7 @@ internal static Hashtable GetParseSynonyms() hash.Add(KEY.Connect_Retry_Count, KEY.Connect_Retry_Count); hash.Add(KEY.Connect_Retry_Interval, KEY.Connect_Retry_Interval); hash.Add(KEY.Authentication, KEY.Authentication); + hash.Add(KEY.IPAddressPreference, KEY.IPAddressPreference); #if ADONET_CERT_AUTH hash.Add(KEY.Certificate, KEY.Certificate); #endif @@ -854,6 +862,7 @@ internal static Hashtable GetParseSynonyms() hash.Add(SYNONYM.UID, KEY.User_ID); hash.Add(SYNONYM.User, KEY.User_ID); hash.Add(SYNONYM.WSID, KEY.Workstation_Id); + hash.Add(SYNONYM.IPADDRESSPREFERENCE, KEY.IPAddressPreference); Debug.Assert(SqlConnectionStringBuilder.KeywordsCount + SynonymCount == hash.Count, "incorrect initial ParseSynonyms size"); _sqlClientSynonyms = hash; } @@ -1077,6 +1086,34 @@ internal SqlConnectionAttestationProtocol ConvertValueToAttestationProtocol() } } + /// + /// Convert the value to SqlConnectionIPAddressPreference + /// + /// + internal SqlConnectionIPAddressPreference ConvertValueToIPAddressPreference() + { + object value = base.Parsetable[KEY.IPAddressPreference]; + + string valStr = value as string; + if (valStr == null) + { + return DEFAULT.s_IPAddressPreference; + } + + try + { + return DbConnectionStringBuilderUtil.ConvertToIPAddressPreference(KEY.IPAddressPreference, valStr); + } + catch (FormatException e) + { + throw ADP.InvalidConnectionOptionValue(KEY.IPAddressPreference, e); + } + catch (OverflowException e) + { + throw ADP.InvalidConnectionOptionValue(KEY.IPAddressPreference, e); + } + } + internal bool ConvertValueToEncrypt() { // If the Authentication keyword is provided, default to Encrypt=true; @@ -1087,4 +1124,3 @@ internal bool ConvertValueToEncrypt() } } } - diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs index 17d62e41bd..15590fe981 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs @@ -65,6 +65,7 @@ private enum Keywords AttestationProtocol, CommandTimeout, + IPAddressPreference, #if ADONET_CERT_AUTH Certificate, @@ -118,6 +119,7 @@ private enum Keywords private SqlConnectionColumnEncryptionSetting _columnEncryptionSetting = DbConnectionStringDefaults.ColumnEncryptionSetting; private string _enclaveAttestationUrl = DbConnectionStringDefaults.EnclaveAttestationUrl; private SqlConnectionAttestationProtocol _attestationProtocol = DbConnectionStringDefaults.AttestationProtocol; + private SqlConnectionIPAddressPreference _ipAddressPreference = DbConnectionStringDefaults.IPAddressPreference; private PoolBlockingPeriod _poolBlockingPeriod = DbConnectionStringDefaults.PoolBlockingPeriod; #if ADONET_CERT_AUTH @@ -168,6 +170,7 @@ static SqlConnectionStringBuilder() validKeywords[(int)Keywords.ColumnEncryptionSetting] = DbConnectionStringKeywords.ColumnEncryptionSetting; validKeywords[(int)Keywords.EnclaveAttestationUrl] = DbConnectionStringKeywords.EnclaveAttestationUrl; validKeywords[(int)Keywords.AttestationProtocol] = DbConnectionStringKeywords.AttestationProtocol; + validKeywords[(int)Keywords.IPAddressPreference] = DbConnectionStringKeywords.IPAddressPreference; #if ADONET_CERT_AUTH validKeywords[(int)Keywords.Certificate] = DbConnectionStringKeywords.Certificate; #endif @@ -215,9 +218,11 @@ static SqlConnectionStringBuilder() hash.Add(DbConnectionStringKeywords.ColumnEncryptionSetting, Keywords.ColumnEncryptionSetting); hash.Add(DbConnectionStringKeywords.EnclaveAttestationUrl, Keywords.EnclaveAttestationUrl); hash.Add(DbConnectionStringKeywords.AttestationProtocol, Keywords.AttestationProtocol); + hash.Add(DbConnectionStringKeywords.IPAddressPreference, Keywords.IPAddressPreference); #if ADONET_CERT_AUTH hash.Add(DbConnectionStringKeywords.Certificate, Keywords.Certificate); #endif + hash.Add(DbConnectionStringSynonyms.IPADDRESSPREFERENCE, Keywords.IPAddressPreference); hash.Add(DbConnectionStringSynonyms.APP, Keywords.ApplicationName); hash.Add(DbConnectionStringSynonyms.APPLICATIONINTENT, Keywords.ApplicationIntent); hash.Add(DbConnectionStringSynonyms.Async, Keywords.AsynchronousProcessing); @@ -357,6 +362,9 @@ public override object this[string keyword] case Keywords.AttestationProtocol: AttestationProtocol = ConvertToAttestationProtocol(keyword, value); break; + case Keywords.IPAddressPreference: + IPAddressPreference = ConvertToIPAddressPreference(keyword, value); + break; #if ADONET_CERT_AUTH case Keywords.Certificate: Certificate = ConvertToString(value); @@ -688,6 +696,26 @@ public SqlConnectionAttestationProtocol AttestationProtocol } } + /// + [DisplayName(DbConnectionStringKeywords.IPAddressPreference)] + [ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Security)] + [ResDescriptionAttribute(StringsHelper.ResourceNames.TCE_DbConnectionString_IPAddressPreference)] + [RefreshPropertiesAttribute(RefreshProperties.All)] + public SqlConnectionIPAddressPreference IPAddressPreference + { + get => _ipAddressPreference; + set + { + if (!DbConnectionStringBuilderUtil.IsValidIPAddressPreference(value)) + { + throw ADP.InvalidEnumerationValue(typeof(SqlConnectionIPAddressPreference), (int)value); + } + + SetIPAddressPreferenceValue(value); + _ipAddressPreference = value; + } + } + /// [DisplayName(DbConnectionStringKeywords.TrustServerCertificate)] [ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Security)] @@ -1267,6 +1295,14 @@ private static SqlConnectionAttestationProtocol ConvertToAttestationProtocol(str return DbConnectionStringBuilderUtil.ConvertToAttestationProtocol(keyword, value); } + /// + /// Convert to SqlConnectionIPAddressPreference + /// + /// + /// + private static SqlConnectionIPAddressPreference ConvertToIPAddressPreference(string keyword, object value) + => DbConnectionStringBuilderUtil.ConvertToIPAddressPreference(keyword, value); + private object GetAt(Keywords index) { switch (index) @@ -1357,6 +1393,8 @@ private object GetAt(Keywords index) return EnclaveAttestationUrl; case Keywords.AttestationProtocol: return AttestationProtocol; + case Keywords.IPAddressPreference: + return IPAddressPreference; #if ADONET_CERT_AUTH case Keywords.Certificate: return Certificate; #endif @@ -1552,6 +1590,9 @@ private void Reset(Keywords index) case Keywords.AttestationProtocol: _attestationProtocol = DbConnectionStringDefaults.AttestationProtocol; break; + case Keywords.IPAddressPreference: + _ipAddressPreference = DbConnectionStringDefaults.IPAddressPreference; + break; default: Debug.Fail("unexpected keyword"); throw ADP.KeywordNotSupported(_validKeywords[(int)index]); @@ -1598,6 +1639,12 @@ private void SetAttestationProtocolValue(SqlConnectionAttestationProtocol value) base[DbConnectionStringKeywords.AttestationProtocol] = DbConnectionStringBuilderUtil.AttestationProtocolToString(value); } + private void SetIPAddressPreferenceValue(SqlConnectionIPAddressPreference value) + { + Debug.Assert(DbConnectionStringBuilderUtil.IsValidIPAddressPreference(value), "Invalid value for SqlConnectionIPAddressPreference"); + base[DbConnectionStringKeywords.IPAddressPreference] = DbConnectionStringBuilderUtil.IPAddressPreferenceToString(value); + } + /// public override bool ShouldSerialize(string keyword) @@ -1923,4 +1970,3 @@ private System.ComponentModel.Design.Serialization.InstanceDescriptor ConvertToI } } - diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs index 5e422fef74..4ac0a23fa4 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -1076,6 +1076,19 @@ public enum SqlConnectionAttestationProtocol HGS = 3 } + /// + public enum SqlConnectionIPAddressPreference + { + /// + IPv4First = 0, // default + + /// + IPv6First = 1, + + /// + UsePlatformDefault = 2 + } + /// public enum SqlAuthenticationMethod { diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index 6b4d322c1f..78015db428 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -593,7 +593,7 @@ internal void Connect(ServerInfo serverInfo, } _physicalStateObj.CreatePhysicalSNIHandle(serverInfo.ExtendedServerName, ignoreSniOpenTimeout, timerExpire, - out instanceName, _sniSpnBuffer, false, true, fParallel, transparentNetworkResolutionState, totalTimeout, FQDNforDNSCahce); + out instanceName, _sniSpnBuffer, false, true, fParallel, transparentNetworkResolutionState, totalTimeout, _connHandler.ConnectionOptions.IPAddressPreference, FQDNforDNSCahce); if (TdsEnums.SNI_SUCCESS != _physicalStateObj.Status) { @@ -656,7 +656,8 @@ internal void Connect(ServerInfo serverInfo, // On Instance failure re-connect and flush SNI named instance cache. _physicalStateObj.SniContext = SniContext.Snix_Connect; - _physicalStateObj.CreatePhysicalSNIHandle(serverInfo.ExtendedServerName, ignoreSniOpenTimeout, timerExpire, out instanceName, _sniSpnBuffer, true, true, fParallel, transparentNetworkResolutionState, totalTimeout, serverInfo.ResolvedServerName); + _physicalStateObj.CreatePhysicalSNIHandle(serverInfo.ExtendedServerName, ignoreSniOpenTimeout, timerExpire, + out instanceName, _sniSpnBuffer, true, true, fParallel, transparentNetworkResolutionState, totalTimeout, _connHandler.ConnectionOptions.IPAddressPreference, serverInfo.ResolvedServerName); if (TdsEnums.SNI_SUCCESS != _physicalStateObj.Status) { diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserSafeHandles.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserSafeHandles.cs index 30e874995c..b61ed1dd34 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserSafeHandles.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserSafeHandles.cs @@ -150,6 +150,7 @@ internal SNIHandle( bool fParallel, TransparentNetworkResolutionState transparentNetworkResolutionState, int totalTimeout, + SqlConnectionIPAddressPreference ipPreference, SQLDNSInfo cachedDNSInfo) : base(IntPtr.Zero, true) { @@ -172,19 +173,19 @@ internal SNIHandle( int transparentNetworkResolutionStateNo = (int)transparentNetworkResolutionState; _status = SNINativeMethodWrapper.SNIOpenSyncEx(myInfo, serverName, ref base.handle, spnBuffer, instanceName, flushCache, fSync, timeout, fParallel, transparentNetworkResolutionStateNo, totalTimeout, - ADP.IsAzureSqlServerEndpoint(serverName), cachedDNSInfo); + ADP.IsAzureSqlServerEndpoint(serverName), ipPreference, cachedDNSInfo); } } // constructs SNI Handle for MARS session - internal SNIHandle(SNINativeMethodWrapper.ConsumerInfo myInfo, SNIHandle parent, SQLDNSInfo cachedDNSInfo) : base(IntPtr.Zero, true) + internal SNIHandle(SNINativeMethodWrapper.ConsumerInfo myInfo, SNIHandle parent, SqlConnectionIPAddressPreference ipPreference, SQLDNSInfo cachedDNSInfo) : base(IntPtr.Zero, true) { RuntimeHelpers.PrepareConstrainedRegions(); try { } finally { - _status = SNINativeMethodWrapper.SNIOpenMarsSession(myInfo, parent, ref base.handle, parent._fSync, cachedDNSInfo); + _status = SNINativeMethodWrapper.SNIOpenMarsSession(myInfo, parent, ref base.handle, parent._fSync, ipPreference, cachedDNSInfo); } } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs index ebd75aaefa..9453f6102a 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs @@ -326,7 +326,7 @@ internal TdsParserStateObject(TdsParser parser, SNIHandle physicalConnection, bo SQLDNSInfo cachedDNSInfo; bool ret = SQLFallbackDNSCache.Instance.GetDNSInfo(_parser.FQDNforDNSCahce, out cachedDNSInfo); - _sessionHandle = new SNIHandle(myInfo, physicalConnection, cachedDNSInfo); + _sessionHandle = new SNIHandle(myInfo, physicalConnection, _parser.Connection.ConnectionOptions.IPAddressPreference, cachedDNSInfo); if (_sessionHandle.Status != TdsEnums.SNI_SUCCESS) { AddError(parser.ProcessSNIError(this)); @@ -852,7 +852,8 @@ private SNINativeMethodWrapper.ConsumerInfo CreateConsumerInfo(bool async) return myInfo; } - internal void CreatePhysicalSNIHandle(string serverName, bool ignoreSniOpenTimeout, long timerExpire, out byte[] instanceName, byte[] spnBuffer, bool flushCache, bool async, bool fParallel, TransparentNetworkResolutionState transparentNetworkResolutionState, int totalTimeout, string cachedFQDN) + internal void CreatePhysicalSNIHandle(string serverName, bool ignoreSniOpenTimeout, long timerExpire, out byte[] instanceName, byte[] spnBuffer, bool flushCache, + bool async, bool fParallel, TransparentNetworkResolutionState transparentNetworkResolutionState, int totalTimeout, SqlConnectionIPAddressPreference ipPreference, string cachedFQDN) { SNINativeMethodWrapper.ConsumerInfo myInfo = CreateConsumerInfo(async); @@ -880,7 +881,8 @@ internal void CreatePhysicalSNIHandle(string serverName, bool ignoreSniOpenTimeo SQLDNSInfo cachedDNSInfo; bool ret = SQLFallbackDNSCache.Instance.GetDNSInfo(cachedFQDN, out cachedDNSInfo); - _sessionHandle = new SNIHandle(myInfo, serverName, spnBuffer, ignoreSniOpenTimeout, checked((int)timeout), out instanceName, flushCache, !async, fParallel, transparentNetworkResolutionState, totalTimeout, cachedDNSInfo); + _sessionHandle = new SNIHandle(myInfo, serverName, spnBuffer, ignoreSniOpenTimeout, checked((int)timeout), + out instanceName, flushCache, !async, fParallel, transparentNetworkResolutionState, totalTimeout, ipPreference, cachedDNSInfo); } internal bool Deactivate() diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs index 925c38901d..f5b5f44a4c 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs @@ -12077,6 +12077,12 @@ internal static string TCE_DbConnectionString_AttestationProtocol { return ResourceManager.GetString("TCE_DbConnectionString_AttestationProtocol", resourceCulture); } } + + /// + /// Looks up a localized string similar to Specifies an IP address preference when connecting to SQL instances. + /// + internal static string TCE_DbConnectionString_IPAddressPreference + => ResourceManager.GetString("TCE_DbConnectionString_IPAddressPreference", resourceCulture); /// /// Looks up a localized string similar to Default column encryption setting for all the commands on the connection.. diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx index 1ffcff4d0b..7ca22b2fe8 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx @@ -4524,6 +4524,9 @@ Specifies an attestation protocol for its corresponding enclave attestation service. + + Specifies an IP address preference when connecting to SQL instances. + The enclave type '{0}' returned from the server is not supported. diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SQLFallbackDNSCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SQLFallbackDNSCache.cs index e18b61cee4..9d4136d01f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SQLFallbackDNSCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SQLFallbackDNSCache.cs @@ -7,7 +7,7 @@ namespace Microsoft.Data.SqlClient { - internal class SQLFallbackDNSCache + internal sealed class SQLFallbackDNSCache { private static readonly SQLFallbackDNSCache _SQLFallbackDNSCache = new SQLFallbackDNSCache(); private static readonly int initialCapacity = 101; // give some prime number here according to MSDN docs. It will be resized if reached capacity. @@ -68,7 +68,7 @@ internal bool IsDuplicate(SQLDNSInfo newItem) } } - internal class SQLDNSInfo + internal sealed class SQLDNSInfo { public string FQDN { get; set; } public string AddrIPv4 { get; set; } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs index 9796b297c2..4a61e10edd 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs @@ -55,6 +55,9 @@ public partial class SqlConnectionStringBuilderTest [InlineData("Initial Catalog = Northwind; Failover Partner = randomserver.sys.local")] [InlineData("Initial Catalog = tempdb")] [InlineData("Integrated Security = true")] + [InlineData("IPAddressPreference = IPv4First")] + [InlineData("IPAddressPreference = IPv6First")] + [InlineData("IPAddressPreference = UsePlatformDefault")] [InlineData("Trusted_Connection = false")] [InlineData("Max Pool Size = 50")] [InlineData("Min Pool Size = 20")] diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionTest.cs index 9289f23768..b3afc51527 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionTest.cs @@ -11,7 +11,7 @@ namespace Microsoft.Data.SqlClient.Tests { public partial class SqlConnectionTest { - private static readonly string[] s_retrieveInternalInfoKeys = + private static readonly string[] s_retrieveInternalInfoKeys = { "SQLDNSCachingSupportedState", "SQLDNSCachingSupportedStateBeforeRedirect" @@ -53,7 +53,7 @@ public void Constructor2() Assert.Null(cn.Site); Assert.Equal(ConnectionState.Closed, cn.State); Assert.False(cn.StatisticsEnabled); - Assert.True(string.Compare (Environment.MachineName, cn.WorkstationId, true) == 0); + Assert.True(string.Compare(Environment.MachineName, cn.WorkstationId, true) == 0); cn = new SqlConnection((string)null); Assert.Equal(string.Empty, cn.ConnectionString); @@ -67,7 +67,7 @@ public void Constructor2() Assert.Null(cn.Site); Assert.Equal(ConnectionState.Closed, cn.State); Assert.False(cn.StatisticsEnabled); - Assert.True(string.Compare (Environment.MachineName, cn.WorkstationId, true) == 0); + Assert.True(string.Compare(Environment.MachineName, cn.WorkstationId, true) == 0); } [Fact] @@ -107,7 +107,7 @@ public void Constructor2_ConnectionString_Invalid() try { new SqlConnection("Packet Size=511"); - } + } catch (ArgumentException ex) { // Invalid 'Packet Size'. The value must be an @@ -1326,7 +1326,7 @@ public void RetrieveInternalInfo_ExpectedKeysInDictionary_Success() Assert.NotEmpty(d.Values); Assert.Equal(s_retrieveInternalInfoKeys.Length, d.Values.Count); - foreach(string key in s_retrieveInternalInfoKeys) + foreach (string key in s_retrieveInternalInfoKeys) { Assert.True(d.ContainsKey(key)); @@ -1343,5 +1343,48 @@ public void RetrieveInternalInfo_UnexpectedKeysInDictionary_Success() IDictionary d = cn.RetrieveInternalInfo(); Assert.False(d.ContainsKey("Foo")); } + + [Fact] + public void ConnectionString_IPAddressPreference() + { + SqlConnection cn = new SqlConnection(); + cn.ConnectionString = "IPAddressPreference=IPv4First"; + cn.ConnectionString = "IPAddressPreference=IPV4FIRST"; + cn.ConnectionString = "IPAddressPreference=ipv4first"; + cn.ConnectionString = "IPAddressPreference=iPv4FirSt"; + cn.ConnectionString = "IPAddressPreference=IPv6First"; + cn.ConnectionString = "IPAddressPreference=IPV6FIRST"; + cn.ConnectionString = "IPAddressPreference=ipv6first"; + cn.ConnectionString = "IPAddressPreference=iPv6FirST"; + cn.ConnectionString = "IPAddressPreference=UsePlatformDefault"; + cn.ConnectionString = "IPAddressPreference=USEPLATFORMDEFAULT"; + cn.ConnectionString = "IPAddressPreference=useplatformdefault"; + cn.ConnectionString = "IPAddressPreference=usePlAtFormdeFault"; + } + + [Theory] + [InlineData("IPAddressPreference=-1")] + [InlineData("IPAddressPreference=0")] + [InlineData("IPAddressPreference=!@#")] + [InlineData("IPAddressPreference=ABC")] + [InlineData("IPAddressPreference=ipv6")] + public void ConnectionString_IPAddressPreference_Invalid(string value) + { + SqlConnection cn = new SqlConnection(); + try + { + cn.ConnectionString = value; + Assert.True(false, $"It mustn't come to this line; Value '{value}' should be invalid."); + } + catch (ArgumentException ex) + { + // Invalid value for key 'ip address preference' + Assert.Equal(typeof(ArgumentException), ex.GetType()); + Assert.Null(ex.InnerException); + Assert.NotNull(ex.Message); + Assert.Contains("'ip address preference'", ex.Message); + Assert.Null(ex.ParamName); + } + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index a97a16a05d..2aefb3fe36 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -9,6 +9,9 @@ using System.Diagnostics.Tracing; using System.Globalization; using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; using System.Security; using System.Threading; using System.Threading.Tasks; @@ -343,6 +346,20 @@ public static bool IsTCPConnectionStringPasswordIncluded() return RetrieveValueFromConnStr(TCPConnectionString, new string[] { "Password", "PWD" }) != string.Empty; } + public static bool DoesHostAddressContainBothIPv4AndIPv6() + { + if (!IsDNSCachingSetup()) + { + return false; + } + using (var connection = new SqlConnection(DNSCachingConnString)) + { + List ipAddresses = Dns.GetHostAddresses(connection.DataSource).ToList(); + return ipAddresses.Exists(ip => ip.AddressFamily == AddressFamily.InterNetwork) && + ipAddresses.Exists(ip => ip.AddressFamily == AddressFamily.InterNetworkV6); + } + } + /// /// Generate a unique name to use in Sql Server; /// some providers does not support names (Oracle supports up to 30). diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 0878e72f74..0a1946d7cc 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -70,6 +70,7 @@ Common\System\Collections\DictionaryExtensions.cs + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs index 2e2a1ca022..e71d6d62f6 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs @@ -28,18 +28,14 @@ public void CancelAsyncConnections() { SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString); builder.MultipleActiveResultSets = false; - RunCancelAsyncConnections(builder, false); - RunCancelAsyncConnections(builder, true); + RunCancelAsyncConnections(builder); builder.MultipleActiveResultSets = true; - RunCancelAsyncConnections(builder, false); - RunCancelAsyncConnections(builder, true); + RunCancelAsyncConnections(builder); } - private void RunCancelAsyncConnections(SqlConnectionStringBuilder connectionStringBuilder, bool makeAsyncBlocking) + private void RunCancelAsyncConnections(SqlConnectionStringBuilder connectionStringBuilder) { SqlConnection.ClearAllPools(); - AppContext.SetSwitch("Switch.Microsoft.Data.SqlClient.MakeReadAsyncBlocking", makeAsyncBlocking); - _watch = Stopwatch.StartNew(); _random = new Random(4); // chosen via fair dice role. ParallelLoopResult results = new ParallelLoopResult(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/ConnectionHelper.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/ConnectionHelper.cs index 54561e1be9..32bac50d08 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/ConnectionHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/ConnectionHelper.cs @@ -17,6 +17,7 @@ internal static class ConnectionHelper private static Type s_dbConnectionInternal = s_MicrosoftDotData.GetType("Microsoft.Data.ProviderBase.DbConnectionInternal"); private static Type s_tdsParser = s_MicrosoftDotData.GetType("Microsoft.Data.SqlClient.TdsParser"); private static Type s_tdsParserStateObject = s_MicrosoftDotData.GetType("Microsoft.Data.SqlClient.TdsParserStateObject"); + private static Type s_SQLDNSInfo = s_MicrosoftDotData.GetType("Microsoft.Data.SqlClient.SQLDNSInfo"); private static PropertyInfo s_sqlConnectionInternalConnection = s_sqlConnection.GetProperty("InnerConnection", BindingFlags.Instance | BindingFlags.NonPublic); private static PropertyInfo s_dbConnectionInternalPool = s_dbConnectionInternal.GetProperty("Pool", BindingFlags.Instance | BindingFlags.NonPublic); private static MethodInfo s_dbConnectionInternalIsConnectionAlive = s_dbConnectionInternal.GetMethod("IsConnectionAlive", BindingFlags.Instance | BindingFlags.NonPublic); @@ -26,6 +27,11 @@ internal static class ConnectionHelper private static FieldInfo s_tdsParserStateObjectProperty = s_tdsParser.GetField("_physicalStateObj", BindingFlags.Instance | BindingFlags.NonPublic); private static FieldInfo s_enforceTimeoutDelayProperty = s_tdsParserStateObject.GetField("_enforceTimeoutDelay", BindingFlags.Instance | BindingFlags.NonPublic); private static FieldInfo s_enforcedTimeoutDelayInMilliSeconds = s_tdsParserStateObject.GetField("_enforcedTimeoutDelayInMilliSeconds", BindingFlags.Instance | BindingFlags.NonPublic); + private static FieldInfo s_pendingSQLDNSObject = s_sqlInternalConnectionTds.GetField("pendingSQLDNSObject", BindingFlags.Instance | BindingFlags.NonPublic); + private static PropertyInfo s_pendingSQLDNS_FQDN = s_SQLDNSInfo.GetProperty("FQDN", BindingFlags.Instance | BindingFlags.Public); + private static PropertyInfo s_pendingSQLDNS_AddrIPv4 = s_SQLDNSInfo.GetProperty("AddrIPv4", BindingFlags.Instance | BindingFlags.Public); + private static PropertyInfo s_pendingSQLDNS_AddrIPv6 = s_SQLDNSInfo.GetProperty("AddrIPv6", BindingFlags.Instance | BindingFlags.Public); + private static PropertyInfo s_pendingSQLDNS_Port = s_SQLDNSInfo.GetProperty("Port", BindingFlags.Instance | BindingFlags.Public); public static object GetConnectionPool(object internalConnection) { @@ -79,5 +85,22 @@ public static void SetEnforcedTimeout(this SqlConnection connection, bool enforc s_enforceTimeoutDelayProperty.SetValue(stateObj, enforce); s_enforcedTimeoutDelayInMilliSeconds.SetValue(stateObj, timeout); } + + /// + /// Resolve the established socket end point information for TCP protocol. + /// + /// Active connection to extract the requested data + /// FQDN, AddrIPv4, AddrIPv6, and Port in sequence + public static Tuple GetSQLDNSInfo(this SqlConnection connection) + { + object internalConnection = GetInternalConnection(connection); + VerifyObjectIsInternalConnection(internalConnection); + object pendingSQLDNSInfo = s_pendingSQLDNSObject.GetValue(internalConnection); + string fqdn = s_pendingSQLDNS_FQDN.GetValue(pendingSQLDNSInfo) as string; + string ipv4 = s_pendingSQLDNS_AddrIPv4.GetValue(pendingSQLDNSInfo) as string; + string ipv6 = s_pendingSQLDNS_AddrIPv6.GetValue(pendingSQLDNSInfo) as string; + string port = s_pendingSQLDNS_Port.GetValue(pendingSQLDNSInfo) as string; + return new Tuple(fqdn, ipv4, ipv6, port); + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConfigurableIpPreferenceTest/ConfigurableIpPreferenceTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConfigurableIpPreferenceTest/ConfigurableIpPreferenceTest.cs new file mode 100644 index 0000000000..8003660889 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConfigurableIpPreferenceTest/ConfigurableIpPreferenceTest.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using Microsoft.Data.SqlClient.ManualTesting.Tests.SystemDataInternals; +using Xunit; + +using static Microsoft.Data.SqlClient.ManualTesting.Tests.DataTestUtility; +using static Microsoft.Data.SqlClient.ManualTesting.Tests.DNSCachingTest; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests +{ + public class ConfigurableIpPreferenceTest + { + private const string CnnPrefIPv6 = ";IPAddressPreference=IPv6First"; + private const string CnnPrefIPv4 = ";IPAddressPreference=IPv4First"; + + private static bool IsTCPConnectionStringSetup() => !string.IsNullOrEmpty(TCPConnectionString); + private static bool IsValidDataSource() + { + SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(TCPConnectionString); + int startIdx = builder.DataSource.IndexOf(':') + 1; + int endIdx = builder.DataSource.IndexOf(','); + string serverName; + if (endIdx == -1) + { + serverName = builder.DataSource.Substring(startIdx); + } + else + { + serverName = builder.DataSource.Substring(startIdx, endIdx - startIdx); + } + + List ipAddresses = Dns.GetHostAddresses(serverName).ToList(); + return ipAddresses.Exists(ip => ip.AddressFamily == AddressFamily.InterNetwork) && + ipAddresses.Exists(ip => ip.AddressFamily == AddressFamily.InterNetworkV6); + } + + [ConditionalTheory(nameof(IsTCPConnectionStringSetup), nameof(IsValidDataSource))] + [InlineData(CnnPrefIPv6)] + [InlineData(CnnPrefIPv4)] + [InlineData(";IPAddressPreference=UsePlatformDefault")] + public void ConfigurableIpPreference(string ipPreference) + { + using (SqlConnection connection = new SqlConnection(TCPConnectionString + ipPreference +#if NETFRAMEWORK + + ";TransparentNetworkIPResolution=false" // doesn't support in .NET Core +#endif + )) + { + connection.Open(); + Assert.Equal(ConnectionState.Open, connection.State); + Tuple DNSInfo = connection.GetSQLDNSInfo(); + if(ipPreference == CnnPrefIPv4) + { + Assert.NotNull(DNSInfo.Item2); //IPv4 + Assert.Null(DNSInfo.Item3); //IPv6 + } + else if(ipPreference == CnnPrefIPv6) + { + Assert.Null(DNSInfo.Item2); + Assert.NotNull(DNSInfo.Item3); + } + else + { + Assert.True((DNSInfo.Item2 != null && DNSInfo.Item3 == null) || (DNSInfo.Item2 == null && DNSInfo.Item3 != null)); + } + } + } + + // Azure SQL Server doesn't support dual-stack IPv4 and IPv6 that is going to be supported by end of 2021. + [ConditionalTheory(typeof(DataTestUtility), nameof(DoesHostAddressContainBothIPv4AndIPv6), nameof(IsUsingManagedSNI))] + [InlineData(CnnPrefIPv6)] + [InlineData(CnnPrefIPv4)] + public void ConfigurableIpPreferenceManagedSni(string ipPreference) + => TestCachedConfigurableIpPreference(ipPreference, DNSCachingConnString); + + private void TestCachedConfigurableIpPreference(string ipPreference, string cnnString) + { + using (SqlConnection connection = new SqlConnection(cnnString + ipPreference)) + { + // each successful connection updates the dns cache entry for the data source + connection.Open(); + var SQLFallbackDNSCacheInstance = GetDnsCache(); + + // get the dns cache entry with the given key. parameters[1] will be initialized as the entry + object[] parameters = new object[] { connection.DataSource, null }; + SQLFallbackDNSCacheGetDNSInfo.Invoke(SQLFallbackDNSCacheInstance, parameters); + var dnsCacheEntry = parameters[1]; + + const string AddrIPv4Property = "AddrIPv4"; + const string AddrIPv6Property = "AddrIPv6"; + const string FQDNProperty = "FQDN"; + + Assert.NotNull(dnsCacheEntry); + Assert.Equal(connection.DataSource, GetPropertyValueFromCacheEntry(FQDNProperty, dnsCacheEntry)); + + if (ipPreference == CnnPrefIPv4) + { + Assert.NotNull(GetPropertyValueFromCacheEntry(AddrIPv4Property, dnsCacheEntry)); + Assert.Null(GetPropertyValueFromCacheEntry(AddrIPv6Property, dnsCacheEntry)); + } + else if (ipPreference == CnnPrefIPv6) + { + string ipv6 = GetPropertyValueFromCacheEntry(AddrIPv6Property, dnsCacheEntry); + Assert.NotNull(ipv6); + Assert.Null(GetPropertyValueFromCacheEntry(AddrIPv4Property, dnsCacheEntry)); + } + } + + object GetDnsCache() => + SQLFallbackDNSCacheType.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public).GetValue(null); + + string GetPropertyValueFromCacheEntry(string property, object dnsCacheEntry) => + (string)SQLDNSInfoType.GetProperty(property).GetValue(dnsCacheEntry); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DNSCachingTest/DNSCachingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DNSCachingTest/DNSCachingTest.cs index 33460acb8d..a23efff073 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DNSCachingTest/DNSCachingTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DNSCachingTest/DNSCachingTest.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Reflection; using Xunit; diff --git a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json index 4c725d8555..6c631187b5 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json +++ b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json @@ -18,6 +18,11 @@ "SupportsLocalDb": false, "SupportsFileStream": false, "UseManagedSNIOnWindows": false, + "DNSCachingConnString": "", + "DNSCachingServerCR": "", + "DNSCachingServerTR": "", + "IsDNSCachingSupportedCR": false, + "IsDNSCachingSupportedTR": false, "IsAzureSynapse": false, "EnclaveAzureDatabaseConnString": "", "UserManagedIdentityClientId": ""