service

package
v4.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 78 Imported by: 0

Documentation

Index

Constants

View Source
const (
	AgentDiagnosticActionServiceStatus  = "service_status"
	AgentDiagnosticActionServiceRestart = "service_restart"
	AgentDiagnosticActionLogTail        = "log_tail"

	// AgentDiagnosticLogTailMaxLines 限制 log_tail 一次最多返回的行数,避免超大输出。
	AgentDiagnosticLogTailMaxLines = 1000
)
View Source
const (
	// ForwardCheckListen: the hop's own listener holds its port.
	ForwardCheckListen = "forward.listen"
	// ForwardCheckPortConflict: no foreign process or nftables rule claims
	// the hop's listen port.
	ForwardCheckPortConflict = "forward.port_conflict"
	// ForwardCheckConnect: a TCP connect to each of the hop's upstreams.
	ForwardCheckConnect = "forward.connect"
	// ForwardCheckUDPProbe: a short datagram to each of the hop's upstreams,
	// waiting for any reply; no reply is inconclusive, not a failure.
	ForwardCheckUDPProbe = "forward.udp_probe"

	// ForwardCheckDefaultTimeoutMS is a check's timeout when it names none.
	ForwardCheckDefaultTimeoutMS = 3000
	// ForwardCheckMinTimeoutMS and ForwardCheckMaxTimeoutMS bound it.
	ForwardCheckMinTimeoutMS = 100
	ForwardCheckMaxTimeoutMS = 10000

	// ForwardTargetPolicyPublicOnly and ForwardTargetPolicyAllowPrivate are
	// the target_policy parameter: the most a check may dial among a last
	// hop's targets. The Agent applies the stricter of it and the hop's own
	// policy.
	ForwardTargetPolicyPublicOnly   = "public_only"
	ForwardTargetPolicyAllowPrivate = "allow_private"
)

The forward checks of the agent.diagnostic operation (forward-sdk.md section 7.6, F3c): node-vantage probes of one hop of one route, which only the kernel's route diagnosis (kernelforward.DiagnoseRoute) sends. The Agent resolves the hop's listener and upstreams from the NodeForwardState it has applied and probes only those, so a check names a hop, never an address to dial; the optional upstream narrows the check to one of the hop's upstreams. sdk/api/agent/v1/PROTOCOL.md, "Forward diagnostic checks", is the Agent's reference.

View Source
const (
	ForwardNodeInventoryScopeAny     = ""
	ForwardNodeInventoryScopeNodeX   = "nodex"
	ForwardNodeInventoryScopeAnsible = "ansible"

	ForwardNodeInventoryTagAnsibleMachine = "ansible-machine"
)
View Source
const (
	InviteCodeFilterAll     = ""
	InviteCodeFilterUnused  = "unused"
	InviteCodeFilterUsed    = "used"
	InviteCodeFilterExpired = "expired"
)

Invite code list filters (the status query parameter).

View Source
const (
	PluginWebUIMenuParentServices   = "services"
	PluginWebUIMenuParentOperations = "operations"
	PluginWebUIMenuParentSystem     = "system"
	PluginWebUIMenuParentExtensions = "extensions"
)

WebUI menu parents are a stable shell contract. Unknown, but otherwise valid, package metadata is kept visible in the explicit extensions bucket instead of creating an unregistered top-level navigation section.

View Source
const (
	NodeLogLevelDebug   = "debug"
	NodeLogLevelInfo    = "info"
	NodeLogLevelWarning = "warning"
	NodeLogLevelError   = "error"
)
View Source
const (
	// NodeSecretReasonPlaceholder: the field holds the administrators' mask
	// (********), so the node would receive the mask as its key.
	NodeSecretReasonPlaceholder = "placeholder"
	// NodeSecretReasonTombstone: the field holds a tombstone of the
	// credential split.
	NodeSecretReasonTombstone = "tombstone"
	// NodeSecretReasonWireGuardKey: an enabled WireGuard entry's
	// server_private_key is missing, or a server_private_key is not a
	// 32-byte standard Base64 X25519 key.
	NodeSecretReasonWireGuardKey = "wireguard_key"
	// NodeSecretReasonWireGuardKeyPair: a WireGuard entry's server public
	// key is not the public key of its server_private_key.
	NodeSecretReasonWireGuardKeyPair = "wireguard_key_pair" // #nosec G101 -- this names a validation reason, not a credential.
	// NodeSecretReasonRealityKey: a Reality private_key is not a 32-byte
	// Base64 key.
	NodeSecretReasonRealityKey = "reality_key"
	// NodeSecretReasonSS2022Key: a Shadowsocks 2022 server_key does not
	// decode to the cipher's key length.
	NodeSecretReasonSS2022Key = "ss2022_key" // #nosec G101 -- this names a validation reason, not a credential.
)

Why a secret fails validation.

View Source
const (
	CacheKeyNodeList      = "nodes:list"
	CacheKeyNode          = "node:"           // + nodeID
	CacheKeyNodeProtocols = "node:protocols:" // + nodeID
)

缓存键

View Source
const (
	// NodeTrafficHour: hourly buckets from the raw traffic log
	// (v2_server_log), aligned to UTC hours.
	NodeTrafficHour = "hour"
	// NodeTrafficDay: daily buckets from the daily server statistics
	// (v2_stat_server, record_type d), aligned to the local midnight the
	// statistics were recorded at.
	NodeTrafficDay = "day"

	// MaxNodeTrafficHours bounds an hourly series: the same 30 days the
	// admin hourly traffic page (GetHourlyTraffic) clamps to.
	MaxNodeTrafficHours = maxHourlyWindow
	// MaxNodeTrafficDays bounds a daily series.
	MaxNodeTrafficDays = 366
)

Granularities of a node's traffic series.

View Source
const (
	// CapabilityNodeOpsForward: ApplyForward, ApplyTunnel,
	// SyncForwardBackend, ApplyLegacyRule.
	CapabilityNodeOpsForward = "kernel.nodeops.forward.v1"
	// CapabilityNodeOpsNodeConfig: SyncNode, RetireNode, RetireProtocol,
	// PutSecretDocument and ValidateNodeConfig.
	CapabilityNodeOpsNodeConfig = "kernel.nodeops.nodeconfig.v1"
	// CapabilityNodeOpsDiagnose: CheckEndpoints, CollectNodeStats,
	// DiagnoseForward, DiagnoseTunnel, RunAgentDiagnostic.
	CapabilityNodeOpsDiagnose = "kernel.nodeops.diagnose.v1"
	// CapabilityNodeOpsAgents: AgentControlOperation and the agent session
	// reads.
	CapabilityNodeOpsAgents = "kernel.nodeops.agents.v1"
	// CapabilityNodeOpsCredentials: IssueCredential, RevokeCredential,
	// IssueRegistrationKey, RevokeRegistrationKey, IssueCleanAgent.
	CapabilityNodeOpsCredentials = "kernel.nodeops.credentials.v1" // #nosec G101 -- a capability name, not a credential.
)

The KernelNodeOps operation families (node-ops-service.md section 3.2). Each lets an official package submit one family's node operations; any one of them lets it read, watch and cancel the operations it owns.

View Source
const (
	// OrderPaymentCompleted: the order was marked paid and completed.
	OrderPaymentCompleted = "completed"
	// OrderPaymentPaid: the order was marked paid, but granting its plan
	// failed; it stays paid for an administrator to complete.
	OrderPaymentPaid = "paid"
	// OrderPaymentRefused: the record does not pay its order, which is left
	// unchanged; the payment stays recorded.
	OrderPaymentRefused = "refused"
)

Outcomes of CompleteOrderPaymentTx: what a paid payment record did to the order it names (docs/architecture/order-service.md).

View Source
const (
	// OrderPaymentReconcileGrace is how long a paid record is left to the
	// callback that paid it before the reconciler completes its order. The
	// payment module commits the record, then asks KernelOrder; the grace
	// keeps the reconciler out of that live second step.
	OrderPaymentReconcileGrace = 2 * time.Minute
	// OrderPaymentReconcileBatch is how many records one query reads.
	OrderPaymentReconcileBatch = 100
	// OrderPaymentReconcileMaxBatches bounds the batches of one run; a
	// larger backlog continues on the next run.
	OrderPaymentReconcileMaxBatches = 10
)
View Source
const (
	// PackageReportRefusedInvalid: plugin_id, kind or version is malformed.
	PackageReportRefusedInvalid = "invalid"
	// PackageReportRefusedOversize: payload_json exceeds 256 KiB.
	PackageReportRefusedOversize = "oversize"
	// PackageReportRefusedFuture: observed_at is more than a minute ahead.
	PackageReportRefusedFuture = "future"
	// PackageReportRefusedUnknownKind: the kernel knows no such kind.
	PackageReportRefusedUnknownKind = "unknown_kind"
	// PackageReportRefusedNotAssigned: the plugin is not enabled on the node.
	PackageReportRefusedNotAssigned = "not_assigned"
	// PackageReportRefusedVersionMismatch: the node is assigned another
	// version of the plugin.
	PackageReportRefusedVersionMismatch = "version_mismatch"
	// PackageReportRefusedUnsigned: the release is not a signed official
	// (AnixOps) release, or its signature no longer verifies.
	PackageReportRefusedUnsigned = "unsigned"
	// PackageReportRefusedMissingCapability: the release is not an Agent
	// release declaring the kind's capability.
	PackageReportRefusedMissingCapability = "missing_capability"
	// PackageReportRefusedBadPayload: the kind's sanitizer refused the
	// payload.
	PackageReportRefusedBadPayload = "bad_payload"
	// PackageReportRefusedUnnegotiated: a forward report (plugin_id
	// "forward", kind "forward.report") on a session that did not negotiate
	// forward.v1.
	PackageReportRefusedUnnegotiated = "unnegotiated"
)

Why a package report was refused: the reason label of anixops_agent_package_reports_refused_total.

View Source
const (
	// PackageRouteDefaultRehearsed: the routes of
	// config/package-route-defaults.json run natively when no mode is stored
	// for them and the installed release is at least the rehearsed one.
	PackageRouteDefaultRehearsed = "rehearsed"
	// PackageRouteDefaultLegacy: the kill switch; no route defaults to
	// native.
	PackageRouteDefaultLegacy = "legacy"
)

Package route default policies (package_routes.default_mode).

View Source
const (
	// RouteModeSourceStored: the installation's configuration stores the
	// mode (anix-control routes set/rollback, the identity cutover).
	RouteModeSourceStored = "stored"
	// RouteModeSourceDefault: no stored mode; the route is in the rehearsed
	// default set and runs natively.
	RouteModeSourceDefault = "default"
	// RouteModeSourceKillSwitch: no stored mode; the route is in the
	// default set, but package_routes.default_mode is legacy.
	RouteModeSourceKillSwitch = "kill-switch"
	// RouteModeSourcePackageTooOld: no stored mode; the route is in the
	// default set, but the installed release is older than the rehearsed
	// one.
	RouteModeSourcePackageTooOld = "package-too-old"
	// RouteModeSourceIdentityAuthority: no stored mode; an identity group A
	// route, native because identity is authoritative.
	RouteModeSourceIdentityAuthority = "identity-authority"
	// RouteModeSourceUnset: no stored mode and no default: legacy.
	RouteModeSourceUnset = "unset"
)

Where a route's mode comes from (RouteModeEntry.Source).

View Source
const (
	RouteCatalogBridged       = "bridged"
	RouteCatalogKernelOwned   = "kernel-owned"
	RouteCatalogNativeFlagged = "native-flagged"
	RouteCatalogNative        = "native"
)

Package extraction modes (config/package-extraction.json).

View Source
const (
	RouteModeLockKernelOwned    = "kernel_owned"
	RouteModeLockIdentityGroupA = "identity_group_a"
	RouteModeLockWebSocket      = "websocket"
	RouteModeLockNotDeclared    = "not_declared"
	RouteModeLockNativeOnly     = "native_only"
)

Reasons a route is locked to its current mode in the administration.

View Source
const (
	PluginPermissionModeLegacy        = "legacy"
	PluginPermissionModeAuthoritative = "authoritative"
	PluginPermissionModeMixed         = "mixed"
)
View Source
const (
	// CapabilityObservedState lets an Agent-target package report observed
	// runtime state.
	CapabilityObservedState = "kernel.observed-state"
	// CapabilityStorage leases a per-package database role and schema.
	CapabilityStorage = "kernel.storage.v1"
	// CapabilityIdentity lets the official identity package call the
	// KernelIdentity contract (subscribers, account projection, revocation).
	CapabilityIdentity = "kernel.identity.v1"
	// CapabilityOrderComplete lets an official package call
	// KernelOrder.CompleteOrderPayment (order-service.md): a paid payment
	// record marks its order paid and completes it.
	CapabilityOrderComplete = "kernel.order.complete.v1"
	// The KernelSubscriber method families (subscriber-service.md): each
	// lets an official package call one part of the subscriber contract.
	CapabilitySubscriberEntitlements = "kernel.subscriber.entitlements.v1"
	CapabilitySubscriberTraffic      = "kernel.subscriber.traffic.v1"
	CapabilitySubscriberCredentials  = "kernel.subscriber.credentials.v1"
	CapabilitySubscriberBalance      = "kernel.subscriber.balance.v1"
	CapabilitySubscriberDirectory    = "kernel.subscriber.directory.v1"
	// CapabilitySubscriberGroups edits one subscription group membership
	// (v2_user_subscription_group) at a time.
	CapabilitySubscriberGroups = "kernel.subscriber.groups.v1"
	// CapabilitySubscriberSummary reads one subscriber's cached
	// subscription summary (kernel-caches.md).
	CapabilitySubscriberSummary = "kernel.subscriber.summary.v1"
	// CapabilityForward lets an official package call ForwardControl
	// (forward-sdk.md section 8, F3a): route writes and reads, the dry-run
	// planner, statistics and health of the kernel's forwarding state.
	CapabilityForward = "kernel.forward.v1"
	// CapabilityTelemetryDashboard lets an official package call
	// KernelTelemetry.GetDashboard (kernel-caches.md): the administrator
	// dashboard's snapshot from the kernel's cache, with the online users
	// as a count.
	CapabilityTelemetryDashboard = "kernel.telemetry.dashboard.v1"
)

Capabilities in the kernel. namespace grant kernel authority to a signed package, so only the forms below are accepted. Other namespaces are package vocabulary and only need a well-formed, unique name.

View Source
const (
	// SettingsNamespaceMail is the SMTP delivery configuration
	// (notification.email.*).
	SettingsNamespaceMail = "mail"
	// SettingsNamespaceInvite is the affiliate's frontend settings
	// (invite.*).
	SettingsNamespaceInvite = "invite"
	// SettingsNamespaceNodeX is the NodeX control plane address, shared
	// token and timeout (forward.runtime.nodex.*).
	SettingsNamespaceNodeX = "nodex"
	// SettingsNamespaceForwardRuntime is the forward runtime backend and
	// its local Ansible settings.
	SettingsNamespaceForwardRuntime = "forward-runtime"
	// SettingsNamespaceBackup is the backup configuration row of
	// v2_backup_config, one key per field (backup.<field>).
	SettingsNamespaceBackup = "backup"
)

Settings namespaces (docs/architecture/settings-service.md). Every setting key belongs to at most one namespace, which the KernelSettings contract authorizes per capability; a key in no namespace is reachable through no contract call.

View Source
const (
	SettingsAccessRead    = "read"
	SettingsAccessWrite   = "write"
	SettingsAccessSecrets = "secrets"
)

Settings capability accesses: kernel.settings.<namespace>.<access>.v1.

View Source
const (
	CacheKeyDashboardStats   = "stats:dashboard"
	CacheKeyUserStats        = "stats:users"
	CacheKeyOrderStats       = "stats:orders"
	CacheKeyNodeStats        = "stats:nodes"
	CacheKeyUserSubscription = "user:subscription:" // + userID
)

缓存键

View Source
const (
	StatsCacheTTL        = 60 * time.Second // 统计数据缓存 60 秒
	SubscriptionCacheTTL = 30 * time.Second // 用户订阅缓存 30 秒
	NodeCacheTTL         = 10 * time.Second // 节点缓存 10 秒
)

缓存过期时间

View Source
const (
	SubscriptionResetRateLimitedMessage = "too many subscription reset attempts, please try again later"
	SubscriptionResetFailedMessage      = "重置订阅失败"
)

The answers of a subscription reset that does not reset; identity-platform answers the same messages.

View Source
const (
	TelegramReasonTimeout  = "timeout"
	TelegramReasonDNS      = "dns"
	TelegramReasonTLS      = "tls"
	TelegramReasonConnect  = "connect"
	TelegramReasonCanceled = "canceled"
	TelegramReasonOther    = "other"
)

The reasons of a network_error, from a fixed list.

View Source
const (
	AdminUserResetTraffic   = "reset_traffic"
	AdminUserResetSubscribe = "reset_subscribe"
)

Administrator resets of one user, for AdminUserResetRequestID.

View Source
const AgentPluginInstallAPIVersion = "anixops.io/plugin-install/v1alpha1"
View Source
const BackupSettingsPrefix = "backup."

BackupSettingsPrefix starts every key of the backup namespace.

View Source
const (

	// DefaultDiagnosisConcurrency bounds the probes one diagnosis runs at
	// once.
	DefaultDiagnosisConcurrency = 8
)
View Source
const DefaultForwardRuntimeJobScrubBatch = 200

DefaultForwardRuntimeJobScrubBatch bounds the rows one scrub transaction rewrites.

View Source
const DefaultInviteCodeLimit = 5

DefaultInviteCodeLimit is how many unused invite codes a user may hold when the invite configuration sets no code_count, as v2board's invite_gen_limit defaults to 5.

View Source
const IdentityPlatformPackageID = "identity-platform"

IdentityPlatformPackageID is the package whose group A the identity module serves once it is authoritative.

View Source
const InviteCodeLimitMessage = "The maximum number of creations has been reached"

InviteCodeLimitMessage is the answer at the limit, v2board's.

View Source
const KernelOperationEnvelopeVersion = "anixops.operation/v1"
View Source
const LegacyForwardCleanupPlaybook = "config/deploy/ansible/playbooks/forward_legacy_cleanup.yml"

LegacyForwardCleanupPlaybook is the Ansible cleanup playbook, relative to the repository root like the other forward playbooks.

View Source
const MaxAdminInviteCodeBatch = 50

MaxAdminInviteCodeBatch is how many codes one administrator request may generate.

View Source
const MaxAdminInviteCodeExpireDays = 3650

MaxAdminInviteCodeExpireDays bounds an administrator's expiry override.

View Source
const MaxOrderPaymentTradeNo = 120

MaxOrderPaymentTradeNo keeps OrderPaymentRequestID within the request ledger's 128 bytes. Generated trade numbers are 26 bytes and the column holds 64.

View Source
const MaxPluginArtifactBytes = 64 << 20
View Source
const NodeLogSourceMaintenance = "maintenance"

NodeLogSourceMaintenance is the source of the node log rows that record an Agent's maintenance events (maintenance.v1).

View Source
const NodeSecretPlaceholder = SensitiveSystemConfigPlaceholder

NodeSecretPlaceholder stands for a node secret in the administrator's answers: a secret setting of a node protocol or of a node's raw configuration, a forward node's API token, a registration key. Sending it back in a write keeps the stored secret, as for system configuration and payment gateway secrets. Nodes and agents read their configuration through their own authenticated paths (UniProxy, the gRPC node service, agent control), which build it from the stored rows and never see the placeholder.

View Source
const NodeSortTiebreaker = "id DESC"

NodeSortTiebreaker orders nodes that share a sorted value, so pages neither repeat nor skip a node.

View Source
const OrderPaymentMethod = "complete_order_payment"

OrderPaymentMethod is the request ledger method of a payment's order completion.

View Source
const OrderSortTiebreaker = "id DESC"

OrderSortTiebreaker orders orders that share a sorted value, so pages neither repeat nor skip an order.

View Source
const PackageRouteModesConfigKey = "routes"

PackageRouteModesConfigKey is the kernel-reserved key of a Control package configuration document. It maps v2 route ids to legacy, shadow or native; the kernel validates it and removes it before the package's own config_schema is applied.

View Source
const PluginAPIGrantResourceType = "plugin_api"
View Source
const PluginArtifactCopyPrefix = ".anix-package-"

PluginArtifactCopyPrefix names the per-process package copies created under the Control plugin artifact directory.

View Source
const RouteModeCLIActor = "system/cli"

RouteModeCLIActor is the actor the command line records in the audit log and the revision history.

View Source
const SensitiveSystemConfigPlaceholder = "********"
View Source
const SubscriberReissueMethod = "reset_credentials"

SubscriberReissueMethod is the subscriber request ledger's method for a new subscription token or proxy uuid (KernelSubscriber.ResetCredentials and the administrator's subscription reset).

View Source
const SystemConfigKeySubscribeDomains = "app.subscribe_domains"
View Source
const UserSortTiebreaker = "id DESC"

UserSortTiebreaker orders users that share a sorted value, so pages neither repeat nor skip a user.

View Source
const UserSubscriptionReset = "user_reset_subscribe"

UserSubscriptionReset is a user's reset of their own subscription token, for UserSubscriptionResetRequestID.

Variables

View Source
var (
	ErrAgentPluginAssignmentDenied   = errors.New("node is not assigned the requested plugin release")
	ErrAgentPluginAssignmentConflict = errors.New("node plugin assignments have conflicting desired state")
	ErrAgentPluginReleaseIntegrity   = errors.New("agent plugin release integrity check failed")
	ErrAgentPluginReconcileNotReady  = errors.New("agent plugin assignment is not ready to reconcile")
	ErrAgentPluginAddressMismatch    = errors.New("agent plugin download address does not match verified content")
)
View Source
var (
	ErrForwardCleanAgentUnauthorized = errors.New("invalid forward clean agent credentials")
	ErrForwardCleanAgentRevoked      = errors.New("forward clean agent revoked")
	// ErrForwardCleanAgentNodeMismatch refuses an agent's registration
	// under a node other than the one its token is bound to.
	ErrForwardCleanAgentNodeMismatch = errors.New("forward clean agent is bound to another node")
	// ErrForwardCleanAgentNodeRequired refuses to issue a token without the
	// forward node it is for.
	ErrForwardCleanAgentNodeRequired = errors.New("nodeId is required: a clean agent token is issued for one forward node")
	// ErrForwardCleanAgentNodeNotFound refuses to issue a token for a forward
	// node that does not exist.
	ErrForwardCleanAgentNodeNotFound = errors.New("forward node not found")
)
View Source
var (
	ErrPanelForwardNotFound     = errors.New("转发不存在")
	ErrPanelTunnelNotFound      = errors.New("隧道不存在")
	ErrPanelForwardNodeNotFound = errors.New("节点不存在")
)

Errors of the diagnoses' lookups. Their texts are what the legacy routes answer.

View Source
var (
	// ErrInviteCodeNotFound is a code id that does not exist.
	ErrInviteCodeNotFound = errors.New("invite code not found")
	// ErrInviteCodeUsed is a code that registration already consumed; it
	// stays as the record of who used it.
	ErrInviteCodeUsed = errors.New("invite code already used")
	// ErrInviteCodeFilter is a status filter the list does not know.
	ErrInviteCodeFilter = errors.New("invalid invite code status filter")
)
View Source
var (
	// ErrWithdrawAmountNotWhole: the commission balance is a whole number
	// of cents, so a withdrawal is too.
	ErrWithdrawAmountNotWhole = errors.New("amount must be a whole number")
	// ErrWithdrawalProcessed: the withdrawal is no longer pending, because
	// another decision processed it first.
	ErrWithdrawalProcessed = errors.New("withdrawal already processed")
)

Withdrawal errors. Their messages are the v2 answers.

View Source
var (
	ErrInvalidKernelOperationID    = errors.New("operation_id must be a UUID")
	ErrKernelOperationNotPending   = errors.New("operation is not cancellable")
	ErrPluginTrustRootRequired     = errors.New("official plugin trust root is required")
	ErrExtensionCatalogIntegrity   = errors.New("extension catalog integrity check failed")
	ErrPluginConfigurationConflict = errors.New("plugin configuration revision conflict")
	ErrPluginDependencyUnsatisfied = errors.New("plugin dependency is not enabled")
	ErrPluginConflict              = errors.New("plugin conflict is enabled")
	ErrPluginArtifactRequired      = errors.New("verified plugin artifact is required")
	ErrPluginArtifactImmutable     = errors.New("plugin artifact is immutable")
	ErrPluginRouteNotFound         = errors.New("plugin control route is not registered")
	ErrPluginRouteForbidden        = errors.New("plugin control route permission denied")
	ErrPackageMigrationImmutable   = errors.New("package migration run is immutable")
	ErrValidationPrecondition      = errors.New("package validation precondition failed")
	ErrCohortTransition            = errors.New("package cohort transition is invalid")
	ErrRollbackPrecondition        = errors.New("package rollback precondition failed")
)
View Source
var (
	ErrOrderPaymentRecordNotFound = errors.New("payment record not found")
	ErrOrderPaymentNotPaid        = errors.New("payment record is not paid")
	ErrOrderPaymentOtherOrder     = errors.New("payment record does not name the order")
	ErrOrderPaymentRequestTaken   = errors.New("the payment's request id is taken by another write")
)

Errors of CompleteOrderPaymentTx. They record nothing: a later call may apply.

View Source
var (
	ErrOrderNotFound     = errors.New("订单不存在")
	ErrOrderPlanNotFound = errors.New("关联套餐不存在")
)
View Source
var (
	// ErrRouteModeRejected: the change breaks a route-mode rule; the
	// message names the route and the rule.
	ErrRouteModeRejected = errors.New("route mode change rejected")
	// ErrRouteModeConfirmationRequired: a switch to native needs an
	// explicit confirmation and a reason.
	ErrRouteModeConfirmationRequired = errors.New("switching to native needs confirmation and a reason")
	// ErrRouteModePackageNotInstalled: the package has no Control
	// installation.
	ErrRouteModePackageNotInstalled = errors.New("the package has no Control installation")
)

Errors of the route-mode administration.

View Source
var (
	ErrPaymentOrderNotFound   = errors.New("订单不存在")
	ErrPaymentOrderNotPending = errors.New("订单已支付或已取消")
	ErrPaymentAmountMismatch  = errors.New("支付金额与订单金额不符")
)

Errors of CheckOrderPayable, in the messages the payment routes answer.

View Source
var (
	ErrPlanNotFound     = errors.New("套餐不存在")
	ErrPlanUserNotFound = errors.New("用户不存在")
)
View Source
var (
	ErrPluginDependencyGraphEmpty             = errors.New("plugin dependency graph is empty")
	ErrPluginDependencyGraphRootRequired      = errors.New("plugin dependency graph root is required")
	ErrPluginDependencyGraphDuplicateManifest = errors.New("duplicate plugin manifest in dependency graph")
	ErrPluginDependencyGraphDuplicateRoot     = errors.New("duplicate plugin dependency graph root")
	ErrPluginDependencyGraphInvalid           = errors.New("invalid plugin dependency graph manifest")
	ErrPluginDependencyGraphSelf              = errors.New("plugin dependency graph contains a self dependency")
	ErrPluginDependencyGraphDuplicate         = errors.New("plugin dependency graph contains a duplicate relationship")
	ErrPluginDependencyGraphMissing           = errors.New("plugin dependency graph dependency is missing")
	ErrPluginDependencyGraphCycle             = errors.New("plugin dependency graph contains a cycle")
	ErrPluginDependencyGraphConflict          = errors.New("plugin dependency graph contains a conflict")
)

The graph resolver operates on releases that have already passed signature and manifest admission. These errors describe graph-level failures so the caller can distinguish a malformed graph from a package-manager failure.

View Source
var (
	ErrStepUpPasswordRequired = errors.New("password required")
	ErrStepUpInvalidPassword  = errors.New("invalid password")
	ErrStepUpMFARequired      = errors.New("mfa code required")
	ErrStepUpInvalidMFACode   = errors.New("invalid mfa code")
)

Step-up errors: the re-authentication a user's own subscription reset needs. Their messages are the panel answers.

View Source
var (
	ErrSubscriptionUserNotFound      = errors.New("用户不存在")
	ErrSubscriptionPlanNotFound      = errors.New("套餐不存在")
	ErrSubscriptionGroupNotFound     = errors.New("分组不存在")
	ErrSubscriptionUserGroupNotFound = errors.New("用户订阅分组不存在")
	ErrSubscriptionPlanGroupNotFound = errors.New("套餐订阅分组不存在")
)
View Source
var (
	ErrTopologyDeploymentNotPlanned = errors.New("topology deployment is not planned")
	ErrTopologyDeploymentTerminal   = errors.New("topology deployment is already terminal")
	ErrTopologyDeploymentBusy       = errors.New("topology has an active deployment")
)
View Source
var AgentDiagnosticActions = map[string]AgentDiagnosticActionSpec{
	AgentDiagnosticActionServiceStatus:  {Params: []string{"service"}, Mutating: false},
	AgentDiagnosticActionServiceRestart: {Params: []string{"service"}, Mutating: true},
	AgentDiagnosticActionLogTail:        {Params: []string{"service", "lines"}, Mutating: false},
}

AgentDiagnosticActions 是终端功能允许下发的全部动作,不接受任意 action 字符串。

View Source
var ErrAgentDiagnosticTaskOfAnotherNode = errors.New("diagnostic task belongs to another node")

ErrAgentDiagnosticTaskOfAnotherNode is CompleteTask's answer to a result for a task that belongs to another node.

View Source
var ErrCapabilityNotAuthorized = errors.New("package is not authorized for the kernel capability")

ErrCapabilityNotAuthorized means the calling host may not use a kernel contract method family.

View Source
var ErrForwardNodeEndpointUnconfirmed = nodesecrets.ErrEndpointUnconfirmed

ErrForwardNodeEndpointUnconfirmed is nodesecrets.ErrEndpointUnconfirmed as the forward runtime reports it: the kernel did not present the node's token because the node's address is not its pinned endpoint.

View Source
var ErrForwardRuleAdminOnly = errors.New("only administrators can create or change legacy forward rules; forward through your tunnels instead")

ErrForwardRuleAdminOnly refuses a user's write of a legacy forward rule.

View Source
var ErrIdentityNotAuthorized = errors.New("package is not authorized for kernel.identity.v1")

ErrIdentityNotAuthorized means the calling host may not use KernelIdentity.

View Source
var ErrInvalidMemberStatus = errors.New("status must be active or expired")

ErrInvalidMemberStatus is a members status filter that is not "active" or "expired".

View Source
var ErrInvalidNodeProtocol = errors.New("invalid node protocol")
View Source
var ErrInviteCodeLimit = errors.New("invite code limit reached")

ErrInviteCodeLimit is returned by GenerateUserInviteCode when the user already holds as many unused invite codes as allowed.

View Source
var ErrMFAAlreadyEnabled = errors.New("MFA already enabled; disable it first")

ErrMFAAlreadyEnabled refuses a new TOTP secret for an account whose second factor is enabled; the identity module says the same.

View Source
var ErrNegativeTraffic = errors.New("traffic values must be non-negative")
View Source
var ErrNodeProtocolNotFound = errors.New("协议不存在")

ErrNodeProtocolNotFound indicates at least one requested node protocol ID does not exist.

View Source
var ErrNodeTrafficNode = errors.New("node not found")

ErrNodeTrafficNode is a node traffic request for a node that does not exist.

View Source
var ErrNodeTrafficWindow = errors.New("invalid node traffic window")

ErrNodeTrafficWindow is a bad granularity or window of a node traffic request; its text says what is wrong.

View Source
var ErrPaymentNotCovered = errors.New("payment not applied")

ErrPaymentNotCovered is returned, wrapped with the reason, when a paid callback does not pay what its payment record asks for. Nothing is written: the record stays pending and its order unpaid.

View Source
var ErrRawNodeConfigNotObject = errors.New("raw config must be an object")

ErrRawNodeConfigNotObject refuses a raw configuration that is not a JSON object.

View Source
var ErrRemoteRuntimeDisabled = errors.New("the remote module runtime is not enabled (module_runtime.enabled)")

ErrRemoteRuntimeDisabled means the remote runtime is not enabled in the kernel configuration.

View Source
var ErrUserNotFound = errors.New("用户不存在")

ErrUserNotFound is returned when a mutating user operation targets no row.

View Source
var ForwardDiagnosticChecks = map[string]AgentDiagnosticActionSpec{
	ForwardCheckListen:       {Params: []string{"route_id", "hop_index", "generation", "timeout_ms"}},
	ForwardCheckPortConflict: {Params: []string{"route_id", "hop_index", "generation", "timeout_ms"}},
	ForwardCheckConnect:      {Params: []string{"route_id", "hop_index", "generation", "timeout_ms", "upstream", "target_policy"}},
	ForwardCheckUDPProbe:     {Params: []string{"route_id", "hop_index", "generation", "timeout_ms", "upstream", "target_policy"}},
}

ForwardDiagnosticChecks lists the forward checks with their parameters. None changes the node.

View Source
var IdentityAccountReadRoutes = []string{
	"identity.user.profile.get",
	"identity.user.dashboard.get",
	"identity.admin.users.id.get",
	"identity.admin.users.get",
	"identity.admin.users.stats.get",
}

IdentityAccountReadRoutes are identity-platform's read routes whose native handlers take the account (email, administrator, staff and ban flags) from identity's own store: the user's profile and dashboard and the administrator's user detail, user list and user statistics (which search and count identity's accounts with Control's subscriber views in one query). Identity's store is current only while identity is authoritative; before, the legacy handlers change accounts and identity learns of it at the next import. These routes therefore leave legacy mode (shadow or native) only while identity is authoritative, and the rollback returns them to legacy with group A. In legacy mode they read Control's projection, which identity keeps current, so they may stay legacy at any time; they are not part of group A and switch on their own.

View Source
var IdentityGroupARoutes = []string{
	"identity.auth.login",
	"identity.auth.register",
	"identity.user.mfa.status.get",
	"identity.user.mfa.totp.setup.post",
	"identity.user.mfa.totp.enable.post",
	"identity.user.mfa.disable.post",
	"identity.user.mfa.verify.post",
	"identity.user.mfa.backup_codes.regenerate.post",
	"identity.admin.mfa.config.get",
	"identity.admin.mfa.config.put",
	"identity.admin.users.post",
	"identity.admin.users.id.put",
	"identity.admin.users.id.ban.post",
	"identity.admin.users.id.unban.post",
	"identity.admin.users.id.delete",
	"identity.user.subscription.reset.post",
}

IdentityGroupARoutes are identity-platform's routes that move from the kernel's legacy handlers to identity together, with the authority state: login, registration, MFA, the admin MFA configuration, the admin account writes, and the user's own subscription reset, which checks the user's password or second factor before it resets.

View Source
var NodeSortColumns = map[string]v2compat.SortColumn{
	"id":            {Expr: "id", Unique: true},
	"name":          {Expr: "name"},
	"host":          {Expr: "host"},
	"sort":          {Expr: "sort"},
	"created_at":    {Expr: "created_at"},
	"last_check_at": {Expr: "last_check_at", Nullable: true},
	"cpu_usage":     {Expr: "cpu_usage"},
	"online_users":  {Expr: "online_users"},
}

NodeSortColumns are the columns GET /api/v2/admin/nodes sorts by (the sort and order query values, v2compat.ParseListSort). The proxy-node package's native list sorts by the same keys (packages/proxy-node/native), so both answer the same order. "sort" is the administrator's own order weight. A node's shown status is derived from its last check and its protocol count from another table, so neither is sortable here; a node that never checked in (NULL last_check_at) sorts last ascending.

View Source
var OrderSortColumns = map[string]v2compat.SortColumn{
	"id":           {Expr: "id", Unique: true},
	"trade_no":     {Expr: "trade_no", Unique: true},
	"status":       {Expr: "status"},
	"type":         {Expr: "type"},
	"total_amount": {Expr: "total_amount"},
	"created_at":   {Expr: "created_at"},
	"paid_at":      {Expr: "paid_at", Nullable: true},
}

OrderSortColumns are the columns GET /api/v2/admin/orders sorts by (the sort and order query values, v2compat.ParseListSort). The order package's native list sorts by the same keys (packages/order/native), so both answer the same order. The buyer's e-mail and the plan's name come from other tables and are not sortable here; an unpaid order (NULL paid_at) sorts last ascending.

PackageReportRefusalReasons lists every refusal reason, in metric order.

View Source
var SubscriptionResetRateLimit = LoginRateLimitOptions{
	Enabled:      true,
	MaxAttempts:  3,
	Window:       time.Hour,
	Lockout:      time.Hour,
	CleanupAfter: 2*time.Hour + 5*time.Minute,
}

SubscriptionResetRateLimit bounds a user's subscription resets: every attempt that checks a credential counts, successful or not, and the third in an hour locks the user's resets for an hour. identity-platform applies the same limit (packages/identity-platform/native).

View Source
var UserSortColumns = map[string]v2compat.SortColumn{
	"id":              {Expr: "id", Unique: true},
	"email":           {Expr: "email", Unique: true},
	"created_at":      {Expr: "created_at"},
	"expired_at":      {Expr: "expired_at", Nullable: true},
	"traffic":         {Expr: "(u + d)"},
	"transfer_enable": {Expr: "transfer_enable"},
}

UserSortColumns are the columns GET /api/v2/admin/users sorts by (the sort and order query values, v2compat.ParseListSort). The identity package's native list sorts by the same keys (packages/identity-platform/native), so both answer the same order. "traffic" is the used traffic, u + d; a user without an expiry (NULL) sorts last ascending.

Functions

func ActiveUsersForNodeQuery added in v4.1.0

func ActiveUsersForNodeQuery(db *gorm.DB, groupID *uint, now time.Time) *gorm.DB

ActiveUsersForNodeQuery restricts a v2_user query to the users a node serves: the active subscribers (subscriber.Active) of the node's plan group, or every active subscriber when groupID is nil. The legacy user pulls (UniProxy user, v2board GetUsers) and the Agent Control user deltas read the same query, so a node gets the same set on either transport.

func AdminUserResetRequestID added in v4.1.0

func AdminUserResetRequestID(kind string, userID uint, token string) string

AdminUserResetRequestID names an administrator's traffic or subscription reset of a user in the subscriber request ledger, so a retried request is applied once. token identifies the HTTP request: its Idempotency-Key, else its request id. The identity package's native handlers derive the same id (packages/identity-platform/native), so a retry is recognized whichever side serves it; a new request is a new reset, as in v2.

func AdvancePackageCohort

func AdvancePackageCohort(db *gorm.DB, generationID uint, target uint8) (*model.PackageRouteGeneration, error)

AdvancePackageCohort creates the next immutable route generation only for the canonical 1, 5, 25, 100 rollout sequence. Retrying the exact request returns the previously-created successor.

func AllocateNodeOperationRevision added in v4.2.0

func AllocateNodeOperationRevision(db *gorm.DB, nodeID uint, floor int64) (int64, error)

AllocateNodeOperationRevision reserves the next desired revision of nodeID: one above both the stored cursor and floor (the highest revision the caller already knows was sent), and stores it.

func AmountPaysOrder added in v4.1.0

func AmountPaysOrder(amount float64, totalCents int64) bool

AmountPaysOrder reports whether amount (yuan) is an order's total (cents) to the cent.

func ApplyTopologyObservedStateTx

func ApplyTopologyObservedStateTx(tx *gorm.DB, update TopologyObservedStateUpdate) (*model.TopologyObservedState, bool, error)

ApplyTopologyObservedStateTx is the transaction form used by gRPC observed callbacks so operation and topology state commit atomically.

func AuditUsername added in v4.1.0

func AuditUsername(db *gorm.DB, userID *uint) string

AuditUsername is the username the audit trail records for userID when the request names none: the user's e-mail, which the kernel's handlers record for a signed-in administrator. A request the package bridge relays to a legacy handler, and a KernelSettings call, carry the actor's id only. An unknown user, or a failed lookup, records none.

func BackfillForwardPortBindings

func BackfillForwardPortBindings(db *gorm.DB) error

func BackupConfigAuditContent added in v4.1.0

func BackupConfigAuditContent(cfg *model.BackupConfig, preservedSensitiveFields []string) string

BackupConfigAuditContent is the content of a backup configuration audit entry.

func BeginPackageMigration

func BeginPackageMigration(db *gorm.DB, input model.PackageMigrationRun) (*model.PackageMigrationRun, error)

BeginPackageMigration creates one generation-fenced migration run. A retry with the same immutable input returns the original run and its latest checkpoint so an interrupted host call can resume safely.

func BootstrapIdentityPlatformPackage

func BootstrapIdentityPlatformPackage(db *gorm.DB, encodedPublicKey, directory string) error

BootstrapIdentityPlatformPackage imports exactly one signed identity package before HTTP routes are exposed. This is the only package allowed through the unauthenticated cold-start path; every later package admission stays behind the authenticated kernel API.

A bootstrap never changes an installation that exists, with one exception (moveRetiredRootIdentityInstallation): after the official signing root changed, an installation whose release is bound to a retired root moves to the bootstrap release, which has just been verified under the active root.

func BuildAgentPluginInstallConfig

func BuildAgentPluginInstallConfig(db *gorm.DB, nodeID uint, pluginID, version string) (string, error)

func BuildLoginRateLimitKey

func BuildLoginRateLimitKey(email, ip string) string

func BuildNodeProtocolConfig

func BuildNodeProtocolConfig(node *model.Node, protocol *model.NodeProtocol) map[string]any

BuildNodeProtocolConfig 从 Node + NodeProtocol 构建下发给节点的完整协议配置。

这是节点拉配置的**唯一真源**: HTTP UniProxy handler 和 gRPC 两个配置构建函数 都调它, 避免"改了一处忘了另一处"(SS2022 server_key 之前就是只在订阅端修了, gRPC 节点拿不到)。输出字段名与老 UniProxy JSON 保持一致:

node_type, type, host, server_port, server_name, tls, network,
cipher, server_key, flow, tls_settings, network_settings,
send_through, routes, base_config, 以及 custom_config 覆盖的任意字段

SS2022 (2022-blake3-*) 的 server_key 若 Settings 未显式给出, 按老 XBoard 算法 从节点 created_at 派生 (子节点用父节点 created_at), 与订阅端 DeriveSS2022ServerKey 和节点端 V2bX 三方一致。

The protocol's secrets are read through the node credential split, in the table's phase, and checked first: a protocol whose secrets fail validation is reported (ReportNodeSecretFindings) and still built, since validation is report-only until decision D7 is enforced.

func BuildRegisterRateLimitKey

func BuildRegisterRateLimitKey(ip string) string

func BuildUniProxyNodeConfig added in v4.1.0

func BuildUniProxyNodeConfig(db *gorm.DB, nodeID uint, preferredType string) (map[string]any, error)

BuildUniProxyNodeConfig builds the configuration UniProxy answers a proxy node (GET /api/v1/server/UniProxy/config, and the /api/v2 alias) for the node type it asks for, "" for none: the node's raw configuration when it has one, with the base fields filled in; else the enabled protocol of that type (the first enabled protocol for none) through BuildNodeProtocolConfig; else, for none, a minimal VLESS configuration. The answer is the map UniProxy encodes, field for field. The desired configuration of the Agent Control stream carries it too (kernelnodeops, node-ops-service.md section 5.5), so a node gets the same configuration on either transport. gorm.ErrRecordNotFound when the node does not exist.

func BumpSettingsGeneration added in v4.1.0

func BumpSettingsGeneration(namespace string) uint64

BumpSettingsGeneration marks the namespace changed, so every in-memory copy of it reloads, and returns the new generation. Call it after the change committed.

func BumpSettingsGenerationAfter added in v4.1.0

func BumpSettingsGenerationAfter(namespace string, before uint64) (uint64, bool)

BumpSettingsGenerationAfter bumps namespace after a write that read its generation before writing, and reports whether the writer may keep the value it wrote as its in-memory copy at the new generation: only when no other write bumped the namespace in between, else the copy must reload.

func CancelKernelOperation

func CancelKernelOperation(db *gorm.DB, operationID string, at time.Time) (*model.KernelOperation, error)

func CanonicalKernelOperationConfig

func CanonicalKernelOperationConfig(raw string) (string, error)

CanonicalKernelOperationConfig normalizes a JSON config before it becomes part of an idempotency identity and Agent envelope. This makes hash and retry behavior independent of request whitespace or object key ordering.

func CanonicalPluginManifest

func CanonicalPluginManifest(manifest PluginManifest) ([]byte, error)

CanonicalPluginManifest is the exact byte representation that AnixOps signs and verifies. Schema JSON is recursively parsed and re-encoded so whitespace or object-key ordering cannot change a release signature.

func CreateKernelOperation

func CreateKernelOperation(db *gorm.DB, operation model.KernelOperation) (*model.KernelOperation, bool, error)

CreateKernelOperation persists a requested operation before it can be sent to an agent. A repeated idempotency key returns the original operation, including after a control-plane restart.

func CreateTopologyRevision

func CreateTopologyRevision(db *gorm.DB, topologyID, actorID uint, input TopologyRevisionInput) (*model.TopologyRevision, error)

func DecodeRawNodeConfig added in v4.1.0

func DecodeRawNodeConfig(raw any) (map[string]any, []byte, error)

DecodeRawNodeConfig decodes a raw configuration an administrator sent inline or as a JSON string into an object; anything else is ErrRawNodeConfigNotObject.

func DefaultBackupConfig added in v4.1.0

func DefaultBackupConfig() model.BackupConfig

DefaultBackupConfig is the backup configuration the kernel creates when there is none.

func DeleteUserTx added in v4.1.0

func DeleteUserTx(tx *gorm.DB, id uint) (authn.Revocation, error)

DeleteUserTx deletes a user and its WireGuard peers in tx and records the revocation of its tokens; apply it with authn.Remember after the commit.

func DeriveSS2022ServerKey

func DeriveSS2022ServerKey(createdAt time.Time, cipher string) string

DeriveSS2022ServerKey 复刻老 XBoard `Helper::getServerKey($timestamp, $len)`: base64(substr(md5(unix秒的十进制字符串), 0, len))。md5 输出取 hex 字符串的前 len 个字符(不是字节), 再 base64。节点端 V2bX 用相同的 server_key, 两边必须一致。 导出供 handler 层 (UniProxy 节点拉配置) 复用, 保证订阅端和节点端算出的值一致。

func DropNodeCache added in v4.1.0

func DropNodeCache(id uint)

DropNodeCache forgets what the kernel caches about a proxy node: its row and the node list. The next read loads the rows as they are.

func EnsureAgentDiagnosticTaskSchema

func EnsureAgentDiagnosticTaskSchema(db *gorm.DB) error

EnsureAgentDiagnosticTaskSchema creates the diagnostic task table if it does not exist. Called unconditionally at startup (including production) because AutoMigrate only runs in dev/test. It touches only the new table, never existing ones.

func EnsureForwardBridgeSchema

func EnsureForwardBridgeSchema(db *gorm.DB) error

EnsureForwardBridgeSchema creates the durable clean_agent bridge mapping table if it does not exist. Called unconditionally at startup (including production) because AutoMigrate only runs in dev/test. Idempotent and only touches this new table.

func EnsureForwardNodeMetricsPortColumn

func EnsureForwardNodeMetricsPortColumn(db *gorm.DB) error

EnsureForwardNodeMetricsPortColumn adds the metrics_port column to the existing v2_forward_node table if it does not exist yet. Called unconditionally at startup (including production) because AutoMigrate only runs in dev/test. It touches only this one column on an existing table, never anything else.

func EnsureForwardPortBindingSchema

func EnsureForwardPortBindingSchema(db *gorm.DB) error

func EnsureForwardRuntimeJobSchema

func EnsureForwardRuntimeJobSchema(db *gorm.DB) error

EnsureForwardRuntimeJobSchema keeps runtime job invariants that GORM cannot express with model tags, especially the partial unique index for active jobs.

func EnsureKernelSchema

func EnsureKernelSchema(db *gorm.DB) error

EnsureKernelSchema only migrates new kernel-owned tables and is safe to run in production without touching legacy plugin-owned tables.

func EnsureNodeRuntimeHealthSchema

func EnsureNodeRuntimeHealthSchema(db *gorm.DB) error

EnsureNodeRuntimeHealthSchema adds the runtime-health columns used by WireGuard relay supervision. It is kept separate because production startup intentionally does not run the complete application AutoMigrate set.

func EnsureObservabilitySchema

func EnsureObservabilitySchema(db *gorm.DB) error

EnsureObservabilitySchema creates the latency bucket table if it does not exist. Called unconditionally at startup (including production) because AutoMigrate only runs in dev/test. It touches only the new observability table, never existing tables.

func EnsurePluginTrustRoot

func EnsurePluginTrustRoot(db *gorm.DB, publicKey ed25519.PublicKey) (*model.PluginTrustRoot, error)

func EnsureStatsSchema

func EnsureStatsSchema(db *gorm.DB) error

EnsureStatsSchema creates or completes the traffic/stats tables: v2_server_log, v2_online_log, v2_stat_user, v2_stat_server. Called unconditionally at startup (including production) because AutoMigrate only runs in dev/test. Idempotent and limited to these tables; existing legacy tables are only expanded with missing columns/indexes.

func EnsureWireGuardPeerSchema

func EnsureWireGuardPeerSchema(db *gorm.DB) error

EnsureWireGuardPeerSchema creates or upgrades only the WireGuard peer table. The server intentionally skips the full AutoMigrate set in production, so newly introduced runtime tables need an explicit idempotent schema hook.

func ExpireKernelOperations

func ExpireKernelOperations(db *gorm.DB, now time.Time) (int64, error)

ExpireKernelOperations marks undispatched or in-flight work that has passed its deadline. A future supervisor worker may call this on a timer; read APIs also invoke it so stale intent is never presented as pending.

func ExtractPluginWebUIBundle

func ExtractPluginWebUIBundle(manifest PluginManifest, artifact []byte) ([]byte, error)

func ForwardNodeAPIToken added in v4.1.0

func ForwardNodeAPIToken(db *gorm.DB, node *model.ForwardNode) (string, error)

ForwardNodeAPIToken is the token the kernel presents to a forward node's management API, through NodeX or gost, at the node's current address: its token when the address is the pinned endpoint (section 3.8), else ErrForwardNodeEndpointUnconfirmed.

func ForwardNodeHasToken added in v4.1.0

func ForwardNodeHasToken(db *gorm.DB, nodeID uint) (bool, error)

ForwardNodeHasToken reports whether a forward node holds a token, read through the credential split.

func ForwardUserResetRequestID added in v4.1.0

func ForwardUserResetRequestID(userID uint, token string) string

ForwardUserResetRequestID names an administrator's subscriber traffic reset through the Flux-style POST /api/v2/user/reset (type 1) in the subscriber request ledger, so a retried request is applied once. token identifies the HTTP request, as for AdminUserResetRequestID. (Until v4.2 the forward package's native handler derived the same id; since F5d the kernel serves the route alone.)

func GenerateForwardNodeToken added in v4.1.0

func GenerateForwardNodeToken() (string, error)

GenerateForwardNodeToken generates a forward node's API token, as the forward node creation does: 16 random bytes, hex encoded.

func GenerateWireGuardKeypair

func GenerateWireGuardKeypair() (privateKey string, publicKey string, err error)

GenerateWireGuardKeypair exposes server-key generation to the authenticated admin workflow without persisting private material until the protocol is explicitly saved.

func GetPluginArtifact

func GetPluginArtifact(db *gorm.DB, releaseID uint) (*model.PluginArtifact, error)

func GetPluginConfiguration

func GetPluginConfiguration(db *gorm.DB, installationID uint) (*model.PluginConfiguration, error)

GetPluginConfiguration returns the persisted document for one installation. An installation with no document has the deterministic empty-object default.

func HashKernelOperationConfig

func HashKernelOperationConfig(raw string) (string, error)

HashKernelOperationConfig returns the deterministic SHA-256 digest used in operation envelopes and persisted plugin configuration rows. The input is canonicalized first so semantically identical JSON documents share a hash.

func IdentityAuthoritative added in v4.1.0

func IdentityAuthoritative(state string) bool

IdentityAuthoritative reports whether identity owns credentials in state.

func IdentityAuthorityState added in v4.1.0

func IdentityAuthorityState(db *gorm.DB) (string, error)

IdentityAuthorityState returns the identity authority state; no row means the kernel.

func InitAdmin

func InitAdmin(cfg *config.Config)

InitAdmin 初始化管理员账号

func InitDefaultAuthKeyFromEnv

func InitDefaultAuthKeyFromEnv()

InitDefaultAuthKeyFromEnv 从环境变量初始化默认授权密钥 环境变量: NODE_DEFAULT_AUTH_KEY 如果设置了该环境变量:

  • 如果密钥内容已存在 → 跳过
  • 如果存在同名默认密钥但内容不同 → 删除旧的,创建新的
  • 如果不存在 → 创建

Note: 改变 AuthKey 只会影响新节点注册,已注册节点使用 APIKey 通信,不受影响

func InitDefaultPlan

func InitDefaultPlan()

InitDefaultPlan 确保数据库中有一个默认套餐(如“基础套餐”)

func InitForwardRuntimeSystemConfig

func InitForwardRuntimeSystemConfig(db *gorm.DB) error

func InitSubscriptionDefaults

func InitSubscriptionDefaults()

InitSubscriptionDefaults 初始化默认订阅分组和模板

func InviteCodeLockKeys added in v4.1.0

func InviteCodeLockKeys(userID uint) (int32, int32)

InviteCodeLockKeys are the two keys of a user's invite code lock: the class and the user id. A user id beyond 31 bits shares its key with another user's, which only makes their generations wait for each other.

func IsAgentPluginOperation

func IsAgentPluginOperation(kind string) bool

IsAgentPluginOperation reports whether an operation has a concrete Agent Supervisor implementation. Topology operations remain mediated through their concrete plugin lifecycle operations.

func IsForwardDiagnosticCheck added in v4.2.0

func IsForwardDiagnosticCheck(action string) bool

IsForwardDiagnosticCheck reports whether action is a forward check.

func IsNodeSecretKey added in v4.1.0

func IsNodeSecretKey(key string) bool

IsNodeSecretKey reports whether a key of a node protocol's settings, or of a node's raw configuration, names a secret: Reality's and TLS's private_key (privateKey), WireGuard's server_private_key and preshared_key, Shadowsocks' server_key, psk and password, the Hysteria2 obfs-password and auth, and any other *_key, password, passwd, pass, secret, token, credential or seed. Public keys, Reality's short_id and the paths of key files (key_file, wss_key_file) are not secrets. The rule is nodesecrets.IsSecretKey, which also places the secrets the credential split moves.

func IsSensitiveSystemConfigKey

func IsSensitiveSystemConfigKey(key string) bool

func IsSuperAdmin added in v4.1.0

func IsSuperAdmin(db *gorm.DB, userID uint) (bool, error)

IsSuperAdmin reports whether userID may switch route modes. The kernel has no separate super-administrator role, so this is its strictest administrator check, read from the database rather than the token: an administrator (is_admin) who is not staff and not banned. A demoted or banned administrator loses the right at once, not when the token expires.

func IsWireGuardExitProtocol

func IsWireGuardExitProtocol(protocol *model.NodeProtocol) bool

IsWireGuardExitProtocol identifies the overseas relay role. Exit nodes do not terminate user WireGuard peers, so they must neither allocate peer keys, appear in user subscriptions, nor receive user runtime metadata.

func IssueForwardNodeTokenTx added in v4.1.0

func IssueForwardNodeTokenTx(tx *gorm.DB, nodeID uint, token string) (bool, error)

IssueForwardNodeTokenTx writes a forward node's token to its row and the split tables, which pin it to the node's management endpoint (nodesecrets). A token that replaces another revokes the node's agent certificates (credentials_replaced), as the forward node update does. It reports whether a token was replaced.

func IssueProxyNodeCredentialsTx added in v4.1.0

func IssueProxyNodeCredentialsTx(tx *gorm.DB, nodeID uint, credentials ProxyNodeCredentials, replaced bool) error

IssueProxyNodeCredentialsTx writes a proxy node's credentials to its row and the split tables. With replaced, the node's agent certificates and enrollments are revoked (credentials_replaced): an agent enrolled with the old key must enroll again.

func IssueRegistrationKeyTx added in v4.1.0

func IssueRegistrationKeyTx(tx *gorm.DB, name string, expireAt *int64) (*model.AuthorizedKey, string, error)

IssueRegistrationKeyTx creates a node registration key and returns its row and the key, which is shown once. expireAt ends the key; nil never.

func KeepNodeRawConfig added in v4.1.0

func KeepNodeRawConfig(incoming string, stored *string) string

KeepNodeRawConfig returns an incoming raw configuration with the stored values of the secrets it sends as NodeSecretPlaceholder (see KeepNodeSecretsJSON).

func KeepNodeSecretsJSON added in v4.1.0

func KeepNodeSecretsJSON(incoming, stored string) string

KeepNodeSecretsJSON returns an incoming node protocol setting or raw configuration (JSON text) in which every secret sent as NodeSecretPlaceholder has its stored value (an empty string when nothing is stored), so an administrator can save what a masked answer showed without retyping its secrets. A placeholder sent for the whole value keeps the whole stored value. Arrays are matched by position.

func KeepSystemConfigFields added in v4.1.0

func KeepSystemConfigFields(key, incoming, stored string) (value string, kept bool)

KeepSystemConfigFields returns incoming, a value about to be stored under key, in which every masked field sent as SensitiveSystemConfigPlaceholder has the value stored before ("" when nothing is stored), so an administrator can save what a masked answer showed without retyping its secrets. The placeholder sent for the whole value keeps the whole stored value. kept reports whether a stored secret was kept. The rest of the text is kept as it is.

func LegacyTopologyObservedHealthJSON

func LegacyTopologyObservedHealthJSON() string

LegacyTopologyObservedHealthJSON is used only by the pre-executor compatibility bridge. Its Agent StateJson is intentionally discarded: that schema was never a public contract and may contain plugin-defined details.

func LoadBackupConfig added in v4.1.0

func LoadBackupConfig(db *gorm.DB) (*model.BackupConfig, error)

LoadBackupConfig reads the first backup configuration row, creating it with the defaults when there is none.

func LoadVerifiedPluginCompatibilityRoutes

func LoadVerifiedPluginCompatibilityRoutes(db *gorm.DB, release model.PluginRelease, publicKey ed25519.PublicKey) ([]byte, error)

LoadVerifiedPluginCompatibilityRoutes returns the immutable compatibility declaration embedded in a signed v2 Control artifact. Callers still decide whether the corresponding installation is active; this helper establishes that the declaration itself is bound to the stored release and artifact.

func LoadVerifiedPluginWebUIAsset

func LoadVerifiedPluginWebUIAsset(db *gorm.DB, release model.PluginRelease, manifest PluginManifest) (*model.PluginWebUIAsset, error)

func LockNodePluginLifecycleTx

func LockNodePluginLifecycleTx(tx *gorm.DB, nodeID uint, pluginID string) (*model.NodePluginLifecycle, error)

LockNodePluginLifecycleTx acquires the aggregate lock without recalculating desired state. Mutating handlers use it before locking assignment rows so all callers follow the same lifecycle -> assignment lock order.

func LockPluginInstallationTarget

func LockPluginInstallationTarget(tx *gorm.DB, target string) error

LockPluginInstallationTarget must be called inside the transaction that validates and mutates plugin installation intent. The shared target row closes races where two packages could otherwise pass dependency/conflict checks against the same stale enabled set.

func LockUserInviteCodes added in v4.1.0

func LockUserInviteCodes(tx *gorm.DB, userID uint) error

LockUserInviteCodes takes the user's invite code lock until tx ends, on PostgreSQL; on SQLite it does nothing.

func MaskAuthorizedKeys added in v4.1.0

func MaskAuthorizedKeys(keys []model.AuthorizedKey)

MaskAuthorizedKeys masks registration keys for an administrator's answer. A key is shown once, in the answer that generates it; a node registers with it by its hash.

func MaskSystemConfigFields added in v4.1.0

func MaskSystemConfigFields(key, value string) string

MaskSystemConfigFields returns value, the JSON object stored under key, with each of its masked fields that has a value (anything but null and a blank string) replaced by SensitiveSystemConfigPlaceholder. The rest of the text is kept as it is. A key without masked fields, a blank value and a JSON value that is not an object are returned unchanged; a value that is not JSON is replaced whole, since its secret cannot be told apart.

func MaskSystemConfigValue

func MaskSystemConfigValue(key, value string) (displayValue string, sensitive bool, hasValue bool)

MaskSystemConfigValue is a value as an administrator's answer shows it: the placeholder for a sensitive key with a value, "" for one without, and any other value with its masked fields masked (MaskSystemConfigFields).

func MaterializePluginControlArtifact

func MaterializePluginControlArtifact(db *gorm.DB, publicKey ed25519.PublicKey, pluginID, version, destinationRoot string) (pluginhost.ArtifactRef, error)

MaterializePluginControlArtifact writes the verified immutable release bytes and the signed v2 Control entrypoint into a private caller-owned directory. Task 3's lifecycle dispatcher consumes the resulting ArtifactRef; it still owns process supervision and never interprets package metadata itself.

func MigratePackageHost

func MigratePackageHost(ctx context.Context, db *gorm.DB, manager *pluginhost.Supervisor, migrationRunID uint, input pluginhost.MigrationInput) (pluginhost.MigrationOutput, error)

MigratePackageHost is the only public orchestration path that can attach a health fence to a migration run. Requiring the concrete Supervisor prevents callers from substituting an arbitrary MigrationManager that fabricates a successful health result.

func NewSubscriberUser added in v4.1.0

func NewSubscriberUser(email, passwordHash string) *model.User

NewSubscriberUser returns a new v2_user row with fresh node UUID and subscription token, as registration creates it.

func NodeHeartbeatStatus added in v4.1.0

func NodeHeartbeatStatus() clause.Expr

NodeHeartbeatStatus is the status column a node's heartbeat writes: online, unless an administrator disabled the node. A heartbeat proves that the node's software runs; it never re-enables a disabled node. A pending node (never seen yet) becomes online, as before.

func NodeOpsCapabilities added in v4.1.0

func NodeOpsCapabilities() []string

NodeOpsCapabilities are the five KernelNodeOps family capabilities, in the order of the contract's OperationFamily enum.

func NodeTrafficBucketBounds added in v4.2.0

func NodeTrafficBucketBounds(granularity string, since, until, now time.Time) (time.Time, time.Time, error)

NodeTrafficBucketBounds aligns a requested window to the buckets of granularity: since rounds down to a bucket start, until up to a bucket boundary; a zero until is the end of the current bucket and a zero since the 24 hours (hour) or 30 days (day) before until. It returns ErrNodeTrafficWindow for an unknown granularity, an empty or reversed window, or one of more than MaxNodeTrafficHours hours or MaxNodeTrafficDays days.

func NormalizeForwardRuntimeBackend added in v4.1.0

func NormalizeForwardRuntimeBackend(value string) (string, bool)

NormalizeForwardRuntimeBackend answers the canonical name of a runtime backend (iptables_ansible is kept as named) and whether value names one.

func NormalizeNodeLogLevel

func NormalizeNodeLogLevel(level string) string

func NormalizeNodeType

func NormalizeNodeType(nodeType string) string

NormalizeNodeType 归一化节点类型别名 (v2ray/vmess-aead → vmess)。

func OrderPaymentRequestID added in v4.1.0

func OrderPaymentRequestID(tradeNo string) string

OrderPaymentRequestID names a payment's order completion in the request ledger (v4_kernel_subscriber_request).

func PackageIndexMigrationID added in v4.1.0

func PackageIndexMigrationID(indexSHA256 string) string

PackageIndexMigrationID names the ledger run that applies a whole migration index, keyed by the index digest.

func PackageMigrationStepsDigest added in v4.1.0

func PackageMigrationStepsDigest(steps []pluginhost.MigrationStep) string

PackageMigrationStepsDigest is the validation digest a host must report after applying steps; see packagestoresdk.StepsDigest.

func PackageReleaseAtLeast added in v4.1.0

func PackageReleaseAtLeast(version, minimum string) bool

PackageReleaseAtLeast reports whether version is a semantic version at least minimum ("4.1.0" is above "4.1.0-rc.5"). A version that does not parse is not.

func PackageReportRefusal added in v4.2.0

func PackageReportRefusal(err error) string

PackageReportRefusal returns the reason of a refusal, or "" for any other error (one the kernel could not record for now).

func PackageReportStale added in v4.2.0

func PackageReportStale(kind string, observedAt, now time.Time) bool

PackageReportStale reports whether a report of kind observed at observedAt is stale at now.

func PackageRouteCatalog added in v4.1.0

func PackageRouteCatalog() (map[string]RouteCatalogEntry, error)

PackageRouteCatalog returns the embedded package extraction map by route id: which routes have a native implementation (native-flagged), which only the legacy one (bridged) and which stay in the kernel (kernel-owned).

func PackageRouteDefaultPolicy added in v4.1.0

func PackageRouteDefaultPolicy() string

PackageRouteDefaultPolicy returns the process's default policy, rehearsed unless SetPackageRouteDefaultPolicy chose legacy.

func PackageRouteDefaultsSummary added in v4.1.0

func PackageRouteDefaultsSummary() (string, error)

PackageRouteDefaultsSummary describes the process policy for the startup log: the policy, and how many routes of how many packages default to native under it (for releases at least their min_version).

func ParseOfficialPluginPublicKey

func ParseOfficialPluginPublicKey(encoded string) (ed25519.PublicKey, error)

func ParseOnlineData

func ParseOnlineData(data map[string]any) (map[uint][]string, error)

ParseOnlineData 解析在线数据

func ParsePluginMigrationIndex added in v4.1.0

func ParsePluginMigrationIndex(manifest PluginManifest, artifact []byte) ([]pluginhost.MigrationStep, error)

ParsePluginMigrationIndex verifies the signed migration index of a package artifact and returns its steps in order. The index must match the manifest digest, name the manifest's package and version, and every step script must exist in the artifact with the declared digest.

func ParseTrafficData

func ParseTrafficData(data map[string]any) (map[uint][2]int64, error)

ParseTrafficData 解析流量数据

func PlanAssignmentRequestID added in v4.1.0

func PlanAssignmentRequestID(planID, userID uint, expireAt *int64, token string) string

PlanAssignmentRequestID names an administrator's plan assignment in the subscriber request ledger (v4_kernel_subscriber_request), so a retried request is applied once. token identifies the HTTP request: its Idempotency-Key, else its request id. A new request is a new grant, as in v2. The plan package's native route derives the same id (packages/plan/native.AssignRequestID), so a retry is recognized whichever side serves it.

func PlanTopologyDeployment

PlanTopologyDeployment compiles the immutable graph into deployment steps. It does not mutate an active revision or create any Agent work.

func PluginAPIPermission

func PluginAPIPermission(pluginID string) string

func PluginArtifactStorageKey

func PluginArtifactStorageKey(pluginID, version, artifactSHA256 string) string

func PluginControlBridgeRouteID

func PluginControlBridgeRouteID(pluginID, route string) string

PluginControlBridgeRouteID turns a signed Control route into a stable, package-bridge-safe operation name. The original route remains kernel-owned request metadata; package processes never choose it themselves.

func PluginRuntimeIsRemote added in v4.1.0

func PluginRuntimeIsRemote(db *gorm.DB) func(context.Context, string) (bool, error)

PluginRuntimeIsRemote adapts PluginRuntimeOf for the host supervisor.

func PluginRuntimeOf added in v4.1.0

func PluginRuntimeOf(ctx context.Context, db *gorm.DB, pluginID string) (string, error)

PluginRuntimeOf returns the runtime of a Control package: remote only when a row selects it.

func PluginTrustRootFingerprint

func PluginTrustRootFingerprint(publicKey ed25519.PublicKey) string

func PluginTrustRootKeyID

func PluginTrustRootKeyID(publicKey ed25519.PublicKey) string

func PluginWebUIAssetStorageKey

func PluginWebUIAssetStorageKey(pluginID, version, bundleSHA256, bundlePath string) string

func PluginWebUIAssetURL

func PluginWebUIAssetURL(pluginID, version, bundleSHA256, bundlePath string) string

func ProxyNodeHasCredentials added in v4.1.0

func ProxyNodeHasCredentials(db *gorm.DB, nodeID uint) (apiKey, secret bool, err error)

ProxyNodeHasCredentials reports whether a proxy node holds an API key and a shared secret, read through the credential split.

func PurgeCompletedAssignmentDeletes

func PurgeCompletedAssignmentDeletes(tx *gorm.DB, lifecycle *model.NodePluginLifecycle, chain *AgentAssignmentOperationChain) error

PurgeCompletedAssignmentDeletes is used by handlers when dispatch is explicitly disabled and the observed plugin is already inactive.

func RaiseNodeOperationRevision added in v4.2.0

func RaiseNodeOperationRevision(db *gorm.DB, nodeID uint, revision int64) error

RaiseNodeOperationRevision raises the stored desired revision of nodeID to at least revision, so the next allocation is above it. An Agent that connects reports the revision it last applied; after a Control restart it can be above the cursor.

func ReconcileAgentAssignments

func ReconcileAgentAssignments(db *gorm.DB, now time.Time) (int, error)

ReconcileAgentAssignments resumes enrolled lifecycle rows. It does not scan legacy generation-zero assignments, preserving the opt-in migration gate.

func RecordMigrationCheckpoint

func RecordMigrationCheckpoint(db *gorm.DB, migrationRunID uint, checkpoint PackageMigrationCheckpoint) (*model.PackageMigrationRun, error)

RecordMigrationCheckpoint durably records an opaque host reply. It cannot activate a route; the health proof is recorded only by the service-owned migration coordinator after the supervisor returns successfully.

func RecordNodePluginOperationSuccessTx

func RecordNodePluginOperationSuccessTx(tx *gorm.DB, operation model.KernelOperation) error

RecordNodePluginOperationSuccessTx captures the version actually selected by the Agent, so later disable/delete never guesses from a stale role row.

func RecordPackageValidation

func RecordPackageValidation(db *gorm.DB, input model.PackageValidationResult) (*model.PackageValidationResult, error)

RecordPackageValidation accepts only a completed migration checkpoint and creates the initial one-percent route generation. The digest remains opaque to the kernel.

func RedactNode added in v4.1.0

func RedactNode(node *model.Node)

RedactNode masks a node's raw configuration and the secrets of its loaded protocols for an administrator's answer (see RedactNodeProtocol). The node's API key and shared secret are never serialized (json:"-").

func RedactNodeProtocol added in v4.1.0

func RedactNodeProtocol(protocol *model.NodeProtocol)

RedactNodeProtocol masks the secrets in a node protocol's settings for an administrator's answer. It changes the protocol it is given, so it is for a row read for that answer only, never one that is saved afterwards. The protocol's node, when loaded, has its raw configuration masked too.

func RedactNodeProtocols added in v4.1.0

func RedactNodeProtocols(protocols []model.NodeProtocol)

RedactNodeProtocols masks the protocols of an administrator's answer.

func RedactNodeSecretsJSON added in v4.1.0

func RedactNodeSecretsJSON(config string) string

RedactNodeSecretsJSON replaces the secret values of a node protocol setting or a raw configuration (JSON text) with NodeSecretPlaceholder. A string or array under a secret key is replaced whole; an object's own keys are checked, and so are the objects in arrays. A value without a secret is returned unchanged, and one that is not JSON is replaced whole, since it cannot be told apart.

The rule is nodesecrets.Redact, which also writes the documents a finalized table keeps (section 4.4), so the stored redacted document and this answer are the same bytes.

func RefreshAgentPluginInstallationObservedState

func RefreshAgentPluginInstallationObservedState(db *gorm.DB, pluginID string) error

RefreshAgentPluginInstallationObservedState projects node lifecycle state in a separate, consistently ordered transaction. Lifecycle mutations must not call this while holding an aggregate lock because global installation writes use target -> installation -> lifecycle ordering.

func RegisterPluginRelease

func RegisterPluginRelease(db *gorm.DB, manifestJSON, signature string, publicKey ed25519.PublicKey) (*model.PluginRelease, error)

RegisterPluginRelease admits only a manifest signed by the configured AnixOps trust root and records the current exclusive trust-root fingerprint. Artifact bytes are uploaded separately and must match the signed manifest hash before an install can be enabled.

func RenumberPendingNodeOperations added in v4.2.0

func RenumberPendingNodeOperations(db *gorm.DB, nodeID uint, operationID string, floor int64) (int64, error)

RenumberPendingNodeOperations moves the durable operation operationID, which the Agent control stream refused because its revision is not above the last revision sent on the node, and the pending operations behind it onto revisions above both the stored cursor and floor, keeping their order, and returns the operation's new revision. A durable operation takes its revision when it is created; a one-off stream operation sent after that takes a higher one, and the Agent supersedes anything at or below the revision it observed, so the older number can never be sent again. The operation goes back to pending so the dispatcher sends it with its new revision. Only an operation with no earlier active operation on the node qualifies, which means every pending operation behind it has never been sent.

func ReportNodeSecretFindings added in v4.1.0

func ReportNodeSecretFindings(findings []NodeSecretFinding)

ReportNodeSecretFindings counts and logs each finding of a configuration build (anixops_node_secrets_invalid_total, once per subject in the log). It excludes nothing.

func RequestTopologyDeploymentApply

func RequestTopologyDeploymentApply(db *gorm.DB, deploymentID uint, _ time.Time) (*model.TopologyDeployment, error)

RequestTopologyDeploymentApply makes a plan eligible for the feature-gated worker. It is idempotent while already applying but never reopens terminal deployments, preserving an audit trail for every revision attempt.

func RequestTopologyDeploymentRollback

func RequestTopologyDeploymentRollback(db *gorm.DB, deploymentID uint, at time.Time) (*model.TopologyDeployment, error)

RequestTopologyDeploymentRollback stops expansion and schedules durable reverse-order compensation. It is safe to call before any operation has dispatched; such a deployment simply reaches rolled_back with skipped steps.

func ResetLoginRateLimiterForTest

func ResetLoginRateLimiterForTest()

ResetLoginRateLimiterForTest forgets every login, registration and subscription reset attempt.

func ResetStatsServiceForTest added in v4.1.0

func ResetStatsServiceForTest()

ResetStatsServiceForTest drops the shared StatsService, which keeps the database it was first built with, so a test that opens a new database gets a service on it.

func ResolveActivePluginWebUIAsset

func ResolveActivePluginWebUIAsset(db *gorm.DB, publicKey ed25519.PublicKey, actorID uint, legacyAdmin bool, pluginID, version, bundleSHA256 string) (*model.PluginWebUIAsset, error)

ResolveActivePluginWebUIAsset binds an immutable asset URL to the currently active control installation and to the requesting actor. Stored signatures, release metadata, artifact bytes and the extracted bundle are revalidated on every request so disabling or replacing an installation invalidates old URLs.

func ResolveEffectivePackageRouteModes added in v4.1.0

func ResolveEffectivePackageRouteModes(db *gorm.DB, packageID, version string, stored map[string]string) (map[string]string, map[string]string, error)

ResolveEffectivePackageRouteModes returns a package's effective route modes and where each comes from. It is what every reader that hands modes to a package host, or shows them, uses: the bridge's GetPackageConfig and the route-mode administration. In order:

  • a stored mode wins, legacy included;
  • identity group A follows the identity authority (ResolvePackageRouteModes);
  • a route of the default set (config/package-route-defaults.json) is native when the policy is rehearsed, the installed release (version) is at least the package's min_version and the extraction map still lists the route as native-flagged for the package;
  • anything else is legacy.

The returned modes name every route that is not legacy plus the stored ones; sources name the stored, identity and default-set routes (any other route is RouteModeSourceUnset).

func ResolvePackageRouteModes added in v4.1.0

func ResolvePackageRouteModes(db *gorm.DB, packageID string, stored map[string]string) (map[string]string, error)

ResolvePackageRouteModes returns a package's effective route modes from its stored route-mode map, for identity group A. Every reader resolves through it (the configuration check directly; the package host's configuration and the route-mode administration through ResolveEffectivePackageRouteModes, which adds the rehearsed defaults), so the rule below holds for any group A route, today's and future ones: identity-platform's group A routes that the stored map does not name are native while identity is authoritative (state identity or finalized), because group A is native exactly then. An installation whose map predates a route added to group A (the cutover wrote only the routes it knew) therefore stays consistent without an operator writing the new key. A route the map names keeps its stored mode, legacy included; in the kernel and importing states a missing route is legacy, as for every other package. The stored map is not changed.

func RevokeCleanAgentTx added in v4.1.0

func RevokeCleanAgentTx(tx *gorm.DB, id uint) error

RevokeCleanAgentTx is RevokeAgent in the caller's transaction: the agent is marked revoked and its split row follows. RevokeCredential runs it in the operation's transaction. An agent that does not exist is gorm.ErrRecordNotFound; one already revoked is revoked again.

func RevokeRegistrationKeyTx added in v4.1.0

func RevokeRegistrationKeyTx(tx *gorm.DB, id uint) (bool, error)

RevokeRegistrationKeyTx deletes a registration key with its split rows. It reports whether a key was deleted; a key already gone is no error.

func RollbackPackageGeneration

func RollbackPackageGeneration(db *gorm.DB, generationID uint, reason string) (*model.PackageRouteGeneration, error)

RollbackPackageGeneration retires the current generation and returns its verified predecessor. Both the reason and any retained backup reference are recorded transactionally; no package-owned table is touched.

func RouteModeActorName added in v4.1.0

func RouteModeActorName(db *gorm.DB, userID uint) string

RouteModeActorName is the audit name of a signed-in administrator: the e-mail, or "user:<id>".

func RunForwardRuntimeJobPayloadScrub added in v4.1.0

func RunForwardRuntimeJobPayloadScrub(ctx context.Context, db *gorm.DB)

RunForwardRuntimeJobPayloadScrub is the start-up pass: it scrubs and logs counts only.

func ScrubForwardRuntimeJobPayload added in v4.1.0

func ScrubForwardRuntimeJobPayload(payload string) (string, bool)

ScrubForwardRuntimeJobPayload removes every credential from a stored job payload: every key IsNodeSecretKey marks, at any depth, is removed with its value, as a new payload omits it. It reports whether anything was removed. A payload that is not JSON is returned as it is. The key is removed, not masked: the placeholder would read as a token present to NodeX and the agents, and a cleared value would be scanned again at the next start.

func ScrubForwardRuntimeJobPayloads added in v4.1.0

func ScrubForwardRuntimeJobPayloads(ctx context.Context, db *gorm.DB, batch int) (int64, error)

ScrubForwardRuntimeJobPayloads rewrites the payloads of the runtime jobs written before NO-7 that still carry a token, in batches, and answers how many rows it changed. It is idempotent: a scrubbed payload no longer matches, so a start after the first pass reads nothing. It runs at start in the singleton worker process (cmd/server), before the job executors serve a row.

func SetPackageRouteDefaultPolicy added in v4.1.0

func SetPackageRouteDefaultPolicy(policy string)

SetPackageRouteDefaultPolicy sets the default policy (package_routes.default_mode) for this process. Anything but legacy means rehearsed, the default.

func SetPackageRouteModesTx added in v4.1.0

func SetPackageRouteModesTx(tx *gorm.DB, publicKey ed25519.PublicKey, installationID uint, modes map[string]string, actorID uint) (*model.PluginConfiguration, error)

SetPackageRouteModesTx sets route modes in a Control installation's configuration inside tx, keeping the rest of the document. Legacy removes the route from the map, except for a route of the rehearsed default set (config/package-route-defaults.json), which keeps an explicit legacy so the default does not take it back. It fails with ErrPluginConfigurationConflict if the document changes concurrently.

func SetPluginRuntime added in v4.1.0

func SetPluginRuntime(ctx context.Context, db *gorm.DB, pluginID, runtime string, remoteEnabled bool, actorID uint) (*model.PluginRuntime, error)

SetPluginRuntime selects a package's runtime. It takes effect at the package's next lifecycle operation. Remote requires the module runtime and PostgreSQL, because module instances cannot share a SQLite file.

func SettingsCapability added in v4.1.0

func SettingsCapability(namespace, access string) string

SettingsCapability is the capability for access to namespace.

func SettingsGeneration added in v4.1.0

func SettingsGeneration(namespace string) uint64

SettingsGeneration returns the namespace's current generation.

func ShadowSampleRoute added in v4.1.0

func ShadowSampleRoute(routeID string) (shadowsamples.RouteInfo, bool)

ShadowSampleRoute looks a route up in the package extraction map for the shadow sample collector: the package that owns it and its path template.

func StorePluginArtifact

func StorePluginArtifact(db *gorm.DB, releaseID uint, artifact []byte) (*model.PluginArtifact, error)

func StringifyConfigMap

func StringifyConfigMap(m map[string]any) map[string]string

StringifyConfigMap 把 map[string]any 转成 map[string]string (proto 的 map<string,string> 字段需要)。数字按整数/浮点合理格式化, 避免 443 变 "443.000000"。

func SubscriptionGroupDeleteRequestID added in v4.1.0

func SubscriptionGroupDeleteRequestID(groupID uint, token string) string

SubscriptionGroupDeleteRequestID names the removal of a deleted group's members, as SubscriptionGroupGrantRequestID names a grant (packages/subscription/native.DeleteGroupRequestID).

func SubscriptionGroupGrantRequestID added in v4.1.0

func SubscriptionGroupGrantRequestID(userID, groupID uint, expireAt, transferEnable, nextRenewPrice *int64, token string) string

SubscriptionGroupGrantRequestID names an administrator's grant of a subscription group to a user in the subscriber request ledger (v4_kernel_subscriber_request), so a retried request is applied once. token identifies the HTTP request: its Idempotency-Key, else its request id. The digest also covers the granted fields, so a key reused for other values is another grant. The subscription package's native route derives the same id (packages/subscription/native.GrantRequestID), so a retry is recognized whichever side serves it.

func SubscriptionGroupRevokeRequestID added in v4.1.0

func SubscriptionGroupRevokeRequestID(userID, groupID uint, token string) string

SubscriptionGroupRevokeRequestID names an administrator's removal of a user's subscription group, as SubscriptionGroupGrantRequestID names a grant (packages/subscription/native.RevokeRequestID).

func SubscriptionResetRateLimitKey added in v4.1.0

func SubscriptionResetRateLimitKey(userID uint) string

SubscriptionResetRateLimitKey is a user's key in the reset limiter.

func SyncAgentInstallationAssignments

func SyncAgentInstallationAssignments(tx *gorm.DB, installation model.PluginInstallation, configRevision int64, queue bool, now time.Time) ([]*model.KernelOperation, error)

SyncAgentInstallationAssignments updates every matching assignment revision and aggregate generation in the caller's configuration transaction.

func SyncNodePluginLifecycle

func SyncNodePluginLifecycle(tx *gorm.DB, nodeID uint, pluginID string, explicitRetry bool, _ time.Time) (*model.NodePluginLifecycle, bool, error)

SyncNodePluginLifecycle serializes every role of one node/plugin pair, computes aggregate desired state and optionally rearms an exhausted chain after an explicit administrator mutation.

func SystemConfigAuditContent added in v4.1.0

func SystemConfigAuditContent(cfg *model.SystemConfig, preserveExisting bool) string

SystemConfigAuditContent is the content of a system configuration audit entry.

func SystemConfigMaskedFields added in v4.1.0

func SystemConfigMaskedFields(key string) []string

SystemConfigMaskedFields returns the masked fields of key's value, none for most keys.

func TouchSettingsKey added in v4.1.0

func TouchSettingsKey(key string)

TouchSettingsKey bumps the generation of key's namespace, if any.

func UpdatePluginConfigurationWithValidatorAndHook

func UpdatePluginConfigurationWithValidatorAndHook(db *gorm.DB, publicKey ed25519.PublicKey, installationID uint, rawConfig string, expectedRevision *int64, actorID uint, validator PluginConfigurationSemanticValidator, hook PluginConfigurationTxHook) (*model.PluginConfiguration, error)

func UpdateUserOnceTx added in v4.1.0

func UpdateUserOnceTx(tx *gorm.DB, requestID, method string, userID uint, updates map[string]any, now time.Time) (bool, *authn.Revocation, error)

UpdateUserOnceTx applies updates to an existing user through UpdateUserTx once per request id, recording it in the subscriber request ledger: a repeated id changes nothing and reports applied false. A user that does not exist is ErrUserNotFound, and nothing is recorded. The revocation, if any, is for authn.Remember after the commit. An empty request id applies every time.

func UpdateUserTx added in v4.1.0

func UpdateUserTx(tx *gorm.DB, id uint, updates map[string]any) (*authn.Revocation, error)

UpdateUserTx updates a user in tx. When the change ends the user's sessions it also records the revocation and returns it; apply it with authn.Remember after the commit.

func UpdateUserTxWithTokenVersion added in v4.1.0

func UpdateUserTxWithTokenVersion(tx *gorm.DB, id uint, updates map[string]any, tokenVersion uint64) (*authn.Revocation, error)

UpdateUserTxWithTokenVersion is UpdateUserTx for a change identity made: tokenVersion, when positive, is the account's new token version, so the revocation ends identity tokens by version rather than by time.

func UserAliveCounts added in v4.2.0

func UserAliveCounts() (map[uint64]int, error)

UserAliveCounts counts each user's online IPs across all nodes: the sets UpdateOnlineStatus keeps per node and user (online:<type>:<node>:<user>), added up per user. Users without any are left out. It is the source of UniProxy alivelist and of the Agent Control stream's AliveList (alive.v1).

func UserSubscriptionResetRequestID added in v4.1.0

func UserSubscriptionResetRequestID(userID uint, token string) string

UserSubscriptionResetRequestID names a user's reset of their own subscription token in the subscriber request ledger, so a retried request is applied once: identity.user_reset_subscribe:<user>:<digest>, a digest of the request's Idempotency-Key (else its request id). The identity package's native handler derives the same id.

func ValidateAgentDiagnosticTask

func ValidateAgentDiagnosticTask(action string, params map[string]any) (map[string]any, error)

ValidateAgentDiagnosticTask 校验 action 是否在白名单内,并将 params 归一化为 仅包含该 action 允许的字段(多余字段被丢弃,数值做范围裁剪)。 返回的 error 可直接展示给管理员。

func ValidateForwardDiagnosticCheck added in v4.2.0

func ValidateForwardDiagnosticCheck(action string, params map[string]any) (map[string]any, error)

ValidateForwardDiagnosticCheck checks a forward check's parameters and answers them normalized: only the check's parameters, hop_index and generation as integers, timeout_ms clamped (ForwardCheckDefaultTimeoutMS when absent), target_policy public_only when absent.

func ValidateNodeProtocol

func ValidateNodeProtocol(protocol *model.NodeProtocol) error

ValidateNodeProtocol validates the fields that affect node runtime. The existing protocol API accepts JSON strings for protocol-specific settings, so validation belongs in the service layer and covers both HTTP and gRPC callers that persist protocols.

func ValidatePluginAPIGrantPermissions

func ValidatePluginAPIGrantPermissions(pluginID, raw string) error

ValidatePluginAPIGrantPermissions rejects malformed or cross-plugin grants before they enter the kernel authorization table.

func ValidatePluginDependencyExecutionPlan

func ValidatePluginDependencyExecutionPlan(db *gorm.DB, plan *PluginDependencyExecutionPlan) error

ValidatePluginDependencyExecutionPlan verifies every selected artifact and validates conflicts against already-enabled packages outside the closure. Callers hold PluginTargetLock while invoking it and applying the plan.

func ValidatePluginInstallationPlan

func ValidatePluginInstallationPlan(db *gorm.DB, release model.PluginRelease, target string, enabled bool, installationID uint) error

func ValidatePluginReleaseTarget

func ValidatePluginReleaseTarget(release model.PluginRelease, target string) error

func ValidateRegistrationEmail

func ValidateRegistrationEmail(email string, policy RegistrationPolicy) error

func ValidateTrafficDelta

func ValidateTrafficDelta(upload, download int64) error

func ValidateWireGuardRuntimeConfig

func ValidateWireGuardRuntimeConfig(config map[string]any) error

ValidateWireGuardRuntimeConfig validates a raw node config after it has been decoded from JSON. Raw configs bypass NodeProtocol persistence, so they need the same runtime guard before UniProxy returns them to a node.

func VerifyPluginArtifact

func VerifyPluginArtifact(manifest PluginManifest, artifact []byte) error

VerifyPluginArtifact checks the bytes fetched from the signed repository before they are handed to an installer. The manifest signature alone is not sufficient when an artifact is replaced at the transport layer.

func WithAgentLifecycleTransaction

func WithAgentLifecycleTransaction(db *gorm.DB, mutation func(*gorm.DB) error) error

WithAgentLifecycleTransaction retries only SQLite writer-contention errors. PostgreSQL locking and all semantic errors are returned immediately.

func WithRetryableTransaction

func WithRetryableTransaction(db *gorm.DB, mutation func(*gorm.DB) error) error

WithRetryableTransaction reruns the whole transaction only when SQLite reports transient writer contention. The mutation may run more than once, so it must keep side effects inside the transaction. Other databases and semantic failures return immediately.

func WithdrawDebitRequestID added in v4.1.0

func WithdrawDebitRequestID(withdrawID uint) string

WithdrawDebitRequestID names a withdrawal's debit of the commission balance in the subscriber request ledger (v4_kernel_subscriber_request). The affiliate package's native route uses the same id.

func WithdrawRefundCents added in v4.1.0

func WithdrawRefundCents(amount float64) (int64, bool)

WithdrawRefundCents is what rejecting a withdrawal returns to the commission balance. Withdrawals are whole amounts; one stored with a fraction, possible before they had to be, is refunded its whole part, which is what its debit took on PostgreSQL: the driver truncated the amount bound to "commission_balance - ?".

func WithdrawRefundRequestID added in v4.1.0

func WithdrawRefundRequestID(withdrawID uint) string

WithdrawRefundRequestID names the refund of a rejected withdrawal, so a withdrawal is refunded once.

Types

type ActorPluginAccess

type ActorPluginAccess struct {
	Unrestricted      bool
	LegacyAdmin       bool
	Permissions       []string
	RestrictedPlugins []string
	// contains filtered or unexported fields
}

ActorPluginAccess is the kernel-authoritative plugin permission view for one actor. A legacy administrator remains unrestricted for plugins that have no explicit grant, while each plugin with at least one grant is evaluated fail-closed. This permits incremental migration of plugin permissions.

func NewFailClosedActorPluginAccess

func NewFailClosedActorPluginAccess() *ActorPluginAccess

NewFailClosedActorPluginAccess is used when authentication has already committed but the optional plugin authorization lookup is temporarily unavailable. It never grants access and lets the caller return a valid session that can retry its profile fetch.

func ResolveActorPluginAccess

func ResolveActorPluginAccess(db *gorm.DB, actorID uint, legacyAdmin bool) (*ActorPluginAccess, error)

ResolveActorPluginAccess reads enabled access-group memberships and their plugin_api grants. Permission arrays and truthy object entries are unioned, de-duplicated and sorted for deterministic API responses.

func (ActorPluginAccess) Allows

func (access ActorPluginAccess) Allows(pluginID, permission string) bool

func (ActorPluginAccess) HasAnyPermission

func (access ActorPluginAccess) HasAnyPermission(pluginID string) bool

func (ActorPluginAccess) PermissionMode

func (access ActorPluginAccess) PermissionMode() string

func (ActorPluginAccess) ProfilePermissions

func (access ActorPluginAccess) ProfilePermissions() []string

func (ActorPluginAccess) ProfileRestrictedPluginList

func (access ActorPluginAccess) ProfileRestrictedPluginList() []string

ProfileRestrictedPluginList intentionally hides the catalog of configured plugin grants from non-administrators. They still receive their effective permissions, but not a cross-tenant enumeration of restricted plugins.

func (ActorPluginAccess) RestrictedPluginList

func (access ActorPluginAccess) RestrictedPluginList() []string

type AgentAssignmentOperationChain

type AgentAssignmentOperationChain struct {
	Install *model.KernelOperation
	Update  *model.KernelOperation
	Enable  *model.KernelOperation
	Disable *model.KernelOperation
}

func QueueAgentAssignmentLifecycle

func QueueAgentAssignmentLifecycle(tx *gorm.DB, assignment model.NodeServiceAssignment, now time.Time) (*AgentAssignmentOperationChain, error)

QueueAgentAssignmentLifecycle remains the focused entry point used by tests and callers that have already mutated one assignment transactionally.

func QueueNodePluginLifecycle

func QueueNodePluginLifecycle(tx *gorm.DB, lifecycle *model.NodePluginLifecycle, now time.Time) (*AgentAssignmentOperationChain, error)

QueueNodePluginLifecycle reuses a valid current chain before touching signed package metadata. Failed chains receive bounded retry epochs with backoff; successful or in-flight chains are never duplicated on periodic reconcile.

type AgentDiagnosticActionSpec

type AgentDiagnosticActionSpec struct {
	Params   []string
	Mutating bool
}

AgentDiagnosticActionSpec 描述一个白名单诊断动作允许携带哪些参数,以及是否为变更类操作。

type AgentDiagnosticTaskService

type AgentDiagnosticTaskService struct {
	// contains filtered or unexported fields
}

AgentDiagnosticTaskService 负责白名单诊断任务的持久化,取代 AgentHandler 里原来的 taskResults sync.Map,使任务结果在面板进程重启后仍可查询。

func NewAgentDiagnosticTaskService

func NewAgentDiagnosticTaskService(db *gorm.DB) *AgentDiagnosticTaskService

func (*AgentDiagnosticTaskService) CompleteTask

func (s *AgentDiagnosticTaskService) CompleteTask(taskID string, nodeID uint, action string, success bool, output string, errMsg string, durationMS int64) error

CompleteTask 写入任务的最终执行结果。如果 task_id 尚无记录(例如 agent 直接 上报、面板端没有先建任务),则补建一条 completed/failed 记录,保持幂等可查询。

nodeID is the authenticated node that reports: it completes only its own tasks, and a task of another node is refused with ErrAgentDiagnosticTaskOfAnotherNode instead of being taken over.

func (*AgentDiagnosticTaskService) CreateTask

func (s *AgentDiagnosticTaskService) CreateTask(taskID string, nodeID uint, action string, params map[string]any) (*model.AgentDiagnosticTask, error)

CreateTask 插入一条 pending 状态的诊断任务记录,params 必须已经过 ValidateAgentDiagnosticTask 归一化。

func (*AgentDiagnosticTaskService) GetTask

GetTask 按 task_id 查询单条任务。

func (*AgentDiagnosticTaskService) ListTasks

func (s *AgentDiagnosticTaskService) ListTasks(nodeID uint, limit int) ([]model.AgentDiagnosticTask, error)

ListTasks 按可选的 node_id 过滤,返回最近的诊断任务历史(供管理页任务历史表使用)。

func (*AgentDiagnosticTaskService) MarkStatus

func (s *AgentDiagnosticTaskService) MarkStatus(taskID string, status string) error

MarkStatus 更新任务的投递状态(如 dispatched),不改变最终结果字段。

func (*AgentDiagnosticTaskService) PullPendingTasks

func (s *AgentDiagnosticTaskService) PullPendingTasks(nodeID uint) ([]model.AgentDiagnosticTask, error)

PullPendingTasks 取出该节点尚未下发 (status=pending) 的诊断任务,并原子标记为 dispatched,供 HTTP 轮询降级路径 (AgentGetTasks) 使用,避免并发重复下发。

type AgentNodeStatus added in v4.1.0

type AgentNodeStatus struct {
	CPUUsage       float64
	MemoryUsage    float64
	DiskUsage      float64
	Uptime         int64
	RuntimeHealthy bool
	RuntimeError   string
}

AgentNodeStatus is a NodeStatus from the Agent Control stream (reports.v1).

type AgentPluginArtifactAddress

type AgentPluginArtifactAddress struct {
	URL    string `json:"url"`
	SHA256 string `json:"sha256"`
	Size   int64  `json:"size"`
}

type AgentPluginInstallConfig

type AgentPluginInstallConfig struct {
	APIVersion string                     `json:"api_version"`
	PluginID   string                     `json:"plugin_id"`
	Version    string                     `json:"version"`
	Artifact   AgentPluginArtifactAddress `json:"artifact"`
	Manifest   AgentPluginManifestAddress `json:"manifest"`
}

type AgentPluginManifestAddress

type AgentPluginManifestAddress struct {
	URL        string `json:"url"`
	SHA256     string `json:"sha256"`
	Size       int64  `json:"size"`
	Signature  string `json:"signature"`
	Publisher  string `json:"publisher"`
	KeyID      string `json:"key_id"`
	APIVersion string `json:"api_version"`
}

type AgentPluginReleaseDownload

type AgentPluginReleaseDownload struct {
	Release      model.PluginRelease
	Manifest     PluginManifest
	ManifestData []byte
	Artifact     model.PluginArtifact
	ManifestHash string
}

AgentPluginReleaseDownload is verified immediately before a node-facing response is written. Data is intentionally kept out of JSON response types.

func LoadAgentPluginArtifactBlob

func LoadAgentPluginArtifactBlob(db *gorm.DB, metadata *AgentPluginReleaseDownload) (*AgentPluginReleaseDownload, error)

LoadAgentPluginArtifactBlob is intentionally separate from authorization and content-address validation. Node-facing handlers call it only after the bounded metadata path has accepted the exact immutable address.

func LoadAuthorizedAgentPluginMetadata

func LoadAuthorizedAgentPluginMetadata(db *gorm.DB, nodeID uint, pluginID, version string) (*AgentPluginReleaseDownload, error)

LoadAuthorizedAgentPluginMetadata verifies assignment, global installation, official signature and persisted size metadata without loading the package blob. Reconcile and manifest requests must stay on this bounded path.

func (*AgentPluginReleaseDownload) ValidateArtifactAddress

func (download *AgentPluginReleaseDownload) ValidateArtifactAddress(sha256Hex string, size int64) error

func (*AgentPluginReleaseDownload) ValidateManifestAddress

func (download *AgentPluginReleaseDownload) ValidateManifestAddress(sha256Hex string, size int64) error

type AuthService

type AuthService struct{}

func NewAuthService

func NewAuthService() *AuthService

func (*AuthService) Authenticate

func (s *AuthService) Authenticate(email, password string) (*model.User, error)

Authenticate validates login credentials and account state without issuing a token.

func (*AuthService) IssueToken

func (s *AuthService) IssueToken(user *model.User, cfg *config.Config) (string, error)

IssueToken signs a JWT for an already authenticated user.

func (*AuthService) RegisterWithInvite

func (s *AuthService) RegisterWithInvite(email, password, inviteCode string, cfg *config.Config) (string, *model.User, error)

RegisterWithInvite creates a user and optionally consumes an invite code.

type BackupService

type BackupService struct {
	// contains filtered or unexported fields
}

BackupService runs backups. It keeps the backup configuration in memory and reloads it when the backup settings namespace changed (SettingsGeneration), so a change made through KernelSettings or another BackupService reaches it before its next backup.

func NewBackupService

func NewBackupService(db *gorm.DB) *BackupService

func (*BackupService) CleanupOldBackups

func (s *BackupService) CleanupOldBackups() error

func (*BackupService) CreateBackup

func (s *BackupService) CreateBackup(backupType string, createdBy *uint) (*model.BackupRecord, error)

func (*BackupService) DeleteBackup

func (s *BackupService) DeleteBackup(id uint) error

func (*BackupService) GetBackupStats

func (s *BackupService) GetBackupStats() (map[string]any, error)

func (*BackupService) GetConfig

func (s *BackupService) GetConfig() (*model.BackupConfig, error)

GetConfig returns the backup configuration: the copy in memory while the backup settings namespace has not changed since it was loaded, else the first row, created with the defaults when there is none.

func (*BackupService) ListBackups

func (s *BackupService) ListBackups(page, pageSize int) ([]model.BackupRecord, int64, error)

func (*BackupService) RestoreBackup

func (s *BackupService) RestoreBackup(id uint) error

func (*BackupService) UpdateConfig

func (s *BackupService) UpdateConfig(cfg *model.BackupConfig) error

UpdateConfig saves cfg and makes it the configuration in memory. Every other copy of the backup settings reloads.

type CreateOrderParams

type CreateOrderParams struct {
	UserID   uint
	PlanID   uint
	Period   string // month, quarter, half_year, year, two_year, three_year, onetime
	CouponID *uint
}

CreateOrderParams 创建订单参数

type CreateRuleRequest

type CreateRuleRequest struct {
	Name         string     `json:"name"`
	RelayNodeID  uint       `json:"relay_node_id"`
	ExitNodeID   uint       `json:"exit_node_id"`
	Protocol     string     `json:"protocol"`
	TargetHost   string     `json:"target_host"`
	TargetPort   int        `json:"target_port"`
	SpeedLimit   *int64     `json:"speed_limit"`
	TrafficLimit *int64     `json:"traffic_limit"`
	ExpireTime   *time.Time `json:"expire_time"`
}

CreateRuleRequest is the request payload for user-owned rule creation.

type DashboardStats

type DashboardStats struct {
	// 用户统计
	TotalUsers    int64 `json:"total_users"`
	ActiveUsers   int64 `json:"active_users"`
	ExpiredUsers  int64 `json:"expired_users"`
	BannedUsers   int64 `json:"banned_users"`
	TodayNewUsers int64 `json:"today_new_users"`

	// 订单统计
	TotalOrders   int64 `json:"total_orders"`
	PendingOrders int64 `json:"pending_orders"`
	PaidOrders    int64 `json:"paid_orders"`
	TotalRevenue  int64 `json:"total_revenue"`  // 总收入 (分)
	MonthlyIncome int64 `json:"monthly_income"` // 月收入 (分)
	TodayIncome   int64 `json:"today_income"`   // 今日收入 (分)

	// 节点统计
	TotalNodes  int64 `json:"total_nodes"`
	ActiveNodes int64 `json:"active_nodes"`
	OnlineUsers int64 `json:"online_users"`

	// 流量统计
	TotalTrafficUsed int64 `json:"total_traffic_used"` // 总已用流量 (字节)
	TodayTraffic     int64 `json:"today_traffic"`      // 今日流量 (字节)

	// 缓存信息
	CachedAt time.Time `json:"cached_at"`
}

DashboardStats 仪表盘统计数据

type DiagnosisOutcome

type DiagnosisOutcome struct {
	Success     bool    `json:"success"`
	Description string  `json:"description"`
	NodeName    string  `json:"nodeName"`
	NodeID      string  `json:"nodeId"`
	TargetIP    string  `json:"targetIp"`
	TargetPort  int     `json:"targetPort,omitempty"`
	Message     string  `json:"message,omitempty"`
	AverageTime float64 `json:"averageTime,omitempty"`
	PacketLoss  float64 `json:"packetLoss,omitempty"`
}

type DiagnosisProbes added in v4.1.0

type DiagnosisProbes struct {
	// Dial opens a probe's TCP connection within timeout.
	Dial func(ctx context.Context, network, address string, timeout time.Duration) (net.Conn, error)
	// Lookup resolves the host name of a target a user chose.
	Lookup func(ctx context.Context, host string) ([]netip.Addr, error)
	// Concurrency bounds the probes in flight; DefaultDiagnosisConcurrency
	// when zero.
	Concurrency int
}

DiagnosisProbes is how a diagnosis reaches the network. The zero value dials with net.Dialer and resolves names with the system resolver, as the legacy routes do; tests replace both.

func (DiagnosisProbes) ProbeEndpoint added in v4.1.0

func (p DiagnosisProbes) ProbeEndpoint(ctx context.Context, node *model.ForwardNode) (*HealthCheckResult, error)

ProbeEndpoint dials node's service port (host:port), as the forward node check does: online with the latency when it connects, offline with the dial's error when it does not. A check whose ctx ended is no check: ctx's error is returned.

func (DiagnosisProbes) PublicHost added in v4.1.0

func (p DiagnosisProbes) PublicHost(ctx context.Context, host string) (netip.Addr, string)

PublicHost resolves the host of a probe Control runs for a user and answers the address to connect to, or the refusal (publicProbeAddress): loopback, private and other non-public addresses are refused.

type DiagnosisReport

type DiagnosisReport struct {
	ForwardName string             `json:"forwardName"`
	Timestamp   int64              `json:"timestamp"`
	Results     []DiagnosisOutcome `json:"results"`
}

type EffectiveAccess

type EffectiveAccess struct {
	ScopeID string                `json:"scope_id"`
	Groups  []model.AccessGroup   `json:"groups"`
	Grants  []model.ResourceGrant `json:"grants"`
	Quotas  []model.QuotaPolicy   `json:"quotas"`
}

func ResolveEffectiveAccess

func ResolveEffectiveAccess(db *gorm.DB, userID uint, planID *uint, scopeID string) (*EffectiveAccess, error)

ResolveEffectiveAccess returns the allow-union of direct and plan-derived memberships for one scope. Calling it once per scope keeps counters and quota policy ownership isolated.

type EndpointCheck added in v4.1.0

type EndpointCheck struct {
	NodeID uint
	// Node is the node's row; nil when the node does not exist.
	Node *model.ForwardNode
	// Result is the check's result; nil when it did not run or failed.
	Result *HealthCheckResult
	// Err is why there is no result: gorm.ErrRecordNotFound for a node that
	// does not exist, or the check's error.
	Err error
}

EndpointCheck is one node's check in CheckEndpoints.

type ForwardAgentBridgeService

type ForwardAgentBridgeService struct {
	// contains filtered or unexported fields
}

ForwardAgentBridgeService handles result write-back for clean_agent bridge jobs. It is shared by the agent result handler so that job/forward state transitions stay identical to the local ansible executor semantics.

func NewForwardAgentBridgeService

func NewForwardAgentBridgeService(db *gorm.DB) *ForwardAgentBridgeService

func (*ForwardAgentBridgeService) CompleteJob

func (s *ForwardAgentBridgeService) CompleteJob(taskID string, success bool, output, errMsg string) (alreadyDone bool, err error)

CompleteJob writes an agent-reported result back to the runtime job, the forward runtime state, and marks the bridge mapping completed. It is idempotent: a mapping already in a terminal state is a no-op (duplicate/late reports return alreadyDone=true).

func (*ForwardAgentBridgeService) LookupBridgeTask

func (s *ForwardAgentBridgeService) LookupBridgeTask(taskID string) (*model.ForwardAgentBridgeTask, error)

LookupBridgeTask returns the durable bridge mapping for a given agent task_id. Returns (nil, nil) when the task_id is not a bridge task (e.g. an admin memory task).

type ForwardAgentBridgeWorker

type ForwardAgentBridgeWorker struct {
	// contains filtered or unexported fields
}

ForwardAgentBridgeWorker claims pending clean_agent runtime jobs, asks NodeX to translate the panel payload into a legacy AgentTask, and persists the durable task_id ↔ runtime_job mapping so the agent-task dispatch path can hand it to the target agent. It only touches backend='clean_agent'; nftables_ansible stays on the local ansible executor and gost stays on the synchronous NodeX execute path.

func NewForwardAgentBridgeWorker

func NewForwardAgentBridgeWorker(db *gorm.DB) *ForwardAgentBridgeWorker

func (*ForwardAgentBridgeWorker) RunPendingJobs

func (w *ForwardAgentBridgeWorker) RunPendingJobs(ctx context.Context) error

func (*ForwardAgentBridgeWorker) Start

type ForwardAnsibleStatsWorker

type ForwardAnsibleStatsWorker struct {
	// contains filtered or unexported fields
}

ForwardAnsibleStatsWorker 定期为 nftables_ansible/iptables_ansible 后端的转发采集流量统计。

nftables 路径在 forward hook 的记账链里用两个具名计数器统计每个转发: ct direction original 记为 upload(客户端到目标),ct direction reply 记为 download(目标回到客户端),与 gost 路径的 u/d 同义,再经同一套 traffic_ratio / flow 计费换算。具名计数器在重新下发时保留,只在删除转发时清除。

尚未迁移到 inet 表的转发(以及 iptables 路径)只上报 nat 链计数, 那是每条连接首包的字节数,只能当作 upload,download 为 0。两种快照使用 不同的游标键,迁移时不会互相抵消。

func NewForwardAnsibleStatsWorker

func NewForwardAnsibleStatsWorker(db *gorm.DB) *ForwardAnsibleStatsWorker

func (*ForwardAnsibleStatsWorker) RunOnce

func (*ForwardAnsibleStatsWorker) Start

type ForwardCleanAgentAction

type ForwardCleanAgentAction struct {
	JobID        uint            `json:"jobId"`
	Action       string          `json:"action"`
	ResourceType string          `json:"resourceType"`
	ResourceID   *uint           `json:"resourceId"`
	ForwardID    *uint           `json:"forwardId"`
	TunnelID     *uint           `json:"tunnelId"`
	NodeID       *uint           `json:"nodeId"`
	Payload      json.RawMessage `json:"payload"`
}

type ForwardCleanAgentCreateInput

type ForwardCleanAgentCreateInput struct {
	Name   string `json:"name"`
	NodeID *uint  `json:"nodeId"`
}

type ForwardCleanAgentHeartbeatInput

type ForwardCleanAgentHeartbeatInput struct {
	AgentID      uint     `json:"agentId"`
	Token        string   `json:"token"`
	Version      string   `json:"version"`
	Hostname     string   `json:"hostname"`
	OS           string   `json:"os"`
	Arch         string   `json:"arch"`
	Kernel       string   `json:"kernel"`
	PublicIP     string   `json:"publicIp"`
	PrivateIP    string   `json:"privateIp"`
	Capabilities []string `json:"capabilities"`
	Limit        int      `json:"limit"`
}

type ForwardCleanAgentRegisterInput

type ForwardCleanAgentRegisterInput struct {
	Name         string   `json:"name"`
	Token        string   `json:"token"`
	NodeID       *uint    `json:"nodeId"`
	Version      string   `json:"version"`
	Hostname     string   `json:"hostname"`
	OS           string   `json:"os"`
	Arch         string   `json:"arch"`
	Kernel       string   `json:"kernel"`
	PublicIP     string   `json:"publicIp"`
	PrivateIP    string   `json:"privateIp"`
	Capabilities []string `json:"capabilities"`
}

type ForwardCleanAgentReportInput

type ForwardCleanAgentReportInput struct {
	AgentID  uint   `json:"agentId"`
	Token    string `json:"token"`
	JobID    uint   `json:"jobId"`
	Status   int    `json:"status"`
	Success  *bool  `json:"success"`
	Result   string `json:"result"`
	Error    string `json:"error"`
	Upload   int64  `json:"upload"`
	Download int64  `json:"download"`
}

type ForwardCleanAgentService

type ForwardCleanAgentService struct {
	// contains filtered or unexported fields
}

func NewForwardCleanAgentService

func NewForwardCleanAgentService(db *gorm.DB) *ForwardCleanAgentService

func (*ForwardCleanAgentService) Heartbeat

func (*ForwardCleanAgentService) Register

Register marks an agent online and binds its token to a node.

A token is bound to the node it was issued for (CreateToken's nodeId) or, for a token an earlier build issued without one, to the node of its first registration that names one. A heartbeat claims the pending jobs of the agent's node, and their payloads carry the node's API token, so a token that could register again under another node could take that node's jobs and credentials. A registration naming another node is refused (ErrForwardCleanAgentNodeMismatch) and changes nothing; one naming no node keeps the binding. Binding to another node takes a new token.

func (*ForwardCleanAgentService) Report

type ForwardCleanAgentTokenResult

type ForwardCleanAgentTokenResult struct {
	Agent *model.ForwardCleanAgent `json:"agent"`
	Token string                   `json:"token"`
}

func CreateCleanAgentTokenTx added in v4.1.0

func CreateCleanAgentTokenTx(tx *gorm.DB, input ForwardCleanAgentCreateInput) (*ForwardCleanAgentTokenResult, error)

CreateCleanAgentTokenTx is CreateToken in the caller's transaction: the agent row and its split row are written in it, and IssueCleanAgent runs it in the operation's transaction. A token that collides with another (never, in practice) is generated again under a savepoint.

type ForwardFlowResetWorker

type ForwardFlowResetWorker struct {
	// contains filtered or unexported fields
}

func NewForwardFlowResetWorker

func NewForwardFlowResetWorker(db *gorm.DB) *ForwardFlowResetWorker

func (*ForwardFlowResetWorker) RunOnce

func (w *ForwardFlowResetWorker) RunOnce(now time.Time) error

func (*ForwardFlowResetWorker) Start

func (w *ForwardFlowResetWorker) Start(ctx context.Context)

type ForwardGostStatsWorker

type ForwardGostStatsWorker struct {
	// contains filtered or unexported fields
}

func NewForwardGostStatsWorker

func NewForwardGostStatsWorker(db *gorm.DB) *ForwardGostStatsWorker

func (*ForwardGostStatsWorker) RunOnce

func (w *ForwardGostStatsWorker) RunOnce(ctx context.Context) error

func (*ForwardGostStatsWorker) Start

func (w *ForwardGostStatsWorker) Start(ctx context.Context)

type ForwardLatencyProber

type ForwardLatencyProber struct {
	// contains filtered or unexported fields
}

ForwardLatencyProber periodically TCPing-probes forward/tunnel/node targets and stores one pre-aggregated bucket row per (target, time-bucket).

func NewForwardLatencyProber

func NewForwardLatencyProber(db *gorm.DB) *ForwardLatencyProber

func (*ForwardLatencyProber) RunOnce

func (w *ForwardLatencyProber) RunOnce(ctx context.Context) error

RunOnce executes a single probe cycle. Exposed for tests.

func (*ForwardLatencyProber) Start

func (w *ForwardLatencyProber) Start(ctx context.Context)

type ForwardNodeService

type ForwardNodeService struct {
	// contains filtered or unexported fields
}

ForwardNodeService 转发节点服务

func NewForwardNodeService

func NewForwardNodeService(db *gorm.DB) *ForwardNodeService

NewForwardNodeService 创建服务

func (*ForwardNodeService) CheckEndpoint added in v4.1.0

func (s *ForwardNodeService) CheckEndpoint(ctx context.Context, probes DiagnosisProbes, nodeID uint, record bool) (*HealthCheckResult, error)

CheckEndpoint checks one node's endpoint from Control and, with record, writes what it found to the node's row.

func (*ForwardNodeService) CheckEndpoints added in v4.1.0

func (s *ForwardNodeService) CheckEndpoints(ctx context.Context, probes DiagnosisProbes, nodeIDs []uint, record bool) ([]EndpointCheck, error)

CheckEndpoints checks the nodes' endpoints from Control, at most probes.Concurrency at once, and with record writes each result to its node's row. When ctx ends it returns the checks that completed and ctx's error, and records nothing. Another error is a failure to load the nodes.

func (*ForwardNodeService) Create

func (s *ForwardNodeService) Create(node *model.ForwardNode) error

Create 创建节点. The node's token is written to the split tables in the same transaction (nodesecrets.Sync), as by Update and Delete.

func (*ForwardNodeService) Delete

func (s *ForwardNodeService) Delete(id uint) error

Delete 删除节点 and revoke its agent certificates. The kernel's part, the token's split row and the certificates, is RetireForwardNodeTx, which RetireNode runs too; the node row goes with it in the same transaction.

func (*ForwardNodeService) GenerateAPIToken

func (s *ForwardNodeService) GenerateAPIToken() (string, error)

GenerateAPIToken 生成API Token

func (*ForwardNodeService) GetByID

func (s *ForwardNodeService) GetByID(id uint) (*model.ForwardNode, error)

GetByID 根据ID获取节点

func (*ForwardNodeService) GetByIDForInventoryScope

func (s *ForwardNodeService) GetByIDForInventoryScope(id uint, scope string) (*model.ForwardNode, error)

func (*ForwardNodeService) GetByType

func (s *ForwardNodeService) GetByType(nodeType string) ([]*model.ForwardNode, error)

GetByType 根据类型获取节点列表

func (*ForwardNodeService) HealthCheck

func (s *ForwardNodeService) HealthCheck(ctx context.Context, nodeID uint) (*HealthCheckResult, error)

HealthCheck 健康检查: the forward node check, from Control, recording what it found (CheckEndpoint).

func (*ForwardNodeService) ListByInventoryScope

func (s *ForwardNodeService) ListByInventoryScope(scope, nodeType string, status *int, page, pageSize int) ([]*model.ForwardNode, int64, error)

func (*ForwardNodeService) RecordHealthCheck added in v4.1.0

func (s *ForwardNodeService) RecordHealthCheck(node *model.ForwardNode, result *HealthCheckResult) error

RecordHealthCheck writes a check's result to the node's row, as the forward node check does: status, last check and latency, and the uptime when the node is online.

func (*ForwardNodeService) Update

func (s *ForwardNodeService) Update(node *model.ForwardNode) error

Update 更新节点. A changed token or a disabled node revokes the node's agent certificates.

type ForwardNodeTraffic added in v4.1.0

type ForwardNodeTraffic struct {
	// Ansible is set for an Ansible machine: it exposes no gost metrics, so
	// its panel-side counters are kept.
	Ansible bool
	// Services are a gost node's services' totals from its metrics
	// endpoint.
	Services map[string]*gost.ServiceTrafficTotals
	// CurrentConn, TotalUpload and TotalDownload are an Ansible machine's
	// counters.
	CurrentConn   int
	TotalUpload   int64
	TotalDownload int64
}

ForwardNodeTraffic is what the statistics sync reads from a forward node.

func CollectForwardNodeTraffic added in v4.1.0

func CollectForwardNodeTraffic(ctx context.Context, manager *gost.Manager, node *model.ForwardNode, ansible bool) (*ForwardNodeTraffic, error)

CollectForwardNodeTraffic reads node's traffic as the statistics sync does: an Ansible machine's panel-side counters, or every gost service's totals from the node's Prometheus metrics endpoint, through manager.

type ForwardObservabilityService

type ForwardObservabilityService struct {
	// contains filtered or unexported fields
}

ForwardObservabilityService serves read-only latency/topology queries backed by the v2_forward_latency_bucket time-series written by ForwardLatencyProber.

func NewForwardObservabilityService

func NewForwardObservabilityService(db *gorm.DB) *ForwardObservabilityService

func (*ForwardObservabilityService) GetMultiIngressLatency

func (s *ForwardObservabilityService) GetMultiIngressLatency(forwardID uint) ([]MultiIngressRow, error)

GetMultiIngressLatency lists, for the forward's exit target, the latest ingress latency of every tunnel reaching the same exit node (incl. the forward's own tunnel).

type ForwardRuleService

type ForwardRuleService struct {
	// contains filtered or unexported fields
}

ForwardRuleService handles forward rule persistence and runtime sync.

func NewForwardRuleService

func NewForwardRuleService(db *gorm.DB, nodeService *ForwardNodeService) *ForwardRuleService

NewForwardRuleService preserves the existing constructor shape and uses the default runtime provider.

func NewForwardRuleServiceWithProvider

func NewForwardRuleServiceWithProvider(db *gorm.DB, nodeService *ForwardNodeService, provider ForwardRuntimeProvider) *ForwardRuleService

NewForwardRuleServiceWithProvider allows future runtime backends to be injected explicitly.

func (*ForwardRuleService) ApplyRuntime added in v4.1.0

func (s *ForwardRuleService) ApplyRuntime(ctx context.Context, rule *model.ForwardRule, action string) error

ApplyRuntime pushes a rule to its relay and exit nodes through the runtime provider (NodeX): create, update or sync apply the row as it is, delete removes it. The legacy routes and the KernelNodeOps forward.legacy_rule executor run it alike; the row is the caller's.

func (*ForwardRuleService) GetByID

func (s *ForwardRuleService) GetByID(id uint) (*model.ForwardRule, error)

GetByID fetches a rule by ID.

func (*ForwardRuleService) GetUserRules

func (s *ForwardRuleService) GetUserRules(userID uint) ([]*model.ForwardRule, error)

GetUserRules fetches rules owned by a user, for the user. Their relay and exit nodes are shown without the nodes' API tokens: a node's token authenticates its agent (verifyForwardNodeToken), so a user who saw it could act as the node.

type ForwardRuntimeOutcome added in v4.1.0

type ForwardRuntimeOutcome struct {
	Backend string
	Status  int
	Message string
	Async   bool
	JobID   uint
}

ForwardRuntimeOutcome is what applying a forward on its node reported: the backend in force, the runtime status and message the forward's row records, whether the work was queued (a job backend), and the runtime job row the change recorded.

type ForwardRuntimeProvider

type ForwardRuntimeProvider interface {
	CreateForwardRule(ctx context.Context, rule *model.ForwardRule) error
	UpdateForwardRule(ctx context.Context, rule *model.ForwardRule) error
	DeleteForwardRule(ctx context.Context, rule *model.ForwardRule) error
	SyncForwardRule(ctx context.Context, rule *model.ForwardRule) error
}

ForwardRuntimeProvider abstracts the runtime backend used by forward rule sync.

func NewForwardRuntimeProvider

func NewForwardRuntimeProvider(db *gorm.DB) ForwardRuntimeProvider

NewForwardRuntimeProvider returns the default runtime provider.

type GroupMember added in v4.2.0

type GroupMember struct {
	UserID uint   `json:"user_id"`
	Email  string `json:"email"`
	Banned int    `json:"banned"`
	PlanID *uint  `json:"plan_id"`
	// ExpireAt is when the membership ends (Unix seconds), null for never.
	ExpireAt *int64 `json:"expire_at"`
	// TransferEnable overrides the user's traffic quota (bytes) while the
	// membership lasts, null for no override.
	TransferEnable *int64 `json:"transfer_enable"`
	// NextRenewPrice is the renewal price in cents, null for none.
	NextRenewPrice *int64 `json:"next_renew_price"`
	// CreatedAt is when the membership was granted.
	CreatedAt time.Time `json:"created_at"`
	// Active is whether the membership has not expired.
	Active bool `json:"active"`
}

GroupMember is a user the administrator granted the subscription group directly (a v2_user_subscription_group row, what POST /admin/subscription/users/:user_id/groups creates), with the grant's own fields and the user's e-mail, ban flag and plan. It carries no credential: never the subscription token or proxy uuid. The users the group reaches through their plan or their primary group (v2_user.group_id) are not members here, as the stats' user_count does not count them.

type GroupMembersParams added in v4.2.0

type GroupMembersParams struct {
	Page     int
	PageSize int
	// Email, when set, keeps the members whose e-mail contains it; % and _
	// match themselves.
	Email string
	// Status, when set, keeps the members whose membership is "active" (no
	// expiry, or one in the future: the stats' enabled users) or "expired".
	Status string
}

GroupMembersParams filters and pages a subscription group's members.

type GroupMembersResult added in v4.2.0

type GroupMembersResult struct {
	Total    int64         `json:"total"`
	Page     int           `json:"page"`
	PageSize int           `json:"page_size"`
	Members  []GroupMember `json:"members"`
}

GroupMembersResult is a page of members and how many match in all.

type GroupStats

type GroupStats struct {
	GroupID       uint   `json:"group_id"`
	GroupName     string `json:"group_name"`
	UserCount     int64  `json:"user_count"`
	TemplateCount int64  `json:"template_count"`
	ProtocolCount int64  `json:"protocol_count"`
	OnlineNodes   int64  `json:"online_nodes"`
	TotalTraffic  int64  `json:"total_traffic"` // 用户已用流量总和 (bytes)
	EnabledUsers  int64  `json:"enabled_users"` // 未过期用户数
	PlanCount     int64  `json:"plan_count"`    // 关联套餐数
}

GroupStats 订阅分组统计数据

type HealthCheckResult

type HealthCheckResult struct {
	NodeID    uint      `json:"node_id"`
	Status    int       `json:"status"`
	Latency   int64     `json:"latency"`
	CheckTime time.Time `json:"check_time"`
	Error     string    `json:"error,omitempty"`
}

HealthCheckResult 健康检查结果

type HourlyTraffic

type HourlyTraffic struct {
	HourTs  int64 `json:"hour_ts"` // 该小时起始的 Unix 秒 (整点)
	Traffic int64 `json:"traffic"` // 该小时流量 (字节, 已按倍率计)
}

HourlyTraffic 单个小时的流量统计

type InviteCodeListQuery added in v4.1.0

type InviteCodeListQuery struct {
	Status   string
	Page     int
	PageSize int
}

InviteCodeListQuery selects a page of invite codes.

type InviteService

type InviteService struct {
	// contains filtered or unexported fields
}

InviteService 邀请服务

It keeps the invite configuration (v2_invite_config) in memory and reloads it when the invite settings namespace changed (SettingsGeneration): the affiliate module writes the table it adopted, then the frontend settings through KernelSettings, which bumps the namespace.

func NewInviteService

func NewInviteService(db *gorm.DB) *InviteService

NewInviteService 创建服务

func (*InviteService) AddCommission

func (s *InviteService) AddCommission(userID, orderID, fromUserID uint, amount float64, recordType int, remark string) error

AddCommission 添加佣金

func (*InviteService) CalculateCommission

func (s *InviteService) CalculateCommission(orderAmount float64) float64

CalculateCommission 计算佣金

func (*InviteService) GenerateAdminInviteCodes added in v4.1.0

func (s *InviteService) GenerateAdminInviteCodes(count int, expireDays *int, now time.Time) ([]model.InviteCode, error)

GenerateAdminInviteCodes creates count codes that belong to no user, in one transaction. expireDays nil uses the configured code_expire_days, 0 makes codes that never expire, and a positive value expires them after so many days.

func (*InviteService) GenerateInviteCode

func (s *InviteService) GenerateInviteCode(userID *uint) (*model.InviteCode, error)

GenerateInviteCode 生成邀请码

func (*InviteService) GenerateUserInviteCode added in v4.1.0

func (s *InviteService) GenerateUserInviteCode(userID uint) (*model.InviteCode, error)

GenerateUserInviteCode generates an invite code a user asked for. As in v2board, a user may hold at most the configured number (code_count, default DefaultInviteCodeLimit) of unused codes; an expired code is not counted, since it can no longer be used. Codes made otherwise (GenerateInviteCode) are not limited.

func (*InviteService) GetCommissionRecords

func (s *InviteService) GetCommissionRecords(userID uint, page, pageSize int) ([]model.CommissionRecord, int64, error)

GetCommissionRecords 获取佣金记录

func (*InviteService) GetConfig

func (s *InviteService) GetConfig() (*model.InviteConfig, error)

GetConfig 获取配置: the copy in memory while the invite settings namespace has not changed since it was loaded.

func (*InviteService) GetInviteStats

func (s *InviteService) GetInviteStats(userID uint) (map[string]any, error)

GetInviteStats 获取邀请统计

func (*InviteService) GetUserInviteCodes

func (s *InviteService) GetUserInviteCodes(userID uint) ([]model.InviteCode, error)

GetUserInviteCodes 获取用户邀请码

func (*InviteService) GetWithdrawRecords

func (s *InviteService) GetWithdrawRecords(userID uint, page, pageSize int) ([]model.CommissionWithdraw, int64, error)

GetWithdrawRecords 获取提现记录

func (*InviteService) ListInviteCodes added in v4.1.0

func (s *InviteService) ListInviteCodes(query InviteCodeListQuery, now time.Time) ([]model.InviteCode, int64, error)

ListInviteCodes returns a page of every invite code, newest first, and the number of codes the filter matches. An unused code past its expiry counts as expired, not unused.

func (*InviteService) ProcessWithdraw added in v4.1.0

func (s *InviteService) ProcessWithdraw(withdraw *model.CommissionWithdraw, status int, remark string, now time.Time) error

ProcessWithdraw approves (status 1) or rejects (status 2) a withdrawal that is still pending, as of now. The decision applies only while the withdrawal is pending, so of two concurrent decisions one fails with ErrWithdrawalProcessed. A rejection returns the amount to the commission balance in the same transaction, once (WithdrawRefundRequestID); if that fails, nothing changes. A withdrawal whose user no longer exists has no one to refund and is rejected all the same. withdraw is updated on success.

func (*InviteService) RequestWithdraw

func (s *InviteService) RequestWithdraw(userID uint, amount float64, method, account, name string) (*model.CommissionWithdraw, error)

RequestWithdraw 申请提现. The amount is a whole number in the commission balance's unit (cents) and at least the configured minimum, read for each request so an administrator's change applies at once. The withdrawal and its debit (WithdrawDebitRequestID) are one transaction, and the debit is taken under the subscriber's row lock and refused below zero (subscriber.AdjustBalanceTx), so concurrent requests cannot overdraw.

func (*InviteService) RevokeInviteCode added in v4.1.0

func (s *InviteService) RevokeInviteCode(id uint) error

RevokeInviteCode deletes an unused code, whoever it belongs to, so no registration can consume it. A used code is refused: it records who registered with it. The delete names the unused state, so a registration that consumes the code first wins.

func (*InviteService) SaveConfig added in v4.1.0

func (s *InviteService) SaveConfig(cfg *model.InviteConfig) error

SaveConfig saves cfg and makes it the configuration in memory; every other copy reloads.

func (*InviteService) SetConfig

func (s *InviteService) SetConfig(cfg *model.InviteConfig)

SetConfig 设置配置: cfg becomes the configuration in memory, and every other copy reloads.

type KernelOperationStatus

type KernelOperationStatus struct {
	ID                   string     `json:"id"`
	NodeID               *uint      `json:"node_id,omitempty"`
	PluginID             string     `json:"plugin_id"`
	TargetVersion        string     `json:"target_version"`
	TopologyDeploymentID *uint      `json:"topology_deployment_id,omitempty"`
	TopologyStepID       *uint      `json:"topology_step_id,omitempty"`
	TopologyRevision     int64      `json:"topology_revision,omitempty"`
	Kind                 string     `json:"kind"`
	OperationChain       string     `json:"operation_chain,omitempty"`
	Revision             int64      `json:"revision"`
	ConfigHash           string     `json:"config_hash,omitempty"`
	State                string     `json:"state"`
	DeadlineAt           *time.Time `json:"deadline_at,omitempty"`
	DispatchedAt         *time.Time `json:"dispatched_at,omitempty"`
	AcknowledgedAt       *time.Time `json:"acknowledged_at,omitempty"`
	ObservedAt           *time.Time `json:"observed_at,omitempty"`
	CancelAt             *time.Time `json:"cancel_at,omitempty"`
	Attempt              int        `json:"attempt"`
	HasResult            bool       `json:"has_result"`
	HasError             bool       `json:"has_error"`
	CreatedAt            time.Time  `json:"created_at"`
	UpdatedAt            time.Time  `json:"updated_at"`
}

KernelOperationStatus is the public, non-secret projection of a durable operation. ConfigJSON, ResultJSON, session identity, and raw Agent errors intentionally stay inside the kernel persistence boundary.

func PublicKernelOperation

func PublicKernelOperation(operation model.KernelOperation) KernelOperationStatus

type LatencyPoint

type LatencyPoint struct {
	BucketAt int64   `json:"bucketAt"` // unix milli
	Min      float64 `json:"min"`
	Avg      float64 `json:"avg"`
	Max      float64 `json:"max"`
	P95      float64 `json:"p95"`
	Loss     float64 `json:"loss"`
	Samples  int     `json:"samples"`
	Success  int     `json:"success"`
}

type LatencyTrend

type LatencyTrend struct {
	TargetKey       string         `json:"targetKey"`
	Label           string         `json:"label"`
	Host            string         `json:"host"`
	Port            int            `json:"port"`
	IntervalSeconds int            `json:"intervalSeconds"`
	Points          []LatencyPoint `json:"points"`
}

type LegacyForwardAnsibleCleaner added in v4.2.0

type LegacyForwardAnsibleCleaner struct {
	DB *gorm.DB
	// Playbook overrides LegacyForwardCleanupPlaybook.
	Playbook string
	// Run replaces running ansible-playbook (tests).
	Run func(ctx context.Context, command string, args []string, workdir string, env map[string]string) (string, error)
	// Timeout bounds one host's run; 5 minutes when zero.
	Timeout time.Duration
}

LegacyForwardAnsibleCleaner runs the cleanup playbook on a host of the Ansible fallback: an Ansible machine, or the execution node of a flux forward on an Ansible backend.

func (*LegacyForwardAnsibleCleaner) CleanNode added in v4.2.0

CleanNode runs the cleanup playbook limited to the node's host.

type LegacyForwardNodeXCleaner added in v4.2.0

type LegacyForwardNodeXCleaner struct {
	DB *gorm.DB
	// HTTPClient replaces the NodeX client's HTTP client (tests).
	HTTPClient interface {
		Do(*http.Request) (*http.Response, error)
	}
}

LegacyForwardNodeXCleaner deletes a node's flux gost services through NodeX. NodeX has no listing call: a delete is idempotent, so running the check again is the re-check.

func (*LegacyForwardNodeXCleaner) CleanNode added in v4.2.0

CleanNode deletes, through NodeX, every flux forward whose tunnel enters at the node on the gost backend, and every legacy rule relayed or exited by the node.

type LoadBalancerService

type LoadBalancerService struct {
	// contains filtered or unexported fields
}

LoadBalancerService 负载均衡服务

func NewLoadBalancerService

func NewLoadBalancerService(db *gorm.DB) *LoadBalancerService

NewLoadBalancerService 创建服务

func (*LoadBalancerService) Create

Create 创建负载均衡器

func (*LoadBalancerService) Delete

func (s *LoadBalancerService) Delete(id uint) error

Delete 删除负载均衡器

func (*LoadBalancerService) GetByID

func (s *LoadBalancerService) GetByID(id uint) (*model.LoadBalancer, error)

GetByID 获取负载均衡器

func (*LoadBalancerService) GetStats

func (s *LoadBalancerService) GetStats(lbID uint) (map[string]any, error)

GetStats 获取负载均衡统计

func (*LoadBalancerService) List

func (s *LoadBalancerService) List(groupID uint) ([]model.LoadBalancer, error)

List 获取负载均衡器列表

func (*LoadBalancerService) RunHealthCheck

func (s *LoadBalancerService) RunHealthCheck(lbID uint) error

RunHealthCheck 执行健康检查

func (*LoadBalancerService) SelectNode

func (s *LoadBalancerService) SelectNode(lbID uint) (*model.ForwardNode, error)

SelectNode 选择最佳节点

func (*LoadBalancerService) Update

Update 更新负载均衡器

type LoginRateLimitOptions

type LoginRateLimitOptions struct {
	Enabled      bool
	MaxAttempts  int
	Window       time.Duration
	Lockout      time.Duration
	CleanupAfter time.Duration
}

func ResolveLoginRateLimitOptions

func ResolveLoginRateLimitOptions(cfg *config.Config) LoginRateLimitOptions

func ResolveRegisterRateLimitOptions

func ResolveRegisterRateLimitOptions(cfg *config.Config) LoginRateLimitOptions

type LoginRateLimiter

type LoginRateLimiter struct {
	// contains filtered or unexported fields
}

func GetLoginRateLimiter

func GetLoginRateLimiter() *LoginRateLimiter

func GetSubscriptionResetLimiter added in v4.1.0

func GetSubscriptionResetLimiter() *LoginRateLimiter

GetSubscriptionResetLimiter returns the limiter of users' subscription resets, kept apart from the login limiter so that neither's cleanup forgets the other's keys early.

func NewLoginRateLimiter

func NewLoginRateLimiter() *LoginRateLimiter

func (*LoginRateLimiter) Check

func (l *LoginRateLimiter) Check(key string, options LoginRateLimitOptions) (bool, time.Duration)

func (*LoginRateLimiter) RecordFailure

func (l *LoginRateLimiter) RecordFailure(key string, options LoginRateLimitOptions)

func (*LoginRateLimiter) RecordSuccess

func (l *LoginRateLimiter) RecordSuccess(key string)

type MFAService

type MFAService struct {
	// contains filtered or unexported fields
}

MFAService MFA服务

func NewMFAService

func NewMFAService(db *gorm.DB, config *model.MFAConfig) *MFAService

NewMFAService 创建服务

func (*MFAService) CheckBruteForce

func (s *MFAService) CheckBruteForce(userID uint, maxAttempts int, window time.Duration) (bool, error)

CheckBruteForce 检查暴力破解

Implemented but not yet wired into the MFA verification flow; see TODO.md ("Implemented but not wired").

func (*MFAService) DisableMFA

func (s *MFAService) DisableMFA(userID uint, password string) error

DisableMFA 禁用MFA

func (*MFAService) EnableTOTP

func (s *MFAService) EnableTOTP(userID uint, code string) error

EnableTOTP 启用TOTP

func (*MFAService) GetRemainingBackupCodes

func (s *MFAService) GetRemainingBackupCodes(userID uint) (int, error)

GetRemainingBackupCodes 获取剩余备用码数量

func (*MFAService) GetUserMFA

func (s *MFAService) GetUserMFA(userID uint) (*model.UserMFA, error)

GetUserMFA 获取用户MFA配置

func (*MFAService) IsEnforcedForUser

func (s *MFAService) IsEnforcedForUser(user *model.User) bool

IsEnforcedForUser 检查是否对用户强制MFA

func (*MFAService) RecordLoginAttempt

func (s *MFAService) RecordLoginAttempt(userID uint, ip, userAgent string, success bool, method string) error

RecordLoginAttempt 记录登录尝试

func (*MFAService) RegenerateBackupCodes

func (s *MFAService) RegenerateBackupCodes(userID uint) ([]string, error)

RegenerateBackupCodes 重新生成备用码

func (*MFAService) SetConfig

func (s *MFAService) SetConfig(config *model.MFAConfig)

SetConfig updates MFA runtime config without recreating service.

func (*MFAService) SetupTOTP

func (s *MFAService) SetupTOTP(userID uint, email string) (*TOTPSetup, error)

SetupTOTP 设置TOTP

func (*MFAService) Verify

func (s *MFAService) Verify(userID uint, code, method string) (bool, error)

Verify 验证MFA代码

func (*MFAService) VerifyBackupCode

func (s *MFAService) VerifyBackupCode(mfa *model.UserMFA, code string) (bool, error)

VerifyBackupCode 验证备用码

func (*MFAService) VerifyTOTP

func (s *MFAService) VerifyTOTP(secret, code string) bool

VerifyTOTP 验证TOTP代码

type MultiIngressRow

type MultiIngressRow struct {
	TunnelID      uint    `json:"tunnelId"`
	TunnelName    string  `json:"tunnelName"`
	IngressNodeID uint    `json:"ingressNodeId"`
	IngressLabel  string  `json:"ingressLabel"`
	IngressIP     string  `json:"ingressIp"`
	AvgRTT        float64 `json:"avgRtt"`
	Loss          float64 `json:"loss"`
	BucketAt      int64   `json:"bucketAt"`
	Online        bool    `json:"online"`
}

type NodeListParams

type NodeListParams struct {
	Page     int
	PageSize int
	Status   *model.NodeStatus
	GroupID  *uint
	Search   string
	// OrderBy is an ORDER BY clause from v2compat.ParseListSort over
	// NodeSortColumns, never request text; empty keeps the administrator's
	// order ("sort ASC, id DESC").
	OrderBy string
}

type NodeListResult

type NodeListResult struct {
	Total int64        `json:"total"`
	List  []model.Node `json:"list"`
}

type NodeLogInput

type NodeLogInput struct {
	Level      string
	Source     string
	Message    string
	TraceID    string
	FieldsJSON string
	LoggedAt   *time.Time
}

type NodeLogListParams

type NodeLogListParams struct {
	NodeID   uint
	Page     int
	PageSize int
	Level    string
	Source   string
	Search   string
}

type NodeLogListResult

type NodeLogListResult struct {
	Total int64           `json:"total"`
	List  []model.NodeLog `json:"list"`
}

type NodeLogService

type NodeLogService struct {
	// contains filtered or unexported fields
}

func NewNodeLogService

func NewNodeLogService() *NodeLogService

func (*NodeLogService) GetLogs

func (*NodeLogService) RecordAgentLogBatch added in v4.1.0

func (s *NodeLogService) RecordAgentLogBatch(nodeKind string, nodeID uint, batchID string, inputs []NodeLogInput) (bool, error)

RecordAgentLogBatch records a LogBatch from the Agent Control stream (reports.v1) as RecordLogs records the legacy NodeLogService batch, once per node and batch id: the batch record (internal/agentreports) is claimed in the transaction that inserts the rows. It reports whether this call recorded the batch; false means a committed transaction recorded it before. gorm.ErrRecordNotFound means the node does not exist.

func (*NodeLogService) RecordAgentMaintenanceEvent added in v4.2.0

func (s *NodeLogService) RecordAgentMaintenanceEvent(nodeKind string, nodeID uint, event agentcontrol.MaintenanceEvent) (bool, error)

RecordAgentMaintenanceEvent records one maintenance event of the Agent Control stream (maintenance.v1) once per node and event id: a v2_node_log row of source "maintenance" whose fields_json is the event, inserted in the transaction that claims the event's record (internal/agentreports, kind maintenance). It reports whether this call recorded the event; false means a committed transaction recorded it before. gorm.ErrRecordNotFound means the node does not exist.

func (*NodeLogService) RecordLogs

func (s *NodeLogService) RecordLogs(nodeID uint, inputs []NodeLogInput) error

func (*NodeLogService) RecordLogsTx added in v4.1.0

func (s *NodeLogService) RecordLogsTx(tx *gorm.DB, nodeID uint, inputs []NodeLogInput) error

RecordLogsTx is RecordLogs inside tx.

type NodeMonthlyResetWorker

type NodeMonthlyResetWorker struct {
	// contains filtered or unexported fields
}

NodeMonthlyResetWorker 按每个节点自己的 MonthlyResetDay 把 monthly_upload/ monthly_download 清零, 用于月流量限额的周期性重置(仅统计层面, 不做任何 自动禁用/限速动作)。

func NewNodeMonthlyResetWorker

func NewNodeMonthlyResetWorker(db *gorm.DB) *NodeMonthlyResetWorker

func (*NodeMonthlyResetWorker) RunOnce

func (w *NodeMonthlyResetWorker) RunOnce(now time.Time) error

func (*NodeMonthlyResetWorker) Start

func (w *NodeMonthlyResetWorker) Start(ctx context.Context)

type NodePluginObservedSnapshot

type NodePluginObservedSnapshot struct {
	PluginID         string
	Version          string
	DesiredRevision  uint64
	ObservedRevision uint64
	ConfigHash       string
	Health           string
	RulesetSHA256    string
	ObservedAt       time.Time
	RuleCounters     []NodePluginRuleCounter
}

NodePluginObservedSnapshot is the bounded data the authenticated Agent may contribute on its heartbeat. It deliberately has no raw plugin payload or error text: those would make the generic observed-state API a secret leak.

type NodePluginRuleCounter

type NodePluginRuleCounter struct {
	RuleID  string `json:"rule_id"`
	Packets uint64 `json:"packets"`
	Bytes   uint64 `json:"bytes"`
}

NodePluginRuleCounter is the kernel-neutral form of one plugin-owned counter. The kernel stores it as opaque, bounded data and does not infer quota or billing semantics from it.

type NodeSecretFinding added in v4.1.0

type NodeSecretFinding struct {
	NodeID uint `json:"node_id"`
	// ProtocolID is zero for a node's raw configuration.
	ProtocolID uint `json:"protocol_id,omitempty"`
	// Type is the protocol's type, or raw_config.
	Type    string `json:"type"`
	Enabled bool   `json:"enabled"`
	Column  string `json:"column"`
	// Field is the secret's JSON pointer; empty for a whole column.
	Field  string `json:"field"`
	Reason string `json:"reason"`
}

NodeSecretFinding names one secret that fails validation: where it is and why, never its value.

func ValidateProtocolSecrets added in v4.1.0

func ValidateProtocolSecrets(protocol *model.NodeProtocol) []NodeSecretFinding

ValidateProtocolSecrets checks the secrets of one node protocol, as the configuration builder uses them: no secret position holds the placeholder or a tombstone, and the keys the node parses (a WireGuard entry's server key pair, a Reality private key, a Shadowsocks 2022 server key) are well formed.

func ValidateRawConfigSecrets added in v4.1.0

func ValidateRawConfigSecrets(node *model.Node) []NodeSecretFinding

ValidateRawConfigSecrets checks the secrets of a node's raw configuration: no secret position holds the placeholder or a tombstone. The kernel already refuses a WireGuard raw configuration that fails its runtime validation.

type NodeSecretValidation added in v4.1.0

type NodeSecretValidation struct {
	// Protocols and RawConfigs count the rows checked.
	Protocols  int64               `json:"protocols"`
	RawConfigs int64               `json:"raw_configs"`
	Findings   []NodeSecretFinding `json:"findings"`
	// Excluded is always zero: validation reports and excludes nothing
	// (D7).
	Excluded int `json:"excluded"`
}

NodeSecretValidation is the outcome of ScanNodeSecrets.

func ScanNodeSecrets added in v4.1.0

func ScanNodeSecrets(ctx context.Context, db *gorm.DB) (*NodeSecretValidation, error)

ScanNodeSecrets checks every node protocol and every node raw configuration as the configuration builder would use them (their secrets read through the node credential split, in the table's phase), and reports the findings. It changes nothing.

type NodeService

type NodeService struct {
	// contains filtered or unexported fields
}

NodeService 节点服务

func NewNodeService

func NewNodeService() *NodeService

NewNodeService 创建节点服务

func (*NodeService) AccumulateTrafficOnly

func (s *NodeService) AccumulateTrafficOnly(nodeID uint, upload, download int64) error

AccumulateTrafficOnly 仅把流量累加到节点自身及其父节点链,不更新心跳指标。

func (*NodeService) AssignProtocolsToGroup

func (s *NodeService) AssignProtocolsToGroup(groupID uint, protocolIDs []uint) error

AssignProtocolsToGroup 为分组分配节点协议

func (*NodeService) BuildUniProxyConfig added in v4.1.0

func (s *NodeService) BuildUniProxyConfig(nodeID uint, preferredType string) (map[string]any, error)

BuildUniProxyConfig is BuildUniProxyNodeConfig over the service's database.

func (*NodeService) CreateNode

func (s *NodeService) CreateNode(node *model.Node) error

CreateNode 创建节点 (手动添加), its own columns only.

Node and node protocol writes take the model or the map an administrator's body was bound to. GORM saves the associations of a model it creates: a node body's "protocols" moved other nodes' protocols to the new node, and the nodes and subscription groups nested in them were created, those nodes without an API key. A body's id chose the new row's id. Protocols have their own routes and are never saved from a node body.

func (*NodeService) CreateProtocol

func (s *NodeService) CreateProtocol(protocol *model.NodeProtocol) error

CreateProtocol 创建协议, its own columns only (see CreateNode): a protocol body's "node" created a node without an API key and moved the protocol to it, and its "subscription_groups" were created and linked. Linking protocols to groups has its own route. A secret sent as NodeSecretPlaceholder is stored empty, since nothing is stored yet.

func (*NodeService) DeleteAuthKey

func (s *NodeService) DeleteAuthKey(id uint) error

DeleteAuthKey 删除授权密钥, as RevokeRegistrationKey does.

func (*NodeService) DeleteNode

func (s *NodeService) DeleteNode(id uint) error

DeleteNode 删除节点

The kernel's part, the protocols with their peers, links and secrets, the credentials and the agent certificates, is RetireProxyNodeTx, which RetireNode runs too; the node row goes with it in the same transaction.

func (*NodeService) DeleteProtocol

func (s *NodeService) DeleteProtocol(id uint) error

DeleteProtocol 删除协议. The kernel's part, the peers, links and secrets, is RetireProtocolTx, which RetireProtocol runs too; the protocol row goes with it in the same transaction.

func (*NodeService) GenerateAuthKey

func (s *NodeService) GenerateAuthKey(name string, expireDays int) (*model.AuthorizedKey, string, error)

GenerateAuthKey 生成授权密钥, as IssueRegistrationKey does.

func (*NodeService) GetAllAvailableProtocols

func (s *NodeService) GetAllAvailableProtocols() ([]model.NodeProtocol, error)

GetAllAvailableProtocols 获取所有可显示在订阅中的协议

func (*NodeService) GetAuthKeys

func (s *NodeService) GetAuthKeys() ([]model.AuthorizedKey, error)

GetAuthKeys 获取授权密钥列表

func (*NodeService) GetNode

func (s *NodeService) GetNode(id uint) (*model.Node, error)

GetNode 获取节点详情

func (*NodeService) GetNodeByAPIKey

func (s *NodeService) GetNodeByAPIKey(apiKey string) (*model.Node, error)

GetNodeByAPIKey 通过API Key获取节点 by its stored hash, for the gRPC node listener and the Agent Control stream. The lookup goes through the node credential split, which applies the table's phase and never matches a tombstone or the placeholder.

func (*NodeService) GetNodeStats

func (s *NodeService) GetNodeStats() (map[string]any, error)

GetNodeStats 获取节点统计

func (*NodeService) GetNodes

func (s *NodeService) GetNodes(params NodeListParams) (*NodeListResult, error)

GetNodes 获取节点列表

func (*NodeService) GetProtocol

func (s *NodeService) GetProtocol(id uint) (*model.NodeProtocol, error)

GetProtocol 获取协议详情

func (*NodeService) GetProtocols

func (s *NodeService) GetProtocols(nodeID uint) ([]model.NodeProtocol, error)

GetProtocols 获取节点的协议列表

func (*NodeService) GetProtocolsByGroup

func (s *NodeService) GetProtocolsByGroup(groupID uint) ([]model.NodeProtocol, error)

GetProtocolsByGroup 获取关联到特定分组的协议列表

func (*NodeService) Heartbeat

func (s *NodeService) Heartbeat(nodeID uint, req *model.NodeHeartbeatRequest) error

Heartbeat 节点心跳

func (*NodeService) LatestPackageReport added in v4.2.0

func (s *NodeService) LatestPackageReport(nodeKind string, nodeID uint, pluginID, kind string, now time.Time) (PackageReport, error)

LatestPackageReport returns the latest report of a node, plugin and kind, or gorm.ErrRecordNotFound.

func (*NodeService) NodeCredentials added in v4.1.0

func (s *NodeService) NodeCredentials(node *model.Node) (apiKey, secret string)

NodeCredentials answers node's API key and shared secret, where the kernel shows them to an administrator, read through the node credential split.

func (*NodeService) PrepareNodeRawConfig added in v4.1.0

func (s *NodeService) PrepareNodeRawConfig(node *model.Node)

PrepareNodeRawConfig reads the secrets of node's raw configuration through the node credential split, in place, before UniProxy builds the node's configuration from it, and reports a secret that fails validation (report-only, D7).

func (*NodeService) RecordAgentNodeStatus added in v4.1.0

func (s *NodeService) RecordAgentNodeStatus(nodeID uint, status AgentNodeStatus) error

RecordAgentNodeStatus writes what the legacy ReportStatus heartbeat and the runtime-health report write together: the system metrics, the heartbeat (last_check_at and the status, which never re-enables a disabled node) and the runtime health. online_users is not here: the stream's TrafficReport.online keeps it. A node that does not exist is gorm.ErrRecordNotFound.

func (*NodeService) RecordNodePluginObservedStates

func (s *NodeService) RecordNodePluginObservedStates(nodeID uint, snapshots []NodePluginObservedSnapshot, receivedAt time.Time) (int, error)

RecordNodePluginObservedStates stores trusted, monotonic plugin runtime snapshots from an authenticated node. A malformed or unauthorized snapshot is rejected independently so one bad plugin cannot suppress another plugin's heartbeat evidence.

func (*NodeService) RecordPackageReport added in v4.2.0

func (s *NodeService) RecordPackageReport(input PackageReportInput, receivedAt time.Time) (bool, error)

RecordPackageReport authorizes, sanitizes and stores a package report as the latest of its node, plugin and kind. It returns true when the report was stored and false when a newer one was stored before (the report is dropped). A *PackageReportRefusedError refuses the report for good; any other error means the kernel could not record it now.

func (*NodeService) RecordPluginTelemetry

func (s *NodeService) RecordPluginTelemetry(nodeID uint, heartbeatMetrics map[string]float64, receivedAt time.Time) (int, error)

RecordPluginTelemetry persists only metrics from enabled assignments on the authenticated node. Non-plugin heartbeat metrics remain kernel-owned and are intentionally excluded from this plugin state table.

func (*NodeService) RegisterNode

func (s *NodeService) RegisterNode(req *model.NodeRegisterRequest, clientIP string) (*model.NodeRegisterResponse, error)

RegisterNode 节点自动注册

func (*NodeService) SyncProtocolToNode

func (s *NodeService) SyncProtocolToNode(nodeID uint) error

SyncProtocolToNode 同步协议配置到节点

func (*NodeService) UpdateLastCheckAt

func (s *NodeService) UpdateLastCheckAt(nodeID uint) error

UpdateLastCheckAt 更新节点最后检查时间 (用于 UniProxy 接口)

func (*NodeService) UpdateNode

func (s *NodeService) UpdateNode(id uint, updates map[string]any) error

UpdateNode 更新节点. The node's id and credentials are never updated, and a new parent is checked whatever spelling names it (see columnUpdates). A raw configuration secret sent as NodeSecretPlaceholder keeps its stored value.

func (*NodeService) UpdateOnlineUsers added in v4.1.0

func (s *NodeService) UpdateOnlineUsers(nodeID uint, count int) error

UpdateOnlineUsers records how many users a node's last online report listed, as the legacy status heartbeat records online_users.

func (*NodeService) UpdateProtocol

func (s *NodeService) UpdateProtocol(id uint, updates map[string]any) error

UpdateProtocol 更新协议. The protocol's id and node are never updated, and the update is checked with the values it writes, whatever spelling names them (see columnUpdates). A secret sent as NodeSecretPlaceholder, as the administrator's answers show it, keeps its stored value.

func (*NodeService) UpdateRuntimeHealth

func (s *NodeService) UpdateRuntimeHealth(nodeID uint, healthy bool, message string) error

UpdateRuntimeHealth records the health of a node's managed runtime process. It deliberately does not change last_check_at: the node can be reachable while its relay process is unhealthy, and those are different signals.

type NodeTrafficPoint added in v4.2.0

type NodeTrafficPoint struct {
	Start time.Time
	Up    int64
	Down  int64
}

NodeTrafficPoint is one bucket of a node's traffic series: the bytes the node's users moved through it, with the node's traffic rate applied (the bytes the panel meters, as /admin/traffic/hourly and the statistics tables count them).

type NodeTrafficSeries added in v4.2.0

type NodeTrafficSeries struct {
	NodeID      uint
	Granularity string
	Since       time.Time
	Until       time.Time
	Points      []NodeTrafficPoint
}

NodeTrafficSeries is a node's traffic over [Since, Until) in buckets of Granularity, ascending, with the buckets without traffic as zeros.

type NotificationService

type NotificationService struct {
	// contains filtered or unexported fields
}

NotificationService 通知服务

func NewNotificationService

func NewNotificationService(db *gorm.DB, cfg *config.Config) *NotificationService

NewNotificationService 创建服务

func (*NotificationService) Broadcast

func (s *NotificationService) Broadcast(title, content string) error

Broadcast 广播通知 (所有用户)

func (*NotificationService) Drain

func (s *NotificationService) Drain()

Drain waits for all asynchronous notifications registered before the call. The registration mutex prevents WaitGroup.Add from racing with Wait.

func (*NotificationService) NotifyAdministrators added in v4.2.0

func (s *NotificationService) NotifyAdministrators(event, title, content string, data map[string]any) error

NotifyAdministrators sends one notification to every administrator who is not banned: always as an in-app entry in the notification log, by e-mail when the e-mail settings are configured (SetEmailConfig), and by Telegram when the administrator bound an account. It is the delivery of the kernel alert monitor's digests (internal/kernelalerts). The title is cut to the log's width.

func (*NotificationService) NotifyNodeOffline

func (s *NotificationService) NotifyNodeOffline(node *model.Node) error

NotifyNodeOffline 节点离线通知 (管理员)

func (*NotificationService) NotifyOrderPaid

func (s *NotificationService) NotifyOrderPaid(order *model.Order, user *model.User) error

NotifyOrderPaid 订单支付成功通知

func (*NotificationService) NotifyTicketReply

func (s *NotificationService) NotifyTicketReply(ticket *model.Ticket, replyBy string) error

NotifyTicketReply 工单回复通知

func (*NotificationService) NotifyTrafficLow

func (s *NotificationService) NotifyTrafficLow(user *model.User, percentLeft float64) error

NotifyTrafficLow 流量不足通知

func (*NotificationService) NotifyUserExpire

func (s *NotificationService) NotifyUserExpire(user *model.User, daysLeft int) error

NotifyUserExpire 用户到期通知

func (*NotificationService) Send

func (s *NotificationService) Send(userID *uint, notifyType, event, title, content string, data map[string]any) error

Send 发送通知

func (*NotificationService) SendEmail

func (s *NotificationService) SendEmail(to, subject, body string) error

SendEmail 发送邮件

func (*NotificationService) SetEmailConfig

func (s *NotificationService) SetEmailConfig(cfg *model.EmailConfig)

SetEmailConfig 设置邮件配置

type OperationLogInput

type OperationLogInput struct {
	UserID     *uint
	Username   string
	Action     string
	Module     string
	TargetType string
	TargetID   *uint
	Content    string
	IP         string
	UserAgent  string
	Status     int
}

func BackupConfigAuditInput added in v4.1.0

func BackupConfigAuditInput(actor SettingsAuditActor, action string, cfg *model.BackupConfig, preservedSensitiveFields []string) *OperationLogInput

BackupConfigAuditInput is the audit entry of a backup configuration change: every field but the S3 keys, whether each S3 key has a value, and which of them were kept.

func SystemConfigAuditInput added in v4.1.0

func SystemConfigAuditInput(actor SettingsAuditActor, action string, cfg *model.SystemConfig, preserveExisting bool) *OperationLogInput

SystemConfigAuditInput is the audit entry of a system configuration change: which key, its group and type, whether it is a secret and has a value, and whether the stored secret was kept. For a value with masked fields (the SMTP password of the e-mail configuration) it also names them and those that hold a value. It never holds the value.

type OperationLogService

type OperationLogService struct {
	// contains filtered or unexported fields
}

func NewOperationLogService

func NewOperationLogService(db *gorm.DB) *OperationLogService

func (*OperationLogService) Record

func (s *OperationLogService) Record(input *OperationLogInput) error

type OrderBuyerRef added in v4.1.0

type OrderBuyerRef struct {
	ID    uint   `json:"id"`
	Email string `json:"email"`
}

OrderBuyerRef names an order's buyer in an administrator's order list and detail answers.

type OrderListParams

type OrderListParams struct {
	Page     int
	PageSize int
	UserID   *uint
	Status   *int
	Type     *int
	TradeNo  string
	Email    string
	// OrderBy is an ORDER BY clause from v2compat.ParseListSort over
	// OrderSortColumns, never request text; empty keeps the default order.
	OrderBy string
}

OrderListParams 订单列表查询参数

type OrderListResult

type OrderListResult struct {
	Total int64       `json:"total"`
	List  []OrderView `json:"list"`
}

OrderListResult 订单列表结果

type OrderPaymentReconciler added in v4.1.0

type OrderPaymentReconciler struct {

	// Grace, Batch and MaxBatches default to the package constants.
	Grace      time.Duration
	Batch      int
	MaxBatches int
	// contains filtered or unexported fields
}

OrderPaymentReconciler completes the orders of paid payment records that are still pending (docs/architecture/order-service.md): the payment module records a paid callback, then asks KernelOrder to complete the order; if that second step fails and the provider never delivers again, the record stays paid and the order pending. The reconciler runs the same second step, CompleteOrderPaymentTx with request id payment:<trade_no>, so it re-checks the record against the order and applies it once; a record that fails the checks is refused and logged, and the order is left unchanged.

func NewOrderPaymentReconciler added in v4.1.0

func NewOrderPaymentReconciler(db *gorm.DB) *OrderPaymentReconciler

NewOrderPaymentReconciler returns a reconciler on db, or on the kernel's database when db is nil.

func (*OrderPaymentReconciler) RunOnce added in v4.1.0

RunOnce applies the candidate records as of now, in batches, each record in its own transaction. It stops early when ctx is cancelled. Running it again, or in two processes at once, changes nothing more: the record's row lock and its request id apply it once.

type OrderPaymentReconciliation added in v4.1.0

type OrderPaymentReconciliation struct {
	// Examined is the number of candidate records the run read.
	Examined int
	// Completed, Paid and Refused count the outcomes the run recorded.
	Completed int
	Paid      int
	Refused   int
	// Unchanged counts records another caller applied first (a callback's
	// repeat, or a reconciler in another process).
	Unchanged int
	// Failed counts records whose completion failed; nothing was recorded,
	// so a later run tries again.
	Failed int
}

OrderPaymentReconciliation counts what one run did.

type OrderPaymentResult added in v4.1.0

type OrderPaymentResult struct {
	// Applied is false when the trade number was applied before; the other
	// fields then describe that first application.
	Applied bool   `json:"-"`
	OrderID uint64 `json:"order_id"`
	Outcome string `json:"outcome"`
	// Reason says why a refused record does not pay its order.
	Reason string `json:"reason,omitempty"`
}

OrderPaymentResult is the outcome of CompleteOrderPaymentTx, as the request ledger records it.

func CompleteOrderPaymentTx added in v4.1.0

func CompleteOrderPaymentTx(tx *gorm.DB, tradeNo string, orderID uint64, now time.Time) (OrderPaymentResult, error)

CompleteOrderPaymentTx applies the paid payment record tradeNo to the order it names, orderID, in tx: it re-checks that the record pays the order, marks the order paid and completes it (grants its plan with request id "order:<id>", in a savepoint). The outcome is recorded in the request ledger under OrderPaymentRequestID, so a trade number is applied once and a repeat answers the first outcome.

The kernel's payment callbacks call it in the transaction that marks the record paid; KernelOrder.CompleteOrderPayment calls it after the payment module committed the record. It locks the record, then the order.

type OrderPlanRef added in v4.1.0

type OrderPlanRef struct {
	ID   uint   `json:"id"`
	Name string `json:"name"`
}

OrderPlanRef names an order's plan in the order list and detail answers.

type OrderService

type OrderService struct {
	// contains filtered or unexported fields
}

OrderService 订单服务

func NewOrderService

func NewOrderService() *OrderService

NewOrderService 创建订单服务

func (*OrderService) Cancel

func (s *OrderService) Cancel(orderID uint) error

Cancel 取消订单

func (*OrderService) Complete

func (s *OrderService) Complete(orderID uint) error

Complete 完成订单 (支付成功后处理)

func (*OrderService) Create

func (s *OrderService) Create(params CreateOrderParams) (*model.Order, error)

Create 创建订单

func (*OrderService) GetAdminView added in v4.1.0

func (s *OrderService) GetAdminView(id uint) (*OrderView, error)

GetAdminView is an administrator's order detail: the order with its plan and buyer.

func (*OrderService) GetByID

func (s *OrderService) GetByID(id uint) (*model.Order, error)

GetByID 根据ID获取订单, without its plan or buyer.

func (*OrderService) GetList

func (s *OrderService) GetList(params OrderListParams) (*OrderListResult, error)

GetList is an administrator's order list, newest first, each order with its buyer.

func (*OrderService) GetStats

func (s *OrderService) GetStats() (map[string]any, error)

GetStats 获取订单统计

func (*OrderService) GetUserOrders

func (s *OrderService) GetUserOrders(userID uint, page, pageSize int) (*OrderListResult, error)

GetUserOrders is a user's own orders, newest first, each with its plan and without the buyer. A user id of zero names no one and has no orders.

func (*OrderService) GetUserView added in v4.1.0

func (s *OrderService) GetUserView(userID, id uint) (*OrderView, error)

GetUserView is a user's order detail: the order, if it is the user's, with its plan. Another user's order is ErrOrderNotFound, as an unknown one.

func (*OrderService) UpdateStatus

func (s *OrderService) UpdateStatus(orderID uint, status int) error

UpdateStatus 更新订单状态

type OrderView added in v4.1.0

type OrderView struct {
	model.Order
	// User and Plan shadow the relations of model.Order, which are never
	// loaded here.
	User *OrderBuyerRef `json:"user,omitempty"`
	Plan *OrderPlanRef  `json:"plan,omitempty"`
}

OrderView is an order as the order list and detail routes answer it: the order's own columns, its plan's id and name, and, for an administrator, its buyer's id and e-mail. A plan or buyer that no longer exists is left out. The answers used to embed the buyer's whole v2_user row (subscription token and UUID included) and the whole v2_plan row.

type PackageHostMigrator added in v4.1.0

type PackageHostMigrator struct {
	DB    *gorm.DB
	Hosts *pluginhost.Supervisor
	// Now defaults to time.Now.
	Now func() time.Time
	// contains filtered or unexported fields
}

PackageHostMigrator runs the migration index of a started storage package through the migration ledger: one run per package lifecycle generation.

func (PackageHostMigrator) MigrateStartedHost added in v4.1.0

func (m PackageHostMigrator) MigrateStartedHost(ctx context.Context, ref pluginhost.ArtifactRef, generation uint64) error

MigrateStartedHost applies ref's migration index in the host that was just started for generation, verifies that the host applied exactly the steps of the verified index, and records the validation. Releases that do not declare kernel.storage.v1, or declare no steps, are left alone. A repeated call for the same generation, for example after a Control restart, resumes or confirms the recorded run instead of starting another one.

type PackageHostOperations added in v4.1.0

type PackageHostOperations struct {
	DB *gorm.DB
	// Storage provisions package storage for LeaseStorage; nil disables it.
	Storage *packagestore.Store
	// FallbackPublicKey verifies releases recorded without a trust root.
	FallbackPublicKey ed25519.PublicKey
}

PackageHostOperations serves the session-scoped package bridge RPCs from the kernel database.

func (PackageHostOperations) AuthorizeCapability added in v4.1.0

func (o PackageHostOperations) AuthorizeCapability(ctx context.Context, host packagebridge.HostIdentity, capability string) error

AuthorizeCapability admits a host to a kernel contract method family on the same terms: the current generation of an official AnixOps package whose verified signed release declares capability.

func (PackageHostOperations) AuthorizeIdentity added in v4.1.0

func (o PackageHostOperations) AuthorizeIdentity(ctx context.Context, host packagebridge.HostIdentity) error

AuthorizeIdentity admits a host to KernelIdentity only when it is the current generation of an official AnixOps package whose signed release, verified again here, declares kernel.identity.v1.

func (PackageHostOperations) LeaseStorage added in v4.1.0

LeaseStorage provisions the calling host's storage from the storage capabilities of its signed release and returns fresh credentials. The host must be the current installation generation.

func (PackageHostOperations) PackageConfig added in v4.1.0

PackageConfig returns the calling host's route modes. The host must be the enabled control installation at its desired version and current lifecycle generation; any other host is fenced.

type PackageMigrationCheckpoint

type PackageMigrationCheckpoint struct {
	OpaqueCheckpoint string
	ValidationDigest string
	Complete         bool
	FailureCode      string
}

PackageMigrationCheckpoint is an opaque host response persisted before the caller can act on that response. Its fields are intentionally not parsed by the kernel.

type PackageReport added in v4.2.0

type PackageReport struct {
	NodeKind   string          `json:"node_kind"`
	NodeID     uint            `json:"node_id"`
	PluginID   string          `json:"plugin_id"`
	Kind       string          `json:"kind"`
	Version    string          `json:"version"`
	Payload    json.RawMessage `json:"payload"`
	ObservedAt time.Time       `json:"observed_at"`
	ReceivedAt time.Time       `json:"received_at"`
	// Stale is true when the report is older than the kind allows (25
	// minutes for systemd.services) at the time it was read.
	Stale bool `json:"stale"`
}

PackageReport is the latest report of a node, plugin and kind, as the kernel stored it.

type PackageReportInput added in v4.2.0

type PackageReportInput struct {
	NodeKind string
	NodeID   uint
	PluginID string
	Kind     string
	Version  string
	Payload  []byte
	// ObservedAt is zero when the Agent did not say; the kernel then uses
	// the receive time.
	ObservedAt time.Time
}

PackageReportInput is one PackageReport from an authenticated node.

type PackageReportRefusedError added in v4.2.0

type PackageReportRefusedError struct {
	Reason string
	Detail string
}

PackageReportRefusedError is a permanent refusal of one report.

func (*PackageReportRefusedError) Error added in v4.2.0

func (e *PackageReportRefusedError) Error() string

type PackageRouteDefaultPackage added in v4.1.0

type PackageRouteDefaultPackage struct {
	PackageID string `json:"package_id"`
	// Batch is the rehearsal batch that signed the package off.
	Batch int `json:"batch"`
	// MinVersion is the oldest release the default applies to: the
	// rehearsed one. Older releases (the signed 4.0 packages) stay legacy.
	MinVersion string   `json:"min_version"`
	Routes     []string `json:"routes"`
}

PackageRouteDefaultPackage is one package of config/package-route-defaults.json.

type PackageRouteDefaultState added in v4.1.0

type PackageRouteDefaultState struct {
	// Policy is package_routes.default_mode.
	Policy string `json:"policy"`
	// Routes is the number of the package's routes in the default set.
	Routes int `json:"routes"`
	// MinVersion is the release the default needs ("" when the package has
	// no default routes).
	MinVersion string `json:"min_version,omitempty"`
	// Source is what the package's unstored default routes get: default,
	// kill-switch or package-too-old ("" without default routes).
	Source string `json:"source,omitempty"`
	// Note explains a default that does not apply.
	Note string `json:"note,omitempty"`
}

PackageRouteDefaultState is how the default policy applies to one installed package.

type PackageRouteDefaults added in v4.1.0

type PackageRouteDefaults struct {
	Format      string                       `json:"format"`
	DefaultMode string                       `json:"default_mode"`
	Packages    []PackageRouteDefaultPackage `json:"packages"`
	// contains filtered or unexported fields
}

PackageRouteDefaults is config/package-route-defaults.json.

func LoadPackageRouteDefaults added in v4.1.0

func LoadPackageRouteDefaults() (*PackageRouteDefaults, error)

LoadPackageRouteDefaults returns the embedded default set.

func (*PackageRouteDefaults) Includes added in v4.1.0

func (d *PackageRouteDefaults) Includes(packageID, route string) bool

Includes reports whether route of packageID is in the default set.

func (*PackageRouteDefaults) Package added in v4.1.0

Package returns a package's defaults.

func (*PackageRouteDefaults) RouteCount added in v4.1.0

func (d *PackageRouteDefaults) RouteCount() int

RouteCount is the number of routes in the default set.

type PackageRouteModes added in v4.1.0

type PackageRouteModes struct {
	PackageID      string `json:"package_id"`
	InstallationID uint   `json:"installation_id"`
	Version        string `json:"version"`
	Enabled        bool   `json:"enabled"`
	ConfigRevision int64  `json:"config_revision"`
	Error          string `json:"error,omitempty"`
	// Defaults is how the rehearsed default set applies to the package.
	Defaults PackageRouteDefaultState `json:"defaults"`
	Routes   []RouteModeEntry         `json:"routes"`
}

PackageRouteModes is one package's routes and their modes.

type PackageStorageGrants added in v4.1.0

type PackageStorageGrants struct {
	Storage bool
	// AdoptTables are existing tables the package role may read and write.
	AdoptTables []string
	// Views are kernel API views the package role may read.
	Views []string
}

PackageStorageGrants is what the storage capabilities of a verified manifest ask the kernel for.

func EffectiveStorageGrants added in v4.1.0

func EffectiveStorageGrants(db *gorm.DB, grants PackageStorageGrants) (PackageStorageGrants, error)

EffectiveStorageGrants are the grants of a verified manifest the kernel honours now: an adoption of a finalized-adoptable table only once its split is finalized (nodesecrets.AdoptionAllowed), and a view of the split's remainder only once it exists (packagestore.FinalizedViewAvailable). What it leaves out is left out of the lease, as if not declared; the package's native routes for it stay legacy (mode_unsupported).

func StorageGrants added in v4.1.0

func StorageGrants(manifest PluginManifest) PackageStorageGrants

StorageGrants extracts the storage grants from a manifest whose capabilities already passed validation.

type PanelAdminTunnelItem

type PanelAdminTunnelItem struct {
	ID            uint    `json:"id"`
	Name          string  `json:"name"`
	InNodeID      uint    `json:"inNodeId"`
	OutNodeID     *uint   `json:"outNodeId"`
	Type          int     `json:"type"`
	Flow          int     `json:"flow"`
	TrafficRatio  float64 `json:"trafficRatio"`
	InterfaceName string  `json:"interfaceName"`
	Protocol      string  `json:"protocol"`
	TCPListenAddr string  `json:"tcpListenAddr"`
	UDPListenAddr string  `json:"udpListenAddr"`
	InIP          string  `json:"inIp"`
	OutIP         string  `json:"outIp"`
	Status        int     `json:"status"`
	CreatedTime   int64   `json:"createdTime"`
	UpdatedTime   int64   `json:"updatedTime"`
}

type PanelForwardFlowData

type PanelForwardFlowData struct {
	N string `json:"n"`
	U int64  `json:"u"`
	D int64  `json:"d"`
}

type PanelForwardInput

type PanelForwardInput struct {
	Name          string `json:"name"`
	TunnelID      uint   `json:"tunnelId"`
	InPort        *int   `json:"inPort"`
	RemoteAddr    string `json:"remoteAddr"`
	InterfaceName string `json:"interfaceName"`
	Strategy      string `json:"strategy"`
}

type PanelForwardListItem

type PanelForwardListItem struct {
	ID                  uint   `json:"id"`
	Name                string `json:"name"`
	TunnelID            uint   `json:"tunnelId"`
	TunnelName          string `json:"tunnelName"`
	InIP                string `json:"inIp"`
	InPort              int    `json:"inPort"`
	RemoteAddr          string `json:"remoteAddr"`
	InterfaceName       string `json:"interfaceName"`
	Strategy            string `json:"strategy"`
	Status              int    `json:"status"`
	InFlow              int64  `json:"inFlow"`
	OutFlow             int64  `json:"outFlow"`
	RuntimeBackend      string `json:"runtimeBackend"`
	RuntimeStatus       int    `json:"runtimeStatus"`
	RuntimeMessage      string `json:"runtimeMessage"`
	LastRuntimeSyncTime int64  `json:"lastRuntimeSyncTime"`
	CreatedTime         int64  `json:"createdTime"`
	UpdatedTime         int64  `json:"updatedTime"`
	UserName            string `json:"userName"`
	UserID              uint   `json:"userId"`
	Inx                 int    `json:"inx"`
}

type PanelForwardOrderUpdate

type PanelForwardOrderUpdate struct {
	ID  uint `json:"id"`
	Inx int  `json:"inx"`
}

type PanelForwardRuntimeAttachmentState

type PanelForwardRuntimeAttachmentState struct {
	Model       string `json:"model"`
	Description string `json:"description"`
}

type PanelForwardRuntimeCommandHints

type PanelForwardRuntimeCommandHints struct {
	PowerShell []string `json:"powerShell"`
	Bash       []string `json:"bash"`
	Upgrade    []string `json:"upgrade"`
	References []string `json:"references"`
}

type PanelForwardRuntimeConfigState

type PanelForwardRuntimeConfigState struct {
	Backend           string `json:"backend"`
	NodeXMode         bool   `json:"nodeXMode"`
	BaseURL           string `json:"baseUrl,omitempty"`
	BaseURLConfigured bool   `json:"baseUrlConfigured"`
	TokenConfigured   bool   `json:"tokenConfigured"`
	TimeoutSeconds    int64  `json:"timeoutSeconds"`
}

type PanelForwardRuntimeDoctorSummary

type PanelForwardRuntimeDoctorSummary struct {
	PanelForwardRuntimeStatusSummary
	Commands PanelForwardRuntimeCommandHints `json:"commands"`
}

type PanelForwardRuntimeJobExecutor

type PanelForwardRuntimeJobExecutor struct {
	// contains filtered or unexported fields
}

func NewPanelForwardRuntimeJobExecutor

func NewPanelForwardRuntimeJobExecutor(db *gorm.DB) *PanelForwardRuntimeJobExecutor

func (*PanelForwardRuntimeJobExecutor) RunPendingJobs

func (e *PanelForwardRuntimeJobExecutor) RunPendingJobs(ctx context.Context) error

func (*PanelForwardRuntimeJobExecutor) Start

type PanelForwardRuntimeProbe

type PanelForwardRuntimeProbe struct {
	OK         bool   `json:"ok"`
	StatusCode int    `json:"statusCode,omitempty"`
	Body       string `json:"body,omitempty"`
	Error      string `json:"error,omitempty"`
}

type PanelForwardRuntimeReadiness

type PanelForwardRuntimeReadiness struct {
	Ready  bool   `json:"ready"`
	Reason string `json:"reason,omitempty"`
}

type PanelForwardRuntimeService

type PanelForwardRuntimeService struct {
	// contains filtered or unexported fields
}

func NewPanelForwardRuntimeService

func NewPanelForwardRuntimeService(db *gorm.DB) *PanelForwardRuntimeService

func (*PanelForwardRuntimeService) Apply

func (s *PanelForwardRuntimeService) Apply(ctx context.Context, action string, forward *model.Forward, tunnel *model.ForwardTunnel) (*panelForwardRuntimeResult, error)

func (*PanelForwardRuntimeService) BackendForForward added in v4.1.0

func (s *PanelForwardRuntimeService) BackendForForward(forward *model.Forward) (string, error)

BackendForForward answers the runtime backend a forward's change runs on: the forward's own, else the backend in force.

func (*PanelForwardRuntimeService) DiagnoseLocalOperator

func (*PanelForwardRuntimeService) DiagnoseNodeXOperator

func (*PanelForwardRuntimeService) DiagnosePanelRuntime

func (*PanelForwardRuntimeService) ForwardsToMoveToBackend added in v4.1.0

func (s *PanelForwardRuntimeService) ForwardsToMoveToBackend(targetBackend string) ([]model.Forward, error)

SyncForwardsToBackend re-synces all active forwards to the current backend. Used when the admin switches between NodeX and Local mode. ForwardsToMoveToBackend lists the active forwards not on targetBackend, oldest first: what the backend sync re-applies.

func (*PanelForwardRuntimeService) GetLocalOperatorStatus

func (*PanelForwardRuntimeService) GetNodeXOperatorStatus

func (*PanelForwardRuntimeService) GetNodeXRuntimeStatus

func (s *PanelForwardRuntimeService) GetNodeXRuntimeStatus(ctx context.Context) (*nodeXForwardRuntimeStatus, error)

func (*PanelForwardRuntimeService) GetPanelRuntimeStatus

func (*PanelForwardRuntimeService) MarkForwardsPendingForBackend

func (s *PanelForwardRuntimeService) MarkForwardsPendingForBackend(targetBackend string) (int64, error)

MarkForwardsPendingForBackend resets forwards that need re-sync to pending status. Call this after a backend switch to mark mismatched forwards for later re-sync.

func (*PanelForwardRuntimeService) MoveForwardToBackend added in v4.1.0

func (s *PanelForwardRuntimeService) MoveForwardToBackend(forwardID uint, backend string) error

MoveForwardToBackend records that a forward now runs on backend; the change is applied by the next sync of the forward.

func (*PanelForwardRuntimeService) ResolveBackend added in v4.1.0

func (s *PanelForwardRuntimeService) ResolveBackend() (string, error)

ResolveBackend answers the runtime backend in force.

func (*PanelForwardRuntimeService) SyncForwardsToBackend

func (s *PanelForwardRuntimeService) SyncForwardsToBackend(targetBackend string) (synced int, failed int, err error)

type PanelForwardRuntimeStatusSnapshot

type PanelForwardRuntimeStatusSnapshot struct {
	OK           bool                             `json:"ok"`
	StatusCode   int                              `json:"statusCode,omitempty"`
	Error        string                           `json:"error,omitempty"`
	Version      string                           `json:"version,omitempty"`
	AuthRequired *bool                            `json:"authRequired,omitempty"`
	ExecutePath  string                           `json:"executePath,omitempty"`
	StatusPath   string                           `json:"statusPath,omitempty"`
	Supports     *nodeXForwardRuntimeSupportState `json:"supports,omitempty"`
	Modes        *nodeXForwardRuntimeModes        `json:"modes,omitempty"`
	Issues       []string                         `json:"issues,omitempty"`
}

type PanelForwardRuntimeStatusSummary

type PanelForwardRuntimeStatusSummary struct {
	BaseURL       string                             `json:"baseUrl,omitempty"`
	CheckedAt     string                             `json:"checkedAt"`
	Config        PanelForwardRuntimeConfigState     `json:"config"`
	Attachment    PanelForwardRuntimeAttachmentState `json:"attachment"`
	Reachability  PanelForwardRuntimeReadiness       `json:"reachability"`
	RuntimeReady  PanelForwardRuntimeReadiness       `json:"runtimeReady"`
	Summary       string                             `json:"summary,omitempty"`
	Warnings      []string                           `json:"warnings,omitempty"`
	Health        PanelForwardRuntimeProbe           `json:"health"`
	RuntimeStatus PanelForwardRuntimeStatusSnapshot  `json:"runtimeStatus"`
	LocalAnsible  *nodeXForwardRuntimeAnsibleStatus  `json:"localAnsible,omitempty"`
}

type PanelForwardService

type PanelForwardService struct {
	// contains filtered or unexported fields
}

func NewPanelForwardService

func NewPanelForwardService(db *gorm.DB) *PanelForwardService

func (*PanelForwardService) ActiveTunnelForwardIDs added in v4.1.0

func (s *PanelForwardService) ActiveTunnelForwardIDs(tunnelID uint) ([]uint, error)

ActiveTunnelForwardIDs lists the active forwards of a tunnel, oldest first: what a tunnel change re-applies.

func (*PanelForwardService) ApplyForwardRuntime added in v4.1.0

func (s *PanelForwardService) ApplyForwardRuntime(ctx context.Context, record *model.Forward, action string) (*ForwardRuntimeOutcome, error)

ApplyForwardRuntime applies one action for a forward on its node through the runtime backend in force (NodeX synchronously, or a local Ansible or clean agent job) and records the runtime columns of the forward's row, as every legacy forward change does. The legacy routes and the KernelNodeOps forward.apply executor run it alike; the status column is the caller's.

func (*PanelForwardService) ApplyForwardTrafficSnapshots

func (s *PanelForwardService) ApplyForwardTrafficSnapshots(records []PanelForwardTrafficSnapshot) error

func (*PanelForwardService) AssignUserTunnel

func (s *PanelForwardService) AssignUserTunnel(input PanelUserTunnelInput) error

func (*PanelForwardService) CreateTunnel

func (*PanelForwardService) DeleteTunnel

func (s *PanelForwardService) DeleteTunnel(id uint) error

func (*PanelForwardService) DiagnoseForwardTargets added in v4.1.0

func (s *PanelForwardService) DiagnoseForwardTargets(ctx context.Context, probes DiagnosisProbes, forwardID uint, publicOnly bool) (*DiagnosisReport, error)

DiagnoseForwardTargets diagnoses forward forwardID from Control for the kernel: the forward is not scoped to an actor, and with publicOnly only public targets are dialled, as for a user.

func (*PanelForwardService) DiagnoseLocalOperator

func (s *PanelForwardService) DiagnoseLocalOperator(ctx context.Context) (*PanelForwardRuntimeDoctorSummary, error)

func (*PanelForwardService) DiagnoseNodeXOperator

func (s *PanelForwardService) DiagnoseNodeXOperator(ctx context.Context) (*PanelForwardRuntimeDoctorSummary, error)

func (*PanelForwardService) DiagnoseRuntime

func (*PanelForwardService) DiagnoseTunnel

func (s *PanelForwardService) DiagnoseTunnel(id uint) (*TunnelDiagnosisReport, error)

DiagnoseTunnel diagnoses a tunnel from Control. It is DiagnoseTunnelContext without a deadline.

func (*PanelForwardService) DiagnoseTunnelContext added in v4.1.0

func (s *PanelForwardService) DiagnoseTunnelContext(ctx context.Context, probes DiagnosisProbes, id uint) (*TunnelDiagnosisReport, error)

DiagnoseTunnelContext diagnoses a tunnel from Control: it dials the tunnel's execution node (job backends), or its entry node and, for a two-hop tunnel, its exit node. When ctx ends it returns the probes that completed and ctx's error.

func (*PanelForwardService) GetForwardWithTunnel added in v4.1.0

func (s *PanelForwardService) GetForwardWithTunnel(id uint) (*model.Forward, error)

GetForwardWithTunnel loads a panel forward with its tunnel, for the runtime: ErrPanelForwardNotFound when there is none.

func (*PanelForwardService) GetLocalOperatorStatus

func (s *PanelForwardService) GetLocalOperatorStatus(ctx context.Context) (*PanelForwardRuntimeStatusSummary, error)

func (*PanelForwardService) GetNodeXOperatorStatus

func (s *PanelForwardService) GetNodeXOperatorStatus(ctx context.Context) (*PanelForwardRuntimeStatusSummary, error)

func (*PanelForwardService) GetRuntimeStatus

func (*PanelForwardService) ListAdminTunnels

func (s *PanelForwardService) ListAdminTunnels() ([]PanelAdminTunnelItem, error)

func (*PanelForwardService) ListForwards

func (s *PanelForwardService) ListForwards(userID uint, isAdmin bool) ([]PanelForwardListItem, error)

func (*PanelForwardService) ListTunnels

func (s *PanelForwardService) ListTunnels(userID uint, isAdmin bool) ([]PanelTunnelListItem, error)

func (*PanelForwardService) ListUserTunnels

func (*PanelForwardService) RecordForwardTraffic

func (s *PanelForwardService) RecordForwardTraffic(records []PanelForwardTrafficRecord) error

func (*PanelForwardService) ResetUserTunnelTraffic

func (s *PanelForwardService) ResetUserTunnelTraffic(id uint) error

func (*PanelForwardService) RuntimeService

func (s *PanelForwardService) RuntimeService() *PanelForwardRuntimeService

func (*PanelForwardService) UpdateOrder

func (s *PanelForwardService) UpdateOrder(userID uint, isAdmin bool, updates []PanelForwardOrderUpdate) error

func (*PanelForwardService) UploadFluxForwardFlow

func (s *PanelForwardService) UploadFluxForwardFlow(data PanelForwardFlowData) error

type PanelForwardTrafficRecord

type PanelForwardTrafficRecord struct {
	ForwardID uint  `json:"forwardId"`
	Upload    int64 `json:"upload"`
	Download  int64 `json:"download"`
}

type PanelForwardTrafficSnapshot

type PanelForwardTrafficSnapshot struct {
	ForwardID     uint   `json:"forwardId"`
	Backend       string `json:"backend"`
	UploadTotal   int64  `json:"uploadTotal"`
	DownloadTotal int64  `json:"downloadTotal"`
}

type PanelForwardUpdateInput

type PanelForwardUpdateInput struct {
	ID            uint   `json:"id"`
	UserID        uint   `json:"userId"`
	Name          string `json:"name"`
	TunnelID      uint   `json:"tunnelId"`
	InPort        *int   `json:"inPort"`
	RemoteAddr    string `json:"remoteAddr"`
	InterfaceName string `json:"interfaceName"`
	Strategy      string `json:"strategy"`
}

type PanelRuntimeJobFilter

type PanelRuntimeJobFilter struct {
	Backend   string
	Status    *int
	ForwardID *uint
	Limit     int
}

type PanelTunnelInput

type PanelTunnelInput struct {
	Name          string   `json:"name"`
	InNodeID      uint     `json:"inNodeId"`
	OutNodeID     *uint    `json:"outNodeId"`
	Type          int      `json:"type"`
	Flow          int      `json:"flow"`
	TrafficRatio  *float64 `json:"trafficRatio"`
	InterfaceName string   `json:"interfaceName"`
	Protocol      string   `json:"protocol"`
	TCPListenAddr string   `json:"tcpListenAddr"`
	UDPListenAddr string   `json:"udpListenAddr"`
}

type PanelTunnelListItem

type PanelTunnelListItem struct {
	ID            uint   `json:"id"`
	Name          string `json:"name"`
	IP            string `json:"ip"`
	InIP          string `json:"inIp"`
	InNodePortSta *int   `json:"inNodePortSta"`
	InNodePortEnd *int   `json:"inNodePortEnd"`
	Type          int    `json:"type"`
	Protocol      string `json:"protocol"`
	Status        int    `json:"status"`
}

type PanelTunnelUpdateInput

type PanelTunnelUpdateInput struct {
	ID            uint     `json:"id"`
	Name          string   `json:"name"`
	Flow          int      `json:"flow"`
	TrafficRatio  *float64 `json:"trafficRatio"`
	InterfaceName string   `json:"interfaceName"`
	Protocol      string   `json:"protocol"`
	TCPListenAddr string   `json:"tcpListenAddr"`
	UDPListenAddr string   `json:"udpListenAddr"`
}

type PanelUserTunnelDetailItem

type PanelUserTunnelDetailItem struct {
	ID             uint   `json:"id"`
	UserID         uint   `json:"userId"`
	TunnelID       uint   `json:"tunnelId"`
	Flow           int64  `json:"flow"`
	Num            int    `json:"num"`
	FlowResetTime  int64  `json:"flowResetTime"`
	ExpTime        int64  `json:"expTime"`
	SpeedID        *uint  `json:"speedId"`
	SpeedLimitName string `json:"speedLimitName"`
	Speed          int64  `json:"speed"`
	TunnelName     string `json:"tunnelName"`
	TunnelFlow     int    `json:"tunnelFlow"`
	InFlow         int64  `json:"inFlow"`
	OutFlow        int64  `json:"outFlow"`
	Status         int    `json:"status"`
}

type PanelUserTunnelInput

type PanelUserTunnelInput struct {
	UserID        uint  `json:"userId"`
	TunnelID      uint  `json:"tunnelId"`
	Flow          int64 `json:"flow"`
	Num           int   `json:"num"`
	FlowResetTime int64 `json:"flowResetTime"`
	ExpTime       int64 `json:"expTime"`
	SpeedID       *uint `json:"speedId"`
}

type PanelUserTunnelQueryInput

type PanelUserTunnelQueryInput struct {
	UserID uint `json:"userId"`
}

type PanelUserTunnelUpdateInput

type PanelUserTunnelUpdateInput struct {
	ID            uint  `json:"id"`
	Flow          int64 `json:"flow"`
	Num           int   `json:"num"`
	FlowResetTime int64 `json:"flowResetTime"`
	ExpTime       int64 `json:"expTime"`
	Status        int   `json:"status"`
	SpeedID       *uint `json:"speedId"`
}

type PaymentGatewayService

type PaymentGatewayService struct {
	// contains filtered or unexported fields
}

PaymentGatewayService 支付网关服务

func NewPaymentGatewayService

func NewPaymentGatewayService(db *gorm.DB) *PaymentGatewayService

NewPaymentGatewayService 创建服务

func (*PaymentGatewayService) CalculateFee

func (s *PaymentGatewayService) CalculateFee(gateway *model.PaymentGateway, amount float64) float64

CalculateFee 计算手续费

func (*PaymentGatewayService) CheckOrderPayable added in v4.1.0

func (s *PaymentGatewayService) CheckOrderPayable(orderID, userID uint, amount float64) error

CheckOrderPayable checks that a payment of amount (yuan) that userID creates for orderID pays it: the order is the caller's, still pending, and the amount is its total. A payment record names the order it pays, and a successful callback marks that order paid and activates it, so a record may not name another user's order or pay less than its total.

func (*PaymentGatewayService) Create

func (s *PaymentGatewayService) Create(gateway *model.PaymentGateway) error

Create 创建网关

func (*PaymentGatewayService) CreateRecord

func (s *PaymentGatewayService) CreateRecord(record *model.PaymentRecord) error

CreateRecord 创建支付记录

func (*PaymentGatewayService) Delete

func (s *PaymentGatewayService) Delete(id uint) error

Delete 删除网关

func (*PaymentGatewayService) FinishPaidOrder added in v4.1.0

func (s *PaymentGatewayService) FinishPaidOrder(record model.PaymentRecord)

FinishPaidOrder applies a payment record that is already paid to its order, once: a repeated callback calls it, so that a payment recorded without its order completion (the payment module records the payment, then calls KernelOrder; a failure in between leaves the order pending) converges whichever side serves the repeat. A record applied before, or not paid, or without an order, changes nothing.

func (*PaymentGatewayService) GenerateTradeNo

func (s *PaymentGatewayService) GenerateTradeNo() string

GenerateTradeNo 生成商户订单号

func (*PaymentGatewayService) GetByID

GetByID 根据ID获取网关

func (*PaymentGatewayService) GetByType

func (s *PaymentGatewayService) GetByType(gatewayType string) (*model.PaymentGateway, error)

GetByType 根据类型获取网关

func (*PaymentGatewayService) GetChannels

func (s *PaymentGatewayService) GetChannels() ([]*model.PaymentChannel, error)

GetChannels 获取支付渠道 (前端显示用)

func (*PaymentGatewayService) GetEnabled

func (s *PaymentGatewayService) GetEnabled() ([]*model.PaymentGateway, error)

GetEnabled 获取启用的网关

func (*PaymentGatewayService) GetRecordByTradeNo

func (s *PaymentGatewayService) GetRecordByTradeNo(tradeNo string) (*model.PaymentRecord, error)

GetRecordByTradeNo 根据订单号获取记录

func (*PaymentGatewayService) GetStats

func (s *PaymentGatewayService) GetStats(start, end time.Time) (*PaymentStats, error)

GetStats 获取支付统计

func (*PaymentGatewayService) GetUserRecords

func (s *PaymentGatewayService) GetUserRecords(userID uint, page, pageSize int) ([]*model.PaymentRecord, int64, error)

GetUserRecords 获取用户支付记录

func (*PaymentGatewayService) List

List 获取网关列表

func (*PaymentGatewayService) ListRecords

func (s *PaymentGatewayService) ListRecords(page, pageSize int, status *int, gatewayType string) ([]*model.PaymentRecord, int64, error)

ListRecords 获取支付记录列表

func (*PaymentGatewayService) MarkOrderPaid

func (s *PaymentGatewayService) MarkOrderPaid(tradeNo string, gatewayTradeNo string, notifyData string) error

MarkOrderPaid 标记支付并更新订单状态 (事务)

func (*PaymentGatewayService) MarkOrderPaidIfCovered added in v4.1.0

func (s *PaymentGatewayService) MarkOrderPaidIfCovered(tradeNo string, gatewayTradeNo string, notifyData string, covers func(model.PaymentRecord) error) error

MarkOrderPaidIfCovered marks the payment paid, as MarkOrderPaid does, only if covers accepts its pending record, read inside the transaction. covers returns an error wrapping ErrPaymentNotCovered to refuse it.

func (*PaymentGatewayService) MarkOrderPaidWithAmount

func (s *PaymentGatewayService) MarkOrderPaidWithAmount(tradeNo string, gatewayTradeNo string, notifyData string, paidAmount *float64) error

MarkOrderPaidWithAmount 标记支付并核对回调金额。

func (*PaymentGatewayService) ParseConfig

func (s *PaymentGatewayService) ParseConfig(gateway *model.PaymentGateway) (any, error)

ParseConfig 解析网关配置

func (*PaymentGatewayService) Toggle

func (s *PaymentGatewayService) Toggle(id uint, enabled bool) error

Toggle 切换网关状态

func (*PaymentGatewayService) Update

func (s *PaymentGatewayService) Update(gateway *model.PaymentGateway) error

Update 更新网关

func (*PaymentGatewayService) ValidateAmount

func (s *PaymentGatewayService) ValidateAmount(gateway *model.PaymentGateway, amount float64) error

ValidateAmount 验证金额范围

func (*PaymentGatewayService) ValidateGatewayUsable

func (s *PaymentGatewayService) ValidateGatewayUsable(gateway *model.PaymentGateway) error

ValidateGatewayUsable verifies that a gateway can be shown to users and used to create new payment records.

type PaymentStats

type PaymentStats struct {
	TotalAmount   float64 `json:"total_amount"`
	TotalCount    int64   `json:"total_count"`
	PendingAmount float64 `json:"pending_amount"`
	PendingCount  int64   `json:"pending_count"`
}

PaymentStats 支付统计

type PlanService

type PlanService struct {
	// contains filtered or unexported fields
}

PlanService 管理套餐

func NewPlanService

func NewPlanService() *PlanService

NewPlanService 创建 PlanService

func (*PlanService) AssignToUser

func (s *PlanService) AssignToUser(planID, userID uint, expireAt *int64, requestID string) error

AssignToUser 将套餐分配给用户(同步修改用户记录),并写事件。requestID (PlanAssignmentRequestID) applies a retried assignment once; the event is written again.

func (*PlanService) Create

func (s *PlanService) Create(p *model.Plan) error

Create 创建套餐并写入事件

func (*PlanService) Delete

func (s *PlanService) Delete(id uint) error

Delete 删除套餐并写事件

func (*PlanService) Get

func (s *PlanService) Get(id uint) (*model.Plan, error)

Get 获取单个套餐

func (*PlanService) List

func (s *PlanService) List() ([]*model.Plan, error)

List 获取所有套餐

func (*PlanService) Update

func (s *PlanService) Update(p *model.Plan) error

Update 更新套餐并写事件

type PluginCompatibilityRoutes

type PluginCompatibilityRoutes struct {
	Path   string `json:"path"`
	SHA256 string `json:"sha256"`
}

type PluginConfigurationSemanticValidator

type PluginConfigurationSemanticValidator func(pluginID, version string, canonicalConfig json.RawMessage) error

PluginConfigurationSemanticValidator supplies optional, version-bound package semantics that cannot be expressed safely in JSON Schema.

type PluginConfigurationTxHook

type PluginConfigurationTxHook func(tx *gorm.DB, installation model.PluginInstallation, configuration model.PluginConfiguration) error

type PluginControlRouteResolution

type PluginControlRouteResolution struct {
	PluginID       string `json:"plugin_id"`
	Version        string `json:"version"`
	InstallationID uint   `json:"installation_id"`
	Generation     uint64 `json:"generation"`
	Route          string `json:"route"`
	MatchedRoute   string `json:"matched_route"`
	Permission     string `json:"permission"`
}

func ResolvePluginControlRoute

func ResolvePluginControlRoute(db *gorm.DB, publicKey ed25519.PublicKey, pluginID, requestPath string, actorID uint, legacyAdmin bool) (*PluginControlRouteResolution, error)

type PluginDependencyExecutionPlan

type PluginDependencyExecutionPlan struct {
	Target string                          `json:"target"`
	RootID string                          `json:"root_id"`
	Order  []string                        `json:"order"`
	Steps  []PluginDependencyExecutionStep `json:"steps"`
}

PluginDependencyExecutionPlan is the dependency-first closure used by the Control lifecycle worker. Unlike the compatibility resolver, it can select the latest registered target-compatible release for a missing dependency so an administrator can enable one root package without first hand-installing every package in its closure.

An existing installation always pins its desired version. That makes an upgrade deterministic and prevents a newly published dependency release from silently changing an already configured package.

func ResolvePluginDependencyExecutionPlan

func ResolvePluginDependencyExecutionPlan(db *gorm.DB, rootRelease model.PluginRelease, target string) (*PluginDependencyExecutionPlan, error)

ResolvePluginDependencyExecutionPlan resolves a root release and all of its transitive dependencies into a stable dependency-first execution plan. It validates the selected stored manifests, including registered trust roots, but does not mutate installation intent.

type PluginDependencyExecutionStep

type PluginDependencyExecutionStep struct {
	PluginID     string                    `json:"plugin_id"`
	Release      model.PluginRelease       `json:"release"`
	Manifest     PluginManifest            `json:"manifest"`
	Installation *model.PluginInstallation `json:"installation,omitempty"`
}

PluginDependencyExecutionStep binds one graph vertex to the immutable release that an execution plan will use. Installation is nil when the package has not been installed on the target yet.

type PluginDependencyResolution

type PluginDependencyResolution struct {
	Roots   []string         `json:"roots"`
	Order   []string         `json:"order"`
	Ordered []PluginManifest `json:"ordered"`
}

PluginDependencyResolution is the deterministic execution plan for a set of selected plugin releases. Ordered and Order contain the same nodes; the former is convenient for lifecycle execution and the latter is convenient for persistence, logging, and test assertions.

Dependencies always precede their dependents. Nodes that are otherwise unrelated are ordered by plugin ID, making the result independent of map, input-slice, and manifest-declaration order.

func ResolvePluginDependencyGraph

func ResolvePluginDependencyGraph(manifests []PluginManifest, rootIDs []string) (*PluginDependencyResolution, error)

ResolvePluginDependencyGraph resolves root plugin IDs from a selected manifest set. The returned order is a stable dependency-first topological order. A shared dependency is emitted once, even when multiple roots depend on it.

func ResolvePluginInstallationDependencyGraph

func ResolvePluginInstallationDependencyGraph(db *gorm.DB, rootRelease model.PluginRelease, target string) (*PluginDependencyResolution, error)

ResolvePluginInstallationDependencyGraph loads the selected desired release for every dependency from the Control database, verifies the stored release metadata, and then delegates graph validation/order to ResolvePluginDependencyGraph. A dependency installation may be disabled: this function is a preflight graph check, not an enable operation. The existing ValidatePluginInstallationPlan check still enforces that every dependency is enabled before production intent is accepted.

Registered releases carry a trust-root fingerprint and are re-verified from the stored trust-root row. Rows created by the pre-signature 3.x migration without a fingerprint retain the existing compatibility behavior and are checked through DecodePluginReleaseManifest.

type PluginDependencyResolver

type PluginDependencyResolver struct {
	// contains filtered or unexported fields
}

PluginDependencyResolver indexes one selected release per plugin ID. A release catalog may contain many versions, but callers must select the version to execute before constructing this resolver.

func NewPluginDependencyResolver

func NewPluginDependencyResolver(manifests []PluginManifest) (*PluginDependencyResolver, error)

NewPluginDependencyResolver validates and indexes the selected manifests. It intentionally performs graph relationship validation here, while full signature/artifact validation remains the responsibility of RegisterPluginRelease/VerifyStoredPluginRelease.

func (*PluginDependencyResolver) Resolve

Resolve computes the dependency closure and a deterministic topological order for rootIDs.

type PluginEntrypoint

type PluginEntrypoint struct {
	Path   string `json:"path"`
	SHA256 string `json:"sha256"`
}

type PluginInstallationStatus

type PluginInstallationStatus struct {
	ID                  uint       `json:"id"`
	PluginID            string     `json:"plugin_id"`
	Target              string     `json:"target"`
	DesiredVersion      string     `json:"desired_version"`
	ObservedVersion     string     `json:"observed_version"`
	PreviousVersion     string     `json:"previous_version"`
	State               string     `json:"state"`
	Health              string     `json:"health"`
	Enabled             bool       `json:"enabled"`
	LifecycleGeneration int64      `json:"lifecycle_generation"`
	ConfigRevision      int64      `json:"config_revision"`
	HasError            bool       `json:"has_error"`
	LastError           string     `json:"last_error,omitempty"`
	CreatedAt           time.Time  `json:"created_at"`
	UpdatedAt           time.Time  `json:"updated_at"`
	DisabledAt          *time.Time `json:"disabled_at,omitempty"`
}

PluginInstallationStatus is the public projection of a plugin installation. Installation LastError can originate in a Control executor or from an Agent lifecycle result, so it is reduced to a stable state summary before it crosses an HTTP boundary.

func PublicPluginInstallation

func PublicPluginInstallation(installation model.PluginInstallation) PluginInstallationStatus

PublicPluginInstallation keeps package lifecycle metadata useful to the WebUI while withholding raw executor and Agent diagnostics.

type PluginManifest

type PluginManifest struct {
	ID                  string                     `json:"id"`
	Name                string                     `json:"name"`
	Version             string                     `json:"version"`
	APIVersion          string                     `json:"api_version"`
	Publisher           string                     `json:"publisher"`
	Targets             []string                   `json:"targets"`
	Architectures       []string                   `json:"architectures"`
	ArtifactSHA256      string                     `json:"artifact_sha256"`
	Capabilities        []string                   `json:"capabilities"`
	Dependencies        []string                   `json:"dependencies"`
	Conflicts           []string                   `json:"conflicts"`
	Permissions         []string                   `json:"permissions"`
	ConfigSchema        json.RawMessage            `json:"config_schema"`
	SecretFields        []string                   `json:"secret_fields"`
	Entrypoints         map[string]string          `json:"entrypoints"`
	Migration           int64                      `json:"migration_version"`
	ControlRoutes       []string                   `json:"control_routes"`
	FrontendSHA256      string                     `json:"frontend_sha256"`
	WebUI               *PluginWebUI               `json:"webui,omitempty"`
	ControlEntrypoint   *PluginEntrypoint          `json:"control_entrypoint,omitempty"`
	AgentEntrypoint     *PluginEntrypoint          `json:"agent_entrypoint,omitempty"`
	Migrations          *PluginMigrations          `json:"migrations,omitempty"`
	CompatibilityRoutes *PluginCompatibilityRoutes `json:"compatibility_routes,omitempty"`
	RouteContractDigest string                     `json:"route_contract_digest,omitempty"`
	RuntimeAPIVersion   string                     `json:"runtime_api_version,omitempty"`
}

func DecodePluginReleaseManifest

func DecodePluginReleaseManifest(release model.PluginRelease) (*PluginManifest, error)

ValidatePluginReleaseTarget verifies the internally persisted release metadata before it is used to create desired state. Releases are immutable, so a mismatch here indicates corrupt or manually modified kernel data.

func VerifyPluginRelease

func VerifyPluginRelease(manifestJSON, signature string, publicKey ed25519.PublicKey) (*PluginManifest, error)

func VerifyStoredPluginRelease

func VerifyStoredPluginRelease(db *gorm.DB, release model.PluginRelease, fallback ed25519.PublicKey) (*PluginManifest, error)

func (PluginManifest) Validate

func (m PluginManifest) Validate() error

type PluginMigrations

type PluginMigrations struct {
	Index  string `json:"index"`
	SHA256 string `json:"sha256"`
}

type PluginWebUI

type PluginWebUI struct {
	Bundle      PluginWebUIBundle  `json:"bundle"`
	Permissions []string           `json:"permissions"`
	Menus       []PluginWebUIMenu  `json:"menus"`
	Routes      []PluginWebUIRoute `json:"routes"`
}

PluginWebUI describes a signed, declarative administrator extension. Bundle paths are relative to the verified plugin artifact; they are not URLs and are never fetched directly by the control kernel.

type PluginWebUIBundle

type PluginWebUIBundle struct {
	Path   string `json:"path"`
	SHA256 string `json:"sha256"`
}

type PluginWebUIMenu

type PluginWebUIMenu struct {
	ID         string `json:"id"`
	Parent     string `json:"parent"`
	Label      string `json:"label"`
	Icon       string `json:"icon"`
	Route      string `json:"route"`
	Permission string `json:"permission"`
	Order      int    `json:"order"`
}

type PluginWebUIRoute

type PluginWebUIRoute struct {
	ID         string `json:"id"`
	Path       string `json:"path"`
	Export     string `json:"export"`
	Permission string `json:"permission"`
}

type ProxyNodeCredentials added in v4.1.0

type ProxyNodeCredentials struct {
	APIKey string
	Secret string
}

ProxyNodeCredentials are a proxy node's API key and shared secret, as generated or typed; an empty one is not issued.

func GenerateProxyNodeCredentials added in v4.1.0

func GenerateProxyNodeCredentials(apiKey, secret bool) (ProxyNodeCredentials, error)

GenerateProxyNodeCredentials generates the credentials of a proxy node: 32 random bytes, hex encoded, each.

func (ProxyNodeCredentials) Apply added in v4.1.0

func (c ProxyNodeCredentials) Apply(node *model.Node)

Apply sets the credentials on a node row about to be written: the key, its hash and the secret.

type RawNodeConfigValidation added in v4.1.0

type RawNodeConfigValidation struct {
	Config map[string]any
	// Normalized is the configuration re-encoded, as the routes store it.
	Normalized []byte
	// Warnings are what the validation route answers besides validity.
	Warnings []string
}

RawNodeConfigValidation is a validated raw configuration.

func ValidateRawNodeConfig added in v4.1.0

func ValidateRawNodeConfig(raw any) (*RawNodeConfigValidation, error)

ValidateRawNodeConfig runs the raw configuration validators of the node routes: the configuration must be a JSON object (ErrRawNodeConfigNotObject or the decoding error), and a WireGuard configuration must be valid (ErrInvalidNodeProtocol). A configuration without server_port is valid with a warning.

type RegistrationPolicy

type RegistrationPolicy struct {
	Enabled             bool
	RequireInvite       bool
	AllowedEmailDomains []string
	BlockedEmailDomains []string
}

func ResolveRegistrationPolicy

func ResolveRegistrationPolicy(cfg *config.Config) RegistrationPolicy

type RetireCounts added in v4.1.0

type RetireCounts struct {
	CredentialsRevoked       int64
	SecretsDeleted           int64
	WireGuardPeersDeleted    int64
	ProtocolsDeleted         int64
	SubscriptionLinksDeleted int64
}

RetireCounts is what a retirement removed.

func RetireForwardNodeTx added in v4.1.0

func RetireForwardNodeTx(tx *gorm.DB, nodeID uint) (RetireCounts, error)

RetireForwardNodeTx removes what the kernel holds for a forward node being deleted: its token in the split table and its agent certificates and enrollments (node_deleted). The node row is the caller's.

func RetireProtocolTx added in v4.1.0

func RetireProtocolTx(tx *gorm.DB, protocolIDs ...uint) (RetireCounts, error)

RetireProtocolTx removes what goes with node protocols being deleted: their users' WireGuard peers, their subscription group links and their secrets in the split table. The protocol rows themselves are the caller's: the legacy deletion removes them in the same transaction, a package after its RetireProtocol succeeded.

func RetireProxyNodeTx added in v4.1.0

func RetireProxyNodeTx(tx *gorm.DB, nodeID uint) (RetireCounts, error)

RetireProxyNodeTx removes what the kernel holds for a proxy node being deleted: its protocols, with their peers, links and secrets (RetireProtocolTx), its credentials and raw configuration secrets in the split tables, and its agent certificates and enrollments (node_deleted). The node row is the caller's.

type RouteCatalogEntry added in v4.1.0

type RouteCatalogEntry struct {
	Method    string `json:"method"`
	Path      string `json:"path"`
	PackageID string `json:"package_id"`
	RouteID   string `json:"route_id"`
	Mode      string `json:"mode"`
	Reason    string `json:"reason,omitempty"`
}

RouteCatalogEntry is one route of config/package-extraction.json.

type RouteHostObservation added in v4.1.0

type RouteHostObservation struct {
	Mode           string `json:"mode"`
	Effective      string `json:"effective"`
	NativeTotal    uint64 `json:"native_total"`
	NativeErrors   uint64 `json:"native_errors"`
	ShadowTotal    uint64 `json:"shadow_total"`
	ShadowMismatch uint64 `json:"shadow_mismatch"`
	ShadowErrors   uint64 `json:"shadow_errors"`
	ShadowSkipped  uint64 `json:"shadow_skipped"`
	// MismatchRate is ShadowMismatch / ShadowTotal (0 without shadow
	// runs).
	MismatchRate float64 `json:"mismatch_rate"`
	// LastMismatchAt is when the host last saw a mismatch.
	LastMismatchAt *time.Time `json:"last_mismatch_at,omitempty"`
}

RouteHostObservation is what a package host reports about one route in its health details: the mode it applies and its native and shadow counters since the host started.

func (RouteHostObservation) WithRate added in v4.1.0

WithRate returns the observation with MismatchRate computed.

type RouteModeActor added in v4.1.0

type RouteModeActor struct {
	UserID    uint
	Name      string
	IP        string
	UserAgent string
}

RouteModeActor is who changes route modes, as the audit log and the revision history record it. UserID 0 is the command line.

type RouteModeAdmin added in v4.1.0

type RouteModeAdmin struct {
	DB        *gorm.DB
	PublicKey ed25519.PublicKey
	// Catalog defaults to PackageRouteCatalog.
	Catalog map[string]RouteCatalogEntry
	// Now defaults to time.Now.
	Now func() time.Time
}

RouteModeAdmin lists and switches the route modes of v2 Control packages. Switches go through SetPackageRouteModesTx, the configuration path the identity cutover uses, so every rule of the configuration check holds and the package hosts pick the change up at their next configuration poll. Each switch records its revisions and an audit entry in the same transaction.

func (*RouteModeAdmin) List added in v4.1.0

func (a *RouteModeAdmin) List(ctx context.Context, packageID string, hosts map[string]map[string]RouteHostObservation) ([]PackageRouteModes, error)

List returns the route modes of every Control installation, or of packageID's. A package whose compatibility routes cannot be verified is listed with Error. hosts, when not nil, adds each host's report by package and route.

func (*RouteModeAdmin) Revisions added in v4.1.0

func (a *RouteModeAdmin) Revisions(ctx context.Context, packageID string, limit int) ([]model.RouteModeRevision, error)

Revisions returns the latest route-mode revisions, newest first, of packageID or of every package.

func (*RouteModeAdmin) Rollback added in v4.1.0

func (a *RouteModeAdmin) Rollback(ctx context.Context, packageID, reason string, actor RouteModeActor) (RouteModeResult, error)

Rollback returns every route of a package to legacy in one change, except identity group A, which only the identity rollback switches. It works on the effective modes: a route that runs natively only by default (no stored mode) gets an explicit legacy, so it stays legacy whatever the default policy becomes. It needs no confirmation; the reason is optional.

func (*RouteModeAdmin) Set added in v4.1.0

Set switches routes of a package to request.Mode. Named routes must all be allowed to switch, or nothing changes; without names every route that may switch to the mode does, and the others are reported as skipped. Switching to native needs request.Confirm and a reason.

type RouteModeChange added in v4.1.0

type RouteModeChange struct {
	RouteID string `json:"route_id"`
	From    string `json:"from"`
	To      string `json:"to"`
}

RouteModeChange is one route's switch.

type RouteModeChangeRequest added in v4.1.0

type RouteModeChangeRequest struct {
	PackageID string
	Routes    []string
	Mode      string
	Reason    string
	// Confirm acknowledges a switch to native.
	Confirm bool
}

RouteModeChangeRequest asks for routes of a package to switch to Mode. No Routes means every route of the package that may switch to Mode.

type RouteModeEntry added in v4.1.0

type RouteModeEntry struct {
	RouteID   string `json:"route_id"`
	Method    string `json:"method"`
	Path      string `json:"path"`
	Transport string `json:"transport"`
	// Catalog is the route's package extraction mode ("" when the
	// extraction map does not list it).
	Catalog string `json:"catalog"`
	// Configured is the stored mode or, when the map does not name the
	// route, the default policy's (native for a route of the rehearsed
	// default set, legacy otherwise); Effective is what the kernel hands
	// the host (ResolveEffectivePackageRouteModes), which differs only for
	// identity group A. Stored is the raw stored mode ("" when none).
	Configured string `json:"configured"`
	Effective  string `json:"effective"`
	Stored     string `json:"stored,omitempty"`
	// Source is where the effective mode comes from: stored, default,
	// kill-switch, package-too-old, identity-authority or unset.
	Source       string   `json:"source"`
	AllowedModes []string `json:"allowed_modes"`
	Locked       string   `json:"locked"`
	LockedReason string   `json:"locked_reason"`
	// Host is the running host's report, when there is one.
	Host *RouteHostObservation `json:"host,omitempty"`
	// MismatchSamples summarizes the stored shadow mismatch samples of the
	// route (GET /api/v4/kernel/route-modes/mismatches), when there are
	// any.
	MismatchSamples *shadowsamples.Summary `json:"mismatch_samples,omitempty"`
}

RouteModeEntry is one compatibility route of a package and its modes.

type RouteModeResult added in v4.1.0

type RouteModeResult struct {
	GroupID        string            `json:"group_id,omitempty"`
	PackageID      string            `json:"package_id"`
	Action         string            `json:"action"`
	Mode           string            `json:"mode"`
	ConfigRevision int64             `json:"config_revision"`
	Changes        []RouteModeChange `json:"changes"`
	Skipped        []RouteModeSkip   `json:"skipped"`
}

RouteModeResult describes a committed change. A request that changes nothing has no GroupID and writes neither revisions nor an audit entry.

type RouteModeSkip added in v4.1.0

type RouteModeSkip struct {
	RouteID string `json:"route_id"`
	Reason  string `json:"reason"`
}

RouteModeSkip is a route a whole-package change left as it is.

type SecretDocumentOutcome added in v4.1.0

type SecretDocumentOutcome struct {
	// Full is the document as stored in the legacy column: every secret
	// in clear.
	Full string
	// Redacted is the document with the placeholder at every secret
	// position: what administrators' answers show, and what a package
	// stores in its own row.
	Redacted string
	// Kept counts the placeholders the incoming document sent, which keep
	// their stored values; Cleared the stored secrets the incoming
	// document no longer holds.
	Kept    int
	Cleared int
}

SecretDocumentOutcome is what PutNodeProtocolSecretDocumentTx and PutNodeRawConfigDocumentTx stored.

func PutNodeProtocolSecretDocumentTx added in v4.1.0

func PutNodeProtocolSecretDocumentTx(tx *gorm.DB, protocolID uint, column, document string) (SecretDocumentOutcome, error)

PutNodeProtocolSecretDocumentTx stores one secret column of a node protocol: the incoming document, with the stored value at every position it sends as the placeholder (KeepNodeSecretsJSON), is written to the column and the split table (putNodeProtocolColumnsTx), as the protocol update does for the columns it carries.

func PutNodeRawConfigDocumentTx added in v4.1.0

func PutNodeRawConfigDocumentTx(tx *gorm.DB, nodeID uint, document string) (SecretDocumentOutcome, error)

PutNodeRawConfigDocumentTx stores a proxy node's raw configuration, as PutNodeProtocolSecretDocumentTx stores a protocol column.

type ServerService

type ServerService struct {
	// contains filtered or unexported fields
}

ServerService 服务器服务

func NewServerService

func NewServerService() *ServerService

NewServerService 创建服务器服务

func (*ServerService) BatchRecordTrafficLog

func (s *ServerService) BatchRecordTrafficLog(serverType model.ServerType, serverID uint, traffics map[uint][2]int64, rate float64) error

BatchRecordTrafficLog 批量记录流量日志

func (*ServerService) BuildNodeConfig

func (s *ServerService) BuildNodeConfig(serverType model.ServerType, serverID uint) (map[string]any, error)

BuildNodeConfig 构建节点配置

func (*ServerService) GetAllUsersOnlineCount

func (s *ServerService) GetAllUsersOnlineCount() (map[string]int, error)

GetAllUsersOnlineCount 获取所有用户的在线IP数量

func (*ServerService) GetRoutesByIDs

func (s *ServerService) GetRoutesByIDs(ids []uint) ([]model.ServerRoute, error)

GetRoutesByIDs 根据ID列表获取路由

func (*ServerService) GetServerByTypeAndID

func (s *ServerService) GetServerByTypeAndID(serverType model.ServerType, serverID uint) (any, error)

GetServerByTypeAndID 根据类型和ID获取服务器

func (*ServerService) GetServerRate

func (s *ServerService) GetServerRate(serverType model.ServerType, serverID uint) float64

GetServerRate 获取服务器流量倍率

func (*ServerService) GetServerUsers

func (s *ServerService) GetServerUsers(serverType model.ServerType, serverID uint) ([]model.User, error)

GetServerUsers 获取服务器可用用户列表

func (*ServerService) GetUserOnlineCount

func (s *ServerService) GetUserOnlineCount(userID uint) (int64, error)

GetUserOnlineCount 获取用户在线IP数量(跨所有节点)

func (*ServerService) RecordAgentTrafficReport added in v4.1.0

func (s *ServerService) RecordAgentTrafficReport(nodeKind string, nodeID uint, batchID string, traffics map[uint][2]int64, rate float64) (bool, error)

RecordAgentTrafficReport applies a TrafficReport batch from the Agent Control stream (reports.v1) exactly as RecordNodeTrafficReport applies the legacy report of the same node, once per node and batch id: the batch record (internal/agentreports) is claimed in the transaction that applies the traffic. It reports whether this call applied the batch; false means a committed transaction applied it before, and nothing was counted again.

func (*ServerService) RecordNodeTrafficReport

func (s *ServerService) RecordNodeTrafficReport(serverType model.ServerType, nodeID uint, traffics map[uint][2]int64, rate float64) error

RecordNodeTrafficReport atomically applies a node traffic report. Node clients retry a report when the panel returns an error, so logs, node counters, daily/monthly stats, and user counters must commit together.

func (*ServerService) RecordServerStat

func (s *ServerService) RecordServerStat(serverType model.ServerType, serverID uint, upload, download int64, recordType string, recordAt int64) error

RecordServerStat 记录节点汇总统计,用于面板中的服务器统计表。 recordType: d=日统计, m=月统计。

func (*ServerService) RecordTrafficLog

func (s *ServerService) RecordTrafficLog(serverType model.ServerType, serverID uint, userID uint, upload, download int64, rate float64) error

RecordTrafficLog 记录流量日志

func (*ServerService) UpdateOnlineStatus

func (s *ServerService) UpdateOnlineStatus(serverType model.ServerType, serverID uint, userIPs map[uint][]string) error

UpdateOnlineStatus 更新用户在线状态

type SettingsAudit added in v4.1.0

type SettingsAudit int

SettingsAudit is the audit entry a write of the namespace records: the one the kernel's own handler records for the same change.

const (
	// SettingsAuditNone records nothing.
	SettingsAuditNone SettingsAudit = iota
	// SettingsAuditSystemConfig records a system_config entry per key, as
	// the system configuration handlers do.
	SettingsAuditSystemConfig
	// SettingsAuditBackupConfig records a backup_config entry, as the
	// backup configuration handler does.
	SettingsAuditBackupConfig
)

type SettingsAuditActor added in v4.1.0

type SettingsAuditActor struct {
	UserID    *uint
	Username  string
	IP        string
	UserAgent string
}

SettingsAuditActor is who changed a setting, as the audit trail records it. The kernel's handlers take it from the request's context; the KernelSettings contract from the calling package's Actor.

type SettingsNamespace added in v4.1.0

type SettingsNamespace struct {
	Name string
	// Keys are exact keys; Prefixes match the longer keys that start with
	// them.
	Keys     []string
	Prefixes []string
	Storage  SettingsStorage
	Audit    SettingsAudit
	// SecretKeys are secrets the kernel's key-name rule does not catch.
	SecretKeys []string
}

SettingsNamespace is one entry of the kernel's namespace table.

func LookupSettingsNamespace added in v4.1.0

func LookupSettingsNamespace(name string) (SettingsNamespace, bool)

LookupSettingsNamespace returns the namespace called name.

func SettingsNamespaceOf added in v4.1.0

func SettingsNamespaceOf(key string) (SettingsNamespace, bool)

SettingsNamespaceOf returns the namespace key belongs to.

func SettingsNamespaces added in v4.1.0

func SettingsNamespaces() []SettingsNamespace

SettingsNamespaces returns the namespace table.

func (SettingsNamespace) Contains added in v4.1.0

func (n SettingsNamespace) Contains(key string) bool

Contains reports whether key belongs to the namespace.

func (SettingsNamespace) Secret added in v4.1.0

func (n SettingsNamespace) Secret(key string) bool

Secret reports whether key's value is a secret: a name the kernel masks for its administrators, or a key the namespace declares secret.

type SettingsStorage added in v4.1.0

type SettingsStorage int

SettingsStorage is where a namespace's keys live.

const (
	// SettingsInSystemConfig keys are rows of v2_system_config.
	SettingsInSystemConfig SettingsStorage = iota
	// SettingsInBackupConfig keys are the fields of the first
	// v2_backup_config row.
	SettingsInBackupConfig
)

type SpeedLimitInput

type SpeedLimitInput struct {
	Name       string `json:"name"`
	Speed      int64  `json:"speed"`
	TunnelID   uint   `json:"tunnelId"`
	TunnelName string `json:"tunnelName"`
}

type SpeedLimitService

type SpeedLimitService struct {
	// contains filtered or unexported fields
}

func NewSpeedLimitService

func NewSpeedLimitService(db *gorm.DB) *SpeedLimitService

func (*SpeedLimitService) Create

func (*SpeedLimitService) Delete

func (s *SpeedLimitService) Delete(id uint) error

func (*SpeedLimitService) List

func (s *SpeedLimitService) List() ([]model.SpeedLimit, error)

type SpeedLimitUpdateInput

type SpeedLimitUpdateInput struct {
	ID uint `json:"id"`
	SpeedLimitInput
}

type StatsService

type StatsService struct {
	// contains filtered or unexported fields
}

StatsService 统计服务 (带缓存)

func NewStatsService

func NewStatsService() *StatsService

NewStatsService 创建统计服务

func NewStatsServiceOn added in v4.1.0

func NewStatsServiceOn(db *gorm.DB) *StatsService

NewStatsServiceOn returns a StatsService on db. The kernel contracts that answer the cached dashboard and subscription summaries (internal/kerneltelemetry, internal/kernelsubscriber) build one on their own database; the caches are the kernel's, shared with the legacy handlers whichever database handle reads them.

func (*StatsService) GetDashboardStats

func (s *StatsService) GetDashboardStats(forceRefresh bool) (*DashboardStats, error)

GetDashboardStats 获取仪表盘统计 (优先从缓存读取)

func (*StatsService) GetHourlyTraffic

func (s *StatsService) GetHourlyTraffic(hours int, userID uint) ([]HourlyTraffic, error)

GetHourlyTraffic 返回最近 hours 个整点小时的流量序列 (含当前未结束的小时), 数据源为 v2_server_log, 按 (log_at/3600) 分桶聚合 SUM((u+d)*rate)。 userID > 0 时只统计该用户的流量; userID == 0 时统计全局。 缺失的小时补零, 结果按时间升序返回, 便于前端直接绘制折线图。

func (*StatsService) GetNodeTraffic added in v4.2.0

func (s *StatsService) GetNodeTraffic(nodeID uint, granularity string, since, until time.Time) (*NodeTrafficSeries, error)

GetNodeTraffic returns one node's traffic series between since and until (see NodeTrafficBucketBounds for how they are aligned and bounded).

Hourly buckets sum the node's rows of v2_server_log, (u * rate) up and (d * rate) down, over every server type the node reported under (agent and legacy reports write "node", UniProxy the protocol name; the node id is the same). It reads the rows of one node in the window through idx_v2_server_log_server_id and idx_v2_server_log_log_at: cheaper than the global GetHourlyTraffic, which scans the window of every node, and bounded by the retention of the raw log (docs/reference/traffic-stats-operations.md). Daily buckets are the node's v2_stat_server day rows, which are kept and small, and are recorded with the rate applied already.

ErrNodeTrafficNode when the node does not exist.

func (*StatsService) GetTrafficLogMeta

func (s *StatsService) GetTrafficLogMeta(userID uint) (TrafficLogMeta, error)

GetTrafficLogMeta returns lightweight diagnostics for traffic chart pages.

func (*StatsService) GetUserSubscription

func (s *StatsService) GetUserSubscription(userID uint, forceRefresh bool) (*UserSubscription, error)

GetUserSubscription 获取用户订阅详情 (优先从缓存读取)

func (*StatsService) GetUserTrafficRanking

func (s *StatsService) GetUserTrafficRanking(hours int, limit int, includeZeroUsers bool) ([]UserTrafficRank, error)

GetUserTrafficRanking 返回最近 hours 小时内按用户聚合的流量排行 (倒序), 数据源为 v2_server_log, 关联 v2_user 取 email。limit 限制返回条数。 includeZeroUsers 为 true 时从用户表出发返回无区间流量的用户, 便于后台按任意用户筛选小时图。

func (*StatsService) UserSubscriptionSummary added in v4.1.0

func (s *StatsService) UserSubscriptionSummary(userID uint, forceRefresh bool, configService *SystemConfigService, cfg *config.Config) (*UserSubscription, error)

UserSubscriptionSummary is the answer of GET /api/v2/user/subscription: the user's cached summary (GetUserSubscription) with the subscription link settings, which are read on every call and not cached. It answers a copy, so the cached entry, which the user dashboard answers too, never carries the link settings and no request writes to it.

type SubscriberEntitlements added in v4.1.0

type SubscriberEntitlements struct {
	Balance        *int64  `json:"balance"`
	PlanID         *uint   `json:"plan_id"`
	GroupID        *uint   `json:"group_id"`
	ExpiredAt      *int64  `json:"expired_at"`
	TransferEnable *int64  `json:"transfer_enable"`
	SpeedLimit     *int64  `json:"speed_limit"`
	DeviceLimit    *int    `json:"device_limit"`
	FlowResetTime  *int64  `json:"flowResetTime"`
	RemarkContent  *string `json:"remark_content"`
}

SubscriberEntitlements are the v2_user fields an administrator edits besides the identity fields. The JSON keys are those of the v2 admin user update request; an absent or null field is left unchanged.

func (SubscriberEntitlements) Updates added in v4.1.0

func (e SubscriberEntitlements) Updates(plans *PlanService) map[string]any

Updates returns the v2_user column updates. A new plan also sets the group and, unless given explicitly, the traffic and limits from the plan, as the v2 admin API does.

type SubscriptionService

type SubscriptionService struct {
	// contains filtered or unexported fields
}

SubscriptionService 订阅服务

func NewSubscriptionService

func NewSubscriptionService() *SubscriptionService

NewSubscriptionService 创建订阅服务

func (*SubscriptionService) AssignGroupToPlan

func (s *SubscriptionService) AssignGroupToPlan(planID, groupID uint) error

AssignGroupToPlan 为套餐分配订阅分组

func (*SubscriptionService) AssignGroupToUser

func (s *SubscriptionService) AssignGroupToUser(userID, groupID uint, expireAt *int64, transferEnable *int64, nextRenewPrice *int64, requestID string) error

AssignGroupToUser 为用户分配订阅分组: the membership is created, or the given fields of an existing one are set, through the subscriber engine (subscriber.GrantSubscriptionGroupTx) once per requestID (SubscriptionGroupGrantRequestID), as the subscription package's native route does through KernelSubscriber. An empty requestID applies every time.

func (*SubscriptionService) BuildWireGuardRuntimeUserExtras

func (s *SubscriptionService) BuildWireGuardRuntimeUserExtras(protocol *model.NodeProtocol, users []*model.User) (map[uint]map[string]string, error)

func (*SubscriptionService) CreateGroup

func (s *SubscriptionService) CreateGroup(group *model.SubscriptionGroup) error

CreateGroup 创建订阅分组, its own columns only.

Group and template writes take the model an administrator's body was bound to, and GORM saves the associations of a model it writes. A group body's "protocols" upserted v2_node_protocol rows, and the nodes nested in them into v2_node (without an API key and without the node routes' checks), and linked them to the group; its "templates" created templates or moved existing ones into the group; a template body's "group" created a group and overrode the group the route names. Those belong to their own routes: the associations of a body are dropped and never saved.

func (*SubscriptionService) CreateTemplate

func (s *SubscriptionService) CreateTemplate(template *model.SubscriptionTemplate) error

CreateTemplate 创建订阅模板, its own columns only (see CreateGroup): the template stays in the group its route names.

func (*SubscriptionService) DeleteGroup

func (s *SubscriptionService) DeleteGroup(id uint, requestID string) error

DeleteGroup 删除订阅分组. Its members' memberships go through the subscriber engine (subscriber.RemoveSubscriptionGroupMembersTx), with requestID (SubscriptionGroupDeleteRequestID) in the subscriber request ledger and a change for each active member, as the subscription package's native route does through KernelSubscriber.

func (*SubscriptionService) DeleteTemplate

func (s *SubscriptionService) DeleteTemplate(id uint) error

DeleteTemplate 删除订阅模板

func (*SubscriptionService) GetGroup

GetGroup 获取订阅分组

func (*SubscriptionService) GetGroupStats

func (s *SubscriptionService) GetGroupStats() ([]GroupStats, error)

GetGroupStats 获取所有订阅分组的统计数据

func (*SubscriptionService) GetGroups

func (s *SubscriptionService) GetGroups() ([]*model.SubscriptionGroup, error)

GetGroups 获取所有订阅分组

func (*SubscriptionService) GetOrCreateWireGuardPeer

func (s *SubscriptionService) GetOrCreateWireGuardPeer(protocolID, userID uint, cidr string) (*model.WireGuardPeer, error)

func (*SubscriptionService) GetPlanGroups

func (s *SubscriptionService) GetPlanGroups(planID uint) ([]*model.SubscriptionGroup, error)

GetPlanGroups 获取套餐的订阅分组

func (*SubscriptionService) GetTemplate

func (s *SubscriptionService) GetTemplate(id uint) (*model.SubscriptionTemplate, error)

GetTemplate 获取订阅模板

func (*SubscriptionService) GetTemplatesByGroup

func (s *SubscriptionService) GetTemplatesByGroup(groupID uint) ([]*model.SubscriptionTemplate, error)

GetTemplatesByGroup 获取分组的模板

func (*SubscriptionService) GetUserGroups

func (s *SubscriptionService) GetUserGroups(userID uint) ([]*model.SubscriptionGroup, error)

GetUserGroups 获取用户的订阅分组

func (*SubscriptionService) GetUserSubscription

GetUserSubscription 获取用户订阅 token: 用户 token format: 订阅格式 groups: 指定分组 (可选) include: 包含关键词 exclude: 排除关键词

func (*SubscriptionService) ListGroupMembers added in v4.2.0

func (s *SubscriptionService) ListGroupMembers(groupID uint, params GroupMembersParams) (*GroupMembersResult, error)

ListGroupMembers lists a subscription group's direct members, newest grant first (members granted in the same instant by id, newest first), so pages neither repeat nor skip one. An unknown group is ErrSubscriptionGroupNotFound.

func (*SubscriptionService) RemoveGroupFromPlan

func (s *SubscriptionService) RemoveGroupFromPlan(planID, groupID uint) error

RemoveGroupFromPlan 移除套餐的订阅分组

func (*SubscriptionService) RemoveGroupFromUser

func (s *SubscriptionService) RemoveGroupFromUser(userID, groupID uint, requestID string) error

RemoveGroupFromUser 移除用户的订阅分组, through the subscriber engine (subscriber.RevokeSubscriptionGroupTx) once per requestID (SubscriptionGroupRevokeRequestID).

func (*SubscriptionService) UpdateGroup

func (s *SubscriptionService) UpdateGroup(group *model.SubscriptionGroup) error

UpdateGroup 更新订阅分组, its own columns only (see CreateGroup).

func (*SubscriptionService) UpdateTemplateFields

func (s *SubscriptionService) UpdateTemplateFields(id uint, fields map[string]any) error

UpdateTemplateFields 按字段局部更新订阅模板

type SubscriptionSettings

type SubscriptionSettings struct {
	SubscribePath    string   `json:"subscribe_path"`
	SubscribeDomains []string `json:"subscribe_domains"`
}

func GetSubscriptionSettings

func GetSubscriptionSettings(configService *SystemConfigService, cfg *config.Config) SubscriptionSettings

type SystemConfigService

type SystemConfigService struct {
	// contains filtered or unexported fields
}

func NewSystemConfigService

func NewSystemConfigService(db *gorm.DB) *SystemConfigService

func (*SystemConfigService) Delete

func (s *SystemConfigService) Delete(key string) error

func (*SystemConfigService) Get

func (s *SystemConfigService) Get(key string) (string, error)

func (*SystemConfigService) GetAll

func (s *SystemConfigService) GetAll() ([]model.SystemConfig, error)

func (*SystemConfigService) GetAsMap

func (s *SystemConfigService) GetAsMap() (map[string]string, error)

func (*SystemConfigService) GetByGroup

func (s *SystemConfigService) GetByGroup(group string) ([]model.SystemConfig, error)

func (*SystemConfigService) GetEntry

func (s *SystemConfigService) GetEntry(key string) (*model.SystemConfig, error)

func (*SystemConfigService) GetJSON

func (s *SystemConfigService) GetJSON(key string, v any) error

func (*SystemConfigService) Set

func (s *SystemConfigService) Set(key, value, cfgType, group, remark string) error

func (*SystemConfigService) SetJSON

func (s *SystemConfigService) SetJSON(key string, v any, group, remark string) error

type TOTPSetup

type TOTPSetup struct {
	Secret      string   `json:"secret"`       // 密钥 (base32)
	URL         string   `json:"url"`          // otpauth:// URL
	QRCode      string   `json:"qr_code"`      // 用于生成二维码
	BackupCodes []string `json:"backup_codes"` // 备用码
}

TOTPSetup TOTP设置结果

type TargetCatalogItem

type TargetCatalogItem struct {
	TargetKey      string  `json:"targetKey"`
	TargetType     string  `json:"targetType"`
	TargetID       uint    `json:"targetId"`
	Label          string  `json:"label"`
	Host           string  `json:"host"`
	Port           int     `json:"port"`
	LatestBucketAt int64   `json:"latestBucketAt"`
	LatestAvgRTT   float64 `json:"latestAvgRtt"`
	LatestLoss     float64 `json:"latestLoss"`
	Online         bool    `json:"online"`
}

type TelegramBotService

type TelegramBotService struct {
	// contains filtered or unexported fields
}

TelegramBotService Telegram Bot服务

func NewTelegramBotService

func NewTelegramBotService(db *gorm.DB) *TelegramBotService

NewTelegramBotService 创建服务

func (*TelegramBotService) BindUser

func (s *TelegramBotService) BindUser(telegramID int64, email string, from *TelegramUser) error

BindUser 绑定用户

func (*TelegramBotService) Broadcast

func (s *TelegramBotService) Broadcast(text string) error

Broadcast 广播消息

func (*TelegramBotService) DeleteWebhook

func (s *TelegramBotService) DeleteWebhook() error

DeleteWebhook 删除Webhook

func (*TelegramBotService) GetBot

func (s *TelegramBotService) GetBot() (*model.TelegramBot, error)

GetBot 获取Bot配置

func (*TelegramBotService) GetTelegramUserService

func (s *TelegramBotService) GetTelegramUserService() *TelegramUserService

GetTelegramUserService 获取Telegram用户服务

func (*TelegramBotService) HandleUpdate

func (s *TelegramBotService) HandleUpdate(update *TelegramUpdate) error

HandleUpdate 处理Webhook更新

func (*TelegramBotService) ProbeSend added in v4.2.0

func (s *TelegramBotService) ProbeSend(ctx context.Context, bot *model.TelegramBot, chatID int64, text string) TelegramProbeResult

ProbeSend sends text to chatID with bot's token and classifies the answer. It follows no redirect and gives up after ten seconds or when ctx ends. bot may be nil (no bot row).

func (*TelegramBotService) SendMessage

func (s *TelegramBotService) SendMessage(chatID int64, text string) error

SendMessage 发送消息

func (*TelegramBotService) SendNotification

func (s *TelegramBotService) SendNotification(telegramID int64, title, content string) error

SendNotification 发送通知

func (*TelegramBotService) SetAPIBase added in v4.2.0

func (s *TelegramBotService) SetAPIBase(base string)

SetAPIBase points the service's probe at another Bot API endpoint. It is for tests; the address comes from code, never from a request.

func (*TelegramBotService) SetWebhook

func (s *TelegramBotService) SetWebhook(webhookURL string) error

SetWebhook 设置Webhook

func (*TelegramBotService) UpdateBot

func (s *TelegramBotService) UpdateBot(bot *model.TelegramBot) error

UpdateBot 更新Bot配置

type TelegramCallback

type TelegramCallback struct {
	ID      string          `json:"id"`
	From    TelegramUser    `json:"from"`
	Message TelegramMessage `json:"message"`
	Data    string          `json:"data"`
}

TelegramCallback Telegram回调

type TelegramChat

type TelegramChat struct {
	ID       int64  `json:"id"`
	Type     string `json:"type"`
	Title    string `json:"title,omitempty"`
	Username string `json:"username,omitempty"`
}

TelegramChat Telegram聊天

type TelegramMessage

type TelegramMessage struct {
	MessageID int64         `json:"message_id"`
	From      *TelegramUser `json:"from,omitempty"`
	Chat      TelegramChat  `json:"chat"`
	Date      int64         `json:"date"`
	Text      string        `json:"text,omitempty"`
}

TelegramMessage Telegram消息

type TelegramProbeClass added in v4.2.0

type TelegramProbeClass string

TelegramProbeClass is the outcome class of a probe.

const (
	// TelegramProbeOK: Telegram accepted the message.
	TelegramProbeOK TelegramProbeClass = "ok"
	// TelegramProbeInvalidToken: the token is malformed or Telegram refused it
	// (401 Unauthorized, or 404 Not Found on the bot path).
	TelegramProbeInvalidToken TelegramProbeClass = "invalid_token"
	// TelegramProbeChatNotFound: the chat does not exist for this bot.
	TelegramProbeChatNotFound TelegramProbeClass = "chat_not_found"
	// TelegramProbeBotBlocked: Telegram answered 403, for example the user
	// blocked the bot or never started it.
	TelegramProbeBotBlocked TelegramProbeClass = "bot_blocked"
	// TelegramProbeRateLimited: Telegram answered 429.
	TelegramProbeRateLimited TelegramProbeClass = "rate_limited"
	// TelegramProbeNotConfigured: there is no bot row or it holds no token.
	TelegramProbeNotConfigured TelegramProbeClass = "not_configured"
	// TelegramProbeNetworkError: Telegram could not be reached (see Reason).
	TelegramProbeNetworkError TelegramProbeClass = "network_error"
	// TelegramProbeUnknown: any other answer.
	TelegramProbeUnknown TelegramProbeClass = "unknown"
)

The classes a probe answers.

type TelegramProbeResult added in v4.2.0

type TelegramProbeResult struct {
	Class TelegramProbeClass `json:"class"`
	// ErrorCode is the Bot API's error_code (else the HTTP status) of a
	// refusal; 0 when there was none.
	ErrorCode int `json:"error_code,omitempty"`
	// Reason refines a network_error.
	Reason string `json:"reason,omitempty"`
}

TelegramProbeResult is the classified outcome of a probe. It holds no token, no URL and no text from Telegram or from a Go error.

func (TelegramProbeResult) Message added in v4.2.0

func (r TelegramProbeResult) Message() string

Message is a fixed sentence per class.

func (TelegramProbeResult) OK added in v4.2.0

func (r TelegramProbeResult) OK() bool

OK reports whether Telegram accepted the message.

type TelegramUpdate

type TelegramUpdate struct {
	UpdateID      int64             `json:"update_id"`
	Message       *TelegramMessage  `json:"message,omitempty"`
	CallbackQuery *TelegramCallback `json:"callback_query,omitempty"`
}

TelegramUpdate Telegram更新

type TelegramUser

type TelegramUser struct {
	ID           int64  `json:"id"`
	IsBot        bool   `json:"is_bot"`
	FirstName    string `json:"first_name"`
	LastName     string `json:"last_name,omitempty"`
	Username     string `json:"username,omitempty"`
	LanguageCode string `json:"language_code,omitempty"`
}

TelegramUser Telegram用户

type TelegramUserService

type TelegramUserService struct {
	// contains filtered or unexported fields
}

TelegramUserService Telegram用户服务

func NewTelegramUserService

func NewTelegramUserService(db *gorm.DB) *TelegramUserService

NewTelegramUserService 创建服务

func (*TelegramUserService) GetByUserID

func (s *TelegramUserService) GetByUserID(userID uint) (*model.TelegramUser, error)

GetByUserID 根据系统用户ID获取绑定

type Topology

type Topology struct {
	Nodes []TopologyNode `json:"nodes"`
	Edges []TopologyEdge `json:"edges"`
}

type TopologyDeploymentDiagnosticCheck

type TopologyDeploymentDiagnosticCheck struct {
	Name    string `json:"name"`
	Status  string `json:"status"`
	Message string `json:"message,omitempty"`
}

type TopologyDeploymentDiagnosticIssue

type TopologyDeploymentDiagnosticIssue struct {
	Code     string `json:"code"`
	Path     string `json:"path"`
	Message  string `json:"message"`
	Severity string `json:"severity"`
}

TopologyDeploymentDiagnosticIssue is stable, machine-readable preflight output. Code and path intentionally mirror ValidateTopology where possible so the editor can point at the same field that the planner would reject.

type TopologyDeploymentEvent

type TopologyDeploymentEvent struct {
	Type        string    `json:"type"`
	At          time.Time `json:"at"`
	OperationID string    `json:"operation_id,omitempty"`
	StepID      *uint     `json:"step_id,omitempty"`
	NodeID      *uint     `json:"node_id,omitempty"`
	PluginID    string    `json:"plugin_id,omitempty"`
	State       string    `json:"state"`
	HasError    bool      `json:"has_error"`
	// Message is a stable kernel-generated lifecycle summary. It is never a
	// persisted deployment, observed-state, or Agent error.
	Message string `json:"message,omitempty"`
}

func PublicTopologyDeploymentEvent

func PublicTopologyDeploymentEvent(event TopologyDeploymentEvent) TopologyDeploymentEvent

PublicTopologyDeploymentEvent never trusts a pre-existing Message field: callers only receive a summary regenerated from the event type and state.

type TopologyDeploymentExecutor

type TopologyDeploymentExecutor struct {
	// contains filtered or unexported fields
}

TopologyDeploymentExecutor turns a previously planned revision into durable Agent operations. It never opens a network/data-plane connection itself; the existing KernelOperationBridge remains the sole Agent transport.

A failed operation fences further expansion immediately, cancels in-flight sibling work, then compensates successful/started steps in reverse DAG order. The compensation restores the previous active revision where possible, or disables a newly introduced plugin vertex when no predecessor exists.

func NewTopologyDeploymentExecutor

func NewTopologyDeploymentExecutor(db *gorm.DB) (*TopologyDeploymentExecutor, error)

func (*TopologyDeploymentExecutor) Run added in v4.1.0

func (e *TopologyDeploymentExecutor) Run(ctx context.Context, interval time.Duration, reportError func(error))

Run reconciles every interval and blocks until ctx is cancelled. It is deliberately separate from plan/apply HTTP paths so topology execution remains opt-in. ctx must not be nil.

func (*TopologyDeploymentExecutor) RunOnce

func (e *TopologyDeploymentExecutor) RunOnce(ctx context.Context) (int, error)

RunOnce reconciles every active topology deployment. Operations are created in database transactions and carry stable idempotency keys, so repeating a pass after a Control restart is safe.

func (*TopologyDeploymentExecutor) Start

func (e *TopologyDeploymentExecutor) Start(ctx context.Context, interval time.Duration, reportError func(error))

Start runs Run in a new goroutine.

type TopologyDeploymentOperation

type TopologyDeploymentOperation struct {
	OperationID   string     `json:"operation_id"`
	StepID        *uint      `json:"step_id,omitempty"`
	NodeID        *uint      `json:"node_id,omitempty"`
	PluginID      string     `json:"plugin_id"`
	TargetVersion string     `json:"target_version"`
	Kind          string     `json:"kind"`
	Revision      int64      `json:"revision"`
	State         string     `json:"state"`
	Attempt       int        `json:"attempt"`
	DeadlineAt    *time.Time `json:"deadline_at,omitempty"`
	DispatchedAt  *time.Time `json:"dispatched_at,omitempty"`
	ObservedAt    *time.Time `json:"observed_at,omitempty"`
	HasError      bool       `json:"has_error"`
	LastError     string     `json:"last_error,omitempty"`
	UpdatedAt     time.Time  `json:"updated_at"`
}

TopologyDeploymentOperation is the safe, linkage-focused projection used by deployment status. ConfigJSON, ResultJSON, and raw Agent errors are intentionally omitted. LastError is a stable kernel-generated summary, not the persisted operation error.

func PublicTopologyDeploymentOperation

func PublicTopologyDeploymentOperation(operation TopologyDeploymentOperation) TopologyDeploymentOperation

PublicTopologyDeploymentOperation defensively regenerates the error summary even though the timeline loader already does so. This keeps the HTTP view safe if an in-process caller constructs a TopologyDeploymentStatus manually.

type TopologyDeploymentPlanInput

type TopologyDeploymentPlanInput struct {
	TopologyID    uint
	RevisionID    uint
	ActorID       uint
	RolloutGroup  string
	FailurePolicy string
}

TopologyDeploymentPlanInput identifies an immutable topology revision to compile into a durable node-operation plan. The plan is intentionally inert: callers must request apply and the separately feature-gated executor must be running before any Agent operation is created.

type TopologyDeploymentPreview

type TopologyDeploymentPreview struct {
	TopologyID         uint                                `json:"topology_id"`
	RevisionID         uint                                `json:"revision_id"`
	PreviousRevisionID *uint                               `json:"previous_revision_id,omitempty"`
	RolloutGroup       string                              `json:"rollout_group,omitempty"`
	FailurePolicy      string                              `json:"failure_policy"`
	Valid              bool                                `json:"valid"`
	Issues             []TopologyDeploymentDiagnosticIssue `json:"issues"`
	Checks             []TopologyDeploymentDiagnosticCheck `json:"checks"`
	Steps              []TopologyDeploymentPreviewStep     `json:"steps"`
}

func PreviewTopologyDeployment

func PreviewTopologyDeployment(db *gorm.DB, input TopologyDeploymentPreviewInput) (*TopologyDeploymentPreview, error)

PreviewTopologyDeployment performs all checks that are safe to perform before PlanTopologyDeployment. It intentionally does not call the planner, since the planner persists a deployment row by design.

type TopologyDeploymentPreviewInput

type TopologyDeploymentPreviewInput struct {
	TopologyID    uint   `json:"topology_id"`
	RevisionID    uint   `json:"revision_id"`
	RolloutGroup  string `json:"rollout_group"`
	FailurePolicy string `json:"failure_policy"`
}

TopologyDeploymentPreviewInput identifies a revision and the optional rollout slice to inspect. Preview is deliberately read-only: it never creates a deployment, operation, or observed-state row.

type TopologyDeploymentPreviewStep

type TopologyDeploymentPreviewStep struct {
	Order         int      `json:"order"`
	VertexID      uint     `json:"vertex_id"`
	VertexKey     string   `json:"vertex_key"`
	NodeID        uint     `json:"node_id"`
	PluginID      string   `json:"plugin_id"`
	Role          string   `json:"role"`
	TargetVersion string   `json:"target_version"`
	Removal       bool     `json:"removal"`
	ApplyAction   string   `json:"apply_action"`
	RollbackMode  string   `json:"rollback_mode"`
	Dependencies  []string `json:"dependencies,omitempty"`
	Operations    []string `json:"operations"`
	ConfigHash    string   `json:"config_hash,omitempty"`
}

TopologyDeploymentPreviewStep describes the durable operations that a real plan would create. Operation IDs are intentionally absent because preview must not reserve identities or mutate the operation stream.

type TopologyDeploymentStatus

type TopologyDeploymentStatus struct {
	Deployment model.TopologyDeployment       `json:"deployment"`
	Steps      []model.TopologyDeploymentStep `json:"steps"`
	Observed   []model.TopologyObservedState  `json:"observed_states"`
	Operations []TopologyDeploymentOperation  `json:"operations"`
	Events     []TopologyDeploymentEvent      `json:"events"`
}

TopologyDeploymentStatus is the transport-neutral status read used by the HTTP handler and tests. All fields are persisted kernel records.

func GetTopologyDeploymentStatus

func GetTopologyDeploymentStatus(db *gorm.DB, deploymentID uint) (*TopologyDeploymentStatus, error)

type TopologyDeploymentStatusView

type TopologyDeploymentStatusView struct {
	Deployment TopologyDeploymentView        `json:"deployment"`
	Steps      []TopologyDeploymentStepView  `json:"steps"`
	Observed   []TopologyObservedStateView   `json:"observed_states"`
	Operations []TopologyDeploymentOperation `json:"operations"`
	Events     []TopologyDeploymentEvent     `json:"events"`
}

TopologyDeploymentStatusView is the only topology deployment status shape intended for HTTP clients. It deliberately leaves configuration documents, rollback documents, Agent result documents, Agent health documents, and raw persisted errors on the kernel side of the boundary.

func PublicTopologyDeploymentStatus

func PublicTopologyDeploymentStatus(status TopologyDeploymentStatus) TopologyDeploymentStatusView

PublicTopologyDeploymentStatus projects an internal status read to its explicit public representation. Do not return TopologyDeploymentStatus from an HTTP handler: it contains persistence models with secret-bearing fields.

type TopologyDeploymentStepView

type TopologyDeploymentStepView struct {
	ID                           uint      `json:"id"`
	DeploymentID                 uint      `json:"deployment_id"`
	VertexID                     uint      `json:"vertex_id"`
	VertexKey                    string    `json:"vertex_key"`
	NodeID                       uint      `json:"node_id"`
	PluginID                     string    `json:"plugin_id"`
	Role                         string    `json:"role"`
	TargetVersion                string    `json:"target_version"`
	ApplyOrder                   int       `json:"apply_order"`
	Removal                      bool      `json:"removal"`
	ApplyAction                  string    `json:"apply_action"`
	RollbackMode                 string    `json:"rollback_mode"`
	State                        string    `json:"state"`
	HasError                     bool      `json:"has_error"`
	LastError                    string    `json:"last_error,omitempty"`
	ConfigureOperationID         string    `json:"configure_operation_id,omitempty"`
	EnableOperationID            string    `json:"enable_operation_id,omitempty"`
	DisableOperationID           string    `json:"disable_operation_id,omitempty"`
	RollbackConfigureOperationID string    `json:"rollback_configure_operation_id,omitempty"`
	RollbackEnableOperationID    string    `json:"rollback_enable_operation_id,omitempty"`
	RollbackDisableOperationID   string    `json:"rollback_disable_operation_id,omitempty"`
	CreatedAt                    time.Time `json:"created_at"`
	UpdatedAt                    time.Time `json:"updated_at"`
}

TopologyDeploymentStepView is a state-only projection. The active and rollback configuration documents remain write-only kernel records.

type TopologyDeploymentView

type TopologyDeploymentView struct {
	ID                   uint       `json:"id"`
	TopologyID           uint       `json:"topology_id"`
	RevisionID           uint       `json:"revision_id"`
	PreviousRevisionID   *uint      `json:"previous_revision_id,omitempty"`
	RolloutGroup         string     `json:"rollout_group"`
	State                string     `json:"state"`
	FailurePolicy        string     `json:"failure_policy"`
	HasError             bool       `json:"has_error"`
	LastError            string     `json:"last_error,omitempty"`
	HealthGateDeadlineAt *time.Time `json:"health_gate_deadline_at,omitempty"`
	RollbackStartedAt    *time.Time `json:"rollback_started_at,omitempty"`
	RollbackCompletedAt  *time.Time `json:"rollback_completed_at,omitempty"`
	CreatedBy            uint       `json:"created_by"`
	CreatedAt            time.Time  `json:"created_at"`
	UpdatedAt            time.Time  `json:"updated_at"`
	CompletedAt          *time.Time `json:"completed_at,omitempty"`
}

TopologyDeploymentView is a status-only projection of a durable deployment. LastError is a stable summary derived from state, never the persisted value.

func PublicTopologyDeployment

func PublicTopologyDeployment(deployment model.TopologyDeployment) TopologyDeploymentView

type TopologyEdge

type TopologyEdge struct {
	ID           string  `json:"id"` // "tunnel-<id>"
	Source       string  `json:"source"`
	Target       string  `json:"target"`
	Label        string  `json:"label"`
	TunnelID     uint    `json:"tunnelId"`
	ForwardCount int     `json:"forwardCount"`
	Status       int     `json:"status"`
	AvgRTT       float64 `json:"avgRtt"`
}

type TopologyNode

type TopologyNode struct {
	ID          string  `json:"id"` // "node-<id>"
	Label       string  `json:"label"`
	Kind        string  `json:"kind"` // relay | exit
	Region      string  `json:"region"`
	Online      bool    `json:"online"`
	Status      int     `json:"status"`
	LatencyMs   int     `json:"latencyMs"`
	Load        float64 `json:"load"`
	CurrentConn int     `json:"currentConn"`
	Weight      int     `json:"weight"`
}

type TopologyObservedStateUpdate

type TopologyObservedStateUpdate struct {
	DeploymentID     uint
	NodeID           uint
	DesiredRevision  int64
	ObservedRevision int64
	State            string
	HealthJSON       string
	LastError        string
	ObservedAt       time.Time
}

TopologyObservedStateUpdate is the transport-neutral observed-state write contract used by the Agent bridge and the future topology executor.

type TopologyObservedStateView

type TopologyObservedStateView struct {
	ID               uint      `json:"id"`
	DeploymentID     uint      `json:"deployment_id"`
	NodeID           uint      `json:"node_id"`
	DesiredRevision  int64     `json:"desired_revision"`
	ObservedRevision int64     `json:"observed_revision"`
	State            string    `json:"state"`
	HealthReported   bool      `json:"health_reported"`
	HasError         bool      `json:"has_error"`
	LastError        string    `json:"last_error,omitempty"`
	UpdatedAt        time.Time `json:"updated_at"`
}

TopologyObservedStateView contains only monotonic state metadata. Health is represented by a presence bit because plugin-provided health JSON is not a stable or safe API contract.

func PublicTopologyObservedState

func PublicTopologyObservedState(observed model.TopologyObservedState) TopologyObservedStateView

func PublicTopologyObservedStates

func PublicTopologyObservedStates(rows []model.TopologyObservedState) []TopologyObservedStateView

type TopologyRevisionInput

type TopologyRevisionInput struct {
	Message  string                 `json:"message"`
	Vertices []model.TopologyVertex `json:"vertices"`
	Edges    []model.TopologyEdge   `json:"edges"`
}

type TopologyValidationIssue

type TopologyValidationIssue struct {
	Code    string `json:"code"`
	Path    string `json:"path"`
	Message string `json:"message"`
}

func ValidateTopologyAssignments

func ValidateTopologyAssignments(db *gorm.DB, scopeID string, input TopologyRevisionInput) []TopologyValidationIssue

ValidateTopologyAssignments verifies that a plugin instance placed on a physical node has a matching enabled service assignment in the topology's scope. It deliberately does not inspect runtime Agent capabilities yet: those are observed by the Supervisor phase, while assignments remain the control-plane source of truth.

type TrafficLogMeta

type TrafficLogMeta struct {
	LatestLogAt int64 `json:"latest_log_at"` // 最近一次流量上报 Unix 秒, 0 表示从未上报
}

type TunnelDiagnosisReport

type TunnelDiagnosisReport struct {
	TunnelID   uint               `json:"tunnelId"`
	TunnelName string             `json:"tunnelName"`
	TunnelType string             `json:"tunnelType"`
	Timestamp  int64              `json:"timestamp"`
	Results    []DiagnosisOutcome `json:"results"`
}

type UserListItem added in v4.1.0

type UserListItem struct {
	ID                uint      `json:"id"`
	Email             string    `json:"email"`
	Balance           int64     `json:"balance"`
	CommissionBalance int64     `json:"commission_balance"`
	DeviceLimit       *int      `json:"device_limit"`
	SpeedLimit        *int64    `json:"speed_limit"`
	FlowResetTime     int64     `json:"flowResetTime"`
	TransferEnable    int64     `json:"transfer_enable"`
	U                 int64     `json:"u"`
	D                 int64     `json:"d"`
	PlanID            *uint     `json:"plan_id"`
	GroupID           *uint     `json:"group_id"`
	ExpiredAt         *int64    `json:"expired_at"`
	Banned            int       `json:"banned"`
	IsAdmin           int       `json:"is_admin"`
	IsStaff           int       `json:"is_staff"`
	CreatedAt         time.Time `json:"created_at"`
	// Plan is never loaded with the row.
	Plan *UserListPlan `gorm:"-" json:"plan,omitempty"`
}

UserListItem is one user in the administrator's list: the account (e-mail, administrator, staff and ban flags), the subscription summary (plan, group, traffic, limits, reset day, expiry, balances) and the plan's id and name: the columns of kapi_user_directory_v1, kapi_subscriber_entitlement_v1 and kapi_plan_name_v1. A plan that no longer exists is left out. It carries no credential: the subscription token and proxy uuid, and the rest of the row, are read one user at a time from GET /api/v2/admin/users/:id. identity-platform's native list answers the same fields (packages/identity-platform/native/directory.go).

type UserListParams

type UserListParams struct {
	Page     int
	PageSize int
	Email    string
	PlanID   *uint
	Status   string // all, active, expired, banned
	// OrderBy is an ORDER BY clause from v2compat.ParseListSort over
	// UserSortColumns, never request text; empty keeps the default order.
	OrderBy string
}

UserListParams 用户列表查询参数

type UserListPlan added in v4.1.0

type UserListPlan struct {
	ID   uint   `json:"id"`
	Name string `json:"name"`
}

UserListPlan names a listed user's plan: its id and name, never the rest of the plan row.

type UserListResult

type UserListResult struct {
	Total int64          `json:"total"`
	List  []UserListItem `json:"list"`
}

UserListResult 用户列表结果

type UserService

type UserService struct {
	// contains filtered or unexported fields
}

UserService 用户服务

func NewUserService

func NewUserService() *UserService

NewUserService 创建用户服务

func NewUserServiceFor added in v4.2.0

func NewUserServiceFor(db *gorm.DB) *UserService

NewUserServiceFor creates a user service over db instead of the process database: for callers that already hold their handle.

func (*UserService) Ban

func (s *UserService) Ban(id uint) error

Ban 封禁用户

func (*UserService) BatchUpdateTraffic

func (s *UserService) BatchUpdateTraffic(traffics map[uint][2]int64) error

BatchUpdateTraffic 批量更新用户流量

func (*UserService) Create

func (s *UserService) Create(user *model.User) error

Create 创建用户

func (*UserService) Delete

func (s *UserService) Delete(id uint) error

Delete 删除用户

func (*UserService) GetActiveUsers

func (s *UserService) GetActiveUsers() ([]model.User, error)

GetActiveUsers 获取所有有效用户

func (*UserService) GetActiveUsersByGroupID

func (s *UserService) GetActiveUsersByGroupID(groupID uint) ([]model.User, error)

GetActiveUsersByGroupID 获取指定分组的有效用户

func (*UserService) GetActiveUsersForNode

func (s *UserService) GetActiveUsersForNode(groupID *uint) ([]*model.User, error)

GetActiveUsersForNode 获取节点可用的有效用户 groupID 为 nil 时返回所有有效用户,否则返回指定分组的用户

func (*UserService) GetByEmail

func (s *UserService) GetByEmail(email string) (*model.User, error)

GetByEmail 根据Email获取用户

func (*UserService) GetByID

func (s *UserService) GetByID(id uint) (*model.User, error)

GetByID 根据ID获取用户

func (*UserService) GetByToken

func (s *UserService) GetByToken(token string) (*model.User, error)

GetByToken 根据Token获取用户

func (*UserService) GetByUUID

func (s *UserService) GetByUUID(uuid string) (*model.User, error)

GetByUUID 根据UUID获取用户

func (*UserService) GetList

func (s *UserService) GetList(params UserListParams) (*UserListResult, error)

GetList 获取用户列表. Users created in the same instant keep a stable order (created_at DESC, id DESC), so pages neither repeat nor skip a user.

func (*UserService) GetStats

func (s *UserService) GetStats() (map[string]any, error)

GetStats 获取用户统计

func (*UserService) ResetToken

func (s *UserService) ResetToken(id uint, requestID string) (string, error)

ResetToken 为用户重新生成订阅 token, 让旧的 /s/<token> 链接立即失效。 不改动 UUID, 所以节点端密码/连接不受影响, 用户只需重新导入订阅。 返回当前 token. requestID (AdminUserResetRequestID) applies a retried reset once, as KernelSubscriber.ResetCredentials does: a repeat changes nothing and returns the token the first one issued, if it is still the current one.

func (*UserService) ResetTraffic

func (s *UserService) ResetTraffic(id uint, requestID string) error

ResetTraffic 重置用户流量. A non-empty requestID (AdminUserResetRequestID) applies a retried reset once, through the subscriber request ledger.

func (*UserService) Unban

func (s *UserService) Unban(id uint) error

Unban 解封用户

func (*UserService) Update

func (s *UserService) Update(id uint, updates map[string]any) error

Update 更新用户

func (*UserService) UpdateTraffic

func (s *UserService) UpdateTraffic(userID uint, upload, download int64) error

UpdateTraffic 更新用户流量

func (*UserService) VerifyStepUp added in v4.1.0

func (s *UserService) VerifyStepUp(userID uint, password, code, method string) (bool, error)

VerifyStepUp re-authenticates a signed-in user before a sensitive change: with a second factor enabled, a TOTP or recovery (backup) code, checked by MFAService.Verify, which consumes a used recovery code; otherwise the current password. checked reports whether a credential was checked, which counts toward the caller's rate limit; a missing credential is not.

type UserSubscription

type UserSubscription struct {
	UserID           uint      `json:"user_id"`
	Email            string    `json:"email"`
	PlanID           *uint     `json:"plan_id"`
	PlanName         string    `json:"plan_name"`
	TransferEnable   int64     `json:"transfer_enable"`  // 总流量 (字节)
	UsedTraffic      int64     `json:"used_traffic"`     // 已用流量 (字节)
	UploadTraffic    int64     `json:"upload_traffic"`   // 上传流量 (字节)
	DownloadTraffic  int64     `json:"download_traffic"` // 下载流量 (字节)
	ExpiredAt        int64     `json:"expired_at"`       // 到期时间
	IsExpired        bool      `json:"is_expired"`
	DaysRemaining    int       `json:"days_remaining"`
	UsagePercent     float64   `json:"usage_percent"`
	SubscribePath    string    `json:"subscribe_path,omitempty"`
	SubscribeDomains []string  `json:"subscribe_domains,omitempty"`
	CachedAt         time.Time `json:"cached_at"`
}

UserSubscription 用户订阅详情

type UserTrafficRank

type UserTrafficRank struct {
	UserID  uint   `json:"user_id"`
	Email   string `json:"email"`
	Traffic int64  `json:"traffic"` // 区间内总流量 (字节, 已按倍率计)
}

UserTrafficRank 单个用户的流量排行项

type WebUIExtension

type WebUIExtension struct {
	PluginID       string               `json:"plugin_id"`
	PluginName     string               `json:"plugin_name"`
	Publisher      string               `json:"publisher"`
	Version        string               `json:"version"`
	APIVersion     string               `json:"api_version"`
	InstallationID uint                 `json:"installation_id"`
	State          string               `json:"state"`
	Bundle         WebUIExtensionBundle `json:"bundle"`
	Permissions    []string             `json:"permissions"`
	Menus          []PluginWebUIMenu    `json:"menus"`
	Routes         []PluginWebUIRoute   `json:"routes"`
	ConfigSchema   json.RawMessage      `json:"config_schema"`
	// ControlRoutes are the signed manifest's control routes, listed only
	// when the actor may call them (the package's api permission). The web
	// app reads a capability from them, such as the forward package's v4
	// API (/api/v4/plugins/forward/*), which F5b's pages need.
	ControlRoutes []string `json:"control_routes,omitempty"`
}

func ListEnabledWebUIExtensionsForActor

func ListEnabledWebUIExtensionsForActor(db *gorm.DB, publicKey ed25519.PublicKey, actorID uint, legacyAdmin bool) ([]WebUIExtension, error)

ListEnabledWebUIExtensionsForActor applies the same authoritative plugin permissions as the control route gateway. Restricted actors only receive routes, menus and declared permissions that they can actually use.

type WebUIExtensionBundle

type WebUIExtensionBundle struct {
	Path   string `json:"path"`
	SHA256 string `json:"sha256"`
	URL    string `json:"url"`
}

type WireGuardPeerRotationResult

type WireGuardPeerRotationResult struct {
	RotatedPeerCount int
	ProtocolIDs      []uint
}

WireGuardPeerRotationResult reports only non-secret rotation metadata.

func RotateWireGuardPeerKeys

func RotateWireGuardPeerKeys(db *gorm.DB, protocolID uint) (*WireGuardPeerRotationResult, error)

RotateWireGuardPeerKeys replaces every selected peer's client private key, public key, and preshared key in one database transaction. Peer IDs, user assignments, and tunnel IPs remain stable so the node can apply the update without reallocating addresses.

Source Files

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL