Documentation
¶
Index ¶
- Variables
- type Login7
- type Server
- func (s *Server) Addr() net.Addr
- func (s *Server) SetApprovalDeps(deps shared.ApprovalDeps)
- func (s *Server) SetDumpUploader(uploader *dump.Uploader)
- func (s *Server) SetRowWriter(writer *shared.RowWriter)
- func (s *Server) Shutdown(ctx context.Context) error
- func (s *Server) Start(addr string) error
- type UpstreamConn
Constants ¶
This section is empty.
Variables ¶
var ( // ErrAuthFailed — the username/password (or API key) did not check out. // One error for every cause on purpose: see clientMessageFor. ErrAuthFailed = errors.New("mssql: authentication failed") // ErrNoDatabaseRequested — the LOGIN7 carried no database name, so there is // nothing to resolve against the dbbat catalog. ErrNoDatabaseRequested = errors.New("mssql: the login named no database") // ErrServerNotFound — no SQL Server target is registered in dbbat under the // requested name. ErrServerNotFound = errors.New("mssql: no SQL Server database registered under that name") // ErrNoActiveGrant — the user has no live grant on that database. ErrNoActiveGrant = errors.New("mssql: no active grant for this database") // ErrAPIKeyOwnerMismatch — the API key is valid but belongs to another // user than the one named in the login. ErrAPIKeyOwnerMismatch = errors.New("mssql: API key does not belong to this user") // ErrUpstreamConnect — dbbat could not open the upstream session. ErrUpstreamConnect = errors.New("mssql: upstream connection failed") // ErrQueryLimitExceeded — the grant's max_query_count quota is spent. ErrQueryLimitExceeded = errors.New("dbbat: query count limit exceeded for this grant") // ErrDataLimitExceeded — the grant's max_bytes_transferred quota is spent. ErrDataLimitExceeded = errors.New("dbbat: data transfer limit exceeded for this grant") )
Client-leg refusals. Every one of them is reported to the client as a proper TDS login failure, so a driver surfaces the reason instead of a dropped socket.
var ( // ErrBulkCopyBlocked — the grant blocks bulk copy, so a BULK INSERT (and // the BulkLoadBCP message that carries its rows) is refused. ErrBulkCopyBlocked = errors.New("dbbat: bulk copy is not permitted: your access grant blocks it") // ErrOpaqueProcedureBlocked — an RPC names a stored procedure whose body // dbbat cannot see, under a grant that restricts what a statement may do. // dbbat cannot prove the procedure respects the restriction, so it fails // closed. See docs/mssql.md. ErrOpaqueProcedureBlocked = errors.New( "dbbat: this grant restricts what statements may do, and dbbat cannot see " + "what a stored procedure does: call it with an explicit statement instead") // ErrUnknownPreparedStatement — an sp_execute named a prepared-statement // handle dbbat never saw prepared on this session, so the statement it // would run is unknown. Fails closed under a restrictive grant. ErrUnknownPreparedStatement = errors.New( "dbbat: this prepared statement was not prepared through this session, " + "so dbbat cannot check it against your grant") // ErrDatabaseSwitchBlocked — a batch carried a `USE <db>` naming a database // other than the one this session's grant was issued on. TDS pins nothing: // the LOGIN7 database field sets the *initial* context only, so without this // a full-write grant on one database reached every other database the // upstream credentials can see — and, because `queries` has no database // column, every statement afterwards was recorded against the granted one. // Refused whatever the grant says, for the same reason Oracle refuses // `ALTER SESSION SET CONTAINER`. See docs/mssql.md. ErrDatabaseSwitchBlocked = errors.New( "dbbat: switching database is not permitted through dbbat: your grant covers " + "this database only, so connect again naming the other dbbat entry") // ErrMalformedRequest — a SQLBatch or RPC message could not be parsed. // Refused rather than relayed: an unparseable request is one dbbat cannot // enforce a grant on. ErrMalformedRequest = errors.New("dbbat: this request could not be parsed by the proxy") )
Statement-level refusals. Unlike the login-leg errors above these are answered mid-session, as an ERROR + DONE token stream on the client leg.
var ( // ErrLogin7TooShort — the payload is smaller than the fixed header. ErrLogin7TooShort = errors.New("mssql: LOGIN7 payload shorter than its fixed header") // ErrLogin7BadLength — the self-declared Length field disagrees with the // payload actually received. ErrLogin7BadLength = errors.New("mssql: LOGIN7 length field does not match the payload") // ErrLogin7BadOffset — an offset/length pair points outside the payload. ErrLogin7BadOffset = errors.New("mssql: LOGIN7 field offset outside the payload") // ErrLogin7Unsupported — the login asks for something dbbat does not do in // v1 (integrated auth, federated auth, a password change). ErrLogin7Unsupported = errors.New("mssql: unsupported LOGIN7 request") // ErrLogin7BadFeatureExt — the FEATUREEXT block does not decode. It is a // refusal rather than something to shrug off: that block is where a client // asks for federated authentication, so a block dbbat cannot read is a block // whose auth intent it cannot rule out. ErrLogin7BadFeatureExt = errors.New("mssql: LOGIN7 feature extension block does not decode") )
LOGIN7 parse errors.
var ( // ErrShortHeader — the peer closed (or the stream desynchronised) partway // through an 8-byte packet header. ErrShortHeader = errors.New("mssql: truncated TDS packet header") // ErrShortPayload — the header promised more bytes than arrived. ErrShortPayload = errors.New("mssql: truncated TDS packet payload") // ErrBadPacketLength — the length field is smaller than the header it // includes, so the packet cannot be parsed at all. ErrBadPacketLength = errors.New("mssql: invalid TDS packet length") // ErrMessageTooLarge — a message spanned more than maxMessageSize bytes // without ever setting the EOM status bit. ErrMessageTooLarge = errors.New("mssql: TDS message exceeds the maximum reassembled size") // ErrMixedMessageTypes — packets belonging to one message disagreed on // their type byte. ErrMixedMessageTypes = errors.New("mssql: TDS message packets have inconsistent types") // ErrMessageIgnored — the terminal packet carried the IGNORE status bit, // meaning the sender wants the whole message discarded. ErrMessageIgnored = errors.New("mssql: TDS message marked IGNORE by the sender") // ErrPacketPending — a streaming packet was left open when the caller // asked for something that needs a clean message boundary. ErrPacketPending = errors.New("mssql: a TDS packet is still open") )
Framing errors. All static, so callers can match on them.
var ( // ErrPreloginTruncated — the option table or a blob it points at runs past // the end of the payload. ErrPreloginTruncated = errors.New("mssql: truncated PRELOGIN payload") // ErrPreloginNoTerminator — the option table never reached the 0xFF // terminator token. ErrPreloginNoTerminator = errors.New("mssql: PRELOGIN option table has no terminator") // ErrEncryptionNotSupported — the client cannot do TLS but the listener // requires it (or the reverse). ErrEncryptionNotSupported = errors.New("mssql: TLS encryption cannot be negotiated with this client") )
PRELOGIN parse errors.
var ( // ErrRPCTruncated — an RPC request ran off the end of its message. ErrRPCTruncated = errors.New("mssql: truncated RPC request") // ErrRPCNoRequest — the RPC message carried no request at all. ErrRPCNoRequest = errors.New("mssql: RPC message carries no request") )
RPC parsing errors.
var ( // ErrUnexpectedFirstMessage — the connection did not start with PRELOGIN. ErrUnexpectedFirstMessage = errors.New("mssql: connection did not start with a PRELOGIN message") // ErrExpectedLogin7 — the message after the handshake was not LOGIN7. ErrExpectedLogin7 = errors.New("mssql: expected a LOGIN7 message") // ErrMARSUnsupported — the client insisted on Multiple Active Result Sets. ErrMARSUnsupported = errors.New("mssql: MultipleActiveResultSets is not supported") )
Session-level errors.
var ( // ErrUpstreamUnexpectedMessage — the upstream answered with a TDS message // type that does not belong at that point in the handshake. ErrUpstreamUnexpectedMessage = errors.New("mssql: unexpected TDS message from the upstream") // ErrUpstreamNoLoginAck — the upstream's login response carried neither a // LOGINACK nor an ERROR token, so there is no way to tell whether the // session is usable. ErrUpstreamNoLoginAck = errors.New("mssql: upstream login response has no LOGINACK") )
Upstream-leg errors.
var ErrHandshakeBytesUnconsumed = errors.New(
"mssql: TLS handshake ended with unread bytes in a PRELOGIN message")
ErrHandshakeBytesUnconsumed — the handshake finished with bytes still sitting in an inbound PRELOGIN message.
TDS is strictly alternating, so a conforming client's last framed message holds exactly its final handshake flight and nothing else. Bytes left over mean the peer packed post-handshake TLS records into the same PRELOGIN message — records that belong to the TLS session and that pass-through mode will never see. Dropping them silently would corrupt the stream a few reads later, somewhere unrelated; failing here says what actually happened.
var ErrHandshakeWrongPacketType = errors.New("mssql: non-PRELOGIN packet during the TLS handshake")
ErrHandshakeWrongPacketType — during the encapsulated handshake the peer sent something other than a PRELOGIN-typed packet, which means the stream is out of step and nothing good can come of continuing.
var ErrTLSConfigInvalid = errors.New("mssql tls: cert_file and key_file must both be set or both empty")
ErrTLSConfigInvalid is returned when only one of cert/key files is set.
var ErrTokenTruncated = errors.New("mssql: truncated TDS token stream")
ErrTokenTruncated — a token's declared length runs past the end of the stream. It means the message was mis-framed, not that the server said something the proxy does not model.
var ErrUnknownDataType = errors.New("mssql: unmodelled TDS data type")
ErrUnknownDataType — a TYPE_INFO named a type this decoder does not model. It stops the walk rather than guessing a length.
Functions ¶
This section is empty.
Types ¶
type Login7 ¶
type Login7 struct {
// Scalar block, in wire order. Little-endian, unlike the TDS packet header
// that precedes it.
TDSVersion uint32
PacketSize uint32
ClientProgVer uint32
ClientPID uint32
ConnectionID uint32
OptionFlags1 byte
OptionFlags2 byte
TypeFlags byte
OptionFlags3 byte
ClientTimeZone int32
ClientLCID uint32
// The variable part. The lengths in the packet are *character* counts for
// the UCS-2 fields and *byte* counts for the extension and SSPI blobs,
// which is the single most common way to get this structure wrong.
HostName string
UserName string
Password string
AppName string
ServerName string
CltIntName string
Language string
Database string
AtchDBFile string
ChangePassword string
ClientID [6]byte
SSPI []byte
// FeatureExt is the FEATUREEXT block that sits past every blob when
// optionFlags3Extension is set (UTF-8 support, column encryption, session
// recovery…). dbbat relays it verbatim — it has to survive a re-serialize or
// the replayed login would lose features the client asked for — and only
// reads it far enough to refuse the one feature that is an authentication
// mechanism, FEDAUTH. See validateFeatureExt.
FeatureExt []byte
}
Login7 is a parsed LOGIN7 message.
Password holds the *descrambled* cleartext. It must never reach a log, at any level: String() omits it, and nothing in this package formats the struct with %v or %+v.
func (*Login7) ChangePasswordRequested ¶
ChangePasswordRequested reports whether the login carries a password change, which the proxy does not relay in v1.
func (*Login7) FederatedAuthRequested ¶
FederatedAuthRequested reports whether the login's FEATUREEXT block asks for federated (Azure AD / Entra ID) authentication.
A block that does not decode answers false here — the undecodable case is not silently allowed, it is refused separately by validateFeatureExt, which walks the block before this question is ever reached. Callers outside Validate should therefore treat a decode failure as its own refusal rather than trust this answer on a login that has not been validated.
func (*Login7) IntegratedSecurity ¶
IntegratedSecurity reports whether the client asked for Windows integrated authentication. dbbat v1 does SQL authentication only.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is the SQL Server (TDS) proxy.
It accepts a client, negotiates PRELOGIN (including the TLS handshake encapsulated inside PRELOGIN packets), parses LOGIN7, authenticates the login against dbbat's own users and API keys, opens the upstream with the stored credentials, and relays the session.
Every statement it relays goes through the same intercept / grant / approval pipeline as the other four protocols, and every response is accounted for.
func NewServer ¶
func NewServer( dataStore *store.Store, encryptionKey []byte, queryStorage config.QueryStorageConfig, dumpConfig config.DumpConfig, authCache *cache.AuthCache, mssqlConfig config.MSSQLConfig, logger *slog.Logger, ) (*Server, error)
NewServer creates a new SQL Server proxy.
func (*Server) Addr ¶
Addr returns the listener's bound address, or nil if the server has not started accepting connections yet.
func (*Server) SetApprovalDeps ¶
func (s *Server) SetApprovalDeps(deps shared.ApprovalDeps)
SetApprovalDeps installs the approval-hold collaborators. A server without them never holds anything.
func (*Server) SetDumpUploader ¶
SetDumpUploader installs the process-wide capture uploader, so this proxy's finished captures are shipped to blob storage alongside every other one. nil (the default) keeps them on local disk.
func (*Server) SetRowWriter ¶
SetRowWriter installs the process-wide result-row writer, replacing (and shutting down) the private one NewServer created, so row batching spans every protocol rather than one writer per proxy.
type UpstreamConn ¶
type UpstreamConn struct {
// TLS reports whether the proxy→upstream leg is encrypted. It is the value
// recorded on the connection row's upstream_tls field.
TLS bool
// LoginResponse is the raw token stream the upstream answered LOGIN7 with
// (LOGINACK, ENVCHANGEs, INFO messages, DONE). The proxy forwards it to the
// client verbatim, so the client sees the real server's identity, collation
// and database context rather than something dbbat made up.
LoginResponse []byte
// contains filtered or unexported fields
}
UpstreamConn is an authenticated TDS connection to a target SQL Server, ready to relay packets over.
It is what both entry points get back: the proxy, which then MITMs it, and the connectivity check, which closes it immediately.
func ConnectUpstream ¶
func ConnectUpstream( ctx context.Context, dial upstream.DialFunc, cfg upstream.MSSQLConfig, template *Login7, ) (*UpstreamConn, error)
ConnectUpstream opens an authenticated TDS 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.
template is the client's own parsed LOGIN7, whose non-credential fields (TDS version, packet size, option flags, host name, FEATUREEXT block) are replayed so the upstream negotiates with the client's real capabilities and the response dbbat forwards back actually answers what the client asked. A nil template — the connectivity check, which has no client — gets a plain TDS 7.4 login.
Opportunistic ssl_modes need two attempts: TDS settles encryption in PRELOGIN before a single byte of login is sent, and a server that refuses the offered transport cannot be talked round on the same socket. The chain redials, and only when the failure says the *transport* was the problem — a rejected login ends it, exactly as the MySQL and PostgreSQL chains do.
func (*UpstreamConn) Close ¶
func (u *UpstreamConn) Close() error
Close tears the connection down. Safe on a nil receiver and safe to call twice, which the session's teardown relies on.