Skip to content

Architecture

Muxus is a pnpm workspace of four TypeScript/ESM packages plus a test package: a Fastify server on loopback, a React client, and an Electron shell that embeds both.

flowchart LR
  subgraph Desktop["Electron shell"]
    Client["client/: React 19 + MUI<br/>pane canvas · keymap · xterm.js"]
    Server["server/: Fastify on 127.0.0.1<br/>ssh_config engine · leases · SFTP · forwards"]
  end
  Client <-->|"REST + /ws/terminal"| Server
  Server -->|ssh2| Hosts["Your hosts"]
  Server -->|node-pty| Local["Local shells"]
  Server -->|serialport| Serial["COM / TTY"]
  Server -->|"RDP (TLS) · VNC"| Desktops["Remote desktops"]
  Server -->|"NETCONF (SSH) · gNMI, gNOI, gNSI (gRPC)"| Devices["Network devices"]
  Server --> DB[("SQLite<br/>metadata · workspaces")]
  Server --> History[("Session history<br/>zstd segments + FTS5")]
  Server -.reads.-> Config[["~/.ssh/config<br/>known_hosts"]]

Packages

Package What it is
shared/ REST DTOs and the zod-validated WebSocket protocol. On /ws/terminal, binary frames are bytes and text frames are control messages, with no framing layered on top of either.
server/ Fastify bound to 127.0.0.1 with a per-run bearer token; a versioned SQLite database; the line-preserving ssh_config parser/resolver/editor; leased ssh2 transports with ProxyJump and OpenSSH-order authentication; known_hosts verification; node-pty local shells; Telnet negotiation; cross-platform serialport access; SFTP routes and the forward manager; the session-history worker; the RDCleanPath proxy for RDP and the VNC relay.
client/ React 19 + MUI. A flat pane canvas over a split tree, so layout changes never remount a session; one declarative keymap dispatched ahead of the terminal; xterm.js with the Image Addon and native kitty keyboard support; IronRDP (WebAssembly, vendored in client/src/vendor/ironrdp) and noVNC for remote desktops, loaded with the first desktop tab.
electron/ The hardened desktop shell: embeds the server in-process, bridges bootstrap credentials through an isolated preload, blocks unexpected navigation.
tests/ vitest units for the security and auth boundaries, persistence and migrations, connection leases, workspace and pane behaviour, SFTP overwrite policy, paste safety and the terminal protocols.

Design decisions

The pane canvas is flat. Panes, tab contents and dividers are siblings in one absolutely positioned layer, each keyed by its own id. Reshaping the tree only moves boxes, so terminals are never unmounted, and splitting, closing and moving tabs cannot disturb a running shell, its scrollback, or its SSH channel.

Connections are leased. One SSH transport serves every consumer that requests the same host: extra terminals, the SFTP panel, the remote editor, ad-hoc forwards. A saved tunnel takes its own lease and survives the closing of every terminal.

The config is a document, not a model. The parser records which lines in which file each block owns, so an edit rewrites those lines and nothing else. Comments, ordering and unmodelled options are preserved. Writes are atomic with a .muxus.bak.

Session history is not in the application database. A dedicated worker writes framed raw events into rotated zstd segments and batches normalized transcript chunks into a separate FTS5 database, with its own quota, eviction and recovery. The recorder never blocks a terminal.

Graphics stream rather than buffer. Kitty APC sequences flow from xterm.js 6.1 into the Image Addon: chunked payloads are parsed incrementally, base64 decoding runs through WebAssembly, and images render as terminal-buffer-aware canvas layers. Muxus adds no second parser and no per-chunk scheduling queue.

Keys are one table. Every command across panes, tabs, terminal and application is a keymap entry with default chords, dispatched before the terminal sees the event. A command that is not applicable returns "not handled", and the key falls through to the shell.

Data flow of one session

  1. The client asks the server to resolve the target through ssh_config.
  2. The server dials it, hop by hop for a jump chain, verifying host keys and running the OpenSSH authentication order, prompting through the socket when input is required.
  3. A shell channel opens; binary frames carry bytes in both directions.
  4. Resize, logging control and status arrive as text frames validated by the shared zod schema.
  5. The lease keeps the transport alive for the file browser, the editor and any forwards until the last consumer releases it.

Remote desktops

RDP and VNC tabs render in the browser: IronRDP compiled to WebAssembly draws RDP into a canvas, and noVNC handles VNC. Neither can open TCP sockets, so the server relays for them. Each tab keeps a control socket (/ws/desktop) for everything that needs the user (the SSH gateway's prompts, credentials, certificate trust) and receives a single-use ticket for the stream socket that carries the picture.

sequenceDiagram
  participant C as Desktop tab
  participant S as Muxus server
  participant R as RDP server
  C->>S: /ws/desktop connect (host profile)
  S-->>C: auth-prompt (password, vault)
  S-->>C: ready (ticket, logon, the profile it dialed)
  C->>S: /ws/desktop/rdp RDCleanPath request (ticket, X.224)
  S->>R: TCP (or SSH channel), X.224 request
  R-->>S: X.224 confirm
  S->>R: TLS handshake
  S-->>C: certificate prompt when untrusted (control socket)
  S-->>C: RDCleanPath response (X.224 confirm, certificate chain)
  C->>R: CredSSP, then the RDP session, relayed through the TLS connection

ready carries the profile the server dialed, so a saved host's current settings (not the tab's snapshot) decide the destination, clipboard sharing and VNC options.

VNC is simpler: noVNC opens /ws/desktop/vnc offering the ticket as a WebSocket subprotocol, and the server relays raw RFB bytes to the VNC server or through an SSH channel. An RSA-AES server's key is only visible to noVNC, so the tab reports it on the control socket (server-key) and continues once the server has checked it against the pinned one or the user has trusted it. The IronRDP client is built from a pinned upstream commit by client/scripts/build-ironrdp.mjs, because the published package lags fixes that xrdp and Windows servers depend on.

NETCONF and gNMI

Each NETCONF or gNMI tab keeps one control socket (/ws/management). The backend speaks the device protocol; the renderer sends requests with an id and gets back decoded, JSON-friendly results, errors and subscription batches under that id.

  • NETCONF opens the netconf SSH subsystem on a leased transport, so ssh_config, jump chains, host keys and the password vault apply as for a terminal. The client handles both RFC 6242 framings, correlates replies by message-id, and passes <rpc-error>s through for the renderer to show.
  • gNMI is gRPC over HTTP/2. Muxus does not use a gRPC library: the backend owns the connection (TCP or an SSH gateway channel), performs TLS itself so certificates can be pinned like RDP's, and runs node:http2 on top. gRPC framing and the gNMI messages are a few hundred lines, with a protobuf codec driven by descriptors transcribed from gnmi.proto, keeping 64-bit counters exact. Subscription updates are batched every 100 ms before they reach the renderer.
  • gNOI and gNSI share the gNMI connection. The backend holds a fixed table of the RPCs the tools use (method path, call kind, message descriptors) and refuses any other method; the renderer sends and receives those messages as JSON (enums by name, bytes as base64, exact 64-bit values). Streaming calls are batched like subscriptions. File transfers are handled in the backend, which chunks uploads with backpressure and checks both directions against the gNOI hash. gRPC reflection, when the device offers it, tells the workbench which services exist.

The workbench (drafts, results, history, the explorer tree) lives in the renderer per tab, so a reconnect loses nothing. Device data from both protocols is merged into one tree; list keys are learned from keyed paths the device sends and otherwise guessed from the entries.

Build

pnpm build      # shared → server → client → electron
pnpm test       # vitest
pnpm lint       # oxlint
pnpm typecheck

CI runs typecheck, lint and tests, then builds unpacked desktop packages on Linux, macOS and Windows.