feat(ssh): forward a tunnel to a Unix socket on the remote server (#1871)#1876
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ddc1004df8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // | ||
|
|
||
| extension DatabaseConnection { | ||
| static let sshForwardUnixSocketPathKey = "sshForwardUnixSocketPath" |
There was a problem hiding this comment.
Preserve socket paths in copied connection URLs
When a connection uses this new key, the copied +ssh connection URL does not round-trip it: I checked ConnectionURLFormatter.formatSSH, which still serializes only connection.host/connection.port for the database endpoint, and ConnectionURLParser/applyParsed has no socket-path field to restore. If a user copies or imports a socket-forwarded connection URL, the imported connection silently falls back to the stale Host/Port and no longer forwards to the Unix socket, so the socket path needs to be included in the URL query and applied on import.
Useful? React with 👍 / 👎.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
Closes #1871.
What
An SSH tunnel can now forward to a Unix domain socket on the remote server, for databases that only listen on a socket. This is what the reporter was doing by hand:
Fill in Socket Path on the General pane (shown only when SSH is on) and TablePro forwards to that socket instead of Host and Port, which dim while it is set. Leave it empty and nothing changes.
Scope note: jump hosts already worked
The issue asks for two things, and one already ships.
LibSSH2TunnelFactory.buildAuthenticatedChainalready chains N hops, verifies each hop's host key before authenticating, and followsProxyJumpfrom~/.ssh/config. The reporter'shost1 -> host2chain composes with a socket destination for free: a session's transport being a relayed socketpair is orthogonal to which channel type it opens. Nothing was needed there.How
The forward destination was a
(remoteHost: String, remotePort: Int)pair threaded through four layers into one hard-codedlibssh2_channel_direct_tcpip_excall. That is the root cause: the destination was two scalars where it needs two legal shapes.It is now a typed
SSHForwardDestination(.tcp/.unixSocket), resolved once at theDatabaseManager+SSHboundary, so the libssh2 call site switches exhaustively instead of four files sniffing strings. Both libssh2 entry points sit behind oneLibSSH2ForwardChannel.open.libssh2_channel_direct_streamlocal_exreturns the sameLIBSSH2_CHANNEL*with the same EAGAIN protocol asdirect_tcpip_ex, so the retry loop and the wholeSSHChannelRelaybyte pump are reused unchanged. libssh2 is already pinned at 1.11.1 and the symbol is already exported by the shipped static libs, so no library rebuild was needed.Jump-hop routing (
LibSSH2TunnelFactory.openChannel) stays TCP and is untouched.The Postgres TLS trap
Over a TCP local forward, libpq's
raddrisAF_INET, so it sends anSSLRequest. The server decides from its own listening socket, which isAF_UNIX, answers "no" unconditionally, and never consultspg_hba. Sosslmode=requireand the verify modes would hard-fail against a socket destination.tunneledConnectionnow drops TLS for socket forwards. That costs nothing: the whole path already runs inside the SSH transport. A test pins this, and a matching test pins that TCP forwards still keep encryption on.peerauth works, which is the reporter's actual goal: the connection to the socket is made by sshd's post-auth child running as the SSH login user.Failing fast
sshdgates socket forwarding behindAllowStreamLocalForwarding, separately fromAllowTcpForwarding. A refusal is otherwise invisible until the driver dials the local port, where it surfaces as an unexplained dropped connection. The tunnel now probes a socket destination once at connect time and reportsSSHTunnelError.socketForwardingRefused, naming the path and the sshd setting.The probe is deliberately not applied to TCP destinations: probing them would open and immediately drop a real database connection on every connect, which logs noise and bumps connection counters for every existing SSH user, for a feature they did not ask for.
Driver impact
None. The driver always gets
127.0.0.1:<localPort>, so this works for PostgreSQL, MySQL, and Redis with no plugin changes.Tests
swiftlint lint --strictis clean. 26 tests pass, including the pre-existing tunnel tests as regression guards.Not covered: the
libssh2_channel_direct_streamlocal_excall against a live sshd. The SSH stack has never had live-server integration coverage (the same gap exists fordirect_tcpip_ex), so this needs a manual pass: key auth to a socket, a jump-host chain to a socket,AllowStreamLocalForwarding no, andpeerauth.iOS needs no change:
TableProMobile/SSH/is a separate, simpler implementation with no jump hosts and no shared code.