Traversio

Forwarding

Understand the difference between direct channels, local forwarding, dynamic forwarding, remote forwarding, and ProxyJump before you choose a Traversio API.

The forwarding surface becomes easier to navigate when you focus on two questions first:

  1. who is listening for the TCP connection
  2. who decides the final target host and port

Most forwarding confusion comes from mixing up those two dimensions.

Direct TCP/IP channel

+-----------------+      +------------+      +----------------+
| Your Swift code | ---> | SSH server | ---> | Remote service |
+-----------------+      +------------+      +----------------+

Direct TCP/IP is the raw primitive.

  • no app-facing listener is created
  • your Swift code opens the stream directly
  • the target is fixed when you call the API

Use this when your own code wants to speak the target protocol itself. Typical examples are:

  • send one HTTP request to an internal service and parse the raw bytes
  • talk to a custom binary protocol
  • build your own higher-level forwarding behavior on top of one raw channel
Local port forwarding

+----------------------+      +----------------+      +------------+      +---------------------+
| Browser / local tool | ---> | Local listener | ---> | SSH server | ---> | Fixed remote target |
+----------------------+      +----------------+      +------------+      +---------------------+

Local port forwarding creates a normal TCP listener on your machine.

  • local tools connect to that local port
  • Traversio forwards everything to one fixed remote target
  • the target is chosen when the forward starts, not per request

This is the usual answer for:

  • "the service runs on the server, but I want to open it locally"
  • "I want my browser to visit a remote-only HTTP service"
  • "I want a local SQL client to reach one remote database host"
Dynamic port forwarding

+----------------------+      +---------------------+      +------------+      +----------------------+
| Browser / curl / DB  | ---> | Local SOCKS listener| ---> | SSH server | ---> | Target chosen by the |
| tool                 |      |                     |      |            |      | SOCKS client request |
+----------------------+      +---------------------+      +------------+      +----------------------+

Dynamic port forwarding also creates something on your machine, but it is a SOCKS proxy rather than a fixed local TCP tunnel.

  • local tools connect to the SOCKS listener
  • the final target is selected by each SOCKS request
  • one SOCKS endpoint can reach many different remote services

Use this when one local tool needs flexible routing, for example:

  • a browser accessing several internal sites
  • curl switching between multiple internal HTTP endpoints
  • a database tool connecting to different hosts through one SSH-backed SOCKS proxy
Remote port forwarding

+---------------+      +-----------------+      +------------+      +--------------------+
| Remote client | ---> | Remote listener | ---> | SSH server | ---> | Your local service |
+---------------+      +-----------------+      +------------+      +--------------------+

Remote port forwarding reverses the direction.

  • the listener is created on the SSH server side
  • remote clients connect there
  • Traversio sends that traffic back to your local service

This is the usual answer for:

  • "my app is running locally, but the remote side needs to reach it"
  • "I want to expose a local dev HTTP server through the SSH host"
  • "I want a machine behind the SSH server to connect back to my laptop service"

Forwarding Modes

WorkflowWho listensWho chooses the final targetGood exampleTraversio API
Direct TCP/IP channelnobody; your code opens one channel directlyyour Swift codefrom Swift, speak HTTP or a custom binary protocol to one remote serviceopenDirectTCPIPChannel(...)
Local port forwardingyour local machineyou choose a fixed target when the forward startsa web service runs on the server; you want to open it in your local browserwithLocalPortForwarding(...)
Dynamic port forwardingyour local machine, as a SOCKS proxyeach local client connection chooses the targetbrowser, curl, or a DB tool should reach many internal services through one SOCKS endpointwithDynamicPortForwarding(...)
Remote port forwardingthe SSH server sideyou choose a fixed local target when the forward startsexpose your local dev server so a remote machine can reach itwithRemotePortForwardListener(...), withRemotePortForwarding(...)
Connection proxyan external proxy serviceyour SSHClientConfiguration chooses the SSH server endpointreach the SSH server itself through a company HTTP CONNECT or SOCKS5 proxySSHClientConfiguration.connectionProxy
ProxyJumpno app-facing listener; it changes how the SSH connection itself reaches the targetyour SSHClientConfiguration chooses the hop chainreach an internal SSH host through a bastion or jump boxSSHClientConfiguration.proxyJumpHosts

One Concrete HTTP Example Per Mode

Direct TCP/IP channel

Scenario: an HTTP server is already running on 127.0.0.1:8080 on the remote side, and your Swift code wants to send one request and parse the raw response itself.

This is the lowest-level forwarding primitive. There is no local listener. No browser or external tool connects anywhere. Your code opens one direct-tcpip channel and speaks the target protocol directly.

Local port forwarding

Scenario: an HTTP server is running on the server side, for example 127.0.0.1:3000, and you want to access it from your laptop at http://127.0.0.1:8080 for debugging.

This is the classic "remote service, local access" tunnel. If someone says "the service is on the server, but I want to open it locally in a browser," this is usually the right answer.

Dynamic port forwarding

Scenario: you want one local SOCKS proxy, then let curl, a browser, or a database GUI decide whether the next connection should go to 127.0.0.1:8080, db.internal:5432, or some other remote-only service.

This is still local forwarding, but the target is not fixed in advance. The local client chooses it per connection through SOCKS.

Remote port forwarding

Scenario: your local machine is running a dev HTTP server on 127.0.0.1:3000, and you want a remote machine to open http://127.0.0.1:8080 on the SSH server side and reach your local app.

This is the opposite direction from local forwarding. The listener is remote, not local.

ProxyJump

Scenario: you cannot SSH to db-admin.internal directly, but you can SSH to bastion.example.com, and that bastion can reach the internal host.

Here you are not exposing an HTTP or TCP port. You are deciding how the SSH connection itself reaches the final SSH server.

Choosing Between Local, Dynamic, And Remote

Use this rule first:

  • if your local machine gets the listener, think local or dynamic
  • if the SSH server side gets the listener, think remote
  • if there is no listener and your code opens one raw stream, think direct-tcpip
  • if the SSH server is only reachable through an external proxy service, think connectionProxy
  • if you are solving "how do I reach the SSH server at all", think ProxyJump

Another practical shortcut:

  • fixed target and local browser/tool connects to a local port: withLocalPortForwarding(...)
  • variable target and local browser/tool speaks SOCKS: withDynamicPortForwarding(...)
  • remote side must reach your local service: withRemotePortForwarding(...)
  • your Swift code alone needs one raw stream: openDirectTCPIPChannel(...)

ProxyJump And Connection Proxies

These two features solve different routing problems:

  • ProxyJump means: first SSH to hop A, then open the next SSH hop through A
  • an HTTP or SOCKS connection proxy means: before SSH starts, tunnel the outer TCP connection through an external proxy service

Traversio supports both models:

  • SSHClientConfiguration.connectionProxy
  • SSHClientConfiguration.proxyJumpHosts

Supported Forwarding Surface

Public APIs in the library:

  • SSHConnection.openDirectTCPIPChannel(...)
  • SSHConnection.withLocalPortForwarding(...)
  • SSHConnection.withDynamicPortForwarding(...)
  • SSHConnection.withRemotePortForwardListener(...)
  • SSHConnection.withRemotePortForwarding(...)
  • SSHClientConfiguration.connectionProxy
  • SSHClientConfiguration.proxyJumpHosts

Validation status as of April 7, 2026:

  • direct raw forwarding round trip: live-validated against the local OpenSSH 9.6 chacha20-poly1305 target
  • local port forwarding: live-validated against the same OpenSSH target
  • dynamic forwarding: live-validated on the SOCKS5 path in both no-auth and username/password modes against the same OpenSSH target
  • connection proxies: live-validated through the local Dante/Tinyproxy Docker matrix for SOCKS5 no-auth, SOCKS5 username/password, HTTP CONNECT, HTTP CONNECT Basic auth, and one combined connectionProxy + proxyJumpHosts path
  • ProxyJump: live-validated through a real jump host hop to a nested SSH handshake
  • remote forwarding bridge data path: live-validated, and the earlier teardown warning has been cleared on the rerun local OpenSSH matrix plus lab-root

Forwarding limits to keep in mind:

  • local and dynamic forwarding are closure-scoped Apple 26+ helpers
  • dynamic forwarding supports SOCKS5 with no-auth or username/password auth, plus SOCKS4 and SOCKS4a when SOCKS5 auth is not configured
  • connection proxies cover SOCKS5 and HTTP CONNECT on the outermost TCP connection
  • remote forwarding targets one fixed local endpoint per helper scope, and accepted remote connections stay isolated per connection

Use the page below that matches your scenario:

On this page