Traversio

Remote Port Forwarding

Ask the SSH server to listen remotely, then either accept raw forwarded connections yourself or bridge them back to one local TCP service.

Remote port forwarding creates a listener on the SSH server side, and incoming remote connections are sent back to your local machine.

This is the classic answer to:

"My local dev server is on my laptop. How do I expose it to the remote side through SSH?"

Mental Model

remote client
    -> remote listener on the SSH server side
        -> SSH tunnel
            -> your local service

Two Public Levels In Traversio

Traversio exposes two remote-forwarding APIs:

  • withRemotePortForwardListener(...)
  • withRemotePortForwarding(...)

Use the listener API when you want raw control over each incoming forwarded-tcpip channel.

Use the bridge helper when every incoming remote connection should go to one fixed local TCP endpoint.

APIWhat Traversio gives youGood fit
withRemotePortForwardListener(...)An SSHRemotePortForwardListener; each accept() returns one raw SSHForwardedTCPIPChannelinspect origin metadata, choose routing per accepted connection, or speak the forwarded bytes directly
withRemotePortForwarding(...)An SSHRemotePortForward; Traversio bridges every accepted remote connection to one local host:portexpose one local service to the remote side

Example: Bridge A Remote Listener To A Local HTTP Server

Suppose your laptop is running a dev server at 127.0.0.1:3000, and you want the remote side to reach it through 127.0.0.1:8080.

That is a fixed-endpoint remote forward:

import Traversio

func exposeLocalDashboard(configuration: SSHClientConfiguration) async throws {
    try await SSHClient.withConnection(configuration: configuration) { connection in
        try await connection.withRemotePortForwarding(
            localHost: "127.0.0.1",
            localPort: 3000,
            remoteHost: "127.0.0.1",
            remotePort: 8080
        ) { forward in
            print("Remote listener: \(forward.remoteHost):\(forward.remotePort)")

            // Keep the forward alive while the remote side needs it.
            try await Task.sleep(for: .seconds(30))
        }
    }
}

If you pass remotePort: 0, the SSH server allocates a free port and Traversio reports it back through SSHRemotePortForward.remotePort.

Example: Accept Raw Remote TCP Channels

Use the raw listener API when Swift should own the accepted stream:

import Traversio

func serveOneRemoteConnection(configuration: SSHClientConfiguration) async throws {
    try await SSHClient.withConnection(configuration: configuration) { connection in
        try await connection.withRemotePortForwardListener(
            remoteHost: "127.0.0.1",
            remotePort: 8080
        ) { listener in
            let channel = try await listener.accept()
            var request: [UInt8] = []

            while let event = try await channel.nextEvent() {
                switch event {
                case let .data(bytes):
                    request += bytes
                case .endOfFile:
                    try await channel.write(
                        "received \(request.count) bytes\n"
                    )
                    try await channel.sendEOF()
                    return
                }
            }
        }
    }
}

When To Use The Raw Listener API

Choose withRemotePortForwardListener(...) when you need details per accepted connection, for example:

  • inspect originatorHost or originatorPort
  • decide how to route each incoming connection yourself
  • speak bytes directly on SSHForwardedTCPIPChannel

The accepted channel exposes:

  • listeningHost
  • listeningPort
  • originatorHost
  • originatorPort
  • write(_:)
  • sendEOF()
  • close()
  • readChunk()
  • nextEvent()
  • events

How This Differs From Local Forwarding

This is the easiest way to avoid mixing them up:

  • local forwarding lets your local machine reach a remote service
  • remote forwarding lets the remote side reach your local service

So if someone says "the server should connect back to something on my laptop," think remote forwarding.

Support Status

  • the raw remote-listener round trip is validated on the documented server paths
  • the fixed-endpoint bridge data path is validated across the documented server families
  • Apple 26+ systems prefer the modern local-bridge transport backend automatically, and older supported releases use the compatibility backend
  • one accepted remote bridge failure stays scoped to that remote connection instead of poisoning later remote clients in the same forwarding scope
  • the fixed-endpoint bridge helper can keep multiple accepted remote connections active at the same time while the scope is open
  • if the remote server rejects cancel-tcpip-forward on teardown, Traversio closes the parent connection to avoid leaving the remote listener active

Practical limits:

  • the helper bridges every accepted remote connection back to one fixed local endpoint
  • this API focuses on remote listeners and fixed local targets

On this page