diff --git a/internal/web/service/port_conflict.go b/internal/web/service/port_conflict.go index ced33a368..8e70af55c 100644 --- a/internal/web/service/port_conflict.go +++ b/internal/web/service/port_conflict.go @@ -5,7 +5,6 @@ import ( "fmt" "strings" - "github.com/mhsanaei/3x-ui/v3/internal/amneziawg" "github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet" "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" @@ -273,18 +272,8 @@ func checkPortConflictTx(db *gorm.DB, inbound *model.Inbound, ignoreId int) (*po return nil, nil } -// checkAmneziawgnetSocksConflict reports whether inbound's own port -// collides with an existing local AmneziaWG inbound's automatic -// Xray SOCKS5 relay port. Unlike the retired kernel-module bridge this -// checks every qualifying AmneziaWG inbound unconditionally: the embedded -// relay has no RouteThroughXray-style opt-in, every one of them gets a -// relay inbound (see injectAmneziawgnetSocks). ignoreId excludes one inbound -// id from the AmneziaWG candidates, the same way the general DB-backed -// conflict query above excludes the inbound being edited from matching -// itself. Takes db rather than fetching its own handle so it runs inside the -// same serialized transaction as the rest of checkPortConflictTx (#6225) -- -// otherwise two concurrent AmneziaWG creates could both pass this check -// before either row commits. +// checkAmneziawgnetSocksConflict: inbound's port vs the relay port every matching +// local row reserves, emitted or not; db keeps it in the caller's transaction (#6225). func checkAmneziawgnetSocksConflict(db *gorm.DB, inbound *model.Inbound, ignoreId int, newBits transportBits) (*portConflictDetail, error) { // A disabled row still owns the slot its id derives: SetInboundEnable flips // the column with no port check, so enabling it later must not collide. @@ -296,10 +285,9 @@ func checkAmneziawgnetSocksConflict(db *gorm.DB, inbound *model.Inbound, ignoreI if err := q.Find(&candidates).Error; err != nil { return nil, err } + // Ownership does not depend on the peers: the relay appears when the first + // client is added, and the client paths run no port check at all. for _, c := range candidates { - if _, ok := amneziawg.InstanceFromInbound(c); !ok { - continue - } if amneziawgnet.SOCKSPortForInbound(c.Id) != inbound.Port { continue } diff --git a/internal/web/service/port_conflict_test.go b/internal/web/service/port_conflict_test.go index 3891a7709..bfc1a32f4 100644 --- a/internal/web/service/port_conflict_test.go +++ b/internal/web/service/port_conflict_test.go @@ -846,12 +846,13 @@ func TestCheckPortConflict_AmneziawgnetSocksRelayReservedRegardlessOfLegacyRoute } } -// A qualifying AmneziaWG inbound with no enabled/valid peer at all never -// gets a relay inbound (amneziawg.InstanceFromInbound returns ok=false), so -// its port isn't reserved. -func TestCheckPortConflict_AmneziawgnetSocksRelayIgnoredWhenNoQualifyingPeer(t *testing.T) { +// A local AmneziaWG inbound owns its relay port from the row, not from its first +// peer: the relay appears when a client is added, and that path runs no port check. +func TestCheckPortConflict_AmneziawgnetSocksRelayReservedBeforeTheFirstPeer(t *testing.T) { setupConflictDB(t) - seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, `{}`) + // The shape normalizeAmneziaWGSettings writes for a fresh AmneziaWG inbound. + seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, + `{"server":{"privateKey":"priv","publicKey":"pub","subnetIp":"10.8.1.0","subnetCidr":24},"clients":[]}`) var awgInbound model.Inbound if err := database.GetDB().Where("tag = ?", "awg-1").First(&awgInbound).Error; err != nil { @@ -866,8 +867,15 @@ func TestCheckPortConflict_AmneziawgnetSocksRelayIgnoredWhenNoQualifyingPeer(t * Port: relayPort, Protocol: model.VLESS, } - if got, err := svc.checkPortConflict(candidate, 0); err != nil || got != nil { - t.Fatalf("an AmneziaWG inbound with no qualifying peer must not reserve its relay port; got=%v err=%v", got, err) + got, err := svc.checkPortConflict(candidate, 0) + if err != nil { + t.Fatalf("checkPortConflict: %v", err) + } + if got == nil { + t.Fatalf("an AmneziaWG inbound with no peer yet still owns relay port %d; the save must be refused", relayPort) + } + if !strings.Contains(got.String(), "awg-1") { + t.Fatalf("the conflict must name the inbound owning the port, got %q", got.String()) } }