Dynamic Port Forwarding
Run a local SOCKS proxy and let each local client choose the remote target per connection.
Dynamic forwarding is the answer to this question:
"I want one local proxy, then let my browser, curl, or another client decide which remote service to reach."
This is similar to ssh -D.
Mental Model
browser / curl / DB tool
-> local SOCKS listener
-> SSH tunnel
-> target chosen by the SOCKS client for each connectionThe final target comes from the local SOCKS request, so the forwarding target is chosen per connection.
Example
import Traversio
@available(macOS 26.0, iOS 26.0, *)
func withSOCKSTunnel(configuration: SSHClientConfiguration) async throws {
try await SSHClient.withConnection(configuration: configuration) { connection in
try await connection.withDynamicPortForwarding(
localHost: "127.0.0.1",
localPort: 0
) { forward in
print("SOCKS proxy ready on \(forward.localHost):\(forward.localPort)")
// While this scope stays open, point local tools at the SOCKS endpoint.
// Example:
// curl --socks5-hostname 127.0.0.1:\(forward.localPort) http://127.0.0.1:8080/health
}
}
}If local tools should authenticate before they can use the SOCKS listener, pass socks5Authentication:
try await connection.withDynamicPortForwarding(
localHost: "127.0.0.1",
localPort: 0,
socks5Authentication: .usernamePassword(
username: "local-user",
password: "local-secret"
)
) { forward in
print("Authenticated SOCKS proxy ready on \(forward.localHost):\(forward.localPort)")
}Good Use Cases
Dynamic forwarding is a better fit than fixed local forwarding when:
- the target host and port vary per connection
- you want to browse multiple internal sites through one SSH-backed SOCKS endpoint
- a local tool already supports SOCKS and you do not want to create one fixed tunnel per target
A typical example is:
- internal HTTP admin UI on one host
- internal PostgreSQL on another host
- one local SOCKS endpoint
- each client picks the destination it needs
How It Differs From Local Port Forwarding
Both modes create something on your machine.
The difference is:
- local forwarding creates a normal TCP listener for one fixed target
- dynamic forwarding creates a SOCKS listener and the target is chosen later by each client
If the target is always known up front, fixed local forwarding is usually simpler.
Supported SOCKS Scope
Supported behavior:
- SOCKS5 with no-auth negotiation
- SOCKS5 username/password auth through
socks5Authentication: .usernamePassword(...) - SOCKS4 connect when SOCKS5 auth is not configured
- SOCKS4a connect when SOCKS5 auth is not configured
Limits:
- enabling SOCKS5 username/password auth disables SOCKS4 and SOCKS4a so local clients cannot bypass auth
- no UDP associate path
- this is a closure-scoped Apple 26+ helper
Local client failures are isolated per connection:
- a SOCKS handshake/auth rejection closes that local client connection
- the listener stays available for later local clients in the same forwarding scope
Not The Same As A Connection Proxy
Dynamic forwarding means Traversio becomes a SOCKS server for other local tools after SSH is already connected.
If Traversio itself needs to reach the SSH server through an external SOCKS5 or HTTP proxy first, that is a Connection Proxy, configured with SSHClientConfiguration.connectionProxy.
Validation
As of April 7, 2026:
- the SOCKS5 dynamic-forward path is live-validated against a real local OpenSSH 9.6 target in both no-auth and username/password modes
- SOCKS4 and SOCKS4a have deterministic coverage, and their live-validation depth is narrower than the SOCKS5 path
Lifetime And Shutdown
The same practical rules as local forwarding apply:
- the SOCKS listener exists only inside
withDynamicPortForwarding(...) - the owning
SSHConnectionmust stay alive - shutdown follows a best-effort listener teardown model