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 serviceTwo 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.
| API | What Traversio gives you | Good fit |
|---|---|---|
withRemotePortForwardListener(...) | An SSHRemotePortForwardListener; each accept() returns one raw SSHForwardedTCPIPChannel | inspect 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:port | expose 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
originatorHostororiginatorPort - decide how to route each incoming connection yourself
- speak bytes directly on
SSHForwardedTCPIPChannel
The accepted channel exposes:
listeningHostlisteningPortoriginatorHostoriginatorPortwrite(_:)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-forwardon 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