Applied Kerckhoffs's principle: the protocol's security and obfuscation
now depend SOLELY on the access key. An adversary who reverse-engineers
the binary cannot build a DPI filter without knowing the key.
Changes:
- Replaced hardcoded salt string ('-ostp-psk-salt') with HKDF-SHA256.
The salt is now derived from the key hash itself — no protocol-specific
strings remain in the binary.
- Unified all secret derivation into derive_all_secrets() which produces
PSK, obfuscation key, and handshake padding range from a single HKDF
invocation.
- Handshake padding range is now key-derived: different access keys
produce different size distributions (min: 16-79, max: +48..+175).
A universal size-based filter is impossible without the key.
- HKDF-SHA256 (RFC 5869) implemented inline using existing hmac+sha2
dependencies — no new crate required.
What remains identifiable in the binary:
- 'Noise_NNpsk0_25519_ChaChaPoly_BLAKE2s' — standard Noise pattern
string, shared with many other projects, NOT OSTP-specific.
- Generic HMAC/SHA-256/ChaCha20-Poly1305 code — standard crypto
primitives used by millions of applications.
The previous commit added random padding after Noise handshake payloads
but the receiver passed the entire raw buffer (including padding) to
snow::read_handshake(), which cannot handle trailing bytes.
New wire format:
[session_id:4][noise_len:2][noise_payload:N][random_padding:32-128]
Changes:
- wrap_datagram_handshake: puts noise_len (u16 BE) at bytes [4..6]
before the Noise payload, followed by 32-128 random padding bytes
- handle_inbound: reads noise_len from [4..6], passes only
raw_vec[6..6+noise_len] to snow, ignoring trailing padding
- obfuscation: handshake mask extended from 4 to 6 bytes to also
cover the noise_len field (prevents DPI from seeing constant u16)
- dispatcher: key-trial loop updated to deobfuscate 6-byte header
Both client and server now produce/consume the same padded format.
DPI/TSPU resistance:
- Handshake packets now padded with 32-128 random bytes (prevents size
fingerprinting — previously every handshake was exactly 52 bytes)
- Frame header reserved bytes randomized instead of always 0 (prevents
known-plaintext oracle inside encrypted payload)
- Padding jitter cap increased from 96 to 256 bytes for better traffic
pattern masking
GUI Windows app (tunnel/proxy not starting):
- CRITICAL: Added CREATE_NO_WINDOW flag to all reg.exe calls in sysproxy.rs.
In Tauri GUI context (no console window), Command::new('reg') was silently
failing because there was no attached console. This prevented the Windows
system proxy from being enabled.
- Added ProxyOverride bypass list (localhost;127.*;10.*;192.168.*;<local>)
to prevent proxy loop for local traffic
- Added comprehensive logging for all registry operations
- Set initial connection_state to 1 (connecting) instead of 0 — prevents
UI polling from immediately flipping back to 'disconnected' before the
handshake has a chance to begin
Code quality:
- Fixed log file paths: log_to_core_file() and log_to_file() now write next
to the executable instead of CWD. In GUI context, CWD could be
C:\Windows\System32, causing write failures or misplaced log files.
Critical fixes (6):
- protocol.rs: in_flight_count() now counts only retransmittable Data frames,
not Ack/Nack control frames — eliminates false backpressure under load
- protocol.rs: NACK is now rate-limited to once per 30ms — prevents
retransmission storm during normal UDP jitter
- protocol.rs: zombie frames exceeding max_retries+4 are evicted each tick —
prevents unbounded memory growth and stale retransmits
- protocol.rs: Closing state now processes final in-flight packets instead
of silently dropping them — prevents data loss at session teardown
- server/lib.rs: stream_tx changed from bounded(10000) to unbounded_channel —
prevents TCP-reader collapse during Speedtest with 50+ streams
- bridge.rs: liveness timeout raised from 30s to 60s — prevents false
reconnect during heavy Speedtest load
Medium fixes (8):
- protocol.rs: ACK range truncation preserves cumulative range (index 0)
- bridge.rs: Ping now uses send_datagram() for correct TURN wrapping
- dispatcher.rs: replay_cache hard-capped at 100k entries (DoS protection)
- dispatcher.rs: old addr cleaned from addr_to_session on roaming
- server/lib.rs: TCP connect_target() now has 10s timeout
- config.rs: TURN section parsed during hot-reload
- proxy.rs: HTTP header parsing uses 512-byte chunks instead of 1-byte reads
- proxy.rs: stream_id wrap-around skips active IDs to prevent collision
- runner.rs: is_essential_log matches actual log strings from bridge.rs
Other:
- kex.rs: clearly marked as dead PQ stub (not used by protocol)
- README.md + README.ru.md: complete rewrite with architecture diagram
- docs/en/specification.md: updated ARQ section with all new semantics