mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 22:42:51 +08:00
🍓 Sync 2026-09-29 07:32:36
This commit is contained in:
1 parent
08d3ec6fdd
commit
085188d2d1
13 files changed
+488
-192
No files matched your search
@@ -2,6 +2,42 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [2.4.0-r1] - sing-box / V2Ray 输出完整配置
|
||||
|
||||
- ⚠️ **破坏性变更**:`target=singbox` 与 `target=v2ray` 由「仅含 `outbounds` 的片段」
|
||||
改为「`outbounds` + 分流的完整可用配置」。2.3.x 的片段需粘进已有配置使用;
|
||||
2.4.0 起可直接作为单文件配置启动。已粘进客户端的老链接会拿到不同结构,请重新导出。
|
||||
- sing-box 完整配置(`output_singbox.lua`)
|
||||
- 节点出站 + `selector`(tag `select`,供手工切换)+ `urltest`(tag `auto`,自动测速)
|
||||
+ `direct` / `block`
|
||||
- `route.final` 指向 `select`;内置 `ip_is_private` → `direct` 私网直连规则;
|
||||
`auto_detect_interface` 开启
|
||||
- **刻意省略 route 规则的 `action` 字段**:该字段自 sing-box 1.11.0 起才存在,其默认值
|
||||
即 `"route"`。sing-box 会拒绝未知字段,而省略默认值在 1.10 与 1.11+ 上都能工作
|
||||
- 无可用节点时不生成 `selector` / `urltest`(其 `outbounds` 不允许为空),`final` 退回 `direct`
|
||||
- V2Ray / Xray 完整配置(`output_v2ray.lua`)
|
||||
- 节点出站 + `freedom`(tag `direct`) / `blackhole`(tag `block`) + `log`
|
||||
- `observatory`(`subjectSelector` / `probeUrl` / `probeInterval`)+ `routing.balancers`
|
||||
(tag `auto`,`leastPing`)——`leastPing` 必须依赖 observatory 的探测结果才会生效
|
||||
- `routing.rules`:`geoip:private` → `direct`,其后 `tcp,udp` 兜底 → `balancerTag: auto`;
|
||||
`domainStrategy` 为 `IPIfNonMatch`
|
||||
- 无可用节点时改为 `outboundTag: direct` 兜底,且不生成 `balancers` / `observatory`
|
||||
- 两者均**不含 `inbounds` / `dns`**:会绑定本地监听端口、覆盖用户既有 DNS 设置,交由用户维护
|
||||
- 修复 `util.json_encode` 无法表达空对象:Lua 空表经 `is_array` 判定会被编码成 `[]`,
|
||||
而 sing-box 的 `tls`、Xray 的 `settings` 必须是对象。新增 `util.JSON_EMPTY_OBJECT` 占位符
|
||||
- 由此修复一处既有缺陷:sing-box 节点 `security=tls` 但无 `sni` / `alpn` 时会产出非法的 `"tls":[]`
|
||||
- 新增 `util.unique_tags(nodes, reserved)`:sing-box 与 Xray 均要求 outbound tag 唯一,
|
||||
节点重名(订阅里很常见)或与保留 tag(`direct` / `block` / `select` / `auto`)同名时
|
||||
自动追加 ` #2`、` #3`
|
||||
- Xray 的 balancer / observatory `selector` 按**前缀**匹配 tag,因此额外把保留 tag 的
|
||||
所有真前缀也登记为冲突——否则名为 `d` 的节点会让 `direct` 出站被误纳入负载均衡
|
||||
- 导出结果可重新导入:完整配置里的 `selector` / `urltest` / `direct` / `block` /
|
||||
`freedom` / `blackhole` 会被解析器正确跳过,只取真实节点(已加往返测试)
|
||||
- 新增测试 `tests/output_full_config_test.lua`(82 项)
|
||||
- `tests/output_formats_test.lua` / `tests/ssr_test.lua` 更新为断言「不含 ssr 出站」
|
||||
而非「outbounds 为空」,适配完整配置语义
|
||||
- 版本号 2.3.0-r2 → 2.4.0-r1;README.md / README.en.md / docs/INSTALL.md 同步
|
||||
|
||||
## [2.3.0-r2] - 新增 Clash 原版与 WireGuard .conf 输出,输出层清理
|
||||
|
||||
- 新增输出格式 **WireGuard / AmneziaWG `.conf`**(`target=wgconf`,别名 `wg` / `wireguard` / `amneziawg` / `amnezia` / `conf`)
|
||||
|
||||
@@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_NAME:=luci-app-substore
|
||||
# 版本约定:每次提交 PKG_RELEASE +1(2.1.2-r1 ~ r9);
|
||||
# 达到 r10 时 PKG_VERSION 末位 +1(2.1.2 -> 2.1.3),PKG_RELEASE 重置为 1。
|
||||
PKG_VERSION:=2.3.0
|
||||
PKG_RELEASE:=6
|
||||
PKG_VERSION:=2.4.0
|
||||
PKG_RELEASE:=7
|
||||
|
||||
LUCI_DEPENDS:=+luci-lua-runtime +luci-compat
|
||||
|
||||
|
||||
@@ -67,6 +67,20 @@ group them, then re-emit them in a format your client can consume.
|
||||
original does not support (vless / hysteria2 / hysteria / tuic / wireguard) are filtered out
|
||||
- **WireGuard / AmneziaWG `.conf`**: wg-quick single-interface config with `[Interface]` /
|
||||
`[Peer]` sections and AmneziaWG obfuscation parameters, importable by AmneziaWG clients
|
||||
- **sing-box / V2Ray (Xray)**: emits a **complete, runnable config** (`outbounds` plus
|
||||
routing), not just an `outbounds` fragment
|
||||
- sing-box: node outbounds + `selector` (manual switch) + `urltest` (auto latency test) +
|
||||
`direct` / `block`; `route.final` points at the selector, with a built-in private-IP
|
||||
direct rule
|
||||
- V2Ray/Xray: node outbounds + `freedom` (direct) / `blackhole` (block) + `observatory` +
|
||||
`routing.balancers` (`leastPing` auto-selection), with built-in `geoip:private` direct
|
||||
and a catch-all route
|
||||
- Deliberately **excludes `inbounds` / `dns`**: those bind local listening ports and
|
||||
override your existing DNS settings — keep them in your own config and merge this
|
||||
output into it
|
||||
- ⚠️ Not compatible with 2.3.x: 2.3.x emitted an `outbounds`-only fragment meant to be
|
||||
pasted into an existing config; from 2.4.0 it is a complete config you can start
|
||||
directly as a single file
|
||||
|
||||
**Subscription links**
|
||||
- Per-subscription random token → public download endpoint
|
||||
@@ -79,18 +93,18 @@ group them, then re-emit them in a format your client can consume.
|
||||
## Installation
|
||||
|
||||
> The version in the package name must match `PKG_VERSION` / `PKG_RELEASE` in the
|
||||
> [Makefile](Makefile) (currently `2.3.0-r2`).
|
||||
> [Makefile](Makefile) (currently `2.4.0-r1`).
|
||||
|
||||
opkg (OpenWrt / ImmortalWrt 24.10 and earlier):
|
||||
|
||||
```bash
|
||||
opkg install luci-app-substore-2.3.0-r2.ipk
|
||||
opkg install luci-app-substore-2.4.0-r1.ipk
|
||||
```
|
||||
|
||||
apk (OpenWrt / ImmortalWrt 25.12+):
|
||||
|
||||
```bash
|
||||
apk add --allow-untrusted luci-app-substore-2.3.0-r2.apk
|
||||
apk add --allow-untrusted luci-app-substore-2.4.0-r1.apk
|
||||
```
|
||||
|
||||
Then open LuCI: **Services → Subscriptions**.
|
||||
|
||||
@@ -59,6 +59,16 @@
|
||||
协议(vless / hysteria2 / hysteria / tuic / wireguard)
|
||||
- **WireGuard / AmneziaWG `.conf`**:wg-quick 单接口配置,含 `[Interface]` / `[Peer]` 与
|
||||
AmneziaWG 混淆参数,可直接导入 AmneziaWG 客户端
|
||||
- **sing-box / V2Ray(Xray)**:输出**完整可用配置**(`outbounds` + 分流),而非仅有
|
||||
`outbounds` 的片段
|
||||
- sing-box:节点出站 + `selector`(手工切换)+ `urltest`(自动测速)+ `direct` / `block`,
|
||||
`route.final` 指向 `selector`,内置私网直连规则
|
||||
- V2Ray/Xray:节点出站 + `freedom`(direct) / `blackhole`(block) + `observatory` +
|
||||
`routing.balancers`(`leastPing` 自动选优),内置 `geoip:private` 直连与兜底分流
|
||||
- 刻意**不含 `inbounds` / `dns`**:这两项会绑定本地监听端口、覆盖你既有的 DNS 设置,
|
||||
请在你自己的配置里维护;把本输出合并进已有配置即可
|
||||
- ⚠️ 与 2.3.x 不兼容:2.3.x 输出的是仅含 `outbounds` 的片段,需要粘进已有配置使用;
|
||||
2.4.0 起是完整配置,可直接作为单文件配置启动
|
||||
|
||||
**订阅链接**
|
||||
- 每个订阅独立随机 token → 公开下载端点
|
||||
@@ -70,18 +80,18 @@
|
||||
## 安装
|
||||
|
||||
> 包名中的版本号必须与 [Makefile](Makefile) 的 `PKG_VERSION` / `PKG_RELEASE` 保持一致
|
||||
> (当前 `2.3.0-r2`)。
|
||||
> (当前 `2.4.0-r1`)。
|
||||
|
||||
opkg(OpenWrt / ImmortalWrt 24.10 及更早):
|
||||
|
||||
```bash
|
||||
opkg install luci-app-substore-2.3.0-r2.ipk
|
||||
opkg install luci-app-substore-2.4.0-r1.ipk
|
||||
```
|
||||
|
||||
apk(OpenWrt / ImmortalWrt 25.12+):
|
||||
|
||||
```bash
|
||||
apk add --allow-untrusted luci-app-substore-2.3.0-r2.apk
|
||||
apk add --allow-untrusted luci-app-substore-2.4.0-r1.apk
|
||||
```
|
||||
|
||||
然后在 LuCI 菜单打开:**服务 → 订阅**。
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
> NOTE: stage-0 skeleton has not yet been installed/verified on a target device.
|
||||
> This document will be updated once installation is validated.
|
||||
>
|
||||
> 包名中的版本号需与 Makefile 的 `PKG_VERSION` / `PKG_RELEASE` 保持同步(当前 2.3.0-r2)。
|
||||
> 包名中的版本号需与 Makefile 的 `PKG_VERSION` / `PKG_RELEASE` 保持同步(当前 2.4.0-r1)。
|
||||
|
||||
## Install (opkg — OpenWrt / ImmortalWrt 24.10 及更早)
|
||||
|
||||
```bash
|
||||
opkg install luci-app-substore-2.3.0-r2.ipk
|
||||
opkg install luci-app-substore-2.4.0-r1.ipk
|
||||
```
|
||||
|
||||
## Install (apk — OpenWrt / ImmortalWrt 25.12+)
|
||||
|
||||
```bash
|
||||
apk add --allow-untrusted luci-app-substore-2.3.0-r2.apk
|
||||
apk add --allow-untrusted luci-app-substore-2.4.0-r1.apk
|
||||
```
|
||||
|
||||
Refresh LuCI:
|
||||
|
||||
@@ -27,6 +27,11 @@ local TYPE_MAP = {
|
||||
http = "http",
|
||||
}
|
||||
|
||||
-- 该协议能否落到 sing-box outbound(SSR 无法表达,跳过)
|
||||
local function supported(proto)
|
||||
return (TYPE_MAP[proto] or proto) ~= "ssr"
|
||||
end
|
||||
|
||||
-- 构建 TLS 字段
|
||||
local function build_tls(n)
|
||||
local tls
|
||||
@@ -47,6 +52,9 @@ local function build_tls(n)
|
||||
elseif n.skip_cert_verify ~= nil then
|
||||
tls.insecure = bool(n.skip_cert_verify)
|
||||
end
|
||||
-- 空表会被 json_encode 编码成 [],而 tls 必须是对象;
|
||||
-- security=tls 但无 sni/alpn/insecure 时就会走到这里
|
||||
if next(tls) == nil then return util.JSON_EMPTY_OBJECT end
|
||||
return tls
|
||||
end
|
||||
return nil
|
||||
@@ -79,12 +87,13 @@ local function build_transport(n)
|
||||
end
|
||||
|
||||
-- 单节点 → sing-box outbound 表
|
||||
function M.to_outbound(n)
|
||||
-- tag 可选:完整配置里由 util.unique_tags 统一分配,避免节点重名导致 tag 冲突
|
||||
function M.to_outbound(n, tag)
|
||||
local stype = TYPE_MAP[n.proto] or n.proto
|
||||
if stype == "ssr" then return nil end -- sing-box 不支持 SSR,跳过
|
||||
local o = {
|
||||
type = stype,
|
||||
tag = n.name or ((n.server or "") .. ":" .. tostring(n.port or "")),
|
||||
tag = tag or n.name or ((n.server or "") .. ":" .. tostring(n.port or "")),
|
||||
server = n.server or "",
|
||||
server_port = tonumber(n.port) or 0,
|
||||
}
|
||||
@@ -145,15 +154,53 @@ function M.to_outbound(n)
|
||||
return o
|
||||
end
|
||||
|
||||
function M.generate(nodes)
|
||||
local outbounds = {}
|
||||
-- 保留 tag:节点名不得与之重名(util.unique_tags 负责消解)
|
||||
local RESERVED_TAGS = { select = true, auto = true, direct = true, block = true }
|
||||
|
||||
-- 生成完整 sing-box 配置(outbounds + route)。
|
||||
-- 不含 inbounds / dns:这两项会绑定本地监听端口、覆盖用户既有 DNS 设置,
|
||||
-- 由用户在自己的配置里维护,本输出只负责节点与分流。
|
||||
--
|
||||
-- 版本兼容说明:route 规则里的 action 字段自 1.11.0 起才存在,其默认值即 "route",
|
||||
-- 因此这里刻意省略 action —— 未知字段会被 sing-box 拒绝,而省略默认值在
|
||||
-- 1.10 与 1.11+ 上都能工作。
|
||||
function M.generate(nodes, options)
|
||||
local usable = {}
|
||||
for _, n in ipairs(nodes or {}) do
|
||||
if type(n) == "table" then
|
||||
local o = M.to_outbound(n)
|
||||
if o then outbounds[#outbounds + 1] = o end
|
||||
end
|
||||
if type(n) == "table" and supported(n.proto) then usable[#usable + 1] = n end
|
||||
end
|
||||
return util.json_encode({ outbounds = outbounds })
|
||||
local tags = util.unique_tags(usable, RESERVED_TAGS)
|
||||
|
||||
local outbounds, proxy_tags = {}, {}
|
||||
for i, n in ipairs(usable) do
|
||||
outbounds[#outbounds + 1] = M.to_outbound(n, tags[i])
|
||||
proxy_tags[#proxy_tags + 1] = tags[i]
|
||||
end
|
||||
|
||||
local final
|
||||
if #proxy_tags > 0 then
|
||||
-- select 供手工切换,auto 自动测速选优
|
||||
local sel = { type = "selector", tag = "select", outbounds = { "auto", "direct" } }
|
||||
for _, t in ipairs(proxy_tags) do sel.outbounds[#sel.outbounds + 1] = t end
|
||||
sel.default = "auto"
|
||||
outbounds[#outbounds + 1] = sel
|
||||
outbounds[#outbounds + 1] = { type = "urltest", tag = "auto", outbounds = proxy_tags }
|
||||
final = "select"
|
||||
else
|
||||
-- 无可用节点时不能生成 selector / urltest(outbounds 不允许为空)
|
||||
final = "direct"
|
||||
end
|
||||
outbounds[#outbounds + 1] = { type = "direct", tag = "direct" }
|
||||
outbounds[#outbounds + 1] = { type = "block", tag = "block" }
|
||||
|
||||
return util.json_encode({
|
||||
outbounds = outbounds,
|
||||
route = {
|
||||
rules = { { ip_is_private = true, outbound = "direct" } },
|
||||
final = final,
|
||||
auto_detect_interface = true,
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -47,13 +47,19 @@ local function build_stream_settings(n)
|
||||
return ss
|
||||
end
|
||||
|
||||
-- 该协议能否落到 V2Ray / Xray outbound(SSR 无法表达,跳过)
|
||||
local function supported(proto)
|
||||
return (proto or "vmess") ~= "ssr"
|
||||
end
|
||||
|
||||
-- 单节点 → V2Ray outbound 表
|
||||
function M.to_outbound(n)
|
||||
-- tag 可选:完整配置里由 util.unique_tags 统一分配,避免节点重名导致 tag 冲突
|
||||
function M.to_outbound(n, tag)
|
||||
local proto = n.proto or "vmess"
|
||||
if proto == "ssr" then return nil end -- V2Ray / Xray 不支持 SSR,跳过
|
||||
if not supported(proto) then return nil end
|
||||
local o = {
|
||||
protocol = (proto == "ss" and "shadowsocks" or proto),
|
||||
tag = n.name or ((n.server or "") .. ":" .. tostring(n.port or "")),
|
||||
tag = tag or n.name or ((n.server or "") .. ":" .. tostring(n.port or "")),
|
||||
settings = {},
|
||||
streamSettings = build_stream_settings(n),
|
||||
}
|
||||
@@ -94,15 +100,61 @@ function M.to_outbound(n)
|
||||
return o
|
||||
end
|
||||
|
||||
function M.generate(nodes)
|
||||
local outbounds = {}
|
||||
for _, n in ipairs(nodes or {}) do
|
||||
if type(n) == "table" then
|
||||
local o = M.to_outbound(n)
|
||||
if o then outbounds[#outbounds + 1] = o end
|
||||
end
|
||||
-- Xray 的 balancer / observatory selector 按「前缀」匹配 outbound tag。
|
||||
-- 若某节点 tag 恰好是 "direct" / "block" / "auto" 的前缀(例如节点名就叫 "d"、"auto"),
|
||||
-- selector 会把这些保留出站一并纳入负载均衡,导致流量被丢进 direct / block。
|
||||
-- 因此把保留 tag 的所有真前缀也登记为冲突,交给 util.unique_tags 加后缀消解。
|
||||
local function reserved_with_prefixes(names)
|
||||
local r = {}
|
||||
for _, s in ipairs(names) do
|
||||
r[s] = true
|
||||
for i = 1, #s - 1 do r[s:sub(1, i)] = true end
|
||||
end
|
||||
return util.json_encode({ outbounds = outbounds })
|
||||
return r
|
||||
end
|
||||
|
||||
-- 生成完整 V2Ray / Xray 配置(outbounds + observatory + routing)。
|
||||
-- 不含 inbounds / dns:这两项会绑定本地监听端口、覆盖用户既有 DNS 设置,
|
||||
-- 由用户在自己的配置里维护,本输出只负责节点与分流。
|
||||
function M.generate(nodes, options)
|
||||
local usable = {}
|
||||
for _, n in ipairs(nodes or {}) do
|
||||
if type(n) == "table" and supported(n.proto) then usable[#usable + 1] = n end
|
||||
end
|
||||
local tags = util.unique_tags(usable, reserved_with_prefixes({ "direct", "block", "auto" }))
|
||||
|
||||
local outbounds, proxy_tags = {}, {}
|
||||
for i, n in ipairs(usable) do
|
||||
outbounds[#outbounds + 1] = M.to_outbound(n, tags[i])
|
||||
proxy_tags[#proxy_tags + 1] = tags[i]
|
||||
end
|
||||
|
||||
-- direct / block 必须存在:路由规则要按 tag 引用它们
|
||||
outbounds[#outbounds + 1] = { protocol = "freedom", tag = "direct", settings = util.JSON_EMPTY_OBJECT }
|
||||
outbounds[#outbounds + 1] = { protocol = "blackhole", tag = "block", settings = { response = { type = "none" } } }
|
||||
|
||||
local routing = {
|
||||
domainStrategy = "IPIfNonMatch",
|
||||
rules = { { type = "field", ip = { "geoip:private" }, outboundTag = "direct" } },
|
||||
}
|
||||
local cfg = { log = { loglevel = "warning" }, outbounds = outbounds, routing = routing }
|
||||
|
||||
if #proxy_tags > 0 then
|
||||
-- leastPing 依赖 observatory 的探测结果;不配 observatory 时该策略不生效
|
||||
cfg.observatory = {
|
||||
subjectSelector = proxy_tags,
|
||||
probeUrl = "https://www.gstatic.com/generate_204",
|
||||
probeInterval = "10s",
|
||||
enableConcurrency = true,
|
||||
}
|
||||
routing.balancers = { { tag = "auto", selector = proxy_tags, strategy = { type = "leastPing" } } }
|
||||
routing.rules[#routing.rules + 1] = { type = "field", network = "tcp,udp", balancerTag = "auto" }
|
||||
else
|
||||
-- 无可用节点时没有 balancer 可引用;首元素 direct 即默认出站
|
||||
routing.rules[#routing.rules + 1] = { type = "field", network = "tcp,udp", outboundTag = "direct" }
|
||||
end
|
||||
|
||||
return util.json_encode(cfg)
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -147,6 +147,11 @@ end
|
||||
-- JSON null 占位符:仅在数组中用于保留位置(对象中的 null 仍解码为 nil)
|
||||
local JSON_NULL = {}
|
||||
|
||||
-- JSON 空对象占位符。Lua 的空表无法区分 {} 与 [](is_array 判定空表为数组),
|
||||
-- 而 sing-box / Xray 的部分字段必须是对象("tls": {}、"settings": {}),
|
||||
-- 编码成 [] 会被客户端拒绝。需要空对象时显式使用本常量。
|
||||
M.JSON_EMPTY_OBJECT = setmetatable({}, { __tostring = function() return "{}" end })
|
||||
|
||||
function M.json_encode(v)
|
||||
local function enc(v)
|
||||
local t = type(v)
|
||||
@@ -169,7 +174,9 @@ function M.json_encode(v)
|
||||
elseif t == "boolean" then
|
||||
return v and "true" or "false"
|
||||
elseif t == "table" then
|
||||
if is_array(v) then
|
||||
if v == M.JSON_EMPTY_OBJECT then
|
||||
return "{}"
|
||||
elseif is_array(v) then
|
||||
local a = {}
|
||||
for i = 1, #v do a[#a + 1] = enc(v[i]) end
|
||||
return "[" .. table.concat(a, ",") .. "]"
|
||||
@@ -378,4 +385,28 @@ function M.human_duration(secs)
|
||||
return "不足1分钟"
|
||||
end
|
||||
|
||||
-- 为一组节点生成唯一 tag(供 sing-box / Xray 完整配置使用)。
|
||||
-- 节点名可能重复,也可能与保留 tag(direct / block / select / auto …)同名;
|
||||
-- sing-box 与 Xray 都要求 outbound tag 唯一,重名时追加 " #2"、" #3"。
|
||||
-- 返回与 nodes 等长的 tag 数组。
|
||||
function M.unique_tags(nodes, reserved)
|
||||
local used = {}
|
||||
for k in pairs(reserved or {}) do used[k] = true end
|
||||
local tags = {}
|
||||
for i, n in ipairs(nodes or {}) do
|
||||
local base = n.name
|
||||
if base == nil or base == "" then
|
||||
base = (n.server or "") .. ":" .. tostring(n.port or "")
|
||||
end
|
||||
local tag, k = base, 2
|
||||
while used[tag] do
|
||||
tag = base .. " #" .. k
|
||||
k = k + 1
|
||||
end
|
||||
used[tag] = true
|
||||
tags[i] = tag
|
||||
end
|
||||
return tags
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -57,15 +57,18 @@ check("qx has [server_local]", qx:find("%[server_local%]") ~= nil)
|
||||
check("qx has vmess line", qx:find("vmess=1.1.1.1:443") ~= nil)
|
||||
check("qx has policy", qx:find("%[policy%]") ~= nil)
|
||||
|
||||
-- sing-box / v2ray 自 2.4.0 起输出完整配置:outbounds 除节点外还含
|
||||
-- selector / urltest(sing-box)与 direct / block,故此处不再断言节点数量,
|
||||
-- 改为断言节点出站按序在前、且结构与分流齐全(详见 output_full_config_test.lua)
|
||||
local singbox = output.generate(nodes, "singbox")
|
||||
check("singbox is json", singbox:find("{") == 1)
|
||||
local sb = util.json_decode(singbox)
|
||||
check("singbox has outbounds", sb and sb.outbounds ~= nil and #sb.outbounds == 4)
|
||||
check("singbox has outbounds", sb and sb.outbounds ~= nil and #sb.outbounds == 4 + 4)
|
||||
check("singbox type vmess", sb and sb.outbounds[1] and sb.outbounds[1].type == "vmess")
|
||||
|
||||
local v2ray = output.generate(nodes, "v2ray")
|
||||
local vr = util.json_decode(v2ray)
|
||||
check("v2ray has outbounds", vr and vr.outbounds ~= nil and #vr.outbounds == 4)
|
||||
check("v2ray has outbounds", vr and vr.outbounds ~= nil and #vr.outbounds == 4 + 2)
|
||||
check("v2ray protocol vmess", vr and vr.outbounds[1] and vr.outbounds[1].protocol == "vmess")
|
||||
|
||||
local uri = output.generate(nodes, "v2rayuri")
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
-- output_full_config_test.lua — sing-box / V2Ray(Xray) 完整配置输出
|
||||
-- 用法:lua5.1 tests/output_full_config_test.lua
|
||||
--
|
||||
-- 覆盖 2.4.0 起的行为:target=singbox / target=v2ray 由「仅 outbounds 片段」
|
||||
-- 改为「outbounds + 分流」的完整配置。
|
||||
|
||||
package.path = "./root/usr/share/?.lua;" .. package.path
|
||||
|
||||
local util = require("substore.util")
|
||||
local output = require("substore.output")
|
||||
local singbox = require("substore.output_singbox")
|
||||
local v2ray = require("substore.output_v2ray")
|
||||
|
||||
local passed, failed = 0, 0
|
||||
|
||||
local function check(name, cond)
|
||||
if cond then
|
||||
passed = passed + 1
|
||||
print("PASS " .. name)
|
||||
else
|
||||
failed = failed + 1
|
||||
print("FAIL " .. name)
|
||||
end
|
||||
end
|
||||
|
||||
local nodes = {
|
||||
{ proto = "vmess", name = "HK 01", server = "1.1.1.1", port = 443, uuid = "u1", net = "tcp" },
|
||||
{ proto = "vless", name = "HK 01", server = "2.2.2.2", port = 443, uuid = "u2" },
|
||||
{ proto = "trojan", name = "JP", server = "3.3.3.3", port = 443, password = "p" },
|
||||
{ proto = "ssr", name = "SSR", server = "4.4.4.4", port = 8388, method = "aes-128-cfb", password = "p" },
|
||||
}
|
||||
|
||||
-- ---------- util.JSON_EMPTY_OBJECT:空表必须能表达成对象 ----------
|
||||
-- Lua 空表无法区分 {} 与 [],is_array({}) 为 true,默认会编码成 [],
|
||||
-- 而 sing-box 的 tls、Xray 的 settings 都必须是对象
|
||||
check("empty table encodes as array (baseline)", util.json_encode({}) == "[]")
|
||||
check("JSON_EMPTY_OBJECT encodes as object", util.json_encode(util.JSON_EMPTY_OBJECT) == "{}")
|
||||
check("JSON_EMPTY_OBJECT nested", util.json_encode({ settings = util.JSON_EMPTY_OBJECT }) == '{"settings":{}}')
|
||||
|
||||
-- ---------- util.unique_tags ----------
|
||||
local ut = util.unique_tags(
|
||||
{ { name = "A" }, { name = "A" }, { name = "B" }, { name = "direct" } },
|
||||
{ direct = true }
|
||||
)
|
||||
check("unique_tags dedupes", ut[1] == "A" and ut[2] == "A #2" and ut[3] == "B")
|
||||
check("unique_tags avoids reserved", ut[4] == "direct #2")
|
||||
check("unique_tags falls back to server:port", util.unique_tags({ { server = "h", port = 1 } })[1] == "h:1")
|
||||
|
||||
-- ============================================================
|
||||
-- sing-box 完整配置
|
||||
-- ============================================================
|
||||
local sb_raw = singbox.generate(nodes)
|
||||
local sb = util.json_decode(sb_raw)
|
||||
|
||||
check("sb is valid json", sb ~= nil)
|
||||
check("sb has outbounds", type(sb.outbounds) == "table")
|
||||
check("sb has route", type(sb.route) == "table")
|
||||
|
||||
-- 不含 inbounds / dns(会绑定本地端口、覆盖用户 DNS)
|
||||
check("sb has no inbounds", sb.inbounds == nil)
|
||||
check("sb has no dns", sb.dns == nil)
|
||||
|
||||
-- 节点出站按序在前;SSR 被丢弃
|
||||
check("sb node1 is vmess", sb.outbounds[1].type == "vmess" and sb.outbounds[1].tag == "HK 01")
|
||||
check("sb node2 tag deduped", sb.outbounds[2].type == "vless" and sb.outbounds[2].tag == "HK 01 #2")
|
||||
check("sb node3 is trojan", sb.outbounds[3].type == "trojan" and sb.outbounds[3].tag == "JP")
|
||||
check("sb drops ssr", sb_raw:find('"ssr"') == nil)
|
||||
check("sb outbound count", #sb.outbounds == 3 + 4) -- 节点 + selector + urltest + direct + block
|
||||
|
||||
-- selector
|
||||
local sel, urltest, direct, block
|
||||
for _, o in ipairs(sb.outbounds) do
|
||||
if o.type == "selector" then sel = o
|
||||
elseif o.type == "urltest" then urltest = o
|
||||
elseif o.type == "direct" then direct = o
|
||||
elseif o.type == "block" then block = o end
|
||||
end
|
||||
check("sb has selector", sel ~= nil and sel.tag == "select")
|
||||
check("sb selector default auto", sel and sel.default == "auto")
|
||||
check("sb selector lists auto+direct first", sel and sel.outbounds[1] == "auto" and sel.outbounds[2] == "direct")
|
||||
check("sb selector lists all proxies", sel and #sel.outbounds == 2 + 3)
|
||||
|
||||
check("sb has urltest", urltest ~= nil and urltest.tag == "auto")
|
||||
check("sb urltest lists all proxies", urltest and #urltest.outbounds == 3)
|
||||
check("sb urltest outbound1", urltest and urltest.outbounds[1] == "HK 01")
|
||||
check("sb urltest outbound2", urltest and urltest.outbounds[2] == "HK 01 #2")
|
||||
|
||||
check("sb has direct", direct ~= nil and direct.tag == "direct")
|
||||
check("sb has block", block ~= nil and block.tag == "block")
|
||||
|
||||
-- route
|
||||
check("sb route.final is select", sb.route.final == "select")
|
||||
check("sb route auto_detect_interface", sb.route.auto_detect_interface == true)
|
||||
check("sb route rule count", #sb.route.rules == 1)
|
||||
check("sb route private -> direct", sb.route.rules[1].ip_is_private == true and sb.route.rules[1].outbound == "direct")
|
||||
-- action 自 1.11.0 起才存在且默认即 "route";省略它才能同时兼容 1.10 与 1.11+
|
||||
check("sb route rule omits action (version compat)", sb.route.rules[1].action == nil)
|
||||
|
||||
-- tls 必须是对象:security=tls 但无 sni 时曾产出非法的 "tls":[]
|
||||
local tls_node = { { proto = "vmess", name = "T", server = "1.1.1.1", port = 443, uuid = "u", security = "tls" } }
|
||||
local tls_raw = singbox.generate(tls_node)
|
||||
check("sb tls is object not array", tls_raw:find('"tls":{}') ~= nil and tls_raw:find('"tls":%[%]') == nil)
|
||||
check("sb tls with sni keeps server_name",
|
||||
singbox.generate({ { proto = "vmess", name = "T", server = "1.1.1.1", port = 443, uuid = "u",
|
||||
security = "tls", sni = "example.com" } }):find('"server_name":"example.com"') ~= nil)
|
||||
|
||||
-- 节点名与保留 tag 重名时不得冲突
|
||||
local clash_name = singbox.generate({ { proto = "vmess", name = "direct", server = "1.1.1.1", port = 443, uuid = "u" } })
|
||||
local cn = util.json_decode(clash_name)
|
||||
local cn_tags = {}
|
||||
for _, o in ipairs(cn.outbounds) do cn_tags[o.tag] = (cn_tags[o.tag] or 0) + 1 end
|
||||
local all_unique = true
|
||||
for _, c in pairs(cn_tags) do if c > 1 then all_unique = false end end
|
||||
check("sb tags unique despite reserved name", all_unique)
|
||||
check("sb renamed reserved node", cn.outbounds[1].tag == "direct #2")
|
||||
|
||||
-- 无可用节点:不生成 selector / urltest(outbounds 不允许为空),final 退回 direct
|
||||
local sb_empty = util.json_decode(singbox.generate({}))
|
||||
check("sb empty has 2 outbounds", #sb_empty.outbounds == 2)
|
||||
check("sb empty final direct", sb_empty.route.final == "direct")
|
||||
local sb_empty_types = {}
|
||||
for _, o in ipairs(sb_empty.outbounds) do sb_empty_types[o.type] = true end
|
||||
check("sb empty no selector/urltest", not sb_empty_types.selector and not sb_empty_types.urltest)
|
||||
check("sb empty keeps direct/block", sb_empty_types.direct and sb_empty_types.block)
|
||||
|
||||
-- 确定性:同样输入两次结果一致
|
||||
check("sb deterministic", singbox.generate(nodes) == singbox.generate(nodes))
|
||||
|
||||
-- ============================================================
|
||||
-- V2Ray / Xray 完整配置
|
||||
-- ============================================================
|
||||
local v2_raw = v2ray.generate(nodes)
|
||||
local v2 = util.json_decode(v2_raw)
|
||||
|
||||
check("v2 is valid json", v2 ~= nil)
|
||||
check("v2 has outbounds", type(v2.outbounds) == "table")
|
||||
check("v2 has routing", type(v2.routing) == "table")
|
||||
check("v2 has log", type(v2.log) == "table" and v2.log.loglevel == "warning")
|
||||
|
||||
-- 不含 inbounds / dns
|
||||
check("v2 has no inbounds", v2.inbounds == nil)
|
||||
check("v2 has no dns", v2.dns == nil)
|
||||
|
||||
-- 节点出站按序在前
|
||||
check("v2 node1 is vmess", v2.outbounds[1].protocol == "vmess" and v2.outbounds[1].tag == "HK 01")
|
||||
check("v2 node2 tag deduped", v2.outbounds[2].protocol == "vless" and v2.outbounds[2].tag == "HK 01 #2")
|
||||
check("v2 node3 is trojan", v2.outbounds[3].protocol == "trojan" and v2.outbounds[3].tag == "JP")
|
||||
check("v2 drops ssr", v2_raw:find('"ssr"') == nil)
|
||||
check("v2 outbound count", #v2.outbounds == 3 + 2) -- 节点 + freedom + blackhole
|
||||
|
||||
-- freedom / blackhole:settings 必须是对象,不能是 []
|
||||
local freedom, blackhole
|
||||
for _, o in ipairs(v2.outbounds) do
|
||||
if o.protocol == "freedom" then freedom = o
|
||||
elseif o.protocol == "blackhole" then blackhole = o end
|
||||
end
|
||||
check("v2 has freedom direct", freedom ~= nil and freedom.tag == "direct")
|
||||
check("v2 has blackhole block", blackhole ~= nil and blackhole.tag == "block")
|
||||
check("v2 settings is object not array", v2_raw:find('"settings":%[%]') == nil)
|
||||
check("v2 blackhole response", blackhole and blackhole.settings.response.type == "none")
|
||||
|
||||
-- observatory:leastPing 必须依赖它才生效
|
||||
check("v2 has observatory", type(v2.observatory) == "table")
|
||||
check("v2 observatory subjectSelector", v2.observatory and #v2.observatory.subjectSelector == 3)
|
||||
check("v2 observatory probeUrl", v2.observatory and v2.observatory.probeUrl ~= nil)
|
||||
check("v2 observatory probeInterval", v2.observatory and v2.observatory.probeInterval == "10s")
|
||||
|
||||
-- routing
|
||||
check("v2 domainStrategy", v2.routing.domainStrategy == "IPIfNonMatch")
|
||||
check("v2 has balancer", type(v2.routing.balancers) == "table" and #v2.routing.balancers == 1)
|
||||
local bal = v2.routing.balancers[1]
|
||||
check("v2 balancer tag auto", bal.tag == "auto")
|
||||
check("v2 balancer strategy leastPing", bal.strategy.type == "leastPing")
|
||||
check("v2 balancer selector lists proxies", #bal.selector == 3)
|
||||
check("v2 rule count", #v2.routing.rules == 2)
|
||||
check("v2 rule1 private -> direct",
|
||||
v2.routing.rules[1].type == "field" and v2.routing.rules[1].ip[1] == "geoip:private"
|
||||
and v2.routing.rules[1].outboundTag == "direct")
|
||||
check("v2 rule2 catchall -> balancer",
|
||||
v2.routing.rules[2].network == "tcp,udp" and v2.routing.rules[2].balancerTag == "auto")
|
||||
|
||||
-- 前缀匹配防护:Xray 的 selector 按前缀匹配,节点名 "d" 是 "direct" 的前缀,
|
||||
-- 若不消解会把 direct 出站也纳入负载均衡
|
||||
local pfx = util.json_decode(v2ray.generate({ { proto = "vmess", name = "d", server = "1.1.1.1", port = 443, uuid = "u" } }))
|
||||
check("v2 renames prefix-of-reserved node", pfx.outbounds[1].tag == "d #2")
|
||||
check("v2 balancer selector excludes direct", pfx.routing.balancers[1].selector[1] == "d #2")
|
||||
|
||||
-- 无可用节点:没有 balancer 可引用,兜底规则改指 direct
|
||||
local v2_empty = util.json_decode(v2ray.generate({}))
|
||||
check("v2 empty has 2 outbounds", #v2_empty.outbounds == 2)
|
||||
check("v2 empty no balancer", v2_empty.routing.balancers == nil)
|
||||
check("v2 empty no observatory", v2_empty.observatory == nil)
|
||||
check("v2 empty rule count", #v2_empty.routing.rules == 2)
|
||||
check("v2 empty catchall -> direct", v2_empty.routing.rules[2].outboundTag == "direct")
|
||||
|
||||
check("v2 deterministic", v2ray.generate(nodes) == v2ray.generate(nodes))
|
||||
|
||||
-- ---------- 往返:完整配置重新导入必须只取真实节点 ----------
|
||||
-- 完整配置里多了 selector / urltest / direct / block(sing-box)与
|
||||
-- freedom / blackhole(Xray),解析器必须跳过它们而非当成节点
|
||||
local parser = require("substore.parser")
|
||||
for _, fmt in ipairs({ "singbox", "v2ray" }) do
|
||||
local txt = (fmt == "singbox") and sb_raw or v2_raw
|
||||
local res = parser.parse(txt)
|
||||
check("roundtrip " .. fmt .. " parses", res ~= nil and #res.nodes == 3)
|
||||
local protos = {}
|
||||
for _, n in ipairs(res and res.nodes or {}) do protos[n.proto] = true end
|
||||
check("roundtrip " .. fmt .. " only real proxies",
|
||||
protos.vmess and protos.vless and protos.trojan
|
||||
and not protos.direct and not protos.selector and not protos.urltest and not protos.freedom)
|
||||
end
|
||||
|
||||
-- ---------- 经 output.generate 分发后行为一致 ----------
|
||||
check("dispatch singbox full", output.generate(nodes, "singbox") == sb_raw)
|
||||
check("dispatch v2ray full", output.generate(nodes, "v2ray") == v2_raw)
|
||||
check("dispatch singbox alias", output.generate(nodes, "sing-box") == sb_raw)
|
||||
|
||||
-- ---------- 结果 ----------
|
||||
print(string.format("\n%d passed, %d failed", passed, failed))
|
||||
os.exit(failed == 0 and 0 or 1)
|
||||
@@ -81,10 +81,21 @@ local surge = output.generate({ n }, "surge")
|
||||
check("surge drops ssr", surge and surge:find("127%.0%.0%.1") == nil)
|
||||
|
||||
-- ---------- sing-box / V2Ray 丢弃 SSR ----------
|
||||
-- 完整配置里 outbounds 恒含 direct / block,因此断言「不含 ssr 出站」
|
||||
-- 而非「outbounds 为空」;无可用节点时也不生成分组
|
||||
local sb = output.generate({ n }, "singbox")
|
||||
check("singbox drops ssr", sb and util.json_decode(sb) and #util.json_decode(sb).outbounds == 0)
|
||||
local sbd = util.json_decode(sb)
|
||||
local sb_types = {}
|
||||
for _, o in ipairs(sbd and sbd.outbounds or {}) do sb_types[o.type] = true end
|
||||
check("singbox drops ssr", sbd and not sb_types.ssr and not sb_types.shadowsocksr)
|
||||
check("singbox no group without nodes", sbd and not sb_types.selector and not sb_types.urltest)
|
||||
check("singbox keeps direct/block", sb_types.direct and sb_types.block)
|
||||
local v2 = output.generate({ n }, "v2ray")
|
||||
check("v2ray drops ssr", v2 and util.json_decode(v2) and #util.json_decode(v2).outbounds == 0)
|
||||
local v2d = util.json_decode(v2)
|
||||
local v2_protos = {}
|
||||
for _, o in ipairs(v2d and v2d.outbounds or {}) do v2_protos[o.protocol] = true end
|
||||
check("v2ray drops ssr", v2d and not v2_protos.ssr and not v2_protos.shadowsocksr)
|
||||
check("v2ray keeps freedom/blackhole", v2_protos.freedom and v2_protos.blackhole)
|
||||
|
||||
-- ---------- 最小 ssr(无参数,默认 origin/plain) ----------
|
||||
local minimal = parser.parse_uri("ssr://" .. util.base64_encode("1.2.3.4:80:origin:aes-256-cfb:plain:" .. util.base64_encode("p")))
|
||||
|
||||
+21
-95
@@ -7,115 +7,41 @@ include $(TOPDIR)/rules.mk
|
||||
LUCI_TITLE:=LuCI for Tailscale
|
||||
LUCI_DEPENDS:=+tailscale +jshn +curl +jq +flock
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=1.2.17
|
||||
PKG_RELEASE:=4
|
||||
PKG_RELEASE:=5
|
||||
|
||||
# Keep UCI configs as ordinary package data. The pre-install hook snapshots an
|
||||
# existing configuration before extraction; the UCI-defaults script restores it
|
||||
# afterwards. Declaring the same files as opkg conffiles conflicts with this
|
||||
# lifecycle and creates inactive *-opkg files during upgrades.
|
||||
define Package/luci-app-tailscale/preinst
|
||||
#!/bin/sh
|
||||
state_dir=/etc/.luci-app-tailscale-upgrade
|
||||
state_pending="$${state_dir}.pending"
|
||||
legacy_hotplug=/etc/hotplug.d/iface/40-tailscale
|
||||
legacy_backup_dir=/etc/tailscale/luci-app-tailscale-legacy
|
||||
|
||||
[ -n "$${IPKG_INSTROOT}" ] && exit 0
|
||||
|
||||
disable_legacy_hotplug() {
|
||||
local backup_file
|
||||
|
||||
[ -e "$${legacy_hotplug}" ] || [ -L "$${legacy_hotplug}" ] || return 0
|
||||
backup_file="$${legacy_backup_dir}/40-tailscale.disabled.$$(date +%Y%m%d%H%M%S).$$$$"
|
||||
if mkdir -p "$${legacy_backup_dir}" 2>/dev/null && cp -p "$${legacy_hotplug}" "$${backup_file}"; then
|
||||
:
|
||||
else
|
||||
rm -f "$${backup_file}" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Disabling the duplicate starter is mandatory even when its backup fails.
|
||||
# chmod is a last-resort guard for a transient remove failure; package
|
||||
# extraction will subsequently remove the obsolete package-owned path.
|
||||
rm -f "$${legacy_hotplug}" || chmod 000 "$${legacy_hotplug}" || return 1
|
||||
}
|
||||
|
||||
disable_legacy_hotplug || exit 1
|
||||
|
||||
# 若系统内无任何相关配置文件,说明是全新安装,直接退出让包管理器正常解压释放默认配置
|
||||
[ -f /etc/config/tailscale ] || [ -f /etc/config/tailscale_openclash ] || [ -f /etc/config/tailscale_policy_routing ] || exit 0
|
||||
[ -f "$${state_dir}/.complete" ] && exit 0
|
||||
|
||||
clear_state_dir() {
|
||||
rm -f "$${1}/tailscale" "$${1}/tailscale_openclash" \
|
||||
"$${1}/tailscale_openclash.absent" "$${1}/tailscale_policy_routing" \
|
||||
"$${1}/.complete"
|
||||
rmdir "$${1}" 2>/dev/null || [ ! -e "$${1}" ]
|
||||
}
|
||||
|
||||
clear_state_dir "$${state_dir}" || exit 1
|
||||
clear_state_dir "$${state_pending}" || exit 1
|
||||
|
||||
umask 077
|
||||
mkdir "$${state_pending}" || exit 1
|
||||
chmod 700 "$${state_pending}" || exit 1
|
||||
|
||||
if [ -f /etc/config/tailscale ]; then
|
||||
cp /etc/config/tailscale "$${state_pending}/tailscale" || exit 1
|
||||
fi
|
||||
|
||||
if [ -f /etc/config/tailscale_openclash ]; then
|
||||
cp /etc/config/tailscale_openclash "$${state_pending}/tailscale_openclash" || exit 1
|
||||
fi
|
||||
|
||||
if [ -f /etc/config/tailscale_policy_routing ]; then
|
||||
cp /etc/config/tailscale_policy_routing "$${state_pending}/tailscale_policy_routing" || exit 1
|
||||
fi
|
||||
|
||||
: >"$${state_pending}/.complete" || exit 1
|
||||
mv "$${state_pending}" "$${state_dir}" || exit 1
|
||||
|
||||
exit 0
|
||||
define Package/luci-app-tailscale/conffiles
|
||||
/etc/config/tailscale
|
||||
/etc/config/tailscale_openclash
|
||||
/etc/config/tailscale_policy_routing
|
||||
endef
|
||||
|
||||
define Package/luci-app-tailscale/prerm
|
||||
#!/bin/sh
|
||||
state_dir=/etc/.luci-app-tailscale-upgrade
|
||||
[ -n "$${IPKG_INSTROOT}" ] && exit 0
|
||||
|
||||
case "$${2:-}" in
|
||||
remove|[0-9]*) ;;
|
||||
*)
|
||||
# default_prerm sources this script for opkg, while OpenWrt 24.10
|
||||
# apk runs pre-deinstall directly with the old package version in $1.
|
||||
case "$${1:-}" in
|
||||
[0-9]*) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
if [ -x /usr/sbin/tailscale_policy_routing ]; then
|
||||
/usr/sbin/tailscale_policy_routing cleanup >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [ -z "$${IPKG_INSTROOT}" ] && [ -x /usr/sbin/tailscale_policy_routing ]; then
|
||||
/usr/sbin/tailscale_policy_routing cleanup >/dev/null 2>&1 || exit 1
|
||||
if [ -x /etc/init.d/tailscale-policy-routing ]; then
|
||||
/etc/init.d/tailscale-policy-routing stop >/dev/null 2>&1 || true
|
||||
/etc/init.d/tailscale-policy-routing disable >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -z "$${IPKG_INSTROOT}" ] && [ -x /etc/init.d/tailscale-policy-routing ]; then
|
||||
/etc/init.d/tailscale-policy-routing disable >/dev/null 2>&1 || exit 1
|
||||
fi
|
||||
if [ -z "$${IPKG_INSTROOT}" ] && [ -x /usr/sbin/tailscale_openclash_bypass ]; then
|
||||
|
||||
if [ -x /usr/sbin/tailscale_openclash_bypass ]; then
|
||||
/usr/sbin/tailscale_openclash_bypass cleanup >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -z "$${IPKG_INSTROOT}" ] && [ -x /etc/init.d/tailscale-openclash-bypass ]; then
|
||||
|
||||
if [ -x /etc/init.d/tailscale-openclash-bypass ]; then
|
||||
/etc/init.d/tailscale-openclash-bypass stop >/dev/null 2>&1 || true
|
||||
/etc/init.d/tailscale-openclash-bypass disable >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -z "$${IPKG_INSTROOT}" ] && [ -x /etc/init.d/tailscale ]; then
|
||||
/etc/init.d/tailscale stop >/dev/null 2>&1 || exit 1
|
||||
|
||||
if [ -x /etc/init.d/tailscale ]; then
|
||||
/etc/init.d/tailscale stop >/dev/null 2>&1 || true
|
||||
fi
|
||||
for cleanup_dir in "$${state_dir}" "$${state_dir}.pending"; do
|
||||
rm -f "$${cleanup_dir}/tailscale" "$${cleanup_dir}/tailscale_openclash" \
|
||||
"$${cleanup_dir}/tailscale_openclash.absent" "$${cleanup_dir}/tailscale_policy_routing" \
|
||||
"$${cleanup_dir}/.complete"
|
||||
rmdir "$${cleanup_dir}" 2>/dev/null || true
|
||||
done
|
||||
|
||||
exit 0
|
||||
endef
|
||||
|
||||
|
||||
@@ -1,84 +1,30 @@
|
||||
#!/bin/sh
|
||||
|
||||
UPGRADE_STATE_DIR="${UPGRADE_STATE_DIR:-/etc/.luci-app-tailscale-upgrade}"
|
||||
TAILSCALE_SECRETS_BIN="${TAILSCALE_SECRETS_BIN:-/usr/sbin/tailscale_secrets}"
|
||||
UCI_BIN="${UCI_BIN:-uci}"
|
||||
OPENCLASH_BYPASS_INIT="${OPENCLASH_BYPASS_INIT:-/etc/init.d/tailscale-openclash-bypass}"
|
||||
POLICY_ROUTING_INIT="${POLICY_ROUTING_INIT:-/etc/init.d/tailscale-policy-routing}"
|
||||
LEGACY_ADGUARD_DNS_INIT="${LEGACY_ADGUARD_DNS_INIT:-/etc/init.d/tailscale-adguard-dns}"
|
||||
LEGACY_ADGUARD_DNS_RC_DIR="${LEGACY_ADGUARD_DNS_RC_DIR:-/etc/rc.d}"
|
||||
LEGACY_ADGUARD_DNS_BACKUP_DIR="${LEGACY_ADGUARD_DNS_BACKUP_DIR:-/etc/tailscale/luci-app-tailscale-legacy}"
|
||||
LUCI_INDEX_CACHE="${LUCI_INDEX_CACHE:-/tmp/luci-indexcache}"
|
||||
|
||||
cleanup_upgrade_state() {
|
||||
rm -f "$UPGRADE_STATE_DIR/tailscale" "$UPGRADE_STATE_DIR/tailscale_openclash" \
|
||||
"$UPGRADE_STATE_DIR/tailscale_policy_routing" \
|
||||
"$UPGRADE_STATE_DIR/.complete"
|
||||
rmdir "$UPGRADE_STATE_DIR" 2>/dev/null || true
|
||||
}
|
||||
|
||||
restore_upgrade_config() {
|
||||
local source_file="$1" target_file="$2" temporary_file
|
||||
|
||||
[ -f "$source_file" ] || return 0
|
||||
temporary_file="$target_file.luci-app-tailscale-upgrade.$$"
|
||||
cp "$source_file" "$temporary_file" || return 1
|
||||
mv "$temporary_file" "$target_file" || {
|
||||
rm -f "$temporary_file"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
cleanup_legacy_adguard_dns_service() {
|
||||
local backup_dir link
|
||||
|
||||
[ -e "$LEGACY_ADGUARD_DNS_INIT" ] || return 0
|
||||
backup_dir="$LEGACY_ADGUARD_DNS_BACKUP_DIR/tailscale-adguard-dns.$(date +%Y%m%d%H%M%S).$$"
|
||||
mkdir -p "$backup_dir" || return 1
|
||||
|
||||
for link in "$LEGACY_ADGUARD_DNS_RC_DIR"/*tailscale-adguard-dns; do
|
||||
[ -e "$link" ] || [ -L "$link" ] || continue
|
||||
printf '%s\t%s\n' "${link##*/}" "$(readlink "$link" 2>/dev/null || true)" >>"$backup_dir/rc-links"
|
||||
done
|
||||
|
||||
if [ -x "$LEGACY_ADGUARD_DNS_INIT" ]; then
|
||||
"$LEGACY_ADGUARD_DNS_INIT" stop || return 1
|
||||
"$LEGACY_ADGUARD_DNS_INIT" disable || true
|
||||
fi
|
||||
|
||||
cp -p "$LEGACY_ADGUARD_DNS_INIT" "$backup_dir/init" || return 1
|
||||
|
||||
for link in "$LEGACY_ADGUARD_DNS_RC_DIR"/*tailscale-adguard-dns; do
|
||||
[ -e "$link" ] || [ -L "$link" ] || continue
|
||||
rm -f "$link" || return 1
|
||||
done
|
||||
|
||||
mv "$LEGACY_ADGUARD_DNS_INIT" "$backup_dir/tailscale-adguard-dns.disabled" || return 1
|
||||
}
|
||||
|
||||
if [ -f "$UPGRADE_STATE_DIR/.complete" ]; then
|
||||
restore_upgrade_config "$UPGRADE_STATE_DIR/tailscale" /etc/config/tailscale || exit 1
|
||||
restore_upgrade_config "$UPGRADE_STATE_DIR/tailscale_openclash" /etc/config/tailscale_openclash || exit 1
|
||||
restore_upgrade_config "$UPGRADE_STATE_DIR/tailscale_policy_routing" /etc/config/tailscale_policy_routing || exit 1
|
||||
cleanup_upgrade_state
|
||||
elif [ -d "$UPGRADE_STATE_DIR" ]; then
|
||||
cleanup_upgrade_state
|
||||
# 初始化密钥存储
|
||||
if [ -x "$TAILSCALE_SECRETS_BIN" ]; then
|
||||
"$TAILSCALE_SECRETS_BIN" migrate || exit 1
|
||||
fi
|
||||
|
||||
cleanup_legacy_adguard_dns_service || exit 1
|
||||
|
||||
"$TAILSCALE_SECRETS_BIN" migrate || exit 1
|
||||
|
||||
# 根据配置启用 OpenClash 旁路分流服务
|
||||
openclash_bypass_enabled="$("$UCI_BIN" -q get tailscale_openclash.settings.enabled 2>/dev/null || true)"
|
||||
if [ "$openclash_bypass_enabled" = '1' ] && [ -x "$OPENCLASH_BYPASS_INIT" ]; then
|
||||
"$OPENCLASH_BYPASS_INIT" enable
|
||||
"$OPENCLASH_BYPASS_INIT" start
|
||||
fi
|
||||
|
||||
# 启用策略路由服务
|
||||
if [ -x "$POLICY_ROUTING_INIT" ]; then
|
||||
"$POLICY_ROUTING_INIT" enable
|
||||
"$POLICY_ROUTING_INIT" start
|
||||
fi
|
||||
|
||||
# 刷新 LuCI 缓存使菜单立即生效
|
||||
rm -f "$LUCI_INDEX_CACHE"
|
||||
|
||||
exit 0
|
||||
Reference in new issue
Block a user