Previously handshake obfuscation used a FIXED mask derived from
HMAC(obf_key, u64::MAX). This meant bytes [4..6] (noise_len XOR
fixed_mask) produced the SAME 2-byte value on every handshake from
the same access key — a correlation fingerprint for DPI.
Now BOTH data and handshake packets use the same payload-sampling
approach:
mask = HMAC-SHA256(obf_key, payload_sample[0..32])
For data packets: payload_sample = AEAD ciphertext (random per packet)
For handshake packets: payload_sample = Noise ephemeral key (random per connection)
Result: every single byte on the wire is cryptographically independent
across packets. No fixed patterns, no correlation between connections.
Wire analysis after this change:
- Packet sizes: random (84-182 for handshake, variable for data)
- All header bytes: unique per packet (XOR with unique HMAC mask)
- Payload bytes: AEAD ciphertext / Noise handshake (indistinguishable from random)
- No protocol signatures, no version fields, no magic bytes visible on wire
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.
- Removed stale KeyExchange re-export from crypto/mod.rs (kex.rs
only exports HybridSharedSecret and HybridKex after stub refactor)
- Removed unused imports in ostp-server/lib.rs (AsyncWriteExt,
tcp::OwnedWriteHalf)
- Suppressed dead_code warning on HelperMsg::Log variant (IPC spec)
- Verified: cargo check passes with zero errors and zero warnings
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