mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 15:47:14 +00:00
fix(amneziawg): keep a disabled row's relay port reserved for port forwards
loadPortConflictContext filtered its query with enable = true, so a client's ForwardedPorts spec could claim the relay port a disabled AmneziaWG row's id derives. That row's relay appears with its first client -- a path that runs no port check -- and when the relay then loses the loopback bind race to the forward listener, Xray refuses the whole config instead of losing one forward (#6542 review, arrived with #6540). The context now loads every local row and gates only the ordinary-port compare on enable, which is what a disabled row's own port is worth: free. Its relay slot is not free, which is the rule #6540 already states for the other two guards. TestCheckForwardedPortsConflict_DisabledAmneziawgRelayPortIsReserved fails without this -- watched red first -- and passes with it, while TestCheckForwardedPortsConflict_IgnoresDisabledInboundPort keeps proving that a disabled inbound's own port stays available.
This commit is contained in:
@@ -313,41 +313,28 @@ func (s *InboundService) normalizeAmneziaWGSettings(inbound *model.Inbound, oldS
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// portConflictContext caches the state checkForwardedPortsConflict needs —
|
// portConflictContext caches what checkForwardedPortsConflict needs — the panel's
|
||||||
// the panel's own port and this host's enabled inbound ports — so validating
|
// own port and this host's inbound rows — so one save costs one query, not N.
|
||||||
// N clients in one save (normalizeAmneziaWGSettings, or a bulk client add)
|
|
||||||
// costs one query total instead of N. Load it once with
|
|
||||||
// loadPortConflictContext and pass it to every checkForwardedPortsConflict
|
|
||||||
// call in that batch.
|
|
||||||
type portConflictContext struct {
|
type portConflictContext struct {
|
||||||
webPort int
|
webPort int
|
||||||
inbounds []*model.Inbound
|
inbounds []*model.Inbound
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadPortConflictContext loads the panel's own port and every enabled
|
// loadPortConflictContext loads the panel's own port and every inbound hosted on
|
||||||
// inbound hosted on THIS panel (node_id IS NULL) — an inbound hosted on a
|
// THIS panel: a node-hosted one listens on that node's host, never on this one.
|
||||||
// different node listens on that node's own host, never this one, so it can
|
|
||||||
// never collide with a DNAT rule this process installs.
|
|
||||||
func (s *InboundService) loadPortConflictContext(db *gorm.DB) (portConflictContext, error) {
|
func (s *InboundService) loadPortConflictContext(db *gorm.DB) (portConflictContext, error) {
|
||||||
var ctx portConflictContext
|
var ctx portConflictContext
|
||||||
if webPort, err := (&SettingService{}).GetPort(); err == nil {
|
if webPort, err := (&SettingService{}).GetPort(); err == nil {
|
||||||
ctx.webPort = webPort
|
ctx.webPort = webPort
|
||||||
}
|
}
|
||||||
err := db.Model(model.Inbound{}).
|
err := db.Model(model.Inbound{}).
|
||||||
Where("enable = ? AND node_id IS NULL", true).
|
Where("node_id IS NULL").
|
||||||
Find(&ctx.inbounds).Error
|
Find(&ctx.inbounds).Error
|
||||||
return ctx, err
|
return ctx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkForwardedPortsConflict reports whether a client's ForwardedPorts spec
|
// checkForwardedPortsConflict names the panel, inbound or AmneziaWG relay port a
|
||||||
// exceeds the cap, covers the panel's own web port, one of this host's own
|
// client's ForwardedPorts spec would collide with: a lost bind race kills the relay.
|
||||||
// enabled inbound listen ports, or an AmneziaWG inbound's own phantom SOCKS5
|
|
||||||
// relay port (SOCKSPortForInbound -- never a real inbounds row, so the loop
|
|
||||||
// below can't see it any other way). A collision on the SOCKS5 port would
|
|
||||||
// let a port-forward listener race Xray's own relay for the bind and, if it
|
|
||||||
// wins, take down that inbound's entire relay rather than just one forward.
|
|
||||||
// Returns a human-readable description of the first collision found, or ""
|
|
||||||
// when there is none.
|
|
||||||
func (s *InboundService) checkForwardedPortsConflict(ctx portConflictContext, forwardedPorts string) string {
|
func (s *InboundService) checkForwardedPortsConflict(ctx portConflictContext, forwardedPorts string) string {
|
||||||
if forwardedPorts == "" {
|
if forwardedPorts == "" {
|
||||||
return ""
|
return ""
|
||||||
@@ -359,7 +346,9 @@ func (s *InboundService) checkForwardedPortsConflict(ctx portConflictContext, fo
|
|||||||
return fmt.Sprintf("the panel's own port (%d)", ctx.webPort)
|
return fmt.Sprintf("the panel's own port (%d)", ctx.webPort)
|
||||||
}
|
}
|
||||||
for _, ib := range ctx.inbounds {
|
for _, ib := range ctx.inbounds {
|
||||||
if amneziawg.ForwardedPortsInclude(forwardedPorts, ib.Port) {
|
// A disabled row's own port is free, but its relay slot is not: the relay
|
||||||
|
// appears with the first client, and no client path re-checks ports.
|
||||||
|
if ib.Enable && amneziawg.ForwardedPortsInclude(forwardedPorts, ib.Port) {
|
||||||
name := ib.Remark
|
name := ib.Remark
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = ib.Tag
|
name = ib.Tag
|
||||||
|
|||||||
@@ -370,6 +370,34 @@ func TestCheckForwardedPortsConflict_CollidesWithAmneziawgnetSocksPort(t *testin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A disabled AmneziaWG row still owns the relay port its id derives (#6540), so
|
||||||
|
// a client's port-forward spec must not be able to claim that same bind.
|
||||||
|
func TestCheckForwardedPortsConflict_DisabledAmneziawgRelayPortIsReserved(t *testing.T) {
|
||||||
|
setupConflictDB(t)
|
||||||
|
seedInboundConflict(t, "awg-off", "0.0.0.0", 51820, model.AmneziaWG, ``, `{}`)
|
||||||
|
|
||||||
|
var off model.Inbound
|
||||||
|
if err := database.GetDB().Where("tag = ?", "awg-off").First(&off).Error; err != nil {
|
||||||
|
t.Fatalf("read seeded row: %v", err)
|
||||||
|
}
|
||||||
|
if err := database.GetDB().Model(model.Inbound{}).Where("id = ?", off.Id).
|
||||||
|
Update("enable", false).Error; err != nil {
|
||||||
|
t.Fatalf("disable the seeded row: %v", err)
|
||||||
|
}
|
||||||
|
relayPort := amneziawgnet.SOCKSPortForInbound(off.Id)
|
||||||
|
|
||||||
|
svc := &InboundService{}
|
||||||
|
ctx, err := svc.loadPortConflictContext(database.GetDB())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("loadPortConflictContext: %v", err)
|
||||||
|
}
|
||||||
|
hit := svc.checkForwardedPortsConflict(ctx, fmt.Sprintf("%d", relayPort))
|
||||||
|
if !strings.Contains(hit, "SOCKS5") {
|
||||||
|
t.Fatalf("inbound #%d is disabled but still owns relay port %d; the spec must be refused, got %q",
|
||||||
|
off.Id, relayPort, hit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A cleared DNS field is meaningful (no DNS line in client configs) and must
|
// A cleared DNS field is meaningful (no DNS line in client configs) and must
|
||||||
// survive the save round-trip instead of resurrecting the frontend defaults.
|
// survive the save round-trip instead of resurrecting the frontend defaults.
|
||||||
func TestNormalizeAmneziaWGSettingsKeepsClearedDNS(t *testing.T) {
|
func TestNormalizeAmneziaWGSettingsKeepsClearedDNS(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user