3x-ui agent: 轻量节点 agent 化
- Agent 通过 token 向主控注册,心跳/拉取 xray 配置/上报流量 - 主控按节点下发完整 xray 配置(含出站+路由),agent 只应用不自建 - Agent 心跳同步更新节点在线状态;后台探针跳过 agent 托管节点 - 「添加节点」支持 Agent 类型,自动生成 token 并弹窗展示 - VLESS/SS2022 入站;同机测试支持 api_port 隔离 - 新增 install-master.sh / install-agent.sh 一键安装脚本 - systemd + OpenWrt procd 服务文件,docs/AGENT.md
This commit is contained in:
@@ -142,6 +142,24 @@ func RemoveIndex(s []any, index int) []any {
|
||||
|
||||
// GetXrayConfig retrieves and builds the Xray configuration from settings and inbounds.
|
||||
func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
return s.buildXrayConfig(nil, "", "")
|
||||
}
|
||||
|
||||
// GetXrayConfigForAgent builds the Xray configuration pushed to a lightweight
|
||||
// agent bound to nodeID: the panel template plus that node's enabled inbounds.
|
||||
// The agent's egress override (outbounds / routing rules JSON, as stored on its
|
||||
// AgentNode row) replaces the template defaults when set; otherwise the
|
||||
// template defaults are kept.
|
||||
func (s *XrayService) GetXrayConfigForAgent(nodeID int, outboundsJSON, routingRulesJSON string) (*xray.Config, error) {
|
||||
return s.buildXrayConfig(&nodeID, outboundsJSON, routingRulesJSON)
|
||||
}
|
||||
|
||||
// buildXrayConfig is the shared config builder. nodeID == nil builds the
|
||||
// local panel config (inbounds with no node); otherwise it builds the config
|
||||
// for the agent bound to that node. Local-only injections (subscription
|
||||
// outbounds, mtproto/amneziawg bridges, panel/node egresses) are skipped for
|
||||
// agents; agents get their egress from the per-agent override instead.
|
||||
func (s *XrayService) buildXrayConfig(nodeID *int, outboundsJSON, routingRulesJSON string) (*xray.Config, error) {
|
||||
templateConfig, err := s.settingService.GetXrayConfigTemplate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -161,7 +179,11 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
// the per-inbound lift below).
|
||||
xrayConfig.OutboundConfigs = liftOutboundsXhttpSessionIDKeys(xrayConfig.OutboundConfigs)
|
||||
|
||||
_, _, _ = s.inboundService.AddTraffic(nil, nil)
|
||||
// The local traffic flush only applies to the panel's own core; agent
|
||||
// traffic arrives through the agent traffic endpoint instead.
|
||||
if nodeID == nil {
|
||||
_, _, _ = s.inboundService.AddTraffic(nil, nil)
|
||||
}
|
||||
|
||||
inbounds, err := s.inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
@@ -171,7 +193,11 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
if !inbound.Enable {
|
||||
continue
|
||||
}
|
||||
if inbound.NodeID != nil {
|
||||
if nodeID == nil {
|
||||
if inbound.NodeID != nil {
|
||||
continue
|
||||
}
|
||||
} else if inbound.NodeID == nil || *inbound.NodeID != *nodeID {
|
||||
continue
|
||||
}
|
||||
if inbound.Protocol == model.MTProto || inbound.Protocol == model.AmneziaWG {
|
||||
@@ -260,8 +286,11 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
mutated = true
|
||||
} else {
|
||||
_, hadClients := settings["clients"]
|
||||
mutated = hadClients || len(finalClients) > 0
|
||||
if mutated {
|
||||
// Don't clobber settings clients with an empty DB result;
|
||||
// the DB is the source of truth only when it has rows.
|
||||
if len(finalClients) > 0 {
|
||||
settings["clients"] = finalClients
|
||||
} else if !hadClients {
|
||||
settings["clients"] = finalClients
|
||||
}
|
||||
}
|
||||
@@ -348,6 +377,15 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
|
||||
}
|
||||
|
||||
if nodeID != nil {
|
||||
// Agent path: the panel is the only source of truth for egress, so the
|
||||
// agent's stored outbounds/routing rules replace the template defaults.
|
||||
if err := applyAgentEgress(xrayConfig, outboundsJSON, routingRulesJSON); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return xrayConfig, nil
|
||||
}
|
||||
|
||||
// Merge subscription-derived outbounds (if any) into the final outbounds array.
|
||||
// These are additive: each subscription is placed before or after the template
|
||||
// outbounds based on its Prepend flag, ordered by Priority. Tags assigned by the
|
||||
@@ -410,6 +448,90 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
return xrayConfig, nil
|
||||
}
|
||||
|
||||
// agentAPIRoutingRule keeps the xray stats API reachable on agent configs.
|
||||
// It is injected when the agent's routing rules don't already carry one, so a
|
||||
// panel-managed egress can never accidentally cut off traffic reporting.
|
||||
var agentAPIRoutingRule = map[string]any{
|
||||
"type": "field",
|
||||
"inboundTag": []any{"api"},
|
||||
"outboundTag": "api",
|
||||
}
|
||||
|
||||
// applyAgentEgress replaces the template's outbounds and routing rules with the
|
||||
// agent's stored egress configuration. Empty inputs keep the template
|
||||
// defaults. The stats-API routing rule is always ensured.
|
||||
func applyAgentEgress(cfg *xray.Config, outboundsJSON, routingRulesJSON string) error {
|
||||
if outboundsJSON != "" {
|
||||
var outbounds []any
|
||||
if err := json.Unmarshal([]byte(outboundsJSON), &outbounds); err != nil {
|
||||
return fmt.Errorf("invalid agent outbounds JSON: %w", err)
|
||||
}
|
||||
raw, err := json.Marshal(outbounds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.OutboundConfigs = raw
|
||||
}
|
||||
|
||||
var routing map[string]any
|
||||
if len(cfg.RouterConfig) > 0 {
|
||||
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
|
||||
return fmt.Errorf("invalid template routing JSON: %w", err)
|
||||
}
|
||||
} else {
|
||||
routing = map[string]any{}
|
||||
}
|
||||
if routingRulesJSON != "" {
|
||||
var rules []any
|
||||
if err := json.Unmarshal([]byte(routingRulesJSON), &rules); err != nil {
|
||||
return fmt.Errorf("invalid agent routing rules JSON: %w", err)
|
||||
}
|
||||
routing["rules"] = rules
|
||||
}
|
||||
routing["rules"] = ensureAPIRoutingRule(routing["rules"])
|
||||
raw, err := json.Marshal(routing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.RouterConfig = raw
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureAPIRoutingRule prepends the stats-API rule when no rule already
|
||||
// matches the "api" inbound tag.
|
||||
func ensureAPIRoutingRule(rules any) []any {
|
||||
list, _ := rules.([]any)
|
||||
for _, r := range list {
|
||||
m, ok := r.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, tag := range inboundTagsOf(m["inboundTag"]) {
|
||||
if tag == "api" {
|
||||
return list
|
||||
}
|
||||
}
|
||||
}
|
||||
return append([]any{agentAPIRoutingRule}, list...)
|
||||
}
|
||||
|
||||
func inboundTagsOf(v any) []string {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return []string{t}
|
||||
case []any:
|
||||
var out []string
|
||||
for _, e := range t {
|
||||
if s, ok := e.(string); ok {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PanelEgressInboundTag is the tag of the loopback SOCKS inbound injected into
|
||||
// the generated config when a panel outbound is configured. The panel's own
|
||||
// HTTP clients dial through it to egress via the chosen outbound.
|
||||
|
||||
Reference in New Issue
Block a user