Eugeny/tabby · error · Error

Cannot remove remote port forward before auth

Error message

Cannot remove remote port forward before auth

What it means

Thrown by removePortForward() when removing a Remote-type forward while this.ssh is not an AuthenticatedSSHClient. Removing a remote forward calls stopForwardingTCPPort on the authenticated client, which is unavailable before auth or after disconnect.

Source

Thrown at tabby-ssh/src/session/ssh.ts:839

                await this.ssh.forwardTCPPort(fw.host, fw.port)
            } catch (err) {
                // eslint-disable-next-line @typescript-eslint/no-base-to-string
                this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`)
                return
            }
            this.emitServiceMessage(colors.bgGreen.black(' <- ') + ` Forwarded ${fw}`)
            this.forwardedPorts.push(fw)
        }
    }

    async removePortForward (fw: ForwardedPort): Promise<void> {
        if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
            fw.stopLocalListener()
            this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
        }
        if (fw.type === PortForwardType.Remote) {
            if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
                throw new Error('Cannot remove remote port forward before auth')
            }
            this.ssh.stopForwardingTCPPort(fw.host, fw.port)
            this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
        }
        this.emitServiceMessage(`Stopped forwarding ${fw}`)
    }

    async destroy (): Promise<void> {
        this.logger.info('Destroying')
        this.willDestroy.next()
        this.willDestroy.complete()
        this.serviceMessage.complete()
        this.ssh.disconnect()
    }

    async openShellChannel (options: { x11: boolean }): Promise<russh.Channel> {
        if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
            throw new Error('Cannot open shell channel before auth')

View on GitHub (pinned to 14e2d60b9b)

Solutions

  1. Guard removal with the instance check and skip/defer if not authenticated.
  2. Track whether the remote forward was actually established (it is pushed to forwardedPorts only on success) and only call stopForwardingTCPPort for those.
  3. Call removePortForward during an active authenticated session, not from a disconnect/destroy handler.
  4. Wrap the call in try/catch during teardown so a lost transport does not abort cleanup.

Example fix

// before
await session.removePortForward(remoteFw) // throws post-disconnect
// after
if (session.ssh instanceof russh.AuthenticatedSSHClient) {
    await session.removePortForward(remoteFw)
} else {
    session.forwardedPorts = session.forwardedPorts.filter(x => x !== remoteFw)
}
Defensive patterns

Strategy: type-guard

Validate before calling

import * as russh from 'russh'
if (fw.type === PortForwardType.Remote &&
    !(session.ssh instanceof russh.AuthenticatedSSHClient)) {
    // just drop it from the local list; transport is gone
    session.forwardedPorts = session.forwardedPorts.filter(x => x !== fw)
    return
}

Type guard

import * as russh from 'russh'
function isAuthed (s: unknown): s is russh.AuthenticatedSSHClient {
    return s instanceof russh.AuthenticatedSSHClient
}

Try / catch

try { await session.removePortForward(fw) } catch (e) {
    if (/before auth/.test(e.message)) { /* transport gone; drop locally */ }
    else throw e
}

Prevention

When it happens

Trigger: removePortForward(fw) called with fw.type === PortForwardType.Remote when this.ssh is a plain SSHClient (not yet authenticated) or after destroy()/disconnect(). Typically a teardown path that runs after the transport already dropped.

Common situations: UI 'stop forwarding' action invoked right as the connection drops; cleanup handler firing during session teardown after auth was lost; the forward was never actually established (forwardedPorts entry from a previous session) and the user tries to remove it on a fresh, unauthenticated session.

Related errors


AI-assisted analysis of Eugeny/tabby@14e2d60b9b (2026-08-12). Data as JSON: /api/errors/5d786e0ac35494a2. Report an issue: GitHub.