Both installers still invoked `ostp --setup` / `ostp.exe --setup` to launch
the first-run wizard on a fresh install. The CLI's subcommand refactor
(2026-07-08, "Refactor CLI to subcommands") turned `setup` into
`Commands::Setup { .. }` with no top-level `--setup` flag left in Args at
all, so every fresh install has hit "error: unexpected argument '--setup'
found" and dropped the user out of the installer instead of the wizard.
Verified `ostp setup --help` parses correctly with the fix.
Matches a real report: traffic counters move (the OSTP tunnel handshakes
fine) but sites don't open, or take very long - on a freshly deployed
DigitalOcean droplet in Amsterdam.
connect_target's fallback path handed the raw "host:port" string straight
to TcpStream::connect, which resolves and tries addresses internally but
shares ONE 10s timeout across the WHOLE attempt (all resolved addresses,
not per-address). Some VPS hosts assign the machine an IPv6 address that
the OS prefers by RFC 6724 ordering but that has no actually-working
outbound route - the connect doesn't get refused, it just hangs. With a
single shared budget, that one dead IPv6 candidate eats the entire 10s and
the working IPv4 candidate is never even attempted: every dual-stack
destination (i.e. most popular sites) times out, while IPv4-only
destinations work fine.
New connect_direct() resolves target itself via lookup_host, sorts IPv4
candidates first, and tries each with its own 3s budget (still bounded
overall by the original 10s outer timeout as a backstop) so a hung IPv6
attempt can't starve the IPv4 fallback of a chance.
Added tests: IPv4-first sort ordering (and stability within a family), a
successful connect against a live local listener, and a refused-port
connect failing well under the timeout (proving failures aren't
needlessly slow). Scoped to connect_target's direct-connect paths; the
SOCKS5/HTTP outbound-proxy paths and the fallback/camouflage TCP proxy
(which targets a fixed admin-configured local address, not arbitrary
dual-stack hostnames) are unaffected.
These were editable in two disconnected places: the profile editor modal
(pm-* fields, written into each saved profile's own tcp_fragmentation/
frag_chunk/frag_sleep/junk_pc/junk_ps) and the simple settings page (cs-*
fields, a global override applied at connect time via buildConfig()'s
merge: `s.tcpFrag || active.tcp_fragmentation`, etc). The simple settings
page already covers the same knobs, so the modal copy was pure duplication
and a source of confusion about which one actually took effect.
Removed the pm-tcp-settings panel and its fields from index.html, and all
now-dead JS: the variable lookups, the open-editor populate/reset logic,
the save-profile field writes (existing profiles keep their previously-
saved values via the {...profiles[idx], ...} merge - only new edits
through this modal no longer touch these fields), and the two change
listeners whose sole job was showing/hiding the removed panel. The
Transport (UDP/UoT) dropdown itself is untouched.
Matches a real user report after 2 weeks on this version: bandwidth is low,
the client reconnects every 10-20 minutes, sites randomly stop loading or
crawl, and ping visibly jitters. Root cause: the per-target-connection
reader task (handle_relay_message's Connect handler) read from the
upstream target as fast as it would send and forwarded every chunk
straight to send_relay_to_stream -> an immediate UDP datagram, with ZERO
awareness of the client-facing OSTP session's actual congestion window.
The client already gates its own uplink on cwnd (bridge.rs's proxy_ev
select arm); the server's download direction had no equivalent.
On a real (lossy/jittery mobile or Wi-Fi) client path, a fast target (a
CDN, say) gets blasted at the client far beyond what the path can sustain.
That's a self-inflicted loss burst: it wrecks the RTT/RTO estimate (the
"ping jitters" symptom), can push the session into a stall bad enough that
the client's 25s/180s keepalive stall-detection gives up and reconnects
(the "every 10-20 minutes" symptom), and produces exactly the "randomly
stops loading or crawls" experience while it's happening.
Fix: Dispatcher::snapshot_backpressure() computes each session's headroom
(clamped cwnd - in_flight, same clamp(16,16384) the client uses) from the
existing 10ms retransmit tick - no new polling loop. Published to a
lock-free-reader Arc<AtomicI64> per session in a shared map so relay reader
tasks (which don't have access to Dispatcher; it lives on the main loop
task) can check it without touching a lock/mutex on every read. Before
each read, a reader task waits (capped at 2s, so a wedged read can't stall
forever) while headroom is <= 0. New sessions default to a healthy 32
packets until their first snapshot lands (worst case 10ms), so this can't
stall the very first bytes of a fresh connection.
Scoped to the primary TCP CONNECT/proxy path (the one actual web browsing
uses); UDP relay/TURN is unaffected. No load test against real network
jitter was possible in this environment - the mechanism directly targets
the identified cause, but real-world confirmation is still needed.
Both installers still invoked `ostp --setup` / `ostp.exe --setup` to launch
the first-run wizard on a fresh install. The CLI's subcommand refactor
(2026-07-08, "Refactor CLI to subcommands") turned `setup` into
`Commands::Setup { .. }` with no top-level `--setup` flag left in Args at
all, so every fresh install has hit "error: unexpected argument '--setup'
found" and dropped the user out of the installer instead of the wizard.
Verified `ostp setup --help` parses correctly with the fix.
beta had two "chore: release 0.4.5-beta"/"0.4.6-beta" commits (pure
Cargo.toml/tauri.conf.json/package.json/pubspec.yaml/.release-state.json
version bumps, no code) from the pre-gha.ps1-rewrite versioning scheme
that was abandoned when the project reset to 0.4.1 as the new baseline
(see the earlier master reconciliation). alpha's manifests are about to
be bumped to the new 0.4.2 target anyway, so alpha's side wins on the
conflicting version files - this is a pure reconciliation, not a content
decision.
scripts/gha.ps1's -Branch ValidateSet accepted 'pre-release' and would
`git checkout pre-release` to promote alpha, but no such branch has ever
existed in this repo — only `beta` does (confirmed: `git branch -a`, and
the existing 0.4.6-beta/0.4.7-beta release history was cut from `beta`).
The very first beta release under the new gha.ps1 versioning scheme would
have failed outright on the checkout step.
This naming mismatch had spread through the whole release surface:
- scripts/gha.ps1: -Branch ValidateSet + all internal checks
- .github/workflows/release.yml: a dead branch-name check (harmless only
because the workflow currently triggers on tag-push, not branch-push)
plus two comments
- CONTRIBUTING.md / .ru.md: branch-strategy table documented a
`pre-release` branch that doesn't exist
- README.md / .ru.md and ostp/src/main.rs: the `ostp update -b <name>`
CLI help text/docs
- scripts/install.sh: the channel match the CLI flag feeds into
Renamed all of it to `beta` to match the branch that actually exists.
Left scripts/gha.ps1:21's "semver pre-release identifier" alone — that's
the generic semver spec term, unrelated to the branch name, and got
reverted after a blanket replace briefly clobbered it.
Note: install.sh's alpha/beta self-update paths still assume a rolling
GitHub release tagged literally "alpha"/"beta" exists, which no gha.ps1
release ever publishes (only versioned tags like v0.4.7-beta.3) - that's
a separate, real bug, tracked apart from this rename since fixing it needs
either a floating tag from gha.ps1 or an API-query rewrite of install.sh.
Brings the crypto security audit into alpha alongside the already-merged
reconnection/UoT/Flutter fixes:
- CRITICAL: transport keys now come from Noise Split() over ck (DH-
inclusive), not the handshake hash — restores forward secrecy.
Wire-breaking, PROTOCOL_VERSION 4->5.
- Rate-limit + cache the O(N_keys) handshake trial path (CPU DoS).
- Quiet hot-path logging; access keys no longer logged verbatim.
- Removed dead 0-RTT resumption module (unsafe XOR ticket crypto).
- Karn's algorithm RTT fix, 32-bit frame-length overflow guard,
replay-cache eviction instead of global reject.
- Updated EN/RU specification docs to match.
No conflicts: protocol-hardening's bridge.rs commit was an independent
duplicate of the same reconnection fix already on alpha, so git merged it
as a no-op on that file.
Junk packets and TCP fragmentation only take effect on the UoT (TCP)
transport — the UDP path applies neither — so showing them (with an
implicit "UoT only" caveat) while UDP is selected was misleading. The
whole DPI OBFUSCATION section is now gated on transportMode == 'uot' and
appears/disappears reactively when the Transport dropdown changes
(setDialogState already rebuilds the dialog).
Two UoT (UDP-over-TCP) correctness issues:
- The accepted UoT stream never had TCP_NODELAY set (the client sets it on
its end, the server didn't). Nagle's algorithm then batched server->client
writes and interacted with the client's delayed ACKs, adding tens-to-
hundreds of ms of stall per burst — throttling the download direction
badly for streaming/video. Every TCP-tunnel proxy disables Nagle; now the
server matches the client.
- handle_tcp_connection join!ed the reader and writer tasks, so a half-open
connection (client's read side gone, no outbound data pending) parked the
writer on rx.recv() forever, leaking the task and a stale tcp_map entry.
Rewrote it with select! so either half closing cancels the other and the
tcp_map entry is always removed. Added duplex-stream tests covering
inbound reassembly across segment boundaries, outbound framing, and
teardown-on-close.
Addresses the PC-after-sleep failure (app either fully disconnects or gets
stuck "Connecting") and the mobile "must reconnect manually" symptom.
- Zombie receiver tasks: each session spawns a task that loops on recv().
On a dead connection recv() never returns, so the task (and the socket it
holds) leaked on every reconnect, piling up across sleep/resume cycles.
SessionState now owns the task's AbortHandle and aborts it on Drop, so
replacing sessions tears the old task down. The three duplicated inline
receiver loops are consolidated into spawn_session_receiver().
(Builds on the tick-storm and resume-detection changes already in tree:
MissedTickBehavior::Skip on all intervals so a post-sleep wake doesn't fire
tens of thousands of catch-up ticks — the 10ms retransmit tick was the
worst — and a wall-clock-gap check that forces one clean reconnect on wake
via handle_keepalive(force=true).)
The vendored netstack-smoltcp directory was removed, but the workspace
[patch.crates-io] entry still pointed at the now-missing path, breaking the
whole build. ostp-client already declares netstack-smoltcp = "0.2.2", so
dropping the patch simply builds against the published crate (0.2.4).
Reflect the crypto hardening in the EN/RU specification:
- Section 6: transport keys now come from Noise Split() over the chaining
key ck (includes the ee DH secret), giving forward secrecy; added the
rationale for why keys must NOT come from the handshake hash h, and the
wire-version-5 gate.
- Section 8: documented the handshake-trial CPU-DoS defense (per-key
secret/marker caching + trial-path token bucket).
- Corrected the handshake replay window (±300s / 5min, was mis-stated as
±30s) and PSK derivation (HKDF-SHA256).
- 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.
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.
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.
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.
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.
A script parameter named exactly $Switch collides with PowerShell's `switch`
statement keyword - confirmed by bisection - and made every `$X = switch (...)
{...}` in the script silently evaluate to empty instead of erroring. This is
what produced the malformed "v0.4.1-.0" tag on the last release attempt
(Channel resolved to "" instead of "stable", Iteration to 0). Renaming the
parameter is the only fix; nothing else about the switch statement itself
was wrong.
master had UAC/SmartScreen fix, RTT/speed-display toggle, and a run-name CI
tweak that never made it back into alpha. Alpha already independently
contains equivalent (UAC fix is byte-identical) or superior (run-name
handles the newer alpha/beta/nightly channel scheme master's version
doesn't know about) versions of all three, so this merge is a pure
reconciliation - alpha's side wins on every conflicting hunk.
No source ever existed in this tree for it - only a built dist/ and a
full node_modules/ dump (6200+ files), and nothing in the codebase
references "ostp-license" anywhere. Leftover from the old commercial-
license-gated era before the AGPLv3 switch; pure bloat since.
Also added **/node_modules/ to .gitignore - its absence is exactly how
this got committed in the first place.
'ostp prober' was never a real subcommand - ostp-prober is a separate,
gitignored standalone tool, not part of the ostp binary's CLI surface
(no Prober variant in the Commands enum, no handler in main.rs). Also
fixed misaligned columns on the proxy-env/proxy-env-clear lines.
The repo switched to AGPLv3 back on 2026-06-18 (commit 9ce9e6d), and
Cargo.toml/README have said AGPL-3.0 ever since — but that license-change
commit was never carried forward into the 0.4.x rebuild branch, so the
actual LICENSE file silently reverted to the pre-rebuild BSL 1.1 text
(with a "converts to MIT in 2030" clause that hasn't applied for months).
Restored the real AGPLv3 text from 9ce9e6d.
Also added the missing `license` field to a few crate manifests that
didn't declare one (ostp-gui/src-tauri, ostp-jni, ostp-tun-helper), and
dropped the Tauri template placeholder authors/description.
Traced the whole traffic-counter pipeline (Dart -> MethodChannel ->
Kotlin -> JNI -> Bridge) end to end; it's architecturally identical to
the working desktop implementation, so no code-level bug was found.
Previously a getMetrics exception was only reported as a PlatformException
that Dart swallows with a bare debugPrint, invisible in the in-app log
viewer users actually have access to. Now it's also written to the
native log buffer via OstpClientSdk.addLog, so if the counter breaks
again the actual cause (exception vs. genuinely-zero atomics) shows up
in View Logs instead of requiring adb.
Mirrors ostp-gui's "Show Speed" / "Show RTT" client settings toggles
(both default on): the home screen now shows live download/upload
throughput (computed from byte deltas between 1s polls, same as
desktop's poll()) as a subtitle under the existing cumulative
Download/Upload totals, and the RTT box is now hideable. Also fixed
"Test Ping" to actually query getMetrics instead of just faking a
500ms spinner with no real measurement.
stealth_sni was never actually consumed to construct any wire bytes —
verified dead in bridge.rs (only stored, never read). It implied
TLS/HTTP SNI mimicry that this project deliberately does not do
(zapret-like: packet-level DPI obfuscation only, no protocol
mimicry). Removed from the runtime schema (config.rs, bridge.rs),
both CLI/GUI local config shapes and their JSON templates, the
Flutter profile model/UI/share-link logic, and README feature docs.
migrate.rs now drops the field from legacy configs with a note
instead of carrying it forward.
- Profile edit dialog: moved junk packets + TCP fragmentation into their own
modals (tap-to-configure), replacing 5 inline field rows with a compact
2-button row. These are occasional/advanced settings, not something every
profile edit needs to see up front.
- Profile card: subtitle repeated the server address verbatim whenever a
profile had no custom name (name falls back to serverAddr) — showing
"1.2.3.4:50000" as both title AND subtitle, with transport mode tacked on
the end of the second copy. Now only shown once; added maxLines/ellipsis
so long addresses truncate instead of wrapping awkwardly.
- Mobile: removed the "Bypass Processes" field entirely (editor UI, prefs
key, config JSON). Android per-app selection (Configure Split Tunneling)
is the real, correct control here — a process-name text field doesn't map
to anything meaningful on Android the way it does on desktop.
- Share icon changed from a QR icon (redundant — the modal already shows a
QR code) to the standard Material share glyph. Share modal title no
longer interpolates the profile's name, which — same root cause as
above — can silently BE the raw server address; title is now generic
("Share Profile") so a screenshot/recording can't leak host:port through it.
- Contrast: the monochrome theme's colorScheme.primary is pure white
(0xFFFFFFFF); several buttons hardcoded white text/icons on top of it
(Bypass/Proxy mode toggles, Copy Link), making them invisible when active.
Added an _onColor() helper (luminance-based black/white pick) and applied
it everywhere a button's foreground sits on a theme color.
- "Configure Split Tunneling" appeared to hang for 10-15s before doing
anything: MainActivity.kt's getInstalledApps handler enumerated every
installed package AND decoded+re-encoded each one's icon synchronously
inside the MethodChannel callback, which runs on the main/UI thread by
default — blocking it for the whole duration meant Flutter couldn't
render ANY frame, not even the loading spinner, until it finished. Moved
the work onto a background Thread; only the final result.success() hops
back via runOnUiThread(). Navigation + spinner now show immediately.
assets/logo.png had NO real alpha transparency — both the background and
the eagle shape were fully opaque (A=255 everywhere), just baked in as
near-black (3,3,3) vs near-white (253,253,253) RGB. Applying `color:
Colors.white` to tint it painted the WHOLE bounding square white (alpha
being 255 across the entire image gives BlendMode nothing to mask against),
which at low Opacity looked like a flat gray square instead of a silhouette.
Converted the asset in place: since it was already grayscale (R=G=B), each
pixel's luminance became its new alpha channel, RGB set to pure white. The
background (near-black, low luminance) is now near-transparent; the eagle
(near-white, high luminance) is now near-opaque. This is the same effect the
desktop GUI gets for free from its logo.svg (a vector eagle path with no
background element at all — inherently transparent), just reproduced for a
raster asset without adding flutter_svg as a new dependency.
The `color: Colors.white` tint in both watermark call sites is now
redundant (the asset is already a pure-white silhouette) and removed.
Previous scheme conflated "which release is this" with "how many times has
it been rebuilt": every run bumped the patch version, so by the time a build
was ready to promote to master the version number had already crept forward
by however many alpha/beta iterations it took to get there.
Now a release cycle has one fixed target version (e.g. 0.4.1) that stays in
every manifest unchanged through all alpha/beta iterations; only a
per-channel counter increments, and that counter lives ONLY in the git tag,
never in Cargo.toml:
v0.4.1-alpha.1 -> v0.4.1-alpha.2 -> ... -> v0.4.1-alpha.N
v0.4.1-beta.1 -> v0.4.1-beta.2 -> ... -> v0.4.1-beta.N
v0.4.1 <- master, iteration dropped
Deliberately "0.4.1-alpha.N" (dot AFTER the hyphen — a semver pre-release
identifier), not "0.4.1.N-alpha" (a 4th dot component before the hyphen):
the latter isn't valid semver and Cargo's version parser rejects it outright,
so it can never appear in Cargo.toml. That's also why the target version
itself never needs to change on a plain iteration — bumping every manifest +
refreshing both Cargo.locks is now skipped entirely unless -Switch actually
changes the target, making a routine alpha/beta push fast (just the state
file's counter + a tag).
Also fixes a real bug found while touching this: release.yml's push trigger
is tags-only ("v*") with no branch trigger, so the old `git push origin
$branch`-only path for alpha/pre-release never actually started a CI run —
only the master path (which already pushed a tag) worked. Every channel now
always pushes a real tag, which is what actually triggers the build.
release.yml's resolve-channel needed no changes: its tag-channel detection
already does substring matching (*-alpha*/*-beta*), so it classifies
"v0.4.1-alpha.37" correctly without modification.
-Prefix is gone — channel was always 1:1 with -Branch (alpha/pre-release/
master), so it was a redundant, independently-settable axis that could
silently drift from the branch (e.g. -Branch alpha -Prefix beta).
Merges the best of both lineages instead of a blind revert to v0.3.21:
kept from v0.3.21: multi-profile management (add via QR scan/link/manual,
single-select active profile, auto-mode transport/MTU probing). Kept from
current: Share Config (QR generation), Check for Updates, curated stealth-SNI
domain list, and actually-rendered exclusions fields (v0.3.21 loaded/saved
them but never showed them in the UI — dead code).
New: junk packets (pc/ps min/max) and TCP fragmentation (chunk/sleep) are now
per-profile fields in the profile editor, mirroring the desktop GUI's profile
object shape 1:1 (ostp-gui/src/main.js) so behavior matches across platforms.
WSS is gone — removed from the model, the UI, and the config builder. The
core dropped TLS-mimicry transports entirely (see §A of the rebuild), so
there was nothing left for it to configure.
Config building now targets the flat single-server schema
(ostp_client::config::ClientConfig) built from ONE active profile, not the
old modular inbounds/outbounds/urltest-failover config — the core no longer
supports connecting to multiple servers at once, matching how the desktop
GUI already works (single activeId). Also dropped a dead nested "tun": {...}
object that neither version's JSON producer nor the real ClientConfig struct
ever actually used — serde silently ignored it.
resolve-channel treated EVERY v* tag as stable, so v0.4.6-beta got published
as a non-prerelease "Latest" release, sitting on top of the release line. Now
a pushed tag is used as-is and its suffix decides the channel: v*-alpha / v*-beta
are prereleases, only a bare vX.Y.Z is stable. (A tag is never recomputed from
Cargo.toml, so the release can't upload to a different tag than the one pushed.)
Version bumped 0.4.5 -> 0.4.7 (0.4.6 is already taken by the mislabelled beta).
This commit's tip is what gets tagged v0.4.7-beta to cut the beta build.
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).
Every process (CLI daemon, GUI, TUN helper) and every subsystem (tracing, the
core event logger, the helper IPC, panic hook) wrote its own file: ostp-cli.log
+ ostp-core.log + ostp-helper.log + ostp-crash.log — a pile per run. Now they
all funnel into a single ostp.log next to the exe.
- logging: LOG_FILE_NAME/log_file_path() as the one source of truth; init_tracing
gains a `truncate` arg. Truncation is gated twice: Windows-only (cfg!(windows))
AND daemon-only. One-shot commands (gk/check/init/-V/...) and the elevated TUN
helper pass truncate=false so they can never wipe a running daemon's log;
invocation_is_daemon() detects the daemon from argv. On Linux the server always
appends (history kept, OS-rotated) as requested.
- runner/helper manual writers + panic hook now target log_file_path(), so their
output lands in the same ostp.log instead of separate files.