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:
@@ -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
|
||||
|
||||
@@ -921,26 +921,48 @@ resolve_remote_ip() {
|
||||
info "$ICON_NET Remote IP: $REMOTE_SERVER"
|
||||
}
|
||||
|
||||
# Resolve any hostname to a Tailscale IPv4 — tries direct lookup, falls back to status parse.
|
||||
# Resolve any hostname to a Tailscale IPv4 — status parse first, DNS as a bounded last resort.
|
||||
# Usage: ip=$(resolve_tailscale_ip "hostname") — returns empty string on failure.
|
||||
#
|
||||
# `tailscale ip -4` used to be tried first, because Tailscale owns the mapping. It also resolves
|
||||
# through MagicDNS, which does not work on this mesh — the two hosts are on separate tailnets
|
||||
# shared into each other — so the name misses and the call blocks on a system DNS lookup until it
|
||||
# times out. Five seconds, every call, on every rsync, partnership check and remote collector.
|
||||
# The status parse costs ten milliseconds and carries the same mapping.
|
||||
#
|
||||
# Exactness is not traded away for the speed: an exact name match against `tailscale status` is as
|
||||
# precise as the lookup it replaces. The prefix match keeps its ambiguity guard, and the DNS path
|
||||
# still runs — bounded, and only when status cannot decide.
|
||||
#
|
||||
# Mirror of vv_resolve_tailscale_ip() in Plugin/unraid/include/config.php; the two must agree.
|
||||
resolve_tailscale_ip() {
|
||||
local hostname="${1,,}"
|
||||
local ip
|
||||
ip=$(tailscale ip -4 "$hostname" 2>/dev/null)
|
||||
if [[ -n "$ip" ]]; then
|
||||
echo "$ip"
|
||||
local status_out exact matches count ip
|
||||
|
||||
status_out=$(tailscale status 2>/dev/null)
|
||||
|
||||
exact=$(echo "$status_out" | awk -v name="$hostname" '
|
||||
{ split(tolower($2), parts, "."); if (parts[1] == name) { print $1; exit } }')
|
||||
if [[ -n "$exact" ]]; then
|
||||
echo "$exact"
|
||||
return
|
||||
fi
|
||||
# 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).
|
||||
local matches count
|
||||
matches=$(tailscale status 2>/dev/null | awk -v name="$hostname" '
|
||||
|
||||
# Unambiguous prefix match, 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 candidates that share a prefix (server1/server10).
|
||||
matches=$(echo "$status_out" | awk -v name="$hostname" '
|
||||
{ split(tolower($2), parts, "."); host = parts[1];
|
||||
if (index(host, name) == 1 || index(name, host) == 1) print $1 }')
|
||||
count=$(echo "$matches" | grep -c .)
|
||||
[[ "$count" -eq 1 ]] && echo "$matches"
|
||||
if [[ "$count" -eq 1 ]]; then
|
||||
echo "$matches"
|
||||
return
|
||||
fi
|
||||
|
||||
# Last resort, and the slow one. Bounded so a broken MagicDNS costs two seconds, not five.
|
||||
ip=$(timeout 2 tailscale ip -4 "$hostname" 2>/dev/null)
|
||||
[[ -n "$ip" ]] && echo "$ip"
|
||||
}
|
||||
|
||||
# Strips the "unraid-" prefix (case-insensitive) and title-cases what remains.
|
||||
|
||||
Reference in New Issue
Block a user