Debian 一键搭建 sing-box SOCKS5 双栈分流节点(附 sb 菜单管理脚本)
一、这篇讲什么
用 sing-box 搭一个带 IPv4 / IPv6 分流的 SOCKS5 代理,再配一个 sb 菜单管理脚本,一条命令呼出菜单,安装、改规则、换端口密码、看日志全在菜单里完成,不用手写 JSON 配置。
- 双栈监听:SOCKS5 入站监听
::,IPv4 和 IPv6 客户端都能连进来。 - 按域名分流:Netflix、YouTube 等按域名集合强制走 IPv4 或 IPv6 出口,规则写法如
ipv4(geosite:netflix),随时增删改。 sb菜单脚本:安装、卸载、启动、停止、重启、运行状态、分流设置、更改节点信息、查看日志、查看节点信息,共 10 个菜单项,彩色界面。
适用环境:Debian 系统,root 用户运行。分流功能要求 sing-box ≥ 1.12(1.12 移除了旧的 geosite 字段,本脚本改用远程 rule_set 实现,旧版本 sing-box 不可用)。
二、安装步骤
1. 把文末附录的完整脚本保存为 /root/sb.sh(nano /root/sb.sh,粘贴保存),然后运行:
bash /root/sb.sh2. 在菜单里选 1(安装 sing-box),按提示输入:
- 监听端口:1–65535,必须手动输入,没有默认值(下面示例用
1080,请换成你自己的端口)。 - 用户名:默认
socks5,可直接回车沿用。 - 密码:直接回车自动生成 16 位随机密码,或自己输入。
3. 安装过程会自动完成:安装 / 升级 sing-box(≥ 1.12)、创建 systemd 服务、停用可能冲突的旧 danted、写入默认分流规则(Netflix、YouTube 走 IPv4)、生成配置、检查配置、启动服务。
4. 装完后直接输入 sb 就能呼出菜单(快捷键装在 /usr/local/bin/sb)。新版脚本带自同步:以后直接运行脚本文件时,如果 sb 快捷键不是最新会自动同步,不会再出现新旧菜单不一致。
三、菜单一览
菜单顶端栏只显示两行:运行状态(运行中 / 已停止 / 未安装,附 sing-box 版本)和当前分流规则。端口、用户名、密码不会显示在顶端,要到「10. 查看节点信息」里看。
- 1. 安装 sing-box:安装或升级 sing-box(≥ 1.12)、创建 systemd 服务、写入默认分流规则并启动服务。
- 2. 卸载 sing-box:删除 sing-box 程序、二进制和
/etc/sing-box下全部配置(含节点信息和分流规则),操作前会二次确认。 - 3. 启动服务:
systemctl start sing-box。 - 4. 停止服务:
systemctl stop sing-box。 - 5. 重启服务:
systemctl restart sing-box。 - 6. 查看运行状态:sing-box 版本、进程 PID、端口监听情况、用户名、分流规则、最近 8 行服务日志。
- 7. 分流设置:添加、修改、删除、清空分流规则,或查看规则说明;每次改动会自动重新生成配置、经
sing-box check检查通过后重启生效。 - 8. 更改节点信息:修改端口、用户名、密码;修改后自动重新生成配置并生效(密码在菜单里只显示为
********)。 - 9. 查看日志:三部分——当前分流规则、实时连接快照(当前经 sing-box 的对外连接统计)、最近 30 行服务日志。
- 10. 查看节点信息:SOCKS5 类型、本机公网 IPv4 / IPv6、端口、用户名、密码、当前分流规则,以及客户端填写说明。
四、分流规则写法
格式:<出口>(<匹配目标>)
- 出口:
ipv4表示命中的流量强制走 IPv4 出口;ipv6表示强制走 IPv6 出口。 - 匹配目标:
geosite:分类名:官方维护的域名集合,如netflix、youtube、google(分类名不区分大小写);domain:域名:指定域名,含其全部子域名;- 裸域名:和
domain:一样,domain:前缀可省略。
示例:
ipv4(geosite:netflix) # Netflix 走 IPv4
ipv6(geosite:youtube) # YouTube 走 IPv6
ipv4(domain:example.com) # example.com 及其子域名走 IPv4
ipv6(bilibili.com) # B 站走 IPv6规则按顺序匹配,命中即走对应出口;都没命中的流量走默认直连。技术实现:geosite 走远程 rule_set(https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-<分类>.srs),分流出口是 direct-v4 / direct-v6,配合 domain_resolver 策略 ipv4_only / ipv6_only 强制解析对应版本的 IP。
五、客户端配置
- 类型:SOCKS5。
- 服务器:菜单第 10 项「查看节点信息」里有本机公网 IPv4 和 IPv6,填任意一个。
- 端口 / 用户名 / 密码:同样在第 10 项里查看(示例端口
1080,换成你自己的)。 - 必须开启「通过代理解析 DNS」(远程解析):客户端如果自己在本地把域名解析成 IP 再发给代理,服务端就看不到域名,分流规则对这类流量不生效。
用 curl 测试分流时要用 socks5h://(h 表示域名交给代理解析),而不是 socks5://:
curl -x socks5h://用户名:密码@服务器IP:1080 https://ifconfig.me
# 用户名或密码含 @ : / 等特殊字符时,需要先做 URL 编码六、常用操作
- 改端口 / 用户名 / 密码:菜单
8。修改后脚本自动重新生成/etc/sing-box/config.json,经sing-box check检查通过后重启服务生效;若检查失败,本次修改不应用,服务保持原配置运行。 - 改分流规则:菜单
7,添加 / 修改 / 删除 / 清空,或选5看规则说明。规则保存在/etc/sing-box/shunt.rules,每改一次自动走一遍”生成配置 → 检查 → 重启生效”流程。 - 看日志:菜单
9,一次看清当前分流规则、实时连接快照、最近 30 行服务日志,排查分流是否命中、分流是否生效先看这里。 - 看运行状态:菜单
6,确认版本、进程、端口监听是否正常。
七、注意事项
geosite:后面的分类名写错(比如拼错成netflixx),会导致sing-box check配置检查失败,本次修改不生效,服务继续用旧配置运行。用菜单9查看日志确认服务仍在运行,然后回菜单7修正规则。- 卸载用菜单
2,会删掉 sing-box 程序、二进制和/etc/sing-box下的全部配置(含节点信息和分流规则),操作不可恢复,谨慎执行。 - 关键文件位置:配置
/etc/sing-box/config.json、节点信息/etc/sing-box/node.info(600 权限)、分流规则/etc/sing-box/shunt.rules、快捷键/usr/local/bin/sb。 - 安装时如果旧版 danted 占用了端口,脚本会自动停用 danted,避免端口冲突。
- SOCKS 入站监听
::即双栈;如果机器内核开了net.ipv6.bindv6only=1,IPv4 客户端会连不上,可用ss -tln检查监听情况。
八、附:完整脚本 sb-socks.sh
保存为 /root/sb.sh,chmod +x /root/sb.sh 后用 bash /root/sb.sh 运行:
#!/bin/bash
# ============================================================
# sb -- sing-box SOCKS5 分流管理菜单
#
# 菜单:
# 1. 安装 sing-box 2. 卸载 sing-box
# 3. 启动服务 4. 停止服务 5. 重启服务
# 6. 查看运行状态(菜单顶端同时显示状态/分流信息)
# 7. 分流设置(格式如 ipv4(geosite:netflix))
# 8. 更改节点信息(端口 / 用户名 / 密码)
# 9. 查看日志(含分流信息) 10. 查看节点信息
# 0. 退出
#
# 安装后直接输入 sb 即可呼出本菜单。
#
# 分流规则写法:
# ipv4(geosite:netflix) Netflix 走 IPv4
# ipv6(geosite:youtube) YouTube 走 IPv6
# ipv4(domain:example.com) 指定域名走 IPv4(含其全部子域名)
# ipv6(example.com) 裸域名同样按子域名匹配,走 IPv6
# ============================================================
SB_DIR="/etc/sing-box"
CONF="$SB_DIR/config.json"
NODE_INFO="$SB_DIR/node.info"
RULES_FILE="$SB_DIR/shunt.rules"
GEO_BASE_URL="https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set"
DEF_PORT="1080"
DEF_USER="socks5"
# ------------------------- 颜色 -------------------------
# 终端美化:标题青色、成功绿色、错误红色、警告黄色、序号黄色
if [ -t 1 ]; then
C_RED=$'\033[0;31m'
C_GREEN=$'\033[0;32m'
C_YELLOW=$'\033[0;33m'
C_CYAN=$'\033[0;36m'
C_BOLD=$'\033[1m'
C_RESET=$'\033[0m'
else
C_RED=''; C_GREEN=''; C_YELLOW=''; C_CYAN=''; C_BOLD=''; C_RESET=''
fi
# ------------------------- 基础工具 -------------------------
pause() { read -rp "按回车返回..." _; }
# JSON 字符串转义(处理 " 和 \)
json_escape() {
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}
trim() {
printf '%s' "$1" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'
}
# ------------------------- 节点信息 -------------------------
load_node() {
PORT="$DEF_PORT"
USERNAME="$DEF_USER"
PASSWORD=""
[ -f "$NODE_INFO" ] || return 0
while IFS='=' read -r k v || [ -n "$k" ]; do
case "$k" in
PORT) PORT="$v" ;;
USERNAME) USERNAME="$v" ;;
PASSWORD) PASSWORD="$v" ;;
esac
done < "$NODE_INFO"
}
save_node() {
mkdir -p "$SB_DIR"
{
printf 'PORT=%s\n' "$PORT"
printf 'USERNAME=%s\n' "$USERNAME"
printf 'PASSWORD=%s\n' "$PASSWORD"
} > "$NODE_INFO"
chmod 600 "$NODE_INFO"
}
# ------------------------- sing-box -------------------------
sb_version() {
command -v sing-box >/dev/null 2>&1 || return 1
sing-box version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1
}
# ver_ge 需要版本 当前版本:当前 >= 需要 时返回 0
ver_ge() {
[ "$1" = "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -1)" ]
}
is_running() { pgrep -x sing-box >/dev/null 2>&1; }
need_installed() {
if ! command -v sing-box >/dev/null 2>&1; then
echo "sing-box 未安装,请先选「1. 安装 sing-box」"
return 1
fi
return 0
}
svc_exists() {
[ -f /etc/systemd/system/sing-box.service ] || [ -f /lib/systemd/system/sing-box.service ]
}
# ------------------------- 配置生成 -------------------------
# 根据节点信息 + 分流规则生成 $CONF,成功返回 0
build_config() {
load_node
[ -n "$PASSWORD" ] || { echo "错误:密码为空,请先在「8. 更改节点信息」里设置密码"; return 1; }
if ! [[ "$PORT" =~ ^[0-9]+$ ]] || [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then
echo "错误:端口无效:$PORT"; return 1
fi
local rule_sets="" rules=""
if [ -f "$RULES_FILE" ]; then
while IFS= read -r raw || [ -n "$raw" ]; do
local line
line="$(trim "$raw")"
[ -z "$line" ] && continue
case "$line" in \#*) continue ;; esac
if [[ "$line" =~ ^(ipv4|ipv6)\((.+)\)$ ]]; then
local ver="${BASH_REMATCH[1]}"
local inner
inner="$(trim "${BASH_REMATCH[2]}")"
local outbound="direct-v4"
[ "$ver" = "ipv6" ] && outbound="direct-v6"
local matcher=""
if [[ "$inner" =~ ^[Gg][Ee][Oo][Ss][Ii][Tt][Ee]:(.+)$ ]]; then
local cat
cat="$(printf '%s' "${BASH_REMATCH[1]}" | tr '[:upper:]' '[:lower:]')"
cat="$(trim "$cat")"
if [ -z "$cat" ]; then
echo "警告:跳过空的 geosite 规则:$line"
continue
fi
local tag url rs
tag="rs-${ver}-geosite-${cat}"
url="${GEO_BASE_URL}/geosite-${cat}.srs"
rs="$(printf '{"type": "remote", "tag": "%s", "format": "binary", "url": "%s", "download_detour": "direct"}' "$tag" "$url")"
if [ -z "$rule_sets" ]; then rule_sets="$rs"; else rule_sets="$rule_sets, $rs"; fi
matcher="$(printf '"rule_set": ["%s"]' "$tag")"
elif [[ "$inner" =~ ^[Dd][Oo][Mm][Aa][Ii][Nn]:(.+)$ ]]; then
local dom
dom="$(json_escape "$(trim "${BASH_REMATCH[1]}")")"
matcher="$(printf '"domain_suffix": ["%s"]' "$dom")"
else
local dom
dom="$(json_escape "$inner")"
matcher="$(printf '"domain_suffix": ["%s"]' "$dom")"
fi
local rule
rule="$(printf '{"action": "route", %s, "outbound": "%s"}' "$matcher" "$outbound")"
if [ -z "$rules" ]; then rules="$rule"; else rules="$rules, $rule"; fi
else
echo "警告:跳过无法识别的规则:$line"
fi
done < "$RULES_FILE"
fi
local esc_user esc_pass
esc_user="$(json_escape "$USERNAME")"
esc_pass="$(json_escape "$PASSWORD")"
mkdir -p "$SB_DIR"
{
printf '{\n'
printf ' "log": { "level": "info" },\n'
printf ' "dns": {\n'
printf ' "servers": [\n'
printf ' { "type": "local", "tag": "dns-local" }\n'
printf ' ]\n'
printf ' },\n'
printf ' "inbounds": [\n'
printf ' {\n'
printf ' "type": "socks",\n'
printf ' "tag": "socks-in",\n'
printf ' "listen": "::",\n'
printf ' "listen_port": %s,\n' "$PORT"
printf ' "users": [\n'
printf ' { "username": "%s", "password": "%s" }\n' "$esc_user" "$esc_pass"
printf ' }\n'
printf ' ],\n'
printf ' "outbounds": [\n'
printf ' { "type": "direct", "tag": "direct" },\n'
printf ' {\n'
printf ' "type": "direct",\n'
printf ' "tag": "direct-v4",\n'
printf ' "domain_resolver": { "server": "dns-local", "strategy": "ipv4_only" }\n'
printf ' },\n'
printf ' {\n'
printf ' "type": "direct",\n'
printf ' "tag": "direct-v6",\n'
printf ' "domain_resolver": { "server": "dns-local", "strategy": "ipv6_only" }\n'
printf ' }\n'
printf ' ],\n'
printf ' "route": {\n'
printf ' "rule_set": [\n'
printf ' %s\n' "$rule_sets"
printf ' ],\n'
printf ' "rules": [\n'
printf ' %s\n' "$rules"
printf ' ],\n'
printf ' "final": "direct",\n'
printf ' "auto_detect_interface": true\n'
printf ' }\n'
printf '}\n'
} > "$CONF"
chmod 600 "$CONF"
echo "配置已生成:$CONF"
}
# 生成配置 + 检查 + 生效(运行中则重启服务)
apply_config() {
build_config || return 1
if ! command -v sing-box >/dev/null 2>&1; then
echo "sing-box 未安装,配置已保存,安装完成后自动生效"
return 0
fi
local check_out
if ! check_out="$(sing-box check -c "$CONF" 2>&1)"; then
echo "配置检查失败,未应用:"
printf '%s\n' "$check_out"
return 1
fi
echo "配置检查通过"
if is_running; then
systemctl restart sing-box
sleep 1
if is_running; then
echo "服务已重启,新配置生效"
else
echo "服务重启后未运行,查看日志:journalctl -u sing-box -n 30"
return 1
fi
else
echo "配置已保存(服务未运行,启动后生效)"
fi
}
# ------------------------- 安装 / 卸载 -------------------------
ensure_service() {
if svc_exists; then
return 0
fi
local bin
bin="$(command -v sing-box)" || { echo "找不到 sing-box 可执行文件"; return 1; }
cat > /etc/systemd/system/sing-box.service <<EOF
[Unit]
Description=sing-box service
After=network.target
[Service]
ExecStart=${bin} run -c ${CONF}
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
echo "已创建 systemd 服务:sing-box.service"
}
install_sb_cmd() {
local me
me="$(readlink -f "$0" 2>/dev/null || printf '%s' "$0")"
if [ "$me" != "/usr/local/bin/sb" ]; then
cp -f "$me" /usr/local/bin/sb
chmod +x /usr/local/bin/sb
hash -r 2>/dev/null
echo "快捷键已安装:以后直接输入 sb 即可呼出菜单"
fi
}
do_install() {
printf '%s---------- 安装 sing-box ----------%s\n' "$C_BOLD" "$C_RESET"
command -v curl >/dev/null 2>&1 || {
echo "正在安装 curl..."
apt update && apt install -y curl || { echo "curl 安装失败"; return 1; }
}
# 1. 安装 / 升级 sing-box(分流功能需要 >= 1.12)
local cur_ver
cur_ver="$(sb_version || true)"
if [ -n "$cur_ver" ]; then
echo "检测到已安装版本:$cur_ver"
if ver_ge "1.12.0" "$cur_ver"; then
echo "版本满足要求(>= 1.12),跳过安装"
else
echo "版本过旧(需要 >= 1.12),正在升级..."
bash <(curl -fsSL https://sing-box.app/install.sh) || { echo "升级失败,请检查网络"; return 1; }
fi
else
echo "正在安装 sing-box(官方脚本)..."
bash <(curl -fsSL https://sing-box.app/install.sh) || { echo "安装失败,请检查网络"; return 1; }
fi
echo "当前版本:$(sb_version)"
# 2. 确保 systemd 服务存在
ensure_service || return 1
# 3. 停用旧的 danted,避免 1080 端口冲突
if systemctl cat danted >/dev/null 2>&1; then
systemctl stop danted 2>/dev/null
systemctl disable danted 2>/dev/null
echo "已停用旧的 danted(释放端口)"
fi
# 4. 安装 sb 快捷键
install_sb_cmd
# 5. 节点信息(端口必须由用户输入,不设默认值)
load_node
local input
while true; do
read -rp "监听端口(1-65535): " input
if [[ "$input" =~ ^[0-9]+$ ]] && [ "$input" -ge 1 ] && [ "$input" -le 65535 ]; then
PORT="$input"
break
fi
echo "端口无效,请重新输入"
done
read -rp "用户名 [${USERNAME}]: " input
[ -n "$input" ] && USERNAME="$input"
read -rsp "密码(留空则自动生成强密码): " input; echo
if [ -z "$input" ]; then
PASSWORD="$(tr -dc 'A-Za-z0-9' </dev/urandom | head -c 16)"
echo "已生成随机密码:$PASSWORD"
echo "请记好,客户端连接需要用它"
else
PASSWORD="$input"
fi
save_node
# 6. 默认分流规则
if [ ! -s "$RULES_FILE" ]; then
cat > "$RULES_FILE" <<'EOF'
ipv4(geosite:netflix)
ipv4(geosite:youtube)
EOF
echo "已写入默认分流规则(Netflix / YouTube 走 IPv4),可在「分流设置」中修改"
fi
# 7. 生成配置、检查、启动
apply_config || return 1
systemctl enable sing-box >/dev/null 2>&1
if ! is_running; then
systemctl start sing-box
sleep 1
fi
if is_running; then
echo ""
echo "安装完成,服务运行中"
echo "客户端填写:服务器=本机公网IP 端口=$PORT 用户名=$USERNAME 类型=SOCKS5"
echo "注意:客户端需开启「通过代理解析 DNS」(远程解析),否则分流规则不生效"
else
echo "服务启动失败,查看日志:journalctl -u sing-box -n 30"
return 1
fi
}
do_uninstall() {
printf '%s---------- 卸载 sing-box ----------%s\n' "$C_BOLD" "$C_RESET"
local c
read -rp "将删除 sing-box 程序、二进制、全部配置,确定吗?(y/N): " c
case "$c" in
y|Y) ;;
*) echo "已取消"; return 0 ;;
esac
systemctl stop sing-box 2>/dev/null
systemctl disable sing-box 2>/dev/null
rm -f /etc/systemd/system/sing-box.service /lib/systemd/system/sing-box.service
systemctl daemon-reload 2>/dev/null
local bin
bin="$(command -v sing-box 2>/dev/null || true)"
[ -n "$bin" ] && rm -f "$bin"
rm -rf "$SB_DIR"
rm -f /usr/local/bin/sb
echo "卸载完成"
}
# ------------------------- 状态显示 -------------------------
list_rules() {
if [ ! -s "$RULES_FILE" ]; then
echo " (无,全部直连)"
return 0
fi
local n=0 line
while IFS= read -r line || [ -n "$line" ]; do
line="$(trim "$line")"
[ -z "$line" ] && continue
case "$line" in \#*) continue ;; esac
n=$((n + 1))
echo " [$n] $line"
done < "$RULES_FILE"
[ "$n" -eq 0 ] && echo " (无,全部直连)"
}
# 菜单顶端 compact 状态栏
show_header() {
load_node
local st st_color ver
if ! command -v sing-box >/dev/null 2>&1; then
st="未安装"; st_color="$C_YELLOW"
elif is_running; then
st="运行中"; st_color="$C_GREEN"
else
st="已停止"; st_color="$C_RED"
fi
ver="$(sb_version || echo "-")"
echo "=============================================="
printf ' %s%ssb · sing-box SOCKS5 分流管理菜单%s\n' "$C_BOLD" "$C_CYAN" "$C_RESET"
echo "----------------------------------------------"
printf ' 状态:%s%s%s(sing-box %s)\n' "$st_color" "$st" "$C_RESET" "$ver"
printf ' 分流:'
if [ -s "$RULES_FILE" ]; then
local r
r="$(grep -v '^[[:space:]]*$' "$RULES_FILE" | grep -v '^[[:space:]]*#' | tr '\n' ' ' | sed 's/ *$//')"
if [ -n "$r" ]; then printf '%s%s%s\n' "$C_GREEN" "$r" "$C_RESET"; else echo "无(全部直连)"; fi
else
echo "无(全部直连)"
fi
echo "=============================================="
}
show_status() {
load_node
echo "---------- 运行状态 ----------"
echo "sing-box 版本:$(sb_version || echo '未安装')"
if is_running; then
echo "服务状态:运行中(PID: $(pgrep -x sing-box | tr '\n' ' '))"
else
echo "服务状态:未运行"
fi
if ss -tln 2>/dev/null | grep -q ":${PORT} "; then
echo "端口监听:${PORT} 正常"
else
echo "端口监听:${PORT} 未监听"
fi
echo "用户名:$USERNAME"
echo "分流规则:"
list_rules
echo "---------- 最近日志 ----------"
journalctl -u sing-box --no-pager -n 8 2>/dev/null | tail -8 || echo "(无日志)"
}
# 查看日志(含分流信息:当前规则 + 实时连接 + 服务日志)
view_logs() {
load_node
echo ""
printf '%s---------- 查看日志 ----------%s\n' "$C_BOLD" "$C_RESET"
printf '%s【当前分流规则】%s\n' "$C_CYAN" "$C_RESET"
list_rules
echo ""
printf '%s【实时连接快照】%s\n' "$C_CYAN" "$C_RESET"
if is_running; then
local conns
conns="$(ss -tnp 2>/dev/null | grep 'sing-box' | awk '{print $5}' | grep -v '^127\.' | grep -v '^\[::1\]' | sort | uniq -c | sort -rn | head -20)"
if [ -n "$conns" ]; then
printf '%s\n' "$conns" | while read -r cnt dst; do
printf ' %sx %s\n' "$cnt" "$dst"
done
else
echo " 当前无对外活动连接"
fi
else
echo " 服务未运行"
fi
echo ""
printf '%s【服务日志(最近 30 行)】%s\n' "$C_CYAN" "$C_RESET"
journalctl -u sing-box --no-pager -n 30 2>/dev/null || echo "(无日志)"
pause
}
# 查看节点信息(连接客户端所需的全部信息)
view_node_info() {
load_node
echo ""
printf '%s---------- 查看节点信息 ----------%s\n' "$C_BOLD" "$C_RESET"
printf '%s分流规则:%s\n' "$C_CYAN" "$C_RESET"
list_rules
echo ""
echo "客户端填写:类型 SOCKS5,服务器填上面任一 IP,端口/用户名/密码如上,"
echo "并开启「通过代理解析 DNS」(远程解析),否则分流规则不生效。"
pause
}
# ------------------------- 分流设置 -------------------------
shunt_menu() {
while true; do
echo ""
printf '%s---------- 分流设置 ----------%s\n' "$C_BOLD" "$C_RESET"
echo "当前规则:"
list_rules
echo "------------------------------"
printf '%s格式:ipv4(geosite:netflix) / ipv6(domain:example.com)%s\n' "$C_CYAN" "$C_RESET"
printf '%s 选 5 查看详细规则说明%s\n' "$C_CYAN" "$C_RESET"
printf ' %s1.%s 添加规则\n' "$C_YELLOW" "$C_RESET"
printf ' %s2.%s 修改规则\n' "$C_YELLOW" "$C_RESET"
printf ' %s3.%s 删除规则\n' "$C_YELLOW" "$C_RESET"
printf ' %s4.%s 清空规则\n' "$C_YELLOW" "$C_RESET"
printf ' %s5.%s 规则说明\n' "$C_YELLOW" "$C_RESET"
printf ' %s0.%s 返回\n' "$C_YELLOW" "$C_RESET"
local c
read -rp "请选择: " c
case "$c" in
1) shunt_add ;;
2) shunt_edit ;;
3) shunt_del ;;
4) shunt_clear ;;
5) shunt_help ;;
0) return 0 ;;
*) echo "无效选择" ;;
esac
done
}
shunt_add() {
local rule
read -rp "输入规则(如 ipv4(geosite:netflix)): " rule
rule="$(trim "$rule")"
if [[ "$rule" =~ ^(ipv4|ipv6)\(.+\)$ ]]; then
mkdir -p "$SB_DIR"
touch "$RULES_FILE"
if grep -qxF "$rule" "$RULES_FILE" 2>/dev/null; then
echo "规则已存在"
else
printf '%s\n' "$rule" >> "$RULES_FILE"
echo "已添加:$rule"
apply_config
fi
else
echo "格式错误!应为 ipv4(...) 或 ipv6(...),例如:ipv4(geosite:netflix)"
fi
pause
}
shunt_edit() {
[ -s "$RULES_FILE" ] || { echo "没有可修改的规则"; pause; return 0; }
local -a items=()
local line
while IFS= read -r line || [ -n "$line" ]; do
line="$(trim "$line")"
[ -z "$line" ] && continue
case "$line" in \#*) continue ;; esac
items+=("$line")
done < "$RULES_FILE"
[ "${#items[@]}" -eq 0 ] && { echo "没有可修改的规则"; pause; return 0; }
local i
for i in "${!items[@]}"; do
echo " [$((i + 1))] ${items[$i]}"
done
local n
read -rp "输入要修改的序号(0 取消): " n
if [[ "$n" =~ ^[0-9]+$ ]] && [ "$n" -ge 1 ] && [ "$n" -le "${#items[@]}" ]; then
local old="${items[$((n - 1))]}"
echo "当前规则:$old"
local new
read -rp "输入新规则: " new
new="$(trim "$new")"
if [[ "$new" =~ ^(ipv4|ipv6)\(.+\)$ ]]; then
if [ "$new" != "$old" ] && grep -qxF "$new" "$RULES_FILE" 2>/dev/null; then
echo "该规则已存在"
else
TARGET_OLD="$old" TARGET_NEW="$new" awk '
BEGIN { done = 0; o = ENVIRON["TARGET_OLD"]; n = ENVIRON["TARGET_NEW"] }
{
line = $0
t = line
gsub(/^[ \t]+|[ \t]+$/, "", t)
if (t == o && done == 0) { done = 1; print n; next }
print
}' "$RULES_FILE" > "$RULES_FILE.tmp" && mv "$RULES_FILE.tmp" "$RULES_FILE"
echo "已修改:$old -> $new"
apply_config
fi
else
echo "格式错误!应为 ipv4(...) 或 ipv6(...),例如:ipv4(geosite:netflix)"
fi
elif [ "$n" = "0" ]; then
echo "已取消"
else
echo "序号无效"
fi
pause
}
shunt_help() {
echo ""
printf '%s---------- 分流规则说明 ----------%s\n' "$C_BOLD" "$C_RESET"
echo "写法:<出口>(<匹配目标>)"
echo ""
printf '%s出口(流量走哪个 IP 版本出去):%s\n' "$C_CYAN" "$C_RESET"
echo " ipv4 命中的流量强制走 IPv4 出口"
echo " ipv6 命中的流量强制走 IPv6 出口"
echo ""
printf '%s匹配目标(哪些网站命中):%s\n' "$C_CYAN" "$C_RESET"
echo " geosite:分类名 官方维护的域名集合,如 netflix / youtube / google"
echo " domain:域名 指定域名(含其全部子域名)"
echo " 裸域名 同 domain:,domain: 可省略"
echo ""
printf '%s示例:%s\n' "$C_CYAN" "$C_RESET"
echo " ipv4(geosite:netflix) Netflix 走 IPv4"
echo " ipv6(geosite:youtube) YouTube 走 IPv6"
echo " ipv4(domain:example.com) example.com 及其子域名走 IPv4"
echo " ipv6(bilibili.com) B站走 IPv6"
echo ""
echo "注意:"
echo " - 规则按顺序匹配,命中即走对应出口;都没命中的走默认直连"
echo " - geosite 分类名不区分大小写;写错分类名会导致配置检查失败、"
echo " 本次修改不生效(用「查看日志」确认服务仍在运行)"
echo " - 客户端须开启「通过代理解析 DNS」,否则规则不生效"
pause
}
shunt_del() {
[ -s "$RULES_FILE" ] || { echo "没有可删除的规则"; pause; return 0; }
local -a items=()
local line
while IFS= read -r line || [ -n "$line" ]; do
line="$(trim "$line")"
[ -z "$line" ] && continue
case "$line" in \#*) continue ;; esac
items+=("$line")
done < "$RULES_FILE"
[ "${#items[@]}" -eq 0 ] && { echo "没有可删除的规则"; pause; return 0; }
local i
for i in "${!items[@]}"; do
echo " [$((i + 1))] ${items[$i]}"
done
local n
read -rp "输入要删除的序号(0 取消): " n
if [[ "$n" =~ ^[0-9]+$ ]] && [ "$n" -ge 1 ] && [ "$n" -le "${#items[@]}" ]; then
local target="${items[$((n - 1))]}"
TARGET="$target" awk '
BEGIN { done = 0; t = ENVIRON["TARGET"] }
{
line = $0
gsub(/^[ \t]+|[ \t]+$/, "", line)
if (line == t && done == 0) { done = 1; next }
print
}' "$RULES_FILE" > "$RULES_FILE.tmp" && mv "$RULES_FILE.tmp" "$RULES_FILE"
echo "已删除:$target"
apply_config
elif [ "$n" = "0" ]; then
echo "已取消"
else
echo "序号无效"
fi
pause
}
shunt_clear() {
local c
read -rp "清空全部规则(之后所有流量直连),确定吗?(y/N): " c
case "$c" in
y|Y)
: > "$RULES_FILE"
echo "已清空"
apply_config
;;
*) echo "已取消" ;;
esac
pause
}
# ------------------------- 节点信息 -------------------------
node_menu() {
while true; do
load_node
echo ""
printf '%s---------- 更改节点信息 ----------%s\n' "$C_BOLD" "$C_RESET"
echo " 当前端口:$PORT"
echo " 当前用户名:$USERNAME"
if [ -n "$PASSWORD" ]; then
echo " 当前密码:********(已设置)"
else
echo " 当前密码:(未设置)"
fi
echo "----------------------------------"
echo " 1. 更改端口"
echo " 2. 更改用户名"
echo " 3. 更改密码"
echo " 0. 返回"
local c input
read -rp "请选择: " c
case "$c" in
1)
read -rp "新端口 [$PORT]: " input
if [ -z "$input" ]; then
echo "未更改"
elif [[ "$input" =~ ^[0-9]+$ ]] && [ "$input" -ge 1 ] && [ "$input" -le 65535 ]; then
PORT="$input"; save_node; echo "端口已改为 $PORT"; apply_config
else
echo "端口无效"
fi
pause ;;
2)
read -rp "新用户名 [$USERNAME]: " input
if [ -n "$input" ]; then
USERNAME="$input"; save_node; echo "用户名已改为 $USERNAME"; apply_config
else
echo "未更改"
fi
pause ;;
3)
read -rsp "新密码: " input; echo
if [ -n "$input" ]; then
PASSWORD="$input"; save_node; echo "密码已更新"; apply_config
else
echo "密码不能为空,未更改"
fi
pause ;;
0) return 0 ;;
*) echo "无效选择"; pause ;;
esac
done
}
# ------------------------- 主菜单 -------------------------
main_menu() {
while true; do
echo ""
show_header
printf ' %s1.%s 安装 sing-box\n' "$C_YELLOW" "$C_RESET"
printf ' %s2.%s 卸载 sing-box\n' "$C_YELLOW" "$C_RESET"
printf ' %s3.%s 启动服务\n' "$C_YELLOW" "$C_RESET"
printf ' %s4.%s 停止服务\n' "$C_YELLOW" "$C_RESET"
printf ' %s5.%s 重启服务\n' "$C_YELLOW" "$C_RESET"
printf ' %s6.%s 查看运行状态\n' "$C_YELLOW" "$C_RESET"
printf ' %s7.%s 分流设置\n' "$C_YELLOW" "$C_RESET"
printf ' %s8.%s 更改节点信息\n' "$C_YELLOW" "$C_RESET"
printf ' %s9.%s 查看日志\n' "$C_YELLOW" "$C_RESET"
printf ' %s10.%s 查看节点信息\n' "$C_YELLOW" "$C_RESET"
printf ' %s0.%s 退出\n' "$C_YELLOW" "$C_RESET"
echo "=============================================="
local c
read -rp "请选择: " c
case "$c" in
1) do_install; pause ;;
2) do_uninstall; pause ;;
3)
need_installed || { pause; continue; }
systemctl start sing-box 2>/dev/null && echo "已启动" || echo "启动失败"
pause ;;
4)
need_installed || { pause; continue; }
systemctl stop sing-box 2>/dev/null && echo "已停止" || echo "停止失败"
pause ;;
5)
need_installed || { pause; continue; }
systemctl restart sing-box 2>/dev/null && echo "已重启" || echo "重启失败"
pause ;;
6)
need_installed || { pause; continue; }
show_status; pause ;;
7) shunt_menu ;;
8) node_menu ;;
9)
need_installed || { pause; continue; }
view_logs ;;
10)
need_installed || { pause; continue; }
view_node_info ;;
0) echo "再见"; exit 0 ;;
*) echo "无效选择"; pause ;;
esac
done
}
# ------------------------- 入口 -------------------------
[ "$(id -u)" -eq 0 ] || { echo "请用 root 用户运行(或加 sudo)"; exit 1; }
# 自同步快捷键:直接运行脚本文件时,若 /usr/local/bin/sb 已存在但内容不是最新,
# 自动同步为当前版本,避免 sb 命令与脚本文件新旧不一致
sync_sb_cmd() {
local me
me="$(readlink -f "$0" 2>/dev/null || printf '%s' "$0")"
if [ "$me" != "/usr/local/bin/sb" ] && [ -f /usr/local/bin/sb ]; then
if ! cmp -s "$me" /usr/local/bin/sb 2>/dev/null; then
cp -f "$me" /usr/local/bin/sb
chmod +x /usr/local/bin/sb
fi
fi
}
sync_sb_cmd
main_menu