Commit Graph
8 Commits
Author SHA1 Message Date
2784b04240 Links are read and made in the core: one answer and one export for every client (#127)
* share: links are read and made in the core, with codes for the apps

share.Read and share.Make answer every entry point with the same JSON
(--parse-link, new --make-link, mobile.ReadShareLink / MakeShareLink, the
iOS OpenFluxShareDecode / OpenFluxShareEncode): the configuration and its
context, the link, or an error code and param. Every link problem now has
a code (share.Code*); the apps word it for their users, the core only
decides which one it is. Error() keeps the English detail for the CLI.

share.Make normalizes what apps spell differently: surrounding spaces, the
default codec, a carrier named after its type, and the context of an
encrypted link, filled in by transport.KDFContexts when not given. One
configuration gives one link whichever client exports it. The node
wizard's link (desktop and Android) comes from one share.NodeConfig, and
--share and ExitShareLink go through share.Make too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* share: a lone carrier's link carries no priority

Apps that keep a priority for a single carrier and apps that do not now
export the same link for the same profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: p1neappleXpress <a@a.a>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:53:31 +03:00
f8f34767a5 One protocol for every client: Session with classic, codec and context fallback (#126)
* utils: status lines reach the app log; count secrets as Kotlin does

Infof now also goes to the log sink, so the Android log screen and the
iOS ring buffer show the status lines the CLI prints. Throttled rate-limits
warnings that would repeat on every packet. SecretChars counts a secret in
UTF-16 units: the core counted bytes, accepting a 10-letter Cyrillic secret
that Desktop and Android reject.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* transport: one KDF context rule, and fall back when the peer's differs

KDFContexts replaces the copies of the context rule that had drifted apart
(CLI, Android bridge, Desktop, iOS). A classic cupsonline client given the
rooms as --url now derives the placeholder like the exit that created them;
it used the room list, and no packet ever decrypted.

The context is a public salt, so a peer may try several: alternates are
what other or older builds derive. A packet that fails under the current
keys is tried under them (derived lazily); the exit answers under the
context the client used, a client that hears nothing cycles through them.
A mismatch that dropped everything in silence now heals itself, and a key
that really differs is reported in the log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* transport: classic codec that accepts both framings and falls back

batched against legacy dropped every packet in silence. CodecTransport
decodes both (they differ in the first byte), sends what the peer sends
(batched once it has seen a batch frame, including the iOS fork's empty
probe), and the client tries the other framing while the peer is silent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* session: classic compatibility on the same carrier, diagnostics

The classic and Session layerings differ in the first byte of a carrier
frame, so frameDemux splits one carrier between a Session and a classic
pipeline sharing its keys. SetClassic turns it on: a single-carrier client
falls back to classic while the exit does not answer the handshake and
switches once it does; a classic-configured exit also serves classic
clients while no Session client is active. Strict Sessions log classic
frames with the fix instead of dropping them silently.

Logs: carrier start failures, takeover by another client, mode switches,
and a handshake diagnosis (nothing arrived / key differs / exit is classic).
The carrier is stopped once instead of twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* manager: match the peer's carriers when names differ

The apps name carriers after their type (mailru, mailru-2) whatever the
exit's .conf or panel called them, so a cookie offer or AuthRequired for an
unknown name went nowhere. Cookie messages carry the document URL now, and
the name is matched by it, then by type when this side has one carrier of
it; what cannot be matched is logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cli: classic setups with a key run as a Session with classic fallback

--transport=X with a key now builds a Session: a client speaks classic
until the exit answers the handshake, an exit also serves classic clients.
A single-carrier --transports/.conf client falls back the same way; only
--negotiate is strict. Without a key the classic path stays, with the
adaptive codec. The context comes from transport.KDFContexts, alternates
included, and a cupsonline exit accepts its room list as one.

transportFactory takes the role: a Session client's cupsonline carrier was
built as an exit and, with no or dead rooms, created rooms of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mobile: same Session, context and codec fallback as the CLI

Classic profiles with a key build a Session with classic fallback, as the
CLI does, so a classic profile works against every node; classic direct
works too. Single-carrier Session profiles fall back to classic; the node
wizard's check stays strict. The context rule is transport.KDFContexts
(the link's context first, the derived one as an alternate).
SetInitialCookies applies cookies got before the start to Yandex carriers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* share: read links the way every client does; --parse-link

Decode accepts what copying and other encoders do to a link: whitespace and
line breaks, base64 padding, the standard alphabet, and says what is wrong
otherwise. Secrets are counted in characters as Kotlin counts them.
--parse-link prints the core's reading of a link as JSON, for apps and
debugging; --share also prints the bare link on its own line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* boards: stop sending client pings; docs: reconnect on dead sockets

boards sent engine.io pings from the client, which an EIO=4 server treats
as an invalid heartbeat and closes the socket: the board dropped every 20
seconds. The server pings and we answer, as before.

yandex and mailru left a socket whose keepalive failed open, so a reader
on a half-open connection could wait forever; they close it now, and bound
each write. Drops and failures to open the document are logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* transport: tests for mixed builds (codecs, contexts, classic vs Session)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ios: the iOS C library on package mobile (mobile/ios)

The iOS app carried its own copy of the core (the saharev1/OpenFlux fork):
no Session, its own link importer, its own control channel, so it reached
no node the wizard or Desktop set up. The root export_ios*.go here were an
even older copy of the same.

mobile/ios is the app's C API (every call the app makes, same names and
signatures) implemented on package mobile, the Android library: an app
that links this core as a submodule and builds it with build_ios.sh gets
the Session with classic fallback, the one context rule with its
alternates, the codec fallback and the core's link reading. New calls:
OpenFluxShareDecode returns a Session profile ready for
OpenFluxStartSession / OpenFluxStartSessionPacketTunnel; OpenFluxMode,
OpenFluxActiveTransport, OpenFluxSetCodec, OpenFluxSetDebugLevel and the
captcha calls the app had no equivalent for.

The Network Extension runs the carriers on a phone profile
(mobile.SetLowMemory, Volga's SlimVolgaConfig) and keeps the fork's local
DNS-over-TLS, ICMP refusal for UDP and GeoSite split tunneling.

PROTOCOL_NEGOTIATION.md gave the Session's layering in the classic order
(AES inside the batch frame); the Session has always encrypted the batch
frame. Fixed, with the classic layering, codec and context fallback and
the context rule documented.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* session: say how to fix a classic peer at a Session-only exit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* crypto: search the context per pipeline; a gone Session client yields in 15s

A client's Session hellos and its classic fallback shared one context
search, so while a classic exit ignored the hellos the search moved the
classic pipeline off the context that exit uses, and the first classic
packets went out under the wrong keys. Keys are still derived once per
carrier (keyStore); each pipeline searches on its own (keyRing), and a
client's pipelines pass on what they learn.

An exit configured classic yielded to a classic client only linkTimeout
(30 s) after its Session client was last heard; a live one is heard every
keepaliveInterval, so it yields after 15 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* build_ios.sh: work from any directory

The output directory was made and listed relative to the caller's
directory while the library was written under the checkout, so running
core/build_ios.sh from an app that links the core as a submodule failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: p1neappleXpress <a@a.a>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:38:10 +03:00
3fda9695ac Put a cups.online exit's created rooms into its share link (#123)
An exit started without a room list creates its rooms at start and only
printed them to stdout. --share then left cupsonline out of the link
altogether, and the phone's ExitShareLink put it in with an empty URL, so
a client importing either link had no rooms to join (and on Android the
stdout block goes nowhere).

The transport now keeps the packed list of the rooms it created or
re-joined (RoomList) and reports changes (OnRoomList). --share fills it
into the link and prints the link again when the list changes, e.g. when a
failed start is retried; the desktop app picks up the newest link for its
QR. ExitShareLink fills it in the same way on the phone.

Co-authored-by: p1neappleXpress <a@a.a>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:03:08 +03:00
51c6f65ca8 mobile: let apps set the embedded core's debug level (#121)
Start/StartSession/StartExit/StartProxy all hardcoded utils.EnableDebug()
(level 2) on every connect, so an embedding app had no way to turn logging
off or ask for packet-only (-d) or hexdump (-ddd) output. Adds
SetDebugLevel(n), matching the CLI's --debug=N; call it before connecting.
Defaults to level 2 so apps that don't call it keep today's behavior.

Co-authored-by: p1neappleXpress <a@a.a>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 20:11:18 +03:00
meepo161 7aa5977c39 mobile: derive the encryption context as the maintainer's core does
The core now picks the KDF context with pickSessionContext: --url, else
the URL of the highest-priority transport, leaving out cupsonline (its
room list only exists once the exit is up) and the "http://#" placeholder,
else "http://#". The bridge still let cupsonline in, and a classic channel
without a document (oneme, direct) fell back to the transport's name, so
the phone and a CLI peer derived different keys.
2026-09-27 04:34:48 +03:00
meepo161 a6231f1f0d mobile: drop the tunnel HTTP proxy from the bridge 2026-09-27 03:04:44 +03:00
meepo161 0bd8445533 mobile: expose tunnel HTTP proxy 2026-09-27 01:58:57 +03:00
meepo161 e555a6455a mobile: the Android bridge, moved from the Android fork
The gomobile package the Android app binds (io.openflux.bridge) lives
next to the core it wraps now: a separate module that replaces openflux
with this checkout. Build: cd mobile && gomobile bind -target=android
-androidapi=26 -javapkg=io.openflux.bridge -ldflags=-checklinkname=0.
2026-09-27 00:45:05 +03:00