Files
omnivox-rs/PARITY-CONTRACT.md
T
michael 268b986bb9 Rust port of OmniVox, wire-compatible with the C++ original
Byte-exact cereal binary layout, RFC 8439 ChaCha20-Poly1305, RFC 7914
scrypt defaults; tokio transport preserving the original's observable
behavior (contract B1-B3, B5 quirks included). Parity evidence: golden
C++ vectors in tests/vectors.rs and loopback behavior tests; both
cross-directions verified against the unmodified C++ binaries.

Source: https://git.0xfeedc0de.com/michael/OmniVox
2026-09-30 06:35:24 -04:00

4.8 KiB

OmniVox Port — Parity Contract

Source: omnivox/ (C++17/20, asio + cereal + CryptoPP + fmt) Target: omnivox-rs/ (Rust, tokio + RustCrypto) End state: A — replacement. Rust crate + example binaries ship; C++ deleted at cut-over. Seam: the TCP network boundary. C++ and Rust must interoperate byte-for-byte at any mix.

Unit of parity

The byte stream on the wire at the TCP boundary, and the callbacks the library delivers to the application (connect / data / disconnect with their context).

Wire format (recovered — this is normative)

All integers little-endian (x86 native memcpy/cereal, never htonl'd anywhere).

  • Framing: u32 payload-length prefix, then that many bytes. Read in two async steps (header, then body) with separate timeouts.
  • AeadPacket (cereal binary archive = raw fields, no magic/header): nonce[12] || tag[16] || u64 len || ciphertext[len]
  • DatagramPayload: u16 id || u64 qword || u64 len || bytes[len]
  • AEAD: ChaCha20-Poly1305 per RFC 8439 (CryptoPP impl is RFC-conformant). No AAD. Random 12-byte nonce per message (AutoSeededRandomPool).
  • Key: scrypt per RFC 7914, N=2^17, r=8, p=1, dkLen=32, passphrase = raw password bytes, salt = raw salt bytes. Both peers must set the identical key.

Not acceptable (any difference = regression)

  • Any wire byte: framing, field layout, key derivation, AEAD construction.
  • Tamper behavior: DecryptAndVerify failure → message dropped, error logged, connection stays open (exception caught in processMessage).
  • Limits: MAX_MESSAGE_SIZE = 5 MiB; header 0 or > 5 MiB → log + disconnect.
  • Timeouts: per-message header-wait = timeoutMs (server, per client arg); body stall = 5000 ms fixed. Server per-client stall timers; client side has no timers (ctx.id == serverContext.id path).
  • Client IDs: server assigns from 1, monotonic (nextClientId), visible in ConnectionContext.id.
  • Callback order on accept: allow → client registered & reading before the connect log; deny → socket closed, disconnect callback fires.
  • sendToServer returns false unless socket open and status Connected; sendToClient returns false if id unknown.
  • Malformed/undecryptable message: logged, skipped, connection survives.

Acceptable (do not chase)

  • Log text, wording, timestamps; steady-clock creationTime values.
  • Thread structure (pool + io_context → tokio tasks); task scheduling.
  • Message processing order: C++ server processes on a worker pool and can reorder concurrent messages; Rust may differ in interleaving. Per-connection stream order must still be preserved.
  • Exact race outcomes under B1/B3 below (timing-dependent in C++ itself).
  • TCP_NODELAY: C++ leaves Nagle ON (NODELAY_OPT never defined); Rust matches default. Never observable in payload bytes.

Bug/quirk inventory — preserved, NOT fixed (fixes = separate commits after parity)

# Source Behaviour Port decision
B1 OmniVox.h:914 Bad header size logs + handleDisconnect without returning — proceeds to read a body of that size (0-byte read succeeds and processes an empty packet → decrypt error logged) Preserve: disconnect + best-effort body read; races allowed
B2 OmniVox.h:990 Decrypt/parse errors are swallowed (log only), connection kept Preserve
B3 OmniVox.h:630 sendToClient does a blocking asio::write while holding the clients lock Rust holds the connection write lock across the full write; log as follow-up improvement
B4 OmniVox.h:960 Processing on thread pool → concurrent reordering Acceptable-difference above
B5 OmniVox.h:869 Client mode never arms stall timers Preserve
B6 OmniVox.h:782 timeoutCheckerThread/timeoutCheckerRunning — dead code, never started Not ported (dead)
B7 OmniVox.h:99 inline std::optional<WorkGuard> global — shadowed by member, dead Not ported (dead)
B8 client.cpp:101 Client sleeps 15 s then stops; server sleeps UINT32_MAX hours Examples mirror this

Deliberately not ported

B6/B7 dead code; the cereal/cryptopp/asio vendored trees; PackedValue64 union (only .qword is ever serialized → port as u64 field on DatagramPayload); ThreadPool_ as a class (tokio replaces it; do not transliterate).

Verification (how parity is proven)

  1. Vector tests: golden scrypt + cereal-layout + AEAD vectors generated by C++ (cppgen helper in omnivox-ref/) checked into tests/vectors/.
  2. Differential cross-tests over loopback: C++ server ↔ Rust client, Rust server ↔ C++ client — echo handshake must round-trip.
  3. Tamper test: flip a ciphertext byte mid-stream → receiver logs and survives.
  4. Differential harness reports inputs-compared and differences-found.

cargo test --all-features && cargo clippy --all-targets --all-features && cargo fmt --check