The install script now checks for a foreign proxy (xray directory, docker marzban/xray) and busy ports
before adding the management key or writing anything, and tells the panel the node is active only after
xray has stayed up with its port listening for three checks in a row.
Chains are real Xray hops: a chain-<code> inbound + outbound + routing rule on the
entry node and a relay-<code> client on the exit node, reconciled by sync_node with
config test, port check, rollback on a failed restart and a per-node lock.
Admin API gets chains CRUD/probe/check, node latency, audit log (middleware, no request
bodies), users list with subscription counters and a csv export that neutralises formulas.
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>
payments: _grant_paid_subscription now validates plan/node exist before
marking a payment paid instead of after (was leaving charged-but-ungranted
payments with no error trail); mark_payment_paid is now a single atomic
UPDATE ... WHERE status='pending' instead of check-then-act, closing a
double-grant race between webhooks and the periodic reconciler; yookassa
webhook now re-verifies payment status server-side via the API instead of
trusting the posted body (platega already had HMAC verification).
hwid: 'user["hwid_limit"] or FALLBACK' treated an explicit 0 (admin fully
blocking a user) as unset — now an explicit None check. Device count-check
and insert are now one atomic transaction (db.add_device_if_under_limit)
instead of two raceable statements.
perf: payment webhooks and _grant_paid_subscription's SSH/HTTP calls now
run via asyncio.to_thread instead of blocking the event loop; same for
bot.py's periodic_sync/reconcile_pending_payments and the manual admin
sync button. Admin endpoints (traffic/subscriptions/payments/gift-codes/
user-card) now resolve node labels from one db.list_nodes() call instead
of a fresh db.get_node() per row. revoke/reset-traffic use a direct PK
lookup instead of scanning up to 5000 rows. Dashboard now asks the API
for 8 rows instead of fetching 200 and slicing client-side.
security: mbs.db (and -wal/-shm) now chmod 600 right after creation —
it held session tokens and subscription bearer tokens world-readable
by default. Node SSH connections now pin host keys via a persisted
known_hosts file (TOFU) instead of accepting any key on every connection.
delete_node now refuses to delete a node with active subscriptions
instead of silently orphaning their xray clients.
deadcode: removed unused xray_manager.list_client_ids and admin.html's
superseded staggerReveal (rows animate via rowAttr() inline now).
Also guards gift-code redemption against a plan/node deleted after the
code was created (was an unhandled KeyError/TypeError crash).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>