A cross-platform Flutter GUI for encrypted proxy client.
CipherRelay (formerly Proxy UI) keeps a direct-download edition and a separate
Mac App Store target. The direct edition produces CipherRelay.app and keeps
its privileged helper, system proxy, LAN access and process bypass. The
appstore scheme produces sandboxed CipherRelay.app with its own bundle
ID and Packet Tunnel extension. See the store build guide.
Version 1.2.16 (42) was submitted on 14 September 2026; it has not passed App Review.
Every platform displays CipherRelay. The Windows executable is
CipherRelay.exe, Linux runs CipherRelay, and iOS/macOS produce
CipherRelay.app. Both macOS editions use this name but keep separate bundle
IDs and settings; use separate installation folders to keep both editions.
Replace old shortcuts that still point to proxy_ui after installing an update.
App IDs, Dart imports (package:proxy_ui), settings keys, Windows installer ID,
and Windows startup/single-instance identifiers retain their existing values.
Linux keeps GTK ID com.proxyui.proxy_ui, which also owns its settings directory.
The native http_proxy library and CLI program names are unchanged.
Delivery filenames use
cipherrelay-v<version>-<build>-<platform>-<architecture>.<extension>.
scripts/release_artifact_name.py reads the version from pubspec.yaml; use it
for local packages as well as the desktop workflow:
python3 scripts/release_artifact_name.py macos universal dmg
python3 scripts/release_artifact_name.py windows x64 zip
python3 scripts/release_artifact_name.py linux x64 tar.gz
python3 scripts/release_artifact_name.py android arm64-v8a apk
python3 scripts/release_artifact_name.py ios arm64 ipaThe Windows production Fastforge jobs target the x64 CI bundle; their
artifact_name templates in distribute_options.yaml use the same convention,
with -setup.exe for the installer. Its executable_name points to
CipherRelay.exe. ZIP naming is configured in distribute_options.yaml, not
an unused ZIP make_config.yaml.
Android and iOS packages must still be built and signed locally. Copy the
verified APK/IPA to build/packages/ using the filename printed above; never
publish Flutter's generic intermediate filename. Increment Android's build
number and verify its application ID and signing certificate before delivery.
Android retains com.proxyui.proxy_ui, Apple direct apps retain
com.proxyui.proxyUi, and the Mac App Store edition retains
com.proxyui.proxyUi.store.
The App proxies sidebar configures Claude natively and creates per-app
launchers for Codex and other apps on macOS (direct download) and Windows.
Installed apps are listed with their own icons and set up in one click with
the current Proxy port; ports and host bypass rules stay editable per app, and
*oa* is direct by default.
See setup, matching semantics and limitations.
The translate button in the global title bar selects System default, 简体中文, or English. The selection is saved across launches and updates navigation, proxy controls, nodes, subscriptions, logs, settings, dialogs, and tray menus without restarting the proxy or TUN. The same control in the TUN guide changes the application language. Original native logs, process names, paths, and executable commands remain intact.
Translations use Flutter gen-l10n with lib/l10n/app_en.arb and
lib/l10n/app_zh.arb. Add matching entries to both files, then run
fvm flutter gen-l10n from this directory. Generated localization sources are
ignored; normal FVM builds regenerate them.
Windows bundles Noto Sans SC (variable weights 100–900) and loads it before the first frame. Both themes use it, including as the Chinese fallback for monospace log text. Font loading needs no network access. The asset and its SIL Open Font License are scoped to Windows, so other platform packages keep their existing typography and size. The upstream font is unmodified from Google Fonts commit a85815a.
Updates are only ever looked for when the user presses Check for updates in
the title bar. Nothing is fetched in the background and nothing is offered
unprompted. The check reads release-manifest.json from the latest release
(through the local proxy while it runs) and trusts it only if
release-manifest.json.sig is a valid Ed25519 signature of its exact bytes by
the key built into the app. If the release is newer, one confirmation starts
the update:
| Installation | What happens |
|---|---|
| macOS (Developer ID) | The disk image is checked against the manifest, its app must be signed by team 5T7QB722FM, accepted by Gatekeeper and carry the expected bundle id and build. The app then quits through its normal path, a helper swaps the bundle and reopens it. |
| Windows installer | The installer is checked against the manifest; the app quits, the installer runs silently (one UAC prompt) and the app reopens. |
| Windows portable, Linux | The archive is checked and saved to Downloads for the user to unpack. |
| Android | The package matching the device's ABI is checked and handed to the system installer, which asks the user to confirm. |
| iOS, Mac App Store | Not offered: the store updates these builds. |
The release workflow signs the manifest with the UPDATE_SIGNING_KEY secret
and verifies it against scripts/update_signing_key.pub.pem. After attaching
locally signed Android packages and updating the manifest, sign it again and
upload the new signature:
python3 scripts/sign_release_manifest.py release-manifest.json --key /path/to/update-signing-ed25519.pem
gh release upload vX.Y.Z release-manifest.json release-manifest.json.sig --clobberAlternatively, after uploading the three locally signed split APKs, dispatch
attach-android-manifest.yaml on the same commit as the desktop release. It
verifies the existing signed manifest, desktop hashes, APK identities and the
stable local certificate, then signs the updated manifest using the existing
update-manifest key. It never builds or signs a mobile package.
Debug builds verify an update but never replace themselves. Set
CIPHERRELAY_UPDATE_FEED to a directory URL holding a release's files to try
the flow against a local copy.
With the matching native library, all native TCP, UDP and ICMP sessions use encryption, including HTTPS handshakes and node probes. There is no encryption toggle or per-IP codec setting. Older saved values are ignored; the existing wire format and FFI layout remain compatible. The optional v3 setting selects a newer encrypted protocol and requires upgraded nodes; it does not turn encryption on or off. Direct and external HTTP/SOCKS routes keep their own protocols. Build/stage the matching native library before using this UI change.
-
TUN ping (1.2.20+46): With native 0.4.34 and an updated native proxy exit, TUN forwards IPv4/IPv6 Echo to the real target. The exit needs OS permission for ICMP sockets. Existing TCP/UDP and default legacy protocol stay compatible with old nodes. External HTTP/SOCKS exits and arbitrary IP protocols are not supported by this Echo extension.
-
Low-latency encrypted transport v3 (1.2.19+45): Requires native 0.4.33 on the client and every relay/server. The switch encrypts TCP, UDP and control without an extra handshake round trip. Fresh sender salts separate connections/directions; whole-connection replay protection is not promised. Default off retains legacy compatibility. Failures never silently downgrade. Mac App Store Packet Tunnel remains legacy-only.
-
Connection recovery (1.2.17+43): Paired with native 0.4.31, Windows refreshes physical egress for new sessions after network changes. Native handle operations are serialized, cleanup runs off the UI thread, logs are bounded, and connection status is checked independently of logs. See the parent repository's
docs/network-resilience.mdfor limits and validation. -
Node galaxy: The node catalogue opens as a 3D map. Each country is a star wearing its flag, placed where the country is on Earth as seen from above the North Pole; its nodes orbit it as planets coloured by their last test (fast, fair, slow, unreachable, untested), and a ring around the star shows the mix. An arc of light runs from this device to the node in use. Drag to orbit, scroll or pinch to zoom, click a star or planet for its addresses, egress IP and latency, and connect or test from the panel. Nodes whose egress has not been located gather in an "Unlocated" cluster and fly to their country once a test finds it. A list view shows the same nodes grouped by country.
-
Proxy Control: A power control on the dashboard and in the navigation rail starts and stops the proxy from any page
Windows, macOS, Linux, Android and iOS share the optimized galaxy renderer.
The phone dashboard does not construct a galaxy scene. A visible map repaints
on interaction and node changes, follows display vsync during animation,
and rests after the interaction and camera transition finish. Background,
minimized and inactive views stop their animation clocks. Backdrops, flag and
planet textures are released with visibility, and label layouts with their
canvas. Native 0.6.5 batches geometry in the independent Rust galaxy-geometry
crate; visible lines and textured planets are submitted in GPU batches.
Testing a country draws nothing while its probes wait: in-flight rings and
progress are still, answers are applied in batches, and the catalogue is saved
once per run. A native library exporting proxy_probe_nodes_v1 runs the probes
as tasks on its shared runtime instead of one blocked isolate per node. See
resource verification.
- Configuration: Server host, port, session key, and local port settings
- Optional LAN Access: Expose the local proxy to trusted LAN devices with a copyable Wi-Fi HTTP proxy link
- Auto Proxy: Geo-based routing (CN direct, others proxy)
- SOCKS5 UDP: Toggle RFC 1928 UDP relay on the same local proxy port
- Windows TUN: Capture device TCP/UDP with runtime process exclusions; ICMP Echo uses the native proxy exit
- Subscriptions: Export UDP-aware Clash and Shadowrocket configurations
- Real-time Logs: Colored log viewer with level filtering (TRACE/DEBUG/INFO/WARN/ERROR)
- Desktop Log Files: Hourly log files with three-day retention and one-click folder access
- Theme Switching: 4 color themes (Cyberpunk, Sunset, Ocean, Forest)
- Dark/Light Mode: Toggle between dark and light appearance
- Responsive Layout: Adapts to phone, tablet, and desktop screens
- Material 3 Design: Modern UI with smooth animations
| Platform | Status |
|---|---|
| Android | ✅ |
| iOS | ✅ |
| Linux | ✅ |
| macOS | ✅ |
| Windows | ✅ |
| Web |
Closing the window (Windows × / Alt-F4, or the macOS red close button) hides it to the tray while the proxy and TUN continue running. Click the tray icon to reopen it; macOS also supports reopening from the Dock.
Use the tray menu's Quit, or Cmd-Q / Dock > Quit on macOS, to stop the services and exit. These quit paths wait for TUN routes and DNS to be restored before terminating the process.
- FVM 4.x
- Visual Studio 2022 with the Desktop development with C++ workload (Windows)
- Rust toolchain from the parent
proxy-everythingrepository
Flutter is pinned to 3.47.8 in .fvmrc. Do not invoke a globally installed
flutter or dart; use fvm flutter and fvm dart so local and CI builds use
the same SDK.
Android builds use Java 17, Gradle 8.14.3, Android Gradle Plugin 8.11.1 and Kotlin 2.2.20, matching the pinned Flutter SDK's minimum supported toolchain. The Gradle distribution checksum is pinned in the wrapper configuration. Build release APKs locally with the existing signing key and increment the build number before packaging; CI APKs are not distributable.
fvm install
fvm flutter pub get
fvm flutter run -d windowsPublic DMGs are Developer ID signed and Apple notarized. Maintainers must configure macOS signing credentials before running the release workflow. The packaging script verifies every bundled binary and Gatekeeper acceptance before uploading the DMG.
Stage the native artifacts from the parent repository:
# in the parent proxy-everything repository
bash scripts/macos/stage-ui-native.sh --configuration ReleaseThat builds and stages two files into native/macos/, both of which the app
needs: libhttp_proxy.dylib and http-proxy-tun-helper. The helper exists
because macOS requires root to create a utun device and offers no way to elevate
a running app, so TUN mode runs in a separate process started through an
administrator prompt. Without it the TUN switch can only report a missing helper.
The equivalent by hand, if you would rather not use the script:
# in the parent proxy-everything repository.
# Two commands: `--bin` filters targets across the whole package selection, so
# combining these would skip the proxy-ffi library and leave a stale dylib.
cargo build -p proxy-ffi --release
cargo build -p proxy-client --bin http-proxy-tun-helper --release
# in this repository
mkdir -p native/macos
cp ../proxy-everything/target/release/libhttp_proxy.dylib native/macos/
cp ../proxy-everything/target/release/http-proxy-tun-helper native/macos/
install_name_tool -id "@rpath/libhttp_proxy.dylib" native/macos/libhttp_proxy.dylib
codesign --force --sign - native/macos/libhttp_proxy.dylib
codesign --force --sign - native/macos/http-proxy-tun-helperThe install_name_tool step is required: without an @rpath install name the
bundled app looks for the dylib at its absolute build path and fails to load it.
Then keep CocoaPods for plugin integration and build:
fvm flutter build macos --releaseCocoaPods is selected in pubspec.yaml for the pinned Flutter SDK. The tracked
macos/Podfile defines both Runner and RunnerStore, so plugin integration is
reproducible without changing the global Flutter configuration.
For the separate sandboxed VPN target, see Mac App Store development.
The native files are only embedded by the Xcode phase, so re-stage and re-sign them after each build, matching what CI does:
APP=build/macos/Build/Products/Release/CipherRelay.app
cp native/macos/libhttp_proxy.dylib "$APP/Contents/Frameworks/"
cp native/macos/http-proxy-tun-helper "$APP/Contents/MacOS/"
codesign --force --sign - "$APP/Contents/Frameworks/libhttp_proxy.dylib"
codesign --force --sign - "$APP/Contents/MacOS/http-proxy-tun-helper"
codesign --force --deep --sign - "$APP"The helper lives in Contents/MacOS/ rather than Contents/Frameworks/ because
native code locates it relative to its own executable path.
Start the local proxy first, then use the TUN switch. Enabling it asks for an administrator password once per session; the app itself stays unprivileged and keeps serving the local SOCKS5 listener while the authorized helper owns the utun device and the system routes. Turning the switch off restores the previous routes and DNS settings, and so does quitting or crashing the app — the helper notices its control socket closing and cleans up on its own.
System proxy mode configures the primary network service (for example Wi-Fi), even while a VPN captures Internet routes. HTTP, HTTPS, and SOCKS settings are restored independently on that same service when the proxy stops. A failure to apply the requested system proxy is reported as a startup error.
Before requesting administrator access, TUN checks whether another tunnel already owns Internet routes. A conflict leaves routes, DNS, and the local proxy unchanged and is reported in the UI. Closing another VPN's window may leave its background service running. The TUN helper sends its forwarding diagnostics to the UI Logs tab, and direct relays retain the physical DNS servers captured before TUN setup changes DNS.
On macOS, the question-mark button next to TUN opens a read-only diagnostic guide; TUN setup failures open the same guide automatically. It checks the selected Internet/fake-IP routes, maps each utun socket to its current owning PID, and displays the executable path without collecting process arguments. Interface names are reusable, so an active CipherRelay helper is identified as this app rather than attributed to a previously running VPN.
The guide provides localized step-by-step instructions, copyable route checks, and application reopen commands. When the current owner is an identified third-party application, it offers a temporary SIGTERM command matched by its escaped full executable path rather than a stale PID. The command requests all running instances of that executable to exit and can interrupt their connections. A specific client hint appears only after its executable is identified. All commands are copied for manual use; diagnostics never stop processes or modify routes. Recheck refreshes the evidence and enables retry only when both route lookups succeed without selecting a utun interface. Unknown owners and system processes do not receive termination commands.
The TUN bypass picker (the shield button next to the switch) selects applications to keep out of the tunnel, and works while a session is running: changes are sent to the helper, which reroutes new connections and closes established ones whose decision changed so the application reconnects on the new path. The app's own executable is always bypassed and cannot be deselected.
The list covers processes owned by the current user. Unlike Windows, there are no
executable icons and no dormant installed applications, because macOS keeps icons
inside .app bundles and only running processes can be enumerated unprivileged.
The configuration dialog can be opened and edited while connected or starting. Changes, including imported settings, remain drafts until Apply configuration. Connection changes reconnect the active proxy/TUN with rollback on startup failure. Changing only process exclusions updates the running policy directly.
Exclude WeGame games and services covers WeGame/LOL launcher trees, games under WeGame/WeGameApps, and independent AntiCheatExpert/SGuard/ACE services. Existing LOL/WeGame exclusions enable this group on upgrade; switching it off is remembered. It covers direct TCP, UDP and process-attributed DNS even when proxy UDP is disabled. A custom game directory with an exited launcher can still require an explicit executable exclusion. Native 0.6.3 or later is required.
The desktop app requires the Rust http_proxy native library. Windows TUN mode
also requires wintun.dll and cipherrelay-updater.exe; the parent build scripts
stage all three native files. When developing
from the parent repository on Windows, the recommended command builds and
stages the DLL before starting Flutter. Run it from the parent repository root:
.\scripts\windows\run-ui.ps1Build the complete Rust workspace and Windows UI together with:
.\scripts\windows\build.ps1 -Configuration ReleaseVersion 2.0.16 requires native 0.6.14. Downloads, integrity/signature checks,
disk logging, subscription HTTP, blocking process/proxy control, galaxy
computation and desktop app proxies use the shared Rust runtime on supported
platforms. The Windows
updater acknowledges its independent worker before quitting, verifies the
installed version, and retains diagnostics on failure. A Windows installation
with the old broken updater needs one manual installation of this release.
See the parent repository's docs/rust-ui-runtime.md for the complete Dart
audit, ABI ownership, measured layout performance and verification commands.
The Windows executable normally runs without administrator privileges. Start
the local proxy first, then use the top-level TUN switch; only that action
requests UAC and relaunches the same GUI elevated without a terminal window.
The outgoing process waits for its local listener to stop, drains tray updates,
releases the native tray, and flushes logs before exiting. Keeping a nativeapi
0.5.2 tray alive until Windows process shutdown can crash in cnativeapi.dll
(0xc0000005) and delay the elevated replacement while Windows reports the crash.
Cleanup has an eight-second budget below the replacement's ten-second wait.
A timeout keeps the window open and cannot cause a later exit. If tray teardown
has begun, the window's close button retries cleanup and exit, while minimizing
keeps the window reachable instead of hiding behind an unavailable tray icon.
The switch reports enabled only after Wintun and route setup are ready. The TUN
process picker is available before and during a connection. Native code protects
the resolved remote proxy routes and always excludes the UI executable itself
to prevent a capture loop. The picker displays Windows executable icons and
expandable launcher/child-process trees; choosing a parent application bypasses
all descendants without persisting volatile child PIDs.
Use GitHub Actions workflow dispatch:
gh workflow run build.yaml \
-f lib_version=<version> \
-f create_release=true \
-f release_tag=v1.0.0Parameters:
lib_version: Native library versioncreate_release: Whether to create GitHub release (default: true)release_tag: Release tag name (e.g., v1.0.0)
- CI (
ci.yml): Runs on every push/PR tomain/dev- checks code analysis, formatting, and builds - Release (
build.yaml): Manual trigger - builds all platforms and creates GitHub release