mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-20 19:46:09 +00:00
fix(nodes): strip central n<id>- tag prefix when pushing inbounds to remote (#5399)
The central panel stores node inbounds with an n<id>- prefix so tags stay unique in its database, but pushes were sending that prefixed tag to the remote node. A no-op save or reconcile could rename the remote inbound and break Xray routing rules that still referenced the original tag. Strip only this node's prefix in wireInbound before add/update so the remote keeps its bare tag while central retains the aliased form locally. Signed-off-by: aleskxyz <39186039+aleskxyz@users.noreply.github.com>
This commit is contained in:
@@ -286,6 +286,22 @@ func (r *Remote) resolveRemoteID(ctx context.Context, tag string) (int, error) {
|
|||||||
return 0, fmt.Errorf("remote inbound with tag %q not found on node %s", tag, r.node.Name)
|
return 0, fmt.Errorf("remote inbound with tag %q not found on node %s", tag, r.node.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nodeInboundTagPrefix is the central-panel alias for an inbound on nodeID.
|
||||||
|
// Kept in sync with service.nodeTagPrefix (port_conflict.go); duplicated here
|
||||||
|
// so runtime does not import service.
|
||||||
|
func nodeInboundTagPrefix(nodeID int) string {
|
||||||
|
return fmt.Sprintf("n%d-", nodeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stripNodeInboundTagPrefix removes the central-only n<id>- prefix before
|
||||||
|
// pushing an inbound to the node so Xray keeps its original tag and routing.
|
||||||
|
func stripNodeInboundTagPrefix(nodeID int, tag string) string {
|
||||||
|
if stripped, ok := strings.CutPrefix(tag, nodeInboundTagPrefix(nodeID)); ok {
|
||||||
|
return stripped
|
||||||
|
}
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
// cacheGetTag looks up a remote inbound id by tag, tolerating an n<id>- prefix
|
// cacheGetTag looks up a remote inbound id by tag, tolerating an n<id>- prefix
|
||||||
// that lives on only one of the two panels: the node may carry the bare tag
|
// that lives on only one of the two panels: the node may carry the bare tag
|
||||||
// while the central panel stores the prefixed form, or vice versa.
|
// while the central panel stores the prefixed form, or vice versa.
|
||||||
@@ -293,7 +309,7 @@ func (r *Remote) cacheGetTag(tag string) (int, bool) {
|
|||||||
if id, ok := r.cacheGet(tag); ok {
|
if id, ok := r.cacheGet(tag); ok {
|
||||||
return id, true
|
return id, true
|
||||||
}
|
}
|
||||||
prefix := fmt.Sprintf("n%d-", r.node.Id)
|
prefix := nodeInboundTagPrefix(r.node.Id)
|
||||||
if stripped, found := strings.CutPrefix(tag, prefix); found {
|
if stripped, found := strings.CutPrefix(tag, prefix); found {
|
||||||
return r.cacheGet(stripped)
|
return r.cacheGet(stripped)
|
||||||
}
|
}
|
||||||
@@ -370,7 +386,7 @@ func (r *Remote) refreshRemoteIDs(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Remote) AddInbound(ctx context.Context, ib *model.Inbound) error {
|
func (r *Remote) AddInbound(ctx context.Context, ib *model.Inbound) error {
|
||||||
payload := wireInbound(ib)
|
payload := wireInbound(ib, r.node.Id)
|
||||||
env, err := r.do(ctx, http.MethodPost, "panel/api/inbounds/add", payload)
|
env, err := r.do(ctx, http.MethodPost, "panel/api/inbounds/add", payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -405,7 +421,7 @@ func (r *Remote) UpdateInbound(ctx context.Context, oldIb, newIb *model.Inbound)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return r.AddInbound(ctx, newIb)
|
return r.AddInbound(ctx, newIb)
|
||||||
}
|
}
|
||||||
payload := wireInbound(newIb)
|
payload := wireInbound(newIb, r.node.Id)
|
||||||
if _, err := r.do(ctx, http.MethodPost, "panel/api/inbounds/update/"+strconv.Itoa(id), payload); err != nil {
|
if _, err := r.do(ctx, http.MethodPost, "panel/api/inbounds/update/"+strconv.Itoa(id), payload); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -609,7 +625,7 @@ func (r *Remote) PushGlobalClientTraffics(ctx context.Context, masterGuid string
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func wireInbound(ib *model.Inbound) url.Values {
|
func wireInbound(ib *model.Inbound, remoteNodeID int) url.Values {
|
||||||
v := url.Values{}
|
v := url.Values{}
|
||||||
v.Set("total", strconv.FormatInt(ib.Total, 10))
|
v.Set("total", strconv.FormatInt(ib.Total, 10))
|
||||||
v.Set("remark", ib.Remark)
|
v.Set("remark", ib.Remark)
|
||||||
@@ -621,7 +637,11 @@ func wireInbound(ib *model.Inbound) url.Values {
|
|||||||
v.Set("protocol", string(ib.Protocol))
|
v.Set("protocol", string(ib.Protocol))
|
||||||
v.Set("settings", ib.Settings)
|
v.Set("settings", ib.Settings)
|
||||||
v.Set("streamSettings", sanitizeStreamSettingsForRemote(ib.StreamSettings))
|
v.Set("streamSettings", sanitizeStreamSettingsForRemote(ib.StreamSettings))
|
||||||
v.Set("tag", ib.Tag)
|
tag := ib.Tag
|
||||||
|
if remoteNodeID > 0 {
|
||||||
|
tag = stripNodeInboundTagPrefix(remoteNodeID, tag)
|
||||||
|
}
|
||||||
|
v.Set("tag", tag)
|
||||||
v.Set("sniffing", ib.Sniffing)
|
v.Set("sniffing", ib.Sniffing)
|
||||||
shareAddrStrategy := strings.TrimSpace(ib.ShareAddrStrategy)
|
shareAddrStrategy := strings.TrimSpace(ib.ShareAddrStrategy)
|
||||||
switch shareAddrStrategy {
|
switch shareAddrStrategy {
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func TestWireInboundIncludesShareAddressFields(t *testing.T) {
|
|||||||
values := wireInbound(&model.Inbound{
|
values := wireInbound(&model.Inbound{
|
||||||
ShareAddrStrategy: "custom",
|
ShareAddrStrategy: "custom",
|
||||||
ShareAddr: "edge.example.com",
|
ShareAddr: "edge.example.com",
|
||||||
})
|
}, 0)
|
||||||
|
|
||||||
if got := values.Get("shareAddrStrategy"); got != "custom" {
|
if got := values.Get("shareAddrStrategy"); got != "custom" {
|
||||||
t.Fatalf("shareAddrStrategy = %q, want custom", got)
|
t.Fatalf("shareAddrStrategy = %q, want custom", got)
|
||||||
@@ -252,30 +252,60 @@ func TestIsNonEmptySlice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWireInboundTrafficReset(t *testing.T) {
|
func TestWireInboundTrafficReset(t *testing.T) {
|
||||||
with := wireInbound(&model.Inbound{TrafficReset: "daily"})
|
with := wireInbound(&model.Inbound{TrafficReset: "daily"}, 0)
|
||||||
if got := with.Get("trafficReset"); got != "daily" {
|
if got := with.Get("trafficReset"); got != "daily" {
|
||||||
t.Fatalf("trafficReset = %q, want daily", got)
|
t.Fatalf("trafficReset = %q, want daily", got)
|
||||||
}
|
}
|
||||||
// Empty TrafficReset must be omitted entirely, not sent as an empty field.
|
// Empty TrafficReset must be omitted entirely, not sent as an empty field.
|
||||||
without := wireInbound(&model.Inbound{})
|
without := wireInbound(&model.Inbound{}, 0)
|
||||||
if without.Has("trafficReset") {
|
if without.Has("trafficReset") {
|
||||||
t.Fatalf("trafficReset must be omitted when empty, got %q", without.Get("trafficReset"))
|
t.Fatalf("trafficReset must be omitted when empty, got %q", without.Get("trafficReset"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWireInboundDefaultsShareAddressStrategy(t *testing.T) {
|
func TestWireInboundDefaultsShareAddressStrategy(t *testing.T) {
|
||||||
values := wireInbound(&model.Inbound{})
|
values := wireInbound(&model.Inbound{}, 0)
|
||||||
|
|
||||||
if got := values.Get("shareAddrStrategy"); got != "node" {
|
if got := values.Get("shareAddrStrategy"); got != "node" {
|
||||||
t.Fatalf("shareAddrStrategy = %q, want node", got)
|
t.Fatalf("shareAddrStrategy = %q, want node", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
values = wireInbound(&model.Inbound{ShareAddrStrategy: "auto"})
|
values = wireInbound(&model.Inbound{ShareAddrStrategy: "auto"}, 0)
|
||||||
if got := values.Get("shareAddrStrategy"); got != "node" {
|
if got := values.Get("shareAddrStrategy"); got != "node" {
|
||||||
t.Fatalf("invalid shareAddrStrategy = %q, want node", got)
|
t.Fatalf("invalid shareAddrStrategy = %q, want node", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStripNodeInboundTagPrefix(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
nodeID int
|
||||||
|
tag string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{2, "n2-in-443-tcp", "in-443-tcp"},
|
||||||
|
{2, "in-443-tcp", "in-443-tcp"},
|
||||||
|
{2, "my-custom", "my-custom"},
|
||||||
|
{2, "n3-in-443-tcp", "n3-in-443-tcp"},
|
||||||
|
{0, "n2-in-443-tcp", "n2-in-443-tcp"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if got := stripNodeInboundTagPrefix(c.nodeID, c.tag); got != c.want {
|
||||||
|
t.Fatalf("stripNodeInboundTagPrefix(%d, %q) = %q, want %q", c.nodeID, c.tag, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWireInboundStripsNodeTagOnPush(t *testing.T) {
|
||||||
|
values := wireInbound(&model.Inbound{Tag: "n2-in-443-tcp"}, 2)
|
||||||
|
if got := values.Get("tag"); got != "in-443-tcp" {
|
||||||
|
t.Fatalf("tag = %q, want in-443-tcp", got)
|
||||||
|
}
|
||||||
|
values = wireInbound(&model.Inbound{Tag: "n2-in-443-tcp"}, 0)
|
||||||
|
if got := values.Get("tag"); got != "n2-in-443-tcp" {
|
||||||
|
t.Fatalf("nodeID 0 must not strip, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSanitizeStreamSettingsForRemote(t *testing.T) {
|
func TestSanitizeStreamSettingsForRemote(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user