B5 switchover: sing-box is the only runtime payload

- master getConfig serves singbox_config only; xray payload retired
  (build failure is a hard 500, agent keeps its current state)
- agent: singbox_config arrival is the switch signal — flush xray
  tail traffic, stop xray, activate sing-box; activation failure
  restarts xray from the last payload and retries next poll; a
  legacy master (no sing-box keys) keeps the xray path untouched
- sing-box log rotation: 8 MiB copy-truncate with one .1 kept,
  checked on the traffic cycle
- install-agent.sh: xray/geo downloads and checks removed, agent
  binary now from the V2.0.0 release, self-check is sing-box
  binary + check on the live config
- display version default 2.0.0 (master V2.0.0 / agent A2.0.0)
This commit is contained in:
Muse
2026-10-23 01:24:43 +00:00
parent 3e2829e49d
commit 4a532e8c5a
9 changed files with 389 additions and 114 deletions
+25 -26
View File
@@ -591,7 +591,20 @@ func (a *AgentController) heartbeat(c *gin.Context) {
jsonObj(c, gin.H{"message": "ok"}, nil)
}
// getConfig returns the full xray config for the agent's bound node.
// getConfig returns the full sing-box config for the agent's bound node.
//
// ITEM6 B5 (switchover): the sing-box config is the only runtime
// payload this master serves. The xray payload (config_hash /
// xray_config) is retired — generation reads only the sing-box data
// model since the B1 migration. Dual-capability agents (A2.0.0) take
// singbox_config as the switch signal: they flush their xray tail
// traffic, stop the xray core and run sing-box alone. Older agents
// ignore the unknown keys and keep running their last applied xray
// config until they are updated, which is why the rollout order is
// agents first, master second (ITEM6-SCOPE §6). A build failure is a
// hard 500: the agent keeps its current runtime state and retries on
// the next poll, and the failure stays visible in both logs instead
// of silently freezing the node's config.
func (a *AgentController) getConfig(c *gin.Context) {
agent, ok := a.findAgent(bearerToken(c))
if !ok {
@@ -603,20 +616,22 @@ func (a *AgentController) getConfig(c *gin.Context) {
c.JSON(409, gin.H{"message": "agent token is not bound to a node"})
return
}
cfg, err := a.xrayService.GetXrayConfigForAgent(*agent.NodeID, agent.Outbounds, agent.RoutingRules)
if err != nil {
jsonMsg(c, "Failed to build xray config", err)
sbCfg, sbErr := a.singboxService.GetSingboxConfigForAgent(*agent.NodeID, agent.SingboxOutbounds, agent.SingboxRoutingRules)
if sbErr != nil {
logger.Warningf("agent config: singbox config build failed for node %d: %v", *agent.NodeID, sbErr)
c.JSON(500, gin.H{"message": "failed to build sing-box config"})
return
}
raw, err := json.Marshal(cfg)
if err != nil {
jsonMsg(c, "Failed to encode xray config", err)
sbRaw, mErr := json.Marshal(sbCfg)
if mErr != nil {
logger.Warningf("agent config: singbox config encode failed for node %d: %v", *agent.NodeID, mErr)
c.JSON(500, gin.H{"message": "failed to encode sing-box config"})
return
}
sum := sha256.Sum256(raw)
sbSum := sha256.Sum256(sbRaw)
payload := gin.H{
"config_hash": hex.EncodeToString(sum[:]),
"xray_config": json.RawMessage(raw),
"singbox_config": json.RawMessage(sbRaw),
"singbox_config_hash": hex.EncodeToString(sbSum[:]),
// Item 7 B4: the agent's probe task set rides the existing
// config poll (no new channel). Always present from this
// master version — an empty list means "run no probes", while
@@ -624,22 +639,6 @@ func (a *AgentController) getConfig(c *gin.Context) {
// set untouched.
"probe_tasks": probe.PingTasksFor(int64(agent.Id)),
}
// ITEM6 B2: the sing-box shape rides alongside the xray config
// (parallel generation path; agents on the current runtime ignore
// the unknown keys, B3's runtime consumes them). A sing-box build
// failure must never take down the production xray payload, so it
// is logged and the keys are simply omitted.
if sbCfg, sbErr := a.singboxService.GetSingboxConfigForAgent(*agent.NodeID, agent.SingboxOutbounds, agent.SingboxRoutingRules); sbErr == nil {
if sbRaw, mErr := json.Marshal(sbCfg); mErr == nil {
sbSum := sha256.Sum256(sbRaw)
payload["singbox_config"] = json.RawMessage(sbRaw)
payload["singbox_config_hash"] = hex.EncodeToString(sbSum[:])
} else {
logger.Warningf("agent config: singbox config encode failed for node %d: %v", *agent.NodeID, mErr)
}
} else {
logger.Warningf("agent config: singbox config build failed for node %d: %v", *agent.NodeID, sbErr)
}
// Item 20: a pending one-click update intent rides along on the config
// poll (no new channel — the agent already polls this endpoint).
if agent.UpdateRequestedAt > 0 &&