From 3ae62986565dea055e54763129d21a8428602068 Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Sun, 16 Aug 2026 21:20:59 -0400 Subject: [PATCH] Let a partner adopt the owner's custom networks from the conf phase 1 already cached, instead of asking for a value it cannot know --- Plugin/unraid/api/setup.php | 41 +++++++++++++++++++++++++++ Plugin/unraid/include/config.php | 24 ++++++++++++++++ Plugin/unraid/pages/setup.php | 48 ++++++++++++++++++++++++++++++-- 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/Plugin/unraid/api/setup.php b/Plugin/unraid/api/setup.php index 60ffc05..2d73ee3 100644 --- a/Plugin/unraid/api/setup.php +++ b/Plugin/unraid/api/setup.php @@ -397,6 +397,47 @@ if (!file_exists(CONF_DIR . '/' . $confFile)) { } } +// ── Adopt the owner's custom docker networks ───────────────────────────────────────────────── +// Sent by the wizard, which read them out of the owner's host conf that onboard Phase 1 cached +// into the RAM conf dir. This is the one setup value a fresh mirror has no way to know: the +// template ships NETWORK_CONNECT_NETWORKS with its only entry commented out, and the owner then +// deploys containers here onto a network named in the *owner's* templates. An empty list is what +// left twelve containers created against a network that did not exist. +// +// Outside the create block above, so it applies to a conf that already exists — the wizard is +// re-runnable and a mirror rebuilt against an existing host conf needs this just as much. +// +// Merged, never replaced: anything already listed here was put there deliberately. +$netsRaw = trim((string)($_POST['networks'] ?? '')); +if ($netsRaw !== '' && $hostIdLow !== 'host1') { + // Written into a file that bash sources, so the name is validated rather than trusted. + // Docker's own charset for a network name is a superset of this; anything outside it is + // far more likely to be an injection attempt than a real network. + $nets = array_values(array_filter( + array_map('trim', explode(',', $netsRaw)), + fn($n) => $n !== '' + && preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/', $n) + && !preg_match('/^(bridge|host|none|br\d)/i', $n) + )); + if ($nets) { + $netVar = $hostId . '_NETWORK_CONNECT_NETWORKS'; + vv_conf_edit($confFile, function (string $cur) use ($netVar, $nets): ?string { + $existing = vv_parse_conf_list($cur, $netVar); + $merged = $existing; + foreach ($nets as $n) { + if (!in_array($n, $merged, true)) $merged[] = $n; + } + if ($merged === $existing) return null; // already adopted — no write, no audit noise + $body = ''; + foreach ($merged as $n) $body .= ' "' . $n . '"' . "\n"; + $new = preg_replace( + '/^(\s*' . preg_quote($netVar, '/') . '\s*=\s*\()(.*?)(^\s*\))/ms', + "\$1\n" . $body . '$3', $cur, 1, $count); + return ($count === 1 && $new !== null) ? $new : null; + }, [], [$netVar]); + } +} + // Write setup state file — lets partner servers know HOST1 is configured. // Read-modify-write: vv_setup_state_write() replaces the file wholesale, and re-running the // wizard must not erase onboarding progress recorded by the partnership phases. diff --git a/Plugin/unraid/include/config.php b/Plugin/unraid/include/config.php index 50a8d5d..4fd0be9 100644 --- a/Plugin/unraid/include/config.php +++ b/Plugin/unraid/include/config.php @@ -883,6 +883,30 @@ function vv_parse_conf_scalar(string $raw, string $key): string { return vv_conf_unquote(ltrim($m[1])); } +// Read a bash array of plain strings out of a conf, skipping commented entries. +// +// Distinct from vv_parse_conf_array() in scheduler.php, which looks the same but keeps only +// entries ending in .sh — it exists to read job lists. Handing it a list of docker networks +// returns an empty array, silently, because none of them are scripts. This one makes no +// assumption about what the entries mean. +// +// The closing paren must be at the start of its own line, matching the shape conf_upgrade +// writes and the same anchor the scheduler parser uses — a value containing ')' would +// otherwise end the array early. +function vv_parse_conf_list(string $raw, string $key): array { + if (!preg_match('/^\s*' . preg_quote($key, '/') . '\s*=\s*\((.*?)^\s*\)/ms', $raw, $m)) { + return []; + } + $out = []; + foreach (explode("\n", $m[1]) as $line) { + if (preg_match('/^\s*#/', $line)) continue; // commented-out entry + if (!preg_match('/"([^"]*)"|\'([^\']*)\'/', $line, $e)) continue; + $val = trim($e[1] !== '' ? $e[1] : ($e[2] ?? '')); + if ($val !== '') $out[] = $val; + } + return $out; +} + // Unquote one bash word the way bash does, because the regexes this replaced did not and the // conf is read by both. Three separate regexes each handled one quoting style in isolation and // none of them handled an escape or two quoted runs in a row, so a value carrying a quote or a diff --git a/Plugin/unraid/pages/setup.php b/Plugin/unraid/pages/setup.php index 317be2d..dfb6afb 100644 --- a/Plugin/unraid/pages/setup.php +++ b/Plugin/unraid/pages/setup.php @@ -51,7 +51,7 @@ $detectedHostname = vv_get_hostname(); // Matching is exact and case-insensitive, never a prefix: two hosts called Tower and Tower2 must // not resolve to each other, and there is no ambiguity to tolerate when the value was written by // the very host it names. -$vvDetected = ['slot' => '', 'primary' => '', 'role' => '']; +$vvDetected = ['slot' => '', 'primary' => '', 'role' => '', 'networks' => [], 'networks_src' => '']; $vvMasterRaw = vv_read_conf_raw('master.conf'); if ($vvMasterRaw !== '' && $detectedHostname !== '') { preg_match_all('/^\s*(HOST\d+)\s*=\s*"([^"]*)"/m', $vvMasterRaw, $vvHm, PREG_SET_ORDER); @@ -70,6 +70,34 @@ if ($vvMasterRaw !== '' && $detectedHostname !== '') { // The primary is whatever HOST1 says, and it is only useful to a host that is not HOST1. if ($vvDetected['role'] === 'partner') $vvDetected['primary'] = $vvSlots['host1'] ?? ''; } + +// ── Custom docker networks, adopted from the owner ─────────────────────────────────────────── +// master.conf names the hosts; it does not name the networks, which live in each host's own +// host*.conf. The owner's copy is available here anyway: onboard Phase 1 caches it into +// VV_CONF_RAM_CACHE_DIR as soon as SSH works, before this node has finished setup. +// +// This is the value a fresh mirror cannot know and cannot be expected to type. host.conf.template +// ships NETWORK_CONNECT_NETWORKS with its only entry commented out, so a mirror comes up with an +// empty list while the owner deploys containers onto a network named in the owner's templates — +// which is exactly how twelve containers ended up created against a network that did not exist. +// +// Adopting the owner's list also means docker_network_connect.sh will recreate the network here +// after an Unraid update wipes it, which is the whole reason that script exists. +if ($vvDetected['role'] === 'partner' && $vvDetected['slot'] !== '') { + $vvOwnerConf = VV_CONF_RAM_CACHE_DIR . '/host1.conf'; + if (is_readable($vvOwnerConf)) { + $vvOwnerRaw = (string)@file_get_contents($vvOwnerConf); + $vvNets = vv_parse_conf_list($vvOwnerRaw, 'HOST1_NETWORK_CONNECT_NETWORKS'); + // br* is ipvlan/macvlan tied to the owner's own hardware — its parent interface does not + // transfer, and adopting the name would attach this host's containers to the wrong thing. + $vvNets = array_values(array_filter($vvNets, fn($n) => + $n !== '' && !preg_match('/^(bridge|host|none|br\d)/i', $n))); + if ($vvNets) { + $vvDetected['networks'] = $vvNets; + $vvDetected['networks_src'] = basename($vvOwnerConf); + } + } +} ?>