fix(hosts): advertise Hosts in every WireGuard, AmneziaWG and TUIC config

Invariant: an inbound's enabled Hosts are the endpoints every client config
for it advertises, whichever surface renders that config.

WireGuard and AmneziaWG broke it. Their raw generators ignored the
externalProxy entries Hosts are injected as and always emitted
resolveInboundAddress, so the raw subscription, the sub page .conf, the
clients links API and "export all links" gave out the panel address while
the JSON and Clash formats of the same inbound used the Host.
advertisedEndpoints now states the fan-out once for mtproto, wireguard and
amneziawg.

The browser-built configs had the same gap. The Clients page WireGuard and
AmneziaWG config blocks and QR panels, and its TUIC Clash config, used the
panel hostname next to server links that already used Hosts; the Inbounds
page peer configs, QR and export ignored them too. withMtprotoHostEndpoints
becomes withHostEndpoints over a shared hostEndpointsFor mirror of the
backend, the tunnel fan-outs render one config per Host, and the clients
page waits for the hosts list the way the inbounds page does, so an empty
list means "no hosts" rather than "not loaded yet".
This commit is contained in:
MHSanaei
2026-09-30 15:03:16 +02:00
parent 8c023d13dc
commit aee45ca3fe
20 changed files with 876 additions and 362 deletions
+40 -26
View File
@@ -30,6 +30,8 @@ import {
findAmneziaWGInbounds,
isAmneziaWGClient,
} from './amneziawgConfig';
import { tunnelConfigEndpoints, tunnelEndpointLabel } from './tunnelEndpoints';
import type { HostRecord } from '@/schemas/api/host';
import './ClientInfoModal.css';
const INBOUND_PROTOCOL_COLORS: Record<string, string> = {
@@ -66,9 +68,12 @@ interface ClientInfoModalProps {
tunnelAllowedIPs?: Record<number, string>;
isOnline: boolean;
subSettings?: SubSettings;
hosts?: HostRecord[];
onOpenChange: (open: boolean) => void;
}
const NO_HOSTS: HostRecord[] = [];
interface ApiMsg<T = unknown> {
success?: boolean;
obj?: T;
@@ -97,6 +102,7 @@ export default function ClientInfoModal({
tunnelAllowedIPs,
isOnline,
subSettings = DEFAULT_SUB,
hosts = NO_HOSTS,
onOpenChange,
}: ClientInfoModalProps) {
const { datepicker } = useDatepicker();
@@ -187,20 +193,19 @@ export default function ClientInfoModal({
);
const wgConfigs = useMemo(() => {
if (!client || !isWireguardClient(client)) return [];
const host = window.location.hostname;
const publicHost = subSettings?.publicHost ?? '';
return wgInbounds
.map((ib) => {
.flatMap((ib) => {
const address = tunnelAllowedIPs?.[ib.id] ?? '';
const text = buildWireguardClientConfig(
client,
ib,
window.location.hostname,
subSettings?.publicHost ?? '',
address,
);
return { inbound: ib, text };
return tunnelConfigEndpoints(ib, hosts, host, publicHost).map((ep) => ({
inbound: ib,
endpoint: tunnelEndpointLabel(ep),
text: buildWireguardClientConfig(client, ib, host, publicHost, address, ep),
}));
})
.filter((c) => !!c.text);
}, [client, wgInbounds, tunnelAllowedIPs, subSettings?.publicHost]);
}, [client, wgInbounds, tunnelAllowedIPs, subSettings?.publicHost, hosts]);
const awgInbounds = useMemo(
() => findAmneziaWGInbounds(client, inboundsById),
@@ -208,20 +213,19 @@ export default function ClientInfoModal({
);
const awgConfigs = useMemo(() => {
if (!client || !isAmneziaWGClient(client)) return [];
const host = window.location.hostname;
const publicHost = subSettings?.publicHost ?? '';
return awgInbounds
.map((ib) => {
.flatMap((ib) => {
const address = tunnelAllowedIPs?.[ib.id] ?? '';
const text = buildAmneziaWGClientConfig(
client,
ib,
window.location.hostname,
subSettings?.publicHost ?? '',
address,
);
return { inbound: ib, text };
return tunnelConfigEndpoints(ib, hosts, host, publicHost).map((ep) => ({
inbound: ib,
endpoint: tunnelEndpointLabel(ep),
text: buildAmneziaWGClientConfig(client, ib, host, publicHost, address, ep),
}));
})
.filter((c) => !!c.text);
}, [client, awgInbounds, tunnelAllowedIPs, subSettings?.publicHost]);
}, [client, awgInbounds, tunnelAllowedIPs, subSettings?.publicHost, hosts]);
async function copyValue(text: string) {
if (!text) return;
@@ -795,11 +799,16 @@ export default function ClientInfoModal({
{wgConfigs.length > 0 && client && (
<>
<Divider>{t('pages.clients.wireguardConfig')}</Divider>
{wgConfigs.map(({ inbound, text }) => {
const meta = formatTunnelConfigMeta(inbound, client.email, wgConfigs.length);
{wgConfigs.map(({ inbound, endpoint, text }) => {
const meta = formatTunnelConfigMeta(
inbound,
client.email,
wgConfigs.length,
endpoint,
);
return (
<ConfigBlock
key={`wg-${inbound.id}`}
key={`wg-${inbound.id}-${endpoint}`}
label={meta.label || t('pages.clients.config')}
text={text}
fileName={meta.fileName}
@@ -814,11 +823,16 @@ export default function ClientInfoModal({
{awgConfigs.length > 0 && client && (
<>
<Divider>{t('pages.clients.amneziaWgConfig')}</Divider>
{awgConfigs.map(({ inbound, text }) => {
const meta = formatTunnelConfigMeta(inbound, client.email, awgConfigs.length);
{awgConfigs.map(({ inbound, endpoint, text }) => {
const meta = formatTunnelConfigMeta(
inbound,
client.email,
awgConfigs.length,
endpoint,
);
return (
<ConfigBlock
key={`awg-${inbound.id}`}
key={`awg-${inbound.id}-${endpoint}`}
label={meta.label || t('pages.clients.config')}
text={text}
fileName={meta.fileName}