gRPC API
Агент — gRPC-клиент, backend — сервер (Архитектура → Направление соединения). Весь контракт — два метода.
service ControlPlane { // TLS (без клиентского сертификата), одноразовый enrollment-токен. rpc Enroll(EnrollRequest) returns (EnrollResponse);
// mTLS. Долгоживущий стрим; вся работа мультиплексируется в нём. rpc Sync(stream AgentMessage) returns (stream ServerMessage);}Идентичность ноды в Sync берётся из клиентского сертификата (CN = node id) — в сообщениях она не передаётся и не может быть подделана.
Enrollment
Section titled “Enrollment”message EnrollRequest { string token = 1; // одноразовый, из панели string csr_pem = 2; // ключ сгенерирован на ноде, ноду не покидает string hostname = 3; string agent_version = 4;}
message EnrollResponse { string node_id = 1; string client_cert_pem = 2; // CSR, подписанный внутренним CA string ca_cert_pem = 3; // для пиннинга серверного сертификата backend}Детали протокола доверия — Безопасность агента.
Sync: сообщения агента
Section titled “Sync: сообщения агента”message AgentMessage { oneof msg { Hello hello = 1; // первое сообщение после открытия стрима ApplyResult apply_result = 2; // итог применения DesiredState StatusReport status = 3; // периодически (30 с) и по изменению UsageReport usage = 4; // накопленные дельты трафика LogBatch logs = 5; // ответ на Command.get_logs / поток лога CertRenewal cert_renewal = 6; // плановая ротация mTLS-сертификата агента }}
message CertRenewal { string csr_pem = 1; // новый ключ сгенерирован на ноде}
message Hello { string hostname = 1; string os = 2; string arch = 3; string agent_version = 4; string xray_version = 5; int64 applied_revision = 6; // что применено локально; backend сравнит и дошлёт}
message ApplyResult { int64 revision = 1; bool success = 2; string error = 3; // пусто при success}
message StatusReport { bool xray_running = 1; double cpu_percent = 2; int64 memory_used_bytes = 3; int64 memory_total_bytes = 4; int64 disk_used_bytes = 5; int64 disk_total_bytes = 6; int32 connections = 7; int64 xray_uptime_seconds = 8;}
message UsageReport { int64 report_id = 1; // монотонный на ноду; идемпотентность repeated UsageEntry entries = 2;}
message UsageEntry { string subscription_id = 1; int64 uplink_bytes = 2; int64 downlink_bytes = 3;}
message LogBatch { string command_id = 1; // какой команде отвечаем repeated LogEntry entries = 2; bool eof = 3; // для get_logs; в live-потоке не используется}
message LogEntry { int64 timestamp_ms = 1; LogSource source = 2; // AGENT | XRAY string line = 3;}Sync: сообщения backend
Section titled “Sync: сообщения backend”message ServerMessage { oneof msg { DesiredState desired_state = 1; UsageAck usage_ack = 2; Command command = 3; CertIssued cert_issued = 4; // ответ на CertRenewal }}
message CertIssued { string client_cert_pem = 1; // агент атомарно заменяет identity; ключ остаётся локальным}
message DesiredState { int64 revision = 1; NodeConfig config = 2; // всегда полный снапшот, не diff}
message UsageAck { int64 report_id = 1; // агент очищает подтверждённый буфер}
message Command { string command_id = 1; oneof cmd { RestartXray restart_xray = 2; // принудительный рестарт процесса GetLogs get_logs = 3; StartLogStream start_log_stream = 4; // live-tail в LogBatch StopLogStream stop_log_stream = 5; }}
message GetLogs { LogSource source = 1; int32 limit = 2; // последние N строк}Backend отправляет DesiredState при открытии стрима (если Hello.applied_revision < desired_revision) и при каждом изменении. Доставляется только последний снапшот: если нода отстала на несколько ревизий, промежуточные не отправляются.
NodeConfig
Section titled “NodeConfig”Полное желаемое состояние ноды. Агент приводит ноду к нему и ничего не додумывает.
message NodeConfig { XrayConfig xray = 1; // конфигурация движка Xray
repeated Certificate certificates = 10; // движко-независимая часть}
message XrayConfig { repeated Inbound inbounds = 1; // клиентские и chain-in repeated Outbound outbounds = 2; // freedom, blackhole, хопы цепочек repeated RoutingRule routing_rules = 3; // порядок значим: первое совпадение побеждает}
message Certificate { string id = 1; string cert_pem = 2; string key_pem = 3;}Xray-специфика изолирована в собственном поле намеренно: появление второго движка (sing-box, WireGuard) — аддитивное поле NodeConfig с новым номером, а не новая версия контракта. Абстракция «обобщённого сервиса» при этом не вводится — общее между движками станет ясно только при реальной второй реализации; сейчас покупается лишь совместимость транспорта. Сертификаты — вне XrayConfig: доставка PEM на ноду от движка не зависит.
message Inbound { string tag = 1; Protocol protocol = 2; uint32 port = 3; string listen = 4; StreamSettings stream = 5; Sniffing sniffing = 6; repeated InboundUser users = 7;}
message InboundUser { string email = 1; // "<subscription_id>@astral" или "chain:<chain_id>:<pos>" oneof account { VlessAccount vless = 10; VmessAccount vmess = 11; TrojanAccount trojan = 12; ShadowsocksAccount shadowsocks = 13; }}
message Outbound { string tag = 1; Protocol protocol = 2; // + FREEDOM | BLACKHOLE string address = 3; // следующий хоп; пусто для freedom/blackhole uint32 port = 4; oneof account { VlessAccount vless = 10; TrojanAccount trojan = 11; ShadowsocksAccount shadowsocks = 12; } StreamSettings stream = 20;}
message RoutingRule { string inbound_tag = 1; string outbound_tag = 2; repeated string protocols = 3; // сниффенные ("bittorrent"); пусто — весь инбаунд}Общие типы
Section titled “Общие типы”enum Protocol { PROTOCOL_UNSPECIFIED = 0; VLESS = 1; VMESS = 2; TROJAN = 3; SHADOWSOCKS = 4; FREEDOM = 5; BLACKHOLE = 6; }enum Network { NETWORK_UNSPECIFIED = 0; TCP = 1; WS = 2; GRPC = 3; HTTPUPGRADE = 4; XHTTP = 5; }enum SecurityT { SECURITY_UNSPECIFIED = 0; NONE = 1; TLS = 2; REALITY = 3; }enum LogSource { LOG_SOURCE_UNSPECIFIED = 0; AGENT = 1; XRAY = 2; }
message VlessAccount { string id = 1; string flow = 2; }message VmessAccount { string id = 1; }message TrojanAccount { string password = 1; }message ShadowsocksAccount { string method = 1; string password = 2; }StreamSettings
Section titled “StreamSettings”message StreamSettings { Network network = 1; SecurityT security = 2; oneof transport { TcpSettings tcp = 10; WsSettings ws = 11; GrpcSettings grpc = 12; HttpUpgradeSettings httpupgrade = 13; XhttpSettings xhttp = 14; } oneof tls { TlsSettings tls_settings = 20; RealitySettings reality = 21; }}
message TcpSettings { bool accept_proxy_protocol = 1; }message WsSettings { string path = 1; map<string, string> headers = 2; }message GrpcSettings { string service_name = 1; bool multi_mode = 2; }message HttpUpgradeSettings { string path = 1; string host = 2; }message XhttpSettings { string path = 1; string host = 2; XhttpMode mode = 3; }enum XhttpMode { XHTTP_MODE_UNSPECIFIED = 0; AUTO = 1; PACKET_UP = 2; STREAM_UP = 3; STREAM_ONE = 4; }
message TlsSettings { repeated string server_names = 1; string alpn = 2; string min_version = 3; string certificate_id = 4; // ссылка на NodeConfig.certificates; пути к файлам — забота агента}
message RealitySettings { string dest = 1; repeated string server_names = 2; string private_key = 3; string public_key = 4; repeated string short_ids = 5; string fingerprint = 6;}
message Sniffing { bool enabled = 1; repeated string dest_override = 2; // http, tls, quic, bittorrent bool route_only = 3;}TlsSettings.certificate_id вместо путей к файлам: контракт не знает про файловую систему ноды — раскладку PEM по путям и подстановку путей в конфиг Xray выполняет агент.
Ошибки
Section titled “Ошибки”| Ситуация | Статус gRPC |
|---|---|
| Невалидный/истёкший/использованный enrollment-токен | UNAUTHENTICATED |
Sync без клиентского сертификата или нода disabled/удалена |
UNAUTHENTICATED |
| Ошибка применения desired state | Не ошибка RPC: агент шлёт ApplyResult{success: false}, стрим живёт |
| Разрыв стрима | Агент переподключается с экспоненциальным backoff (1 с … 60 с, jitter) |
Эволюция контракта
Section titled “Эволюция контракта”Поля только добавляются (proto3, новые номера); удаление — резервированием номера. Несовместимые изменения — новый пакет astral.controlplane.v2, backend обслуживает обе версии на период миграции агентов. Версия агента известна из Hello — backend может подсветить устаревшие ноды в панели.