KDX Client & Server
A from-scratch Rust rebuild of KDX — an early-2000s Hotline-style chat and file-sharing system. A custom binary protocol over TLS 1.3, a challenge-response login where the password never leaves the machine, chat rooms, a virtual file tree with drop boxes, and resumable chunked transfers both directions. The client is a Tauri desktop app in the classic black / blood-red / phosphor-green aesthetic.
This document is the reference for what the interface does and how each feature works underneath. Read the interface sections to operate the app; read "Under the hood" to understand or extend the machinery each window drives.
kdx-protocol and kdx-crypto crates the server uses, so the wire format has a single implementation shared by both sides — no drift between what the server speaks and what the client expects.
System map
Every user action follows one path: the webview calls a Tauri command, the command forwards to the async client library, which frames the request and writes it to the TLS socket. Server pushes travel back the same channels as events.
| Crate / dir | Role |
|---|---|
kdx-protocol | Wire format: 20-byte framed packets, codec, fragmentation, typed messages. Shared by client and server. |
kdx-crypto | Argon2id password hashing + challenge-response helpers. |
kdx-server-core | Connection state machine, auth, chat rooms, file tree, transfers, tracker. |
kdx-storage | sqlx / SQLite persistence (accounts, file tree, resume state). |
kdx-client-core | Async client library, zero UI deps. TOFU trust, transfer engines, event stream. |
kdxd | The server daemon binary (+ useradd subcommand). |
apps/kdx-client | Tauri v2 desktop app: a thin Rust shell + the webview frontend. |
The desktop
The client is a single OS window hosting a desktop metaphor: four draggable, focusable panels rendered in the webview, not separate OS windows. A menu bar across the top focuses (and un-hides) each panel; a footer shows connection state. Click a title bar to drag; click anywhere in a panel to bring it forward.
Menu bar
- File / Chat / Transfers — focus that window.
- Status chip (right) — offline connecting online, showing
kdx://host:portonce connected.
The four windows
- Connect — server address, login, TOFU trust prompt.
- Chat — the lobby: messages, users, topic.
- Files — browse, upload, download.
- Transfers — live per-chunk progress.
Connect window
The entry point. Enter server host, port, and account, then Authenticate. The window remembers your last host / port / username via local storage, and stays available after login so it doubles as the reconnect surface.
The authenticate flow
- 1 · connect — TCP + TLS + KDX version handshake.
- 2 · trust check — first contact triggers the TOFU prompt (below).
- 3 · login — challenge-response; password never sent.
- 4 · join lobby — you land in
#lobbyand the Chat window focuses.
Status appears inline below the button: logged in as phraq (power user) or a red error line.
Trust-on-first-use prompt
KDX pins server certificates SSH-style. The first time you reach a server, or any time its key changes, the connect step is refused and an inline prompt shows the certificate fingerprint for you to verify out-of-band before trusting it.
sha256:… fingerprint. Trust & connect pins it and reconnects. Expected against the self-signed dev server — its key regenerates each run, so you'll re-prompt on every restart.
Chat window
A topic bar, a scrolling message stream, a live user list, and an input line. The title-bar tag shows the current member count.
Message line types
- normal —
<sender> text, phosphor green, sender highlighted. - action —
/memessages, italic,*** sender text. - system — join/leave and server notices, dim italic (clients can't forge these).
- warning — flood control and advisories, amber.
- error — server error frames, red.
Input commands
| You type | Effect | Command |
|---|---|---|
| hello there | Send a normal message to the room. | send_chat(flags 0) |
| /me waves | Action message. | send_chat(flags 1) |
| /topic digital freedom | Set the room topic (needs privilege). | set_topic |
Files window
A path bar with Up / Refresh / Upload controls over a directory listing. Each row shows a kind tag and — for files — a size. Clicking a folder navigates in; clicking a file opens a native save dialog and downloads it. Uploading opens a native file picker and sends into the current directory.
Node kinds
[DIR]— a folder; click to enter.[FILE]— click to download to a chosen path.[DROP]— a drop box, shown in amber. You may upload into it but never list or read it — enforced server-side for every class, admins included.[UP]— an upload folder.
Transfers window
One card per active transfer, each with a live chunk-bitmap grid: every cell is one chunk, lighting up phosphor-green as it is sent (upload) or received and hash-verified (download). The card shows direction (↑/↓), percent, and done/total chunks; on completion it flips to verified ✓ or a failure message.
The bitmap is driven directly by the transfer_progress event's bitmap field, so what you see is the actual resume state — the same bytes persisted for resuming an interrupted transfer.
Command reference
The webview calls these via __TAURI__.core.invoke. Each is a thin async forward into kdx-client-core; none holds business logic. Errors return a tagged object — kind: "untrusted_certificate" (with host, port, fingerprint, previous) drives the TOFU prompt, everything else is kind: "message".
| Command | Arguments | Returns | Effect |
|---|---|---|---|
| connect | host, port | "connected" | TCP + TLS + KDX handshake; spawns the event forwarder. May return an untrusted-cert error. |
| trust | host, port, fingerprint | — | Pin a fingerprint in known_hosts.toml so the next connect trusts it. |
| login | username, password | class: u8 | Challenge-response login; returns the account's base class (0–3). |
| join_room | room | — | Join (or create, with privilege) a room. |
| leave_room | room | — | Leave a room. |
| send_chat | room, flags, text | — | Send a message; flags bit 0 = action (/me). |
| set_topic | room, topic | — | Set a room topic (needs CHAT_SET_TOPIC). |
| list_files | path | {path, entries[]} | List a directory; entries carry name, kind, size. |
| upload | local, remote_dir | — | Upload a local file; resolves when verified. Progress via events. |
| download | remote_path, local | — | Download to a local path, resuming from a sidecar if present. |
| disconnect | — | — | Close the connection. |
Event stream
The server's pushes and transfer progress arrive on one channel. kdx-client-core emits a typed Event; the Tauri shell forwards each as a "kdx" webview event whose payload is tagged by type (snake_case). The UI switches on that tag.
| type | Payload | What the UI does |
|---|---|---|
| connected | fingerprint | Informational; connection is considered up after login succeeds. |
| authenticated | class | Reserved — login() already returns the class directly. |
| chat | room, sender, timestamp, flags, text | Render a chat line (normal / action / system by flags). |
| user_list | room, users[] | Repaint the user list; update the member-count tag. |
| topic | room, topic | Update the topic bar. |
| transfer_progress | id, direction, done, total, bitmap | Update / create the transfer card and its chunk-bitmap grid. |
| transfer_complete | id, direction, status, message | Mark the card done (status 0 = verified); refresh Files. |
| server_warning | text | Amber chat line (e.g. flood control). |
| server_error | text | Red chat line. |
| server_info | text | System-styled chat line. |
| disconnected | reason | Drop to offline, tag chat "offline", focus Connect for reconnection. |
Protocol & framing
Every message is a length-framed packet. The header is a fixed 20 bytes, all multi-byte fields big-endian:
// 20-byte header magic 4 b"KDX\0" version 1 packet_type 1 handshake / auth / file / chat / system ranges flags 2 bitfield incl. fragmented, more-fragments sequence 4 per-connection monotonic length 4 payload bytes (u32) reserved 2 checksum 2 CRC32C-folded — framing guard, not security
- Fragmentation — payloads over a 16 KB frame cap (a file chunk, say) split across frames flagged
FRAGMENTED/MORE_FRAGMENTSand reassemble on the far side, guarded by a size cap and timeout. - Integrity comes from TLS and per-chunk SHA-256 — the header checksum only catches framing corruption.
Auth & trust
Challenge-response login
The server stores only an Argon2id hash. To log in, the client receives a one-time random challenge plus the account's salt and cost parameters, recomputes the Argon2 verifier locally, and answers with SHA-256(challenge ‖ verifier). The server compares in constant time. The password never crosses the wire — not even inside TLS — and a captured answer is useless because each login gets a fresh challenge. Unknown usernames still get a plausible decoy challenge, so the wire can't distinguish "no such user" from "wrong password."
Trust-on-first-use
The client pins the SHA-256 of the server's certificate per host:port in known_hosts.toml. An unknown or changed key fails the TLS handshake; the library surfaces the observed fingerprint (and any previous one) so the UI can prompt. This is the desktop-app answer to self-signed and rotating server certs without a CA.
Chat & user classes
Each chat room is its own actor task owning its membership, topic, and per-member flood gate — broadcasting a message is N channel sends with no shared lock on the hot path. A token-bucket flood gate (burst 5, refill 1/s) throttles a spammer with a server_warning. Clients cannot forge system-flagged messages.
Classes & privileges
Four base classes with strictly-increasing privilege sets; per-account overrides can grant beyond or revoke from the class.
| Class | Gains (on top of the one below) |
|---|---|
| guest | CHAT_SEND, FILE_LIST |
| user | CHAT_PRIVATE, FILE_DOWNLOAD, FILE_UPLOAD |
| power | CHAT_CREATE_ROOM, CHAT_SET_TOPIC, FILE_DELETE, FILE_MANAGE_TREE |
| admin | everything (USER_KICK/BAN/ADMIN, SERVER_ADMIN) |
Files & ACLs
The file tree is loaded from SQLite into memory (read-heavy, write-rare) and written through on mutation. Every folder carries a minimum-class-to-read and minimum-class-to-write. File bytes live on the filesystem, never in the database.
- Drop boxes reject listing and reading structurally — the check sits in the ACL layer and fires for every caller regardless of class, so write-only is a property of the node, not a convention.
- Downloads resolve the file through the same read-ACL path, so a file inside a drop box can be uploaded but never downloaded.
Transfers & resume
Files move in 32 KB chunks, each carrying its own SHA-256; the whole file is SHA-256-verified before it's accepted. Both directions are throttled by a token bucket and support resume — the difference is who remembers what:
Upload ↑
The server holds the resume state: a received-chunk bitmap in the transfer_state table, updated after every chunk-ack. A reconnecting client is told which chunks the server already has and sends only the rest. A crash loses at most one chunk.
Download ↓
The client holds the resume state: a .kdxpart sidecar next to the partial file records the bitmap. On resume the client sends its bitmap and the server streams only the missing chunks; the whole-file hash is checked before .part is renamed to the final name.
The chunk grid in the Transfers window is a direct view of this bitmap — the visualization and the resume mechanism are the same bytes.
Running it
Server
# create an account: guest | user | power | admin cargo run -p kdxd -- useradd phraq s3cret power # start it (defaults to 0.0.0.0:10700, self-signed dev cert) cargo run -p kdxd
Client
cargo run -p kdx-client
The window opens; enter host 127.0.0.1, port 10700, your account, and Authenticate. Accept the TOFU fingerprint on first contact. No Node toolchain is needed — the bundled frontend is served directly.
cargo install tauri-cli --version '^2', then in apps/kdx-client/src-tauri run cargo tauri icon icons/icon.png and cargo tauri build for a signed .app + .dmg. A placeholder icon ships so cargo run works out of the box.
Verification
The whole stack runs green under cargo test --workspace — 107 tests, no external services. The client library is tested headlessly against a real in-process kdxd (real TLS over 127.0.0.1), and the server's own tests are an independent second implementation of the wire flows.
- Protocol — codec round-trips, checksum corruption, fragment reassembly.
- Crypto — Argon2 verify, challenge round-trip, replay rejection.
- End-to-end — login, chat both directions, uploads/downloads with kill-and-resume in both directions, DropBox refusal, throttle pacing.
- Cross-process smoke —
cargo run -p kdx-client-core --example smokedrives a separately-runningkdxd: TOFU-pin → login → join → chat → list, over a real socket.
Gaps & next steps
kdx-server-core as an in-process registry but has no client-facing wire protocol yet — so there is no Tracker window. Exposing it is its own protocol milestone (new packet types + client method), on the model of the download work.
kdxd useradd CLI. No in-app admin UI for creating / editing accounts or setting privilege overrides.
- Private messaging — the
CHAT_PRIVATEprivilege exists but direct messages aren't wired end to end yet. - Multi-room UI — the client fixes on
#lobby; the protocol supports many rooms, so a room switcher is a UI-only addition. - Postgres — persistence is SQLite; a Postgres swap is confined to
kdx-storage. - GUI manual smoke — the desktop window itself is verified by hand (
cargo run -p kdx-client); every layer it wraps is covered by automated tests.