Зонд переживає перезапуск і знає, куди підключатись
Some checks are pending
CI / web (push) Waiting to run
CI / server (push) Waiting to run
CI / agent (push) Waiting to run

Дві помилки, які видно лише на живому розгортанні.

Сервер віддавав зондам свою адресу прослуховування замість тієї, за
якою до нього дістаються: у посвідченні опинялось ":9443". Реєстрація
проходила, а підключитись після перезапуску зонд не міг ніколи.
Порожнє значення тепер означає «лишись на адресі, якою прийшов».

Запрошення одноразове, але живе в змінних оточення й лишається
назавжди — кожен рестарт контейнера падав із «запрошення недійсне»,
хоча посвідчення поруч робоче. Наявне посвідчення тепер важить більше.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
byrsapty 2026-08-25 18:20:29 +03:00
parent 6843bd3280
commit 55e10cbd08
3 changed files with 102 additions and 5 deletions

View file

@ -2931,3 +2931,70 @@ NAT цілком може слухати SSH на 2222.
другий вибір список не закрився, «Доступи 2»
клік по підкладці форма закривається
```
## Перше розгортання на живому сервері
Debian 13, 4 ядра, 4 ГБ. Образи зібрались із першого разу — приємна
несподіванка для коду, який жодного разу не збирався. Далі знайшлось
пʼять речей, і чотири з них не знайшлись би ніде, крім живого запуску.
### memlock
`ulimits: memlock: -1` для Dragonfly дозволений не всюди: у контейнерній
віртуалізації ядро відмовляє, і контейнер не стартує взагалі з
«error setting rlimit type 8». Швидкість того не варта — обмеження знято.
### TLS на голому IP
Домену ще немає, і Caddy нескінченно просив Let's Encrypt видати
сертифікат на IP-адресу, чого той не робить. Сайт лишався без TLS.
Тепер порожня `ACME_EMAIL` означає самопідписаний сертифікат — одна
директива `tls {$NETPULSE_TLS}` покриває обидва випадки.
Далі зʼясувалось друге: рукостискання обривалось «internal error» навіть
із готовим сертифікатом. SNI не містить IP-адрес — стандарт дозволяє там
лише доменні імена, тож браузер не каже нічого, і Caddy не має за чим
обрати сертифікат. Лікується `default_sni`.
### Команда з README не працювала
```
docker compose run --rm api netpulse-user …
```
У образу `ENTRYPOINT` — тож імʼя команди йшло аргументом до
`netpulse-api`, і замість заведення власника піднімався ще один сервер.
Потрібен `--entrypoint netpulse-user`.
Заразом виявилось, що свіжа база не має жодного кабінету, а користувач
без кабінету нікуди не належить — те саме коло, заради розриву якого
існує ця команда, лише рівнем вище. Додано `-create-tenant`.
### Зонд не міг підключитись після реєстрації
У посвідченні опинилось `"endpoint": ":9443"`. Сервер віддавав зондам
свою адресу **прослуховування** замість тієї, за якою до нього
дістаються. Помітно це стає лише після перезапуску: реєстрація проходить,
а підключитись зонд більше не може ніколи.
Порожнє значення тепер означає «лишись на тій адресі, якою прийшов» — і
це правильна поведінка за замовчуванням: зонд дійшов, отже, вона робоча.
### Зонд не переживав перезапуск
Запрошення одноразове, а живе в змінних оточення — тобто лишається
назавжди. Кожен рестарт контейнера намагався зареєструватись повторно й
падав із «запрошення недійсне», хоча посвідчення поруч і цілком робоче.
Тепер наявне посвідчення важить більше за запрошення.
### Підсумок
```
27 міграцій, база UTF8
власник заведений, вхід працює
147 профілів збору, 5 шаблонів
зонд online, переживає перезапуск
хост «перевірка звʼязку» → up, 5 зразків ICMP, rtt 16.73 мс
icmp-basic сам створив 2 правила сповіщень
```

View file

@ -63,6 +63,26 @@ func run() error {
Arch: runtime.GOARCH,
}
// Наявне посвідчення важить більше за запрошення.
//
// Запрошення одноразове, а живе воно в змінних оточення чи в unit-
// файлі — тобто лишається на місці назавжди. Кожен перезапуск зонда
// намагався б зареєструватись повторно й падав із «запрошення
// недійсне», хоча посвідчення поруч і цілком робоче.
//
// Знайдено живим розгортанням: контейнер із NETPULSE_ENROLL у .env
// піднявся один раз, а після першого ж restart уже не вставав.
saved, err := config.LoadIdentity(cfg.IdentityPath)
if err != nil {
return err
}
if saved != nil && cfg.EnrollToken != "" {
fmt.Fprintf(os.Stderr,
"netpulse-agent: посвідчення вже є (%s), запрошення проігноровано\n",
cfg.IdentityPath)
cfg.EnrollToken = ""
}
// Реєстрація перед усім іншим.
//
// Посвідчення зберігається на диск одразу: одноразовий токен згорає
@ -77,10 +97,7 @@ func run() error {
fmt.Fprintf(os.Stderr, "netpulse-agent: зареєстровано як %q, посвідчення в %s\n",
id.AgentName, cfg.IdentityPath)
} else if cfg.AgentID == "" || cfg.Token == "" {
id, err := config.LoadIdentity(cfg.IdentityPath)
if err != nil {
return err
}
id := saved
if id == nil {
return fmt.Errorf(
"зонд не зареєстрований: посвідчення не знайдено (%s). "+

View file

@ -48,6 +48,8 @@ func run() error {
logLevel = flag.String("log-level", envOr("NETPULSE_LOG_LEVEL", "info"), "debug|info|warn|error")
gitRoot = flag.String("git-root", envOr("NETPULSE_GIT_ROOT", "/var/lib/netpulse/git"),
"корінь сховища версій конфігів; порожньо — без Git")
controlEndpoint = flag.String("control-endpoint", os.Getenv("NETPULSE_CONTROL_ENDPOINT"),
"адреса, за якою зонди мають підключатись; порожньо — та, якою вони прийшли")
)
flag.Parse()
@ -113,7 +115,18 @@ func run() error {
npv1.RegisterAgentServiceServer(srv, svc)
// Реєстрація йде БЕЗ токена зонда, тож окремим сервісом: інакше
// довелося б робити виняток усередині інтерсептора автентифікації.
npv1.RegisterEnrollmentServiceServer(srv, grpcapi.NewEnrollService(st, *listen))
// Адреса для зондів — не та, на якій ми слухаємо.
//
// Раніше сюди йшов -listen, і зонд зберігав у посвідченні ":9443"
// або "0.0.0.0:9443" — адресу, за якою до сервера не достукатись
// нізвідки. Помітно це стає лише після перезапуску зонда: реєстрація
// проходить, а підключитись він більше не може ніколи.
//
// Порожнє значення означає «лишись на тій адресі, якою прийшов» — і
// це правильна поведінка за замовчуванням: зонд дійшов, отже, вона
// робоча. Прапорець потрібен лише там, де реєстрацію й роботу
// свідомо розводять на різні адреси.
npv1.RegisterEnrollmentServiceServer(srv, grpcapi.NewEnrollService(st, *controlEndpoint))
lis, err := net.Listen("tcp", *listen)
if err != nil {