Resolve a peer from tailscale status before falling back to DNS

MagicDNS does not work on this mesh, so `tailscale ip -4` missed and blocked on
a system DNS lookup until it timed out: 5.04s per call against 0.010s for the
status parse, paid by every mesh operation on both hosts. Exact match first, so
no precision is traded for the speed.
This commit is contained in:
Gmer4Lfe
2026-08-21 08:38:37 -04:00
parent fe23c59e0c
commit 331b7b6b13
2 changed files with 67 additions and 26 deletions
+33 -14
View File
@@ -416,28 +416,47 @@ function vv_get_hostname(bool $flush = false): string {
return $name;
}
// Mirror of common.sh resolve_tailscale_ip(): tries `tailscale ip -4` first (Tailscale manages
// the mapping so this survives IP changes), falls back to parsing `tailscale status` text.
// Mirror of common.sh resolve_tailscale_ip(): resolves a hostname to a Tailscale IPv4.
//
// `tailscale status` is asked first, and that ordering is the whole point. It used to try
// `tailscale ip -4` first, on the reasoning that Tailscale owns the mapping — true, but that call
// resolves through MagicDNS, and MagicDNS does not work here: these hosts sit on separate tailnets
// shared into each other, so the name misses and the call falls through to a system DNS lookup
// that times out. Measured at **5.04 seconds, every call**, against 0.010s for the status parse.
//
// Nine files resolve peers this way — the AI RPC, node_chat, the arr collector, the media mesh
// view — so that was five seconds added to every mesh operation on both hosts, quietly, for as
// long as the mesh has existed. Nothing looked broken; everything was just slow.
//
// Order is now: exact match, unambiguous prefix match, then the DNS path bounded to two seconds as
// a last resort. Exactness is not given up to get the speed — `tailscale status` carries the same
// mapping `tailscale ip` would return, and an exact name match against it is exactly as precise.
//
// Memoised per request: a page that resolves the same partner four times paid four lookups.
function vv_resolve_tailscale_ip(string $hostname): string {
$h = strtolower($hostname);
$ip = trim(shell_exec('tailscale ip -4 ' . escapeshellarg($h) . ' 2>/dev/null') ?: '');
if ($ip) return $ip;
static $cache = [];
$h = strtolower($hostname);
if (isset($cache[$h])) return $cache[$h];
// Fallback: unambiguous prefix match against tailscale status (either direction) — handles
// Unraid's 15-char NetBIOS hostname truncation vs. a longer name recorded in master.conf.
// Only accept the match when exactly one peer could qualify; never guess between multiple
// candidates that happen to share a prefix (e.g. server1/server10).
$out = shell_exec('tailscale status 2>/dev/null') ?: '';
$out = shell_exec('tailscale status 2>/dev/null') ?: '';
$matches = [];
foreach (explode("\n", $out) as $line) {
$cols = preg_split('/\s+/', trim($line));
if (!isset($cols[1])) continue;
$peerHost = strtolower(explode('.', $cols[1])[0]);
if (str_starts_with($peerHost, $h) || str_starts_with($h, $peerHost)) {
$matches[] = $cols[0];
}
if ($peerHost === $h) return $cache[$h] = $cols[0];
// Unambiguous prefix match, either direction — handles Unraid's 15-char NetBIOS hostname
// truncation against a longer name recorded in master.conf. Only accepted when exactly one
// peer could qualify; never a guess between candidates sharing a prefix (server1/server10).
if (str_starts_with($peerHost, $h) || str_starts_with($h, $peerHost)) $matches[] = $cols[0];
}
return count($matches) === 1 ? $matches[0] : '';
if (count($matches) === 1) return $cache[$h] = $matches[0];
// Only reached when status cannot decide. Bounded, because this is the path that blocks on DNS
// when MagicDNS is unavailable — which is the normal case on this mesh.
return $cache[$h] = trim(shell_exec('timeout 2 tailscale ip -4 ' . escapeshellarg($h) . ' 2>/dev/null') ?: '');
}
// Cached alongside the others: this reads master.conf in full and is called by vv_conf_vars() on