{"id":50708191,"url":"https://github.com/octopusgarage/net-auto-switch","last_synced_at":"2026-06-09T13:03:10.785Z","repository":{"id":363270591,"uuid":"1253099914","full_name":"OctopusGarage/net-auto-switch","owner":"OctopusGarage","description":"Layered WiFi + Clash Verge auto-switch daemon for macOS — automatically keeps your network healthy (region-aware node selection, profile fallback, launchd auto-start, uv-managed).","archived":false,"fork":false,"pushed_at":"2026-06-08T07:54:31.000Z","size":221,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-08T08:11:43.296Z","etag":null,"topics":["automation","clash","clash-verge","daemon","launchd","macos","networking","proxy","python","uv","wifi"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/OctopusGarage.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-29T06:41:10.000Z","updated_at":"2026-06-08T07:54:35.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/OctopusGarage/net-auto-switch","commit_stats":null,"previous_names":["octopusgarage/net-auto-switch"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/OctopusGarage/net-auto-switch","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OctopusGarage%2Fnet-auto-switch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OctopusGarage%2Fnet-auto-switch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OctopusGarage%2Fnet-auto-switch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OctopusGarage%2Fnet-auto-switch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/OctopusGarage","download_url":"https://codeload.github.com/OctopusGarage/net-auto-switch/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OctopusGarage%2Fnet-auto-switch/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34107866,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-09T02:00:06.510Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","clash","clash-verge","daemon","launchd","macos","networking","proxy","python","uv","wifi"],"created_at":"2026-06-09T13:03:10.252Z","updated_at":"2026-06-09T13:03:10.780Z","avatar_url":"https://github.com/OctopusGarage.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# net-auto-switch\n\n[![CI](https://github.com/OctopusGarage/net-auto-switch/actions/workflows/ci.yml/badge.svg)](https://github.com/OctopusGarage/net-auto-switch/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](.python-version)\n\n**English** · [简体中文](README.zh-CN.md)\n\nA **layered network self-healing daemon** for macOS: the lower layer switches WiFi on demand, the upper layer auto-switches Clash Verge nodes / subscriptions. When the network degrades, it restores connectivity and proxy quality without manual intervention.\n\n## Features\n\n- **Layered orchestration** — each round checks WiFi (physical layer) first, then Clash (proxy layer): \"first make sure you're online, then make sure the proxy is good.\"\n- **WiFi layer is optional + low-frequency** — toggle it on; an independent check interval plus a switch cooldown prevent flapping.\n- **Smart Clash node selection** — grouped by configurable regions (default SG → Tokyo → JP_Other), latency-tested with priority fallback.\n- **Profile fallback** — when every node is unreachable, switch the subscription via AppleScript.\n- **Fully externalized config** — thresholds / intervals / ports / secret / region regexes all live in `config.toml`; the secret is never committed.\n- **`--dry-run`** — rehearsal mode with zero side effects (no real switching).\n- **Fault isolation** — a transient error in any one layer never takes down the daemon.\n- **Launch at boot** — a launchd service with `RunAtLoad` + `KeepAlive` (auto-restart on crash).\n\n## Feature Overview\n\n| Area | What it does |\n|------|--------------|\n| **Layered orchestration** | Each cycle runs the WiFi layer first, then the Clash layer — get online, then optimize the proxy. Layers are isolated: a failure in one never affects the other or kills the daemon. |\n| **WiFi layer** (optional, low-frequency) | Detects the current network and ping-tests latency / loss; flags a \"bad\" network past your thresholds; builds candidates from *preferred ∩ currently-visible* networks; switches only if a candidate is faster by at least `min_improvement_ms`. Guarded by a separate check interval **and** a post-switch cooldown. |\n| **Clash node selection** | Groups nodes by region (SG / Tokyo / JP_Other, regex-configurable); keeps the current node while it's stable (`delay_limit`); otherwise speed-tests and picks the best in-group, falling back across regions by `group_priority`. JP nodes that don't name a city are checked by IP geolocation to spot Tokyo. |\n| **Profile fallback** | When every node is unreachable, switches the subscription profile via AppleScript UI automation. |\n| **Rate limiting** | Node switches ≤ `max_switch_per_min`; profile switches ≤ `max_profile_switch_per_30min`. |\n| **Run modes** | Long-running daemon, single cycle (`--once`), and zero-side-effect rehearsal (`--dry-run`); custom config via `--config`. |\n| **Install \u0026 ops** | One-line `curl` installer, guided `init` wizard (auto-detects Clash Verge), one-command `update`, and a launchd service (boot start + crash restart). Logs rotate daily and self-clean after 14 days. |\n| **Config \u0026 safety** | Everything tunable lives in `config.toml` (validated on load); the secret is never committed — only `config.example.toml` is tracked. |\n\n## Architecture\n\n```\ncli.py  (argparse entry: --once / --dry-run / --config + logging)\n   │\n   └── orchestrator.py  (main loop: WiFi first → Clash; rate/cooldown; fault isolation)\n         ├── wifi.py    (WiFi layer: probe/scan/switch via networksetup/system_profiler/ping)\n         ├── clash.py   (ClashController: grouping/selection/node switch/profile fallback)\n         └── config.py  (TOML load → dataclass + validation)\n```\n\nSee [`CONTEXT.md`](CONTEXT.md) (domain glossary \u0026 invariants) and [`docs/adr/`](docs/adr/) (architecture decisions).\n\n## Quick Start\n\n### One-line install (recommended)\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/OctopusGarage/net-auto-switch/main/install.sh | bash\n```\n\nInstalls [uv](https://docs.astral.sh/uv/) if needed, downloads the **latest release**\ninto `~/.net-auto-switch`, syncs deps, adds a global `net-auto-switch` command, and\nruns the guided `init` wizard. Re-run it any time — it updates an existing install.\nPin a version with `NET_AUTO_SWITCH_VERSION=v0.3.4`.\n\n### Manual install\n\n```bash\ngit clone https://github.com/OctopusGarage/net-auto-switch.git\ncd net-auto-switch\nuv sync                  # create .venv (Python pinned by .python-version) and install deps\nuv run net-auto-switch init   # guided setup — see below\n```\n\nPrefer a download? Every [release](https://github.com/OctopusGarage/net-auto-switch/releases)\nships an auto-generated source tarball / zip.\n\n### Guided setup (`init`)\n\n`init` reads your Clash Verge config to **auto-detect** the API endpoint, secret,\nproxy port, and `profiles.yaml` path, verifies the connection, **runs a health\ncheck** (aborting with guidance if no nodes are reachable), **checks each\nsubscription's auto-update / expiry / traffic and guides you to fix stale ones**,\n**scans your subscription's actual nodes to detect which regions you have (US, JP,\nHK, …) and lets you choose which to prioritize**, writes `config.toml` (backing up\nany existing one), and offers to install the launchd service:\n\n```bash\nuv run net-auto-switch init          # interactive\nuv run net-auto-switch init --yes    # non-interactive (accept all defaults)\n```\n\nAt every step `init` checks the environment and tells you exactly what to fix —\nif you're not on macOS, Clash Verge isn't installed / hasn't run / isn't\nreachable, the secret is wrong, or there are no working nodes.\n\nPrefer to configure by hand (or not using Clash Verge)? Copy the template\ninstead: `cp config.example.toml config.toml` and edit it.\n\n```bash\n# Verify without switching anything\nuv run net-auto-switch --once --dry-run\n```\n\n### Updating\n\n```bash\nnet-auto-switch update    # download the latest release, re-sync deps, reload the service\n```\n\nUpdates pull the latest published release (skipping the download if you're already\ncurrent); `--version vX.Y.Z` installs a specific release and `--force` reinstalls.\n`config.toml` is never touched. (For a manual clone: `git pull \u0026\u0026 uv sync`, then\nre-run `./scripts/install-launchd.sh`.)\n\n## Usage\n\n```bash\nuv run net-auto-switch init                 # guided setup (see Quick Start)\nuv run net-auto-switch update               # update to the latest version\nuv run net-auto-switch --once --dry-run    # single round, rehearsal\nuv run net-auto-switch --once              # single round\nuv run net-auto-switch                      # long-running\nuv run net-auto-switch --config /path/to/config.toml\n```\n\n`uv run net-auto-switch` is equivalent to `uv run python -m net_auto_switch.cli`.\n\n### Process management scripts\n\n```bash\n./scripts/start.sh    # start in background (writes .net-auto-switch.pid)\n./scripts/status.sh   # is it running?\n./scripts/stop.sh     # stop it\n```\n\n## Configuration\n\nAll settings live in `config.toml` (template: `config.example.toml`).\n\n| Key | Default | Description |\n|-----|---------|-------------|\n| `main_interval` | `600` | Main loop interval (seconds) |\n| `wifi.enabled` | `true` | Enable the WiFi layer |\n| `wifi.check_interval` | `3600` | WiFi check interval (seconds) |\n| `wifi.switch_cooldown` | `7200` | Cooldown after a WiFi switch (seconds) |\n| `wifi.bad_latency_ms` | `200` | Latency threshold for \"bad network\" |\n| `wifi.bad_loss_pct` | `5` | Packet-loss threshold for \"bad network\" (%) |\n| `wifi.min_improvement_ms` | `100` | Only switch if improvement reaches this |\n| `wifi.interface` | `en0` | WiFi interface |\n| `clash.api` | `http://127.0.0.1:9097` | Clash external-control API |\n| `clash.secret` | *(required)* | Clash API secret |\n| `clash.proxy_port` | `7890` | Clash HTTP proxy port (used for IP geolocation) |\n| `clash.delay_limit` | `300` | Stability threshold for the current node (ms) |\n| `clash.max_switch_per_min` | `3` | Max node switches per minute |\n| `clash.max_profile_switch_per_30min` | `1` | Max profile switches per 30 minutes |\n| `clash.profiles_yaml` | *(Clash Verge path)* | Location of `profiles.yaml` |\n| `clash.group_priority` | `[\"SG\",\"Tokyo\",\"JP_Other\"]` | Region fallback priority (names must be defined in `regions`) |\n| `clash.trial` | `试用` | Nodes whose name matches this regex are ignored |\n| `clash.regions` | SG / Tokyo / JP_Other | Region name → regex, matched in order (first match wins). Fully configurable |\n| `clash.ip_enrich` | Tokyo ← JP_Other | Optional: reclassify nodes into a region by IP geolocation; remove to disable |\n\n**Custom regions** — `regions` is fully configurable, so you can prefer any region.\nFor a US-first setup:\n\n```toml\ngroup_priority = [\"US\", \"JP\", \"SG\"]\n\n[clash.regions]\nUS = \"(US|United States|美国|🇺🇸)\"\nJP = \"(JP|Japan|日本|🇯🇵)\"\nSG = \"(SG|Singapore|新加坡|🇸🇬)\"\n```\n\nNodes are classified by the **first** matching region (define more specific ones\nfirst); anything matching none is left untouched.\n\n## Production Deployment (macOS launchd)\n\nRun as a launchd service: launch at boot + auto-restart on crash.\n\n```bash\n./scripts/install-launchd.sh     # install deps + generate plist + register \u0026 load\n./scripts/uninstall-launchd.sh   # unload\n\n# Inspect manually\nlaunchctl list com.octopusgarage.net-auto-switch\ntail -f logs/launchd.err.log\n```\n\n**What it gives you:**\n- `RunAtLoad` — starts at boot.\n- `KeepAlive` + `ThrottleInterval=10` — auto-restart on crash, with a 10s minimum interval (crash-loop guard).\n- launchd stdout/stderr → `logs/launchd.out.log` / `logs/launchd.err.log`.\n\n## Resilience\n\n| Mechanism | Behavior |\n|-----------|----------|\n| Layer isolation | WiFi / Clash each wrapped in try/except; one layer failing affects neither the other nor the process |\n| Clash API error | `RequestException` caught, logged, then on to the next round |\n| All nodes down | Auto-switch the subscription profile as a fallback (rate-limited to 30 min) |\n| Switch rate limit | Nodes ≤ 3/min, profiles ≤ 1/30 min |\n| Process self-heal | launchd `KeepAlive` auto-restarts on crash |\n\n## Logs\n\n- **Program log (authoritative):** `~/Library/Logs/net_auto_switch.log` — **rotated at midnight daily, cleaned up after 14 days** (`TimedRotatingFileHandler`); never grows unbounded.\n- When run via launchd: stdout is discarded (`/dev/null`, to avoid duplicating the rotated log); `logs/launchd.err.log` only captures crashes that happen before the logging system initializes (normally empty).\n- When run via `start.sh`: output is appended to `logs/net-auto-switch.out.log` (for development).\n\nRetention is controlled by `LOG_BACKUP_DAYS` in `cli.py` (default 14).\n\n## Project Layout\n\n```\nnet-auto-switch/\n├── net_auto_switch/     # package: config / setup / wifi / clash / orchestrator / cli\n├── tests/               # pytest unit tests\n├── scripts/             # ops scripts + launchd plist + wrapper\n├── docs/\n│   └── adr/             # architecture decision records\n├── install.sh           # one-line curl installer (bootstrap)\n├── config.example.toml  # config template (config.toml is gitignored)\n├── CONTEXT.md           # domain glossary \u0026 invariants\n├── pyproject.toml       # dependencies + tool config (pytest / ruff)\n├── uv.lock              # uv-locked dependency versions (committed)\n└── .python-version      # pinned Python version (read by uv)\n```\n\n## Testing\n\n```bash\nuv run pytest          # full unit-test suite\nuv run ruff check .    # static checks\nuv run ruff format .   # format\n```\n\n## Requirements\n\n- macOS, with [uv](https://docs.astral.sh/uv/) (auto-manages Python 3.12, see `.python-version`).\n- Clash Verge running with external control enabled (API port \u0026 secret matching the config).\n- WiFi switching needs the relevant system permissions; profile fallback depends on authorizing **System Settings → Privacy \u0026 Security → Accessibility**.\n\n## Contributing\n\nContributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).\n\n## License\n\n[MIT](LICENSE) © Kingson Wu\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foctopusgarage%2Fnet-auto-switch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foctopusgarage%2Fnet-auto-switch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foctopusgarage%2Fnet-auto-switch/lists"}