All three on-disk config.json shapes (client, server, relay) used to be
declared locally inside ostp/src/main.rs, invisible to any other consumer —
which is exactly how ostp_client::migrate ended up matching against loosely
typed serde_json::Value instead of a real schema, with no guarantee its
hand-built output actually matched what the CLI parser expected.
Moved every one of those definitions (AppMode, UnifiedConfig, ServerConfig,
RelayServerConfig, ClientFileConfig, TunConfig, ExcludeConfig, MuxConfig,
TransportConfigRaw, ApiConfig, FallbackCfg, ListenConfig, UserConfig) into
ostp_client::config — the same file that already held the runtime
ClientConfig/OstpConfig/etc. main.rs now imports them instead of
re-declaring them (`ClientFileConfig as ClientConfig` to avoid colliding
with the runtime ClientConfig, which stays separate on purpose: it's the
engine's internal shape — handshake/io timeouts and the like a user never
sets in config.json — built FROM one of these via the mapping in
run_client_directly, not the same thing).
ServerConfig.dns is now Option<serde_json::Value> rather than
Option<ostp_server::dns::DnsConfig> — ostp-client doesn't (and shouldn't)
depend on ostp-server just to name that type. main.rs, which already depends
on both crates, deserializes it right before handing it to run_server().
cmd_migrate now proves its output against this schema before ever writing
to disk (serde_json::from_value::<UnifiedConfig>(migrated)) — a migrator/
schema drift is now a hard error at migrate time, not a corrupted
config.json discovered later. Added a matching unit test
(every_migrated_output_matches_the_canonical_schema) that exercises this
same check on all three migration paths (modular, legacy-flat, server).
- fix(cli): stop printing the startup banner ("ostp-cli vX.Y.Z | OS: ...")
to stderr on every single command invocation. init_tracing() ran
unconditionally before command dispatch, so `ostp -V`, `ostp gk`, etc. all
showed it. It's still written to the log file (useful there), just no
longer echoed via the stderr tracing layer for one-shot commands.
- feat(client): add ostp-client::migrate, the ONE place config migration
runs. Previously there were three uncoordinated migration paths: a Python
snippet embedded in scripts/install.sh (only touched server api.* fields,
ran on every update), the old 0.3.x line's auto-migration on every hot
reload (silent besides a log warning), and nothing at all for the current
rebuild. Consolidated into one module covering every config shape that's
actually existed:
- v0.3.1-v0.3.21 modular (inbounds/outbounds/routing) -> current flat
schema, including correctly resolving routing.default_outbound through
a urltest/selector group to the real server, and reporting (not
silently dropping) every additional server a multi-server config had.
- pre-0.3.1 flat configs carrying now-dead fields (tun.wintun_path,
tun.ipv4_address, transport.wss) -> dropped with an explicit reason,
everything else passes through untouched.
- server configs -> backfills api.* defaults and drops legacy api.token
(ported straight from the install.sh Python, same behavior, correct
place).
6 unit tests cover all of the above against realistic fixtures. Wired up
as `ostp migrate` (was missing from Commands entirely) — no other code
path calls into this module, so a config's shape only ever changes when
explicitly asked.
- feat(cli): `ostp import <url>` now asks the same TUN/mux/debug questions
`ostp connect <url>` always did. Previously import just wrote flat
defaults to disk with no way to turn any of that on short of hand-editing
the resulting config.json afterward. Extracted the shared prompt into
prompt_client_options() so both paths stay in sync.
- chore(install): remove the embedded Python config-migration snippet from
install.sh; schema migration must never happen implicitly during an
install/update. Points users at `ostp migrate` instead.
run_client_core previously ran once: if the OSTP protocol connection, the
TUN device, or the local proxy listener ended for any reason (network
change stranding the socket/adapter on a dead interface, a transient
crash, a drop the inner Bridge-level "TunnelStopped" retry couldn't
recover from), the whole client returned/errored and just stayed down.
Wrapped the existing body (now run_client_once) in an outer supervising
loop: any non-shutdown-requested exit triggers a full clean restart —
fresh DNS resolution, fresh Bridge, fresh TUN/proxy — with backoff
(1/2/5/10/20/30s, resetting once a run has been stable for 60s). Only an
explicit shutdown request stops the loop. connection_state reports
"connecting" during the retry wait so the UI shows reconnecting, not
disconnected.
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>
Anti-DPI obfuscation the project wants to keep, ported from 0.3.x with the
harmful UDP behaviour designed out from the start.
- Junk: before the handshake on a UoT/TCP connection, send 2-5 random
length-prefixed frames (100-1000 B). The server reads each as a frame,
fails to authenticate it, drops it and keeps reading (drop-and-continue),
so junk perturbs DPI flow analysis without breaking the connection. Junk
is NEVER sent over UDP — there each junk would be a lone datagram
indistinguishable from a port scan (probe-flood / wasted CPU / the very
"self-ban" risk the plan calls out). Verified the server has no
probe-based ban, and the unauthorized-probe log is already rate-limited
(§B), so junk-over-UoT produces one debug line, not a flood.
- TCP fragmentation: new `transport.tcp_fragmentation` flag (default off).
When set, the writer splits the first real frame (the handshake) — length
header byte-by-byte then payload in 2-byte chunks with short gaps — so DPI
can't classify the handshake from a single read.
- Ranges are hardcoded for now; §E fine-tuning (configurable Jc/Jmin/Jmax,
S1/S2, H1..H4) is deferred.
Verified by loopback E2E: a UoT client with tcp_fragmentation=true connects
(junk logged as one rate-limited probe, then real handshake accepted) and
curl via SOCKS5 tunnels HTTPS successfully.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "system" TUN stack that shelled out to a bundled tun2socks binary is
long-dead — Flutter already hardcodes the native OSTP stack — and only
bloats the build.
- ostp-jni: drop the tun2socks spawn branch and the tun_child handle;
the native TUN (run_native_tunnel_from_fd) is now unconditional. The
JNI signature is kept ABI-stable (t2sBinPath/localProxy retained but
ignored) to avoid breaking the Kotlin linkage without an Android build.
- Delete the committed 10 MB tun2socks-arm64 asset; drop the tun2socks
download steps from the Android build scripts.
- Remove the dead tun2socks.exe entry from the desktop build_dist.js
(it required a file nothing downloads, breaking the GUI dist build).
- Reword stale tun2socks references in proxy.rs, the GUI config comment,
install.ps1, the release workflow matrix, and CONTRIBUTING.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
When using xhttp (UoT) mode on Android, the underlying TcpStream was
not protected with VpnService.protect(fd). This caused the TCP connection
to be routed back into the TUN interface, creating an infinite routing
loop and failing the connection immediately.
Added Android-specific socket protection to the TcpStream in connect_xhttp.
This fixes xhttp/UoT mode on mobile networks.
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