The junk marker was a per-key CONSTANT sent in plaintext at a fixed offset in
junk frames. Junk is meant to look like random noise (zapret-style), but a
constant prefix is a recognizable per-user structure: an on-path observer
watching one user sees the same 4 bytes on every junk packet, i.e. an OSTP
fingerprint. (The earlier fix only removed the GLOBAL constant.)
Now the marker rotates every 60s window: junk_marker = HKDF(key, ver, 0x04 ||
window). To an observer the prefix changes each window (no fixed signature),
and a captured marker is only valid for ~1 window — the "bit of protection"
against a leaked marker. Only a key holder can compute it, so an outsider still
can't forge a silently-dropped junk packet (and silent-drop is cheaper than
normal processing anyway, so junk spam was never a DoS lever to begin with).
- core: derive_junk_marker(key, window) + current_junk_window() (60s window),
same version-gated HKDF scheme; junk_marker dropped from DerivedSecrets.
- client: stamps junk with the current window's marker.
- server: checks current AND previous window per key (absorbs ~1 window of
clock skew) before falling through to unauthorized-probe handling.
- Not a wire break: only junk framing changes; real handshake/data untouched.
During mixed rollout, unmatched junk merely logs as a probe (cosmetic).
The base already derives all secrets from the access key, so derived
secrets were never the gap — the gap was that nothing distinguished a
current handshake from an older-format one, so an old client could still
connect to a new server.
Rather than the plan's literal "plaintext version byte before the crypto
layer" (which would add a constant, DPI-visible marker and defeat the
project's stealth north-star), fold the version INTO the HKDF derivation:
- Add PROTOCOL_VERSION (= 4 for 0.4.0), mixed into the IKM of
derive_all_secrets so a different version yields a completely different
obfuscation key / psk / padding. No marker ever appears on the wire —
the output stays indistinguishable from random.
- A pre-0.4.0 peer derives a different obfuscation key, so the 0.4.0
server cannot recover its handshake header and drops it as an
unauthorized probe. Bump PROTOCOL_VERSION on any future wire break.
Verified:
- cargo test -p ostp-core: 36/36 incl. new test_protocol_version_gates_
old_clients (old-version obf key does NOT recover the session_id).
- Loopback E2E: new client <-> new server connects and tunnels HTTPS
(curl via SOCKS5 returns egress IP).
- Old v0.2.98 client vs new server: handshake times out / aborts, server
accepts 0 clients — exactly the plan's §C criterion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TUN Interface:
- Fixed adapter name to always be 'ostp_tun' by cleaning up stale
adapters before launch (prevents 'ostp_tun 2', 'ostp_tun 3', etc.)
- Parallelized route setup with tun2socks launch to save ~3 seconds
- Replaced fixed 2-second sleep with adapter readiness polling
- Added -NoProfile to all PowerShell calls for faster execution
Speed:
- Reduced handshake timeout from 10s to 5s
- Reduced tun2socks spawn buffer from 300ms to 0 (removed)
GUI:
- Added i18n support: English and Russian translations
- Language toggle button in header (EN/RU)
- Merged 'IP Ranges' field into 'Bypass IPs / CIDR Ranges'
- Removed separate IP ranges field
- All static text uses data-i18n attributes
- Status messages, labels, toasts all translated
- Replaced alert() calls with toast notifications
CI/CD:
- Added separate GUI build job for Windows x64 and arm64
- Produces ostp-windows-gui-{arch}.zip with: ostp-gui.exe + wintun.dll + tun2socks.exe
- Uses Tauri CLI v2 for build