Repository: microsoft/mssql-jdbc
Pull request #1766 (2022-03-10 16:12:57)

Add Configurable IPv6 Support
A new connection property IPAddressPreference is added to specify the IP preference by the client application when doing TCP connection. There are three valid types:
• IPv4First
This is the default preference. The driver will traverse IPv4 addresses first. If none of them can be connected successfully, it continues to try IPv6 addresses if there are any.
• IPv6First
The driver will traverse IPv6 addresses first. If none of them can be connected successfully, it continues to try IPv4 addresses if there are any.
• UsePlatformDefault
The driver will traverse all the IP addresses in their initial orders from DNS resolution.

tkyc: Hmm, for filtering of the addresses to either ipv6 or ipv4, possibly do it in one pass using 2 member lists instead of local lists? If it makes sense to do so with the implementation. Does the address list change often?

Patch:
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
index d242a44b..f05cee33 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
@@ -18,6 +18,8 @@ import java.io.UnsupportedEncodingException;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.math.RoundingMode;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Socket;
@@ -35,8 +37,6 @@ import java.nio.charset.Charset;
 import java.security.KeyStore;
 import java.security.Provider;
 import java.security.Security;
-import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
 import java.sql.Timestamp;
 import java.text.MessageFormat;
 import java.time.LocalDate;
@@ -45,11 +45,9 @@ import java.time.OffsetTime;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Calendar;
-import java.util.Collection;
 import java.util.GregorianCalendar;
 import java.util.Iterator;
 import java.util.LinkedList;
-import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -749,17 +747,21 @@ final class TDSChannel implements Serializable {
 
     /**
      * Opens the physical communications channel (TCP/IP socket and I/O streams) to the SQL Server.
+     * 
+     * @param iPAddressPreference
+     *        Preferred type of IP address to use first
      *
      * @return InetSocketAddress of the connection socket.
      */
     final InetSocketAddress open(String host, int port, int timeoutMillis, boolean useParallel, boolean useTnir,
-            boolean isTnirFirstAttempt, int timeoutMillisForFullTimeout) throws SQLServerException {
+            boolean isTnirFirstAttempt, int timeoutMillisForFullTimeout,
+            String iPAddressPreference) throws SQLServerException {
         if (logger.isLoggable(Level.FINER))
             logger.finer(this.toString() + ": Opening TCP socket...");
 
         SocketFinder socketFinder = new SocketFinder(traceID, con);
         channelSocket = tcpSocket = socketFinder.findSocket(host, port, timeoutMillis, useParallel, useTnir,
-                isTnirFirstAttempt, timeoutMillisForFullTimeout);
+                isTnirFirstAttempt, timeoutMillisForFullTimeout, iPAddressPreference);
         try {
             // Set socket options
             tcpSocket.setTcpNoDelay(true);
@@ -1538,274 +1540,6 @@ final class TDSChannel implements Serializable {
         }
     }
 
-    /**
-     * This class implements an X509TrustManager that always accepts the X509Certificate chain offered to it.
-     *
-     * A PermissiveX509TrustManager is used to "verify" the authenticity of the server when the trustServerCertificate
-     * connection property is set to true.
-     */
-    private final class PermissiveX509TrustManager implements X509TrustManager {
-        private final TDSChannel tdsChannel;
-        private final Logger logger;
-        private final String logContext;
-
-        PermissiveX509TrustManager(TDSChannel tdsChannel) {
-            this.tdsChannel = tdsChannel;
-            this.logger = tdsChannel.getLogger();
-            this.logContext = tdsChannel.toString() + " (PermissiveX509TrustManager):";
-        }
-
-        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
-            if (logger.isLoggable(Level.FINER))
-                logger.finer(logContext + " Trusting client certificate (!)");
-        }
-
-        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
-            if (logger.isLoggable(Level.FINER))
-                logger.finer(logContext + " Trusting server certificate");
-        }
-
-        public X509Certificate[] getAcceptedIssuers() {
-            return new X509Certificate[0];
-        }
-    }
-
-    /**
-     * This class implements an X509TrustManager that hostname for validation.
-     *
-     * This validates the subject name in the certificate with the host name
-     */
-    private final class HostNameOverrideX509TrustManager implements X509TrustManager {
-        private final Logger logger;
-        private final String logContext;
-        private final X509TrustManager defaultTrustManager;
-        private String hostName;
-
-        HostNameOverrideX509TrustManager(TDSChannel tdsChannel, X509TrustManager tm, String hostName) {
-            this.logger = tdsChannel.getLogger();
-            this.logContext = tdsChannel.toString() + " (HostNameOverrideX509TrustManager):";
-            defaultTrustManager = tm;
-            // canonical name is in lower case so convert this to lowercase too.
-            this.hostName = hostName.toLowerCase(Locale.ENGLISH);
-        }
-
-        // Parse name in RFC 2253 format
-        // Returns the common name if successful, null if failed to find the common name.
-        // The parser tuned to be safe than sorry so if it sees something it cant parse correctly it returns null
-        private String parseCommonName(String distinguishedName) {
-            int index;
-            // canonical name converts entire name to lowercase
-            index = distinguishedName.indexOf("cn=");
-            if (index == -1) {
-                return null;
-            }
-            distinguishedName = distinguishedName.substring(index + 3);
-            // Parse until a comma or end is reached
-            // Note the parser will handle gracefully (essentially will return empty string) , inside the quotes (e.g
-            // cn="Foo, bar") however
-            // RFC 952 says that the hostName cant have commas however the parser should not (and will not) crash if it
-            // sees a , within quotes.
-            for (index = 0; index < distinguishedName.length(); index++) {
-                if (distinguishedName.charAt(index) == ',') {
-                    break;
-                }
-            }
-            String commonName = distinguishedName.substring(0, index);
-            // strip any quotes
-            if (commonName.length() > 1 && ('\"' == commonName.charAt(0))) {
-                if ('\"' == commonName.charAt(commonName.length() - 1))
-                    commonName = commonName.substring(1, commonName.length() - 1);
-                else {
-                    // Be safe the name is not ended in " return null so the common Name wont match
-                    commonName = null;
-                }
-            }
-            return commonName;
-        }
-
-        private boolean validateServerName(String nameInCert) {
-            // Failed to get the common name from DN or empty CN
-            if (null == nameInCert) {
-                if (logger.isLoggable(Level.FINER)) {
-                    logger.finer(logContext + " Failed to parse the name from the certificate or name is empty.");
-                }
-                return false;
-            }
-            // We do not allow wildcards in IDNs (xn--).
-            if (!nameInCert.startsWith("xn--") && nameInCert.contains("*")) {
-                int hostIndex = 0, certIndex = 0, match = 0, startIndex = -1, periodCount = 0;
-                while (hostIndex < hostName.length()) {
-                    if ('.' == hostName.charAt(hostIndex)) {
-                        periodCount++;
-                    }
-                    if (certIndex < nameInCert.length() && hostName.charAt(hostIndex) == nameInCert.charAt(certIndex)) {
-                        hostIndex++;
-                        certIndex++;
-                    } else if (certIndex < nameInCert.length() && '*' == nameInCert.charAt(certIndex)) {
-                        startIndex = certIndex;
-                        match = hostIndex;
-                        certIndex++;
-                    } else if (startIndex != -1 && 0 == periodCount) {
-                        certIndex = startIndex + 1;
-                        match++;
-                        hostIndex = match;
-                    } else {
-                        logFailMessage(nameInCert);
-                        return false;
-                    }
-                }
-                if (nameInCert.length() == certIndex && periodCount > 1) {
-                    logSuccessMessage(nameInCert);
-                    return true;
-                } else {
-                    logFailMessage(nameInCert);
-                    return false;
-                }
-            }
-            // Verify that the name in certificate matches exactly with the host name
-            if (!nameInCert.equals(hostName)) {
-                logFailMessage(nameInCert);
-                return false;
-            }
-            logSuccessMessage(nameInCert);
-            return true;
-        }
-
-        private void logFailMessage(String nameInCert) {
-            if (logger.isLoggable(Level.FINER)) {
-                logger.finer(logContext + " The name in certificate " + nameInCert
-                        + " does not match with the server name " + hostName + ".");
-            }
-        }
-
-        private void logSuccessMessage(String nameInCert) {
-            if (logger.isLoggable(Level.FINER)) {
-                logger.finer(logContext + " The name in certificate:" + nameInCert + " validated against server name "
-                        + hostName + ".");
-            }
-        }
-
-        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
-            if (logger.isLoggable(Level.FINEST))
-                logger.finest(logContext + " Forwarding ClientTrusted.");
-            defaultTrustManager.checkClientTrusted(chain, authType);
-            // Explicitly validate the expiry dates
-            for (X509Certificate cert : chain) {
-                cert.checkValidity();
-            }
-        }
-
-        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
-            if (logger.isLoggable(Level.FINEST))
-                logger.finest(logContext + " Forwarding Trusting server certificate");
-            defaultTrustManager.checkServerTrusted(chain, authType);
-            // Explicitly validate the expiry dates
-            for (X509Certificate cert : chain) {
-                cert.checkValidity();
-            }
-            if (logger.isLoggable(Level.FINEST))
-                logger.finest(logContext + " default serverTrusted succeeded proceeding with server name validation");
-
-            validateServerNameInCertificate(chain[0]);
-        }
-
-        private void validateServerNameInCertificate(X509Certificate cert) throws CertificateException {
-            String nameInCertDN = cert.getSubjectX500Principal().getName("canonical");
-            if (logger.isLoggable(Level.FINER)) {
-                logger.finer(logContext + " Validating the server name:" + hostName);
-                logger.finer(logContext + " The DN name in certificate:" + nameInCertDN);
-            }
-
-            boolean isServerNameValidated;
-            String dnsNameInSANCert = "";
-
-            // the name in cert is in RFC2253 format parse it to get the actual subject name
-            String subjectCN = parseCommonName(nameInCertDN);
-
-            isServerNameValidated = validateServerName(subjectCN);
-
-            if (!isServerNameValidated) {
-
-                Collection<List<?>> sanCollection = cert.getSubjectAlternativeNames();
-
-                if (sanCollection != null) {
-                    // find a subjectAlternateName entry corresponding to DNS Name
-                    for (List<?> sanEntry : sanCollection) {
-
-                        if (sanEntry != null && sanEntry.size() >= 2) {
-                            Object key = sanEntry.get(0);
-                            Object value = sanEntry.get(1);
-
-                            if (logger.isLoggable(Level.FINER)) {
-                                logger.finer(logContext + "Key: " + key + "; KeyClass:"
-                                        + (key != null ? key.getClass() : null) + ";value: " + value + "; valueClass:"
-                                        + (value != null ? value.getClass() : null));
-
-                            }
-
-                            // From
-                            // Documentation(http://download.oracle.com/javase/6/docs/api/java/security/cert/X509Certificate.html):
-                            // "Note that the Collection returned may contain
-                            // more than one name of the same type."
-                            // So, more than one entry of dnsNameType can be present.
-                            // Java docs guarantee that the first entry in the list will be an integer.
-                            // 2 is the sequence no of a dnsName
-                            if ((key != null) && (key instanceof Integer) && ((Integer) key == 2)) {
-                                // As per RFC2459, the DNSName will be in the
-                                // "preferred name syntax" as specified by RFC
-                                // 1034 and the name can be in upper or lower case.
-                                // And no significance is attached to case.
-                                // Java docs guarantee that the second entry in the list
-                                // will be a string for dnsName
-                                if (value != null && value instanceof String) {
-                                    dnsNameInSANCert = (String) value;
-
-                                    // Use English locale to avoid Turkish i issues.
-                                    // Note that, this conversion was not necessary for
-                                    // cert.getSubjectX500Principal().getName("canonical");
-                                    // as the above API already does this by default as per documentation.
-                                    dnsNameInSANCert = dnsNameInSANCert.toLowerCase(Locale.ENGLISH);
-
-                                    isServerNameValidated = validateServerName(dnsNameInSANCert);
-
-                                    if (isServerNameValidated) {
-                                        if (logger.isLoggable(Level.FINER)) {
-                                            logger.finer(logContext + " found a valid name in certificate: "
-                                                    + dnsNameInSANCert);
-                                        }
-                                        break;
-                                    }
-                                }
-
-                                if (logger.isLoggable(Level.FINER)) {
-                                    logger.finer(logContext
-                                            + " the following name in certificate does not match the serverName: "
-                                            + value);
-                                }
-                            }
-
-                        } else {
-                            if (logger.isLoggable(Level.FINER)) {
-                                logger.finer(logContext + " found an invalid san entry: " + sanEntry);
-                            }
-                        }
-                    }
-
-                }
-            }
-
-            if (!isServerNameValidated) {
-                MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_certNameFailed"));
-                Object[] msgArgs = {hostName, dnsNameInSANCert};
-                throw new CertificateException(form.format(msgArgs));
-            }
-        }
-
-        public X509Certificate[] getAcceptedIssuers() {
-            return defaultTrustManager.getAcceptedIssuers();
-        }
-    }
-
     enum SSLHandhsakeState {
         SSL_HANDHSAKE_NOT_STARTED,
         SSL_HANDHSAKE_STARTED,
@@ -1860,15 +1594,18 @@ final class TDSChannel implements Serializable {
                 trustStoreType = SQLServerDriverStringProperty.TRUST_STORE_TYPE.getDefaultValue();
             }
 
+            String serverCert = con.activeConnectionProperties
+                    .getProperty(SQLServerDriverStringProperty.SERVER_CERTIFICATE.toString());
+
             isFips = Boolean.valueOf(
                     con.activeConnectionProperties.getProperty(SQLServerDriverBooleanProperty.FIPS.toString()));
-            sslProtocol = con.activeConnectionProperties
-                    .getProperty(SQLServerDriverStringProperty.SSL_PROTOCOL.toString());
-
             if (isFips) {
                 validateFips(trustStoreType, trustStoreFileName);
             }
 
+            sslProtocol = con.activeConnectionProperties
+                    .getProperty(SQLServerDriverStringProperty.SSL_PROTOCOL.toString());
+
             byte requestedEncryptLevel = con.getRequestedEncryptionLevel();
             assert TDS.ENCRYPT_OFF == requestedEncryptLevel || // Login only SSL
                     TDS.ENCRYPT_ON == requestedEncryptLevel || // Full SSL
@@ -1895,88 +1632,101 @@ final class TDSChannel implements Serializable {
             // Otherwise, we'll validate the certificate using a real TrustManager obtained
             // from the a security provider that is capable of validating X.509 certificates.
             else {
-                if (logger.isLoggable(Level.FINER))
-                    logger.finer(toString() + " SSL handshake will validate server certificate");
-
-                KeyStore ks = null;
+                if (isTDSS) {
+                    if (logger.isLoggable(Level.FINEST))
+                        logger.finest(toString() + " Verify server certificate for TDSS");
 
-                // If we are using the system default trustStore and trustStorePassword
-                // then we can skip all of the KeyStore loading logic below.
-                // The security provider's implementation takes care of everything for us.
-                if (null == trustStoreFileName && null == trustStorePassword) {
+                    if (null != hostNameInCertificate) {
+                        tm = new TrustManager[] {
+                                new ServerCertificateX509TrustManager(this, serverCert, hostNameInCertificate)};
+                    } else {
+                        tm = new TrustManager[] {new ServerCertificateX509TrustManager(this, serverCert, host)};
+                    }
+                } else {
                     if (logger.isLoggable(Level.FINER))
-                        logger.finer(toString() + " Using system default trust store and password");
-                }
+                        logger.finer(toString() + " SSL handshake will validate server certificate");
 
-                // Otherwise either the trustStore, trustStorePassword, or both was specified.
-                // In that case, we need to load up a KeyStore ourselves.
-                else {
-                    // First, obtain an interface to a KeyStore that can load trust material
-                    // stored in Java Key Store (JKS) format.
-                    if (logger.isLoggable(Level.FINEST))
-                        logger.finest(toString() + " Finding key store interface");
+                    KeyStore ks = null;
+
+                    // If we are using the system default trustStore and trustStorePassword
+                    // then we can skip all of the KeyStore loading logic below.
+                    // The security provider's implementation takes care of everything for us.
+                    if (null == trustStoreFileName && null == trustStorePassword && !isTDSS) {
+                        if (logger.isLoggable(Level.FINER)) {
+                            logger.finer(toString() + " Using system default trust store and password");
+                        }
+                    } else {
+                        // Otherwise either the trustStore, trustStorePassword, or both was specified.
+                        // In that case, we need to load up a KeyStore ourselves.
 
-                    ks = KeyStore.getInstance(trustStoreType);
-                    ksProvider = ks.getProvider();
+                        // First, obtain an interface to a KeyStore that can load trust material
+                        // stored in Java Key Store (JKS) format.
+                        if (logger.isLoggable(Level.FINEST))
+                            logger.finest(toString() + " Finding key store interface");
 
-                    // Next, load up the trust store file from the specified location.
-                    // Note: This function returns a null InputStream if the trust store cannot
-                    // be loaded. This is by design. See the method comment and documentation
-                    // for KeyStore.load for details.
-                    InputStream is = loadTrustStore(trustStoreFileName);
+                        ks = KeyStore.getInstance(trustStoreType);
+                        ksProvider = ks.getProvider();
 
-                    // Finally, load the KeyStore with the trust material (if any) from the
-                    // InputStream and close the stream.
-                    if (logger.isLoggable(Level.FINEST))
-                        logger.finest(toString() + " Loading key store");
+                        // Next, load up the trust store file from the specified location.
+                        // Note: This function returns a null InputStream if the trust store cannot
+                        // be loaded. This is by design. See the method comment and documentation
+                        // for KeyStore.load for details.
+                        InputStream is = loadTrustStore(trustStoreFileName);
 
-                    try {
-                        ks.load(is, (null == trustStorePassword) ? null : trustStorePassword.toCharArray());
-                    } finally {
-                        // We are also done with the trust store input stream.
-                        if (null != is) {
-                            try {
-                                is.close();
-                            } catch (IOException e) {
-                                if (logger.isLoggable(Level.FINE))
-                                    logger.fine(toString() + " Ignoring error closing trust material InputStream...");
+                        // Finally, load the KeyStore with the trust material (if any) from the
+                        // InputStream and close the stream.
+                        if (logger.isLoggable(Level.FINEST))
+                            logger.finest(toString() + " Loading key store");
+
+                        try {
+                            ks.load(is, (null == trustStorePassword) ? null : trustStorePassword.toCharArray());
+                        } finally {
+                            // We are also done with the trust store input stream.
+                            if (null != is) {
+                                try {
+                                    is.close();
+                                } catch (IOException e) {
+                                    if (logger.isLoggable(Level.FINE))
+                                        logger.fine(
+                                                toString() + " Ignoring error closing trust material InputStream...");
+                                }
                             }
                         }
                     }
-                }
 
-                // Either we now have a KeyStore populated with trust material or we are using the
-                // default source of trust material (cacerts). Either way, we are now ready to
-                // use a TrustManagerFactory to create a TrustManager that uses the trust material
-                // to validate the server certificate.
+                    // Either we now have a KeyStore populated with trust material or we are using the
+                    // default source of trust material (cacerts). Either way, we are now ready to
+                    // use a TrustManagerFactory to create a TrustManager that uses the trust material
+                    // to validate the server certificate.
 
-                // Next step is to get a TrustManagerFactory that can produce TrustManagers
-                // that understands X.509 certificates.
-                TrustManagerFactory tmf = null;
+                    // Next step is to get a TrustManagerFactory that can produce TrustManagers
+                    // that understands X.509 certificates.
+                    TrustManagerFactory tmf = null;
 
-                if (logger.isLoggable(Level.FINEST))
-                    logger.finest(toString() + " Locating X.509 trust manager factory");
+                    if (logger.isLoggable(Level.FINEST))
+                        logger.finest(toString() + " Locating X.509 trust manager factory");
 
-                tmfDefaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
-                tmf = TrustManagerFactory.getInstance(tmfDefaultAlgorithm);
-                tmfProvider = tmf.getProvider();
+                    tmfDefaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
+                    tmf = TrustManagerFactory.getInstance(tmfDefaultAlgorithm);
+                    tmfProvider = tmf.getProvider();
 
-                // Tell the TrustManagerFactory to give us TrustManagers that we can use to
-                // validate the server certificate using the trust material in the KeyStore.
-                if (logger.isLoggable(Level.FINEST))
-                    logger.finest(toString() + " Getting trust manager");
+                    // Tell the TrustManagerFactory to give us TrustManagers that we can use to
+                    // validate the server certificate using the trust material in the KeyStore.
+                    if (logger.isLoggable(Level.FINEST))
+                        logger.finest(toString() + " Getting trust manager");
 
-                tmf.init(ks);
-                tm = tmf.getTrustManagers();
+                    tmf.init(ks);
+                    tm = tmf.getTrustManagers();
 
-                // if the host name in cert provided use it or use the host name Only if it is not FIPS
-                if (!isFips) {
-                    if (null != hostNameInCertificate) {
-                        tm = new TrustManager[] {new HostNameOverrideX509TrustManager(this, (X509TrustManager) tm[0],
-                                hostNameInCertificate)};
-                    } else {
-                        tm = new TrustManager[] {
-                                new HostNameOverrideX509TrustManager(this, (X509TrustManager) tm[0], host)};
+                    // if the host name in cert provided use it or use the host name Only if it is not FIPS
+                    if (!isFips) {
+                        if (null != hostNameInCertificate) {
+                            tm = new TrustManager[] {new HostNameOverrideX509TrustManager(this,
+                                    (X509TrustManager) tm[0], hostNameInCertificate)};
+                        } else {
+                            tm = new TrustManager[] {
+                                    new HostNameOverrideX509TrustManager(this, (X509TrustManager) tm[0], host)};
+                        }
                     }
                 }
             } // end if (!con.trustServerCertificate())
@@ -2579,6 +2329,9 @@ final class SocketFinder {
     // necessary for raising exceptions so that the connection pool can be notified
     private final SQLServerConnection conn;
 
+    // list of addresses for ip selection by type preference
+    private static ArrayList<InetAddress> addressList = new ArrayList<>();
+
     /**
      * Constructs a new SocketFinder object with appropriate traceId
      * 
@@ -2598,11 +2351,13 @@ final class SocketFinder {
      * @param hostName
      * @param portNumber
      * @param timeoutInMilliSeconds
+     * @param iPAddressPreference
      * @return connected socket
      * @throws IOException
      */
     Socket findSocket(String hostName, int portNumber, int timeoutInMilliSeconds, boolean useParallel, boolean useTnir,
-            boolean isTnirFirstAttempt, int timeoutInMilliSecondsForFullTimeout) throws SQLServerException {
+            boolean isTnirFirstAttempt, int timeoutInMilliSecondsForFullTimeout,
+            String iPAddressPreference) throws SQLServerException {
         assert timeoutInMilliSeconds != 0 : "The driver does not allow a time out of 0";
 
         try {
@@ -2612,9 +2367,10 @@ final class SocketFinder {
                 // MSF is false. TNIR could be true or false. DBMirroring could be true or false.
                 // For TNIR first attempt, we should do existing behavior including how host name is resolved.
                 if (useTnir && isTnirFirstAttempt) {
-                    return getDefaultSocket(hostName, portNumber, SQLServerConnection.TnirFirstAttemptTimeoutMs);
+                    return getSocketByIPPreference(hostName, portNumber, SQLServerConnection.TnirFirstAttemptTimeoutMs,
+                            iPAddressPreference);
                 } else if (!useTnir) {
-                    return getDefaultSocket(hostName, portNumber, timeoutInMilliSeconds);
+                    return getSocketByIPPreference(hostName, portNumber, timeoutInMilliSeconds, iPAddressPreference);
                 }
             }
 
@@ -2907,19 +2663,85 @@ final class SocketFinder {
         return socketFactory;
     }
 
-    // This method contains the old logic of connecting to
-    // a socket of one of the IPs corresponding to a given host name.
-    // In the old code below, the logic around 0 timeout has been removed as
-    // 0 timeout is not allowed. The code has been re-factored so that the logic
-    // is common for hostName or InetAddress.
-    private Socket getDefaultSocket(String hostName, int portNumber, int timeoutInMilliSeconds) throws IOException {
-        // Open the socket, with or without a timeout, throwing an UnknownHostException
-        // if there is a failure to resolve the host name to an InetSocketAddress.
-        //
+    /**
+     * Helper function which traverses through list of InetAddresses to find a resolved one
+     * @param hostName 
+     * 
+     * @param portNumber
+     *        Port Number
+     * @return First resolved address or unresolved address if none found
+     * @throws IOException
+     * @throws SQLServerException
+     */
+    private InetSocketAddress getInetAddressByIPPreference(String hostName, int portNumber) throws IOException, SQLServerException {
+        InetSocketAddress addr = InetSocketAddress.createUnresolved(hostName, portNumber);
+        for (int i = 0; i < addressList.size(); i++) {
+            addr = new InetSocketAddress(addressList.get(i), portNumber);
+            if (!addr.isUnresolved())
+                return addr;
+        }
+        return addr;
+    }
+
+    /**
+     * This method makes a connected socket given a hostname or IP address.
+     * 
+     * @param hostName
+     *        Hostname or IP Address
+     * @param portNumber
+     *        Port number
+     * @param timeoutInMilliSeconds
+     *        Timeout
+     * @param iPAddressPreference
+     *        Preferred IP address type
+     * @return Connected Socket
+     * @throws IOException
+     * @throws SQLServerException
+     */
+    private Socket getSocketByIPPreference(String hostName, int portNumber, int timeoutInMilliSeconds,
+            String iPAddressPreference) throws IOException, SQLServerException {
+        InetSocketAddress addr = null;
+        InetAddress addresses[] = InetAddress.getAllByName(hostName);
+        IPAddressPreference pref = IPAddressPreference.valueOfString(iPAddressPreference);
+        switch (pref) {
+            case IPv6First:
+                // Try to connect to first choice of IP address type
+                fillAddressList(addresses, true);
+                addr = getInetAddressByIPPreference(hostName, portNumber);
+                if (!addr.isUnresolved())
+                    return getConnectedSocket(addr, timeoutInMilliSeconds);
+                // No unresolved addresses of preferred type, try the other
+                fillAddressList(addresses, false);
+                addr = getInetAddressByIPPreference(hostName, portNumber);
+                if (!addr.isUnresolved())
+                    return getConnectedSocket(addr, timeoutInMilliSeconds);
+                break;
+            case IPv4First:
+                // Try to connect to first choice of IP address type
+                fillAddressList(addresses, false);
+                addr = getInetAddressByIPPreference(hostName, portNumber);
+                if (!addr.isUnresolved())
+                    return getConnectedSocket(addr, timeoutInMilliSeconds);
+                // No unresolved addresses of preferred type, try the other
+                fillAddressList(addresses, true);
+                addr = getInetAddressByIPPreference(hostName, portNumber);
+                if (!addr.isUnresolved())
+                    return getConnectedSocket(addr, timeoutInMilliSeconds);
+                break;
+            case UsePlatformDefault:
+                for (InetAddress address : addresses) {
+                    addr = new InetSocketAddress(address, portNumber);
+                    if (!addr.isUnresolved())
+                        return getConnectedSocket(addr, timeoutInMilliSeconds);
+                }
+                break;
+            default:
+                break;
+        }
+
         // Note that Socket(host, port) throws an UnknownHostException if the host name
         // cannot be resolved, but that InetSocketAddress(host, port) does not - it sets
         // the returned InetSocketAddress as unresolved.
-        InetSocketAddress addr = new InetSocketAddress(hostName, portNumber);
         if (addr.isUnresolved()) {
             if (logger.isLoggable(Level.FINER)) {
                 logger.finer(this.toString() + "Failed to resolve host name: " + hostName
@@ -2928,9 +2750,32 @@ final class SocketFinder {
             InetSocketAddress cacheEntry = SQLServerConnection.getDNSEntry(hostName);
             addr = (null != cacheEntry) ? cacheEntry : addr;
         }
+
         return getConnectedSocket(addr, timeoutInMilliSeconds);
     }
 
+    /**
+     * Fills static array of IP Addresses with addresses of the preferred protocol version.
+     * @param addresses Array of all addresses
+     * @param ipv6first Boolean switch for IPv6 first
+     */
+    private void fillAddressList(InetAddress[] addresses, boolean ipv6first) {
+        addressList.clear();
+        if (ipv6first) {
+            for (InetAddress addr : addresses) {
+                if (addr instanceof Inet6Address) {
+                    addressList.add(addr);
+                }
+            }
+        } else {
+            for (InetAddress addr : addresses) {
+                if (addr instanceof Inet4Address) {
+                    addressList.add(addr);
+                }
+            }
+        }
+    }
+
     private Socket getConnectedSocket(InetAddress inetAddr, int portNumber,
             int timeoutInMilliSeconds) throws IOException {
         InetSocketAddress addr = new InetSocketAddress(inetAddr, portNumber);
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
index a74a3bd1..a224eb7f 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
@@ -391,4 +391,20 @@ public interface ISQLServerConnection extends java.sql.Connection {
      *        boolean value for 'delayLoadingLobs'.
      */
     void setDelayLoadingLobs(boolean delayLoadingLobs);
+    
+    /**
+     * Sets the name of the preferred type of IP Address.
+     * 
+     * @param iPAddressPreference
+     *        A String that contains the preferred type of IP Address.
+     */
+    void setIPAddressPreference(String iPAddressPreference);
+    
+    /**
+     * Gets the name of the preferred type of IP Address.
+     * 
+     * @return IPAddressPreference
+     *        A String that contains the preferred type of IP Address.
+     */
+    String getIPAddressPreference();
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
index 0102e6ea..238c4e51 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
@@ -125,6 +125,21 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
      */
     String getEncrypt();
 
+    /**
+     * Returns the path to the server certificate.
+     *
+     * @return serverCertificate property value
+     */
+    String getServerCertificate();
+
+    /**
+     * Sets the connection property 'serverCertificate' on the connection.
+     *
+     * @param cert
+     *        The path to the server certificate.
+     */
+    void setServerCertificate(String cert);
+
     /**
      * Sets the value to enable/disable Transparent Network IP Resolution (TNIR). Beginning in version 6.0 of the
      * Microsoft JDBC Driver for SQL Server, a new connection property transparentNetworkIPResolution (TNIR) is added
@@ -372,6 +387,22 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
      * @return A String that contains the server name or null if no value is set.
      */
     String getServerName();
+    
+    /**
+     * Sets the name of the preferred type of IP Address.
+     * 
+     * @param iPAddressPreference
+     *        A String that contains the preferred type of IP Address.
+     */
+    void setIPAddressPreference(String iPAddressPreference);
+    
+    /**
+     * Gets the name of the preferred type of IP Address.
+     * 
+     * @return IPAddressPreference
+     *        A String that contains the preferred type of IP Address.
+     */
+    String getIPAddressPreference();
 
     /**
      * Sets the name of the failover server that is used in a database mirroring configuration.
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java
index fd7134e7..162d592f 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java
@@ -17,6 +17,7 @@ import java.math.BigInteger;
 import java.nio.Buffer;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.nio.channels.FileChannel;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.security.GeneralSecurityException;
@@ -35,8 +36,14 @@ import java.security.cert.X509Certificate;
 import java.security.spec.InvalidKeySpecException;
 import java.security.spec.PKCS8EncodedKeySpec;
 import java.security.spec.RSAPrivateCrtKeySpec;
+import java.text.MessageFormat;
 import java.util.Arrays;
 import java.util.Base64;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import javax.crypto.Cipher;
 import javax.crypto.SecretKey;
@@ -54,6 +61,9 @@ import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
 
 final class SQLServerCertificateUtils {
 
+    private static final Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.SQLServerCertificateUtils");
+    private static final String logContext = Thread.currentThread().getStackTrace()[1].getClassName() + ": ";
+
     static KeyManager[] getKeyManagerFromFile(String certPath, String keyPath,
             String keyPassword) throws IOException, GeneralSecurityException, SQLServerException {
         if (keyPath != null && keyPath.length() > 0) {
@@ -63,6 +73,237 @@ final class SQLServerCertificateUtils {
         }
     }
 
+    /**
+     * Parse name in RFC 2253 format Returns the common name if successful, null if failed to find the common name. The
+     * parser tuned to be safe than sorry so if it sees something it can't parse correctly it returns null
+     * 
+     * @param distinguishedName
+     *        server name to parse
+     * @return subject name
+     */
+    static String parseCommonName(String distinguishedName) {
+        int index;
+        // canonical name converts entire name to lowercase
+        index = distinguishedName.indexOf("cn=");
+        if (index == -1) {
+            return null;
+        }
+        distinguishedName = distinguishedName.substring(index + 3);
+        // Parse until a comma or end is reached
+        // Note the parser will handle gracefully (essentially will return empty string) , inside the quotes (e.g
+        // cn="Foo, bar") however
+        // RFC 952 says that the hostName cant have commas however the parser should not (and will not) crash if it
+        // sees a , within quotes.
+        for (index = 0; index < distinguishedName.length(); index++) {
+            if (distinguishedName.charAt(index) == ',') {
+                break;
+            }
+        }
+        String commonName = distinguishedName.substring(0, index);
+        // strip any quotes
+        if (commonName.length() > 1 && ('\"' == commonName.charAt(0))) {
+            if ('\"' == commonName.charAt(commonName.length() - 1))
+                commonName = commonName.substring(1, commonName.length() - 1);
+            else {
+                // Be safe the name is not ended in " return null so the common Name wont match
+                commonName = null;
+            }
+        }
+        return commonName;
+    }
+
+    /**
+     * Validate server name in certificate matches hostname
+     * 
+     * @param nameInCert
+     *        server name in certificate
+     * @param hostName
+     *        hostname
+     * @return if the server name is valid and matches hostname
+     */
+    static boolean validateServerName(String nameInCert, String hostName) {
+        // Failed to get the common name from DN or empty CN
+        if (null == nameInCert) {
+            if (logger.isLoggable(Level.FINER)) {
+                logger.finer(logContext + " Failed to parse the name from the certificate or name is empty.");
+            }
+            return false;
+        }
+        // We do not allow wildcards in IDNs (xn--).
+        if (!nameInCert.startsWith("xn--") && nameInCert.contains("*")) {
+            int hostIndex = 0, certIndex = 0, match = 0, startIndex = -1, periodCount = 0;
+            while (hostIndex < hostName.length()) {
+                if ('.' == hostName.charAt(hostIndex)) {
+                    periodCount++;
+                }
+                if (certIndex < nameInCert.length() && hostName.charAt(hostIndex) == nameInCert.charAt(certIndex)) {
+                    hostIndex++;
+                    certIndex++;
+                } else if (certIndex < nameInCert.length() && '*' == nameInCert.charAt(certIndex)) {
+                    startIndex = certIndex;
+                    match = hostIndex;
+                    certIndex++;
+                } else if (startIndex != -1 && 0 == periodCount) {
+                    certIndex = startIndex + 1;
+                    match++;
+                    hostIndex = match;
+                } else {
+                    logFailMessage(nameInCert, hostName);
+                    return false;
+                }
+            }
+            if (nameInCert.length() == certIndex && periodCount > 1) {
+                logSuccessMessage(nameInCert, hostName);
+                return true;
+            } else {
+                logFailMessage(nameInCert, hostName);
+                return false;
+            }
+        }
+        // Verify that the name in certificate matches exactly with the host name
+        if (!nameInCert.equals(hostName)) {
+            logFailMessage(nameInCert, hostName);
+            return false;
+        }
+        logSuccessMessage(nameInCert, hostName);
+        return true;
+    }
+
+    /**
+     * Validate server name in certificate
+     * 
+     * @param cert
+     *        X509 certificate
+     * @param hostName
+     *        hostname
+     * @throws CertificateException
+     */
+    static void validateServerNameInCertificate(X509Certificate cert, String hostName) throws CertificateException {
+        String nameInCertDN = cert.getSubjectX500Principal().getName("canonical");
+
+        if (logger.isLoggable(Level.FINER)) {
+            logger.finer(logContext + " Validating the server name:" + hostName);
+            logger.finer(logContext + " The DN name in certificate:" + nameInCertDN);
+        }
+
+        boolean isServerNameValidated;
+        String dnsNameInSANCert = "";
+
+        // the name in cert is in RFC2253 format parse it to get the actual subject name
+        String subjectCN = parseCommonName(nameInCertDN);
+
+        isServerNameValidated = validateServerName(subjectCN, hostName);
+
+        if (!isServerNameValidated) {
+            Collection<List<?>> sanCollection = cert.getSubjectAlternativeNames();
+
+            if (sanCollection != null) {
+                // find a subjectAlternateName entry corresponding to DNS Name
+                for (List<?> sanEntry : sanCollection) {
+
+                    if (sanEntry != null && sanEntry.size() >= 2) {
+                        Object key = sanEntry.get(0);
+                        Object value = sanEntry.get(1);
+
+                        if (logger.isLoggable(Level.FINER)) {
+                            logger.finer(logContext + "Key: " + key + "; KeyClass:"
+                                    + (key != null ? key.getClass() : null) + ";value: " + value + "; valueClass:"
+                                    + (value != null ? value.getClass() : null));
+                        }
+
+                        // From
+                        // Documentation(http://download.oracle.com/javase/6/docs/api/java/security/cert/X509Certificate.html):
+                        // "Note that the Collection returned may contain
+                        // more than one name of the same type."
+                        // So, more than one entry of dnsNameType can be present.
+                        // Java docs guarantee that the first entry in the list will be an integer.
+                        // 2 is the sequence no of a dnsName
+                        if ((key != null) && (key instanceof Integer) && ((Integer) key == 2)) {
+                            // As per RFC2459, the DNSName will be in the
+                            // "preferred name syntax" as specified by RFC
+                            // 1034 and the name can be in upper or lower case.
+                            // And no significance is attached to case.
+                            // Java docs guarantee that the second entry in the list
+                            // will be a string for dnsName
+                            if (value != null && value instanceof String) {
+                                dnsNameInSANCert = (String) value;
+
+                                // Use English locale to avoid Turkish i issues.
+                                // Note that, this conversion was not necessary for
+                                // cert.getSubjectX500Principal().getName("canonical");
+                                // as the above API already does this by default as per documentation.
+                                dnsNameInSANCert = dnsNameInSANCert.toLowerCase(Locale.ENGLISH);
+
+                                isServerNameValidated = validateServerName(dnsNameInSANCert, hostName);
+                                if (isServerNameValidated) {
+                                    if (logger.isLoggable(Level.FINER)) {
+                                        logger.finer(
+                                                logContext + " found a valid name in certificate: " + dnsNameInSANCert);
+                                    }
+                                    break;
+                                }
+                            }
+
+                            if (logger.isLoggable(Level.FINER)) {
+                                logger.finer(logContext
+                                        + " the following name in certificate does not match the serverName: " + value);
+                            }
+                        }
+
+                    } else {
+                        if (logger.isLoggable(Level.FINER)) {
+                            logger.finer(logContext + " found an invalid san entry: " + sanEntry);
+                        }
+                    }
+                }
+            }
+        }
+
+        if (!isServerNameValidated) {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_certNameFailed"));
+            Object[] msgArgs = {hostName, dnsNameInSANCert};
+            throw new CertificateException(form.format(msgArgs));
+        }
+    }
+
+    /**
+     * Validate certificate provided in path against server X509 certificate
+     * 
+     * @param cert
+     *        X509 certificate
+     * @param certFile
+     *        path to certificate file to validate
+     * @throws CertificateException
+     */
+    static void validateServerCerticate(X509Certificate cert, String certFile) throws CertificateException {
+        try (InputStream is = fileToStream(certFile)) {
+            if (!CertificateFactory.getInstance("X509").generateCertificate(is).getPublicKey()
+                    .equals(cert.getPublicKey())) {
+                MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_serverCertError"));
+                Object[] msgArgs = {certFile};
+                throw new CertificateException(form.format(msgArgs));
+            }
+        } catch (Exception e) {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_serverCertError"));
+            Object[] msgArgs = {certFile, e.getMessage()};
+            throw new CertificateException(form.format(msgArgs));
+        }
+    }
+
+    private static void logFailMessage(String nameInCert, String hostName) {
+        if (logger.isLoggable(Level.FINER)) {
+            logger.finer(logContext + " The name in certificate " + nameInCert + " does not match with the server name "
+                    + hostName + ".");
+        }
+    }
+
+    private static void logSuccessMessage(String nameInCert, String hostName) {
+        if (logger.isLoggable(Level.FINER)) {
+            logger.finer(logContext + " The name in certificate:" + nameInCert + " validated against server name "
+                    + hostName + ".");
+        }
+    }
+
     // PKCS#12 format
     private static final String PKCS12_ALG = "PKCS12";
     private static final String SUN_X_509 = "SunX509";
@@ -148,8 +389,8 @@ final class SQLServerCertificateUtils {
             String keyPass) throws IOException, GeneralSecurityException, SQLServerException {
         File f = new File(keyPath);
         ByteBuffer buffer = ByteBuffer.allocate((int) f.length());
-        try (FileInputStream in = new FileInputStream(f)) {
-            in.getChannel().read(buffer);
+
+        try (FileInputStream in = new FileInputStream(f); FileChannel channel = in.getChannel()) {
             ((Buffer) buffer.order(ByteOrder.LITTLE_ENDIAN)).rewind();
 
             long magic = buffer.getInt() & 0xFFFFFFFFL;
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
index acc07ae9..ec306b05 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -391,7 +391,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         private Instant lastNetworkActivity = Instant.now();
 
         /**
-         * An “idle” connection will only ever get its socket disconnected by a keepalive packet after a connection has
+         * An "idle" connection will only ever get its socket disconnected by a keepalive packet after a connection has
          * been severed. KeepAlive packets are only sent on idle sockets. Default setting by the driver (on platforms
          * that have Java support for setting it) and the recommended setting is 30s (and OS default for those that
          * don't set it is 2 hrs).
@@ -754,6 +754,13 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         return trustServerCertificate;
     }
 
+    /** server certificate for encrypt=strict */
+    private String serverCertificate = null;
+
+    final String getServerCertificate() {
+        return serverCertificate;
+    }
+
     /** negotiated encryption level */
     private byte negotiatedEncryptionLevel = TDS.ENCRYPT_INVALID;
 
@@ -1930,6 +1937,15 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                 if (null != sPropValuePort) {
                     trustedServerNameAE += ":" + sPropValuePort;
                 }
+                
+                sPropKey = SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.getDefaultValue();
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                } else {
+                    activeConnectionProperties.setProperty(sPropKey, IPAddressPreference.valueOfString(sPropValue).toString());
+                }
 
                 sPropKey = SQLServerDriverStringProperty.APPLICATION_NAME.toString();
                 sPropValue = activeConnectionProperties.getProperty(sPropKey);
@@ -2094,6 +2110,14 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                     // do not trust server cert for strict
                     trustServerCertificate = false;
 
+                    sPropKey = SQLServerDriverStringProperty.SERVER_CERTIFICATE.toString();
+                    sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                    if (null == sPropValue) {
+                        sPropValue = SQLServerDriverStringProperty.SERVER_CERTIFICATE.getDefaultValue();
+                    }
+                    serverCertificate = activeConnectionProperties
+                            .getProperty(SQLServerDriverStringProperty.SERVER_CERTIFICATE.toString());
+
                     // prelogin TLS handshake is required
                     isTDSS = true;
                 } else {
@@ -3214,10 +3238,11 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
         // if the timeout is infinite slices are infinite too.
         tdsChannel = new TDSChannel(this);
+        String iPAddressPreference = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString());
 
         InetSocketAddress inetSocketAddress = tdsChannel.open(serverInfo.getParsedServerName(),
                 serverInfo.getPortNumber(), (0 == timeOutFullInSeconds) ? 0 : timeOutSliceInMillis, useParallel,
-                useTnir, isTnirFirstAttempt, timeOutsliceInMillisForFullTimeout);
+                useTnir, isTnirFirstAttempt, timeOutsliceInMillisForFullTimeout, iPAddressPreference);
 
         setState(State.Connected);
 
@@ -7609,6 +7634,17 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     String getServerName() {
         return this.trustedServerNameAE;
     }
+
+    @Override
+    public void setIPAddressPreference(String iPAddressPreference) {
+        activeConnectionProperties.setProperty(SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString(), iPAddressPreference);
+        
+    }
+
+    @Override
+    public String getIPAddressPreference() {
+        return activeConnectionProperties.getProperty(SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString());
+    }
 }
 
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
index 91678f39..364f549d 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
@@ -619,4 +619,15 @@ class SQLServerConnectionPoolProxy implements ISQLServerConnection, java.io.Seri
     public void setDelayLoadingLobs(boolean delayLoadingLobs) {
         wrappedConnection.setDelayLoadingLobs(delayLoadingLobs);
     }
+
+    @Override
+    public void setIPAddressPreference(String iPAddressPreference) {
+        wrappedConnection.setIPAddressPreference(iPAddressPreference);
+        
+    }
+
+    @Override
+    public String getIPAddressPreference() {
+        return wrappedConnection.getIPAddressPreference();
+    }
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
index 669a190e..c30d6b15 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
@@ -323,6 +323,16 @@ public class SQLServerDataSource
                 SQLServerDriverStringProperty.ENCRYPT.getDefaultValue());
     }
 
+    @Override
+    public void setServerCertificate(String cert) {
+        setStringProperty(connectionProps, SQLServerDriverStringProperty.SERVER_CERTIFICATE.toString(), cert);
+    }
+
+    @Override
+    public String getServerCertificate() {
+        return getStringProperty(connectionProps, SQLServerDriverStringProperty.SERVER_CERTIFICATE.toString(), null);
+    }
+
     @Override
     public void setTransparentNetworkIPResolution(boolean tnir) {
         setBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.TRANSPARENT_NETWORK_IP_RESOLUTION.toString(),
@@ -584,6 +594,24 @@ public class SQLServerDataSource
     public String getServerName() {
         return getStringProperty(connectionProps, SQLServerDriverStringProperty.SERVER_NAME.toString(), null);
     }
+    
+    /**
+     * Set the preferred type of IP Address
+     * 
+     * @param iPAddressPreference Preferred IP Address type
+     */
+    @Override
+    public void setIPAddressPreference(String iPAddressPreference) {
+        setStringProperty(connectionProps, SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString(), iPAddressPreference);
+    }
+    
+    /**
+     * Gets the preferred type of IP Address
+     */
+    @Override
+    public String getIPAddressPreference() {
+        return getStringProperty(connectionProps, SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString(), SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.getDefaultValue());
+    }
 
     /**
      * Sets the realm for Kerberos authentication.
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
index ff123be9..43209c8f 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
@@ -257,6 +257,43 @@ enum SSLProtocol {
 }
 
 
+enum IPAddressPreference {
+    IPv4First("IPv4First"),
+    IPv6First("IPv6First"),
+    UsePlatformDefault("UsePlatformDefault");
+
+    private final String name;
+
+    IPAddressPreference(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+
+    static IPAddressPreference valueOfString(String value) throws SQLServerException {
+        IPAddressPreference iptype = null;
+
+        if (value.toLowerCase(Locale.US).equalsIgnoreCase(IPAddressPreference.IPv4First.toString())) {
+            iptype = IPAddressPreference.IPv4First;
+        } else if (value.toLowerCase(Locale.US).equalsIgnoreCase(IPAddressPreference.IPv6First.toString())) {
+            iptype = IPAddressPreference.IPv6First;
+        } else if (value.toLowerCase(Locale.US)
+                .equalsIgnoreCase(IPAddressPreference.UsePlatformDefault.toString())) {
+            iptype = IPAddressPreference.UsePlatformDefault;
+
+        } else {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidIPAddressPreference"));
+            Object[] msgArgs = {value};
+            throw new SQLServerException(form.format(msgArgs), null);
+        }
+        return iptype;
+    }
+}
+
+
 enum KeyStoreAuthentication {
     JavaKeyStorePassword,
     KeyVaultClientSecret,
@@ -422,6 +459,7 @@ enum SQLServerDriverStringProperty {
     SELECT_METHOD("selectMethod", "direct"),
     DOMAIN("domain", ""),
     SERVER_NAME("serverName", ""),
+    IPADDRESS_PREFERENCE("iPAddressPreference", IPAddressPreference.IPv4First.toString()),
     SERVER_SPN("serverSpn", ""),
     REALM("realm", ""),
     SOCKET_FACTORY_CLASS("socketFactoryClass", ""),
@@ -453,7 +491,8 @@ enum SQLServerDriverStringProperty {
     AAD_SECURE_PRINCIPAL_ID("AADSecurePrincipalId", ""),
     AAD_SECURE_PRINCIPAL_SECRET("AADSecurePrincipalSecret", ""),
     MAX_RESULT_BUFFER("maxResultBuffer", "-1"),
-    ENCRYPT("encrypt", EncryptOption.True.toString());
+    ENCRYPT("encrypt", EncryptOption.True.toString()),
+    SERVER_CERTIFICATE("serverCertificate", "");
 
     private final String name;
     private final String defaultValue;
@@ -595,6 +634,8 @@ public final class SQLServerDriver implements java.sql.Driver {
                     new String[] {EncryptOption.False.toString(), EncryptOption.No.toString(),
                             EncryptOption.Optional.toString(), EncryptOption.True.toString(),
                             EncryptOption.Mandatory.toString(), EncryptOption.Strict.toString()}),
+            new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SERVER_CERTIFICATE.toString(),
+                    SQLServerDriverStringProperty.SERVER_CERTIFICATE.getDefaultValue(), false, null),
             new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.PREPARE_METHOD.toString(),
                     SQLServerDriverStringProperty.PREPARE_METHOD.getDefaultValue(), false,
                     new String[] {PrepareMethod.PREPEXEC.toString(), PrepareMethod.PREPARE.toString()}),
@@ -649,6 +690,11 @@ public final class SQLServerDriver implements java.sql.Driver {
                     SQLServerDriverStringProperty.DOMAIN.getDefaultValue(), false, null),
             new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SERVER_NAME.toString(),
                     SQLServerDriverStringProperty.SERVER_NAME.getDefaultValue(), false, null),
+            new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString(),
+                    SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.getDefaultValue(), false,
+                    new String[] {IPAddressPreference.IPv4First.toString(),
+                            IPAddressPreference.IPv6First.toString(),
+                            IPAddressPreference.UsePlatformDefault.toString()}),
             new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SERVER_SPN.toString(),
                     SQLServerDriverStringProperty.SERVER_SPN.getDefaultValue(), false, null),
             new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.REALM.toString(),
@@ -698,7 +744,6 @@ public final class SQLServerDriver implements java.sql.Driver {
                             SqlAuthentication.ActiveDirectoryMSI.toString(),
                             SqlAuthentication.ActiveDirectoryServicePrincipal.toString(),
                             SqlAuthentication.ActiveDirectoryInteractive.toString()}),
-
             new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.SOCKET_TIMEOUT.toString(),
                     Integer.toString(SQLServerDriverIntProperty.SOCKET_TIMEOUT.getDefaultValue()), false, null),
             new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.FIPS.toString(),
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
index 94cde8fc..da1b8d3d 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
@@ -165,6 +165,7 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_databaseNamePropertyDescription", "The name of the database to connect to."},
         {"R_domainPropertyDescription", "The Windows domain to authenticate in using NTLM."},
         {"R_serverNamePropertyDescription", "The computer running SQL Server."},
+        {"R_iPAddressPreferencePropertyDescription", "The preferred type of IP address."},
         {"R_portNumberPropertyDescription", "The TCP port where an instance of SQL Server is listening."},
         {"R_realmPropertyDescription", "The realm for Kerberos authentication."},
         {"R_serverSpnPropertyDescription", "SQL Server SPN."},
@@ -192,6 +193,7 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_failoverPartnerPropertyDescription", "The name of the failover server used in a database mirroring configuration."},
         {"R_packetSizePropertyDescription", "The network packet size used to communicate with SQL Server."},
         {"R_encryptPropertyDescription", "Determines if Secure Sockets Layer (SSL) encryption should be used between the client and the server."},
+        {"R_serverCertificatePropertyDescription", "The path to the server certificate file."},
         {"R_prepareMethodPropertyDescription", "Determines the prepare method used in the driver."},
         {"R_socketFactoryClassPropertyDescription", "The class to instantiate as the SocketFactory for connections"},
         {"R_socketFactoryConstructorArgPropertyDescription", "The optional argument to pass to the constructor specified by socketFactoryClass"},
@@ -285,8 +287,7 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_PlainTextNullAE", "Internal error. Plaintext value cannot be null."},
         {"R_StreamingDataTypeAE", "Data of length greater than {0} is not supported in encrypted {1} column."},
         {"R_AE_NotSupportedByServer", "SQL Server in use does not support column encryption."},
-        {"R_InvalidAEVersionNumber", "Received invalid version number \"{0}\" for Always Encrypted."}, // From
-                                                                                                       // Server
+        {"R_InvalidAEVersionNumber", "Received invalid version number \"{0}\" for Always Encrypted."}, // From server
         {"R_NullEncryptedColumnEncryptionKey", "Internal error. Encrypted column encryption key cannot be null."},
         {"R_EmptyEncryptedColumnEncryptionKey", "Internal error. Empty encrypted column encryption key specified."},
         {"R_InvalidMasterKeyDetails", "Invalid master key details specified."},
@@ -502,7 +503,10 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_crClientUnrecoverable","The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection."},
         {"R_crClientSSLStateNotRecoverable", "The server did not preserve SSL encryption during a recovery attempt, connection recovery is not possible."},
         {"R_crCommandCannotTimeOut", "Request failed to time out and SQLServerConnection does not exist"},
+        {"R_InvalidIPAddressPreference", "IP address preference {0} is not valid."},
         {"R_UnableLoadAuthDll", "Unable to load authentication DLL {0}"},
+        {"R_illegalArgumentTrustManager", "Interal error. Peer certificate chain or key exchange algorithem can not be null or empty."},
+        {"R_serverCertError", "Error validating Server Certificate: {0}."},
     };
 }
 // @formatter:on
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerTrustManager.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerTrustManager.java
new file mode 100644
index 00000000..7598af75
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerTrustManager.java
@@ -0,0 +1,166 @@
+package com.microsoft.sqlserver.jdbc;
+
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.X509Certificate;
+import java.text.MessageFormat;
+import java.util.Locale;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.net.ssl.X509TrustManager;
+
+
+/**
+ * This class implements an X509TrustManager that always accepts the X509Certificate chain offered to it.
+ *
+ * A PermissiveX509TrustManager is used to "verify" the authenticity of the server when the trustServerCertificate
+ * connection property is set to true.
+ */
+final class PermissiveX509TrustManager implements X509TrustManager {
+    private final Logger logger;
+    private final String logContext;
+
+    PermissiveX509TrustManager(TDSChannel tdsChannel) {
+        this.logger = tdsChannel.getLogger();
+        this.logContext = tdsChannel.toString() + " (PermissiveX509TrustManager):";
+    }
+
+    @Override
+    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+        if (logger.isLoggable(Level.FINER))
+            logger.finer(logContext + " Trusting client certificate (!)");
+    }
+
+    @Override
+    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+        if (logger.isLoggable(Level.FINER))
+            logger.finer(logContext + " Trusting server certificate");
+    }
+
+    @Override
+    public X509Certificate[] getAcceptedIssuers() {
+        return new X509Certificate[0];
+    }
+}
+
+
+/**
+ * This class implements an X509TrustManager that validates hostname.
+ *
+ * This validates the subject name in the certificate with the host name
+ */
+final class HostNameOverrideX509TrustManager implements X509TrustManager {
+    private final Logger logger;
+    private final String logContext;
+    private final X509TrustManager defaultTrustManager;
+    private String hostName;
+
+    HostNameOverrideX509TrustManager(TDSChannel tdsChannel, X509TrustManager tm, String hostName) {
+        this.logger = tdsChannel.getLogger();
+        this.logContext = tdsChannel.toString() + " (HostNameOverrideX509TrustManager):";
+        defaultTrustManager = tm;
+
+        // canonical name is in lower case so convert this to lowercase too.
+        this.hostName = hostName.toLowerCase(Locale.ENGLISH);
+    }
+
+    @Override
+    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+        if (logger.isLoggable(Level.FINEST)) {
+            logger.finest(logContext + " Forwarding ClientTrusted.");
+        }
+
+        defaultTrustManager.checkClientTrusted(chain, authType);
+
+        // Explicitly validate the expiry dates
+        for (X509Certificate cert : chain) {
+            cert.checkValidity();
+        }
+    }
+
+    @Override
+    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+        if (logger.isLoggable(Level.FINEST)) {
+            logger.finest(logContext + " Forwarding Trusting server certificate");
+        }
+
+        defaultTrustManager.checkServerTrusted(chain, authType);
+
+        // Explicitly validate the expiry dates
+        for (X509Certificate cert : chain) {
+            cert.checkValidity();
+        }
+
+        if (logger.isLoggable(Level.FINEST)) {
+            logger.finest(logContext + " Default serverTrusted succeeded proceeding with server name validation");
+        }
+
+        SQLServerCertificateUtils.validateServerNameInCertificate(chain[0], hostName);
+    }
+
+    public X509Certificate[] getAcceptedIssuers() {
+        return defaultTrustManager.getAcceptedIssuers();
+    }
+}
+
+
+/**
+ * This class implements an X509TrustManager that validates the server certificate provided. This is applicable when
+ * encrypt connection property is set to "strict"
+ *
+ */
+final class ServerCertificateX509TrustManager implements X509TrustManager {
+    private final Logger logger;
+    private final String logContext;
+    private String hostName;
+    private String serverCert;
+
+    ServerCertificateX509TrustManager(TDSChannel tdsChannel, String cert, String hostName) {
+        this.logger = tdsChannel.getLogger();
+        this.logContext = tdsChannel.toString() + " (ServerCertificateX509TrustManager):";
+        // canonical name is in lower case so convert this to lowercase too.
+        this.hostName = hostName.toLowerCase(Locale.ENGLISH);
+        this.serverCert = cert;
+    }
+
+    @Override
+    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+        if (logger.isLoggable(Level.FINEST))
+            logger.finest(logContext + " Trusting client certificate (!)");
+    }
+
+    @Override
+    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+        if (logger.isLoggable(Level.FINEST)) {
+            logger.finest(logContext + " Check if server trusted.");
+        }
+
+        if (null == chain || 0 == chain.length || null == authType || authType.isEmpty()) {
+            throw new IllegalArgumentException(SQLServerException.getErrString("R_illegalArgumentTrustManager"));
+        }
+
+        try {
+            // validate expiry dates
+            for (X509Certificate c : chain) {
+                c.checkValidity();
+            }
+
+            // 1st element in the certificate chain is the server cert
+            // 2nd element in the certificate chain is the trusted root cert
+            if (null == serverCert) {
+                SQLServerCertificateUtils.validateServerNameInCertificate(chain[0], hostName);
+            } else {
+                SQLServerCertificateUtils.validateServerCerticate(chain[0], serverCert);
+            }
+        } catch (Exception e) {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_serverCertError"));
+            Object[] msgArgs = {serverCert, e.getMessage()};
+            throw new CertificateExpiredException(form.format(msgArgs));
+        }
+    }
+
+    @Override
+    public X509Certificate[] getAcceptedIssuers() {
+        return new X509Certificate[0];
+    }
+}


Test patch:
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
index 0f206df8..1450ffa4 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
@@ -82,6 +82,9 @@ public class SQLServerConnectionTest extends AbstractTest {
 
         ds.setPortNumber(intPropValue);
         assertEquals(intPropValue, ds.getPortNumber(), TestResource.getResource("R_valuesAreDifferent"));
+        
+        ds.setIPAddressPreference(stringPropValue);
+        assertEquals(stringPropValue, ds.getIPAddressPreference(), TestResource.getResource("R_valuesAreDifferent"));
 
         ds.setURL(stringPropValue);
         assertEquals(stringPropValue, ds.getURL(), TestResource.getResource("R_valuesAreDifferent"));
@@ -166,6 +169,9 @@ public class SQLServerConnectionTest extends AbstractTest {
         assertEquals(Boolean.toString(booleanPropValue), ds.getEncrypt(),
                 TestResource.getResource("R_valuesAreDifferent"));
 
+        ds.setServerCertificate(stringPropValue);
+        assertEquals(stringPropValue, ds.getServerCertificate(), TestResource.getResource("R_valuesAreDifferent"));
+
         ds.setPrepareMethod(stringPropValue);
         assertEquals(stringPropValue, ds.getPrepareMethod(), TestResource.getResource("R_valuesAreDifferent"));
 
@@ -906,4 +912,39 @@ public class SQLServerConnectionTest extends AbstractTest {
         persistentTokenAspect.beforeCacheAccess(tokenCacheAccessContext);
         PersistentTokenCacheAccessAspect.clearUserTokenCache();
     }
+
+    /**
+     * test bad serverCertificate property
+     * 
+     * @throws SQLException
+     */
+    @Test
+    public void testBadServerCert() throws SQLException {
+        SQLServerDataSource ds = new SQLServerDataSource();
+        ds.setURL(connectionString);
+        ds.setServerCertificate("badCert");
+        ds.setEncrypt(Constants.STRICT);
+        ds.setTrustServerCertificate(false);
+
+        // test using datasource
+        try (Connection con = ds.getConnection()) {
+            fail(TestResource.getResource("R_expectedFailPassed"));
+        } catch (SQLException e) {
+            // TODO: servers which do not support TDSS will return SSL failed error, test should be updated once server
+            // available
+            assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_serverCertError"))
+                    || e.getMessage().matches(TestUtils.formatErrorMsg("R_sslFailed")), e.getMessage());
+        }
+
+        // test connection string
+        try (Connection con = PrepUtil.getConnection(
+                connectionString + ";encrypt=strict;trustServerCertificate=false;serverCertificate=badCert")) {
+            fail(TestResource.getResource("R_expectedFailPassed"));
+        } catch (SQLException e) {
+            // TODO: servers which do not support TDSS will return SSL failed error, test should be updated once server
+            // available
+            assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_serverCertError"))
+                    || e.getMessage().matches(TestUtils.formatErrorMsg("R_sslFailed")), e.getMessage());
+        }
+    }
 }
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java
index 9c09d736..5309780c 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java
@@ -15,10 +15,8 @@ import java.sql.Statement;
 import java.util.Enumeration;
 import java.util.Properties;
 import java.util.logging.Logger;
-import java.util.stream.Stream;
 
 import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.junit.platform.runner.JUnitPlatform;
 import org.junit.runner.RunWith;
@@ -35,7 +33,7 @@ public class SQLServerDriverTest extends AbstractTest {
 
     @BeforeAll
     public static void setupTests() throws Exception {
-        connectionString = TestUtils.addOrOverrideProperty(connectionString,"trustServerCertificate", "true");
+        connectionString = TestUtils.addOrOverrideProperty(connectionString, "trustServerCertificate", "true");
         setConnection();
     }
 
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/ConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/ConnectionTest.java
index 40110c6a..0f28b61f 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/ConnectionTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/ConnectionTest.java
@@ -12,10 +12,14 @@ import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.platform.runner.JUnitPlatform;
 import org.junit.runner.RunWith;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
 
 import com.microsoft.sqlserver.jdbc.SQLServerDataSource;
+import com.microsoft.sqlserver.jdbc.TestResource;
 import com.microsoft.sqlserver.testframework.AbstractTest;
 
+
 /*
  * This test is for testing various connection options
  */
@@ -36,10 +40,33 @@ public class ConnectionTest extends AbstractTest {
         ds.setKeyStoreSecret("placeholder");
 
         // Multiple, successive connections should not fail
-        try (Connection con = ds.getConnection()) {
-        }
+        try (Connection con = ds.getConnection()) {}
+
+        try (Connection con = ds.getConnection()) {}
+    }
+
+    @Test
+    public void testConnectWithIPAddressPreference() throws SQLException {
+        SQLServerDataSource ds = new SQLServerDataSource();
+        ds.setURL(connectionString);
+        ds.setIPAddressPreference("IPv4First");
+        try (Connection con = ds.getConnection()) {}
+        ds.setIPAddressPreference("IPv6First");
+        try (Connection con = ds.getConnection()) {}
+        ds.setIPAddressPreference("UsePlatformDefault");
+        try (Connection con = ds.getConnection()) {}
+    }
 
+    @Test
+    public void testInvalidConnectWithIPAddressPreference() throws SQLException {
+        SQLServerDataSource ds = new SQLServerDataSource();
+        ds.setURL(connectionString);
+        ds.setIPAddressPreference("Bogus");
         try (Connection con = ds.getConnection()) {
+            fail(TestResource.getResource("R_expectedFailPassed"));
+        } catch (Exception e) {
+            assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidIPAddressPreference")));
         }
     }
+
 }
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
index ee768ccf..5ab5428c 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
@@ -497,6 +497,8 @@ public class RequestBoundaryMethodsTest extends AbstractTest {
         verifiedMethodNames.add("registerColumnEncryptionKeyStoreProvidersOnConnection");
         verifiedMethodNames.add("getPrepareMethod");
         verifiedMethodNames.add("setPrepareMethod");
+        verifiedMethodNames.add("getIPAddressPreference");
+        verifiedMethodNames.add("setIPAddressPreference");
 
         return verifiedMethodNames;
     }