Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ca21f9ee9 | |||
| caaed88298 | |||
| fc1160ecb3 | |||
| ee16cd3266 | |||
| e1e43adc80 | |||
| a3ad50aa75 | |||
| 5bc5ed1e62 | |||
| 398e6e61cb | |||
| 7e357ced0a |
+10
-10
@@ -20,7 +20,7 @@ on:
|
||||
# · runs-on: ubuntu-latest —— 在 catthehacker 容器里跑,仅用于纯 bash 扫描
|
||||
# (redline/cleartext/portable-sql);容器内**不能**嵌套 `docker run`(DinD 挂载
|
||||
# 失败,$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 镜像。
|
||||
# golden 保留 ghcr.io/cirruslabs/flutter Linux 容器 → 与入库基线渲染一致。
|
||||
jobs:
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
# ── Job 1: Lint (shellcheck) ─────────────────────────────────────────────
|
||||
lint:
|
||||
name: Lint — shellcheck
|
||||
runs-on: nas
|
||||
runs-on: mac
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
# ── Job 2: OpenAPI Sync Check ────────────────────────────────────────────
|
||||
openapi-check:
|
||||
name: OpenAPI Sync Check
|
||||
runs-on: nas
|
||||
runs-on: mac
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
# ── Job 4: Flutter 客户端(分析 + 单测/组件测试)────────────────────────
|
||||
flutter-client:
|
||||
name: Flutter — analyze + test
|
||||
runs-on: nas
|
||||
runs-on: mac
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
# (改了 design/colors_and_type.css 没重生成,或手改了生成物)。
|
||||
codegen-drift:
|
||||
name: Codegen Drift — token 生成物未漂移
|
||||
runs-on: nas
|
||||
runs-on: mac
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
|
||||
ds-flow:
|
||||
name: DS-flow — 原型/跨端同源/代码色单源闸
|
||||
runs-on: nas
|
||||
runs-on: mac
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
# -tags integration(需 docker 起 mysql/redis),见 go-integration job。
|
||||
go-server:
|
||||
name: Go — build + test
|
||||
runs-on: nas
|
||||
runs-on: mac
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -215,7 +215,7 @@ jobs:
|
||||
# 详见 scripts/e2e-smoke.sh + server/test/e2e/。
|
||||
e2e-smoke:
|
||||
name: E2E Smoke — L4 进程级端到端
|
||||
runs-on: nas
|
||||
runs-on: mac
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -241,7 +241,7 @@ jobs:
|
||||
# · -p 1 串行:一次只起一个 mysql 容器,避免并发把 Docker Desktop 压垮/端口资源争用。
|
||||
go-integration:
|
||||
name: Go — integration (mysql/redis testcontainers)
|
||||
runs-on: nas
|
||||
runs-on: mac
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
# tablet/desktop-stats golden 与 stats-overhaul 工作区耦合,待其合并后并入本 job。
|
||||
golden:
|
||||
name: Golden — 视觉回归 (全量:components/auth/desktop/tablet)
|
||||
runs-on: nas
|
||||
runs-on: mac
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -34,11 +34,15 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build-android:
|
||||
runs-on: nas
|
||||
runs-on: mac
|
||||
env:
|
||||
GOPROXY: https://goproxy.cn,direct
|
||||
PUB_HOSTED_URL: https://pub.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:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -106,11 +110,14 @@ jobs:
|
||||
# simply picks up whatever artifacts DO exist — an absent "macos"
|
||||
# artifact is not an error there.
|
||||
build-macos:
|
||||
runs-on: nas
|
||||
runs-on: mac
|
||||
continue-on-error: true
|
||||
env:
|
||||
GOPROXY: https://goproxy.cn,direct
|
||||
PUB_HOSTED_URL: https://pub.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:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -137,11 +144,14 @@ jobs:
|
||||
path: dist/
|
||||
|
||||
build-ios:
|
||||
runs-on: nas
|
||||
runs-on: mac
|
||||
continue-on-error: true
|
||||
env:
|
||||
GOPROXY: https://goproxy.cn,direct
|
||||
PUB_HOSTED_URL: https://pub.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:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -168,7 +178,7 @@ jobs:
|
||||
# (对应平台下载保留 pangolin1 上一版,待可用时下个 client-v* 追上)。
|
||||
release-deploy:
|
||||
needs: [build-android]
|
||||
runs-on: nas
|
||||
runs-on: mac
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
// 国内镜像优先(见 settings.gradle 同注);原仓库保留作 fallback。
|
||||
maven { url "https://maven.aliyun.com/repository/google" }
|
||||
maven { url "https://maven.aliyun.com/repository/public" }
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
+3
-1
@@ -2,4 +2,6 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
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
|
||||
|
||||
@@ -10,6 +10,11 @@ pluginManagement {
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
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()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
|
||||
@@ -206,6 +206,7 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol
|
||||
let ipv4 = NEIPv4Settings(addresses: v4addrs.map(\.address),
|
||||
subnetMasks: v4addrs.map(\.mask))
|
||||
ipv4.includedRoutes = includedRoutes4(options)
|
||||
ipv4.excludedRoutes = excludedRoutes4(options)
|
||||
settings.ipv4Settings = ipv4
|
||||
}
|
||||
let v6addrs = routePrefixes(options.getInet6Address())
|
||||
@@ -213,6 +214,7 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol
|
||||
let ipv6 = NEIPv6Settings(addresses: v6addrs.map(\.address),
|
||||
networkPrefixLengths: v6addrs.map { NSNumber(value: $0.prefix) })
|
||||
ipv6.includedRoutes = includedRoutes6(options)
|
||||
ipv6.excludedRoutes = excludedRoutes6(options)
|
||||
settings.ipv6Settings = ipv6
|
||||
}
|
||||
if let dns = try? options.getDNSServerAddress(), !dns.value.isEmpty {
|
||||
@@ -311,6 +313,19 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol
|
||||
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 技法)
|
||||
private static func tunFd() -> Int32? {
|
||||
var ctlInfo = ctl_info()
|
||||
|
||||
@@ -201,6 +201,7 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol
|
||||
let ipv4 = NEIPv4Settings(addresses: v4addrs.map(\.address),
|
||||
subnetMasks: v4addrs.map(\.mask))
|
||||
ipv4.includedRoutes = includedRoutes4(options)
|
||||
ipv4.excludedRoutes = excludedRoutes4(options)
|
||||
settings.ipv4Settings = ipv4
|
||||
}
|
||||
let v6addrs = routePrefixes(options.getInet6Address())
|
||||
@@ -208,6 +209,7 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol
|
||||
let ipv6 = NEIPv6Settings(addresses: v6addrs.map(\.address),
|
||||
networkPrefixLengths: v6addrs.map { NSNumber(value: $0.prefix) })
|
||||
ipv6.includedRoutes = includedRoutes6(options)
|
||||
ipv6.excludedRoutes = excludedRoutes6(options)
|
||||
settings.ipv6Settings = ipv6
|
||||
}
|
||||
if let dns = try? options.getDNSServerAddress(), !dns.value.isEmpty {
|
||||
@@ -301,6 +303,19 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol
|
||||
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 技法)
|
||||
private static func tunFd() -> Int32? {
|
||||
var ctlInfo = ctl_info()
|
||||
|
||||
+3
-3
@@ -10,13 +10,13 @@
|
||||
| 级别 | **项目级**(绑 `wangjia/pangolin`,只服务本仓库) |
|
||||
| 机器 | mac 本机(`/Users/wangjia/bin/act_runner` v0.6.1) |
|
||||
| 执行模式 | **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` |
|
||||
|
||||
### 为什么是 host 模式 + mac
|
||||
- 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>` 可用。
|
||||
- 理想归宿其实是 **NAS(Linux)host runner**(给 NAS 现有 runner 加 `nas:host` label),mac 只是当前落点。
|
||||
- 理想归宿其实是 **NAS(Linux)host runner**(给 NAS 现有 runner 加 `mac:host` label),mac 只是当前落点。
|
||||
|
||||
### 为什么要 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。
|
||||
@@ -30,7 +30,7 @@ runner:
|
||||
capacity: 1
|
||||
timeout: 3h
|
||||
labels:
|
||||
- "nas:host"
|
||||
- "mac:host"
|
||||
cache:
|
||||
enabled: true
|
||||
host:
|
||||
|
||||
@@ -54,7 +54,10 @@ gomobile init
|
||||
echo "==> [2/3] 拉 sing-box $SINGBOX_VERSION 源码"
|
||||
mkdir -p "$WORK"
|
||||
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"
|
||||
|
||||
echo "==> [3/3] 用官方 build_libbox 编核心库"
|
||||
|
||||
@@ -113,6 +113,12 @@ func BuildClientConfig(node *nodes.NodeRow, dpUUID, deriveKey string, opts Clien
|
||||
"auto_route": true,
|
||||
"strict_route": true,
|
||||
"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 仅在启用时加入(否则不进配置/探测组)。
|
||||
|
||||
@@ -193,3 +193,45 @@ func TestRulesHandler(t *testing.T) {
|
||||
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)")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user