Sing-box 节点搭建全流程
从一台空白的 Debian / Ubuntu 服务器开始,走完系统加固、防火墙、TLS 证书、 服务端配置、节点分发和排错的全过程。七种协议的服务端与客户端配置成对给出, 复制粘贴即可运行,只需要替换高亮标出的几处自己的信息。
先把边界划清楚:这份教程做什么、需要你准备什么、以及哪些内容是必须自己动手改的。
你会得到什么
一台同时跑七种协议节点的服务器,一条命令生成全部分享链接与二维码, 以及一套按用户或按端口分流到不同住宅落地的链式中转。
需要多久
不含域名解析生效等待,全程约 30 分钟。只用 Reality 一种协议的话 10 分钟以内。
开工清单
七种协议怎么选
| 协议 | 传输层 | 要域名 | 本教程端口 | 适用场景 |
|---|---|---|---|---|
| VLESS + Reality | TCP / TLS | 不需要 | 443 | 无需证书,借用真实网站的 TLS 指纹,最难识别,新手首选 |
| VLESS + WS + TLS | WebSocket | 需要 | 8443 | 唯一能套 Cloudflare CDN 的方案,可隐藏 VPS 真实 IP |
| Trojan | TCP / TLS | 需要 | 9443 | 伪装成普通 HTTPS,实现成熟,客户端支持面最广 |
| Shadowsocks 2022 | TCP / UDP | 不需要 | 80 | 全加密无明文特征,协议开销最小,适合做备用线路 |
| Hysteria2 | QUIC / UDP | 可选 | 5678 | 高丢包链路上速度优势明显,移动网络体验最好 |
| TUIC v5 | QUIC / UDP | 可选 | 7788 | 基于 QUIC,握手快、延迟低,适合游戏和实时应用 |
| AnyTLS | TLS | 需要 | 10443 | 缓解 TLS-in-TLS 特征,需要 sing-box 1.12+ 双端 |
关于域名与证书
需要证书的协议共用一张通配符证书,第 7 节用 Cloudflare DNS 验证一次签发, 因此域名必须托管在 Cloudflare(NS 指向 CF)。一张 *.yourdomain.com 覆盖下面全部子域名,后续加节点不用重新签发。
| 子域名 | Cloudflare 云朵 | 解析到 | 给谁用 |
|---|---|---|---|
| node.yourdomain.com | 灰色(DNS only) | VPS 真实 IP | Trojan / Hysteria2 / TUIC / AnyTLS |
| cf.yourdomain.com | 橙色(Proxied) | VPS 真实 IP | VLESS + WS,走 CDN 隐藏真实 IP |
server_name 写成橙云域名,
握手会被 Cloudflare 拦下;把 WS 节点写成灰云域名,则等于没套 CDN。全文已按上表区分,
照抄时留意别改错。需要你替换的内容
全文只用一种标记:黄色高亮就是必须换成你自己信息的地方, 包括 YOUR_SERVER_IP、yourdomain.com、 各协议的 UUID 与密码。没有高亮的部分可以原样使用。
Esc
(或 Ctrl/⌘ + Enter)退出编辑态。改动只存在当前这个浏览器标签页里:点还原回到教程原文, 关掉页面再打开也一律是原始内容——不会有半份改过的教程留在那儿误导你。 所以真正要长期保存的配置,请存到 VPS 上或你自己的笔记里,别指望这个页面。 终端输出示例(浅色的那种框)不给编辑,那些是给你对照结果的,不是拿来执行的。
本教程约定:能一条命令做完的写成代码块,需要判断的写成提示框, 可能出错的写成第 13 节的折叠排错项。
先用 SSH 登进服务器,后面所有命令都在这个终端里执行。
把高亮部分换成你的公网 IP。首次连接会提示确认主机指纹,输入 yes 回车。
# 默认 22 端口 ssh root@YOUR_SERVER_IP # 服务商给了非标准端口时,用 -p 指定 ssh -p 22222 root@YOUR_SERVER_IP
提示符变成 root@主机名:~# 就说明登录成功。顺手确认一下系统版本和架构。
cat /etc/os-release | head -n 2 uname -m
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)" NAME="Debian GNU/Linux" x86_64
ssh 命令,直接在 PowerShell 里执行即可,
不需要额外装 PuTTY。若提示找不到命令,在「设置 → 应用 → 可选功能」里安装 OpenSSH 客户端。uname -m 输出 x86_64 或 aarch64。更新系统、装齐工具、校准时区、打开 BBR。这四件事做完,后面的步骤才不会莫名报错。
后面十几节要用到的命令,绝大多数都不在系统的默认安装里——
ufw(第 3 节的防火墙)、dig(第 6 节验证解析)、
nc(第 13 节测端口)这些没装上,到那一步会直接报
command not found。这里一次装齐,后面就不用回头补。
# 先更新索引,再升级已经装上的包 apt update && apt upgrade -y # 一次装齐全教程要用到的工具(一行一组,按用途分组) apt install -y \ ca-certificates curl wget gnupg tar unzip nano \ ufw \ socat cron logrotate \ jq qrencode openssl python3 python3-cryptography \ iproute2 bind9-dnsutils netcat-openbsd iputils-ping lsof tcpdump htop
装完可以对着这张表核一遍,每个都能说清是哪一步在用:
| 包名 | 提供的命令 | 教程里哪一步用 |
|---|---|---|
ufw | ufw | 第 3 节配置防火墙、第 4 节改完 SSH 端口后放行新端口 |
bind9-dnsutils | dig、nslookup | 第 6 节确认域名解析到本机、第 8 节 Reality 借站自查 |
netcat-openbsd | nc | 第 13 节从本地测 TCP 端口通不通 |
iproute2 | ss、ip | 第 12 节看监听端口、第 13 节查端口被谁占了 |
cron | crontab | 第 7 节 acme.sh 装完会自己写一条续期定时任务,没有 cron 服务证书就不会自动续 |
socat | socat | 本教程用 DNS-01 签证书,不需要它;只有改用 acme.sh 的 --standalone 模式才要,装上算留个退路 |
jq、qrencode、python3、python3-cryptography | jq、qrencode、python3 | 第 11 节的 vps-share 分享脚本:读配置、打二维码、从私钥反推 Reality 公钥 |
openssl | openssl | 第 5 节生成各协议的随机密码 |
curl、wget、ca-certificates、gnupg | — | 第 5 节装 sing-box(含 APT 源的签名校验)、第 7 节装 acme.sh |
nano、tar、unzip | — | 第 8 节编辑 config.json;解压备份、下载的规则集 |
logrotate | logrotate | 第 13 节给 sing-box 日志做切割,不装的话那份 /etc/logrotate.d/sing-box 写了也没人读 |
iputils-ping | ping | 第 13 节判断是被墙还是服务没起来 |
lsof、tcpdump、htop | 同名 | 不是必须,但排错到第 13 节后半段基本都会用上,先装免得临时找 |
dnsutils 在 Debian 13 已经没有了,要写 bind9-dnsutils;
netcat 从 Debian 12 起就不是有效包名,要写 netcat-openbsd;
apt-transport-https 现在是个空的过渡包,不要装,
APT 早就内置 https 支持了。iptables、procps,不是漏了
iptables 是 ufw 的依赖,装 ufw 时自动带上;
sysctl 来自 procps,那是 Priority: required 的包,
任何一个能开机的系统上都有。
logrotate 和 iputils-ping 是 Priority: important,
正常的 Debian / Ubuntu 云镜像里本来就装好了,
上面仍然把它们写进去,是因为少数服务商提供的 minimal 镜像会把这一档裁掉——
写上去顶多是「已是最新版」一行输出,不写就可能到第 13 节才发现少东西。ufw 和 iptables-persistent 不能共存
在 Debian 12 / 13 与 Ubuntu 24.04 上,ufw 的包元数据里写着
Breaks: iptables-persistent, netfilter-persistent,
所以 apt 不会让两边同时装着——装 ufw 时如果机器上已经有
netfilter-persistent,apt 会提示把它移除,
请先看清 The following packages will be REMOVED: 那一行再回车。
这也意味着:本教程不需要、也不要手动去装 iptables-persistent。ufw 从上面那条命令里删掉
OCI 平台镜像靠一批预置 iptables 规则挂载自己的系统盘,
Oracle 官方的 Known Issues 明确写了启用 ufw 可能清掉这些规则,
导致实例下次重启开不了机,并要求
Do not use UFW to edit firewall rules。
那台机器的防火墙走第 3 节末尾折叠里的
/etc/iptables/rules.v4 做法。apt upgrade 过程中若弹出蓝底的「需要重启哪些服务」
或「保留还是替换配置文件」对话框,前者选默认(回车),后者选
keep the local version。升级内核后提示需要重启就执行
reboot,重新登录继续,不影响后面的步骤。Reality 与 TLS 握手对时间偏差敏感,服务器时间不准会直接导致连接失败。 时区本身统一用 UTC:日志时间戳、证书有效期、服务商后台的记录 都按 UTC 记,不换算就能对上;本教程后面所有示例输出里的时间也都是 UTC。
# 设为世界时间 UTC(多数 VPS 镜像出厂就是,跑一遍图个确定) timedatectl set-timezone UTC # 打开网络时间同步 timedatectl set-ntp true # 确认 System clock synchronized: yes timedatectl status
Local time: Sat 2026-09-05 12:30:15 UTC Universal time: Sat 2026-09-05 12:30:15 UTC RTC time: Sat 2026-09-05 12:30:15 Time zone: UTC (UTC, +0000) System clock synchronized: yes NTP service: active RTC in local TZ: no
timedatectl 在真实终端里是把左边的标签右对齐成一列的,
上面为了和本文其它输出块统一,改成了标签左顶格、值对齐——只是排版差异,内容一致。
Time zone 显示 Etc/UTC 而不是 UTC——
同一个时区的两个名字,偏移都是 +0000,不用管。
set-ntp true 报 NTP not supported——
这台机器没装时间同步服务,执行
apt install -y systemd-timesyncd 后再跑一遍;
但如果它上面已经在跑 chrony 或 ntpsec,
就别装(会把原有的顶掉),改用 chronyc tracking
确认同步正常即可。BBR 能显著改善跨境链路的吞吐。写成 /etc/sysctl.d/ 下的独立文件,重复执行不会产生重复配置。
cat > /etc/sysctl.d/99-bbr.conf <<'EOF' net.core.default_qdisc = fq net.ipv4.tcp_congestion_control = bbr net.ipv4.tcp_fastopen = 3 EOF # 立即生效 sysctl --system >/dev/null # 验证:两条都应输出 bbr / fq sysctl net.ipv4.tcp_congestion_control sysctl net.core.default_qdisc
net.ipv4.tcp_congestion_control = bbr net.core.default_qdisc = fq
/etc/sysctl.conf
用 cat >> 追加到主配置文件,每执行一次就多一份重复条目,
时间久了很难看出到底哪一行在生效。/etc/sysctl.d/ 下的独立文件可以直接覆盖重写,
也方便删掉回退。tcp_congestion_control 输出 bbr,
timedatectl 显示时间已同步。先放行端口再启用防火墙,顺序反了会把自己锁在门外。
ufw allow 放行 SSH 端口,再 ufw enable。
反过来执行会立刻断开当前 SSH 连接,且无法再登录,只能去服务商控制台用 VNC 救援。ufw 是 Oracle 官方列出的已知问题,
会导致实例下次重启开不了机。那台机器请跳过本节的 ufw 步骤,
直接看最后那个折叠里的 /etc/iptables/rules.v4 做法。下面只放行两个口:SSH 的 22 和第 8 节默认要用的 443。
后面每启用一个协议,再照着同样的格式补一条——
UDP 类协议(Hysteria2 / TUIC)别漏了 /udp,协议写错等于没放行。
# SSH:最先放行,务必确认端口号正确 ufw allow 22/tcp # TCP 口写法示例:443 是第 8 节 VLESS + Reality 的端口 ufw allow 443/tcp # UDP 口写法示例:5678 是 Hysteria2 的端口,用到才加 ufw allow 5678/udp # 启用防火墙。在 SSH 里执行会先问一句,输入 y 回车 # Command may disrupt existing ssh connections. Proceed with operation (y|n)? # 不想被问就写 ufw --force enable(--force 必须放在 enable 前面) ufw enable # 核对规则列表 ufw status verbose
Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), disabled (routed) New profiles: skip To Action From -- ------ ---- 22/tcp ALLOW IN Anywhere 443/tcp ALLOW IN Anywhere 5678/udp ALLOW IN Anywhere 22/tcp (v6) ALLOW IN Anywhere (v6) 443/tcp (v6) ALLOW IN Anywhere (v6) 5678/udp (v6) ALLOW IN Anywhere (v6)
ufw allow 443/tcp 会同时生成 v4 和 v6 两条规则,
带 (v6) 后缀的那条就是 IPv6 版本,
命令本身也会回两行 Rules updated / Rules updated (v6)。
另外 Default: 里的 disabled (routed) 表示这台机器没开转发,
全新 VPS 的正常状态;New profiles: skip 是 ufw 的出厂默认值,
不用改。22 和 443,
第 10 节挑了别的协议再回来补对应的一条 ufw allow。
删一条规则用 ufw delete allow 5678/udp,
写法要和当初放行时一字不差才能删掉。需要一次开一整段时(典型场景是 Hysteria2 的端口跳跃,
客户端在一段 UDP 口里轮换,用来对抗按端口的限速),
用冒号连接起止端口。端口段必须带协议,
写成 ufw allow 40000:50000 会被 ufw 拒绝。
# 放行 40000-50000 这一段 UDP ufw allow 40000:50000/udp # TCP 段同理 ufw allow 40000:50000/tcp # 删掉时写法要和放行时完全一致 ufw delete allow 40000:50000/udp
40000:50000/udp 等于放行一万个口,
以后这台机器上任何监听在这个范围内的程序都会直接暴露到公网。
不做端口跳跃就不要开;真要开,把范围压到实际用得到的宽度,
并且记住第 8 节的配置里也要同步写上对应的跳跃范围,两边不一致就是不通。放行之后启动 sing-box 前先看一眼:有输出说明端口已被别的程序占用,
需要先停掉对应服务,或者把节点换到别的端口。
80 和 443 最容易冲突——很多 VPS 镜像预装了 nginx 或 apache。
把括号里的端口换成你实际要用的那几个:
# TCP ss -tlnp | grep -E ':(80|443)\b' # UDP ss -ulnp | grep -E ':(5678)\b'
还有一层:服务商安全组
ufw 放行了但安全组没放行,端口依然不通,而且客户端只会显示超时,
很难看出原因。DigitalOcean、Vultr、Hetzner 默认不开安全组,可以跳过这一步。| 服务商 | 控制台位置 | 注意 |
|---|---|---|
| 阿里云 / 腾讯云 | 实例详情 → 安全组 → 配置规则 → 入方向 | TCP 与 UDP 要分别加规则 |
| AWS EC2 | 实例 → Security groups → Inbound rules | 默认只放行 22,其余全部要手动加 |
| Oracle Cloud | VCN → 子网 → 安全列表 → 入站规则 | Ubuntu 镜像不能用 ufw,见本节末的折叠 |
| Google Cloud | VPC 网络 → 防火墙 → 创建规则 | 需要给实例打对应的网络标记 |
Oracle Cloud 的 Ubuntu 镜像:不要用 ufw,用 iptables 会开不了机
169.254.0.2:3260 与 169.254.2.0/24:3260)。
ufw 可能把这些规则清掉,于是下次重启时实例挂不上系统盘,直接开不了机。
官方给的处置是一句话:Do not use UFW to edit firewall rules。所以在 Oracle Cloud 的 Ubuntu 实例上,第 2 节不要装 ufw、第 3 节整节跳过,
改成直接编辑官方指定的规则文件 /etc/iptables/rules.v4。
先看一眼现状:
iptables -L INPUT -n --line-numbers nano /etc/iptables/rules.v4
出厂文件的 INPUT 段大致是下面这样。要放行的端口
必须插在最后那条 REJECT 之前——
插在它后面等于没写,而且客户端看到的报错是
no route to host,很难联想到是顺序问题:
# 下面几行是镜像自带的,一行都不要删 -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT -A INPUT -p icmp -j ACCEPT -A INPUT -i lo -j ACCEPT -A INPUT -p udp --sport 123 -j ACCEPT -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT # ↓↓↓ 新增的放行规则加在这里,也就是 REJECT 之前 ↓↓↓ -A INPUT -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT -A INPUT -p udp -m state --state NEW -m udp --dport 5678 -j ACCEPT -A INPUT -j REJECT --reject-with icmp-host-prohibited
存盘后让它立即生效(官方给的就是这一条命令,不需要
netfilter-persistent save;文件本身就是开机加载的那份):
iptables-restore < /etc/iptables/rules.v4 iptables -L INPUT -n --line-numbers
iptables -F INPUT——那会连 iSCSI 那几条一起冲掉,
后果和启用 ufw 一样。
② IPv6 是另一份文件 /etc/iptables/rules.v6,要放行 IPv6 就同样改它。
③ 系统防火墙放行完,VCN 安全列表的入站规则还要单独加一遍,
两层都通了端口才通。默认 22 端口每天会被扫上千次。改成高位端口并关闭密码连接、改用密钥登录,能挡掉绝大多数自动化爆破,这一步可以跳过[cite: 1]。
如果你本机已经有 SSH 密钥(通常在本地电脑的 ~/.ssh/id_rsa.pub 或 ~/.ssh/id_ed25519.pub),可以直接复制其内容备用,跳过下面的生成步骤。如果没有密钥,请先在本机生成:
# 生成密钥(如提示路径和密码,一路回车默认即可) ssh-keygen -t ed25519
将本机的公钥添加到服务器,有以下两种方式。完成之后请测试 ssh root@YOUR_SERVER_IP 是否能免密直接进入服务器。
如果你使用类似 FinalShell、Xshell 等带有可视化文件管理的 SSH 客户端,可以按以下步骤操作:
- 复制本机公钥:在本机终端执行
cat ~/.ssh/id_ed25519.pub,复制输出的完整公钥内容(形如ssh-ed25519 AAAA...)。 - 创建认证文件夹:在服务器上进入
/root/目录。新建名为.ssh的文件夹(若已有则跳过),并将文件夹权限设置为700。 - 写入公钥并设置权限:在
.ssh文件夹下,新建或打开名为authorized_keys的文件。将刚才复制的公钥内容粘贴进去,保存并退出。最后将该文件的权限设置为600。
# 将公钥传到服务器(期间需要输入当前服务器 root 密码) ssh-copy-id root@YOUR_SERVER_IP
顺序同样重要:先让新端口通,再改配置[cite: 1]。这样即使改错了,旧连接还在,可以退回去[cite: 1]。
ufw allow YOUR_SSH_PORT/tcp ufw status | grep YOUR_SSH_PORT
ufw 放行不等于它放行[cite: 1]。AWS 的默认安全组只开 22,
新端口不去入方向加规则,下一步验证时一定连不上,
而且现象跟「sshd 没改对」一模一样,很容易查错方向[cite: 1]。
先去控制台加 YOUR_SSH_PORT/tcp 入方向规则,再往下走[cite: 1]。ufw,上面那条会是 command not found[cite: 1]。
改成在 /etc/iptables/rules.v4 里、
紧挨着已有那条 22 端口 ACCEPT 的下面补一行
-A INPUT -p tcp --dport YOUR_SSH_PORT -j ACCEPT,
然后 iptables-restore < /etc/iptables/rules.v4[cite: 1]。
看到 command not found 千万别当成无关紧要跳过——
新端口没放行就重启 sshd,等于把自己关在门外[cite: 1]。此处需要完成两件独立的事:① 修改 SSH 端口;② 关闭密码连接及 root 密码连接。
通过 SSH 工具的可视化文件管理功能,进入 /etc/ssh/ 文件夹,打开 sshd_config 文件进行编辑:
- 修改端口:在文件中找到带有
Port 22的行(前面可能被#号注释掉了),将其改为Port YOUR_SSH_PORT,并确保删除了开头的#号使其生效。 - 关闭密码连接:找到
PasswordAuthentication yes的行,将其改为PasswordAuthentication no,并去掉开头的#号。 - 关闭 root 密码连接:找到
PermitRootLogin yes的行,将其改为PermitRootLogin prohibit-password,并去掉开头的#号。
确认无误后,保存文件并退出。
# 1. 备份原文件 cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak # 2. 修改端口(-E 用扩展正则,^#? 兼容注释与非注释) sed -i -E 's/^#?Port +22$/Port YOUR_SSH_PORT/' /etc/ssh/sshd_config # 3. 关闭密码连接与 root 密码连接 sed -i -E 's/^#?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config sed -i -E 's/^#?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config # 4. 确认改成功了,应输出新端口与关闭密码的配置 grep -E '^(Port|PasswordAuthentication|PermitRootLogin)' /etc/ssh/sshd_config
配置修改完成后,进行语法校验并重启服务:
# 1. 语法校验,无输出表示通过(这一步不能跳)[cite: 1] sshd -t # 2. 校验通过后再重启[cite: 1] systemctl restart ssh || systemctl restart sshd # 3. 必须确认新端口真的监听上了(这一步比第 4 步更重要)[cite: 1] ss -ltnp | grep -E ':(22|YOUR_SSH_PORT)\b'
:22、没有新端口 → 是 socket 激活在管端口
Debian 13 与 Ubuntu 24.04 的 openssh 默认由 ssh.socket 决定监听端口,
端口写在它的 ListenStream= 里,
此时 sshd_config 的 Port 被完全忽略[cite: 1]。
最坑的是前面每一步都「成功」:grep ^Port 有输出、
sshd -t 无报错、restart 也不报错,只有第 3 步能看出来[cite: 1]。
先确认是不是这种情况:systemctl is-enabled ssh.socket
返回 enabled 就是[cite: 1]。# 用 drop-in 改端口。第一行空的 ListenStream= 是清空官方单元里的 22, # 不写这一行就变成 22 和 YOUR_SSH_PORT 同时监听[cite: 1] mkdir -p /etc/systemd/system/ssh.socket.d cat > /etc/systemd/system/ssh.socket.d/port.conf <<'EOF' [Socket] ListenStream= ListenStream=YOUR_SSH_PORT EOF systemctl daemon-reload systemctl restart ssh.socket # 再查一遍,这次应该只剩新端口[cite: 1] ss -ltnp | grep -E ':(22|YOUR_SSH_PORT)\b'
systemctl disable --now ssh.socket && systemctl enable --now ssh.service,
之后端口就回到 sshd_config 说话[cite: 1]。
两条路选一条即可,官方单元里
ssh.socket 和 ssh.service 是 Conflicts= 关系,
不会同时生效[cite: 1]。不要关掉当前窗口[cite: 1]。另开一个终端用新端口和密钥登录, 成功之后再回来关旧窗口,并撤掉 22 端口[cite: 1]。
# 本机新终端里执行[cite: 1] ssh -p YOUR_SSH_PORT root@YOUR_SERVER_IP # 新端口能免密登录后,在服务器上撤掉 22[cite: 1] ufw delete allow 22/tcp
加官方 APT 源装稳定版,顺手生成后面配置里要用的 UUID、Reality 密钥和密码。
官方给了两种装法,这里用软件源那种:装完之后
apt 就能管升级,第 12 节的日常升级命令才成立。
三条命令分别是放签名密钥、写源文件、刷新索引。
# 1. 放官方签名公钥(deb822 的 Signed-By 直接吃 ASCII armored 的 .asc) mkdir -p /etc/apt/keyrings curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc chmod a+r /etc/apt/keyrings/sagernet.asc # 2. 写源文件(deb822 格式,注意扩展名是 .sources 而不是 .list) cat > /etc/apt/sources.list.d/sagernet.sources <<'EOF' Types: deb URIs: https://deb.sagernet.org/ Suites: * Components: * Enabled: yes Signed-By: /etc/apt/keyrings/sagernet.asc EOF # 3. 刷新索引并安装稳定版 apt update apt install -y sing-box
systemd-sysusers 建出
sing-box 这个系统用户,不会 enable、
也不会 start 服务。
所以现在执行 systemctl status sing-box 应该看到
inactive (dead),这是正常的:
第 8 节写配置,第 12 节才正式 enable --now 启动。:: 的 shadowsocks 入站,端口 8080,
密码是写死在官方公开仓库里的固定值。
这时候要是自己 systemctl start sing-box,
等于对外开了一个「密码全世界都知道」的代理。两道保险:一是第 3 节的防火墙先做完了,8080 并不在放行名单里,外面连不进来; 二是第 8 节会把这个文件整份覆盖掉。在第 8 节写完自己的配置之前,不要启动服务。 这也正是本教程把防火墙放在装 sing-box 之前的原因。
/etc/sing-box/config.json 是 dpkg 的 conffile
官方打包时把它登记成了「配置文件」,dpkg 因此会记住它的校验和。
以后第 12 节升级 sing-box 时,只要官方那份默认配置在两个版本之间变过,
而你又改过本地文件,dpkg 就会停下来问你留哪一份(没变过就不会问,直接保留你的)。
真被问到时长这样,直接回车即可——默认就是保留你自己的配置:Configuration file '/etc/sing-box/config.json'
==> Modified (by you or by a script) since installation.
==> Package distributor has shipped an updated version.
What would you like to do about it ? Your options are:
Y or I : install the package maintainer's version
N or O : keep your currently-installed version
D : show the differences between the versions
Z : start a shell to examine the situation
The default action is to keep your current version.
*** config.json (Y/I/N/O/D/Z) [default=N] ?
Y
Y / I 是「用打包者的版本」,
一按下去你整套服务端配置就被那份 8080 演示配置替换掉,节点当场全部失效。
要留自己的就选 N(或 O,或者直接回车用默认值)。
想先看看官方改了什么,可以先按 D 看 diff,看完再选 N。sing-box-beta 是预览版包,和 sing-box
互斥,把第 3 步的包名换掉即可。官方的另一种装法是一次性脚本,它直接抓 GitHub Release 的 deb/rpm 装上—— 好处是不加源,代价是以后升级得再跑一遍脚本, 第 12 节的
apt 升级对它无效:
curl -fsSL https://sing-box.app/install.sh | sh,
预览版加 sh -s -- --beta,
指定版本加 sh -s -- --version 1.14.0。本教程全部在 1.14.0 上实测。
dns.servers 的 type/server 写法与
default_domain_resolver 需要 1.12 以上,
第 14 节客户端配置里的 http_clients 与第 8 节提到的 cache_file.store_dns
则是 1.14.0 才有,版本低于 1.14 那两处要跳过。
sing-box version
sing-box version 1.14.0 Environment: go1.26.7 linux/amd64 Tags: with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,… Revision: 0b8995879f29a9b98ee027bc17b75e101445b238 CGO: disabled
Tags: 真实输出是一长串(上面省略了后半段),
只要里面有 with_quic(Hysteria2 / TUIC 要用)和
with_utls(Reality 要用)就够了,官方发布的包都带。
CGO: disabled 也是官方发布包的正常特征。每条命令只生成一样东西,需要哪个跑哪一条——只想要一个 UUID,就只跑第一条。 每个代码框右上角的文件名标注了它对应第 8、9 节里哪个黄色占位符,填配置时对着抄不会串。 生成出来的值立刻复制进本地文本文件保存。
# 一个。VLESS 每个用户一个,TUIC 也要一个 sing-box generate uuid # 一次要三个就用这行 for i in 1 2 3; do sing-box generate uuid; done
sing-box generate reality-keypair
PrivateKey: YOUR_REALITY_PRIVATE_KEY PublicKey: YOUR_REALITY_PUBLIC_KEY
PrivateKey 填服务端的 reality.private_key,
PublicKey 填客户端的 reality.public_key,
而且必须是同一次命令输出的这一对。
装反或配错的症状是客户端一直连不上,服务端日志里一行
REALITY: processed invalid connection。
私钥只在这里出现一次,终端刷屏后无法找回。# 4 字节 = 8 位十六进制。允许 0-8 字节,服务端和客户端必须写同一个值 sing-box generate rand 4 --hex
# 输出形如 /1a2b3c4d5e6f7a8b。带上开头的斜杠,两端一模一样 echo "/$(openssl rand -hex 8)"
# 2022-blake3-aes-256-gcm 要求正好 32 字节的 base64,长度不对启动就会失败 sing-box generate rand 32 --base64
# Trojan / Hysteria2 / TUIC / AnyTLS 的密码,以及 Hysteria2 的 obfs 密码, # 都是任意字符串,这一条命令通用。用到几个就跑几遍,别几个协议共用同一个 openssl rand -base64 18
+ / =
这是 base64 的正常字符,填进 JSON 配置没问题。
但拼进分享链接时必须做 URL 编码——第 11 节的
vps-share 已经替你处理了,只有手写链接才需要留意。
嫌麻烦就换成 openssl rand -hex 24,输出只有十六进制字符。一次全要:整段生成 + 对齐打印 可选
七个协议全都要开、想一次把所有值凑齐时用这一段:先把每个随机值存进变量, 最后统一对齐打印。把输出完整复制到本地文本文件保存, 后面第 8 节和客户端配置都要用。
# ── 1) 生成 ────────────────────────────────── # 三个 UUID:第 8 节的 VLESS 一个用户/一个端口占一个,TUIC 复用 UUID_2 UUID_1=$(sing-box generate uuid) UUID_2=$(sing-box generate uuid) UUID_3=$(sing-box generate uuid) RKP=$(sing-box generate reality-keypair) RK_PRIV=$(echo "$RKP" | awk '/PrivateKey/{print $2}') RK_PUB=$(echo "$RKP" | awk '/PublicKey/{print $2}') SHORT_ID=$(sing-box generate rand 4 --hex) # 0-8 字节的十六进制 WS_PATH="/$(openssl rand -hex 8)" # WS 路径,两端必须一模一样 SS_PSK=$(sing-box generate rand 32 --base64) # 2022-blake3-aes-256-gcm 要 32 字节 TROJAN_PW=$(openssl rand -base64 18) HY2_PW=$(openssl rand -base64 18) HY2_OBFS=$(openssl rand -base64 18) # 可选,默认不用;要用见第 10 节 Hysteria2 的服务端栏 TUIC_PW=$(openssl rand -base64 18) ANYTLS_PW=$(openssl rand -base64 18) # ── 2) 对齐打印 ────────────────────────────── printf '%-22s %s\n' \ "UUID_1" "$UUID_1" \ "UUID_2" "$UUID_2" \ "UUID_3" "$UUID_3" \ "REALITY_PRIVATE_KEY" "$RK_PRIV" \ "REALITY_PUBLIC_KEY" "$RK_PUB" \ "SHORT_ID" "$SHORT_ID" \ "WS_PATH" "$WS_PATH" \ "SS_PSK_BASE64" "$SS_PSK" \ "TROJAN_PASSWORD" "$TROJAN_PW" \ "HY2_PASSWORD" "$HY2_PW" \ "HY2_OBFS_PASSWORD" "$HY2_OBFS" \ "TUIC_PASSWORD" "$TUIC_PW" \ "ANYTLS_PASSWORD" "$ANYTLS_PW"
UUID_1 YOUR_UUID_1 UUID_2 YOUR_UUID_2 UUID_3 YOUR_UUID_3 REALITY_PRIVATE_KEY YOUR_REALITY_PRIVATE_KEY REALITY_PUBLIC_KEY YOUR_REALITY_PUBLIC_KEY SHORT_ID YOUR_SHORT_ID WS_PATH /YOUR_WS_PATH SS_PSK_BASE64 YOUR_SS_PSK_BASE64 TROJAN_PASSWORD YOUR_TROJAN_PASSWORD HY2_PASSWORD YOUR_HY2_PASSWORD HY2_OBFS_PASSWORD YOUR_HY2_OBFS_PASSWORD TUIC_PASSWORD YOUR_TUIC_PASSWORD ANYTLS_PASSWORD YOUR_ANYTLS_PASSWORD
3f7d2b18-… 这种形状,
Reality 公私钥是 43 个字符的 base64url),
这里用占位符表示,是为了防止有人照抄示例值。sing-box version 输出 1.14.0(或更高),
并且本地已保存好一份含 UUID、Reality 公私钥、short_id 和各协议密码的清单。只用 Reality 或 Shadowsocks 的话,前面三步可以跳过。 但本节末尾「IP 被封之后」那一段值得先看一眼—— 那是唯一在 IP 被封之后还能救回来的方案,而它要求你提前把域名和证书准备好。
在 Cloudflare 添加站点后,它会给出两个 NS 地址,去域名注册商那里替换原有 NS。 生效时间通常几分钟到几小时。
在 Cloudflare 的 DNS → 记录里添加,两条都指向同一个 VPS IP, 区别只在代理状态(云朵颜色)。
| 类型 | 名称 | 内容 | 代理状态 | TTL |
|---|---|---|---|---|
| A | YOUR_DNS_NODE | YOUR_SERVER_IP | 仅 DNS(灰云) | 自动 |
| A | YOUR_DNS_CF | YOUR_SERVER_IP | 已代理(橙云) | 自动 |
灰云域名应解析到你的真实 IP;橙云域名会解析到 Cloudflare 的 IP,这是正常的。
# 灰云:应输出你的 VPS 真实 IP dig +short node.yourdomain.com # 橙云:应输出 104.x / 172.6x 等 Cloudflare 段的 IP dig +short cf.yourdomain.com
橙云和灰云的区别
| 对比项 | 灰云(仅 DNS) | 橙云(已代理) |
|---|---|---|
| 真实 IP | 公开可见 | 被 Cloudflare 隐藏 |
| 连接路径 | 客户端直连 VPS | 客户端 → CF 边缘 → VPS |
| 可用端口 | 任意 | 仅 CF 支持的 HTTPS 端口 |
| 支持的协议 | 全部 | 只有 WebSocket / gRPC 等 HTTP 系传输 |
| 延迟 | 取决于直连质量 | 可用「优选 IP」改善,但多一跳 |
443、2053、2083、2087、
2096、8443。本教程 WS 节点用 8443 正好在列表内。
改成别的端口(比如 9443)套 CDN 会直接不通。套了 CDN 的节点速度不理想 优选 IP
Cloudflare 分配给你的边缘节点不一定是本地最优。做法是把客户端配置里的
server 从域名换成一个延迟更低的 Cloudflare IP,
而 Host 头与 server_name 仍然填真实域名不变 ——
服务端完全不用改。
Host 头得显式写出来
sing-box 的 Host 是从 server 字段来的,
不会自动跟着 tls.server_name 走——这一点抓包实测过。
所以 server 一改成 IP 或别的域名,就必须同时在
transport.headers 里补上 Host,
否则 CF 找不到回源站点。完整写法见第 10 节 ② 的客户端栏。server 改成别的 IP 会直接连不上。优选 IP 可以用各类第三方测速站获取,也可以在本机用
ping / httping 自己测几段 Cloudflare IP 挑最快的。
下面「IP 被封之后」那一段里给的 saas.sin.fan 也是同一类手段,
区别只是它把「挑 IP」这件事做成了一个域名。
IP 被封之后:切 Cloudflare 反代
这是给「用了一段时间突然全断」准备的退路。先确认真的是 IP 被封,
再动配置——第 12.4 节有一张 ping / TCP 443 / SSH 端口的三列对照表,
只有落在「ping 通、443 不通、SSH 端口也不通」那一行才算。
如果只是某一个端口不通,换端口就行,用不着走这一节。
思路很简单:被封的是「你家 → 你的 VPS 这个 IP」这条路。 客户端改成去连 Cloudflare 的边缘节点,由 CF 回源到你的 VPS, 那条被封的路就不再出现在链路里。 回源走的是境外线路,不受影响;服务端一个字都不用改—— 前提是第 6 节的橙云记录和第 10 节的 WS 入站你本来就配好了。
| 要动的地方 | 改成什么 | 不改会怎样 |
|---|---|---|
| Cloudflare DNS | cf 那条 A 记录确认是橙云,内容仍然填被封的那个真实 IP | 灰云状态下客户端还是直连你的 IP,等于没切 |
| Cloudflare SSL/TLS | 加密模式选 Full (strict) | 选 Flexible 时 CF 用明文回源,8443 上的 TLS 入站握不上手,客户端表现是 525/连不上 |
| 服务端 | 什么都不用改。只是此刻只有 WS 那个入站还能用 | — |
| 防火墙 | ufw allow 8443/tcp(第 3 节只开了 22 和 443) | CF 回源被挡在门外,网页侧会看到 521 |
客户端 server | 填 saas.sin.fan | 填被封的 IP 或灰云域名 = 还是走那条断掉的路 |
客户端 Host 头和 tls.server_name | 保持你自己的橙云域名不变 | CF 靠这个名字才知道该回源到哪台机器,改错直接 404/526 |
saas.sin.fan 是干什么的
它是一个把「优选 Cloudflare IP」做成域名的服务:客户端的
server 填上它,解析出来的就是一个挑过的 CF 边缘 IP,
不用自己一段段测。本教程没有实测过这个域名
(它自己带 Cloudflare 人机验证,抓不到页面),
所以把它当成一个可替换的来源看:不通就换成自己测出来的 CF IP,
或者干脆退回填你的橙云域名——那样也能用,只是边缘节点由 CF 就近分配、不一定最快。
客户端配置见第 10 节 ② VLESS + WS 的「sing-box 客户端」栏。521/522,这时只能换 IP。
二是你只有 Reality 一个入站:Reality、Trojan、Hysteria2、TUIC、AnyTLS
都不能走橙云,切过来之后一个都不通。
所以真正的准备动作是提前把 WS 入站配好、证书签好,
平时不用,出事那天只改客户端一行。用 acme.sh 走 Cloudflare DNS 验证签发一张通配符证书,一次搞定所有子域名, 而且不需要占用 80 端口。
把邮箱换成你自己的,用于接收证书到期提醒。
curl https://get.acme.sh | sh -s email=YOUR_EMAIL # 让 acme.sh 命令在当前会话立即可用 source ~/.bashrc # 设为默认使用 Let's Encrypt(默认 CA 可能是 ZeroSSL) ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
在 Cloudflare 后台 My Profile → API Tokens → Create Token, 选 Edit zone DNS 模板。
| 配置项 | 要选的值 |
|---|---|
| Permissions | Zone · DNS · Edit |
| Zone Resources | Include · Specific zone · 你的域名 |
| TTL | 留空(不过期)或设一个足够长的时间 |
-d 两个参数分别覆盖根域名和所有一级子域名。
用 ECC 证书(--keylength ec-256)握手更快、体积更小。
# 1. 导出 Token(当前会话有效,acme.sh 会自动记住供续期使用) export CF_Token="YOUR_CF_TOKEN" # 2. 申请证书(DNS 记录传播需要等,命令会自己等待,别中断) ~/.acme.sh/acme.sh --issue --dns dns_cf \ -d yourdomain.com \ -d "*.yourdomain.com" \ --keylength ec-256
[Sat Sep 5 12:40:11 UTC 2026] Cert success. -----BEGIN CERTIFICATE----- ... [Sat Sep 5 12:40:11 UTC 2026] Your cert is in: /root/.acme.sh/yourdomain.com_ecc/yourdomain.com.cer
Verifying 属正常,DNS 记录传播通常 30 秒到 2 分钟。
超过 5 分钟仍失败,多半是 Token 权限不对或域名 NS 还没切到 Cloudflare。不要直接把配置指向 ~/.acme.sh/ 里的文件——
续期时那些文件名会变。用 --install-cert 复制到固定路径,
acme.sh 会在每次续期后自动更新这两个文件并重启 sing-box。
mkdir -p /etc/sing-box/certs ~/.acme.sh/acme.sh --install-cert -d yourdomain.com --ecc \ --fullchain-file /etc/sing-box/certs/fullchain.pem \ --key-file /etc/sing-box/certs/key.pem \ --reloadcmd "systemctl restart sing-box" # 收紧权限:私钥只有 root 可读 chmod 600 /etc/sing-box/certs/key.pem chmod 644 /etc/sing-box/certs/fullchain.pem # 确认两个文件都在 ls -l /etc/sing-box/certs/
--reloadcmd,服务会在这里被启动
--install-cert 不只是登记续期后要做什么——它复制完证书就会把 --reloadcmd 跑一遍。
也就是说 systemctl restart sing-box 现在就执行了,而 restart 对一个没在跑的服务等于 start。
所以这一步之后 systemctl status sing-box 会变成 active (running),
和第 5 节说的 inactive (dead) 不一样,这是预期的,不是哪里做错了。此时用的还是 deb 自带的默认配置(
shadowsocks,端口 8080)。
第 5 节红框里的两道保险仍然成立:8080 不在第 3 节的放行名单里,第 8 节会把配置整份覆盖并重启。
想让状态跟第 5 节完全对得上,可以顺手 systemctl stop sing-box,不停也不影响后面。600 的私钥确实能读到,但原因不是「以 root 运行」
官方 deb 的单元里写的是 User=sing-box——
sing-box 不是以 root 跑的。
它能读 600 的私钥,是因为同一份单元还给了
AmbientCapabilities=… CAP_DAC_READ_SEARCH,
这个能力按内核定义会「绕过所有针对文件与目录的读取、搜索权限检查」,
属主是 root 也照样读得到。所以这里不需要
chown。
但反过来要注意:如果你在自己的 drop-in 里重写了
AmbientCapabilities= 而漏掉 CAP_DAC_READ_SEARCH,
证书就会读不出来,日志里报 permission denied。
那种情况下要么把这个能力补回去,要么改属主:
chown -R sing-box:sing-box /etc/sing-box/certs。acme.sh 安装时会写入一条 cron 任务,证书到期前 30 天自动续期并执行上面的
--reloadcmd。
# 应看到一条 acme.sh --cron 的定时任务 crontab -l | grep acme # cron 服务必须是 running,否则续期不会执行 systemctl is-active cron # 查看证书有效期 openssl x509 -in /etc/sing-box/certs/fullchain.pem -noout -dates
fullchain.pem 与
key.pem 两个文件,openssl 能读出 90 天左右的有效期,
crontab -l 里有 acme.sh 的续期任务。证书申请失败的几种原因 高频
| 报错关键字 | 原因 | 处理 |
|---|---|---|
invalid domain / NXDOMAIN | 域名 NS 还没切到 Cloudflare | dig NS yourdomain.com 确认返回的是 CF 的 NS |
Invalid request headers | Token 权限不足或复制时带了空格 | 重新创建 Token,确认是 Zone-DNS-Edit,重新 export |
CF_Token is not set | 换了终端窗口,export 的变量丢了 | 重新执行 export CF_Token="..." 再申请 |
rateLimited | 同一域名短时间反复申请,触发 Let's Encrypt 限流 | 等一小时;调试期间加 --staging 用测试环境 |
整份教程的核心。本节给两份完整配置,都是链式形态: 客户端连中转机,中转机把流量交给你买的住宅 SOCKS5 落地,再由落地访问目标站。 两份的差别只有一处——怎么决定谁走哪台落地: 方案 A 按用户名分,方案 B 按端口分。 入站都只用 VLESS + Reality,DNS、路由、出站、缓存这些槽位按最终形态铺好了, 以后要加协议、加规则、加出口,往骨架上挂即可,不用重写。
链路长什么样
客户端(你的手机 / 电脑)
│
│ VLESS + Reality:线路优化,延迟低
▼
中转机(香港 / 日本)—— 本教程配置的就是这一台
│
│ SOCKS5 出站:公网一跳,明文,务必锁定来源 IP
▼
住宅 SOCKS5 落地(从服务商买的,你不用配它)
│
│ 直连
▼
目标站点 —— 它看到的是住宅 IP
为什么要两跳:直连远端延迟高、丢包多;而机房 IP 大多被流媒体和 AI 服务 标记为「代理」。中转机负责线路质量,落地负责 IP 干净。
两份配置怎么选
| 方案 A · 按用户分流 | 方案 B · 按端口分流 | |
|---|---|---|
| 对外开的端口 | 443 一个 | 443 / 8443 / 2053 三个 |
| 靠什么区分出口 | 同一个端口,靠 UUID 对应的用户名 | 靠端口,每个端口一个独立 UUID |
| 识别面 | 最小,一个 IP 上只有 443 在做 TLS,和普通网站一样 | 较大,三个端口同时做 TLS,正常网站不长这样 |
| 想换出口时 | 客户端换一条节点(换 UUID) | 客户端换一条节点(端口和 UUID 要同时换) |
| 再加一台落地 | 只改中转机配置,客户端不动防火墙不动 | 要开新端口、改 ufw、加新节点 |
| 适合 | 自己 + 家人 + 客人共用一台中转,各人固定走各自的出口 | 一个人想手动在几条出口之间来回切 |
配置文件的结构
| 顶层字段 | 作用 | 本节两份配置怎么用 |
|---|---|---|
log | 日志级别与格式 | warn 级别,正常访问不落盘(见本节后面「日志级别到底记了什么」) |
dns | 域名解析策略 | 用户流量走一台加密 DoH(1.1.1.1);配置里的域名走系统解析。不做任何 reject |
inbounds | 节点入口,每个协议 / 每个端口一个对象 | 方案 A 一个 Reality 三个用户;方案 B 三个 Reality 各一个用户。另外六个协议见第 10 节 |
outbounds | 流量出口 | direct + 两台住宅 SOCKS5 落地。落地换成自己的 sing-box 就把 socks 改成 vless/trojan 等对应类型,其余不动 |
endpoints | 顶层,WireGuard 从 1.13 起放这里 | 本教程不用。要挂 WARP 才需要,且必须写在顶层、不能写进 outbounds |
route | 流量走哪个出口 | 嗅探 → DNS 劫持 → 分流 → 解析,未命中的直连 |
experimental | 缓存等附加能力 | 只是结构占位——按本节配置它其实什么都不缓存,原因见本节后面的 cache_file 折叠 |
装包时带的那份 config.json 先留一份,
改坏了能对照。缓存目录必须提前建好——
配置里写了 cache_file 而目录不存在,
sing-box check 照样返回 0,启动才 FATAL,
而且 sing-box 不会自己建这个目录。
# 有就备份,没有就跳过,不会报错 [ -f /etc/sing-box/config.json ] && cp /etc/sing-box/config.json /etc/sing-box/config.json.bak # 配置目录 + 缓存目录 # 缓存目录必须归 sing-box 用户,否则服务以 User=sing-box 跑起来后写不进 cache.db mkdir -p /etc/sing-box /var/lib/sing-box chown sing-box:sing-box /var/lib/sing-box chmod 700 /var/lib/sing-box
下面两栏挑一栏,那就是 /etc/sing-box/config.json 的完整内容——整份复制进去,不用再拼接别的片段。 写完记得按下面那条命令收一下权限。
// 行注释、
/* */ 块注释、末尾多余的逗号都能过 sing-box check,
不需要粘贴前手工删掉。
更省事的是 sing-box format -w 会保留注释,
格式化之后注释还在,只是块注释会被挪到上一行末尾——所以统一用 // 最省心。
这两点都是在 1.14.0 上实测的。一个端口、三个用户:me 走落地 A,family 走落地 B,
guest 一条规则都不命中,从中转机本地直接出网。
加人就在上面的参数面板点「添加用户」,users 和 route.rules 会自动生成,
端口和防火墙都不用动。
// 配置由上方参数面板实时生成,填完直接点「复制」
auth_user,不是 user
user / user_id 匹配的是服务器上的 Linux 进程属主,
和 process_name 是同一类进程字段,跟节点用户毫无关系。
写成 user 的后果是静默失效:check 通过、启动零告警,客户端也照样能上网,
只是三个人全落到了 final。
本配置在路由末尾加了兜底 reject,这类笔误会变成「连不上」而不是「悄悄走错出口」。
auth_user 对应入站 users[].name。name 不能省,UUID 也别复用
没有 name 的用户在路由里没法被 auth_user 匹配到。
UUID 用 sing-box generate uuid 一人生成一个,
共用一个就没法区分身份,也没法单独停掉某个人。
name 改名时,dns.rules 和 route.rules 里的引用要同步改。三个端口各是一个独立入站:443 走落地 A,8443 走落地 B,
2053 从中转机本地直接出网。
三个入站的 UUID、private_key、short_id 全部各自独立,
泄露任何一个都不影响另外两个。
// 配置由上方参数面板实时生成,填完直接点「复制」
ERROR inbound/vless[in-8443]: process connection from …: unknown UUID: …,
客户端那边只表现为超时。要切出口就整条节点一起换。22 和 443,
选方案 B 就要再补两条:
ufw allow 8443/tcp 和 ufw allow 2053/tcp。
而且云厂商控制台的安全组是另一层,
阿里云 / 腾讯云 / AWS / OCI / GCP 还要在入方向单独加这两个端口,
否则端口在机器上监听着、外面照样进不来。
自查一条:ss -ltnp | grep -E ':(443|8443|2053)\b' 有三行,
就说明服务端这侧没问题,剩下的只能是防火墙或安全组。放上去的办法两种,选一种即可。写完一定要收权限:
这份文件里有 Reality 私钥和所有人的 UUID,
默认的 644 意味着这台机器上任何一个账号都能读到它。
# 方式一:在服务器上直接编辑,粘贴后 Ctrl+O 回车保存、Ctrl+X 退出 nano /etc/sing-box/config.json # 方式二:本地存成 config.json,再传上去 # 做过第 4 节改端口的用 -P 22222;没做过第 4 节就把 -P 22222 去掉 # scp -P 22222 config.json root@YOUR_SERVER_IP:/etc/sing-box/config.json # 两种方式都要跑这一条 chmod 600 /etc/sing-box/config.json
nano 默认不硬折行,粘进去是什么样就是什么样。
但如果你是从网页里复制到某些带「自动换行」的编辑器再传上去,
长行被真的插进换行符会让 JSON 变成非法——
sing-box check 会报 invalid character。
下一步的 check 就是用来拦这种问题的。1080 就改成实际的——"server_port"
这个位置必须是数字,填错 check
只会报 invalid character,不会告诉你是哪一行。只有一台落地、或者暂时不想用链式,在面板里把多余的落地节点删掉就行, 引用它的 DNS 和路由规则会跟着一起消失,其余不用改。
route.rules
里引用它们的部分会自动消失,其余一个字不动——剩下的流量会落到
"final": "direct" 上,从 VPS 直接出去。
不要图省事把占位符原样留着:
LANDING_A_IP 这种中文/英文占位符是合法的 JSON 字符串,
sing-box check 会通过、服务也起得来,
但走到那条规则的流量全部连不出去,表现是「节点连上了、网页转圈」,
排查起来比直接报错麻烦得多。# ① 语法 + 字段合法性检查。这一步过不了就不要启动服务 sing-box check -c /etc/sing-box/config.json && echo "配置 OK" # ② 顺便格式化(缩进对齐、字段排序),注释会被保留 sing-box format -c /etc/sing-box/config.json -w # ③ 起服务,然后确认端口真的监听上了 systemctl restart sing-box systemctl --no-pager status sing-box ss -tlnp | grep sing-box
配置 OK
check 过了不等于起得来
有两种错 check 抓不到,只在启动时炸:①
cache_file 的父目录不存在 →
initialize cache-file: open /var/lib/sing-box/cache.db: no such file or directory。
上面第一步的 mkdir -p 就是为了这个。②
route.rules 里引用了没在 route.rule_set 声明过的 tag →
端口会先正常监听上,然后进程立刻退出,日志只有一行
initialize rule[2]: rule-set not found: …。所以每次改完配置都要
systemctl status 加 ss -tlnp 各看一眼,
光看 check 的返回值不够。decode config ... invalid character '}' → 多了或少了逗号;
missing tls certificate → 证书路径不对,回第 7 节确认;
parse private key → Reality 私钥粘漏了字符;
missing domain resolver for domain server address →
dns.servers[].server 写成了域名,那里必须写 IP;
detour to an empty direct outbound makes no sense →
某处写了 "detour": "direct",把那一整行删掉。
注意这条既可能是启动时的 FATAL(写在 DNS 或 http_clients 上),
也可能只是每条连接一行 ERROR、服务照样显示在跑(写在出站上)。落地那边要拿到什么
住宅 SOCKS5 是买来的服务,落地机上的东西不由你配。
你只需要从供应商那里拿到下面这几项,填进 outbounds 对应的位置:
| 要拿到的信息 | 填进配置哪一项 | 注意 |
|---|---|---|
| SOCKS5 入口地址 | server | IP 或域名都行。写域名时靠 default_domain_resolver 解析,本节配置已经设成系统解析 |
| 端口 | server_port | 必须是数字,不是 1080 就照实改 |
| 用户名 / 密码 | username / password | 供应商一般都会给。确实不需要认证就把这两行删掉 |
支不支持 UDP ASSOCIATE | — | 不支持的话见下面 UDP 那一条 |
| 出口 IP 是固定还是轮换 | — | 轮换套餐会让流媒体反复要求重新登录,选节点时先问清楚 |
| 并发连接数上限 | — | 多人共用一台落地时会先撞到这个上限,表现是「有时候打不开」 |
username / password 什么时候能省
这两个字段本身是可选的,socks 出站只有
server 和 server_port 必填。但省不省要看落地:
落地要求认证而出站没写,连接直接失败,
日志里是 socks5: unsupported auth method: 255;
反过来落地不要求认证、出站却写了凭据,凭据被忽略,照样通。
所以模板里每台都写上——多写无害,漏写会断,而且这个错是响亮的,
不会静默。atyp=3),
否则解锁能力就作废了。选供应商时把这一点算进去。
真要给这一跳加密,只能换成落地那边支持的加密协议(比如对方也给 VLESS / Trojan 节点):
把 landing-a 那段出站的 "type": "socks" 换成
"vless"/"trojan",字段按第 10 节对应协议的客户端栏填,
route.rules 里引用它的 tag 不用动。socks 出站默认 TCP / UDP 都启用,但 UDP 需要落地支持
UDP ASSOCIATE,不少住宅代理只做了 TCP。
症状是普通网站正常、走 QUIC / HTTP3 的站点很慢或打不开。
两种处理:在落地出站里加 "network": "tcp" 把 UDP 关掉,
或者在分流规则里加 "network": ["tcp"] 限定条件,
让 UDP 落回 direct。
不要用 reject——直接拒绝 QUIC 会让客户端一直重试,
反而比让它慢慢超时更显眼,而本教程服务端一条 reject 都不留。
最省事的其实是客户端关掉 HTTP/3,浏览器自己就会退回 TCP。确认分流真的生效
配置能起来只说明语法对了,不说明流量走对了。三个身份分别连上,各查一次出口 IP:
curl -s https://api.ip.sb/geoip
| 身份 | 方案 A 用哪个 | 方案 B 用哪个 | 应该看到谁的 IP |
|---|---|---|---|
| 1 | 用户 me | 端口 443 | 落地 A |
| 2 | 用户 family | 端口 8443 | 落地 B |
| 3 | 用户 guest | 端口 2053 | 中转机自己 |
auth_user 写成了 user,
再查 users[].name 有没有漏填、和规则里的值是否逐字一致。
方案 B 查 route.rules 里的 inbound
和入站的 tag 对不对得上。
中转机上临时把 log.level 改成 info 再
journalctl -u sing-box -f,能直接看到每条连接选了哪个出站 tag,
查完记得改回 warn。链路排查:节点连上了但访问不了目标站点
按这个顺序查,基本一次定位。前两步在中转机上执行:
# ① 中转机能不能连到落地的端口 nc -vz LANDING_A_IP 1080 # ② 直接拿落地当代理试一次,绕开 sing-box 单独验证落地本身 curl -sS -o /dev/null -w '%{http_code}\n' --max-time 15 \ -x socks5h://LANDING_A_USER:LANDING_A_PASS@LANDING_A_IP:1080 \ https://www.example.com/ # ③ 看每条连接选中了哪个出站 tag(临时开 info) journalctl -u sing-box -f | grep -E 'outbound|landing'
| 现象 | 原因 | 处理 |
|---|---|---|
nc 超时 | 落地限制了来源 IP,或者你的套餐要先在面板里绑定 IP | 把中转机 IP 加进供应商的白名单 |
| 步骤 ② 就失败 | 问题在落地或凭据,和 sing-box 无关 | 核对地址 / 端口 / 账号密码,或找供应商 |
日志 unsupported auth method: 255 | 落地要求认证,出站没写凭据 | 补 username / password |
日志一直是 direct | 分流规则没命中 | 核对 auth_user / inbound 与入站里的 name / tag 是否逐字一致 |
| 普通站点正常、QUIC 站点打不开 | 落地不支持 UDP ASSOCIATE | 见上面 UDP 那个提示框 |
| 落地收到的是 IP 不是域名 | resolve 排在分流规则前面了 | 把分流规则移到 resolve 之前 |
| 流媒体 / AI 站点仍提示不可用 | 落地 IP 本身被标记,或套餐在轮换 IP | 换落地,与配置无关 |
客户端直接超时、服务端日志有 unknown UUID | 方案 B 里端口和 UUID 没配套改 | 整条节点一起换 |
配置之外还剩三处
配置文件管得住经过节点的流量,管不住服务器自己。下面三件事顺手做掉, 长期挂着才不会在别的地方漏出来。
# ① 配置和证书里全是密钥,别留全局可读 chmod 600 /etc/sing-box/config.json chmod 600 /etc/sing-box/certs/key.pem chmod 700 /etc/sing-box/certs chown -R root:root /etc/sing-box # ② 缓存目录。配置里写了 cache_file 就必须有这个目录,否则启动失败 # 属主必须是 sing-box:服务不是以 root 跑的,root 的目录它写不进去 mkdir -p /var/lib/sing-box chown sing-box:sing-box /var/lib/sing-box chmod 700 /var/lib/sing-box
apt update、证书续期、时间同步走的是
/etc/resolv.conf 里的明文 DNS,机房照样能看到。
这些暴露的是服务器行为、不是用户访问记录,威胁小得多,一般不必处理;
真要处理就在本机跑一个 DoH 代理再把 resolv.conf 指向它,属于另一个话题。日志级别到底记了什么 实测
用同一份配置访问两个站点,只改 log.level:
level = info 11 行日志,其中 5 行出现访问域名 level = warn 1 行日志,其中 1 行出现访问域名(仅解析失败那条) level = error 1 行日志,其中 1 行出现访问域名 info 级别的典型两行: INFO inbound/vless[reality-in]: inbound connection to www.wikipedia.org:443 INFO outbound/direct[direct]: outbound connection to www.wikipedia.org:443
info 会给每条连接写两行带完整域名的记录,全部进 journald。
一台跑几个月的节点,等于在服务器上留了一份完整浏览历史——
服务商快照、机器被入侵、甚至只是别人拿到 root,这份记录都在。
warn 是推荐值,也是上面那份配置用的值:正常访问不记,出错才记。
但它不是完全静默的——有人拿探测工具扫你的 443,
日志里会出现 ERROR ... REALITY: processed invalid connection。
这条其实有用:它说明有人在主动试你这个端口。
要更彻底可以用 "level": "fatal" 或 "disabled": true,
代价是出问题时几乎没有线索可查,也看不到有没有人在探测。
配合 journald 限容一起用:
# 限制 journald 总容量并缩短保留期,避免日志无限增长 mkdir -p /etc/systemd/journald.conf.d cat > /etc/systemd/journald.conf.d/99-limit.conf <<'EOF' [Journal] SystemMaxUse=200M MaxRetentionSec=7day EOF systemctl restart systemd-journald
关于 cache_file:现在留着它是为了以后 实测
experimental.cache_file 默认只持久化三类东西:远程
rule_set 的内容、FakeIP 映射表、Clash API 里的选择状态。
按本节的配置(enabled 之外什么都不加),
DNS 解析结果和访问过的域名都不写盘。
store_dns 这个字段是存在的,别手滑打开
store_dns 是 1.14.0 新增字段,默认 false。
写成 "store_dns": true 之后实测:跑两次解析,
cache.db 从 32768 涨到 65536 字节,
用 strings 就能在里面搜到访问过的域名
(测了两个站,一个出现 12 处、一个 10 处)。
也就是说这个开关一开,缓存文件里就会有一份域名清单。
默认不开的话实测一个域名都不落盘。
顺带一句:拼错成 store_dns_cache 会直接 unknown field
退出——所以「这个字段不存在」的说法是错的,只是名字不同。关键的一点:本地 rule_set 也不进缓存。
用 type: local(把 .srs 放在
/etc/sing-box/rules/ 里)配三个规则集,
跑起来 cache.db 还是空的。三种情况各跑一次实测:
32768 B 不配 rule_set —— bolt 空库,从未写入 32768 B 3 个 rule_set,type = local —— 一样是空的,本地文件直接读盘 262144 B 3 个 rule_set,type = remote —— 规则集内容已落盘 三种情况下,缓存文件里出现过的访问域名:均为 0 处
所以按本文的推荐配置,cache_file 是一个结构占位,
不是在干活。留着的理由只有一个:以后你要改用远程规则集、
或者在服务端开 FakeIP / clash_api,不用再回来动结构。
它也不构成隐私隐患,因为它不记录任何访问过的域名。
想删掉整个 experimental 块也完全没问题,
那就把 mkdir -p /var/lib/sing-box 一起省了。
顺带三个 1.14 的字段变化,抄旧教程的话留意:
store_rdrc 已废弃(1.16 移除);
远程规则集的 download_detour 同样废弃;
客户端侧的 independent_cache 也是废弃字段。
这三个还留在配置里只会多一行 WARN,但迟早会变成硬错误。
systemd 单元加固(可选) 未在本文实测
下面这些选项限制 sing-box 进程能碰到的系统资源,属于纵深防御—— 即使 sing-box 本身出现漏洞,能造成的破坏也有限。 这段与前面的配置不同,没有在本文的测试环境里跑过(沙箱里没有 systemd), 按下面的顺序自己验证一遍再长期用。
mkdir -p /etc/systemd/system/sing-box.service.d cat > /etc/systemd/system/sing-box.service.d/99-hardening.conf <<'EOF' [Service] NoNewPrivileges=true ProtectSystem=strict ProtectHome=true PrivateTmp=true PrivateDevices=true ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true RestrictSUIDSGID=true RestrictNamespaces=true LockPersonality=true MemoryDenyWriteExecute=true RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK AF_UNIX # ProtectSystem=strict 会把整个文件系统变成只读,这个目录必须放行 # 以后若按第 13 节把日志写进文件,要把日志目录一起加上,写成: # ReadWritePaths=/var/lib/sing-box /var/log/sing-box ReadWritePaths=/var/lib/sing-box # 能力清单照抄官方单元,一个都别少: # CAP_NET_BIND_SERVICE 绑 443/80 这类特权端口 # CAP_DAC_READ_SEARCH 以 sing-box 用户读 root 的 600 证书私钥 # CAP_NET_ADMIN/RAW tun、嗅探等功能要用 AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH EOF systemctl daemon-reload systemctl restart sing-box # 验证三步,缺一不可 systemctl status sing-box --no-pager # 1. 起来了没 ss -tlnp | grep sing-box # 2. 端口绑上了没 systemd-analyze security sing-box.service # 3. 加固评分(越低越好)
ProtectSystem=strict 和 MemoryDenyWriteExecute=true
是最容易出问题的两项。日志里出现 Read-only file system 就是前者,
补 ReadWritePaths=;出现莫名崩溃就删后者。
每加一项重启验证一次,别一次全上。AmbientCapabilities= 那行不要自己删项
官方单元给的是
CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
五项,上面照抄了。少写 CAP_NET_BIND_SERVICE 会绑不上 443;
少写 CAP_DAC_READ_SEARCH 会读不到第 6 节那份
chmod 600 的证书私钥(服务以 User=sing-box 运行,不是 root),
日志里表现为 permission denied——而且这两种错都是「重启后才发现」,
所以改完必须走下面的三步验证。check 零告警、能启动、端口正常监听,运行期间
WARN 与 ERROR 都是 0 行,注释原样留在文件里不影响解析。
分流是端到端验的:在落地那一侧架了一个会打日志的 SOCKS5 桩,
方案 A 的三个用户分别落到落地 A / 落地 B / 本机直出,
方案 B 的三个端口同样,桩上能看到正确的账号密码,
并且收到的是原始域名(atyp=3)而不是已经解析好的 IP——
这正是链式中转要的效果。方案 B 还验了「端口换了 UUID 没换」会被拒。
DoH 被打掉时的表现也验了:把 DoH 指向一个黑洞地址,
Reality 握手和入站照常工作,用户查域名则明确报
dns: lookup failed——说明 default_domain_resolver
确实接住了配置里的域名。
探测抗性:openssl 直连 Reality 端口拿回的是 Apple 的真实 EV 证书、校验通过,
curl 拿到 200 和真实首页;换一个不存在的 SNI 去连,收到标准
handshake failure。
没验的部分:上面那段 systemd 加固(沙箱里没有 systemd),
以及真实住宅 SOCKS5 供应商的行为差异。这两处按说明自己复核。端口与协议对照(开了多协议才用得上)
上面两份配置只用 Reality:方案 A 一个 443,方案 B 是
443 / 8443 / 2053 三个同协议端口。
只有当你从第 10 节挑了别的协议加进 inbounds,下面这张表才有意义——
用来核对端口、传输层、域名、证书四项有没有配错,
以及第 3 节的 ufw 有没有把对应的口放行。
| 协议 | 端口 | 传输层 | 域名 | 要证书 |
|---|---|---|---|---|
| VLESS + Reality | 443 | TCP | —(借用他人域名) | 否 |
| VLESS + WS + TLS | 8443 | TCP | cf. 橙云 | 是 |
| Trojan | 9443 | TCP | node. 灰云 | 是 |
| Shadowsocks 2022 | 80 | TCP + UDP | —(用 IP) | 否 |
| Hysteria2 | 5678 | UDP | node. 灰云 | 是 |
| TUIC v5 | 7788 | UDP | node. 灰云 | 是 |
| AnyTLS | 10443 | TCP | node. 灰云 | 是 |
ss -tlnp | grep ':80\b' 确认一下;
被占用就 systemctl disable --now nginx,或把 SS 换到别的端口。9落地机配置Landing Server
落地机是你买的第二台 VPS,专门负责把流量送出去。第 8 节的 landing-a 出站就是连到它。这一节在落地机上装 sing-box,配好入站和 SSRF 防护。
9.1 落地机是干什么的
回顾第 8 节的链路:
你的设备 → 入口机(VLESS Reality 443)→ 落地机(VLESS/SS)→ 目标网站
落地机只做两件事:收(从入口机收流量)、送(送到公网)。它不需要管用户、不需要分流、不需要伪装——越简单越好。
9.2 基础环境
跟入口机一样,走一遍第 1–5 节:SSH 登录、更新系统、防火墙、SSH 安全、装 sing-box。防火墙只放行给入口机用的端口(比如 8388),其他全关。
# 只放行落地端口(示例 8388),替换成你实际用的
ufw allow 8388/tcp
ufw allow 8388/udp9.3 sing-box 配置
落地机的配置比入口机简单得多:一个入站(给入口机连)、一个直连出站、三条路由规则(SSRF 防护)。
填好后下方配置自动替换。跟第 8 节落地列表里的参数保持一致。
// 落地机配置由上方参数面板生成9.4 跟入口机对接
两边参数必须一致,否则连不上:
| 参数 | 落地机(本节) | 入口机(第 8 节落地列表) |
|---|---|---|
| 协议 | VLESS / SS | 下拉选一样的 |
| 端口 | 监听端口 | 落地端口填一样的 |
| UUID/密码 | 入站凭证 | 落地密码填一样的 |
| 服务器 | — | 填落地机 IP |
对接完,在入口机跑 sing-box check -c /etc/sing-box/config.json,再在落地机跑一遍,两边都过才算完。
七个协议,每个给三栏:服务端入站块、sing-box 客户端出站、mihomo(Clash.Meta)YAML
加分享链接。第 8 节的配置默认只开 Reality 一个入站,
要加别的协议就把对应的「sing-box 服务端」那一栏整段并进 inbounds 数组,
放在 reality-in 后面用逗号隔开,路由和出站一个字都不用改。
server_name 与服务端证书域名不一致。
另外第 11 节有个脚本能从服务端配置自动生成客户端链接,
手填容易错的话直接用脚本。check、七个口都能监听——这一点是实测过的,
但「能跑」和「该开」是两件事。| 协议 | 端口 | 被识别的代价 |
|---|---|---|
| VLESS + Reality | 443/tcp | 最低,对外就是一台普通 HTTPS 服务器 |
| VLESS + WS + TLS | 8443/tcp | 低,套 CDN 后源 IP 不暴露;但 8443 这个端口本身偏门 |
| Trojan | 9443/tcp | 中,证书是你自己的域名,主动探测能看出这个 IP 只服务一个冷门域名 |
| AnyTLS | 10443/tcp | 中,同上,且协议较新、实现分布窄 |
| Hysteria2 | 5678/udp | 中偏高,长期大流量 UDP 到单一境外 IP,行为特征明显 |
| TUIC v5 | 7788/udp | 中偏高,同上 |
| Shadowsocks 2022 | 80/tcp | 最高,80 口不做 HTTP 却跑高熵流量,是最容易被打标的形态之一 |
22 和 443。
每加一个协议就补一条:TCP 口写 ufw allow 9443/tcp,
UDP 口写 ufw allow 5678/udp——协议写错等于没放行。
云厂商控制台的安全组是另一层,也要同步加。
需要证书的几个协议用的是第 7 节签下来的路径,先把第 7 节做完。VLESS + Reality · TCP 443
客户端要额外填 public_key(第 5 节 reality-keypair 输出的
PublicKey,不是服务端用的 PrivateKey)和 short_id。
{
"tag": "reality-in",
"type": "vless",
"listen": "::", // 同时收 IPv4 和 IPv6
"listen_port": 443,
// 多个人用就往数组里加,name 是分流用的标识
"users": [
{ "name": "me", "uuid": "YOUR_UUID_1", "flow": "xtls-rprx-vision" }
],
"tls": {
"enabled": true,
"server_name": "updates.cdn-apple.com", // 必须与 handshake.server 一致
"reality": {
"enabled": true,
// 借站目标:客户端握手时真的会连到这里
"handshake": { "server": "updates.cdn-apple.com", "server_port": 443 },
"private_key": "YOUR_REALITY_PRIVATE_KEY",
"short_id": ["YOUR_SHORT_ID"],
// 客户端时间偏差超过 1 分钟直接拒,挡掉一部分重放式探测
"max_time_difference": "1m"
}
}
}
updates.cdn-apple.com 的真证书。
借站目标怎么挑,见下面的折叠。Reality 借哪个站:四条硬指标和一条自查命令
借站目标(handshake.server)必须同时满足四条,
差一条都会让节点变得容易被识别,甚至直接连不上:
| 要求 | 为什么 |
|---|---|
| 证书 CN/SAN 含该域名本身 | 探测方拿到的证书要和它请求的 SNI 对得上,否则一眼假 |
| HTTP 首行不是 3xx | 会跳转的站说明这不是内容服务器,行为对不上 |
| 支持 TLS 1.3 | Reality 只在 1.3 上工作 |
| 支持 X25519 密钥交换 | 同上,握手要用 |
D=updates.cdn-apple.com # 1) 证书主题里必须出现 $D 本身 echo | openssl s_client -connect $D:443 -servername $D -tls1_3 2>/dev/null \ | openssl x509 -noout -subject -ext subjectAltName # 2) 首行不能是 301/302/307 curl -sI https://$D | head -1
实测可用:www.apple.com(200)、
www.cloudflare.com(200)、updates.cdn-apple.com(403 也可以,
403 说明服务器在正常应答,只是拒绝了这个路径)。
实测不可用:www.bing.com(证书 CN 是 r.bing.com,还 302 跳 cn.bing.com)、
swdist.apple.com(CN 是 swcdn.apple.com)、
www.nvidia.com(CN 是 it.nvidia.com,307)。
addons.mozilla.org 勉强可用(301)。
{
"tag": "Reality-443",
"type": "vless",
"server": "YOUR_SERVER_IP",
"server_port": 443,
"uuid": "YOUR_UUID_1",
"flow": "xtls-rprx-vision",
"packet_encoding": "xudp",
"tls": {
"enabled": true,
"server_name": "updates.cdn-apple.com",
"utls": { "enabled": true, "fingerprint": "chrome" },
"reality": {
"enabled": true,
"public_key": "YOUR_REALITY_PUBLIC_KEY",
"short_id": "YOUR_SHORT_ID"
}
}
}
short_id 在服务端是数组、在客户端是单个字符串,
写法不一样但值必须相同。utls.fingerprint 建议保持 chrome。- name: "Reality-443" type: vless server: YOUR_SERVER_IP port: 443 uuid: YOUR_UUID_1 network: tcp udp: true tls: true flow: xtls-rprx-vision servername: updates.cdn-apple.com client-fingerprint: chrome reality-opts: public-key: YOUR_REALITY_PUBLIC_KEY short-id: "YOUR_SHORT_ID"
vless://YOUR_UUID_1@YOUR_SERVER_IP:443?encryption=none&security=reality &sni=updates.cdn-apple.com&fp=chrome&pbk=YOUR_REALITY_PUBLIC_KEY&sid=YOUR_SHORT_ID &type=tcp&flow=xtls-rprx-vision#Reality-443
VLESS + WS + TLS · TCP 8443 · 唯一可套 CDN
七个节点里只有这一个能过 Cloudflare,也是IP 被封之后唯一还能救回来的那一个。 客户端栏直接给的就是第 6 节「IP 被封之后」的反代写法: 连 CF 边缘、回源到你的机器,服务端一个字不用改。
{
"tag": "ws-in",
"type": "vless",
"listen": "::",
"listen_port": 8443,
"users": [ { "name": "YOUR_USERNAME", "uuid": "YOUR_UUID_1" } ],
// 走 WS 的不能填 flow,vision 和 WS 不兼容
"transport": {
"type": "ws",
"path": "/YOUR_WS_PATH",
// 0-RTT 早期数据,少一轮往返
"max_early_data": 2048,
"early_data_header_name": "Sec-WebSocket-Protocol"
},
"tls": {
"enabled": true,
// 这里填第 6 节的橙云域名
"server_name": "cf.yourdomain.com",
"min_version": "1.3",
"certificate_path": "/etc/sing-box/certs/fullchain.pem",
"key_path": "/etc/sing-box/certs/key.pem"
}
}
8443 在列表内,
改成 9443 之类的会直接不通——完整列表见第 6 节。
另外 path 两端必须一模一样,
大小写和开头的斜杠都算。{
"tag": "WS-CDN-8443",
"type": "vless",
// 这里填的是 CF 边缘的入口,不是你的服务器。
// saas.sin.fan 会自动解析出一个优选过的 Cloudflare IP
"server": "saas.sin.fan",
"server_port": 8443,
"uuid": "YOUR_UUID_1",
"transport": {
"type": "ws",
"path": "/YOUR_WS_PATH",
// 必写!CF 靠这个名字才知道回源到哪台机器
"headers": { "Host": "cf.yourdomain.com" },
"max_early_data": 2048,
"early_data_header_name": "Sec-WebSocket-Protocol"
},
"tls": {
"enabled": true,
// 和上面的 Host 一致:你自己的橙云域名,证书也是签给它的
"server_name": "cf.yourdomain.com",
"utls": { "enabled": true, "fingerprint": "chrome" }
}
}
server 一旦不是你的域名,headers.Host 就是必填的
sing-box 发出的 Host 头取的是 server 这个字段本身,
不是 tls.server_name。用 nc 和一个本地 TLS 服务端各抓了一次原始请求:
只写 server_name 而不写 headers.Host 时,
发出去的是 Host: 127.0.0.1:18082——服务器地址原样带端口。
换成 saas.sin.fan 就会变成 Host: saas.sin.fan:8443,
CF 那边匹配不到你的站点,网页侧直接 404。
所以优选 IP/优选域名和 headers.Host 必须成对出现。server 换成 cf.yourdomain.com,
这时 headers 那两行可以整段删掉——
Host 取 server,正好就是对的。边缘节点交给 CF 就近分配,
省事但不一定最快。IP 没被封的时候更该用的是灰云直连的
Reality(第 10 节 ①),CDN 这条路只是退路。path、max_early_data、early_data_header_name。
只要有一个不同,握手能成功但数据传不动,表现为「连上了但打不开网页」。
headers.Host 不在这个名单里——它只给 CF 看,服务端不校验。- name: "WS-CDN-8443" type: vless # CF 边缘入口;不用优选就换成 cf.yourdomain.com server: saas.sin.fan port: 8443 uuid: YOUR_UUID_1 network: ws udp: true tls: true servername: cf.yourdomain.com client-fingerprint: chrome ws-opts: path: "/YOUR_WS_PATH" headers: Host: cf.yourdomain.com max-early-data: 2048 early-data-header-name: Sec-WebSocket-Protocol
vless://YOUR_UUID_1@saas.sin.fan:8443?encryption=none &security=tls&sni=cf.yourdomain.com&fp=chrome&type=ws &host=cf.yourdomain.com&path=%2FYOUR_WS_PATH#WS-CDN-8443
@ 后面那一段是连哪里,
host= 是 Host 头,sni= 是
TLS 名字——后两个都填你自己的橙云域名,只有第一个换成优选地址。
path 要 URL 编码:开头的 / 写成 %2F。第 11 节的
vps-share 是从服务端配置里读信息的,它不知道你在客户端用了优选,
所以它生成的这条链接里 @ 后面是你的域名——想用优选,把那一段手工改掉即可,
其余参数都不用动。Trojan · TCP 9443 · 兼容性最好
字段最少、几乎所有客户端都支持,适合给不会折腾的人用。
{
"tag": "trojan-in",
"type": "trojan",
"listen": "::",
"listen_port": 9443,
"users": [ { "name": "YOUR_USERNAME", "password": "YOUR_TROJAN_PASSWORD" } ],
"tls": {
"enabled": true,
// 灰云域名,Trojan 不能走 CDN
"server_name": "node.yourdomain.com",
"min_version": "1.3",
"certificate_path": "/etc/sing-box/certs/fullchain.pem",
"key_path": "/etc/sing-box/certs/key.pem"
}
}
min_version: "1.3" 不要降,1.2 的握手特征更容易被区分。{
"tag": "Trojan-9443",
"type": "trojan",
"server": "node.yourdomain.com",
"server_port": 9443,
"password": "YOUR_TROJAN_PASSWORD",
"tls": {
"enabled": true,
"server_name": "node.yourdomain.com",
"utls": { "enabled": true, "fingerprint": "chrome" }
}
}
- name: "Trojan-9443" type: trojan server: node.yourdomain.com port: 9443 password: "YOUR_TROJAN_PASSWORD" udp: true sni: node.yourdomain.com client-fingerprint: chrome
trojan://YOUR_TROJAN_PASSWORD@node.yourdomain.com:9443 ?security=tls&sni=node.yourdomain.com&fp=chrome&type=tcp#Trojan-9443
@ # ? & /
这些字符,放进链接前必须百分号编码,否则会被解析成分隔符。
第 5 节用 openssl rand -hex 生成密码就是为了避开这个问题。Shadowsocks 2022 · TCP + UDP 80 · 延迟最低
没有 TLS 握手,建连最快、CPU 开销最小。缺点是流量特征相对明显, 适合当保底/低延迟节点,不适合长期大流量。
{
"tag": "ss-in",
"type": "shadowsocks",
"listen": "::",
"listen_port": 80,
"method": "2022-blake3-aes-256-gcm",
// 2022 系列的 password 必须是 32 字节 base64:sing-box generate rand 32 --base64
"password": "YOUR_SS_PSK_BASE64"
}
80 端口不做 HTTP 却跑高熵流量——
这是最容易被打标的形态之一。
它的优点只有一个:没有 TLS 握手,延迟最低。
网络环境敏感的地区不建议长期用。多个人用同一个 SS 入站:加 users 数组
SS2022 支持多用户:入站的 password 是服务端 PSK,
再给每个人一个自己的 PSK。
{
"tag": "ss-in",
// …前面的字段不变…
"password": "YOUR_SS_PSK_BASE64", // 服务端 PSK
"users": [
{ "name": "me", "password": "YOUR_SS_USER_PSK_1" },
{ "name": "family", "password": "YOUR_SS_USER_PSK_2" }
]
}
服务端PSK:用户PSK——中间一个冒号,两段都是 32 字节 base64。
只填一段的症状是握手直接失败。
第 11 节的 vps-share 会自动拼好,不用手算。{
"tag": "SS2022-80",
"type": "shadowsocks",
"server": "YOUR_SERVER_IP",
"server_port": 80,
"method": "2022-blake3-aes-256-gcm",
"password": "YOUR_SS_PSK_BASE64",
"udp_over_tcp": false
}
method 必须与服务端完全一致,
2022-blake3-aes-256-gcm 要求密码是 32 字节 base64;
若用 2022-blake3-aes-128-gcm 则是 16 字节,长度不对会直接拒绝启动。- name: "SS2022-80" type: ss server: YOUR_SERVER_IP port: 80 cipher: 2022-blake3-aes-256-gcm password: "YOUR_SS_PSK_BASE64" udp: true
SS 的分享链接格式(SIP002)是 ss:// + userinfo 的 base64,
userinfo 为 方法:密码:
# SIP002 要求 URL-safe base64:+ → - ,/ → _ ,去掉末尾 = # 用标准 base64 会让部分客户端解析失败,这是最容易踩的坑 USERINFO=$(printf '%s' "2022-blake3-aes-256-gcm:YOUR_SS_PSK_BASE64" \ | base64 -w0 | tr '+/' '-_' | tr -d '=') echo "ss://${USERINFO}@YOUR_SERVER_IP:80#SS2022-80"
base64 直接输出
SIP002 规定 userinfo 用的是 URL-safe 且不带填充的 base64。
标准 base64 的输出末尾会带 =,
部分客户端把它当成参数分隔符或直接拒收,症状是「密码明明对却握手失败」。
上面的 tr 两步转换必不可少——tr '+/' '-_' 换字母表,
tr -d '=' 去填充。+ 或 /,
套一层 base64 后这两个字符出现在 URL 里就会解析失败」。
前半句对,后半句不成立:外层 base64 只在原文出现
> ? ~ 这类字节时才会产出第 62/63 号字符
(+ / /),而 PSK 的字符全在 base64 字母表内,
所以外层结果只会是 A–Z a–z 0–9。
真正需要处理的只有末尾那个 =。
tr '+/' '-_' 仍然照写——它对别的密码形式有用,而且不会有副作用。Hysteria2 · UDP 5678 · 弱网首选
基于 QUIC,丢包率高的线路(晚高峰、移动网络)优势明显。 前提是运营商没有对 UDP 限速——部分地区 QoS 会砍 UDP,那就退回 TCP 协议。
{
"tag": "hy2-in",
"type": "hysteria2",
"listen": "::",
"listen_port": 5678,
// 服务端不写 up_mbps/down_mbps:写了会启用 Brutal 把所有客户端限死
"ignore_client_bandwidth": false,
"users": [ { "name": "YOUR_USERNAME", "password": "YOUR_HY2_PASSWORD" } ],
"tls": {
"enabled": true,
"server_name": "node.yourdomain.com",
"alpn": ["h3"],
"certificate_path": "/etc/sing-box/certs/fullchain.pem",
"key_path": "/etc/sing-box/certs/key.pem"
}
}
ufw allow 5678/udp。写成 /tcp 等于没放行,
而症状是客户端一直转圈、服务端日志一行不多——很难猜。
云厂商安全组同样要放 UDP。再加一层 obfs(salamander):把 QUIC 特征也盖住 可选
第 5 节生成的那个 HY2_OBFS_PASSWORD 就是留给它的。
开了之后 QUIC 握手包会被一层对称加密盖住,
探测方连「这是 QUIC」都看不出来;代价是每个包多一次加解密,
CPU 很弱的小鸡上会掉一点速度。
不加也完全能用,上面那份配置默认没加。
要加就必须两端一起加。
{
"tag": "hy2-in",
// …前面的字段都不变…
"users": [ { "name": "YOUR_USERNAME", "password": "YOUR_HY2_PASSWORD" } ],
// 加在 users 和 tls 之间。type 只有 salamander 一个取值
"obfs": {
"type": "salamander",
"password": "YOUR_HY2_OBFS_PASSWORD"
},
"tls": { /* …不变… */ }
}
{
"tag": "Hysteria2-5678",
// …前面的字段都不变…
"password": "YOUR_HY2_PASSWORD",
// 和服务端一字不差,写错就是连不上
"obfs": {
"type": "salamander",
"password": "YOUR_HY2_OBFS_PASSWORD"
},
"tls": { /* …不变… */ }
}
connection: … context canceled),
而服务端日志除了启动那行 udp server started at 之外
什么都不会多出来——连 inbound connection from 都没有,
更不会有「密码错误」之类的提示。
因为 obfs 那层在 QUIC 之下,对不上的包直接被当垃圾丢掉,根本进不到认证环节。所以别拿「服务端日志没反应」当成服务端出问题的证据, 这个症状和「防火墙没放行 UDP 口」几乎一模一样。 加了 obfs 之后排查先核对两边的这一个字符串。
vps-share 会自己从配置里读 obfs.type 和
obfs.password,在生成的链接里补上
&obfs=salamander&obfs-password=…(密码会做 URL 编码)。
mihomo 那边对应的两个字段是 obfs: salamander 和 obfs-password:。
端口没变,防火墙不用动。{
"tag": "Hysteria2-5678",
"type": "hysteria2",
"server": "node.yourdomain.com",
"server_port": 5678,
"password": "YOUR_HY2_PASSWORD",
// up_mbps / down_mbps 同样建议不写:两边都不写才会用 BBR 自适应,
// 写了就切 Brutal 并硬限速。只在明确知道自己带宽上限时才填。
"tls": {
"enabled": true,
"alpn": ["h3"],
"server_name": "node.yourdomain.com"
}
}
insecure
本教程第 7 节签的是真实证书,客户端开 "insecure": true
会让中间人替换证书也能连上,等于白签。
只有用自签证书时才需要它——那种情况下也应该改用 Reality。- name: "Hysteria2-5678" type: hysteria2 server: node.yourdomain.com port: 5678 password: "YOUR_HY2_PASSWORD" sni: node.yourdomain.com alpn: - h3 skip-cert-verify: false
hysteria2://YOUR_HY2_PASSWORD@node.yourdomain.com:5678 ?sni=node.yourdomain.com&alpn=h3&insecure=0#Hysteria2-5678
TUIC v5 · UDP 7788 · UDP 转发能力强
同样基于 QUIC。相比 Hysteria2,TUIC 的原生 UDP 中继更完整, 打游戏、语音通话、BT 这类场景更合适。
{
"tag": "tuic-in",
"type": "tuic",
"listen": "::",
"listen_port": 7788,
"users": [
{
"name": "YOUR_USERNAME",
"uuid": "YOUR_UUID_2",
"password": "YOUR_TUIC_PASSWORD"
}
],
"congestion_control": "bbr",
// 0-RTT 省一轮握手,但会带来可重放风险,默认关
"zero_rtt_handshake": false,
"tls": {
"enabled": true,
"server_name": "node.yourdomain.com",
"alpn": ["h3"],
"certificate_path": "/etc/sing-box/certs/fullchain.pem",
"key_path": "/etc/sing-box/certs/key.pem"
}
}
ufw allow 7788/udp。
uuid 和 password 两个都必须和客户端对上,
congestion_control 也要两端一致。{
"tag": "TUIC-7788",
"type": "tuic",
"server": "node.yourdomain.com",
"server_port": 7788,
"uuid": "YOUR_UUID_2",
"password": "YOUR_TUIC_PASSWORD",
"congestion_control": "bbr",
// native = 原生 UDP 中继;quic = 封进 QUIC 流(NAT 严格时改这个)
"udp_relay_mode": "native",
"zero_rtt_handshake": false,
"tls": {
"enabled": true,
"alpn": ["h3"],
"server_name": "node.yourdomain.com"
}
}
uuid 与 password 两个都要,
这是 TUIC 与 VLESS 最大的区别。congestion_control 两端要一致。- name: "TUIC-7788" type: tuic server: node.yourdomain.com port: 7788 uuid: YOUR_UUID_2 password: "YOUR_TUIC_PASSWORD" sni: node.yourdomain.com alpn: - h3 congestion-controller: bbr udp-relay-mode: native reduce-rtt: false skip-cert-verify: false
tuic://YOUR_UUID_2:YOUR_TUIC_PASSWORD@node.yourdomain.com:7788 ?sni=node.yourdomain.com&alpn=h3&congestion_control=bbr &udp_relay_mode=native&allow_insecure=0#TUIC-7788
AnyTLS · TCP 10443 · 1.12 新增
专门针对「TLS in TLS」流量指纹做了填充与分帧处理, 在深度包检测较严的网络下比裸 Trojan 更耐用。需要 1.12+ 的服务端和客户端。
{
"tag": "anytls-in",
"type": "anytls",
"listen": "::",
"listen_port": 10443,
"users": [ { "name": "YOUR_USERNAME", "password": "YOUR_ANYTLS_PASSWORD" } ],
// 空数组 = 用内置默认填充。写成 "" 是类型错误,check 会直接失败
"padding_scheme": [],
"tls": {
"enabled": true,
"server_name": "node.yourdomain.com",
"min_version": "1.3",
"certificate_path": "/etc/sing-box/certs/fullchain.pem",
"key_path": "/etc/sing-box/certs/key.pem"
}
}
ufw allow 10443/tcp 别忘了开。{
"tag": "AnyTLS-10443",
"type": "anytls",
"server": "node.yourdomain.com",
"server_port": 10443,
"password": "YOUR_ANYTLS_PASSWORD",
// 连接池,减少频繁握手;不确定就用这两个值
"idle_session_check_interval": "30s",
"idle_session_timeout": "30s",
"min_idle_session": 5,
// 客户端这边同样不要写 padding_scheme,让它用内置默认方案
"tls": {
"enabled": true,
"server_name": "node.yourdomain.com",
"utls": { "enabled": true, "fingerprint": "chrome" }
}
}
padding_scheme 不是字符串
它是字符串数组,元素形如 "stop=8"、"0=30-30"、
"1=100-400"。写成 "padding_scheme": "" 会让
sing-box 启动失败并报类型错误。除非你清楚自己在调什么,
直接省略这个字段。- name: "AnyTLS-10443" type: anytls server: node.yourdomain.com port: 10443 password: "YOUR_ANYTLS_PASSWORD" sni: node.yourdomain.com client-fingerprint: chrome udp: true skip-cert-verify: false
anytls:// 的常见形式拼一条出来,
认这个格式的客户端可以直接导入;不认的就用上面的 YAML / JSON 手动添加。选哪个节点
日常上网
优先 Reality。不需要域名和证书,被封的概率最低,
xtls-rprx-vision 的性能也最好。这是唯一一个「不小心暴露 IP 也很难被确认」的方案。
IP 被墙之后
切 VLESS+WS 走 Cloudflare。客户端连的是 CF 的 IP, 你的服务器 IP 不再直接暴露,被墙也照样能用。代价是延迟增加、速度受 CF 免费线路限制。
晚高峰卡顿
试 Hysteria2 或 TUIC。QUIC 对丢包的容忍度远高于 TCP。 如果反而更慢,说明本地运营商在对 UDP 做 QoS,退回 TCP 协议。
给别人用
Trojan 或 SS2022。字段少、客户端支持广, 发个链接对方扫码就能用,不用解释什么是 short_id。
手拼分享链接容易出错,尤其是 SS2022 的 base64 编码和含特殊字符的密码。 这一节装一个脚本,直接从服务端配置读出所有节点,一次生成全部链接和终端二维码。
private_key 现场推导 Reality 公钥,不需要你另外保存;
③ 密码含 @ # ? & / 时自动做百分号编码,
并在终端直接打出二维码——手机对着屏幕扫一下就导入了,不用把链接复制来复制去。11.1 安装脚本
jq、qrencode、openssl、curl、
python3-cryptography,第 2 节那条 apt install 里都有。
真的缺了脚本启动时会自己 apt-get install 补上,不会中途报错退出。cat > /usr/local/bin/vps-share << 'SCRIPTEOF' #!/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 单行注释 CLEAN_JSON=$(sed -E -e 's@(^|[[:space:]])//.*$@@' "$CONFIG") # 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} (已修复高强度密码特殊字符的 URL 编码) ${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') TRANSPORT=$(echo "$node" | jq -r '.transport.type // "tcp"') QUERY="sni=${SNI:-$SERVER_IP}" 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:-$SERVER_IP}:${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}" SCRIPTEOF chmod +x /usr/local/bin/vps-share
private_key 就能算出客户端要填的 public_key,
结果与第 5 节 sing-box generate reality-keypair 给你的那一行逐字一致。
反过来说,私钥泄露就等于公钥泄露——公钥本来就是公开的,
真正要守住的只有私钥,按第 14 节的方式离线保管。11.2 使用
装完就在 PATH 里了,在哪个目录下都能直接敲 vps-share 回车。
它读 /etc/sing-box/config.json(含 // 注释也能读),
公网 IP 用 ifconfig.me / ip.sb 自动探测。
# 打印全部节点的链接 + 终端二维码 vps-share # 只要纯链接:去掉颜色码,挑出链接行,导出成文件 vps-share | sed 's/\x1b\[[0-9;]*m//g' | \ grep -E '^(vless|trojan|ss|hy2|tuic|anytls)://' > /root/nodes.txt # 改完配置重启后再跑一次,链接会跟着变 systemctl restart sing-box && vps-share
========================================
Sing-box 终极节点自动导出工具
(已修复高强度密码特殊字符的 URL 编码)
========================================
[VLESS] reality-in-me (传输: tcp)
vless://11111111-2222-3333-4444-555555555555@203.0.113.10:443?type=tcp
&encryption=none&security=reality&pbk=kQx7NfP0…9dZsA1
↑ 脚本用 private_key 现场算出来的,不用你自己填
&sni=updates.cdn-apple.com&sid=a1b2c3d4&fp=chrome&flow=xtls-rprx-vision#reality-in-me
扫码:
█▀▀▀▀▀█ ▀▄█ ▄▀▄ █▀▀▀▀▀█
█ ███ █ █▀▄▀▄▀█ █ ███ █ ← 手机对着屏幕扫这个就导入了
█▀▀▀▀▀█ ▀ ▀▄▀ ▄ █▀▀▀▀▀█
[VLESS] reality-in-family (传输: tcp)
↑ 同一个 443 入站里的第二个用户,自动单独出一条
vless://66666666-7777-8888-9999-000000000000@203.0.113.10:443?type=tcp
&encryption=none&security=reality&pbk=kQx7NfP0…9dZsA1
&sni=updates.cdn-apple.com&sid=a1b2c3d4&fp=chrome&flow=xtls-rprx-vision#reality-in-family
扫码:
(略)
[Trojan] trojan-in (传输: tcp)
trojan://p%40ss%3Fw0rd%231@node.yourdomain.com:9443?sni=node.yourdomain.com#trojan-in
↑ 配置里写的是 p@ss?w0rd#1,@ ? # 已自动做百分号编码
[Shadowsocks] ss-in
ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206WU9VUl9TU19QU0tfQkFTRTY0@203.0.113.10:80#ss-in
↑ SIP002 要求的 URL-safe base64,末尾不带 =
…(其余四个协议同理)
========================================
节点导出完毕
========================================
users 里的人拆开,每个用户单独出一条链接和二维码,
节点名是「入站 tag + 用户 name」,例如 reality-in-me、
reality-in-family、reality-in-guest,
直接把对应那条发给对应的人即可,不用手改 UUID。
users 里只有一个人的入站原样导出,tag 不变。
第 8 节方案 B 是三个独立入站,各自导出一条,行为和以前一样。password),
每个用户还有自己的 PSK(users[].password)。
客户端要填的是两段用冒号拼起来的
服务端PSK:用户PSK,只填一段一定连不上。
脚本已经按这个规则拼好了,单用户(只有全局 password、没有
users)的写法不受影响。rm 掉。
二维码同理——它就是链接本身,别对着屏幕录屏或直播。脚本里三处和老版本不一样的地方已修正
这个脚本沿用得比较久,本次只动了三处,其余逐字未改:
| 位置 | 老写法的问题 | 现在的写法 |
|---|---|---|
剥离配置注释CLEAN_JSON= |
老写法把整行里第一个 // 之后全部删掉,只对 http:// 做了例外。
于是密码里只要出现 //,那一行就被截断,
jq 直接报 parse error,整个工具一个节点都出不来。 |
只删「行首或空格后」的 //。
注释照样剥干净,密码中间的 // 不受影响,
https:// 也不用再开例外。 |
SS 链接的 userinfoB64_CRED= |
用的是标准 base64,末尾带 = 填充。
SIP002 要求的是 URL-safe 且不带填充的 base64,
部分客户端会把 = 当成参数分隔符或直接拒收。 |
加一步 tr '+/' '-_' | tr -d '=',
转成 SIP002 要求的形式,解码回去与配置里的
method:password 逐字一致。 |
遍历入站jq -c '.inbounds[]?' |
每个入站只取 users[0],
所以一个入站挂三个人时只导出第一个人,
另外两个人得手动改 UUID。 |
在 jq 里先把多用户入站拆成一人一条,
再走原来的逻辑。users 始终只有一个元素,
所以下面所有 users[0] 的读法一行都不用改。 |
+ 和 / 会让客户端解析失败」。
对 SS2022 来说这两个字符根本不会出现:
PSK 本身就在 base64 字母表内,再套一层 base64 后
只可能产出 A–Z a–z 0–9,第 62/63 号字符
(+ / /)需要原文里有 > ? ~ 才会出现。
实际有偏差的只有末尾那个 =。上面那行改动修的就是它。配置校验通过之后启动服务,并确认配置里写了的端口都真的在监听。 这一节也是日后维护时最常回来看的一节。
# 校验放在启动前,配置有错就不要动服务 sing-box check -c /etc/sing-box/config.json || { echo "配置有错,先修配置"; exit 1; } systemctl enable --now sing-box systemctl --no-pager status sing-box
● sing-box.service - sing-box service
Loaded: loaded (/usr/lib/systemd/system/sing-box.service; enabled)
Active: active (running) since Fri 2026-09-04 10:12:33 UTC; 3s ago
Docs: https://sing-box.sagernet.org
Main PID: 8421 (sing-box)
Tasks: 9 (limit: 1131)
Memory: 18.4M
CPU: 121ms
CGroup: /system.slice/sing-box.service
└─8421 /usr/bin/sing-box -D /var/lib/sing-box -C /etc/sing-box run
/lib/systemd/system/ 也是对的
deb 包把单元文件装在 /usr/lib/systemd/system/sing-box.service,
而现在的 Debian/Ubuntu 上 /lib 就是指向 /usr/lib 的符号链接,
旧版 systemd 会按 /lib/... 的形式打印同一个文件。
enabled 后面各版本还会再跟一段
vendor preset: enabled 或 preset: enabled(措辞随 systemd 版本变),
那只是发行版的默认策略,跟当前是否开机自启无关 —— 真正管用的是前面那个 enabled。
再往后 Tasks / Memory / CPU 的数字每台机器都不同,
平时只需盯 Active: 那一行。active (running) 就先看日志
activating (auto-restart) 表示启动后立刻崩了、systemd 在反复重试;
failed 表示彻底起不来。两种情况都执行
journalctl -u sing-box -n 50 --no-pager 看真实原因,
不要反复 restart。# TCP:443 / 8443 / 9443 / 80 / 10443 ss -tlnp | grep sing-box # UDP:5678(Hysteria2)/ 7788(TUIC) ss -ulnp | grep sing-box
LISTEN 0 4096 *:443 *:* users:(("sing-box",pid=8421,fd=9))
LISTEN 0 4096 *:8443 *:* users:(("sing-box",pid=8421,fd=10))
LISTEN 0 4096 *:9443 *:* users:(("sing-box",pid=8421,fd=11))
LISTEN 0 4096 *:80 *:* users:(("sing-box",pid=8421,fd=12))
LISTEN 0 4096 *:10443 *:* users:(("sing-box",pid=8421,fd=13))
UNCONN 0 0 *:5678 *:* users:(("sing-box",pid=8421,fd=14))
UNCONN 0 0 *:7788 *:* users:(("sing-box",pid=8421,fd=15))
*:443 一行,这不是出错
上面这份输出是「第 10 节把 7 个协议全开了」的样子。
按第 8 节那份配置直接部署的话,服务端只有一个 443 入站,
所以 ss -tlnp 只会看到 *:443,ss -ulnp 一行都没有——
这时候就是对的。以你 config.json 里实际写了几个
inbounds 为准,每开一个协议,这里就多一行。ss -tlnp | grep ':80\b' 看是谁占的(常见是 nginx / apache / caddy),
systemctl disable --now nginx 让位,或把该协议换个端口。先排除服务端问题,再去查客户端和防火墙:
# TLS 端口能否正常握手(应看到证书信息) echo | openssl s_client -connect 127.0.0.1:9443 \ -servername node.yourdomain.com 2>/dev/null \ | openssl x509 -noout -subject -issuer -dates # Reality 端口:应当返回被借用网站的证书,看起来跟真访问那个站一样 echo | openssl s_client -connect 127.0.0.1:443 \ -servername updates.cdn-apple.com 2>/dev/null \ | openssl x509 -noout -subject
short_id 的握手会被透明转发
给真实的 updates.cdn-apple.com,所以主动探测者看到的与真访问那个站完全一致。
这也是它难以被识别的原因。日常命令
| 操作 | 命令 | 说明 |
|---|---|---|
| 启动 / 停止 | systemctl start|stop sing-box | — |
| 重启 | systemctl restart sing-box | 改完配置用这个,现有连接会断开 |
| 重载 | systemctl reload sing-box | 发 SIGHUP 让 sing-box 重读配置。它会重建全部入站,现有连接同样会断——不是「平滑重载」,只是省了一次进程重启 |
| 状态 | systemctl --no-pager status sing-box | 看是否 running、内存占用、最近几行日志 |
| 开机自启 | systemctl enable|disable sing-box | 官方 deb 装完默认没有 enable,要手动开 |
| 实时日志 | journalctl -u sing-box -f | Ctrl+C 退出 |
| 校验配置 | sing-box check -C /etc/sing-box | 每次改配置后、重启前都跑一次。用 -C 目录跟 systemd 实际加载的一致 |
| 格式化配置 | sing-box format -c /etc/sing-box/config.json -w | 缩进对齐、去掉空字段;注释会保留 |
| 看版本 | sing-box version | 确认 ≥ 1.14(本教程的配置用到 1.14 才有的字段) |
reload 的取舍
既然两者都会断连接,改配置时直接用 restart 更直观,
而且能顺带确认「重启后还能正常起来」——避免下次机器重启时才发现配置有问题。
reload 的唯一优势是不重置 Restart= 的失败计数。升级 sing-box
# ① 备份配置(升级偶尔会带来字段变更) # 备份名千万别以 .json 结尾,原因见下面那个红框 cp /etc/sing-box/config.json /etc/sing-box/config.json.$(date +%F) # ② 只升级 sing-box 这一个包 # 注意不要写 apt upgrade -y sing-box —— upgrade 子命令不接受包名参数, # 实际效果是升级【全部】软件包,可能连内核一起换掉。 apt-get update apt-get install --only-upgrade -y sing-box # ③ 新版本校验配置。有字段废弃时这里会直接报出来 # 用 -C 目录(而不是 -c 单文件),跟 systemd 实际加载的完全一致 sing-box version sing-box check -C /etc/sing-box # ④ 没问题再重启 systemctl restart sing-box && systemctl --no-pager status sing-box
.json 文件都会被合并成一份配置
官方 systemd 单元里是
sing-box -D /var/lib/sing-box -C /etc/sing-box run——
-C 是「目录」而不是单文件,sing-box 会把该目录下
每一个 .json 都读进来合并。
实测:目录里放两个各含一个入站的 .json,启动后两个端口同时在监听。所以备份、试验性副本一律不要用
.json 结尾。
config.json.2026-09-05、config.json.bak 都是安全的(实测不会被加载),
config-old.json、backup.json 会被悄悄合进去。
更稳妥的做法是把备份放到 /etc/sing-box/ 之外,比如 /root/sb-backup/。*** config.json (Y/I/N/O/D/Z) [default=N] ?。
直接回车(默认 N = 保留你自己的)。
选 Y 会把你的配置换成官方那份 8080 演示配置,节点当场全废。github.com/SagerNet/sing-box/releases,
并且保留上一步的配置备份——真出问题时
apt-get install -y sing-box=旧版本号 可以退回去。连不上的时候,按「服务端 → 防火墙 → 客户端」的顺序查, 一层一层排除,不要跳着猜。绝大多数问题在前两层就能定位。
13.1 日志在哪
log 段如果没写 output,
就不会产生任何日志文件。
tail -f /var/log/sing-box.log 只会得到「No such file or directory」。# 实时跟踪(最常用) journalctl -u sing-box -f # 最近 100 行 journalctl -u sing-box -n 100 --no-pager # 只看本次启动之后 / 只看错误级别 journalctl -u sing-box -b --no-pager journalctl -u sing-box -p err --no-pager # 按时间段捞 journalctl -u sing-box --since "1 hour ago" --no-pager journalctl -u sing-box --since "2026-09-04 10:00" --until "2026-09-04 11:00"
确实想要日志文件(配置 + 轮转)
在配置的 log 段加 output,sing-box 才会写文件:
"log": {
"disabled": false,
"level": "info",
"timestamp": true,
// 写了 output 之后日志【只】进文件,journald 里就看不到了
"output": "/var/log/sing-box/sing-box.log"
}
# 目录要先建好,否则启动失败 # 属主必须给 sing-box:服务以 User=sing-box 运行,root 的目录它建不了日志文件 mkdir -p /var/log/sing-box chown sing-box:sing-box /var/log/sing-box # 加轮转,否则 debug 级别下几天就能吃掉几个 G cat > /etc/logrotate.d/sing-box <<'LREOF' /var/log/sing-box/*.log { daily rotate 7 compress missingok notifempty copytruncate } LREOF systemctl restart sing-box tail -f /var/log/sing-box/sing-box.log
copytruncate 而不是默认的 create,
因为 sing-box 不会在轮转后重开文件句柄——用 create 会导致轮转后日志写进已删除的 inode,
表现为「日志文件永远是空的」。ProtectSystem=strict,它把整个文件系统变成只读,只放行 ReadWritePaths= 里列出的目录。
上面刚建好的 /var/log/sing-box 不在里面,直接 restart 会起不来。
把 /etc/systemd/system/sing-box.service.d/99-hardening.conf 里那一行改成
ReadWritePaths=/var/lib/sing-box /var/log/sing-box(两个路径用空格隔开),
再 systemctl daemon-reload && systemctl restart sing-box。
没做过第 8 节加固的机器不用管这条。13.2 排查顺序
客户端连不上
│
├─ ① 服务端起来了吗?
│ systemctl status sing-box
│ └─ 不是 running,就看 journalctl -u sing-box -n 50,对照 13.3 的表
│
├─ ② 端口在监听吗?
│ ss -tlnp | grep sing-box
│ └─ 少了端口,多半是端口被占用,换端口或停掉占用进程
│
├─ ③ 外网能到端口吗?
│ nc -vz 服务器IP 端口
│ └─ 超时,就是防火墙或安全组没放行,回第 3 节
│
├─ ④ TLS 握手成功吗?
│ openssl s_client -connect IP:端口 -servername 域名
│ └─ 证书报错,是域名或证书路径不对,回第 7 节
│
└─ ⑤ 前面都正常但还是用不了
客户端参数对不上,回第 10 节逐字核对:
UUID / 密码 / 端口 / server_name / short_id / Reality 公钥
nc 在你自己的电脑上执行,不是在服务器上——
在服务器上连自己的端口永远是通的,测不出防火墙问题。
另外 nc -vz 只能测 TCP:
Hysteria2、TUIC 走 UDP,nc -vzu 的结果不可信(UDP 无连接,
不回包也可能显示成功)。这两个协议直接跳到第 ⑤ 步,用客户端实际连一次判断。13.3 常见报错对照
| 日志 / 现象 | 原因 | 处理 |
|---|---|---|
decode config: invalid character |
JSON 语法错:多/少逗号,或占位符没换(尤其数字位置) | sing-box check 会给出行号;检查数组末尾多余的逗号 |
parse padding scheme / 类型错误 |
AnyTLS 写了 "padding_scheme": "" |
该字段是字符串数组,直接删掉整行 |
open certificate: no such file |
证书路径不对,或 acme.sh 还没 --install-cert |
ls -l /etc/sing-box/certs/ 确认;回第 7 节 |
bad password: not base64 |
SS2022 密码不是 32 字节 base64 | openssl rand -base64 32 重新生成,两端同步改 |
address already in use |
端口被别的进程占了 | ss -tlnp | grep ':端口\b' 找到占用者 |
parse private key |
Reality 私钥粘漏或粘错(把 PublicKey 填成了 private_key) | 重新 sing-box generate reality-keypair,公私钥别混 |
| 能连上,网页打不开 | WS 的 path 或 early-data 参数两端不一致 |
回第 10 节核对三个 ws 字段 |
| 能上网,但 DNS 解析很慢 | DNS 走了被污染或超时的服务器 | 确认 strategy 与 default_domain_resolver 已配置 |
| 部分网站走了直连 | 客户端拿到了 HTTPS/SVCB 记录里的 ECH / Alt-Svc | 在客户端的 dns.rules 里加一条
{ "query_type": "HTTPS", "action": "reject" }。
服务端不做这件事——服务端一条 reject 都不留,理由见第 8 节 |
| UDP 协议连不上,TCP 正常 | 防火墙只放了 TCP,或运营商 QoS 掉 UDP | ufw allow 5678/udp;仍不行就换 TCP 协议 |
| Hysteria2 速度被限死 | 服务端或客户端写了 up_mbps/down_mbps,切到了 Brutal |
两端都删掉这两个字段,退回 BBR |
| 规则集下载失败反复重试 | 访问 GitHub 受限 | 改用 local 类型 rule_set,或手动下载 .srs |
| 用几天后突然全断 | IP 被墙 | ping 通但 TCP 不通即为封 IP;换 IP 或切 CDN 节点(第 6 节) |
13.4 判断是不是 IP 被墙
# ① ICMP 通不通(有些机房本身就禁 ping,通不过不能直接判定被墙) ping -c 4 YOUR_SERVER_IP # ② TCP 端口通不通 —— 这个才是关键 nc -vz YOUR_SERVER_IP 443 # ③ 换个端口试。只有个别端口不通 = 端口被阻断,不是 IP 被墙 nc -vz YOUR_SERVER_IP 22222
| ping | TCP 443 | SSH 端口 | 结论 |
|---|---|---|---|
| 通 | 通 | 通 | 网络正常,问题在配置或客户端 |
| 通 | 不通 | 通 | 443 端口被针对性阻断 → 换端口 |
| 通 | 不通 | 不通 | IP 被墙 → 换 IP,或切 CDN 节点 |
| 不通 | 通 | 通 | 机房禁 ICMP,正常 |
| 不通 | 不通 | 不通 | 先去服务商后台看机器是否还在运行 |
服务端搭完只是一半。这一节给各平台客户端的选择、 一份可直接用的完整客户端配置,以及一张日常维护清单。
14.1 客户端选择
| 平台 | 推荐 | 支持全部七协议 | 备注 |
|---|---|---|---|
| Windows | sing-box 官方 CLI + GUI 前端 / mihomo 系客户端 | 是 | 要 AnyTLS 就必须用 1.12+ 的内核 |
| macOS | sing-box(App Store) | 是 | 官方出品,内核版本跟得最紧 |
| iOS / iPadOS | sing-box | 是 | 需要非中国区 Apple ID |
| Android | sing-box for Android | 是 | GitHub Releases 直接下 APK |
| Linux 桌面 | sing-box CLI(tun 模式) | 是 | 与服务端同一个二进制 |
| 路由器 | 取决于固件,多为 mihomo 内核 | 部分 | AnyTLS / TUIC 支持看固件版本 |
unknown outbound type。TUIC v5 与 Hysteria2 的要求相对宽松,
但 1.8 以下的客户端建议直接升级,配置字段差异太多。14.2 完整客户端配置
把第 10 节各协议的出站片段填进 outbounds,
就得到一份带 tun 全局代理和国内外分流的可用配置。
展开完整客户端 config.json(tun + 分流 + 节点选择)
{
"log": { "level": "warn", "timestamp": true },
// ── DNS:国内域名用国内 DNS 直连解析,其余走代理解析防污染 ──
"dns": {
"servers": [
{ "tag": "proxy-dns", "type": "tls", "server": "1.1.1.1", "detour": "节点选择" },
// local-dns 这行千万别加 "detour": "direct",会启动失败,原因见下面红框
{ "tag": "local-dns", "type": "udp", "server": "223.5.5.5" }
],
"rules": [
{ "rule_set": ["geosite-cn"], "action": "route", "server": "local-dns" }
],
"final": "proxy-dns",
"strategy": "prefer_ipv4"
},
// ── 入站:tun 全局接管 ──
"inbounds": [
{
"type": "tun",
"tag": "tun-in",
"address": ["172.19.0.1/30", "fdfe:dcba:9876::1/126"],
"auto_route": true,
"strict_route": true,
"stack": "mixed"
},
{
// 给不走 tun 的程序留一个手动代理口
"type": "mixed",
"tag": "mixed-in",
"listen": "127.0.0.1",
"listen_port": 7890
}
],
// ── 规则集下载用的 HTTP 客户端(1.14.0 起必须显式声明,见下方说明)──
"http_clients": [
{ "tag": "rs-client", "detour": "节点选择" }
],
// ── 出站 ──
"outbounds": [
{
"type": "selector",
"tag": "节点选择",
// 这里列出下面实际存在的节点 tag
"outbounds": ["自动选择", "Reality-443", "Trojan-9443", "Hysteria2-5678"],
"default": "自动选择"
},
{
"type": "urltest",
"tag": "自动选择",
"outbounds": ["Reality-443", "Trojan-9443", "Hysteria2-5678"],
"url": "https://www.gstatic.com/generate_204",
"interval": "3m",
"tolerance": 50
},
// ↓↓↓ 把第 10 节的出站片段粘到这里 ↓↓↓
// { "tag": "Reality-443", "type": "vless", ... },
// { "tag": "Trojan-9443", "type": "trojan", ... },
// { "tag": "Hysteria2-5678", "type": "hysteria2", ... },
{ "tag": "direct", "type": "direct" }
],
// ── 路由分流 ──
"route": {
"rules": [
{ "action": "sniff" },
{ "protocol": "dns", "action": "hijack-dns" },
// 局域网与本机地址直连
{ "ip_is_private": true, "action": "route", "outbound": "direct" },
// 国内 DNS 服务器本身直连(放在规则集前面,第一次开、规则集还没下好时也生效)
{ "ip_cidr": ["223.5.5.5/32"], "action": "route", "outbound": "direct" },
// 国内域名与国内 IP 直连
{ "rule_set": ["geosite-cn", "geoip-cn"], "action": "route", "outbound": "direct" },
// 广告拦截(可选,不需要就删掉这条和对应 rule_set)
{ "rule_set": ["geosite-category-ads-all"], "action": "reject" }
],
"rule_set": [
{
"tag": "geosite-cn", "type": "remote", "format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs",
"update_interval": "7d"
},
{
"tag": "geoip-cn", "type": "remote", "format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs",
"update_interval": "7d"
},
{
"tag": "geosite-category-ads-all", "type": "remote", "format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-ads-all.srs",
"update_interval": "7d"
}
],
"final": "节点选择",
"auto_detect_interface": true,
// 上面那个 http_clients 的 tag:规则集就从这里下载
"default_http_client": "rs-client",
"default_domain_resolver": { "server": "local-dns" }
},
"experimental": {
"cache_file": { "enabled": true },
// 面板:可以在浏览器里切节点、看连接和实时速率
"clash_api": {
"external_controller": "127.0.0.1:9090",
"default_mode": "rule"
}
}
}
"detour": "direct"
很多老教程会给国内 DNS 服务器写 "detour": "direct",想让它「绕开路由直连」。
在 1.14 上这会直接启动失败:
start dns/udp[local-dns]: detour to an empty direct outbound makes no sense
——指向一个空的 direct 出站本来就等于什么都没做,所以新版本干脆拒绝启动。
正确做法就是上面这样:不写 detour,
让这条查询走正常路由,再用 route.rules 里那条
"ip_cidr": ["223.5.5.5/32"] 把它钉在直连上。
换成别的国内 DNS(如 119.29.29.29)时,记得把这条 ip_cidr 一起改。ip_cidr,geoip-cn 不是已经覆盖了吗
覆盖了,但只在规则集下载完之后。第一次开客户端时本地还没有规则集缓存,
geoip-cn 是空的,国内 DNS 的查询会落到 route.final 上、绕一趟代理出去。
单独那条 ip_cidr 不依赖任何下载,第一次开就生效。
相反,proxy-dns 保留 detour: "节点选择" 是对的:
它指向的是真实节点,不是空的 direct。external_controller 必须留在 127.0.0.1
这是 Clash 面板的控制接口,能切节点、看全部连接。
官方文档对它的原话是:如果监听在 0.0.0.0 上,
务必设置 secret。
这里写 127.0.0.1:9090 就是只允许本机访问,所以不用 secret;
一旦你改成 0.0.0.0:9090 或 :9090,
等于把「随意改你代理配置」的权限开给整个网络,那时 secret 不是可选项。selector.outbounds、urltest.outbounds 里的名字,
必须与你实际粘进去的节点 tag 一个字都不差。
少列一个:不报错,只是那个节点在面板里选不到。
多列一个(比如照抄了上面的骨架却没粘节点进去):
sing-box check 照样通过,
启动时才 FATAL,而且报错指向的是组、不是缺的那个节点——实测原文:start service: dependency[Reality-443] not found for outbound[自动选择]所以粘完节点后,把这两处成员表和下面的节点
tag 对着数一遍,
这一步不能靠 check 帮你兜。download_detour
官方 1.14.0 的废弃清单里写明:download_detour 已废弃,
1.16.0 移除,改用 http_client;同时
「不声明任何 HTTP 客户端、隐式走默认出站」这个老行为也一并废弃了。
所以上面用的是顶层 http_clients + route.default_http_client
这一对。实测这两种老写法启动时各会打一行 WARN:
legacy `download_detour` … is deprecated in sing-box 1.14.0 与
implicit default HTTP client using default outbound for remote rule-sets is deprecated …;
按上面写则一行警告都没有。rs-client 的 detour 指向 "节点选择",
意思是规则集通过代理下载。所以第一次启动、或者节点参数填错时,
下载失败会让 sing-box 整个拒绝启动。实测报错长这样(三个规则集各一段):FATAL start service: (initialize rule-set[0]: initial rule-set: geosite-cn:
Get "https://raw.githubusercontent.com/…": reality verification failed | …)注意这行字面上在说「规则集下载不下来」,真正的原因是节点连不上—— 很容易让人跑去改规则集的 URL。
check 这一步是通过的,看不出问题。两个办法:把
rs-client 的 detour 那一行整行删掉
(不写就是走正常路由直连下载,实测同一份配置立刻 0 FATAL 起来),
等 cache.db 里存下规则集之后再加回去;
或者给每个 rule_set 补一行 initial_path
(1.14.0 新增,指向一份本地 .srs)——
实测补上之后,即使节点完全不通也照样 0 FATAL / 0 ERROR 启动,
这是长期最省事的写法。"detour": "direct"
这是上一条里最容易踩的坑:http_clients 的 detour
和 DNS 的一样受那条限制。实测把它改成 "direct" 后
check 仍然通过,但启动时每个规则集都会报
initial rule-set: … detour to an empty direct outbound makes no sense
并直接 FATAL。要直连就是删掉整行,不是填 "direct"。ip.sb 显示服务器所在地区的 IP。
延迟正常但 IP 仍是本地的,说明分流规则把目标站点判成了国内。14.3 日常维护清单
每月
每季度
安全底线
出问题前先备份
tar -czf /root/sing-box-backup-$(date +%F).tar.gz \ /etc/sing-box/config.json \ /etc/sing-box/certs \ /root/.acme.sh/account.conf 2>/dev/null echo "备份完成,记得下载到本地:" ls -lh /root/sing-box-backup-*.tar.gz
rm 掉服务器上的副本。
用 scp -P 22222 root@服务器IP:/root/sing-box-backup-*.tar.gz ./ 拉到本地。14.4 参考
官方文档 sing-box.sagernet.org 是唯一权威来源。
配置字段在版本间会变,遇到本教程与官方文档不一致时以官方文档为准,
并注意查看文档右上角的版本切换——不同版本的字段说明差异很大。
| 资源 | 位置 | 用途 |
|---|---|---|
| 配置字段手册 | sing-box.sagernet.org/configuration/ | 查字段类型与默认值 |
| 版本变更 | github.com/SagerNet/sing-box/releases | 升级前看废弃字段 |
| 规则集 | github.com/SagerNet/sing-geosite / sing-geoip | 可用的 .srs 列表 |
| 迁移指南 | sing-box.sagernet.org/migration/ | 跨大版本升级时的字段对照 |
到这里,七个协议的节点、证书自动续期、分享链接生成、 链式中转和排查手段都已就位。日后只需要按 14.3 的清单定期维护。