Skip to content

feat(macOS): full macOS support — platform backend, helper, DNS, packaging - #9

Closed
asgarihope wants to merge 5 commits into
devlifeX:mainfrom
asgarihope:feat/macos-support
Closed

asgarihope wants to merge 5 commits into
devlifeX:mainfrom
asgarihope:feat/macos-support

Conversation

@asgarihope

Copy link
Copy Markdown
Contributor

Summary

This PR adds full macOS support to BiFlow. Previously the app only built and ran on Linux and Windows; macOS had zero support. This change ports the entire stack — platform backend, privileged helper, config normalization, Mihomo integration, desktop wiring, build/packaging, and frontend presets — so macOS users get the same transparent TUN split-routing experience as Linux and Windows.

What changed

1. New crate iran-split-platform-macos (crates/iran-split-platform-macos/)

  • Implements PlatformBackend with macOS paths (~/Library/Application Support/biflow/), Hiddify/Happ .app bundle discovery via Launch Services, system_proxy.rs via networksetup, TUN detection via GET /configs tun.enable (mirrors the Windows ADR 0104 approach — ifconfig is unreliable because the live utun device name is kernel-assigned).
  • spawn_local_proxy detects the enclosing .app bundle and launches it via open <bundle>.app (Launch Services). Running the inner Mach-O binary directly starts the process but the Flutter/Electron app never opens its proxy port.
  • resolve_macos_binary rejects Linux ELF binaries (e.g., a stale AppImage symlink) by checking the Mach-O magic (0xfeedfacf / 0xfeedface), so discovery never picks a binary that cannot run on macOS.
  • client_start_timeout doubles the configured budget on macOS (Flutter .app bundles can take over a minute to open their proxy port on a cold start).

2. macOS helper module (crates/iran-split-helper/src/macos.rs — new)

  • run_macos: launchd daemon, Unix socket with root:authorized_gid mode 0o660 permissions (workspace forbids unsafe, so getpeereid(2) is not called directly — access control is enforced by the socket itself).
  • apply_system_dns / restore_system_dns: snapshot every network service's DNS via networksetup, point them all at 127.0.0.1 on Connect, restore on Disconnect. Uses the absolute path /usr/sbin/networksetup because launchd's minimal PATH can cause a bare networksetup lookup to fail silently.
  • start() calls apply_system_dns() (cfg macOS), cleanup() calls restore_system_dns() (cfg macOS).

3. Config normalization (crates/iran-split-config/src/lib.rs)

  • normalize_tun_name_for_platform(): rewrites tun_name to MACOS_TUN_NAME = \"utun9\" at config load. The macOS kernel com.apple.net.utun control rejects arbitrary names (e.g., clash-iran), so Mihomo never creates the TUN and readiness fails.
  • normalize_dns_port_for_platform(): rewrites dns_port to MACOS_DNS_PORT = 53. The macOS system DNS always uses port 53, and the LAN/router resolver bypasses the TUN (it is on the directly-connected en0 subnet), so Mihomo's dns-hijack: any:53 never sees DNS queries. Mihomo runs as root via the helper, so it can bind 127.0.0.1:53; the helper then points the system DNS at 127.0.0.1 so queries flow through the TUN.
  • Regression tests for both normalizations.

4. Mihomo macOS support (crates/iran-split-mihomo/src/lib.rs)

  • Added Platform::Macos variant.
  • routing-mark is now Linux-only (platform == Platform::Linux instead of platform != Platform::Windows) — it is a Linux fwmark that does not steer sockets on macOS.
  • DriverPlatform::Macos mapping and macOS process bypass rules (tailscaled, iran-split-desktop, BiFlow).

5. helper_version plumbing (core + all backends + CLI)

  • RuntimeHealth and StackSnapshot gained a helper_version: Option<String> field (#[serde(default)]).
  • Each backend's runtime_health() populates it from the helper status. Windows connect_progress_health() caches it.
  • prepare_stack_start (desktop) checks helper_version vs app_version(); on mismatch it auto-reinstalls the helper so a new app with DNS/TUN code does not silently use a stale installed daemon.

6. Desktop macOS wiring (src-tauri/)

  • helper_install.rs: install_macos stages helper + mihomo + plist + helper.toml and runs an install shell script via osascript … with administrator privileges. Computes the real mihomo_sha256 (an empty hash made launchd crash-loop the helper). Writes tun_name into helper.toml.
  • lib.rs: prepare_stack_start version-mismatch reinstall check; macOS NativeBackend + paths wiring.
  • diagnostics.rs: macOS gate for environment snapshot.
  • deps.rs: macOS dependency discovery (Hiddify/Happ .app bundles).
  • github_update.rs: macOS update flow (DMG).
  • hiddify_reset.rs: macOS Fresh Hiddify start (kill by Mach-O image match, not command-line substring).
  • environment/mod.rs: macOS collector gate.
  • tauri.macos.conf.json (new): .app/.dmg bundle config, icon.icns.

7. Build/packaging

  • build.sh: ci-macos target, macOS packaging stages (compile/dmg/collect).
  • scripts/stage-helper.sh: macOS helper staging (native build, no cross-compile).
  • scripts/build-plan.mjs, scripts/check-bundled-assets.mjs: macOS bundle config and asset verification.
  • resources/external-assets.toml: macOS mihomo asset entry.
  • vendor/mihomo/darwin/mihomo: macOS arm64 mihomo binary.

8. Frontend

  • apps/desktop/src/lib/presets.ts: macOS download URLs for all presets (Hiddify, OpenVPN, Happ, v2rayN, Windscribe).

9. Docs

  • AGENTS.md: appended macOS lessons (utun naming, .app launch, mihomo_sha256 fix, networksetup absolute path, DNS system resolver redirect, Mach-O binary validation, start timeout doubling).

Testing

  • ✅ cargo clippy --workspace --all-targets -- -D warnings — clean
  • ✅ cargo test --workspace — all pass (core, config, helper, mihomo, clients, platform-macos, desktop, cli)
  • ✅ cargo fmt --all --check — clean
  • ✅ pnpm check — 90 tests pass
  • ✅ pnpm build — clean
  • ✅ Built and tested on real macOS hardware: Hiddify auto-launches via open, TUN (utun9) routes correctly, DNS points to 127.0.0.1:53, foreign sites (facebook.com, google.com, youtube.com) open without manual browser proxy.

Cross-platform

  • No regressions on Linux or Windows: all existing tests pass, routing-mark change only affects macOS (Linux unchanged, Windows unchanged), helper_version field has #[serde(default)] so existing saved state remains readable, all macOS-specific code is cfg(target_os = \"macos\")-gated.
  • One fix for Windows/CLI: DemoBackend::runtime_health() in iran-split-cli was missing the new helper_version field (would have broken the Windows build).

Privacy

No telemetry, analytics, or tracking added. Browsing data (live connection hosts) stays in memory and is never logged to debug.log or transmitted to any external server. The only external requests are the same as before (GitHub Releases for updates/rules, api.country.is/api.ipify.org for IP detection, Cloudflare DoH for DNS).

Made with Cursor

asgarihope and others added 5 commits October 1, 2026 10:34
…mihomo support

- New crate iran-split-platform-macos implementing PlatformBackend with
  macOS paths, Hiddify/Happ .app bundle discovery, system_proxy via
  networksetup, TUN detection via GET /configs (ADR 0104 mirror)
- New helper module macos.rs: launchd daemon, socket permissions,
  apply_system_dns/restore_system_dns (networksetup → 127.0.0.1),
  using absolute /usr/sbin/networksetup path for launchd's minimal PATH
- Config: normalize TUN name to utun9 and DNS port to 53 on macOS
  (kernel rejects non-utun names; LAN router DNS bypasses TUN)
- Mihomo: add Platform::Macos, routing-mark Linux-only, process bypass
- Clients: add DriverPlatform::Macos variant
- Core: add helper_version to RuntimeHealth/StackSnapshot for
  auto-reinstall on version mismatch; populate on all backends + CLI
- Helper: start() calls apply_system_dns, cleanup() calls restore_system_dns
  (cfg macOS); spawn_local_proxy uses open for .app bundles; reject
  Linux ELF binaries in resolve_macos_binary; double start timeout

Co-authored-by: Cursor <cursoragent@cursor.com>
…ironment

- helper_install: install_macos stages helper+mihomo+plist via osascript
  admin prompt, computes real mihomo_sha256 (not empty), writes
  tun_name into helper.toml
- lib.rs: prepare_stack_start version-mismatch check auto-reinstalls
  stale helper on Connect; macOS NativeBackend + paths wiring
- diagnostics: macOS gate for environment snapshot trigger
- deps: macOS dependency discovery (Hiddify/Happ .app bundles)
- github_update: macOS update flow (DMG/AppImage path)
- hiddify_reset: macOS Fresh Hiddify start (kill by Mach-O image match)
- environment/mod.rs: macOS collector gate
- tauri.macos.conf.json: .app/.dmg bundle config, icon.icns
- tauri.conf.json: macOS bundle resources

Co-authored-by: Cursor <cursoragent@cursor.com>
- build.sh: ci-macos target, macOS packaging stages (compile/dmg/collect),
  prefetch, xwin alignment
- scripts/build-plan.mjs: macOS bundle config
- scripts/check-bundled-assets.mjs: macOS asset verification
- scripts/stage-helper.sh: macOS helper staging (no cross-compile needed)
- resources/external-assets.toml: macOS mihomo asset entry
- Cargo.toml/Cargo.lock: iran-split-platform-macos workspace member
- vendor/mihomo/darwin/mihomo: macOS arm64 mihomo binary

Co-authored-by: Cursor <cursoragent@cursor.com>
…ent lessons

- presets.ts: macOS download URLs for Hiddify, OpenVPN, Happ, v2rayN, Windscribe
- AGENTS.md: append macOS lessons — utun naming, .app launch via open,
  mihomo_sha256 fix, networksetup absolute path for launchd PATH,
  DNS system resolver redirect, Mach-O binary validation, start timeout
- version: 6.2.52 → 6.2.54 (pnpm version:sync)

Co-authored-by: Cursor <cursoragent@cursor.com>
Skip fake-ip for localhost, exclude loopback from the TUN, and actually turn Hiddify's system proxy off so browsers stop getting 502 on local dev servers. Also make the DMG fallback survive leftover hdiutil files.

Co-authored-by: Cursor <cursoragent@cursor.com>
@asgarihope

Copy link
Copy Markdown
Contributor Author

Superseded by a new PR with 6.2.55: localhost/fake-ip, Hiddify system-proxy clear, and a working macOS DMG. Closing this 6.2.54 draft so the latest branch can be reviewed in one place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant