- packaging/repo.sh: download release assets over HTTPS (no GitHub API),
show nfpm errors, PKGREL for packaging-only revisions, and declare ALSA
as a hard dependency (the release binary links libasound.so.2), with
libasound2t64 preferred so Ubuntu 24.04 doesn't pick the OSS shim.
Verified by clean installs on Debian 12/13, Ubuntu 22.04/24.04,
Fedora 42 and Arch.
- README: Install section (Homebrew cask, apt, dnf/zypper, pacman, zips).
CI hadn't run on macOS or Windows for a while (billing); once it did:
- Windows: the RDP shared-drive code and its symlink test are Unix-only
but were compiled there (and the unused pieces failed clippy). The drive
type, its impls, imports and test are now cfg(unix); refusing a drive
request is a free function used everywhere. Other Windows-only warnings
fixed (serial test import, unused mut, harden_process).
- macOS: the local shell's directory was sampled on the first output
after Enter, which is the shell echoing the command before a cd runs,
so restored tabs opened one directory behind. It's now sampled shortly
after Enter and again once the command has had time to finish.
- packaging/repo.sh publishes the 'nexusterminal' .deb/.rpm/Arch packages
to repo.dixon.cx from the release's own Linux binaries, verified
against SHA256SUMS.
- The desktop entry mentions RDP.
- LICENSE: the GNU Affero General Public License, version 3 (verbatim
from gnu.org).
- SPDX-License-Identifier: AGPL-3.0-only plus a copyright line on every
first-party source file (Rust, shell, Python, PHP); vendored crates keep
their own licenses.
- Cargo manifests carry license/authors/repository; the macOS bundle
copyright and the About page state the license and link the source.
- Release zips (and the macOS app bundle) include LICENSE.
- README: License section.
- Saved credentials are keyed by profile id. A sync peer (or a crafted
import using 'overwrite') could keep an existing profile's id but point
it at another host, user, GitHub API URL, RD Gateway or jump host, and
the saved password/token went there on the next connect without a
prompt. Such a change now forgets that profile's saved credentials
(audited), so the user is asked again.
- --answer / NEXUS_ANSWERS auto-fill every credential prompt and --answer
is visible in the process list; they're now refused unless --screenshot
is given (they exist for screenshot automation).
- Docs: IRC passwords are only sent over TLS/tunnel/loopback; sync's
retarget behaviour; the --answer restriction.
- Release Builds: the build/test matrix ran ~1,400 third-party build
scripts and tests with a contents:write token persisted in .git/config.
It now runs with contents:read and persist-credentials: false; a
separate publish job (no project or dependency code) downloads the
artefacts, writes SHA256SUMS and attaches both to the release. The
release now waits for every platform instead of publishing partially.
- CI/Fuzz checkouts don't persist credentials; cargo-audit is pinned
(0.22.2), fails on yanked crates, and also audits fuzz/Cargo.lock;
fuzz runs set -timeout=10 so slow-input bugs (like the IRC OSC one)
are reported instead of hiding under libFuzzer's 20-minute default.
- updates/index.php: array-valued query parameters no longer raise
TypeErrors/warnings; nosniff header.
- Each migration runs in a transaction. Several rebuild the profiles
table (create, copy, DROP, rename); a crash between the DROP and the
rename left a database that failed to open on every later launch.
- Migration 009 removes the Go-era proxyPassword/totpSecret keys still
sitting in old ssh_config JSON, then VACUUMs and checkpoints the WAL so
the old pages leave the file. secure_delete is on from now on.
- nexus.db (and -wal/-shm) are created and kept 0600.
- Profile ids are limited to [A-Za-z0-9._-]{1,64}: ids name keychain and
vault entries (profile:<id>:<kind>), so a crafted id containing ':'
could shadow or purge another profile's credentials on import/sync.
- Hosts reject control characters (NUL got through) and >253 bytes; jump
hosts and forward destination/bind addresses get the same host check.
- The local clipboard was offered to the RDP server every time the
desktop gained focus, so any server could read the last thing copied
(a password, a token) without a paste. It now goes only on Ctrl/Cmd+V
or Paste Clipboard.
- The server-chosen desktop size is checked before DecodedImage
allocates it (65535x65535 is 16 GiB, and allocation failure aborts the
whole app), at connect and on reactivation.
- Drive reads are clamped to 8 MiB per request (the native backend
allocated the server's u32 length up front).
- Print jobs get unique files created exclusively (same-second jobs used
to truncate each other) and at most 4 may be open at once.
- ircformat: an unterminated OSC made the stripper advance two bytes and
rescan the rest, so one hostile 1 MiB line of ESC ] froze the UI thread
for minutes. It now stops at the first OSC with no terminator (none can
follow), which is linear.
- Server lines are capped at 64 KiB (512 bytes + 8 KiB of tags is the
real ceiling) instead of 1 MiB.
- Spoofed self-JOINs can't grow channel state past 500 rooms (they were
also persisted and auto-joined next time); CAP LS is ignored after
registration and its gathered list is capped.
- PASS, SASL PLAIN and NickServ IDENTIFY are only sent over TLS, an SSH
tunnel or a loopback server; otherwise the Server tab says why.
- TcpConn: a local close no longer races the reader into reporting a
spurious read error (regression from 00f110f, which shut the socket
down before taking the stream).
- is_web_url sliced the string at byte 7/8, panicking (and killing every
session) when a remote host printed an OSC 8 or plain link with a
multi-byte character at that offset and the user hovered it. It now
compares bytes.
- Paste confirmation also triggers on control characters other than tab
(readline's C-o runs a line with no newline), and the preview shows
them escaped.
- Remote clipboard toasts escape control characters and state the line
count, so 'ls\ncurl … | sh' no longer reads as a harmless 'ls'.
- Inline images: dimensions are read from the header first and anything
over the 32 Mpx per-terminal budget is refused before decoding.
- Toasts: repeats extend the visible toast and at most 8 are kept, so
trigger or OSC 52 floods can't stall frames.
- The URL regex is compiled once.
- Enable eframe's 'links' feature: without it every 'open in browser'
(Cmd/Ctrl+click, GitHub repo links, the update link) was a silent
no-op. Adds webbrowser 1.2.4 to the lockfile.
- VNC: ZRLE and Tight inflation is capped at what the rectangle can
legitimately decode to; a ~64 KiB zlib bomb previously expanded to
gigabytes and the OOM killer took the whole app. Large scratch buffers
are released after use. Queued input and a local close are now handled
after every read, so a server that streams without pause can't starve
keystrokes or keep a closed tab's connection alive.
- Telnet: option replies are sent only when the option's state changes
(RFC 854), ending DONT/WONT ping-pong and 1:1 reply amplification; NAWS
doubles 0xFF so 255/511-column windows don't emit a bare IAC.
- Telnet/RAW: writes time out after 10 s and close() shuts the socket via
a second handle without waiting for the stream lock, so a peer that
stops reading can no longer wedge the UI thread or the tab's close.
- Local shells skip non-UTF-8 environment entries instead of panicking.
- known_hosts: the @cert-authority pattern matcher was recursive and
backtracking; a pattern of a few hundred thousand stars overflowed the
connect thread's stack on every handshake and '*a*a*a…b' went
exponential. It is now the iterative two-pointer match, and patterns
over 1 KiB are refused on import and in Trust a CA.
- Agent-forward and remote-forward channels are only honoured on the
final hop: a jump host could previously open auth-agent channels and
reach remote-forward destinations on the client machine.
- Remote forwards with no bind address listen on the server's loopback
(as ssh -R does) instead of every interface.
- Keyboard-interactive gives up after 10 rounds; dials through jump hosts
and run_command have timeouts; the handshake timeout leaves room for
the host-key prompt so a slow reader isn't timed out mid-decision.
- Known hosts are looked up per key type, so an imported known_hosts with
ed25519 and RSA rows no longer produces a false 'key changed' warning.
- SFTP download suggests a sanitised base name in the save dialog.
- sync: timestamps and tombstones in the shared file are clipped to the
local clock, and an edit wins a tie with a deletion, so a forged
future-dated record can no longer override every later local change
(or keep deleting a re-created profile) for as long as the forged date.
- write_private: the temp file is created O_EXCL under a random name, so a
symlink pre-placed in a shared folder can't be followed or overwritten.
- Vault/keychain reads return Zeroizing<String>; has_sync_password is
cached for a few seconds instead of fetching the real value every frame.
- generate_ssh_key creates a missing ~/.ssh as 0700.
- Honest wording: encrypted exports and the sync file carry login-script
values; the audit entry, the Settings hint and the docs now say so.
Bump the workspace to 3.0.1 and bring the user guide up to date: an RDP
chapter, shell integration and inline images, SSH certificates and CAs,
known_hosts import/export, stored credentials and master password change,
profile sync (replacing the Alchementary Link migration chapter), the
Diagnostics tab and log, and refreshed screenshots throughout. The welcome
screen's protocol line now lists RDP.
An opt-in integration test tunnels through the test sshd (direct-tcpip)
to a real RDP server and checks that desktop updates arrive — the
route VNC already used, now verified for RDP (against xrdp).
- Settings → Security lists every profile's stored credentials (kind and
whether it's in the keychain or the vault; the keychain is probed per
kind on a background thread since it can't be listed) with Forget.
- Change Master Password re-encrypts every vault entry under a fresh
salt and key with a new verifier, after checking the current password,
in one atomic write (Vault::change_password; tested that the old
password stops working and every value survives).
- Settings → Diagnostics shows version, platform, data/database/log
paths and sizes, keychain and vault state, counts, sync and launch
settings, with Copy Diagnostics; it never includes credentials.
Openable with --open settings:diagnostics.
Profiles can be kept in step across machines through any folder they
share (cloud drive, network share, git checkout): one file,
nexus-profiles.nexus, in the encrypted-export format under a sync
password kept in the keychain/vault. Each sync merges profile by profile
(newest updated_at wins), propagates deletions as dated tombstones (an
edit after a deletion brings the profile back; tombstones expire after
180 days), validates every incoming profile, writes the file only when
it changed, and never syncs credentials.
Settings → Security → Profile sync picks the folder and sets the
password; syncs run at launch, 3 s after a profile change, every 10
minutes and on Sync Now (automatic runs stay quiet unless something
changed). An end-to-end test drives create, edit and delete between two
data directories sharing a folder.
Registration now opens with CAP LS 302 (servers without IRCv3 ignore it
and register as before). From the offered capabilities (multi-line LS
lists included) the client requests server-time and, when the profile
enables it, sasl; then AUTHENTICATE PLAIN with the account (defaulting
to the nick) and a stored SASL password (irc-sasl-password), base64 in
400-byte chunks, and CAP END on success or failure — failures are shown
on the Server tab and the connection carries on (NickServ still works).
server-time tags stamp events with the server's clock, so replayed
history shows real times.
Tested against the fake IRC server: multi-line LS, REQ/ACK, the PLAIN
exchange, 903, CAP END, and a tagged message's timestamp.
- Host certificates: certificate authorities are trusted per OpenSSH host
pattern (stored as @cert-authority known-hosts rows, matched against
'host' or '[host]:port' as OpenSSH does, with *, ? and !). For a host a
CA covers, the client asks for a certificate; one signed by a matching
CA, of host type, naming the host and in date is trusted without a
first-use prompt, and anything else is refused. Hosts no CA covers keep
the existing key checks. Applies to jump hosts and tunnels too.
- User certificates: <key>-cert.pub next to the configured private key is
used for publickey login (falling back to the plain key).
- Settings → Known Hosts: import ~/.ssh/known_hosts (hashed names and
@revoked lines skipped and counted), export in OpenSSH format, and a
Trust a CA form.
Verified against OpenSSH sshd with ssh-keygen-issued certificates: host
cert accepted without prompting and user-cert login with only
TrustedUserCAKeys; a CA for other hosts falls back to the key check; a
different CA is refused.
The tab layout (tab order and names, pins, the active tab, split trees
with their ratios) is saved whenever it changes — checked once a second,
written only on change, so it survives a crash — and reopened at the
next launch: saved profiles reconnect and local shells start in the
directory they were in (on Linux the live /proc cwd, so it isn't one
command behind). Quick Connect tabs and placeholder panes are skipped.
A General setting turns it off; it never runs when launched with
--connect, --run-local or --open, and screenshot runs never save.
Verified: a local tab left in /usr/share came back there with a
'Restored 1 tab(s)' toast.
Images sent with the iTerm2 inline protocol (imgcat and friends) are
decoded (PNG, JPEG, GIF first frame) and drawn at the cursor, sized from
the width/height arguments in cells, pixels or percent (aspect kept,
never wider than the terminal); the cursor moves below so text
continues after them. They are anchored to the same stable line index
as the shell-integration marks, scroll with the text and are dropped
with the scrollback or on clear.
Remote data, so decoding is bounded (8192 px a side, 256 MiB
allocation), OSC bodies are capped at 16 MiB, inline=0 downloads are
ignored, and each terminal keeps at most 32 Mpx of images, oldest
first. Sixel and kitty graphics are not implemented.
Cells carrying an OSC 8 hyperlink get a dotted underline, and Ctrl/Cmd+
click opens the link. Because a link's text can say anything, hovering
one always shows its real target (and says when it won't be opened).
Both OSC 8 and plain-text URLs now open through the web-only check, so
remote output can't launch file:, ftp: or custom-scheme handlers (plain
text ftp:// links used to be opened).
When the remote shell reports its working directory (OSC 7: fish, or
the bash/zsh hook in the manual), opening the SFTP browser from that
terminal goes there, including when it was already open elsewhere.
Without it the browser still falls back to the exec'd 'pwd' (the login
directory) and then the SFTP home. The File menu entry now goes through
the same request as the toolbar and palette.
Terminals now pick up OSC 133 prompt/command/output/finished marks,
which the VT parser ignores: a scanner (app/src/osc.rs, split-safe
across reads, bodies capped at 4 KiB) finds them in the output stream
and each mark records the cursor line. Lines are stable indexes that
account for lines a full scrollback rotates out, so marks stay on their
text; clearing the scrollback drops them.
With marks present: Ctrl/Cmd+Shift+Up/Down and Previous/Next Prompt in
the context menu jump between prompts, Copy Last Command Output copies
what the last finished command printed, and a green/red bar beside each
prompt shows the exit status (hover for code and duration). OSC 7
working-directory reports are parsed too (used by the next change).
The manual shows bash and zsh snippets; fish 4 and common prompt themes
already send the marks.
An RDP profile can name a Kerberos KDC; CredSSP then authenticates with
Kerberos instead of NTLM, reaching the KDC through a small TCP client
(RFC 4120 length-prefixed framing, 5 s connect / 10 s read timeouts,
replies capped at 1 MiB so a hostile KDC can't force a huge allocation).
UDP KDCs are refused with a clear message. Tested against a fake KDC
(round trip, oversized reply, UDP); not against a real realm.
Smart-card logon is not implemented: it needs PC/SC hardware and a card
to build and test against.
RDP profiles can go through a Remote Desktop Gateway (MS-TSGU over an
HTTPS WebSocket) with its own username and password, stored like other
credentials (rdp-gateway-password). The gateway stream is bridged to a
loopback socket so the blocking RDP client uses it like any transport.
IronRDP's gateway crate (0.0.1) is vendored with fixes: it used
ironrdp-tls, which accepts any certificate, before sending Basic
credentials — the gateway is now verified against the web PKI; its
asserts on gateway-supplied lengths are errors instead of panics; and
the server port is passed through. Checked against a self-signed
openssl s_server: the connection is refused ('invalid peer certificate')
before any request or credential is sent. Not tested against a real
gateway; Basic-auth gateways only (no NTLM).
An optional 'Nexus Terminal' printer (IronRDP's PostScript driver
announce) appears on the remote desktop; each job is written to
'Nexus print <time>.ps' in Downloads, with a toast saying where. Jobs
are capped at 256 MiB and empty jobs leave no file. The shared drive and
the printer now share one RDPDR backend (rdp_devices); the printer
works on every platform, the drive on Linux and macOS.
Tested with a unit test driving create/write/close; xrdp has no printer
redirection, so not exercised against a server.
- Audio: the RDPSND channel with IronRDP's cpal playback backend (PCM;
Opus left out to avoid another native library). On by default for new
RDP profiles. Building now needs the ALSA headers on Linux
(libasound2-dev), added to CI, release and fuzz jobs and the build docs.
- Shared folder (Linux/macOS): one local folder appears on the remote
desktop as a drive. IronRDP's native backend joins server paths onto
the share unchecked, so it is wrapped in ConfinedDrive: create, list
and rename paths with '..' components, NUL bytes, or that resolve
through a symlink outside the share are refused (ACCESS_DENIED);
tests cover the escapes.
Verified against xrdp (with FUSE): the remote read the shared file, and
writes, mkdir and rename landed in the local folder. Not verified:
audio playback (the test container has no sound module), and xrdp lists
the drive as 'ignored' and can't list its directories (an IronRDP/xrdp
naming and listing mismatch, fixed upstream for the name).
A CLIPRDR channel with a text-only backend (CF_UNICODETEXT): text copied
on the remote desktop is fetched and goes through the app's remote-
clipboard preference like VNC and OSC 52; local text is offered to the
remote when the desktop gains focus, on Ctrl+V and with Paste Clipboard
(which no longer types the text). Backend callbacks run inside the
active stage, so they queue requests that the session loop sends.
Verified against xrdp: the remote X clipboard read back the offered
text, and a remote copy arrived through on_clipboard.
rust/fuzz (cargo-fuzz, nightly, its own workspace with the same IronRDP
patches) has targets for the Telnet IAC parser, the IRC parser + state
machine + mIRC formatter, VNC framebuffer updates and decoders,
~/.ssh/config parsing and the update manifest. They reach the private
parsers through hidden entry points behind nexus-core's 'fuzzing'
feature. A nightly workflow runs each for two minutes and uploads any
crashing input. A first 45 s run of each locally found nothing.
- Credential callbacks (SecretFn) now return Zeroizing<String>, as do the
session-only/pending caches they read and the RDP client's password,
so Nexus's own copies are wiped; a plain String copy is made only
where a library API needs one (SSH password auth, IRC, VNC, CredSSP).
- Release builds disable core dumps (RLIMIT_CORE 0) and, on Linux, mark
the process non-dumpable (PR_SET_DUMPABLE 0), which also blocks ptrace
attach and /proc/<pid>/mem reads by other processes of the same user.
Debug builds are unchanged so they stay debuggable. Verified on the
release binary: core limit 0, /proc/<pid>/{environ,mem} root-owned and
unreadable, local shells (including cwd capture) still work.
Closes the remaining parts of SEC-20261012-007 that the app controls;
egui's internal text buffers remain outside its reach.
The app now writes a diagnostic log to nexus.log in the data directory
(0600, rotated at 5 MiB): connection attempts and results, remote
closes, reconnect scheduling, host-key/certificate checks with the
policy applied, and credentials that couldn't be stored (kind only).
Credentials and terminal data are never logged. RUST_LOG sets the level
and also mirrors the log to stderr. The remaining eprintln! diagnostics
in storage and URL handling go through it too.
Every client's reader thread (Telnet/RAW, serial, local shell, IRC, VNC,
RDP) and SSH's channel reader task now run under clients::guarded /
guarded_task. A panic there, such as a decoder bug triggered by a
hostile server, reports 'internal error: …' through the session's
on_error and then closes it, so the tab shows the error (and
auto-reconnect can run) instead of looking connected with nothing
reading. Other sessions were already unaffected since the switch to
parking_lot locks.
Every modal read Enter/Escape globally, so a prompt that popped up over a
dialog, the palette, Quick Connect or an SFTP dialog could be answered
and the modal underneath confirmed or dismissed by the same key press.
All modals except the prompts now read those keys through
chrome::modal_key, which yields while a prompt is on screen.
- Protocol::Rdp with RdpConfig (domain, password mode, certificate
policy, TLS-only fallback, fixed or tab-sized resolution, SOCKS5/HTTP
proxy, SSH tunnel); migration 008 adds rdp_config and drops the
protocol CHECK constraint so new protocols no longer need a rebuild.
- Session manager: connect_rdp dials directly, via the shared proxy
module (proxy password from the secret store) or an SSH tunnel; the
server certificate goes through the known-hosts check (key type
rdp-tls); a tab-sized desktop uses the tab's reported pixel size, and
resizes wait until the server opens Display Control. Rejected NLA
passwords count as auth failures, so a saved one is asked again.
- The VNC viewer becomes DesktopView for both protocols; a small adapter
turns its keysyms and pointer masks into RDP scancodes, Unicode keys,
buttons and wheel events. For RDP, Paste Clipboard types the text.
- Profile editor RDP section, Quick Connect, badges, docs.
Verified against xrdp in the app: logon, desktop, input, and connections
through SOCKS5 and HTTP CONNECT relays. Not verified here: Windows/NLA
servers, the SSH tunnel route, and Display Control resizing (xrdp
0.9.24 never opened the channel).
clients::rdp connects over any caller-dialled TCP stream (direct, proxy
or SSH tunnel), upgrades to TLS and logs on with NLA (CredSSP/NTLM),
optionally falling back to TLS-only logon for servers without NLA such
as xrdp (credentials are passed so they log straight in). The server
certificate is pinned through a caller check on its SHA256 fingerprint
before any credentials are sent. The desktop decodes into the VNC
client's RGBA framebuffer; input (IronRDP's scancode/Unicode/mouse
operations), Display Control resizes and the deactivation-reactivation
sequence run on the session thread.
IronRDP's releases need three vendored patches (vendor/README.md):
picky and sspi with their RustCrypto release-candidate pins relaxed so
they share russh's stable versions (their own suites, 402 tests, pass
against them), and ironrdp-session with two unreleased upstream bitmap
fixes (#1486 stride, #1524 RDP6 orientation) without which xrdp desktops
render sheared.
Verified live against xrdp (scottyhardy/docker-remote-desktop): logon,
full desktop rendering, keyboard and mouse input.
SSH's async SOCKS5 and HTTP CONNECT code moves to clients::proxy as a
blocking dialer (SSH runs it on a blocking thread), so RDP and others can
use the same implementation. The proxy handshake now has a read timeout,
and new tests drive both proxy types against fake proxies, including
SOCKS5 username/password auth and a refused HTTP CONNECT (407).
The Go-generated link.db, profiles.link and rec1.cast fixtures and the
tests that read them are removed. Their coverage is kept by tests on
data this build writes: a fresh database round-trips SSH profiles (env
vars, login scripts, forwards), serial profiles, IRC room state, known
hosts, snippets and preferences, and encrypted exports round-trip and
reject a wrong password. The import dialog no longer offers .link files
and the docs stop mentioning them.
Unlock now always checks the verifier, so an empty vault can never
accept a wrong master password and the 'verify against the first entry'
fallback for Go/older vaults is gone. A vault without a verifier is
refused with a message naming the file to remove (SEC-20261012-016).
The Go vault fixture and its test go with it.
Keychain reads no longer fall back to the AlchementaryLink service and
copy entries forward, and Windows Credential Manager entries use
keyring's own target naming instead of go-keyring's 'service:user'.
Credentials saved by Alchementary Link, or by earlier builds on Windows,
are no longer found and need re-entering.
Startup no longer looks for an AlchementaryLink data directory to copy
into NexusTerminal, and the 'Imported your Alchementary Link data' toast
and the migration docs are gone. Existing Alchementary Link installs are
no longer picked up automatically.