fix(panel): use the hosting node address for WireGuard client configs (#5679)

* fix(panel): use the hosting node address for WireGuard client configs

The clients page rendered a node-managed WireGuard inbound's config with the
master panel's host in Endpoint instead of the hosting node's address, so the
copied/QR config pointed at the wrong server. The subscription path already
resolves this via resolveInboundAddress; the UI generator did not.

Expose the share-host resolution inputs (node address, listen, share-address
strategy/address) on InboundOption and route buildWireguardClientConfig through
the same canonical resolver the inbounds-page share links use, extracted as
resolveShareHost. This also brings local inbounds with a shareable listen or a
listen/custom share strategy into parity with the subscription Endpoint; the
common listen=0.0.0.0 case still falls back to the panel host.

* fix(frontend): keep a raw fallback host and refresh node-fed inbound options

Code review of the WireGuard node-endpoint change surfaced two gaps.
resolveShareHost normalized its last-resort fallbackHostname, so a panel
reached via a hostname the share-host grammar rejects (underscore label,
trailing-dot FQDN) emitted a broken 'Endpoint = :51820'; the fallback now
stays verbatim when normalization empties it. Node mutations only
invalidated the nodes query, leaving the staleTime-Infinity inbound
options cache serving an edited node address until the sync job
broadcast (never, for disabled/offline nodes); they now invalidate the
options key too.

Also folds the ShareHostFields projections into direct structural passes,
elides the default node shareAddrStrategy so omitempty drops it, and
replaces the nullable node-address scan with COALESCE.

---------

Co-authored-by: STRENCH0 <17428017+STRENCH0@users.noreply.github.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
Grigoriy
2026-07-03 02:12:32 +03:00
committed by GitHub
parent dbdecda03f
commit f90e4a6962
12 changed files with 267 additions and 46 deletions
+41 -21
View File
@@ -967,20 +967,51 @@ function isShareableHost(host: string): boolean {
return true;
}
function shareableListen(inbound: Inbound): string {
const listen = inbound.listen.trim();
return listen.length > 0 && !isUnixSocketListen(listen) && isShareableHost(listen)
? normalizeShareHost(listen)
function shareableListenFrom(listen: string): string {
const trimmed = listen.trim();
return trimmed.length > 0 && !isUnixSocketListen(trimmed) && isShareableHost(trimmed)
? normalizeShareHost(trimmed)
: '';
}
type ShareAddrStrategy = 'node' | 'listen' | 'custom';
function shareAddrStrategy(inbound: Inbound): ShareAddrStrategy {
const strategy = inbound.shareAddrStrategy;
return strategy === 'listen' || strategy === 'custom'
? strategy
: 'node';
function normalizeShareAddrStrategy(strategy: string | undefined): ShareAddrStrategy {
return strategy === 'listen' || strategy === 'custom' ? strategy : 'node';
}
// ShareHostFields is the subset of an inbound resolveShareHost needs, so callers
// holding only a lightweight projection (e.g. the clients page InboundOption)
// can pick the same host as the full-inbound share/QR path.
export interface ShareHostFields {
listen?: string;
shareAddr?: string;
shareAddrStrategy?: string;
}
// resolveShareHost picks the host that goes into share/QR links, the browser-side
// analog of the backend resolveInboundAddress. hostOverride is the hosting node's
// address (empty for this panel's own inbounds); fallbackHostname is the
// already-resolved panel/public host used as the last resort — kept verbatim when
// it fails normalization (e.g. an underscore intranet hostname) so the last
// resort never degrades to an empty host.
export function resolveShareHost(
fields: ShareHostFields,
hostOverride: string,
fallbackHostname: string,
): string {
const nodeAddr = normalizeShareHost(hostOverride);
const listenAddr = shareableListenFrom(fields.listen ?? '');
const customAddr = normalizeShareHost(fields.shareAddr ?? '');
const fallbackAddr = normalizeShareHost(fallbackHostname) || fallbackHostname.trim();
switch (normalizeShareAddrStrategy(fields.shareAddrStrategy)) {
case 'listen':
return listenAddr || nodeAddr || fallbackAddr;
case 'custom':
return customAddr || nodeAddr || listenAddr || fallbackAddr;
default:
return nodeAddr || listenAddr || fallbackAddr;
}
}
// Orchestrators.
@@ -989,18 +1020,7 @@ function shareAddrStrategy(inbound: Inbound): ShareAddrStrategy {
// node-managed inbounds; other strategies let a row prefer its listen address
// or a custom endpoint.
export function resolveAddr(inbound: Inbound, hostOverride: string, fallbackHostname: string): string {
const nodeAddr = normalizeShareHost(hostOverride);
const listenAddr = shareableListen(inbound);
const customAddr = normalizeShareHost(inbound.shareAddr ?? '');
const fallbackAddr = normalizeShareHost(fallbackHostname);
switch (shareAddrStrategy(inbound)) {
case 'listen':
return listenAddr || nodeAddr || fallbackAddr;
case 'custom':
return customAddr || nodeAddr || listenAddr || fallbackAddr;
default:
return nodeAddr || listenAddr || fallbackAddr;
}
return resolveShareHost(inbound, hostOverride, fallbackHostname);
}
// A loopback browser host means the panel was reached through a tunnel (e.g.