diff --git a/README.md b/README.md index e263315..446b086 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,178 @@ -# Magicdian's static route helper +# StaticRouteHelper + +

+ StaticRouteHelper icon +

+ +

+ A macOS static route manager built with SwiftUI + privileged helper (XPC). +

+ +

+ Latest Release + Release Workflow + macOS 12+ + License +

+ +English | [简体中文](./README_CN.md) + +## Table of Contents + +- [Overview](#overview) +- [Feature Highlights](#feature-highlights) +- [Compatibility](#compatibility) +- [Download & Installation](#download--installation) +- [Usage Flow](#usage-flow) +- [Usage Screenshots](#usage-screenshots) +- [Architecture](#architecture) +- [Build from Source](#build-from-source) +- [Repository Layout](#repository-layout) +- [Security Notes & Limitations](#security-notes--limitations) +- [License](#license) + +## Overview + +StaticRouteHelper helps you manage IPv4 static routes on macOS with a desktop UI. + +- Frontend: SwiftUI app (`StaticRouter`) +- Privileged operations: helper daemon (`RouteHelper`) +- IPC: typed XPC messages (`RouteWriteRequest` / `RouteWriteReply`) +- Route write path: PF_ROUTE socket (`RTM_ADD` / `RTM_DELETE`) + +## Feature Highlights + +- Add, edit, delete static routes +- Enable/disable routes individually +- Support both gateway modes: + - IPv4 gateway address + - Network interface (for example `utun3`, `en0`) +- System route table viewer: + - Search + - Refresh + - "Show only my routes" filter +- Route groups (macOS 14+): + - Create, rename, reorder, delete groups + - Assign route to multiple groups +- Startup route-state calibration (sync saved state with actual system route table) +- Helper install status banner and guided recovery for SMAppService XPC failures +- English + Simplified Chinese localization + +## Compatibility + +| macOS | Data layer | UI mode | Helper install method | +| --- | --- | --- | --- | +| 12-13 | Core Data | Legacy navigation | SMJobBless | +| 14+ | SwiftData (with legacy migration) | NavigationSplitView + sidebar groups | SMAppService (recommended) or SMJobBless | + +Current project version in Xcode settings: `2.2.3` (build `73`). ## Download & Installation -Pre-built binaries are available on the [GitHub Releases](../../releases) page. +Pre-built binaries are available on [GitHub Releases](https://github.com/jdjingdian/StaticRouteHelper/releases). -Because this project is not signed with a paid Apple Developer certificate, the app is distributed with **ad-hoc code signing** and is **not notarized**. macOS Gatekeeper will block it from opening after download. To remove the restriction, run the following command in Terminal after unzipping: +1. Download and unzip the release package. +2. Move `Static Router.app` to your preferred location (for example `~/Applications/`). +3. Run the following command once in Terminal: ```bash xattr -cr /path/to/Static\ Router.app ``` -Replace `/path/to/Static\ Router.app` with the actual path where you placed the app (e.g., `~/Applications/Static\ Router.app`). This command removes the `com.apple.quarantine` flag that Gatekeeper sets on downloaded files, allowing the app to launch normally. +4. Launch the app, open **Settings -> General**, and install the helper. + +Why step 3 is required: + +- The project uses **ad-hoc code signing** (no paid Apple Developer certificate). +- The app is **not notarized**. +- Gatekeeper adds a quarantine flag to downloaded apps; `xattr -cr` removes it. + +## Usage Flow + +1. Open app and install helper from **Settings -> General**. +2. Add a route (`destination/prefix`, `gateway type`, `gateway`). +3. Toggle route activation in route list. +4. Open **System Route Table** to verify actual kernel routes. +5. Optionally group routes for organization (macOS 14+). + +## Usage Screenshots + +### 1) Route list with group organization and activation toggle + +![Route list](./docs/images/workflow-01-route-list.png) + +### 2) System route table with search and "Only My Routes" filter + +![System route table](./docs/images/workflow-02-system-route-table.png) + +### 3) Add route dialog (network, gateway mode, group assignment) + +![Add route dialog](./docs/images/workflow-03-add-route-dialog.png) + +## Architecture + +```mermaid +flowchart LR + A["StaticRouter (SwiftUI App)"] --> B["RouterService"] + B --> C["PrivilegedHelperManager"] + C --> D["RouteHelper (XPC Server)"] + D --> E["PF_ROUTE Socket (Kernel Routing Table)"] + B --> F["SystemRouteReader"] + F --> E +``` + +## Build from Source + +Requirements: + +- macOS 12+ +- Xcode 15+ recommended + +Build Debug: + +```bash +xcodebuild \ + -project StaticRouteHelper.xcodeproj \ + -scheme "Static Router" \ + -configuration Debug \ + build +``` + +Build Release package (same direction as CI): + +```bash +xcodebuild \ + -project StaticRouteHelper.xcodeproj \ + -scheme "Static Router" \ + -configuration Release \ + -derivedDataPath build/DerivedData + +ditto -c -k --keepParent \ + "build/DerivedData/Build/Products/Release/Static Router.app" \ + "StaticRouteHelper-local.zip" +``` -## Notes +Useful scripts: -This is a helper tool for macOS written in Swift and SwiftUI. You can use it to manage macOS network route. It use `/sbin/route` to do the job, and it need root privileges. It gains root privileges with official API. +- `scripts/bump-version.sh `: bump marketing/build version in `project.pbxproj` +- `scripts/validate-smappservice-health.sh [service_label]`: quick health check for SMAppService launchd job -## Feature && TODO +## Repository Layout -- [x] show system routes -- [x] Add/Delete route -- [ ] **SMAppService** for macOS 13.0+ -- [ ] Rewrite the UI -- [ ] Using Network Extension to replace using `/sbin/route` -- [ ] Localization -- [ ] Dark Mode +- `StaticRouter/`: macOS app (SwiftUI) +- `RouteHelper/`: privileged helper daemon +- `Shared/`: shared XPC contracts/constants +- `.github/workflows/release.yml`: build/sign/package/release workflow +- `openspec/`: spec-driven change history +## Security Notes & Limitations +- Route operations require root privileges and a successfully installed helper. +- Current write/read implementation focuses on IPv4 routes. +- Misconfigured routes can affect host connectivity. Test carefully before applying broad destination ranges. +- If you use SMAppService on macOS 14+, system background-item approval may be required in System Settings. -License -------- +## License StaticRouteHelper is licensed under the [Apache License 2.0](./LICENSE). Copyright © 2021, Derek Jing diff --git a/README_CN.md b/README_CN.md index c7ba0e6..d2d9dd8 100644 --- a/README_CN.md +++ b/README_CN.md @@ -1,38 +1,178 @@ -# 典の静态路由小助手 +# StaticRouteHelper + +

+ StaticRouteHelper 图标 +

+ +

+ 基于 SwiftUI + 特权 Helper(XPC)的 macOS 静态路由管理工具。 +

+ +

+ Latest Release + Release Workflow + macOS 12+ + License +

+ +[English](./README.md) | 简体中文 + +## 目录 + +- [项目概览](#项目概览) +- [功能亮点](#功能亮点) +- [系统兼容性](#系统兼容性) +- [下载与安装](#下载与安装) +- [使用流程](#使用流程) +- [使用示意图](#使用示意图) +- [架构说明](#架构说明) +- [源码构建](#源码构建) +- [仓库结构](#仓库结构) +- [安全提示与限制](#安全提示与限制) +- [开源协议](#开源协议) + +## 项目概览 + +StaticRouteHelper 用于在 macOS 上管理 IPv4 静态路由,提供桌面 UI 与受控提权能力。 + +- 前端应用:SwiftUI(`StaticRouter`) +- 特权操作:Helper 守护进程(`RouteHelper`) +- 通信方式:类型安全 XPC 消息(`RouteWriteRequest` / `RouteWriteReply`) +- 路由写入:PF_ROUTE socket(`RTM_ADD` / `RTM_DELETE`) + +## 功能亮点 + +- 静态路由新增、编辑、删除 +- 路由单条启用/停用 +- 支持两种网关模式: + - IPv4 网关地址 + - 网络接口名(例如 `utun3`、`en0`) +- 系统路由表查看: + - 搜索 + - 刷新 + - 仅显示“我的路由”过滤 +- 路由分组(macOS 14+): + - 新增、重命名、排序、删除分组 + - 单条路由可归属多个分组 +- 启动时自动校准路由激活状态(持久化状态与系统真实路由对齐) +- SMAppService XPC 异常时提供引导与自动恢复流程 +- 中英文本地化支持 + +## 系统兼容性 + +| macOS | 数据层 | 界面模式 | Helper 安装方式 | +| --- | --- | --- | --- | +| 12-13 | Core Data | 兼容模式导航 | SMJobBless | +| 14+ | SwiftData(含旧数据迁移) | NavigationSplitView + 分组侧边栏 | SMAppService(推荐)或 SMJobBless | + +当前 Xcode 工程版本:`2.2.3`(build `73`)。 ## 下载与安装 -编译好的二进制文件可以在 [GitHub Releases](../../releases) 页面下载。 +预编译包可在 [GitHub Releases](https://github.com/jdjingdian/StaticRouteHelper/releases) 下载。 -由于本项目未申请付费的 Apple 开发者证书,发布的应用采用 **Ad-hoc 方式签名**,**未经 Apple 公证**。在 macOS 上首次打开时,Gatekeeper 会阻止其运行。解压后,请在终端执行以下命令移除系统的隔离限制: +1. 下载并解压发布包。 +2. 将 `Static Router.app` 移动到你常用目录(例如 `~/Applications/`)。 +3. 在终端执行一次以下命令: ```bash xattr -cr /path/to/Static\ Router.app ``` -将 `/path/to/Static\ Router.app` 替换为应用的实际路径(例如 `~/Applications/Static\ Router.app`)。该命令会移除 macOS 在下载文件时自动添加的 `com.apple.quarantine` 隔离属性,之后即可正常启动应用。 +4. 打开应用,在 **Settings -> General** 中安装 Helper。 -## 应用说明 +为什么必须执行第 3 步: -这是一个用Swift和SwiftUI写的macOS下的静态路由管理助手,可以方便地添加自己需要的路由,因为route命令需要超级用户权限,所以应用第一次运行的时候会需要输入密码。程序使用CoreData保存了用户添加的静态路由信息,再此后启动的时候就可以自动加载,避免重复输入。点击退出按钮可以安全退出应用,退出的时候会清空手动添加过的路由表,如果是意外退出,电脑重启后也会清空手动添加的路由表。 +- 项目当前使用 **Ad-hoc 签名**(未使用付费 Apple Developer 证书)。 +- 应用 **未经过 Apple 公证(Notarization)**。 +- 下载后 Gatekeeper 会添加隔离标记,`xattr -cr` 用于移除该标记。 -在工作状态下,状态图标为绿色,并且此时无法从列表中删除该路由,点击图标后会清除该路由在系统中的设置,此时会有一个按钮出现,可以用来从列表中删除该路由,从而下次启动的时候不会加载。 +## 使用流程 -## 学到的一些知识点: +1. 启动应用,在 **Settings -> General** 安装 Helper。 +2. 新建路由(目标网段/前缀、网关类型、网关值)。 +3. 在路由列表中切换启用状态。 +4. 打开 **System Route Table** 校验系统实际路由。 +5. 如有需要,可通过分组组织规则(macOS 14+)。 -慢慢补充ing…… +## 使用示意图 +### 1)路由列表:分组管理 + 单条激活开关 +![路由列表](./docs/images/workflow-01-route-list.png) -## TODO +### 2)系统路由表:搜索 + 仅我的路由过滤 -- 重写SwiftUI,MVVM -- 使用Network Extension的方式添加路由(比较遥远,网上似乎没什么教程,而且要开通付费开发者账号……但这样的话iOS也可以使用了(V2) -- 想办法复用Views -- 添加汉语支持 +![系统路由表](./docs/images/workflow-02-system-route-table.png) -License -------- +### 3)添加路由弹窗:目标网段、路由方式、分组归属 + +![添加路由弹窗](./docs/images/workflow-03-add-route-dialog.png) + +## 架构说明 + +```mermaid +flowchart LR + A["StaticRouter (SwiftUI App)"] --> B["RouterService"] + B --> C["PrivilegedHelperManager"] + C --> D["RouteHelper (XPC Server)"] + D --> E["PF_ROUTE Socket (Kernel Routing Table)"] + B --> F["SystemRouteReader"] + F --> E +``` + +## 源码构建 + +环境要求: + +- macOS 12+ +- 建议 Xcode 15+ + +Debug 构建: + +```bash +xcodebuild \ + -project StaticRouteHelper.xcodeproj \ + -scheme "Static Router" \ + -configuration Debug \ + build +``` + +Release 打包(与 CI 方向一致): + +```bash +xcodebuild \ + -project StaticRouteHelper.xcodeproj \ + -scheme "Static Router" \ + -configuration Release \ + -derivedDataPath build/DerivedData + +ditto -c -k --keepParent \ + "build/DerivedData/Build/Products/Release/Static Router.app" \ + "StaticRouteHelper-local.zip" +``` + +常用脚本: + +- `scripts/bump-version.sh `:更新 `project.pbxproj` 中的版本号与构建号 +- `scripts/validate-smappservice-health.sh [service_label]`:快速检查 SMAppService 的 launchd 健康状态 + +## 仓库结构 + +- `StaticRouter/`:macOS 客户端(SwiftUI) +- `RouteHelper/`:特权 Helper 守护进程 +- `Shared/`:共享常量与 XPC 消息定义 +- `.github/workflows/release.yml`:构建/签名/打包/发布流水线 +- `openspec/`:规格驱动的变更历史 + +## 安全提示与限制 + +- 路由操作依赖 root 权限与可用的 Helper。 +- 当前路由读写能力聚焦 IPv4。 +- 错误路由规则可能影响主机网络连通性,请先小范围验证。 +- 在 macOS 14+ 使用 SMAppService 时,可能需要在系统设置中手动允许后台项目。 + +## 开源协议 StaticRouteHelper 基于 [Apache License 2.0](./LICENSE) 协议开源。 Copyright © 2021, Derek Jing diff --git a/StaticRouteHelper.xcodeproj/project.pbxproj b/StaticRouteHelper.xcodeproj/project.pbxproj index a9e976f..541faf4 100644 --- a/StaticRouteHelper.xcodeproj/project.pbxproj +++ b/StaticRouteHelper.xcodeproj/project.pbxproj @@ -571,7 +571,7 @@ CODE_SIGN_IDENTITY = "-"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 73; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; @@ -602,7 +602,7 @@ CODE_SIGN_IDENTITY = "-"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 73; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; @@ -764,9 +764,9 @@ "$(inherited)", "@executable_path/../Frameworks", ); - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 73; MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 2.2.2; + MARKETING_VERSION = 2.2.3; PRODUCT_BUNDLE_IDENTIFIER = cn.magicdian.staticrouter; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ROUTER; @@ -794,9 +794,9 @@ "$(inherited)", "@executable_path/../Frameworks", ); - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 73; MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 2.2.2; + MARKETING_VERSION = 2.2.3; PRODUCT_BUNDLE_IDENTIFIER = cn.magicdian.staticrouter; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ROUTER; diff --git a/docs/images/.gitkeep b/docs/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/images/workflow-01-route-list.png b/docs/images/workflow-01-route-list.png new file mode 100644 index 0000000..9262d8c Binary files /dev/null and b/docs/images/workflow-01-route-list.png differ diff --git a/docs/images/workflow-02-system-route-table.png b/docs/images/workflow-02-system-route-table.png new file mode 100644 index 0000000..982dfc0 Binary files /dev/null and b/docs/images/workflow-02-system-route-table.png differ diff --git a/docs/images/workflow-03-add-route-dialog.png b/docs/images/workflow-03-add-route-dialog.png new file mode 100644 index 0000000..3568831 Binary files /dev/null and b/docs/images/workflow-03-add-route-dialog.png differ