From 11ecdbf219d09d3e6cb0d13960e23e00a3aa867a Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Wed, 24 Mar 2021 17:16:41 -0700 Subject: [PATCH] ssl: reuse *tls.Config if connection settings are identical Previously, we would reload and re-parse a certificate from disk every single time we initialized a connection and the sslrootcert setting was enabled. This results in a lot of allocations that can be avoided. Instead, save the *tls.Config for a given configuration hash, and reuse it when we see it again. Fixes #1032. --- conn.go | 18 ++++++++++++++++++ ssl.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/conn.go b/conn.go index db0b6cef5..38c6204d1 100644 --- a/conn.go +++ b/conn.go @@ -16,6 +16,7 @@ import ( "os/user" "path" "path/filepath" + "sort" "strconv" "strings" "sync/atomic" @@ -394,6 +395,23 @@ func network(o values) (string, string) { type values map[string]string +// Hash returns a deterministic hash of values. +func (v values) Hash() []byte { + keys := make([]string, len(v)) + i := 0 + for key := range v { + keys[i] = key + i++ + } + sort.Strings(keys) + h := sha256.New() + for _, key := range keys { + h.Write([]byte(key)) + h.Write([]byte(v[key])) + } + return h.Sum(nil) +} + // scanner implements a tokenizer for libpq-style option strings. type scanner struct { s []rune diff --git a/ssl.go b/ssl.go index 881c2219b..dcb820a7f 100644 --- a/ssl.go +++ b/ssl.go @@ -8,12 +8,34 @@ import ( "os" "os/user" "path/filepath" + "sync" ) -// ssl generates a function to upgrade a net.Conn based on the "sslmode" and -// related settings. The function is nil when no upgrade should take place. -func ssl(o values) (func(net.Conn) (net.Conn, error), error) { +// To avoid allocating the map if we never use ssl +var configMapOnce sync.Once +var configMapMu sync.Mutex +var configMap map[string]*ssldata + +type ssldata struct { + Conf *tls.Config + VerifyCAOnly bool +} + +func getTLSConf(o values) (*ssldata, error) { verifyCaOnly := false + configMapOnce.Do(func() { + configMap = make(map[string]*ssldata) + }) + // this function modifies o, so take the hash before any modifications are + // made + hash := string(o.Hash()) + // This pseudo-parameter is not recognized by the PostgreSQL server, so let's delete it after use. + configMapMu.Lock() + conf, ok := configMap[hash] + configMapMu.Unlock() + if ok { + return conf, nil + } tlsConf := tls.Config{} switch mode := o["sslmode"]; mode { // "require" is the default. @@ -69,10 +91,27 @@ func ssl(o values) (func(net.Conn) (net.Conn, error), error) { // also initiates renegotiations and cannot be reconfigured. tlsConf.Renegotiation = tls.RenegotiateFreelyAsClient + data := &ssldata{&tlsConf, verifyCaOnly} + configMapMu.Lock() + configMap[hash] = data + configMapMu.Unlock() + return data, nil +} + +// ssl generates a function to upgrade a net.Conn based on the "sslmode" and +// related settings. The function is nil when no upgrade should take place. +func ssl(o values) (func(net.Conn) (net.Conn, error), error) { + data, err := getTLSConf(o) + if data == nil && err == nil { + return nil, nil + } + if err != nil { + return nil, err + } return func(conn net.Conn) (net.Conn, error) { - client := tls.Client(conn, &tlsConf) - if verifyCaOnly { - err := sslVerifyCertificateAuthority(client, &tlsConf) + client := tls.Client(conn, data.Conf) + if data.VerifyCAOnly { + err := sslVerifyCertificateAuthority(client, data.Conf) if err != nil { return nil, err } @@ -86,8 +125,7 @@ func ssl(o values) (func(net.Conn) (net.Conn, error), error) { // in the user's home directory. The configured files must exist and have // the correct permissions. func sslClientCertificates(tlsConf *tls.Config, o values) error { - sslinline := o["sslinline"] - if sslinline == "true" { + if o["sslinline"] == "true" { cert, err := tls.X509KeyPair([]byte(o["sslcert"]), []byte(o["sslkey"])) // Clear out these params, in case they were to be sent to the PostgreSQL server by mistake o["sslcert"] = "" @@ -98,7 +136,6 @@ func sslClientCertificates(tlsConf *tls.Config, o values) error { tlsConf.Certificates = []tls.Certificate{cert} return nil } - // user.Current() might fail when cross-compiling. We have to ignore the // error and continue without home directory defaults, since we wouldn't // know from where to load them.