Netpulse_SasS/proto/netpulse/v1/agent.proto
byrsapty ddaae60fa0 Етап 8: реєстрація зонда одноразовим запрошенням
Найбільше вузьке місце до запуску: агент заводився INSERT-ом у базу, а
токен вписувався в командний рядок руками. Поставити зонд у клієнта було
неможливо.

core.agent_enrollments тримає sha256 одноразового токена; сам токен
повертається рівно один раз. Видача під FOR UPDATE в одній транзакції:
два агенти з однієї скопійованої команди інакше створили б два зонди з
одного запрошення. Відповідь на «немає», «згоріло» і «використано»
однакова — розрізняти їх означає підказувати тому, хто підбирає токени.

Токен зонда їде окремим полем agent_token, а не в certificate:
сертифікат відповідає на інше питання й живе за іншим циклом.

Агент зберігає посвідчення в /etc/netpulse/agent.json з правами 0600,
через тимчасовий файл і перейменування — обрив живлення посеред запису
інакше лишив би половину токена.

Знайдено живим прогоном: реєстрація не проходила автентифікацію, бо
інтерсептор стоїть на всьому сервері, а не на окремому сервісі — мій же
коментар стверджував протилежне. І запуск із самим посвідченням падав:
validate() вимагав -agent-id, не знаючи про файл.

Сторінка зондів: команда встановлення з токеном, відкликання
запрошень, керування модулями й лімітами, видалення.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:50:57 +03:00

364 lines
15 KiB
Protocol Buffer
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// =====================================================================
// NetPulse :: agent.proto
// Головний контракт агент↔сервер.
//
// ФУНДАМЕНТАЛЬНЕ ОБМЕЖЕННЯ: усі з'єднання ініціює агент.
// Сервер ніколи не стукає в мережу клієнта — там немає ані відкритих
// портів, ані прокидання NAT. Тому "команда з сервера" фізично є
// повідомленням у зустрічному напрямку вже відкритого агентом
// bidi-стріму Control.
// =====================================================================
syntax = "proto3";
package netpulse.v1;
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
import "netpulse/v1/common.proto";
import "netpulse/v1/discovery.proto";
import "netpulse/v1/logs.proto";
import "netpulse/v1/ncm.proto";
import "netpulse/v1/telemetry.proto";
option go_package = "github.com/netpulse/netpulse/gen/go/netpulse/v1;netpulsev1";
// ---------------------------------------------------------------------
// Реєстрація зонда
//
// Єдиний RPC без клієнтського сертифіката: агент приходить із
// одноразовим enrollment-токеном (згенерованим у UI) і CSR, а йде
// з власним сертифікатом. Далі — тільки mTLS.
// ---------------------------------------------------------------------
service EnrollmentService {
rpc Enroll(EnrollRequest) returns (EnrollResponse);
}
message EnrollRequest {
// Одноразовий токен із UI: "np_enroll_...". Згорає після використання.
string enrollment_token = 1;
// PKCS#10. Приватний ключ ніколи не залишає агента.
bytes csr = 2;
string hostname = 3;
AgentBuild build = 4;
// Бажане ім'я зонда; сервер може змінити на унікальне.
string requested_name = 5;
}
message EnrollResponse {
string agent_id = 1;
string agent_name = 2;
// Підписаний клієнтський сертифікат + ланцюг CA сервера.
bytes certificate = 3;
bytes ca_chain = 4;
google.protobuf.Timestamp certificate_expires_at = 5;
// Куди підключатись далі (може відрізнятись від адреси реєстрації:
// балансувальник, регіональний шлюз).
string control_endpoint = 6;
// Постійний токен зонда: визначає, ЯКИЙ це зонд. Видається рівно
// один раз — далі агент зберігає його сам.
//
// Окреме поле, а не certificate: сертифікат відповідає на інше
// питання («чи має право говорити з сервером») і живе за іншим
// життєвим циклом. Складати два різні секрети в одне поле означає
// зафіксувати проміжний етап у протоколі назавжди.
string agent_token = 7;
}
// ---------------------------------------------------------------------
// Основний сервіс
// ---------------------------------------------------------------------
service AgentService {
// Довгоживучий двонаправлений канал керування. Одна сесія = одне
// з'єднання. Розрив стріму = кінець сесії з усім її станом
// (таблиця серій, in-flight батчі).
rpc Control(stream ControlUp) returns (stream ControlDown);
// Телеметрія окремим стрімом, щоб пачка на 10 000 семплів
// не блокувала heartbeat і не затримувала команду з сервера.
rpc StreamTelemetry(stream TelemetryBatch) returns (stream TelemetryAck);
// Syslog/трапи — теж окремо: сплеск логів під час аварії не має
// топити телеметрію, за якою ця аварія й видно.
rpc StreamLogs(stream LogBatch) returns (stream LogAck);
// Автовиявлення: рідко, великими звітами.
rpc ReportDiscovery(DiscoveryReport) returns (DiscoveryAck);
// Вивантаження зібраного конфігу (чанками).
rpc UploadConfig(stream ConfigUpload) returns (ConfigReceipt);
}
// ---------------------------------------------------------------------
// Агент → Сервер
// ---------------------------------------------------------------------
message ControlUp {
// Монотонний номер повідомлення в сесії — для трасування.
uint64 seq = 1;
oneof payload {
Hello hello = 2;
Heartbeat heartbeat = 3;
TaskStatusUpdate task_status = 4;
ModuleStatusUpdate module_status = 5;
ConfigApplyResult config_apply_result = 6;
CredentialRequest credential_request = 7;
Pong pong = 8;
AgentEvent event = 9;
}
}
// Перше повідомлення в стрімі. Сервер відповідає Welcome.
message Hello {
string agent_id = 1;
AgentBuild build = 2;
string hostname = 3;
// Локальні адреси зонда — корисно для L2-виявлення й діагностики NAT.
repeated string local_addresses = 4;
google.protobuf.Timestamp started_at = 5;
// Хеш конфігурації задач, яку агент має локально. Якщо збігається
// з серверним — сервер не шле повний план, лише дельти.
bytes task_plan_hash = 6;
// Останній батч, який агент вважає підтвердженим. Дозволяє
// продовжити з місця розриву замість повного перезливу.
uint64 last_acked_batch_id = 7;
}
message Heartbeat {
google.protobuf.Timestamp ts = 1;
AgentHealth health = 2;
// Скільки задач зараз виконується / чекає в черзі.
uint32 tasks_running = 3;
uint32 tasks_queued = 4;
}
// Життєвий цикл задачі. Сервер оновлює core.checks.last_run_at/last_error.
message TaskStatusUpdate {
string check_id = 1;
enum State {
STATE_UNSPECIFIED = 0;
STATE_ACCEPTED = 1;
STATE_RUNNING = 2;
STATE_SUCCEEDED = 3;
STATE_FAILED = 4;
STATE_SKIPPED = 5; // не встиг у вікно інтервалу
STATE_REJECTED = 6; // модуль не активний або параметри невалідні
}
State state = 2;
google.protobuf.Timestamp ts = 3;
Error error = 4;
}
message ModuleStatusUpdate {
string module_key = 1;
bool active = 2;
string version = 3;
Error error = 4;
}
// Агент просить креденшели: або вперше, або бо старі протермінувались.
message CredentialRequest {
repeated string device_ids = 1;
string reason = 2; // "expired" | "missing" | "auth_failed"
}
message Pong {
uint64 ping_id = 1;
google.protobuf.Timestamp agent_time = 2;
}
// Позапланова подія самого зонда (не пристрою).
message AgentEvent {
enum Kind {
KIND_UNSPECIFIED = 0;
KIND_STARTED = 1;
KIND_STOPPING = 2;
KIND_MODULE_CRASH = 3;
KIND_BUFFER_OVERFLOW = 4;
KIND_CLOCK_JUMP = 5;
KIND_UPDATE_APPLIED = 6;
KIND_CONFIG_REJECTED = 7;
}
Kind kind = 1;
google.protobuf.Timestamp ts = 2;
string message = 3;
map<string, string> details = 4;
}
// ---------------------------------------------------------------------
// Сервер → Агент
// ---------------------------------------------------------------------
message ControlDown {
uint64 seq = 1;
oneof payload {
Welcome welcome = 2;
TaskPlan task_plan = 3;
TaskDelta task_delta = 4;
ModuleControl module_control = 5;
CredentialBundle credentials = 6;
ConfigJob config_job = 7;
ConfigApplyJob config_apply_job = 8;
DiscoveryRequest discovery_request = 9;
Ping ping = 10;
Directive directive = 11;
}
}
message Welcome {
string session_id = 1;
// Час сервера — агент рахує з нього clock_skew. Мітки часу в
// телеметрії лишаються агентськими, але сервер знає поправку.
google.protobuf.Timestamp server_time = 2;
google.protobuf.Duration heartbeat_interval = 3;
// Параметри батчингу телеметрії. Сервер диктує їх на льоту —
// так можна пригальмувати балакучого агента без оновлення бінарника.
uint32 telemetry_max_batch_size = 4;
google.protobuf.Duration telemetry_max_batch_interval = 5;
uint32 telemetry_max_in_flight = 6;
// Скільки чеків агенту дозволено виконувати паралельно.
uint32 max_concurrent_checks = 7;
// Ліміт ICMP, щоб зонд не виглядав як сканер для IDS клієнта.
uint32 icmp_rate_pps = 8;
// Повний план задач надійде окремим TaskPlan, якщо хеш не збігся.
bool task_plan_follows = 9;
}
// Повна синхронізація: те, що агент має виконувати. Заміщає все.
message TaskPlan {
bytes plan_hash = 1;
repeated Task tasks = 2;
repeated DeviceTarget devices = 3;
// План може прийти частинами; агент застосовує його атомарно
// лише після final = true.
bool final = 4;
uint32 part = 5;
}
// Інкрементальна зміна плану — типовий випадок після додавання
// одного пристрою в UI. Перезаливати 50 000 задач заради цього не треба.
message TaskDelta {
bytes plan_hash = 1; // хеш плану ПІСЛЯ застосування дельти
repeated Task upsert = 2;
repeated string remove_check_ids = 3;
repeated DeviceTarget upsert_devices = 4;
repeated string remove_device_ids = 5;
}
// Одна задача опитування. Дзеркалить core.checks.
message Task {
string check_id = 1;
string device_id = 2;
// Заповнюється для чеків рівня інтерфейсу.
string interface_id = 3;
// "<plugin>.<check>": icmp.ping, snmp.if, http.status, ncm.backup.
// Модуль-виконавець визначається префіксом до крапки.
string check_type = 4;
// Параметри чека — непрозорий для ядра JSON. Валідність гарантує
// params_schema з core.check_types; розбирає їх сам модуль.
// Так новий плагін не потребує зміни .proto.
bytes params_json = 5;
google.protobuf.Duration interval = 6;
google.protobuf.Duration timeout = 7;
uint32 retries = 8;
bool enabled = 9;
// Зсув у межах інтервалу, щоб 5000 чеків не стартували одночасно.
// Розраховує сервер — детерміновано від check_id, щоб зберігався
// між перезапусками.
google.protobuf.Duration schedule_offset = 10;
string credential_id = 11;
}
// Активація/деактивація модулів на зонді.
message ModuleControl {
repeated ModuleSpec modules = 1;
// Модулі, відсутні в списку, вимкнути.
bool exclusive = 2;
}
message ModuleSpec {
string key = 1; // icmp, snmp, topology, ncm, modbus
bool enabled = 2;
string min_version = 3;
// Налаштування модуля (JSON), напр. розмір SNMP-пулу.
bytes config_json = 4;
}
message CredentialBundle {
// device_id → креденшели, які до нього застосовні, за пріоритетом.
map<string, CredentialList> by_device = 1;
// Спільний строк придатності комплекту.
google.protobuf.Timestamp expires_at = 2;
}
message CredentialList {
repeated Credential credentials = 1;
}
message DiscoveryRequest {
string run_id = 1;
repeated DiscoveryProto protocols = 2;
// Пристрої, які опитати на предмет сусідів.
repeated string device_ids = 3;
// Підмережі для сканування (CIDR). Порожньо — не сканувати.
repeated string subnets = 4;
// Обмеження темпу сканування, щоб не збурювати мережу клієнта.
uint32 scan_rate_pps = 5;
google.protobuf.Duration timeout = 6;
}
message Ping {
uint64 ping_id = 1;
google.protobuf.Timestamp server_time = 2;
}
// Команди життєвого циклу самого агента.
message Directive {
enum Action {
ACTION_UNSPECIFIED = 0;
// Перечитати конфігурацію, не розриваючи сесію.
ACTION_RELOAD = 1;
// Дозбирати й відправити буфер, потім коректно завершитись.
ACTION_DRAIN = 2;
// Зупинити опитування, лишити канал (несплата → grace-період).
ACTION_PAUSE = 3;
ACTION_RESUME = 4;
// Доступне оновлення: деталі в update.
ACTION_UPDATE = 5;
// Перепідключитись (перебалансування сервера). Агент чекає
// reconnect_after і йде на новий endpoint.
ACTION_RECONNECT = 6;
// Скинути локальну таблицю series_ref і перереєструвати серії.
ACTION_RESET_SERIES_TABLE = 7;
}
Action action = 1;
string reason = 2;
google.protobuf.Duration reconnect_after = 3;
string new_endpoint = 4;
UpdateInfo update = 5;
}
message UpdateInfo {
string version = 1;
string download_url = 2;
// sha256 бінарника — агент зобов'язаний звірити перед запуском.
bytes sha256 = 3;
// Підпис постачальника (Ed25519) над sha256. Без валідного підпису
// оновлення не застосовується: інакше компрометація CDN
// перетворюється на RCE в мережі кожного клієнта.
bytes signature = 4;
bool mandatory = 5;
}