Files
3x-ui/internal/web/service/node_contract.go
T
VAIO73 746bdbe9f4 fix(monitoring): read LXC CPU, swap and uptime from the container (#6920)
In an LXC container with LXCFS the Overview showed the container's RAM
beside the host's swap, the host's CPU topology and a CPU percentage from
the host's view. Swap and uptime now come from LXCFS's virtualised files
and CPU from the container's cgroup v2 counter, normalised by the visible
vCPUs and any quota; container CPU and swap history is kept apart from
the earlier host samples.

A value the reader cannot measure (the first CPU sample, or a layout it
cannot verify such as cgroup v1) is reported as unavailable instead of 0:
the dashboard, the node list, node CPU history and the Telegram and
Discord uptime show a dash, and nodes keep the flags in a new
unavailable_metrics column.
2026-10-24 20:24:34 +02:00

189 lines
7.8 KiB
Go

package service
import (
"strings"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
)
// NodeView is the browser/API read contract for nodes. Credentials are
// write-only: responses expose only whether a node has a token configured.
type NodeView struct {
Id int `json:"id" example:"1"`
Name string `json:"name" example:"edge-1"`
Remark string `json:"remark" example:"Primary edge"`
Scheme string `json:"scheme" example:"https"`
Address string `json:"address" example:"node.example.com"`
Port int `json:"port" example:"2053"`
BasePath string `json:"basePath" example:"/"`
HasApiToken bool `json:"hasApiToken" example:"true"`
Enable bool `json:"enable" example:"true"`
AllowPrivateAddress bool `json:"allowPrivateAddress" example:"false"`
TlsVerifyMode string `json:"tlsVerifyMode" example:"verify"`
PinnedCertSha256 string `json:"pinnedCertSha256" example:""`
InboundSyncMode string `json:"inboundSyncMode" example:"all"`
InboundTags []string `json:"inboundTags" example:"[\"in-443-tcp\"]"`
OutboundTag string `json:"outboundTag" example:"direct"`
Guid string `json:"guid" example:"node-guid"`
Status string `json:"status" example:"online"`
LastHeartbeat int64 `json:"lastHeartbeat" example:"1700000000"`
LatencyMs int `json:"latencyMs" example:"42"`
XrayVersion string `json:"xrayVersion" example:"25.10.31"`
PanelVersion string `json:"panelVersion" example:"v3.x.x"`
CpuPct float64 `json:"cpuPct" example:"12.5"`
MemPct float64 `json:"memPct" example:"45.2"`
UptimeSecs uint64 `json:"uptimeSecs" example:"86400"`
UnavailableMetrics []string `json:"unavailableMetrics,omitempty"`
NetUp uint64 `json:"netUp" example:"2097152"`
NetDown uint64 `json:"netDown" example:"1048576"`
LastError string `json:"lastError" example:""`
XrayState string `json:"xrayState" example:"running"`
XrayError string `json:"xrayError" example:""`
ConfigDirty bool `json:"configDirty" example:"false"`
ConfigDirtyAt int64 `json:"configDirtyAt" example:"0"`
InboundCount int `json:"inboundCount" example:"3"`
ClientCount int `json:"clientCount" example:"25"`
OnlineCount int `json:"onlineCount" example:"5"`
ActiveCount int `json:"activeCount" example:"20"`
DisabledCount int `json:"disabledCount" example:"2"`
DepletedCount int `json:"depletedCount" example:"1"`
ParentGuid string `json:"parentGuid,omitempty" example:""`
Transitive bool `json:"transitive,omitempty" example:"false"`
CreatedAt int64 `json:"createdAt" example:"1700000000"`
UpdatedAt int64 `json:"updatedAt" example:"1700003600"`
}
func toNodeView(n *model.Node) *NodeView {
if n == nil {
return nil
}
return &NodeView{
Id: n.Id,
Name: n.Name,
Remark: n.Remark,
Scheme: n.Scheme,
Address: n.Address,
Port: n.Port,
BasePath: n.BasePath,
HasApiToken: n.ApiToken != "",
Enable: n.Enable,
AllowPrivateAddress: n.AllowPrivateAddress,
TlsVerifyMode: n.TlsVerifyMode,
PinnedCertSha256: n.PinnedCertSha256,
InboundSyncMode: n.InboundSyncMode,
InboundTags: n.InboundTags,
OutboundTag: n.OutboundTag,
Guid: n.Guid,
Status: n.Status,
LastHeartbeat: n.LastHeartbeat,
LatencyMs: n.LatencyMs,
XrayVersion: n.XrayVersion,
PanelVersion: n.PanelVersion,
CpuPct: n.CpuPct,
MemPct: n.MemPct,
UptimeSecs: n.UptimeSecs,
UnavailableMetrics: n.UnavailableMetrics,
NetUp: n.NetUp,
NetDown: n.NetDown,
LastError: n.LastError,
XrayState: n.XrayState,
XrayError: n.XrayError,
ConfigDirty: n.ConfigDirty,
ConfigDirtyAt: n.ConfigDirtyAt,
InboundCount: n.InboundCount,
ClientCount: n.ClientCount,
OnlineCount: n.OnlineCount,
ActiveCount: n.ActiveCount,
DisabledCount: n.DisabledCount,
DepletedCount: n.DepletedCount,
ParentGuid: n.ParentGuid,
Transitive: n.Transitive,
CreatedAt: n.CreatedAt,
UpdatedAt: n.UpdatedAt,
}
}
func toNodeViews(nodes []*model.Node) []*NodeView {
views := make([]*NodeView, 0, len(nodes))
for _, node := range nodes {
views = append(views, toNodeView(node))
}
return views
}
// NodeMutationRequest is the node write/probe contract. ApiToken is accepted
// only as input. On update, nil means keep the stored token; replacement and
// clearing are explicit and mutually exclusive.
type NodeMutationRequest struct {
Id int `json:"id" form:"id"`
Name string `json:"name" form:"name" validate:"required"`
Remark string `json:"remark" form:"remark"`
Scheme string `json:"scheme" form:"scheme" validate:"omitempty,oneof=http https"`
Address string `json:"address" form:"address" validate:"required"`
Port int `json:"port" form:"port" validate:"gte=1,lte=65535"`
BasePath string `json:"basePath" form:"basePath"`
ApiToken *string `json:"apiToken,omitempty" form:"apiToken"`
ClearApiToken bool `json:"clearApiToken,omitempty" form:"clearApiToken"`
Enable bool `json:"enable" form:"enable"`
AllowPrivateAddress bool `json:"allowPrivateAddress" form:"allowPrivateAddress"`
TlsVerifyMode string `json:"tlsVerifyMode" form:"tlsVerifyMode" validate:"omitempty,oneof=verify skip pin mtls"`
PinnedCertSha256 string `json:"pinnedCertSha256" form:"pinnedCertSha256"`
InboundSyncMode string `json:"inboundSyncMode" form:"inboundSyncMode" validate:"omitempty,oneof=all selected"`
InboundTags []string `json:"inboundTags" form:"inboundTags"`
OutboundTag string `json:"outboundTag" form:"outboundTag"`
}
func (r *NodeMutationRequest) validateCredentials(create bool) error {
if r == nil {
return common.NewError("node request is required")
}
if r.ApiToken != nil && r.ClearApiToken {
return common.NewError("apiToken and clearApiToken are mutually exclusive")
}
if r.ApiToken != nil {
*r.ApiToken = strings.TrimSpace(*r.ApiToken)
if *r.ApiToken == "" {
if create {
return common.NewError("apiToken is required unless mtls is enabled")
}
r.ApiToken = nil
}
}
if create {
if r.ClearApiToken {
return common.NewError("credentials cannot be cleared while creating a node")
}
if r.ApiToken == nil && r.TlsVerifyMode != "mtls" {
return common.NewError("apiToken is required unless mtls is enabled")
}
}
if r.ClearApiToken && r.Enable && r.TlsVerifyMode != "mtls" {
return common.NewError("disable the node or enable mtls before clearing its apiToken")
}
return nil
}
func (r *NodeMutationRequest) toNode() *model.Node {
n := &model.Node{
Id: r.Id,
Name: r.Name,
Remark: r.Remark,
Scheme: r.Scheme,
Address: r.Address,
Port: r.Port,
BasePath: r.BasePath,
Enable: r.Enable,
AllowPrivateAddress: r.AllowPrivateAddress,
TlsVerifyMode: r.TlsVerifyMode,
PinnedCertSha256: r.PinnedCertSha256,
InboundSyncMode: r.InboundSyncMode,
InboundTags: r.InboundTags,
OutboundTag: r.OutboundTag,
}
if r.ApiToken != nil {
n.ApiToken = *r.ApiToken
}
return n
}