Repository: microsoft/go-mssqldb
Pull request #61 (2022-10-17 19:29:03)

Implement support for named pipe connections
Step 1 is to refactor the existing `tcp` logic into separate interfaces so we can add `np:` parsing and dialing without more core changes.

The first push for this PR is that refactor, while only implementing the `tcp` dialer. 
Once the reviewers sign off on this approach I will add the named pipe dialer.



Patch:
diff --git a/README.md b/README.md
index e42219d..b197e9e 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
 
 Requires Go 1.8 or above.
 
-Install with `go get github.com/microsoft/go-mssqldb` .
+Install with `go install github.com/microsoft/go-mssqldb@latest`.
 
 ## Connection Parameters and DSN
 
@@ -22,7 +22,7 @@ Other supported formats are listed below.
 * `password`
 * `database`
 * `connection timeout` - in seconds (default is 0 for no timeout), set to 0 for no timeout. Recommended to set to 0 and use context to manage query and connection timeouts.
-* `dial timeout` - in seconds (default is 15), set to 0 for no timeout
+* `dial timeout` - in seconds (default is 15 times the number of registered protocols), set to 0 for no timeout.
 * `encrypt`
   * `disable` - Data send between client and server is not encrypted.
   * `false` - Data sent between client and server is not encrypted beyond the login packet. (Default)
@@ -358,6 +358,8 @@ db.QueryContext(ctx, `select * from t2 where user_name = @p1;`, mssql.VarChar(na
 * Supports connections to AlwaysOn Availability Group listeners, including re-direction to read-only replicas.
 * Supports query notifications
 * Supports Kerberos Authentication
+* Pluggable Dialer implementations through `msdsn.ProtocolParsers` and `msdsn.ProtocolDialers`
+* A `namedpipe` package to support connections using named pipes on Windows
 
 ## Tests
 
diff --git a/appveyor.yml b/appveyor.yml
index 1912109..c57f868 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -13,6 +13,7 @@ environment:
   DATABASE: test
   GOVERSION: 113
   RACE: -race -cpu 4
+  TAGS:
   matrix:
     - GOVERSION: 110
       SQLINSTANCE: SQL2017
@@ -50,6 +51,8 @@ environment:
       SQLINSTANCE: SQL2017
       GOARCH: 386
       RACE:
+      PROTOCOL: np
+      TAGS: -tags np
 install:
   - set GOROOT=c:\go%GOVERSION%
   - set PATH=%GOPATH%\bin;%GOROOT%\bin;%PATH%
@@ -58,19 +61,31 @@ install:
   - go get -u github.com/golang-sql/civil
   - go get -u github.com/golang-sql/sqlexp
   - go get -u golang.org/x/crypto/md4
+
 build_script:
   - go build
 
 before_test:
   # setup SQL Server
   - ps: |
+      [reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") | Out-Null
+      [reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.SqlWmiManagement") | Out-Null
+      $smo = 'Microsoft.SqlServer.Management.Smo.'
+      $wmi = new-object ($smo + 'Wmi.ManagedComputer')
+      $serverName = $env:COMPUTERNAME
       $instanceName = $env:SQLINSTANCE
-      Start-Service "MSSQL`$$instanceName"
+      # Enable named pipes
+      $uri = "ManagedComputer[@Name='$serverName']/ServerInstance[@Name='$instanceName']/ServerProtocol[@Name='Np']"
+      $Np = $wmi.GetSmoObject($uri)
+      $Np.IsEnabled = $true
+      $Np.Alter()
       Start-Service "SQLBrowser"
+      Start-Service "MSSQL`$$instanceName"
+      Start-Sleep -Seconds 10
   - sqlcmd -S "(local)\%SQLINSTANCE%" -Q "Use [master]; CREATE DATABASE test;"
-  - sqlcmd -S "(local)\%SQLINSTANCE%" -h -1 -Q "set nocount on; Select @@version"
+  - sqlcmd -S "np:.\%SQLINSTANCE%" -h -1 -Q "set nocount on; Select @@version"
   - pip install codecov
 
 test_script:
-  - go test %RACE% -coverprofile=coverage.txt -covermode=atomic
+  - go test %RACE% -coverprofile=coverage.txt -covermode=atomic %TAGS%
   - codecov -f coverage.txt
diff --git a/go.mod b/go.mod
index f9de173..fd812da 100644
--- a/go.mod
+++ b/go.mod
@@ -14,4 +14,5 @@ require (
 	github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
 	golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88
 	golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 // indirect
+	gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce
 )
diff --git a/go.sum b/go.sum
index ff200c9..b00c2e2 100644
--- a/go.sum
+++ b/go.sum
@@ -89,6 +89,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
+gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/msdsn/conn_str.go b/msdsn/conn_str.go
index 74799ee..c25f090 100644
--- a/msdsn/conn_str.go
+++ b/msdsn/conn_str.go
@@ -69,12 +69,16 @@ type Config struct {
 
 	// Do not use the following.
 
-	DialTimeout time.Duration // DialTimeout defaults to 15s. Set negative to disable.
+	DialTimeout time.Duration // DialTimeout defaults to 15s per protocol. Set negative to disable.
 	ConnTimeout time.Duration // Use context for timeouts.
 	KeepAlive   time.Duration // Leave at default.
 	PacketSize  uint16
 
 	Parameters map[string]string
+	// Protocols is an ordered list of protocols to dial
+	Protocols []string
+	// ProtocolParameters are written by non-tcp ProtocolParser implementations
+	ProtocolParameters map[string]interface{}
 }
 
 func SetupTLS(certificate string, insecureSkipVerify bool, hostInCertificate string, minTLSVersion string) (*tls.Config, error) {
@@ -112,7 +116,10 @@ func SetupTLS(certificate string, insecureSkipVerify bool, hostInCertificate str
 var skipSetup = errors.New("skip setting up TLS")
 
 func Parse(dsn string) (Config, error) {
-	p := Config{}
+	p := Config{
+		ProtocolParameters: map[string]interface{}{},
+		Protocols:          []string{},
+	}
 
 	var params map[string]string
 	var err error
@@ -140,15 +147,7 @@ func Parse(dsn string) (Config, error) {
 		}
 		p.LogFlags = Log(flags)
 	}
-	server := params["server"]
-	parts := strings.SplitN(server, `\`, 2)
-	p.Host = parts[0]
-	if p.Host == "." || strings.ToUpper(p.Host) == "(LOCAL)" || p.Host == "" {
-		p.Host = "localhost"
-	}
-	if len(parts) > 1 {
-		p.Instance = parts[1]
-	}
+
 	p.Database = params["database"]
 	p.User = params["user id"]
 	p.Password = params["password"]
@@ -198,13 +197,18 @@ func Parse(dsn string) (Config, error) {
 		}
 		p.ConnTimeout = time.Duration(timeout) * time.Second
 	}
-	p.DialTimeout = 15 * time.Second
+	f := len(p.Protocols)
+	if f == 0 {
+		f = 1
+	}
+	p.DialTimeout = time.Duration(15*f) * time.Second
 	if strdialtimeout, ok := params["dial timeout"]; ok {
 		timeout, err := strconv.ParseUint(strdialtimeout, 10, 64)
 		if err != nil {
 			f := "invalid dial timeout '%v': %v"
 			return p, fmt.Errorf(f, strdialtimeout, err.Error())
 		}
+
 		p.DialTimeout = time.Duration(timeout) * time.Second
 	}
 
@@ -330,6 +334,28 @@ func Parse(dsn string) (Config, error) {
 		p.DisableRetry = disableRetryDefault
 	}
 
+	server := params["server"]
+	protocol, ok := params["protocol"]
+
+	for _, parser := range ProtocolParsers {
+		if !ok || parser.Protocol() == protocol {
+			err = parser.ParseServer(server, &p)
+			if err != nil {
+				// if the caller only wants this protocol , fail right away
+				if ok {
+					return p, err
+				}
+			} else {
+				// Only enable a protocol if it can handle the server name
+				p.Protocols = append(p.Protocols, parser.Protocol())
+			}
+
+		}
+	}
+	if ok && len(p.Protocols) == 0 {
+		return p, fmt.Errorf("No protocol handler is available for protocol: '%s'", protocol)
+	}
+
 	return p, nil
 }
 
@@ -348,6 +374,10 @@ func (p Config) URL() *url.URL {
 		host = fmt.Sprintf("%s:%d", p.Host, p.Port)
 	}
 	q.Add("disableRetry", fmt.Sprintf("%t", p.DisableRetry))
+	protocol, ok := p.Parameters["protocol"]
+	if ok {
+		q.Add("protocol", protocol)
+	}
 	res := url.URL{
 		Scheme: "sqlserver",
 		Host:   host,
@@ -356,9 +386,11 @@ func (p Config) URL() *url.URL {
 	if p.Instance != "" {
 		res.Path = p.Instance
 	}
+	q.Add("dial timeout", strconv.FormatFloat(float64(p.DialTimeout.Seconds()), 'f', 0, 64))
 	if len(q) > 0 {
 		res.RawQuery = q.Encode()
 	}
+
 	return &res
 }
 
@@ -394,9 +426,14 @@ func splitConnectionString(dsn string) (res map[string]string) {
 			name = synonym
 		}
 		// "server" in ADO can include a protocol and a port.
-		// We only support tcp protocol
 		if name == "server" {
-			value = strings.TrimPrefix(value, "tcp:")
+			for _, parser := range ProtocolParsers {
+				prot := parser.Protocol() + ":"
+				if strings.HasPrefix(value, prot) {
+					res["protocol"] = parser.Protocol()
+				}
+				value = strings.TrimPrefix(value, prot)
+			}
 			serverParts := strings.Split(value, ",")
 			if len(serverParts) == 2 && len(serverParts[1]) > 0 {
 				value = serverParts[0]
@@ -635,3 +672,33 @@ func resolveServerPort(port uint64) uint64 {
 func generateSpn(host string, port string) string {
 	return fmt.Sprintf("MSSQLSvc/%s:%s", host, port)
 }
+
+// ProtocolParser can populate Config with parameters to dial using its protocol
+type ProtocolParser interface {
+	ParseServer(server string, p *Config) error
+	Protocol() string
+}
+
+// ProtocolParsers is an ordered list of protocols that can be dialed. Each parser must have a corresponding Dialer in mssql.ProtocolDialers
+var ProtocolParsers []ProtocolParser = []ProtocolParser{
+	tcpParser{},
+}
+
+type tcpParser struct{}
+
+func (t tcpParser) ParseServer(server string, p *Config) error {
+	// a server name can have different forms
+	parts := strings.SplitN(server, `\`, 2)
+	p.Host = parts[0]
+	if p.Host == "." || strings.ToUpper(p.Host) == "(LOCAL)" || p.Host == "" {
+		p.Host = "localhost"
+	}
+	if len(parts) > 1 {
+		p.Instance = parts[1]
+	}
+	return nil
+}
+
+func (t tcpParser) Protocol() string {
+	return "tcp"
+}
diff --git a/msdsn/extensions.go b/msdsn/extensions.go
new file mode 100644
index 0000000..221b92e
--- /dev/null
+++ b/msdsn/extensions.go
@@ -0,0 +1,20 @@
+package msdsn
+
+import (
+	"context"
+	"net"
+)
+
+type BrowserData map[string]map[string]string
+
+// ProtocolDialer makes the network connection for a protocol
+type ProtocolDialer interface {
+	// Translates data from SQL Browser to parameters in the config
+	ParseBrowserData(data BrowserData, p *Config) error
+	// DialConnection eturns a Dialer to make the connection
+	DialConnection(ctx context.Context, p Config) (conn net.Conn, err error)
+	// Returns true if information is needed from the SQL Browser service to make a connection
+	CallBrowser(p *Config) bool
+}
+
+var ProtocolDialers map[string]ProtocolDialer = map[string]ProtocolDialer{}
diff --git a/mssql.go b/mssql.go
index d036129..28288a4 100644
--- a/mssql.go
+++ b/mssql.go
@@ -41,6 +41,7 @@ func init() {
 		}
 		return netDialer{&net.Dialer{KeepAlive: ka}}
 	}
+	msdsn.ProtocolDialers["tcp"] = tcpDialer{}
 }
 
 var createDialer func(p *msdsn.Config) Dialer
diff --git a/namedpipe/namedpipe.go b/namedpipe/namedpipe.go
new file mode 100644
index 0000000..d33e8a6
--- /dev/null
+++ b/namedpipe/namedpipe.go
@@ -0,0 +1,18 @@
+package namedpipe
+
+import (
+	"runtime"
+
+	"github.com/microsoft/go-mssqldb/msdsn"
+)
+
+type namedPipeDialer struct{}
+
+var dialer namedPipeDialer = namedPipeDialer{}
+
+func init() {
+	if runtime.GOOS == "windows" {
+		msdsn.ProtocolParsers = append(msdsn.ProtocolParsers, dialer)
+		msdsn.ProtocolDialers["np"] = dialer
+	}
+}
diff --git a/namedpipe/namedpipe_windows.go b/namedpipe/namedpipe_windows.go
new file mode 100644
index 0000000..d69bed1
--- /dev/null
+++ b/namedpipe/namedpipe_windows.go
@@ -0,0 +1,76 @@
+package namedpipe
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"reflect"
+	"strings"
+	"time"
+
+	"github.com/microsoft/go-mssqldb/msdsn"
+	"gopkg.in/natefinch/npipe.v2"
+)
+
+type namedPipeData struct {
+	PipeName string
+}
+
+func (n namedPipeDialer) ParseServer(server string, p *msdsn.Config) error {
+	// assume a server name starting with \\ is the full named pipe path
+	if strings.HasPrefix(server, `\\`) {
+		p.ProtocolParameters[n.Protocol()] = &namedPipeData{PipeName: server}
+	} else if p.Port > 0 {
+		return fmt.Errorf("Named pipes disallowed due to port being specified")
+	} else if p.Host == "" { // if the string specifies np:host\instance, tcpParser won't have filled in p.Host
+		parts := strings.SplitN(server, `\`, 2)
+		p.Host = parts[0]
+		if p.Host == "." || strings.ToUpper(p.Host) == "(LOCAL)" {
+			p.Host = "localhost"
+		}
+		if len(parts) > 1 {
+			p.Instance = parts[1]
+		}
+	}
+	return nil
+}
+
+func (n namedPipeDialer) Protocol() string {
+	return "np"
+}
+
+func (n namedPipeDialer) ParseBrowserData(data msdsn.BrowserData, p *msdsn.Config) error {
+	// If instance is specified, but no port, check SQL Server Browser
+	// for the instance and discover its port.
+	p.Instance = strings.ToUpper(p.Instance)
+	instance := p.Instance
+	if instance == "" {
+		instance = "MSSQLSERVER"
+	}
+	pipename, ok := data[instance]["np"]
+	if !ok {
+		f := "no named pipe instance matching '%v' returned from host '%v'"
+		return fmt.Errorf(f, p.Instance, p.Host)
+	}
+	p.ProtocolParameters[n.Protocol()] = namedPipeData{PipeName: pipename}
+	return nil
+}
+
+func (n namedPipeDialer) DialConnection(ctx context.Context, p msdsn.Config) (conn net.Conn, err error) {
+	data := p.ProtocolParameters[n.Protocol()]
+	switch d := data.(type) {
+	case namedPipeData:
+		dl, ok := ctx.Deadline()
+		if ok {
+			duration := dl.Sub(time.Now())
+			return npipe.DialTimeout(d.PipeName, duration)
+		}
+		return npipe.Dial(d.PipeName)
+	}
+	return nil, fmt.Errorf("Unexpected protocol data specified for connection: %v", reflect.TypeOf(data))
+}
+
+func (n namedPipeDialer) CallBrowser(p *msdsn.Config) bool {
+	_, ok := p.ProtocolParameters[n.Protocol()]
+	return !ok
+}
diff --git a/namedpipe/stub.go b/namedpipe/stub.go
new file mode 100644
index 0000000..acf5ed2
--- /dev/null
+++ b/namedpipe/stub.go
@@ -0,0 +1,33 @@
+//go:build !windows
+// +build !windows
+
+package namedpipe
+
+import (
+	"context"
+	"fmt"
+	"net"
+
+	"github.com/microsoft/go-mssqldb/msdsn"
+)
+
+func (n namedPipeDialer) ParseServer(server string, p *msdsn.Config) error {
+	return fmt.Errorf("Named pipe connections are not supported on this operating system")
+}
+
+func (n namedPipeDialer) Protocol() string {
+	return "np"
+}
+
+func (n namedPipeDialer) ParseBrowserData(data msdsn.BrowserData, p *msdsn.Config) error {
+	return fmt.Errorf("Named pipe connections are not supported on this operating system")
+}
+
+func (n namedPipeDialer) DialConnection(ctx context.Context, p msdsn.Config) (conn net.Conn, err error) {
+
+	return nil, fmt.Errorf("Named pipe connections are not supported on this operating system")
+}
+
+func (n namedPipeDialer) CallBrowser(p *msdsn.Config) bool {
+	return false
+}
diff --git a/protocol.go b/protocol.go
new file mode 100644
index 0000000..a3d3513
--- /dev/null
+++ b/protocol.go
@@ -0,0 +1,118 @@
+package mssql
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"strconv"
+	"strings"
+
+	"github.com/microsoft/go-mssqldb/msdsn"
+)
+
+type MssqlProtocolDialer interface {
+	// DialSqlConnection creates a net.Conn from a Connector based on the Config
+	DialSqlConnection(ctx context.Context, c *Connector, p msdsn.Config) (conn net.Conn, err error)
+}
+
+type tcpDialer struct{}
+
+func (t tcpDialer) ParseBrowserData(data msdsn.BrowserData, p *msdsn.Config) error {
+	// If instance is specified, but no port, check SQL Server Browser
+	// for the instance and discover its port.
+	p.Instance = strings.ToUpper(p.Instance)
+	strport, ok := data[p.Instance]["tcp"]
+	if !ok {
+		f := "no instance matching '%v' returned from host '%v'"
+		return fmt.Errorf(f, p.Instance, p.Host)
+	}
+	port, err := strconv.ParseUint(strport, 0, 16)
+	if err != nil {
+		f := "invalid tcp port returned from Sql Server Browser '%v': %v"
+		return fmt.Errorf(f, strport, err.Error())
+	}
+	p.Port = port
+	return nil
+}
+
+func (t tcpDialer) DialConnection(ctx context.Context, p msdsn.Config) (conn net.Conn, err error) {
+	return nil, fmt.Errorf("tcp dialer requires a Connector instance")
+}
+
+// SQL Server AlwaysOn Availability Group Listeners are bound by DNS to a
+// list of IP addresses.  So if there is more than one, try them all and
+// use the first one that allows a connection.
+func (t tcpDialer) DialSqlConnection(ctx context.Context, c *Connector, p msdsn.Config) (conn net.Conn, err error) {
+	var ips []net.IP
+	ip := net.ParseIP(p.Host)
+	if ip == nil {
+		ips, err = net.LookupIP(p.Host)
+		if err != nil {
+			return
+		}
+	} else {
+		ips = []net.IP{ip}
+	}
+	if len(ips) == 1 {
+		d := c.getDialer(&p)
+		addr := net.JoinHostPort(ips[0].String(), strconv.Itoa(int(resolveServerPort(p.Port))))
+		conn, err = d.DialContext(ctx, "tcp", addr)
+
+	} else {
+		//Try Dials in parallel to avoid waiting for timeouts.
+		connChan := make(chan net.Conn, len(ips))
+		errChan := make(chan error, len(ips))
+		portStr := strconv.Itoa(int(resolveServerPort(p.Port)))
+		for _, ip := range ips {
+			go func(ip net.IP) {
+				d := c.getDialer(&p)
+				addr := net.JoinHostPort(ip.String(), portStr)
+				conn, err := d.DialContext(ctx, "tcp", addr)
+				if err == nil {
+					connChan <- conn
+				} else {
+					errChan <- err
+				}
+			}(ip)
+		}
+		// Wait for either the *first* successful connection, or all the errors
+	wait_loop:
+		for i := range ips {
+			select {
+			case conn = <-connChan:
+				// Got a connection to use, close any others
+				go func(n int) {
+					for i := 0; i < n; i++ {
+						select {
+						case conn := <-connChan:
+							conn.Close()
+						case <-errChan:
+						}
+					}
+				}(len(ips) - i - 1)
+				// Remove any earlier errors we may have collected
+				err = nil
+				break wait_loop
+			case err = <-errChan:
+			}
+		}
+	}
+	// Can't do the usual err != nil check, as it is possible to have gotten an error before a successful connection
+	if conn == nil {
+		f := "unable to open tcp connection with host '%v:%v': %v"
+		return nil, fmt.Errorf(f, p.Host, resolveServerPort(p.Port), err.Error())
+	}
+	return conn, err
+}
+
+func (t tcpDialer) CallBrowser(p *msdsn.Config) bool {
+	return len(p.Instance) > 0 && p.Port == 0
+}
+
+func resolveServerPort(port uint64) uint64 {
+	if port == 0 {
+		return defaultServerPort
+	}
+
+	return port
+}
diff --git a/tds.go b/tds.go
index 82da5f0..abbe063 100644
--- a/tds.go
+++ b/tds.go
@@ -10,7 +10,6 @@ import (
 	"io/ioutil"
 	"net"
 	"sort"
-	"strconv"
 	"strings"
 	"time"
 	"unicode/utf16"
@@ -20,8 +19,8 @@ import (
 	"github.com/microsoft/go-mssqldb/msdsn"
 )
 
-func parseInstances(msg []byte) map[string]map[string]string {
-	results := map[string]map[string]string{}
+func parseInstances(msg []byte) msdsn.BrowserData {
+	results := msdsn.BrowserData{}
 	if len(msg) > 3 && msg[0] == 5 {
 		out_s := string(msg[3:])
 		tokens := strings.Split(out_s, ";")
@@ -49,8 +48,8 @@ func parseInstances(msg []byte) map[string]map[string]string {
 	return results
 }
 
-func getInstances(ctx context.Context, d Dialer, address string) (map[string]map[string]string, error) {
-	conn, err := d.DialContext(ctx, "udp", address+":1434")
+func getInstances(ctx context.Context, d Dialer, address string) (msdsn.BrowserData, error) {
+	conn, err := d.DialContext(ctx, "udp", net.JoinHostPort(address, "1434"))
 	if err != nil {
 		return nil, err
 	}
@@ -837,70 +836,30 @@ func sendAttention(buf *tdsBuffer) error {
 	return buf.FinishPacket()
 }
 
-// SQL Server AlwaysOn Availability Group Listeners are bound by DNS to a
-// list of IP addresses.  So if there is more than one, try them all and
-// use the first one that allows a connection.
-func dialConnection(ctx context.Context, c *Connector, p msdsn.Config) (conn net.Conn, err error) {
-	var ips []net.IP
-	ip := net.ParseIP(p.Host)
-	if ip == nil {
-		ips, err = net.LookupIP(p.Host)
-		if err != nil {
-			return
+// Makes an attempt to connect with each available protocol, in order, until one succeeds or the timeout elapses
+func dialConnection(ctx context.Context, c *Connector, p msdsn.Config, logger ContextLogger) (conn net.Conn, err error) {
+	for _, protocol := range p.Protocols {
+		dialer := msdsn.ProtocolDialers[protocol]
+		sqlDialer, ok := dialer.(MssqlProtocolDialer)
+		if logger != nil {
+			logger.Log(ctx, msdsn.LogDebug, "Dialing with protocol "+protocol)
 		}
-	} else {
-		ips = []net.IP{ip}
-	}
-	if len(ips) == 1 {
-		d := c.getDialer(&p)
-		addr := net.JoinHostPort(ips[0].String(), strconv.Itoa(int(resolveServerPort(p.Port))))
-		conn, err = d.DialContext(ctx, "tcp", addr)
-
-	} else {
-		//Try Dials in parallel to avoid waiting for timeouts.
-		connChan := make(chan net.Conn, len(ips))
-		errChan := make(chan error, len(ips))
-		portStr := strconv.Itoa(int(resolveServerPort(p.Port)))
-		for _, ip := range ips {
-			go func(ip net.IP) {
-				d := c.getDialer(&p)
-				addr := net.JoinHostPort(ip.String(), portStr)
-				conn, err := d.DialContext(ctx, "tcp", addr)
-				if err == nil {
-					connChan <- conn
-				} else {
-					errChan <- err
-				}
-			}(ip)
+		if !ok {
+			conn, err = dialer.DialConnection(ctx, p)
+		} else {
+			conn, err = sqlDialer.DialSqlConnection(ctx, c, p)
 		}
-		// Wait for either the *first* successful connection, or all the errors
-	wait_loop:
-		for i := range ips {
-			select {
-			case conn = <-connChan:
-				// Got a connection to use, close any others
-				go func(n int) {
-					for i := 0; i < n; i++ {
-						select {
-						case conn := <-connChan:
-							conn.Close()
-						case <-errChan:
-						}
-					}
-				}(len(ips) - i - 1)
-				// Remove any earlier errors we may have collected
-				err = nil
-				break wait_loop
-			case err = <-errChan:
+		if err != nil && logger != nil {
+			logger.Log(ctx, msdsn.LogErrors, "Unable to connect with protocol "+protocol+":"+err.Error())
+		}
+		if conn != nil {
+			if logger != nil {
+				logger.Log(ctx, msdsn.LogDebug, "Returning connection from protocol "+protocol)
 			}
+			return
 		}
 	}
-	// Can't do the usual err != nil check, as it is possible to have gotten an error before a successful connection
-	if conn == nil {
-		f := "unable to open tcp connection with host '%v:%v': %v"
-		return nil, fmt.Errorf(f, p.Host, resolveServerPort(p.Port), err.Error())
-	}
-	return conn, err
+	return
 }
 
 func preparePreloginFields(p msdsn.Config, fe *featureExtFedAuth) map[uint8][]byte {
@@ -1029,12 +988,58 @@ func prepareLogin(ctx context.Context, c *Connector, p msdsn.Config, logger Cont
 	return l, nil
 }
 
+func queryBrowser(ctx context.Context, dialCtx context.Context, c *Connector, logger ContextLogger, p *msdsn.Config) error {
+	var instances msdsn.BrowserData
+	var err error
+	pErrors := make(map[string]error)
+	for _, protocol := range p.Protocols {
+		pd, ok := msdsn.ProtocolDialers[protocol]
+		if !ok {
+			return fmt.Errorf("No dialer is configured for protocol '%s'", protocol)
+		}
+		if pd.CallBrowser(p) {
+			if instances == nil {
+				d := c.getDialer(p)
+				instances, err = getInstances(dialCtx, d, p.Host)
+				if err != nil {
+					f := "unable to get instances from Sql Server Browser on host %v: %v"
+					return fmt.Errorf(f, p.Host, err.Error())
+				}
+			}
+			pErr := pd.ParseBrowserData(instances, p)
+			if pErr != nil {
+				pErrors[protocol] = pErr
+				if logger != nil {
+					logger.Log(ctx, msdsn.Log(logErrors), "Removing protocol "+protocol+" from dialers. Error:"+pErr.Error())
+				}
+			}
+		}
+	}
+	// If any dialer got an error parsing instances, remove it from the dialer list
+	// If no dialers are left, return an error
+	if len(pErrors) == len(p.Protocols) {
+		return fmt.Errorf("Unable to find a matching instance for any supported protocol on host %v", p.Host)
+	}
+	if len(pErrors) > 0 {
+		validProtocols := make([]string, len(p.Protocols)-len(pErrors))
+		i := 0
+		for _, protocol := range p.Protocols {
+			_, hasError := pErrors[protocol]
+			if !hasError {
+				validProtocols[i] = protocol
+				i++
+			}
+		}
+	}
+	return nil
+}
+
 func connect(ctx context.Context, c *Connector, logger ContextLogger, p msdsn.Config) (res *tdsSession, err error) {
 	dialCtx := ctx
 	if p.DialTimeout >= 0 {
 		dt := p.DialTimeout
 		if dt == 0 {
-			dt = 15 * time.Second
+			dt = time.Duration(15*len(p.Protocols)) * time.Second
 		}
 		var cancel func()
 		dialCtx, cancel = context.WithTimeout(ctx, dt)
@@ -1047,29 +1052,12 @@ func connect(ctx context.Context, c *Connector, logger ContextLogger, p msdsn.Co
 		// when port is specified instance name is not used
 		// you should not provide instance name when you provide port
 		logger.Log(ctx, msdsn.LogDebug, "WARN: You specified both instance name and port in the connection string, port will be used and instance name will be ignored")
-	} else if len(p.Instance) > 0 && p.Port == 0 {
-		// If instance is specified, but no port, check SQL Server Browser
-		// for the instance and discover its port.
-		p.Instance = strings.ToUpper(p.Instance)
-		d := c.getDialer(&p)
-		instances, err := getInstances(dialCtx, d, p.Host)
-		if err != nil {
-			f := "unable to get instances from Sql Server Browser on host %v: %v"
-			return nil, fmt.Errorf(f, p.Host, err.Error())
-		}
-		strport, ok := instances[p.Instance]["tcp"]
-		if !ok {
-			f := "no instance matching '%v' returned from host '%v'"
-			return nil, fmt.Errorf(f, p.Instance, p.Host)
-		}
-		port, err := strconv.ParseUint(strport, 0, 16)
-		if err != nil {
-			f := "invalid tcp port returned from Sql Server Browser '%v': %v"
-			return nil, fmt.Errorf(f, strport, err.Error())
-		}
-		p.Port = port
 	}
 
+	err = queryBrowser(ctx, dialCtx, c, logger, &p)
+	if err != nil {
+		return nil, err
+	}
 	if p.Port == 0 {
 		p.Port = defaultServerPort
 	}
@@ -1089,7 +1077,7 @@ func connect(ctx context.Context, c *Connector, logger ContextLogger, p msdsn.Co
 	}
 
 initiate_connection:
-	conn, err := dialConnection(dialCtx, c, p)
+	conn, err := dialConnection(dialCtx, c, p, logger)
 	if err != nil {
 		return nil, err
 	}
@@ -1279,11 +1267,3 @@ initiate_connection:
 	}
 	return &sess, nil
 }
-
-func resolveServerPort(port uint64) uint64 {
-	if port == 0 {
-		return defaultServerPort
-	}
-
-	return port
-}
diff --git a/token.go b/token.go
index 5f9e820..d8cf772 100644
--- a/token.go
+++ b/token.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
+	"net"
 	"strconv"
 
 	"github.com/golang-sql/sqlexp"
@@ -674,6 +675,13 @@ func processSingleResponse(ctx context.Context, sess *tdsSession, ch chan tokenS
 		if sess.logFlags&logErrors != 0 {
 			sess.logger.Log(ctx, msdsn.LogErrors, fmt.Sprintf("BeginRead failed %v", err))
 		}
+		switch e := err.(type) {
+		case *net.OpError:
+			err = e
+		default:
+			// the named pipe provider returns a raw win32 error so fake an OpError
+			err = &net.OpError{Op: "Read", Err: err}
+		}
 		ch <- err
 		return
 	}


Test patch:
diff --git a/accesstokenconnector_test.go b/accesstokenconnector_test.go
index 52940fc..303c531 100644
--- a/accesstokenconnector_test.go
+++ b/accesstokenconnector_test.go
@@ -1,3 +1,4 @@
+//go:build go1.10
 // +build go1.10
 
 package mssql
@@ -79,7 +80,7 @@ func TestNewAccessTokenConnector(t *testing.T) {
 
 func TestAccessTokenConnectorFailsToConnectIfNoAccessToken(t *testing.T) {
 	errorText := "This is a test"
-	dsn := "Server=server.database.windows.net;Database=db"
+	dsn := "Server=tcp:server.database.windows.net;Database=db"
 	tp := func() (string, error) { return "", errors.New(errorText) }
 	sut, err := NewAccessTokenConnector(dsn, tp)
 	if err != nil {
diff --git a/bad_server_test.go b/bad_server_test.go
index ff0e57d..2c29c81 100644
--- a/bad_server_test.go
+++ b/bad_server_test.go
@@ -47,7 +47,7 @@ func testBadServer(t *testing.T, handler func(net.Conn)) {
 	tl := testLogger{t: t}
 	defer tl.StopLogging()
 	SetLogger(&tl)
-	testConnectionBad(t, fmt.Sprintf("host=%s;port=%d;log=255", addr.IP.String(), addr.Port))
+	_ = testConnectionBad(t, fmt.Sprintf("host=%s;port=%d;log=255;protocol=tcp", addr.IP.String(), addr.Port))
 }
 
 func TestBadServerCloseConnection(t *testing.T) {
diff --git a/msdsn/conn_str_test.go b/msdsn/conn_str_test.go
index 20a6cd2..8b0f16f 100644
--- a/msdsn/conn_str_test.go
+++ b/msdsn/conn_str_test.go
@@ -203,7 +203,7 @@ func TestSplitConnectionStringURL(t *testing.T) {
 }
 
 func TestConnParseRoundTripFixed(t *testing.T) {
-	connStr := "sqlserver://sa:sa@localhost/sqlexpress?database=master&log=127&disableretry=true"
+	connStr := "sqlserver://sa:sa@localhost/sqlexpress?database=master&log=127&disableretry=true&dial+timeout=30"
 	params, err := Parse(connStr)
 	if err != nil {
 		t.Fatal("Test URL is not valid", err)
diff --git a/msdsn/protocolparse_test.go b/msdsn/protocolparse_test.go
new file mode 100644
index 0000000..8ea712c
--- /dev/null
+++ b/msdsn/protocolparse_test.go
@@ -0,0 +1,78 @@
+package msdsn
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+)
+
+type testProtocol struct{}
+
+var protocolImpl = testProtocol{}
+
+func (t testProtocol) ParseServer(server string, p *Config) error {
+	if strings.HasPrefix(server, "**") {
+		p.ProtocolParameters[t.Protocol()] = "special"
+	}
+	if server == "fail" {
+		return fmt.Errorf("ParseServer fail")
+	}
+	// p.Host is empty if tst protocol was specified
+	if p.Host == "" {
+		p.Host = strings.TrimPrefix(server, "**")
+	}
+	return nil
+}
+
+func (t testProtocol) Protocol() string {
+	return "tst"
+}
+
+func init() {
+	ProtocolParsers = append(ProtocolParsers, protocolImpl)
+}
+
+func TestProtocolParseExtension(t *testing.T) {
+	type tst struct {
+		dsn            string
+		expectedConfig func(c *Config) bool
+	}
+	tests := []tst{
+		{"server=myserver", func(c *Config) bool {
+			return len(c.Protocols) == 2 && c.Protocols[0] == "tcp" && c.Protocols[1] == "tst" && c.Host == "myserver" && c.ProtocolParameters["tst"] == nil
+		}},
+		{"server=**myserver", func(c *Config) bool {
+			return len(c.Protocols) == 2 && c.Protocols[0] == "tcp" && c.Protocols[1] == "tst" && c.Host == "**myserver" && c.ProtocolParameters["tst"] == "special"
+		}},
+		{"server=tst:**myserver", func(c *Config) bool {
+			return len(c.Protocols) == 1 && c.Protocols[0] == "tst" && c.Host == "myserver" && c.ProtocolParameters["tst"] == "special"
+		}},
+		{"server=tst:myserver", func(c *Config) bool {
+			return len(c.Protocols) == 1 && c.Protocols[0] == "tst" && c.Host == "myserver" && c.ProtocolParameters["tst"] == nil
+		}},
+		{"sqlserver://user@myserver", func(c *Config) bool {
+			return len(c.Protocols) == 2 && c.Protocols[0] == "tcp" && c.Protocols[1] == "tst" && c.Host == "myserver" && c.ProtocolParameters["tst"] == nil
+		}},
+		{"sqlserver://**myserver", func(c *Config) bool {
+			return len(c.Protocols) == 2 && c.Protocols[0] == "tcp" && c.Protocols[1] == "tst" && c.Host == "**myserver" && c.ProtocolParameters["tst"] == "special"
+		}},
+		{"sqlserver://**myserver?protocol=tst", func(c *Config) bool {
+			return len(c.Protocols) == 1 && c.Protocols[0] == "tst" && c.Host == "myserver" && c.ProtocolParameters["tst"] == "special"
+		}},
+		{"sqlserver://myserver?protocol=tst", func(c *Config) bool {
+			return len(c.Protocols) == 1 && c.Protocols[0] == "tst" && c.Host == "myserver" && c.ProtocolParameters["tst"] == nil
+		}},
+		{"sqlserver://fail", func(c *Config) bool {
+			return len(c.Protocols) == 1 && c.Protocols[0] == "tcp" && c.Host == "fail" && c.ProtocolParameters["tst"] == nil
+		}},
+	}
+	for _, test := range tests {
+		c, err := Parse(test.dsn)
+		if err != nil {
+			t.Fatalf("Unexpected error parsing '%s':'%s'", test.dsn, err.Error())
+		}
+		if !test.expectedConfig(&c) {
+			t.Fatalf("Config validation failed for '%s'. Config: '%v'", test.dsn, c)
+		}
+	}
+}
diff --git a/namedpipe_test.go b/namedpipe_test.go
new file mode 100644
index 0000000..a8fcfe5
--- /dev/null
+++ b/namedpipe_test.go
@@ -0,0 +1,16 @@
+//go:build np
+// +build np
+
+package mssql
+
+import (
+	"github.com/microsoft/go-mssqldb/msdsn"
+	_ "github.com/microsoft/go-mssqldb/namedpipe"
+	"testing"
+)
+
+func TestNamedPipeProtocolInstalled(t *testing.T) {
+	if len(msdsn.ProtocolParsers) != 2 {
+		t.Fatal("np protocol not registered")
+	}
+}
diff --git a/net_go116_test.go b/net_go116_test.go
index 27776d3..c1b4952 100644
--- a/net_go116_test.go
+++ b/net_go116_test.go
@@ -1,3 +1,4 @@
+//go:build go1.16
 // +build go1.16
 
 package mssql
@@ -56,6 +57,7 @@ func TestTimeoutConn(t *testing.T) {
 }
 
 func TestTLSHandshakeConn(t *testing.T) {
+	skipIfNamedPipesEnabled(t)
 	tl := testLogger{t: t}
 	defer tl.StopLogging()
 	SetLogger(&tl)
@@ -68,7 +70,7 @@ func TestTLSHandshakeConn(t *testing.T) {
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
 	defer cancel()
 
-	toconn, err := dialConnection(ctx, connector, connector.params)
+	toconn, err := dialConnection(ctx, connector, connector.params, nil)
 	if err != nil {
 		t.Error(err)
 	}
@@ -115,6 +117,7 @@ func TestTLSHandshakeConn(t *testing.T) {
 }
 
 func TestPassthroughConn(t *testing.T) {
+	skipIfNamedPipesEnabled(t)
 	tl := testLogger{t: t}
 	defer tl.StopLogging()
 	SetLogger(&tl)
@@ -127,7 +130,7 @@ func TestPassthroughConn(t *testing.T) {
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
 	defer cancel()
 
-	toconn, err := dialConnection(ctx, connector, connector.params)
+	toconn, err := dialConnection(ctx, connector, connector.params, nil)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/queries_go19_test.go b/queries_go19_test.go
index 60cde99..6f462f0 100644
--- a/queries_go19_test.go
+++ b/queries_go19_test.go
@@ -746,6 +746,7 @@ func TestParamNoName(t *testing.T) {
 //
 // Issue https://github.com/denisenkom/go-mssqldb/issues/166
 func TestTLSServerReadClose(t *testing.T) {
+	skipIfNamedPipesEnabled(t)
 	query := `
 with
     config_cte (config) as (
@@ -1079,7 +1080,7 @@ func TestMessageQueue(t *testing.T) {
 	defer conn.Close()
 	defer logger.StopLogging()
 	retmsg := &sqlexp.ReturnMessage{}
-	latency, _ := getLatency(t)
+	latency := getLatency(t)
 	ctx, cancel := context.WithTimeout(context.Background(), latency+200000*time.Millisecond)
 	defer cancel()
 	rows, err := conn.QueryContext(ctx, "PRINT 'msg1'; select 100 as c; PRINT 'msg2'", retmsg)
@@ -1261,7 +1262,7 @@ func TestTimeoutWithNoResults(t *testing.T) {
 	defer conn.Close()
 	defer logger.StopLogging()
 
-	latency, _ := getLatency(t)
+	latency := getLatency(t)
 	ctx, cancel := context.WithTimeout(context.Background(), latency+5000*time.Millisecond)
 	defer cancel()
 	retmsg := &sqlexp.ReturnMessage{}
@@ -1299,21 +1300,27 @@ func TestCancelWithNoResults(t *testing.T) {
 	defer conn.Close()
 	defer logger.StopLogging()
 
-	latency, _ := getLatency(t)
-	ctx, cancel := context.WithTimeout(context.Background(), latency+5000*time.Millisecond)
+	latency := getLatency(t)
+
+	ctx, cancel := context.WithTimeout(context.Background(), latency+15000*time.Millisecond)
 	retmsg := &sqlexp.ReturnMessage{}
+	t.Logf("Calling QueryContext")
 	r, err := conn.QueryContext(ctx, `waitfor delay '00:00:15'; select 100`, retmsg)
+	t.Logf("QueryContext returned")
 	if err != nil {
 		cancel()
 		t.Fatal(err.Error())
 	}
 	defer r.Close()
-	time.Sleep(latency + 100*time.Millisecond)
+	// QueryContext doesn't return until the connection is established
+	time.Sleep(100 * time.Millisecond)
+	t.Logf("Calling Cancel")
 	cancel()
+	t.Logf("Cancel returned")
 	active := true
 	for active {
 		msg := retmsg.Message(ctx)
-		t.Logf("Got a message: %s", reflect.TypeOf(msg))
+		t.Logf("%v Got a message: %s", time.Now(), reflect.TypeOf(msg))
 		switch m := msg.(type) {
 		case sqlexp.MsgNextResultSet:
 			active = r.NextResultSet()
@@ -1371,7 +1378,7 @@ func TestSprocWithCursorNoResult(t *testing.T) {
 		t.Fatalf("Unable to create test sproc: %v", e)
 	}
 	defer conn.Exec(DropSprocWithCursor)
-	latency, _ := getLatency(t)
+	latency := getLatency(t)
 	ctx, cancel := context.WithTimeout(context.Background(), latency+500*time.Millisecond)
 	defer cancel()
 	retmsg := &sqlexp.ReturnMessage{}
@@ -1415,7 +1422,7 @@ func TestErrorAsLastResult(t *testing.T) {
 	conn, logger := open(t)
 	defer conn.Close()
 	defer logger.StopLogging()
-	latency, _ := getLatency(t)
+	latency := getLatency(t)
 	ctx, cancel := context.WithTimeout(context.Background(), latency+5000*time.Millisecond)
 	defer cancel()
 	retmsg := &sqlexp.ReturnMessage{}
diff --git a/queries_test.go b/queries_test.go
index 20bcfb6..e8ac23d 100644
--- a/queries_test.go
+++ b/queries_test.go
@@ -1499,7 +1499,7 @@ func TestLongConnection(t *testing.T) {
 		{"2", "00:00:01", 6 * time.Second, false},
 
 		// Check no connection timeout.
-		{"0", "00:00:01", 2 * time.Second, false},
+		{"0", "00:00:01", 6 * time.Second, false},
 		// {"0", "00:00:45", 60 * time.Second, false}, // Skip for normal testing to limit time.
 	}
 
@@ -1632,42 +1632,46 @@ func TestColumnTypeIntrospection(t *testing.T) {
 	defer conn.Close()
 	defer logger.StopLogging()
 	for _, tt := range tests {
-		rows, err := conn.Query("select " + tt.expr)
-		if err != nil {
-			t.Fatalf("Query failed with unexpected error %s", err)
-		}
-		defer rows.Close()
-		ct, err := rows.ColumnTypes()
-		if err != nil {
-			t.Fatalf("Query failed with unexpected error %s", err)
-		}
-		if ct[0].DatabaseTypeName() != tt.typeName {
-			t.Errorf("Expected type %s but returned %s", tt.typeName, ct[0].DatabaseTypeName())
-		}
-		size, ok := ct[0].Length()
-		if ok != tt.hasSize {
-			t.Errorf("Expected has size %v but returned %v for %s", tt.hasSize, ok, tt.expr)
-		} else {
-			if ok && size != tt.size {
-				t.Errorf("Expected size %d but returned %d for %s", tt.size, size, tt.expr)
+		t.Run(tt.expr, func(t *testing.T) {
+			rows, err := conn.Query("select " + tt.expr)
+			if err != nil {
+				t.Fatalf("Query failed with unexpected error %s", err)
 			}
-		}
-
-		prec, scale, ok := ct[0].DecimalSize()
-		if ok != tt.hasPrecScale {
-			t.Errorf("Expected has prec/scale %v but returned %v for %s", tt.hasPrecScale, ok, tt.expr)
-		} else {
-			if ok && prec != tt.precision {
-				t.Errorf("Expected precision %d but returned %d for %s", tt.precision, prec, tt.expr)
+			// defer keyword is function scoped, so calling it inside a for loop within one function
+			// prevents the open connection from being reused in subsequent loop iterations
+			defer rows.Close()
+			ct, err := rows.ColumnTypes()
+			if err != nil {
+				t.Fatalf("Query failed with unexpected error %s", err)
 			}
-			if ok && scale != tt.scale {
-				t.Errorf("Expected scale %d but returned %d for %s", tt.scale, scale, tt.expr)
+			if ct[0].DatabaseTypeName() != tt.typeName {
+				t.Errorf("Expected type %s but returned %s", tt.typeName, ct[0].DatabaseTypeName())
+			}
+			size, ok := ct[0].Length()
+			if ok != tt.hasSize {
+				t.Errorf("Expected has size %v but returned %v for %s", tt.hasSize, ok, tt.expr)
+			} else {
+				if ok && size != tt.size {
+					t.Errorf("Expected size %d but returned %d for %s", tt.size, size, tt.expr)
+				}
 			}
-		}
 
-		if ct[0].ScanType() != tt.reflType {
-			t.Errorf("Expected ScanType %v but got %v for %s", tt.reflType, ct[0].ScanType(), tt.expr)
-		}
+			prec, scale, ok := ct[0].DecimalSize()
+			if ok != tt.hasPrecScale {
+				t.Errorf("Expected has prec/scale %v but returned %v for %s", tt.hasPrecScale, ok, tt.expr)
+			} else {
+				if ok && prec != tt.precision {
+					t.Errorf("Expected precision %d but returned %d for %s", tt.precision, prec, tt.expr)
+				}
+				if ok && scale != tt.scale {
+					t.Errorf("Expected scale %d but returned %d for %s", tt.scale, scale, tt.expr)
+				}
+			}
+
+			if ct[0].ScanType() != tt.reflType {
+				t.Errorf("Expected ScanType %v but got %v for %s", tt.reflType, ct[0].ScanType(), tt.expr)
+			}
+		})
 	}
 }
 
@@ -2008,7 +2012,7 @@ func TestQueryCancelHighLevel(t *testing.T) {
 	defer logger.StopLogging()
 
 	ctx, cancel := context.WithCancel(context.Background())
-	latency, _ := getLatency(t)
+	latency := getLatency(t)
 
 	go func() {
 		time.Sleep(latency + 500*time.Millisecond)
@@ -2029,35 +2033,38 @@ func TestQueryCancelHighLevel(t *testing.T) {
 }
 
 // returns the minimum time needed to connect to the server
-// Minimum return value is 1 ms, max is 2k ms
-func getLatency(t *testing.T) (latency time.Duration, increment time.Duration) {
-	latency = 1 * time.Millisecond
-	connstr := makeConnStr(t)
-	// .Host may not have a port
-	host := connstr.Hostname()
-	port := connstr.Port()
-	increment = 50 * time.Millisecond
-	if host == "." || host == "localhost" {
-		increment = 1 * time.Millisecond
-	}
-	if port == "" {
-		port = "1433"
-	}
-	for latency < 2000*time.Millisecond {
-		t.Logf("Dialing host %s with timeout %s", host, latency)
-		_, err := net.DialTimeout("tcp", host+":"+port, latency)
-		if err == nil {
-			return latency, increment
-		}
-		if oe, ok := err.(*net.OpError); ok {
-			if !oe.Timeout() {
-				t.Logf("Got non-timeout error %s", oe.Error())
-				return latency, increment
+func getLatency(t *testing.T) time.Duration {
+	t.Helper()
+	params, err := msdsn.Parse(makeConnStr(t).String())
+	if err != nil {
+		t.Fatalf("Invalid connection string: %s", err.Error())
+	}
+	c := &Connector{params: params}
+	now := time.Now()
+	if err := queryBrowser(context.Background(), context.Background(), c, nil, &params); err != nil {
+		t.Fatalf("queryBrowser failed: %s", err.Error())
+	}
+	// Dialing both tcp and np for a named-pipes only connection takes a long time
+	if len(params.Protocols) > 1 && testing.Short() {
+		t.Skip("short")
+	}
+	for _, protocol := range params.Protocols {
+		dialer := msdsn.ProtocolDialers[protocol]
+		sqlDialer, ok := dialer.(MssqlProtocolDialer)
+		if !ok {
+			conn, err := dialer.DialConnection(context.Background(), params)
+			if err == nil {
+				conn.Close()
+			}
+		} else {
+			conn, err := sqlDialer.DialSqlConnection(context.Background(), c, params)
+			if err == nil {
+				conn.Close()
 			}
-			latency += increment
 		}
+
 	}
-	return latency, increment
+	return time.Since(now)
 }
 
 func TestQueryTimeout(t *testing.T) {
@@ -2065,7 +2072,7 @@ func TestQueryTimeout(t *testing.T) {
 	defer conn.Close()
 	defer logger.StopLogging()
 	// 2 seconds should be enough time to complete the login
-	latency, _ := getLatency(t)
+	latency := getLatency(t)
 	ctx, cancel := context.WithTimeout(context.Background(), latency+2000*time.Millisecond)
 	defer cancel()
 	_, err := conn.ExecContext(ctx, "waitfor delay '00:00:20'")
@@ -2089,9 +2096,9 @@ func TestLoginTimeout(t *testing.T) {
 	defer logger.StopLogging()
 	// Try to timeout during the login using a small delta from raw connect time
 	// In environments where latency is really low this degenerates into TestQueryTimeout
-	latency, increment := getLatency(t)
+	latency := getLatency(t)
 
-	ctx, cancel := context.WithTimeout(context.Background(), latency+(2*increment))
+	ctx, cancel := context.WithTimeout(context.Background(), latency+200*time.Millisecond)
 	defer cancel()
 	_, err := conn.ExecContext(ctx, "waitfor delay '00:00:03'")
 	t.Logf("Got error type %v: %s ", reflect.TypeOf(err), err.Error())
@@ -2288,6 +2295,8 @@ func TestDisconnect1(t *testing.T) {
 	if testing.Short() {
 		t.Skip("short")
 	}
+
+	skipIfNamedPipesEnabled(t)
 	checkConnStr(t)
 	tl := testLogger{t: t}
 	defer tl.StopLogging()
@@ -2344,6 +2353,7 @@ func TestDisconnect1(t *testing.T) {
 // TestDisconnect2 tests a read error so the query is started
 // but results cannot be read.
 func TestDisconnect2(t *testing.T) {
+	skipIfNamedPipesEnabled(t)
 	if testing.Short() {
 		t.Skip("short")
 	}
@@ -2351,7 +2361,7 @@ func TestDisconnect2(t *testing.T) {
 	tl := testLogger{t: t}
 	defer tl.StopLogging()
 	SetLogger(&tl)
-	latency, _ := getLatency(t)
+	latency := getLatency(t)
 
 	// Revert to the normal dialer after the test is done.
 	normalCreateDialer := createDialer
@@ -2420,6 +2430,7 @@ func TestDisconnect3(t *testing.T) {
 	if testing.Short() {
 		t.Skip("short")
 	}
+	skipIfNamedPipesEnabled(t)
 	checkConnStr(t)
 	tl := testLogger{t: t}
 	defer tl.StopLogging()
@@ -2705,3 +2716,10 @@ func TestTypeSizesFromQuery(t *testing.T) {
 		t.Errorf("VARBINARY(30) reported as (variable, length): (%v, %d)", ok, l)
 	}
 }
+
+func skipIfNamedPipesEnabled(t *testing.T) {
+	t.Helper()
+	if len(msdsn.ProtocolParsers) > 1 {
+		t.Skip("Skipping test due to named pipes dialer")
+	}
+}
diff --git a/tds_login_test.go b/tds_login_test.go
index e6d1beb..ab5e00c 100644
--- a/tds_login_test.go
+++ b/tds_login_test.go
@@ -119,7 +119,7 @@ func testLoginSequenceServer(result chan error, conn net.Conn, expectedPackets,
 }
 
 func TestLoginWithSQLServerAuth(t *testing.T) {
-	conn, err := NewConnector("sqlserver://test:secret@localhost:1433?Workstation ID=localhost&log=128")
+	conn, err := NewConnector("sqlserver://test:secret@localhost:1433?Workstation ID=localhost&log=128&protocol=tcp")
 	if err != nil {
 		t.Errorf("Unable to parse dummy DSN: %v", err)
 	}
@@ -170,7 +170,7 @@ func TestLoginWithSQLServerAuth(t *testing.T) {
 }
 
 func TestLoginWithSecurityTokenAuth(t *testing.T) {
-	config, err := msdsn.Parse("sqlserver://localhost:1433?Workstation ID=localhost&log=128")
+	config, err := msdsn.Parse("sqlserver://localhost:1433?Workstation ID=localhost&log=128&protocol=tcp")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -231,7 +231,7 @@ func TestLoginWithSecurityTokenAuth(t *testing.T) {
 }
 
 func TestLoginWithADALUsernamePasswordAuth(t *testing.T) {
-	config, err := msdsn.Parse("sqlserver://localhost:1433?Workstation ID=localhost&log=128")
+	config, err := msdsn.Parse("sqlserver://localhost:1433?Workstation ID=localhost&log=128&protocol=tcp")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -305,7 +305,7 @@ func TestLoginWithADALUsernamePasswordAuth(t *testing.T) {
 }
 
 func TestLoginWithADALManagedIdentityAuth(t *testing.T) {
-	config, err := msdsn.Parse("sqlserver://localhost:1433?Workstation ID=localhost&log=128")
+	config, err := msdsn.Parse("sqlserver://localhost:1433?Workstation ID=localhost&log=128&protocol=tcp")
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/tds_test.go b/tds_test.go
index 0dfeeb2..e48ffbf 100644
--- a/tds_test.go
+++ b/tds_test.go
@@ -233,14 +233,22 @@ func GetConnParams() (*msdsn.Config, error) {
 		return &params, nil
 	}
 	if len(os.Getenv("HOST")) > 0 && len(os.Getenv("DATABASE")) > 0 {
-		return &msdsn.Config{
-			Host:     os.Getenv("HOST"),
-			Instance: os.Getenv("INSTANCE"),
-			Database: os.Getenv("DATABASE"),
-			User:     os.Getenv("SQLUSER"),
-			Password: os.Getenv("SQLPASSWORD"),
-			LogFlags: logFlags,
-		}, nil
+		c := &msdsn.Config{
+			Host:       os.Getenv("HOST"),
+			Instance:   os.Getenv("INSTANCE"),
+			Database:   os.Getenv("DATABASE"),
+			User:       os.Getenv("SQLUSER"),
+			Password:   os.Getenv("SQLPASSWORD"),
+			LogFlags:   logFlags,
+			Parameters: make(map[string]string),
+		}
+		if c.Instance == "" {
+			c.Instance = os.Getenv("SQLINSTANCE")
+		}
+		if os.Getenv("PROTOCOL") != "" {
+			c.Parameters["protocol"] = os.Getenv("PROTOCOL")
+		}
+		return c, nil
 	}
 	// try loading connection string from file
 	f, err := os.Open(".connstr")
@@ -358,6 +366,7 @@ func TestConnectViaIp(t *testing.T) {
 	if params.Encryption == msdsn.EncryptionRequired {
 		t.Skip("Unable to test connection to IP for servers that expect encryption")
 	}
+	skipIfNamedPipesEnabled(t)
 
 	if params.Host == "." {
 		params.Host = "127.0.0.1"
@@ -576,14 +585,14 @@ func TestBadCredentials(t *testing.T) {
 	params := testConnParams(t)
 	params.Password = "padpwd"
 	params.User = "baduser"
-	testConnectionBad(t, params.URL().String())
+	_ = testConnectionBad(t, params.URL().String())
 }
 
 func TestBadHost(t *testing.T) {
 	params := testConnParams(t)
 	params.Host = "badhost"
 	params.Instance = ""
-	testConnectionBad(t, params.URL().String())
+	_ = testConnectionBad(t, params.URL().String())
 }
 
 func TestSqlBrowserNotUsedIfPortSpecified(t *testing.T) {
@@ -593,6 +602,8 @@ func TestSqlBrowserNotUsedIfPortSpecified(t *testing.T) {
 	params := testConnParams(t)
 	params.Host = "badhost"
 	params.Instance = "foobar"
+	protocols := params.Protocols
+	params.Protocols = []string{"tcp"}
 
 	// Specify no port, so error must indicate SQL Browser lookup failed
 	params.Port = 0 // No port spcified, sql browser should be used
@@ -605,6 +616,7 @@ func TestSqlBrowserNotUsedIfPortSpecified(t *testing.T) {
 
 	// Specify port, ensure error does not indicate SQL Browser lookup failed
 	params.Port = 1500 // Specify a port, sql browser should not be tried
+	params.Protocols = protocols
 	err = testConnectionBad(t, params.URL().String())
 
 	if strings.Contains(err.Error(), errorSubstrStringToCheckFor) {