工具脚本
节点分享脚本
读取 sing-box 服务端配置,自动生成 VLESS / Hysteria2 / Trojan / SS / TUIC / AnyTLS 分享链接与二维码。
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 后面自动加上用户名后缀。