Traversio

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 connection

The 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.

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 SSHConnection must stay alive
  • shutdown follows a best-effort listener teardown model

On this page