mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-15 10:25:00 +03:00
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:
@@ -72,36 +72,70 @@ function splitAdvertisedHost(value: string, inboundPort: number): [string, numbe
|
||||
return match ? [match[1], Number(match[2])] : [host, inboundPort];
|
||||
}
|
||||
|
||||
export function withMtprotoHostEndpoints(
|
||||
export interface HostEndpoint {
|
||||
dest: string;
|
||||
port: number;
|
||||
remark: string;
|
||||
sni?: string;
|
||||
alpn?: string[];
|
||||
allowInsecure?: boolean;
|
||||
}
|
||||
|
||||
// hostEndpointsFor mirrors the backend hostEndpoints + hostToExternalProxyMap:
|
||||
// enabled Hosts of that sub type only; a blank address or port inherits the inbound's.
|
||||
export function hostEndpointsFor(
|
||||
records: HostRecord[],
|
||||
inboundId: number,
|
||||
inboundPort: number,
|
||||
defaultDest: string,
|
||||
subType: 'raw' | 'clash' = 'raw',
|
||||
): HostEndpoint[] {
|
||||
const endpoints: HostEndpoint[] = [];
|
||||
for (const record of records) {
|
||||
if (
|
||||
record.isDisabled ||
|
||||
!record.inboundIds.includes(inboundId) ||
|
||||
record.excludeFromSubTypes?.includes(subType)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (const value of record.hosts) {
|
||||
const [address, port] = splitAdvertisedHost(value, inboundPort);
|
||||
const dest = address || defaultDest;
|
||||
const endpoint: HostEndpoint = { dest, port, remark: record.remark || '' };
|
||||
const sni = record.overrideSniFromAddress ? dest : record.sni;
|
||||
if (!record.keepSniBlank && sni) endpoint.sni = sni;
|
||||
if (record.alpn && record.alpn.length > 0) endpoint.alpn = record.alpn;
|
||||
if (record.allowInsecure) endpoint.allowInsecure = true;
|
||||
endpoints.push(endpoint);
|
||||
}
|
||||
}
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
// Panel-built links of these protocols read Hosts; the rest still show the
|
||||
// inbound's own address on the inbounds page.
|
||||
const HOST_LINK_PROTOCOLS: ReadonlySet<string> = new Set(['mtproto', 'wireguard', 'amneziawg']);
|
||||
|
||||
export function withHostEndpoints(
|
||||
inbound: Inbound,
|
||||
inboundId: number,
|
||||
records: HostRecord[],
|
||||
hostOverride: string,
|
||||
fallbackHostname: string,
|
||||
): Inbound {
|
||||
if (inbound.protocol !== 'mtproto') return inbound;
|
||||
const endpoints: ExternalProxyEntry[] = [];
|
||||
for (const record of records) {
|
||||
if (
|
||||
record.isDisabled ||
|
||||
!record.inboundIds.includes(inboundId) ||
|
||||
record.excludeFromSubTypes?.includes('raw')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (const value of record.hosts) {
|
||||
const [dest, port] = splitAdvertisedHost(value, inbound.port);
|
||||
endpoints.push({
|
||||
forceTls: 'same',
|
||||
dest: dest || resolveAddr(inbound, hostOverride, fallbackHostname),
|
||||
port,
|
||||
remark: record.remark || '',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!HOST_LINK_PROTOCOLS.has(inbound.protocol)) return inbound;
|
||||
const defaultDest = resolveAddr(inbound, hostOverride, fallbackHostname);
|
||||
const endpoints = hostEndpointsFor(records, inboundId, inbound.port, defaultDest);
|
||||
if (endpoints.length === 0) return inbound;
|
||||
const externalProxy: ExternalProxyEntry[] = endpoints.map(({ dest, port, remark }) => ({
|
||||
forceTls: 'same',
|
||||
dest,
|
||||
port,
|
||||
remark,
|
||||
}));
|
||||
return {
|
||||
...inbound,
|
||||
streamSettings: { ...inbound.streamSettings, externalProxy: endpoints },
|
||||
streamSettings: { ...inbound.streamSettings, externalProxy },
|
||||
} as Inbound;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export function formatTunnelConfigMeta(
|
||||
inbound: { id?: number; tag?: string; remark?: string },
|
||||
email?: string,
|
||||
totalCount = 1,
|
||||
endpoint = '',
|
||||
): {
|
||||
label?: string;
|
||||
fileName: string;
|
||||
@@ -20,13 +21,18 @@ export function formatTunnelConfigMeta(
|
||||
const inboundName =
|
||||
formatInboundLabel(inbound.tag, inbound.remark) ||
|
||||
(inbound.id != null ? `inbound-${inbound.id}` : '');
|
||||
const label = totalCount > 1 ? inboundName : undefined;
|
||||
const suffix = inbound.remark || inbound.tag || (inbound.id != null ? `${inbound.id}` : '');
|
||||
const name = [inboundName, endpoint].filter(Boolean).join(' - ');
|
||||
const label = totalCount > 1 ? name : undefined;
|
||||
const suffix = [
|
||||
inbound.remark || inbound.tag || (inbound.id != null ? `${inbound.id}` : ''),
|
||||
endpoint,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('-');
|
||||
const safeSuffix = suffix ? `-${suffix.replace(/[^\w.-]+/g, '_')}` : '';
|
||||
const emailPrefix = email || 'client';
|
||||
const fileName = `${emailPrefix}${totalCount > 1 ? safeSuffix : ''}.conf`;
|
||||
const qrRemark =
|
||||
totalCount > 1 && inboundName ? [inboundName, email].filter(Boolean).join(' - ') : email || '';
|
||||
const qrRemark = totalCount > 1 && name ? [name, email].filter(Boolean).join(' - ') : email || '';
|
||||
|
||||
return { label, fileName, qrRemark };
|
||||
}
|
||||
|
||||
@@ -1117,44 +1117,43 @@ export interface GenAmneziaWGFanoutInput {
|
||||
fallbackHostname: string;
|
||||
}
|
||||
|
||||
export function genAmneziaWGLinks(input: GenAmneziaWGFanoutInput): string {
|
||||
function amneziaWGFanout(
|
||||
input: GenAmneziaWGFanoutInput,
|
||||
render: (input: GenAmneziaWGLinkInput) => string,
|
||||
): string[][] {
|
||||
const { inbound, remark = '', hostOverride = '', fallbackHostname } = input;
|
||||
if (inbound.protocol !== 'amneziawg') return '';
|
||||
const addr = resolveAddr(inbound, hostOverride, fallbackHostname);
|
||||
const sep = '-';
|
||||
if (inbound.protocol !== 'amneziawg') return [];
|
||||
const endpoints = tunnelEndpoints(inbound, resolveAddr(inbound, hostOverride, fallbackHostname));
|
||||
const settings = inbound.settings as AmneziawgInboundSettings;
|
||||
const clients = settings.clients ?? [];
|
||||
return clients
|
||||
.map((c, i) =>
|
||||
genAmneziaWGLink({
|
||||
return clients.map((c, i) =>
|
||||
endpoints.map((e) =>
|
||||
render({
|
||||
settings,
|
||||
address: addr,
|
||||
port: inbound.port,
|
||||
remark: `${remark}${sep}${i + 1}${wgPeerCommentSuffix(c)}`,
|
||||
address: e.address,
|
||||
port: e.port,
|
||||
remark: tunnelPeerRemark(remark, e.remark, i, c),
|
||||
peerIndex: i,
|
||||
}),
|
||||
)
|
||||
.join('\r\n');
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Per-peer lists with one entry per advertised endpoint (Host), peer-major.
|
||||
export function genAmneziaWGPeerLinks(input: GenAmneziaWGFanoutInput): string[][] {
|
||||
return amneziaWGFanout(input, genAmneziaWGLink);
|
||||
}
|
||||
|
||||
export function genAmneziaWGPeerConfigs(input: GenAmneziaWGFanoutInput): string[][] {
|
||||
return amneziaWGFanout(input, genAmneziaWGConfig);
|
||||
}
|
||||
|
||||
export function genAmneziaWGLinks(input: GenAmneziaWGFanoutInput): string {
|
||||
return genAmneziaWGPeerLinks(input).flat().join('\r\n');
|
||||
}
|
||||
|
||||
export function genAmneziaWGConfigs(input: GenAmneziaWGFanoutInput): string {
|
||||
const { inbound, remark = '', hostOverride = '', fallbackHostname } = input;
|
||||
if (inbound.protocol !== 'amneziawg') return '';
|
||||
const addr = resolveAddr(inbound, hostOverride, fallbackHostname);
|
||||
const sep = '-';
|
||||
const settings = inbound.settings as AmneziawgInboundSettings;
|
||||
const clients = settings.clients ?? [];
|
||||
return clients
|
||||
.map((c, i) =>
|
||||
genAmneziaWGConfig({
|
||||
settings,
|
||||
address: addr,
|
||||
port: inbound.port,
|
||||
remark: `${remark}${sep}${i + 1}${wgPeerCommentSuffix(c)}`,
|
||||
peerIndex: i,
|
||||
}),
|
||||
)
|
||||
.join('\r\n');
|
||||
return genAmneziaWGPeerConfigs(input).flat().join('\r\n');
|
||||
}
|
||||
|
||||
export function wireguardConfigFromLink(link: string, fallbackRemark = ''): string {
|
||||
@@ -1624,46 +1623,67 @@ function wgRenderPeers(settings: WireguardInboundSettings): WireguardInboundPeer
|
||||
return settings.peers;
|
||||
}
|
||||
|
||||
export function genWireguardLinks(input: GenWireguardFanoutInput): string {
|
||||
// Hosts reach wireguard/amneziawg as externalProxy entries (withHostEndpoints);
|
||||
// with none, every peer is advertised on the inbound's own address.
|
||||
function tunnelEndpoints(
|
||||
inbound: Inbound,
|
||||
addr: string,
|
||||
): Array<{ address: string; port: number; remark: string }> {
|
||||
const externals = inbound.streamSettings?.externalProxy;
|
||||
if (Array.isArray(externals) && externals.length > 0) {
|
||||
return externals.map((ep) => ({ address: ep.dest, port: ep.port, remark: ep.remark ?? '' }));
|
||||
}
|
||||
return [{ address: addr, port: inbound.port, remark: '' }];
|
||||
}
|
||||
|
||||
function tunnelPeerRemark(
|
||||
remark: string,
|
||||
endpointRemark: string,
|
||||
index: number,
|
||||
peer: unknown,
|
||||
): string {
|
||||
const base = [remark, endpointRemark].filter((x) => x.length > 0).join('-');
|
||||
return `${base}-${index + 1}${wgPeerCommentSuffix(peer)}`;
|
||||
}
|
||||
|
||||
function wireguardFanout(
|
||||
input: GenWireguardFanoutInput,
|
||||
render: (input: GenWireguardLinkInput) => string,
|
||||
): string[][] {
|
||||
const { inbound, remark = '', hostOverride = '', fallbackHostname } = input;
|
||||
if (inbound.protocol !== 'wireguard') return '';
|
||||
const addr = resolveAddr(inbound, hostOverride, fallbackHostname);
|
||||
const sep = '-';
|
||||
if (inbound.protocol !== 'wireguard') return [];
|
||||
const endpoints = tunnelEndpoints(inbound, resolveAddr(inbound, hostOverride, fallbackHostname));
|
||||
const baseSettings = inbound.settings as WireguardInboundSettings;
|
||||
const peers = wgRenderPeers(baseSettings);
|
||||
const settings: WireguardInboundSettings = { ...baseSettings, peers };
|
||||
return peers
|
||||
.map((p, i) =>
|
||||
genWireguardLink({
|
||||
return peers.map((p, i) =>
|
||||
endpoints.map((e) =>
|
||||
render({
|
||||
settings,
|
||||
address: addr,
|
||||
port: inbound.port,
|
||||
remark: `${remark}${sep}${i + 1}${wgPeerCommentSuffix(p)}`,
|
||||
address: e.address,
|
||||
port: e.port,
|
||||
remark: tunnelPeerRemark(remark, e.remark, i, p),
|
||||
peerIndex: i,
|
||||
}),
|
||||
)
|
||||
.join('\r\n');
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Per-peer lists with one entry per advertised endpoint (Host), peer-major.
|
||||
export function genWireguardPeerLinks(input: GenWireguardFanoutInput): string[][] {
|
||||
return wireguardFanout(input, genWireguardLink);
|
||||
}
|
||||
|
||||
export function genWireguardPeerConfigs(input: GenWireguardFanoutInput): string[][] {
|
||||
return wireguardFanout(input, genWireguardConfig);
|
||||
}
|
||||
|
||||
export function genWireguardLinks(input: GenWireguardFanoutInput): string {
|
||||
return genWireguardPeerLinks(input).flat().join('\r\n');
|
||||
}
|
||||
|
||||
export function genWireguardConfigs(input: GenWireguardFanoutInput): string {
|
||||
const { inbound, remark = '', hostOverride = '', fallbackHostname } = input;
|
||||
if (inbound.protocol !== 'wireguard') return '';
|
||||
const addr = resolveAddr(inbound, hostOverride, fallbackHostname);
|
||||
const sep = '-';
|
||||
const baseSettings = inbound.settings as WireguardInboundSettings;
|
||||
const peers = wgRenderPeers(baseSettings);
|
||||
const settings: WireguardInboundSettings = { ...baseSettings, peers };
|
||||
return peers
|
||||
.map((p, i) =>
|
||||
genWireguardConfig({
|
||||
settings,
|
||||
address: addr,
|
||||
port: inbound.port,
|
||||
remark: `${remark}${sep}${i + 1}${wgPeerCommentSuffix(p)}`,
|
||||
peerIndex: i,
|
||||
}),
|
||||
)
|
||||
.join('\r\n');
|
||||
return genWireguardPeerConfigs(input).flat().join('\r\n');
|
||||
}
|
||||
|
||||
// Peer comments (#5168) are panel-side annotations; when present they ride
|
||||
|
||||
Reference in New Issue
Block a user