# SSH Port Forwarding in Tabby: Data Flow and Implementation Guide

> Learn SSH port forwarding in Tabby. Explore the three-layer architecture and data flow from UI config to TCP listeners and SSH channels for secure remote connections.

- Repository: [Eugene/tabby](https://github.com/Eugeny/tabby)
- Tags: deep-dive
- Published: 2026-03-03

---

**Tabby implements SSH port forwarding through a three-layer architecture where UI configuration creates `ForwardedPort` instances that establish TCP listeners or SSH channels, wiring local sockets to remote servers via `setupSocketChannelEvents` in [`tabby-ssh/src/session/ssh.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/session/ssh.ts).**

SSH port forwarding in Tabby enables secure tunneling of network traffic through established SSH connections. The Eugeny/tabby repository implements this feature in the **tabby-ssh** package, routing data from local TCP sockets through encrypted SSH channels using the russh library. Understanding this data flow reveals how local, remote, and dynamic forwards handle bi-directional byte streams between client applications and remote hosts.

## Architecture Overview

Tabby organizes SSH port forwarding into three distinct logical layers, each handling a specific phase of the tunnel lifecycle.

The **UI and configuration layer** manages user input through Angular components. When you create a forwarding rule in the *Port-forwarding* dialog, [[`SSHPortForwardingModal.component.ts`](https://github.com/Eugeny/tabby/blob/main/SSHPortForwardingModal.component.ts)](https://github.com/Eugeny/tabby/blob/master/tabby-ssh/src/components/sshPortForwardingModal.component.ts) constructs a `ForwardedPortConfig` object and passes it to the active SSH session.

The **session handling layer** processes these configurations in `SSHSession`. The [`addPortForward`](https://github.com/Eugeny/tabby/blob/master/tabby-ssh/src/session/ssh.ts#L758-L801) method receives the config, instantiates a concrete `ForwardedPort` object, and determines whether to start a local listener or request a remote forward from the server.

The **data tunnel layer** manages the actual byte streams. For local and dynamic forwards, [`ForwardedPort.startLocalListener`](https://github.com/Eugeny/tabby/blob/master/tabby-ssh/src/session/forwards.ts#L16-L31) creates a TCP or SOCKSv5 server. Each incoming connection triggers `openTCPForwardChannel` to open a russh channel, while [`setupSocketChannelEvents`](https://github.com/Eugeny/tabby/blob/master/tabby-ssh/src/session/ssh.ts#L53-L99) wires the socket and channel streams together for full-duplex communication.

## Local and Dynamic Port Forwarding Data Flow

Local and dynamic port forwards follow an identical data path through the Tabby stack, differing only in the type of local server created.

### Step 1: Configuration Capture

When a user defines a forward in the UI modal, the component emits a `forwardAdded` event carrying a `ForwardedPortConfig` object. This configuration specifies the forward type (local or dynamic), local binding address, local port, and target destination.

### Step 2: Session Registration

The modal component converts the configuration into a `ForwardedPort` instance using `Object.assign(new ForwardedPort(), config)` and calls `session.addPortForward(fw)`. According to the source in [`tabby-ssh/src/session/ssh.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/session/ssh.ts), this method detects `PortForwardType.Local` or `PortForwardType.Dynamic` and invokes `fw.startLocalListener()` to begin listening.

### Step 3: Local Server Initialization

In [[`tabby-ssh/src/session/forwards.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/session/forwards.ts)](https://github.com/Eugeny/tabby/blob/master/tabby-ssh/src/session/forwards.ts), `startLocalListener` creates the appropriate server:

- **Local forwards**: Uses Node.js `net.createServer` to listen on the specified TCP port
- **Dynamic forwards**: Uses `socksv5.createServer` to establish a SOCKSv5 proxy

### Step 4: Connection Handling

When a local client connects to the listening port, the server callback receives `accept`, `reject`, source address/port, and target address/port parameters. The session immediately opens a TCP forward channel through the SSH connection:

```typescript
const channel = await this.ssh.activateChannel(
    await this.ssh.openTCPForwardChannel({
        addressToConnectTo: targetAddress,
        portToConnectTo: targetPort,
        originatorAddress: sourceAddress ?? '127.0.0.1',
        originatorPort: sourcePort ?? 0,
    })
)

```

The `openTCPForwardChannel` method sends a **direct-tcpip** request to the remote SSH server, establishing the tunnel endpoint.

### Step 5: Stream Wiring

The `setupSocketChannelEvents` function (lines 53-99 in [`ssh.ts`](https://github.com/Eugeny/tabby/blob/main/ssh.ts)) creates the bi-directional data pipe:

- **Remote to local**: `channel.data$` pipes to `socket.write`
- **Local to remote**: `socket.on('data')` pipes to `channel.write`
- **Lifecycle management**: EOF, close, and error handlers on both streams ensure clean teardown when either side disconnects

This wiring creates a transparent byte tunnel where application data flows through the local socket, across the encrypted SSH channel, and emerges at the target address on the remote network.

### Step 6: Cleanup

When the user removes a forward rule, `SSHSession.removePortForward` calls `fw.stopLocalListener()` to close the TCP server and removes the entry from the session's `forwardedPorts` registry.

## Remote Port Forwarding Data Flow

Remote port forwarding reverses the connection direction, allowing the remote SSH server to accept connections and tunnel them back to the local machine.

When `addPortForward` detects `PortForwardType.Remote`, it invokes `ssh.forwardTCPPort(host, port)` instead of starting a local listener. This registers the forward with the remote SSH server, which then listens on the specified host and port.

The actual data tunnel creation happens automatically when the remote server receives a connection. The server forwards the connection to the Tabby client as a standard SSH channel, which the client handles using the same `setupSocketChannelEvents` logic to wire the channel to a local socket connection.

## Implementation Examples

### Adding a Local Forward Programmatically

You can create port forwards directly through the TypeScript API without using the UI:

```typescript
import { SSHSession } from 'tabby-ssh/src/session/ssh';
import { ForwardedPort } from 'tabby-ssh/src/session/forwards';
import { PortForwardType } from 'tabby-ssh/src/api';

// Assume `session` is an authenticated SSHSession instance
const fw = new ForwardedPort();
fw.type = PortForwardType.Local;
fw.host = '127.0.0.1';
fw.port = 8080;                 // Local listening port
fw.targetAddress = '10.0.0.5'; // Destination host on remote network
fw.targetPort = 80;              // Destination port

await session.addPortForward(fw);

```

This triggers the full flow: `startLocalListener` creates the `net.Server`, and each client connection opens a `direct-tcpip` channel through `setupSocketChannelEvents`.

### Removing a Forward

To terminate a forward and close all active tunnels:

```typescript
await session.removePortForward(fw);   // Cleans up local listeners or remote registrations

```

### UI Component Integration

The Angular template for configuring forwards in profile settings:

```pug
ssh-port-forwarding-config(
  [(model)]="sessionProfile.forwardedPorts"
  (forwardAdded)="onForwardAdded($event)"
  (forwardRemoved)="onForwardRemoved($event)"
)

```

The component emits configuration objects that [`SSHPortForwardingModal.component.ts`](https://github.com/Eugeny/tabby/blob/main/SSHPortForwardingModal.component.ts) transforms into `ForwardedPort` instances for the session.

## Key Source Files

Understanding the complete data flow requires examining these specific files in the Eugeny/tabby repository:

- **[`tabby-ssh/src/session/ssh.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/session/ssh.ts)**: Core session implementation containing `addPortForward`, `removePortForward`, and `setupSocketChannelEvents` that bind the tunnel streams
- **[`tabby-ssh/src/session/forwards.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/session/forwards.ts)**: Definition of the `ForwardedPort` class and its `startLocalListener`/`stopLocalListener` methods for managing local and dynamic forward servers
- **[`tabby-ssh/src/components/sshPortForwardingModal.component.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/components/sshPortForwardingModal.component.ts)**: UI bridge that receives user-defined forward configs and passes them to the session
- **[`tabby-ssh/src/components/sshPortForwardingConfig.component.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/components/sshPortForwardingConfig.component.ts)**: Angular component for editing `ForwardedPortConfig` objects
- **[`tabby-ssh/src/api/interfaces.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/api/interfaces.ts)**: TypeScript declarations for `ForwardedPortConfig` and the `PortForwardType` enum

## Summary

- **Tabby implements SSH port forwarding** across three layers: UI configuration ([`SSHPortForwardingModal.component.ts`](https://github.com/Eugeny/tabby/blob/main/SSHPortForwardingModal.component.ts)), session management (`SSHSession.addPortForward`), and data tunneling (`setupSocketChannelEvents`).
- **Local and dynamic forwards** create TCP or SOCKSv5 servers via `ForwardedPort.startLocalListener`, then open russh channels using `openTCPForwardChannel` for each incoming connection.
- **Remote forwards** register with the SSH server via `forwardTCPPort`, with the server initiating tunnel connections back to the client.
- **Bi-directional data flow** occurs through `setupSocketChannelEvents`, which wires socket streams to SSH channel streams for full-duplex communication.
- **Cleanup** happens through `removePortForward`, which stops local listeners with `stopLocalListener` or unregisters remote forwards with `stopForwardingTCPPort`.

## Frequently Asked Questions

### How does Tabby handle bi-directional data flow in SSH tunnels?

Tabby uses the `setupSocketChannelEvents` function in [`tabby-ssh/src/session/ssh.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/session/ssh.ts) to create a full-duplex pipe. It attaches `channel.data$` to `socket.write` for remote-to-local traffic, and `socket.on('data')` to `channel.write` for local-to-remote traffic. Error and close handlers on both sides ensure the tunnel closes cleanly if either the local socket or SSH channel terminates.

### What is the difference between local and remote port forwarding implementation in Tabby?

Local port forwarding uses `ForwardedPort.startLocalListener` to create a local TCP or SOCKSv5 server, then opens a `direct-tcpip` channel to the remote server for each connection. Remote port forwarding calls `ssh.forwardTCPPort` to register the forward with the remote SSH server, which then listens on the target port and initiates connections back to the Tabby client when traffic arrives.

### How does dynamic port forwarding (SOCKS) differ from local forwarding in the codebase?

Dynamic forwards use the same code path as local forwards through `addPortForward`, but `startLocalListener` creates a `socksv5.createServer` instead of a plain `net.createServer`. The SOCKS server handles the initial protocol negotiation, then follows the same channel opening and stream wiring logic as standard local forwards.

### Which russh library methods does Tabby use to establish SSH channels for port forwarding?

Tabby uses `ssh.openTCPForwardChannel` to send **direct-tcpip** requests for local/dynamic forwards, and `ssh.forwardTCPPort` to request remote port forwarding. Both methods return channel objects that `setupSocketChannelEvents` binds to local sockets using the `activateChannel` helper for stream management.