mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 15:47:14 +00:00
fix(xray): refuse a config the running core cannot bind (#6547)
* fix(xray): refuse a config the running core cannot bind RestartXray stopped a working core before handing it a config whose listens collide, so the failed bind exited the whole process (main/run.go:94) and the one-second watchdog retried it in a loop: every protocol down, cause only in the logs. The save-time port guards cannot cover this -- SetInboundEnable, the AmneziaWG relay created on the first peer, template and bridge edits all reach a colliding config with no guard on that path. Probe the generated config at the single restart funnel instead. Collisions the running core already serves are excused, so an established setup is never refused by a static read being wrong about it, and the port-bucketed pass costs nothing on a clean config. * fix(xray): surface a refused config and re-key the bind excuse set Round-1 findings on this PR. Refusing the swap left the running core on its previous config with nothing but a log line to show for it, so the status response now carries the reason while the core runs and the overview marks it; the node list picks the same field up through that response. The excuse set is keyed on the two listens, the port and the shared transports instead of the tag pair, so a pair whose listen moves onto the other's address is refused again, while the same two sockets stay excused however the generator orders them. TestBindConflicts/excused_pair_whose_listen_changed_into_a_real_collision fails without the key change -- watched red first.
This commit is contained in:
@@ -33,6 +33,8 @@ type xrayLifecycle struct {
|
||||
mu sync.RWMutex
|
||||
process *xray.Process
|
||||
result string
|
||||
// heldBack is why the running core still serves the previous config.
|
||||
heldBack string
|
||||
}
|
||||
|
||||
func (s *xrayLifecycle) snapshot() (*xray.Process, string) {
|
||||
@@ -45,9 +47,22 @@ func (s *xrayLifecycle) replace(process *xray.Process) {
|
||||
s.mu.Lock()
|
||||
s.process = process
|
||||
s.result = ""
|
||||
s.heldBack = ""
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *xrayLifecycle) holdBack(reason string) {
|
||||
s.mu.Lock()
|
||||
s.heldBack = reason
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *xrayLifecycle) heldBackReason() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.heldBack
|
||||
}
|
||||
|
||||
func (s *xrayLifecycle) storeResult(process *xray.Process, result string) {
|
||||
s.mu.Lock()
|
||||
if s.process == process && s.result == "" {
|
||||
@@ -104,6 +119,12 @@ func (s *XrayService) GetXrayErr() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetHeldBackConfig returns why the running core still serves its previous
|
||||
// config, or "" when the pending config was applied.
|
||||
func (s *XrayService) GetHeldBackConfig() string {
|
||||
return xrayState.heldBackReason()
|
||||
}
|
||||
|
||||
// GetXrayResult returns the result string from the Xray process.
|
||||
func (s *XrayService) GetXrayResult() string {
|
||||
process, cachedResult := xrayState.snapshot()
|
||||
@@ -1373,11 +1394,27 @@ func (s *XrayService) RestartXray(isForce bool) error {
|
||||
logger.Debug("It does not need to restart Xray")
|
||||
return nil
|
||||
}
|
||||
// A config the core cannot bind never replaces one that works: its failed
|
||||
// start exits the core, and the watchdog would then loop on it forever.
|
||||
if conflicts := bindConflicts(xrayConfig, process.GetConfig()); len(conflicts) > 0 {
|
||||
refused := fmt.Sprintf("config refused: %s", conflicts[0])
|
||||
for _, conflict := range conflicts {
|
||||
logger.Error("xray config refused:", conflict.String())
|
||||
}
|
||||
// The refusal is otherwise invisible: the operator's request
|
||||
// succeeded, so the status page has to carry the stale state.
|
||||
xrayState.holdBack(refused)
|
||||
return fmt.Errorf("xray %s", refused)
|
||||
}
|
||||
if !isForce && !configUnchanged && s.tryHotApply(process, xrayConfig) {
|
||||
logger.Info("Xray config changes applied through the core API, no restart needed")
|
||||
return nil
|
||||
}
|
||||
_ = process.Stop()
|
||||
} else if conflicts := bindConflicts(xrayConfig, nil); len(conflicts) > 0 {
|
||||
// Nothing is running to protect and the core is the authority on what it
|
||||
// can bind: start it and let its own error name the port it lost.
|
||||
logger.Warning("xray config may not start:", conflicts[0].String())
|
||||
}
|
||||
|
||||
process = xray.NewProcess(xrayConfig)
|
||||
|
||||
Reference in New Issue
Block a user