Repository navigation
feat(ssh): open a SOCKS5 proxy over SSH for drivers with the socks5_proxy capability - #1467
vinimcorrea wants to merge 1 commit into
Conversation
…roxy capability (TabularisDB#1449) Drivers that declare the new socks5_proxy capability get a dynamic SOCKS5 forward through the SSH session instead of a fixed -L forward, so they can reach every node their connection names (SRV seedlists, replica sets). The host leaves host, port and connection_uri unchanged and passes socks5_proxy_host/socks5_proxy_port, plus socks5_proxy_username and socks5_proxy_password when the listener requires them. Other drivers keep the fixed forward. - russh backend: in-process SOCKS5 server requiring per-tunnel random RFC 1929 credentials, 10s handshake timeout, channels opened concurrently so a slow destination does not stall the tunnel. - System ssh backend: ssh -D, which cannot authenticate; bound to 127.0.0.1 and documented as a limitation. - socks5_proxy_* values sent by callers are ignored. - A cached tunnel whose SSH session ended is reopened instead of reused, and concurrent opens keep a single shared tunnel per bastion. - SSH keepalives (15s, 3 misses) on both backends detect silent drops. - SOCKS5 CONNECT destinations are logged at debug level. - The capability, fields, plugin obligations and limits (local SRV/TXT lookups, ssh -D auth) are documented in PLUGIN_GUIDE.md, the manifest schema and the web-ui plugin types.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Reviewed the SOCKS5 dynamic-forward support end to end. I traced the capability flow (manifest to registry to Notes (no action required):
Files Reviewed (20 files)
Reviewed by claude-sonnet-5.5 · Input: 274 · Output: 5.1K · Cached: 1.1M Review guidance: REVIEW.md from base branch |
NewtTheWolf
left a comment
There was a problem hiding this comment.
Thanks for adding an explicit capability and preserving the driver's connection URI. I checked e52d9ec on Linux: typecheck, the production build, all 2,550 Rust library tests and the no-default-features server check pass.
I reproduced one registry race that needs fixing. register_driver publishes the synchronous SOCKS5 flag before awaiting the registry write lock, while unregister_driver clears it after removing the old entry. In a single-threaded Tokio probe I held that lock, first polled unregister_driver, then first polled registration of a replacement advertising socks5_proxy=true, released the lock and awaited removal followed by registration. The replacement remained registered with its manifest flag true, but is_socks5_proxy_driver returned false.
Tunnel resolution uses that false value to select the fixed forward for a driver requiring SOCKS5 routing. Please keep the registry entry and capability projections consistent across overlapping lifecycle operations and add this regression. The simple set/clear test does not cover the interleaving. I am leaving the PR open; native SSH/MongoDB E2E was not run after this initial blocker.
Since this is your first PR here, you are welcome to join us on Discord: https://discord.com/invite/K2hmhfHRSt
Closes #1449
Plugin side: TabularisDB/tabularis-mongodb-plugin#13
Problem
With SSH enabled,
resolve_ssh_paramsopens one-Lforward to the connection'shost:portand rewrites those two fields to127.0.0.1:<local_port>. A driver whose connection names several endpoints can't use that. Amongodb+srv://URI or a replica-set seedlist makes the driver discover the other nodes and connect to them directly, so it never uses the rewritten address. TabularisDB/tabularis-mongodb-plugin#12 hits this with the MongoDB plugin.Fix
A driver can now declare a
socks5_proxycapability. For those drivers, an SSH connection opens a SOCKS5 proxy over the SSH session (a dynamic forward, likessh -D) instead of the fixed forward. The host leaveshost,portandconnection_urias the user entered them and sends the plugin:socks5_proxy_host127.0.0.1socks5_proxy_portsocks5_proxy_username/socks5_proxy_passwordThe driver connects to each node through the proxy by name, so the bastion resolves the hostnames. Drivers without the capability keep the fixed forward exactly as before.
drivers/driver_trait.rs,registry.rs. Newsocks5_proxycapability (camelCasesocks5Proxyalso accepted, defaultfalse). The registry keeps a synchronous set of drivers that declare it, like the existing local-path set. The built-in drivers set it tofalse.ssh_tunnel.rs. A tunnel has aForwardTarget, eitherFixed { host, port }(what exists today) orSocks5. The systemsshbackend gets-Dinstead of-L. The russh backend runs a SOCKS5 server and opens onedirect-tcpipchannel per accepted connection, to the destination the client asked for. Channels open concurrently, so a slow destination doesn't hold up the others, and a client that doesn't finish the SOCKS5 handshake within 10 s is dropped.ssh_socks5.rs(new). The SOCKS5 server side:CONNECTonly. A domain name is passed on as a name, not resolved locally.application/tunnels.rs.resolve_ssh_paramspicks the forward target and the tunnel key from the capability, then makes a singleSshTunnel::new_with_targetcall. A SOCKS5 tunnel is keyed by SSH user, host and port only, so every connection through the same bastion shares it.socks5_proxy_*values sent by a client are dropped before resolving.-Lforwards too.sshgets-o ServerAliveInterval=15 -o ServerAliveCountMax=3.SOCKS5 CONNECT <host:port>line is atdebuglevel.SOCKS5 127.0.0.1:<port>) instead of the database port.PLUGIN_GUIDE.md("SSH Tunnels through a SOCKS5 Proxy"),manifest.schema.jsonand the web-ui plugin types.Points from #1449
Explicit capability. The switch is
capabilities.socks5_proxy, not the presence ofconnection_uri. A driver without it is unaffected. Covered by unit tests on the registry and onresolve_ssh_params.URI proxy conflict. The host only supplies the endpoint, and the plugin owns the URI, so the check lives in the plugin.
PLUGIN_GUIDE.mdmakes it a requirement for any driver with this capability: fail with a clear error when the user's connection already sets a different proxy. The MongoDB plugin does this (second screenshot).DNS. Only the destinations the driver connects to go through the proxy, and the bastion resolves those names. Lookups the driver makes before connecting, such as the SRV and TXT queries for
mongodb+srv://, still use the local resolver. If those records exist only in the private network's DNS, discovery fails, and the user needs a seedlist URI. This is listed under Limits inPLUGIN_GUIDE.mdand in the plugin README. I haven't been able to test a network where the SRV records are private-only.Local listener authentication. My call, by backend:
ssh -D(key file, ssh-agent or default keys): OpenSSH's dynamic forward can't authenticate clients, so the username and password are absent. While the tunnel is open, any local process can use it to reach anything the bastion can reach. It binds to127.0.0.1only. I documented this as a limit instead of forcing key logins through russh, which would change today's key, agent and passphrase handling for every SSH connection.If you'd rather close the
ssh -Dgap, two options: only open SOCKS5 through russh, or put an authenticating relay in front ofssh -D. I'm happy to do either here or as a follow-up.Sharing, cleanup and recovery. Covered in Testing below.
Screenshots
tabularis webfrom this branch with the MongoDB plugin branch, through an OpenSSH bastion in Docker. The replica set is reachable only asmongo-internal, a name that resolves on the bastion's network and not on the host.proxyHostBoth connections went through the same bastion and show the same proxy port: one shared tunnel. The 29 s gap before the database step is a side effect of how I seeded the test connection, not the tunnel. Its URI wasn't saved, so the modal's database-list request fell back to the form default
localhost:3306and held the plugin until it timed out.Testing
End to end (Docker:
linuxserver/openssh-serverbastion plus amongo:7.0replica setrs0whose only member ismongo-internal:27017, using the plugin branch):ssh -Dpath (key file): the same queries.proxyHost/proxyPort: the conflict error in the screenshot.Rust
cargo test --lib: 2537 passed, 4 failed, none in code this PR touches.mainon my machine.reconcile_active_drivers_keeps_ids_present_in_the_active_listfails when run with the whole suite becauseexport::read_only_tests(feat(connections): add enforced read-only mode per connection #1463, just merged) leavesreadonly-export-*manifests registered. It passes on its own.transport::web::tests::rejects_rpc_body_delayed_until_after_session_cleanupis timing-sensitive here and fails intermittently, on its own as well.-L/-Darguments and keepalive optionssocks5_*fieldscargo check --all-targetspasses.cargo clippyreports nothing new;rustfmt --checkon the changed files is clean.Limits
ssh -Dlistener. See "Local listener authentication" above.-Ltunnels.tabularis web; I also ran the desktop build against the same setup but couldn't capture its window from my terminal.