Two modes, one infrastructure line:GreenfieldTake a bare metal machine to "hardening complete, proxy usable, information archived";InspectionRun a health check on a group of nodes and relay chains, read-only. Machines that complete greenfield go into the inspection list; problems found during inspection go back to the greenfield pitfall list for cross-reference.
Usage
/ray-vps root@1.2.3.4 # 开荒:裸机全流程
/ray-vps myhost --harden-only # 开荒:只做加固,不装代理栈
/ray-vps myhost --proxy-only # 开荒:只装代理栈(机器已加固过)
/ray-vps check # 巡检:全部节点 + 中转链
/ray-vps check us-node-1 # 巡检:单节点深查
/ray-vps check --sub # 巡检:只核订阅有效性
When the argument is a host, enter greenfield mode;check enter inspection mode.
Greenfield Mode
From getting a bare metal machine to "hardening complete, proxy usable, information archived". All steps execute in order, with a gate before every destructive action.
Ask three things before starting (there are defaults, use defaults if the user doesn't answer):
- What is this machine for? (Proxy node / application server / relay) — determines whether to install the proxy stack
- Which SSH public key to use? (Default
~/.ssh/id_ed25519.pub) - Where to record panel credentials and ports? (By default appended to the user-specified credential file)
Step 0: Connectivity and Inventory
ssh -o ConnectTimeout=10 <host> 'uname -m && cat /etc/os-release | head -2 && free -m | head -2 && df -h / | tail -1'
ssh <host> "ss -tlnp | awk 'NR>1{print \$4}'" # 已占端口清单
Record: architecture (amd64/arm64), distribution, memory, disk,set of occupied ports。
Port conflicts are the number one cause of deployment failure. Before choosing a port for any service, check against this list first.
Step 1: System Hardening
Fixed order, each step idempotent (harmless to rerun):
1a. SSH Key Login
ssh-copy-id -i <pubkey> <host> # 或手动追加 authorized_keys
ssh -i <key> <host> 'echo ok' # 必须先验证密钥能登
1b. SSH Tightening (Lockout Protection Gate)
Edit /etc/ssh/sshd_config(or drop-in /etc/ssh/sshd_config.d/99-harden.conf):
PasswordAuthentication no
PermitRootLogin prohibit-password
Iron rules:
- After editing, first run
sshd -tsyntax check, reload only after it passes - Do not exit the current session, open a new connection to verify login works; only after verification passes is it considered complete
- Roll back immediately if key verification fails; never disable password auth when only passwords are usable
1c. Kernel and Network: BBR
cat >> /etc/sysctl.d/99-bbr.conf <<'EOF'
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
EOF
sysctl --system
sysctl net.ipv4.tcp_congestion_control # 确认输出 bbr
1d. Basic Hygiene
timedatectl set-timezone UTC # 或用户指定时区
apt-get update && apt-get install -y unattended-upgrades && dpkg-reconfigure -f noninteractive unattended-upgrades
Add swap for small instances with ≤1G memory:
fallocate -l 1G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
1e. Time Synchronization: confirm timedatectl shows NTP active (Reality handshake is sensitive to clock).
Step 2: Proxy Stack (3x-ui + VLESS+Reality)
If you have your own batch deployment script (the kind that deploys one machine with one command), you can use it directly for automation; the manual sequence below is only for when there is no ready-made script — butthe pitfall list must be checked for both paths。
2a. Choose Port: pick the first from the preference sequence 443 → 2083 → 8443 → 2053 → 2087 → 2096 that is not in the occupied list. Choose another high port for the panel port.
2b. Install 3x-ui (install latest release)
# master 分支的 install.sh 不带版本参数即装最新稳定 release
echo 'y' | bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
x-ui version # 记下实际安装的版本号,写进交接清单以便日后复现与排障
- The installation script is interactive, pipe is unreliable — the strategy isinstall defaults, then force reset credentials via CLI:
/usr/local/x-ui/x-ui setting -username <u> -password <p> -port <panel_port> -webBasePath /
systemctl restart x-ui
Not pinning the version trades for security fixes and new features, at the cost of possible upstream behavior changes. Fallback relies on two places: the end-to-end verification in Step 4 will expose most destructive changes; the pitfall notes in 2c/2d below are all written as "field names and behavior drift with version", not bound to a single version. If installing the latest really hits an unknown pitfall, then roll back to a known-good official tag (append the tag to the end of the command above, e.g.
... install.sh) v3.5.0) and note it in the handoff list.
2c. Generate Reality keys (version pitfalls)
xray x25519 The output field names drift across versions; when parsing, use the following table for compatibility:
| Xray version | Private key field | Public key field |
|---|---|---|
| Older versions | Private key: |
Public key: |
| Newer versions (from v26.x) | PrivateKey: |
Password (PublicKey): or Password: |
Installing the latest release usually hits the latter line, but the field names are subject to xray x25519 's actual output—don't force a fit. xray binary location:/usr/local/x-ui/bin/xray-linux-*(glob match, compatible with amd64/arm64).
2d. Inbound configuration (three field-tested pitfalls)
- SNI selection: The TLS certificate of the :dest site must be ≤8192 bytes (xray-core hardcoded limit, long-standing and version-independent).
www.microsoft.com's certificate already exceeds the limit; handshake fails on newer xray-core—usewww.apple.com/www.cloudflare.com/www.bing.com - client email must be non-empty and unique: 3x-ui will silently drop clients with empty email, causing
clients: null, all handshakes failing with no error (tested on 3.4.x; whether newer versions have fixed it depends on the actual behavior; in any case, filling in a non-empty and unique email is always safe). Naming suggestion<remark>-<随机3字节hex> - Key fields:
flow: xtls-rprx-vision、fingerprint: chrome、shortIduse 4-byte hex, sniffingdestOverride: [http, tls, quic, fakedns]
Step 3: Firewall
Probe-style handling, don't assume what's on the machine:
ufw status 含 active → ufw allow <每个端口>/tcp && ufw reload
否则 iptables INPUT 默认策略是 DROP/REJECT → iptables -A INPUT -p tcp --dport <p> -j ACCEPT
两者都不是 → 跳过(云厂商安全组在外层,提醒用户去控制台开)
Ports to open = proxy port + panel port + SSH port.Verify from local immediately after opening; don't trust the green light in the panel.
Step 4: Verification and handover
4a. End-to-end verification
curl -sk -o /dev/null -w '%{http_code}' --connect-timeout 10 https://<server>:<port>
Returning 400 or 200 both count as alive—Reality returns 400 for non-VLESS clients; this is expected behavior, not a fault.
4b. Handover checklist(output to the user; for sensitive items, only give the storage location, don't print plaintext):
主机: <ip> (<架构>/<发行版>/<内存>)
SSH: 密钥登录,密码已关,root 仅密钥
BBR: ✅
面板: https://<ip>:<panel_port> 凭证 → <凭证档位置>
节点: vless+reality :<port> SNI=<sni>
防火墙: <ufw/iptables/云安全组> 已放行 <端口列表>
待办: [ ] 订阅同步 [ ] 加入巡检清单(/ray-vps check)
Greenfield failure handling principles
- If any step fails, first lay out thecompleted-steps checklistand theraw error of the failure point; don't blindly retry
- For SSH configuration-type failures, prioritize restoring logability; everything else comes after
- Installation script timeout (>180s) is most likely a network issue: switch to a GitHub mirror or fetch via proxy, don't
--insecure
Inspection mode
Do a health check on a group of proxy nodes and relay chains, producing a table that shows at a glance "which machine is sick, and where".Read-only, no writes—when problems are found, give judgment and suggested actions; don't auto-switch, don't auto-restart (switching is a human decision).
First locate the node list source:
- If there is a node list repo/config file (such as one in
config.jsoncontainingnodes[]) → read out name/server/port/ssh_host - If there are relay chain probe logs → read on each relay host's probe log directory
<probe_log_dir>/<chain>.log(directory depends on your deployment) - If neither → ask the user to provide a node list (server:port + ssh alias)
Step 1: Per-node liveness probe
For each node, run in parallel:
1a. Port liveness
curl -sk -o /dev/null -w '%{http_code}' --connect-timeout 10 https://<server>:<port>
Criterion: returning 400 or 200 = alive(Reality returns 400 for non-VLESS clients, which is normal); timeout/connection refused/000 = dead.
1b. Connection latency
curl -sk -o /dev/null -w '%{time_connect}' --connect-timeout 10 https://<server>:<port>
Grading: <0.5s good / 0.5–2s slow / >2s or timeout = dead.
1c. Panel-side live status(Optional if the node runs 3x-ui)
Pull the inbound's real-time traffic (↑/↓) and client status via the panel API, and by the way confirm clients is not null (a leftover from the empty email pitfall recorded in pioneer mode 2d).
Step 2: Exit IP verification
The public IP seen from the node's actual exit, to verify whether it is the expected landing:
ssh <ssh_host> 'curl -s --connect-timeout 10 https://api.ipify.org'
Purpose: ① Confirm it has not been identified/polluted ② On a relay chain, confirm the exit lands on the correct end machine ③ Early warning for IP changes (CGNAT drift). Compare the result with the last record; if it changed, mark it yellow.
Step 3: Relay chain probe logs (if your environment has a relay chain probe)
The probe hops every 15s and writes logs as single-line logfmt. Read the latest status:
ssh <relay_host> 'tail -5 <probe_log_dir>/<chain>.log'
ssh <relay_host> 'grep -vE "result=ok" <probe_log_dir>/<chain>.log | tail -50' # 只看异常
Log fields and interpretation:
chain=<chain> tcp_connect_ms=42 wg_hs_age_s=18 probe_dur_ms=45 result=ok
| result | Meaning | Threshold | What to do |
|---|---|---|---|
ok |
Healthy | tcp<500ms and wg handshake<180s | — |
slow |
High latency | tcp 500–2000ms | Observe; if it occurs continuously, check the intermediate hops |
tcp_timeout |
Forwarding down | tcp>2000ms or cannot connect | The relay hop is down; consider manually switching the chain |
wg_stale |
Tunnel effectively disconnected | wg handshake≥180s (about 7 missed keepalives) | WG tunnel is down; check the peer/restart wg |
wg_unknown |
No handshake detected | wg command failed/no output | Probe environment or wg interface issue |
wg_hs_age_s=-1 means it has never handshaked or the probe failed. For the chain-switch event history, see the probe log directory under switches.log。
Step 4: Subscription validity
curl -s <订阅URL> | head -50
Check:
- It can be fetched and is valid YAML (mihomo config)
proxies:Node count == expected node count (fewer means some node dropped out of the subscription)proxy-groupsThe node names in it match the actual nodes- rule-providers'
.mrssource URL is reachable (if the self-hosted source is down, client rule loading will fail) - Spot check: each node's server:port in the subscription matches the Step 1 liveness result
Step 5: Health table
Output an overall table, sorted by severity (dead ones at the top):
节点 端口 延迟 出口IP 状态 备注
─────────────────────────────────────────────────────────
us-node-1 ❌死 — — 🔴 curl 超时,查中转链
us-node-2 ✅活 0.8s 1.2.3.4 🟡 延迟偏高
jp-node-1 ✅活 0.1s 5.6.7.8 🟢
中转链 chain-a — 42ms — 🟢 wg 握手 18s,正常
中转链 chain-b — timeout — 🔴 tcp_timeout,最近12跳异常
订阅 — — — 🟢 4/4 节点在册,源可达
Append below the table:
- Suggested actions for 🔴 items(one sentence each, e.g. "us-node-1 dead + relay chain chain-b tcp_timeout, same origin, check the machine at the end of the chain-b chain first")
- Switches requiring manual decisionMark them clearly; do not switch on the user's behalf
- Differences from the last inspection (newly added red/yellow, recovered green)
Inspection discipline
- Read-only throughout: do not restart services, do not change configuration, do not switch chains
- A probe timeout does not equal a dead node—distinguish "probe environment issue" from "node truly dead"; if unsure, probe once more or change the probe point
- Sensitive information (subscription URL containing token, exit IP) is retained in the output, but remind the user not to post this table publicly