Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
15 KiB
Changelog
All notable changes to the OpenFlux core. Format loosely follows Keep a Changelog.
[Unreleased]
[0.3.0] - 2026-10-01
Added
-
phpbox page (
deploy/phpbox): opening an exit's URL in a browser shows a status page (OpenFlux look, light and dark) instead of silently running: live state, a debug log, Start/Stop, and a check that a node is already running on that target (a heartbeat plus a lock), so a second open or a pinger attaches to the first instead of starting another. Anything that is not a browser (a pinger, curl) runs the node exactly as before, and the old?url=/?room=addresses are unchanged. The page readsopenflux://links and draws their QR in the browser with the core's ownsharepackage compiled to WebAssembly (cmd/sharewasm,deploy/phpbox/build-wasm.sh), so there is one link parser for every client; the secret in a link never leaves the browser. -
deploy/phpbox/build-bundle.shbuilds the upload set from the sources (token inconfig.php, which also works whereputenvis disabled). -
Link compatibility tests (
share/compat_test.go): links written by earlier builds are frozen as literals and must keep reading, including how a link is mangled by chats and terminals, unknown JSON fields, and the stability of the error codes. -
phpbox self-renewing tunnel (
&chain=1, what the page asks for): before a generation ends it starts the next one with a request to its own host; the new one joins, takes every new stream, and the old one drains the streams it has. A stream goes to exactly one generation (an atomicmkdirmarker decides while both are up).a=stopends the whole chain. -
--mode=streamlogs like the packet modes:-done line per mux frame ([STREAM] -> 526 bytes - stream 7 DATA),-ddoperational logs (streams opening and closing, a busy carrier),-dddhexdumps of DATA payloads. Until now the level was set after the stream branch had returned, so none of it showed. -
Stream mode is now a library (
streamproxy: carrier -> mux -> SOCKS5 and an optional HTTP proxy, with counters) used by--mode=streamand, next, the mobile bridges;--mode=streamalso takes--http-proxyand--ipc-socket. -
openflux://links and QR codes can name the stream mode (share.Config.Mode,"stream"): one carrier (cups.online or Mail.ru: the two the PHP exit has ports for), no session, no secret. Links without a mode are the classic tunnel as before; codesunknown_mode,stream_transport,stream_one_transport,stream_plain_only. -
phpbox flow control: windows per stream (256 KB) and over all streams (512 KB) with ACK frames, agreed in OPEN (
host:port\0fc/ OPEN_OKfc), so old clients and exits are unaffected. Without it a saturated carrier queue buried small frames: over Mail.ru, four parallel downloads starved new TLS handshakes and uploads for minutes. With it (same test): 60 of 60 handshakes complete while four 8 MB downloads run, and a 4 MB upload takes 33 s instead of timing out. -
Own node without a server:
provision/phphostputs the PHP exit on any web host over FTP and checks that it runs.probefinds the web folder (also a level or two down:domains/<site>/public_html,www/<site>) and whether it is writable,deployuploads the bundle embedded in the core (deploy/phpbox, with the link parser as WebAssembly), keeps the token of an earlier install and checks file sizes,checkasks the site (passing the iFastNet-style AES browser check in plain Go, so no browser is needed),startruns the node and waits for it, plusstop,node,newRoom,link,remove. Answers are codes (ftp_login,ftp_no_webroot,site_antibot,php_missing, ...), never text. One dispatcher,phphost.Call, serves the desktop wizard (--node-wizard, methodsphp.*, with progress lines), the Android bridge (PhpCall,PhpProgress,PhpCancel) and the iOS C API (OpenFluxPhpCall). The node answersa=pingfor it. -
Android and iOS start the stream mode:
StartStreamProxy/OpenFluxStartStreamClient(SOCKS5 with the usual auth and bypass list). -
The node's page shows the link and QR code apps scan for that node (made by the core as a stream-mode
openflux://link). -
Stream mode as a full tunnel (
tunnel.StreamNet): the device's IP packets (utun/Wintun, Android VpnService, an iOS packet tunnel) go into a local stack that opens one mux stream per TCP connection. DNS is answered on the device with fake addresses (198.18.0.0/16) and the name is opened at the exit, so nothing is resolved locally; TCP on ports 80/443 only, QUIC, other UDP and IPv6 are dropped and apps fall back to TCP. It is atransport.Transport, so every packet client runs on it unchanged:--mode=stream --inbound=tun,mobile.StartStreamPacket,OpenFluxStartStreamPacketTunnel.
Fixed
-
The mode-without-a-server node now survives any host's limits and keeps the tunnel up across generations, found on a local emulation of a free host (Apache + PHP-FPM, a 60 s CPU cap, the host's disabled functions, a hidden wall-clock kill):
- a disabled
set_time_limit/ignore_user_abort/getenv/getmypidended the node at once under PHP 8 (calling a disabled function is a fatal error); every such call is now guarded, and the installer'spinglists what the host has taken away. - a generation hands over to its successor early and consistently (about two thirds of the known limit, never later than 45 s) instead of reaching for a longer run: on a host whose real limit we have not seen, aiming high got a generation killed before it had started a successor, and the chain broke. A CPU or wall limit the node has actually hit lowers the handover further.
- a successor takes new streams only once its carrier link has stayed up a few seconds (a Mail.ru document drops the first connections right after they join, and streams handed over in that moment were lost); the client re-asks an unanswered OPEN and the exit answers a repeated one without dialing twice.
- over cups.online the node no longer slept between messages inside its loop (which stalled new connections under a download) and sends only as fast as the server confirms, so the server does not drop the link; two generations sharing one room no longer corrupt each other's data.
- the status page restarts a node that ended on its own while the page is open.
- a disabled
-
--mode=streamwithout--inboundis SOCKS5 again on every OS (0.3.0 took the macOS client to utun, which needs root); the Desktop app names--inbound=socks5for a proxy profile. -
phpbox chain mode did not renew on real hosts: the successor was started by a request its predecessor closed at once, the node wrote its first lines (
joined ...) into that closed connection, and on hosts whereignore_user_abortdoes not hold PHP ends the script at such a write, so every successor died right after joining and the tunnel needed a manual restart at each cap. A successor now writes nothing to its response (a first run only its opening lines); the request that starts it also reads a quick answer (a redirect, the host's browser check, an error page), logs it, and a retry tries the other of http/https. -
Mail.ru transport dropped data under load: the server batches several cursor entries into one message, and only the first was read; a message that merely mentioned a peer's keep-alive was dropped whole. Every entry is delivered now, in order (
cursorPayloads). -
Stream client:
Mux.sendignored the carrier's "write queue full" and dropped the frame, which corrupts the stream (a lost byte inside a TLS record fails the handshake). Sends wait with backoff, for up to 15 s, and report an error instead of losing data;conn.Writepasses it on. -
phpbox WebSocket client: a frame arriving in pieces (a 22 KB message on a slow link) was cut short and desynchronised the stream; frames already in PHP's TLS buffer were not seen by
stream_select; fragmented messages were not reassembled; a closed link was indistinguishable from a timeout, so a node stayed deaf after the server dropped it. Reading is buffered, fragments are reassembled, and the mux reconnects (with backoff). -
phpbox mux: when a run ends, the client is told (CLOSE) about the streams that end with it; streams idle for 300 s are closed (a lost CLOSE no longer leaks a socket); a destination with several addresses is retried on the next one when the first does not answer.
Changed
-
phpbox mux: destinations are dialed asynchronously (a slow one no longer stalls the others), writes to a full destination are queued instead of dropped, and the frame buffer is consumed by offset.
-
The node wizard (
--node-wizard,mobile.Node*) lets a new channel use any mix of a Yandex document, a Mail.ru public document and cups.online rooms besides direct (provision.ChannelTransport); the rooms are created by the app (cupsonline.CreateRoomList) so the node keeps them, and its link, across restarts.provision.ShareLinkbuilds the link from the same priorities and encryption contextnode-install.shwrites to node.conf. -
node-install.sh updateand the optionalopenflux-node-update.timer: the node moves itself to the newestnode-v*release, verified against that release'snode-install.shandSHA256SUMS, and rolls back if a channel does not stay up. An app with an older pinned script no longer downgrades a server the updater has moved on.
[0.2.0] - 2026-09-28
Every client now behaves alike: peers of different builds and modes find
each other instead of dropping every packet in silence. The node wizard
(desktop and Android) installs this core as node-v1.1.0.
Added
- Classic compatibility inside the Session (
PROTOCOL_NEGOTIATION.md): a classic setup with a key (--transport=X, the apps' classic profiles) runs a Session and speaks classic to an exit that does not answer the handshake, switching once it does; a classic-configured exit serves classic and Session clients.--negotiatestays strict; exits configured as a Session (wizard,.conf,--transports) serve Session clients only. - Codec fallback: the classic codec decodes batch-v2 and legacy frames,
sends what the peer sends, and the client tries the other framing when
the peer is silent.
--codecis a preference now, not a requirement. - KDF context fallback: one rule for the context (
transport.KDFContexts) and alternates for what other builds derive; a record that fails under the current keys is tried under them, the exit answers under the client's context, a silent client cycles through them. mobile/ios: the iOS C library (build_ios.sh) on packagemobile, replacing the rootexport_ios*.go: the calls the iOS app makes, plus Session profiles (OpenFluxShareDecodereturns one ready to start), mode and captcha calls. An app that links this core as a submodule gets the same Session, links and fallbacks as Android.mobile:ShareSessionSpecs,SetInitialCookies,SetLowMemory(Volga's newSlimVolgaConfigfor the iOS extension),ReadTimeout,ConnectionMode.- Links are read and made by the core only.
share.Read/share.Makeanswer every entry point with the same JSON (--parse-link, new--make-link,mobile.ReadShareLink/MakeShareLink, the iOSOpenFluxShareDecode/OpenFluxShareEncode): the configuration and its context, the link, or an errorcode(andparam) that the apps put in their own words.share.Makenormalizes what apps spell differently (the default codec is left out, an encrypted link always names its context, by the one rule when not given), so one configuration gives one link on every client; the node wizard's link, desktop and Android, comes from oneshare.NodeConfig. - Logs a user can act on without
-dd: key or context mismatch, the peer running the other layering, codec and context fallbacks, a second client taking over the exit, carriers failing to start, documents dropping, and a diagnosis when the handshake does not complete.
Fixed
- A classic cupsonline client given the rooms as
--urlderived another key than the exit that created them: nothing got through. - boards sent engine.io pings from the client, which an EIO=4 server answers by closing the socket: the board dropped every 20 seconds.
- A Session client's cupsonline carrier was built as an exit: with no or dead rooms it created rooms of its own and waited in them.
- openflux:// links: base64 padding, the standard alphabet, whitespace and line breaks are accepted; secrets are counted in characters as Kotlin counts them, not bytes.
- Carrier names that differ between the two sides no longer break cookie exchange and exit checks (messages carry the document URL).
- yandex / mailru: a socket whose keepalive failed is closed so the reconnect runs; writes are bounded.
utils.Infofreaches the apps' log screens.- A Session stopped each carrier twice.
[0.1.0] - 2026-09-27
First release from the current main line (encrypted-logging + the
maintainer's multi-transport work folded together) and the first cut by
release.yml instead of a manual build.
Added
mobile/: the Android/iOS gomobile bridge now lives in this repository (moved frommeepo161/openfluxfork, full history and authorship preserved), so building the mobile clients needs only this checkout..github/workflows/release.yml: av*tag cross-compiles the CLI for Linux (amd64/arm/arm64), Windows (386/amd64/arm64) and macOS (amd64/arm64) and publishes it withSHA256SUMS.txt. Replaces the ad-hoc manually-built0.0.xreleases.deploy/node-install.shnow downloads the exit-node core from this repository's ownnode-v*releases instead ofmeepo161/openfluxfork;provision/pin.go's pinned script commit/hash points here too.
Fixed
main.go: bench-send/bench-sink never resolved to the exit-side session role under--negotiate, so a negotiated session between two bench processes hung retrying the handshake and derived encryption keys in the same direction on both sides instead of swapped.main.go: the startup banner still printed the project's pre-rename name (=== Universal Bypass Tool ===) instead of=== OpenFlux ===..github/workflows/node-release.yml: the pinned Go version (1.26.8) had drifted fromgo.mod(1.26.4), so its "reproducible" build didn't actually reproduce the hashesdeploy/node-install.shexpects.
Credits
androidApp/desktopApp/shared for OpenFluxAndroid
and OpenFluxDesktop now
run the Compose Multiplatform app built by @meepo161
in OpenFluxClient, moved into
those repositories with his agreement.
[0.0.1] - [0.0.5]
Manually built and published cross-platform CLI binaries, before this CHANGELOG and the automated release workflow existed.