This document enumerates every non-negotiable security rule from
DEVELOPMENT.md §12 and points to the test or runtime
check that enforces it. A rule without an enforcer is a TODO that must close
before the gating phase ships.
Nexray runs on a user's desktop and forwards arbitrary user traffic to remote servers chosen by the user. The hostile parties we model:
- The local network (ISP / Wi-Fi / state-level censor): can fingerprint TLS, do active probing, and block IPs. Defenses: REALITY borrowed SNI, uTLS fingerprint, CDN fronting (cdn-ws).
- A malicious or compromised subscription provider: can ship malformed
links, attempt to silently downgrade us to a legacy protocol, or smuggle
allowInsecure: true. Defenses: schema rejection at parse time, no silent defaults that change semantics, logging redaction. - A local attacker on the same machine: can probe localhost ports.
Defenses: Stats API bound to
127.0.0.1:<random>, never exposed.
The first defense is what we let through the parser. Nexray accepts
exactly two deployment profiles — cdn-ws and reality — because they
are the only profiles whose wire traffic is indistinguishable from
legitimate browser-to-CDN traffic against the network adversary above.
Every other proxy protocol or transport combination has a known
fingerprinting attack, an active-probe oracle, or a cert-chain
information leak. They are rejected at the parser (the share-link
decoder and the subscription classifier) and never reach xray-core.
Per-protocol rationale, with attack references, lives in
PROFILES.md § Why only these two
and § What we do not support.
Each rejection produces a structured SkipReason that the UI surfaces
verbatim in the subscription's "X servers skipped: …" line — operators
should never have to guess why a server was filtered.
| # | Rule | Enforcer |
|---|---|---|
| 1 | Subscription fetch over HTTPS only. Reject http:// URLs at parse time. |
Phase 1: subscription fetcher in src-tauri/src/subscription.rs + integration test. |
| 2 | Never persist subscription credentials in plain text. macOS: Keychain. Windows: DPAPI. Linux: libsecret. | Open — Phase 7. Phase 3 uses tauri-plugin-store which writes a plain JSON file under the OS app-data directory; UUIDs and public keys are stored as plaintext. Phase 7 will move secret fields into the OS keyring (keyring crate or tauri-plugin-stronghold). Tracked in CHANGELOG.md. |
| 3 | Validate fingerprint against the whitelist. Unknown ⇒ hard failure. |
tests/profile.test.ts ("rejects unknown fingerprint"); tests/share-link.test.ts (MALFORMED_BAD_FP). |
| 4 | Refuse to materialize an Xray config if any mandatory field is missing — fail loudly with a structured error. | Phase 2: materializer in src-tauri/src/config.rs + unit test. |
| 5 | No telemetry. No crash reporting that ships data off-device by default. | Manual review per PR. The Settings page exposes no telemetry opt-in switch — there is nothing to opt into. Adding telemetry would require a new IPC, schema field, and visible UI surface, all reviewable in one PR. |
| 6 | Stats API bound to 127.0.0.1:<random>, never exposed. |
Phase 2: startup assertion in src-tauri/src/stats.rs that the bind addr is loopback; fails closed. |
| 7 | Bundled xray-core SHA-256 pinned. CI verifies. Never auto-upgrade. | scripts/fetch-core.mjs HASHES table; download refused if SHA mismatches; CI runs pnpm fetch-core for the matrix. |
Per DEVELOPMENT.md §11: UUIDs, public keys, short IDs,
and full subscription URLs must be redacted at the formatter, not the call
site. Phase 2+ adds tracing middleware and a consola formatter that
substitute these patterns with *** before emit. Each logger has a unit test
that scans output for the redaction patterns.
Per-phase mitigations land in this file as their phase ships. The threat model itself will be expanded in Phase 7 after the first external review.