DocsByRcDocsByRc
节点分享脚本
工具脚本

节点分享脚本

读取 sing-box 服务端配置,自动生成 VLESS / Hysteria2 / Trojan / SS / TUIC / AnyTLS 分享链接与二维码。

适用sing-box 服务端
难度简单
阅读—

vps-share.sh 是一个 Sing-box 节点分享脚本:读取服务端的 /etc/sing-box/config.json,自动为每个入站生成对应协议的分享链接(如 vless://、hy2://),并在终端直接打出二维码,手机扫码即用。

支持的协议
  • VLESS(Reality / TLS / WS / gRPC)
  • Hysteria2、Trojan、Shadowsocks(含 2022)、TUIC v5、AnyTLS

多用户入站会自动拆成「一人一条」。

下载脚本

在装有 sing-box 的服务端上执行:

BASH
# 下载脚本
curl -fsSL https://docs.232777.xyz/assets/vps-share.sh -o vps-share.sh
# 或手动上传到服务器

# 赋予执行权限并运行
chmod +x vps-share.sh
sudo ./vps-share.sh

脚本会自动检查并安装缺失的依赖(qrencode、jq、openssl、curl、python3 等),然后逐个输出每个节点的分享链接和二维码。

完整脚本内容如下:

BASH
#!/bin/bash
CONFIG="/etc/sing-box/config.json"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

# 1. 自动检查并安装依赖
MISSING_PKGS=""
for pkg in qrencode jq openssl curl python3 python3-cryptography base64; do
  command -v $pkg &>/dev/null || dpkg -s $pkg &>/dev/null || MISSING_PKGS="$MISSING_PKGS $pkg"
done
if [ -n "$MISSING_PKGS" ]; then
  echo -e "${YELLOW}正在安装缺失的依赖: $MISSING_PKGS${NC}"
  apt-get update -qq && apt-get install -y $MISSING_PKGS coreutils &>/dev/null
fi

[ -f "$CONFIG" ] || { echo -e "${RED}错误:找不到配置文件 $CONFIG${NC}"; exit 1; }

# 2. 清理 JSON 单行注释(逐字符扫描,避开双引号字符串内的 //,如 https://)
CLEAN_JSON=$(python3 - "$CONFIG" << 'PYEOF_INNER'
import sys
text = open(sys.argv[1], encoding='utf-8').read()
out = []
i, n = 0, len(text)
in_str = False
while i < n:
    c = text[i]
    if in_str:
        out.append(c)
        if c == '\\' and i + 1 < n:
            out.append(text[i + 1])
            i += 2
            continue
        elif c == '"':
            in_str = False
    else:
        if c == '"':
            in_str = True
            out.append(c)
        elif c == '/' and i + 1 < n and text[i + 1] == '/':
            while i < n and text[i] != '\n':
                i += 1
            continue
        else:
            out.append(c)
    i += 1
print(''.join(out))
PYEOF_INNER
)

# 3. 获取服务器 IP
SERVER_IP=$(curl -s4 --max-time 5 ifconfig.me || curl -s4 --max-time 5 ip.sb)

echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN}     Sing-box 终极节点自动导出工具      ${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""

# X25519 公钥推导函数 (REALITY 必备)
derive_pubkey() {
  python3 - "$1" << 'PYEOF'
import sys, base64
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
try:
    priv_b64 = sys.argv[1].replace('-', '+').replace('_', '/')
    priv_b64 += '=' * (-len(priv_b64) % 4)
    priv_key = X25519PrivateKey.from_private_bytes(base64.b64decode(priv_b64))
    pub_bytes = priv_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
    print(base64.urlsafe_b64encode(pub_bytes).rstrip(b'=').decode())
except Exception:
    pass
PYEOF
}

# 核心安全组件:urlencode 函数(修复密码含 @ # ? 等符号破坏 URI 的问题)
urlencode() {
  local LC_ALL=C
  for (( i=0; i<${#1}; i++ )); do
    local c="${1:i:1}"
    case "$c" in
      [a-zA-Z0-9.~_-]) printf "$c" ;;
      *) printf '%%%02X' "'$c" ;;
    esac
  done
}

# 遍历所有入站节点。users 里有多个人的入站会被拆成「一人一条」:
#   2 个用户 → 输出 2 条记录,tag 后面各自补上 name,users 只留他自己那一个
#   只有 1 个用户的入站原样通过,tag 不变(和以前的行为完全一致)
echo "$CLEAN_JSON" | jq -c '.inbounds[]?
  | if (.users? | type) == "array" and (.users | length) > 1
    then (.users | to_entries[]) as $e
         | (.users = [$e.value]
            | .tag = "\(.tag)-\($e.value.name // "user\($e.key + 1)")")
    else .
    end' | while read -r node; do
  TYPE=$(echo "$node" | jq -r '.type // empty')
  TAG=$(echo "$node" | jq -r '.tag // "Unknown-Node"')
  PORT=$(echo "$node" | jq -r '.listen_port // empty')
  
  [ -z "$TYPE" ] && continue

  # ================= 1. VLESS =================
  if [ "$TYPE" == "vless" ]; then
    UUID=$(echo "$node" | jq -r '.users[0].uuid // empty')
    ENC_UUID=$(urlencode "$UUID")
    FLOW=$(echo "$node" | jq -r '.users[0].flow // empty')
    TRANSPORT=$(echo "$node" | jq -r '.transport.type // "tcp"')
    HAS_TLS=$(echo "$node" | jq -r '.tls.enabled // "false"')
    HAS_REALITY=$(echo "$node" | jq -r '.tls.reality.enabled // "false"')
    SERVER_NAME=$(echo "$node" | jq -r '.tls.server_name // empty')
    FP=$(echo "$node" | jq -r '.tls.utls.fingerprint // "chrome"')

    QUERY="type=${TRANSPORT}&encryption=none"

    if [ "$HAS_REALITY" == "true" ]; then
      PRIVKEY=$(echo "$node" | jq -r '.tls.reality.private_key // empty')
      PBK=$(derive_pubkey "$PRIVKEY")
      SNI=$(echo "$node" | jq -r '.tls.reality.handshake.server // empty')
      SID=$(echo "$node" | jq -r '.tls.reality.short_id[0] // empty')
      
      [ -z "$PBK" ] && { echo -e "${RED}[跳过] $TAG (VLESS):public key 推导失败${NC}"; continue; }
      QUERY="${QUERY}&security=reality&pbk=${PBK}&sni=${SNI}&sid=${SID}&fp=${FP}"
      [ "$FLOW" != "null" ] && [ -n "$FLOW" ] && QUERY="${QUERY}&flow=${FLOW}"
      HOST_ADDR=$SERVER_IP

    elif [ "$HAS_TLS" == "true" ]; then
      QUERY="${QUERY}&security=tls&sni=${SERVER_NAME}&fp=${FP}"
      [ "$FLOW" != "null" ] && [ -n "$FLOW" ] && QUERY="${QUERY}&flow=${FLOW}"
      HOST_ADDR=${SERVER_NAME:-$SERVER_IP}
    else
      QUERY="${QUERY}&security=none"
      HOST_ADDR=$SERVER_IP
    fi

    if [ "$TRANSPORT" == "ws" ]; then
      WS_PATH=$(echo "$node" | jq -r '.transport.path // "/"')
      WS_HOST=$(echo "$node" | jq -r '.transport.headers.Host // empty')
      QUERY="${QUERY}&path=$(urlencode "$WS_PATH")"
      [ "$WS_HOST" != "null" ] && [ -n "$WS_HOST" ] && QUERY="${QUERY}&host=${WS_HOST}"
    elif [ "$TRANSPORT" == "grpc" ]; then
      SVC_NAME=$(echo "$node" | jq -r '.transport.service_name // empty')
      QUERY="${QUERY}&serviceName=$(urlencode "$SVC_NAME")"
    fi

    LINK="vless://${ENC_UUID}@${HOST_ADDR}:${PORT}?${QUERY}#$(urlencode "$TAG")"
    echo -e "${GREEN}[VLESS] $TAG${NC} (传输: $TRANSPORT)"

  # ================= 2. Hysteria2 =================
  elif [ "$TYPE" == "hysteria2" ]; then
    PASS=$(echo "$node" | jq -r '.users[0].password // empty')
    ENC_PASS=$(urlencode "$PASS")
    UP=$(echo "$node" | jq -r '.up_mbps // empty')
    DOWN=$(echo "$node" | jq -r '.down_mbps // empty')
    
    OBFS_TYPE=$(echo "$node" | jq -r '.obfs?.type // empty')
    OBFS_PASS=$(echo "$node" | jq -r '.obfs?.password // empty')
    ENC_OBFS_PASS=$(urlencode "$OBFS_PASS")
    
    NODE_SNI=$(echo "$node" | jq -r '.tls.server_name // empty')
    if [ -z "$NODE_SNI" ] || [ "$NODE_SNI" == "null" ]; then
      CERT=$(echo "$node" | jq -r '.tls.certificate_path // empty')
      if [ -n "$CERT" ] && [ "$CERT" != "null" ] && [ -f "$CERT" ]; then
        NODE_SNI=$(openssl x509 -in "$CERT" -noout -subject 2>/dev/null | sed 's/.*CN\s*=\s*//' | tr -d ' ')
      fi
    fi

    QUERY=""
    [ -n "$NODE_SNI" ] && [ "$NODE_SNI" != "null" ] && QUERY="sni=${NODE_SNI}&insecure=0" || QUERY="insecure=1"
    [ -n "$UP" ] && [ "$UP" != "null" ] && [ "$UP" != "0" ] && QUERY="${QUERY}&up=${UP}"
    [ -n "$DOWN" ] && [ "$DOWN" != "null" ] && [ "$DOWN" != "0" ] && QUERY="${QUERY}&down=${DOWN}"
    
    if [ -n "$OBFS_TYPE" ] && [ "$OBFS_TYPE" != "null" ]; then
      QUERY="${QUERY}&obfs=${OBFS_TYPE}&obfs-password=${ENC_OBFS_PASS}"
    fi

    HOST_ADDR=${NODE_SNI:-$SERVER_IP}
    [ -z "$NODE_SNI" ] || [ "$NODE_SNI" == "null" ] && HOST_ADDR=$SERVER_IP

    LINK="hy2://${ENC_PASS}@${HOST_ADDR}:${PORT}?${QUERY}#$(urlencode "$TAG")"
    echo -e "${GREEN}[Hysteria2] $TAG${NC}"
    [ -n "$OBFS_TYPE" ] && [ "$OBFS_TYPE" != "null" ] && echo -e "  包含混淆: $OBFS_TYPE"

  # ================= 3. Trojan =================
  elif [ "$TYPE" == "trojan" ]; then
    PASS=$(echo "$node" | jq -r '.users[0].password // empty')
    ENC_PASS=$(urlencode "$PASS")
    SNI=$(echo "$node" | jq -r '.tls.server_name // empty')
    [ -z "$SNI" ] || [ "$SNI" == "null" ] && SNI="$SERVER_IP"
    TRANSPORT=$(echo "$node" | jq -r '.transport.type // "tcp"')

    QUERY="sni=${SNI}"
    
    if [ "$TRANSPORT" == "ws" ]; then
       WS_PATH=$(echo "$node" | jq -r '.transport.path // "/"')
       WS_HOST=$(echo "$node" | jq -r '.transport.headers.Host // empty')
       QUERY="${QUERY}&type=ws&path=$(urlencode "$WS_PATH")"
       [ "$WS_HOST" != "null" ] && [ -n "$WS_HOST" ] && QUERY="${QUERY}&host=${WS_HOST}"
    elif [ "$TRANSPORT" == "grpc" ]; then
       SVC_NAME=$(echo "$node" | jq -r '.transport.service_name // empty')
       QUERY="${QUERY}&type=grpc&serviceName=$(urlencode "$SVC_NAME")"
    fi

    LINK="trojan://${ENC_PASS}@${SNI}:${PORT}?${QUERY}#$(urlencode "$TAG")"
    echo -e "${GREEN}[Trojan] $TAG${NC} (传输: $TRANSPORT)"

  # ================= 4. Shadowsocks =================
  elif [ "$TYPE" == "shadowsocks" ]; then
    METHOD=$(echo "$node" | jq -r '.method // empty')
    # 2022 系列多用户时,客户端密码是「服务端 PSK:用户 PSK」两段拼起来
    PASS=$(echo "$node" | jq -r 'if (.users? | type) == "array" and (.users | length) > 0
      then "\(.password):\(.users[0].password)" else .password end // empty')
    
    B64_CRED=$(echo -n "${METHOD}:${PASS}" | base64 | tr -d '\n\r' | tr '+/' '-_' | tr -d '=')
    LINK="ss://${B64_CRED}@${SERVER_IP}:${PORT}#$(urlencode "$TAG")"
    echo -e "${GREEN}[Shadowsocks] $TAG${NC}"

  # ================= 5. TUIC v5 =================
  elif [ "$TYPE" == "tuic" ]; then
    UUID=$(echo "$node" | jq -r '.users[0].uuid // empty')
    PASS=$(echo "$node" | jq -r '.users[0].password // empty')
    ENC_UUID=$(urlencode "$UUID")
    ENC_PASS=$(urlencode "$PASS")
    CC=$(echo "$node" | jq -r '.congestion_control // "bbr"')
    ALPN=$(echo "$node" | jq -r 'if (.tls.alpn|type)=="array" then (.tls.alpn|join(",")) else (.tls.alpn // "h3") end')
    NODE_SNI=$(echo "$node" | jq -r '.tls.server_name // empty')
    if [ -z "$NODE_SNI" ] || [ "$NODE_SNI" == "null" ]; then
      CERT=$(echo "$node" | jq -r '.tls.certificate_path // empty')
      if [ -n "$CERT" ] && [ "$CERT" != "null" ] && [ -f "$CERT" ]; then
        NODE_SNI=$(openssl x509 -in "$CERT" -noout -subject 2>/dev/null | sed 's/.*CN\s*=\s*//' | tr -d ' ')
      fi
    fi
    if [ -n "$NODE_SNI" ] && [ "$NODE_SNI" != "null" ]; then
      QUERY="congestion_control=${CC}&alpn=$(urlencode "$ALPN")&sni=${NODE_SNI}&udp_relay_mode=native"
      HOST_ADDR=$NODE_SNI
    else
      QUERY="congestion_control=${CC}&alpn=$(urlencode "$ALPN")&allow_insecure=1&udp_relay_mode=native"
      HOST_ADDR=$SERVER_IP
    fi
    LINK="tuic://${ENC_UUID}:${ENC_PASS}@${HOST_ADDR}:${PORT}?${QUERY}#$(urlencode "$TAG")"
    echo -e "${GREEN}[TUIC v5] $TAG${NC}"

  # ================= 6. AnyTLS =================
  elif [ "$TYPE" == "anytls" ]; then
    PASS=$(echo "$node" | jq -r '.users[0].password // empty')
    ENC_PASS=$(urlencode "$PASS")
    SNI=$(echo "$node" | jq -r '.tls.server_name // empty')
    FP=$(echo "$node" | jq -r '.tls.utls.fingerprint // "chrome"')
    HOST_ADDR=${SNI:-$SERVER_IP}
    if [ -n "$SNI" ] && [ "$SNI" != "null" ]; then
      QUERY="sni=${SNI}&insecure=0&fp=${FP}"
    else
      QUERY="insecure=1&fp=${FP}"
    fi
    LINK="anytls://${ENC_PASS}@${HOST_ADDR}:${PORT}?${QUERY}#$(urlencode "$TAG")"
    echo -e "${GREEN}[AnyTLS] $TAG${NC}"

  else
    echo -e "${YELLOW}[忽略] $TAG:暂不支持 '$TYPE' 协议生成分享链接。${NC}"
    echo ""
    continue
  fi

  # 输出链接与二维码
  echo "$LINK"
  echo -e "${CYAN}扫码:${NC}"
  qrencode -t ansiutf8 "$LINK"
  echo ""

done

echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN}            节点导出完毕                ${NC}"
echo -e "${CYAN}========================================${NC}"
配置文件路径:脚本默认读取 /etc/sing-box/config.json,如果你的配置文件在别处,请修改脚本开头的 CONFIG 变量。
Reality 公钥:VLESS Reality 节点的分享链接需要公钥,脚本会用 Python 从私钥自动推导,无需手动填写。
多用户入站:如果一个入站有多个用户,脚本会为每个用户生成一条独立链接,tag 后面自动加上用户名后缀。