Commit Graph

1370 Commits

Author SHA1 Message Date
zjdndjf
a4b1a7f59a fix: improve core startup failure handling 2026-09-13 01:27:23 +08:00
zjdndjf
0e66ee90d3 fix: handle core process errors 2026-09-13 01:27:23 +08:00
MOMO0302-02
ef6f73ae27 fix(macos): register the login item through Service Management
`checkAutoRun` / `enableAutoRun` / `disableAutoRun` drove macOS auto start by
sending Apple events to System Events (`osascript -e 'tell application "System
Events" ...'`). That path fails in several ways that all look identical to the
user, because the switch state is re-read from `checkAutoRun` right after the
write and simply snaps back with no error:

- The app ships with `hardenedRuntime: true` but neither the
  `com.apple.security.automation.apple-events` entitlement
  (`build/entitlements.mac.plist`) nor `NSAppleEventsUsageDescription`
  (`electron-builder.yml` `mac.extendInfo`), so the Automation request against
  System Events can be denied outright instead of prompting.
- `checkAutoRun` matched the login item by
  `exePath().split('.app')[0].replace('/Applications/', '')` against a list of
  login item *names*. Outside `/Applications` the needle is an absolute path and
  can never match, so the switch stays off even after the item was created.
- `disableAutoRun` deleted the item by that same guessed name and threw when it
  did not exist.

Use Electron's own login item API instead, which goes through
`SMAppService` on macOS 13+ and `LSSharedFileList` below that. No Apple event,
no name guessing, and the read-back state comes from the OS. Registration
failures now surface the `SMAppService` status instead of silently doing
nothing.

Login items written by earlier versions through System Events are not managed by
Service Management, so they are cleaned up (best effort) when the user toggles
auto start, otherwise the app would launch twice at login.

Closes #1302
Closes #1102

(cherry picked from commit 3784139f951896c4818add5374e2cf8a858644fd)
2026-09-12 14:38:25 +08:00
MOMO0302-02
7367fa58e4 fix: remove the Smart strategy setting, which the core never reads 2026-09-12 09:54:15 +08:00
ezequielnick
eb0498b33d feat: protect subscription DNS settings from unconfirmed overrides
- Detect custom DNS in raw subscriptions and require confirmation
- Bind confirmation to the subscription and DNS content for the current session
- Sync the override switch after successful core application
- Retain auto-disable notices until the window becomes visible
2026-09-10 14:05:26 +08:00
Memory
0cd5d86193 fix: allow named GEOIP categories in rule editor 2026-09-10 11:16:36 +08:00
Cervol Liu
1700bfebe6 fix: preserve numeric override IDs before JSON cloning (#2142) 2026-09-09 18:12:03 +08:00
ezequielnick
348db6baa0 chore: bump version 2026-09-09 12:17:51 +08:00
ezequielnick
39e26c60f7 update changelog 2026-09-09 12:17:51 +08:00
Memory
74cbbfeaa8 fix: enable profile auto-update by default 2026-09-08 23:49:42 +08:00
ezequielnick
a092eb50c3 feat(plugin): CPX v2 availability hardening (squash of cpx-v2-availability-hardening)
Client-side hardening of the CPX airport plugin, squashed from 46 commits
forked at 89c2bb0e. Behaviour changes:

- Unified operation model (§0.4/0.5): one deadline + one AbortSignal + one
  persistence commit per operation; per-plugin lock → vault lock hierarchy;
  tombstone on delete; every wait (lock, DNS preflight, proxy resolution,
  vault decrypt) is bounded by the same budget.
- Routing: direct/proxy auto-fallback with a pre-send guard; proxied https
  builds its own CONNECT tunnel (an aborted hung CONNECT closes its socket);
  invalid local-proxy ports are refused instead of falling back to :80; the
  core's inbound credentials are carried to the local proxy; NAT64 and
  site-local IPv6 ranges are non-public.
- Gateways: multi-gateway recovery with one rediscovery per operation,
  normalized endpoint paths, signed discovery documents (Ed25519, seq/digest
  accept/align/rollback/equivocation), commit order vault → plugin.yaml.
- Subscriptions: a fetched subscription is validated by the core (mihomo -t)
  against the current override set before it replaces the profile, inside
  the profile write critical section (profile.yaml and override.yaml share
  one write queue); schedule fields are read at write time; the first
  subscription is activated through the real switch flow; profile deletion
  removes the record last so any failure stays retryable.
- Devices: a re-login that replaces a still-valid device records it in the
  vault (staleDevices) and retires it after the login, after later
  successful fetches and on removal; enroll compensation restores the old
  vault and keeps an un-revoked new device for retirement.
- Vault: on Linux the vault is persisted only behind a system secret store
  (backend name + ciphertext-prefix canary); otherwise it stays in memory.
  A cache-miss read releases the caller at the budget while the lock is held
  until the decrypt ends.
- Config caches (plugin.yaml, profile.yaml, override.yaml) can no longer be
  rolled back by a late cold read.
- Reference gateway and provider guides updated (deploy contract, discoveryUrls
  same-origin rule, /revoke after re-login); https-proxy-agent dropped.

Reviewed in a Codex loop (gpt-6, 38 calls): 74 findings, 68 fixed and
verified, 6 invalid, no backlog. Tests: vitest 501, gateway 106.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 22:44:13 +08:00
v-star0719
2a19b02266 fix: stop IPC ghost listeners leaking across contextBridge calls (#2131)
contextBridge wraps every function crossing the world boundary in a fresh
proxy object, so ipcRenderer.on() and removeListener() each receive a
different wrapper and reference-based removal always fails. Components
that subscribe in useEffect then return removeListener as cleanup leave a
ghost listener behind after unmount.

The connections page is the worst offender: the /connections stream
pushes the full connection list every second, and each ghost handler
dispatches a fresh connection tree into a React update queue that is
never rendered — heap grows ~22-25MB/min after visiting the page a few
times, scaling with the number of visits.

Make on() return an unsubscribe closure that captures the exact wrapper
registered with ipcRenderer, and switch every renderer call site to use
it as its effect cleanup.

Verified with a minimal Electron harness (removed listener stops firing)
and against the real app: hammering the connections page 20x now leaves
memory flat instead of growing ~25MB/min, with zero connections-chunk
allocations sampled.

Co-authored-by: tuqiming <tuqiming@camel4u>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-03 21:47:31 +08:00
Memory
0f26f22987 chore: update electron 2026-09-03 21:40:48 +08:00
zjdndjf
7271e0dfda fix: coalesce connection updates 2026-09-03 02:31:48 +08:00
zjdndjf
b5293b3f65 refactor: rewrite traffic usage logging 2026-09-03 01:54:28 +08:00
zjdndjf
89c2bb0ec0 fix: cancel pending WebDAV config updates
Prevent delayed writes after leaving the WebDAV settings page.
2026-08-31 22:26:53 +08:00
MOMO0302-02
d82ab58081 fix: correct config fetching, debouncing, pagination and rules editor (#2030)
- create-config-context passed the SWR key straight to the fetcher. SWR
  calls fetcher(key), so getAppConfig/getProfileConfig/getOverrideConfig/
  getPluginConfig all received a non-empty string as their `force`
  argument and re-read from disk on every single revalidation. Call the
  fetcher with no arguments.

- webdav-config created its debounced writer inline during render, so
  every keystroke produced a fresh function with a fresh timer and the
  debounce never took effect - each character rewrote config.yaml. Keep
  one instance across renders and cancel it on unmount.

- The traffic table's pager acted on the raw page number rather than the
  clamped one, so after the list shrank the "previous page" button did
  nothing until it had been clicked as many times as the overshoot.

- The rules editor swallowed load failures, then saved anyway - which
  overwrote the user's existing prepend/append/delete rule overrides with
  an empty set. Refuse to save when the current content was never loaded.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 22:22:05 +08:00
MOMO0302-02
3b296bf54b fix: apply Smart core settings on hot reload and stop re-downloading the model
* fix: apply Smart core settings on hot reload

The Smart core switches (LightGBM, data collection, strategy, collector
size) are delivered to the kernel through a generated global override
script, but that script was only regenerated inside prepareCore(). The
settings page persists the change and then hot reloads, so the reload
re-applied the previous script and the new values silently did not reach
the kernel until the core happened to be restarted.

Regenerate the override before generating the profile on hot reload, and
skip the write when the generated content is unchanged so routine hot
reloads no longer churn the override file.

* fix: reuse the downloaded Smart model when checking a profile

The Smart core downloads Model.bin when it is missing from its working
directory. Profile checks run the core with `-d <test dir>`, which never
receives the model, so every check starts a fresh download.

That download runs after the previous core has already been stopped and
the system proxy torn down, so it times out instead of completing, and a
failed download leaves nothing behind - the next restart repeats it. On a
restart here it cost 20s, against 0.5s once the model is in place.

Copy the model over from the working directory before running the check,
mirroring how the geo databases are already shared with the test dir.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 17:04:14 +08:00
zjdndjf
911e090537 fix: stop connection resource work on unmount 2026-08-30 19:40:00 +08:00
zjdndjf
cc94edda3f fix: dispose Monaco editor models 2026-08-30 18:55:00 +08:00
zjdndjf
d50e281383 fix: stop orphaned macOS cores on restart
Closes #2115
2026-08-30 18:03:24 +08:00
Memory
26dd08e07b refactor: streamline validation utilities 2026-08-28 21:51:20 +08:00
Memory
061faeefd1 fix: support YAML merge tags in rule editor 2026-08-25 15:45:17 +08:00
Memory
de8ebb2f62 fix: hot reload config after saving rules 2026-08-24 21:36:14 +08:00
zjdndjf
9776b8791a fix: patch vite-plugin-monaco-editor for modern Node 2026-08-24 01:12:00 +08:00
zjdndjf
ebbcc9d64d perf: speed up app startup 2026-08-24 01:12:00 +08:00
Felix
6bb385822b fix: reset proxy list measurements after visibility toggle (#1884)
Co-authored-by: MackJack023 <141124084+MackJack023@users.noreply.github.com>
2026-08-21 18:14:40 +08:00
Felix
dafc2393dd feat: add quick global toggle for overrides (#1280) (#1928)
Co-authored-by: MackJack023 <141124084+MackJack023@users.noreply.github.com>
2026-08-21 18:12:58 +08:00
MOMO0302-02
8356020931 fix: make core downloads fail loudly and stop clobbering a working core (#2018)
Three problems in installMihomoCore / downloadGitHubAsset.

1. chromeRequest resolves for any status code, so downloadGitHubAsset
   never noticed an HTTP error. A mirror answering 404 or 502 with an
   HTML page was treated as a successful download: the error page was
   written out as the core binary and the function returned, so the
   remaining mirrors and the direct GitHub URL were never tried. Check
   the status and throw so the fallback loop continues.

2. The .gz path decompressed straight onto the live core file. Any
   failure mid-stream left the previously working core truncated with
   nothing to fall back to. Decompress to a staging file and rename it
   into place only after it is complete, removing the staging file if
   anything goes wrong. The pipeline also only had an error handler on
   the gunzip and write streams, so a read error was unhandled; wire all
   three and destroy them on failure.

3. Permissions were set with `execSync('chmod 755 ' + targetPath)`, with
   the path neither quoted nor escaped. On macOS the data directory is
   under "~/Library/Application Support/", so the command always split on
   the space and failed. The failure was only logged as a warning, so the
   result was a freshly installed core with no execute permission - it
   looks installed but will not start. Use fs.chmodSync.

The downloaded archive was also left behind whenever extraction failed,
accumulating one copy per retry; clean it up on the error path.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 18:11:56 +08:00
MOMO0302-02
9490285a83 fix: remove read-modify-write races and validate override downloads (#2024)
- addOverrideItem read the config outside the write queue, then awaited
  createOverride - which for a remote override downloads over the network
  and can take seconds - and finally wrote the stale snapshot back in
  full. Any change made meanwhile was lost. updateOverrideItem and
  removeOverrideItem had the same shape. Move the read inside the queue
  and re-read from disk there, matching what profile.ts already does.

- The remote override download never checked the HTTP status, so a 404 or
  502 error page was written out as the override body. applyOverrides then
  parses it with no try, and the failure surfaces as a core restart error.

- The download built its proxy from the mixed port with a destructuring
  default, so with the port turned off (0) it went to 127.0.0.1:0.

- patchControledMihomoConfig ran the patch through JSON.parse(stringify),
  which drops keys whose value is undefined. The startup migration uses
  exactly that to delete obsolete keys such as external-controller-unix,
  so those deletions never happened - the keys stayed in mihomo.yaml and
  the migration re-ran its full rewrite, config regeneration and Gist
  upload on every launch. Restore top-level undefined keys after the
  round-trip so yaml.stringify can drop them for real.
2026-08-21 18:11:36 +08:00
MOMO0302-02
9714b7435b fix: repair drag-and-drop on the override page (#2026)
- Dropping a file read `(file as File & { path: string }).path`. Electron
  removed File.path, so the value is undefined and readTextFile always
  failed. Use webUtils.getPathForFile, which preload already exposes and
  which profiles.tsx already uses for the same purpose.

- Reordering computed indices against the sorted copy but spliced in
  `items[activeIndex]` - an element from the unsorted array. Whenever the
  two orders differ this duplicated one entry and dropped another. Splice
  out and reinsert the same element, as profiles.tsx does, and bail out if
  either index is -1.

- The drop handler read `file.name` without checking that the FileList was
  non-empty, so dropping selected text threw a TypeError. The throw also
  skipped the setFileOver(false) reset, leaving the whole page stuck
  behind a blur. Guard the access and reset the flag in a finally block.
2026-08-21 18:11:09 +08:00
MOMO0302-02
ef50d8e9a0 fix: keep the copy button clickable on profile cards (#2028)
The copy button only rendered while the card was hovered, and the proxy
checkbox that sits next to it re-laid the row out on hover, so the button
moved out from under the pointer as it was being approached.

Closes #476
2026-08-21 18:10:43 +08:00
MOMO0302-02
637a875aba fix: handle failures in the PAC server, Gist sync and floating window (#2032)
- The PAC server called listen() with no 'error' listener. A host the user
  cannot bind to - or a port already in use - emitted an unhandled 'error'
  event, which in Node terminates the process, so a bad PAC host setting
  crashed the whole main process. Attach an error handler and surface the
  failure to the caller.

- The Gist upload never checked the HTTP status. Once the token expired,
  GitHub answered 401 and the code still recorded the runtime config as
  successfully synced, so the user saw a healthy backup that did not
  exist. Check the status and report the failure.

- On 'render-process-gone' the floating window handler only set the module
  reference to null without destroying the BrowserWindow. The dead window
  stayed on screen, always-on-top, with nothing left holding a reference
  to close it. Destroy it before clearing the reference.
2026-08-21 18:10:23 +08:00
MOMO0302-02
7caecd6474 fix: never rewrite rules to a smart group that does not exist (#2036)
The generated Smart override creates the "Smart Group" conditionally but
rewrites rules unconditionally, so the rules can end up pointing at a
group that was never added.

Two reachable cases:

  - The group is only created when `config.proxies` is a non-empty array.
    A subscription that ships only `proxy-providers` (which the app
    explicitly accepts) skips creation, yet every non-builtin rule target
    is still replaced with the literal string 'Smart Group'.

  - When the profile already contains a `type: smart` group under a
    different name, `smartGroupExists` becomes true so nothing is
    created, and the existing group is never renamed - but the rules are
    still rewritten to 'Smart Group'.

In both cases the generated config.yaml references a missing proxy
group, so the core refuses to start ("proxy Smart Group not found") and
the app is left with no working core.

Track the name of the smart group that actually exists - the existing
group's own name, or 'Smart Group' when one was created - and use it for
the replacement. When neither applies, skip rule rewriting entirely and
leave the profile's own targets untouched.

Closes #885

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 18:04:47 +08:00
luantu
f9c841a6f6 feat: auto-switch profile by WiFi SSID
* feat: auto-switch profile by WiFi SSID

Add ssidProfileMap (SSID -> profile ID) and ssidProfileRestore to app config.
When connecting to a mapped WiFi SSID, automatically switch to the
corresponding subscription profile. Optionally restore the previous
profile when leaving the mapped SSID. pauseSSID takes priority.

* fix: send profileConfigUpdated after SSID-triggered profile switch

changeCurrentProfile() does not notify the renderer via IPC.
The renderer relies on the profileConfigUpdated event to revalidate
its SWR cache, so SSID-based switching was effective in the backend
but the UI didn't reflect it until a manual refresh.

* perf: event-driven SSID detection on macOS via scutil -w

Replace 30s polling with scutil -w child process on macOS.
scutil -w blocks on a SystemConfiguration key, consuming zero CPU
until the network state changes, then exits. On exit we debounce
500ms and re-check the SSID.

Windows/Linux keep polling, reduced from 30s to 15s.
Also add anti-tight-loop guard (1s minimum restart interval).
2026-08-20 10:17:47 +08:00
MOMO0302-02
588aaa6991 fix: prevent command injection in convertMrsRuleset
convertMrsRuleset built a shell command string from `behavior` and the
resolved ruleset path and ran it through `exec`. `behavior` comes from
the runtime config's `rule-providers`, which originates from the user's
subscription YAML and is therefore untrusted input.

A malicious or compromised subscription can set a behavior such as
`domain & <command> &`; opening that ruleset in the resource viewer then
executes arbitrary commands with the app's privileges. The quoted paths
are also breakable on Windows via an embedded double quote.

Switch to `execFile` with an argument array so nothing is parsed by a
shell, and reject any behavior outside the set the core supports.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 10:16:50 +08:00
MOMO0302-02
316c41c05b ci: run lint, typecheck, format and unit tests on PRs and pushes
The existing build.yml only runs on v* tags and never invokes the test
suite, so pull requests get no automated checks. Add a lightweight CI
workflow that runs format:check, lint:check and typecheck once on Linux
and the unit tests on both Linux and Windows. Uses --ignore-scripts so it
does not download the mihomo cores, which the checks and tests do not need.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-20 10:12:19 +08:00
MOMO0302-02
c779799bee ci: staple the notarization ticket to the macOS pkg
The macos and macos10 jobs submit the signed .pkg to notarytool but never
staple the resulting ticket to it. Without a stapled ticket Gatekeeper has to
reach Apple's notary service to verify the package, so an install on a machine
that is offline — or behind a blocked network, which is a plausible state for
this particular app before it is running — fails with "cannot be opened because
Apple cannot check it for malicious software".

Staple after a successful submission, which is the documented final step of the
Developer ID distribution flow.
2026-08-20 10:10:21 +08:00
zjdndjf
4e86b53db7 fix: preserve Smart model in profile work dirs 2026-08-19 14:07:46 +08:00
zjdndjf
2330e7790e feat: support custom GitHub download proxy (#2096) 2026-08-19 13:30:01 +08:00
zjdndjf
fca31a84b3 fix: crash when geo-update-interval is null in config 2026-08-17 10:54:12 +08:00
Shen Zehou
41c2f26538 fix: load legacy sysproxy binding on Windows 8/8.1 2026-08-16 10:05:52 +08:00
Memory
cf13cb9d28 fix Windows ARM64 NSIS installer 2026-08-15 13:00:53 +08:00
Memory2314
ec8bfc542a chore: bump version v2.0.2 2026-08-14 22:55:28 +08:00
Memory2314
354c4e6385 update changelog 2026-08-14 22:54:55 +08:00
Memory
48c703a354 revert: downgrade Electron to 43.2.0 2026-08-14 22:46:43 +08:00
zjdndjf
3fc2223448 docs: add AGENTS.md and CLAUDE.md restricting AI agents from bypassing commit checks 2026-08-13 05:28:32 +08:00
zjdndjf
147ecaa7d9 chore: skip pnpm verify-deps auto-install on script runs 2026-08-13 05:21:16 +08:00
zjdndjf
60dba7895d fix: use valid popover side for welcome tour step 2026-08-13 05:07:07 +08:00
zjdndjf
f4a4d9b31d fix: use valid Electron accelerator names for numpad operator keys 2026-08-13 05:05:51 +08:00