mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-02 16:37:14 +00:00
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:
@@ -1828,6 +1828,13 @@
|
|||||||
"example": 1,
|
"example": 1,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"listen": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"nodeAddress": {
|
||||||
|
"description": "Share-host resolution inputs, mirroring the subscription's\nresolveInboundAddress so the clients page renders a node-managed WireGuard\nEndpoint that points at the node, not the master panel. NodeAddress is the\nhosting node's externally reachable address (empty for this panel's own\ninbounds); Listen and ShareAddrStrategy/ShareAddr feed the same\nnode→listen→custom fallback the share/QR links already use.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"nodeId": {
|
"nodeId": {
|
||||||
"description": "Hosting node; nil for this panel's own inbounds. Lets the clients\npage map a node filter onto inbound IDs (#4997).",
|
"description": "Hosting node; nil for this panel's own inbounds. Lets the clients\npage map a node filter onto inbound IDs (#4997).",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
@@ -1845,6 +1852,12 @@
|
|||||||
"example": "VLESS-443",
|
"example": "VLESS-443",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"shareAddr": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"shareAddrStrategy": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"ssMethod": {
|
"ssMethod": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -2783,10 +2796,14 @@
|
|||||||
"obj": [
|
"obj": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"listen": "",
|
||||||
|
"nodeAddress": "",
|
||||||
"nodeId": null,
|
"nodeId": null,
|
||||||
"port": 443,
|
"port": 443,
|
||||||
"protocol": "vless",
|
"protocol": "vless",
|
||||||
"remark": "VLESS-443",
|
"remark": "VLESS-443",
|
||||||
|
"shareAddr": "",
|
||||||
|
"shareAddrStrategy": "",
|
||||||
"ssMethod": "",
|
"ssMethod": "",
|
||||||
"tag": "in-443-tcp",
|
"tag": "in-443-tcp",
|
||||||
"tlsFlowCapable": true,
|
"tlsFlowCapable": true,
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ export interface RemoteInboundOption {
|
|||||||
|
|
||||||
export function useNodeMutations() {
|
export function useNodeMutations() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: keys.nodes.root() });
|
const invalidate = () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: keys.nodes.root() });
|
||||||
|
queryClient.invalidateQueries({ queryKey: keys.inbounds.options() });
|
||||||
|
};
|
||||||
|
|
||||||
const createMut = useMutation({
|
const createMut = useMutation({
|
||||||
mutationFn: (payload: Partial<NodeRecord>) =>
|
mutationFn: (payload: Partial<NodeRecord>) =>
|
||||||
|
|||||||
@@ -400,10 +400,14 @@ export const EXAMPLES: Record<string, unknown> = {
|
|||||||
},
|
},
|
||||||
"InboundOption": {
|
"InboundOption": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"listen": "",
|
||||||
|
"nodeAddress": "",
|
||||||
"nodeId": null,
|
"nodeId": null,
|
||||||
"port": 443,
|
"port": 443,
|
||||||
"protocol": "vless",
|
"protocol": "vless",
|
||||||
"remark": "VLESS-443",
|
"remark": "VLESS-443",
|
||||||
|
"shareAddr": "",
|
||||||
|
"shareAddrStrategy": "",
|
||||||
"ssMethod": "",
|
"ssMethod": "",
|
||||||
"tag": "in-443-tcp",
|
"tag": "in-443-tcp",
|
||||||
"tlsFlowCapable": true,
|
"tlsFlowCapable": true,
|
||||||
|
|||||||
@@ -1802,6 +1802,13 @@ export const SCHEMAS: Record<string, unknown> = {
|
|||||||
"example": 1,
|
"example": 1,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"listen": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"nodeAddress": {
|
||||||
|
"description": "Share-host resolution inputs, mirroring the subscription's\nresolveInboundAddress so the clients page renders a node-managed WireGuard\nEndpoint that points at the node, not the master panel. NodeAddress is the\nhosting node's externally reachable address (empty for this panel's own\ninbounds); Listen and ShareAddrStrategy/ShareAddr feed the same\nnode→listen→custom fallback the share/QR links already use.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"nodeId": {
|
"nodeId": {
|
||||||
"description": "Hosting node; nil for this panel's own inbounds. Lets the clients\npage map a node filter onto inbound IDs (#4997).",
|
"description": "Hosting node; nil for this panel's own inbounds. Lets the clients\npage map a node filter onto inbound IDs (#4997).",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
@@ -1819,6 +1826,12 @@ export const SCHEMAS: Record<string, unknown> = {
|
|||||||
"example": "VLESS-443",
|
"example": "VLESS-443",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"shareAddr": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"shareAddrStrategy": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"ssMethod": {
|
"ssMethod": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -394,10 +394,14 @@ export interface InboundFallback {
|
|||||||
|
|
||||||
export interface InboundOption {
|
export interface InboundOption {
|
||||||
id: number;
|
id: number;
|
||||||
|
listen?: string;
|
||||||
|
nodeAddress?: string;
|
||||||
nodeId?: number | null;
|
nodeId?: number | null;
|
||||||
port: number;
|
port: number;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
remark: string;
|
remark: string;
|
||||||
|
shareAddr?: string;
|
||||||
|
shareAddrStrategy?: string;
|
||||||
ssMethod: string;
|
ssMethod: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
tlsFlowCapable: boolean;
|
tlsFlowCapable: boolean;
|
||||||
|
|||||||
@@ -421,10 +421,14 @@ export type InboundFallback = z.infer<typeof InboundFallbackSchema>;
|
|||||||
|
|
||||||
export const InboundOptionSchema = z.object({
|
export const InboundOptionSchema = z.object({
|
||||||
id: z.number().int(),
|
id: z.number().int(),
|
||||||
|
listen: z.string().optional(),
|
||||||
|
nodeAddress: z.string().optional(),
|
||||||
nodeId: z.number().int().nullable().optional(),
|
nodeId: z.number().int().nullable().optional(),
|
||||||
port: z.number().int(),
|
port: z.number().int(),
|
||||||
protocol: z.string(),
|
protocol: z.string(),
|
||||||
remark: z.string(),
|
remark: z.string(),
|
||||||
|
shareAddr: z.string().optional(),
|
||||||
|
shareAddrStrategy: z.string().optional(),
|
||||||
ssMethod: z.string(),
|
ssMethod: z.string(),
|
||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
tlsFlowCapable: z.boolean(),
|
tlsFlowCapable: z.boolean(),
|
||||||
|
|||||||
@@ -967,20 +967,51 @@ function isShareableHost(host: string): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shareableListen(inbound: Inbound): string {
|
function shareableListenFrom(listen: string): string {
|
||||||
const listen = inbound.listen.trim();
|
const trimmed = listen.trim();
|
||||||
return listen.length > 0 && !isUnixSocketListen(listen) && isShareableHost(listen)
|
return trimmed.length > 0 && !isUnixSocketListen(trimmed) && isShareableHost(trimmed)
|
||||||
? normalizeShareHost(listen)
|
? normalizeShareHost(trimmed)
|
||||||
: '';
|
: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShareAddrStrategy = 'node' | 'listen' | 'custom';
|
type ShareAddrStrategy = 'node' | 'listen' | 'custom';
|
||||||
|
|
||||||
function shareAddrStrategy(inbound: Inbound): ShareAddrStrategy {
|
function normalizeShareAddrStrategy(strategy: string | undefined): ShareAddrStrategy {
|
||||||
const strategy = inbound.shareAddrStrategy;
|
return strategy === 'listen' || strategy === 'custom' ? strategy : 'node';
|
||||||
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.
|
// Orchestrators.
|
||||||
@@ -989,18 +1020,7 @@ function shareAddrStrategy(inbound: Inbound): ShareAddrStrategy {
|
|||||||
// node-managed inbounds; other strategies let a row prefer its listen address
|
// node-managed inbounds; other strategies let a row prefer its listen address
|
||||||
// or a custom endpoint.
|
// or a custom endpoint.
|
||||||
export function resolveAddr(inbound: Inbound, hostOverride: string, fallbackHostname: string): string {
|
export function resolveAddr(inbound: Inbound, hostOverride: string, fallbackHostname: string): string {
|
||||||
const nodeAddr = normalizeShareHost(hostOverride);
|
return resolveShareHost(inbound, hostOverride, fallbackHostname);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A loopback browser host means the panel was reached through a tunnel (e.g.
|
// A loopback browser host means the panel was reached through a tunnel (e.g.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { formatInboundLabel } from '@/lib/inbounds/label';
|
import { formatInboundLabel } from '@/lib/inbounds/label';
|
||||||
import { preferPublicHost } from '@/lib/xray/inbound-link';
|
import { preferPublicHost, resolveShareHost } from '@/lib/xray/inbound-link';
|
||||||
import type { ClientRecord, InboundOption } from '@/hooks/useClients';
|
import type { ClientRecord, InboundOption } from '@/hooks/useClients';
|
||||||
|
|
||||||
export function isWireguardClient(client: ClientRecord | null | undefined): boolean {
|
export function isWireguardClient(client: ClientRecord | null | undefined): boolean {
|
||||||
@@ -22,7 +22,7 @@ export function buildWireguardClientConfig(
|
|||||||
host = window.location.hostname,
|
host = window.location.hostname,
|
||||||
publicHost = '',
|
publicHost = '',
|
||||||
): string {
|
): string {
|
||||||
const endpointHost = preferPublicHost(host, publicHost);
|
const endpointHost = resolveShareHost(inbound ?? {}, inbound?.nodeAddress ?? '', preferPublicHost(host, publicHost));
|
||||||
const address = client.allowedIPs || '10.0.0.2/32';
|
const address = client.allowedIPs || '10.0.0.2/32';
|
||||||
const endpoint = `${endpointHost}:${inbound?.port || ''}`;
|
const endpoint = `${endpointHost}:${inbound?.port || ''}`;
|
||||||
const inboundName = inbound ? formatInboundLabel(inbound.tag, inbound.remark) : '';
|
const inboundName = inbound ? formatInboundLabel(inbound.tag, inbound.remark) : '';
|
||||||
|
|||||||
@@ -54,6 +54,13 @@ export const InboundOptionSchema = z.object({
|
|||||||
wgDns: z.string().optional(),
|
wgDns: z.string().optional(),
|
||||||
// Hosting node id; absent/null for this panel's own inbounds (#4997).
|
// Hosting node id; absent/null for this panel's own inbounds (#4997).
|
||||||
nodeId: z.number().nullable().optional(),
|
nodeId: z.number().nullable().optional(),
|
||||||
|
// Share-host resolution inputs, mirroring the backend resolveInboundAddress so
|
||||||
|
// the clients page picks the same WireGuard endpoint host as the subscription:
|
||||||
|
// the hosting node address, the inbound listen, and its share-address strategy.
|
||||||
|
nodeAddress: z.string().optional(),
|
||||||
|
listen: z.string().optional(),
|
||||||
|
shareAddr: z.string().optional(),
|
||||||
|
shareAddrStrategy: z.string().optional(),
|
||||||
}).loose();
|
}).loose();
|
||||||
|
|
||||||
export const InboundOptionsSchema = z.array(InboundOptionSchema);
|
export const InboundOptionsSchema = z.array(InboundOptionSchema);
|
||||||
|
|||||||
@@ -52,4 +52,41 @@ describe('buildWireguardClientConfig', () => {
|
|||||||
const cfg = buildWireguardClientConfig({ ...client, preSharedKey: undefined }, inbound, 'example.com', '');
|
const cfg = buildWireguardClientConfig({ ...client, preSharedKey: undefined }, inbound, 'example.com', '');
|
||||||
expect(cfg).not.toContain('PresharedKey');
|
expect(cfg).not.toContain('PresharedKey');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses the hosting node address as the endpoint host for node-managed inbounds', () => {
|
||||||
|
const cfg = buildWireguardClientConfig(client, { ...inbound, nodeAddress: 'node.example.net' }, 'master.example.com', '');
|
||||||
|
expect(cfg).toContain('Endpoint = node.example.net:51820');
|
||||||
|
expect(cfg).not.toContain('master.example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the panel host when the node address is blank', () => {
|
||||||
|
const cfg = buildWireguardClientConfig(client, { ...inbound, nodeAddress: ' ' }, 'master.example.com', '');
|
||||||
|
expect(cfg).toContain('Endpoint = master.example.com:51820');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('honors the custom share-address strategy over the node address', () => {
|
||||||
|
const cfg = buildWireguardClientConfig(
|
||||||
|
client,
|
||||||
|
{ ...inbound, nodeAddress: 'node.example.net', shareAddrStrategy: 'custom', shareAddr: 'vpn.example.com' },
|
||||||
|
'master.example.com',
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
expect(cfg).toContain('Endpoint = vpn.example.com:51820');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('honors the listen share-address strategy over the node address', () => {
|
||||||
|
const cfg = buildWireguardClientConfig(
|
||||||
|
client,
|
||||||
|
{ ...inbound, nodeAddress: 'node.example.net', shareAddrStrategy: 'listen', listen: '198.51.100.7' },
|
||||||
|
'master.example.com',
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
expect(cfg).toContain('Endpoint = 198.51.100.7:51820');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps a panel hostname that fails share-host normalization instead of emitting an empty endpoint', () => {
|
||||||
|
const cfg = buildWireguardClientConfig(client, { ...inbound, listen: '0.0.0.0' }, 'wg_gw.corp.lan', '');
|
||||||
|
expect(cfg).toContain('Endpoint = wg_gw.corp.lan:51820');
|
||||||
|
expect(cfg).not.toContain('Endpoint = :51820');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -305,24 +305,39 @@ type InboundOption struct {
|
|||||||
// Hosting node; nil for this panel's own inbounds. Lets the clients
|
// Hosting node; nil for this panel's own inbounds. Lets the clients
|
||||||
// page map a node filter onto inbound IDs (#4997).
|
// page map a node filter onto inbound IDs (#4997).
|
||||||
NodeId *int `json:"nodeId,omitempty"`
|
NodeId *int `json:"nodeId,omitempty"`
|
||||||
|
// Share-host resolution inputs, mirroring the subscription's
|
||||||
|
// resolveInboundAddress so the clients page renders a node-managed WireGuard
|
||||||
|
// Endpoint that points at the node, not the master panel. NodeAddress is the
|
||||||
|
// hosting node's externally reachable address (empty for this panel's own
|
||||||
|
// inbounds); Listen and ShareAddrStrategy/ShareAddr feed the same
|
||||||
|
// node→listen→custom fallback the share/QR links already use.
|
||||||
|
NodeAddress string `json:"nodeAddress,omitempty"`
|
||||||
|
Listen string `json:"listen,omitempty"`
|
||||||
|
ShareAddr string `json:"shareAddr,omitempty"`
|
||||||
|
ShareAddrStrategy string `json:"shareAddrStrategy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error) {
|
func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
var rows []struct {
|
var rows []struct {
|
||||||
Id int `gorm:"column:id"`
|
Id int `gorm:"column:id"`
|
||||||
Remark string `gorm:"column:remark"`
|
Remark string `gorm:"column:remark"`
|
||||||
Tag string `gorm:"column:tag"`
|
Tag string `gorm:"column:tag"`
|
||||||
Protocol string `gorm:"column:protocol"`
|
Protocol string `gorm:"column:protocol"`
|
||||||
Port int `gorm:"column:port"`
|
Port int `gorm:"column:port"`
|
||||||
StreamSettings string `gorm:"column:stream_settings"`
|
StreamSettings string `gorm:"column:stream_settings"`
|
||||||
Settings string `gorm:"column:settings"`
|
Settings string `gorm:"column:settings"`
|
||||||
NodeId *int `gorm:"column:node_id"`
|
Listen string `gorm:"column:listen"`
|
||||||
|
ShareAddr string `gorm:"column:share_addr"`
|
||||||
|
ShareAddrStrategy string `gorm:"column:share_addr_strategy"`
|
||||||
|
NodeId *int `gorm:"column:node_id"`
|
||||||
|
NodeAddress string `gorm:"column:node_address"`
|
||||||
}
|
}
|
||||||
err := db.Table("inbounds").
|
err := db.Table("inbounds").
|
||||||
Select("id, remark, tag, protocol, port, stream_settings, settings, node_id").
|
Select("inbounds.id, inbounds.remark, inbounds.tag, inbounds.protocol, inbounds.port, inbounds.stream_settings, inbounds.settings, inbounds.listen, inbounds.share_addr, inbounds.share_addr_strategy, inbounds.node_id, COALESCE(nodes.address, '') AS node_address").
|
||||||
Where("user_id = ?", userId).
|
Joins("LEFT JOIN nodes ON nodes.id = inbounds.node_id").
|
||||||
Order("id ASC").
|
Where("inbounds.user_id = ?", userId).
|
||||||
|
Order("inbounds.id ASC").
|
||||||
Scan(&rows).Error
|
Scan(&rows).Error
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -330,18 +345,26 @@ func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error)
|
|||||||
out := make([]InboundOption, 0, len(rows))
|
out := make([]InboundOption, 0, len(rows))
|
||||||
for _, r := range rows {
|
for _, r := range rows {
|
||||||
wgPublicKey, wgMtu, wgDns := inboundWireguardHints(r.Protocol, r.Settings)
|
wgPublicKey, wgMtu, wgDns := inboundWireguardHints(r.Protocol, r.Settings)
|
||||||
|
shareAddrStrategy := r.ShareAddrStrategy
|
||||||
|
if shareAddrStrategy == "node" {
|
||||||
|
shareAddrStrategy = ""
|
||||||
|
}
|
||||||
out = append(out, InboundOption{
|
out = append(out, InboundOption{
|
||||||
Id: r.Id,
|
Id: r.Id,
|
||||||
Remark: r.Remark,
|
Remark: r.Remark,
|
||||||
Tag: r.Tag,
|
Tag: r.Tag,
|
||||||
Protocol: r.Protocol,
|
Protocol: r.Protocol,
|
||||||
Port: r.Port,
|
Port: r.Port,
|
||||||
TlsFlowCapable: inboundCanEnableTlsFlow(r.Protocol, r.StreamSettings, r.Settings),
|
TlsFlowCapable: inboundCanEnableTlsFlow(r.Protocol, r.StreamSettings, r.Settings),
|
||||||
SsMethod: inboundShadowsocksMethod(r.Protocol, r.Settings),
|
SsMethod: inboundShadowsocksMethod(r.Protocol, r.Settings),
|
||||||
WgPublicKey: wgPublicKey,
|
WgPublicKey: wgPublicKey,
|
||||||
WgMtu: wgMtu,
|
WgMtu: wgMtu,
|
||||||
WgDns: wgDns,
|
WgDns: wgDns,
|
||||||
NodeId: r.NodeId,
|
NodeId: r.NodeId,
|
||||||
|
NodeAddress: r.NodeAddress,
|
||||||
|
Listen: r.Listen,
|
||||||
|
ShareAddr: r.ShareAddr,
|
||||||
|
ShareAddrStrategy: shareAddrStrategy,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestGetInboundOptions_NodeAddress verifies that a node-managed inbound carries
|
||||||
|
// its hosting node's externally reachable address, while this panel's own
|
||||||
|
// inbounds report an empty NodeAddress. The clients page uses it as the
|
||||||
|
// WireGuard endpoint host so a copied config points at the node, not the master.
|
||||||
|
func TestGetInboundOptions_NodeAddress(t *testing.T) {
|
||||||
|
setupConflictDB(t)
|
||||||
|
|
||||||
|
node := &model.Node{Name: "de-fra-1", Address: "node.example.net", Port: 2053, Enable: true}
|
||||||
|
if err := database.GetDB().Create(node).Error; err != nil {
|
||||||
|
t.Fatalf("create node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeInbound := &model.Inbound{
|
||||||
|
UserId: 1,
|
||||||
|
Tag: "in-51820-udp",
|
||||||
|
Enable: true,
|
||||||
|
Listen: "0.0.0.0",
|
||||||
|
Port: 51820,
|
||||||
|
Protocol: model.WireGuard,
|
||||||
|
Settings: `{"clients":[],"secretKey":"QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0="}`,
|
||||||
|
NodeID: &node.Id,
|
||||||
|
}
|
||||||
|
localInbound := &model.Inbound{
|
||||||
|
UserId: 1,
|
||||||
|
Tag: "in-443-tcp",
|
||||||
|
Enable: true,
|
||||||
|
Listen: "0.0.0.0",
|
||||||
|
Port: 443,
|
||||||
|
Protocol: model.VLESS,
|
||||||
|
StreamSettings: `{"network":"tcp"}`,
|
||||||
|
Settings: `{"clients":[]}`,
|
||||||
|
ShareAddrStrategy: "custom",
|
||||||
|
ShareAddr: "vpn.example.com",
|
||||||
|
}
|
||||||
|
if err := database.GetDB().Create(nodeInbound).Error; err != nil {
|
||||||
|
t.Fatalf("create node inbound: %v", err)
|
||||||
|
}
|
||||||
|
if err := database.GetDB().Create(localInbound).Error; err != nil {
|
||||||
|
t.Fatalf("create local inbound: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := &InboundService{}
|
||||||
|
options, err := svc.GetInboundOptions(1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetInboundOptions: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
byID := make(map[int]InboundOption, len(options))
|
||||||
|
for _, o := range options {
|
||||||
|
byID[o.Id] = o
|
||||||
|
}
|
||||||
|
|
||||||
|
got, ok := byID[nodeInbound.Id]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("node inbound %d missing from options", nodeInbound.Id)
|
||||||
|
}
|
||||||
|
if got.NodeAddress != "node.example.net" {
|
||||||
|
t.Fatalf("node inbound NodeAddress = %q, want node.example.net", got.NodeAddress)
|
||||||
|
}
|
||||||
|
if got.Listen != "0.0.0.0" {
|
||||||
|
t.Fatalf("node inbound Listen = %q, want 0.0.0.0", got.Listen)
|
||||||
|
}
|
||||||
|
if got.ShareAddrStrategy != "" {
|
||||||
|
t.Fatalf("node inbound ShareAddrStrategy = %q, want empty (the default node strategy is elided so omitempty drops it)", got.ShareAddrStrategy)
|
||||||
|
}
|
||||||
|
|
||||||
|
local, ok := byID[localInbound.Id]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("local inbound %d missing from options", localInbound.Id)
|
||||||
|
}
|
||||||
|
if local.NodeAddress != "" {
|
||||||
|
t.Fatalf("local inbound NodeAddress = %q, want empty", local.NodeAddress)
|
||||||
|
}
|
||||||
|
if local.ShareAddrStrategy != "custom" {
|
||||||
|
t.Fatalf("local inbound ShareAddrStrategy = %q, want custom", local.ShareAddrStrategy)
|
||||||
|
}
|
||||||
|
if local.ShareAddr != "vpn.example.com" {
|
||||||
|
t.Fatalf("local inbound ShareAddr = %q, want vpn.example.com", local.ShareAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user