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:
- who is listening for the TCP connection
- 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
curlswitching 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
| Workflow | Who listens | Who chooses the final target | Good example | Traversio API |
|---|---|---|---|---|
| Direct TCP/IP channel | nobody; your code opens one channel directly | your Swift code | from Swift, speak HTTP or a custom binary protocol to one remote service | openDirectTCPIPChannel(...) |
| Local port forwarding | your local machine | you choose a fixed target when the forward starts | a web service runs on the server; you want to open it in your local browser | withLocalPortForwarding(...) |
| Dynamic port forwarding | your local machine, as a SOCKS proxy | each local client connection chooses the target | browser, curl, or a DB tool should reach many internal services through one SOCKS endpoint | withDynamicPortForwarding(...) |
| Remote port forwarding | the SSH server side | you choose a fixed local target when the forward starts | expose your local dev server so a remote machine can reach it | withRemotePortForwardListener(...), withRemotePortForwarding(...) |
| Connection proxy | an external proxy service | your SSHClientConfiguration chooses the SSH server endpoint | reach the SSH server itself through a company HTTP CONNECT or SOCKS5 proxy | SSHClientConfiguration.connectionProxy |
| ProxyJump | no app-facing listener; it changes how the SSH connection itself reaches the target | your SSHClientConfiguration chooses the hop chain | reach an internal SSH host through a bastion or jump box | SSHClientConfiguration.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
localordynamic - 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:
ProxyJumpmeans: 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.connectionProxySSHClientConfiguration.proxyJumpHosts
Supported Forwarding Surface
Public APIs in the library:
SSHConnection.openDirectTCPIPChannel(...)SSHConnection.withLocalPortForwarding(...)SSHConnection.withDynamicPortForwarding(...)SSHConnection.withRemotePortForwardListener(...)SSHConnection.withRemotePortForwarding(...)SSHClientConfiguration.connectionProxySSHClientConfiguration.proxyJumpHosts
Validation status as of April 7, 2026:
- direct raw forwarding round trip: live-validated against the local OpenSSH 9.6
chacha20-poly1305target - 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 + proxyJumpHostspath - 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
Read Next
Use the page below that matches your scenario: