server

package
v0.2.3 Latest Latest
Warning

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

Go to latest
Published: Sep 8, 2026 License: MIT Imports: 70 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// ReachabilityOnline means the node is beating within the stale threshold.
	ReachabilityOnline = "online"
	// ReachabilityOffline means the node has reported before and has since gone
	// quiet. LastSeen says when it was last heard from.
	ReachabilityOffline = "offline"
	// ReachabilityNever means the node has never reported. CreatedAt says how
	// long it has been waiting, which is the number worth showing.
	ReachabilityNever = "never"
)

Reachability is what the control plane actually knows about a node's contact history. model.Node.Online is a bool, so it collapses two situations an operator has to tell apart: a node that has been reporting and stopped, and a node that has never reported at all.

The second is not a corner case. handleEnrollNode writes the node record with neither LastSeen nor Online set, so every node is in it from enrollment until its first beat. Rendering that as "offline" tells the operator something broke, when what actually happened is that the agent was never installed or has never reached the control plane. Those have different fixes.

This is derived at view time rather than stored. The inputs already exist and already move together on the heartbeat path; a stored copy would be a second source of truth with nothing keeping it in step.

View Source
const (
	NodeStatusNeverReported = "never_reported"
	NodeStatusOnline        = "online"
	NodeStatusDegraded      = "degraded"
	NodeStatusOffline       = "offline"
	NodeStatusDisabled      = "disabled"
)

Node status is the one word the console renders for a node. It is derived here, at view time, so the nodes list, node detail, the overview KPIs, the map and the terminal picker all read the same rule at the same instant; the same machine used to read offline on one page and degraded on another because each page derived its own word from Online, LastSeen and metrics.

Online and Reachability stay on the wire for consumers that have not moved. Both read the stored Online flag as it is, so in the window described below, between a beat going stale and the liveness sweep flipping that flag, they still say online while Status has already moved to offline. Status is the derived answer and the one to trust; anything that counts, groups or filters nodes derives from it rather than from Online.

Precedence, first match wins:

disabled        Disabled is set. An operator switched the node off, and
                that outranks whatever the agent is or is not doing.
never_reported  LastSeen is zero. The record has existed since enrollment
                and no report has ever arrived. Since is CreatedAt.
offline         Online is false, or the last report is older than
                nodeOfflineThreshold. The second clause covers the window
                between a beat going stale and the liveness sweep flipping
                the flag. Since is LastSeen, the last contact.
degraded        In contact, but a probe the control plane holds fresh
                evidence for says something on the node is broken; see
                nodeDegradations for the inputs. Since is the earliest
                problem's start.
online          In contact within the threshold and nothing proven broken.
                Since is OnlineSince, omitted for records written before
                that field existed.
View Source
const (
	ApprovalWaitTaskFailed            = "task_failed"
	ApprovalWaitTaskRunning           = "task_running"
	ApprovalWaitAwaitingConfirmation  = "awaiting_confirmation"
	ApprovalWaitVersionMismatch       = "version_mismatch"
	ApprovalWaitTaskFinished          = "task_finished"
	ApprovalWaitPlanSuperseded        = "plan_superseded"
	ApprovalWaitNodeUnknown           = "node_unknown"
	ApprovalWaitNodeDisabled          = "node_disabled"
	ApprovalWaitNodeNeverReported     = "node_never_reported"
	ApprovalWaitNodeOffline           = "node_offline"
	ApprovalWaitCapabilityExcluded    = "capability_excluded"
	ApprovalWaitTaskExecutionDisabled = "task_execution_disabled"
	ApprovalWaitNotQueued             = "not_queued"
	ApprovalWaitTaskQueued            = "task_queued"
)

Why an approved approval has not applied.

An approval whose status is "approved" has left the operator's hands and has not reached the node. That is a legitimate state and the console used to render it as a bare word, so the one item that could never clear looked identical to the ones that were about to. The page said Pending 0 next to a non-empty inbox and an operator reasonably read that as "everything ran".

The explanation cannot be derived by the console. Whether an apply task was ever queued is not on the approvals wire at all, the node's contact state lives behind a second endpoint, and the capability gate and the fleet kill switch are server state with no client-visible projection. Correlating three endpoints to produce a sentence that may still be wrong is worse than saying nothing, so the control plane answers instead.

The vocabulary is deliberately borrowed. Node reasons carry the exact status word node_status.go derives (disabled, never_reported, offline) with its "since" instant, so the sentence on this page and the word on the Nodes page cannot disagree about the same machine.

Precedence, first match wins. Evidence of what already happened outranks a prediction of what will not:

task_failed         An apply task ran and ended failed or cancelled.
task_running        An apply task is leased right now. Nothing is wrong.
awaiting_confirmation  The apply task of an agent update finished and the
                    node has not reported the target version yet. The
                    approval is applied the moment it does; nothing is
                    wrong. Only while the node is reporting: a silent node
                    falls through to the codes below.
version_mismatch    The apply task of an agent update finished, the node
                    has been reporting since well past the scheduled
                    restart, and it still runs a version that is not the
                    target. The restart did not take.
task_finished       An apply task finished and the approval was never
                    marked applied. The control plane does not know why;
                    it says so rather than picking a story.
plan_superseded     A newer approval exists for the same node, plugin and
                    action. This plan is dead whatever the node does.
node_unknown        The target node record is gone.
node_disabled       An operator switched the node off.
node_never_reported The node was enrolled and has never reported.
node_offline        The node has stopped reporting.
capability_excluded The plugin's capability is excluded for this node, so
                    queueing an apply is refused.
task_execution_disabled  The fleet kill switch is on; nothing leases.
not_queued          Approved without queueing an apply, and nothing since
                    has queued one. Nothing will run on its own.
task_queued         Queued, node reporting, waiting for the next poll.
                    The ordinary case, and the only one that clears itself.
View Source
const (
	EventServiceDown      = "service.down"
	EventServiceRecovered = "service.recovered"
)

Typed notification kinds for service liveness (design-19), declared beside their emitter per the convention: new callers say what the event is, so a reworded title can never silently change who receives it.

View Source
const (
	// EventSSHCompromiseSuspected fires on a successful login from a source
	// that failed first. It is the only pressure-derived event that notifies.
	EventSSHCompromiseSuspected = "ssh.compromise_suspected"
	// EventSSHPressureWindow is recorded, never notified.
	EventSSHPressureWindow = "ssh.pressure_window"
)

SSH authentication pressure: what the fleet reports, and the one thing worth waking someone for.

Alerting on "you are being brute-forced" is the wrong design. Every public node in this fleet is being brute-forced, one of them 8449 times in a day, so that alert fires constantly, gets muted, and is muted on the day it matters.

The signal that earns an alert is a SUCCESS from a source that had been failing. That is a possible compromise and it needs a person now. The volume itself is posture: it belongs in an audit trail and a report, where it informs a decision made at leisure.

So this file records every window and notifies for almost none of them.

View Source
const (
	NodeStatusUnknown = "unknown"
)

GET /api/nodes/status-history?days=7[&node_id=]: each readable node's online record over the window, summarised here so the table, the card and the detail strip read one rule, as node_status.go does for the current word.

Three states: online, offline and unknown. Unknown is what nobody observed, which is the time before a node's first recorded edge and the time the control plane itself was down (the rows the store writes under NodeStatusServerID at start). A node's rows are transitions, so its state at any instant is the last row at or before it; the initial state is the one at the window start, and every later row in the window is echoed back with the cause that wrote it, control-plane edges included, so a strip is drawn by walking events from initial with nothing else to know.

Episodes count offline stretches that begin in the window plus one for a window that opens offline. A control-plane gap pauses a stretch rather than ending it: the node was offline on both sides, and the gap says nothing. The opening state is the observed one, so a window that opens inside a gap opens unknown, and the stretch the restart then reveals was already under way rather than one that opened here.

Variables

This section is empty.

Functions

This section is empty.

Types

type AuditHeadShippingOptions added in v0.2.0

type AuditHeadShippingOptions struct {
	URL         string
	BearerToken string
	Interval    time.Duration
	HTTPClient  *http.Client
}

AuditHeadShippingOptions configures automated off-box custody for the verified audit WAL head. Empty URL disables shipping.

type BuildInfo added in v0.2.0

type BuildInfo struct {
	ServerVersion         string `json:"server_version"`
	ServerCommit          string `json:"server_commit"`
	ServerDate            string `json:"server_date"`
	DashboardRef          string `json:"dashboard_ref,omitempty"`
	DashboardBuilt        string `json:"dashboard_built,omitempty"`
	TaskExecutionDisabled bool   `json:"task_execution_disabled,omitempty"`
}

type KnownCapability added in v0.2.3

type KnownCapability struct {
	ID       string `json:"id"`
	Enforced bool   `json:"enforced"`
	Mutates  bool   `json:"mutates"`
	// Derived is whether this capability can answer for a node that has no
	// explicit enrolment. Enforcing one that cannot refuses the whole fleet on
	// the first request, so the console has to be able to say so.
	Derived bool `json:"derived"`
}

KnownCapability describes one declared capability to the console.

Enforced and Mutates are both exported because they answer different questions an operator asks: whether a decision here currently bites, and whether this capability is one that changes the machine (which is what makes it opt-in in the first place). Without Enforced the console would render fourteen inert capabilities beside the one live one and imply they all matter equally.

type Line added in v0.2.0

type Line struct {
	ID                 string `json:"id"`           // == LineHashID (stable handle)
	LineHashID         string `json:"line_hash_id"` // stable across re-probes; see lineHash / stableLineHandle
	LineID             string `json:"line_id,omitempty"`
	NodeID             string `json:"node_id"`
	NodeIdentityUUID   string `json:"node_identity_uuid,omitempty"`
	LineUUID           string `json:"line_uuid,omitempty"`            // design-15 D1: durable control-plane identity (vpnmeta/lineuuid)
	DownstreamLineUUID string `json:"downstream_line_uuid,omitempty"` // design-15 §6: declared chain edge target
	Core               string `json:"core"`                           // sing-box | xray | mihomo
	Source             string `json:"source"`                         // managed | discovered | imported
	Managed            bool   `json:"managed"`                        // under Lattice config management
	Name               string `json:"name"`
	Tag                string `json:"tag,omitempty"`
	// InboundTags is every sing-box inbound tag the node reported for the conf
	// file this line stands for. Tag above is that file's name, which equals the
	// inbound tag only by the helper script's convention; a hand-written file, or
	// one holding a relay pair, carries tags of its own. Traffic counters and
	// connection records arrive keyed by the core's real inbound tags, so these
	// are what those joins have to run against. Empty when the node's sing-box
	// helper predates the field, which leaves the convention in force and every
	// existing join unchanged.
	InboundTags []string `json:"inbound_tags,omitempty"`
	// NamedUsers and UnnamedUsers count the credentials on this line's conf file
	// that the node can and cannot count individually. sing-box builds its stats
	// user allowlist by name, so an unnamed credential never gets a per-user
	// counter and its traffic stays inside the inbound total. That is a
	// permanent property of the config rather than a failed attribution, and the
	// two are worth telling apart on screen. Both zero means the node did not
	// report, which is not the same as a line with no credentials.
	NamedUsers   int    `json:"named_users,omitempty"`
	UnnamedUsers int    `json:"unnamed_users,omitempty"`
	Type         string `json:"type,omitempty"` // protocol
	Transport    string `json:"transport,omitempty"`
	Security     string `json:"security,omitempty"`
	ListenHost   string `json:"listen_host,omitempty"`
	ListenPort   int    `json:"listen_port,omitempty"`
	PublicHost   string `json:"public_host,omitempty"`
	// PublicPort is where the outside actually reaches this line, when that
	// differs from ListenPort. Declared by the node, because a mapping that
	// lives in a provider's router cannot be read from the config here. Zero
	// means the listen port is also the public one.
	PublicPort int `json:"public_port,omitempty"`
	// ProviderEdge is the hostname a provider forwards into this node from. A
	// relay names it as its outbound server, so it is the only host under which
	// a chain into a NAT node can be matched back to the line that ends it.
	ProviderEdge   string   `json:"provider_edge,omitempty"`
	Domain         string   `json:"domain,omitempty"`
	OutboundRef    string   `json:"outbound_ref,omitempty"`    // direct | <host/tag> | "" unknown
	OutboundServer string   `json:"outbound_server,omitempty"` // downstream server host the outbound routes to
	OutboundPort   int      `json:"outbound_port,omitempty"`   // downstream server port the outbound routes to
	JumpEdges      []string `json:"jump_edges,omitempty"`      // line_hash_ids this line relays to
	// DeclaredJumpEdges is the subset of JumpEdges resolved from the sidecar's
	// declared downstream_line_uuid (design-15 §6), not inferred from outbound
	// host/port — the UI badges these as orchestrated edges.
	DeclaredJumpEdges []string `json:"declared_jump_edges,omitempty"`
	// design-17: a line backed by a server-owned managed-line definition (the
	// overlay) carries the definition's state. The join is by line_hash_id —
	// the compiler pre-computes the hash discovery will assign, so a
	// rediscovered applied line lands on its definition exactly.
	Overlay       bool   `json:"overlay,omitempty"`
	OverlayStatus string `json:"overlay_status,omitempty"` // planned | applied | failed
	OverlayUser   string `json:"overlay_user,omitempty"`
	UserCount     int    `json:"user_count"`
	UserKnown     bool   `json:"user_known"`       // false ⇒ discovered line, count not yet inspected
	Status        string `json:"status,omitempty"` // ok | pending | error | stale
	LastError     string `json:"last_error,omitempty"`
	// design-19: what the service is doing, as opposed to what the config
	// says. Status above answers "does the configuration check out";
	// ServiceState answers "is anything actually running and holding this
	// line's port". They must never be merged back into one field: their
	// disagreement is exactly the incident signal.
	ServiceState     string    `json:"service_state,omitempty"` // running | down | restarting | unknown
	ServiceCheckedAt time.Time `json:"service_checked_at,omitempty"`
	// ServiceNote is the probe's own account of why the state is not
	// "running": the refused candidate and the rule it failed, or the command
	// that could not run. Empty when the service is proven running, so a
	// consumer that prints it prints only what needs a hand.
	ServiceNote string            `json:"service_note,omitempty"`
	Metadata    map[string]string `json:"metadata,omitempty"` // sing-box `_lattice` block (future enrich)
}

Line is the unified, node-grouped view of a proxy "line" — an inbound/endpoint regardless of origin: a Lattice-managed inbound rendered onto a node, or a proxy discovered on-box via `sb --json list`. It replaces the split between the old managed Inbounds view and the Discovered view (design-12). It is a DERIVED, read-model type computed on demand from the proxy store + live discovery inventory; it is not persisted and is never sent to the agent (so it lives in the server package, not the shared SDK). Secret-free: it carries only connection-shape metadata, never private keys or passwords.

type LineBinding added in v0.2.0

type LineBinding struct {
	LineHashID   string `json:"line_hash_id"`
	Enabled      bool   `json:"enabled"`
	FlowOverride string `json:"flow_override,omitempty"`
}

LineBinding attaches a user to a specific Line (by its stable line_hash_id).

type LineGroup added in v0.2.0

type LineGroup struct {
	NodeID   string `json:"node_id"`
	NodeName string `json:"node_name,omitempty"`
	Lines    []Line `json:"lines"`
}

LineGroup is the set of lines on one node — the unit the dashboard renders.

type NodeProfileRuntime added in v0.2.0

type NodeProfileRuntime struct {
	NodeID          string                 `json:"node_id"`
	NodeName        string                 `json:"node_name,omitempty"`
	Managed         bool                   `json:"managed"`          // a Lattice ProxyNodeProfile exists
	Origin          string                 `json:"origin,omitempty"` // managed | discovered, when a profile exists
	Core            string                 `json:"core,omitempty"`
	CoreVersion     string                 `json:"core_version,omitempty"`
	ConfigPath      string                 `json:"config_path,omitempty"`
	StatsAPI        string                 `json:"stats_api,omitempty"`
	Applied         bool                   `json:"applied"`
	LastApplyAt     string                 `json:"last_apply_at,omitempty"`
	LastError       string                 `json:"last_error,omitempty"`
	InboundCount    int                    `json:"inbound_count"`
	DiscoveredCount int                    `json:"discovered_count"`
	DiscoveryStatus string                 `json:"discovery_status,omitempty"`
	DiscoveryError  string                 `json:"discovery_error,omitempty"`
	DiscoveredAt    string                 `json:"discovered_at,omitempty"`
	Collector       *UsageCollectorRuntime `json:"collector,omitempty"`
	Capabilities    []string               `json:"capabilities"`
}

NodeProfileRuntime is the vpn-core per-node runtime view (design-12 S4): the operator-facing "is this node under vpn-core management, what core/version is on it, is the config applied, is the collector healthy, what was last probed" page. It is a DERIVED read-model unioning the Lattice ProxyNodeProfile (managed config + apply/collector status) with the live discovered SingBoxInventory (core version + discovered node count + discovery status). A node appears if it has either.

type Options

type Options struct {
	// SubscriptionDecoy shapes the response every non-servable public
	// subscription request receives. Its zero value is an empty-bodied 404, which
	// a reverse proxy can replace with its own error page so /sub/<anything> and
	// an unknown path are indistinguishable.
	SubscriptionDecoy subscriptionDecoyOptions
	Store             *store.Store
	WebFS             fs.FS
	// LogStore is the dedicated bounded log-line database (logs.db). Nil disables
	// the log-ingestion feature: its endpoints return 503 and agents are told to
	// tail nothing. Injected by main (opened beside the state file with the same
	// cipher), mirroring Store.
	LogStore *logstore.Store
	// TraceStore is the sing-box connection trace database (trace.db). Nil
	// disables tracing: its endpoints return 503 and agents are told to collect
	// nothing. Injected by main beside LogStore with the same cipher.
	TraceStore    *tracestore.Store
	AdminUsername string
	AdminPassword string
	Build         BuildInfo
	SecureCookies bool
	// TrustProxy enables reading the client address from proxy headers
	// (CF-Connecting-IP, then X-Forwarded-For). Only enable when the server
	// sits behind a trusted reverse proxy / Cloudflare; otherwise clients can
	// spoof the header and evade per-IP rate limiting.
	TrustProxy bool
	// RequireTOTP forces interactive user sessions to enable TOTP before they can
	// use non-setup APIs. Existing password/SSO login still issues a session so
	// the operator can enroll, but withAuth gates every other route until TOTP is
	// active. Bearer PATs are not interactive sessions and are unaffected.
	RequireTOTP bool
	// PluginDir is the root directory of installed plugin bundles. Empty disables
	// plugin loading entirely.
	PluginDir string
	// PluginBundleCacheDir is the private content-addressed extraction cache for
	// signed manifest v2 archives. V2 plugins fail closed when it is empty.
	PluginBundleCacheDir string
	// PluginTrust is the operator policy used to verify plugin signatures at load
	// time. The zero value is fail-closed: host-risk plugins require a trusted
	// publisher signature.
	PluginTrust plugin.TrustPolicy
	// PluginRuntimeDir, when set, enables the Tier-2 system runner: each system
	// plugin gets a confined 0700 working dir under this path and its verified
	// artifact is executed there (design-08). Empty keeps the noop runner (broker
	// armed, artifact NOT executed) — the deliberate default.
	PluginRuntimeDir string
	// PluginRuntimeEnv is the explicit environment-variable allowlist forwarded
	// to Tier-2 system plugins. Empty means plugins receive only the runner's
	// fixed safe PATH/HOME/TMPDIR.
	PluginRuntimeEnv []string
	// PluginRuntimePool controls persistent stdio-json-v2 worker capacity and
	// retirement. Nil uses the system runner's safe host defaults.
	PluginRuntimePool *plugin.SystemPoolConfig
	// PublicURL is the externally-reachable base URL of this server (scheme +
	// host, no trailing slash), used to build the OIDC redirect URL. Required
	// for SSO login; empty disables the OIDC start/callback flow. It is also the
	// canonical source for the WebAuthn relying-party ID and origin (RPID = host,
	// RPOrigin = scheme://host[:port]); passkeys fail closed when it is empty.
	PublicURL string
	// MetricsToken enables the /metrics endpoint when non-empty. The endpoint
	// accepts only Authorization: Bearer <token>; empty keeps it hidden so public
	// deployments do not expose fleet runtime counters by default.
	MetricsToken string
	// CoreDNSBinary optionally pins the CoreDNS executable that self-host DNS
	// apply scripts may install. Empty preserves the fail-closed precondition
	// that coredns already exists on the node.
	CoreDNSBinary selfdns.CoreDNSBinarySource
	// GeoResolver maps node public IPs to advisory coordinates for the Fleet Map.
	// Nil keeps automatic lookup disabled; manual NodeGeo remains available.
	GeoResolver geoip.Resolver
	// AgentReleaseRepo is the trusted GitHub repository used by the simplified
	// node-agent update UX. Empty uses LatticeNet/lattice-node-agent.
	AgentReleaseRepo string
	// AuditHeadShipping periodically POSTs the verified, locally anchored audit
	// WAL head to an operator-controlled HTTPS endpoint. Empty URL disables it.
	AuditHeadShipping AuditHeadShippingOptions
	// TaskExecutionDisabled is a server-side fleet kill switch. When true, new
	// tasks are not queued and agents receive no task leases. Already leased
	// task results are still accepted so in-flight work can report terminal state.
	TaskExecutionDisabled bool
	// ApprovalAutoRules is the operator's JSON-encoded auto-approve policy list
	// (LATTICE_APPROVAL_AUTO_RULES). Empty keeps approvals fully manual — the
	// default. Malformed input is logged and ignored, never fatal.
	ApprovalAutoRules string
	// RenewalReminderInterval controls the machine-renewal reminder scheduler.
	// Zero uses the production default. DisableRenewalScheduler is intended for
	// tests that need full control over reminder evaluation.
	RenewalReminderInterval time.Duration
	DisableRenewalScheduler bool
	Logger                  *log.Logger
}

type Server

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

func New

func New(opts Options) (*Server, error)

func (*Server) Close added in v0.2.3

func (s *Server) Close(ctx context.Context) error

Close stops runtime-owned subprocesses and waits for their transports to be reaped before the server process exits.

func (*Server) Handler

func (s *Server) Handler() http.Handler

func (*Server) KnownCapabilities added in v0.2.3

func (s *Server) KnownCapabilities() []KnownCapability

KnownCapabilities lists every capability this fleet has, compiled-in and plugin-provided, with the enforcement each one currently has.

type SubscriptionSummary added in v0.2.0

type SubscriptionSummary struct {
	UserID          string `json:"user_id"`
	Email           string `json:"email,omitempty"`
	Enabled         bool   `json:"enabled"`
	Eligible        bool   `json:"eligible"` // enabled AND not expired
	HasSubToken     bool   `json:"has_sub_token"`
	BindingCount    int    `json:"binding_count"`
	CredentialCount int    `json:"credential_count"`
	ExpiresAt       string `json:"expires_at,omitempty"`
}

SubscriptionSummary is the producer-side, per-identity subscription state (design-12 S5). The locked boundary: vpn-core PRODUCES the source (identities, credentials, line bindings, and a sub token). A separately installed plugin may consume that source and publish it. This read-model intentionally exposes only subscription STATE (eligibility, binding/credential counts, whether a sub token exists) — NOT the raw sub token or rendered links, which remain with the legacy /sub substrate and any independently installed publisher plugin.

type UsageByNode added in v0.2.0

type UsageByNode struct {
	NodeID    string `json:"node_id"`
	NodeName  string `json:"node_name,omitempty"`
	UsedBytes int64  `json:"used_bytes"`
	UserCount int    `json:"user_count"`
	At        string `json:"at,omitempty"`
}

type UsageByUser added in v0.2.0

type UsageByUser struct {
	UserID     string `json:"user_id"`
	Email      string `json:"email,omitempty"`
	UsedBytes  int64  `json:"used_bytes"`
	QuotaBytes int64  `json:"quota_bytes,omitempty"`
	Status     string `json:"status,omitempty"`
	LastSeen   string `json:"last_seen,omitempty"`
}

Usage is the vpn-core 3-D usage read-model (design-12 S3). It presents traffic from two operator-facing dimensions — by user and by node — plus the per-(node, user) breakdown. It is DERIVED on demand from the existing accounting substrate: ProxyUser.UsedBytes (monotonic per-user total) and the latest ProxyUsageSnapshot per node (raw per-(node,user) counters), mapped onto VpnUser identities.

S3b line-aware collectors populate line_user_bytes in ProxyUsageSnapshot; older collectors continue to produce aggregate rows with line_hash_id empty.

type UsageCollector added in v0.2.0

type UsageCollector struct {
	NodeID    string `json:"node_id"`
	NodeName  string `json:"node_name,omitempty"`
	Source    string `json:"source,omitempty"`
	Status    string `json:"status,omitempty"`
	Error     string `json:"error,omitempty"`
	CheckedAt string `json:"checked_at,omitempty"`
}

type UsageCollectorRuntime added in v0.2.0

type UsageCollectorRuntime struct {
	Source    string `json:"source,omitempty"`
	Status    string `json:"status,omitempty"`
	CheckedAt string `json:"checked_at,omitempty"`
	LastOKAt  string `json:"last_ok_at,omitempty"`
	LastError string `json:"last_error,omitempty"`
}

type UsageRow added in v0.2.0

type UsageRow struct {
	NodeID     string `json:"node_id"`
	NodeName   string `json:"node_name,omitempty"`
	UserID     string `json:"user_id"`
	Email      string `json:"email,omitempty"`
	LineHashID string `json:"line_hash_id,omitempty"` // empty until S3b sb-stats collector
	Bytes      int64  `json:"bytes"`
}

type VpnCredential added in v0.2.0

type VpnCredential struct {
	Protocol string `json:"protocol"`           // vless|vmess|trojan|shadowsocks|hysteria2|tuic|anytls
	UUID     string `json:"uuid,omitempty"`     // vless/vmess/tuic
	Password string `json:"password,omitempty"` // trojan/shadowsocks/hysteria2/anytls
	Flow     string `json:"flow,omitempty"`     // vless xtls flow
	Method   string `json:"method,omitempty"`   // shadowsocks cipher
	Security string `json:"security,omitempty"` // vmess security
}

VpnCredential is one per-protocol credential. Only the fields relevant to the protocol are populated. uuid/password are secret material.

type VpnUser added in v0.2.0

type VpnUser struct {
	ID          string          `json:"id"`
	Email       string          `json:"email"`
	Name        string          `json:"name,omitempty"`
	Enabled     bool            `json:"enabled"`
	Credentials []VpnCredential `json:"credentials"`
	Bindings    []LineBinding   `json:"bindings"`
	SubID       string          `json:"sub_id,omitempty"`
	QuotaBytes  int64           `json:"quota_bytes,omitempty"`
	// QuotaPeriod is none (lifetime, the default) or monthly; QuotaResetDay is
	// the day of month (1..28) a monthly period starts. Period usage is summed
	// from the user-day rows, never stored.
	QuotaPeriod            string    `json:"quota_period,omitempty"`
	QuotaResetDay          int       `json:"quota_reset_day,omitempty"`
	ExpiresAt              time.Time `json:"expires_at,omitempty"`
	Group                  string    `json:"group,omitempty"`
	Comment                string    `json:"comment,omitempty"`
	SubscriptionGeneration uint64    `json:"-"`

	// MigratedFromProxyUser records the legacy ProxyUser this identity was derived
	// from, so the migration is idempotent and the subscription substrate is traceable.
	MigratedFromProxyUser string `json:"migrated_from_proxy_user,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

VpnUser is the vpn-core identity model (design-12 S2): one human/account identity that carries a credential SET (per protocol) and is bound to many Lines. It is OWNED by the vpn-core plugin — persisted in the plugin's durable KV bucket (plugin:latticenet.vpn-core), not the SDK-typed store — so it is genuinely plugin-owned data with no SDK release coupling. It is additive: the legacy model.ProxyUser stays as the subscription-render + usage-accounting substrate this slice; VpnUsers are derived from ProxyUsers by an idempotent migration.

Credential secrets (uuid/password) are NEVER returned through the read RPC; the gateway-facing views are redacted (see vpnUserView).

Source Files

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL