Один коміт, а не десяток тематичних, свідомо: теми переплетені в
спільних файлах (store.go, docker-compose.yml, deploy/README.md), і
розділити їх можна було б лише індексуванням шматків. Коміти, які не
збираються, гірші за один великий — тим паче що це рівно той стан, який
перевірявся разом.
ЩО ПРАЦЮЄ НА СТЕНДІ Й ПЕРЕВІРЕНО ТАМ
0058 подієві алерти: syslog, ncm, compliance спрацьовують у мить
події; правило з нереалізованим джерелом більше не зберігається
мовчки
0059 snmp.walk і прототипи шаблонів — таблиці з динамічним індексом
описуються шаблоном, а не Go
0060 відкат конфігу: план як різниця, маскування паролів із підписом
плану, обов'язковий контрольний збір, verifying при обриві
0061 кнопки Telegram: довге опитування, авторизація не з callback_data
0062 аудит і архів хостів; тест на AST, що падає на ключі без назви
0063 RLS: три ролі, окремий пул для фонових тактів
0064 строки зберігання даних і сторінка сховища
0065 приймач SNMP-трапів; перевірено справжніми пакетами по дроту,
переклад v1→v2 за RFC 3584 дає правильний OID
0066 ескалації сповіщень
0067 алерт про вичерпання диска
0068 поля заливки конфігу переїхали в каталог профілів
Плюс: 137 тестів вебу з нуля (їх не було взагалі), одинадцять справжніх
вад, знайдених ними й виправлених, і виправлення двох інтеграційних
тестів grpcapi, які мовчки пропускались півтора року.
ЩО ЩЕ НЕ ЗАПУСКАЛОСЬ
netpulse установник: одна команда замість 18 змінних і
593 рядків інструкції
RLS з першого запуску нова інсталяція під політиками одразу;
RLS-EXISTING-INSTALL.md лишається тільки для
старих інсталяцій
.forgejo + CI раннер не зареєстрований
Ці три перевірені компіляцією й міркуванням, але не виконанням.
ГОЛОВНИЙ ВИСНОВОК ДВОХ СЕСІЙ
Зелена перевірка доводить рівно те, що вона перевіряє. Тест ізоляції RLS
був правильний і зелений — і пропустив зламаний вхід, бо перевіряв «чи
не видно чужого», коли зламалось «чи видно своє». Інтеграційні тести
grpcapi були зелені, бо не виконувались. Схема, довідник і протокол
описували те, чого в коді не існувало, і виглядало це як готове.
Тому в кожному завданні цих сесій стояла вимога назвати НЕПОКРИТЕ, а
чотири задачі закінчились не можливістю, а відмовою: правило з
нереалізованим джерелом не зберігається, профіль без команд заливки
каже про це замість мовчазної кнопки, міграція RLS валить сама себе на
таблиці без політики, тест словника аудиту падає на ключі без назви.
Подробиці — HISTORY.md, розділи за 26 і 27 серпня.
|
||
|---|---|---|
| .. | ||
| netpulse/v1 | ||
| README.md | ||
NetPulse — контракт агент↔сервер (Етап 2)
gRPC / protobuf, пакет netpulse.v1. Джерело істини — файли в netpulse/v1/;
gen/go згенерований і комітиться, щоб збірка агента не залежала від наявності buf.
Головне обмеження, з якого випливає вся форма контракту
Усі з'єднання ініціює агент. У мережі клієнта немає ані відкритих портів, ані
прокидання NAT — зонд стоїть за фаєрволом і має лише вихідний доступ. Тому
«команда з сервера» фізично є повідомленням у зустрічному напрямку вже відкритого
агентом bidi-стріму Control. Це не деталь реалізації, а причина, чому контракт
побудований навколо довгоживучих потоків, а не навколо RPC-викликів у бік агента.
Файли
| Файл | Зміст |
|---|---|
common.proto |
Спільні типи: Status, Transport, DiscoveryProto, Error, DeviceTarget, Credential, AgentBuild, AgentHealth |
agent.proto |
EnrollmentService, AgentService, ControlUp/ControlDown і все, що всередині них |
telemetry.proto |
SeriesDescriptor, MetricSample, IcmpResult, InterfaceCounters, TelemetryBatch, TelemetryAck |
discovery.proto |
NeighborRecord, InterfaceRecord, DiscoveredDevice, DiscoveryReport |
ncm.proto |
ConfigJob, ConfigUpload (header/chunk/trailer), ConfigApplyJob |
logs.proto |
SyslogEntry, SnmpTrap, LogBatch |
Сервіси
EnrollmentService
Enroll(EnrollRequest) → EnrollResponse одноразово, без клієнтського сертифіката
AgentService усе далі — тільки mTLS
Control(stream ControlUp) → stream ControlDown керування, довгоживучий
StreamTelemetry(stream TelemetryBatch) → stream TelemetryAck
StreamLogs(stream LogBatch) → stream LogAck
ReportDiscovery(DiscoveryReport) → DiscoveryAck
UploadConfig(stream ConfigUpload) → ConfigReceipt
Чотири окремі стріми, а не один — навмисно. Пачка на 10 000 семплів не має блокувати heartbeat і затримувати команду з сервера, а сплеск syslog під час аварії не має топити телеметрію, за якою цю аварію й видно.
Життєвий цикл сесії
sequenceDiagram
participant A as Агент (мережа клієнта)
participant S as Сервер
Note over A,S: одноразово
A->>S: Enroll(enrollment_token, CSR)
S-->>A: сертифікат + CA + control_endpoint
Note over A,S: кожна сесія, тільки вихідне з'єднання
A->>S: Control: Hello(build, task_plan_hash, last_acked_batch_id)
S-->>A: Welcome(session_id, server_time, ліміти батчингу)
S-->>A: TaskPlan(tasks, devices) або TaskDelta
S-->>A: ModuleControl(які модулі активувати)
S-->>A: CredentialBundle(розшифровані креди, з TTL)
loop опитування
A->>S: StreamTelemetry: TelemetryBatch(new_series?, samples, icmp, interfaces)
S-->>A: TelemetryAck(acked_through, max_in_flight)
A->>S: Control: Heartbeat(AgentHealth)
S-->>A: Control: Ping
A->>S: Control: Pong
end
Note over A,S: за подією або розкладом
S-->>A: Control: ConfigJob(commands, prompt_regex)
A->>S: UploadConfig: header → chunk* → trailer(sha256)
S-->>A: ConfigReceipt(commit_sha | unchanged)
Рішення, які варто розуміти перед реалізацією агента
Інтернування серій
Повторювати device_id (36 байт) + metric_key + labels у кожному семплі задорого:
при 50 000 пристроїв це десятки байт службових даних на одне число float64.
Тому агент реєструє серію один раз під локальним номером series_ref, далі шле
лише номер і значення. Сервер тримає мапу series_ref → ts.series.id на час сесії.
Заміряно в test/contract: 66 байт → 25 байт на семпл.
series_ref дійсний рівно в межах сесії. Після реконекту агент нумерує з 1 і
реєструє все заново. Якщо сервер втратив стан — відповідає reset_series_table = true,
а не мовчки викидає дані.
Хто рахує швидкості
Агент шле і сирі 64-бітні лічильники, і вже пораховані in_bps/out_bps/
util_*_pct. Швидкості рахує агент, бо лише він знає точний інтервал між двома
опитуваннями — мережева затримка робить серверний розрахунок неточним. Сирі
лічильники потрібні, щоб сервер міг перерахувати заднім числом. Обробка
wrap/reset — на агенті: при перезавантаженні пристрою виставляється
counter_reset = true, і швидкості цього разу недійсні.
Хто чистить конфіги
Агент віддає сирий текст. Scrub (прибрати uptime та timestamp'и, щоб не шуміти
в diff), redact (замаскувати паролі) і коміт у Git — на сервері. Це навмисно:
правила очищення живуть у ncm.profiles і мають змінюватись без оновлення
агентів у полі.
Хто вирішує топологію
Агент доповідає сире: «на порту X я бачу chassis-id Y, port-id Z». Він не
вирішує, хто з ким з'єднаний. Резолвер на сервері зводить це в topo.links і
виставляє confidence. Так автовиявлення лишається відтворюваним і не залежить
від версії агента в полі.
Непрозорі параметри задач
Task.params_json — байти, а не типізоване поле. Валідність гарантує
params_schema з core.check_types, розбирає їх сам модуль. Завдяки цьому новий
плагін не потребує зміни .proto — саме те, заради чого затівалась плагінна
архітектура. Модуль-виконавець визначається префіксом check_type до крапки:
snmp.if → модуль snmp.
Семантика доставки
At-least-once. Дедуплікацію забезпечує сама схема БД: первинні ключі
(ts, device_id), (ts, interface_id), (ts, series_id) роблять повторний запис
безпечним. Агент тримає неacknowledged батчі й після реконекту шле їх із
is_retransmit = true, продовжуючи від last_acked_batch_id з Hello.
Зворотний тиск
Розмір батчу, інтервал і max_in_flight диктує сервер у Welcome, а коригує в
кожному TelemetryAck. Балакучого агента можна пригальмувати на льоту, не
оновлюючи бінарник у полі. AgentHealth.dropped_samples показує, коли ліміти
затиснуті надто сильно.
Безпека
- mTLS на всьому, крім
Enroll. Агент приходить із одноразовим enrollment-токеном і CSR, іде з власним сертифікатом. Приватний ключ ніколи не залишає агента. - Креденшели передаються розшифрованими — сервер дістає їх із
core.secretsі розшифровує. Агент тримає їх лише в пам'яті, не пише на диск, не логує, і зобов'язаний занулити післяCredential.expires_at. - Самооновлення підписане.
UpdateInfoнесе sha256 бінарника і підпис Ed25519 над ним. Без валідного підпису оновлення не застосовується: інакше компрометація CDN перетворюється на RCE в мережі кожного клієнта. - Відкат конфігурації (
ConfigApplyJob) агент виконує беззастережно — уся логіка погодження (ncm.rollbacks: draft → awaiting_approval → approved) лишається на сервері.confirm_timeoutвмикає confirmed commit там, де пристрій це підтримує: найкращий захист від втрати керування після помилкового правила фаєрвола.
Версіонування
Пакет netpulse.v1. Правила: не змінювати номери полів, не перевикористовувати
видалені (позначати reserved), нові поля — тільки додавати. Ламкі зміни —
через netpulse.v2 поруч, бо агенти в полі оновлюються не одночасно з сервером.
buf breaking у CI проти головної гілки ловить порушення автоматично.
Кодогенерація
buf generate
Без buf (те, чим це перевірялось):
protoc -I proto --go_out=gen/go --go_opt=paths=source_relative --go-grpc_out=gen/go --go-grpc_opt=paths=source_relative proto/netpulse/v1/*.proto
Стан перевірки
Перевірено на стенді Debian 13: protoc 3.21.12, Go 1.24.4, buf 1.72.0.
| Крок | Результат |
|---|---|
protoc — валідність усіх 6 файлів і залежностей |
OK |
buf lint (STANDARD) |
без зауважень |
Генерація Go + gRPC, go build, go vet |
OK |
go test ./test/contract/... — 5 наскрізних тестів на bufconn |
усі PASS |
Тести в test/contract/contract_test.go перевіряють не компіляцію, а поведінку:
| Тест | Що доводить |
|---|---|
TestControlHandshakeAndTaskPush |
Hello → Welcome → TaskPlan доїжджає в зустрічному напрямку стріму; params_json розбирається; модуль виводиться з check_type; Ping/Pong міряє clock skew |
TestTelemetrySeriesInterning |
Серія реєструється раз, далі розв'язується з номера; ICMP і util_out_pct доїжджають без спотворень; економія 66 → 25 байт |
TestTelemetryUnknownSeriesRefTriggersReset |
Невідомий series_ref дає reset_series_table + retryable-помилку, а не тихе відкидання даних |
TestConfigUploadChunked |
header → chunk × N → trailer збирається байт-у-байт зі звіркою sha256 |
TestConfigUploadRejectsBadChecksum |
Пошкоджений конфіг відхиляється з checksum_mismatch, а не комітиться в Git |