Sing-Box 安装与配置详细教程
一、简介
Sing-Box 是一款高性能的代理平台,支持多种协议(如 VLESS、Trojan、Hysteria2、Shadowsocks 等),适用于科学上网、内网穿透或流量转发等场景。其配置灵活,资源占用低,在 Linux 系统中部署非常方便。
二、安装前准备
环境要求:
支持的 Linux 发行版:Ubuntu 20.04+/Debian 10+
已安装基础软件包(如
curlunzipvim)确保系统已更新至最新:
sudo apt update && sudo apt upgrade -y
三、安装 Sing-Box(推荐官方一键脚本)
方法 A:使用官方一键安装脚本
curl -fsSL https://sing-box.app/install.sh | sudo bash⚠️ 注意:如果网络受限,可以设置代理:
export https_proxy=http://<proxy_ip>:<port>
curl -fsSL https://sing-box.app/install.sh | sudo bash脚本会自动完成以下操作:
下载 Sing-Box 安装包
创建系统用户(如
singbox)配置 Systemd 服务并设置开机自启
创建配置文件路径
/etc/sing-box设置工作目录为
/var/lib/sing-box
四、常见错误排查与解决方案
1. 错误:status=217/USER
现象:
systemctl status sing-box
● sing-box.service loaded failed: No such file or directory
/lib/systemd/system/sing-box.service原因:
Systemd 服务文件缺失或未正确创建
解决步骤:
✅ 检查 Systemd 服务文件是否存在
ls /etc/systemd/system/sing-box.service如不存在,则手动创建该文件:
✅ 创建 systemd 服务文件
sudo vim /etc/systemd/system/sing-box.service将以下内容复制粘贴进来(根据你的实际路径调整):
[Unit]
Description=sing-box service
After=network.target
[Service]
User=singbox
Group=singbox
WorkingDirectory=/var/lib/sing-box
ExecStart=/usr/bin/sing-box -D /var/lib/sing-box -C /etc/sing-box run
Restart=on-failure
LimitNOFILE=infinity
[Install]
WantedBy=multi-user.target📌 如果你尚未创建
/var/lib/sing-box,请先执行:
sudo mkdir -p /var/lib/sing-box✅ 汇总配置文件的权限
sudo chown -R singbox:singbox /var/lib/sing-box✅ 重新加载 systemd
sudo systemctl daemon-reload✅ 启动服务并设置开机自启
sudo systemctl start sing-box
sudo systemctl enable sing-box2. 错误:status=203/EXEC(找不到可执行文件)
现象:Failed to start sing-box.service: Unit is not loaded properly.
解决:
1. 检查 Sing-Box 可执行文件路径是否正确:
ls -l /usr/bin/sing-box2. 若不存在,则重新下载安装:
wget https://github.com/SagerNet/sing-box/releases/download/v1.13.0/sing-box_1.13.0_linux_arm64.deb
sudo dpkg -i sing-box_1.13.0_linux_arm64.deb📌 如需安装其他架构,请前往 [GitHub releases 页面](https://github.com/SagerNet/sing-box/releases) 选择对应的版本。
3. 赋予执行权限:
chmod +x /usr/bin/sing-box3. 错误:status=200/CHDIR(工作目录不存在)
现象:Failed to start sing-box.service: Working directory not accessible
解决步骤:
1. 创建工作目录(如未创建):
sudo mkdir -p /var/lib/sing-box2. 设置目录权限:
sudo chown -R singbox:singbox /var/lib/sing-box4. 配置文件 JSON 格式错误
现象:Failed to parse config file.
解决步骤:
1. 使用 sing-box 自带工具检查:
sing-box check -c /etc/sing-box/config.json2. 或使用在线 JSON 校验工具(如 [JSONLint](https://jsonlint.com/))进行格式验证。
3. 修改配置文件后,重启服务:
sudo systemctl restart sing-box5. 端口冲突(bind: address already in use)
解决方法:
sudo ss -tulpn | grep <端口号>若发现有其他服务占用,可选择停止该服务或在 config.json 中修改端口设置。
五、配置文件示例(供参考)
在 /etc/sing-box/config.json 中添加如下结构内容:
{
"inbounds": [
{
"type": "vless",
"listen": "0.0.0.0:443",
"tag": "vless-inbounds",
"settings": {
"clients": [
{
"id": "your-uuid-here",
"flow": ""
}
]
},
"streamSettings": {
"network": "tcp"
}
}
],
"outbounds": [
{
"type": "direct"
}
]
}📌 实际配置请参考官方文档或根据实际需求修改。
六、常用管理命令速查
七、参考资源
官方文档:[https://sing-box.sagernet.org/](https://sing-box.sagernet.org/)
GitHub 项目仓库:[https://github.com/SagerNet/sing-box](https://github.com/SagerNet/sing-box)
配置示例:[https://sing-box.sagernet.org/configuration/](https://sing-box.sagernet.org/configuration/)
一键安装脚本项目(需自行甄别):GitHub 上搜索
sing-box-install
八、附录:操作记录与命令回顾
以下是一些典型的安装和调试命令,供参考:
sudo systemctl status sing-box # 查看服务状态
journalctl -u sing-box -n 30 # 显示最近30行日志
sudo systemctl daemon-reload # 重新加载 systemd 配置还有一个自动化配置脚本如下:
#!/usr/bin/env bash
#
# sbp v1.5 — sing-box HTTPS 代理管理器(管理端运行于 macOS)
#
# 架构: macOS 本地持根 CA(ca.key) → SSH 到远端 Linux 服务器部署 sing-box
# 服务器上生成 CSR/私钥,证书拉回本地签发后推送(ca.key 不上传)
# 入站: HTTPS 代理(双栈 ::) + Basic 认证 出站: direct(本机出口)
#
# v1.5 新增:
# - v6 测试前探测本机 IPv6 出口: 无 v6 路由时跳过(避免误报服务器故障)
# - 测试失败自动分类: 超时→安全组/防火墙放行提示; 秒拒→本机 VPN/TUN 或远端 REJECT
# v1.4: init 失败不写配置; -p/--port 显式值不被旧配置覆盖; SSH 端口预检回退
# v1.3: sbp port 在线换端口
# v1.2: bash 3.2 兼容(${var} 写法)
# v1.1: LibreSSL 兼容(ecparam 生成密钥)
#
set -u
set -o pipefail 2>/dev/null || true
SBP_DIR="${SBP_DIR:-$HOME/.config/sbp}"
CONF="$SBP_DIR/config" # 服务器连接配置
CREDS="$SBP_DIR/creds" # 账号/密码/IP
TRUST_FP="$SBP_DIR/trust.fp" # 钥匙串证书指纹记录
CA_KEY="$SBP_DIR/ca.key" # 根 CA 私钥(勿分发!)
CA_CRT="$SBP_DIR/ca.crt" # 根 CA 证书(分发给客户端)
CA_CN="sbp Local CA"
CA_DAYS=3650 # CA 有效期(不受 Apple 398 天限制)
LEAF_DAYS=397 # leaf 有效期(满足 Apple ≤398 天)
# ---------- 输出 ----------
if [ -t 1 ]; then
B=$'\e[1m'; G=$'\e[32m'; R=$'\e[31m'; Y=$'\e[33m'; C=$'\e[36m'; N=$'\e[0m'
else B=""; G=""; R=""; Y=""; C=""; N=""; fi
step(){ printf '%b\n' "${C}==>${N} ${B}$1${N}"; }
ok(){ printf ' %b\n' "${G}[OK]${N} $1"; }
warn(){ printf ' %b\n' "${Y}[!!]${N} $1" >&2; }
die(){ printf ' %b\n' "${R}[XX]${N} $1" >&2; exit 1; }
# ---------- 基础 ----------
ensure_dir(){ mkdir -p "$SBP_DIR"; chmod 700 "$SBP_DIR" 2>/dev/null || true; }
load_conf(){
[ -f "$CONF" ] || die "尚未初始化,请先运行: sbp init"
. "$CONF"
SSH_PORT="${SSH_PORT:-22}"
LISTEN_PORT="${LISTEN_PORT:-443}"
}
load_creds(){
[ -f "$CREDS" ] || die "凭据不存在(${CREDS}),请先运行: sbp init"
. "$CREDS"
}
rsh(){
ssh -p "${SSH_PORT:-22}" -o ConnectTimeout=10 \
-o StrictHostKeyChecking=accept-new "${SERVER:?}" "$@"
}
gen_username(){ echo "u_$(openssl rand -hex 4)"; }
gen_password(){ openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | cut -c1-24; }
validate_port(){
local p=$1
case "$p" in
''|*[!0-9]*) die "端口必须为 1-65535 的数字(收到: ${p})" ;;
esac
[ "$p" -ge 1 ] && [ "$p" -le 65535 ] || die "端口范围 1-65535(收到: ${p})"
}
# ssh_tcp_probe <host> <port>: 输出 ok/fail/unknown(尽力而为,仅预检用)
ssh_tcp_probe(){
local host=$1 port=$2 err
command -v nc >/dev/null 2>&1 || { echo unknown; return; }
err=$(mktemp /tmp/sbp-nc.XXXXXX)
if nc -z -G 5 "$host" "$port" >/dev/null 2>"$err"; then
rm -f "$err"; echo ok; return
fi
# nc 不认识 -G(老系统): 视为无法探测,而非端口不通
if grep -qi 'option' "$err" 2>/dev/null; then
rm -f "$err"; echo unknown; return
fi
rm -f "$err"; echo fail
}
# ---------- 本地 CA ----------
gen_key(){
local out=$1 err; err=$(mktemp /tmp/sbp-err.XXXXXX)
if ! openssl ecparam -name prime256v1 -genkey -noout -out "$out" 2>"$err"; then
openssl genrsa -out "$out" 3072 2>>"$err" \
|| { cat "$err" >&2; rm -f "$err"; die "生成私钥失败"; }
fi
rm -f "$err"
}
ensure_ca(){
[ -f "$CA_CRT" ] && [ -f "$CA_KEY" ] && return 0
step "生成本地根 CA(${CA_CN},${CA_DAYS} 天)"
local cnf err
cnf=$(mktemp /tmp/sbp-ca.XXXXXX)
err=$(mktemp /tmp/sbp-ca-err.XXXXXX)
cat > "$cnf" <<EOF
[req]
distinguished_name = dn
x509_extensions = v3_ca
prompt = no
[dn]
CN = ${CA_CN}
[v3_ca]
basicConstraints = critical, CA:TRUE
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
EOF
gen_key "$CA_KEY"
openssl req -new -x509 -key "$CA_KEY" -out "$CA_CRT" -days "$CA_DAYS" \
-config "$cnf" 2>"$err" \
|| { cat "$err" >&2; rm -f "$cnf" "$err"; die "生成 CA 证书失败"; }
rm -f "$cnf" "$err"; chmod 600 "$CA_KEY"
ok "CA 已生成: ${CA_CRT}(ca.key 严格保密,勿放服务器/勿分发)"
}
# 签发 leaf: $1=csr $2=输出 $3=ipv4 $4=ipv6(可空)
sign_leaf(){
local csr=$1 out=$2 ip4=$3 ip6=${4:-}
local san="IP:${ip4},IP:127.0.0.1,DNS:localhost"
[ -n "$ip6" ] && san="$san,IP:${ip6},IP:::1"
local ext; ext=$(mktemp /tmp/sbp-ext.XXXXXX)
{
echo "subjectAltName = $san"
echo "keyUsage = critical, digitalSignature, keyEncipherment"
echo "extendedKeyUsage = serverAuth"
} > "$ext"
openssl x509 -req -in "$csr" -CA "$CA_CRT" -CAkey "$CA_KEY" \
-CAcreateserial -days "$LEAF_DAYS" -extfile "$ext" -out "$out" \
>/dev/null 2>&1 || { rm -f "$ext"; die "签发服务器证书失败"; }
rm -f "$ext"
}
# ---------- 远端: 探测 IP + 装 sing-box + 生成 CSR ----------
remote_env(){
local out
if ! out=$(rsh "bash -s" 2>&1 <<'REMOTE'
set -eu
SUDO=""
[ "$(id -u)" = 0 ] || SUDO="sudo"
IPV4=""
for u in https://api.ip.sb/ip https://ipv4.icanhazip.com https://4.ipw.cn https://ifconfig.me; do
t=$(curl -4 -fs --max-time 5 "$u" 2>/dev/null | tr -d '[:space:]') || continue
printf '%s' "$t" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' && { IPV4=$t; break; }
done
if [ -z "$IPV4" ]; then
IPV4=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++)if($i=="src"){print $(i+1);exit}}') || true
printf '%s' "$IPV4" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' || IPV4=""
fi
[ -n "$IPV4" ] || { echo "ERROR: 未探测到公网 IPv4" >&2; exit 1; }
IPV6=""
for u in https://api.ip.sb/ip https://ipv6.icanhazip.com https://6.ipw.cn; do
t=$(curl -6 -fs --max-time 5 "$u" 2>/dev/null | tr -d '[:space:]') || continue
case "$t" in *:*) IPV6=$t; break ;; esac
done
if [ -z "$IPV6" ]; then
IPV6=$(ip -6 route get 2001:4860:4860::8888 2>/dev/null | awk '{for(i=1;i<=NF;i++)if($i=="src"){print $(i+1);exit}}' | cut -d% -f1) || true
fi
case "$IPV6" in fe80*|"") IPV6="" ;; esac
echo "SBP_IPV4=$IPV4"
echo "SBP_IPV6=$IPV6"
if ! command -v sing-box >/dev/null 2>&1; then
echo "installing sing-box..." >&2
curl -fsSL https://sing-box.app/install.sh | $SUDO bash >/dev/null 2>&1 || true
fi
command -v sing-box >/dev/null 2>&1 || { echo "ERROR: sing-box 安装失败,请手动安装后重试" >&2; exit 1; }
$SUDO mkdir -p /etc/sing-box /var/lib/sing-box
if ! $SUDO openssl ecparam -name prime256v1 -genkey -noout -out /etc/sing-box/key.pem 2>/dev/null; then
$SUDO openssl genrsa -out /etc/sing-box/key.pem 3072 2>/dev/null
fi
$SUDO chmod 600 /etc/sing-box/key.pem
$SUDO openssl req -new -key /etc/sing-box/key.pem -out /etc/sing-box/server.csr \
-subj "/CN=sbp-server" 2>/dev/null
echo "SBP_CSR_BEGIN"
cat /etc/sing-box/server.csr
echo "SBP_CSR_END"
REMOTE
); then
die "远端准备失败:\n$(printf '%s\n' "$out" | tail -n 5)\n提示: -p 是 SSH 登录端口(sshd,默认 22),代理监听端口请用 --port"
fi
REM_IPV4=$(printf '%s\n' "$out" | sed -n 's/^SBP_IPV4=//p' | head -1)
REM_IPV6=$(printf '%s\n' "$out" | sed -n 's/^SBP_IPV6=//p' | head -1)
printf '%s\n' "$out" | grep -q '^SBP_CSR_BEGIN' \
|| die "远端未返回 CSR:\n$(printf '%s\n' "$out" | tail -n 5)"
SBP_CSR_FILE=$(mktemp /tmp/sbp-csr.XXXXXX)
printf '%s\n' "$(printf '%s\n' "$out" | sed -n '/^SBP_CSR_BEGIN$/,/^SBP_CSR_END$/p' | sed '1d;$d')" > "$SBP_CSR_FILE"
ok "公网 IPv4: $REM_IPV4"
if [ -n "$REM_IPV6" ]; then ok "公网 IPv6: $REM_IPV6"; else warn "未检测到公网 IPv6(仅签发 v4)"; fi
}
# ---------- 远端: 写证书/配置/服务 ----------
remote_apply(){
local cert_b64=$1 u=$2 p=$3 lp=$4 with_conf=$5 out
if ! out=$(rsh "CERT_B64='$cert_b64' USERNAME='$u' PASSWORD='$p' LISTEN_PORT='$lp' WITH_CONF='$with_conf' bash -s" 2>&1 <<'REMOTE'
set -eu
SUDO=""
[ "$(id -u)" = 0 ] || SUDO="sudo"
if [ -n "$CERT_B64" ]; then
printf '%s' "$CERT_B64" | base64 -d | $SUDO tee /etc/sing-box/cert.pem >/dev/null
$SUDO chmod 644 /etc/sing-box/cert.pem
fi
if [ "$WITH_CONF" = "yes" ]; then
$SUDO tee /etc/sing-box/config.json >/dev/null <<JSON
{
"log": { "level": "info", "timestamp": true },
"inbounds": [
{
"type": "http",
"tag": "https-in",
"listen": "::",
"listen_port": ${LISTEN_PORT},
"users": [
{ "username": "${USERNAME}", "password": "${PASSWORD}" }
],
"tls": {
"enabled": true,
"certificate_path": "/etc/sing-box/cert.pem",
"key_path": "/etc/sing-box/key.pem"
}
}
],
"outbounds": [ { "type": "direct", "tag": "direct" } ],
"route": { "auto_detect_interface": true }
}
JSON
$SUDO chmod 600 /etc/sing-box/config.json
fi
$SUDO sing-box check -C /etc/sing-box || { echo "ERROR: sing-box 配置校验失败" >&2; exit 1; }
BIN=$(command -v sing-box)
$SUDO tee /etc/systemd/system/sing-box.service >/dev/null <<UNIT
[Unit]
Description=sing-box service
After=network.target network-online.target
[Service]
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
ExecStart=${BIN} -D /var/lib/sing-box -C /etc/sing-box run
ExecReload=/bin/kill -HUP \$MAINPID
Restart=on-failure
RestartSec=10s
LimitNOFILE=infinity
[Install]
WantedBy=multi-user.target
UNIT
$SUDO systemctl daemon-reload
$SUDO systemctl enable sing-box >/dev/null 2>&1 || true
$SUDO systemctl restart sing-box
sleep 1
$SUDO systemctl is-active --quiet sing-box || { echo "ERROR: 服务未运行,请 journalctl -u sing-box 排查" >&2; exit 1; }
if command -v ss >/dev/null 2>&1; then
if ! ss -tln 2>/dev/null | grep -q ":${LISTEN_PORT} "; then
echo "WARN: 端口 ${LISTEN_PORT} 未监听(可能 bind 失败/被占用)" >&2
fi
fi
echo "REMOTE_OK"
REMOTE
); then
die "远端部署失败:\n$(printf '%s\n' "$out" | tail -n 5)\n(可在服务器上 journalctl -u sing-box 排查)"
fi
return 0
}
save_creds(){
cat > "$CREDS" <<EOF
USERNAME="$1"
PASSWORD="$2"
IPV4="$3"
IPV6="$4"
EOF
chmod 600 "$CREDS"
}
save_conf(){ # $1=server $2=ssh_port $3=listen_port
cat > "$CONF" <<EOF
SERVER="$1"
SSH_PORT="$2"
LISTEN_PORT="$3"
EOF
chmod 600 "$CONF"
}
print_summary(){
echo
echo "${B}──────────── 连接信息 ────────────${N}"
echo " 协议 : HTTPS 代理(HTTP over TLS + Basic 认证)"
echo " IPv4 : ${IPV4}:${LISTEN_PORT}"
[ -n "${IPV6:-}" ] && echo " IPv6 : [${IPV6}]:${LISTEN_PORT}"
echo " 用户名 : ${USERNAME}"
echo " 密码 : ${PASSWORD}"
echo " CA : ${CA_CRT}"
echo "──────────────────────────────────${N}"
}
# ---------- 命令 ----------
cmd_init(){
local server="" ssh_port="" listen_port="" ssh_explicit=0
while [ $# -gt 0 ]; do
case "$1" in
-s|--server) [ $# -ge 2 ] || die "缺少参数值"; server=$2; shift 2 ;;
-p|--ssh-port) [ $# -ge 2 ] || die "缺少参数值"; ssh_port=$2; ssh_explicit=1; shift 2 ;;
--port) [ $# -ge 2 ] || die "缺少参数值"; listen_port=$2; shift 2 ;;
*) die "未知参数: $1" ;;
esac
done
if [ -z "$server" ]; then
[ -t 0 ] || die "非交互环境请用: sbp init -s user@host"
printf '服务器 (user@host,需 root 或 sudo 权限): '
read server
[ -n "$server" ] || die "未输入服务器"
fi
ensure_dir
# 读取旧配置(仅作默认值参考;本次失败不会写回)
local old_server="" old_port="" old_ssh=""
if [ -f "$CONF" ]; then
. "$CONF"
old_server="${SERVER:-}"; old_port="${LISTEN_PORT:-}"; old_ssh="${SSH_PORT:-}"
fi
# 监听端口: --port 参数 > SBP_LISTEN_PORT 环境变量 > 旧配置 > 443
[ -n "$listen_port" ] || listen_port="${SBP_LISTEN_PORT:-${old_port:-443}}"
# SSH 端口: -p 参数 > 旧配置 > 22
[ -n "$ssh_port" ] || ssh_port="${old_ssh:-22}"
validate_port "$listen_port"
validate_port "$ssh_port"
# SSH 端口 TCP 预检(尽力而为):继承的坏端口可自动回退 22
local host probe
host="${server#*@}"; host="${host#\[}"; host="${host%\]}"
probe=$(ssh_tcp_probe "$host" "$ssh_port")
if [ "$probe" = "fail" ]; then
if [ "$ssh_explicit" = 0 ] && [ "$ssh_port" != 22 ] \
&& [ "$(ssh_tcp_probe "$host" 22)" = "ok" ]; then
warn "SSH 端口 ${ssh_port} 不通,检测到 22 可用 → 自动改用 22(固定端口请显式 -p)"
ssh_port=22
else
die "SSH 端口 ${ssh_port} 无法连通(${host})。\n-p 是 SSH 登录端口(sshd),代理监听端口用 --port;端口无误则检查防火墙/安全组"
fi
fi
if [ "$listen_port" = "$ssh_port" ]; then
die "监听端口与 SSH 端口相同(${listen_port})。\n注意: -p 是 SSH 登录端口(sshd),代理监听端口请用 --port"
fi
[ -z "$old_server" ] || [ "$old_server" = "$server" ] \
|| warn "服务器变更: ${old_server} → ${server}"
[ -z "$old_port" ] || [ "$old_port" = "$listen_port" ] \
|| warn "监听端口变更: ${old_port} → ${listen_port}"
[ -z "$old_ssh" ] || [ "$old_ssh" = "$ssh_port" ] \
|| warn "SSH 端口变更: ${old_ssh} → ${ssh_port}"
# 只设内存变量;部署成功后才写入 config(失败不污染配置)
SERVER="$server"; SSH_PORT="$ssh_port"; LISTEN_PORT="$listen_port"
ensure_ca
step "连接服务器:探测公网 IP / 安装 sing-box / 生成 CSR"
remote_env
local san_ip="$REM_IPV4"
[ -n "$REM_IPV6" ] && san_ip="$san_ip, $REM_IPV6"
step "签发服务器证书(SAN: ${san_ip},${LEAF_DAYS} 天)"
local certfile; certfile=$(mktemp /tmp/sbp-cert.XXXXXX)
sign_leaf "$SBP_CSR_FILE" "$certfile" "$REM_IPV4" "$REM_IPV6"
local u p
if [ -f "$CREDS" ]; then
. "$CREDS"
u="${USERNAME:-$(gen_username)}"; p="${PASSWORD:-$(gen_password)}"
ok "沿用已有账号密码(更换请用: sbp rotate)"
else
u=$(gen_username); p=$(gen_password)
ok "已生成随机账号密码"
fi
step "部署配置并启动服务(端口 ${LISTEN_PORT})"
remote_apply "$(base64 < "$certfile" | tr -d '\n')" "$u" "$p" "$LISTEN_PORT" yes
save_creds "$u" "$p" "$REM_IPV4" "${REM_IPV6:-}"
save_conf "$SERVER" "$SSH_PORT" "$LISTEN_PORT"
rm -f "$SBP_CSR_FILE" "$certfile"
IPV4="$REM_IPV4"; IPV6="${REM_IPV6:-}"; USERNAME="$u"; PASSWORD="$p"
ok "部署完成!"
print_summary
echo " 下一步: ${B}sbp trust${N} && ${B}sbp test${N}"
}
cmd_port(){
load_conf; load_creds
[ $# -ge 1 ] || die "用法: sbp port <新端口号>"
local new_port=$1
validate_port "$new_port"
[ "$new_port" != "$SSH_PORT" ] || die "新端口不能与 SSH 端口相同(${SSH_PORT})"
if [ "$new_port" = "$LISTEN_PORT" ]; then
ok "端口未变化(${LISTEN_PORT}),仅重写配置并重启"
else
step "更换监听端口: ${LISTEN_PORT} → ${new_port}"
local busy
busy=$(rsh "if command -v ss >/dev/null 2>&1; then if ss -tln 2>/dev/null | grep -q ':${new_port} '; then echo yes; else echo no; fi; else echo unknown; fi" 2>/dev/null || echo unknown)
if [ "$busy" = "yes" ]; then
die "远端端口 ${new_port} 已被占用(可 sbp ssh 后执行 ss -tlnp | grep ${new_port} 排查)"
fi
[ "$busy" != "unknown" ] || warn "无法预检端口占用(远端无 ss),继续尝试"
fi
step "重写远端配置并重启(证书/账密不变)"
remote_apply "" "$USERNAME" "$PASSWORD" "$new_port" yes
save_conf "$SERVER" "$SSH_PORT" "$new_port"
LISTEN_PORT="$new_port"
ok "监听端口已更新为 ${new_port}"
warn "记得放行防火墙/安全组 ${new_port}/tcp(v4+v6),客户端端口需同步修改"
echo " 可运行: ${B}sbp test${N} 验证"
}
cmd_renew(){
load_conf; load_creds; ensure_ca
local old4="$IPV4" old6="${IPV6:-}"
step "探测服务器当前公网 IP"
remote_env
if [ "$REM_IPV4" = "$old4" ] && [ "$REM_IPV6" = "$old6" ]; then
warn "公网 IP 未变化,仍将重签证书并重启(可当作续期)"
else
ok "IP 变化: ${old4} → ${REM_IPV4}"
fi
step "重签服务器证书"
local certfile; certfile=$(mktemp /tmp/sbp-cert.XXXXXX)
sign_leaf "$SBP_CSR_FILE" "$certfile" "$REM_IPV4" "$REM_IPV6"
step "推送证书并重启远端服务"
remote_apply "$(base64 < "$certfile" | tr -d '\n')" "$USERNAME" "$PASSWORD" "$LISTEN_PORT" no
save_creds "$USERNAME" "$PASSWORD" "$REM_IPV4" "${REM_IPV6:-}"
rm -f "$SBP_CSR_FILE" "$certfile"
ok "完成(客户端信任的 CA 未变,无需任何更新)"
}
cmd_rotate(){
load_conf; load_creds
step "生成新账密并更新远端配置"
local u p; u=$(gen_username); p=$(gen_password)
remote_apply "" "$u" "$p" "$LISTEN_PORT" yes
save_creds "$u" "$p" "$IPV4" "${IPV6:-}"
ok "已轮换 → 用户名: $u 密码: $p"
}
cmd_trust(){
[ -f "$CA_CRT" ] || die "CA 不存在: ${CA_CRT}(先 sbp init)"
step "导入 CA 到 macOS 系统钥匙串(需管理员密码)"
sudo -v
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain "$CA_CRT" || die "导入失败"
local fp
fp=$(sudo security find-certificate -a -c "$CA_CN" -Z /Library/Keychains/System.keychain 2>/dev/null \
| awk '/SHA-1 hash/ {print $3; exit}')
if [ -n "$fp" ]; then echo "$fp" > "$TRUST_FP"; chmod 600 "$TRUST_FP"; fi
ok "已信任 ${CA_CN}(指纹: ${fp:-未知})"
warn "Safari/Chrome 重启后生效;Firefox 见 about:config → security.enterprise_roots.enabled"
}
cmd_untrust(){
if [ -f "$TRUST_FP" ]; then
local fp; fp=$(cat "$TRUST_FP")
sudo security delete-certificate -Z "$fp" /Library/Keychains/System.keychain \
|| die "按指纹删除失败"
rm -f "$TRUST_FP"
else
warn "无指纹记录,按名称删除: $CA_CN"
sudo security delete-certificate -c "$CA_CN" /Library/Keychains/System.keychain \
|| die "删除失败"
fi
ok "已移除系统信任"
}
# 本机 IPv6 出口探测: ok / fail / unknown
# ok = 本机可直连 v6(测试结果可信)
# fail = 本机无 v6 路由(跳过 v6 测试,非服务器问题)
# unknown= 有 v6 路由但探测站点不通(无法定论,照常测试)
local_v6_egress(){
if curl -6 -fsS --max-time 5 --noproxy '*' https://api.ip.sb/ip >/dev/null 2>&1; then
echo ok; return
fi
if route -n get -inet6 2606:4700:4700::1111 >/dev/null 2>&1; then
echo unknown
else
echo fail
fi
}
test_proxy(){
local ip=$1 r hostport last
case "$ip" in *:*) hostport="[$ip]:$LISTEN_PORT" ;; *) hostport="$ip:$LISTEN_PORT" ;; esac
r=$(curl -fsS --max-time 15 $TEST_CACERT \
-x "https://${USERNAME}:${PASSWORD}@${hostport}" https://api.ip.sb/ip 2>&1)
if [ $? -eq 0 ]; then
ok "代理可用(${ip} → 出口 $(printf '%s' "$r" | tr -d '[:space:]'))"
return 0
fi
last=$(printf '%s' "$r" | tail -n 1)
warn "测试失败(${ip}): ${last}"
case "$last" in
*timed\ out*|*Timed\ out*)
warn " → 超时: 多为安全组/防火墙未放行 ${LISTEN_PORT}/tcp(v4 与 v6 规则独立,需分别放行)" ;;
*refused*|*"Couldn't connect"*|*"Failed to connect"*)
warn " → 秒拒: 疑似本机 VPN/TUN 拦截 IPv6(暂停后重试),或远端防火墙 REJECT" ;;
esac
return 1
}
run_tests(){
local rc=0 target="${TEST_TARGET:-all}"
if [ "$target" = all ] || [ "$target" = v4 ]; then
if [ -n "${IPV4:-}" ]; then test_proxy "$IPV4" || rc=1; else warn "未记录 IPv4"; fi
fi
if [ "$target" = all ] || [ "$target" = v6 ]; then
if [ -z "${IPV6:-}" ]; then
warn "未记录 IPv6"
else
case "$(local_v6_egress)" in
fail)
warn "本机无 IPv6 出口,跳过 v6 测试(非服务器问题;换有 v6 的网络可再验证)" ;;
*)
test_proxy "$IPV6" || rc=1 ;;
esac
fi
fi
return $rc
}
cmd_test(){
load_conf; load_creds
TEST_CACERT="--proxy-cacert $CA_CRT"; TEST_TARGET=all
while [ $# -gt 0 ]; do
case "$1" in
--system) TEST_CACERT=""; warn "--system 依赖系统信任(部分 curl 构建不读钥匙串,建议默认模式)" ;;
-4|--4) TEST_TARGET=v4 ;;
-6|--6) TEST_TARGET=v6 ;;
*) die "未知参数: $1" ;;
esac
shift
done
run_tests
}
cmd_info(){
load_conf; load_creds
print_summary
echo
echo "${B}── Clash / mihomo 客户端片段 ──${N}"
cat <<YAML
proxies:
- name: "sbp-v4"
type: http
server: ${IPV4}
port: ${LISTEN_PORT}
username: ${USERNAME}
password: "${PASSWORD}"
tls: true
skip-cert-verify: false # 已 sbp trust;未信任则改 true
YAML
if [ -n "${IPV6:-}" ]; then
cat <<YAML
- name: "sbp-v6"
type: http
server: "${IPV6}"
port: ${LISTEN_PORT}
username: ${USERNAME}
password: "${PASSWORD}"
tls: true
skip-cert-verify: false
YAML
fi
echo
echo "${B}── sing-box 客户端出站(内嵌 CA,免系统信任) ──${N}"
local ca_json; ca_json=$(awk '{printf "%s\\n", $0}' "$CA_CRT")
cat <<JSON
{
"type": "http",
"tag": "sbp-proxy",
"server": "${IPV4}",
"server_port": ${LISTEN_PORT},
"username": "${USERNAME}",
"password": "${PASSWORD}",
"tls": { "enabled": true, "certificate": "${ca_json}" }
}
JSON
echo
echo " 手动测试: curl --proxy-cacert $CA_CRT -x \"https://${USERNAME}:${PASSWORD}@${IPV4}:${LISTEN_PORT}\" https://api.ip.sb/ip"
}
cmd_doctor(){
load_conf
local fail=0
step "本地检查"
[ -f "$CA_CRT" ] && ok "CA 证书存在" || { warn "缺少 CA"; fail=1; }
[ -f "$CA_KEY" ] && ok "CA 私钥存在" || { warn "缺少 ca.key"; fail=1; }
[ -f "$CREDS" ] && ok "凭据存在" || { warn "缺少凭据"; fail=1; }
step "远端检查(${SERVER})"
rsh "true" 2>/dev/null && ok "SSH 可达" || die "SSH 不可达,先修复连接"
rsh "systemctl is-active --quiet sing-box" 2>/dev/null \
&& ok "sing-box 服务运行中" || { warn "服务未运行"; fail=1; }
rsh "ss -tln 2>/dev/null | grep -q ':${LISTEN_PORT} '" 2>/dev/null \
&& ok "端口 ${LISTEN_PORT} 监听中" || warn "端口 ${LISTEN_PORT} 未监听(或远端无 ss)"
load_creds
local cur4
cur4=$(rsh "curl -4 -fs --max-time 5 https://api.ip.sb/ip 2>/dev/null | tr -d '[:space:]'")
if [ -n "$cur4" ]; then
if [ "$cur4" = "$IPV4" ]; then
ok "公网 IPv4 未变化(${cur4})"
else
warn "IPv4 已变化: ${IPV4} → ${cur4}(运行 sbp renew)"; fail=1
fi
fi
rsh "openssl x509 -in /etc/sing-box/cert.pem -noout -text 2>/dev/null | grep -A1 'Subject Alternative Name'" 2>/dev/null \
| grep -q "$IPV4" && ok "证书 SAN 包含当前 IPv4" || warn "证书 SAN 不含 ${IPV4}(sbp renew)"
step "本地信任状态"
security find-certificate -c "$CA_CN" /Library/Keychains/System.keychain >/dev/null 2>&1 \
&& ok "系统已信任 CA" || warn "未信任(可运行 sbp trust)"
step "本机 IPv6 出口"
case "$(local_v6_egress)" in
ok) ok "本机 IPv6 出口正常" ;;
fail) warn "本机无 IPv6 出口(v6 测试将被跳过,属正常)" ;;
unknown) warn "本机 v6 路由存在但出口探测不通(可能有 VPN/TUN 干扰)" ;;
esac
step "代理连通性"
TEST_CACERT="--proxy-cacert $CA_CRT"; TEST_TARGET=all
run_tests || fail=1
if [ "$fail" = 0 ]; then ok "一切正常"; else warn "存在问题,按上面提示处理"; fi
return $fail
}
cmd_service(){
load_conf
[ $# -ge 1 ] || die "用法: sbp status|start|stop|restart"
case "$1" in
status) rsh "systemctl status sing-box --no-pager -l" ;;
start|stop|restart)
rsh "systemctl $1 sing-box" && ok "sing-box 已 $1" ;;
*) die "未知子命令: $1" ;;
esac
}
cmd_logs(){
load_conf
local n=100 follow=""
while [ $# -gt 0 ]; do
case "$1" in
-f|--follow) follow="-f" ;;
-n|--lines) [ $# -ge 2 ] || die "缺少参数值"; n=$2; shift ;;
esac
shift
done
rsh "journalctl -u sing-box -n $n --no-pager $follow"
}
cmd_ssh(){ load_conf; exec ssh -p "$SSH_PORT" -o ConnectTimeout=10 "$SERVER"; }
cmd_uninstall(){
load_conf
[ -t 0 ] || die "请在交互终端运行"
printf '将卸载 %s 上的 sing-box(本地数据保留)。输入 yes 确认: ' "$SERVER"
read ans
[ "$ans" = "yes" ] || die "已取消"
step "卸载远端服务"
rsh "bash -s" <<'REMOTE'
set -e
SUDO=""; [ "$(id -u)" = 0 ] || SUDO="sudo"
$SUDO systemctl disable --now sing-box 2>/dev/null || true
$SUDO rm -rf /etc/sing-box /var/lib/sing-box
$SUDO rm -f /etc/systemd/system/sing-box.service
$SUDO systemctl daemon-reload || true
echo "REMOTE_OK"
REMOTE
ok "远端已卸载"
printf '是否同时删除本地数据(CA/凭据/配置)? [y/N] '
read a
case "$a" in
y|Y) rm -rf "$SBP_DIR"; ok "本地数据已删除" ;;
*) ok "本地数据保留: $SBP_DIR" ;;
esac
}
usage(){
cat <<'EOF'
sbp — sing-box HTTPS 代理管理器(管理端运行于 macOS)
用法: sbp <命令> [参数]
端口参数说明(重点区分):
-p, --ssh-port SSH 登录端口(sshd 监听端口,默认 22;sshd 非默认才需要指定)
--port 代理监听端口(sing-box HTTPS 入站,默认 443)
取值优先级: 显式参数 > 环境变量(SBP_LISTEN_PORT) > 旧配置 > 默认值
init 失败不会写回配置(避免坏配置残留)
部署:
init [-s user@host] [-p ssh端口] [--port 监听端口] 首次部署/重新部署
port <新端口号> 在线更换监听端口(重写配置并重启,证书/账密不变)
renew 公网 IP 变化/证书到期时重签(客户端无需变动)
rotate 轮换账号密码
uninstall 卸载远端服务(可选清理本地数据)
本地信任:
trust 将 CA 导入 macOS 系统钥匙串(需 sudo)
untrust 移除系统信任
使用/诊断:
info 连接信息 + Clash/mihomo/sing-box 客户端配置片段
test [--system] [-4|-6] 走代理实测(默认用 CA 严格校验)
v1.5: 本机无 IPv6 出口时自动跳过 v6 测试;
失败自动分类提示(超时→安全组; 秒拒→本机VPN/TUN)
doctor 全面体检(含本机 v6 出口检查)
status|start|stop|restart 远端服务控制
logs [-f] [-n 行数] 远端日志
ssh 直连服务器
文件(~/.config/sbp/):
config 服务器连接配置 creds 账号/密码/IP(600)
ca.crt 根 CA 证书(分发用) ca.key 根 CA 私钥(勿分发/勿上传服务器)
前置要求: 远端 Linux(root 或 sudo)、macOS 自带 openssl/ssh/curl
建议配置 SSH 免密登录(init 会发起多次 SSH 连接)
EOF
}
# ---------- 入口 ----------
[ $# -eq 0 ] && { usage; exit 0; }
cmd=$1; shift
case "$cmd" in
init) cmd_init "$@" ;;
port) cmd_port "$@" ;;
renew) cmd_renew ;;
rotate) cmd_rotate ;;
trust) cmd_trust ;;
untrust) cmd_untrust ;;
info) cmd_info ;;
test) cmd_test "$@" ;;
doctor) cmd_doctor ;;
status|start|stop|restart) cmd_service "$cmd" ;;
logs) cmd_logs "$@" ;;
ssh) cmd_ssh ;;
uninstall) cmd_uninstall ;;
help|-h|--help) usage ;;
version|--version) echo "sbp 1.5.0" ;;
*) die "未知命令: ${cmd}(sbp help 查看用法)" ;;
esac