Documentation
¶
Overview ¶
Package upstream owns the single path dbbat takes from "a server row" to "an authenticated connection to that server". Both entry points use it: the proxies, which then MITM the connection they get back, and the connectivity check, which closes it immediately.
Why a package of its own rather than internal/proxy/shared: shared is the grab-bag every protocol package *and* conncheck already import, and it stays deliberately dependency-light (net, ssh, store). The connectors here pull in pgproto3, go-mysql and go-ora, and nothing that only wants a byte counter or a SQL validator should inherit those. A dedicated package also names the invariant — one implementation of the ssl_mode policy and one implementation of each protocol login — which is the whole point of the split.
Import direction: upstream imports shared (for the transport) and store; the protocol packages and conncheck import upstream. Nothing here imports a protocol package, so no cycle is possible.
Two things this package does NOT deliver, both worth knowing before trusting the headline claim ("a green connectivity check proves the proxy can get in"):
MongoDB's upstream login is implemented in internal/proxy/mongodb, not here, because it is written on top of that package's OP_MSG codec, which the whole MongoDB proxy is built from and which would have to be hoisted wholesale to move the connector. It uses the same Plan from this package, so the ssl_mode policy is still defined exactly once and the probe and the proxy still run the same code; only the code's postal address differs. conncheck calls it directly.
Oracle is the one protocol where probe and proxy are genuinely NOT the same code, and where they do not even agree on encryption. The proxy relays the client's own TNS Connect descriptor byte for byte, so it has no standalone login to share and no TLS at all: an Oracle session's upstream leg is plaintext whatever the row's ssl_mode says (recorded honestly as upstream_tls=false on the connection row). ConnectOracle, which only the probe runs, gates TLS on Plan.RequiresTLS, so it encrypts under require and verify-* and stays plaintext under the opportunistic modes. A green check on an Oracle row at ssl_mode=require therefore proves more than the proxy will actually do — the one real exception to the claim above. Giving the Oracle proxy upstream TLS is out of scope here and would be a change to its TNS relay, not to this package.
Index ¶
Constants ¶
const ( // SSLModeDisable never encrypts and never offers to. SSLModeDisable = "disable" // SSLModeAllow prefers plaintext and accepts TLS when the server insists. SSLModeAllow = "allow" // SSLModePrefer offers TLS and falls back to plaintext when refused. The // empty string means this too — it is the default of every unset row. SSLModePrefer = "prefer" // SSLModeRequire encrypts without authenticating the server. SSLModeRequire = "require" // SSLModeVerifyCA encrypts and verifies the certificate chain. SSLModeVerifyCA = "verify-ca" // SSLModeVerifyFull encrypts and verifies chain and hostname. SSLModeVerifyFull = "verify-full" )
SSL modes recognized on a server row. The names and the semantics are libpq's, because that is what an operator filling in the field expects.
Variables ¶
var ( // ErrPostgresTLSRequired means the server refused to encrypt while the // ssl_mode demanded it. ErrPostgresTLSRequired = errors.New("upstream rejected TLS but ssl_mode requires it") // ErrPostgresSSLResponse means the server answered the SSLRequest with // something other than 'S' or 'N' — it is probably not a Postgres server. ErrPostgresSSLResponse = errors.New("unexpected upstream SSL response byte") // ErrPostgresAuthFailed means the server sent an ErrorResponse during // login: bad password, unknown role, no matching pg_hba line, missing // database. ErrPostgresAuthFailed = errors.New("upstream authentication failed") )
PostgreSQL upstream errors. They are the classification surface both callers key off: the proxy to decide what to tell its client, the connectivity check to decide whether it is looking at a TLS problem or a credentials problem.
var ( // ErrSCRAMNoSupportedMechanism means the server offered only mechanisms we // decline to speak — in practice SCRAM-SHA-256-PLUS only. ErrSCRAMNoSupportedMechanism = errors.New("upstream offered no SCRAM mechanism we support") // ErrSCRAMServerNonceMismatch means the server's nonce did not extend ours, // so the exchange is not the one we started. ErrSCRAMServerNonceMismatch = errors.New("SCRAM server nonce did not extend client nonce") // ErrSCRAMServerSignature means the server could not prove it holds the // password — either a mismatch, or an explicit server-side rejection. ErrSCRAMServerSignature = errors.New("SCRAM server signature mismatch") // ErrSCRAMUnexpectedMessage means a SASL message arrived out of order. ErrSCRAMUnexpectedMessage = errors.New("unexpected SASL message from upstream") // ErrSCRAMMalformedMessage means a SASL payload did not parse. ErrSCRAMMalformedMessage = errors.New("malformed SCRAM message from upstream") )
Upstream SCRAM/SASL errors raised when authenticating with a target Postgres server using SCRAM-SHA-256.
var ErrMySQLNoAttempt = errors.New("mysql: ssl_mode produced no connection attempt")
ErrMySQLNoAttempt is returned when the attempt chain ended without producing either a connection or an error. It cannot happen with a plan from PlanFor (which always has at least one attempt) and exists so the exhausted-loop path is not a silent nil.
var ErrOracleConnectorShape = errors.New("go-ora connector does not expose a dialer hook")
ErrOracleConnectorShape guards against a go-ora upgrade changing what NewConnector returns: without the concrete type we cannot inject the transport, and a caller that silently dialed on its own would bypass the SSH tunnel entirely.
Functions ¶
func ConnectOracle ¶
ConnectOracle runs a real TNS Connect + TTC login against the target with go-ora, over the injected transport.
Unlike the other three protocols, this is *not* what the Oracle proxy does: the proxy's TNS/TTC handshake only exists inside a live session with a downstream client attached, because it relays the client's own Connect descriptor byte for byte. go-ora is the standalone client half of that same protocol. The connector lives here anyway so the ssl_mode policy and the transport injection are the shared ones, and so there is a single place to look for "how does dbbat log in to an Oracle server".
Encryption is where that divergence bites, so state it plainly: the Oracle proxy has no upstream TLS at all — an Oracle session's upstream leg is plaintext whatever the row's ssl_mode says, which is why its connection rows hardcode upstream_tls=false. This function, which only the connectivity check runs, does encrypt under require/verify-*. A green check on an Oracle row at ssl_mode=require therefore proves more than a real session will do. See the package doc.
Types ¶
type Attempt ¶
type Attempt struct {
// TLS is the configuration for an encrypted attempt, or nil for plaintext.
TLS *tls.Config
}
Attempt is one way of reaching the target. A nil TLS config means "connect in plaintext"; a non-nil one means "encrypt with exactly this config".
type DialFunc ¶
DialFunc opens the raw transport to the target — directly, or through the SSH bastion chain. Connectors never dial themselves: the caller injects this so the proxy and the probe provably traverse the same tunnel.
type MySQLConfig ¶
type MySQLConfig struct {
// Host and Port name the target. They build the address go-mysql reports
// in its errors and the TLS server name; the transport itself comes from
// the injected DialFunc.
Host string
Port int
// Username, Password and Database are the stored upstream credentials.
Username string
Password string
Database string
// ProgramName is the "program_name" connection attribute, so a DBA reading
// performance_schema.session_connect_attrs can tell who is connected.
ProgramName string
// SSLMode is the row's ssl_mode; interpreted by PlanFor.
SSLMode string
}
MySQLConfig is everything a MySQL/MariaDB login needs from a server row.
type MySQLUpstream ¶
type MySQLUpstream struct {
// Conn is the live go-mysql client connection.
Conn *gomysqlclient.Conn
// TLS reports whether the connection is encrypted.
TLS bool
}
MySQLUpstream is an authenticated MySQL/MariaDB connection plus the one fact the row could not state: whether it ended up encrypted.
func ConnectMySQL ¶
func ConnectMySQL(ctx context.Context, dial DialFunc, cfg MySQLConfig) (*MySQLUpstream, error)
ConnectMySQL opens an authenticated MySQL/MariaDB connection over the injected transport. It is the one implementation both the proxy and the connectivity check use, so a green check exercises the proxy's exact login.
go-mysql handles auth-plugin negotiation (caching_sha2_password on MySQL 8.x) transparently — that is the plugin support dbbat deliberately does not implement on its own server-facing side.
Opportunistic ssl_modes need two attempts. go-mysql decides whether to encrypt from the handshake's CLIENT_SSL capability, and the option callback runs before that handshake is read, so a single connection cannot express "encrypt if you can". The chain therefore redials — and only when the failure says the *transport* was the problem. An authentication failure ends it, exactly as pgconn's fallback chain does: retrying a rejected password in plaintext would be a downgrade triggered by the wrong signal.
func (*MySQLUpstream) Close ¶
func (u *MySQLUpstream) Close() error
Close tears the connection down. Safe on a nil receiver.
type OracleConfig ¶
type OracleConfig struct {
// Host and Port build the DSN go-ora reports in its errors; the transport
// comes from the injected DialFunc.
Host string
Port int
// ServiceName is the SERVICE_NAME to present, already resolved by the
// caller (the dedicated column, or the database name as a fallback).
ServiceName string
// Username and Password are the stored upstream credentials.
Username string
Password string
// ProgramName is the PROGRAM option, so a DBA reading v$session can tell
// who is connected.
ProgramName string
// SSLMode is the row's ssl_mode; interpreted by PlanFor.
SSLMode string
}
OracleConfig is everything an Oracle login needs from a server row.
type Plan ¶
type Plan struct {
// Mode is the ssl_mode this plan was built from, kept for error messages.
Mode string
// Attempts are the permitted ways to connect, in preference order. Never
// empty.
Attempts []Attempt
}
Plan is the ordered list of attempts an ssl_mode authorizes, best first. It is the *only* place dbbat decides what an ssl_mode means — every protocol, on both the proxy side and the probe side, reads its policy from here.
Not every protocol can honor the *order*, and that is the one place where the single policy still produces more than one behavior. Read the list as the SET of attempts a mode permits; the order is advisory and only binding for protocols that can act on it:
- PostgreSQL negotiates in band (SSLRequest / 'S' / 'N') and so never walks the list: it reads OffersTLS and AllowsPlaintext and nothing else. Under `allow` it therefore behaves exactly like `prefer` — it offers TLS and upgrades if the server says yes. That is the pre-existing behavior of both the proxy and libpq-with-one-round-trip, it errs toward encryption, and re-ordering it would cost a second dial for no confidentiality gain.
- MySQL (CLIENT_SSL capability) and MongoDB (TLS from the first byte) cannot negotiate in band, so they redial between attempts and DO honor the order: `allow` really is plaintext-first for them.
- Oracle only reads RequiresTLS: the opportunistic modes never encrypt. See the note on ConnectOracle.
So `allow` is the only mode whose meaning still varies by protocol, and it varies within what the mode permits — never outside it. `disable`, `require` and the verify-* modes have a single attempt each, so order cannot apply and every protocol agrees exactly.
func PlanFor ¶
PlanFor maps an ssl_mode (and the host it applies to) onto its attempts, encoding libpq's semantics once:
- disable: plaintext only, no TLS offered.
- allow: plaintext and TLS both permitted, plaintext preferred — but see the Plan doc: only the redialing protocols honor that preference, and PostgreSQL treats allow as prefer.
- prefer / "": TLS first, plaintext when the server refuses.
- require: TLS only, certificate not verified — libpq's "encrypt, don't authenticate".
- verify-ca / verify-full: TLS only, chain *and* hostname verified.
verify-ca is deliberately treated as verify-full: Go's TLS stack cannot cleanly express "verify the chain but not the name" without a custom VerifyPeerCertificate, and erring stricter than libpq is the safe direction.
An unrecognized mode is treated as prefer, matching the historical behavior of both the proxy and the probe: a typo'd row keeps working, opportunistically encrypted, rather than failing closed on a field nobody validates.
func (Plan) AllowsPlaintext ¶
AllowsPlaintext reports whether an unencrypted connection is acceptable. Its negation is "TLS is mandatory": a server that refuses to encrypt must be treated as a failure rather than silently downgraded.
func (Plan) OffersTLS ¶
OffersTLS reports whether any attempt encrypts, i.e. whether dbbat should tell the server it can speak TLS at all.
func (Plan) PrefersTLS ¶
PrefersTLS reports whether the first (best) attempt is the encrypted one. Only protocols that redial between attempts care: it is the difference between "allow" and "prefer".
func (Plan) RequiresTLS ¶
RequiresTLS reports whether the mode forbids a plaintext fallback.
type PostgresAuthError ¶
type PostgresAuthError struct {
// Response is the upstream's ErrorResponse, copied out of pgproto3's
// reusable buffer.
Response *pgproto3.ErrorResponse
}
PostgresAuthError is an ErrorResponse the upstream sent during login. It keeps the raw message so the proxy can hand its own client the server's verbatim error instead of a paraphrase, and unwraps to ErrPostgresAuthFailed so callers that only classify can use errors.Is.
func (*PostgresAuthError) Error ¶
func (e *PostgresAuthError) Error() string
Error renders the upstream's message.
func (*PostgresAuthError) Unwrap ¶
func (e *PostgresAuthError) Unwrap() error
Unwrap makes errors.Is(err, ErrPostgresAuthFailed) true.
type PostgresConfig ¶
type PostgresConfig struct {
// Host is the target hostname. Used only for TLS server-name
// verification — the transport comes from the injected DialFunc.
Host string
// Username, Password and Database are the stored upstream credentials.
Username string
Password string
Database string
// ApplicationName is advertised in the StartupMessage, so a DBA reading
// pg_stat_activity can tell who is connected and why.
ApplicationName string
// SSLMode is the row's ssl_mode; interpreted by PlanFor.
SSLMode string
}
PostgresConfig is everything the login needs from a server row. The caller resolves it (decrypting the password, building the application name) so this package stays free of store and crypto concerns.
type PostgresUpstream ¶
type PostgresUpstream struct {
// Conn is the live connection — TLS-wrapped when the negotiation upgraded.
Conn net.Conn
// Frontend speaks pgproto3 over Conn with dbbat in the client role.
Frontend *pgproto3.Frontend
// ParameterStatuses are the server's startup parameters, in arrival order.
// Copies: pgproto3 reuses its message structs.
ParameterStatuses []*pgproto3.ParameterStatus
// BackendKeyData is the cancellation key the server issued, or nil when it
// issued none.
BackendKeyData *pgproto3.BackendKeyData
// ReadyForQuery is the message that ended the login.
ReadyForQuery *pgproto3.ReadyForQuery
// TLS reports whether the connection is encrypted. It answers "are we
// actually encrypted?" for a row whose ssl_mode only expressed a
// preference.
TLS bool
}
PostgresUpstream is an authenticated upstream connection, plus everything the login produced that a proxy has to replay to its own client.
The connector stops the instant the upstream is authenticated and ready: it does not forward anything, set session state, or register cancel keys. The proxy does that from these fields; the connectivity check ignores them and closes Conn.
func ConnectPostgres ¶
func ConnectPostgres(ctx context.Context, dial DialFunc, cfg PostgresConfig, logger *slog.Logger) (*PostgresUpstream, error)
ConnectPostgres dials the target through dial, negotiates TLS per cfg.SSLMode and completes the PostgreSQL login with cfg's credentials. It returns as soon as the upstream reports ReadyForQuery.
logger may be nil; it is only used to note protocol messages that arrive where none were expected.
func (*PostgresUpstream) Close ¶
func (u *PostgresUpstream) Close() error
Close tears the connection down. Safe on a nil receiver.