mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-09 06:06:08 +00:00
feat: add inbound share address strategy (#5162)
* feat: add inbound share address strategy Allow node-managed inbounds to choose whether exported share links use the node address, routable listen address, or a custom endpoint. Preserve locally configured share address fields during remote node traffic sync. Refs #5161 Refs #4891 * fix: preserve inbound share address settings Forward share address fields to remote nodes, keep existing values when older update payloads omit them, align localhost handling between frontend and subscriptions, and preserve share address settings when cloning inbounds. * fix: keep share address strategy out of subscriptions Limit the new share address strategy to direct exported share links and QR codes. Restore subscription address resolution to the existing panel-owned behavior and update the UI help text accordingly. * fix: address share address review feedback * fix: validate custom share address * fix --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -6,6 +6,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > http
|
||||
"Remark",
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -20,6 +21,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > hyste
|
||||
"Remark",
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -34,6 +36,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > mixed
|
||||
"Remark",
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -48,6 +51,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > shado
|
||||
"Remark",
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -62,6 +66,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > troja
|
||||
"Remark",
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -76,6 +81,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > tun 1
|
||||
"Remark",
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -90,6 +96,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > tunne
|
||||
"Remark",
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -104,6 +111,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > vless
|
||||
"Remark",
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -118,6 +126,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > vmess
|
||||
"Remark",
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -132,6 +141,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > wireg
|
||||
"Remark",
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
|
||||
@@ -27,6 +27,8 @@ exports[`InboundSchema (full) fixtures > parses hysteria-v1-tls byte-stably 1`]
|
||||
],
|
||||
"version": 1,
|
||||
},
|
||||
"shareAddr": "",
|
||||
"shareAddrStrategy": "node",
|
||||
"sniffing": {
|
||||
"destOverride": [
|
||||
"http",
|
||||
@@ -112,6 +114,8 @@ exports[`InboundSchema (full) fixtures > parses shadowsocks-tcp-2022 byte-stably
|
||||
"network": "tcp,udp",
|
||||
"password": "ZmFrZS1zZXJ2ZXItcGFzc3dvcmQtMDAwMQ==",
|
||||
},
|
||||
"shareAddr": "",
|
||||
"shareAddrStrategy": "node",
|
||||
"sniffing": {
|
||||
"destOverride": [
|
||||
"http",
|
||||
@@ -168,6 +172,8 @@ exports[`InboundSchema (full) fixtures > parses trojan-ws-tls byte-stably 1`] =
|
||||
],
|
||||
"fallbacks": [],
|
||||
},
|
||||
"shareAddr": "",
|
||||
"shareAddrStrategy": "node",
|
||||
"sniffing": {
|
||||
"destOverride": [
|
||||
"http",
|
||||
@@ -257,6 +263,8 @@ exports[`InboundSchema (full) fixtures > parses vless-tcp-reality byte-stably 1`
|
||||
"encryption": "none",
|
||||
"fallbacks": [],
|
||||
},
|
||||
"shareAddr": "",
|
||||
"shareAddrStrategy": "node",
|
||||
"sniffing": {
|
||||
"destOverride": [
|
||||
"http",
|
||||
@@ -341,6 +349,8 @@ exports[`InboundSchema (full) fixtures > parses vless-ws-tls byte-stably 1`] = `
|
||||
"encryption": "none",
|
||||
"fallbacks": [],
|
||||
},
|
||||
"shareAddr": "",
|
||||
"shareAddrStrategy": "node",
|
||||
"sniffing": {
|
||||
"destOverride": [
|
||||
"http",
|
||||
@@ -430,6 +440,8 @@ exports[`InboundSchema (full) fixtures > parses vless-ws-tls-pinned byte-stably
|
||||
"encryption": "none",
|
||||
"fallbacks": [],
|
||||
},
|
||||
"shareAddr": "",
|
||||
"shareAddrStrategy": "node",
|
||||
"sniffing": {
|
||||
"destOverride": [
|
||||
"http",
|
||||
@@ -520,6 +532,8 @@ exports[`InboundSchema (full) fixtures > parses vmess-tcp-tls byte-stably 1`] =
|
||||
},
|
||||
],
|
||||
},
|
||||
"shareAddr": "",
|
||||
"shareAddrStrategy": "node",
|
||||
"sniffing": {
|
||||
"destOverride": [
|
||||
"http",
|
||||
@@ -603,6 +617,8 @@ exports[`InboundSchema (full) fixtures > parses wireguard-server byte-stably 1`]
|
||||
],
|
||||
"secretKey": "iJ2cBkrSGqRwIfYIDIxk7hr5RXfdR93MfJUL7yqkkH8=",
|
||||
},
|
||||
"shareAddr": "",
|
||||
"shareAddrStrategy": "node",
|
||||
"sniffing": {
|
||||
"destOverride": [
|
||||
"http",
|
||||
|
||||
@@ -104,6 +104,8 @@ describe('rawInboundToFormValues', () => {
|
||||
if (name === 'empty stream settings drop to undefined') {
|
||||
expect(values.streamSettings).toBeUndefined();
|
||||
}
|
||||
expect(values.shareAddrStrategy).toBe('node');
|
||||
expect(values.shareAddr).toBe('');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -215,6 +217,17 @@ describe('formValuesToWirePayload', () => {
|
||||
expect(payload.nodeId).toBe(42);
|
||||
});
|
||||
|
||||
it('round-trips share address strategy fields', () => {
|
||||
const values = rawInboundToFormValues({
|
||||
...vlessRow,
|
||||
shareAddrStrategy: 'custom',
|
||||
shareAddr: 'edge.example.test',
|
||||
});
|
||||
const payload = formValuesToWirePayload(values);
|
||||
expect(payload.shareAddrStrategy).toBe('custom');
|
||||
expect(payload.shareAddr).toBe('edge.example.test');
|
||||
});
|
||||
|
||||
it('round-trips top-level fields through raw → values → payload → values', () => {
|
||||
// settings/streamSettings/sniffing don't round-trip byte-equal because
|
||||
// the wire payload prunes empty arrays and collapses disabled sniffing
|
||||
|
||||
@@ -309,6 +309,58 @@ describe('resolveAddr precedence', () => {
|
||||
'fallback.test',
|
||||
)).toBe('fallback.test');
|
||||
});
|
||||
|
||||
it('uses listen strategy with a shareable IPv6 listen before node override', () => {
|
||||
expect(resolveAddr(
|
||||
{ ...baseInbound, listen: '[2001:db8::1]', shareAddrStrategy: 'listen', shareAddr: '' } as never,
|
||||
'node.example.test',
|
||||
'fallback.test',
|
||||
)).toBe('[2001:db8::1]');
|
||||
});
|
||||
|
||||
it('uses listen strategy to prefer listen and fall back to node override', () => {
|
||||
expect(resolveAddr(
|
||||
{ ...baseInbound, listen: '10.0.0.1', shareAddrStrategy: 'listen', shareAddr: '' } as never,
|
||||
'node.example.test',
|
||||
'fallback.test',
|
||||
)).toBe('10.0.0.1');
|
||||
expect(resolveAddr(
|
||||
{ ...baseInbound, listen: '0.0.0.0', shareAddrStrategy: 'listen', shareAddr: '' } as never,
|
||||
'node.example.test',
|
||||
'fallback.test',
|
||||
)).toBe('node.example.test');
|
||||
expect(resolveAddr(
|
||||
{ ...baseInbound, listen: 'localhost', shareAddrStrategy: 'listen', shareAddr: '' } as never,
|
||||
'node.example.test',
|
||||
'fallback.test',
|
||||
)).toBe('node.example.test');
|
||||
});
|
||||
|
||||
it('uses custom strategy address before node override', () => {
|
||||
expect(resolveAddr(
|
||||
{ ...baseInbound, listen: '10.0.0.1', shareAddrStrategy: 'custom', shareAddr: 'edge.example.test' } as never,
|
||||
'node.example.test',
|
||||
'fallback.test',
|
||||
)).toBe('edge.example.test');
|
||||
});
|
||||
|
||||
it('normalizes a bare IPv6 custom strategy address', () => {
|
||||
expect(resolveAddr(
|
||||
{ ...baseInbound, listen: '10.0.0.1', shareAddrStrategy: 'custom', shareAddr: '2001:db8::2' } as never,
|
||||
'node.example.test',
|
||||
'fallback.test',
|
||||
)).toBe('[2001:db8::2]');
|
||||
});
|
||||
|
||||
it('ignores invalid custom strategy addresses and falls back to node override', () => {
|
||||
for (const shareAddr of ['https://edge.example.test', 'edge.example.test:8443', '[2001:db8::2]:8443', 'bad host']) {
|
||||
expect(resolveAddr(
|
||||
{ ...baseInbound, listen: '10.0.0.1', shareAddrStrategy: 'custom', shareAddr } as never,
|
||||
'node.example.test',
|
||||
'fallback.test',
|
||||
)).toBe('node.example.test');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// #4829: reaching the panel through an SSH tunnel (127.0.0.1/localhost) must not
|
||||
|
||||
Reference in New Issue
Block a user