examples/mysql-public-endpoint-acceptance/main.go
Rama main · réplica de lectura del repositorio original.
// Disposable native MySQL public endpoint acceptance probe. Never prints credentials.
package main
import (
"bytes"
"context"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"database/sql"
"encoding/hex"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"os"
"os/signal"
"strconv"
"syscall"
"time"
mysql "github.com/go-sql-driver/mysql"
)
func fail(message string) { fmt.Fprintln(os.Stderr, message); os.Exit(1) }
func step() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), 5*time.Second)
}
func main() {
mode := flag.String("mode", "probe", "probe or hold")
check := flag.String("check", "verified", "verified, crud, data-preserved, reader-write-rejected, plaintext-rejected, wrong-hostname-rejected, wrong-ca-rejected, bad-password-rejected or unreachable")
host := flag.String("host", "", "reviewed public hostname")
port := flag.Uint("port", 0, "reviewed public port")
ca := flag.String("ca", "", "public CA file")
fingerprint := flag.String("expect-fingerprint", "", "optional SHA256 of the expected served leaf")
purpose := flag.String("purpose", "read_write", "read_write or read_only")
revocation := flag.Duration("expect-revocation-within", 0, "require the held session to close within this duration")
flag.Parse()
if *mode == "hold" {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
select {
case <-ctx.Done():
case <-time.After(2 * time.Hour):
}
return
}
checks := map[string]bool{"verified": true, "crud": true, "data-preserved": true, "reader-write-rejected": true, "plaintext-rejected": true, "wrong-hostname-rejected": true, "wrong-ca-rejected": true, "bad-password-rejected": true, "unreachable": true}
if *mode != "probe" || !checks[*check] || *host == "" || len(*host) > 253 || *port < 1 || *port > 65535 || *ca == "" || os.Getenv("MYSQL_PWD") == "" || (*purpose != "read_write" && *purpose != "read_only") || *revocation < 0 || *revocation > 10*time.Minute {
fail("provide valid bounded probe options and MYSQL_PWD")
}
file, err := os.Open(*ca)
if err != nil {
fail("public CA unavailable")
}
pem, err := io.ReadAll(io.LimitReader(file, 65537))
file.Close()
roots := x509.NewCertPool()
if err != nil || len(pem) > 65536 || !roots.AppendCertsFromPEM(pem) {
fail("public CA invalid")
}
if *fingerprint != "" {
raw, err := hex.DecodeString(*fingerprint)
if err != nil || len(raw) != 32 {
fail("expected fingerprint is invalid")
}
}
address := net.JoinHostPort(*host, strconv.Itoa(int(*port)))
config := mysql.NewConfig()
config.User = "app"
config.Passwd = os.Getenv("MYSQL_PWD")
config.Net = "tcp"
config.Addr = address
config.DBName = "app"
config.MaxAllowedPacket = 1 << 20
config.Logger = log.New(io.Discard, "", 0)
config.Timeout = 5 * time.Second
config.ReadTimeout = 5 * time.Second
config.WriteTimeout = 5 * time.Second
config.TLS = &tls.Config{MinVersion: tls.VersionTLS12, RootCAs: roots, ServerName: *host}
if *fingerprint != "" {
config.TLS.VerifyConnection = func(state tls.ConnectionState) error {
if len(state.PeerCertificates) == 0 {
return errors.New("server certificate missing")
}
hash := sha256.Sum256(state.PeerCertificates[0].Raw)
if hex.EncodeToString(hash[:]) != *fingerprint {
return errors.New("server certificate differs from expected issued leaf")
}
return nil
}
}
open := func(candidate *mysql.Config) (*sql.DB, *sql.Conn, error) {
connector, err := mysql.NewConnector(candidate)
if err != nil {
return nil, nil, err
}
db := sql.OpenDB(connector)
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(0)
ctx, cancel := step()
defer cancel()
conn, err := db.Conn(ctx)
if err != nil {
db.Close()
return nil, nil, err
}
if err = conn.PingContext(ctx); err != nil {
conn.Close()
db.Close()
return nil, nil, err
}
return db, conn, nil
}
db, conn, err := open(config)
if *check == "unreachable" {
if err == nil {
conn.Close()
db.Close()
fail("unlisted source reached the public MySQL endpoint")
}
var dns *net.DNSError
var authentication *mysql.MySQLError
var certificate *tls.CertificateVerificationError
var hostname x509.HostnameError
var authority x509.UnknownAuthorityError
if errors.As(err, &dns) || errors.As(err, &authentication) || errors.As(err, &certificate) || errors.As(err, &hostname) || errors.As(err, &authority) {
fail("source refusal cannot be proved by DNS, authentication or certificate failure")
}
fmt.Println("PASS unreachable")
return
}
if err != nil {
fail("verified MySQL TLS connection failed")
}
defer db.Close()
defer conn.Close()
ctx, cancel := step()
var name, version string
var readOnly int
err = conn.QueryRowContext(ctx, "SHOW SESSION STATUS LIKE 'Ssl_version'").Scan(&name, &version)
cancel()
if err != nil || name != "Ssl_version" || (version != "TLSv1.2" && version != "TLSv1.3") {
fail("native session TLS evidence unavailable")
}
ctx, cancel = step()
err = conn.QueryRowContext(ctx, "SELECT @@super_read_only").Scan(&readOnly)
cancel()
if err != nil || (readOnly != 0 && readOnly != 1) || (readOnly == 1) != (*purpose == "read_only") {
fail("endpoint served wrong MySQL role")
}
if *check == "crud" || *check == "data-preserved" || *check == "reader-write-rejected" {
payload := []byte{0, 255, 10, 13, 128, 65}
exec := func(query string, args ...any) error {
ctx, cancel := step()
defer cancel()
_, err := conn.ExecContext(ctx, query, args...)
return err
}
if *check == "crud" {
if readOnly != 0 {
fail("CRUD requires writer")
}
if exec("CREATE TABLE IF NOT EXISTS public_endpoint_acceptance (id INT PRIMARY KEY, payload VARBINARY(64) NOT NULL)") != nil {
fail("fixture table creation failed")
}
if exec("INSERT INTO public_endpoint_acceptance VALUES (1, ?) ON DUPLICATE KEY UPDATE payload=VALUES(payload)", []byte("initial")) != nil || exec("UPDATE public_endpoint_acceptance SET payload=? WHERE id=1", payload) != nil {
fail("binary write failed")
}
if exec("INSERT INTO public_endpoint_acceptance VALUES (2, ?)", payload) != nil || exec("DELETE FROM public_endpoint_acceptance WHERE id=2") != nil {
fail("fixture insert/delete failed")
}
}
var actual []byte
ctx, cancel = step()
err = conn.QueryRowContext(ctx, "SELECT payload FROM public_endpoint_acceptance WHERE id=1").Scan(&actual)
cancel()
if err != nil || !bytes.Equal(actual, payload) {
fail("binary data differs")
}
if *check == "data-preserved" && readOnly == 0 {
if exec("INSERT INTO public_endpoint_acceptance VALUES (4, ?)", payload) != nil || exec("DELETE FROM public_endpoint_acceptance WHERE id=4") != nil {
fail("recovered writer did not accept new writes")
}
}
if *check == "reader-write-rejected" {
if readOnly != 1 {
fail("write refusal requires reader")
}
err = exec("INSERT INTO public_endpoint_acceptance VALUES (3, ?)", payload)
var mysqlErr *mysql.MySQLError
if !errors.As(err, &mysqlErr) || mysqlErr.Number != 1290 {
fail("reader did not explicitly reject write")
}
}
fmt.Println("PASS " + *check)
return
}
if *check != "verified" {
negative := config.Clone()
switch *check {
case "plaintext-rejected":
negative.TLS = nil
negative.TLSConfig = "false"
case "wrong-hostname-rejected":
negative.TLS = config.TLS.Clone()
negative.TLS.ServerName = "wrong-hostname.invalid"
case "wrong-ca-rejected":
negative.TLS = config.TLS.Clone()
negative.TLS.RootCAs = x509.NewCertPool()
case "bad-password-rejected":
negative.Passwd = "invalid-fixture-password"
}
badDB, badConn, badErr := open(negative)
if badErr == nil {
badConn.Close()
badDB.Close()
fail("negative connection unexpectedly succeeded")
}
var mysqlErr *mysql.MySQLError
var hostErr x509.HostnameError
var caErr x509.UnknownAuthorityError
switch *check {
case "bad-password-rejected":
if !errors.As(badErr, &mysqlErr) || mysqlErr.Number != 1045 {
fail("bad password did not produce authentication refusal")
}
case "wrong-hostname-rejected":
if !errors.As(badErr, &hostErr) {
fail("hostname failure was not certificate validation")
}
case "wrong-ca-rejected":
if !errors.As(badErr, &caErr) {
fail("CA failure was not certificate validation")
}
case "plaintext-rejected":
if !errors.As(badErr, &mysqlErr) || (mysqlErr.Number != 3159 && mysqlErr.Number != 2026 && mysqlErr.Number != 1045) {
fail("plaintext failure lacked explicit MySQL refusal")
}
}
// Prove the valid path still works after the negative attempt.
goodDB, goodConn, goodErr := open(config)
if goodErr != nil {
fail("verified reconnect failed after negative check")
}
goodConn.Close()
goodDB.Close()
fmt.Println("PASS " + *check)
return
}
fmt.Printf("READY tls=%s read_only=%t\n", version, readOnly == 1)
if *revocation == 0 {
return
}
deadline := time.Now().Add(*revocation)
for time.Now().Before(deadline) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
err = conn.PingContext(ctx)
cancel()
if err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
fail("session closure probe timed out")
}
var timeout net.Error
if errors.As(err, &timeout) && timeout.Timeout() {
fail("session closure probe timed out")
}
fmt.Println("REVOKED existing MySQL session closed")
return
}
time.Sleep(250 * time.Millisecond)
}
fail("existing MySQL session remained open after deadline")
}