mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-03 19:09:36 +00:00
fix(inbound): re-derive auto tags on edit and keep node tags consistent
Auto-generated inbound tags (in-<port>-<l4>, n<id>- prefixed for node inbounds) now re-derive when port/listen/transport change on update instead of keeping the stale round-tripped value. The resolved tag is mirrored onto the API response, and NodeID is pinned to the stored row so a node inbound never loses its n<id>- prefix on edit. The edit form recomputes the tag live via a Go-parity helper so the JSON preview matches what gets saved. Make node/central tag matching prefix-agnostic in all three places (traffic attribution, remote-id resolution, and the orphan sweep) so an n<id>- prefix present on only one side can no longer spawn duplicate inbounds or drop traffic on sync. Force LF on shell scripts via .gitattributes (CRLF broke the Docker build shebang when the repo is checked out on Windows) and add a .dockerignore to keep node_modules/.git out of the build context. Adds Go and frontend tests covering tag re-derivation, prefix-agnostic matching, and node-snapshot prefix mismatch.
This commit is contained in:
@@ -146,18 +146,32 @@ func (r *Remote) do(ctx context.Context, method, path string, body any) (*envelo
|
||||
}
|
||||
|
||||
func (r *Remote) resolveRemoteID(ctx context.Context, tag string) (int, error) {
|
||||
if id, ok := r.cacheGet(tag); ok {
|
||||
if id, ok := r.cacheGetTag(tag); ok {
|
||||
return id, nil
|
||||
}
|
||||
if err := r.refreshRemoteIDs(ctx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if id, ok := r.cacheGet(tag); ok {
|
||||
if id, ok := r.cacheGetTag(tag); ok {
|
||||
return id, nil
|
||||
}
|
||||
return 0, fmt.Errorf("remote inbound with tag %q not found on node %s", tag, r.node.Name)
|
||||
}
|
||||
|
||||
// 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
|
||||
// while the central panel stores the prefixed form, or vice versa.
|
||||
func (r *Remote) cacheGetTag(tag string) (int, bool) {
|
||||
if id, ok := r.cacheGet(tag); ok {
|
||||
return id, true
|
||||
}
|
||||
prefix := fmt.Sprintf("n%d-", r.node.Id)
|
||||
if stripped, found := strings.CutPrefix(tag, prefix); found {
|
||||
return r.cacheGet(stripped)
|
||||
}
|
||||
return r.cacheGet(prefix + tag)
|
||||
}
|
||||
|
||||
func (r *Remote) cacheGet(tag string) (int, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
@@ -3,8 +3,39 @@ package runtime
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
)
|
||||
|
||||
// cacheGetTag must resolve a remote inbound id even when the n<id>- prefix
|
||||
// sits on only one side: the node may store the bare tag while the central
|
||||
// panel pushes the prefixed form, or vice versa. Without this a mismatch makes
|
||||
// the push create a duplicate inbound on the node.
|
||||
func TestCacheGetTag_PrefixAgnostic(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
cacheTag string
|
||||
lookup string
|
||||
wantID int
|
||||
wantFound bool
|
||||
}{
|
||||
{"exact", "n1-in-443-tcp", "n1-in-443-tcp", 7, true},
|
||||
{"node bare, lookup prefixed", "in-443-tcp", "n1-in-443-tcp", 7, true},
|
||||
{"node prefixed, lookup bare", "n1-in-443-tcp", "in-443-tcp", 7, true},
|
||||
{"unrelated tag", "in-443-tcp", "in-999-tcp", 0, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
r := NewRemote(&model.Node{Id: 1, Name: "n1"})
|
||||
r.cacheSet(c.cacheTag, 7)
|
||||
id, ok := r.cacheGetTag(c.lookup)
|
||||
if ok != c.wantFound || id != c.wantID {
|
||||
t.Fatalf("cacheGetTag(%q) = (%d, %v), want (%d, %v)", c.lookup, id, ok, c.wantID, c.wantFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeStreamSettingsForRemote(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user