linbit-tunl: linbit-tunl

linbit-tunl(1) -- LINBIT support session client

NAME

linbit-tunl -- open a shared terminal session so LINBIT support can assist on this system

SYNOPSIS

linbit-tunl [--case CASE] [--token TOKEN] [--user USER]
            [--restricted] [--tunnel]
            [--attach] [--debug] [--target HOST:PORT] SESSION_ID

DESCRIPTION

linbit-tunl connects to the LINBIT relay server and creates a shared terminal session. LINBIT support engineers see the customer's terminal in real time and can type commands (unless restricted). An optional reverse-tunnel upgrade lets support SSH directly into the customer system.

tmux is required for all modes except --tunnel. The script creates a tmux session at a dedicated socket with two visible panes: the customer's working shell (main) on top and a chat pane (bottom) whose top rows are pinned to show session status (connection state, support viewer count, mode). The daemon that bridges tmux control-mode to the relay runs in a separate hidden background window, not in a visible pane. If tmux is not installed, only --tunnel mode is available; all other modes exit with an error.

The customer retains full visibility and control:

Session lifecycle

  1. A four-word session ID is provided by support (generated via tunl create-session on the relay).
  2. System context (hostname, IPs, case number, user) is gathered into a context string.
  3. An ephemeral ed25519 keypair is generated. The public key is submitted to POST /api/v1/sign with a one-time token; the relay CA signs it. Fallback: keyboard-interactive PAM authentication using the session ID or a 6-digit token.
  4. An SSH connection is opened to the relay: ssh tunl@relay "linbit-tunl/1 id=SID host=... mode=share ...". The relay's ForceCommand (relay-share.py produce) spawns a per-session broker.
  5. A tmux session is created at a dedicated socket (/tmp/linbit-tunl-{hash}/tmux.sock) with the two-pane layout described above. The daemon runs in-process in a hidden background window tagged @linbit-role=inner (created via break-pane -d); it never appears as a visible pane.
  6. The daemon attaches to tmux in control-mode, reads %output events from the main pane, and forwards them over SSH stdin to the broker. Broker commands arrive on SSH stdout.
  7. A reconnect loop with exponential backoff (1 s ... 30 s) handles network drops transparently.
  8. Ctrl-b Q sends SIGINT to the daemon, triggering cleanup: the SSH connection is closed, the session is deregistered, and the tmux server is killed (non-attach mode). The session directory is removed unless --debug was specified or the exit was abnormal.

Modes

share (default) : Support can see the terminal, type in the main shell, and request a reverse-tunnel upgrade. Upgrade requests show a confirmation popup; the customer explicitly grants or denies each one. The customer can pre-grant future requests at any time with Ctrl-b u (equivalent to --tunnel behavior) or block all requests via the menu.

--restricted : Watch-only. Support can see the terminal and send chat messages but cannot type or request a tunnel.

--tunnel : Pre-open the reverse tunnel at session start and auto-grant further requests without a confirmation popup. The only mode that works without tmux.

Authorized-keys marker format

When a reverse tunnel is granted, linbit-tunl adds a bracketed block to TUNL_AUTHKEYS_FILE:

# linbit-tunl-begin SESSION_ID
ssh-ed25519 AAAA... engineer@linbit.com
# linbit-tunl-end SESSION_ID

On startup, stale blocks from any previous session are removed before a new block is added.

Hotkeys

All hotkeys use the tmux prefix Ctrl-b (the tmux default). The customer can pick a different prefix for the tunl outer tmux by setting the environment variable TUNL_PREFIX before invoking linbit-tunl.py, which is useful when nesting tmux: setting TUNL_PREFIX=C-a (or any tmux key spec, e.g. M-Space, F12) keeps the customer's own tmux inside the shell pane fully usable with its default Ctrl-b. An unrecognized value is rejected by tmux and silently falls back to Ctrl-b with a warning on stderr. The cheat sheet (Ctrl-b ?), status bar, and pane labels all render the active prefix.

Key Action
Ctrl-b Q End session for good (orderly shutdown)
Ctrl-b c Toggle focus between chat and shell panes
Ctrl-b C-c Same as Ctrl-b c (chord variant; mirrors support side)
Ctrl-b u Pre-grant tunnel / unblock requests
Ctrl-b U Revoke tunnel (closes active tunnel, returns to confirm state)
Ctrl-b X Block all future tunnel requests (no popup, auto-reject)
Enter (on a dead shell pane) Respawn the shell
q (on a dead shell pane) End session for good
Ctrl-b L / C-l Reset pane layout
F1 / Ctrl-b Enter Open main menu
Ctrl-b ? Show project-specific cheat sheet
Ctrl-b z Toggle zoom on the current pane

Ctrl-b u behavior depends on current tunnel state:

Ctrl-b U revokes the active tunnel or clears pre-grant status, and returns to the default confirm state.

Key Action
c Go to chat -- move focus to the chat pane (same as Ctrl-b c)
L Reset layout -- rebuild split view from scratch (same as Ctrl-b L)
u Grant tunnel -- pre-grant or unblock (same as Ctrl-b u)
U Revoke tunnel -- close active tunnel, return to confirm state
X Block tunnel requests -- reject all future requests without a popup
Q End session for good (orderly shutdown)

BRB (Be Right Back) is a chat slash command rather than a menu entry. Type /brb in the chat pane to freeze keystroke forwarding from support; /back to restore. Support can still watch the terminal and chat while BRB is active. BRB is not available while a tunnel is active -- revoke the tunnel first. Enforcement is on the customer side: the daemon drops KEYS commands locally regardless of relay behavior.

Ending the session: Q (End session for good) triggers an orderly shutdown: the daemon closes the SSH connection to the relay, deregisters the session, removes temporary files, and then kills the tmux server (or the tmux window when running with --attach). This is the only way to end a session.

The orderly shutdown has bounded internal waits and a watchdog: if cleanup ever exceeds ten seconds, the daemon escalates by sending SIGKILL to itself. One button, predictable. If Q ever appears stuck for longer, kill the terminal externally and file a bug -- the daemon should not need an external rescue.

OPTIONS

SESSION_ID : Four lowercase words separated by hyphens, e.g. swift-fox-jumps-barrel. Provided by support. Required. The same ID can be reused to reconnect to an existing relay session.

--case CASE : Support case number or short free-text description. Included in the session context string and shown in tunl list. Optional.

--token TOKEN : Six-digit pre-registration token for certificate signing. Obtain from LINBIT support before invoking the script. On reconnect after a network drop, the relay remembers the session and does not require the token again.

--user USER : Username for the reverse-tunnel SSH connection. Default: auto-detected from $USER or $LOGNAME.

--restricted : Watch-only mode. Support can see the terminal and send chat messages but cannot type in the main shell or request a reverse tunnel.

--tunnel : Pre-open the reverse tunnel at session start and auto-grant further tunnel requests without a confirmation popup. The only mode that works without tmux.

--attach : Create a window inside the current tmux session instead of starting a dedicated tmux server. Automatic when re-run from inside an existing linbit-tunl tmux session.

--debug : Preserve the per-session temp directory (/tmp/linbit-tunl-{hash}/) after exit. Without this flag, the directory is preserved only on abnormal exit (error or short-lived session) and removed on normal exit.

--target HOST:PORT : Forward the reverse tunnel to HOST:PORT instead of localhost:22. Useful when the target SSH daemon listens on a non-default port.

--help, -h : Print usage and exit.

--version, -V : Print version and exit.

ENVIRONMENT

TUNL_RELAY : Relay hostname. Default: tunl.linbit.com.

TUNL_RELAY_PORT : Relay SSH port. Default: 22.

TUNL_USER : SSH username on the relay. Default: tunl.

TUNL_API : Base URL for the relay API. Default: https://tunl.linbit.com.

TUNL_AUTHKEYS_FILE : Path to the authorized_keys file where support keys are installed during a tunnel upgrade. Default: ~/.ssh/authorized_keys.

TUNL_TARGET_HOST : Equivalent to the host part of --target. Default: localhost.

TUNL_TARGET_PORT : Equivalent to the port part of --target. Default: 22.

TUNL_RELAY_CA_PUBKEY : Override the embedded relay CA public key. Intended for testing.

TUNL_COMPRESS : Enable SSH compression. Default: 1. Set to 0 to disable.

FILES

All per-session files live under /tmp/linbit-tunl-{hash}/, where {hash} is the first 12 hex digits of SHA-256(session_id). This keeps the session ID out of /tmp directory listings. The directory is removed on normal exit and preserved on error or with --debug.

/tmp/linbit-tunl-{hash}/tmux.sock : Dedicated tmux socket for the session.

/tmp/linbit-tunl-{hash}/ssh-ctl.sock : SSH ControlMaster socket for connection multiplexing.

/tmp/linbit-tunl-{hash}/debug.log : Append-only stderr tee. All daemon stderr output is captured here.

/tmp/linbit-tunl-{hash}/ssh.log : Append-only SSH debug log. Every line is timestamped and appended here. Survives reconnects. Useful for diagnosing authentication failures or network errors.

/tmp/linbit-tunl-{hash}/chat.fifo : Named pipe for chat pane input.

/tmp/linbit-tunl-{hash}/ctl.fifo : Named pipe for daemon control commands (tunnel grant/revoke).

/tmp/linbit-tunl-{hash}/known_hosts : Temporary known_hosts file trusting the relay CA.

/tmp/linbit-tunl-{hash}/sharekey/ : Ephemeral keypair and CA-signed certificate for this session.

/tmp/linbit-tunl-{hash}/askpass.sh : SSH_ASKPASS helper (KI/PAM fallback only; not created when using certs).

~/.ssh/authorized_keys (or TUNL_AUTHKEYS_FILE) : Support keys are appended here when a reverse tunnel is granted and removed on disconnect.

Session recording

The session recording (asciinema v2 .cast file) is written to a persistent location outside the session temp directory so it survives normal cleanup. The location is chosen in order of preference:

  1. /var/log/linbit-tunl/linbit-tunl_{timestamp}_{hostname}.cast
  2. ~/.local/share/linbit-tunl/linbit-tunl_{timestamp}_{hostname}.cast
  3. /tmp/linbit-tunl_{timestamp}_{hostname}.cast

The first writable location is used. The path is printed on session exit. Replay with asciinema play -i 2 <path> (caps idle gaps at 2 seconds).

EXIT STATUS

0 : Normal exit (Ctrl-b Q, SIGTERM, or clean completion).

1 : Fatal error (relay unreachable, authentication failure, invalid session ID, etc.).

130 : Daemon exited via SIGINT (delivered by Ctrl-b Q or kill -INT).

143 : Daemon exited via SIGTERM.

EXAMPLES

Connect with a session ID and case number:

sudo linbit-tunl.py --case CASE-4711 swift-fox-jumps-barrel

Connect with a pre-registration token:

sudo linbit-tunl.py --token 123456 swift-fox-jumps-barrel

Point the reverse tunnel at a non-default SSH port:

sudo linbit-tunl.py --target localhost:2222 swift-fox-jumps-barrel

Watch-only mode (support cannot type or request a tunnel):

sudo linbit-tunl.py --restricted swift-fox-jumps-barrel

Attach inside an existing tmux session:

sudo linbit-tunl.py --attach swift-fox-jumps-barrel

End the session (from inside the tmux session):

Ctrl-b Q     # disconnect from relay, kill tmux server
Ctrl-b K     # kill the entire tmux session

From outside tmux:

kill -TERM $(pgrep -f linbit-tunl)

SEE ALSO

tunl(1), ssh(1), tmux(1)

docs/architecture.html -- trust model, auth paths, wire protocol