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:
@@ -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 &&
|
||||
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
// ITEM6 B2: the agent config endpoint serves the sing-box shape
|
||||
// alongside the xray config (parallel generation path). The xray
|
||||
// payload must be untouched; the singbox keys carry the node's
|
||||
// fragments re-projected against the identity layer, plus a hash of
|
||||
// the exact bytes served.
|
||||
// ITEM6 B2→B5: the agent config endpoint serves the sing-box shape.
|
||||
// Since the B5 switchover it is the ONLY runtime payload — the xray
|
||||
// keys (config_hash / xray_config) are retired and must not reappear;
|
||||
// the singbox keys carry the node's fragments re-projected against
|
||||
// the identity layer, plus a hash of the exact bytes served.
|
||||
const sbToken = "singbox-config-token-00000000000000000001"
|
||||
|
||||
func TestConfigCarriesSingboxShape(t *testing.T) {
|
||||
@@ -94,8 +94,8 @@ func TestConfigCarriesSingboxShape(t *testing.T) {
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("config response not JSON: %v", err)
|
||||
}
|
||||
if resp.ConfigHash == "" || len(resp.XrayConfig) == 0 {
|
||||
t.Fatalf("xray payload damaged: hash=%q len=%d", resp.ConfigHash, len(resp.XrayConfig))
|
||||
if resp.ConfigHash != "" || len(resp.XrayConfig) != 0 {
|
||||
t.Fatalf("xray payload must be retired since B5: hash=%q len=%d", resp.ConfigHash, len(resp.XrayConfig))
|
||||
}
|
||||
if len(resp.SingboxConfig) == 0 || resp.SingboxConfigHash == "" {
|
||||
t.Fatalf("singbox payload missing: len=%d hash=%q", len(resp.SingboxConfig), resp.SingboxConfigHash)
|
||||
|
||||
Reference in New Issue
Block a user