IOS fetch: change icon - #37
Open
saharev1 wants to merge 26 commits into
Open
Conversation
Build a shippable iOS client for OpenFlux and the tooling to sign and upload it to App Store Connect / TestFlight. Go core: - Add a cgo export bridge (export_ios.go, //go:build ios) exposing a real C API — OpenFluxStartClient/Stop/IsRunning/IsConnected/StatsJSON/ReadLog/ FreeString. Previously the iOS "library" had no exports and shipped a hand-written, non-functional header. - socks5: add SOCKS5Server.Close() so the bridge can stop the listener. - utils: add SetOutput() to pipe logs into the app UI (replaces debug.go). - build_ios.sh: drop the fake header; cgo now generates liboflux.h from the //export directives. iOS app (ios-app/, SwiftUI + XcodeGen): - ContentView / TunnelController / bridging header linking liboflux.a. - Start/Stop, live status + log, and a Test button that routes a request through the local SOCKS5 proxy to verify the tunnel. - App icon, Info.plist keys (ATS, ITSAppUsesNonExemptEncryption=false), automatic signing for team 8GQH8GQ252. Tooling & docs: - build_ios_app.sh: one-command build -> archive -> App Store IPA export. - ExportOptions.plist, README.md, DISTRIBUTION.ru.md (setup, upload steps, and App Store distribution constraints for a circumvention tool). - .gitignore: exclude build artifacts, the generated xcodeproj, and *.p8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
Reflect the current tree in README.md / README.ru.md: iOS cgo bridge
(export_ios.go), ios-app/ SwiftUI client, build_ios_app.sh, per-OS
rawsocket_{linux,darwin,windows}.go, transport/compressor.go, and
utils/logging.go.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
- UI: transport picker (Yandex Docs / MAX) with the right fields per backend (URL vs token + user id); previously only a Yandex URL was exposed even though the MAX (oneme) transport is supported. - UI: editable local SOCKS5 port with an uncommon default (10808) to avoid clashing with other local proxies; the running address is shown. - Fix crash on start when the SOCKS5 port is in use: the port is now bound synchronously and "address already in use" is reported to the UI (start code 4) instead of failing in a background goroutine. The start path and the serve goroutine are wrapped in recover() so no panic can unwind into the Swift caller and kill the app. - oneme: remove os.Exit(1) on receiver connection loss — a library must never terminate the host process; it now signals reconnect instead. - socks5: split Bind() from Start() to support the pre-bind check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
Root cause (from the build 3 TestFlight crash report, EXC_CRASH/SIGABRT on a Go worker thread): a panic in a goroutine that had no recover propagated to the runtime and aborted the whole app. - socks5: the per-connection handler is spawned as its own goroutine, so a panic there crashed the process. It now recovers, and the domain-name (ATYP 0x03) parser is bounds-checked against the bytes actually read — previously a large domain length indexed past the 256-byte buffer (index out of range). - utils: add SafeGo(name, fn) — a panic-recovering goroutine launcher. - yandex / tunnel: launch keepAlive, writer, printStats and the connect goroutine so a panic is contained instead of killing the app. - oneme: recover at the top of the read/keepalive loops. - build_ios.sh: drop -s from ldflags (keep -w) so crash reports symbolize via the retained Go symbol table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
The build-4 log showed the real fault the crash had been masking:
"interface conversion: interface {} is nil, not string" in the connect
goroutine. fetchDocInfo asserted officeActionData / balancer_url / token /
document / key straight off the decoded JSON, so any missing field paniced
(now caught, but the transport still failed to connect).
Look each field up with comma-ok and return a descriptive error instead —
e.g. "officeActionData missing (not a legacy Yandex Docs URL?)" or
"balancer_url missing" — so a wrong URL or non-legacy doc reports a clear
reason in the log rather than a panic.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
The doc fetch could hang until the 30s timeout with no clue why. Add: - a 10-redirect cap (a private doc redirects to passport in a loop); - a 15s client timeout; - logging of the request URL, response status, final URL and body size; - a "config not found" error that hints at a login page vs a new-editor page so the log states the actual reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
On device the transport stalled after a successful fetchDocInfo with no
further log — the balancer WebSocket dial hung with no effective timeout.
Add an explicit NetDialContext (10s TCP) plus a 15s handshake timeout, log
the dial target, and log success ("WebSocket connected") or failure with
the HTTP status and error, so the connect step is observable and can retry
instead of hanging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
…ion) A userspace TCP/IP stack for a future NEPacketTunnelProvider: a gvisor channel endpoint + TCP forwarder that terminates the device's TCP flows and forwards each through TCPTunnel.DialTCP (i.e. over the OpenFlux transport to the exit node). WriteInbound injects device packets; ReadOutbound returns packets destined for the device. Only TCP is handled — the transport is TCP-only, so UDP/plain DNS is not carried and will need a DNS-over-TCP shim in the extension. Not wired to a target yet; compiles as part of the tunnel package. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
End-to-end test reached the exit node, but the client resolved ifconfig.me to 240.0.1.72 (a reserved address) — the mobile network's DNS is poisoned for censored hosts, so the exit node got a SYN to an unroutable IP. Override net.DefaultResolver in the iOS bridge to use DNS-over-TLS (Yandex, Google, Cloudflare, tried in order) so DialTCP gets real IPs to hand the exit node. Log the resolved IP in DialTCP. Only the iOS client is affected; the exit node keeps the system resolver (clean network). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
ReconnectDelay was 0, so a server that closes the WebSocket right after connect produced a tight connect/close loop (seen on device: close 1005 every ~150ms), burning battery and never recovering. scheduleReconnect now backs off exponentially with jitter (0.5s..15s); a session that stayed up >15s resets the backoff so normal long-lived reconnects stay fast. Verified end-to-end: desktop client through the SOCKS proxy returns the exit node's IP, connection stays stable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
Full device VPN on top of the proven transport: - tunnel.PacketTunnel gains a UDP :53 forwarder that proxies DNS as DNS-over-TCP through the tunnel (transport is TCP-only; also bypasses local DNS poisoning). Non-DNS UDP is dropped. - export_ios_packet.go: cgo packet-mode API — OpenFluxStartPacketTunnel / TunWritePacket / TunReadPacket / StopPacketTunnel. - OpenFluxTunnel extension target (app-extension) with PacketTunnelProvider: captures all IPv4 + DNS, pumps packets to/from the Go stack. - App VPNController drives NETunnelProviderManager; ContentView gains a "System VPN (all traffic)" section. Config (transport/url/creds) passed via providerConfiguration. - NE entitlement (packet-tunnel-provider) on both app and extension. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
XcodeGen regenerates Info.plist from info.properties, which dropped the hand-written NSExtension dict (App Store rejected the build: missing NSExtensionPointIdentifier). Declare NSExtension (point identifier + principal class) in project.yml so it survives generation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
Device test showed the VPN routed traffic but then collapsed: the extension's own transport socket got the tunnel's source address (10.10.0.2 -> Yandex:443) — its backend traffic and the DoT DNS queries were being routed back into the tunnel, resetting the transport; and the extension neared the NE memory limit. - PacketTunnelProvider: excludedRoutes for the Yandex IP ranges and the DoT resolvers, so the extension's own connections bypass the tunnel. - OpenFluxStartPacketTunnel: debug.SetMemoryLimit(45MB) + SetGCPercent(20) to keep the Go heap under the extension's cap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
The VPN worked (~50 Mbit) but the extension kept crossing the ~50MB NE memory limit and got killed. gvisor's per-connection TCP buffers (up to 1MB send + 1MB recv each) were the main hog. Make the buffer range configurable (tunnel.TCPBuf*, applied via SetTCPBuffers to both the transport and packet-tunnel stacks); the exit node keeps the large defaults, while the iOS bridge shrinks them (default 32KB, max 256KB) and lowers the packet channel queue. Also tighten the extension's Go heap (SetMemoryLimit 40MB, GCPercent 10). Trades some peak throughput for staying under the cap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
The two-gvisor-stack tun2socks design doubled per-connection memory and crossed the NE ~50MB cap under load (crash), and shrinking buffers to compensate dropped throughput to ~3 Mbit. The exit node already hardcodes returns to 10.10.10.2 and NATs via its raw socket, so the extension doesn't need to terminate TCP at all: give the tunnel interface 10.10.10.2 and forward the device's raw IP packets straight over the transport. No gvisor on the client -> minimal memory and full end-to-end TCP throughput. DNS (UDP 53, sent to an in-tunnel address) is answered locally over DoT; other UDP is dropped (transport is TCP-only). No exit-node changes required. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
The recommended `iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP` has no match, so it drops every outbound RST on the host: closed ports answer with silence (scanners see filtered, not closed) and the host can't reset unrelated connections. `-m owner --uid-owner` can't fix it — the tunnel-breaking RSTs are kernel-generated with no owning socket — and port scoping isn't possible (tunnel uses the client apps' ephemeral ports). Add --local-ip so the exit node egresses from a dedicated alias IP; the RST drop can then be scoped as `-s <ip>`, leaving the host's other services untouched. Update the printed hint and README (EN/RU) to recommend the scoped rule (or a network namespace) and keep the host-wide form only as a documented single-purpose-box fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
Verbose logging was forced on in the iOS bridge, so every run paid for per-packet Debugf (with ParsePacketInfo formatting). Default it off: - utils.SetDebug(bool) toggles verbose at runtime. - The iOS bridge no longer force-enables debug; OpenFluxSetDebug exposes the toggle, applied from a "Verbose log" switch in the app (default off, so the VPN extension and app run lean). - Exit node is unchanged: it stays quiet unless --debug is passed (drop the flag for production to remove the per-packet logging cost). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqZJxeewiSYYTHLG4z5Fdt
The exit node crashed under load on a 512MB VPS. Set GCPercent=20 for the exit-node role so the heap stays tight (honors GOMEMLIMIT for a soft cap). The main amplifier remains per-packet debug logging — run without --debug in production. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Py7PiNZ2gfiMaRfde7s3RJ
Previous commit didn't compile — the local `debug` flag variable shadowed the runtime/debug package. Import it as godebug. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Py7PiNZ2gfiMaRfde7s3RJ
The system VPN extension crashed under heavy load (speedtest). Hardening of the gvisor-free L3 path: - Restore the Go memory cap lost in the L3 rewrite: SetMemoryLimit(40MB) + GCPercent(20) so the extension stays under the NE limit. - Bound concurrent DNS-over-TLS resolutions with a semaphore (16) so a DNS burst can't spawn an unbounded pile of goroutines + TLS handshakes. - recover() in OpenFluxTunWritePacket so a malformed packet can never crash the extension. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Py7PiNZ2gfiMaRfde7s3RJ
Apps (YouTube, Safari) prefer QUIC over UDP:443, which the TCP-only transport can't carry. Silently dropping it made them stall on QUIC before falling back to TCP. Now reply ICMP type 3 code 3 (port unreachable) for any non-DNS UDP datagram, so the sender switches to TCP immediately. Note: sustained throughput is still bounded by the covert cursor channel (gzip+base64 over one websocket) — this fixes startup stalls, not raw bandwidth. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Py7PiNZ2gfiMaRfde7s3RJ
Toolbar "info.circle" opens an About sheet with SOL/ETH donation addresses; tap a row to copy to the clipboard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Py7PiNZ2gfiMaRfde7s3RJ
После первого RST по соединению порт удаляется из трекинга (activePorts). Первый RST доходит до клиента (тот закрывает соединение), а весь дальнейший шторм по этому порту дропается на ноде и НЕ уходит в транспорт. Канал освобождается под реальные данные → скорость должна подрасти.
- VPNController enables NEOnDemandRuleConnect so iOS relaunches the tunnel automatically after any drop (extension OOM, network change), instead of the user re-enabling it by hand. Stop() disables on-demand first so it doesn't immediately reconnect. - New cgo export OpenFluxSetDoTResolver(spec) with a thread-safe dotServers snapshot (getDoTServers); spec is ";"-separated "addr[:port]@sni", empty restores built-in defaults. - ContentView DNS picker (Default/Cloudflare/Google/Quad9/AdGuard/Custom). Applied to the in-app core on appear/change and to the packet-tunnel extension via providerConfiguration["dns"]. - Bump CURRENT_PROJECT_VERSION to 20.
- Import upstream transport/yandex/vyandex.go (volga.yandex.ru relay + push.yandex.ru WS, batched worker pool) — much faster than the socket.io cursor channel. - SlimVolgaConfig (4 workers, 4096 queue, 256KB batches) + NewYandexVolgaTransportWithConfig so the iOS NE stays under its ~40MB memory cap; shrink b64BufPool initial cap 16MB->256KB (grows on demand). - Wire "volga"/"vyandex" (wrapped in CompressedTransport) into main.go, export_ios.go (SOCKS) and export_ios_packet.go (NE, slim profile). - UI: TransportKind.volga picker segment (uses the Yandex.Docs URL). - Bump CURRENT_PROJECT_VERSION to 21.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
смена иконки приложения:
