feat: validate xray config before every restart, local + managed nodes

Runs 'xray run -test' against the candidate config before writing it and
restarting the service (local node, and over SSH for managed nodes) —
a bad config now fails loudly with the panel/bot call raising an error
instead of xray crash-looping in production.

Also checks TLS certificate/key file permissions against the actual
xray service user (nobody:nogroup) before accepting a config — this is
the exact class of bug that caused yesterday's WS+TLS outage (cert
readable by root but not by nobody). Verified live against the real
shayba server: replaying that exact bad config now gets rejected with
'cert permission problem(s): ... keyFile=... not readable' instead of
being written and restarted.

Managed-node restarts now also check systemctl's own exit status
instead of discarding it, so a restart failure surfaces as an error
too, not just silently swallowed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Savsis? 2026-09-12 09:34:52 +05:00
parent f9ee4f5933
commit abdf18faae
2 changed files with 86 additions and 6 deletions

View file

@ -250,6 +250,10 @@ def _mgmt_connect(address: str, ssh_port: int = 22) -> paramiko.SSHClient:
return client
class RemoteConfigError(Exception):
pass
def _remote_edit_clients(node: dict, mutate_fn):
client = _mgmt_connect(node["address"])
try:
@ -265,14 +269,27 @@ def _remote_edit_clients(node: dict, mutate_fn):
if new_clients is not None:
ib["settings"]["clients"] = new_clients
changed = True
if changed:
data = json.dumps(cfg, indent=2).encode()
with sftp.open("/usr/local/etc/xray/config.json", "wb") as f:
f.write(data)
if not changed:
sftp.close()
client.exec_command("systemctl restart xray")[1].channel.recv_exit_status()
else:
return
data = json.dumps(cfg, indent=2).encode()
tmp_path = "/usr/local/etc/xray/config.json.validate.tmp"
with sftp.open(tmp_path, "wb") as f:
f.write(data)
_, stdout, stderr = client.exec_command(f"/usr/local/bin/xray run -test -format=json -config {tmp_path}", timeout=15)
test_exit = stdout.channel.recv_exit_status()
test_out = (stdout.read().decode(errors="replace") + stderr.read().decode(errors="replace")).strip()
if test_exit != 0:
client.exec_command(f"rm -f {tmp_path}")
sftp.close()
raise RemoteConfigError(f"config test failed on {node['address']}: {test_out}")
client.exec_command(f"mv {tmp_path} /usr/local/etc/xray/config.json")[1].channel.recv_exit_status()
sftp.close()
_, stdout, stderr = client.exec_command("systemctl restart xray", timeout=20)
restart_exit = stdout.channel.recv_exit_status()
if restart_exit != 0:
err = stderr.read().decode(errors="replace").strip()
raise RemoteConfigError(f"xray restart failed on {node['address']}: {err}")
finally:
client.close()