mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +00:00
fix(inbounds): drop listen address from auto-generated inbound tag
A non-empty, non-any Address (listen) leaked into the tag as in-<listen>:<port>-<transport> (e.g. in-127.0.0.1:443-tcp). The tag is now always in-<port>-<transport>, with the node prefix and numeric dedup suffix still handling uniqueness across nodes and same-port/different-listen inbounds. Mirrored in the Go authority and the TS form preview, kept in parity by tests. Existing colon-form tags are now treated as custom, so editing such an inbound preserves its tag rather than rewriting it; new inbounds (or a cleared tag field) get the clean form.
This commit is contained in:
parent
48f470c465
commit
a3dca4b82d
6 changed files with 33 additions and 45 deletions
|
|
@ -49,12 +49,8 @@ function transportTagSuffix(bits: TransportBits): string {
|
||||||
return 'any';
|
return 'any';
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAnyListen(listen: string): boolean {
|
function baseInboundTag(port: number): string {
|
||||||
return listen === '' || listen === '0.0.0.0' || listen === '::' || listen === '::0';
|
return `in-${port}`;
|
||||||
}
|
|
||||||
|
|
||||||
function baseInboundTag(listen: string, port: number): string {
|
|
||||||
return isAnyListen(listen) ? `in-${port}` : `in-${listen}:${port}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeTagPrefix(nodeId: number | null | undefined): string {
|
function nodeTagPrefix(nodeId: number | null | undefined): string {
|
||||||
|
|
@ -62,7 +58,6 @@ function nodeTagPrefix(nodeId: number | null | undefined): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InboundTagInput {
|
export interface InboundTagInput {
|
||||||
listen: string;
|
|
||||||
port: number;
|
port: number;
|
||||||
nodeId: number | null | undefined;
|
nodeId: number | null | undefined;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
|
|
@ -74,7 +69,7 @@ export function composeInboundTag(input: InboundTagInput): string {
|
||||||
const bits = inboundTransports(input.protocol, input.streamSettings, input.settings);
|
const bits = inboundTransports(input.protocol, input.streamSettings, input.settings);
|
||||||
return (
|
return (
|
||||||
nodeTagPrefix(input.nodeId)
|
nodeTagPrefix(input.nodeId)
|
||||||
+ baseInboundTag(input.listen ?? '', input.port ?? 0)
|
+ baseInboundTag(input.port ?? 0)
|
||||||
+ '-'
|
+ '-'
|
||||||
+ transportTagSuffix(bits)
|
+ transportTagSuffix(bits)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,6 @@ export default function InboundFormModal({
|
||||||
const security = Form.useWatch(['streamSettings', 'security'], form) ?? 'none';
|
const security = Form.useWatch(['streamSettings', 'security'], form) ?? 'none';
|
||||||
const streamEnabled = canEnableStream({ protocol });
|
const streamEnabled = canEnableStream({ protocol });
|
||||||
|
|
||||||
const wListen = Form.useWatch('listen', form) ?? '';
|
|
||||||
const wPort = Form.useWatch('port', form);
|
const wPort = Form.useWatch('port', form);
|
||||||
const wNodeId = Form.useWatch('nodeId', form) ?? null;
|
const wNodeId = Form.useWatch('nodeId', form) ?? null;
|
||||||
const wTag = Form.useWatch('tag', form) ?? '';
|
const wTag = Form.useWatch('tag', form) ?? '';
|
||||||
|
|
@ -169,7 +168,6 @@ export default function InboundFormModal({
|
||||||
const autoTagRef = useRef(true);
|
const autoTagRef = useRef(true);
|
||||||
const lastWrittenTagRef = useRef('');
|
const lastWrittenTagRef = useRef('');
|
||||||
const currentTagInput = (): InboundTagInput => ({
|
const currentTagInput = (): InboundTagInput => ({
|
||||||
listen: typeof wListen === 'string' ? wListen : '',
|
|
||||||
port: typeof wPort === 'number' ? wPort : 0,
|
port: typeof wPort === 'number' ? wPort : 0,
|
||||||
nodeId: typeof wNodeId === 'number' ? wNodeId : null,
|
nodeId: typeof wNodeId === 'number' ? wNodeId : null,
|
||||||
protocol,
|
protocol,
|
||||||
|
|
@ -293,7 +291,6 @@ export default function InboundFormModal({
|
||||||
form.setFieldsValue(initial);
|
form.setFieldsValue(initial);
|
||||||
const initialTag = (initial.tag ?? '') as string;
|
const initialTag = (initial.tag ?? '') as string;
|
||||||
autoTagRef.current = isAutoInboundTag(initialTag, {
|
autoTagRef.current = isAutoInboundTag(initialTag, {
|
||||||
listen: initial.listen ?? '',
|
|
||||||
port: initial.port ?? 0,
|
port: initial.port ?? 0,
|
||||||
nodeId: initial.nodeId ?? null,
|
nodeId: initial.nodeId ?? null,
|
||||||
protocol: initial.protocol,
|
protocol: initial.protocol,
|
||||||
|
|
@ -329,7 +326,7 @@ export default function InboundFormModal({
|
||||||
form.setFieldValue('tag', next);
|
form.setFieldValue('tag', next);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [open, wListen, wPort, wNodeId, protocol, network, mixedUdpOn, wSsNetwork, wTunnelNetwork]);
|
}, [open, wPort, wNodeId, protocol, network, mixedUdpOn, wSsNetwork, wTunnelNetwork]);
|
||||||
|
|
||||||
// Why: protocol picker reset cascades through the form — clearing the
|
// Why: protocol picker reset cascades through the form — clearing the
|
||||||
// settings DU branch and dropping a nodeId that no longer applies. The
|
// settings DU branch and dropping a nodeId that no longer applies. The
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import { composeInboundTag, isAutoInboundTag, type InboundTagInput } from '@/lib
|
||||||
// tag the backend re-derives on save.
|
// tag the backend re-derives on save.
|
||||||
describe('composeInboundTag transport suffix parity', () => {
|
describe('composeInboundTag transport suffix parity', () => {
|
||||||
const base = (over: Partial<InboundTagInput>): InboundTagInput => ({
|
const base = (over: Partial<InboundTagInput>): InboundTagInput => ({
|
||||||
listen: '0.0.0.0',
|
|
||||||
port: 443,
|
port: 443,
|
||||||
nodeId: null,
|
nodeId: null,
|
||||||
protocol: 'vless',
|
protocol: 'vless',
|
||||||
|
|
@ -36,9 +35,9 @@ describe('composeInboundTag transport suffix parity', () => {
|
||||||
expect(composeInboundTag(input)).toBe(want);
|
expect(composeInboundTag(input)).toBe(want);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('scopes a non-any listen and node prefix', () => {
|
it('ignores the listen address and adds the node prefix', () => {
|
||||||
expect(composeInboundTag(base({ listen: '127.0.0.1', port: 8443, streamSettings: { network: 'tcp' } })))
|
expect(composeInboundTag(base({ port: 8443, streamSettings: { network: 'tcp' } })))
|
||||||
.toBe('in-127.0.0.1:8443-tcp');
|
.toBe('in-8443-tcp');
|
||||||
expect(composeInboundTag(base({ nodeId: 1, port: 443, streamSettings: { network: 'tcp' } })))
|
expect(composeInboundTag(base({ nodeId: 1, port: 443, streamSettings: { network: 'tcp' } })))
|
||||||
.toBe('n1-in-443-tcp');
|
.toBe('n1-in-443-tcp');
|
||||||
});
|
});
|
||||||
|
|
@ -47,7 +46,7 @@ describe('composeInboundTag transport suffix parity', () => {
|
||||||
// Parity with TestIsAutoGeneratedTag.
|
// Parity with TestIsAutoGeneratedTag.
|
||||||
describe('isAutoInboundTag', () => {
|
describe('isAutoInboundTag', () => {
|
||||||
const input: InboundTagInput = {
|
const input: InboundTagInput = {
|
||||||
listen: '0.0.0.0', port: 443, nodeId: null, protocol: 'vless', streamSettings: { network: 'tcp' },
|
port: 443, nodeId: null, protocol: 'vless', streamSettings: { network: 'tcp' },
|
||||||
};
|
};
|
||||||
|
|
||||||
it('recognises canonical, dedup-suffixed and empty as auto', () => {
|
it('recognises canonical, dedup-suffixed and empty as auto', () => {
|
||||||
|
|
|
||||||
|
|
@ -762,7 +762,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||||
|
|
||||||
tag := oldInbound.Tag
|
tag := oldInbound.Tag
|
||||||
oldBits := inboundTransports(oldInbound.Protocol, oldInbound.StreamSettings, oldInbound.Settings)
|
oldBits := inboundTransports(oldInbound.Protocol, oldInbound.StreamSettings, oldInbound.Settings)
|
||||||
oldTagWasAuto := isAutoGeneratedTag(tag, oldInbound.Listen, oldInbound.Port, oldInbound.NodeID, oldBits)
|
oldTagWasAuto := isAutoGeneratedTag(tag, oldInbound.Port, oldInbound.NodeID, oldBits)
|
||||||
|
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
tx := db.Begin()
|
tx := db.Begin()
|
||||||
|
|
|
||||||
|
|
@ -171,12 +171,9 @@ func sameNode(a, b *int) bool {
|
||||||
return *a == *b
|
return *a == *b
|
||||||
}
|
}
|
||||||
|
|
||||||
func baseInboundTag(listen string, port int) string {
|
func baseInboundTag(port int) string {
|
||||||
if isAnyListen(listen) {
|
|
||||||
return fmt.Sprintf("in-%v", port)
|
return fmt.Sprintf("in-%v", port)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("in-%v:%v", listen, port)
|
|
||||||
}
|
|
||||||
|
|
||||||
func transportTagSuffix(b transportBits) string {
|
func transportTagSuffix(b transportBits) string {
|
||||||
switch b {
|
switch b {
|
||||||
|
|
@ -200,12 +197,12 @@ func nodeTagPrefix(nodeID *int) string {
|
||||||
return fmt.Sprintf("n%d-", *nodeID)
|
return fmt.Sprintf("n%d-", *nodeID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func composeInboundTag(listen string, port int, nodeID *int, bits transportBits) string {
|
func composeInboundTag(port int, nodeID *int, bits transportBits) string {
|
||||||
return nodeTagPrefix(nodeID) + baseInboundTag(listen, port) + "-" + transportTagSuffix(bits)
|
return nodeTagPrefix(nodeID) + baseInboundTag(port) + "-" + transportTagSuffix(bits)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isAutoGeneratedTag(tag, listen string, port int, nodeID *int, bits transportBits) bool {
|
func isAutoGeneratedTag(tag string, port int, nodeID *int, bits transportBits) bool {
|
||||||
base := composeInboundTag(listen, port, nodeID, bits)
|
base := composeInboundTag(port, nodeID, bits)
|
||||||
if tag == base {
|
if tag == base {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -223,7 +220,7 @@ func isAutoGeneratedTag(tag, listen string, port int, nodeID *int, bits transpor
|
||||||
|
|
||||||
func (s *InboundService) generateInboundTag(inbound *model.Inbound, ignoreId int) (string, error) {
|
func (s *InboundService) generateInboundTag(inbound *model.Inbound, ignoreId int) (string, error) {
|
||||||
bits := inboundTransports(inbound.Protocol, inbound.StreamSettings, inbound.Settings)
|
bits := inboundTransports(inbound.Protocol, inbound.StreamSettings, inbound.Settings)
|
||||||
candidate := composeInboundTag(inbound.Listen, inbound.Port, inbound.NodeID, bits)
|
candidate := composeInboundTag(inbound.Port, inbound.NodeID, bits)
|
||||||
exists, err := s.tagExists(candidate, ignoreId)
|
exists, err := s.tagExists(candidate, ignoreId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
|
||||||
|
|
@ -331,10 +331,11 @@ func TestGenerateInboundTag_IgnoresSelfOnUpdate(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// specific listen address gets the listen-prefixed shape and same suffix.
|
// the listen address never appears in the tag; the transport suffix still
|
||||||
func TestGenerateInboundTag_SpecificListenSameDisambiguation(t *testing.T) {
|
// keeps a udp inbound distinct from a tcp one on the same port.
|
||||||
|
func TestGenerateInboundTag_ListenIgnoredTransportDisambiguates(t *testing.T) {
|
||||||
setupConflictDB(t)
|
setupConflictDB(t)
|
||||||
seedInboundConflict(t, "in-1.2.3.4:443", "1.2.3.4", 443, model.VLESS, `{"network":"tcp"}`, `{}`)
|
seedInboundConflict(t, "in-443-tcp", "1.2.3.4", 443, model.VLESS, `{"network":"tcp"}`, `{}`)
|
||||||
|
|
||||||
svc := &InboundService{}
|
svc := &InboundService{}
|
||||||
udp := &model.Inbound{
|
udp := &model.Inbound{
|
||||||
|
|
@ -346,8 +347,8 @@ func TestGenerateInboundTag_SpecificListenSameDisambiguation(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("generateInboundTag: %v", err)
|
t.Fatalf("generateInboundTag: %v", err)
|
||||||
}
|
}
|
||||||
if got != "in-1.2.3.4:443-udp" {
|
if got != "in-443-udp" {
|
||||||
t.Fatalf("expected in-1.2.3.4:443-udp, got %q", got)
|
t.Fatalf("expected in-443-udp, got %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -644,26 +645,25 @@ func TestIsAutoGeneratedTag(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
tag string
|
tag string
|
||||||
listen string
|
|
||||||
port int
|
port int
|
||||||
nodeID *int
|
nodeID *int
|
||||||
bits transportBits
|
bits transportBits
|
||||||
want bool
|
want bool
|
||||||
}{
|
}{
|
||||||
{"canonical", "in-443-tcp", "0.0.0.0", 443, nil, tcp, true},
|
{"canonical", "in-443-tcp", 443, nil, tcp, true},
|
||||||
{"canonical udp", "in-443-udp", "0.0.0.0", 443, nil, transportUDP, true},
|
{"canonical udp", "in-443-udp", 443, nil, transportUDP, true},
|
||||||
{"dedup suffix", "in-443-tcp-2", "0.0.0.0", 443, nil, tcp, true},
|
{"dedup suffix", "in-443-tcp-2", 443, nil, tcp, true},
|
||||||
{"listen scoped", "in-127.0.0.1:443-tcp", "127.0.0.1", 443, nil, tcp, true},
|
{"node prefixed", "n1-in-443-tcp", 443, intPtr(1), tcp, true},
|
||||||
{"node prefixed", "n1-in-443-tcp", "0.0.0.0", 443, intPtr(1), tcp, true},
|
{"legacy listen-scoped is now custom", "in-127.0.0.1:443-tcp", 443, nil, tcp, false},
|
||||||
{"custom tag", "my-cool-tag", "0.0.0.0", 443, nil, tcp, false},
|
{"custom tag", "my-cool-tag", 443, nil, tcp, false},
|
||||||
{"stale port", "in-443-tcp", "0.0.0.0", 8443, nil, tcp, false},
|
{"stale port", "in-443-tcp", 8443, nil, tcp, false},
|
||||||
{"stale transport", "in-443-tcp", "0.0.0.0", 443, nil, transportUDP, false},
|
{"stale transport", "in-443-tcp", 443, nil, transportUDP, false},
|
||||||
{"non-numeric suffix", "in-443-tcp-x", "0.0.0.0", 443, nil, tcp, false},
|
{"non-numeric suffix", "in-443-tcp-x", 443, nil, tcp, false},
|
||||||
{"empty suffix", "in-443-tcp-", "0.0.0.0", 443, nil, tcp, false},
|
{"empty suffix", "in-443-tcp-", 443, nil, tcp, false},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
if got := isAutoGeneratedTag(c.tag, c.listen, c.port, c.nodeID, c.bits); got != c.want {
|
if got := isAutoGeneratedTag(c.tag, c.port, c.nodeID, c.bits); got != c.want {
|
||||||
t.Fatalf("isAutoGeneratedTag(%q) = %v, want %v", c.tag, got, c.want)
|
t.Fatalf("isAutoGeneratedTag(%q) = %v, want %v", c.tag, got, c.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue