How can I add a 10 second timeout with tls.Dial ? (There is no tls.DialTimeout to correspond to net.DialTimeout)

Viewed 1273

What is the best way to add a timeout when using tls.Dial in Go?

I see the net package has net.DialTimeout, but unfortunately, the tls package doesn't have a corresponding function.

I presume I should be using a context or Dialer to implement a timeout, but I'm not an expert in Go and I can't find any good examples.

(1) I found tls.DialWithDialer, but I'm not sure how to create a net.Dialer that is configured with a timeout.

func DialWithDialer(dialer *net.Dialer, network, addr string, config *Config) (*Conn, error)

(2) I also found tls.DialContext, but I'm not sure how to use that to implement a timeout.

func (d *Dialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error)

(3) I presume it might be possible to establish an initial connection using net.DialTimeout and then upgrade the connection and continue with the TLS handshake, but I can't find any examples that show how to do that.

Any help or guidance would be appreciated.

Here is my simple program that connects to a list of servers and prints some info about the certificate chain. When a server is not responding, this program hangs for a long time. All I want to do is time out after 10 seconds.

package main

import (
    "bufio"
    "crypto/tls"
    "fmt"
    "os"
)

func main() {
    port := "443"
    conf := &tls.Config{
        InsecureSkipVerify: true,
    }
    s := bufio.NewScanner(os.Stdin)
    for s.Scan() {
        host := s.Text()
        conn, err := tls.Dial("tcp", host+":"+port, conf)
        if err != nil {
            fmt.Println("Host:", host, "Dial:", err)
            continue
        }
        defer conn.Close()
        certs := conn.ConnectionState().PeerCertificates
        for _, cert := range certs {
            fmt.Println("Host:", host, "Issuer:", cert.Issuer)
        }
    }
}
1 Answers

As you mention in your question there are a few options; using DialContext is a common technique:

package main

import (
    "bufio"
    "context"
    "crypto/tls"
    "fmt"
    "os"
    "time"
)

func main() {
    port := "443"
    conf := &tls.Config{
        InsecureSkipVerify: true,
    }
    s := bufio.NewScanner(os.Stdin)
    for s.Scan() {
        host := s.Text()
        ctx, cancel := context.WithTimeout(context.Background(), 10 * time.Second)
        d := tls.Dialer{
            Config: conf,
        }
        conn, err := d.DialContext(ctx,"tcp", host+":"+port)
        cancel() // Ensure cancel is always called
        if err != nil {
            fmt.Println("Host:", host, "Dial:", err)
            continue
        }

        // warning: using defer in a loop may not have the expected result
        // the connection will remain open until the function exists
        defer conn.Close()

        tlsConn := conn.(*tls.Conn)
        certs := tlsConn.ConnectionState().PeerCertificates
        for _, cert := range certs {
            fmt.Println("Host:", host, "Issuer:", cert.Issuer)
        }
    }
}

Using the above approach makes it relatively simple to allow users of your code to cancel the request (accept a context and use it where the above has context.Background()). If this is not important to you then using a Dialer with Timeout is simpler:

conn, err := tls.DialWithDialer(&net.Dialer{Timeout:  10 * time.Second}, "tcp", host+":"+port, conf)
if err != nil { 
    fmt.Println("Host:", host, "Dial:", err)
    continue
}

certs := conn.ConnectionState().PeerCertificates
for _, cert := range certs {
    fmt.Println("Host:", host, "Issuer:", cert.Issuer)
}
conn.Close()
Related