6.7 KiB
Core Private Node Routes
These routes are consumed only by the node agent and require the x-node-auth-key header.
Transport
The node uses two transports to communicate with Core:
- HTTP — registration only (
POST /node/register). - gRPC — all other node-to-core calls: heartbeat, usage reporting, config sync, and stats. Core listens on
GRPC_PORT(default3002). The node connects viaCORE_GRPC_URL. SetCORE_INSECURE=trueon the node to use plaintext instead of TLS.
The HTTP routes for heartbeat, usage, config, and error remain available as a fallback but the node agent uses gRPC by default.
Authentication
POST /node/registeris the only public route in this group.- All other HTTP routes require the
x-node-auth-keyheader. - gRPC calls pass the auth key as the
x-node-auth-keymetadata header (or in theauthKeyrequest field as fallback). - The auth key is issued by Core during registration and persisted on the node side.
Route table
| Method | Route | Input | Output / Notes |
|---|---|---|---|
POST |
/node/register |
RegisterNodeDto |
Returns { authKey, nodeId } |
POST |
/node/heartbeat |
NodeHeartbeatDto |
Returns { ok: true, needSync } |
POST |
/node/usage |
NodeUsageDto |
Returns { ok: true } |
POST |
/node/outbound-usage |
NodeOutboundUsageDto |
Returns { ok: true } |
POST |
/node/session-events |
SessionEventsDto |
Returns { ok: true } |
GET |
/node/config/latest |
none | Returns NodeConfig |
POST |
/node/config/applied |
NodeConfigAppliedDto |
Returns { ok: true } |
POST |
/node/error |
NodeErrorDto |
Returns { ok: true } |
Request payloads
RegisterNodeDto
code: stringname: stringip: stringport: number— agent HTTP port (required; auto-detected fromPORTenv on the node)region: stringdomain?: string— optional domain name (omitted on auto-registration; can be set later via admin panel)
NodeHeartbeatDto
ip?: stringrevision?: number
The node sends its current revision in heartbeats. Core uses it to decide whether a sync is needed.
NodeUsageDto
entries: UsageEntryDto[]
UsageEntryDto fields:
userId: numbersubscriptionId?: number | nullnodeProtocolId: numberbytesUp: numberbytesDown: numberrecordedAt: string
NodeOutboundUsageDto
items: OutboundUsageItem[]
OutboundUsageItem fields:
tag: stringbytesUp: numberbytesDown: number
SessionEventsDto
events: SessionEventEntryDto[]
SessionEventEntryDto fields:
userId: numberprotocol: VpnProtocoleventType: SessionEventTypeoccurredAt: stringipAddress?: string
NodeConfigAppliedDto
revision: number
NodeErrorDto
message: stringcode?: stringdetails?: Record<string, unknown>
NodeConfig
The config returned by GET /node/config/latest has this structure:
{
revision: number
clientMap: Record<string, { userId: number; subscriptionId: number | null }>
mtprotoClientMap: Record<string, { userId: number; subscriptionId: number | null }>
services: {
xray?: XrayServiceConfig
mtproto?: MtProtoServiceConfig
}
}
Important notes:
clientMapmaps subscription-link IDs (lsId) to the user and subscription — used for both xray and telemt traffic/online-IP attribution (the telemt username now mirrors the xray emailu{userId}.l{linkId}.s{lsId}.i{protoId}).mtprotoClientMap(lsUuid-keyed) is retained on the wire for backward compatibility but is no longer used for attribution.services.xraycontains the Xray inbounds, routing, and outbounds for the node.services.mtprotocontains MTProto inbounds fortelemt.- The node stores the current revision and client maps locally after a successful apply.
Route semantics
POST /node/heartbeatupdateslastSeenAtand can request a sync when the node revision is behind.POST /node/usagestores per-minute usage rows and increments subscription traffic counters.POST /node/outbound-usageincrementsnode_outbounds.traffic_bytes_up/down.POST /node/session-eventsstores online/offline events insession_events.POST /node/config/appliedmarks the node sync status assynced.POST /node/errormarks the node aserrorand the sync status asfailed.
gRPC interface (NodeService)
Core exposes a gRPC service that the node agent uses for all real-time communication. The service name is nodeservice.NodeService and is defined in proto/node-service.proto.
Authentication: pass x-node-auth-key as gRPC metadata (or in the authKey field of the request as fallback).
| Method | Request fields | Response fields | Notes |
|---|---|---|---|
Heartbeat |
authKey, revision?, ip? |
ok, needSync |
Equivalent to POST /node/heartbeat |
ReportUsage |
authKey, entries[] |
ok |
Equivalent to POST /node/usage |
ReportOutboundUsage |
authKey, items[] |
ok |
Equivalent to POST /node/outbound-usage |
GetLatestConfig |
authKey |
revision, clientMapJson, mtprotoClientMapJson, servicesJson |
Config fields are JSON-encoded strings |
ConfirmConfigApplied |
authKey, revision |
ok |
Equivalent to POST /node/config/applied |
ReportNodeStats |
authKey, system{}, xray{}, mtprotoJson |
ok |
Reports CPU/RAM/disk/network/xray/mtproto snapshot; stored in Redis; drives GET /admin/nodes/:id/runtime-status and stats history |
ReportOnlineUsers |
authKey, users[] |
ok |
Reports currently connected users with their IPs every 5 s; Core stores Redis TTL keys and updates IP history |
ReportNodeStats nested fields:
system:uptimeSeconds,cpuPercent,cpuCores,ramUsed,ramTotal,diskUsed,diskTotal,netRxPerSec,netTxPerSec,netRxTotal,netTxTotalxray:running,uptimeSeconds?mtprotoJson: JSON stringRecord<tag, MtProtoStatus>
ReportOnlineUsers.users[] entry: userId, nodeProtocolId, ips[]{ip, lastSeen}.
The node sends ReportNodeStats every 2 s. GET /admin/nodes/:id/runtime-status returns the latest stored snapshot without polling the node; ?fromNode=true forces a live HTTP fetch instead.
The node sends ReportOnlineUsers every 5 s. Core sets online:user:{userId} and online:inbound:{nodeProtocolId}:user:{userId} Redis keys with TTL 20 s. Existing (userId, ip) rows only receive a new lastSeenAt; missing pairs are inserted. IP information enrichment supports IPinfo.io and ip-api.com. A daily job refreshes all known addresses, while a job running every 10 minutes processes addresses with firstSeenAt > now - 10 minutes. Results are stored in user_ips.info. Core also emits a user.online domain event when the user had a gap > 10 s.