From c0a0be876d02652626514ed97b0349f245e3bf76 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 22 Dec 2021 08:33:10 -0800 Subject: [PATCH 01/10] Fix TLS connection timeout --- pgconn.go | 32 ++++++++++----------- pgconn_test.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 16 deletions(-) diff --git a/pgconn.go b/pgconn.go index dad522c6..7e5d585b 100644 --- a/pgconn.go +++ b/pgconn.go @@ -241,13 +241,6 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig pgConn.parameterStatuses = make(map[string]string) - if fallbackConfig.TLSConfig != nil { - if err := pgConn.startTLS(fallbackConfig.TLSConfig); err != nil { - pgConn.conn.Close() - return nil, &connectError{config: config, msg: "tls error", err: err} - } - } - pgConn.status = connStatusConnecting pgConn.contextWatcher = ctxwatch.NewContextWatcher( func() { pgConn.conn.SetDeadline(time.Date(1, 1, 1, 1, 1, 1, 1, time.UTC)) }, @@ -257,6 +250,15 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig pgConn.contextWatcher.Watch(ctx) defer pgConn.contextWatcher.Unwatch() + if fallbackConfig.TLSConfig != nil { + tlsConn, err := startTLS(pgConn.conn, fallbackConfig.TLSConfig) + if err != nil { + pgConn.conn.Close() + return nil, &connectError{config: config, msg: "tls error", err: err} + } + pgConn.conn = tlsConn + } + pgConn.frontend = config.BuildFrontend(pgConn.conn, pgConn.conn) startupMsg := pgproto3.StartupMessage{ @@ -344,24 +346,22 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig } } -func (pgConn *PgConn) startTLS(tlsConfig *tls.Config) (err error) { - err = binary.Write(pgConn.conn, binary.BigEndian, []int32{8, 80877103}) +func startTLS(conn net.Conn, tlsConfig *tls.Config) (net.Conn, error) { + err := binary.Write(conn, binary.BigEndian, []int32{8, 80877103}) if err != nil { - return + return nil, err } response := make([]byte, 1) - if _, err = io.ReadFull(pgConn.conn, response); err != nil { - return + if _, err = io.ReadFull(conn, response); err != nil { + return nil, err } if response[0] != 'S' { - return errors.New("server refused TLS connection") + return nil, errors.New("server refused TLS connection") } - pgConn.conn = tls.Client(pgConn.conn, tlsConfig) - - return nil + return tls.Client(conn, tlsConfig), nil } func (pgConn *PgConn) txPasswordMessage(password string) (err error) { diff --git a/pgconn_test.go b/pgconn_test.go index 43e97eef..b22792fb 100644 --- a/pgconn_test.go +++ b/pgconn_test.go @@ -161,6 +161,84 @@ func TestConnectTimeout(t *testing.T) { } } +func TestConnectTimeoutStuckOnTLSHandshake(t *testing.T) { + t.Parallel() + tests := []struct { + name string + connect func(connStr string) error + }{ + { + name: "via context that times out", + connect: func(connStr string) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*10) + defer cancel() + _, err := pgconn.Connect(ctx, connStr) + return err + }, + }, + { + name: "via config ConnectTimeout", + connect: func(connStr string) error { + conf, err := pgconn.ParseConfig(connStr) + require.NoError(t, err) + conf.ConnectTimeout = time.Millisecond * 10 + _, err = pgconn.ConnectConfig(context.Background(), conf) + return err + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ln, err := net.Listen("tcp", "127.0.0.1:") + require.NoError(t, err) + defer ln.Close() + + serverErrChan := make(chan error) + defer close(serverErrChan) + go func() { + conn, err := ln.Accept() + if err != nil { + serverErrChan <- err + return + } + defer conn.Close() + + var buf []byte + _, err = conn.Read(buf) + if err != nil { + serverErrChan <- err + return + } + + // Sleeping to hang the TLS handshake. + time.Sleep(time.Minute) + }() + + parts := strings.Split(ln.Addr().String(), ":") + host := parts[0] + port := parts[1] + connStr := fmt.Sprintf("host=%s port=%s", host, port) + + errChan := make(chan error) + go func() { + err := tt.connect(connStr) + errChan <- err + }() + + select { + case err = <-errChan: + require.True(t, pgconn.Timeout(err), err) + case err = <-serverErrChan: + t.Fatalf("server failed with error: %s", err) + case <-time.After(time.Millisecond * 100): + t.Fatal("exceeded connection timeout without erroring out") + } + }) + } +} + func TestConnectInvalidUser(t *testing.T) { t.Parallel() From 024de4c8f330d2e0bd96dd3b99c866ae05a3f25a Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Thu, 23 Dec 2021 08:53:33 -0800 Subject: [PATCH 02/10] Unwatch and re-watch tls --- pgconn.go | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/pgconn.go b/pgconn.go index 7e5d585b..6fde4e50 100644 --- a/pgconn.go +++ b/pgconn.go @@ -230,7 +230,7 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig var err error network, address := NetworkAddress(fallbackConfig.Host, fallbackConfig.Port) - pgConn.conn, err = config.DialFunc(ctx, network, address) + netConn, err := config.DialFunc(ctx, network, address) if err != nil { var netErr net.Error if errors.As(err, &netErr) && netErr.Timeout() { @@ -239,26 +239,28 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig return nil, &connectError{config: config, msg: "dial error", err: err} } - pgConn.parameterStatuses = make(map[string]string) - - pgConn.status = connStatusConnecting - pgConn.contextWatcher = ctxwatch.NewContextWatcher( - func() { pgConn.conn.SetDeadline(time.Date(1, 1, 1, 1, 1, 1, 1, time.UTC)) }, - func() { pgConn.conn.SetDeadline(time.Time{}) }, - ) - + pgConn.contextWatcher = contextWatcher(netConn) pgConn.contextWatcher.Watch(ctx) defer pgConn.contextWatcher.Unwatch() + pgConn.status = connStatusConnecting + pgConn.conn = netConn + if fallbackConfig.TLSConfig != nil { - tlsConn, err := startTLS(pgConn.conn, fallbackConfig.TLSConfig) + tlsConn, err := startTLS(netConn, fallbackConfig.TLSConfig) if err != nil { - pgConn.conn.Close() + netConn.Close() return nil, &connectError{config: config, msg: "tls error", err: err} } + + pgConn.contextWatcher.Unwatch() + pgConn.contextWatcher = contextWatcher(tlsConn) + pgConn.contextWatcher.Watch(ctx) + pgConn.conn = tlsConn } + pgConn.parameterStatuses = make(map[string]string) pgConn.frontend = config.BuildFrontend(pgConn.conn, pgConn.conn) startupMsg := pgproto3.StartupMessage{ @@ -346,6 +348,13 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig } } +func contextWatcher(conn net.Conn) *ctxwatch.ContextWatcher { + return ctxwatch.NewContextWatcher( + func() { conn.SetDeadline(time.Date(1, 1, 1, 1, 1, 1, 1, time.UTC)) }, + func() { conn.SetDeadline(time.Time{}) }, + ) +} + func startTLS(conn net.Conn, tlsConfig *tls.Config) (net.Conn, error) { err := binary.Write(conn, binary.BigEndian, []int32{8, 80877103}) if err != nil { @@ -1709,10 +1718,7 @@ func Construct(hc *HijackedConn) (*PgConn, error) { cleanupDone: make(chan struct{}), } - pgConn.contextWatcher = ctxwatch.NewContextWatcher( - func() { pgConn.conn.SetDeadline(time.Date(1, 1, 1, 1, 1, 1, 1, time.UTC)) }, - func() { pgConn.conn.SetDeadline(time.Time{}) }, - ) + pgConn.contextWatcher = contextWatcher(pgConn.conn) return pgConn, nil } From 01a6923376d212fe4174b676f2667c817ee696f4 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Thu, 23 Dec 2021 08:55:38 -0800 Subject: [PATCH 03/10] Rename fn to new --- pgconn.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pgconn.go b/pgconn.go index 6fde4e50..c437c119 100644 --- a/pgconn.go +++ b/pgconn.go @@ -239,7 +239,7 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig return nil, &connectError{config: config, msg: "dial error", err: err} } - pgConn.contextWatcher = contextWatcher(netConn) + pgConn.contextWatcher = newContextWatcher(netConn) pgConn.contextWatcher.Watch(ctx) defer pgConn.contextWatcher.Unwatch() @@ -254,7 +254,7 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig } pgConn.contextWatcher.Unwatch() - pgConn.contextWatcher = contextWatcher(tlsConn) + pgConn.contextWatcher = newContextWatcher(tlsConn) pgConn.contextWatcher.Watch(ctx) pgConn.conn = tlsConn @@ -348,7 +348,7 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig } } -func contextWatcher(conn net.Conn) *ctxwatch.ContextWatcher { +func newContextWatcher(conn net.Conn) *ctxwatch.ContextWatcher { return ctxwatch.NewContextWatcher( func() { conn.SetDeadline(time.Date(1, 1, 1, 1, 1, 1, 1, time.UTC)) }, func() { conn.SetDeadline(time.Time{}) }, @@ -1718,7 +1718,7 @@ func Construct(hc *HijackedConn) (*PgConn, error) { cleanupDone: make(chan struct{}), } - pgConn.contextWatcher = contextWatcher(pgConn.conn) + pgConn.contextWatcher = newContextWatcher(pgConn.conn) return pgConn, nil } From b148a14bbee1d4667bd2be01ea2d12253d7d1931 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 27 Dec 2021 10:29:21 -0800 Subject: [PATCH 04/10] Fix defer usage --- pgconn.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pgconn.go b/pgconn.go index c437c119..4bec872d 100644 --- a/pgconn.go +++ b/pgconn.go @@ -239,27 +239,28 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig return nil, &connectError{config: config, msg: "dial error", err: err} } - pgConn.contextWatcher = newContextWatcher(netConn) - pgConn.contextWatcher.Watch(ctx) - defer pgConn.contextWatcher.Unwatch() - pgConn.status = connStatusConnecting pgConn.conn = netConn + pgConn.contextWatcher = newContextWatcher(netConn) + pgConn.contextWatcher.Watch(ctx) + if fallbackConfig.TLSConfig != nil { tlsConn, err := startTLS(netConn, fallbackConfig.TLSConfig) + pgConn.contextWatcher.Unwatch() // Always unwatch `netConn` after TLS. if err != nil { netConn.Close() return nil, &connectError{config: config, msg: "tls error", err: err} } - pgConn.contextWatcher.Unwatch() pgConn.contextWatcher = newContextWatcher(tlsConn) pgConn.contextWatcher.Watch(ctx) pgConn.conn = tlsConn } + defer pgConn.contextWatcher.Unwatch() + pgConn.parameterStatuses = make(map[string]string) pgConn.frontend = config.BuildFrontend(pgConn.conn, pgConn.conn) From a1852214fe10eeeb5b7f8634a8dd1e928884f2cf Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 27 Dec 2021 10:36:38 -0800 Subject: [PATCH 05/10] Keep status connecting after tls --- pgconn.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pgconn.go b/pgconn.go index 4bec872d..f8b8a659 100644 --- a/pgconn.go +++ b/pgconn.go @@ -239,9 +239,7 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig return nil, &connectError{config: config, msg: "dial error", err: err} } - pgConn.status = connStatusConnecting pgConn.conn = netConn - pgConn.contextWatcher = newContextWatcher(netConn) pgConn.contextWatcher.Watch(ctx) @@ -253,15 +251,15 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig return nil, &connectError{config: config, msg: "tls error", err: err} } + pgConn.conn = tlsConn pgConn.contextWatcher = newContextWatcher(tlsConn) pgConn.contextWatcher.Watch(ctx) - - pgConn.conn = tlsConn } defer pgConn.contextWatcher.Unwatch() pgConn.parameterStatuses = make(map[string]string) + pgConn.status = connStatusConnecting pgConn.frontend = config.BuildFrontend(pgConn.conn, pgConn.conn) startupMsg := pgproto3.StartupMessage{ From 3ce8a835e1d61d5b25c9e0305af1560ec01bc160 Mon Sep 17 00:00:00 2001 From: Oscar Date: Tue, 28 Dec 2021 14:43:01 +0100 Subject: [PATCH 06/10] add support for read-only, primary, standby, prefer-standby target_session_attributes --- config.go | 60 ++++++++++++++++++++++++++++++++++-- config_test.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 139 insertions(+), 4 deletions(-) diff --git a/config.go b/config.go index 172e7478..c48905f9 100644 --- a/config.go +++ b/config.go @@ -329,10 +329,19 @@ func ParseConfig(connString string) (*Config, error) { } } - if settings["target_session_attrs"] == "read-write" { + switch tsa := settings["target_session_attrs"]; tsa { + case "read-write": config.ValidateConnect = ValidateConnectTargetSessionAttrsReadWrite - } else if settings["target_session_attrs"] != "any" { - return nil, &parseConfigError{connString: connString, msg: fmt.Sprintf("unknown target_session_attrs value: %v", settings["target_session_attrs"])} + case "read-only": + config.ValidateConnect = ValidateConnectTargetSessionAttrsReadOnly + case "primary": + config.ValidateConnect = ValidateConnectTargetSessionAttrsPrimary + case "standby": + config.ValidateConnect = ValidateConnectTargetSessionAttrsStandby + case "any", "prefer-standby": + // do nothing + default: + return nil, &parseConfigError{connString: connString, msg: fmt.Sprintf("unknown target_session_attrs value: %v", tsa)} } return config, nil @@ -727,3 +736,48 @@ func ValidateConnectTargetSessionAttrsReadWrite(ctx context.Context, pgConn *PgC return nil } + +// ValidateConnectTargetSessionAttrsReadOnly is an ValidateConnectFunc that implements libpq compatible +// target_session_attrs=read-only. +func ValidateConnectTargetSessionAttrsReadOnly(ctx context.Context, pgConn *PgConn) error { + result := pgConn.ExecParams(ctx, "show transaction_read_only", nil, nil, nil, nil).Read() + if result.Err != nil { + return result.Err + } + + if string(result.Rows[0][0]) != "on" { + return errors.New("connection is not read only") + } + + return nil +} + +// ValidateConnectTargetSessionAttrsStandby is an ValidateConnectFunc that implements libpq compatible +// target_session_attrs=standby. +func ValidateConnectTargetSessionAttrsStandby(ctx context.Context, pgConn *PgConn) error { + result := pgConn.ExecParams(ctx, "select pg_is_in_recovery()", nil, nil, nil, nil).Read() + if result.Err != nil { + return result.Err + } + + if string(result.Rows[0][0]) != "f" { + return errors.New("server is not in hot standby mode") + } + + return nil +} + +// ValidateConnectTargetSessionAttrsPrimary is an ValidateConnectFunc that implements libpq compatible +// target_session_attrs=primary. +func ValidateConnectTargetSessionAttrsPrimary(ctx context.Context, pgConn *PgConn) error { + result := pgConn.ExecParams(ctx, "select pg_is_in_recovery()", nil, nil, nil, nil).Read() + if result.Err != nil { + return result.Err + } + + if string(result.Rows[0][0]) == "t" { + return errors.New("server is in standby mode") + } + + return nil +} diff --git a/config_test.go b/config_test.go index d29173d1..da28782d 100644 --- a/config_test.go +++ b/config_test.go @@ -541,7 +541,7 @@ func TestParseConfig(t *testing.T) { }, }, { - name: "target_session_attrs", + name: "target_session_attrs read-write", connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable&target_session_attrs=read-write", config: &pgconn.Config{ User: "jack", @@ -554,6 +554,87 @@ func TestParseConfig(t *testing.T) { ValidateConnect: pgconn.ValidateConnectTargetSessionAttrsReadWrite, }, }, + { + name: "target_session_attrs read-only", + connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable&target_session_attrs=read-only", + config: &pgconn.Config{ + User: "jack", + Password: "secret", + Host: "localhost", + Port: 5432, + Database: "mydb", + TLSConfig: nil, + RuntimeParams: map[string]string{}, + ValidateConnect: pgconn.ValidateConnectTargetSessionAttrsReadOnly, + }, + }, + { + name: "target_session_attrs primary", + connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable&target_session_attrs=primary", + config: &pgconn.Config{ + User: "jack", + Password: "secret", + Host: "localhost", + Port: 5432, + Database: "mydb", + TLSConfig: nil, + RuntimeParams: map[string]string{}, + ValidateConnect: pgconn.ValidateConnectTargetSessionAttrsPrimary, + }, + }, + { + name: "target_session_attrs standby", + connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable&target_session_attrs=standby", + config: &pgconn.Config{ + User: "jack", + Password: "secret", + Host: "localhost", + Port: 5432, + Database: "mydb", + TLSConfig: nil, + RuntimeParams: map[string]string{}, + ValidateConnect: pgconn.ValidateConnectTargetSessionAttrsStandby, + }, + }, + { + name: "target_session_attrs prefer-standby", + connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable&target_session_attrs=prefer-standby", + config: &pgconn.Config{ + User: "jack", + Password: "secret", + Host: "localhost", + Port: 5432, + Database: "mydb", + TLSConfig: nil, + RuntimeParams: map[string]string{}, + }, + }, + { + name: "target_session_attrs any", + connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable&target_session_attrs=any", + config: &pgconn.Config{ + User: "jack", + Password: "secret", + Host: "localhost", + Port: 5432, + Database: "mydb", + TLSConfig: nil, + RuntimeParams: map[string]string{}, + }, + }, + { + name: "target_session_attrs not set (any)", + connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable", + config: &pgconn.Config{ + User: "jack", + Password: "secret", + Host: "localhost", + Port: 5432, + Database: "mydb", + TLSConfig: nil, + RuntimeParams: map[string]string{}, + }, + }, } for i, tt := range tests { From 3aaf3409ce9137e534e9433cc4256aaef7d88e7e Mon Sep 17 00:00:00 2001 From: Oscar Date: Tue, 28 Dec 2021 14:44:04 +0100 Subject: [PATCH 07/10] remove redundant map value type --- config.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/config.go b/config.go index c48905f9..406a2877 100644 --- a/config.go +++ b/config.go @@ -248,21 +248,21 @@ func ParseConfig(connString string) (*Config, error) { config.LookupFunc = makeDefaultResolver().LookupHost notRuntimeParams := map[string]struct{}{ - "host": struct{}{}, - "port": struct{}{}, - "database": struct{}{}, - "user": struct{}{}, - "password": struct{}{}, - "passfile": struct{}{}, - "connect_timeout": struct{}{}, - "sslmode": struct{}{}, - "sslkey": struct{}{}, - "sslcert": struct{}{}, - "sslrootcert": struct{}{}, - "target_session_attrs": struct{}{}, - "min_read_buffer_size": struct{}{}, - "service": struct{}{}, - "servicefile": struct{}{}, + "host": {}, + "port": {}, + "database": {}, + "user": {}, + "password": {}, + "passfile": {}, + "connect_timeout": {}, + "sslmode": {}, + "sslkey": {}, + "sslcert": {}, + "sslrootcert": {}, + "target_session_attrs": {}, + "min_read_buffer_size": {}, + "service": {}, + "servicefile": {}, } for k, v := range settings { From 109c4c2d95fd0925d0a4f842c0c9878ea2f16971 Mon Sep 17 00:00:00 2001 From: Oscar Date: Tue, 28 Dec 2021 14:56:49 +0100 Subject: [PATCH 08/10] fix standby mode validation --- config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.go b/config.go index 406a2877..0eab23af 100644 --- a/config.go +++ b/config.go @@ -760,7 +760,7 @@ func ValidateConnectTargetSessionAttrsStandby(ctx context.Context, pgConn *PgCon return result.Err } - if string(result.Rows[0][0]) != "f" { + if string(result.Rows[0][0]) != "t" { return errors.New("server is not in hot standby mode") } From 05d532b5df9e43f11a521f61529404aedb578212 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Thu, 20 Jan 2022 16:40:44 -0600 Subject: [PATCH 09/10] Fix connect when receiving NoticeResponse refs #102 --- pgconn.go | 2 +- pgconn_test.go | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pgconn.go b/pgconn.go index f8b8a659..7bf2f20e 100644 --- a/pgconn.go +++ b/pgconn.go @@ -335,7 +335,7 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig } } return pgConn, nil - case *pgproto3.ParameterStatus: + case *pgproto3.ParameterStatus, *pgproto3.NoticeResponse: // handled by ReceiveMessage case *pgproto3.ErrorResponse: pgConn.conn.Close() diff --git a/pgconn_test.go b/pgconn_test.go index b22792fb..32186fc6 100644 --- a/pgconn_test.go +++ b/pgconn_test.go @@ -1298,6 +1298,7 @@ func TestConnOnNotice(t *testing.T) { config.OnNotice = func(c *pgconn.PgConn, notice *pgconn.Notice) { msg = notice.Message } + config.RuntimeParams["client_min_messages"] = "notice" // Ensure we only get the message we expect. pgConn, err := pgconn.ConnectConfig(context.Background(), config) require.NoError(t, err) @@ -1954,7 +1955,11 @@ func TestConnSendBytesAndReceiveMessage(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - pgConn, err := pgconn.Connect(ctx, os.Getenv("PGX_TEST_CONN_STRING")) + config, err := pgconn.ParseConfig(os.Getenv("PGX_TEST_CONN_STRING")) + require.NoError(t, err) + config.RuntimeParams["client_min_messages"] = "notice" // Ensure we only get the messages we expect. + + pgConn, err := pgconn.ConnectConfig(context.Background(), config) require.NoError(t, err) defer closeConn(t, pgConn) From 3e5de443149f6d3dbbe1bf7387203c413d0efab4 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Mon, 7 Feb 2022 10:54:39 -0600 Subject: [PATCH 10/10] Release v1.11.0 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63933a3a..a37eecfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 1.11.0 (February 7, 2022) + +* Support port in ip from LookupFunc to override config (James Hartig) +* Fix TLS connection timeout (Blake Embrey) +* Add support for read-only, primary, standby, prefer-standby target_session_attributes (Oscar) +* Fix connect when receiving NoticeResponse + # 1.10.1 (November 20, 2021) * Close without waiting for response (Kei Kamikawa)