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-box

2. 错误:status=203/EXEC(找不到可执行文件)

现象:Failed to start sing-box.service: Unit is not loaded properly.

解决:

1. 检查 Sing-Box 可执行文件路径是否正确:

ls -l /usr/bin/sing-box

2. 若不存在,则重新下载安装:

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-box

3. 错误:status=200/CHDIR(工作目录不存在)

现象:Failed to start sing-box.service: Working directory not accessible

解决步骤:

1. 创建工作目录(如未创建):

sudo mkdir -p /var/lib/sing-box

2. 设置目录权限:

sudo chown -R singbox:singbox /var/lib/sing-box

4. 配置文件 JSON 格式错误

现象:Failed to parse config file.

解决步骤:

1. 使用 sing-box 自带工具检查:

sing-box check -c /etc/sing-box/config.json

2. 或使用在线 JSON 校验工具(如 [JSONLint](https://jsonlint.com/))进行格式验证。

3. 修改配置文件后,重启服务:

sudo systemctl restart sing-box

5. 端口冲突(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"
    }
  ]
}

📌 实际配置请参考官方文档或根据实际需求修改。

六、常用管理命令速查

操作

命令

启动服务

sudo systemctl start sing-box

停止服务

sudo systemctl stop sing-box

重启服务

sudo systemctl restart sing-box

设置开机自启

sudo systemctl enable sing-box

查看状态

systemctl status sing-box

查看日志

journalctl -u sing-box -f

验证配置文件

sing-box check -c /etc/sing-box/config.json

热重载配置

sudo systemctl reload sing-box

七、参考资源

  • 官方文档:[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


Sing-Box 安装与配置详细教程
https://blog.cikaros.cn/archives/sing-box-an-zhuang-yu-pei-zhi-xiang-xi-jiao-cheng
作者
Cikaros
发布于
2026年03月04日
许可协议