Один коміт, а не десяток тематичних, свідомо: теми переплетені в
спільних файлах (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 серпня.
|
||
|---|---|---|
| .. | ||
| cmd/netpulse-agent | ||
| internal | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
NetPulse Agent
Легкий зонд збору телеметрії. Єдиний статичний бінарник, усі з'єднання вихідні — у мережі клієнта не треба відкривати жодного порту.
go build -trimpath -ldflags "-s -w" -o netpulse-agent ./cmd/netpulse-agent
./netpulse-agent -server monitor.example.com:443 -agent-id <uuid> -cert agent.pem -key agent.key
Будова
cmd/netpulse-agent/ точка входу, збирання компонентів
internal/
config/ прапорці + NETPULSE_*, mTLS
module/ контракт модуля, реєстр, маршрутизація check_type → модуль
telemetry/
interner.go series_ref: серія реєструється раз за сесію
buffer.go обмежений буфер із бюджетом пам'яті
scheduler/ min-heap за часом запуску, семафор паралельності
session/ gRPC-клієнт, рукостискання, реконект, ack
snmpx/ спільний SNMP-транспорт + розбір індексів OID
modules/
icmp/ ping, RTT, jitter, втрати
snmp/ лічильники інтерфейсів (v2c/v3), довільні OID
topology/ LLDP, CDP, ARP, FDB + інвентар портів
Рішення, які варто розуміти
Модулі вкомпільовані, а не завантажуються динамічно. Плагінність на агенті
навмисно простіша, ніж на сервері: жодних .so. Причина — той самий бінарник має
працювати на Alpine, Windows і роутері з musl, а бюджет RSS не дозволяє тягнути
рантайм плагінів. «Активація» означає, що сервер дозволив модуль цьому зонду
(ControlDown.ModuleControl).
Креденшели беруться на момент виконання, а не з плану. У них TTL. Якби вони копіювались у задачу, після ротації паролів агент довбав би пристрої простроченими даними, доки не приїде новий план — і заблокував би обліковий запис на половині комутаторів. Прострочені не віддаються взагалі: явна помилка «немає креденшелів» краща за блокування.
Розклад вирівняний по сітці інтервалу, а не «зараз + інтервал». Після рестарту
агента задача повертається у свій слот. schedule_offset рахує сервер
детерміновано від check_id, тому 5000 чеків з інтервалом 60 с розведені й
переживають перезапуск без збою фази.
Буфер викидає найстаріші дані, а не найновіші. Коли зв'язок відновиться,
оператору потрібен передусім поточний стан мережі. Зміни статусу пристрою
викидаються останніми: без них мапа показуватиме пристрій живим, поки він лежить.
Кількість викинутого їде в AgentHealth.dropped_samples, щоб діра була видимою.
Швидкості інтерфейсів рахує агент. Лише він знає фактичний інтервал між двома
опитуваннями; серверний розрахунок за номінальним інтервалом помиляється рівно на
мережеву затримку й джитер планувальника. При перевороті лічильника виставляється
counter_reset, і швидкості не заповнюються — краще діра в графіку, ніж стрибок
на терабіт і хибний алерт.
Один писар у контрольний стрім. gRPC не допускає паралельних Send, а слати
треба і heartbeat, і статуси задач. Тому окрема горутина-писар і канал. Читання
підтверджень телеметрії — на іншій горутині: Send + Recv на різних горутинах
це єдине, що gRPC дозволяє робити паралельно на одному стрімі.
GOMEMLIMIT виставляється в коді (48 МБ). Зонд часто живе на роутері або в
контейнері з 64 МБ. Хай збирач працює агресивніше, ніж OOM killer вб'є процес і
осліпить моніторинг саме тоді, коли він потрібен.
Параметри
| Прапорець | Змінна | Призначення |
|---|---|---|
-server |
NETPULSE_SERVER |
адреса сервера host:port |
-agent-id |
NETPULSE_AGENT_ID |
ідентифікатор зонда з Enroll |
-cert / -key / -ca |
NETPULSE_CERT / _KEY / _CA |
mTLS |
-insecure |
NETPULSE_INSECURE=1 |
без TLS, лише локальний стенд |
-modules |
NETPULSE_MODULES |
активні до першої відповіді сервера |
-max-concurrency |
NETPULSE_MAX_CONCURRENCY |
скільки чеків паралельно |
-buffer-items / -buffer-bytes |
NETPULSE_BUFFER_* |
ліміти буфера |
Перевірка сертифіката сервера не вимикається прапорцем: зонд ходить через інтернет, і «тимчасово без перевірки» — це той самий канал, яким їдуть паролі від усіх комутаторів клієнта.
Параметри чеків
icmp.ping
{"count": 3, "packet_size": 56, "interval_ms": 100}
snmp.if — перелік інтерфейсів приходить від сервера, бо він уже є в
inv.interfaces. Агент не ходить по ifTable, щоб з'ясувати, що існує:
виявлення нових інтерфейсів — робота модуля topology, і вона окрема саме тому,
що впирається в ліміт тарифу.
{"use_hc_counters": true,
"interfaces": [{"if_index": 1, "interface_id": "<uuid>", "speed_bps": 1000000000}]}
snmp.get
{"oids": [{"oid": "1.3.6.1.4.1.9.9.109.1.1.1.1.8.1",
"metric_key": "cpu.util", "unit": "pct", "scale": 1}]}
topology.discover — сусіди й інвентар портів беруться з одного SNMP-обходу,
тому окремий чек для інтерфейсів був би зайвим трафіком.
{"protos": ["lldp", "cdp", "arp", "fdb"], "collect_interfaces": true}
Префікс типу чека зобов'язаний дорівнювати ключу модуля: саме за ним агент
обирає виконавця. Реєстр відхиляє модуль, який оголошує чужий тип, а в БД те саме
закріплено обмеженням check_types_prefix_matches_plugin.
Стан перевірки
Стенд Debian 13, Go 1.25.13. go vet чисто, тести з -race:
| Пакет | Результат |
|---|---|
internal/scheduler |
ok |
internal/telemetry |
ok |
internal/session |
ok |
internal/snmpx |
ok |
internal/modules/icmp |
ok |
Заміряно на релізному бінарнику (CGO_ENABLED=0, -trimpath -s -w):
- розмір — 12 МБ
- базовий RSS у циклі реконекту — 11.6 МБ (бюджет 30 МБ)
Що саме доводять тести:
| Тест | Що перевіряє |
|---|---|
TestNextRunSurvivesRestart |
слот задачі не з'їжджає після перезапуску агента |
TestOffsetsSpreadLoad |
60 різних offset дають 60 різних моментів запуску |
TestSchedulerSkipsWhenPreviousStillRunning |
повільний чек не множить сам себе; пропуск явний |
TestSchedulerRejectsUnknownModule |
задача на неактивний модуль відхиляється зі слідом |
TestInternerLabelOrderDoesNotMatter |
недетермінований порядок map не плодить серій |
TestBufferDropsOldestOnOverflow |
при переповненні лишається найновіше, викинуте пораховане |
TestBufferPrioritisesStatusChanges |
зміна стану пролазить у батч поперед метрик |
TestBufferRequeuePreservesData |
розрив між Send і Ack не втрачає ні семпли, ні дескриптори |
TestSessionFullCycle |
Hello → план у зустрічному напрямку → виконання з креденшелами → телеметрія на сервері |
TestSessionReconnectsAfterDrop |
агент повертається сам після розриву |
TestSplitIndex |
розбір індексу OID, включно з пасткою «схожий префікс» (.1.2.20 не є нащадком .1.2) |
TestFormatMAC |
сирі байти, 00:11:..., 0011.2233.4455 і дефіси зводяться до одного вигляду |
TestPingLoopback |
реальний ICMP-сокет, не мок |
Перевірка проти справжнього SNMP-агента
На стенді піднято snmpd + lldpd (LLDP-MIB через AgentX). Живий прогін
topology.discover і snmp.get кожні 10 с:
| Що перевірено | Результат |
|---|---|
Інвентар портів зі справжнього ifTable |
lo (softwareLoopback) і eth0 (ethernetCsmacd, MAC bc:24:11:07:68:67) |
ifHighSpeed замість 32-бітного ifSpeed |
10 Гбіт/с — 32-бітне поле впиралося б у 4.29 Гбіт/с |
| Сусіди зі справжньої ARP-таблиці | 2 записи, обидва з локальним портом eth0 |
| Зіставлення за MAC | шлюз 192.168.1.1 розпізнано, впевненість 90 |
| Зведення лінка | snmp-host:eth0 → gateway, джерело arp, capacity_bps 10 Гбіт/с |
| Три проходи поспіль | лінк один, дублікатів немає |
Метрики snmp.get |
sys.uptime збирається, помилок чеків немає |
Обмеження перевірки: TestPingLoopback пропускається під звичайним
користувачем в unprivileged LXC — ядро не дає ані unprivileged-, ані raw-сокета,
і sysctl net.ipv4.ping_group_range там недоступний. Під root на тому ж стенді
тест проходить. У проді агенту треба CAP_NET_RAW або дозволений
ping_group_range.
LLDP і CDP не перевірені на живих сусідах: lldpd на стенді зареєстрував
LLDP-MIB, але сусідів у нього немає — поруч немає другого пристрою, який шле LLDP.
Розбір lldpRemTable і cdpCacheTable покритий лише тестами на індекси OID;
ARP-гілка того самого коду перевірена на живих даних. CDP-гілка чекає на Cisco.
Так само не перевірено snmp.if: сервер поки не генерує для нього перелік
інтерфейсів, тому чек нікуди не призначається. Логіка перевороту лічильників і
util_pct лишається без живої перевірки.