Compare commits

..

9 Commits

Author SHA1 Message Date
wangjia 4ca21f9ee9 ci: 修外网发版 GFW 网络坑(sing-box 公网镜像 + gradle/maven 国内镜像)(#5)
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 26s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 21s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 24s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Failing after 13m27s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 13m36s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 13m46s
ci-pangolin / Go — build + test (push) Failing after 13m56s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Failing after 14m7s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Failing after 14m17s
ci-pangolin / Flutter — analyze + test (push) Failing after 14m28s
ci-pangolin / OpenAPI Sync Check (push) Failing after 14m36s
ci-pangolin / Lint — shellcheck (push) Failing after 14m48s
Deploy Client / build-windows (push) Successful in 1m46s
Deploy Client / build-android (push) Failing after 44m39s
Deploy Client / release-deploy (push) Has been skipped
Deploy Client / build-macos (push) Failing after 2m5s
Deploy Client / build-ios (push) Successful in 10m6s
2026-07-26 12:44:36 +00:00
wangjia caaed88298 ci(android): gradle 发行版 + maven 依赖走国内镜像(修 #215 22min 挂)
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 20s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 17s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 16s
ci-pangolin / Lint — shellcheck (pull_request) Failing after 7s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 12m2s
ci-pangolin / Flutter — analyze + test (pull_request) Successful in 4m19s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Failing after 11m12s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Failing after 11m22s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 11m32s
ci-pangolin / Go — build + test (pull_request) Failing after 11m42s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Failing after 11m51s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Failing after 11m59s
#215 android 跑到 flutter build apk 的 Gradle 阶段挂了 ~17min:
"Gradle threw an error while downloading artifacts from the network"
"java.io.EOFException: SSL peer shut down incorrectly"(org.gradle.wrapper.Download)
——这是 sing-box 之外的第二个 GFW 网络坑:Gradle 要下载发行版和 maven 依赖,
走了被墙的 services.gradle.org / dl.google.com / maven central,SSL 被重置。

env 里配了 GOPROXY/PUB/FLUTTER 镜像,唯独 Gradle+Maven 没配。改:
- gradle-wrapper.properties: distributionUrl → 腾讯云 gradle 镜像(同一份 zip)。
- settings.gradle / build.gradle: repositories 前置阿里云 google/public/gradle-plugin
  镜像,原 google()/mavenCentral()/gradlePluginPortal() 保留作 fallback。

镜像已逐个验证:腾讯 gradle-8.7-bin.zip(206)、阿里云 AGP 8.6.0 pom(200)、
kotlin.android 2.2.0(200)均可取。全球通用,墙内不再依赖被墙源。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-26 11:15:14 +08:00
wangjia fc1160ecb3 ci(client): sing-box 源码统一走公网镜像 git.51yanmei.com(android/ios/macos)
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 24s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 20s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 18s
ci-pangolin / Lint — shellcheck (pull_request) Successful in 3m17s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 9m23s
ci-pangolin / Flutter — analyze + test (pull_request) Successful in 1m20s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 5m29s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 54s
ci-pangolin / Go — build + test (pull_request) Failing after 1m20s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 17m49s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Failing after 5m24s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Successful in 1m16s
android/ios/macos 三端都内嵌 libbox,编译期都要 git clone sing-box 源码。
此前只有 build-android 注入了 SINGBOX_GIT(且指向本地 relay 127.0.0.1:13000,
依赖 tailscale/LAN,Mac runner 出外网即失效),ios/macos 完全没注入 → 直接
clone github.com → 被 GFW/DNS 污染打挂(SSL_ERROR_SYSCALL,exit 128)。

统一改用稳定公网域名 git.51yanmei.com(ali frps ← 家里 NAS frpc,nginx 443
反代 gitea)——外网/内网都通,不依赖本地 relay/tailscale。已用真实 clone 命令
从外网验证 v1.13.13 可拉。ios/macos 同时补 GOPROXY(gomobile go install)。
build-windows 保持 LAN 镜像不动(Windows 台式机固定在家,不移动)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-07-26 10:36:08 +08:00
wangjia ee16cd3266 ci(android): libbox 从 NAS gitea 镜像取 sing-box 源码,绕开 github 不可达
Deploy Client / build-windows (push) Successful in 1m46s
Deploy Client / build-android (push) Failing after 22m36s
Deploy Client / release-deploy (push) Has been skipped
Deploy Client / build-macos (push) Failing after 3m40s
Deploy Client / build-ios (push) Failing after 1m16s
build-android 失败根因:build-libbox.sh 第一步 git clone github.com/SagerNet/
sing-box,而 mac runner(经 pangolin 隧道)连不上 github(SSL_ERROR_SYSCALL,
exit 128)。windows 成功是因为它从 NAS 镜像取 sing-box.exe、不碰 github。

修:build-libbox.sh clone 改用 $SINGBOX_GIT(默认仍 github,本地开发不受影响);
deploy-client.yml 的 build-android 设 SINGBOX_GIT=http://127.0.0.1:13000/wangjia/
sing-box.git,经 runner 的 gitea relay 取 NAS 上的 sing-box 镜像(已用 gitea
migrate 从 github 拉好,含 v1.13.13)。与 windows 的 DESKTOP_BIN_MIRROR 同思路,
CI 不再依赖 github。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 08:24:03 +08:00
wangjia e1e43adc80 fix(client-tun): macOS/iOS 应用 excludedRoutes,私有 LAN 真直连
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 26s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 20s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 21s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Successful in 2m5s
ci-pangolin / Go — build + test (push) Failing after 2m37s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 3m47s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Failing after 11m31s
ci-pangolin / Flutter — analyze + test (push) Failing after 11m39s
ci-pangolin / OpenAPI Sync Check (push) Failing after 11m50s
ci-pangolin / Lint — shellcheck (push) Failing after 12m1s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 6m57s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Failing after 10m29s
Deploy Client / build-windows (push) Successful in 1m51s
Deploy Client / build-android (push) Failing after 3m7s
Deploy Client / release-deploy (push) Has been skipped
Deploy Client / build-macos (push) Failing after 27s
Deploy Client / build-ios (push) Failing after 5m17s
上一版加了服务端 route_exclude_address,但 macOS/iOS 路由由 Swift 的
NEPacketTunnelNetworkSettings 控制,而 openTun 只设了 includedRoutes、
从不设 excludedRoutes → libbox 解析出的排除网段被丢弃,strict_route 仍把
LAN(192.168/16、10/8)抓进隧道,VPN 开着连不上局域网/NAS/家里机器/内网 gitea。

修:openTun 里 ipv4/ipv6 各补 excludedRoutes = excludedRoutes4/6(options),
读 libbox 的 getInet4/6RouteExcludeAddress(sing-box config 的 route_exclude_address
经此暴露)。镜像已有的 includedRoutes4/6 写法。

生效:客户端重新构建发版后,私有 LAN 直连恢复 → CI runner 不再需要经 ali 的
中继隧道(relay),从 Mac 也能直连家里所有内网机器。
Android(VpnService.excludeRoute,API33+ 分版本)作后续。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 07:17:09 +08:00
wangjia a3ad50aa75 fix(client-config): TUN 加 route_exclude_address 让私有 LAN 直连
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 27s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 20s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 20s
ci-pangolin / Flutter — analyze + test (push) Failing after 10m20s
ci-pangolin / OpenAPI Sync Check (push) Failing after 10m29s
ci-pangolin / Lint — shellcheck (push) Failing after 10m41s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 14m28s
ci-pangolin / Go — build + test (push) Failing after 14m37s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Failing after 14m49s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Failing after 14m57s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 19m19s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Failing after 12m37s
隧道开着时连不上局域网/NAS/家里机器(SSH/gitea 全 reset)。根因:TUN 入站
auto_route+strict_route 在 OS 层把所有流量(含 LAN)强抓进隧道,而 route.rules
里的 ip_cidr(192.168/16…)→direct 在 macOS 被 strict_route 抵消(direct 出站的
包又被捕回隧道)。

修法:TUN 入站加 route_exclude_address=[192.168.0.0/16, 10.0.0.0/8],在 auto_route
层就把这些网段排除出隧道,LAN 走系统直连。**刻意不含 172.16.0.0/12**——隧道自身
地址与 DNS(172.19.x)在此段,排除会断 DNS。route_exclude_address 为 sing-box
v1.13 合法 TUN 字段(option/tun.go)。

影响:VPN 不再黑洞局域网——直连 NAS/家里机器/内网 gitea 恢复,CI runner 也不再
需要经 ali 的中继隧道(relay)。客户端需完整重连拉新配置生效。

测试:TestBuildClientConfigLANExclude 断言 tun 含 192.168/16+10/8、不含 172.16/12;
go test ./internal/httpapi 全绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 01:53:35 +08:00
wangjia 5bc5ed1e62 ci: pangolin 的 runs-on 从 nas 统一改为 mac
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 29s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 21s
ci-pangolin / Lint — shellcheck (push) Successful in 49s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 22s
ci-pangolin / OpenAPI Sync Check (push) Successful in 10m53s
ci-pangolin / Flutter — analyze + test (push) Failing after 12s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 28s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Successful in 24s
ci-pangolin / Go — build + test (push) Failing after 46s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 36s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 5m15s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Successful in 1m3s
runner 命名/标签统一:mac-pangolin-2 是跑在 Mac 上的个人级 runner,原来挂
`nas` 标签(历史上按"理想归宿是 NAS Linux runner"预留),命名与实体不符、
易混。现把 runner 标签改为 `mac`(个人级不变,仍服务名下所有仓库),并把
pangolin 全部 workflow 的 `runs-on: nas` → `runs-on: mac` 对齐(ci.yml 9 个
job + 解释注释、deploy-client.yml 4 个 job)。与 jiu 的 `runs-on: mac` 统一,
一个个人级 mac runner 即可服务两边。ci-runner.md 标签引用同步更新。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 00:55:26 +08:00
wangjia 398e6e61cb Revert "ci(site): web 发版从 ubuntu-latest 迁到 nas runner"
This reverts commit 7e357ce.

原判断有误:以为 ubuntu-latest 无对应 runner。管理后台确认 gitea 有一个
**全局**在线 runner `nas-runner`(id 2, label ubuntu-latest),它服务所有仓库
(含 pangolin)——web 发版本来就有 runner、并未卡住。恢复 runs-on: ubuntu-latest,
用回这个专供 web/node 的全局 runner,而非把 web 挪到 Mac host runner。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 02:48:11 +08:00
wangjia 7e357ced0a ci(site): web 发版从 ubuntu-latest 迁到 nas runner
pangolin 只注册了一个 runner(mac-pangolin-2, label nas, host 模式);
deploy-site.yml 原 runs-on: ubuntu-latest 无对应 runner → site-v* 发版
永久排队,官网/用户中心走 CI 发不出来。

改 runs-on: nas,直接在宿主 Mac 上 host 跑(Mac 自带 node v25/npm)。
内存无忧:Astro 轻,Next.js next build 峰值 ~1-2GB,Mac 足够——刻意不放
内存紧的 pangolin1(960MB)或 NAS-Linux。wrangler pages deploy 需网络 +
CF token(CLOUDFLARE_API_TOKEN/ACCOUNT_ID secret 已配)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9G7E3wmAYL9KeYCVZVsqu
2026-07-25 00:44:42 +08:00
11 changed files with 120 additions and 19 deletions
+10 -10
View File
@@ -20,7 +20,7 @@ on:
# · runs-on: ubuntu-latest —— 在 catthehacker 容器里跑,仅用于纯 bash 扫描 # · runs-on: ubuntu-latest —— 在 catthehacker 容器里跑,仅用于纯 bash 扫描
# (redline/cleartext/portable-sql);容器内**不能**嵌套 `docker run`(DinD 挂载 # (redline/cleartext/portable-sql);容器内**不能**嵌套 `docker run`(DinD 挂载
# 失败,$PWD 在宿主不存在),故套 docker 的 job 不能用它。 # 失败,$PWD 在宿主不存在),故套 docker 的 job 不能用它。
# · runs-on: nas —— host 模式(mac-pangolin-2 直接在宿主跑),`docker run` 是宿主 # · runs-on: mac —— host 模式(mac-pangolin-2 直接在宿主跑),`docker run` 是宿主
# 真 docker(非嵌套),可正常拉/跑 node/golang/flutter/python/shellcheck 镜像。 # 真 docker(非嵌套),可正常拉/跑 node/golang/flutter/python/shellcheck 镜像。
# golden 保留 ghcr.io/cirruslabs/flutter Linux 容器 → 与入库基线渲染一致。 # golden 保留 ghcr.io/cirruslabs/flutter Linux 容器 → 与入库基线渲染一致。
jobs: jobs:
@@ -28,7 +28,7 @@ jobs:
# ── Job 1: Lint (shellcheck) ───────────────────────────────────────────── # ── Job 1: Lint (shellcheck) ─────────────────────────────────────────────
lint: lint:
name: Lint — shellcheck name: Lint — shellcheck
runs-on: nas runs-on: mac
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -81,7 +81,7 @@ jobs:
# ── Job 2: OpenAPI Sync Check ──────────────────────────────────────────── # ── Job 2: OpenAPI Sync Check ────────────────────────────────────────────
openapi-check: openapi-check:
name: OpenAPI Sync Check name: OpenAPI Sync Check
runs-on: nas runs-on: mac
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -120,7 +120,7 @@ jobs:
# ── Job 4: Flutter 客户端(分析 + 单测/组件测试)──────────────────────── # ── Job 4: Flutter 客户端(分析 + 单测/组件测试)────────────────────────
flutter-client: flutter-client:
name: Flutter — analyze + test name: Flutter — analyze + test
runs-on: nas runs-on: mac
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -156,7 +156,7 @@ jobs:
# (改了 design/colors_and_type.css 没重生成,或手改了生成物)。 # (改了 design/colors_and_type.css 没重生成,或手改了生成物)。
codegen-drift: codegen-drift:
name: Codegen Drift — token 生成物未漂移 name: Codegen Drift — token 生成物未漂移
runs-on: nas runs-on: mac
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -170,7 +170,7 @@ jobs:
ds-flow: ds-flow:
name: DS-flow — 原型/跨端同源/代码色单源闸 name: DS-flow — 原型/跨端同源/代码色单源闸
runs-on: nas runs-on: mac
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -189,7 +189,7 @@ jobs:
# -tags integration(需 docker 起 mysql/redis),见 go-integration job。 # -tags integration(需 docker 起 mysql/redis),见 go-integration job。
go-server: go-server:
name: Go — build + test name: Go — build + test
runs-on: nas runs-on: mac
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -215,7 +215,7 @@ jobs:
# 详见 scripts/e2e-smoke.sh + server/test/e2e/。 # 详见 scripts/e2e-smoke.sh + server/test/e2e/。
e2e-smoke: e2e-smoke:
name: E2E Smoke — L4 进程级端到端 name: E2E Smoke — L4 进程级端到端
runs-on: nas runs-on: mac
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -241,7 +241,7 @@ jobs:
# · -p 1 串行:一次只起一个 mysql 容器,避免并发把 Docker Desktop 压垮/端口资源争用。 # · -p 1 串行:一次只起一个 mysql 容器,避免并发把 Docker Desktop 压垮/端口资源争用。
go-integration: go-integration:
name: Go — integration (mysql/redis testcontainers) name: Go — integration (mysql/redis testcontainers)
runs-on: nas runs-on: mac
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -256,7 +256,7 @@ jobs:
# tablet/desktop-stats golden 与 stats-overhaul 工作区耦合,待其合并后并入本 job。 # tablet/desktop-stats golden 与 stats-overhaul 工作区耦合,待其合并后并入本 job。
golden: golden:
name: Golden — 视觉回归 (全量:components/auth/desktop/tablet) name: Golden — 视觉回归 (全量:components/auth/desktop/tablet)
runs-on: nas runs-on: mac
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
+14 -4
View File
@@ -34,11 +34,15 @@ concurrency:
jobs: jobs:
build-android: build-android:
runs-on: nas runs-on: mac
env: env:
GOPROXY: https://goproxy.cn,direct GOPROXY: https://goproxy.cn,direct
PUB_HOSTED_URL: https://pub.flutter-io.cn PUB_HOSTED_URL: https://pub.flutter-io.cn
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
# sing-box 源码经 NAS gitea 公网镜像取(mac runner 连不上 github)。
# 用稳定公网域名 git.51yanmei.com(ali frps ← 家里 NAS frpc,nginx 443 反代
# gitea)—— 外网/内网都通,不依赖本地 relay/tailscale。见 scripts/build-libbox.sh。
SINGBOX_GIT: https://git.51yanmei.com/wangjia/sing-box.git
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -106,11 +110,14 @@ jobs:
# simply picks up whatever artifacts DO exist — an absent "macos" # simply picks up whatever artifacts DO exist — an absent "macos"
# artifact is not an error there. # artifact is not an error there.
build-macos: build-macos:
runs-on: nas runs-on: mac
continue-on-error: true continue-on-error: true
env: env:
GOPROXY: https://goproxy.cn,direct
PUB_HOSTED_URL: https://pub.flutter-io.cn PUB_HOSTED_URL: https://pub.flutter-io.cn
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
# macOS 同样内嵌 libbox → 同走 NAS gitea 公网镜像取 sing-box 源码(见 build-android)。
SINGBOX_GIT: https://git.51yanmei.com/wangjia/sing-box.git
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -137,11 +144,14 @@ jobs:
path: dist/ path: dist/
build-ios: build-ios:
runs-on: nas runs-on: mac
continue-on-error: true continue-on-error: true
env: env:
GOPROXY: https://goproxy.cn,direct
PUB_HOSTED_URL: https://pub.flutter-io.cn PUB_HOSTED_URL: https://pub.flutter-io.cn
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
# iOS 同样内嵌 libbox → 同走 NAS gitea 公网镜像取 sing-box 源码(见 build-android)。
SINGBOX_GIT: https://git.51yanmei.com/wangjia/sing-box.git
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -168,7 +178,7 @@ jobs:
# (对应平台下载保留 pangolin1 上一版,待可用时下个 client-v* 追上)。 # (对应平台下载保留 pangolin1 上一版,待可用时下个 client-v* 追上)。
release-deploy: release-deploy:
needs: [build-android] needs: [build-android]
runs-on: nas runs-on: mac
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
+3
View File
@@ -1,5 +1,8 @@
allprojects { allprojects {
repositories { repositories {
// 国内镜像优先(见 settings.gradle 同注);原仓库保留作 fallback。
maven { url "https://maven.aliyun.com/repository/google" }
maven { url "https://maven.aliyun.com/repository/public" }
google() google()
mavenCentral() mavenCentral()
} }
+3 -1
View File
@@ -2,4 +2,6 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip # 国内镜像:services.gradle.org 被 GFW 墙(SSL 重置),CI runner 在墙内下载发行版会失败。
# 腾讯云 gradle 镜像提供同一份 zip。见 client/android/{settings,build}.gradle 的 maven 镜像同理。
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.7-bin.zip
+5
View File
@@ -10,6 +10,11 @@ pluginManagement {
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories { repositories {
// 国内镜像优先(CI runner 在墙内,直连 google/central/gradlePluginPortal 会被 GFW 重置);
// 原仓库保留作 fallback。与 deploy-client.yml 的 GOPROXY/PUB 镜像同理。
maven { url "https://maven.aliyun.com/repository/google" }
maven { url "https://maven.aliyun.com/repository/public" }
maven { url "https://maven.aliyun.com/repository/gradle-plugin" }
google() google()
mavenCentral() mavenCentral()
gradlePluginPortal() gradlePluginPortal()
@@ -206,6 +206,7 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol
let ipv4 = NEIPv4Settings(addresses: v4addrs.map(\.address), let ipv4 = NEIPv4Settings(addresses: v4addrs.map(\.address),
subnetMasks: v4addrs.map(\.mask)) subnetMasks: v4addrs.map(\.mask))
ipv4.includedRoutes = includedRoutes4(options) ipv4.includedRoutes = includedRoutes4(options)
ipv4.excludedRoutes = excludedRoutes4(options)
settings.ipv4Settings = ipv4 settings.ipv4Settings = ipv4
} }
let v6addrs = routePrefixes(options.getInet6Address()) let v6addrs = routePrefixes(options.getInet6Address())
@@ -213,6 +214,7 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol
let ipv6 = NEIPv6Settings(addresses: v6addrs.map(\.address), let ipv6 = NEIPv6Settings(addresses: v6addrs.map(\.address),
networkPrefixLengths: v6addrs.map { NSNumber(value: $0.prefix) }) networkPrefixLengths: v6addrs.map { NSNumber(value: $0.prefix) })
ipv6.includedRoutes = includedRoutes6(options) ipv6.includedRoutes = includedRoutes6(options)
ipv6.excludedRoutes = excludedRoutes6(options)
settings.ipv6Settings = ipv6 settings.ipv6Settings = ipv6
} }
if let dns = try? options.getDNSServerAddress(), !dns.value.isEmpty { if let dns = try? options.getDNSServerAddress(), !dns.value.isEmpty {
@@ -311,6 +313,19 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol
networkPrefixLength: NSNumber(value: $0.prefix)) } networkPrefixLength: NSNumber(value: $0.prefix)) }
} }
// :sing-box config route_exclude_address( LAN ) libbox
// includedRoutes excludedRoutes strict_route LAN
// ,VPN /NAS/ excludedRoutes LAN
private func excludedRoutes4(_ o: any LibboxTunOptionsProtocol) -> [NEIPv4Route] {
routePrefixes(o.getInet4RouteExcludeAddress())
.map { NEIPv4Route(destinationAddress: $0.address, subnetMask: $0.mask) }
}
private func excludedRoutes6(_ o: any LibboxTunOptionsProtocol) -> [NEIPv6Route] {
routePrefixes(o.getInet6RouteExcludeAddress())
.map { NEIPv6Route(destinationAddress: $0.address,
networkPrefixLength: NSNumber(value: $0.prefix)) }
}
// utun fd:getpeername sockaddr_ctl, utun id(WireGuard ) // utun fd:getpeername sockaddr_ctl, utun id(WireGuard )
private static func tunFd() -> Int32? { private static func tunFd() -> Int32? {
var ctlInfo = ctl_info() var ctlInfo = ctl_info()
@@ -201,6 +201,7 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol
let ipv4 = NEIPv4Settings(addresses: v4addrs.map(\.address), let ipv4 = NEIPv4Settings(addresses: v4addrs.map(\.address),
subnetMasks: v4addrs.map(\.mask)) subnetMasks: v4addrs.map(\.mask))
ipv4.includedRoutes = includedRoutes4(options) ipv4.includedRoutes = includedRoutes4(options)
ipv4.excludedRoutes = excludedRoutes4(options)
settings.ipv4Settings = ipv4 settings.ipv4Settings = ipv4
} }
let v6addrs = routePrefixes(options.getInet6Address()) let v6addrs = routePrefixes(options.getInet6Address())
@@ -208,6 +209,7 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol
let ipv6 = NEIPv6Settings(addresses: v6addrs.map(\.address), let ipv6 = NEIPv6Settings(addresses: v6addrs.map(\.address),
networkPrefixLengths: v6addrs.map { NSNumber(value: $0.prefix) }) networkPrefixLengths: v6addrs.map { NSNumber(value: $0.prefix) })
ipv6.includedRoutes = includedRoutes6(options) ipv6.includedRoutes = includedRoutes6(options)
ipv6.excludedRoutes = excludedRoutes6(options)
settings.ipv6Settings = ipv6 settings.ipv6Settings = ipv6
} }
if let dns = try? options.getDNSServerAddress(), !dns.value.isEmpty { if let dns = try? options.getDNSServerAddress(), !dns.value.isEmpty {
@@ -301,6 +303,19 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol
networkPrefixLength: NSNumber(value: $0.prefix)) } networkPrefixLength: NSNumber(value: $0.prefix)) }
} }
// :sing-box config route_exclude_address( LAN ) libbox
// includedRoutes excludedRoutes strict_route LAN
// ,VPN /NAS/ excludedRoutes LAN
private func excludedRoutes4(_ o: any LibboxTunOptionsProtocol) -> [NEIPv4Route] {
routePrefixes(o.getInet4RouteExcludeAddress())
.map { NEIPv4Route(destinationAddress: $0.address, subnetMask: $0.mask) }
}
private func excludedRoutes6(_ o: any LibboxTunOptionsProtocol) -> [NEIPv6Route] {
routePrefixes(o.getInet6RouteExcludeAddress())
.map { NEIPv6Route(destinationAddress: $0.address,
networkPrefixLength: NSNumber(value: $0.prefix)) }
}
// utun fd:getpeername sockaddr_ctl, utun id(WireGuard ) // utun fd:getpeername sockaddr_ctl, utun id(WireGuard )
private static func tunFd() -> Int32? { private static func tunFd() -> Int32? {
var ctlInfo = ctl_info() var ctlInfo = ctl_info()
+3 -3
View File
@@ -10,13 +10,13 @@
| 级别 | **项目级**(绑 `wangjia/pangolin`,只服务本仓库) | | 级别 | **项目级**(绑 `wangjia/pangolin`,只服务本仓库) |
| 机器 | mac 本机(`/Users/wangjia/bin/act_runner` v0.6.1) | | 机器 | mac 本机(`/Users/wangjia/bin/act_runner` v0.6.1) |
| 执行模式 | **host**(非容器) | | 执行模式 | **host**(非容器) |
| label | `nas`(即 `runs-on: nas`) | | label | `mac`(即 `runs-on: mac`) |
| 连接 | 经 jiu 的 TCP relay `127.0.0.1:13000` → NAS gitea `192.168.3.200:3000` | | 连接 | 经 jiu 的 TCP relay `127.0.0.1:13000` → NAS gitea `192.168.3.200:3000` |
### 为什么是 host 模式 + mac ### 为什么是 host 模式 + mac
- ci.yml 的 9 个 job **全是 docker-in-docker**(每个 job 内部 `docker run …` 拉 golang/flutter/redis 等镜像)。container 模式(像 jiu 的 `ubuntu-latest:docker://catthehacker`)在非特权容器里跑不了 DinD,所以必须 **host 模式** - ci.yml 的 9 个 job **全是 docker-in-docker**(每个 job 内部 `docker run …` 拉 golang/flutter/redis 等镜像)。container 模式(像 jiu 的 `ubuntu-latest:docker://catthehacker`)在非特权容器里跑不了 DinD,所以必须 **host 模式**
- mac 的 Docker Desktop 能跑 Linux 容器,host 模式下 `docker run <linux-image>` 可用。 - mac 的 Docker Desktop 能跑 Linux 容器,host 模式下 `docker run <linux-image>` 可用。
- 理想归宿其实是 **NAS(Linux)host runner**(给 NAS 现有 runner 加 `nas:host` label),mac 只是当前落点。 - 理想归宿其实是 **NAS(Linux)host runner**(给 NAS 现有 runner 加 `mac:host` label),mac 只是当前落点。
### 为什么要 relay(13000) ### 为什么要 relay(13000)
mac 上的 Shadowrocket 网络扩展会拦截 act_runner 这个 Go 二进制的流量,直连 NAS gitea 失败。jiu 已有一个常驻 python TCP relay(`~/bin/tcp_relay.py`,`127.0.0.1:13000 → 192.168.3.200:3000`),act_runner 连本地 relay 绕过。pangolin runner **复用**这个 relay。 mac 上的 Shadowrocket 网络扩展会拦截 act_runner 这个 Go 二进制的流量,直连 NAS gitea 失败。jiu 已有一个常驻 python TCP relay(`~/bin/tcp_relay.py`,`127.0.0.1:13000 → 192.168.3.200:3000`),act_runner 连本地 relay 绕过。pangolin runner **复用**这个 relay。
@@ -30,7 +30,7 @@ runner:
capacity: 1 capacity: 1
timeout: 3h timeout: 3h
labels: labels:
- "nas:host" - "mac:host"
cache: cache:
enabled: true enabled: true
host: host:
+4 -1
View File
@@ -54,7 +54,10 @@ gomobile init
echo "==> [2/3] 拉 sing-box $SINGBOX_VERSION 源码" echo "==> [2/3] 拉 sing-box $SINGBOX_VERSION 源码"
mkdir -p "$WORK" mkdir -p "$WORK"
rm -rf "$WORK/sing-box" rm -rf "$WORK/sing-box"
git clone --depth 1 --branch "$SINGBOX_VERSION" https://github.com/SagerNet/sing-box.git "$WORK/sing-box" # sing-box 源码:默认 github,CI 里国内 runner 连不上 github → 经 SINGBOX_GIT 指向
# NAS gitea 镜像(wangjia/sing-box,gitea migrate 自 github)。见 deploy-client.yml。
SINGBOX_GIT="${SINGBOX_GIT:-https://github.com/SagerNet/sing-box.git}"
git clone --depth 1 --branch "$SINGBOX_VERSION" "$SINGBOX_GIT" "$WORK/sing-box"
cd "$WORK/sing-box" cd "$WORK/sing-box"
echo "==> [3/3] 用官方 build_libbox 编核心库" echo "==> [3/3] 用官方 build_libbox 编核心库"
+6
View File
@@ -113,6 +113,12 @@ func BuildClientConfig(node *nodes.NodeRow, dpUUID, deriveKey string, opts Clien
"auto_route": true, "auto_route": true,
"strict_route": true, "strict_route": true,
"stack": "system", "stack": "system",
// 私有 LAN 直连:strict_route 会在 OS 层把所有流量(含 LAN)强抓进隧道,
// 光靠 route.rules 的 ip_cidr→direct 在 macOS 不生效(direct 出站的包被
// strict_route 重新捕回隧道)。route_exclude_address 在 auto_route 层就把这些
// 网段排除出隧道,LAN 走系统直连(修「隧道开着连不上局域网/NAS/家里机器」)。
// **不含 172.16.0.0/12**:隧道自身地址与 DNS(172.19.x)在此段,排除会断 DNS。
"route_exclude_address": []string{"192.168.0.0/16", "10.0.0.0/8"},
} }
// 代理出站集合:REALITY 必有;Hy2 仅在启用时加入(否则不进配置/探测组)。 // 代理出站集合:REALITY 必有;Hy2 仅在启用时加入(否则不进配置/探测组)。
@@ -193,3 +193,45 @@ func TestRulesHandler(t *testing.T) {
t.Errorf("allowed but missing file: code=%d, want 404", code) t.Errorf("allowed but missing file: code=%d, want 404", code)
} }
} }
// TUN 入站必须把私有 LAN 网段从隧道排除(route_exclude_address),否则 strict_route
// 会在 macOS 把 LAN 强抓进隧道 → 隧道开着连不上局域网/NAS。不得含 172.16/12
// (隧道自身 172.19.x 在此段,排除会断 DNS)。
func TestBuildClientConfigLANExclude(t *testing.T) {
cfg, err := BuildClientConfig(testNode(), "uuid-1", "k", ClientConfigOpts{})
if err != nil {
t.Fatalf("build: %v", err)
}
var m map[string]any
if err := json.Unmarshal(cfg, &m); err != nil {
t.Fatalf("unmarshal: %v", err)
}
var tun map[string]any
for _, in := range m["inbounds"].([]any) {
im := in.(map[string]any)
if im["type"] == "tun" {
tun = im
break
}
}
if tun == nil {
t.Fatal("no tun inbound")
}
exRaw, ok := tun["route_exclude_address"]
if !ok {
t.Fatal("tun inbound missing route_exclude_address (LAN would be captured by strict_route)")
}
got := map[string]bool{}
for _, v := range exRaw.([]any) {
got[v.(string)] = true
}
if !got["192.168.0.0/16"] {
t.Error("route_exclude_address must contain 192.168.0.0/16")
}
if !got["10.0.0.0/8"] {
t.Error("route_exclude_address must contain 10.0.0.0/8")
}
if got["172.16.0.0/12"] {
t.Error("route_exclude_address must NOT contain 172.16.0.0/12 (tunnel DNS 172.19.x lives there)")
}
}