# How Tabby Implements X11 Forwarding Over SSH: A Deep Dive into the russh Integration

> Discover how Tabby implements X11 forwarding over SSH using the russh library. Learn about channel requests and local X11 socket plumbing in this developer deep dive.

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

---

**Tabby implements X11 forwarding over SSH by using the russh library to request X11 channels from the remote server, then plumbing data between those channels and a local X11 socket created via `X11Socket.resolveDisplaySpec` in [`tabby-ssh/src/session/x11.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/session/x11.ts).**

Tabby, the popular open-source terminal emulator, provides seamless X11 forwarding over SSH by integrating the russh Rust library with Node.js socket handling. This implementation allows graphical applications from remote Linux servers to render on your local machine across Windows, macOS, and Linux platforms.

## Core Architecture of X11 Forwarding in Tabby

Tabby’s X11 forwarding implementation consists of three tightly-coupled components working together to bridge remote X11 traffic to your local display server:

1. **X11 Display Resolution and Socket Handling** – Parses the `DISPLAY` environment variable and creates a local `net.Socket` connection to the X server.
2. **SSH Channel Request and Server-Side Handling** – Sends X11 forward requests when opening shells and handles incoming `x11ChannelOpen` events from the remote server.
3. **Bidirectional Data Plumbing** – Connects the remote X11 channel to the local X socket, forwarding data both ways and handling errors, EOF, and close events.

## Resolving the X11 Display: X11Socket.resolveDisplaySpec

The `X11Socket` class in [`tabby-ssh/src/session/x11.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/session/x11.ts) handles the complex logic of resolving display specifications across different platforms. The static method `resolveDisplaySpec` parses the `DISPLAY` environment variable or a user-provided spec and returns a `SocketConnectOpts` object ready for Node.js networking.

```typescript
// tabby-ssh/src/session/x11.ts
static resolveDisplaySpec (spec?: string|null): SocketConnectOpts {
    // Use supplied spec, otherwise the DISPLAY env var, fallback to localhost:0
    let [_, xHost, xDisplay] = /^(.+):(\d+)(?:.(\d+))$/.exec(
        spec ?? process.env.DISPLAY ?? 'localhost:0') ?? [undefined, undefined, undefined];

    // Platform-specific default host
    if (process.platform === 'win32') {
        xHost ??= 'localhost';
    } else {
        xHost ??= 'unix';
    }

    // Absolute socket path overrides host logic
    if (spec?.startsWith('/')) {
        xHost = spec;
    }

    const display = parseInt(xDisplay ?? '0');
    const port = display < 100 ? display + 6000 : display;

    // Turn "unix" into the standard Unix domain socket path
    if (xHost === 'unix') {
        xHost = `/tmp/.X11-unix/X${display}`;
    }

    // Return a net.Socket connect option (path for Unix socket, host+port otherwise)
    return xHost.startsWith('/')
        ? { path: xHost }
        : { host: xHost, port };
}

```

This method supports Unix domain sockets (`/tmp/.X11-unix/...`) on Linux and macOS, TCP connections (`localhost:6000`) on Windows, and absolute socket paths for custom configurations.

## Initiating X11 Forwarding Requests

When opening a shell channel with X11 forwarding enabled, Tabby sends an X11 forward request to the remote SSH server. This occurs in the `openShellChannel()` method within [`tabby-ssh/src/session/ssh.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/session/ssh.ts).

```typescript
// tabby-ssh/src/session/ssh.ts (openShellChannel)
if (options.x11) {
    await ch.requestX11Forwarding({
        singleConnection: false,
        authProtocol: 'MIT-MAGIC-COOKIE-1',
        authCookie: crypto.randomBytes(16).toString('hex'),
        screenNumber: 0,
    });
}

```

The `requestX11Forwarding` call uses the `MIT-MAGIC-COOKIE-1` authentication protocol with a randomly generated 16-byte hex cookie. Once the remote server accepts this request, it will open X11 channels for any remote X clients attempting to connect to the forwarded display.

## Handling Incoming X11 Channels

Tabby listens for incoming X11 channel open events through the `x11ChannelOpen$` observable provided by the russh library. The subscription in [`tabby-ssh/src/session/ssh.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/session/ssh.ts) handles the connection from the remote X client.

```typescript
// tabby-ssh/src/session/ssh.ts
this.ssh.x11ChannelOpen$.subscribe(async event => {
    this.logger.info(`Incoming X11 connection from ${event.clientAddress}:${event.clientPort}`);

    // Resolve the display the user (or config) wants to use
    const displaySpec = (this.config.store.ssh.x11Display || process.env.DISPLAY) ?? 'localhost:0';
    this.logger.debug(`Trying display ${displaySpec}`);

    const channel = await this.ssh.activateChannel(event.channel);
    const socket = new X11Socket();

    try {
        const x11Stream = await socket.connect(displaySpec);
        this.logger.info('Connection forwarded');
        this.setupSocketChannelEvents(channel, x11Stream, 'X11 forward');
    } catch (e) {
        this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not connect to the X server: ${e}`);
        this.emitServiceMessage(
            `    Tabby tried to connect to ${JSON.stringify(X11Socket.resolveDisplaySpec(displaySpec))} ` +
            `based on the DISPLAY environment var (${displaySpec})`
        );
        if (process.platform === 'win32') {
            this.emitServiceMessage('    To use X forwarding, you need a local X server, e.g.:');
            this.emitServiceMessage('    * VcXsrv: https://sourceforge.net/projects/vcxsrv/');
            this.emitServiceMessage('    * Xming: https://sourceforge.net/projects/xming/');
        }
        channel.close();
    }
});

```

This handler resolves the display specification, activates the SSH channel, creates an `X11Socket`, and attempts to connect to the local X server. If successful, it establishes bidirectional data flow; if it fails, it provides detailed error messages including platform-specific guidance for Windows users.

## Bidirectional Data Plumbing

The actual data transfer between the remote X11 channel and the local socket is handled by `setupSocketChannelEvents()` in [`tabby-ssh/src/session/ssh.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/session/ssh.ts). This generic helper is used for all port-forwarding types, including X11.

```typescript
// tabby-ssh/src/session/ssh.ts (excerpt)
private setupSocketChannelEvents (channel: russh.Channel, socket: Socket, logPrefix: string): void {
    // Remote → local
    channel.data$.subscribe({
        next: data => socket.write(data),
        error: err => { this.logger.error(`${logPrefix}: channel data error: ${err}`); socket.destroy(); },
    });

    // Local → remote
    socket.on('data', data => {
        try {
            channel.write(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
        } catch (err) {
            this.logger.error(`${logPrefix}: channel write error: ${err}`);
            socket.destroy(new Error(`${logPrefix}failed to write to channel: ${err}`));
        }
    });

    // Close / EOF handling (omitted for brevity)
}

```

This method subscribes to the channel's data observable to write incoming SSH data to the local socket, and listens to the socket's `data` event to write local data back to the SSH channel. Error handling ensures that failures on either side trigger proper cleanup of both the socket and the channel.

## Configuration and User Interface

X11 forwarding is controlled through Tabby's SSH profile settings. The [`sshSettingsTab.component.ts`](https://github.com/Eugeny/tabby/blob/main/sshSettingsTab.component.ts) component reads the default display configuration, while `sshProfileSettings.component.pug` provides the UI checkbox to enable forwarding.

When a user enables X11 forwarding in a profile, the `options.x11` flag is set to `true` and passed to `openShellChannel()`. Users can optionally specify a custom display via the `x11Display` configuration option, which overrides the `DISPLAY` environment variable.

## Summary

- **Tabby implements X11 forwarding over SSH** using the russh Rust library combined with Node.js socket handling in [`tabby-ssh/src/session/ssh.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/session/ssh.ts).
- **Display resolution** is handled by `X11Socket.resolveDisplaySpec()` in [`tabby-ssh/src/session/x11.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/session/x11.ts), supporting Unix domain sockets on Linux/macOS and TCP connections on Windows.
- **Channel establishment** occurs when `openShellChannel()` calls `requestX11Forwarding()` with `MIT-MAGIC-COOKIE-1` authentication.
- **Incoming connections** are handled via the `x11ChannelOpen$` observable, which activates channels and connects them to local X11 sockets.
- **Data transfer** is managed by `setupSocketChannelEvents()`, providing bidirectional flow between SSH channels and local sockets with robust error handling.

## Frequently Asked Questions

### What SSH library does Tabby use for X11 forwarding?

Tabby uses the **russh** Rust library to handle the underlying SSH protocol operations. The TypeScript code in [`tabby-ssh/src/session/ssh.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/session/ssh.ts) interacts with russh through Node.js bindings, utilizing observables like `x11ChannelOpen$` to handle asynchronous channel events from the remote server.

### How does Tabby resolve the X11 display on different platforms?

Tabby resolves the X11 display through the `X11Socket.resolveDisplaySpec()` method in [`tabby-ssh/src/session/x11.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/session/x11.ts). On Linux and macOS, it defaults to Unix domain sockets at `/tmp/.X11-unix/X{display}`, while on Windows it defaults to TCP connections to `localhost` on port `6000` plus the display number. The method also supports absolute socket paths and custom display specifications provided by the user.

### What authentication protocol does Tabby use for X11 forwarding?

Tabby uses the **MIT-MAGIC-COOKIE-1** authentication protocol when requesting X11 forwarding. When `openShellChannel()` sends the X11 request, it generates a random 16-byte hexadecimal cookie using `crypto.randomBytes(16).toString('hex')`. This cookie is passed to the remote SSH server, which uses it to authenticate incoming X11 connections from remote X clients.

### Can I configure a custom X11 display in Tabby?

Yes, you can configure a custom X11 display in Tabby's SSH profile settings. The `x11Display` option in the profile configuration allows you to override the default `DISPLAY` environment variable. If not specified, Tabby falls back to the `DISPLAY` environment variable, or ultimately defaults to `localhost:0`. This configuration is handled in the UI components [`sshSettingsTab.component.ts`](https://github.com/Eugeny/tabby/blob/main/sshSettingsTab.component.ts) and `sshProfileSettings.component.pug`.