Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@ import path or driver name.
| --- | --- | --- |
| [`QueryResultContext`](unified.go) | Executes arbitrary SQL and returns the response in the shape the server chose — exactly one of `driver.Rows` or `driver.Result`. Callers handling SQL they did not write (a proxy, a REPL) otherwise have to classify statements up front to pick between `QueryContext` and `ExecContext`, and a misclassification either discards a resultset or loses the OK-packet metadata. | Raised upstream as [go-sql-driver/mysql#1793](https://github.com/go-sql-driver/mysql/issues/1793), still open. Merged here as [#1](https://github.com/block/mysql/pull/1). |
| [`Warnings()`](warnings.go) | Exposes the warning count from the OK/EOF packet that terminated the last statement — the same number MySQL reports as `@@warning_count`. Warnings themselves live in per-connection state that only `SHOW WARNINGS` can read, so the count is what makes surfacing them affordable: it says whether that round trip would return anything. | Not yet raised upstream. Merged here as [#2](https://github.com/block/mysql/pull/2). |
| [RDS auto-TLS](rds.go) | A connection to an `*.rds.amazonaws.com` endpoint verifies against Amazon's RDS root bundle, embedded here, unless the DSN asked for something else. Without it every deployment ships its own copy of the bundle and its own `tls=` wiring, and an unencrypted RDS connection is a silent omission rather than an error. | Not yet raised upstream. |

Both are reached through `(*sql.Conn).Raw` and a structural interface
The first two are reached through `(*sql.Conn).Raw` and a structural interface
assertion, so a consumer can depend on the *capability* without a compile-time
dependency on this module. See the doc comments in `unified.go` and
`warnings.go` for the exact contracts.

RDS auto-TLS needs no API at all: it applies to any connection whose address
looks like an RDS or Aurora endpoint. Anything the DSN specifies still wins,
including `tls=false`, and `mysql.RDSTLSConfig()` returns the same
configuration for an RDS instance reached under a name that doesn't look like
one (a CNAME, or a proxy).

## What this fork changes

Two things, both for packaging reasons only. Neither alters protocol behaviour.
Expand Down Expand Up @@ -76,9 +83,10 @@ git fetch upstream
git merge upstream/master
```

Edits to upstream files are confined to two things: the module path and driver
name (`go.mod`, `driver.go`, plus doc comments and test call sites that spell
either one out), and the CI matrix (see below). The capabilities above live in
Edits to upstream files are confined to three things: the module path and
driver name (`go.mod`, `driver.go`, plus doc comments and test call sites that
spell either one out), the CI matrix (see below), and a one-line call in
`Config.normalize` that hands off to `rds.go`. The capabilities above live in
files upstream does not have, which is what keeps merges near-mechanical.
Additions are cheapest when they follow the same shape: new files, or new
methods on existing types, in preference to reworking an upstream code path.
Expand Down
5 changes: 5 additions & 0 deletions dsn.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ func (cfg *Config) normalize() error {
cfg.Addr = ensureHavePort(cfg.Addr)
}

// Fork addition: an RDS endpoint with no TLS asked for in the DSN gets the
// embedded RDS trust store. Runs before the switch below so that anything
// the DSN did specify still wins. See rds.go.
cfg.applyRDSAutoTLS()

if cfg.TLS == nil {
switch cfg.TLSConfig {
case "false", "":
Expand Down
108 changes: 108 additions & 0 deletions rds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2026 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.

package mysql

import (
"crypto/tls"
"crypto/x509"
_ "embed"
"regexp"
"sync"
)

// rdsGlobalBundle is Amazon's global RDS certificate bundle, containing the
// root CAs for every RDS region in the aws partition.
//
// Refresh it from the canonical location, which is a concatenation of the
// per-region bundles and is what the AWS documentation tells operators to
// download:
//
// curl -o rdsGlobalBundle.pem https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
//
// Adding a root is backwards compatible, so refreshing early costs nothing.
// The bundle covers the `aws` partition only; the China (`amazonaws.com.cn`)
// and GovCloud partitions publish separate trust stores, which is also why
// rdsAddr does not match their endpoint forms — see RDSTLSConfig for how to
// use a different bundle.
Comment thread
Copilot marked this conversation as resolved.
Outdated
//
//go:embed rdsGlobalBundle.pem
var rdsGlobalBundle []byte

// rdsAddr matches an Amazon RDS or Aurora endpoint, with an optional port.
//
// The leading dot is load-bearing: without it the pattern also accepts
// `notrds.amazonaws.com`, so a host outside RDS could pull a connection onto
// the RDS trust store. That misfires safely — verification against RDS roots
// fails, rather than trusting the wrong CA — but a confusing handshake error
// is still worse than not matching.
var rdsAddr = regexp.MustCompile(`\.rds\.amazonaws\.com(:\d+)?$`)

// IsRDSAddr reports whether addr is an Amazon RDS or Aurora endpoint, with or
// without a port. Connections to such an address are given TLS automatically;
// see RDSTLSConfig.
func IsRDSAddr(addr string) bool {
return rdsAddr.MatchString(addr)
}

// rdsRootCAs parses the embedded bundle once. A CertPool is safe for
// concurrent use once built, and every TLS config below shares this one.
var rdsRootCAs = sync.OnceValue(func() *x509.CertPool {
pool := x509.NewCertPool()
// A parse failure here would mean the embedded bundle is malformed, which
// is a build-time mistake rather than a runtime condition: the result is an
// empty pool, and every RDS connection then fails to verify. There is
// nothing useful to do about it at this point in the call path, and
// TestRDSGlobalBundleParses catches it before it can ship.
pool.AppendCertsFromPEM(rdsGlobalBundle)
return pool
})

// RDSTLSConfig returns a TLS configuration that verifies an Amazon RDS or
// Aurora server against the embedded RDS root bundle.
//
// Connections to an address IsRDSAddr recognizes get this automatically, so
// most callers never need it. Use it for an RDS instance reached under a name
// that does not look like one — a CNAME, or a proxy — by registering it and
// naming it in the DSN:
//
// mysql.RegisterTLSConfig("rds", mysql.RDSTLSConfig())
// db, _ := sql.Open("block-mysql", "user:pass@tcp(db.internal:3306)/schema?tls=rds")
//
// Each call returns a new config, so it can be modified freely — to trust a
// different partition's bundle, for instance. The returned config verifies the
// server name, so a proxy must present a certificate for the name dialed.
func RDSTLSConfig() *tls.Config {
return &tls.Config{
RootCAs: rdsRootCAs(),
// RDS has supported TLS 1.2 everywhere for years, and 1.0/1.1 are
// deprecated. Upstream leaves this to the Go default (currently 1.2 for
// clients); pinning it means a future default change cannot quietly
// weaken an RDS connection.
MinVersion: tls.VersionTLS12,
}
}

// applyRDSAutoTLS gives a connection to an RDS endpoint a verified TLS
// configuration when the DSN did not ask for one.
//
// This is a fork addition (see README.md). Upstream leaves TLS entirely to the
// DSN, which in an RDS deployment means every caller has to carry the bundle
// and wire up `tls=` itself — Block had three separate copies of exactly that
// before this existed. Doing it in the driver makes the safe thing the default
// while leaving it fully overridable: any explicit `tls=` in the DSN, including
// `tls=false`, is honoured, because this only fires when nothing else set one.
func (cfg *Config) applyRDSAutoTLS() {
if cfg.TLS != nil || cfg.TLSConfig != "" {
return
}
if !IsRDSAddr(cfg.Addr) {
return
}
cfg.TLS = RDSTLSConfig()
}
Loading
Loading