Найбільше вузьке місце до запуску: агент заводився 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>
364 lines
15 KiB
Protocol Buffer
364 lines
15 KiB
Protocol Buffer
// =====================================================================
|
||
// 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;
|
||
}
|