Skip to content

Commit 2f11b3a

Browse files
committed
Add support for protocol 3.2
This adds support for the 3.2 protocol version, introduced with PostgreSQL 18. It follows postgres in the sense that the default is still 3.0, but this allows for allocations to allow the 3.2 version of the protocol with longer secret key data. This is to both improve security and to provide room for additional metadata for middleware. Signed-off-by: Dirkjan Bussink <d.bussink@gmail.com>
1 parent eec526c commit 2f11b3a

6 files changed

Lines changed: 204 additions & 11 deletions

File tree

conn.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,12 @@ type conn struct {
174174
// (ErrBadConn) or getForNext().
175175
err syncErr
176176

177-
processID, secretKey int // Cancellation key data for use with CancelRequest messages.
178-
inCopy bool // If true this connection is in the middle of a COPY
179-
noticeHandler func(*Error) // If not nil, notices will be synchronously sent here
180-
notificationHandler func(*Notification) // If not nil, notifications will be synchronously sent here
181-
gss GSS // GSSAPI context
177+
processID int // Cancellation key data for use with CancelRequest messages.
178+
secretKey []byte //
179+
inCopy bool // If true this connection is in the middle of a COPY
180+
noticeHandler func(*Error) // If not nil, notices will be synchronously sent here
181+
notificationHandler func(*Notification) // If not nil, notifications will be synchronously sent here
182+
gss GSS // GSSAPI context
182183
}
183184

184185
type syncErr struct {
@@ -1186,7 +1187,11 @@ func (cn *conn) ssl(cfg Config) error {
11861187

11871188
func (cn *conn) startup(cfg Config) error {
11881189
w := cn.writeBuf(0)
1189-
w.int32(proto.ProtocolVersion30)
1190+
maxProto, err := parseProtocolVersion(cfg.MaxProtocolVersion)
1191+
if err != nil {
1192+
return err
1193+
}
1194+
w.int32(maxProto)
11901195

11911196
w.string("user")
11921197
w.string(cfg.User)
@@ -1227,6 +1232,16 @@ func (cn *conn) startup(cfg Config) error {
12271232
if err != nil {
12281233
return err
12291234
}
1235+
case proto.NegotiateProtocolVersion:
1236+
newestMinor := r.int32()
1237+
serverVersion := proto.ProtocolVersion30&0xFFFF0000 | newestMinor
1238+
minProto, err := parseProtocolVersion(cfg.MinProtocolVersion)
1239+
if err != nil {
1240+
return err
1241+
}
1242+
if serverVersion < minProto {
1243+
return fmt.Errorf("pq: server does not support minimum protocol version (server newest minor: %d)", newestMinor)
1244+
}
12301245
case proto.ReadyForQuery:
12311246
cn.processReadyForQuery(r)
12321247
return nil
@@ -1561,7 +1576,8 @@ func (cn *conn) readReadyForQuery() error {
15611576

15621577
func (cn *conn) processBackendKeyData(r *readBuf) {
15631578
cn.processID = r.int32()
1564-
cn.secretKey = r.int32()
1579+
cn.secretKey = make([]byte, len(*r))
1580+
copy(cn.secretKey, *r)
15651581
}
15661582

15671583
func (cn *conn) readParseResponse() error {

conn_go18.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ func (cn *conn) cancel(ctx context.Context) error {
152152
w := cn2.writeBuf(0)
153153
w.int32(proto.CancelRequestCode)
154154
w.int32(cn.processID)
155-
w.int32(cn.secretKey)
155+
w.bytes(cn.secretKey)
156156
if err := cn2.sendStartupPacket(w); err != nil {
157157
return err
158158
}

connector.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"unicode"
2020

2121
"github.com/lib/pq/internal/pqutil"
22+
"github.com/lib/pq/internal/proto"
2223
)
2324

2425
type (
@@ -110,6 +111,11 @@ const (
110111

111112
var loadBalanceHosts = []LoadBalanceHosts{LoadBalanceHostsDisable, LoadBalanceHostsRandom}
112113

114+
const (
115+
defaultMinProtocolVersion = "3.0"
116+
defaultMaxProtocolVersion = "3.0"
117+
)
118+
113119
// Connector represents a fixed configuration for the pq driver with a given
114120
// dsn. Connector satisfies the [database/sql/driver.Connector] interface and
115121
// can be used to create any number of DB Conn's via [sql.OpenDB].
@@ -303,6 +309,15 @@ type Config struct {
303309
// to the same server.
304310
LoadBalanceHosts LoadBalanceHosts `postgres:"load_balance_hosts" env:"PGLOADBALANCEHOSTS"`
305311

312+
// Minimum acceptable PostgreSQL protocol version. If the server does not
313+
// support at least this version, the connection will fail. Valid values:
314+
// "3.0", "3.2", "latest". Defaults to "3.0".
315+
MinProtocolVersion string `postgres:"min_protocol_version" env:"PGMINPROTOCOLVERSION"`
316+
317+
// Maximum PostgreSQL protocol version to request from the server. Valid
318+
// values: "3.0", "3.2", "latest". Defaults to "3.0".
319+
MaxProtocolVersion string `postgres:"max_protocol_version" env:"PGMAXPROTOCOLVERSION"`
320+
306321
// Runtime parameters: any unrecognized parameter in the DSN will be added
307322
// to this and sent to PostgreSQL during startup.
308323
Runtime map[string]string `postgres:"-" env:"-"`
@@ -487,9 +502,40 @@ func newConfig(dsn string, env []string) (Config, error) {
487502
cfg.SSLMode = SSLModeDisable
488503
}
489504

505+
// Default protocol versions.
506+
if cfg.MinProtocolVersion == "" {
507+
cfg.MinProtocolVersion = defaultMinProtocolVersion
508+
}
509+
if cfg.MaxProtocolVersion == "" {
510+
cfg.MaxProtocolVersion = defaultMaxProtocolVersion
511+
}
512+
minProto, err := parseProtocolVersion(cfg.MinProtocolVersion)
513+
if err != nil {
514+
return Config{}, err
515+
}
516+
maxProto, err := parseProtocolVersion(cfg.MaxProtocolVersion)
517+
if err != nil {
518+
return Config{}, err
519+
}
520+
if minProto > maxProto {
521+
return Config{}, fmt.Errorf("pq: min_protocol_version %q cannot be greater than max_protocol_version %q",
522+
cfg.MinProtocolVersion, cfg.MaxProtocolVersion)
523+
}
524+
490525
return cfg, nil
491526
}
492527

528+
func parseProtocolVersion(s string) (int, error) {
529+
switch s {
530+
case "", "3.0":
531+
return proto.ProtocolVersion30, nil
532+
case "3.2", "latest":
533+
return proto.ProtocolVersion32, nil
534+
default:
535+
return 0, fmt.Errorf("pq: invalid protocol version: %q", s)
536+
}
537+
}
538+
493539
func (cfg Config) network() (string, string) {
494540
if cfg.Hostaddr != (netip.Addr{}) {
495541
return "tcp", net.JoinHostPort(cfg.Hostaddr.String(), strconv.Itoa(int(cfg.Port)))
@@ -514,7 +560,7 @@ func (cfg *Config) fromEnv(env []string) error {
514560
case "PGREQUIREAUTH", "PGCHANNELBINDING", "PGSERVICE", "PGSERVICEFILE", "PGREALM",
515561
"PGSSLCERTMODE", "PGSSLCOMPRESSION", "PGREQUIRESSL", "PGSSLCRL", "PGREQUIREPEER",
516562
"PGSYSCONFDIR", "PGLOCALEDIR", "PGSSLCRLDIR", "PGSSLMINPROTOCOLVERSION", "PGSSLMAXPROTOCOLVERSION",
517-
"PGGSSENCMODE", "PGGSSDELEGATION", "PGMINPROTOCOLVERSION", "PGMAXPROTOCOLVERSION", "PGGSSLIB":
563+
"PGGSSENCMODE", "PGGSSDELEGATION", "PGGSSLIB":
518564
return fmt.Errorf("pq: environment variable $%s is not supported", k)
519565
case "PGKRBSRVNAME":
520566
if newGss == nil {
@@ -654,6 +700,8 @@ func (cfg *Config) setFromTag(o map[string]string, tag string) error {
654700
sslnegotiation = (tag == "postgres" && k == "sslnegotiation") || (tag == "env" && k == "PGSSLNEGOTIATION")
655701
targetsessionattrs = (tag == "postgres" && k == "target_session_attrs") || (tag == "env" && k == "PGTARGETSESSIONATTRS")
656702
loadbalancehosts = (tag == "postgres" && k == "load_balance_hosts") || (tag == "env" && k == "PGLOADBALANCEHOSTS")
703+
minprotocolversion = (tag == "postgres" && k == "min_protocol_version") || (tag == "env" && k == "PGMINPROTOCOLVERSION")
704+
maxprotocolversion = (tag == "postgres" && k == "max_protocol_version") || (tag == "env" && k == "PGMAXPROTOCOLVERSION")
657705
)
658706
if k == "" || k == "-" {
659707
continue
@@ -706,6 +754,9 @@ func (cfg *Config) setFromTag(o map[string]string, tag string) error {
706754
if loadbalancehosts && !slices.Contains(loadBalanceHosts, LoadBalanceHosts(v)) {
707755
return fmt.Errorf(f+`%q is not supported; supported values are %s`, k, v, pqutil.Join(loadBalanceHosts))
708756
}
757+
if (minprotocolversion || maxprotocolversion) && v != "3.0" && v != "3.2" && v != "latest" {
758+
return fmt.Errorf(f+`%q is not supported; supported values are "3.0", "3.2", "latest"`, k, v)
759+
}
709760
if host {
710761
vv := strings.Split(v, ",")
711762
v = vv[0]
@@ -835,7 +886,7 @@ func (cfg Config) string() string {
835886
switch k {
836887
case "datestyle", "client_encoding":
837888
continue
838-
case "host", "port", "user", "sslsni":
889+
case "host", "port", "user", "sslsni", "min_protocol_version", "max_protocol_version":
839890
if !cfg.isset(k) {
840891
continue
841892
}

connector_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,19 @@ func TestNewConfig(t *testing.T) {
415415
{"host=a,b,c hostaddr=1.1.1.1,2.2.2.2", nil, "", "could not match 3 host names to 2 hostaddr values"},
416416
{"host=a hostaddr=1.1.1.1,2.2.2.2", nil, "", "could not match 1 host names to 2 hostaddr values"},
417417
{"", []string{"PGHOST=a,,b", "PGHOSTADDR=1.1.1.1,,2.2.2.2", "PGPORT=3,,4"}, "host=a,localhost,b hostaddr=1.1.1.1,,2.2.2.2 port=3,5432,4", ""},
418+
419+
// Protocol version
420+
{"min_protocol_version=3.0", nil, "min_protocol_version=3.0", ""},
421+
{"max_protocol_version=3.2", nil, "max_protocol_version=3.2", ""},
422+
{"min_protocol_version=3.2 max_protocol_version=3.2", nil, "max_protocol_version=3.2 min_protocol_version=3.2", ""},
423+
{"min_protocol_version=latest max_protocol_version=latest", nil, "max_protocol_version=latest min_protocol_version=latest", ""},
424+
{"min_protocol_version=3.0 max_protocol_version=latest", nil, "max_protocol_version=latest min_protocol_version=3.0", ""},
425+
{"", []string{"PGMINPROTOCOLVERSION=3.0", "PGMAXPROTOCOLVERSION=3.2"}, "max_protocol_version=3.2 min_protocol_version=3.0", ""},
426+
{"min_protocol_version=bogus", nil, "", `pq: wrong value for "min_protocol_version": "bogus" is not supported`},
427+
{"max_protocol_version=bogus", nil, "", `pq: wrong value for "max_protocol_version": "bogus" is not supported`},
428+
{"", []string{"PGMINPROTOCOLVERSION=bogus"}, "", `pq: wrong value for $PGMINPROTOCOLVERSION: "bogus" is not supported`},
429+
{"", []string{"PGMAXPROTOCOLVERSION=bogus"}, "", `pq: wrong value for $PGMAXPROTOCOLVERSION: "bogus" is not supported`},
430+
{"min_protocol_version=3.2 max_protocol_version=3.0", nil, "", `min_protocol_version "3.2" cannot be greater than max_protocol_version "3.0"`},
418431
}
419432

420433
t.Parallel()
@@ -723,3 +736,95 @@ func TestConnectionTargetSessionAttrs(t *testing.T) {
723736
})
724737
}
725738
}
739+
740+
func TestProtocolVersion32(t *testing.T) {
741+
t.Run("BackendKeyData", func(t *testing.T) {
742+
t.Parallel()
743+
744+
f := pqtest.NewFake(t, func(f pqtest.Fake, cn net.Conn) {
745+
if _, ok := f.ReadStartup(cn); !ok {
746+
return
747+
}
748+
f.WriteMsg(cn, proto.AuthenticationRequest, "\x00\x00\x00\x00")
749+
f.WriteBackendKeyData(cn, 12345, make([]byte, 32))
750+
f.WriteMsg(cn, proto.ReadyForQuery, "I")
751+
for {
752+
code, _, ok := f.ReadMsg(cn)
753+
if !ok {
754+
return
755+
}
756+
switch code {
757+
case proto.Query:
758+
f.WriteMsg(cn, proto.EmptyQueryResponse, "")
759+
f.WriteMsg(cn, proto.ReadyForQuery, "I")
760+
case proto.Terminate:
761+
cn.Close()
762+
return
763+
}
764+
}
765+
})
766+
defer f.Close()
767+
768+
db := pqtest.MustDB(t, f.DSN()+" max_protocol_version=3.2")
769+
if err := db.Ping(); err != nil {
770+
t.Fatal(err)
771+
}
772+
})
773+
774+
t.Run("NegotiateProtocolVersion accepted", func(t *testing.T) {
775+
t.Parallel()
776+
777+
f := pqtest.NewFake(t, func(f pqtest.Fake, cn net.Conn) {
778+
if _, ok := f.ReadStartup(cn); !ok {
779+
return
780+
}
781+
f.WriteMsg(cn, proto.AuthenticationRequest, "\x00\x00\x00\x00")
782+
// Server says it only supports minor version 0 (i.e. 3.0).
783+
f.WriteNegotiateProtocolVersion(cn, 0, nil)
784+
f.WriteMsg(cn, proto.ReadyForQuery, "I")
785+
for {
786+
code, _, ok := f.ReadMsg(cn)
787+
if !ok {
788+
return
789+
}
790+
switch code {
791+
case proto.Query:
792+
f.WriteMsg(cn, proto.EmptyQueryResponse, "")
793+
f.WriteMsg(cn, proto.ReadyForQuery, "I")
794+
case proto.Terminate:
795+
cn.Close()
796+
return
797+
}
798+
}
799+
})
800+
defer f.Close()
801+
802+
// min=3.0, max=3.2: server negotiates down to 3.0, which is >= min, so OK.
803+
db := pqtest.MustDB(t, f.DSN()+" min_protocol_version=3.0 max_protocol_version=3.2")
804+
if err := db.Ping(); err != nil {
805+
t.Fatal(err)
806+
}
807+
})
808+
809+
t.Run("NegotiateProtocolVersion rejected", func(t *testing.T) {
810+
t.Parallel()
811+
812+
f := pqtest.NewFake(t, func(f pqtest.Fake, cn net.Conn) {
813+
if _, ok := f.ReadStartup(cn); !ok {
814+
return
815+
}
816+
f.WriteMsg(cn, proto.AuthenticationRequest, "\x00\x00\x00\x00")
817+
// Server says it only supports minor version 0 (i.e. 3.0).
818+
f.WriteNegotiateProtocolVersion(cn, 0, nil)
819+
// Client will disconnect; don't try to read further.
820+
})
821+
defer f.Close()
822+
823+
// min=3.2, max=3.2: server negotiates down to 3.0, which is < min, so error.
824+
db := pqtest.MustDB(t, f.DSN()+" min_protocol_version=3.2 max_protocol_version=3.2")
825+
err := db.Ping()
826+
if !pqtest.ErrorContains(err, "server does not support minimum protocol version") {
827+
t.Fatalf("wrong error\nhave: %v\nwant: server does not support minimum protocol version", err)
828+
}
829+
})
830+
}

internal/pqtest/fake.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,24 @@ func (f Fake) SimpleQuery(cn net.Conn, tag string, values ...any) {
246246

247247
f.WriteMsg(cn, proto.CommandComplete, tag+"\x00")
248248
}
249+
250+
// WriteBackendKeyData sends a BackendKeyData message with the given process ID
251+
// and secret key (variable length).
252+
func (f Fake) WriteBackendKeyData(cn net.Conn, pid int, secretKey []byte) {
253+
b := make([]byte, 4+len(secretKey))
254+
binary.BigEndian.PutUint32(b[0:4], uint32(pid))
255+
copy(b[4:], secretKey)
256+
f.WriteMsg(cn, proto.BackendKeyData, string(b))
257+
}
258+
259+
// WriteNegotiateProtocolVersion sends a NegotiateProtocolVersion message.
260+
func (f Fake) WriteNegotiateProtocolVersion(cn net.Conn, newestMinor int, options []string) {
261+
b := make([]byte, 8)
262+
binary.BigEndian.PutUint32(b[0:4], uint32(newestMinor))
263+
binary.BigEndian.PutUint32(b[4:8], uint32(len(options)))
264+
for _, o := range options {
265+
b = append(b, o...)
266+
b = append(b, 0)
267+
}
268+
f.WriteMsg(cn, proto.NegotiateProtocolVersion, string(b))
269+
}

internal/proto/proto.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
// Constants from pqcomm.h
1111
const (
1212
ProtocolVersion30 = (3 << 16) | 0 //lint:ignore SA4016 x
13-
ProtocolVersion32 = (3 << 16) | 2 // PostgreSQL ≥18; not yet supported.
13+
ProtocolVersion32 = (3 << 16) | 2 // PostgreSQL ≥18.
1414
CancelRequestCode = (1234 << 16) | 5678
1515
NegotiateSSLCode = (1234 << 16) | 5679
1616
NegotiateGSSCode = (1234 << 16) | 5680

0 commit comments

Comments
 (0)