Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ If a required command fails or emits a warning from project code, fix it in the
- Values read from Windows-edited config files in shell scripts must strip a trailing carriage return before passing them to tools; rustup rejects `1.88.0\r` as an invalid toolchain. Keep build-script contract tests for this normalization.
- Windscribe profiles can retain a mixed `ncp-ciphers` list containing AES-256-CBC even when BiFlow passes AEAD-only `--data-ciphers`. On Windows OpenVPN 2.7 this disables ovpn-dco and can fall back to TAP, which has no usable gateway. Drop `ncp-ciphers` only from the Windows sanitized temporary copy and keep regression fixtures for Windows removal and Linux preservation (ADR 0106).
- Git Bash on Windows does not resolve `pnpm.cmd` as `pnpm`. The pre-commit hook must use `pnpm` when present and fall back to `pnpm.cmd`, with a script contract test, so the CI-mirror frontend gate still runs.
- Windows TUN `strict-route` can disrupt localhost services independently of Mihomo's DIRECT rules. Put loopback CIDRs in `tun.route-exclude-address`; configure `kubectl.exe` and other app process routes through List Management rather than hard-coding their outbound (ADRs 0108–0109).
- Windows TUN `strict-route` and macOS TUN `auto-route` can disrupt localhost services independently of Mihomo's DIRECT rules. Put loopback CIDRs in `tun.route-exclude-address` on both; keep Linux unchanged. Also put `localhost` and `+.localhost` in `fake-ip-filter` — `+.local` does not cover `localhost`, and on macOS the helper points system DNS at Mihomo so the name can receive a `198.18.0.0/16` fake-ip that then collides with `private.txt` (witness: `http://localhost:4200`). Configure `kubectl.exe` and other app process routes through List Management rather than hard-coding their outbound (ADRs 0108–0109).
- When extending `RoutePinsDocument`, also extend `RawPinsDocument` and every explicit struct initializer; use `#[serde(default)]` so existing saved rule files remain readable.
- `getByText("1 running", { exact: true })` can match multiple application rows; scope repeated values to a row or select a locator with `.first()` in Playwright assertions.
- A missing Playwright Chromium executable is an environment setup failure. Install it with `pnpm exec playwright install chromium`, then rerun the e2e command.
Expand All @@ -283,6 +283,41 @@ If a required command fails or emits a warning from project code, fix it in the

- On Windows, top-level `ipv6: false` makes Mihomo drop the TUN inet6 address. sing-tun `strict-route` then installs an unconditional WFP "block ipv6" connect filter that only exempts Mihomo, so `localhost` -> `::1` fails instantly while connected. A route exclusion cannot fix a WFP block. Keep top-level `ipv6: true` on Windows and restrict AAAA through `dns.ipv6: false` instead (ADR 0112).

- On macOS the LAN resolver (the router, e.g. `192.168.31.1`) is on the
directly-connected `en0` subnet, so DNS queries to it route around the
TUN and Mihomo's `dns-hijack: any:53` never sees them; the router then
cannot resolve blocked domains (facebook.com → "No answer") and foreign
sites never open even though Hiddify is up. Mihomo runs as root via the
helper, so it can bind `127.0.0.1:53`. `MihomoConfig::normalize_dns_port_for_platform`
rewrites `dns_port` to `MACOS_DNS_PORT = 53` at config load (alongside
`normalize_tun_name_for_platform` → `utun9`), and the helper's
`apply_system_dns`/`restore_system_dns` snapshot every network service's
DNS via `networksetup`, point them all at `127.0.0.1` on Connect, and
restore the snapshot on Disconnect. That same redirect is why
`localhost` must skip fake-ip (`fake-ip-filter: localhost`, `+.localhost`)
and why macOS TUN must `route-exclude-address` `127.0.0.0/8` and `::1/128`:
otherwise `http://localhost:4200` resolves to `198.18.0.0/16` and never
reaches the real loopback listener. The helper is the only thing that
can hold port 53 and re-point the system resolver; the desktop cannot.
Ship a helper-version field in `RuntimeHealth`/`StackSnapshot` so a new
app with DNS code auto-reinstalls an older installed helper on Connect
(`prepare_stack_start` version-mismatch check) instead of silently
using the stale daemon. The helper daemon runs under launchd with a
minimal environment, so `Command::new("networksetup")` can fail PATH
lookup silently and leave the system DNS on the router; use the
absolute path `/usr/sbin/networksetup` in the helper's DNS helpers and
log a `helper.dns_apply_empty` warning when no services are enumerated.
A macOS `.app` bundle (Hiddify is Flutter) can take over a minute to
initialize Launch Services, build the UI, and open the proxy port; the
45s default `start_timeout_seconds` is too short on a cold start, so
`client_start_timeout` doubles the budget on macOS and
`launch_local_proxy_if_needed` uses that deadline instead of the raw
configured seconds. Clearing a Hiddify system proxy on macOS must set
every `networksetup` proxy kind to `off`; replaying the snapshot's
`*_enabled` flags leaves HTTP/SOCKS pointed at Hiddify, and the browser
then returns 502 for `localhost` (ADR 0062). Use `/usr/sbin/networksetup`
in the desktop backend too — a GUI `PATH` can omit `/usr/sbin`.

- Older Pillow has no `Image.Resampling`; generate icons with `Image.LANCZOS` / `Image.BICUBIC`.
- Inner `#![allow(...)]` attributes must be the first item in a Rust module, before `use`.
- Vitest coverage config requires `provider: "v8"` (or `istanbul`) in this Vite version.
Expand Down Expand Up @@ -353,7 +388,7 @@ If a required command fails or emits a warning from project code, fix it in the
- `cargo fmt --all --check` is the first Rust CI step and part of the done gate. Clippy and tests can pass while rustfmt still wants a one-line `.and_then` or a wrapped `assert!`. Run the check before calling the change done; apply `cargo fmt --all` in the same change if it fails.
- Tauri 2.5 AppImage bundling downloads `AppRun-x86_64` with ureq/rustls. GitHub can close TLS without `close_notify`, which rustls reports as `peer closed connection without sending TLS close_notify`. Prefetch the tools into `~/.cache/tauri` with `curl --retry` (`scripts/prefetch-appimage-tools.sh`) before `tauri build`; Tauri skips the download when the files already exist.
- `bundle.createUpdaterArtifacts` plus a committed updater public key makes local `tauri build` fail with `A public key has been found, but no private key` after the packages already exist. When `TAURI_SIGNING_PRIVATE_KEY` is unset, `build.sh` merges `createUpdaterArtifacts: false` so unsigned local packages still finish. GitHub Release signing that fails with `incorrect updater private key password: Missing comment in secret key` is a secret-format or password mismatch, not a bundler bug: the NSIS/deb/AppImage already exist. Use repository secrets (not environment secrets); store the Base64 private key from `pnpm tauri signer generate`; set `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` only when the key was generated with a password. Do not export an empty password env var at workflow scope. Validate with `scripts/prepare-tauri-signing.mjs --require --verify-sign` before `tauri-action`. Invoke the Tauri CLI as `node node_modules/@tauri-apps/cli/tauri.js`; `spawn("pnpm")` is `ENOENT` on Windows runners. After rotating keys, commit the new `plugins.updater.pubkey` in the same change.
- `pnpm tauri build --bundles deb,appimage` always rebuilds the frontend, the release binary, and both packages. Split `build.sh` into compile/deb/appimage/collect (and compile/nsis/collect on Windows). Skip a stage when this version's artifact already exists; `--from STAGE` starts at the failed stage; `--force` rebuilds all. Skip `beforeBuildCommand` when `apps/desktop/dist/index.html` is present, and prefetch AppImage tools only in the AppImage stage.
- `pnpm tauri build --bundles deb,appimage` always rebuilds the frontend, the release binary, and both packages. Split `build.sh` into compile/deb/appimage/collect (and compile/nsis/collect on Windows). Skip a stage when this version's artifact already exists; `--from STAGE` starts at the failed stage; `--force` rebuilds all. Skip `beforeBuildCommand` when `apps/desktop/dist/index.html` is present, and prefetch AppImage tools only in the AppImage stage. macOS `bundle_dmg.sh --skip-jenkins` writes the `.dmg` into cwd and `hdiutil convert` fails with `File exists` if a leftover from the Finder-AppleScript attempt is still there; delete `rw.*.dmg` plus the versioned name and run the fallback from `target/release/bundle/dmg` so `macos_dmg_path` matches.
- `set -e` treats a trailing `[[ -f missing ]] && log` as a failed function. `print_summary` must use `if` so a Linux-only package run does not exit 1 after writing artifacts.
- WebKitGTK 2.44+ DMA-BUF rendering paints a blank Tauri window on VMware SVGA and NVIDIA while JavaScript and IPC still run. Set `WEBKIT_DISABLE_DMABUF_RENDERER=1` before GTK starts; also set `WEBKIT_DISABLE_COMPOSITING_MODE=1` on virtual/NVIDIA GPUs and `LIBGL_ALWAYS_SOFTWARE=1` on virtual machines. Workspace `unsafe_code = "forbid"` blocks `env::set_var`, so `exec()` the process with those variables instead of `Command::status()`-waiting on a child (that leftover parent keeps a terminal open). Do not wait until after `tauri::Builder::build`. Closing the window leaves the process in the tray; `tauri-plugin-single-instance` then shows that old window when a new package is launched. Include the full `X.Y.Z` version in the Linux D-Bus id (`app.biflow.desktop.v1_2_6`) so a newly built package is not swallowed by an older tray instance. The plugin `semver` feature only suffixes the major version and does not separate 1.2.2 from 1.2.5.
- A Windows GUI exe without `#![cfg_attr(windows, windows_subsystem = "windows")]` is a console binary, so Explorer and NSIS open a cmd window behind the UI. Put that attribute on `src-tauri/src/main.rs` and `iran-split-helper`; logs already go to `debug.log`. Pin Linux `.desktop` `Terminal=false` via `bundle.linux.deb.desktopTemplate`.
Expand Down Expand Up @@ -402,6 +437,25 @@ already in progress"`. Cache the last `UpdateInfo` (never log asset URLs).
- On Linux the running Mihomo workdir under `/var/lib/iran-split` is root-only. A desktop read of the overlaid `config.yaml` fails with `EACCES`, so every Live apply failed and client recovery retried every 10 s. Send the desktop's staged copy of the same generation (the helper verifies its SHA-256). Each staged generation is ~1 MB: prune staging to the newest few before creating one (ADR 0111).
- A Windows Connect that dies at Mihomo readiness with `error sending request for url` is not an internal server error. `CoreError::Platform` maps to `errors.internal` in the UI; map a readiness timeout to `ControllerTimeout`. The controller client must use `no_proxy()` or Hiddify's HTTP proxy intercepts `127.0.0.1:19090`. The helper must not `env_clear()` Windows Mihomo down to PATH-only — restore `SYSTEMROOT` (and spawn with `CREATE_NO_WINDOW`), wait briefly for an immediate exit, and ship `wintun.dll` next to `mihomo.exe`.
- A later Windows field log reached `ready: 7` / `rules_loaded: 65734` and then rolled Mihomo back with "process or TUN disappeared". `GET /configs` is still the TUN authority (no adapter enumeration), but `tun.device` on Windows is often `Meta` or empty — treat truthy `tun.enable` as active, retry the post-readiness process/TUN check for 5s, and split the error. Generate Windows YAML like clash-master: `find-process-mode: always`, `ipv6: false`, `auto-redirect: false`, DoH `#VPN`.
- macOS `utun` devices must be named `utunN`; the kernel `com.apple.net.utun`
control rejects arbitrary names such as `clash-iran`, so Mihomo never
creates the TUN and the readiness check sees `mihomo.tun_or_process_missing`
even though the controller is ready. Normalize `mihomo.tun_name` to `utun9`
on macOS at config load time (`MihomoConfig::normalize_tun_name_for_platform`,
`is_valid_macos_tun_name`), and detect the TUN through `GET /configs`
`tun.enable` — the live `utun` device name is kernel-assigned and need not
equal the configured name, so `ifconfig <name>` is unreliable (mirror the
Windows backend, ADR 0104). Launch a `.app` bundle (Hiddify/Happ) through
`open <bundle>.app` via Launch Services, not the inner Mach-O binary
directly; running the inner executable starts the process but the
Flutter/Electron app often never opens its proxy port because it expects
the bundle environment (`enclosing_app_bundle`).
- The privileged helper validates `mihomo_sha256` on startup and refuses to
serve with an empty hash (`UnsafeConfig("mihomo_sha256 must be lowercase
SHA-256")`); launchd `KeepAlive` then restarts it in a crash loop and the
desktop times out with `helper.install_timeout`. `install_macos` must
compute `sha256_file(&mihomo_stage)` and write the real digest into
`helper.toml`, not an empty string (mirror `install_linux`).
- An interrupted `cargo test`/`clippy` can leave `corrupt metadata` in `target/debug/deps/*.rmeta`. Delete only the file rustc names (and its sibling `.rlib`) and rebuild that crate. Do not `cargo clean`.
- A `tokio::sync::watch` subscriber misses intermediate `operation_stage`
values when several `update()` calls run in one worker poll. Yield after
Expand Down
50 changes: 38 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ members = [
"crates/iran-split-rules",
"crates/iran-split-platform-linux",
"crates/iran-split-platform-win",
"crates/iran-split-platform-macos",
"crates/iran-split-helper",
"crates/iran-split-helper-winacl",
"crates/iran-split-cli",
"src-tauri",
]

[workspace.package]
version = "6.2.52"
version = "6.2.55"
edition = "2021"
license = "MIT OR Apache-2.0"
rust-version = "1.88"
Expand All @@ -36,6 +37,7 @@ idna = "1.0.3"
ipnet = { version = "2.11.0", features = ["serde"] }
psl = "2.1.148"
nix = { version = "0.30.1", features = ["socket", "user", "signal", "process", "fs"] }
libc = "0.2.174"
rand = "0.9.1"
regex = "1.11.1"
reqwest = { version = "0.12.15", default-features = false, features = ["json", "rustls-tls", "socks"] }
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@iran-split/desktop",
"version": "6.2.52",
"version": "6.2.55",
"private": true,
"type": "module",
"scripts": {
Expand Down
17 changes: 16 additions & 1 deletion apps/desktop/src/lib/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type PresetStatus = "working" | "catalog" | "unsupported";
export interface PresetDownloads {
linux: string;
windows: string;
macos: string;
}

export interface PresetSpec {
Expand All @@ -37,6 +38,7 @@ export const PRESETS: PresetSpec[] = [
downloads: {
linux: "https://github.com/hiddify/hiddify-app/releases/latest",
windows: "https://github.com/hiddify/hiddify-app/releases/latest",
macos: "https://github.com/hiddify/hiddify-app/releases/latest",
},
},
{
Expand All @@ -50,6 +52,7 @@ export const PRESETS: PresetSpec[] = [
downloads: {
linux: "https://openvpn.net/community-downloads/",
windows: "https://openvpn.net/community-downloads/",
macos: "https://openvpn.net/community-downloads/",
},
},
{
Expand All @@ -63,6 +66,7 @@ export const PRESETS: PresetSpec[] = [
downloads: {
linux: "https://www.happ.su/main/download",
windows: "https://www.happ.su/main/download",
macos: "https://www.happ.su/main/download",
},
},
{
Expand All @@ -76,6 +80,7 @@ export const PRESETS: PresetSpec[] = [
downloads: {
linux: "https://github.com/2dust/v2rayN/releases/latest",
windows: "https://github.com/2dust/v2rayN/releases/latest",
macos: "https://github.com/2dust/v2rayN/releases/latest",
},
},
{
Expand All @@ -88,6 +93,7 @@ export const PRESETS: PresetSpec[] = [
downloads: {
linux: "https://github.com/MatsuriDayo/nekoray/releases/latest",
windows: "https://github.com/MatsuriDayo/nekoray/releases/latest",
macos: "https://github.com/MatsuriDayo/nekoray/releases/latest",
},
},
{
Expand All @@ -102,6 +108,7 @@ export const PRESETS: PresetSpec[] = [
linux: "https://github.com/shadowsocks/shadowsocks-rust/releases/latest",
windows:
"https://github.com/shadowsocks/shadowsocks-windows/releases/latest",
macos: "https://github.com/shadowsocks/shadowsocks-rust/releases/latest",
},
},
{
Expand All @@ -115,6 +122,7 @@ export const PRESETS: PresetSpec[] = [
downloads: {
linux: "https://www.wireguard.com/install/",
windows: "https://www.wireguard.com/install/",
macos: "https://www.wireguard.com/install/",
},
},
{
Expand All @@ -128,6 +136,7 @@ export const PRESETS: PresetSpec[] = [
downloads: {
linux: "https://windscribe.com/getconfig/openvpn",
windows: "https://windscribe.com/getconfig/openvpn",
macos: "https://windscribe.com/getconfig/openvpn",
},
},
];
Expand All @@ -138,7 +147,13 @@ export function presetById(id: PresetId): PresetSpec {

/** Official vendor download page for the current desktop platform. */
export function downloadUrlFor(spec: PresetSpec, platform: string): string {
return platform === "windows" ? spec.downloads.windows : spec.downloads.linux;
if (platform === "windows") {
return spec.downloads.windows;
}
if (platform === "macos") {
return spec.downloads.macos;
}
return spec.downloads.linux;
}

export type PresetDownloadLabel =
Expand Down
Loading