From 5f53e3b36b20b5e668cd3813c4c2e9f3ca3c5f75 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Fri, 4 Sep 2026 09:41:51 +0000 Subject: [PATCH 1/9] Honor BeforeConnect changes across connection setup --- connection.go | 32 +++++++-------- connector.go | 29 +++++++------- connector_test.go | 99 +++++++++++++++++++++++++++++++++++++++++++++++ packets.go | 12 +++--- packets_test.go | 16 ++++---- 5 files changed, 142 insertions(+), 46 deletions(-) diff --git a/connection.go b/connection.go index daff945ec..32e5ea354 100644 --- a/connection.go +++ b/connection.go @@ -24,22 +24,22 @@ import ( ) type mysqlConn struct { - buf buffer - netConn net.Conn - rawConn net.Conn // underlying connection when netConn is TLS connection. - result mysqlResult // managed by clearResult() and handleOkPacket(). - compIO *compIO - cfg *Config - connector *connector - maxAllowedPacket int - maxWriteSize int - capabilities capabilityFlag - extCapabilities extendedCapabilityFlag - status statusFlag - sequence uint8 - compressSequence uint8 - parseTime bool - compress bool + buf buffer + netConn net.Conn + rawConn net.Conn // underlying connection when netConn is TLS connection. + result mysqlResult // managed by clearResult() and handleOkPacket(). + compIO *compIO + cfg *Config + encodedAttributes string + maxAllowedPacket int + maxWriteSize int + capabilities capabilityFlag + extCapabilities extendedCapabilityFlag + status statusFlag + sequence uint8 + compressSequence uint8 + parseTime bool + compress bool // for context support (Go 1.8+) watching bool diff --git a/connector.go b/connector.go index 3d3760477..9cce18980 100644 --- a/connector.go +++ b/connector.go @@ -19,8 +19,7 @@ import ( ) type connector struct { - cfg *Config // immutable private copy. - encodedAttributes string // Encoded connection attributes. + cfg *Config // immutable private copy. } func encodeConnectionAttributes(cfg *Config) string { @@ -55,10 +54,8 @@ func encodeConnectionAttributes(cfg *Config) string { } func newConnector(cfg *Config) *connector { - encodedAttributes := encodeConnectionAttributes(cfg) return &connector{ - cfg: cfg, - encodedAttributes: encodedAttributes, + cfg: cfg, } } @@ -79,11 +76,11 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { // New mysqlConn mc := &mysqlConn{ - maxAllowedPacket: maxPacketSize, - maxWriteSize: maxPacketSize - 1, - closech: make(chan struct{}), - cfg: cfg, - connector: c, + maxAllowedPacket: maxPacketSize, + maxWriteSize: maxPacketSize - 1, + closech: make(chan struct{}), + cfg: cfg, + encodedAttributes: encodeConnectionAttributes(cfg), } mc.parseTime = mc.cfg.ParseTime @@ -91,12 +88,12 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { dctx := ctx if mc.cfg.Timeout > 0 { var cancel context.CancelFunc - dctx, cancel = context.WithTimeout(ctx, c.cfg.Timeout) + dctx, cancel = context.WithTimeout(ctx, mc.cfg.Timeout) defer cancel() } - if c.cfg.DialFunc != nil { - mc.netConn, err = c.cfg.DialFunc(dctx, mc.cfg.Net, mc.cfg.Addr) + if mc.cfg.DialFunc != nil { + mc.netConn, err = mc.cfg.DialFunc(dctx, mc.cfg.Net, mc.cfg.Addr) } else { dialsLock.RLock() dial, ok := dials[mc.cfg.Net] @@ -116,7 +113,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { // Enable TCP Keepalives on TCP connections if tc, ok := mc.netConn.(*net.TCPConn); ok { if err := tc.SetKeepAlive(true); err != nil { - c.cfg.Logger.Print(err) + mc.cfg.Logger.Print(err) } } @@ -145,7 +142,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { authResp, err := mc.auth(authData, plugin) if err != nil { // try the default auth plugin, if using the requested plugin failed - c.cfg.Logger.Print("could not use requested auth plugin '"+plugin+"': ", err.Error()) + mc.cfg.Logger.Print("could not use requested auth plugin '"+plugin+"': ", err.Error()) plugin = defaultAuthPlugin authResp, err = mc.auth(authData, plugin) if err != nil { @@ -153,7 +150,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { return nil, err } } - mc.initCapabilities(serverCapabilities, serverExtCapabilities, mc.cfg) + mc.initCapabilities(serverCapabilities, serverExtCapabilities) if err = mc.writeHandshakeResponsePacket(authResp, plugin); err != nil { mc.cleanup() return nil, err diff --git a/connector_test.go b/connector_test.go index 82d8c5989..753f8f73b 100644 --- a/connector_test.go +++ b/connector_test.go @@ -1,7 +1,9 @@ package mysql import ( + "bytes" "context" + "errors" "net" "testing" "time" @@ -28,3 +30,100 @@ func TestConnectorReturnsTimeout(t *testing.T) { t.Fatalf("expected %T, got %T", nerr, err) } } + +func TestBeforeConnectUsesEffectiveTimeout(t *testing.T) { + dialErr := errors.New("stop after observing dial context") + var remaining time.Duration + + cfg := NewConfig() + cfg.Timeout = 2 * time.Hour + cfg.DialFunc = func(ctx context.Context, _, _ string) (net.Conn, error) { + deadline, ok := ctx.Deadline() + if !ok { + t.Fatal("dial context has no deadline") + } + remaining = time.Until(deadline) + return nil, dialErr + } + if err := cfg.Apply(BeforeConnect(func(_ context.Context, cfg *Config) error { + cfg.Timeout = time.Hour + return nil + })); err != nil { + t.Fatal(err) + } + + connector, err := NewConnector(cfg) + if err != nil { + t.Fatal(err) + } + if _, err := connector.Connect(context.Background()); !errors.Is(err, dialErr) { + t.Fatalf("Connect() error = %v, want %v", err, dialErr) + } + if remaining < 59*time.Minute || remaining > 61*time.Minute { + t.Fatalf("dial timeout = %v, want about 1h", remaining) + } +} + +func TestBeforeConnectUsesEffectiveDialerAndAttributes(t *testing.T) { + mock := &mockConn{ + data: []byte{72, 0, 0, 0, 10, 53, 46, 53, 46, 56, 0, 165, 0, 0, 0, + 60, 70, 63, 58, 68, 104, 34, 97, 0, 223, 247, 33, 2, 0, 31, 128, 21, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 98, 120, 114, 47, 85, 75, 109, 99, 51, 77, + 50, 64, 0, 109, 121, 115, 113, 108, 95, 110, 97, 116, 105, 118, 101, 95, + 112, 97, 115, 115, 119, 111, 114, 100}, + queuedReplies: [][]byte{ + {7, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0}, + }, + } + + var ( + initialDialCalled bool + dialNetwork string + dialAddress string + ) + cfg := NewConfig() + cfg.Addr = "initial.example:3306" + cfg.ConnectionAttributes = "phase:initial" + cfg.DialFunc = func(context.Context, string, string) (net.Conn, error) { + initialDialCalled = true + return nil, errors.New("initial dialer must not be called") + } + if err := cfg.Apply(BeforeConnect(func(_ context.Context, cfg *Config) error { + cfg.Addr = "callback.example:3306" + cfg.ConnectionAttributes = "phase:callback" + cfg.DialFunc = func(_ context.Context, network, address string) (net.Conn, error) { + dialNetwork = network + dialAddress = address + return mock, nil + } + return nil + })); err != nil { + t.Fatal(err) + } + + connector, err := NewConnector(cfg) + if err != nil { + t.Fatal(err) + } + conn, err := connector.Connect(context.Background()) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + + if initialDialCalled { + t.Fatal("Connect() used the pre-callback DialFunc") + } + if dialNetwork != "tcp" || dialAddress != "callback.example:3306" { + t.Fatalf("dialed %s(%s), want tcp(callback.example:3306)", dialNetwork, dialAddress) + } + if !bytes.Contains(mock.written, []byte("phase\bcallback")) { + t.Fatalf("handshake response does not contain callback attributes: %q", mock.written) + } + if bytes.Contains(mock.written, []byte("phase\x07initial")) { + t.Fatalf("handshake response contains stale attributes: %q", mock.written) + } + if !bytes.Contains(mock.written, []byte("callback.example")) { + t.Fatalf("handshake response does not contain callback server host: %q", mock.written) + } +} diff --git a/packets.go b/packets.go index d0b21b06c..7af7202b2 100644 --- a/packets.go +++ b/packets.go @@ -277,7 +277,7 @@ func (mc *mysqlConn) readHandshakePacket() (data []byte, capabilities capability } // initCapabilities initializes the capabilities based on server support and configuration -func (mc *mysqlConn) initCapabilities(serverCapabilities capabilityFlag, serverExtCapabilities extendedCapabilityFlag, cfg *Config) { +func (mc *mysqlConn) initCapabilities(serverCapabilities capabilityFlag, serverExtCapabilities extendedCapabilityFlag) { clientCapabilities := clientMySQL | clientLongFlag | @@ -291,10 +291,10 @@ func (mc *mysqlConn) initCapabilities(serverCapabilities capabilityFlag, serverE clientConnectAttrs | clientDeprecateEOF - if cfg.ClientFoundRows { + if mc.cfg.ClientFoundRows { clientCapabilities |= clientFoundRows } - if cfg.compress { + if mc.cfg.compress { clientCapabilities |= clientCompress } // To enable TLS / SSL @@ -305,7 +305,7 @@ func (mc *mysqlConn) initCapabilities(serverCapabilities capabilityFlag, serverE if mc.cfg.MultiStatements { clientCapabilities |= clientMultiStatements } - if n := len(cfg.DBName); n > 0 { + if n := len(mc.cfg.DBName); n > 0 { clientCapabilities |= clientConnectWithDB } @@ -405,9 +405,9 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string // Connection Attributes if mc.capabilities&clientConnectAttrs != 0 { - connAttrsLen := len(mc.connector.encodedAttributes) + connAttrsLen := len(mc.encodedAttributes) data = appendLengthEncodedInteger(data, uint64(connAttrsLen)) - data = append(data, mc.connector.encodedAttributes...) + data = append(data, mc.encodedAttributes...) } // Send Auth packet diff --git a/packets_test.go b/packets_test.go index b487051e2..cb2039d25 100644 --- a/packets_test.go +++ b/packets_test.go @@ -96,15 +96,15 @@ var _ net.Conn = new(mockConn) func newRWMockConn(sequence uint8) (*mockConn, *mysqlConn) { conn := new(mockConn) - connector := newConnector(NewConfig()) + cfg := NewConfig() mc := &mysqlConn{ - buf: newBuffer(), - cfg: connector.cfg, - connector: connector, - netConn: conn, - closech: make(chan struct{}), - maxAllowedPacket: defaultMaxAllowedPacket, - sequence: sequence, + buf: newBuffer(), + cfg: cfg, + encodedAttributes: encodeConnectionAttributes(cfg), + netConn: conn, + closech: make(chan struct{}), + maxAllowedPacket: defaultMaxAllowedPacket, + sequence: sequence, } return conn, mc } From 614066819d158aa9e30b385012fa75ca8173cbaa Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Fri, 4 Sep 2026 10:32:03 +0000 Subject: [PATCH 2/9] Update derived TLS server name after BeforeConnect --- connector.go | 1 + connector_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ dsn.go | 18 ++++++++++++------ dsn_test.go | 1 + 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/connector.go b/connector.go index 9cce18980..bb3299479 100644 --- a/connector.go +++ b/connector.go @@ -72,6 +72,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { if err != nil { return nil, err } + cfg.normalizeTLSConfigServerName() } // New mysqlConn diff --git a/connector_test.go b/connector_test.go index 753f8f73b..68859eebf 100644 --- a/connector_test.go +++ b/connector_test.go @@ -3,6 +3,7 @@ package mysql import ( "bytes" "context" + "crypto/tls" "errors" "net" "testing" @@ -64,6 +65,49 @@ func TestBeforeConnectUsesEffectiveTimeout(t *testing.T) { } } +func TestBeforeConnectUpdatesDerivedTLSServerName(t *testing.T) { + dialErr := errors.New("stop after observing effective config") + tests := []struct { + name string + serverName string + want string + }{ + {"derived", "", "callback.example"}, + {"explicit", "database.example", "database.example"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var effectiveCfg *Config + + cfg := NewConfig() + cfg.Addr = "initial.example:3306" + cfg.TLS = &tls.Config{ServerName: tc.serverName} + cfg.DialFunc = func(context.Context, string, string) (net.Conn, error) { + return nil, dialErr + } + if err := cfg.Apply(BeforeConnect(func(_ context.Context, cfg *Config) error { + cfg.Addr = "callback.example:3306" + effectiveCfg = cfg + return nil + })); err != nil { + t.Fatal(err) + } + + connector, err := NewConnector(cfg) + if err != nil { + t.Fatal(err) + } + if _, err := connector.Connect(context.Background()); !errors.Is(err, dialErr) { + t.Fatalf("Connect() error = %v, want %v", err, dialErr) + } + if got := effectiveCfg.TLS.ServerName; got != tc.want { + t.Errorf("TLS ServerName = %q, want %q", got, tc.want) + } + }) + } +} + func TestBeforeConnectUsesEffectiveDialerAndAttributes(t *testing.T) { mock := &mockConn{ data: []byte{72, 0, 0, 0, 10, 53, 46, 53, 46, 56, 0, 165, 0, 0, 0, diff --git a/dsn.go b/dsn.go index d43367eaf..f815a8514 100644 --- a/dsn.go +++ b/dsn.go @@ -84,6 +84,7 @@ type Config struct { paramOrder []string // Order of connection parameters parsed from the DSN pubKey *rsa.PublicKey // Server public key timeTruncate time.Duration // Truncate time.Time values to the specified duration + tlsServerName string // TLS server name automatically derived from Addr charsets []string // Connection charset. When set, this will be set in SET NAMES query } @@ -246,12 +247,7 @@ func (cfg *Config) normalize() error { } } - if cfg.TLS != nil && cfg.TLS.ServerName == "" && !cfg.TLS.InsecureSkipVerify { - host, _, err := net.SplitHostPort(cfg.Addr) - if err == nil { - cfg.TLS.ServerName = host - } - } + cfg.normalizeTLSConfigServerName() if cfg.ServerPubKey != "" { cfg.pubKey = getServerPubKey(cfg.ServerPubKey) @@ -267,6 +263,16 @@ func (cfg *Config) normalize() error { return nil } +func (cfg *Config) normalizeTLSConfigServerName() { + if cfg.TLS != nil && cfg.TLS.ServerName == cfg.tlsServerName && !cfg.TLS.InsecureSkipVerify { + host, _, err := net.SplitHostPort(cfg.Addr) + if err == nil { + cfg.TLS.ServerName = host + cfg.tlsServerName = host + } + } +} + func writeDSNParam(buf *bytes.Buffer, hasParam *bool, name, value string) { buf.Grow(1 + len(name) + 1 + len(value)) if !*hasParam { diff --git a/dsn_test.go b/dsn_test.go index 2c1f3f889..c8e4e6fab 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -241,6 +241,7 @@ func TestDSNParser(t *testing.T) { // pointer not static cfg.TLS = nil + cfg.tlsServerName = "" if !reflect.DeepEqual(cfg, tst.out) { t.Errorf("%d. ParseDSN(%q) mismatch:\ngot %+v\nwant %+v", i, tst.in, cfg, tst.out) From ef9f06bcd7fb53702c430d03962bc3517dd11f7a Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Fri, 4 Sep 2026 12:05:05 +0000 Subject: [PATCH 3/9] Revert "Update derived TLS server name after BeforeConnect" This reverts commit 614066819d158aa9e30b385012fa75ca8173cbaa. --- connector.go | 1 - connector_test.go | 44 -------------------------------------------- dsn.go | 18 ++++++------------ dsn_test.go | 1 - 4 files changed, 6 insertions(+), 58 deletions(-) diff --git a/connector.go b/connector.go index bb3299479..9cce18980 100644 --- a/connector.go +++ b/connector.go @@ -72,7 +72,6 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { if err != nil { return nil, err } - cfg.normalizeTLSConfigServerName() } // New mysqlConn diff --git a/connector_test.go b/connector_test.go index 68859eebf..753f8f73b 100644 --- a/connector_test.go +++ b/connector_test.go @@ -3,7 +3,6 @@ package mysql import ( "bytes" "context" - "crypto/tls" "errors" "net" "testing" @@ -65,49 +64,6 @@ func TestBeforeConnectUsesEffectiveTimeout(t *testing.T) { } } -func TestBeforeConnectUpdatesDerivedTLSServerName(t *testing.T) { - dialErr := errors.New("stop after observing effective config") - tests := []struct { - name string - serverName string - want string - }{ - {"derived", "", "callback.example"}, - {"explicit", "database.example", "database.example"}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var effectiveCfg *Config - - cfg := NewConfig() - cfg.Addr = "initial.example:3306" - cfg.TLS = &tls.Config{ServerName: tc.serverName} - cfg.DialFunc = func(context.Context, string, string) (net.Conn, error) { - return nil, dialErr - } - if err := cfg.Apply(BeforeConnect(func(_ context.Context, cfg *Config) error { - cfg.Addr = "callback.example:3306" - effectiveCfg = cfg - return nil - })); err != nil { - t.Fatal(err) - } - - connector, err := NewConnector(cfg) - if err != nil { - t.Fatal(err) - } - if _, err := connector.Connect(context.Background()); !errors.Is(err, dialErr) { - t.Fatalf("Connect() error = %v, want %v", err, dialErr) - } - if got := effectiveCfg.TLS.ServerName; got != tc.want { - t.Errorf("TLS ServerName = %q, want %q", got, tc.want) - } - }) - } -} - func TestBeforeConnectUsesEffectiveDialerAndAttributes(t *testing.T) { mock := &mockConn{ data: []byte{72, 0, 0, 0, 10, 53, 46, 53, 46, 56, 0, 165, 0, 0, 0, diff --git a/dsn.go b/dsn.go index f815a8514..d43367eaf 100644 --- a/dsn.go +++ b/dsn.go @@ -84,7 +84,6 @@ type Config struct { paramOrder []string // Order of connection parameters parsed from the DSN pubKey *rsa.PublicKey // Server public key timeTruncate time.Duration // Truncate time.Time values to the specified duration - tlsServerName string // TLS server name automatically derived from Addr charsets []string // Connection charset. When set, this will be set in SET NAMES query } @@ -247,7 +246,12 @@ func (cfg *Config) normalize() error { } } - cfg.normalizeTLSConfigServerName() + if cfg.TLS != nil && cfg.TLS.ServerName == "" && !cfg.TLS.InsecureSkipVerify { + host, _, err := net.SplitHostPort(cfg.Addr) + if err == nil { + cfg.TLS.ServerName = host + } + } if cfg.ServerPubKey != "" { cfg.pubKey = getServerPubKey(cfg.ServerPubKey) @@ -263,16 +267,6 @@ func (cfg *Config) normalize() error { return nil } -func (cfg *Config) normalizeTLSConfigServerName() { - if cfg.TLS != nil && cfg.TLS.ServerName == cfg.tlsServerName && !cfg.TLS.InsecureSkipVerify { - host, _, err := net.SplitHostPort(cfg.Addr) - if err == nil { - cfg.TLS.ServerName = host - cfg.tlsServerName = host - } - } -} - func writeDSNParam(buf *bytes.Buffer, hasParam *bool, name, value string) { buf.Grow(1 + len(name) + 1 + len(value)) if !*hasParam { diff --git a/dsn_test.go b/dsn_test.go index c8e4e6fab..2c1f3f889 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -241,7 +241,6 @@ func TestDSNParser(t *testing.T) { // pointer not static cfg.TLS = nil - cfg.tlsServerName = "" if !reflect.DeepEqual(cfg, tst.out) { t.Errorf("%d. ParseDSN(%q) mismatch:\ngot %+v\nwant %+v", i, tst.in, cfg, tst.out) From 04475502d238208636ac36482f79c082b6405d9c Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Sat, 5 Sep 2026 07:50:13 +0000 Subject: [PATCH 4/9] Store encoded attributes in normalized config --- connection.go | 31 +++++++++++++++---------------- connector.go | 12 +++++++----- dsn.go | 12 +++++++----- dsn_test.go | 1 + packets.go | 4 ++-- packets_test.go | 16 +++++++++------- 6 files changed, 41 insertions(+), 35 deletions(-) diff --git a/connection.go b/connection.go index 32e5ea354..99423d0d8 100644 --- a/connection.go +++ b/connection.go @@ -24,22 +24,21 @@ import ( ) type mysqlConn struct { - buf buffer - netConn net.Conn - rawConn net.Conn // underlying connection when netConn is TLS connection. - result mysqlResult // managed by clearResult() and handleOkPacket(). - compIO *compIO - cfg *Config - encodedAttributes string - maxAllowedPacket int - maxWriteSize int - capabilities capabilityFlag - extCapabilities extendedCapabilityFlag - status statusFlag - sequence uint8 - compressSequence uint8 - parseTime bool - compress bool + buf buffer + netConn net.Conn + rawConn net.Conn // underlying connection when netConn is TLS connection. + result mysqlResult // managed by clearResult() and handleOkPacket(). + compIO *compIO + cfg *Config + maxAllowedPacket int + maxWriteSize int + capabilities capabilityFlag + extCapabilities extendedCapabilityFlag + status statusFlag + sequence uint8 + compressSequence uint8 + parseTime bool + compress bool // for context support (Go 1.8+) watching bool diff --git a/connector.go b/connector.go index 9cce18980..8da918938 100644 --- a/connector.go +++ b/connector.go @@ -72,15 +72,17 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { if err != nil { return nil, err } + if err = cfg.normalize(); err != nil { + return nil, err + } } // New mysqlConn mc := &mysqlConn{ - maxAllowedPacket: maxPacketSize, - maxWriteSize: maxPacketSize - 1, - closech: make(chan struct{}), - cfg: cfg, - encodedAttributes: encodeConnectionAttributes(cfg), + maxAllowedPacket: maxPacketSize, + maxWriteSize: maxPacketSize - 1, + closech: make(chan struct{}), + cfg: cfg, } mc.parseTime = mc.cfg.ParseTime diff --git a/dsn.go b/dsn.go index d43367eaf..74abd7587 100644 --- a/dsn.go +++ b/dsn.go @@ -80,11 +80,12 @@ type Config struct { compress bool // Enable zlib compression tinyInt1IsBool bool // Treat signed TINYINT(1) as boolean - beforeConnect func(context.Context, *Config) error // Invoked before a connection is established - paramOrder []string // Order of connection parameters parsed from the DSN - pubKey *rsa.PublicKey // Server public key - timeTruncate time.Duration // Truncate time.Time values to the specified duration - charsets []string // Connection charset. When set, this will be set in SET NAMES query + beforeConnect func(context.Context, *Config) error // Invoked before a connection is established + encodedAttributes string // Encoded connection attributes + paramOrder []string // Order of connection parameters parsed from the DSN + pubKey *rsa.PublicKey // Server public key + timeTruncate time.Duration // Truncate time.Time values to the specified duration + charsets []string // Connection charset. When set, this will be set in SET NAMES query } // Functional Options Pattern @@ -263,6 +264,7 @@ func (cfg *Config) normalize() error { if cfg.Logger == nil { cfg.Logger = defaultLogger } + cfg.encodedAttributes = encodeConnectionAttributes(cfg) return nil } diff --git a/dsn_test.go b/dsn_test.go index 2c1f3f889..120550cf4 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -33,6 +33,7 @@ func newTestConfig(update func(*Config)) *Config { if update != nil { update(cfg) } + cfg.encodedAttributes = encodeConnectionAttributes(cfg) return cfg } diff --git a/packets.go b/packets.go index 7af7202b2..d08969d9b 100644 --- a/packets.go +++ b/packets.go @@ -405,9 +405,9 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string // Connection Attributes if mc.capabilities&clientConnectAttrs != 0 { - connAttrsLen := len(mc.encodedAttributes) + connAttrsLen := len(mc.cfg.encodedAttributes) data = appendLengthEncodedInteger(data, uint64(connAttrsLen)) - data = append(data, mc.encodedAttributes...) + data = append(data, mc.cfg.encodedAttributes...) } // Send Auth packet diff --git a/packets_test.go b/packets_test.go index cb2039d25..17bfe15c8 100644 --- a/packets_test.go +++ b/packets_test.go @@ -97,14 +97,16 @@ var _ net.Conn = new(mockConn) func newRWMockConn(sequence uint8) (*mockConn, *mysqlConn) { conn := new(mockConn) cfg := NewConfig() + if err := cfg.normalize(); err != nil { + panic(err) + } mc := &mysqlConn{ - buf: newBuffer(), - cfg: cfg, - encodedAttributes: encodeConnectionAttributes(cfg), - netConn: conn, - closech: make(chan struct{}), - maxAllowedPacket: defaultMaxAllowedPacket, - sequence: sequence, + buf: newBuffer(), + cfg: cfg, + netConn: conn, + closech: make(chan struct{}), + maxAllowedPacket: defaultMaxAllowedPacket, + sequence: sequence, } return conn, mc } From 5507d8b54e4c158f3f822c8be78b894d9b8c9238 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Sat, 5 Sep 2026 08:14:01 +0000 Subject: [PATCH 5/9] Document mock handshake packet fields --- connector_test.go | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/connector_test.go b/connector_test.go index 753f8f73b..0725877e4 100644 --- a/connector_test.go +++ b/connector_test.go @@ -65,15 +65,32 @@ func TestBeforeConnectUsesEffectiveTimeout(t *testing.T) { } func TestBeforeConnectUsesEffectiveDialerAndAttributes(t *testing.T) { + serverHandshake := []byte( + "\x48\x00\x00\x00" + // Packet header: 72-byte payload, sequence 0. + "\x0a" + // Protocol version 10. + "5.5.8\x00" + // NUL-terminated server version. + "\xa5\x00\x00\x00" + // Connection ID 165. + " Date: Sat, 5 Sep 2026 08:24:09 +0000 Subject: [PATCH 6/9] Refresh derived TLS server name after BeforeConnect --- connector_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ dsn.go | 8 +++++--- dsn_test.go | 1 + 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/connector_test.go b/connector_test.go index 0725877e4..593d9857c 100644 --- a/connector_test.go +++ b/connector_test.go @@ -3,6 +3,7 @@ package mysql import ( "bytes" "context" + "crypto/tls" "errors" "net" "testing" @@ -64,6 +65,52 @@ func TestBeforeConnectUsesEffectiveTimeout(t *testing.T) { } } +func TestBeforeConnectUpdatesDerivedTLSServerName(t *testing.T) { + dialErr := errors.New("stop after observing effective config") + tests := []struct { + name string + serverName string + want string + wantDerived bool + }{ + {"derived", "", "callback.example", true}, + {"explicit", "database.example", "database.example", false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var effectiveCfg *Config + cfg := NewConfig() + cfg.Addr = "initial.example:3306" + cfg.TLS = &tls.Config{ServerName: tc.serverName} + cfg.DialFunc = func(context.Context, string, string) (net.Conn, error) { + return nil, dialErr + } + if err := cfg.Apply(BeforeConnect(func(_ context.Context, cfg *Config) error { + cfg.Addr = "callback.example:3306" + effectiveCfg = cfg + return nil + })); err != nil { + t.Fatal(err) + } + + connector, err := NewConnector(cfg) + if err != nil { + t.Fatal(err) + } + if _, err := connector.Connect(context.Background()); !errors.Is(err, dialErr) { + t.Fatalf("Connect() error = %v, want %v", err, dialErr) + } + if got := effectiveCfg.TLS.ServerName; got != tc.want { + t.Errorf("TLS ServerName = %q, want %q", got, tc.want) + } + if got := effectiveCfg.tlsServerNameDerived; got != tc.wantDerived { + t.Errorf("tlsServerNameDerived = %v, want %v", got, tc.wantDerived) + } + }) + } +} + func TestBeforeConnectUsesEffectiveDialerAndAttributes(t *testing.T) { serverHandshake := []byte( "\x48\x00\x00\x00" + // Packet header: 72-byte payload, sequence 0. diff --git a/dsn.go b/dsn.go index 74abd7587..d4d48922c 100644 --- a/dsn.go +++ b/dsn.go @@ -77,8 +77,9 @@ type Config struct { // unexported fields. new options should be come here. // boolean first. alphabetical order. - compress bool // Enable zlib compression - tinyInt1IsBool bool // Treat signed TINYINT(1) as boolean + compress bool // Enable zlib compression + tinyInt1IsBool bool // Treat signed TINYINT(1) as boolean + tlsServerNameDerived bool // Whether TLS.ServerName was derived from Addr beforeConnect func(context.Context, *Config) error // Invoked before a connection is established encodedAttributes string // Encoded connection attributes @@ -247,10 +248,11 @@ func (cfg *Config) normalize() error { } } - if cfg.TLS != nil && cfg.TLS.ServerName == "" && !cfg.TLS.InsecureSkipVerify { + if cfg.TLS != nil && (cfg.TLS.ServerName == "" || cfg.tlsServerNameDerived) && !cfg.TLS.InsecureSkipVerify { host, _, err := net.SplitHostPort(cfg.Addr) if err == nil { cfg.TLS.ServerName = host + cfg.tlsServerNameDerived = true } } diff --git a/dsn_test.go b/dsn_test.go index 120550cf4..825e205ac 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -242,6 +242,7 @@ func TestDSNParser(t *testing.T) { // pointer not static cfg.TLS = nil + cfg.tlsServerNameDerived = false if !reflect.DeepEqual(cfg, tst.out) { t.Errorf("%d. ParseDSN(%q) mismatch:\ngot %+v\nwant %+v", i, tst.in, cfg, tst.out) From 4b4fbcbd68a157b11a18b777961c2026e013921a Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Sat, 5 Sep 2026 14:29:50 +0000 Subject: [PATCH 7/9] Revert "Refresh derived TLS server name after BeforeConnect" This reverts commit 1f0dac37a5c9eb00667f65fd7873e3de1a59836a. --- connector_test.go | 47 ----------------------------------------------- dsn.go | 8 +++----- dsn_test.go | 1 - 3 files changed, 3 insertions(+), 53 deletions(-) diff --git a/connector_test.go b/connector_test.go index 593d9857c..0725877e4 100644 --- a/connector_test.go +++ b/connector_test.go @@ -3,7 +3,6 @@ package mysql import ( "bytes" "context" - "crypto/tls" "errors" "net" "testing" @@ -65,52 +64,6 @@ func TestBeforeConnectUsesEffectiveTimeout(t *testing.T) { } } -func TestBeforeConnectUpdatesDerivedTLSServerName(t *testing.T) { - dialErr := errors.New("stop after observing effective config") - tests := []struct { - name string - serverName string - want string - wantDerived bool - }{ - {"derived", "", "callback.example", true}, - {"explicit", "database.example", "database.example", false}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var effectiveCfg *Config - cfg := NewConfig() - cfg.Addr = "initial.example:3306" - cfg.TLS = &tls.Config{ServerName: tc.serverName} - cfg.DialFunc = func(context.Context, string, string) (net.Conn, error) { - return nil, dialErr - } - if err := cfg.Apply(BeforeConnect(func(_ context.Context, cfg *Config) error { - cfg.Addr = "callback.example:3306" - effectiveCfg = cfg - return nil - })); err != nil { - t.Fatal(err) - } - - connector, err := NewConnector(cfg) - if err != nil { - t.Fatal(err) - } - if _, err := connector.Connect(context.Background()); !errors.Is(err, dialErr) { - t.Fatalf("Connect() error = %v, want %v", err, dialErr) - } - if got := effectiveCfg.TLS.ServerName; got != tc.want { - t.Errorf("TLS ServerName = %q, want %q", got, tc.want) - } - if got := effectiveCfg.tlsServerNameDerived; got != tc.wantDerived { - t.Errorf("tlsServerNameDerived = %v, want %v", got, tc.wantDerived) - } - }) - } -} - func TestBeforeConnectUsesEffectiveDialerAndAttributes(t *testing.T) { serverHandshake := []byte( "\x48\x00\x00\x00" + // Packet header: 72-byte payload, sequence 0. diff --git a/dsn.go b/dsn.go index d4d48922c..74abd7587 100644 --- a/dsn.go +++ b/dsn.go @@ -77,9 +77,8 @@ type Config struct { // unexported fields. new options should be come here. // boolean first. alphabetical order. - compress bool // Enable zlib compression - tinyInt1IsBool bool // Treat signed TINYINT(1) as boolean - tlsServerNameDerived bool // Whether TLS.ServerName was derived from Addr + compress bool // Enable zlib compression + tinyInt1IsBool bool // Treat signed TINYINT(1) as boolean beforeConnect func(context.Context, *Config) error // Invoked before a connection is established encodedAttributes string // Encoded connection attributes @@ -248,11 +247,10 @@ func (cfg *Config) normalize() error { } } - if cfg.TLS != nil && (cfg.TLS.ServerName == "" || cfg.tlsServerNameDerived) && !cfg.TLS.InsecureSkipVerify { + if cfg.TLS != nil && cfg.TLS.ServerName == "" && !cfg.TLS.InsecureSkipVerify { host, _, err := net.SplitHostPort(cfg.Addr) if err == nil { cfg.TLS.ServerName = host - cfg.tlsServerNameDerived = true } } diff --git a/dsn_test.go b/dsn_test.go index 825e205ac..120550cf4 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -242,7 +242,6 @@ func TestDSNParser(t *testing.T) { // pointer not static cfg.TLS = nil - cfg.tlsServerNameDerived = false if !reflect.DeepEqual(cfg, tst.out) { t.Errorf("%d. ParseDSN(%q) mismatch:\ngot %+v\nwant %+v", i, tst.in, cfg, tst.out) From 5472c9e0e459bdd7f294f1e0f9f070ccb97e8680 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Sat, 5 Sep 2026 14:44:14 +0000 Subject: [PATCH 8/9] Only refresh connection attributes after BeforeConnect --- connector.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/connector.go b/connector.go index 8da918938..0a90f0367 100644 --- a/connector.go +++ b/connector.go @@ -72,9 +72,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { if err != nil { return nil, err } - if err = cfg.normalize(); err != nil { - return nil, err - } + cfg.encodedAttributes = encodeConnectionAttributes(cfg) } // New mysqlConn From 80d4888679c880a304574daf06f6414e59a57671 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Sat, 5 Sep 2026 15:00:02 +0000 Subject: [PATCH 9/9] Document TLS requirements for BeforeConnect --- dsn.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dsn.go b/dsn.go index 74abd7587..0bbb2ea9a 100644 --- a/dsn.go +++ b/dsn.go @@ -147,6 +147,9 @@ func TimeTruncate(d time.Duration) Option { } // BeforeConnect sets the function to be invoked before a connection is established. +// If the function changes [Config.Addr] while [Config.TLS] is non-nil and its +// InsecureSkipVerify field is false, it must also update ServerName to match +// the hostname in the new address. func BeforeConnect(fn func(context.Context, *Config) error) Option { return func(cfg *Config) error { cfg.beforeConnect = fn