mirror of
https://github.com/fscarmen/sing-box.git
synced 2026-08-10 17:55:16 +08:00
v1.3.14 feat: add custom warp-ep route rule management
- add optional custom warp-ep route rule management via [sb -d] - expand custom route entries by individual domain_suffix and rule_set values v1.3.14 feat: 增加自定义 warp-ep 路由规则管理 - 增加可选的自定义 warp-ep 路由规则管理,可通过 [sb -d] 按需配置 - 将 domain_suffix 和 rule_set 数组按单项展开显示
This commit is contained in:
@@ -25,6 +25,8 @@
|
|||||||
* * *
|
* * *
|
||||||
## 1.更新信息
|
## 1.更新信息
|
||||||
|
|
||||||
|
2026.05.29 v1.3.14 Added optional custom warp route rule management, available after installation via [sb -d] without affecting the main installation flow; 增加可选的自定义 warp 路由规则管理,不影响主程序安装流程,安装后可通过 [sb -d] 按需管理
|
||||||
|
|
||||||
2026.05.18 v1.3.13 Added top-level http_clients configuration; 增加顶层的 http_clients 配置
|
2026.05.18 v1.3.13 Added top-level http_clients configuration; 增加顶层的 http_clients 配置
|
||||||
|
|
||||||
2026.05.14 v1.3.12 1. Add Hysteria2 Realm support for machines without public inbound access, with optional WARP-assisted hole punching for strict NAT environments; 2. Realm configuration export is supported for Clash/Mihomo and sing-box clients; 3. Hysteria2 Realm can be enabled or disabled directly via sb -d; 4. Non-interactive installs support --HY2_REALM and --HY2_WARP parameters; 1. 增加 Hysteria2 Realm 支持,适用于没有公网入口的机器,并可选 WARP 辅助打洞; 2. Realm 已支持导出 Clash/Mihomo 和 sing-box 客户端配置; 3. 修改节点配置时可直接开启或关闭 Hysteria2 Realm; 4. 无交互安装支持 --HY2_REALM 与 --HY2_WARP 参数
|
2026.05.14 v1.3.12 1. Add Hysteria2 Realm support for machines without public inbound access, with optional WARP-assisted hole punching for strict NAT environments; 2. Realm configuration export is supported for Clash/Mihomo and sing-box clients; 3. Hysteria2 Realm can be enabled or disabled directly via sb -d; 4. Non-interactive installs support --HY2_REALM and --HY2_WARP parameters; 1. 增加 Hysteria2 Realm 支持,适用于没有公网入口的机器,并可选 WARP 辅助打洞; 2. Realm 已支持导出 Clash/Mihomo 和 sing-box 客户端配置; 3. 修改节点配置时可直接开启或关闭 Hysteria2 Realm; 4. 无交互安装支持 --HY2_REALM 与 --HY2_WARP 参数
|
||||||
@@ -600,6 +602,7 @@ services:
|
|||||||
| |-- 05_dns.json # DNS 规则文件
|
| |-- 05_dns.json # DNS 规则文件
|
||||||
| |-- 06_ntp.json # 服务端时间同步配置文件
|
| |-- 06_ntp.json # 服务端时间同步配置文件
|
||||||
| |-- 07_http_clients.json # 专门给 sing-box 内部组件发 HTTP 请求配置文件
|
| |-- 07_http_clients.json # 专门给 sing-box 内部组件发 HTTP 请求配置文件
|
||||||
|
| |-- 08_custom_route.json # 用户自定义 warp-ep 出站路由规则文件,支持 domain_suffix / rule_set 分流
|
||||||
| |-- 11_xtls-reality_inbounds.json # Reality vision 协议配置文件
|
| |-- 11_xtls-reality_inbounds.json # Reality vision 协议配置文件
|
||||||
| |-- 12_hysteria2_inbounds.json # Hysteria2 协议配置文件
|
| |-- 12_hysteria2_inbounds.json # Hysteria2 协议配置文件
|
||||||
| |-- 13_tuic_inbounds.json # Tuic V5 协议配置文件 # Hysteria2 协议配置文件
|
| |-- 13_tuic_inbounds.json # Tuic V5 协议配置文件 # Hysteria2 协议配置文件
|
||||||
|
|||||||
+440
-3
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# 当前脚本版本号
|
# 当前脚本版本号
|
||||||
VERSION='v1.3.13 (2026.05.18)'
|
VERSION='v1.3.14 (2026.05.29)'
|
||||||
|
|
||||||
# Github 反代加速代理
|
# Github 反代加速代理
|
||||||
GITHUB_PROXY=('https://hub.glowp.xyz/' 'https://proxy.vvvv.ee/')
|
GITHUB_PROXY=('https://hub.glowp.xyz/' 'https://proxy.vvvv.ee/')
|
||||||
@@ -39,8 +39,8 @@ mkdir -p "$TEMP_DIR"
|
|||||||
|
|
||||||
E[0]="Language:\n 1. English (default) \n 2. 简体中文"
|
E[0]="Language:\n 1. English (default) \n 2. 简体中文"
|
||||||
C[0]="${E[0]}"
|
C[0]="${E[0]}"
|
||||||
E[1]="1. Added Hysteria2 Realm support for machines without public inbound access, with optional WARP-assisted hole punching; 2. Realm is exported for Clash/Mihomo and sing-box clients; 3. Hysteria2 Realm can be toggled directly in node configuration changes; 4. Unified the fixed STUN server list for Hysteria2 Realm; 5. Added top-level http_clients configuration"
|
E[1]="1. Added optional custom warp route rule management, available after installation via [sb -d] without affecting the main installation flow; 2. Added Hysteria2 Realm support for machines without public inbound access, with optional WARP-assisted hole punching"
|
||||||
C[1]="1. 增加 Hysteria2 Realm 支持,适用于没有公网入口的机器,并可选 WARP 辅助打洞; 2. Realm 已支持导出 Clash/Mihomo 和 sing-box 客户端配置; 3. 修改节点配置时可直接开启或关闭 Hysteria2 Realm; 4. 统一 Hysteria2 Realm 固定 STUN 服务器列表。 5. 增加顶层的 http_clients 配置"
|
C[1]="1. 增加可选的自定义 warp 路由规则管理,不影响主程序安装流程,安装后可通过 [sb -d] 按需管理; 2. 增加 Hysteria2 Realm 支持,适用于没有公网入口的机器,并可选 WARP 辅助打洞"
|
||||||
E[2]="Downloading Sing-box. Please wait a seconds ..."
|
E[2]="Downloading Sing-box. Please wait a seconds ..."
|
||||||
C[2]="下载 Sing-box 中,请稍等 ..."
|
C[2]="下载 Sing-box 中,请稍等 ..."
|
||||||
E[3]="Input errors up to 5 times.The script is aborted."
|
E[3]="Input errors up to 5 times.The script is aborted."
|
||||||
@@ -335,6 +335,34 @@ E[147]="Hysteria2 Realm is useful for China-back routing or machines without pub
|
|||||||
C[147]="Hysteria2 Realm 适用于回国或者没有公网入口的机器;有公网入口时不建议使用。是否启用?[y/N]:"
|
C[147]="Hysteria2 Realm 适用于回国或者没有公网入口的机器;有公网入口时不建议使用。是否启用?[y/N]:"
|
||||||
E[148]="WARP-assisted hole punching is useful in strict NAT environments. When direct hole punching fails, Cloudflare WARP can provide a CF egress path to improve success. Enable it? [y/N]:"
|
E[148]="WARP-assisted hole punching is useful in strict NAT environments. When direct hole punching fails, Cloudflare WARP can provide a CF egress path to improve success. Enable it? [y/N]:"
|
||||||
C[148]="WARP 辅助打洞(适用于 NAT 严格环境):当 NAT 类型较严格(如对称 NAT)导致直连打洞失败时,可借助 Cloudflare WARP 获取一个 CF 出口 IP 作为中转,提升打洞成功率。是否启用?[y/N]:"
|
C[148]="WARP 辅助打洞(适用于 NAT 严格环境):当 NAT 类型较严格(如对称 NAT)导致直连打洞失败时,可借助 Cloudflare WARP 获取一个 CF 出口 IP 作为中转,提升打洞成功率。是否启用?[y/N]:"
|
||||||
|
E[150]="Custom warp-ep outbounds rules (rules: \${CUSTOM_ROUTE_COUNT:-0})"
|
||||||
|
C[150]="自定义 warp-ep 出站路由规则 (规则数: \${CUSTOM_ROUTE_COUNT:-0})"
|
||||||
|
E[151]="1. Add rule\n 2. View rules\n 3. Delete rule\n 0. Back"
|
||||||
|
C[151]="1. 添加规则\n 2. 查看规则\n 3. 删除规则\n 0. 返回"
|
||||||
|
E[152]="Select rule type:\\n 1. domain_suffix\\n 2. rule_set"
|
||||||
|
C[152]="选择规则类型:\\n 1. domain_suffix (域名后缀)\\n 2. rule_set (规则集)"
|
||||||
|
E[153]="Enter domain suffix (comma-separated, e.g. google.com,telegram.org):"
|
||||||
|
C[153]="输入域名后缀 (逗号分隔,如 google.com,telegram.org):"
|
||||||
|
E[154]="Enter rule_set name (comma-separated, e.g. geosite-google,geosite-telegram):"
|
||||||
|
C[154]="输入规则集名称 (逗号分隔,如 geosite-google,geosite-telegram):"
|
||||||
|
E[155]="Matched custom route rules will use warp-ep outbound."
|
||||||
|
C[155]="命中的自定义路由规则将使用 warp-ep 出站。"
|
||||||
|
E[156]="Rule set \"\${RULE_NAME}\" not found in SagerNet or MetaCubeX repositories. Please re-enter:"
|
||||||
|
C[156]="规则集 \"\${RULE_NAME}\" 在 SagerNet 和 MetaCubeX 仓库中均未找到,请重新输入:"
|
||||||
|
E[157]="Custom route rule added successfully."
|
||||||
|
C[157]="自定义路由规则添加成功。"
|
||||||
|
E[158]="No custom route rules configured."
|
||||||
|
C[158]="未配置自定义路由规则。"
|
||||||
|
E[159]="Enter warp-ep outbound rule number(s) to delete (comma-separated):"
|
||||||
|
C[159]="输入要删除的 warp-ep 出站规则编号 (逗号分隔):"
|
||||||
|
E[160]="Custom route rule(s) deleted."
|
||||||
|
C[160]="自定义路由规则已删除。"
|
||||||
|
E[161]="Invalid domain format: \${DOMAIN}"
|
||||||
|
C[161]="无效的域名格式: \${DOMAIN}"
|
||||||
|
E[162]="API check failed, using SagerNet URL as default. (Warning: rule_set may not exist)"
|
||||||
|
C[162]="API 校验失败,默认使用 SagerNet 地址。(警告: 规则集可能不存在)"
|
||||||
|
E[163]="Current custom route rules:"
|
||||||
|
C[163]="当前自定义路由规则:"
|
||||||
|
|
||||||
# 自定义字体彩色,read 函数
|
# 自定义字体彩色,read 函数
|
||||||
warning() { echo -e "\033[31m\033[01m$*\033[0m"; } # 红色
|
warning() { echo -e "\033[31m\033[01m$*\033[0m"; } # 红色
|
||||||
@@ -630,6 +658,12 @@ change_config() {
|
|||||||
MENU_IDX+=(139) && MENU_KEY+=(hy2hopping) && MENU_VAL+=("${HY2_PORT_HOPPING_RANGE}")
|
MENU_IDX+=(139) && MENU_KEY+=(hy2hopping) && MENU_VAL+=("${HY2_PORT_HOPPING_RANGE}")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 自定义路由规则(仅在 warp-ep 存在时显示)
|
||||||
|
grep -q '"warp-ep"' ${WORK_DIR}/conf/02_endpoints.json 2>/dev/null && {
|
||||||
|
CUSTOM_ROUTE_COUNT=$(custom_route_count)
|
||||||
|
MENU_IDX+=(150) && MENU_KEY+=(customroute) && MENU_VAL+=("${CUSTOM_ROUTE_COUNT}")
|
||||||
|
}
|
||||||
|
|
||||||
[ "${#MENU_IDX[@]}" -eq 0 ] && error " $(text 110) "
|
[ "${#MENU_IDX[@]}" -eq 0 ] && error " $(text 110) "
|
||||||
|
|
||||||
# 显示动态菜单
|
# 显示动态菜单
|
||||||
@@ -741,6 +775,9 @@ change_config() {
|
|||||||
|
|
||||||
export_list
|
export_list
|
||||||
return
|
return
|
||||||
|
elif [ "$KEY" = "customroute" ]; then
|
||||||
|
custom_route_menu
|
||||||
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
hint ""
|
hint ""
|
||||||
@@ -1144,6 +1181,406 @@ sync_hy2_warp_route() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ===================== 自定义路由规则 =====================
|
||||||
|
|
||||||
|
# 统计自定义路由规则数量(按数组里的单项统计,不按整条 route rule 统计)
|
||||||
|
custom_route_count() {
|
||||||
|
local CUSTOM_FILE="${WORK_DIR}/conf/08_custom_route.json"
|
||||||
|
if [ -s "$CUSTOM_FILE" ]; then
|
||||||
|
jq_exec '[.route.rules[]? | ((.domain_suffix // []) | length) + ((.rule_set // []) | length)] | add // 0' "$CUSTOM_FILE" 2>/dev/null || echo 0
|
||||||
|
else
|
||||||
|
echo 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 将 warp-ep 的 domain_suffix / rule_set 合并到同一条 route rule,保持 08_custom_route.json 更简洁
|
||||||
|
custom_route_compact_rules() {
|
||||||
|
local CUSTOM_FILE="${WORK_DIR}/conf/08_custom_route.json"
|
||||||
|
local TMP_FILE="${CUSTOM_FILE}.tmp"
|
||||||
|
[ ! -s "$CUSTOM_FILE" ] && return
|
||||||
|
|
||||||
|
jq_exec '
|
||||||
|
(.route.rules // []) as $rules |
|
||||||
|
($rules | map(select((.outbound // "warp-ep") == "warp-ep") | .domain_suffix // []) | add // [] | reduce .[] as $x ([]; if index($x) then . else . + [$x] end)) as $domains |
|
||||||
|
($rules | map(select((.outbound // "warp-ep") == "warp-ep") | .rule_set // []) | add // [] | reduce .[] as $x ([]; if index($x) then . else . + [$x] end)) as $sets |
|
||||||
|
($rules | map(select((.outbound // "warp-ep") != "warp-ep"))) as $others |
|
||||||
|
.route.rules = (
|
||||||
|
$others +
|
||||||
|
(if (($domains | length) + ($sets | length)) > 0 then
|
||||||
|
[((if ($sets | length) > 0 then {rule_set:$sets} else {} end)
|
||||||
|
+ (if ($domains | length) > 0 then {domain_suffix:$domains} else {} end)
|
||||||
|
+ {outbound:"warp-ep"})]
|
||||||
|
else [] end)
|
||||||
|
)
|
||||||
|
' "$CUSTOM_FILE" > "$TMP_FILE" && mv "$TMP_FILE" "$CUSTOM_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 通过 GitHub API 校验 rule_set 是否存在,返回下载 URL
|
||||||
|
# 参数: $1 = rule_set 名称(不含 .srs)
|
||||||
|
# 输出: 下载 URL 或空字符串
|
||||||
|
check_rule_set_exists() {
|
||||||
|
local NAME="$1"
|
||||||
|
local SRS_NAME="${NAME}.srs"
|
||||||
|
local CACHE_DIR="${TEMP_DIR}/ruleset_cache"
|
||||||
|
mkdir -p "$CACHE_DIR"
|
||||||
|
|
||||||
|
# SagerNet 源
|
||||||
|
local SAGERNET_CACHE="${CACHE_DIR}/sagernet_tree.json"
|
||||||
|
if [ ! -s "$SAGERNET_CACHE" ]; then
|
||||||
|
curl -sL --connect-timeout 5 --max-time 15 "https://api.github.com/repos/SagerNet/sing-geosite/git/trees/rule-set?recursive=1" > "$SAGERNET_CACHE" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -s "$SAGERNET_CACHE" ] && jq_exec -e ".tree[]? | select(.path == \"${SRS_NAME}\")" "$SAGERNET_CACHE" >/dev/null 2>&1; then
|
||||||
|
echo "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/${SRS_NAME}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# MetaCubeX 源
|
||||||
|
local METACUBEX_CACHE="${CACHE_DIR}/metacubex_tree.json"
|
||||||
|
if [ ! -s "$METACUBEX_CACHE" ]; then
|
||||||
|
curl -sL --connect-timeout 5 --max-time 15 "https://api.github.com/repos/MetaCubeX/meta-rules-dat/git/trees/sing?recursive=1" > "$METACUBEX_CACHE" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -s "$METACUBEX_CACHE" ]; then
|
||||||
|
local MATCH_PATH
|
||||||
|
MATCH_PATH=$(jq_exec -r "[.tree[]? | select(.path | endswith(\"/${SRS_NAME}\") or . == \"${SRS_NAME}\") | .path] | first // empty" "$METACUBEX_CACHE" 2>/dev/null)
|
||||||
|
if [ -n "$MATCH_PATH" ]; then
|
||||||
|
echo "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/${MATCH_PATH}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 两处 API 都没数据(可能是 rate limit 或网络问题)
|
||||||
|
if [ ! -s "$SAGERNET_CACHE" ] && [ ! -s "$METACUBEX_CACHE" ]; then
|
||||||
|
# API 不可用,降级使用默认 URL
|
||||||
|
warning " $(text 162) "
|
||||||
|
echo "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/${SRS_NAME}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 确实在两处都没找到
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加自定义路由规则
|
||||||
|
custom_route_add() {
|
||||||
|
local CUSTOM_FILE="${WORK_DIR}/conf/08_custom_route.json"
|
||||||
|
|
||||||
|
# 选择规则类型
|
||||||
|
hint "\n $(text 152) "
|
||||||
|
reading " $(text 24) " RULE_TYPE_CHOICE
|
||||||
|
case "$RULE_TYPE_CHOICE" in
|
||||||
|
1 ) local RULE_TYPE="domain_suffix" ;;
|
||||||
|
2 ) local RULE_TYPE="rule_set" ;;
|
||||||
|
* ) info " $(text 135) " && return ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local VALIDATED_VALUES=()
|
||||||
|
local RULE_SET_URLS=()
|
||||||
|
|
||||||
|
if [ "$RULE_TYPE" = "domain_suffix" ]; then
|
||||||
|
reading " $(text 153) " DOMAIN_INPUT
|
||||||
|
[ -z "$DOMAIN_INPUT" ] && info " $(text 135) " && return
|
||||||
|
|
||||||
|
local DOMAINS=()
|
||||||
|
custom_route_csv_to_array "$DOMAIN_INPUT" DOMAINS
|
||||||
|
local DOMAIN
|
||||||
|
for DOMAIN in "${DOMAINS[@]}"; do
|
||||||
|
DOMAIN=$(sed 's/。/./g' <<< "${DOMAIN,,}")
|
||||||
|
if [[ "$DOMAIN" =~ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?\.[a-z]{2,}$ ]]; then
|
||||||
|
VALIDATED_VALUES+=("$DOMAIN")
|
||||||
|
else
|
||||||
|
warning " $(text 161) "
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ "${#VALIDATED_VALUES[@]}" -eq 0 ] && warning " $(text 135) " && return
|
||||||
|
|
||||||
|
elif [ "$RULE_TYPE" = "rule_set" ]; then
|
||||||
|
# 输入规则集名称
|
||||||
|
reading " $(text 154) " RULESET_INPUT
|
||||||
|
[ -z "$RULESET_INPUT" ] && info " $(text 135) " && return
|
||||||
|
|
||||||
|
local RULESETS=()
|
||||||
|
custom_route_csv_to_array "$RULESET_INPUT" RULESETS
|
||||||
|
|
||||||
|
local RULE_NAME RETRY URL
|
||||||
|
for RULE_NAME in "${RULESETS[@]}"; do
|
||||||
|
RULE_NAME="${RULE_NAME,,}"
|
||||||
|
RULE_NAME=$(sed -E 's#^.*/##; s/\.srs$//I' <<< "$RULE_NAME")
|
||||||
|
[[ -n "$RULE_NAME" && ! "$RULE_NAME" =~ ^geo(site|ip)- ]] && RULE_NAME="geosite-${RULE_NAME}"
|
||||||
|
[ -z "$RULE_NAME" ] && continue
|
||||||
|
|
||||||
|
RETRY=3
|
||||||
|
URL=""
|
||||||
|
while [ $RETRY -gt 0 ]; do
|
||||||
|
URL=$(check_rule_set_exists "$RULE_NAME")
|
||||||
|
if [ -n "$URL" ]; then
|
||||||
|
VALIDATED_VALUES+=("$RULE_NAME")
|
||||||
|
RULE_SET_URLS+=("$URL")
|
||||||
|
break
|
||||||
|
else
|
||||||
|
((RETRY--))
|
||||||
|
if [ $RETRY -gt 0 ]; then
|
||||||
|
warning " $(text 156) "
|
||||||
|
reading " " RULE_NAME
|
||||||
|
RULE_NAME="${RULE_NAME,,}"
|
||||||
|
RULE_NAME=$(sed -E 's#^.*/##; s/\.srs$//I' <<< "$RULE_NAME")
|
||||||
|
[[ -n "$RULE_NAME" && ! "$RULE_NAME" =~ ^geo(site|ip)- ]] && RULE_NAME="geosite-${RULE_NAME}"
|
||||||
|
else
|
||||||
|
warning " $(text 156) "
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
[ "${#VALIDATED_VALUES[@]}" -eq 0 ] && warning " $(text 135) " && return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 自定义路由固定使用 warp-ep 出站
|
||||||
|
local OUTBOUND="warp-ep"
|
||||||
|
hint " $(text 155) "
|
||||||
|
|
||||||
|
# 初始化 JSON 文件(如果不存在)
|
||||||
|
if [ ! -s "$CUSTOM_FILE" ]; then
|
||||||
|
echo '{"route":{"rule_set":[],"rules":[]}}' | jq_exec '.' > "$CUSTOM_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local TMP_FILE="${CUSTOM_FILE}.tmp"
|
||||||
|
|
||||||
|
if [ "$RULE_TYPE" = "domain_suffix" ]; then
|
||||||
|
# 构建 domain_suffix 数组 JSON,先对输入本身去重
|
||||||
|
local DOMAINS_JSON
|
||||||
|
DOMAINS_JSON=$(printf '%s\n' "${VALIDATED_VALUES[@]}" | jq_exec -R . | jq_exec -s 'reduce .[] as $x ([]; if index($x) then . else . + [$x] end)')
|
||||||
|
|
||||||
|
# 合并到同一条 warp-ep route rule;rule_set 和 domain_suffix 共用一条规则
|
||||||
|
jq_exec --argjson domains "$DOMAINS_JSON" --arg out "$OUTBOUND" '
|
||||||
|
.route.rules = (.route.rules // []) |
|
||||||
|
(.route.rules | map(select((.outbound // $out) == $out) | .domain_suffix // []) | add // []) as $old_domains |
|
||||||
|
(.route.rules | map(select((.outbound // $out) == $out) | .rule_set // []) | add // []) as $old_sets |
|
||||||
|
(.route.rules | map(select((.outbound // $out) != $out))) as $others |
|
||||||
|
(($old_domains + $domains) | reduce .[] as $x ([]; if index($x) then . else . + [$x] end)) as $new_domains |
|
||||||
|
($old_sets | reduce .[] as $x ([]; if index($x) then . else . + [$x] end)) as $new_sets |
|
||||||
|
.route.rules = (
|
||||||
|
$others +
|
||||||
|
(if (($new_domains | length) + ($new_sets | length)) > 0 then
|
||||||
|
[((if ($new_sets | length) > 0 then {rule_set:$new_sets} else {} end)
|
||||||
|
+ (if ($new_domains | length) > 0 then {domain_suffix:$new_domains} else {} end)
|
||||||
|
+ {outbound:$out})]
|
||||||
|
else [] end)
|
||||||
|
)
|
||||||
|
' "$CUSTOM_FILE" > "$TMP_FILE" && mv "$TMP_FILE" "$CUSTOM_FILE"
|
||||||
|
|
||||||
|
elif [ "$RULE_TYPE" = "rule_set" ]; then
|
||||||
|
# 添加 rule_set 定义到 route.rule_set(去重)
|
||||||
|
for i in "${!VALIDATED_VALUES[@]}"; do
|
||||||
|
local RS_NAME="${VALIDATED_VALUES[$i]}"
|
||||||
|
local RS_URL="${RULE_SET_URLS[$i]}"
|
||||||
|
|
||||||
|
jq_exec --arg tag "$RS_NAME" --arg url "$RS_URL" '
|
||||||
|
.route.rule_set = (.route.rule_set // []) |
|
||||||
|
.route.rule_set |= (
|
||||||
|
if any(.[]; .tag == $tag) then .
|
||||||
|
else . + [{"tag": $tag, "type": "remote", "format": "binary", "url": $url}]
|
||||||
|
end
|
||||||
|
)
|
||||||
|
' "$CUSTOM_FILE" > "$TMP_FILE" && mv "$TMP_FILE" "$CUSTOM_FILE"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 构建 rule_set 名称数组,先对输入本身去重
|
||||||
|
local RS_NAMES_JSON
|
||||||
|
RS_NAMES_JSON=$(printf '%s\n' "${VALIDATED_VALUES[@]}" | jq_exec -R . | jq_exec -s 'reduce .[] as $x ([]; if index($x) then . else . + [$x] end)')
|
||||||
|
|
||||||
|
# 合并到同一条 warp-ep route rule;rule_set 和 domain_suffix 共用一条规则
|
||||||
|
jq_exec --argjson names "$RS_NAMES_JSON" --arg out "$OUTBOUND" '
|
||||||
|
.route.rules = (.route.rules // []) |
|
||||||
|
(.route.rules | map(select((.outbound // $out) == $out) | .domain_suffix // []) | add // []) as $old_domains |
|
||||||
|
(.route.rules | map(select((.outbound // $out) == $out) | .rule_set // []) | add // []) as $old_sets |
|
||||||
|
(.route.rules | map(select((.outbound // $out) != $out))) as $others |
|
||||||
|
($old_domains | reduce .[] as $x ([]; if index($x) then . else . + [$x] end)) as $new_domains |
|
||||||
|
(($old_sets + $names) | reduce .[] as $x ([]; if index($x) then . else . + [$x] end)) as $new_sets |
|
||||||
|
.route.rules = (
|
||||||
|
$others +
|
||||||
|
(if (($new_domains | length) + ($new_sets | length)) > 0 then
|
||||||
|
[((if ($new_sets | length) > 0 then {rule_set:$new_sets} else {} end)
|
||||||
|
+ (if ($new_domains | length) > 0 then {domain_suffix:$new_domains} else {} end)
|
||||||
|
+ {outbound:$out})]
|
||||||
|
else [] end)
|
||||||
|
)
|
||||||
|
' "$CUSTOM_FILE" > "$TMP_FILE" && mv "$TMP_FILE" "$CUSTOM_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
custom_route_compact_rules
|
||||||
|
|
||||||
|
info " $(text 157) "
|
||||||
|
hint " $(text 112) "
|
||||||
|
cmd_systemctl restart sing-box
|
||||||
|
sleep 2
|
||||||
|
cmd_systemctl status sing-box &>/dev/null && \
|
||||||
|
info "\n Sing-box $(text 28) $(text 37) \n" || \
|
||||||
|
warning "\n Sing-box $(text 27) $(text 38) \n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 将逗号分隔输入转为数组:支持半角/全角逗号、顿号、分号、竖线;不使用 IFS
|
||||||
|
custom_route_csv_to_array() {
|
||||||
|
local _input="$1"
|
||||||
|
local -n _out_array="$2"
|
||||||
|
_out_array=()
|
||||||
|
|
||||||
|
mapfile -t _out_array < <(
|
||||||
|
printf '%s\n' "$_input" |
|
||||||
|
sed 's/\x1b\[[0-9;?]*[A-Za-z]//g; s/\^\[\[[0-9;?]*[A-Za-z]//g; s/[,、;;|]/,/g; s/[[:space:]]//g; s/,/\n/g; /^$/d'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 输出展开后的自定义路由项,每个 domain_suffix / rule_set 数组元素单独一行
|
||||||
|
custom_route_items_json() {
|
||||||
|
local CUSTOM_FILE="${WORK_DIR}/conf/08_custom_route.json"
|
||||||
|
jq_exec -c '
|
||||||
|
[.route.rules[]?] as $rules |
|
||||||
|
reduce range(0; ($rules | length)) as $i ([];
|
||||||
|
($rules[$i]) as $r |
|
||||||
|
.
|
||||||
|
+ [($r.rule_set[]? | {rule_index:$i,type:"rule_set",match:.,outbound:($r.outbound // "warp-ep")})]
|
||||||
|
+ [($r.domain_suffix[]? | {rule_index:$i,type:"domain_suffix",match:.,outbound:($r.outbound // "warp-ep")})]
|
||||||
|
+ (if (($r.rule_set? == null) and ($r.domain_suffix? == null)) then [{rule_index:$i,type:"unknown",match:"N/A",outbound:($r.outbound // "warp-ep")}] else [] end)
|
||||||
|
) | .[]
|
||||||
|
' "$CUSTOM_FILE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# 查看自定义路由规则
|
||||||
|
custom_route_view() {
|
||||||
|
local CUSTOM_FILE="${WORK_DIR}/conf/08_custom_route.json"
|
||||||
|
|
||||||
|
if [ ! -s "$CUSTOM_FILE" ]; then
|
||||||
|
hint " $(text 158) "
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local ROUTE_ITEMS=()
|
||||||
|
mapfile -t ROUTE_ITEMS < <(custom_route_items_json)
|
||||||
|
|
||||||
|
if [ "${#ROUTE_ITEMS[@]}" -eq 0 ]; then
|
||||||
|
hint " $(text 158) "
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
hint "\n $(text 163) \n"
|
||||||
|
printf " %-4s %-16s %s\n" "#" "Type" "Match"
|
||||||
|
printf " %-4s %-16s %s\n" "---" "---------------" "---------------------------------------"
|
||||||
|
|
||||||
|
local IDX=0
|
||||||
|
local ITEM TYPE MATCH
|
||||||
|
for ITEM in "${ROUTE_ITEMS[@]}"; do
|
||||||
|
((IDX++)) || true
|
||||||
|
TYPE=$(jq_exec -r '.type' <<< "$ITEM")
|
||||||
|
MATCH=$(jq_exec -r '.match' <<< "$ITEM")
|
||||||
|
printf " %-4s %-16s %s\n" "$IDX" "$TYPE" "$MATCH"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 删除自定义路由规则:按展开后的单项编号删除,支持删除数组里的某个元素
|
||||||
|
custom_route_delete() {
|
||||||
|
local CUSTOM_FILE="${WORK_DIR}/conf/08_custom_route.json"
|
||||||
|
|
||||||
|
custom_route_view || return
|
||||||
|
|
||||||
|
local ROUTE_ITEMS=()
|
||||||
|
mapfile -t ROUTE_ITEMS < <(custom_route_items_json)
|
||||||
|
[ "${#ROUTE_ITEMS[@]}" -eq 0 ] && info " $(text 135) " && return
|
||||||
|
|
||||||
|
reading " $(text 159) " DELETE_INPUT
|
||||||
|
[ -z "$DELETE_INPUT" ] && info " $(text 135) " && return
|
||||||
|
|
||||||
|
local DELETE_NUMS=()
|
||||||
|
custom_route_csv_to_array "$DELETE_INPUT" DELETE_NUMS
|
||||||
|
|
||||||
|
local DELETE_ITEM_LINES=()
|
||||||
|
local NUM
|
||||||
|
for NUM in "${DELETE_NUMS[@]}"; do
|
||||||
|
NUM=$(sed 's/[^0-9]//g' <<< "$NUM")
|
||||||
|
if [[ "$NUM" =~ ^[0-9]+$ ]] && [ "$NUM" -ge 1 ] && [ "$NUM" -le "${#ROUTE_ITEMS[@]}" ]; then
|
||||||
|
DELETE_ITEM_LINES+=("${ROUTE_ITEMS[$((NUM - 1))]}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
[ "${#DELETE_ITEM_LINES[@]}" -eq 0 ] && info " $(text 135) " && return
|
||||||
|
|
||||||
|
local DELETE_ITEMS_JSON TMP_FILE
|
||||||
|
DELETE_ITEMS_JSON=$(printf '%s\n' "${DELETE_ITEM_LINES[@]}" | jq_exec -s 'unique_by(.rule_index, .type, .match)')
|
||||||
|
TMP_FILE="${CUSTOM_FILE}.tmp"
|
||||||
|
|
||||||
|
# 只删除被选中的数组元素;数组清空后才删除整条 route rule
|
||||||
|
jq_exec --argjson del "$DELETE_ITEMS_JSON" '
|
||||||
|
.route.rules |= (
|
||||||
|
[.[]?] as $rules |
|
||||||
|
reduce range(0; ($rules | length)) as $idx ([];
|
||||||
|
($rules[$idx]) as $rule |
|
||||||
|
($del | map(select(.rule_index == $idx and .type == "domain_suffix") | .match)) as $remove_domains |
|
||||||
|
($del | map(select(.rule_index == $idx and .type == "rule_set") | .match)) as $remove_sets |
|
||||||
|
($rule
|
||||||
|
| if .domain_suffix? != null then .domain_suffix = ([.domain_suffix[]? as $v | select(($remove_domains | index($v)) | not) | $v]) else . end
|
||||||
|
| if .rule_set? != null then .rule_set = ([.rule_set[]? as $v | select(($remove_sets | index($v)) | not) | $v]) else . end
|
||||||
|
| if ((.domain_suffix // []) | length) == 0 then del(.domain_suffix) else . end
|
||||||
|
| if ((.rule_set // []) | length) == 0 then del(.rule_set) else . end
|
||||||
|
) as $new_rule |
|
||||||
|
if (($new_rule.domain_suffix? != null) or ($new_rule.rule_set? != null)) then
|
||||||
|
. + [$new_rule]
|
||||||
|
elif ($del | any(.rule_index == $idx and .type == "unknown")) then
|
||||||
|
.
|
||||||
|
else
|
||||||
|
. + [$new_rule]
|
||||||
|
end
|
||||||
|
)
|
||||||
|
)
|
||||||
|
' "$CUSTOM_FILE" > "$TMP_FILE" && mv "$TMP_FILE" "$CUSTOM_FILE"
|
||||||
|
|
||||||
|
custom_route_compact_rules
|
||||||
|
|
||||||
|
# 清理孤立的 rule_set 定义:只保留仍被 rules 引用的 rule_set
|
||||||
|
jq_exec '
|
||||||
|
(.route.rules | [.[]? | .rule_set // [] | .[]] | unique) as $used |
|
||||||
|
.route.rule_set |= [.[]? | select(.tag as $t | $used | index($t) | not | not)]
|
||||||
|
' "$CUSTOM_FILE" > "$TMP_FILE" && mv "$TMP_FILE" "$CUSTOM_FILE"
|
||||||
|
|
||||||
|
# 如果没有任何规则了,删除文件
|
||||||
|
local REMAINING
|
||||||
|
REMAINING=$(jq_exec '.route.rules | length' "$CUSTOM_FILE" 2>/dev/null)
|
||||||
|
if [ "${REMAINING:-0}" -eq 0 ]; then
|
||||||
|
rm -f "$CUSTOM_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info " $(text 160) "
|
||||||
|
hint " $(text 112) "
|
||||||
|
cmd_systemctl restart sing-box
|
||||||
|
sleep 2
|
||||||
|
cmd_systemctl status sing-box &>/dev/null && \
|
||||||
|
info "\n Sing-box $(text 28) $(text 37) \n" || \
|
||||||
|
warning "\n Sing-box $(text 27) $(text 38) \n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 自定义路由规则子菜单
|
||||||
|
custom_route_menu() {
|
||||||
|
while true; do
|
||||||
|
CUSTOM_ROUTE_COUNT=$(custom_route_count)
|
||||||
|
hint "\n $(text 150) \n"
|
||||||
|
hint " $(text 151) "
|
||||||
|
hint ""
|
||||||
|
reading " $(text 24) " CUSTOM_ROUTE_CHOICE
|
||||||
|
|
||||||
|
case "$CUSTOM_ROUTE_CHOICE" in
|
||||||
|
1 ) custom_route_add ;;
|
||||||
|
2 ) custom_route_view ;;
|
||||||
|
3 ) custom_route_delete ;;
|
||||||
|
0 ) return ;;
|
||||||
|
* ) info " $(text 135) " && return ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===================== 自定义路由规则 END =====================
|
||||||
|
|
||||||
# 输入 Reality 密钥
|
# 输入 Reality 密钥
|
||||||
input_reality_key() {
|
input_reality_key() {
|
||||||
[[ "$NONINTERACTIVE_INSTALL" != 'noninteractive_install' && "$IS_FAST_INSTALL" != 'is_fast_install' ]] && [ -z "$REALITY_PRIVATE" ] && reading "\n ${TOTAL_STEPS:+(${STEP_NUM}/${TOTAL_STEPS}) }$(text 70) " REALITY_PRIVATE
|
[[ "$NONINTERACTIVE_INSTALL" != 'noninteractive_install' && "$IS_FAST_INSTALL" != 'is_fast_install' ]] && [ -z "$REALITY_PRIVATE" ] && reading "\n ${TOTAL_STEPS:+(${STEP_NUM}/${TOTAL_STEPS}) }$(text 70) " REALITY_PRIVATE
|
||||||
|
|||||||
Reference in New Issue
Block a user