Skip to content

add SOCKS4/5 dynamic port forwarding (SSH tunnel proxy)#33

Open
kerneltravel wants to merge 9 commits into
GOODBOY008:mainfrom
kerneltravel:main
Open

add SOCKS4/5 dynamic port forwarding (SSH tunnel proxy)#33
kerneltravel wants to merge 9 commits into
GOODBOY008:mainfrom
kerneltravel:main

Conversation

@kerneltravel

Copy link
Copy Markdown

feat: add SOCKS4/5 dynamic port forwarding (SSH tunnel proxy), and support Session Restore.

- socks_proxy.rs: SOCKS4/4a/5 handshake + bidirectional relay over SSH
- ssh/mod.rs: get_session_handle(), open_direct_tcpip() for channel sharing
- connection_manager.rs: SocksProxyInfo type, start/stop/list/cleanup lifecycle
- commands.rs: Tauri commands start_socks_proxy, stop_socks_proxy, list_socks_proxies
- lib.rs: register modules and commands
- port-forwarding-panel.tsx: React panel with proxy list and create form
- App.tsx: add Port Fwd tab to right sidebar
- Replace manual select! relay loop with tokio::io::copy_bidirectional
  which properly handles half-close semantics (no dropped data on EOF)
- Send graceful shutdown (EOF) on both sides after relay completes
- Fix SOCKS5 reply ATYP: always use IPv4 (0.0.0.0) instead of echoing
  request ATYP=3 which produced a malformed response missing the
  required 1-byte length prefix for domain names
…se deadlock

Replace Channel::into_stream() + copy_bidirectional with direct
Channel::wait() and Channel::data() calls in a single select! loop.

Root cause: ChannelStream::poll_read does not convert SSH Close message
to EOF — when the server closes the channel, poll_read parks forever.
With copy_bidirectional this causes the relay to hang until timeout,
producing ERR_CONNECTION_CLOSED.

Fix: use Channel::wait() which returns Some(ChannelMsg::Close) that
we handle properly as an EOF condition, along with Channel::data()
for writes. Single-task select! avoids mutex deadlock issues.
Tauri v2 renames snake_case command parameters to camelCase for the JS
API. stop_socks_proxy(proxy_id) expects { proxyId } from JS, not { proxy_id }.
…ging

- Send SSH_MSG_CHANNEL_CLOSE after relay exit to avoid leaking channels
- Add SO_REUSEADDR via socket2 on the TCP listener for Windows port rebinding
- Add tracing::debug! logs around relay lifecycle for future diagnostics
- Extend ActiveConnectionState with optional proxies array
- Save active SOCKS proxy config alongside connection state on tab changes
- Restore proxies after SSH connection is re-established during session restore
…e effect

- Revert save effect to synchronous (fixes timing bug that broke session restore)
- Persist proxy list to localStorage on panel refresh/poll (every 3s) and on start/stop
- Read proxy state from localStorage during restore after SSH reconnects
Root cause: PortForwardingPanel's polling refresh (every 3s) persisted proxy state
on every tick. On first mount after restart the backend has zero proxies,
so refresh saved [] and overwrote the saved state before the restore effect
could read it.

Fix:
- Remove persistProxyState() from polling refresh (UI-only now)
- Add global effect in App.tsx that persists non-empty proxy lists every 10s
  with 1s initial delay so restore effect reads saved state first
- Start/stop handlers in PortForwardingPanel still persist immediately
@GOODBOY008 GOODBOY008 self-requested a review June 8, 2026 06:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant