Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Q
qarm_setup_scripts
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
jaysurplus
qarm_setup_scripts
Commits
85b2d526
Commit
85b2d526
authored
Oct 25, 2026
by
jaysurplus@126.com
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
docs(iteration): 归档项目迭代 V0.4.2 - 路由模式与 TUI 完善
parent
4fa3be35
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
1110 additions
and
84 deletions
+1110
-84
docs/tailscale/README.md
+94
-14
meta/ARCHITECTURE.md
+9
-3
meta/FEATURES.md
+1
-1
meta/ITERATIONS.md
+11
-0
meta/PROJECT_MANIFEST.json
+9
-4
meta/REGRESSION.md
+12
-1
scripts/tailscale/tailscale-route-manager.sh
+705
-60
tests/tailscale/test-route-manager.sh
+269
-1
No files found.
docs/tailscale/README.md
View file @
85b2d526
# Tailscale 路由管理器
`scripts/tailscale/tailscale-route-manager.sh`
用于在 QArm 目标 Linux 设备上安装 Tailscale,并
将设备配置为以下局域网的 subnet router
:
`scripts/tailscale/tailscale-route-manager.sh`
用于在 QArm 目标 Linux 设备上安装 Tailscale,并
管理两种互斥的子网路由模式。默认和推荐使用 4via6,为每台 QArm 的重复 IPv4 维护地址提供唯一入口
:
-
`192.168.11.0/24`
-
`192.168.22.0/24`
-
`192.168.11.201/32`
(维护端口
`5800`
)
-
`192.168.22.202/32`
(维护端口
`5800`
)
4via6 模式下,每台 QArm 必须配置唯一的
`Site ID`
。端口不属于 subnet route;应在 Tailnet Policy 中只允许维护人员访问生成的 4via6 地址的 TCP
`5800`
。仅旧 IPv4 模式用于兼容历史部署,不需要 Site ID,但重复网段场景只能由一台 QArm 发布。
## 功能
-
检测、下载并安装 Tailscale。
-
检测
`tailscaled`
和登录状态,必要时运行
`tailscale up`
。
-
提供独立的 Tailscale 登录和退出登录操作;登录时可选择网页认证或 OAuth 自动注册,退出登录前需要确认。
-
使用 OAuth Client 在线读取可分配标签,在 TUI 中输入设备名称并多选标签,自动注册新设备。
-
注册名称默认从
`/opt/armaster/data/hw.json`
读取
`sn`
并生成
`<sn>-`
前缀,文件不可用时回退到系统 hostname。
-
每台设备动态创建 10 分钟有效、单次使用、非临时且预批准的 Auth Key。
-
启用 IPv4 forwarding,并在系统支持时启用 IPv6 forwarding。
-
使用
`tailscale set --advertise-routes=...`
发布两个固定网段。
-
识别路由的“已启用”“部分启用”和“已禁用”状态。
-
禁用时只移除本脚本管理的两个网段,保留设备上其他 advertised routes。
-
支持“仅 4via6(推荐)”和“仅旧 IPv4”两种互斥模式;切换前必须先禁用当前路由。
-
4via6 模式保存本设备唯一的
`Site ID`
,使用
`tailscale debug via`
为两个 IPv4
`/32`
生成路由。
-
旧 IPv4 模式直接发布
`192.168.11.0/24`
、
`192.168.22.0/24`
。
-
使用
`tailscale set --advertise-routes=...`
发布当前模式的受管路由,并移除另一模式的受管路由。
-
识别路由的“未配置”“已启用”“部分启用”和“已禁用”状态。
-
禁用时只移除本脚本管理的 4via6 和旧版 IPv4 路由,保留设备上其他 advertised routes。
-
仅在 forwarding 由本脚本修改时恢复原始状态。
-
优先使用支持逐段着色的
`dialog`
,其次使用
`whiptail`
,否则使用普通终端菜单。
-
对安装、运行、连接和 Route 状态使用统一的状态颜色。
-
状态页显示当前设备在 Tailscale 网络中的名称。
-
主菜单提供独立的“网络检测”,完整显示
`tailscale netcheck`
的结果并返回主页面。
## TUI 导航规则
-
主页面的“退出”是 TUI 正常结束的唯一入口。
-
安装、登录、启用、禁用和状态查看等分支完成后统一返回主页面。
-
安装、
Site ID 配置、登录、退出
登录、启用、禁用和状态查看等分支完成后统一返回主页面。
-
在分支流程中选择“否”、取消、关闭提示框或操作失败时,也会返回主页面。
-
在
`whiptail`
或
`dialog`
主菜单按 Esc 不会退出脚本,而是重新显示主页面。
-
在“是否现在运行
`tailscale up`
”中选择 No,会取消本次启用并直接返回主页面。
-
`dialog`
/
`whiptail`
下选择菜单操作后会立即显示中文处理中界面,网络检测、状态读取和 OAuth 请求期间不会短暂露出底层终端。
-
网页登录和首次安装器执行需要展示认证 URL 或安装日志,因此这两类明确的交互仍会进入终端;结束后返回主页面。
-
`install`
、
`enable`
、
`disable`
等命令行直接调用模式不进入 TUI 循环,仍通过退出码向调用方报告结果。
-
状态页显示脚本版本,可用于确认目标设备是否仍在运行旧副本。
...
...
@@ -49,6 +59,7 @@ sudo apt-get install -y dialog
## 支持环境
-
Linux 目标设备。
-
Tailscale
`1.102.3`
或更新版本;这是 4via6 subnet router 当前的最低安全版本。
-
Bash、Python 3、
`sysctl`
、
`sed`
、
`awk`
、
`grep`
、
`sort`
和
`paste`
。
-
安装阶段需要 HTTPS 网络访问和
`curl`
。
-
自动补装
`curl`
时支持
`apt-get`
、
`dnf`
、
`yum`
或
`zypper`
。
...
...
@@ -78,10 +89,15 @@ sudo tailscale-route-manager
```
bash
sudo
tailscale-route-manager install
sudo
tailscale-route-manager route-mode 4via6
sudo
tailscale-route-manager site-id 101
sudo
tailscale-route-manager login
sudo
tailscale-route-manager
logout
sudo
--preserve-env
=
TS_OAUTH_CLIENT_ID,TS_OAUTH_CLIENT_SECRET
\
tailscale-route-manager register
sudo
tailscale-route-manager
enable
tailscale-route-manager status
tailscale-route-manager netcheck
sudo
tailscale-route-manager disable
```
...
...
@@ -123,6 +139,8 @@ OAuth 自动注册适用于批量部署新的、尚未连接到 Tailscale 的 QA
默认
Provisioner
标签是
`tag
:
qarm-provisioner`。如
Tailnet
使用其他名称,可额外设置
`TS_OAUTH_OWNER_TAG`,并把它加入
`sudo
--preserve-env`。TUI
会读取完整
`tagOwners`,但只展示该
Provisioner
直接拥有、OAuth
Client
实际可以分配的标签。
当可分配标签中存在
`tag
:
qarm`
时,脚本会把它作为基础设备标签自动选中,用户只需按需增加其他标签。普通终端直接回车表示仅使用
`tag
:
qarm`,输入
`
0
`
取消。可通过
`TS_DEFAULT_DEVICE_TAG`
在启动脚本前覆盖默认标签名称。
Provisioner
和设备标签建议全部使用小写,例如
`tag
:
qarm`。Auth
Key
标签与
`tailscale
up
--advertise-tags`
必须完全一致,避免混合大小写被控制端拒绝。
设备名称输入框会优先读取
`/opt/armaster/data/hw.json`
的
`sn`,例如默认显示
`CA
2
EUP
-20261020-001
-`,用户在末尾补充名称即可。脚本只读该文件;文件缺失、JSON
无效、SN
不符合
DNS
标签字符规则或没有后缀空间时,默认值回退到系统
hostname。
...
...
@@ -131,6 +149,16 @@ Provisioner 和设备标签建议全部使用小写,例如 `tag:qarm`。Auth K
OAuth
注册仅用于新设备。已连接设备不会被脚本自动重新认证,避免意外改变设备身份、标签或现有配置。未提供
OAuth
环境变量时,启用路由仍保留原有浏览器登录流程。
##
登录与退出登录
主菜单中的“登录
Tailscale”会提供两种方式:选择“网页登录”时使用
`tailscale
login`
打开标准登录流程;选择“OAuth
自动注册”时复用设备名称和标签选择流程,无需点击登录链接。设备已经连接时不会重复登录。主菜单仍保留“OAuth
注册新设备”快捷入口。
logout
后可以使用任一种方式重新认证。OAuth
方式按新设备流程创建登录身份,旧设备记录可能仍保留在
Tailscale
管理后台,需要管理员按实际情况清理。
“退出登录”使用
`tailscale
logout`,会立即断开设备并使当前登录凭据失效,下次登录需要重新认证。
登录和退出登录只管理
Tailscale
身份状态。退出登录不会自动移除
advertised
routes,也不会恢复
IPv
4
/IPv
6
forwarding;如需清理本脚本管理的子网路由和转发状态,应先选择“禁用子网路由”。
##
安装安全边界
脚本不会使用
`curl
|
sh`。它会:
...
...
@@ -142,17 +170,56 @@ OAuth 注册仅用于新设备。已连接设备不会被脚本自动重新认
SHA
-256
仅用于本次文件识别;由于官方页面未在此流程中提供固定校验值,它不能替代
TLS、来源审阅或软件包签名验证。需要严格供应链验证时,应改用与目标发行版匹配的
Tailscale
官方软件仓库安装步骤。
##
首次启用
##
Site
ID
与
4
via
6
同一
Tailnet
中,每台
QArm
的
Site
ID
必须唯一,允许范围为
`
1-65535
`。脚本不会根据
SN
猜测编号,也不会连接中心服务检查冲突;管理员需要在批量部署清单中分配编号。
执行
`enable`
后,脚本会发布:
可以在
TUI
中选择“配置
4
via
6
Site
ID”,也可以非交互执行:
```
bash
sudo tailscale-route-manager site-id 101
```
配置保存在:
```
text
192.
168.11.0/24,192.168.22.0/24
/var/lib/tailscale-route-manager.site
```
首次发布通常还需要在 [Tailscale Machines](https://login.tailscale.com/admin/machines) 中找到当前设备并批准 advertised routes。若 tailnet 已配置相应 `autoApprovers`,路由可能自动获批。
如果当前 Site ID 的路由仍在发布,脚本会拒绝修改编号。必须先禁用子网路由,避免遗留旧编号对应的 4via6 route。
## 路由模式
目标设备必须能够直接访问这两个局域网。发布成功只表示设备向 tailnet 声明路由,不代表后台已经批准,也不代表局域网链路已验证。
脚本只允许选择一种模式,不会同时发布 4via6 与旧 IPv4 路由:
- `4via6`:显示为“仅 4via6(推荐)”,需要 Site ID,适合多台 QArm 存在相同局域网地址的场景。
- `legacy`:显示为“仅旧 IPv4”,发布 `192.168.11.0/24`、`192.168.22.0/24`,不需要 Site ID。同一 tailnet 中多个 QArm 存在重复网段时,只能选择其中一台发布这些路由。
可以在 TUI 中选择“选择路由模式”,也可以执行:
```
bash
sudo tailscale-route-manager route-mode 4via6
sudo tailscale-route-manager route-mode legacy
```
切换模式前必须先执行 `disable`。如果当前模式的任一路由仍在发布,脚本会拒绝切换。启用新模式时会移除另一模式中本脚本已知的受管路由,同时保留其他 advertised routes。
未包含 `ROUTE_MODE` 的 `0.4.0` 配置文件会继续按 `4via6` 处理。配置文件 `/var/lib/tailscale-route-manager.site` 现在同时保存路由模式和可选的 Site ID。
## 首次启用
在 4via6 模式下,假设本设备 Site ID 为 `101`,执行 `enable` 时等价于分别生成:
```
bash
tailscale debug via 101 192.168.11.201/32
tailscale debug via 101 192.168.22.202/32
```
脚本将当前模式的路由合并到现有 advertised routes,同时移除另一模式的受管路由。设备上的其他路由保持不变。
首次发布通常还需要在 [Tailscale Machines](https://login.tailscale.com/admin/machines) 中找到当前设备并批准生成的 IPv6 advertised routes。若 tailnet 已配置相应 `autoApprovers`,路由可能自动获批。
目标 QArm 必须能够直接访问两个 IPv4 维护地址。发布成功只表示设备向 tailnet 声明路由,不代表后台已经批准,也不代表 `5800` 维护页面可用。
## 客户端访问
...
...
@@ -164,6 +231,15 @@ sudo tailscale set --accept-routes=true
Windows、macOS、iOS 和 Android 通常会自动接受已批准的 subnet routes。
启用 MagicDNS 后,可以使用 Tailscale 自动生成的 4via6 名称。Site ID 为 `101` 时,访问形式为:
```
text
http://192-168-11-201-via-101:5800
http://192-168-22-202-via-101:5800
```
其他 QArm 使用不同 Site ID,因此可以由多个维护人员同时访问,不会被选到另一台设备的重复 IPv4 subnet。
Tailscale subnet router 默认使用 SNAT,因此多数场景不需要在局域网路由器上增加到 `100.64.0.0/10` 的返回路由。本脚本不修改 SNAT 默认值。
## 系统变更与恢复
...
...
@@ -172,10 +248,11 @@ Tailscale subnet router 默认使用 SNAT,因此多数场景不需要在局域
- `/etc/sysctl.d/99-tailscale-routes.conf`
- `/var/lib/tailscale-route-manager.state`
- `/var/lib/tailscale-route-manager.site`
状态文件记录脚本首次修改前的 IPv4/IPv6 forwarding 状态。执行 `disable` 时:
1. 从当前 advertised routes 中移除本
脚本管理的两个网段
。
1. 从当前 advertised routes 中移除本
设备已知 Site ID 对应的 4via6 routes,以及旧版管理的两个 IPv4 `/24`
。
2. 保留其他 advertised routes。
3. 删除带有本脚本标记的 sysctl 文件。
4. 恢复记录的 forwarding 状态并删除状态文件。
...
...
@@ -206,11 +283,14 @@ shellcheck scripts/tailscale/tailscale-route-manager.sh
# 目标设备只读检查
scripts/tailscale/tailscale-route-manager.sh status
scripts/tailscale/tailscale-route-manager.sh netcheck
# 启用后检查
sudo tailscale-route-manager route-mode 4via6
sudo tailscale-route-manager site-id 101
sudo tailscale-route-manager enable
tailscale status
sysctl net.ipv4.ip_forward net.ipv6.conf.all.forwarding
```
最后从另一台已加入同一 tailnet 的设备
访问两个网段中的目标地址
,确认端到端路由可用。
最后从另一台已加入同一 tailnet 的设备
分别访问两个
`via-<site-id>`
MagicDNS 名称的
`5800`
端口
,确认端到端路由可用。
meta/ARCHITECTURE.md
View file @
85b2d526
...
...
@@ -25,7 +25,8 @@
-
下载并审阅 Tailscale 官方安装器后执行安装。
-
检查
`tailscaled`
、登录状态和当前 advertised routes。
-
发布或移除
`192.168.11.0/24`
、
`192.168.22.0/24`
,同时保留设备上其他 advertised routes。
-
管理互斥的“仅 4via6”和“仅旧 IPv4”模式。4via6 模式保存全 Tailnet 唯一的 Site ID,为两个维护地址生成路由;旧 IPv4 模式用于历史兼容。
-
启用任一模式时移除另一模式的受管路由,同时保留设备上其他 advertised routes。
-
在启用子网路由时配置 IP forwarding,在禁用时恢复脚本执行前的状态。
-
通过
`whiptail`
、
`dialog`
或普通终端提供中文单出口 TUI。
-
从
`/opt/armaster/data/hw.json`
只读获取
`sn`
作为设备名默认前缀,收集用户补充名称和标签选择,并调用 helper 完成 OAuth 自动注册;配置缺失或无效时回退到系统 hostname。
...
...
@@ -36,6 +37,7 @@
-
`/etc/sysctl.d/99-tailscale-routes.conf`
:仅在脚本需要开启 forwarding 时创建,包含管理标记。
-
`/var/lib/tailscale-route-manager.state`
:记录 IPv4/IPv6 forwarding 的原始状态,不保存凭证。
-
`/var/lib/tailscale-route-manager.site`
:保存路由模式和可选的 Site ID,带管理标记且不保存凭证;旧配置缺少模式字段时默认为 4via6。
-
OAuth Secret、Access Token 和 Auth Key:只在进程内或权限为
`0600`
的短期文件中存在,不属于持久状态。
主要执行流:
...
...
@@ -44,13 +46,17 @@
主页面
-> 安装/检查 Tailscale
-> OAuth 注册(设备名称 + 在线标签多选)或浏览器登录
-> 选择互斥路由模式
-> 4via6:配置唯一 Site ID 并生成两个 IPv4 /32 对应的 routes
-> 旧 IPv4:使用两个历史 IPv4 /24 routes
-> 读取并合并 advertised routes
-> 移除非当前模式的受管 routes
-> 记录并开启 forwarding
-> 发布
受管子网
-> 发布
当前模式的受管 routes
-> 返回主页面
禁用受管路由
-> 从当前路由集合移除受管
子网
-> 从当前路由集合移除受管
4via6 和旧版 IPv4 routes
-> 保留其他 advertised routes
-> 恢复 forwarding 原状态
-> 返回主页面
...
...
meta/FEATURES.md
View file @
85b2d526
...
...
@@ -4,5 +4,5 @@
| --- | --- | --- |
| Agent 治理骨架 | Done | AGENTS、workflow、rule、skill 与多 Agent 适配层已初始化 |
| 脚本安全约束 | Done | 已定义敏感信息、幂等、权限、危险操作与验证边界 |
| Tailscale Route Manager | Done (V0.
3.2) | 支持 OAuth 批量注册、在线标签多选、短期密钥安全清理及基于
`hw.json`
SN 的设备命名前缀;成功入网和多设备批量验收
待现场复验 |
| Tailscale Route Manager | Done (V0.
4.2) | 支持 OAuth 批量注册、默认 QArm 标签、SN 设备名前缀、登录/退出、网络检测,以及互斥的 4via6 与旧 IPv4 路由模式;多设备批量注册与 TUI 视觉效果仍
待现场复验 |
| 其他 QArm 业务配置脚本 | Pending | 等待后续按真实目标平台和配置需求添加 |
meta/ITERATIONS.md
View file @
85b2d526
# 迭代记录
## [V0.4.2] 4via6 路由模式与 TUI 完善 (Route Modes and TUI Polish)
-
**功能**
: 增加网页登录与 OAuth 登录选择、退出登录、默认
`tag:qarm`
、Tailscale 网络名称和独立
`netcheck`
;将重复 IPv4 维护地址升级为 Site ID 驱动的 4via6 路由,并保留互斥的旧 IPv4 兼容模式;为 TUI 分支增加处理中界面,减少操作间露出底层终端。
-
**改动文件**
:
-
`scripts/tailscale/tailscale-route-manager.sh`
: 实现登录状态操作、网络诊断、Site ID、路由模式互斥、受管路由保护、中文状态和 TUI 过渡。
-
`tests/tailscale/test-route-manager.sh`
: 覆盖路由模式切换、旧配置兼容、路由集合保护、登录状态、默认标签、网络名称、netcheck 与 TUI 过渡。
-
`docs/tailscale/README.md`
: 补充 4via6、旧 IPv4 模式、登录/退出、网络检测、TUI 行为、系统影响和人工验收说明。
-
`meta/ARCHITECTURE.md`
: 更新路由模式、Site ID 持久状态及启用/禁用数据流。
-
`meta/REGRESSION.md`
: 增加路由模式、网络诊断、登录状态和 TUI 过渡回归项。
-
**影响**
: 多台具有重复局域网地址的 QArm 可通过唯一 Site ID 提供独立维护入口,同时允许单设备继续使用旧 IPv4 subnet。
`0.4.2`
已部署到
`192.168.81.118`
,只读复验确认原 Site ID、4via6 routes 和 forwarding 保持不变;真实 TUI 视觉效果及多设备端到端访问仍需现场验收。
## [V0.3.2] OAuth 批量注册与设备命名 (OAuth Provisioning and Device Naming)
-
**功能**
: 新增 OAuth Client 自动注册链路,在线读取 Provisioner 可直接分配的标签,在中文 TUI 中输入设备名称并多选标签;动态创建短期一次性 Auth Key,安全清理临时凭证,并使用
`/opt/armaster/data/hw.json`
的
`sn`
作为设备名称默认前缀。
...
...
meta/PROJECT_MANIFEST.json
View file @
85b2d526
{
"project_name"
:
"qarm_setup_scripts"
,
"version"
:
"0.
3
.2"
,
"iteration_count"
:
4
,
"last_update"
:
"2026-10-2
3
"
,
"version"
:
"0.
4
.2"
,
"iteration_count"
:
5
,
"last_update"
:
"2026-10-2
5
"
,
"stack_summary"
:
[
"PowerShell"
,
"Shell"
,
...
...
@@ -40,6 +40,11 @@
"tailscale_whiptail_width_safe"
:
true
,
"tailscale_dialog_color_ready"
:
true
,
"tailscale_oauth_registration_ready"
:
true
,
"tailscale_hardware_sn_prefix_ready"
:
true
"tailscale_hardware_sn_prefix_ready"
:
true
,
"tailscale_login_logout_ready"
:
true
,
"tailscale_default_qarm_tag_ready"
:
true
,
"tailscale_network_diagnostics_ready"
:
true
,
"tailscale_route_modes_ready"
:
true
,
"tailscale_tui_transition_ready"
:
true
}
}
meta/REGRESSION.md
View file @
85b2d526
...
...
@@ -14,12 +14,22 @@
| 敏感信息检查 | 检查凭证、私钥、token、生产地址和未脱敏日志 | Ready |
| Tailscale 脚本 Bash 语法 |
`bash -n scripts/tailscale/tailscale-route-manager.sh`
| Ready |
| Tailscale 只读命令 |
`tailscale-route-manager.sh help`
与
`status`
不修改系统 | Ready |
| 路由集合保护 | 启用时合并、禁用时仅移除两个 managed routes | Ready |
| 路由集合保护 | 启用时合并当前模式并移除另一模式;禁用时仅移除已知 4via6 与旧版受管 IPv4 routes | Ready |
| 路由模式互斥 | 仅支持
`4via6`
或
`legacy`
;当前模式路由仍发布时拒绝切换;无模式字段的旧配置默认为 4via6 | Ready |
| 4via6 Site ID | 仅接受
`1-65535`
;配置持久化;当前编号路由仍发布时拒绝换号 | Ready |
| 4via6 路由生成 | Site ID 与两个 IPv4
`/32`
生成稳定的独立路由;生成失败时不修改 forwarding 或 advertised routes | Ready |
| 旧版路由兼容 | legacy 模式无需 Site ID;发布两个 IPv4
`/24`
并移除已知 4via6 路由,同时保留无关路由 | Ready |
| Forwarding 恢复 | IPv4/IPv6 原始状态均能独立记录和恢复 | Pending |
| 目标设备端到端路由 | 从 tailnet 客户端访问两个 QArm 局域网 | Pending |
| Tailscale 状态颜色 |
`dialog`
与交互终端显示彩色圆点和中文状态,
`whiptail`
仅显示中文状态,重定向或
`NO_COLOR`
时为纯文本 | Ready |
| Tailscale 网络名称 | 已连接时状态页显示
`Self.HostName`
;未连接时显示“未连接”,字段缺失时显示“未知” | Ready |
| Tailscale 网络检测 | 主菜单和
`netcheck`
命令显示完整检测输出;失败信息可见且 TUI 返回主页面 | Ready |
| Tailscale TUI 操作过渡 |
`dialog`
/
`whiptail`
菜单分支在执行前显示处理中界面;窗口间不主动清屏暴露底层终端 | Ready |
| Whiptail 边框宽度 | 状态文本不得包含双列 emoji;右侧边框不因状态行发生错位 | Ready |
| Tailscale TUI 单出口 | 分支完成、取消、Esc 或失败后返回主页面;仅主页面“退出”结束 TUI | Ready |
| Tailscale 登录 | 未登录设备可执行
`tailscale login`
;已登录设备不重复操作;完成后返回主页面 | Ready |
| Tailscale 登录方式 | TUI 可选择网页登录或 OAuth 自动注册;取消选择返回主页面;OAuth 快捷入口继续保留 | Ready |
| Tailscale 退出登录 | TUI 退出前二次确认;已退出设备不重复操作;不隐式修改子网路由或 forwarding | Ready |
| 未登录时取消启用 |
`tailscale up`
确认框选择 No 后不退出脚本,直接返回主页面 | Ready |
| OAuth 凭证不持久化 | 检查仓库、状态文件和输出不包含 Client Secret、Access Token 或 Auth Key | Ready |
| OAuth 标签权限过滤 | Policy 中仅由 Provisioner 直接拥有的标签进入 TUI 列表 | Ready |
...
...
@@ -27,5 +37,6 @@
| OAuth 注册失败恢复 | 取消或失败后删除临时 Key 文件并返回主页面 | Ready |
| OAuth Auth Key 描述 | 描述只包含 API 接受的 ASCII 字母数字和连字符,且不超过 50 字符 | Ready |
| OAuth 标签大小写 | Provisioner 与设备标签统一使用小写,Auth Key 标签和
`tailscale up`
请求必须一致 | Ready |
| OAuth 默认 QArm 标签 |
`tag:qarm`
可用时自动包含它,用户可增加其他标签;不存在时保持普通多选 | Ready |
| 硬件 SN 名称前缀 | 有效
`hw.json.sn`
生成
`<sn>-`
默认值;缺失、损坏、非法或过长时回退 hostname | Ready |
| OAuth 批量真实注册 | 在至少两台新设备连续运行并核对名称、标签和连接状态 | Pending |
scripts/tailscale/tailscale-route-manager.sh
View file @
85b2d526
...
...
@@ -6,22 +6,31 @@ set -euo pipefail
#
# 支持平台:使用 systemd 或兼容服务管理方式的 Linux。
# 权限要求:安装、修改 sysctl 和配置路由时必须以 root 运行。
# 管理范围:只管理
MANAGED_ROUTES 中声明的子网
路由。
# 管理范围:只管理
TARGET_IPV4_ROUTES 对应的 4via6 路由和旧版
路由。
# 恢复行为:禁用时保留其他 Tailscale advertised routes,并仅恢复本脚本
# 首次启用 forwarding 前记录的内核状态。
readonly
INSTALL_URL
=
"https://tailscale.com/install.sh"
readonly
SCRIPT_VERSION
=
"0.
3
.2"
readonly
SCRIPT_VERSION
=
"0.
4
.2"
readonly
RC_CANCELLED
=
10
readonly
SYSCTL_FILE
=
"/etc/sysctl.d/99-tailscale-routes.conf"
readonly
STATE_FILE
=
"/var/lib/tailscale-route-manager.state"
readonly
MANAGED_MARKER
=
"# Managed by qarm tailscale-route-manager"
readonly
OAUTH_HELPER_NAME
=
"tailscale-oauth-helper.py"
readonly
DEFAULT_HARDWARE_CONFIG_FILE
=
"/opt/armaster/data/hw.json"
readonly
DEFAULT_DEVICE_TAG
=
"
${
TS_DEFAULT_DEVICE_TAG
:-
tag
:qarm
}
"
readonly
SITE_CONFIG_FILE
=
"
${
TS_SITE_CONFIG_FILE
:-
/var/lib/tailscale-route-manager.site
}
"
readonly
SITE_CONFIG_MARKER
=
"# Managed by qarm tailscale-route-manager site config"
readonly
DEFAULT_ROUTE_MODE
=
"4via6"
TEMP_INSTALLER
=
""
TEMP_AUTH_KEY_FILE
=
""
readonly
-a
MANAGED_ROUTES
=(
readonly
-a
TARGET_IPV4_ROUTES
=(
"192.168.11.201/32"
"192.168.22.202/32"
)
readonly
-a
LEGACY_ROUTES
=(
"192.168.11.0/24"
"192.168.22.0/24"
)
...
...
@@ -61,7 +70,6 @@ message_box() {
if
command_exists dialog
;
then
dialog
--colors
--title
"
$title
"
--msgbox
"
$message
"
22 76
||
true
clear
||
true
elif
command_exists whiptail
;
then
whiptail
--title
"
$title
"
--msgbox
"
$message
"
22 76
||
true
else
...
...
@@ -74,6 +82,35 @@ message_box() {
fi
}
output_box
()
{
local
title
=
"
$1
"
local
content
=
"
$2
"
if
command_exists dialog
;
then
dialog
--colors
--scrollbar
--title
"
$title
"
--msgbox
"
$content
"
24 84
||
true
elif
command_exists whiptail
;
then
whiptail
--scrolltext
--title
"
$title
"
--msgbox
"
$content
"
24 84
||
true
else
clear
||
true
printf
'============================================================\n'
printf
'%s\n'
"
$title
"
printf
'============================================================\n\n'
printf
'%s\n\n'
"
$content
"
pause_for_user
fi
}
show_tui_progress
()
{
local
title
=
"
$1
"
local
message
=
"
$2
"
if
command_exists dialog
;
then
dialog
--colors
--title
"
$title
"
--infobox
"
$message
"
7 68
||
true
elif
command_exists whiptail
;
then
whiptail
--title
"
$title
"
--infobox
"
$message
"
7 68
||
true
fi
}
yes_no
()
{
local
title
=
"
$1
"
local
message
=
"
$2
"
...
...
@@ -81,7 +118,6 @@ yes_no() {
if
command_exists dialog
;
then
dialog
--title
"
$title
"
--yesno
"
$message
"
18 76
local
result
=
$?
clear
||
true
return
"
$result
"
elif
command_exists whiptail
;
then
whiptail
--title
"
$title
"
--yesno
"
$message
"
18 76
...
...
@@ -100,7 +136,6 @@ input_value() {
if
command_exists dialog
;
then
dialog
--stdout
--title
"
$title
"
--inputbox
"
$message
"
12 76
"
$default_value
"
local
result
=
$?
clear
||
true
return
"
$result
"
elif
command_exists whiptail
;
then
whiptail
--title
"
$title
"
--inputbox
"
$message
"
12 76
"
$default_value
"
3>&1 1>&2 2>&3
...
...
@@ -114,56 +149,97 @@ input_value() {
select_tags
()
{
local
available_tags
=
"
$1
"
local
-a
tags
=()
local
-a
optional_tags
=()
local
default_available
=
0
local
tag
while
IFS
=
read
-r
tag
;
do
[[
-n
"
$tag
"
]]
&&
tags+
=(
"
$tag
"
)
[[
-n
"
$tag
"
]]
||
continue
tags+
=(
"
$tag
"
)
if
[[
"
${
tag
,,
}
"
==
"
${
DEFAULT_DEVICE_TAG
,,
}
"
]]
;
then
default_available
=
1
else
optional_tags+
=(
"
$tag
"
)
fi
done
<<<
"
$available_tags
"
if
[[
"
${#
tags
[@]
}
"
-eq
0
]]
;
then
return
1
fi
if
[[
"
$default_available
"
-eq
0
]]
;
then
optional_tags
=(
"
${
tags
[@]
}
"
)
elif
[[
"
${#
optional_tags
[@]
}
"
-eq
0
]]
;
then
printf
'%s\n'
"
$DEFAULT_DEVICE_TAG
"
return
0
fi
local
prompt
=
"使用空格选择标签,使用 Enter 确认:"
if
[[
"
$default_available
"
-eq
1
]]
;
then
prompt
=
"已默认选择
$DEFAULT_DEVICE_TAG
。请按需选择其他标签:"
fi
if
command_exists dialog
;
then
local
-a
options
=()
for
tag
in
"
${
tags
[@]
}
"
;
do
for
tag
in
"
${
optional_
tags
[@]
}
"
;
do
options+
=(
"
$tag
"
""
"off"
)
done
dialog
--stdout
--separate-output
--title
"选择设备标签"
\
--checklist
"使用空格选择标签,使用 Enter 确认:"
22 76 12
\
"
${
options
[@]
}
"
local
result
=
$?
clear
||
true
return
"
$result
"
local
selected_output
=
""
local
result
=
0
selected_output
=
"
$(
dialog
--stdout
--separate-output
--title
"选择设备标签"
\
--checklist
"
$prompt
"
22 76 12
"
${
options
[@]
}
"
)
"
||
result
=
$?
[[
"
$result
"
-eq
0
]]
||
return
"
$result
"
[[
"
$default_available
"
-eq
1
]]
&&
printf
'%s\n'
"
$DEFAULT_DEVICE_TAG
"
[[
-n
"
$selected_output
"
]]
&&
printf
'%s\n'
"
$selected_output
"
elif
command_exists whiptail
;
then
local
-a
options
=()
for
tag
in
"
${
tags
[@]
}
"
;
do
for
tag
in
"
${
optional_
tags
[@]
}
"
;
do
options+
=(
"
$tag
"
""
"off"
)
done
whiptail
--separate-output
--title
"选择设备标签"
\
--checklist
"使用空格选择标签,使用 Enter 确认:"
22 76 12
\
"
${
options
[@]
}
"
3>&1 1>&2 2>&3
local
selected_output
=
""
local
result
=
0
selected_output
=
"
$(
whiptail
--separate-output
--title
"选择设备标签"
\
--checklist
"
$prompt
"
22 76 12
"
${
options
[@]
}
"
3>&1 1>&2 2>&3
)
"
||
result
=
$?
[[
"
$result
"
-eq
0
]]
||
return
"
$result
"
[[
"
$default_available
"
-eq
1
]]
&&
printf
'%s\n'
"
$DEFAULT_DEVICE_TAG
"
[[
-n
"
$selected_output
"
]]
&&
printf
'%s\n'
"
$selected_output
"
else
printf
'\n可分配的设备标签:\n\n'
>
&2
if
[[
"
$default_available
"
-eq
1
]]
;
then
printf
'\n默认设备标签:%s\n\n可选附加标签:\n\n'
"
$DEFAULT_DEVICE_TAG
"
>
&2
else
printf
'\n可分配的设备标签:\n\n'
>
&2
fi
local
index
for
index
in
"
${
!tags[@]
}
"
;
do
printf
'%d) %s\n'
"
$((
index
+
1
))
"
"
${
tags
[
$index
]
}
"
>
&2
for
index
in
"
${
!
optional_
tags[@]
}
"
;
do
printf
'%d) %s\n'
"
$((
index
+
1
))
"
"
${
optional_
tags
[
$index
]
}
"
>
&2
done
printf
'\n请输入序号,多个序号使用逗号分隔;直接回车取消。\n'
>
&2
if
[[
"
$default_available
"
-eq
1
]]
;
then
printf
'\n请输入附加标签序号,多个序号使用逗号分隔;直接回车仅使用默认标签,输入 0 取消。\n'
>
&2
else
printf
'\n请输入序号,多个序号使用逗号分隔;直接回车取消。\n'
>
&2
fi
local
selection
read
-r
-p
"标签序号: "
selection
[[
-n
"
$selection
"
]]
||
return
"
$RC_CANCELLED
"
if
[[
-z
"
$selection
"
]]
;
then
if
[[
"
$default_available
"
-eq
1
]]
;
then
printf
'%s\n'
"
$DEFAULT_DEVICE_TAG
"
return
0
fi
return
"
$RC_CANCELLED
"
fi
[[
"
$selection
"
!=
"0"
]]
||
return
"
$RC_CANCELLED
"
local
-a
selected
=()
[[
"
$default_available
"
-eq
1
]]
&&
selected+
=(
"
$DEFAULT_DEVICE_TAG
"
)
local
-a
requested_indices
=()
local
raw_index
IFS
=
','
read
-r
-a
requested_indices
<<<
"
$selection
"
for
raw_index
in
"
${
requested_indices
[@]
}
"
;
do
raw_index
=
"
${
raw_index
//[[
:space:]]/
}
"
if
[[
!
"
$raw_index
"
=
~ ^[0-9]+
$
]]
\
||
((
raw_index < 1
||
raw_index
>
${#
tags
[@]
}
))
;
then
||
((
raw_index < 1
||
raw_index
>
${#
optional_
tags
[@]
}
))
;
then
return
1
fi
tag
=
"
${
tags
[
$((
raw_index
-
1
))
]
}
"
tag
=
"
${
optional_
tags
[
$((
raw_index
-
1
))
]
}
"
if
[[
!
"
${
selected
[*]
}
"
=
~
"
${
tag
}
"
]]
;
then
selected+
=(
"
$tag
"
)
fi
...
...
@@ -295,6 +371,12 @@ get_backend_state() {
| head
-n
1
}
get_tailscale_network_name
()
{
tailscale status
--json
2>/dev/null
\
| sed
-n
's/.*"HostName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'
\
| head
-n
1
}
is_tailscale_logged_in
()
{
[[
"
$(
get_backend_state
||
true
)
"
==
"Running"
]]
}
...
...
@@ -416,6 +498,7 @@ oauth_register_device() {
local
helper
helper
=
"
$(
find_oauth_helper
)
"
local
available_tags
show_tui_progress
"OAuth 自动注册"
"正在读取可分配的设备标签,请稍候……"
if
!
available_tags
=
"
$(
python3
"
$helper
"
list-tags 2>&1
)
"
;
then
message_box
"标签读取失败"
"
$available_tags
"
return
1
...
...
@@ -453,6 +536,7 @@ $(printf '%s\n' "$selected_tags" | sed 's/^/ /')
fi
local
auth_key
show_tui_progress
"OAuth 自动注册"
"正在创建临时注册密钥,请稍候……"
if
!
auth_key
=
"
$(
python3
"
$helper
"
create-auth-key
\
--tags
"
$selected_csv
"
\
--description
"qarm registration
$device_name
"
\
...
...
@@ -467,6 +551,7 @@ $(printf '%s\n' "$selected_tags" | sed 's/^/ /')
unset
auth_key
local
up_result
=
0
show_tui_progress
"OAuth 自动注册"
"正在将设备注册到 Tailscale,请稍候……"
tailscale up
\
--auth-key
=
"file:
$TEMP_AUTH_KEY_FILE
"
\
--hostname
=
"
$device_name
"
\
...
...
@@ -515,6 +600,111 @@ ensure_tailscale_ready() {
is_tailscale_logged_in
}
login_tailscale
()
{
if
!
is_tailscale_installed
;
then
message_box
"无法登录"
"Tailscale 尚未安装。
\n\n
请先选择“安装/检查 Tailscale”。"
return
1
fi
if
command_exists systemctl
&&
!
systemctl start tailscaled
;
then
message_box
"登录失败"
"无法启动 tailscaled 服务。请检查 systemd 状态。"
return
1
fi
if
is_tailscale_logged_in
;
then
message_box
"Tailscale 登录"
"当前设备已经登录,无需重复操作。
\n\n
网络名称:
$(
get_tailscale_network_name 2>/dev/null
||
echo
未知
)
"
return
0
fi
clear
||
true
local
login_result
=
0
tailscale login
||
login_result
=
$?
if
[[
"
$login_result
"
-ne
0
]]
||
!
is_tailscale_logged_in
;
then
message_box
"登录失败"
"Tailscale 未能完成登录。
\n\n
请检查终端输出、网络连接和登录授权。"
return
1
fi
message_box
"登录完成"
"设备已登录 Tailscale。
\n\n
网络名称:
$(
get_tailscale_network_name 2>/dev/null
||
echo
未知
)
"
}
logout_tailscale
()
{
if
!
is_tailscale_installed
;
then
message_box
"无法退出登录"
"Tailscale 尚未安装。"
return
1
fi
if
!
is_tailscale_logged_in
;
then
message_box
"Tailscale 退出登录"
"当前设备已经处于未登录状态。"
return
0
fi
if
!
tailscale
logout
;
then
message_box
"退出登录失败"
"Tailscale 未能完成退出登录。请检查上方输出。"
return
1
fi
if
is_tailscale_logged_in
;
then
message_box
"退出登录失败"
"命令已执行,但设备仍显示为已连接状态。"
return
1
fi
message_box
"退出登录完成"
"设备已断开 Tailscale,当前登录凭据已经失效。
\n\n
本操作未修改子网路由设置或 IP 转发状态。"
}
confirm_logout_tailscale
()
{
if
!
yes_no
"确认退出 Tailscale"
"退出登录会立即断开当前设备,并使当前登录凭据失效。
\n\n
本操作不会自动禁用子网路由或恢复 IP 转发。是否继续?"
;
then
return
"
$RC_CANCELLED
"
fi
logout_tailscale
}
select_login_method
()
{
local
choice
local
prompt
=
"请选择 Tailscale 登录方式。
\n\n
OAuth 自动注册会按新设备流程创建登录身份,旧设备记录可能仍需在管理后台清理。"
if
command_exists dialog
;
then
choice
=
"
$(
dialog
--stdout
--title
"选择登录方式"
\
--menu
"
$prompt
"
18 78 4
\
"manual"
"网页登录(需要打开链接)"
\
"oauth"
"OAuth 自动注册(无需点击链接)"
)
"
||
return
"
$RC_CANCELLED
"
elif
command_exists whiptail
;
then
choice
=
"
$(
whiptail
--title
"选择登录方式"
\
--menu
"
$prompt
"
18 78 4
\
"manual"
"网页登录(需要打开链接)"
\
"oauth"
"OAuth 自动注册(无需点击链接)"
\
3>&1 1>&2 2>&3
)
"
||
return
"
$RC_CANCELLED
"
else
printf
'\n请选择 Tailscale 登录方式:\n\n'
>
&2
printf
'1) 网页登录(需要打开链接)\n'
>
&2
printf
'2) OAuth 自动注册(无需点击链接)\n'
>
&2
printf
'0) 取消并返回主页面\n\n'
>
&2
read
-r
-p
"请选择 [0-2]: "
choice
case
"
$choice
"
in
1
)
choice
=
"manual"
;;
2
)
choice
=
"oauth"
;;
0
)
return
"
$RC_CANCELLED
"
;;
*
)
return
1
;;
esac
fi
printf
'%s\n'
"
$choice
"
}
login_from_menu
()
{
if
is_tailscale_installed
&&
is_tailscale_logged_in
;
then
login_tailscale
return
$?
fi
local
method
method
=
"
$(
select_login_method
)
"
||
return
$?
case
"
$method
"
in
manual
)
login_tailscale
;;
oauth
)
oauth_register_device
;;
*
)
return
1
;;
esac
}
ipv4_forward_enabled
()
{
[[
"
$(
sysctl
-n
net.ipv4.ip_forward 2>/dev/null
||
echo
0
)
"
==
"1"
]]
}
...
...
@@ -622,6 +812,233 @@ restore_forwarding() {
rm
-f
--
"
$STATE_FILE
"
}
valid_site_id
()
{
local
value
=
"
$1
"
[[
"
$value
"
=
~ ^[1-9][0-9]
*
$
]]
&&
((
value <
=
65535
))
}
valid_route_mode
()
{
[[
"
$1
"
==
"4via6"
]]
||
[[
"
$1
"
==
"legacy"
]]
}
get_route_mode
()
{
if
[[
!
-f
"
$SITE_CONFIG_FILE
"
]]
;
then
printf
'%s\n'
"
$DEFAULT_ROUTE_MODE
"
return
0
fi
grep
-Fqx
"
$SITE_CONFIG_MARKER
"
"
$SITE_CONFIG_FILE
"
||
return
1
local
value
value
=
"
$(
sed
-n
's/^ROUTE_MODE=//p'
"
$SITE_CONFIG_FILE
"
| head
-n
1
)
"
[[
-n
"
$value
"
]]
||
value
=
"
$DEFAULT_ROUTE_MODE
"
valid_route_mode
"
$value
"
||
return
1
printf
'%s\n'
"
$value
"
}
get_site_id
()
{
[[
-f
"
$SITE_CONFIG_FILE
"
]]
||
return
1
grep
-Fqx
"
$SITE_CONFIG_MARKER
"
"
$SITE_CONFIG_FILE
"
||
return
1
local
value
value
=
"
$(
sed
-n
's/^SITE_ID=\([0-9][0-9]*\)$/\1/p'
"
$SITE_CONFIG_FILE
"
| head
-n
1
)
"
valid_site_id
"
$value
"
||
return
1
printf
'%s\n'
"
$value
"
}
write_route_config
()
{
local
site_id
=
"
$1
"
local
route_mode
=
"
$2
"
[[
-z
"
$site_id
"
]]
||
valid_site_id
"
$site_id
"
||
return
1
valid_route_mode
"
$route_mode
"
||
return
1
if
[[
-f
"
$SITE_CONFIG_FILE
"
]]
&&
!
grep
-Fqx
"
$SITE_CONFIG_MARKER
"
"
$SITE_CONFIG_FILE
"
;
then
message_box
"配置冲突"
"
$SITE_CONFIG_FILE
已存在且不是本脚本创建的文件。
\n\n
为避免覆盖人工配置,本次操作已停止。"
return
1
fi
local
temporary
temporary
=
"
$(
mktemp /tmp/tailscale-site-config.XXXXXX
)
"
{
printf
'%s\n'
"
$SITE_CONFIG_MARKER
"
printf
'ROUTE_MODE=%s\n'
"
$route_mode
"
[[
-z
"
$site_id
"
]]
||
printf
'SITE_ID=%s\n'
"
$site_id
"
}
>
"
$temporary
"
install
-d
-m
0755
"
$(
dirname
"
$SITE_CONFIG_FILE
"
)
"
if
!
install
-m
0644
"
$temporary
"
"
$SITE_CONFIG_FILE
"
;
then
rm
-f
--
"
$temporary
"
return
1
fi
rm
-f
--
"
$temporary
"
}
save_site_id
()
{
local
value
=
"
$1
"
local
route_mode
valid_site_id
"
$value
"
||
return
1
route_mode
=
"
$(
get_route_mode 2>/dev/null
||
printf
'%s\n'
"
$DEFAULT_ROUTE_MODE
"
)
"
write_route_config
"
$value
"
"
$route_mode
"
}
save_route_mode
()
{
local
route_mode
=
"
$1
"
local
site_id
=
""
valid_route_mode
"
$route_mode
"
||
return
1
site_id
=
"
$(
get_site_id 2>/dev/null
||
true
)
"
write_route_config
"
$site_id
"
"
$route_mode
"
}
get_4via6_routes_for_site
()
{
local
site_id
=
"
$1
"
valid_site_id
"
$site_id
"
||
return
1
is_tailscale_installed
||
return
1
local
target
local
generated
for
target
in
"
${
TARGET_IPV4_ROUTES
[@]
}
"
;
do
generated
=
"
$(
tailscale debug via
"
$site_id
"
"
$target
"
2>/dev/null | tail
-n
1
)
"
||
return
1
[[
"
$generated
"
=
~ ^[0-9A-Fa-f:]+/[0-9]+
$
]]
||
return
1
printf
'%s\n'
"
$generated
"
done
}
get_managed_routes
()
{
local
route_mode
route_mode
=
"
$(
get_route_mode
)
"
||
return
1
if
[[
"
$route_mode
"
==
"legacy"
]]
;
then
printf
'%s\n'
"
${
LEGACY_ROUTES
[@]
}
"
return
0
fi
local
site_id
site_id
=
"
$(
get_site_id
)
"
||
return
1
get_4via6_routes_for_site
"
$site_id
"
}
get_all_owned_routes
()
{
printf
'%s\n'
"
${
LEGACY_ROUTES
[@]
}
"
local
site_id
site_id
=
"
$(
get_site_id 2>/dev/null
||
true
)
"
if
[[
-n
"
$site_id
"
]]
&&
is_tailscale_installed
;
then
get_4via6_routes_for_site
"
$site_id
"
2>/dev/null
||
true
fi
}
configure_site_id
()
{
local
requested
=
"
${
1
:-}
"
local
current
=
""
current
=
"
$(
get_site_id 2>/dev/null
||
true
)
"
if
[[
-z
"
$requested
"
]]
;
then
requested
=
"
$(
input_value
"配置 Site ID"
"请输入本设备唯一的 4via6 Site ID。
\n\n
允许范围:1-65535
\n
同一 Tailnet 中不得与其他 QArm 重复。"
"
$current
"
)
"
||
return
"
$RC_CANCELLED
"
fi
if
!
valid_site_id
"
$requested
"
;
then
message_box
"Site ID 无效"
"Site ID 必须是 1-65535 之间的整数。"
return
1
fi
if
[[
-n
"
$current
"
]]
&&
[[
"
$current
"
!=
"
$requested
"
]]
&&
is_tailscale_installed
;
then
local
advertised
local
current_site_routes
if
!
advertised
=
"
$(
get_all_advertised_routes
)
"
;
then
message_box
"无法修改 Site ID"
"无法读取当前 advertised routes。为避免遗留旧路由,本次操作已停止。"
return
1
fi
if
!
current_site_routes
=
"
$(
get_4via6_routes_for_site
"
$current
"
)
"
;
then
message_box
"无法修改 Site ID"
"无法生成当前 Site ID 的 4via6 路由。为避免遗留旧路由,本次操作已停止。"
return
1
fi
local
route
while
IFS
=
read
-r
route
;
do
if
printf
'%s\n'
"
$advertised
"
|
grep
-Fqx
"
$route
"
;
then
message_box
"无法修改 Site ID"
"当前 Site ID 的 4via6 路由仍在发布。
\n\n
请先禁用子网路由,再修改 Site ID。"
return
1
fi
done
<<<
"
$current_site_routes
"
fi
save_site_id
"
$requested
"
||
return
1
message_box
"Site ID 已保存"
"本设备 Site ID:
$requested
\n\n
启用子网路由时,将为配置的 IPv4 目标生成唯一 4via6 路由。"
}
select_route_mode
()
{
local
choice
if
command_exists dialog
;
then
choice
=
"
$(
dialog
--stdout
--title
"选择路由模式"
\
--menu
"两种模式互斥。切换模式前必须先禁用当前路由。"
16 76 4
\
"4via6"
"仅 4via6(推荐)"
\
"legacy"
"仅旧 IPv4"
)
"
||
return
"
$RC_CANCELLED
"
elif
command_exists whiptail
;
then
choice
=
"
$(
whiptail
--title
"选择路由模式"
\
--menu
"两种模式互斥。切换模式前必须先禁用当前路由。"
16 76 4
\
"4via6"
"仅 4via6(推荐)"
\
"legacy"
"仅旧 IPv4"
\
3>&1 1>&2 2>&3
)
"
||
return
"
$RC_CANCELLED
"
else
printf
'\n请选择路由模式:\n\n'
>
&2
printf
'1) 仅 4via6(推荐)\n'
>
&2
printf
'2) 仅旧 IPv4\n'
>
&2
printf
'0) 取消并返回主页面\n\n'
>
&2
read
-r
-p
"请选择 [0-2]: "
choice
case
"
$choice
"
in
1
)
choice
=
"4via6"
;;
2
)
choice
=
"legacy"
;;
0
)
return
"
$RC_CANCELLED
"
;;
*
)
return
1
;;
esac
fi
printf
'%s\n'
"
$choice
"
}
configure_route_mode
()
{
local
requested
=
"
${
1
:-}
"
local
current
current
=
"
$(
get_route_mode
)
"
||
{
message_box
"路由模式无效"
"
$SITE_CONFIG_FILE
中的路由模式无效。"
return
1
}
if
[[
-z
"
$requested
"
]]
;
then
requested
=
"
$(
select_route_mode
)
"
||
return
$?
fi
if
!
valid_route_mode
"
$requested
"
;
then
message_box
"路由模式无效"
"只允许使用 4via6 或 legacy。"
return
1
fi
if
[[
"
$requested
"
==
"
$current
"
]]
;
then
message_box
"路由模式"
"当前已经是:
$(
[[
"
$current
"
==
"4via6"
]]
&&
echo
'仅 4via6(推荐)'
||
echo
'仅旧 IPv4'
)
"
return
0
fi
if
is_tailscale_installed
;
then
local
advertised
local
current_mode_routes
=
""
if
!
advertised
=
"
$(
get_all_advertised_routes
)
"
;
then
message_box
"无法切换路由模式"
"无法读取当前 advertised routes。"
return
1
fi
if
[[
"
$current
"
==
"legacy"
]]
||
get_site_id
>
/dev/null 2>&1
;
then
if
!
current_mode_routes
=
"
$(
get_managed_routes
)
"
;
then
message_box
"无法切换路由模式"
"无法确定当前模式管理的 routes。"
return
1
fi
fi
local
route
while
IFS
=
read
-r
route
;
do
[[
-z
"
$route
"
]]
&&
continue
if
printf
'%s\n'
"
$advertised
"
|
grep
-Fqx
"
$route
"
;
then
message_box
"无法切换路由模式"
"当前模式的路由仍在发布。
\n\n
请先禁用子网路由,再切换模式。"
return
1
fi
done
<<<
"
$current_mode_routes
"
fi
save_route_mode
"
$requested
"
||
return
1
message_box
"路由模式已保存"
"当前路由模式:
$(
[[
"
$requested
"
==
"4via6"
]]
&&
echo
'仅 4via6(推荐)'
||
echo
'仅旧 IPv4'
)
"
}
get_all_advertised_routes
()
{
local
prefs
prefs
=
"
$(
tailscale debug prefs 2>/dev/null
)
"
||
return
1
...
...
@@ -643,9 +1060,9 @@ get_all_advertised_routes() {
route_is_managed
()
{
local
candidate
=
"
$1
"
local
managed
for
managed
in
"
${
MANAGED_ROUTES
[@]
}
"
;
do
while
IFS
=
read
-r
managed
;
do
[[
"
$candidate
"
==
"
$managed
"
]]
&&
return
0
done
done
< <
(
get_all_owned_routes
)
return
1
}
...
...
@@ -655,17 +1072,24 @@ route_is_advertised() {
}
all_routes_enabled
()
{
local
managed_routes
managed_routes
=
"
$(
get_managed_routes
)
"
||
return
1
[[
-n
"
$managed_routes
"
]]
||
return
1
local
route
for
route
in
"
${
MANAGED_ROUTES
[@]
}
"
;
do
while
IFS
=
read
-r
route
;
do
route_is_advertised
"
$route
"
||
return
1
done
done
<<<
"
$managed_routes
"
}
any_route_enabled
()
{
local
managed_routes
managed_routes
=
"
$(
get_managed_routes
)
"
||
return
1
local
route
for
route
in
"
${
MANAGED_ROUTES
[@]
}
"
;
do
while
IFS
=
read
-r
route
;
do
route_is_advertised
"
$route
"
&&
return
0
done
done
<<<
"
$managed_routes
"
return
1
}
...
...
@@ -679,13 +1103,41 @@ routes_without_managed() {
merge_with_managed_routes
()
{
local
current_routes
=
"
$1
"
local
remaining_routes
local
managed_routes
remaining_routes
=
"
$(
printf
'%s\n'
"
$current_routes
"
| routes_without_managed
)
"
managed_routes
=
"
$(
get_managed_routes
)
"
||
return
1
{
printf
'%s\n'
"
$
current
_routes
"
printf
'%s\n'
"
$
{
MANAGED_ROUTES
[@]
}
"
printf
'%s\n'
"
$
remaining
_routes
"
printf
'%s\n'
"
$
managed_routes
"
}
| sed
'/^$/d'
| sort
-u
| paste
-sd
, -
}
enable_routes
()
{
if
!
is_tailscale_installed
;
then
install_tailscale
||
return
1
fi
local
route_mode
if
!
route_mode
=
"
$(
get_route_mode
)
"
;
then
message_box
"启用失败"
"
$SITE_CONFIG_FILE
中的路由模式无效。"
return
1
fi
if
[[
"
$route_mode
"
==
"4via6"
]]
&&
!
get_site_id
>
/dev/null
;
then
configure_site_id
||
return
$?
fi
local
managed_routes
if
!
managed_routes
=
"
$(
get_managed_routes
)
"
;
then
if
[[
"
$route_mode
"
==
"4via6"
]]
;
then
message_box
"启用失败"
"无法生成 4via6 路由。
\n\n
请检查 Tailscale 版本、Site ID 和目标配置。"
else
message_box
"启用失败"
"无法读取旧 IPv4 路由配置。"
fi
return
1
fi
local
ready_result
=
0
ensure_tailscale_ready
||
ready_result
=
$?
...
...
@@ -704,18 +1156,45 @@ enable_routes() {
return
1
fi
local
merged_routes
merged_routes
=
"
$(
merge_with_managed_routes
"
$current_routes
"
)
"
||
return
1
enable_forwarding
||
return
1
tailscale
set
--advertise-routes
=
"
$(
merge_with_managed_routes
"
$current_routes
"
)
"
if
!
tailscale
set
--advertise-routes
=
"
$merged_routes
"
;
then
message_box
"启用失败"
"Tailscale 未能更新 advertised routes。"
return
1
fi
message_box
"子网路由已启用"
"已发布以下 Tailscale 子网路由:
if
[[
"
$route_mode
"
==
"4via6"
]]
;
then
message_box
"子网路由已启用"
"路由模式:仅 4via6(推荐)
${
MANAGED_ROUTES
[0]
}
${
MANAGED_ROUTES
[1]
}
已发布以下 4via6 路由:
$(
printf
'%s\n'
"
$managed_routes
"
| sed
's/^/ /'
)
对应 IPv4 目标:
$(
printf
'%s\n'
"
${
TARGET_IPV4_ROUTES
[@]
}
"
| sed
's/^/ /'
)
IPv4 Forwarding:
$(
sysctl
-n
net.ipv4.ip_forward
)
IPv6 Forwarding:
$(
sysctl
-n
net.ipv6.conf.all.forwarding 2>/dev/null
||
echo
N/A
)
首次启用时,还需在 Tailscale Admin Console 中批准这些路由。"
旧版重复 IPv4 /24 路由已从本设备的发布列表移除。
首次启用时,还需在 Tailscale Admin Console 中批准这些 4via6 路由。"
else
message_box
"子网路由已启用"
"路由模式:仅旧 IPv4
已发布以下旧 IPv4 路由:
$(
printf
'%s\n'
"
$managed_routes
"
| sed
's/^/ /'
)
IPv4 Forwarding:
$(
sysctl
-n
net.ipv4.ip_forward
)
IPv6 Forwarding:
$(
sysctl
-n
net.ipv6.conf.all.forwarding 2>/dev/null
||
echo
N/A
)
本设备已知的 4via6 路由已从发布列表移除。
注意:多个 QArm 设备存在重复 IPv4 网段时,只能有一台设备发布这些旧 IPv4 路由。"
fi
}
disable_routes
()
{
...
...
@@ -724,6 +1203,18 @@ disable_routes() {
return
0
fi
if
[[
-f
"
$SITE_CONFIG_FILE
"
]]
;
then
local
route_mode
if
!
route_mode
=
"
$(
get_route_mode
)
"
;
then
message_box
"禁用失败"
"
$SITE_CONFIG_FILE
中的路由模式无效。
\n\n
为避免删除错误路由,本次操作已停止。"
return
1
fi
if
[[
"
$route_mode
"
==
"4via6"
]]
&&
!
get_site_id
>
/dev/null 2>&1
;
then
message_box
"禁用失败"
"
$SITE_CONFIG_FILE
缺少有效 Site ID。
\n\n
为避免遗留无法识别的 4via6 路由,本次操作已停止。"
return
1
fi
fi
local
current_routes
if
!
current_routes
=
"
$(
get_all_advertised_routes
)
"
;
then
message_box
"禁用失败"
"无法读取当前 advertised routes。为避免删除其他路由,未执行任何修改。"
...
...
@@ -737,13 +1228,16 @@ disable_routes() {
remaining_csv
=
"
$(
printf
'%s\n'
"
$remaining_routes
"
| paste
-sd
, -
)
"
fi
tailscale
set
--advertise-routes
=
"
$remaining_csv
"
if
!
tailscale
set
--advertise-routes
=
"
$remaining_csv
"
;
then
message_box
"禁用失败"
"Tailscale 未能更新 advertised routes。"
return
1
fi
restore_forwarding
||
return
1
message_box
"子网路由已禁用"
"已停止发布本脚本管理的
子网:
message_box
"子网路由已禁用"
"已停止发布本脚本管理的
4via6 路由和旧版 IPv4 路由。
${
MANAGED_ROUTES
[0]
}
${
MANAGED_ROUTES
[1]
}
IPv4 目标:
$(
printf
'%s\n'
"
${
TARGET_IPV4_ROUTES
[@]
}
"
| sed
's/^/ /'
)
设备上其他 advertised routes 已保留。
...
...
@@ -752,7 +1246,9 @@ IPv6 Forwarding: $(sysctl -n net.ipv6.conf.all.forwarding 2>/dev/null || echo N/
}
get_route_status
()
{
if
all_routes_enabled
;
then
if
!
get_managed_routes
>
/dev/null 2>&1
;
then
echo
"未配置"
elif
all_routes_enabled
;
then
echo
"已启用"
elif
any_route_enabled
;
then
echo
"部分启用"
...
...
@@ -823,12 +1319,32 @@ get_status_text() {
local
installed
local
daemon
local
connected
local
network_name
=
"未连接"
local
route_mode
local
route_mode_label
local
site_id
=
"未配置"
local
managed_routes_text
=
"请先配置 Site ID"
local
inactive_status
=
"无"
local
advertised
=
"无"
local
route_status
installed
=
"
$(
format_status_value error
"未安装"
"
$mode
"
)
"
daemon
=
"
$(
format_status_value warn
"未知"
"
$mode
"
)
"
connected
=
"
$(
format_status_value error
"未连接"
"
$mode
"
)
"
route_mode
=
"
$(
get_route_mode 2>/dev/null
||
echo
invalid
)
"
case
"
$route_mode
"
in
4via6
)
route_mode_label
=
"仅 4via6(推荐)"
;;
legacy
)
route_mode_label
=
"仅旧 IPv4"
;;
*
)
route_mode_label
=
"配置无效"
;;
esac
site_id
=
"
$(
get_site_id 2>/dev/null
||
true
)
"
if
[[
-z
"
$site_id
"
]]
;
then
if
[[
"
$route_mode
"
==
"legacy"
]]
;
then
site_id
=
"未配置(旧 IPv4 模式不需要)"
else
site_id
=
"未配置"
fi
fi
if
is_tailscale_installed
;
then
installed
=
"
$(
format_status_value ok
"已安装"
"
$mode
"
)
"
...
...
@@ -841,11 +1357,42 @@ get_status_text() {
fi
if
is_tailscale_logged_in
;
then
connected
=
"
$(
format_status_value ok
"已连接"
"
$mode
"
)
"
network_name
=
"
$(
get_tailscale_network_name 2>/dev/null
||
true
)
"
[[
-z
"
$network_name
"
]]
&&
network_name
=
"未知"
fi
advertised
=
"
$(
get_all_advertised_routes 2>/dev/null
||
echo
未知
)
"
[[
-z
"
$advertised
"
]]
&&
advertised
=
"无"
fi
if
[[
"
$route_mode
"
==
"legacy"
]]
;
then
managed_routes_text
=
"
$(
get_managed_routes 2>/dev/null
||
echo
读取失败
)
"
elif
[[
"
$route_mode
"
==
"4via6"
]]
&&
[[
"
$site_id
"
!=
"未配置"
]]
&&
is_tailscale_installed
;
then
managed_routes_text
=
"
$(
get_managed_routes 2>/dev/null
||
echo
生成失败
)
"
elif
[[
"
$route_mode
"
==
"invalid"
]]
;
then
managed_routes_text
=
"路由模式配置无效"
elif
[[
"
$site_id
"
!=
"未配置"
]]
;
then
managed_routes_text
=
"Tailscale 未安装,无法生成 4via6 路由"
fi
local
inactive_route
local
inactive_routes
=
""
if
[[
"
$route_mode
"
==
"4via6"
]]
;
then
inactive_routes
=
"
$(
printf
'%s\n'
"
${
LEGACY_ROUTES
[@]
}
"
)
"
elif
[[
"
$route_mode
"
==
"legacy"
]]
;
then
local
configured_site_id
=
""
configured_site_id
=
"
$(
get_site_id 2>/dev/null
||
true
)
"
if
[[
-n
"
$configured_site_id
"
]]
&&
is_tailscale_installed
;
then
inactive_routes
=
"
$(
get_4via6_routes_for_site
"
$configured_site_id
"
2>/dev/null
||
true
)
"
fi
fi
while
IFS
=
read
-r
inactive_route
;
do
[[
-z
"
$inactive_route
"
]]
&&
continue
if
printf
'%s\n'
"
$advertised
"
|
grep
-Fqx
"
$inactive_route
"
;
then
inactive_status
=
"检测到(启用当前模式时移除)"
break
fi
done
<<<
"
$inactive_routes
"
route_status
=
"
$(
get_route_status
)
"
case
"
$route_status
"
in
已启用
)
route_status
=
"
$(
format_status_value ok
"
$route_status
"
"
$mode
"
)
"
;;
...
...
@@ -863,13 +1410,20 @@ Tailscale 状态
安装状态 :
$installed
运行状态 :
$daemon
连接状态 :
$connected
网络名称 :
$network_name
路由模式 :
$route_mode_label
Site ID :
$site_id
IPv4 维护目标
------------------------------------------------------------
$(
printf
'%s\n'
"
${
TARGET_IPV4_ROUTES
[@]
}
"
)
受管子网
路由
当前受管
路由
------------------------------------------------------------
${
MANAGED_ROUTES
[0]
}
${
MANAGED_ROUTES
[1]
}
$managed_routes_text
路由状态 :
$route_status
非当前模式路由 :
$inactive_status
IP 转发
------------------------------------------------------------
...
...
@@ -885,6 +1439,10 @@ EOF
get_main_action
()
{
if
!
is_tailscale_installed
;
then
echo
"安装 Tailscale"
elif
!
get_route_mode
>
/dev/null 2>&1
;
then
echo
"选择路由模式"
elif
[[
"
$(
get_route_mode
)
"
==
"4via6"
]]
&&
!
get_site_id
>
/dev/null 2>&1
;
then
echo
"配置 Site ID"
elif
all_routes_enabled
;
then
echo
"禁用子网路由"
else
...
...
@@ -892,9 +1450,20 @@ get_main_action() {
fi
}
get_main_status_text
()
{
get_status_text
"
${
1
:-
text
}
"
| awk
'
NR <= 11 { print; next }
/^路由状态[[:space:]]*:/ || /^非当前模式路由[[:space:]]*:/ { print }
'
}
run_main_action
()
{
if
!
is_tailscale_installed
;
then
install_tailscale
elif
!
get_route_mode
>
/dev/null 2>&1
;
then
configure_route_mode
elif
[[
"
$(
get_route_mode
)
"
==
"4via6"
]]
&&
!
get_site_id
>
/dev/null 2>&1
;
then
configure_site_id
elif
all_routes_enabled
;
then
disable_routes
else
...
...
@@ -911,11 +1480,39 @@ show_status() {
elif
[[
-t
1
]]
&&
[[
-z
"
${
NO_COLOR
:-}
"
]]
&&
[[
"
${
TERM
:-}
"
!=
"dumb"
]]
;
then
mode
=
"ansi"
fi
message_box
"Tailscale 路由状态"
"
$(
get_status_text
"
$mode
"
)
"
output_box
"Tailscale 路由状态"
"
$(
get_status_text
"
$mode
"
)
"
}
run_netcheck
()
{
if
!
is_tailscale_installed
;
then
printf
'Tailscale 尚未安装,无法执行网络检测。\n'
>
&2
return
1
fi
tailscale netcheck
}
show_netcheck
()
{
local
output
local
result
=
0
output
=
"
$(
run_netcheck 2>&1
)
"
||
result
=
$?
[[
-z
"
$output
"
]]
&&
output
=
"网络检测没有返回内容。"
if
[[
"
$result
"
-ne
0
]]
;
then
output
=
"
$output
网络检测失败(退出码:
$result
)。"
fi
output_box
"Tailscale 网络检测"
"
$output
"
}
run_tui_branch
()
{
local
progress_message
=
"
$1
"
shift
# TUI 分支中的取消或命令失败必须返回主页面,不能被全局 set -e 终止。
show_tui_progress
"Tailscale 路由管理器"
"
$progress_message
"
set
+e
"
$@
"
set
-e
...
...
@@ -931,20 +1528,30 @@ run_menu() {
if
command_exists dialog
;
then
if
!
choice
=
"
$(
dialog
--colors
--stdout
--no-cancel
--title
"Tailscale 路由管理器"
\
--menu
"
$(
get_
status_text dialog
)
\n\n
请选择操作:"
31 78 7
\
--menu
"
$(
get_
main_status_text dialog
)
\n\n
请选择操作:"
29 84 12
\
"toggle"
"
$action
"
\
"route-mode"
"选择路由模式"
\
"site-id"
"配置 4via6 Site ID"
\
"login"
"登录 Tailscale"
\
"logout"
"退出登录"
\
"register"
"OAuth 注册新设备"
\
"status"
"刷新/查看状态"
\
"netcheck"
"网络检测"
\
"install"
"安装/检查 Tailscale"
\
"exit"
"退出"
)
"
;
then
continue
fi
elif
command_exists whiptail
;
then
if
!
choice
=
"
$(
whiptail
--nocancel
--title
"Tailscale 路由管理器"
\
--menu
"
$(
get_
status_text whiptail
)
\n\n
请选择操作:"
31 78 7
\
--menu
"
$(
get_
main_status_text whiptail
)
\n\n
请选择操作:"
29 84 12
\
"toggle"
"
$action
"
\
"route-mode"
"选择路由模式"
\
"site-id"
"配置 4via6 Site ID"
\
"login"
"登录 Tailscale"
\
"logout"
"退出登录"
\
"register"
"OAuth 注册新设备"
\
"status"
"刷新/查看状态"
\
"netcheck"
"网络检测"
\
"install"
"安装/检查 Tailscale"
\
"exit"
"退出"
3>&1 1>&2 2>&3
)
"
;
then
continue
...
...
@@ -953,31 +1560,51 @@ run_menu() {
clear
get_status_text
printf
'\n============================================================\n'
printf
'1) %s\n2)
OAuth 注册新设备\n3) 刷新/查看状态\n4) 安装/检查 Tailscale\n5
) 退出\n'
"
$action
"
printf
'1) %s\n2)
选择路由模式\n3) 配置 4via6 Site ID\n4) 登录 Tailscale\n5) 退出登录\n6) OAuth 注册新设备\n7) 刷新/查看状态\n8) 网络检测\n9) 安装/检查 Tailscale\n10
) 退出\n'
"
$action
"
printf
'============================================================\n'
read
-r
-p
"请选择 [1-
5
]: "
choice
read
-r
-p
"请选择 [1-
10
]: "
choice
case
"
$choice
"
in
1
)
choice
=
"toggle"
;;
2
)
choice
=
"register"
;;
3
)
choice
=
"status"
;;
4
)
choice
=
"install"
;;
5
)
choice
=
"exit"
;;
2
)
choice
=
"route-mode"
;;
3
)
choice
=
"site-id"
;;
4
)
choice
=
"login"
;;
5
)
choice
=
"logout"
;;
6
)
choice
=
"register"
;;
7
)
choice
=
"status"
;;
8
)
choice
=
"netcheck"
;;
9
)
choice
=
"install"
;;
10
)
choice
=
"exit"
;;
*
)
continue
;;
esac
fi
case
"
$choice
"
in
toggle
)
run_tui_branch run_main_action
run_tui_branch
"正在处理子网路由,请稍候……"
run_main_action
;;
route-mode
)
run_tui_branch
"正在打开路由模式设置……"
configure_route_mode
;;
site-id
)
run_tui_branch
"正在打开 Site ID 设置……"
configure_site_id
;;
login
)
run_tui_branch
"正在检查 Tailscale 登录状态……"
login_from_menu
;;
logout
)
run_tui_branch
"正在检查 Tailscale 登录状态……"
confirm_logout_tailscale
;;
register
)
run_tui_branch oauth_register_device
run_tui_branch
"正在准备 OAuth 自动注册……"
oauth_register_device
;;
status
)
run_tui_branch show_status
run_tui_branch
"正在读取 Tailscale 状态……"
show_status
;;
netcheck
)
run_tui_branch
"正在执行网络检测,请稍候……"
show_netcheck
;;
install
)
run_tui_branch install_tailscale
run_tui_branch
"正在检查 Tailscale 安装状态……"
install_tailscale
;;
exit
)
break
;;
*
)
continue
;;
...
...
@@ -993,10 +1620,15 @@ show_help() {
命令:
menu 打开交互菜单(默认)
install 安装或检查 Tailscale
route-mode 配置路由模式(4via6 或 legacy)
site-id 配置本设备唯一的 4via6 Site ID(1-65535)
login 登录 Tailscale
logout 退出 Tailscale,并使当前登录凭据失效
register 通过 OAuth 选择设备名称和标签并自动注册
enable 登录并发布本脚本管理的子网路由
disable 停止发布本脚本管理的子网路由并恢复 forwarding
status 输出状态,不修改系统
netcheck 执行 Tailscale 网络检测并输出完整结果
help 显示帮助
EOF
}
...
...
@@ -1008,14 +1640,27 @@ main() {
status
)
get_status_text
;;
netcheck
)
run_netcheck
;;
help
|
-h
|
--help
)
show_help
;;
menu|install|register|enable|disable
)
site-id
)
require_root
configure_site_id
"
${
2
:-}
"
;;
route-mode
)
require_root
configure_route_mode
"
${
2
:-}
"
;;
menu|install|login|logout|register|enable|disable
)
require_root
case
"
$action
"
in
menu
)
run_menu
;;
install
)
install_tailscale
;;
login
)
login_tailscale
;;
logout
)
logout_tailscale
;;
register
)
oauth_register_device
;;
enable
)
enable_routes
;;
disable
)
disable_routes
;;
...
...
tests/tailscale/test-route-manager.sh
View file @
85b2d526
...
...
@@ -3,6 +3,8 @@
set
-euo
pipefail
repo_root
=
"
$(
cd
--
"
$(
dirname
--
"
${
BASH_SOURCE
[0]
}
"
)
/../.."
&&
pwd
)
"
test_tmp_dir
=
"
$(
mktemp
-d
)
"
export
TS_SITE_CONFIG_FILE
=
"
$test_tmp_dir
/site-config"
# shellcheck source=../../scripts/tailscale/tailscale-route-manager.sh
source
"
$repo_root
/scripts/tailscale/tailscale-route-manager.sh"
...
...
@@ -20,7 +22,7 @@ if valid_device_name "qarm_001"; then
fi
hw_file
=
"
$(
mktemp
)
"
trap
'
rm -f -- "$hw_file"
'
EXIT
trap
'
cleanup; rm -f -- "$hw_file" "$TS_SITE_CONFIG_FILE"; rmdir "$test_tmp_dir" 2>/dev/null || true
'
EXIT
printf
'%s\n'
'{"sn":"CA2EUP-20261020-001"}'
>
"
$hw_file
"
QARM_HW_CONFIG_FILE
=
"
$hw_file
"
export
QARM_HW_CONFIG_FILE
...
...
@@ -40,18 +42,284 @@ helper_path="$(find_oauth_helper)"
[[
"
$helper_path
"
==
"
$repo_root
/scripts/tailscale/tailscale-oauth-helper.py"
]]
\
||
fail
"OAuth helper was not found beside the manager"
TAILSCALE_SET_ARGUMENT
=
""
MOCK_ADVERTISED_ROUTES
=
'"192.168.11.0/24","fd7a:115c:a1e0:b1a:0:65:c0a8:bc9/128","fd7a:115c:a1e0:b1a:0:65:c0a8:16ca/128","10.20.30.0/24"'
tailscale
()
{
if
[[
"
${
1
:-}
"
==
"set"
]]
;
then
TAILSCALE_SET_ARGUMENT
=
"
${
2
:-}
"
return
0
fi
if
[[
"
${
1
:-}
${
2
:-}
"
==
"debug via"
]]
;
then
case
"
${
3
:-}
${
4
:-}
"
in
"101 192.168.11.201/32"
)
printf
'%s\n'
'fd7a:115c:a1e0:b1a:0:65:c0a8:bc9/128'
;;
"101 192.168.22.202/32"
)
printf
'%s\n'
'fd7a:115c:a1e0:b1a:0:65:c0a8:16ca/128'
;;
*
)
return
1
;;
esac
return
0
fi
case
"
${
1
:-}
${
2
:-}
"
in
"status --json"
)
cat
<<
'
JSON
'
{
"BackendState": "Running",
"Self": {
"HostName": "CA2EUP-20261020-001-coffee"
},
"Peer": {
"peer-id": {
"HostName": "other-device"
}
}
}
JSON
;;
"netcheck "
)
printf
'%s\n'
'Report:'
'* UDP: true'
;;
"debug prefs"
)
printf
'{"AdvertiseRoutes":[%s]}\n'
"
$MOCK_ADVERTISED_ROUTES
"
;;
*
)
return
1
;;
esac
}
is_tailscale_installed
()
{
return
0
;
}
valid_site_id 1
||
fail
"minimum site ID was rejected"
valid_site_id 65535
||
fail
"maximum site ID was rejected"
if
valid_site_id 0
||
valid_site_id 65536
||
valid_site_id invalid
;
then
fail
"invalid site ID was accepted"
fi
[[
"
$(
get_route_status
)
"
==
"未配置"
]]
||
fail
"missing site ID was not reported"
[[
"
$(
get_route_mode
)
"
==
"4via6"
]]
||
fail
"missing route config did not default to 4via6"
printf
'%s\nROUTE_MODE=invalid\n'
"
$SITE_CONFIG_MARKER
"
>
"
$TS_SITE_CONFIG_FILE
"
if
get_route_mode
>
/dev/null 2>&1
;
then
fail
"invalid route mode was accepted"
fi
printf
'%s\nSITE_ID=101\n'
"
$SITE_CONFIG_MARKER
"
>
"
$TS_SITE_CONFIG_FILE
"
[[
"
$(
get_route_mode
)
"
==
"4via6"
]]
||
fail
"0.4.0 route config did not default to 4via6"
save_site_id 101
||
fail
"site ID was not saved"
[[
"
$(
get_site_id
)
"
==
"101"
]]
||
fail
"saved site ID was not loaded"
[[
"
$(
get_route_mode
)
"
==
"4via6"
]]
||
fail
"saving site ID did not preserve the route mode"
managed_routes
=
"
$(
get_managed_routes
)
"
[[
"
$managed_routes
"
==
$'fd7a:115c:a1e0:b1a:0:65:c0a8:bc9/128
\n
fd7a:115c:a1e0:b1a:0:65:c0a8:16ca/128'
]]
\
||
fail
"4via6 routes were not generated from the configured site ID"
remaining_routes
=
"
$(
printf
'%s\n'
\
'192.168.11.0/24'
\
'fd7a:115c:a1e0:b1a:0:65:c0a8:bc9/128'
\
'10.20.30.0/24'
| routes_without_managed
)
"
[[
"
$remaining_routes
"
==
"10.20.30.0/24"
]]
\
||
fail
"managed and legacy routes were not removed safely"
merged_routes
=
"
$(
merge_with_managed_routes
$'192.168.22.0/24
\n
10.20.30.0/24'
)
"
[[
"
$merged_routes
"
==
*
"10.20.30.0/24"
*
]]
\
&&
[[
"
$merged_routes
"
==
*
"fd7a:115c:a1e0:b1a:0:65:c0a8:bc9/128"
*
]]
\
&&
[[
"
$merged_routes
"
!=
*
"192.168.22.0/24"
*
]]
\
||
fail
"route merge did not preserve unrelated routes or migrate legacy routes"
[[
"
$(
get_tailscale_network_name
)
"
==
"CA2EUP-20261020-001-coffee"
]]
\
||
fail
"Tailscale network name was not read from Self.HostName"
[[
"
$(
get_status_text text
)
"
==
*
"网络名称 : CA2EUP-20261020-001-coffee"
*
]]
\
||
fail
"status text did not show the Tailscale network name"
[[
"
$(
run_netcheck
)
"
==
$'Report:
\n
* UDP: true'
]]
\
||
fail
"tailscale netcheck output was not preserved"
# TUI branches must cover the terminal immediately after the main menu closes.
TUI_EVENT_FILE
=
"
$test_tmp_dir
/tui-events"
dialog
()
{
if
[[
"
$*
"
==
*
" --infobox "
*
]]
;
then
printf
'progress:%s\n'
"
$*
"
>>
"
$TUI_EVENT_FILE
"
elif
[[
"
$*
"
==
*
" --msgbox "
*
]]
;
then
printf
'message:%s\n'
"
$*
"
>>
"
$TUI_EVENT_FILE
"
fi
return
0
}
command_exists
()
{
[[
"
$1
"
==
"dialog"
]]
}
tui_probe_action
()
{
printf
'action\n'
>>
"
$TUI_EVENT_FILE
"
}
CLEAR_CALLS
=
0
clear
()
{
CLEAR_CALLS
=
$((
CLEAR_CALLS
+
1
))
;
}
run_tui_branch
"正在执行网络检测,请稍候……"
tui_probe_action
[[
"
$(
sed
-n
'1p'
"
$TUI_EVENT_FILE
"
)
"
==
progress:
*
"正在执行网络检测,请稍候……"
*
]]
\
||
fail
"TUI branch did not show a progress box before running the action"
[[
"
$(
sed
-n
'2p'
"
$TUI_EVENT_FILE
"
)
"
==
"action"
]]
\
||
fail
"TUI action did not run after the progress box"
message_box
"测试"
"完成"
[[
"
$CLEAR_CALLS
"
-eq
0
]]
||
fail
"dialog message box exposed the terminal by clearing the screen"
:
>
"
$TUI_EVENT_FILE
"
whiptail
()
{
if
[[
"
$*
"
==
*
" --infobox "
*
]]
;
then
printf
'progress:%s\n'
"
$*
"
>>
"
$TUI_EVENT_FILE
"
fi
return
0
}
command_exists
()
{
[[
"
$1
"
==
"whiptail"
]]
}
run_tui_branch
"正在读取 Tailscale 状态……"
tui_probe_action
[[
"
$(
sed
-n
'1p'
"
$TUI_EVENT_FILE
"
)
"
==
progress:
*
"正在读取 Tailscale 状态……"
*
]]
\
||
fail
"whiptail branch did not show a progress box before running the action"
[[
"
$(
sed
-n
'2p'
"
$TUI_EVENT_FILE
"
)
"
==
"action"
]]
\
||
fail
"whiptail action did not run after the progress box"
# Force the ordinary terminal adapter so this test does not depend on local TUI packages.
command_exists
()
{
return
1
}
mode
=
"
$(
printf
'1\n'
| select_route_mode
)
"
[[
"
$mode
"
==
"4via6"
]]
||
fail
"recommended 4via6 route mode was not selected"
mode
=
"
$(
printf
'2\n'
| select_route_mode
)
"
[[
"
$mode
"
==
"legacy"
]]
||
fail
"legacy route mode was not selected"
if
printf
'0\n'
| select_route_mode
>
/dev/null
;
then
fail
"route mode cancellation was reported as successful"
else
result
=
$?
[[
"
$result
"
-eq
"
$RC_CANCELLED
"
]]
||
fail
"route mode cancellation returned an unexpected status"
fi
FORWARDING_CALLS
=
0
message_box
()
{
return
0
;
}
enable_forwarding
()
{
FORWARDING_CALLS
=
$((
FORWARDING_CALLS
+
1
))
;
}
restore_forwarding
()
{
return
0
;
}
if
configure_route_mode legacy
;
then
fail
"route mode changed while current 4via6 routes were still advertised"
fi
[[
"
$(
get_route_mode
)
"
==
"4via6"
]]
||
fail
"rejected route mode switch changed the saved mode"
enable_routes
||
fail
"mocked 4via6 route enable failed"
[[
"
$FORWARDING_CALLS
"
-eq
1
]]
||
fail
"route enable did not request forwarding"
[[
"
$TAILSCALE_SET_ARGUMENT
"
==
*
"10.20.30.0/24"
*
]]
\
&&
[[
"
$TAILSCALE_SET_ARGUMENT
"
==
*
"fd7a:115c:a1e0:b1a:0:65:c0a8:bc9/128"
*
]]
\
&&
[[
"
$TAILSCALE_SET_ARGUMENT
"
!=
*
"192.168.11.0/24"
*
]]
\
||
fail
"route enable did not migrate and preserve advertised routes"
TAILSCALE_SET_ARGUMENT
=
""
disable_routes
||
fail
"mocked 4via6 route disable failed"
[[
"
$TAILSCALE_SET_ARGUMENT
"
==
"--advertise-routes=10.20.30.0/24"
]]
\
||
fail
"route disable did not preserve only the unrelated route"
MOCK_ADVERTISED_ROUTES
=
'"10.20.30.0/24"'
configure_route_mode legacy
||
fail
"route mode did not switch after managed routes were disabled"
[[
"
$(
get_route_mode
)
"
==
"legacy"
]]
||
fail
"legacy route mode was not saved"
[[
"
$(
get_managed_routes
)
"
==
$'192.168.11.0/24
\n
192.168.22.0/24'
]]
\
||
fail
"legacy route mode did not manage the expected IPv4 routes"
TAILSCALE_SET_ARGUMENT
=
""
enable_routes
||
fail
"mocked legacy route enable failed"
[[
"
$FORWARDING_CALLS
"
-eq
2
]]
||
fail
"legacy route enable did not request forwarding"
[[
"
$TAILSCALE_SET_ARGUMENT
"
==
*
"192.168.11.0/24"
*
]]
\
&&
[[
"
$TAILSCALE_SET_ARGUMENT
"
==
*
"192.168.22.0/24"
*
]]
\
&&
[[
"
$TAILSCALE_SET_ARGUMENT
"
==
*
"10.20.30.0/24"
*
]]
\
&&
[[
"
$TAILSCALE_SET_ARGUMENT
"
!=
*
"fd7a:115c:a1e0:b1a:0:65"
*
]]
\
||
fail
"legacy route enable did not replace 4via6 routes and preserve unrelated routes"
MOCK_ADVERTISED_ROUTES
=
'"192.168.11.0/24","192.168.22.0/24","10.20.30.0/24"'
TAILSCALE_SET_ARGUMENT
=
""
disable_routes
||
fail
"mocked legacy route disable failed"
[[
"
$TAILSCALE_SET_ARGUMENT
"
==
"--advertise-routes=10.20.30.0/24"
]]
\
||
fail
"legacy route disable did not preserve only the unrelated route"
write_route_config
""
legacy
||
fail
"legacy route mode without a site ID was not saved"
if
get_site_id
>
/dev/null 2>&1
;
then
fail
"site ID unexpectedly remained in a legacy-only config"
fi
MOCK_ADVERTISED_ROUTES
=
'"10.20.30.0/24"'
TAILSCALE_SET_ARGUMENT
=
""
enable_routes
||
fail
"legacy route enable incorrectly required a site ID"
[[
"
$TAILSCALE_SET_ARGUMENT
"
==
*
"192.168.11.0/24"
*
]]
\
&&
[[
"
$TAILSCALE_SET_ARGUMENT
"
==
*
"192.168.22.0/24"
*
]]
\
||
fail
"legacy route enable without a site ID did not publish both IPv4 routes"
[[
"
$(
get_status_text text
)
"
==
*
"路由模式 : 仅旧 IPv4"
*
]]
\
||
fail
"status text did not show the legacy route mode"
MOCK_ADVERTISED_ROUTES
=
'"192.168.11.0/24","192.168.22.0/24","10.20.30.0/24"'
disable_routes
||
fail
"legacy route disable without a site ID failed"
selected
=
"
$(
printf
'1,2\n'
| select_tags
$'tag:germany
\n
tag:qarm-router'
)
"
[[
"
$selected
"
==
$'tag:germany
\n
tag:qarm-router'
]]
\
||
fail
"terminal checklist did not return selected tags"
selected
=
"
$(
printf
'\n'
| select_tags
$'tag:qarm
\n
tag:europe
\n
tag:sea'
)
"
[[
"
$selected
"
==
"tag:qarm"
]]
\
||
fail
"default qarm tag was not selected"
selected
=
"
$(
printf
'1,2\n'
| select_tags
$'tag:qarm
\n
tag:europe
\n
tag:sea'
)
"
[[
"
$selected
"
==
$'tag:qarm
\n
tag:europe
\n
tag:sea'
]]
\
||
fail
"optional tags were not added to the default qarm tag"
if
printf
'0\n'
| select_tags
$'tag:qarm
\n
tag:europe'
>
/dev/null
;
then
fail
"terminal default-tag selection did not support cancellation"
else
result
=
$?
[[
"
$result
"
-eq
"
$RC_CANCELLED
"
]]
||
fail
"terminal cancellation returned an unexpected status"
fi
default_value
=
"
$(
printf
'\n'
| input_value
"测试"
"设备名称"
"qarm-default"
)
"
[[
"
$default_value
"
==
"qarm-default"
]]
||
fail
"terminal input default was not preserved"
method
=
"
$(
printf
'1\n'
| select_login_method
)
"
[[
"
$method
"
==
"manual"
]]
||
fail
"manual login method was not selected"
method
=
"
$(
printf
'2\n'
| select_login_method
)
"
[[
"
$method
"
==
"oauth"
]]
||
fail
"OAuth login method was not selected"
if
printf
'0\n'
| select_login_method
>
/dev/null
;
then
fail
"login method cancellation was reported as successful"
else
result
=
$?
[[
"
$result
"
-eq
"
$RC_CANCELLED
"
]]
||
fail
"login method cancellation returned an unexpected status"
fi
# Exercise login/logout state transitions without contacting Tailscale.
LOGIN_STATE
=
0
LOGIN_CALLS
=
0
LOGOUT_CALLS
=
0
is_tailscale_installed
()
{
return
0
;
}
is_tailscale_logged_in
()
{
[[
"
$LOGIN_STATE
"
-eq
1
]]
;
}
message_box
()
{
return
0
;
}
tailscale
()
{
case
"
$1
"
in
login
)
LOGIN_CALLS
=
$((
LOGIN_CALLS
+
1
))
LOGIN_STATE
=
1
;;
logout
)
LOGOUT_CALLS
=
$((
LOGOUT_CALLS
+
1
))
LOGIN_STATE
=
0
;;
status
)
printf
'%s\n'
'{"Self":{"HostName":"qarm-login-test"}}'
;;
*
)
return
1
;;
esac
}
login_tailscale
||
fail
"mocked Tailscale login failed"
[[
"
$LOGIN_STATE
"
-eq
1
&&
"
$LOGIN_CALLS
"
-eq
1
]]
\
||
fail
"login did not transition to the connected state"
login_tailscale
||
fail
"already-connected login should be successful"
[[
"
$LOGIN_CALLS
"
-eq
1
]]
||
fail
"already-connected device logged in again"
yes_no
()
{
return
1
;
}
if
confirm_logout_tailscale
;
then
fail
"cancelled logout was reported as successful"
else
result
=
$?
[[
"
$result
"
-eq
"
$RC_CANCELLED
"
]]
||
fail
"cancelled logout returned an unexpected status"
fi
[[
"
$LOGIN_STATE
"
-eq
1
&&
"
$LOGOUT_CALLS
"
-eq
0
]]
\
||
fail
"cancelled logout changed the login state"
logout_tailscale
||
fail
"mocked Tailscale logout failed"
[[
"
$LOGIN_STATE
"
-eq
0
&&
"
$LOGOUT_CALLS
"
-eq
1
]]
\
||
fail
"logout did not transition to the disconnected state"
logout_tailscale
||
fail
"already-logged-out operation should be successful"
[[
"
$LOGOUT_CALLS
"
-eq
1
]]
||
fail
"already-logged-out device logged out again"
# Exercise the registration orchestration without contacting Tailscale or changing the host.
export
TS_OAUTH_CLIENT_ID
=
"test-client"
export
TS_OAUTH_CLIENT_SECRET
=
"test-secret"
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment