* fix: patch critical security vulnerabilities (GHSA-5fqh, GHSA-ccm8, GHSA-wqfw, GHSA-xmjh) - Remove passwordHash from /users/list API response - Require both password and TOTP code for MFA-critical operations - Restrict tunnel kill commands to tunnelMarker-only matching - Add session ownership middleware for file manager endpoints * fix: allow navigating away from split-view to non-pane tabs Show the normal view container on top of the split view when the active tab is not assigned to any pane, so users can switch to dashboard or other tabs while split mode is active. Closes #739 * fix: add inline quick-action buttons on host name row Show Terminal, Files, RDP, and VNC shortcut icons on the host name row on hover, so users can launch connections with a single click without expanding the full action tray. Closes #736 * fix: restore SSH keepalive interval to 30s to prevent random disconnects Revert keepalive defaults from 60s/5 to 30s/3 across terminal, tunnel, and server-stats SSH connections. The 60s interval introduced in 2.3.0 causes firewalls and NAT devices to drop idle connections before the next keepalive probe. Closes #733 * fix: apply guacamole-lite protocol patch in Docker builds The Dockerfile uses --ignore-scripts which skips the postinstall hook that patches guacamole-lite for guacd 1.6.0 protocol VERSION_1_5_0. Without this patch, the timezone handshake instruction is not sent for protocol versions above 1.1.0, causing VNC connections to fail immediately on connect. Closes #734 * fix: show correct icons for network interface types Detect interface type from name pattern and show appropriate icons: WiFi for wlan/wl*, Ethernet (Cable) for eth/en*, Container for docker/bridge/virtual, generic Network for others. Closes #720 * fix: resolve sudo password for shared host users The password endpoint required hosts.userId to match the requesting user, which fails for shared hosts. Now falls back to decrypting with the owner's key when the requesting user doesn't own the host. Closes #717 * fix: use jump hosts for online status check and metrics collection Status polling now pings the first jump host instead of the unreachable target when jump hosts are configured. The /metrics/start endpoint now tunnels through the jump host chain to reach the target host. Closes #716 * fix: broaden sudo prompt detection for newer distros Add patterns for 'password for <user>:' and bare 'Password:' prompts in addition to the existing [sudo] and sudo: patterns. Covers Ubuntu 26.04 and other distros that use different sudo prompt formats. Closes #718 * fix: recalculate terminal layout after web fonts load xterm.js measures character widths at open() time. If custom fonts haven't loaded yet, measurements use the fallback font and spacing becomes incorrect. Now refresh and re-fit the terminal once document.fonts.ready resolves. Closes #710 * fix: improve terminal cwd detection and initial directory command Remove '&& pwd' from initial directory command — the shell prompt shows the new directory naturally. Fixes PowerShell 5.1 which doesn't support '&&' as a statement separator. Prepend Ctrl+U to get_cwd command to clear any pending input before injecting the cwd probe, reducing interference with foreground programs. Closes #713, #714 * fix: decode base64 file content as UTF-8 in file manager Replace bare atob() with TextDecoder('utf-8') for base64 content decoding. atob() only handles Latin-1, so multi-byte UTF-8 characters like 'é' were decoded as 'é'. Closes #719 * fix: normalize lazy import default exports for iOS compatibility Wrap all lazy() imports with explicit .then(m => ({ default: m.default })) to ensure consistent module resolution across platforms. iOS Safari/WebView may handle bare lazy(() => import(...)) differently, returning the module object instead of extracting the default export. Closes #721 * fix: prevent RDP display from snapping back after container resize Remove immediate rescaleDisplay() from ResizeObserver callback. The display.onresize event already triggers rescaling when the RDP server responds with the new resolution. Calling rescaleDisplay before the server responds uses stale display dimensions, causing the bottom of the screen to be truncated. Closes #725 * fix: add portal Desktop DBus permission for Flatpak URL opening Flatpak sandbox blocks window.open() without the portal permission, causing terminal link clicks to open about:blank. Add talk-name for org.freedesktop.portal.Desktop to enable xdg-desktop-portal URL handling. Closes #704 * chore: remove unused code and fix PR checks (#851) * chore: remove unused frontend code * chore: prune unused theme exports * ci: fix pr check failures * chore: reduce lint warnings * feat(oidc): expose admin_group via OIDC_ADMIN_GROUP env var (#828) The admin-group OIDC sync added in 2.3.0 (#782) reads `config.admin_group` to sync the user's admin flag from OIDC group membership on each login. That field is only populated when the OIDC config is stored in the in-app DB — `getOIDCConfigFromEnv()` does not expose it, so deployments using the env-var config path (declarative IaC: Helm/Compose/Puppet) cannot enable the feature without abandoning env vars and pasting the client_secret into the admin UI. Add `admin_group: process.env.OIDC_ADMIN_GROUP || ""` to the env-config return type and object. Backward compatible: when unset, the existing `if (config.admin_group)` guard at users.ts:1336 keeps the sync block skipped, matching today's behavior. * chore: reduce explicit-any warnings * chore: reduce more explicit-any warnings * chore: reduce lint warnings * chore: silence intentional hook dependency warnings * chore: clean dependency tooling * chore: narrow frontend tsconfig scope * chore: reduce type assertion debt * refactor: split host manager components * refactor: split host editor sections * refactor: split api client modules * refactor: split more api clients * refactor: split user settings api clients * refactor: split tab and history api clients * refactor: split tunnel api clients * refactor: split server stats api client * refactor: split file manager data api * refactor: split ssh file operations api * refactor: split host editor general tab * refactor: split host editor guacamole tabs * refactor: split ssh host management api * refactor: split admin general settings sections * refactor: split admin database section * refactor: split admin management sections * refactor: split admin keys and dialogs * refactor: split system status api clients * refactor: split user route helpers * refactor: split host route helpers * refactor: split file manager ssh helpers * refactor: split file manager session helpers * refactor: split file manager listing routes * refactor: split host opkssh routes * refactor: split file manager content routes * refactor: split user api key routes * refactor: split host folder routes * refactor: split user settings routes * refactor: split user totp routes * refactor: split host file manager bookmark routes * refactor: split file manager operation routes * refactor: split server stats settings routes * refactor: split user session routes * refactor: split host command history routes * refactor: split server stats viewer routes * refactor: split docker container routes * refactor: split user oidc account routes * refactor: split host autostart routes * refactor: split host internal routes * refactor: split host network routes * refactor: split user password reset routes * refactor: split user admin routes * refactor: split user data access routes * refactor: split credential key routes * refactor: split credential deploy routes * refactor: split host bulk routes * refactor: split server stats connection helpers * refactor: split tunnel helpers * refactor: split file manager action routes * refactor: split terminal auth helpers * refactor: split terminal jump host helpers * refactor: split tunnel relay helpers * refactor: split tunnel socks relay helpers * refactor: split tunnel c2s relay handlers * refactor: split server stats session helpers * refactor: split terminal presentation helpers * refactor: split file manager presentation helpers * refactor: split file manager toolbar * fix(guacamole-lite): send name instruction for protocol >= 1.3.0 The Guacamole protocol added the `name` handshake instruction in 1.3.0 (an optional human-readable identifier for the joining user). guacd 1.6.0 began requiring it during the VNC handshake even when negotiating older protocol versions, causing connections to silently drop right after the "User joined" log line with no client-visible error. This patch extends scripts/patch-guacamole-lite.cjs with a third idempotent string-replacement that injects the `name` instruction send when guacamole-lite has negotiated protocol VERSION_1_3_0 or VERSION_1_5_0. Verified end-to-end: guacd debug logs now show `Processing instruction: name` and `Client is using protocol version "VERSION_1_5_0"` (previously stuck at VERSION_1_1_0). VNC session connects successfully against guacd 1.5.5 / macOS Tahoe target. Related: Termix-SSH/Support#567, #734 * fix: resolve recent support bugs * fix(admin): wire up OIDC-to-password link dialog submit + visibility The admin user-management UI already shipped a link icon and a "Link Account" dialog, but two things blocked the flow: 1. The submit button had no onClick handler and the username input was uncontrolled (no value/onChange). Clicking "Link Accounts" was a no-op — no network request, no console error, no toast. 2. The link icon's visibility condition was `user.isOidc && !user.passwordHash`, which hid the button on OIDC users that had been auto-provisioned with a passwordHash. Termix's OIDC provisioning sets a passwordHash by default, so the button was hidden on virtually every OIDC-provisioned user. This change: - Adds `linkOIDCToPasswordAccount` to the imports from `@/main-axios`. - Adds two pieces of dialog state: `linkAccountTargetUsername` and `linkAccountSubmitting`. - Makes the dialog's Input field a controlled component. - Wires the submit Button's onClick to call `linkOIDCToPasswordAccount`, emit success/error toasts, refresh the local user list, and close the dialog. - Loosens the visibility condition to `user.isOidc` (the backend handler already enforces all integrity checks). - Adds `linkAccountSuccess`, `linkAccountFailed`, and `linkAccountInProgress` translation keys to `en.json`. Verified locally: full Docker build via docker/Dockerfile passes; `tsc --noEmit` is clean; `prettier --check .` is clean; ESLint produces the same warning count as upstream (16 pre-existing `any`-type warnings, 0 errors). * fix: support native oidc callbacks (#856) * docs: add cloudflare tunnel guidance (#857) * fix: sync appearance preferences (#858) * fix: pass through terminal tab completion (#859) * fix: resolve terminal jump hosts server-side (#860) * fix(electron): auto-allow SSL certificates for private network hosts (#861) Add private network IP detection (RFC 1918, link-local, loopback, IPv6 ULA) to the Electron certificate-error handler so that connections to local/private servers like 192.168.x.x bypass SSL validation automatically. Also add an explicit "Allow invalid certificate" toggle in the server config UI for public HTTPS servers with self-signed certs. * fix: restore host password copy actions (#862) * feat: support single-host direct tunnels (ssh -L style) (#863) Add direct tunnel mode that uses a single SSH host for port forwarding, matching the behavior of ssh -L / ssh -R / ssh -D without requiring a second endpoint host in the Termix database. The Termix server creates a local TCP listener and forwards through the SSH channel directly. * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * fix: backend build errors (Type) * fix: mobile auth failing to login with webview * fix: mobile app geting incorrectly sent auth token * feat: commit existing frontend/backend e2e/unit tests (skipped tests containing private info like OIDC and real server testing) * feat: host-to-host file transfer via server relay * feat: removed host management from command palette, fixed command palette opening wrong protocol, export/import failing for ssh key hosts, docker ssh2 native crypto not compiled, persisted terminal tabs attempt SSh on RDP hosts after migration, improved layout for click to expand hosts, show ip/username without having to hover over hosts * fix: credentials not indexing into host manager until refresh * feat: update credentials lists to match hosts list UI/UX * feat: add rename folder UI * feat: improve transfer to host UI/UX * chore: increment ver * feat: improve transfer to host UI * feat: implement initial auto release system --------- Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> Co-authored-by: nicodarge <43711429+nicodarge@users.noreply.github.com> Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com> Co-authored-by: luc <luc_cook@hotmail.co.uk>
17 KiB
Host-to-host file transfer
This document describes the host-to-host copy/move feature. It is intended for operators and contributors. Direct host-to-host routing via SSH tunnels is not implemented; that is documented under Future work.
Overview
Host-to-host transfer copies or moves files from one SSH host to another through the Termix server as a relay. The UI lives in File Manager: right-click files or folders and choose Copy to host… or Move to host….
Compared to copying via your laptop (scp -3 or ssh one 'cat …' | ssh two 'cat …'), Termix keeps the job on the server so transfers continue if you close the browser, and you get integrated progress, cancel, retry, and cleanup.
flowchart LR
Browser[Browser_UI]
Termix[Termix_server]
Src[Source_host]
Dst[Destination_host]
Browser -->|start_transfer_API| Termix
Termix -->|dedicated_SSH_xfer_src| Src
Termix -->|dedicated_SSH_xfer_dst| Dst
Termix -->|SFTP_read_then_write| Termix
Data for remote-to-remote paths always passes through the Termix process (pipelined SFTP buffers, or a tar archive stream). There is no source→destination SSH tunnel for file bytes today.
Prerequisites
- Two hosts with File Manager enabled, saved in Host Manager.
- Both hosts reachable from the Termix server on SSH (each host’s jump hosts and SOCKS5 proxy chain apply to the Termix→host connection, not host→host).
- File Manager open on the source host (browse session connected). The destination host should show Ready in the transfer dialog; if it shows authentication required, open File Manager on that host once first.
- For multi-file or folder transfers, pick a destination directory (not a file path).
The dialog shows a reminder: “Both hosts must be reachable from the Termix server. Direct host-to-host routing is not supported.”
Using the UI
Start a transfer
- Open File Manager on the source host.
- Select one or more files or folders.
- Right-click → Copy to host… or Move to host… (sidebar tree supports the same context menu).
- In the dialog:
- Choose destination host (other connected file-manager hosts).
- Set destination path (type a path or use Browse destination folders).
- Optionally pick a recent destination (collapsed by default).
- For multi-item transfers, choose transfer method (Auto / Tar archive / Per-file SFTP); a preview explains what Auto will pick.
- Pin folders with Add to shortcuts (per-destination host; uses normal file-manager shortcuts, not a separate favourites list).
- Confirm Copy or Move.
During transfer
- A progress toast shows phase (compressing, transferring, extracting), bytes, speed, and Cancel.
- Transfer monitor (desktop, when authenticated) picks up active transfers started in another tab or window.
- Large single files use segmented SFTP (256 MiB segments) with 2 parallel lanes by default (separate dedicated SSH session pairs per lane). The toast can show total speed and lane count when multiple lanes are active.
After completion or failure
| Outcome | What happens |
|---|---|
| Success | Toast success; destination path saved to recent destinations for that source host; file manager can refresh. |
| Partial | Some paths failed; toast lists failed paths; source may be kept on move. |
| Error | Toast with Retry when partial data on destination allows resume. |
| Cancelled | Toast with optional Clean up destination to remove partial files. |
Move deletes source files only after a successful full transfer (same as copy, then source delete). Cancelled or partial moves may leave files on both sides.
Metrics
On success, expanded timing details can include: prepare destination, compress (tar), per-hop throughput (source→server, server→dest), extract, source delete, total duration.
Transfer methods
| Method | When used | Behavior |
|---|---|---|
| Stream (single file) | Exactly one file, copy or move | Pipelined SFTP read on source → write on destination; segmented above 32 MiB; parallel lanes for throughput. |
| Tar archive | Multi-file/folder when Auto or user selects Tar, and both sides are Unix with tar |
tar -czf on source → one archive streamed through Termix → tar -xzf on destination. |
| Per-file SFTP | Windows involved, tar unavailable, or Auto chooses it | Each file copied sequentially over SFTP through Termix. |
Auto heuristics (see src/backend/ssh/transfer-routing.ts) consider file count, total size, largest file, and compressibility (e.g. many small files → tar; large incompressible sets → per-file SFTP).
Method preview is locked for the current source path set until you change Auto/Tar/Per-file preference, so changing only the destination host does not re-scan the source.
Architecture (implementation)
| Component | Role |
|---|---|
src/backend/ssh/host-transfer.ts |
Transfer engine: sessions, SFTP pipeline, tar path, retry, cancel, progress. |
src/backend/ssh/file-manager.ts |
HTTP routes, openDedicatedTransferSession, jump/SOCKS connect. |
src/backend/ssh/transfer-routing.ts |
Tar vs per-file SFTP selection. |
src/backend/ssh/transfer-paths.ts |
Path normalization (Unix/Windows). |
src/ui/.../TransferToHostDialog.tsx |
Transfer dialog. |
src/ui/.../transferProgressMonitor.tsx |
Toasts, cancel, retry, cleanup. |
src/ui/.../TransferMonitor.tsx |
Global active-transfer polling. |
Sessions: Browse sessions identify hosts. Each transfer opens dedicated SSH sessions (xfer:{transferId}:src / :dst) so browsing and transfers do not share channels. Parallel lanes add xfer:{transferId}:src:pN / :dst:pN.
Special case: If the destination host is the same machine as Termix (local SSH endpoint), writes use the local filesystem via fastGet instead of dest SFTP; data still originates from the remote source through Termix.
Persistence: Recent destinations are stored in transfer_recent (per user, per source host). Folder shortcuts use file_manager_shortcuts on the destination host.
HTTP API (file manager service)
| Endpoint | Purpose |
|---|---|
POST /ssh/file_manager/ssh/transferMethodPreview |
Scan source; return resolved tar vs item_sftp and reason. |
POST /ssh/file_manager/ssh/transferToHost |
Start transfer; body includes sourceSessionId, destSessionId, sourcePaths, destPath, move, methodPreference, optional parallelSegmentCount (1–8, default 2). |
GET /ssh/file_manager/ssh/transferStatus/:transferId |
Poll progress. |
GET /ssh/file_manager/ssh/activeTransfers |
List running transfers for user. |
POST /ssh/file_manager/ssh/transferCancel/:transferId |
Request cancel. |
POST /ssh/file_manager/ssh/transferCleanup/:transferId |
Remove partial destination artifacts after cancel/failure. |
POST /ssh/file_manager/ssh/transferRetry/:transferId |
Retry with same snapshot (resume when possible). |
Database (main API): GET/POST /host/transfer/recent for recent destinations.
Reliability features
- Resume: Destination file size is probed; SFTP write opens with resume when a partial file exists (per segment on large files).
- Retry: Reconnects dedicated sessions; segment-level and full-copy retries with backoff; fresh SSH pairs after repeated failures (lane reset).
- Stall detection: ~45 s without progress on a segment; hung transfer/reconnect probing on status polls.
- Cancel: Aborts in-flight SFTP; user can clean up destination paths that were created or partially written.
- Overlap guard: Refuses transfer when source and destination paths overlap in a destructive way.
Limitations
- No direct host-to-host data path — Termix must reach both hosts independently (with each host’s jump/proxy settings).
- Not the same as S2S SSH tunnels — Tunnels in Host Manager forward TCP ports; they do not carry file-manager transfers today.
- Throughput — Remote-to-remote speed is bounded by Termix CPU/RAM and min(Termix↔source, Termix↔dest) links; very large files on a small Termix box may be slower than
scp -3from a powerful desktop. - Parallel lanes — Writes are out of order on disk; fine for copy, not for playing media from a partially written file. Default is 2 lanes; UI may not expose lane count (API default applies).
- Tar — Requires
taron both Unix hosts; temporary archive under/tmpon source during transfer. - Windows — Tar path disabled; per-file SFTP only for Windows endpoints.
- Jump hosts on S2S tunnels — Server-to-server tunnel connect does not use jump hosts; only transfer/browse SSH does. A host reachable only via jump may work for transfer but not as an S2S tunnel source until tunnel code is aligned.
Troubleshooting
| Symptom | Things to check |
|---|---|
| No destination hosts listed | Open File Manager on another host; ensure host has File Manager enabled. |
| Destination “Authentication required” | Connect File Manager on that host once in this session. |
| Transfer fails immediately | SSH from Termix to both hosts (firewall, jump host, SOCKS5, credentials). |
| Slow speed | Termix link to slower side; try off-peak; for single huge files, parallel lanes help if CPU/network allow. |
| Stuck progress | Wait for stall/reconnect; cancel and retry; check server logs for host_transfer / transfer_ssh_*. |
| Partial files after cancel | Use Clean up destination in the toast. |
| 28 GB / 25 GB style progress | Usually parallel progress accounting; status polls use destination size probes. |
Comparison to manual scp between remotes
See Unix & Linux: scp from one remote server to another. Naive scp one:file two:file runs from the first host and fails unless that host can SSH to the second. scp -3 relays through your workstation. Termix relay is analogous to scp -3 through the Termix server, with richer lifecycle management, not analogous to direct ssh source 'scp … dest'.
Future work: direct routing via tunnels
The following are planned / discussed enhancements, not shipped in the current build. They build on existing S2S SSH tunnels (src/backend/ssh/tunnel.ts), which already connect source → endpoint using forwardOut from the source host to the endpoint’s SSH port.
Why tunnels matter
Many homelabs have a destination that is only reachable from another host (e.g. NAS on LAN behind a Pi), while Termix runs elsewhere. Today that destination cannot receive a dedicated xfer:dst session from Termix even if an S2S tunnel from Pi → NAS is configured and working.
Possible routes (future)
| Route | Termix SSH legs | Data path | Benefit vs today |
|---|---|---|---|
| Relay (current) | 2 | Termix buffers SFTP | Works when both hosts reachable from Termix. |
| Tunnel-bridged SFTP | 1 (+ bridge) | Still through Termix memory | Dest reached via source forwardOut; fixes reachability; reuses most of current engine. |
| Direct remote (rsync/scp on source) | 1 (control) | Source → dest bytes | Best throughput; Termix orchestrates rsync/scp on source when forward + tools allow. |
Integration ideas (not implemented)
transfer-bridgemodule — SharedforwardOutprobe andconnectDestThroughSource(extracted from tunnel code); lookup matchingtunnel_connectionson the source host record.- Route resolver — Auto-select relay vs bridged vs direct; expose route in method preview (“via Termix” vs “direct host-to-host”).
- Reuse active S2S tunnel — If Host Manager tunnel source→dest is already connected, reuse
endpointClientinstead of opening a second bridge. - Jump hosts on S2S tunnel source — Align tunnel connect with file-manager jump chains so tunnel and transfer eligibility match.
- Fallback — Always fall back to current relay when probe or remote
rsyncfails (Windows, missing tools, forwarding denied).
What would be preserved
Cancel, partial cleanup, retry/resume (rsync --partial on direct path; existing SFTP segment resume on relay/bridged), dedicated sessions, transfer monitor, recent destinations, folder shortcuts, and tar/per-file method selection (with direct-path variants for multi-file).
References
- Internal plan:
.cursor/plans/host-to-host_direct_transfer_*.plan.md(if present in your checkout). - Tunnel implementation:
src/backend/ssh/tunnel.ts—connectEndpointThroughSource,establishManagedS2STunnel. - Transfer engine:
src/backend/ssh/host-transfer.ts.