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
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.
Listener Binding
Dynamic forwarding uses the same local listener binding rules as fixed local forwarding:
- pass
localPort: 0when the operating system should choose an available SOCKS port - pass a fixed
localPortwhen local tools need a stable SOCKS endpoint localHost: "localhost"is accepted for fixed ports; Traversio normalizes the listener bind endpoint to numeric loopback so the requested port is honoredforward.localHoststill reports the requested host string, so useforward.localPortas the authoritative bound port- use
127.0.0.1or::1when you need to choose an explicit IPv4 or IPv6 loopback bind
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 helper at the package floor declared in
Package.swift - Apple 26+ systems prefer the modern listener backend automatically, and older supported releases use the compatibility listener backend
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
- the SOCKS5 dynamic-forward path is validated in both no-auth and username/password modes
- the broader forwarding path is validated across the documented server families
- SOCKS4 and SOCKS4a have deterministic coverage, and their live deployment evidence 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