Repository navigation
Conversation
…ycle fixes (#1) * Support Entra ID access token login (TDS FEDAUTH SecurityToken) Add an opt-in :access_token connection option (string, 0-arity fun or MFA, resolved on every connect so pooled connections pick up refreshed tokens). When set, LOGIN7 sets fExtension and points ibExtension at a FeatureExt block carrying the FEDAUTH feature (bFedAuthLibrary SecurityToken, fFedAuthEcho mirroring the server's PRELOGIN FEDAUTHREQUIRED response, UTF-16LE token), with empty username and password. The token is refused unless ssl is enabled. FEATUREEXTACK is now decoded and ignored during login, and FEDAUTHINFO fails the login with a clear error since tokens are passed up front. Without :access_token the PRELOGIN and LOGIN7 packets are unchanged. * Document the :access_token option * Test LOGIN7 split across three packets for a 6000 character token * Add max_response_bytes option to cap buffered responses When set, msg_recv counts the bytes read from the socket for the current response (packet headers included). Once the count goes over the limit it stops reading, closes the socket and returns {:disconnect, %Tds.Error{message: "response exceeded max_response_bytes (N)"}, state}, so DBConnection drops the connection and the caller gets the error. The login path and transaction manager requests handle it too. Without the option nothing changes. * Return Tds.ResponseTooLargeError when max_response_bytes is exceeded The error carries the configured limit and the bytes read so far, so callers can match on the struct instead of a message string. Tds.query returns {:error, %Tds.ResponseTooLargeError{limit: n, received: bytes}} and the connection is closed. Document the option in the README. * Return a proper disconnect when a send or receive fails msg_send wrapped receive failures as {0, {:disconnect, ...}} and passed send failures through as {:error, reason}, so DBConnection raised "bad return value" and logged the whole protocol state. Send and receive failures now return {:disconnect, %Tds.Error{}, state}, and the callers pass that through: decode (the SET batch after login), ping, transactions, and connect, which closes the socket and returns {:error, exception}. A failed routing redirect returns a 3-tuple error. Tests use a loopback fake server that closes the socket mid-response or stops answering until the DBConnection timeout. * Keep the access token out of inspectable connection state The resolved token now lives in a dedicated state field that is cleared after login, and opts carry :REDACTED from the moment the token is resolved. Tds.Protocol implements Inspect to redact password, access_token and proxy_password, so crash reports and DBConnection errors never print credentials. The fake test server can now negotiate TLS the way SQL Server does (handshake wrapped in PRELOGIN packets), so the tests run the real access token login over TLS and check that LOGIN7 carries the token while logs from failed logins, closed sockets and timeouts do not. * Stop the TLS transport normally and with its socket Tds.Tls is linked to the connection process and stopped with :tcp_closed when the server hung up, so a server-side close killed the pooled connection process instead of coming back as a disconnect. It also outlived its socket: disconnect/2 and the max_response_bytes close left the GenServer running, one per disconnect. The transport now monitors its socket and stops :normal when the socket closes or the server hangs up. A routing redirect closes the gateway leg before connecting to the routed host, so the gateway hanging up later cannot take the routed connection down. * Fail the connect when the access token function raises or hangs A raising :access_token function or MFA crashed the connection process from connect/1, so DBConnection never applied its backoff. Resolution now runs in a monitored process bounded by :connect_timeout (15 seconds by default). A raise, throw, exit or timeout returns {:error, %Tds.Error{message: "access token could not be fetched: ..."}} naming only the exception module, and the pool backs off and retries. * Report a dead socket as DBConnection.ConnectionError Send and receive failures surfaced as DBConnection.ConnectionError before (through DBConnection's bad return handling) and had become %Tds.Error{}, breaking `rescue DBConnection.ConnectionError`. They now return {:disconnect, %DBConnection.ConnectionError{}, state} with "tds connection closed (the pool may have closed it after a timeout): <reason>", the way postgrex and myxql do, for queries, ping and login. * Validate max_response_bytes and cap query responses only The cap applied to every response, so a limit smaller than the login exchange made every connect fail, and any value was accepted. start_link and child_spec now raise ArgumentError unless the option is a positive integer or nil, and the limit is set on the connection state once login completes, leaving PRELOGIN, LOGIN7 and the session SET batch unbounded. The README notes that over TLS the count is decrypted bytes. The tests now use the shared fake server and assert received > limit instead of an exact byte count. * Cover the redirect token, ssl check, FEDAUTH echo and live state Mutations of these paths survived the suite. New tests check that a routing redirect sends the token to the routed server, that a token is refused when :ssl is nil or omitted, that LOGIN7 echoes the server's FEDAUTHREQUIRED value (the fake server can now send 0 or 1), that a close after the connection SET batch fails the connect with a ConnectionError, and that the driver state of a live pooled connection never shows the token or password. * Fail a token login the server did not acknowledge as federated The login parser skipped FEATUREEXTACK, so a server that ignored the FEDAUTH feature still produced a "successful" token login. LOGINACK now carries the acknowledged FeatureIds, and a token login without the FEDAUTH acknowledgement fails with "server did not acknowledge federated authentication", as tedious does. Password logins are unaffected. The fake server returns {:error, reason} instead of crashing when the client hangs up mid-exchange. * Send the routed host as SNI after a gateway redirect The redirect reused ssl_opts as they were, so a pinned server_name_indication (as in the README's federated example) kept naming the gateway on the routed connection, and the hostname check ran against the wrong name. A pinned SNI is now replaced by the routed host. The fake server reports the SNI it receives so the test can check both legs. * Warn when an access token is sent without certificate verification A bearer token over TLS with verify: :verify_none can be intercepted by anyone who can present a certificate. connect/1 now logs a warning when the token is resolved with that setting. The routing redirect reuses the token resolved for the connect instead of going through connect/1 again, so the warning fires once per connect and the token is not put back into opts. * Redact ssl_opts keys and passwords when inspecting the state Inspect for Tds.Protocol hid the top level credentials but printed ssl_opts as given, including a client private key, its password and the same inside certs_keys. Those are now shown as :REDACTED too. * Close a failed TLS login through the TLS transport When login failed after PRELOGIN had switched to TLS (wrong password, an expired or unacknowledged token, a failed routed leg), connect closed the raw TCP socket and left the ssl connection process running until the connecting process died, one more per backoff retry. The error path now closes the socket in use, like disconnect/2, which also stops Tds.Tls. * Return the resolved token as a message, not an exit reason The resolver process exited with {:access_token, {:ok, token}}, an abnormal exit, so any process the token function linked to (a lazily started cache or HTTP client) received the token in an exit signal, and logged it when it trapped exits. The resolver now sends the result to the connecting process and exits :normal. On timeout it is killed and a result that raced the kill is dropped from the mailbox. * Stop the token resolver when the connecting process dies A token function still running when the connecting process died (pool shutdown, a killed connection) kept its resolver process alive forever. A small watcher now monitors both and kills the resolver if the connecting process goes first. * Keep the {:error, reason} values out of the token fetch error An {:error, reason} from the token function was inspected verbatim into the connect error, so a reason such as %{client_secret: "..."} ended up in the logs. The error now names only an atom reason or the reason's struct module, and says nothing more for any other term. * Test that socket errors stop Tds.Tls normally and a slow resolver is killed Two earlier fixes had no test: Tds.Tls stopping :normal on tcp_error/ssl_error (and on tcp_closed/ssl_closed) after passing the event on, and the token resolver being killed once connect_timeout passes. * Note that a stalled TLS handshake or login can block a connect The TLS handshake runs with an :infinity timeout and the LOGIN7 response is read without one. This predates the access token work, so it is only documented in the federated authentication section for now.
A Fabric warehouse refused every login from Tds. Four changes, each checked against a live warehouse: - PRELOGIN decoded the INSTOPT option as the encryption level, so a server that sends INSTOPT looked like it had encryption off. - A server answering ENCRYPT_REQ was refused although the client asked for encryption. Both ENCRYPT_ON and ENCRYPT_REQ mean the whole connection is encrypted. - PRELOGIN now sends TRACEID (connection id, activity id, sequence). A routed Fabric login without it fails with error 18456. - The gateway routes to `host\instance`. The routed LOGIN7 named only the host, and the backend hung up without an error. It now names `host\instance`, as SqlClient and go-mssqldb do.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds SOCKS5 proxy connections and Microsoft Entra ID access-token authentication for Azure SQL and Microsoft Fabric SQL endpoints. Also adds an optional response-size limit and fixes connection cleanup and disconnect errors.
Changes
:proxy_host,:proxy_port(default1080), and optional:proxy_username/:proxy_passwordcredentials.:access_tokenas a string, zero-arity function, or MFA tuple. Token providers run on each connection attempt, with bounded execution and sanitized failures. Implement the FEDAUTH Security Token workflow, require TLS, redact credentials, and preserve authentication across gateway redirects.:max_response_bytesto cap buffered query responses. Exceeding the limit closes the connection and returnsTds.ResponseTooLargeError.DBConnection.ConnectionErrorfor closed sockets.Validation
mix compile --warnings-as-errors,mix format --check-formatted, andmix deps.unlock --check-unusedpass with Elixir 1.18.3 / OTP 27.sqlcmdare unavailable. Live Azure SQL / Fabric authentication was not validated in this run.Credit to @raul-gracia for the federated authentication contribution.