merge: maestro/tsk__R8M4jEw43JR [tsk__R8M4jEw43JR]

This commit is contained in:
wangjia
2026-06-13 17:04:17 +08:00
25 changed files with 2963 additions and 0 deletions
+212
View File
@@ -0,0 +1,212 @@
// Package pangolin.agent.v1 is the frozen control-plane <-> node-agent contract.
//
// CONNECTION DIRECTION: the node agent ALWAYS dials the control plane (never the
// reverse). Every RPC except Enroll requires a verified mTLS client certificate
// whose Subject CN equals the node UUID (see server/internal/mtls). Enroll is the
// single bootstrap exception, authenticated by a one-time bootstrap token instead.
//
// DATA-PLANE RED LINE (doc/06 §3, doc/04 §2): messages on this contract carry ONLY
// the opaque data-plane credential id (dp_uuid) and aggregate counters. They MUST
// NOT carry user_id, email, device id, destination address, SNI or any DNS query.
//
// NOTE ON CODE GENERATION: this repository has no protoc/buf toolchain wired yet
// (Makefile `generate` is a stub). Until task #5's proto pipeline lands, the Go
// types and gRPC stubs in server/internal/pb/agentv1 are hand-written to mirror
// this file 1:1 and are wire-encoded with a JSON gRPC codec. When protoc is
// introduced this file is the source of truth; field numbers are reserved here so
// the generated code stays compatible.
syntax = "proto3";
package pangolin.agent.v1;
option go_package = "github.com/wangjia/pangolin/server/internal/pb/agentv1;agentv1";
// AgentService is implemented by the control plane and consumed by node agents.
service AgentService {
// Enroll exchanges a one-time bootstrap token + CSR for a node client
// certificate (CN = node_uuid, 90d). Exempt from mTLS — this is the only RPC an
// un-enrolled agent may call. Idempotent only within the token TTL.
rpc Enroll(EnrollRequest) returns (EnrollResponse);
// Register is called after (re)connecting over mTLS. It returns the authoritative
// full ConfigSnapshot so the agent can converge local state.
rpc Register(RegisterRequest) returns (ConfigSnapshot);
// Heartbeat reports liveness + load every ~30s and the agent's local
// config_version. The control plane flags need_full_resync when it detects drift.
rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse);
// Subscribe is a server-streaming feed of Commands. The agent passes the last
// command_id it durably processed so the control plane can resume without gaps
// (at-least-once; commands are idempotent and Ack'd individually).
rpc Subscribe(SubscribeRequest) returns (stream Command);
// Ack confirms a command was applied. Safe to retry.
rpc Ack(AckRequest) returns (AckResponse);
// ReportUsage uploads per-dp_uuid aggregate byte/minute counters. No identities.
rpc ReportUsage(UsageReport) returns (UsageAck);
}
// ─── enroll ──────────────────────────────────────────────────────────────────
message EnrollRequest {
string bootstrap_token = 1; // one-time, injected via cloud-init
bytes csr_pem = 2; // PKCS#10 CSR, agent-generated key never leaves node
string agent_version = 3;
}
message EnrollResponse {
string node_uuid = 1; // authoritative; becomes the cert CN
bytes cert_pem = 2; // signed leaf, 90d, EKU=clientAuth
bytes ca_pem = 3; // CA to pin for server validation
int64 not_after_unix = 4;
}
// ─── register / config ─────────────────────────────────────────────────────────
message RegisterRequest {
string node_uuid = 1;
string agent_version = 2;
int64 local_config_version = 3; // 0 on a fresh node
}
// Protocol selects which inbound(s) a credential is provisioned on.
enum Protocol {
PROTOCOL_UNSPECIFIED = 0;
PROTOCOL_REALITY = 1; // VLESS+REALITY inbound, user = {uuid, flow}
PROTOCOL_HY2 = 2; // Hysteria2 inbound, user = {password}
PROTOCOL_BOTH = 3; // both inbounds share the same dp_uuid
}
// Credential is the ONLY per-subscriber object a node ever sees.
message Credential {
string dp_uuid = 1; // opaque data-plane id; REALITY user uuid
Protocol protocol = 2;
string flow = 3; // e.g. "xtls-rprx-vision"
int64 expires_at_unix = 4; // agent-side TTL; 0 = no expiry
}
// ConfigSnapshot is the full authoritative node state.
message ConfigSnapshot {
int64 config_version = 1;
repeated Credential credentials = 2;
// Inbound parameters (node-wide, not per-user).
RealityInbound reality = 3;
Hy2Inbound hy2 = 4;
// Baseline command id already reflected in this snapshot; the agent resumes
// Subscribe from here.
int64 last_command_id = 5;
}
message RealityInbound {
int32 listen_port = 1;
string private_key = 2; // REALITY server private key (base64)
string short_id = 3;
string server_name = 4; // masquerade SNI handshake target
string handshake_server = 5;
int32 handshake_port = 6;
}
message Hy2Inbound {
int32 listen_port = 1;
string masquerade = 2;
string cert_path = 3;
string key_path = 4;
}
// ─── heartbeat ───────────────────────────────────────────────────────────────
message HeartbeatRequest {
string node_uuid = 1;
int64 config_version = 2;
int32 online_peers = 3;
int64 bandwidth_up_bps = 4;
int64 bandwidth_down_bps = 5;
double cpu_percent = 6;
int64 timestamp_unix = 7;
}
message HeartbeatResponse {
bool need_full_resync = 1; // agent must Register + overwrite local state
int64 server_time_unix = 2;
}
// ─── command stream ──────────────────────────────────────────────────────────
enum CommandType {
COMMAND_TYPE_UNSPECIFIED = 0;
COMMAND_TYPE_UPSERT = 1; // add/update a credential
COMMAND_TYPE_REVOKE = 2; // remove a credential
COMMAND_TYPE_ROTATE_CREDENTIAL = 3; // new replaces old, old kept until grace
COMMAND_TYPE_APPLY_CONFIG = 4; // replace inbound params / full snapshot
COMMAND_TYPE_LIFECYCLE = 5; // drain / resume / shutdown
}
enum LifecycleAction {
LIFECYCLE_ACTION_UNSPECIFIED = 0;
LIFECYCLE_ACTION_DRAIN = 1;
LIFECYCLE_ACTION_RESUME = 2;
LIFECYCLE_ACTION_SHUTDOWN = 3;
}
message Command {
int64 command_id = 1; // monotonically increasing per node
CommandType type = 2;
UpsertPayload upsert = 3;
RevokePayload revoke = 4;
RotatePayload rotate = 5;
ConfigSnapshot apply_config = 6;
LifecyclePayload lifecycle = 7;
}
message UpsertPayload {
Credential credential = 1;
}
message RevokePayload {
string dp_uuid = 1;
}
message RotatePayload {
string old_dp_uuid = 1;
Credential new_credential = 2;
int64 grace_until_unix = 3; // old credential kept active until this time
}
message LifecyclePayload {
LifecycleAction action = 1;
}
message SubscribeRequest {
string node_uuid = 1;
int64 last_command_id = 2; // resume point; 0 = from the beginning
}
message AckRequest {
string node_uuid = 1;
int64 command_id = 2;
}
message AckResponse {}
// ─── usage ───────────────────────────────────────────────────────────────────
message UsageEntry {
string dp_uuid = 1; // never a user id
int64 bytes_up = 2;
int64 bytes_down = 3;
int64 session_minutes = 4;
}
message UsageReport {
string node_uuid = 1;
int64 window_start_unix = 2;
int64 window_end_unix = 3;
repeated UsageEntry entries = 4;
}
message UsageAck {}