Writing down what each endpoint actually guarantees made the places it didn't obvious — shell arguments reaching a crontab or a bash -c unescaped, master.conf written without tmp+rename, and conf edits that could be saved without ever being parsed.
61 lines
3.1 KiB
PHP
61 lines
3.1 KiB
PHP
<?php
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
// PURPOSE
|
|
// Partner connectivity test. Runs one SSH `echo ok` against a named host slot and reports
|
|
// success plus round-trip latency — the "Test" button on the partnership tab.
|
|
//
|
|
// OPERATIONAL MODEL
|
|
// Deliberately a GET, despite being an action. php -S and this nginx setup drop POST
|
|
// bodies on Unraid PHP 8.4, so a POST would arrive with no id at all. The operation is
|
|
// safe to repeat and changes nothing, which is what makes GET acceptable here rather than
|
|
// merely convenient.
|
|
//
|
|
// DESIGN PRINCIPLES
|
|
// Tests the real path, not a substitute for it.
|
|
// The probe is SSH over the Tailscale IP with the same key every partnership operation
|
|
// uses. An ICMP ping would answer a question nobody is asking — what matters is whether
|
|
// this host can actually drive the partner.
|
|
//
|
|
// Distinguishes why it failed.
|
|
// vv_pt_ping() returns separate errors for an unknown slot, a missing key, and an
|
|
// unresolvable Tailscale name, because those need three different fixes.
|
|
//
|
|
// OPERATIONAL SAFEGUARDS
|
|
// The host id is pattern-matched before it is used.
|
|
// ^host\d+$ (case-insensitive) is enforced here, and the value is then only used as a
|
|
// conf-key lookup — it never reaches a path or a shell. The remote command is the fixed
|
|
// literal `echo ok`; nothing from the request composes it.
|
|
//
|
|
// The probe is time-boxed at 8 seconds.
|
|
// vv_pt_ping() passes an explicit timeout to vv_pt_ssh(), which also sets BatchMode=yes
|
|
// so it can never sit waiting for a password. A powered-off partner costs 8s, not a
|
|
// stuck request.
|
|
//
|
|
// Read-only on the partner. `echo ok` is the entire remote payload.
|
|
//
|
|
// Never cached.
|
|
// Cache-Control: no-store, no-cache — a connectivity test served from cache is worse
|
|
// than no test, because it reports a partner reachable after it has gone dark.
|
|
//
|
|
// REQUEST
|
|
// GET ?id=host<n> (case-insensitive; the slot name, not the hostname)
|
|
//
|
|
// RESPONSE
|
|
// {"ok":true,"ms":int,…} on success
|
|
// {"ok":false,"error":string} invalid id, no key, unresolvable, or no reply
|
|
//
|
|
// DEPENDS ON
|
|
// include/partnership.php vv_pt_ping() → vv_pt_ssh(), vv_resolve_tailscale_ip()
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
header('Content-Type: application/json');
|
|
header('Cache-Control: no-store, no-cache');
|
|
require_once dirname(__DIR__) . '/include/partnership.php';
|
|
|
|
$id = trim($_GET['id'] ?? '');
|
|
if (!preg_match('/^host\d+$/i', $id)) {
|
|
echo json_encode(['ok' => false, 'error' => 'Invalid host id']);
|
|
exit;
|
|
}
|
|
|
|
echo json_encode(vv_pt_ping($id));
|