Repository navigation
Conversation
Starting a server on an SSH host left it unreachable from here: the `PortForwardManager` only recorded forwards in a HashMap and logged "not fully implemented - connections will not be forwarded". Implement local (`-L`) forwarding for real. Each forward binds a listener and splices accepted connections onto a `direct-tcpip` channel, so the remote sshd dials the service and both directions are copied. The remote endpoint is a forward's identity; the local port is only an allocation. A requested local port that is taken is replaced rather than refused, and the port originally asked for comes back with the mapping so the UI can say the address moved instead of silently handing over a different one. Privileged ports fall back the same way. Three failure modes worth naming: - Channels are opened through SSHConnectionManager per connection instead of against a cached handle, so a forward survives an automatic reconnect. That same path would let a forward nobody stopped dial back out to a host the user explicitly disconnected from, so each connection checks liveness first, and disconnect tears down the forwards it carried. - A local Docker workspace has no SSH session to carry a channel. It is rejected when the mapping is added rather than on the first request. - Forwards bind loopback only. Republishing a remote service to the local network stays behind an explicit opt-in. Add remote listening-port discovery (ss, then lsof, then netstat, with a marker line naming the dialect) so users can pick a real port instead of guessing one. Discovery forwards nothing on its own. Expose it through a manual dialog reached from the workspace menu while connected: nothing is ever forwarded automatically. The table keeps the remote port and the local address in separate columns because they routinely differ. Reverse forwarding keeps its entry point but now fails loudly; it needs `tcpip-forward` global requests and a `forwarded-tcpip` handler that the session handler does not carry.
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.
Summary
Starting a server on an SSH host left it unreachable from the machine running BitFun.
PortForwardManagerexisted but was a placeholder: it recorded forwards in aHashMapand logged"Port forwarding is not fully implemented - connections will not be forwarded".This implements local (
-L) forwarding for real, and exposes it as a manual mapping UI — nothing is ever forwarded automatically.Each forward binds a listener and splices accepted connections onto a
direct-tcpipchannel, so the remote sshd dials the service and both directions are copied. That primitive was already in production use for ProxyJump chains and container sshd probing; this puts it behind a managed listener.Type and Areas
Type: Feature
Areas: Rust core (
services-integrations/remote_ssh), desktop/Tauri commands, web UI, localesMotivation / Impact
The remote endpoint is a forward's identity; the local port is only an allocation. A requested local port that is already taken is replaced rather than refused, and the port originally asked for is returned with the mapping so the UI can say "local 3000 was in use, bound 3001 instead" rather than silently handing over a different address. The table keeps "remote port" and "local address" in separate columns for the same reason — they routinely differ. Privileged ports (
<1024) fall back the same way instead of surfacing a bare permission error.Three failure modes worth calling out for review:
SSHConnectionManagerper connection rather than against a cachedArc<Handle>, so a forward survives an automatic reconnect (reconnect installs a new handle). That same path would let a forward nobody stopped dial back out to a host the user explicitly disconnected from — so each connection checks liveness first, andssh_disconnect/ssh_disconnect_alltear down the forwards they carried.docker execand has no SSH session to carry a channel. It is rejected when the mapping is added, not on the first request.Also adds remote listening-port discovery (
ss, thenlsof, thennetstat— that order matters, since macOSnetstat -pmeans "protocol") so users can pick a real port instead of guessing. A marker line names the dialect so the parser doesn't have to guess. Discovery forwards nothing on its own.Reverse (
-R) forwarding keeps its entry point but now fails loudly instead of silently succeeding; it needstcpip-forwardglobal requests and aforwarded-tcpipchannel handler that the session handler does not carry.Verification
AI-assisted. Testing level: lightly tested — unit coverage is thorough, but the change has not been run against a live SSH host, so the accept →
direct-tcpip→copy_bidirectionaldata path is unverified end to end. Reviewers should weigh that.cargo check -p bitfun-services-integrations --features remote-ssh-concretecargo check -p bitfun-services-integrations --features remote-ssh(stub build)cargo test -p bitfun-services-integrations --features remote-ssh-concretecargo clippy -p bitfun-services-integrations --features remote-ssh-concrete --all-targetscargo check -p bitfun-desktoppnpm --dir src/web-ui run type-checkpnpm --dir src/web-ui exec eslint(changed files)pnpm --dir src/web-ui exec vitest run src/features/ssh-remotepnpm run i18n:auditpnpm run theme:color-auditpnpm run check:repo-hygienepnpm run fmt:rsTwo checks I could not run, both pre-existing and unrelated (each verified by stashing this branch's changes and reproducing):
cargo test -p bitfun-desktop— blocked by a compile error insrc/apps/desktop/src/logging.rs:786(format_args!temporary dropped while borrowed) in an existing#[cfg(test)]block. Thebitfun-desktoplib target compiles clean.pnpm run check:core-boundaries— fails inside a git worktree becauseBitFun-Installer/src-tauri/Cargo.tomlresolves the workspace to the main repo root.Reviewer Notes
New unit coverage worth a look: the bind-fallback path, the three listening-port dialect parsers (
ss/netstat/lsof, including shell-banner tolerance and dual-stack row merging), and the wire-shape contract tests for the new camelCase DTOs.The duplicate check, bind, and registry insert are held under one write lock, so two concurrent requests for the same remote endpoint cannot both bind. Concurrency per forward is capped at 256 via a semaphore that queues rather than drops, and the accept loop's permit wait is under the same cancellation as
acceptso a stopped forward does not linger.last_erroris the forward's health signal and self-heals: it clears after any connection completes normally, so creating a mapping before starting the remote service does not leave a permanent error badge.UI screenshots are not included — the dialog has not been exercised against a live host.
Checklist
ssh.portForward.*in zh-CN / zh-TW / en-US).