package model // AgentNode binds a lightweight agent (token-registered, no web panel) to a // 3x-ui node row and carries the per-agent egress configuration. // // Agents are the only writers of their own LastSeen; the panel owns everything // else. Authentication uses only the SHA-256 hex of the token; since the // next-core batch (item 19) the raw token is additionally kept recoverably // in TokenPlain — an operator-accepted tradeoff so the node edit page can // re-display the full install command. type AgentNode struct { Id int `json:"id" gorm:"primaryKey;autoIncrement"` TokenHash string `json:"-" gorm:"size:64;uniqueIndex"` TokenPrefix string `json:"tokenPrefix" gorm:"size:16;index"` // TokenPlain is a recoverable copy of the raw token: stored at // issuance, and backfilled from the first authenticated agent request // for rows created before this field existed (item 19). Existing // values are never overwritten by requests; only issuance (create / // panel-side re-issue) writes a fresh one. Never serialized. TokenPlain string `json:"-" gorm:"type:text"` Name string `json:"name"` // NodeID is the 3x-ui node whose inbounds this agent serves. Nil means the // token was issued but not yet bound to a node. NodeID *int `json:"nodeId" gorm:"index"` // Outbounds is the full xray "outbounds" JSON array pushed to this agent, // replacing the template defaults. Empty means "use template defaults". Outbounds string `json:"outbounds" gorm:"type:text"` // RoutingRules is the full xray "routing.rules" JSON array pushed to this // agent, replacing the template defaults. Empty means "use template defaults". RoutingRules string `json:"routingRules" gorm:"type:text"` LastSeen int64 `json:"lastSeen"` CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"` UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"` // Heartbeat telemetry (P13): the agent piggybacks host/runtime facts on // its 60s heartbeat and the panel overlays them onto the node list row. // All fields are best-effort: an agent that never reported telemetry has // TelemetryAt == 0 and the UI keeps showing "-" for its row. AgentVersion string `json:"agentVersion"` XrayVersion string `json:"xrayVersion"` UptimeSecs uint64 `json:"uptimeSecs"` LatencyMs int `json:"latencyMs"` CpuPct float64 `json:"cpuPct"` MemPct float64 `json:"memPct"` IPv4 string `json:"ipv4"` IPv6 string `json:"ipv6"` // TelemetryAt is the unix-milli time of the last heartbeat that carried // a telemetry object. TelemetryAt int64 `json:"telemetryAt"` // Seen source addresses (public-address resolution): the source IP of // the agent's own authenticated requests, as observed by the master — // the CF-Connecting-IP header when the agent comes through Cloudflare, // else the direct peer address. An agent behind 1:1 NAT reports only // its private interface address in telemetry (its public address is // not assigned to any local interface, so enumeration can never see // it); the observed source address is the master's view of the // agent's public side. Writes are change-only (see // AgentController.captureSeenIP); SeenIPAt is the unix-milli time of // the last change, not of the last request. SeenIPv4 string `json:"seenIpv4"` SeenIPv6 string `json:"seenIpv6"` SeenIPAt int64 `json:"seenIpAt"` // One-click update (item 20): the master records an update intent here; // the agent picks it up from its config poll, downloads + verifies + // swaps its binary, restarts itself and reports the outcome back on // the heartbeat. UpdateRequestedAt == 0 means no intent is pending. UpdateRequestedAt int64 `json:"updateRequestedAt"` UpdateTarget string `json:"updateTarget"` // A-form display target, e.g. "A1.0.1" UpdateTag string `json:"updateTag"` // release tag to download, e.g. "V1.0.1" // UpdateState: "" | pending | downloading | swapped | failed | // current | success (success = new A version observed by the master). UpdateState string `json:"updateState"` UpdateError string `json:"updateError"` UpdateStateAt int64 `json:"updateStateAt"` }