Commit Graph

56 Commits

Author SHA1 Message Date
ospab f789167a22 feat(congestion): actually pace sends instead of releasing whole windows
pacing_rate had been computed on every ACK since the controller was written
and never read by anything: admission was decided purely by cwnd. But cwnd
bounds how much may be UNACKNOWLEDGED, not how fast it reaches the wire, so a
full window went out back-to-back. On a bottleneck with a deep buffer that
burst is absorbed rather than dropped, and it lands as standing queue — the
mechanism behind the multi-second RTT this protocol has been showing on
mobile. It is also why BBR could not simply be dropped in: BBR's whole model
is "send at the estimated bottleneck rate", which is meaningless without a
pacer underneath it.

Adds a token bucket to CongestionController, charged in on_send so every byte
that reaches the wire pays exactly once — retransmits included, since those
are precisely what must not bypass the limit and pile into a full queue.

Burst allowance is 10ms-at-rate rather than one packet. Pacing intervals here
are fractions of a millisecond, so strict per-packet release would need a
sub-millisecond timer per packet; sizing the burst to the loop's existing
~10ms wakeup lets the configured rate still be saturated. There is a floor of
4 MTU so a cold or collapsed rate estimate can never wedge sending entirely.

Wired into both directions. The client gates its proxy-event branch on it,
and the server's per-session backpressure snapshot reports zero headroom when
the bucket is empty. The download path matters most here — that is the one
carrying video — and it was also still clamped to the old 16384-packet
ceiling (~20 MB outstanding), now aligned with MAX_CWND_PACKETS.

Tested that the bucket denies once drained and refills over time; the second
is what keeps a stalled bucket from wedging sending permanently.
2026-07-31 19:18:42 +03:00
ospab f7e9215331 fix(congestion): stop the bufferbloat spiral behind multi-second RTT stalls
Reported symptom: on mobile the reported RTT jumps to 15-20s (worst case
~150s), video stops loading, and it takes ~5 minutes to recover — or doesn't,
until the user reconnects.

Nothing on the network takes 150 seconds. That delay was our own queue. The
controller only ever treated LOSS as congestion, and mobile carrier buffers
are deep enough to absorb a burst rather than drop it, so the loss signal
never arrived and three things compounded:

  - slow start grew cwnd unbounded, with the client permitting up to 16384
    packets in flight (~20 MB — minutes of queue on a mobile uplink);
  - the resulting standing queue inflated RTT samples, which raised SRTT,
    which raised the adaptive RTO, so retransmits piled into the same queue;
  - backoff multiplied that already-inflated RTO by up to 64x. With RTO_MAX
    at 16s a frame could sit unretransmitted for ~17 minutes.

Reconnecting "fixed" it only because a fresh session resets cwnd to 32.

Three bounds, matching how delay-based controllers handle this:
  - Treat sustained RTT inflation as congestion: leave slow start at 2x the
    observed path floor, actively halve cwnd at 4x. This is the part that
    works where loss never comes.
  - Hard cwnd ceiling of 1024 packets (~1.2 MB), well above any real BDP here
    but far below a queue measured in seconds. The client's in-flight gate is
    lowered to match.
  - Cap the post-backoff retransmit timer at 8s.

Tested for the no-loss inflation case and the ceiling. Both encode the bug
directly, since neither is observable from loss-driven tests.

Not addressed here: pacing is still computed and unused, so sending remains
bursty. That is the next lever and wants real-link validation.
2026-07-31 18:47:14 +03:00
ospab 88e0634f09 fix: two independent causes of the tunnel freezing at 0 b/s
Both produce the same reported symptom - traffic stops dead, the session
itself looks fine, and only a manual reconnect recovers it.

1. protocol.rs: a retry could be charged to a frame that was never sent.
   The retransmit loop is budget-limited per tick, but it bumped `retries`
   and reset `last_sent` for every due frame regardless of whether the
   budget actually allowed a send. The budget is smallest exactly when loss
   is heaviest (it is derived from cwnd, which collapses under loss), so
   under real packet loss frames accumulated "phantom retries" they never
   received - measured at 40 retries charged for 8 frames actually sent in
   one tick. After max_retries+2 such rounds the zombie eviction dropped
   them as dead. That data was never delivered and never would be: the
   stream stalls permanently while pings keep flowing, so nothing upstream
   notices anything is wrong. Retries/timers are now only charged on an
   actual transmit, and the loop stops scanning once the budget is spent
   (sent_history is in send order, so this also keeps retransmit priority
   oldest-first). Covered by a new test that asserts retries charged ==
   datagrams emitted; verified it fails against the old code.

2. bridge.rs: the stall detector was reset by datagrams that never
   validated. `last_valid_recv` - "last VALID recv" - was assigned before
   decryption, so a datagram that failed to decrypt still refreshed it on
   its way to the error return. Anything landing on that port kept the
   client convinced the tunnel was healthy: frames from a session the
   server had already evicted, stale retransmits, or plain garbage from an
   off-path source that knows the ip:port. The 25s background reconnect in
   handle_keepalive therefore never fired. It also made the UI health
   indicator report a dead tunnel as fine, and gave any off-path sender a
   trivial way to pin a client in a dead session indefinitely. Now set only
   after the datagram authenticates and decrypts.
2026-07-30 00:36:17 +03:00
ospab 77e42b77f7 fix(protocol): recover from an unrecoverable gap instead of freezing forever
The freeze users hit every few minutes: traffic drops to 0 B/s, the RTT
readout sticks at its last value, and only a manual reconnect clears it.

Delivery is gated on expected_recv_nonce, so one missing frame holds back
every frame behind it. That is correct only while the sender can still
retransmit — but the sender drops a frame from sent_history once it passes
max_retries + 2 attempts (zombie eviction in handle_tick). Past that point
the frame no longer exists anywhere and both sides deadlock: the receiver
buffers indefinitely and NACKs a nonce nobody can resend.

The watchdog could not save it, which is why it froze rather than
reconnecting. Retransmits, ACKs and NACKs keep arriving throughout, so the
client's last_valid_recv keeps refreshing and its 25s stall detector never
fires. The frozen RTT has the same cause: Pong travels in a Data frame,
stuck behind the very gap it would have reported.

The machinery for this was half-built: last_recv_advance was declared,
initialised and written on every advance, and its doc comment describes
exactly this recovery — but nothing ever read it, and a warning elsewhere
already referred to "gap recovery" that did not exist.

So implement it. Once the sequence has been stuck longer than the sender's
retransmit budget could plausibly last (8x the live RTO, clamped to 2..10s
so fast links do not discard merely-late frames and slow ones still
unblock), skip to the lowest buffered nonce, drain, and mark an ACK
pending so the peer stops retransmitting into a void.

This runs on the inbound path, not on Tick, for two reasons: both tick
handlers discard DeliverApp actions (client bridge.rs and server
dispatcher.rs match only SendDatagram/Multiple), and inbound frames keep
flowing all through the stall, so the path is reliably reached.

Skipping the hole drops one frame's payload — one RelayMessage, a chunk of
a single stream. That is a real cost, paid only when the data was already
lost for good, against a tunnel that otherwise stays dead until the user
intervenes.

Both tests were confirmed to fail without the fix (0 frames released
instead of 2), so they pin the deadlock rather than just the happy path.
2026-07-29 19:56:09 +03:00
ospab e31c4b2268 fix(protocol): don't abandon slow start over a single isolated packet loss
Root cause of "connection takes 20-30s, sometimes 1-2 minutes, to reach
stable throughput" (trickle of KB for a while, then a sudden jump to full
speed): on_loss during SlowStart unconditionally halved cwnd AND
permanently switched to ProbeBandwidth's linear (+1 MTU/RTT) growth on the
very FIRST loss. Real mobile/Wi-Fi links have a non-zero background loss
rate from ordinary wireless noise and handover blips that has nothing to
do with congestion; on such a link the first RTT or two of slow start would
hit a loss, get knocked into linear growth from a still-small window, and
take an enormous number of RTTs to claw back up to full speed - directly
contradicting the module's own stated BBR-inspired design intent, since
real BBR is deliberately loss-tolerant during startup instead of treating
any loss as a hard congestion signal.

Fix: track losses within a short (500ms) window and only pay the full
exit-slow-start-and-halve cost once SLOW_START_LOSS_TOLERANCE (3) losses
land within it - sustained loss is still treated as real congestion.  A
single isolated loss now takes a mild, temporary haircut (cwnd *= 0.8) but
stays in slow start, so exponential growth continues instead of being
abandoned over a one-off dropped packet.
2026-07-21 18:08:04 +03:00
ospab a9509a235d fix: low-severity hardening (Karn RTT, 32-bit frame overflow, replay-cache DoS)
- Karn's algorithm: drop_acked_frames no longer samples RTT from frames
  that were retransmitted (last_sent is bumped on each retransmit, so an
  ACK for the original transmission would measure a spuriously small RTT
  and drag SRTT/RTO down). Added CongestionController::on_ack_no_rtt for
  the case where every acked frame was ambiguous, so the window still
  advances without polluting the RTT estimator. Refactored the shared
  window-growth into grow_window.
- Frame decode: header+payload+pad length now uses checked_add. payload_len
  is a u32 from the header and on 32-bit targets (MIPS/ARMv7 routers are
  supported) the sum could wrap usize and slip past the truncation check.
- Replay cache: a full cache used to reject ALL new handshakes globally
  until the next tick, letting one flooding key-holder deny service to
  everyone. Now it reclaims expired entries and, if still full, evicts the
  single oldest — new handshakes always get in. Fixed the mislabelled
  "100000" log (cap is 50000) and named it REPLAY_CACHE_MAX.
2026-07-11 21:25:36 +03:00
ospab 5754689e09 refactor: remove dead 0-RTT resumption module (unsafe XOR ticket crypto)
The resumption module (SessionTicket/TicketValidator) was never wired into
the client or server — nothing issued or validated tickets, and no Resume
frame was ever sent. But it "encrypted" tickets by XOR-ing them with a
single static keystream SHA256(psk || const) and had no MAC (despite a doc
comment claiming HMAC): a textbook many-time-pad, trivially broken from a
couple of captured tickets, and malleable. Leaving it in-tree invited
someone to wire up a broken 0-RTT path later.

Removed the module, its FrameKind::Resume wire variant, and the protocol
handler for it. 0-RTT can be reintroduced later on a real AEAD-sealed
ticket if desired.
2026-07-11 21:22:45 +03:00
ospab b5735fe8c2 fix: quiet hot-path logging and stop logging access keys verbatim
Two classes of issue:
  - Hot-path/attacker-triggerable events logged at info/error with internal
    detail: a per-handshake info! byte dump (raw_vec[0..6]) and a per-packet
    error! on session-id mismatch that dumped expected/got session ids.
    Both are log-flood + info-leak surfaces; downgraded to debug and
    stripped of the sensitive detail. Close/Resume frame handling likewise
    moved from info to debug.
  - The access key (a shared secret) was written to logs verbatim in three
    places (session drop, key-created UI event, API create-user) and as an
    8-char prefix in one. Added key_fp() — a short SHA-256 fingerprint — and
    routed all key logging through it so operators can still correlate
    events without the secret ever hitting the log.
2026-07-11 21:21:01 +03:00
ospab f904695760 fix(server): rate-limit + cache the O(N_keys) handshake trial path (CPU DoS)
Every datagram from an unrecognized source ran the full key-trial loop:
for each registered access key, an HKDF (derive_all_secrets) plus two
HMACs (junk markers) plus a Noise read. A garbage flood from spoofed
sources could therefore force unbounded O(N_keys) crypto per packet — a
CPU-amplification DoS with no throttle (the existing token bucket only
guarded the roaming path, not this one).

Two mitigations:
  - Memoize the per-key derived secrets (pure function of key+version) and
    the per-window junk markers, so the trial loop is now cheap comparisons
    plus one Noise read per key instead of HKDF+2*HMAC per key per packet.
    Also speeds up every legitimate new connection. Caches are pruned in
    on_tick when keys are deleted.
  - Gate the trial path behind a global token bucket (TRIAL_RATE=100/s,
    same burst). The established-session fast path and roaming are not
    gated, so live sessions are unaffected; only unknown-datagram trials
    are bounded. Over-budget datagrams are dropped silently.
2026-07-11 21:18:05 +03:00
ospab 29554a71f1 fix(crypto)!: derive transport keys from DH-inclusive Noise Split, not the handshake hash
CRITICAL forward-secrecy fix. Session transport keys were derived as
SHA256(get_handshake_hash() || label). The Noise handshake hash `h` only
ever absorbs PUBLIC transcript data (ephemeral pubkeys + on-wire
ciphertexts, via MixHash); the ephemeral ee DH result is mixed via MixKey
into the chaining key `ck` ONLY, never into `h` (confirmed in snow 0.9.6
symmetricstate.rs). So the data-transport keys depended on the PSK and the
public transcript but NOT on the DH secret, meaning:

  - zero forward secrecy: anyone who later learns the access-key PSK can
    decrypt all recorded past sessions from the observed handshake alone;
  - any PSK holder can passively decrypt any other session on that key;
  - the ephemeral Diffie-Hellman was cryptographically wasted.

Fix: take the two directional keys from Noise's Split() over the final `ck`
via snow's dangerously_get_raw_split (risky-raw-split feature). These keys
depend on ee, restoring forward secrecy. The custom out-of-order AEAD,
explicit nonces, session_id AAD, framing and reordering are all unchanged
- only the key SOURCE moved. The dead into_transport()/handshake_hash()
paths and the unreachable NoiseSession::Transport variant are removed.

Wire-breaking: PROTOCOL_VERSION 4 -> 5 so pre-fix peers derive different
keys and cannot interop (version gate is invisible on the wire).

Added noise unit tests for the .0/.1 -> send/recv role mapping and the
not-finished guard.
2026-07-11 21:14:15 +03:00
ospab 5ab6833eab feat(core): time-rotating junk marker — kill the static per-user fingerprint
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).
2026-07-09 14:43:40 +03:00
ospab acab38c551 0.4.1: per-key junk marker, GUI polish, pre-release pipeline
security / protocol:
- Derive a PER-KEY junk marker (obfuscation.rs, info byte 0x04) instead of the
  global constant [0x88,0x1A,0x93,0x5D]. A fixed marker was a universal DPI
  signature identifying ALL OSTP users at once — exactly what the HKDF version
  gate avoids for the handshake. Server drops junk via a new DispatchOutcome::Junk
  inside the existing key-trial loop (secrets already derived → zero extra cost);
  client stamps its own key's marker.
- §E: configurable junk/fragmentation params (junk_pc / junk_ps / frag_chunk / frag_sleep).

GUI (desktop):
- Light theme + toggle, GUI version footer in Settings.
- Fix mouse-wheel scroll on Settings (flex child needed min-height: 0).
- Drop the false "process exclusions unsupported in TUN mode" warning — they DO
  work (native_handler maps port->process via GetExtendedTcpTable).

release / infra:
- build.ps1: add -PreRelease (tag CURRENT version as v<ver>-beta.N, no bump, no
  master commit); guard the panel build when ostp-control ships no source; bump
  the real ostp-gui/package.json instead of the nonexistent ostp-control one.
- release.yml: mark hyphenated tags as GitHub pre-releases; don't hard-fail the
  web-panel step when there is no source (use committed dist/).
- Versions aligned to 0.4.1; README license badge BSL 1.1 -> AGPL v3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 16:33:57 +03:00
ospab e1bf18e653 core_fixes 2026-06-28 17:11:27 +03:00
ospab 39127d30f3 §B: fix Closing-state teardown data loss (from 47d44fa)
In the Closing state the old code force-transitioned to Closed after a
SINGLE inbound packet, so any data/ACKs the peer still had in flight when we
initiated Close were dropped (Closed returns Noop for everything). Stay in
Closing and process inbound normally; handle_inbound already owns the
Close->Closed transition when it actually receives the peer's Close frame.
Also handle Tick in Closing so our own Close frame is retransmitted until
acknowledged.

Ported surgically from 47d44fa — only the Closing-state correctness fix, NOT
that commit's bundled RFC-6298 RTO / congestion rewrite (a behavioural change
to the working base) or the sent_history BTreeMap perf swap (broad hot-path
change for a perf-only gain). cargo test -p ostp-core: 36/36 incl.
test_close_sequence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 01:44:37 +03:00
ospab 89a5eea20d §C: protocol version gate via key derivation (reject old clients)
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>
2026-06-27 16:57:23 +03:00
ospab 92d6b06d75 §A: remove WSS + Reality (TLS-mimicry); bump to 0.4.0 / AGPL-3.0
Clean-rebuild on the stably-working v0.2.98 base. The project's stealth
path is zapret-like (packet obfuscation / junk / fragmentation), NOT
TLS-mimicry, so WSS and Reality are dropped entirely.

- Delete dead orphan files: ostp-client/src/transport/xhttp.rs and
  ostp-core/src/crypto/reality.rs (never declared as modules → not even
  compiled), plus ostp-core/src/framing/wss.rs.
- Scrub the `wss` transport field from client config/bridge, the unified
  CLI (ostp/src/main.rs), the Tauri GUI backend, the GUI frontend
  (index.html/main.js), and the Flutter UI; also drop the Reality
  pbk/sid plumbing and XTLS auto-search modes from both frontends.
- Drop now-unused client deps (x25519-dalek, chacha20poly1305, hex).
- Bump workspace to version 0.4.0 and license AGPL-3.0; make ostp's
  ostp-core dep path-only so the version bump resolves.
- gitignore ostp-control/ (panel assets built separately; a dummy dist
  is created for the rust-embed build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 16:29:42 +03:00
ospab a9e4511190 Fix CLI setup permissions, enforce global debug tracing, and fix GUI silent startup crash 2026-06-13 01:25:54 +03:00
ospab 29e9ef739c Refactor: Phase 1 and 2 - Async architecture, JNI fixes, SmolTCP data races, and Tunnel optimizations 2026-06-03 02:06:06 +03:00
ospab 8825cf0838 fix: resolve deadlock, multiplexing backpressure, and LTE fragmentation issues 2026-05-30 22:21:12 +03:00
ospab 7986b1ca5b fix(reality): fix TLS 1.3 handshake causing 1KB DPI cutoff on mobile
The core bug: server sent 5 TLS records in server_hello but client only
read the first one (ServerHello), then passed remaining bytes (CCS + fake
records) into RealityStream. RealityStream saw 0x14 (CCS) != 0x17 and
immediately returned an error, killing the connection.

Changes:
- reality.rs: append ChangeCipherSpec after ClientHello (RFC 8446 D.4)
  export REALITY_SERVER_HANDSHAKE_RECORDS=5 constant
- xhttp.rs: drain all 5 server handshake records before creating RealityStream
- uot.rs: rebuild server_hello as proper 5-record TLS 1.3 flight:
  ServerHello + CCS + fake EE (108B) + fake Cert (812B) + fake Fin (52B)
  drain client CCS from raw stream before wrapping in RealityStream
2026-05-29 16:21:59 +03:00
ospab 7656f3a3ce feat: implement custom Reality protocol with ChaCha20Poly1305 and X25519 2026-05-29 15:00:17 +03:00
ospab f4830f043f feat: implement optional WSS framing for DPI bypass & extract framing logic 2026-05-29 13:59:59 +03:00
ospab ba71af2abb feat: implement split-tunneling bypass for TCP/UDP and native UDP NAT 2026-05-29 00:06:11 +03:00
ospab 1b836b26ab Fix Windows TUN NLA delays, UI timer, and Android UDP DNS resolution 2026-05-28 18:19:01 +03:00
ospab 543e36e60e Add session id mismatch error trace 2026-05-28 13:49:33 +03:00
ospab 9ac0908c1e fix(server): generate correct public IP for client configs instead of 0.0.0.0 2026-05-27 18:17:11 +03:00
ospab abcb8999ce fix: integrate BBR cwnd for bufferbloat and relax mobile timeouts 2026-05-26 20:54:30 +03:00
ospab 7c84c17336 fix(core): add raw_len and noise_len to noise-read error 2026-05-21 18:02:42 +03:00
ospab 855ef7655f fix(core): improve UoT tracing and test coverage 2026-05-21 15:54:39 +03:00
ospab ceb760e4ce feat: implement server-side UoT and MTU tuning 2026-05-21 02:23:49 +03:00
ospab a0e38c462e fix: clamp padding size to prevent UDP fragmentation on LTE/cellular and dynamically report connection status 2026-05-18 22:03:33 +03:00
ospab 3a4b5a8c63 chore: fix cargo clippy warnings
- Boxed HandshakeState in NoiseSession to reduce enum variant sizes
- Used is_ok() instead of let Ok(_) pattern
- Applied automatic clippy fixes for minor warnings
2026-05-17 22:22:39 +03:00
ospab 9b01466953 test: integration tests for ProtocolMachine (handshake, data, close, wrong-psk, CC, multi-frame)
8 new integration tests in ostp-core::protocol::tests:
- test_full_handshake: Noise handshake -> Established state
- test_data_exchange_client_to_server: encrypt/decrypt data frame C->S
- test_data_exchange_server_to_client: encrypt/decrypt data frame S->C
- test_close_sequence: Close frame -> Closed state
- test_wrong_psk_handshake_fails: bad PSK rejected, never reaches Established
- test_congestion_controller_after_handshake: CC budget >= 2 in SlowStart
- test_multiple_data_frames: 10 sequential frames, payload integrity verified
- test_tick_no_crash: Tick event stable on both sides

Total: 43 tests, 0 failures
2026-05-17 21:58:01 +03:00
ospab 73f84a951a feat: wire-level 0-RTT Resume frame, subscription API, adaptive pacing integration
Wire protocol:
- FrameKind::Resume (7) for 0-RTT session resumption
- Protocol handles Resume as early data delivery (zero round-trip)

Management API:
- GET /api/subscribe/{key} — returns client config JSON (sub-store compatible)
- Accept: text/plain returns ostp:// share link
- No Bearer token required — key itself is authentication
- ApiState extended with server_host/server_port for link generation

Graceful shutdown:
- Already implemented via wait_for_shutdown_signal() + tokio::select!
- Server drains in-flight frames before exit

35 tests pass, 0 failures, 0 warnings.
2026-05-17 21:42:01 +03:00
ospab 05583e189e feat: v0.2.0 — BBR congestion control, 0-RTT session resumption, management REST API, fallback server, multi-listener
Architecture:
- BBR-inspired congestion controller (SlowStart/ProbeBandwidth/ProbeRTT phases)
- 0-RTT session resumption with anti-replay ticket validation
- Management REST API (axum): /api/users CRUD, /api/server/status, Bearer auth
- TCP fallback proxy for anti-DPI camouflage (nginx/caddy passthrough)
- Multi-listener: bind to multiple UDP addresses simultaneously
- Per-user traffic stats with atomic counters and limit enforcement

Code quality:
- Structured logging: 0 eprintln in server/core/client, all tracing::{info,debug,warn,error}
- 35 unit tests across congestion, resumption, relay, outbound, obfuscation
- Removed dead code: kex.rs, unused dependencies (async-trait, x25519-dalek, rand_distr)
- Modular server: api.rs, fallback.rs, outbound.rs, relay.rs extracted from monolithic lib.rs

CLI:
- --check: config validation
- --generate-key: secure key generation (hex/base64, batch)
- --links: share link generation from server config
- --init: fallback section in server template

Documentation:
- README rewritten with architecture diagram, API examples, CLI reference
- Wiki: Management-API (EN+RU), Configuration (EN+RU), Home (EN+RU) updated
2026-05-17 21:05:44 +03:00
ospab 49d97dbee3 test: add obfuscation round-trip tests, fix i18n module import
- 7 passing tests verify client-server compatibility:
  * Handshake obfuscation round-trip (correct key recovers session_id)
  * Wrong key produces garbage (prevents unauthorized probes)
  * Data packet obfuscation round-trip
  * Deterministic derivation (same key = same secrets)
  * Different keys produce different secrets
  * Legacy API consistency
  * Padding range validation (100 random keys)

- Fixed test module import path to use crate::crypto::obfuscation::*
- Added i18n.js module for GUI localization
2026-05-17 16:27:43 +03:00
ospab 69e4426152 feat: release preparation — TUN fix, i18n, GUI CI/CD, speed improvements
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
2026-05-17 16:25:30 +03:00
ospab a4d8da2460 security: Kerckhoffs's principle — all secrets derived from access key via HKDF
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.
2026-05-17 15:32:07 +03:00
ospab 8abffde0fd security: per-packet handshake masks (eliminates correlation fingerprint)
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
2026-05-17 15:20:21 +03:00
ospab 8fe0589ea6 fix: handshake padding wire format (breaking fix)
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.
2026-05-17 15:16:02 +03:00
ospab 77ec0e3a44 fix: DPI resistance, GUI proxy/tunnel, and code quality
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.
2026-05-17 14:40:13 +03:00
ospab 032f694821 feat: comprehensive diagnostic logging across all modules
protocol.rs:
- Gap recovery logs: skipped frames count, delivered count, remaining buffer
- Duplicate frame detection with nonce values
- Max reorder window exceeded with gap width
- NACK handling: retransmit success vs frame evicted from history
- Reorder buffer overflow with capacity stats
- Close frame receipt
- Zombie frame eviction count
- sent_history overflow (the root cause of speedtest death)

dispatcher.rs:
- New session authentication with peer IP, session count, replay cache size
- Client roaming detection (old addr -> new addr)
- Handshake rejection reasons: timestamp drift, replay cache full, max sessions
- Protocol errors and tick errors with session context

bridge.rs:
- UDP socket buffer diagnostics (requested vs actual)
- Handshake response size and RTT
- Inbound protocol errors with session index
- Outbound packing errors with stream_id

signal.rs:
- Specific shutdown signal identification (SIGTERM/SIGINT/Ctrl+C/Close/Break)

server lib.rs:
- Startup banner with access key count and ARQ config
- UDP buffer diagnostics
- Relay CONNECT/CLOSE/error always visible (not gated by debug)
- All println! -> eprintln! for proper stderr logging
- Hot-reload prefix fixed [ostp-server] -> [ostp]
2026-05-17 14:31:21 +03:00
ospab dc6635e248 fix: tunnel death after speedtest — gap recovery and ARQ tuning 2026-05-17 14:22:50 +03:00
ospab 696d416eff fix: resolve KeyExchange import error and clean compiler warnings
- 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
2026-05-17 03:35:39 +03:00
ospab 7424ccc0ff fix: resolve critical ARQ bugs causing Speedtest tunnel drops + docs overhaul
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
2026-05-17 03:20:50 +03:00
ospab 5bd653e9d2 fix: immediately ACK duplicate packets instead of silently dropping them to unblock client retries when ACKs are lost 2026-05-17 02:56:16 +03:00
ospab 5c33f08a9b fix: resolve fatal connection halt caused by unrecoverable dropped untracked Ack/Nack frames. Control frames are now saved in sent_history without auto-retransmission to allow targeted Nack recovery. 2026-05-17 02:40:52 +03:00
ospab 5c71c6cc9e feat: introduce ciphertext-derived dynamic obfuscation to fully mask the nonce on the wire 2026-05-16 23:58:07 +03:00
ospab ec35769b9f fix: implement non-blocking unbounded channels and clean stream reset on reconnect 2026-05-16 23:41:04 +03:00
ospab b082c158fd fix: throw error on ARQ max_retries exceeded to prevent silent deadlock that caused infinite upload timeouts 2026-05-15 20:04:07 +03:00