Public Access
Licence is MIT, set solution-wide rather than only on the packable project: DodoSSH.Contracts is published so clients can build against it, and a package with no licence expression is one a corporate policy scanner rejects outright. The SSH.NET spike is the M1 client gate and it passes. SSH.NET 2025.1.0 exposes ShellStream.ChangeWindowSize, but a method existing is not the remote observing it, so the tests read `stty size` back from a real sshd after resizing rather than asserting the call did not throw. Repeated resizes each take effect too, which matters because dragging a window edge produces a stream of them. The IChannelSession fallback is not needed. Also verified against a real sshd: password and public-key auth, that the host key arrives as a raw blob we can fingerprint ourselves rather than reading SSH.NET's MD5 property, and that refusing the key via CanTrust actually aborts the connection -- without which the TOFU dialog would be decoration. Kept as a permanent suite, not deleted after the spike. An upgrade that silently stopped sending the request would present as wrapped output only after a resize, which is easy to misattribute to the terminal emulator. Two bugs in the test itself, both worth naming because either would have been read as "resize does not work": - A PTY emits CRLF, and the anchored regex rejected the CR. The output visibly contained `24 80` while the match failed. - Each read can begin with output still buffered from the previous command, including its size line. Taking the first match would have reported the pre-resize size. platform-flags.md now records window-change as resolved rather than unverified -- a stale flag is worse than none -- plus the three real SSH.NET limits found on the way: ShellStream does not override ReadAsync so every idle session parks a pool thread, one connection cannot serve both SshClient and SftpClient, and agent forwarding needs an upstream change.
111 lines
7.0 KiB
Markdown
111 lines
7.0 KiB
Markdown
# Platform flags
|
|
|
|
Things known or suspected to behave differently outside Windows, plus deployment gotchas that
|
|
have already cost time once. **Development and testing are currently Windows-only**, so anything
|
|
here marked *unverified* has not run on the platform in question and must not be assumed to work.
|
|
|
|
Each entry says what the risk is, why it matters, and what to do about it. Delete an entry when it
|
|
has been verified or made moot — not when it merely stops being convenient.
|
|
|
|
## Cryptography
|
|
|
|
**`ChaCha20Poly1305.IsSupported` is false on macOS**, and on Windows builds before 10.0.20142.
|
|
This is why the client uses NSec (libsodium) rather than the BCL for content encryption; see
|
|
docs/crypto.md §1. *Already mitigated* — but if a BCL AEAD path is ever added as a fallback it
|
|
**must** gate on `IsSupported` rather than assuming availability, or the client will fail to open
|
|
any vault on macOS.
|
|
|
|
**Argon2id timings are measured on one Windows machine only.** 256 MiB with t=4 took 323 ms here.
|
|
The floor and ceiling in `EnrollmentLimits` were chosen against that number. *Unverified
|
|
elsewhere:* recalibrate on the slowest target platform before recommending a default profile,
|
|
because a cost that is comfortable on a desktop can make unlock unusable on a low-power laptop —
|
|
and the parameters are stored per user at enrollment, so a bad default is a per-user migration.
|
|
|
|
**libsodium ships native binaries per RID.** This complicates single-file and AOT publishing, and
|
|
on macOS every native library (`libsodium`, `libSkiaSharp`, `libHarfBuzzSharp`, `libe_sqlite3`)
|
|
must be signed **individually** with `--options runtime --timestamp` before the bundle is signed,
|
|
or notarization fails with an error that does not name the offending file.
|
|
|
|
## Desktop client
|
|
|
|
**The Avalonia WebView on Linux is unproven, and is the single largest risk in the plan.** The
|
|
official control uses WPE WebKit (`libwpewebkit-2.0`); the community `NativeWebView` uses WebKitGTK
|
|
(`libwebkit2gtk-4.1`), which is far more widely installed. Avalonia's own documentation
|
|
contradicts itself on whether offscreen rendering works there. *Unverified:* the spike must cover
|
|
Ubuntu on both Wayland and X11, Fedora KDE, and macOS 15. This is why the terminal sits behind
|
|
`ITerminalHost` — that abstraction is what preserves the option to swap backends, and it should
|
|
not be collapsed away for convenience.
|
|
|
|
**SSH.NET's `window-change` is verified working** as of 2025.1.0 — resolved, not a flag.
|
|
`ShellStream.ChangeWindowSize(columns, rows, width, height)` exists and the remote genuinely
|
|
observes it: `PtyAndResizeSpikeTests` reads `stty size` back from a real sshd after resizing, and
|
|
repeated resizes each take effect. The `IChannelSession` fallback is not needed. That suite stays
|
|
in place as a regression guard, because an upgrade that silently stopped sending the request would
|
|
present as wrapped output only after a resize — easy to misattribute to the terminal emulator.
|
|
|
|
**`ShellStream` does not override `ReadAsync`.** The base `Stream` implementation therefore runs
|
|
the blocking `Read` on a thread-pool thread, so every open session parks one thread for as long as
|
|
it is idle. Fine for the handful of tabs M1 targets; revisit before advertising many concurrent
|
|
sessions, since the fix is either an upstream change or driving `IChannelSession` directly.
|
|
|
|
**SSH.NET cannot share one connection between `SshClient` and `SftpClient`.** A shell plus SFTP to
|
|
the same host means two TCP connections, two authentications and — later — two relay sockets.
|
|
Connect SFTP lazily and reuse the cached decrypted credential so the user is not prompted twice.
|
|
|
|
**Agent forwarding is de-scoped from v1.** It needs an upstream SSH.NET change. A vault-backed
|
|
agent of our own plus ProxyJump covers the real use cases.
|
|
|
|
**The SSH suite pulls `linuxserver/openssh-server` from Docker Hub**, which is rate-limited for
|
|
unauthenticated pulls. If CI starts failing on image pulls rather than on tests, that is why.
|
|
|
|
**MSIX packaging is ruled out, not merely deprioritised.** A packaged app runs WebView2 in an
|
|
AppContainer where loopback connections are blocked without a `CheckNetIsolation` exemption. The
|
|
terminal data plane *is* a loopback WebSocket, so MSIX would break the product outright. Velopack
|
|
for Windows/macOS/AppImage; Flatpak and deb/rpm defer updates to the package manager.
|
|
|
|
**Linux ships AppImage and Flatpak first**, specifically so the WebKit runtime is bundled rather
|
|
than assumed present on the user's machine.
|
|
|
|
## Build and CI
|
|
|
|
**Integration tests need a Docker daemon** (Testcontainers). They run on `ubuntu-latest` in CI.
|
|
macOS runners have no Docker daemon, and the Windows CI job is deliberately build-only. So
|
|
anything proved by an integration test is proved on Linux only — which is the right place for
|
|
server code, and no coverage at all for client platform behaviour.
|
|
|
|
**`[CallerFilePath]` is rewritten to `/_/...` under `ContinuousIntegrationBuild`.** Any test that
|
|
locates a fixture by source path passes locally and fails in CI. Copy fixtures to the output
|
|
directory and read them via `AppContext.BaseDirectory` instead; `GoldenVectorTests` shows the
|
|
pattern.
|
|
|
|
**`dotnet format --verify-no-changes` is part of the CI gate** and exits non-zero on style
|
|
warnings, not just whitespace. Run it before pushing; a build with zero warnings can still fail
|
|
that step.
|
|
|
|
## Deployment
|
|
|
|
**PostgreSQL 18 moved its data directory** to `/var/lib/postgresql`, not `/var/lib/postgresql/data`
|
|
as in 17 and earlier. A compose file carried over from an older version silently gets an empty
|
|
volume — the database appears to work and loses everything on restart. Relevant to any compose
|
|
file other than `deploy/docker-compose.dev.yml`, which is already correct.
|
|
|
|
**Keycloak in the dev stack listens on host port 18080, not 8080.** On this machine an unrelated
|
|
Apache Tomcat holds `127.0.0.1:8080`, and a loopback-specific bind wins over Docker's `0.0.0.0`
|
|
publish when resolving `localhost` — so every realm request returned 404 while the container
|
|
looked healthy. If discovery fails against a locally-published container, check for another
|
|
process bound specifically to loopback before suspecting the container.
|
|
|
|
**`Sync:CursorSigningKey` generates an ephemeral per-process key when unset.** Fine for a single
|
|
node; on a multi-node deployment cursors issued by one node are rejected by another, so clients
|
|
resync from the beginning repeatedly. Must be configured explicitly before running more than one
|
|
instance. `WarnOnRiskyConfiguration` logs this at startup.
|
|
|
|
**Rate limiting is not implemented yet** (M2). `POST /api/v1/me/enrollment` and the sync endpoints
|
|
are reachable by any authenticated caller at any rate. Enrollment requires a valid access token
|
|
and is idempotent, so the exposure is resource consumption rather than a credential-guessing
|
|
surface — but it is still an unmetered write path.
|
|
|
|
**`/api/v1/me` does not update `last_seen_at_utc`.** Deliberate: a GET that writes on every call is
|
|
a smell, and nothing depends on the value yet. Revisit when device management lands, since that is
|
|
the first feature that needs it.
|