Traversio

Local Port Forwarding

Bind a local port on your machine and forward it to one fixed remote TCP target over SSH.

Local port forwarding is the classic answer to this question:

"A service is running on the server. How do I open it locally for debugging?"

If a remote HTTP server is listening on 127.0.0.1:3000, and you want to visit it locally as http://127.0.0.1:8080, this is the mode you want.

Mental Model

browser / URLSession / local tool
    -> local listener on your machine
        -> SSH tunnel
            -> one fixed remote target

The local port is what other local software connects to.

The remote target is fixed when you create the forward.

Example: Read A Remote HTTP Service Locally

import Foundation
import Traversio

@available(macOS 26.0, iOS 26.0, *)
func readInternalStatus(configuration: SSHClientConfiguration) async throws -> String {
    try await SSHClient.withConnection(configuration: configuration) { connection in
        try await connection.withLocalPortForwarding(
            targetHost: "127.0.0.1",
            targetPort: 3000,
            localHost: "127.0.0.1",
            localPort: 8080
        ) { forward in
            let url = URL(
                string: "http://\(forward.localHost):\(forward.localPort)/health"
            )!

            let (data, _) = try await URLSession.shared.data(from: url)
            return String(decoding: data, as: UTF8.self)
        }
    }
}

The same pattern works if you want to:

  • open the forwarded URL in a browser
  • point a REST client at a remote-only API
  • let a local database tool connect to a remote database with one fixed host and port

How It Differs From Dynamic Forwarding

Both local and dynamic forwarding create something on your local machine.

The difference is who chooses the target:

  • local forwarding: Traversio fixes the target when the forward starts
  • dynamic forwarding: each local client chooses the target later through SOCKS

So if you already know the remote target is always 127.0.0.1:3000, local forwarding is simpler.

If you want one local proxy that can reach many different destinations, use Dynamic Port Forwarding instead.

What The Closure Receives

The closure gets an SSHLocalPortForward:

  • localHost
  • localPort
  • targetHost
  • targetPort

That tells you exactly which local endpoint is active and which remote TCP service it maps to.

Availability And Lifetime

Important lifecycle rules:

  • this helper is currently available on Apple 26+ only
  • the local listener exists only inside the withLocalPortForwarding(...) body
  • once the body exits, Traversio stops forwarding new traffic through that scope
  • the underlying SSHConnection must still be alive for the tunnel to work

Shutdown Behavior

The local helper uses a best-effort shutdown contract.

That means:

  • Traversio stops bridging data when the scope ends
  • late local accepts are closed as quickly as possible
  • the bound port may remain connectable for a brief window while shutdown completes

Accepted connection failures are isolated per connection too:

  • if one accepted local client cannot open its remote direct-tcpip channel, Traversio closes that local client connection
  • the listener stays available for later local clients in the same forwarding scope

Support Status

As of April 6, 2026:

  • the local forwarding data path is live-validated against a real local OpenSSH 9.6 target
  • it is a closure-scoped helper
  • openDirectTCPIPChannel(...) is the expert path for raw per-connection control

On this page