tunl -- manage linbit-tunl sessions from the support side
tunl list
tunl pick
tunl show SESSION_ID
tunl join [--read-only] [--debug] [SESSION_ID]
tunl browser [--profile NAME] [--kill] SESSION_ID
tunl ssh [SESSION_ID]
tunl upgrade SESSION_ID
tunl create-session [SESSION_ID] [--case CASE] [--customer NAME] [--expires-in MINUTES] [--join]
tunl expect [--ttl MINUTES]
tunl close [SESSION_ID]
tunl cancel [SESSION_ID]
tunl detach [SESSION_ID]
tunl help
tunl is the support-side CLI for managing
linbit-tunl sessions. It runs on support staff laptops or
jumpboxes.
Every subcommand that queries or modifies sessions reaches the relay
API through an SSH local port-forward to the relay's localhost. The
relay API (relay-api) listens on 127.0.0.1 only and is
never exposed to the internet; tunl forwards a local
ephemeral port to 127.0.0.1:TUNL_API_PORT on the relay for
each API call.
tunl connects to the relay as
TUNL_SUPPORT_USER (default: support).
Configure the relay's /home/support/.ssh/authorized_keys
with support staff personal keys. A standard ~/.ssh/config
stanza works:
Host tunl.linbit.com
User support
IdentityFile ~/.ssh/id_ed25519
tunl listList active and pending sessions. Pending sessions (created by
support but not yet connected by a customer) are shown with a
pending mode label.
Output columns:
| Column | Description |
|---|---|
| SESSION ID | Four-word session identifier |
| MODE | Session mode: share, share+tunnel,
pending |
| HOST | Customer hostname (or customer name for pending sessions) |
| CASE | Support case number (if provided) |
| AGE | Time since registration or creation |
tunl pickInteractive numbered menu of all active and pending sessions. Select by number or by session ID prefix.
When only one session exists it is selected automatically and the table is skipped.
At the action prompt (varies by session type):
j or Enter (share sessions): join the session
(tunl join)s or Enter (tunnel-only sessions): SSH to customer
(tunl ssh)b: open a proxied browser
(tunl browser)i: print session details (tunl show)q: quit without connectingFor pending sessions, only i (info) and q
(quit) are offered.
tunl show SESSION_IDPrint full details for one session, including registration time, mode, relay port, tunnel port (if active), hostname, IP addresses, customer name, case number, user, and remote IP.
For pending sessions, prints the session ID, status, case, customer name, and age.
Exits with status 1 if the session is not found.
tunl join [--read-only] [--debug] [SESSION_ID]Open a local tmux session showing the customer's terminal in real-time via the relay broker:
| Window | Name | Purpose |
|---|---|---|
| 0 | main | Full VT stream of the customer's shell |
| 1 | chat | Bidirectional chat; session info in pinned header |
| 2+ | -- | Extra windows opened from the menu (ssh, proxy, ...) |
Two SSH connections from the support side connect to the broker (one
for the main VT stream, one for chat-plus-status). Each is an
independent SSH connection with ControlPath=none to prevent
the user's ~/.ssh/config from multiplexing them over an
unrelated master. In the main window, keystrokes are forwarded to the
customer's shell. In the chat window, messages typed by support are
displayed on the customer's side.
With --read-only, keystrokes are not forwarded in either
window.
With --debug, the per-session temp directory
(/tmp/tunl-{sid}/) is preserved after the join session ends
instead of being cleaned up.
If SESSION_ID is omitted, the relay is queried and a
session is selected automatically (or prompted if multiple exist). If
the selected session is still pending, tunl join waits for
the customer to connect before setting up the local tmux.
Key bindings within the join session. All hotkeys use the tmux prefix
Ctrl-b by default; export TUNL_PREFIX
(e.g. TUNL_PREFIX=C-a, M-Space, or any tmux
key spec) before tunl join to pick a different prefix for
the join-session tmux. Useful when nesting under a tmux you already run
on your workstation -- the inner tmux keeps its default Ctrl-b. An
unrecognized value is rejected by tmux and falls back to Ctrl-b with a
warning on stderr. The cheat sheet (Ctrl-b ?), pane labels,
and console banner all render the active prefix. The setting is
per-side: support and customer pick their prefixes independently.
| Binding | Action |
|---|---|
| Ctrl-b 0/1 | Switch between main (0) and chat (1) windows |
| Ctrl-b C-c | Toggle main/chat window (mirrors customer-side Ctrl-b c) |
| F1 / Ctrl-b Enter | Main menu |
| Ctrl-b ? | Project-specific cheat sheet |
| Ctrl-b t | Open SSH window to customer (tunl ssh) -- not available
in restricted mode |
| Ctrl-b z | Toggle customer-side zoom |
| Ctrl-b d | Detach (leave session running, disconnect your client) |
| Ctrl-b Q | Close join session (kills local tmux; customer stays connected) |
| Enter | On a dead pane: reconnect; on a live pane: pass through |
| Key | Action |
|---|---|
| s | SSH to customer (opens tunl ssh in a new window) |
| x | Proxy shell (SSH + SOCKS5 proxy env vars in a new window) |
| b | Browser with throwaway profile (proxied Chrome/Chromium) |
| B | Browser -- choose or create a named persistent profile |
| c | Go to chat (switch to window 1) |
| z | Toggle customer-side zoom |
| R | Force my dimensions (bypasses smallest-wins, temporary) |
| d | Detach (resume with tmux attach) |
| Q | Close my window (customer continues) |
Items s/x/b/B are not shown in restricted sessions (no tunnel available).
Detach vs. Close join session: d
(Detach) disconnects your tmux client from
tunl-join-SESSION_ID but leaves that tmux session running.
Other engineers attached to the same session stay connected. You can
reattach later with
tmux attach -t tunl-join-SESSION_ID.
Q (Close join session) kills
tunl-join-SESSION_ID entirely -- all windows, all SSH
connections to the broker. The relay session and the customer's
connection are unaffected; the customer stays connected and other
engineers can still join. Use this when you are done with your local
view of the session.
The chat window has monitor-activity enabled so its tab
blinks when the customer sends a message.
The local tmux session is named tunl-join-SESSION_ID,
allowing multiple engineers to join the same session concurrently from
the same machine.
tunl browser [--profile NAME] [--kill] SESSION_IDOpen a SOCKS5 proxy through the reverse tunnel (via a shared SSH
ControlMaster) and launch Chrome or Chromium with a named persistent
profile. All browser traffic is routed through the SOCKS5 proxy
(--proxy-server=socks5://...) so DNS resolves on the
customer side.
Profile storage:
~/.local/share/linbit-tunl/profiles/tunl-PROFILE/
With --profile NAME, the named profile is used directly
(created if it does not exist). Without --profile, an
interactive menu is shown:
1 tunl-acme-prod (existing)
2 tunl-beta-customer (existing)
n Create new named profile
t Throwaway (deleted on exit)
Select profile [1-2/n/t]:
Named profiles persist across browser sessions (bookmarks, logins, history). Throwaway profiles are deleted when the browser exits.
With --kill, terminates a previously launched browser
for this session (by saved PID) without starting a new connection.
If the session does not yet have a reverse tunnel, an upgrade is requested automatically.
tunl ssh [SESSION_ID]SSH directly to the customer machine as the customer user via the reverse tunnel. If the session does not yet have a tunnel, requests an upgrade first (the relay broker picks a free port).
A shared SSH ControlMaster is established carrying a SOCKS5 proxy.
When running inside tmux, a proxy-env window is opened with
access information.
If SESSION_ID is omitted, the relay is queried and a
session is selected automatically.
Use this for quick command-line work without the full join session
UI. SSH and proxy shell access are also available from the join session
menu (Ctrl-b Enter) as the s and x items
respectively.
tunl upgrade SESSION_IDRequest a tunnel upgrade for a share session without connecting. The relay broker picks a free port and instructs the customer to open a reverse tunnel. Prints the confirmed relay port on success.
If the session already has an active tunnel, reports the existing port.
tunl create-session [SESSION_ID] [--case CASE] [--customer NAME] [--ad-hoc] [--expires-in MINUTES] [--join]Pre-register a session on the relay. The relay generates a session ID if none is provided. Prints the session ID and the command the customer should run.
Options:
--case CASE: attach a case number or reference.
Preferred: from the case number we can look up the customer.--customer NAME: attach a customer name or
organisation. Acceptable on its own; the case can often be deduced from
timestamp + context and filled in later with
tunl set-meta.--ad-hoc: declare that this session is not tied to any
support case yet (mutually exclusive with
--case/--customer). Use this for impromptu
testing or pre-case work; case and customer can be filled in later via
tunl set-meta once the work is bound to a case.--expires-in MINUTES: expiry window (default: relay
default, typically 1 hour).--join: immediately enter join mode after creating the
session, waiting for the customer to connect.At least one of --case, --customer, or
--ad-hoc is required. Sessions with no metadata at all are
intentionally refused so that support reporting doesn't accumulate
anonymous-looking records.
tunl expect [--ttl MINUTES]Generate a one-time 6-digit pre-registration token for customer authentication. The token is consumed on first successful use.
[tunl] Pre-registration token created.
[tunl] Token: 482917
[tunl] Expires: 2024-04-15 15:30 UTC (60 minutes)
[tunl]
[tunl] Share this token with the customer (verbally or via case system).
[tunl] Customer runs: sudo linbit-tunl.py --token 482917
Pass --ttl MINUTES to override the default TTL (default
60 minutes).
tunl close [SESSION_ID]Terminate the customer's share session from the support side. Sends TERMINATE through the broker to the customer daemon, which kills the customer's tmux session. The session is deregistered automatically when the customer daemon exits.
Also cleans up local per-session temp files (ControlMaster socket, PID files, browser PID).
If SESSION_ID is omitted, the relay is queried and a
session is selected interactively.
tunl cancel [SESSION_ID]Remove a session from the relay registry.
For pending sessions: cancels the invitation before the customer connects. For active sessions: removes the registry entry without terminating the customer's process (the SSH connection and tmux survive until the customer disconnects normally).
Use tunl close to actively terminate the customer's
session.
If SESSION_ID is omitted, the relay is queried and a
session is selected interactively.
tunl detach [SESSION_ID]Evict all current support clients (other engineers currently watching) of a share session. The customer's share session continues uninterrupted.
Useful when "changing seats": disconnects all watchers so a clean watch session can be started from a new machine.
If SESSION_ID is omitted, the relay is queried and a
session is selected interactively.
tunl set-meta SESSION_ID [--case CASE] [--customer NAME]Attach a case number and/or customer name to a session that doesn't have them yet (or fix a typo on an existing one). Useful for ad-hoc sessions that turn out to be tied to a real support ticket after the fact — the case (and downstream filesystem layout, slug-based filenames in archive bundles, etc.) starts reflecting the right metadata from that point on.
At least one of --case / --customer must be
supplied (nothing to do otherwise). Both validate by the same rules as
tunl create-session.
tunl cast SESSION_ID [--output FILE | -]Download the relay-side asciinema recording for a session. Works for
active, closed, and archived sessions transparently — for archived
sessions the cast member is extracted from the session's
.tar.gz / .tar.zst archive on the fly.
--output FILE writes to a file path (default:
<SESSION_ID>.cast in the current directory);
--output - streams the cast to stdout (binary) so it can be
piped into asciinema play, cat, or another
tool.
The endpoint behind this command is pod-internal; the call goes over the existing SSH port-forward.
tunl helpPrint usage summary and exit.
TUNL_RELAY : Relay hostname. Default:
tunl.linbit.com.
TUNL_RELAY_PORT : Relay SSH port. Default:
22.
TUNL_SUPPORT_USER : SSH username on the relay for
support staff. Default: support.
TUNL_API_PORT : Port of the relay API on the relay's
localhost. Default: 8080.
TUNL_SOCKS_PORT : Local port for the SOCKS5 proxy.
Default: 1080. If the port is in use, the next free port in
a range of 20 is selected automatically.
TUNL_FORWARD_AGENT : Set to 1,
yes, or true to allow SSH agent forwarding to
the customer system. Default: disabled.
TUNL_SSH_LOG : Path for SSH debug log output. Default:
/tmp/tunl-ssh.log.
TUNL_DISPLAY_NAME : Display name shown to the customer
in join/chat mode. Default: the value of
git config user.name, falling back to
$USER@hostname.
0 : Success.
1 : Session not found, relay unreachable, SSH connection
failed, or invalid arguments.
List active and pending sessions:
tunl list
Create a session and wait for the customer:
tunl create-session --case CASE-4711 --customer "Acme Corp" --join
Join an existing session:
tunl join swift-fox-jumps-barrel
Join in read-only mode (no keystroke forwarding):
tunl join --read-only swift-fox-jumps-barrel
SSH directly to the customer system:
tunl ssh swift-fox-jumps-barrel
Open a browser routed through the customer's network:
tunl browser swift-fox-jumps-barrel
tunl browser --profile acme-prod swift-fox-jumps-barrel
tunl browser --kill swift-fox-jumps-barrel
Generate a one-time customer token:
tunl expect --ttl 120
Terminate a session:
tunl close swift-fox-jumps-barrel
Cancel a pending session:
tunl cancel swift-fox-jumps-barrel
Override relay host for testing:
TUNL_RELAY=10.0.0.5 TUNL_RELAY_PORT=2222 tunl list
linbit-tunl(1), ssh(1), ssh_config(5)
docs/architecture.html -- trust model, session lifecycle,
wire protocol