Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ jobs:
# whitelist относительно protect.conf (#38); PSI три состояния (#37); серты в
# /opt/*/certs и снятые с renew (#39); глубина журнала (#35); RPS-юнит vs факт (#30).
run: bash tests/diagnose-unit.sh
- name: na-report читает журнал всех загрузок (-b all), не только текущей (#41)
# `-k` подразумевает `-b`: после ребута отчёт «за 24ч» молча становился «с момента
# загрузки» (×31 занижение на ноде флота). Оба вызова journalctl -k — с -b all.
run: |
N="$(grep -cE 'journalctl -k -b all --since' scripts/na-report.sh)"
[ "$N" -ge 2 ] || { echo "в na-report.sh journalctl -k без -b all (найдено $N из 2)"; exit 1; }
if grep -nE 'journalctl -k( |$)' scripts/na-report.sh | grep -vq -- '-b all'; then
echo "остался journalctl -k без -b all:"; grep -nE 'journalctl -k( |$)' scripts/na-report.sh | grep -v -- '-b all'; exit 1
fi
bash scripts/na-report.sh --json | grep -q '"journal_span_h":' || { echo "na-report --json без journal_span_h"; exit 1; }
echo "ok: -b all в обоих вызовах, journal_span_h в json"

# ── Smoke-тесты на матрице дистрибутивов ────────────────────────────────────
# Ловит ровно тот класс регрессий, из-за которых появился этот PR: установка
Expand Down
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,49 @@
# Changelog

## v4.1.1 — 2026-09-02

Два хвоста того же аудита, заведённые уже после сборки v4.1
([#40](https://github.com/jestivald/node-accelerator/issues/40),
[#41](https://github.com/jestivald/node-accelerator/issues/41)). Оба — про молчаливую
подмену смысла: «ротация настроена» при отсутствующей стансе и «за 24 часа» при отчёте
с момента загрузки.

### 🟠 Уступив маски чужой стансе, тулкит рапортовал зелёным (#40)

Само поведение «отдать путь чужой стансе» правильное — спор решает `logrotate`, и в v4.0.1 он
решается честно. Дефект был в том, что уступка не доводилась до оператора там, где он смотрит:
`optimize` печатал «проверь, что там задан maxsize» и заканчивался `ok`, `na-logrotate.timer`
включался, `na-diagnose` светил `✔ ротация логов: часовой таймер активен` — при отсутствующей
`na-node-logs`. На трёх нодах флота ротацию при этом держала ручная станса `weekly` без
`maxsize`, то есть никакого капа не было, а диагностика была полностью зелёной.

Теперь `optimize` **сам находит стансу-владельца** (по реальным путям, которые `logrotate` назвал
дубликатами) и **сам смотрит в неё**: есть `maxsize`/`size` — info «кап работает», нет —
`warn` с именем файла и периодом («`weekly` БЕЗ maxsize/size — размер ничем не ограничен») и
советом. Полная уступка всех масок — `warn`, а не info, и повторяется в итоге прогона. Факт
уступки пишется в состояние (`logrotate.ceded`: маска, владелец, кап; `logrotate.owned`: что
держит наша станса) — `na-diagnose` разделяет «таймер активен» и «станса существует»: ▲ при
уступке (с владельцами и пометкой у кого нет капа) и ▲ при активном таймере без стансы; в
`--json` — `logrotate_owned_masks`, `logrotate_ceded_masks`, `logrotate_ceded_nocap`.
`ENABLE_LOGROTATE=0` снимает состояние, чтобы диагностика не пересказывала прошлые прогоны.

### 🟠 `na-report` после ребута занижал события в разы (#41)

`journalctl -k` подразумевает `-b` — только текущая загрузка, а окно задаётся через `--since`.
После перезагрузки отчёт «за 24 часа» молча становился отчётом «с момента загрузки»: на ноде
флота 2 298 событий против 72 485 реальных (×31), причём ровно в те сутки, когда в отчёт
смотрят чаще всего. Оба вызова (`collect_events` и `--port`) теперь с `-b all` — граница по
времени остаётся, границы загрузок перестают резать окно. В шапке — фактическая глубина журнала
(самая старая запись): если она меньше окна, отчёт предупреждает, что «событий мало» может значить
«журнал вытеснен»; в `--json` — `journal_span_h`.

### 🧪 Тесты

`tests/logrotate-unit.sh`: владелец уступленной маски назван и проверен на `maxsize` (кейсы
«без капа» / «с капом»), состояние `ceded`/`owned` пишется и снимается, полная уступка — warn.
`tests/diagnose-unit.sh`: ▲ и поля `--json` при уступке. CI: статическая проверка, что оба
`journalctl -k` в `na-report` — с `-b all`, и `journal_span_h` в его `--json`.

## v4.1 — 2026-09-02

Релиз по итогам аудита боевого флота (11 нод + панель на v4.0.1, Debian 12/13, XanMod 6.18):
Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
сертификат). Штатной ротации мало: `maxsize` проверяется только в момент запуска logrotate, а тот
суточный, поэтому файл проскакивает лимит между прогонами — наблюдалось превышение заявленного
капа в девять раз. Ставится сам пакет `logrotate` (на минимальных образах его нет, и станса лежит
мёртвым грузом), станса на пути ноды и `na-logrotate.timer` с часовым прогоном.
мёртвым грузом), станса на пути ноды и `na-logrotate.timer` с часовым прогоном. Маску, которую уже
держит чужая станса, тулкит отдаёт ей (спор решает сам `logrotate`), **сам проверяет в ней
`maxsize`/`size`** и пишет факт уступки в состояние — `na-diagnose` показывает, кто ротирует и есть ли
кап, вместо зелёного «таймер активен».

### 🛡 Защита (`scripts/protect.sh`)
`nftables`-движок в **своей** таблице `inet na_filter` (не `flush ruleset` — сосуществует с CrowdSec и Docker):
Expand Down Expand Up @@ -86,7 +89,7 @@
Read-only отчёт: ядро/BBR, sysctl, лимиты, conntrack, NIC/RPS, swap/THP/governor, firewall, CrowdSec, порты, RTT — с итогом ✔/▲/✘ и рекомендациями. Плюс сенсоры **стека ноды**: контейнер `remnanode` (статус/рестарты/`SPAWN_ERROR` за час — ловит коллизию node-address в панели), **рассинхрон порта node-агента с файрволом** (агент слушает `:3000`, а strict-правила держат `:2222` → «нода недоступна» для панели; в отчёте — с командой пожарного фикса, в `--json` — `node_port_detected`/`node_port_fw`), **сроки TLS-сертификатов** (LE/acme.sh/`/opt/*/certs`; серты, снятые с renew в acme.sh, не считаются; свои пути — `NA_CERT_PATHS="глоб1 глоб2"`), **whitelist в трёх местах** (живой сет vs `na_filter.nft` vs `protect.conf`: адрес, который переживёт ребут, но не ре-ран `protect`, называется по имени), **датчик `CONN_LIMIT` по тому же срезу, что и правило** (только входящие на сервисные порты, без loopback/whitelist — раньше на ноде с внутренним nginx он горел всегда), **PSI** (различает «не собран», «выключен в сборке ядра» и «работает»), **ретеншен journald в часах**, **замороженные дефолты в сохранённом конфиге**, **свежесть fleet-sync/blocklist**, IPv6 default-route, UDP `RcvbufErrors` (QUIC/Hysteria2). После установки доступна как команда **`na-diagnose`** (`--json` для мониторинга/панели — теперь с `na_version`, `hostname`, `uptime_s`, `load1`, `mem_used_pct`, WAN rx/tx-байтами; `--retrans [--window N]` — разбор причин TCP-retransmits).

### 🔥 Форензика атак (`scripts/na-report.sh`)
Read-only: кто/откуда/чем/когда — из журнала ядра, nft-сетов и CrowdSec. **`na-report`** (человекочитаемо) или **`na-report --json`**: `drops_by_reason`, `timeline`, `top_ips` с вердиктом, `top_asn` (ASN/гео — best-effort через Team Cymru whois). Флаги: `--hours N`, `--top N`, `--ip <addr>`.
Read-only: кто/откуда/чем/когда — из журнала ядра, nft-сетов и CrowdSec. **`na-report`** (человекочитаемо) или **`na-report --json`**: `drops_by_reason`, `timeline`, `top_ips` с вердиктом, `top_asn` (ASN/гео — best-effort через Team Cymru whois). Флаги: `--hours N`, `--top N`, `--ip <addr>`. Журнал читается по всем загрузкам (`-b all`), а не с момента ребута; в шапке — фактическая глубина журнала (`journal_span_h`), чтобы «событий мало» не путать с «журнал вытеснен».

---

Expand Down Expand Up @@ -118,11 +121,11 @@ curl -fsSL https://raw.githubusercontent.com/jestivald/node-accelerator/main/ins

# прод-режим: пиньте тег через NA_REF — компрометация ветки main тогда не утечёт
# сразу на весь флот (скрипты тянутся из того же тега):
export NA_REF=v4.1
export NA_REF=v4.1.1
curl -fsSL "https://raw.githubusercontent.com/jestivald/node-accelerator/$NA_REF/install.sh" | sudo -E bash -s all

# максимум: + проверка minisign-подписей модулей (подписи лежат в дереве с v3.6):
export NA_REF=v4.1 NA_REQUIRE_SIG=1 \
export NA_REF=v4.1.1 NA_REQUIRE_SIG=1 \
NA_MINISIGN_PUBKEY="RWQrJghT9nkdBC3ntiEXF29zrS8o429WhObHKq6I7CKoftVDhQBrBscu"
curl -fsSL "https://raw.githubusercontent.com/jestivald/node-accelerator/$NA_REF/install.sh" | sudo -E bash -s all
```
Expand Down
2 changes: 1 addition & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# curl-bash:
# curl -fsSL https://raw.githubusercontent.com/jestivald/node-accelerator/main/install.sh | sudo bash -s all
# # прод: пиньте тег (компрометация ветки main тогда не утечёт сразу на весь флот):
# export NA_REF=v4.1; curl -fsSL "https://raw.githubusercontent.com/jestivald/node-accelerator/$NA_REF/install.sh" | sudo -E bash -s all
# export NA_REF=v4.1.1; curl -fsSL "https://raw.githubusercontent.com/jestivald/node-accelerator/$NA_REF/install.sh" | sudo -E bash -s all
# # + подписи модулей: NA_REQUIRE_SIG=1 NA_MINISIGN_PUBKEY=<ключ из README>
#
# ENV: NA_REF=<ветка|тег> NA_REPO_URL=<база raw-URL, для форка/зеркала>
Expand Down
29 changes: 27 additions & 2 deletions scripts/diagnose.sh
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,12 @@ emit_json() {
dklogmax="$(find /var/lib/docker/containers -xdev -type f -name '*-json.log' -printf '%s\n' 2>/dev/null | sort -rn | head -1)"
[[ "$dklogmax" =~ ^[0-9]+$ ]] || dklogmax=0
lrt=0; systemctl is-active --quiet na-logrotate.timer 2>/dev/null && lrt=1
# Таймер ≠ станса: маски, отданные чужим стансам, тулкит не ротирует, а таймер при
# этом активен (issue #40). owned/ceded пишет optimize; пусто = станс/уступок нет.
lro="$(paste -sd' ' "$STATE_DIR/logrotate.owned" 2>/dev/null || true)"; lro="${lro:-}"
lrc="$(cut -f1 "$STATE_DIR/logrotate.ceded" 2>/dev/null | paste -sd' ' - || true)"; lrc="${lrc:-}"
lrcn="$(awk -F'\t' '$3=="none"' "$STATE_DIR/logrotate.ceded" 2>/dev/null | grep -c .)"; [[ "$lrcn" =~ ^[0-9]+$ ]] || lrcn=0
lro="$(json_escape "$lro")"; lrc="$(json_escape "$lrc")"

# Каждое строковое значение — через json_escape. Раньше каждое поле полагалось на то,
# что источник «и так чистый», и одного сырого перевода строки из docker хватило,
Expand Down Expand Up @@ -375,8 +381,10 @@ emit_json() {
# мониторинг. −1 везде = «не измерено», а не «ноль».
printf '"max_conn_per_ip":%s,"journal_span_h":%s,"portscan_log_lines_boot":%s,"psi":"%s",' \
"$mcpi" "$jsp" "$psl" "$psist"
printf '"whitelist_drift_conf":%s,"conf_stale_defaults":%s,"cert_min_file":"%s"}\n' \
printf '"whitelist_drift_conf":%s,"conf_stale_defaults":%s,"cert_min_file":"%s",' \
"$wdc" "$csd" "$certf"
printf '"logrotate_owned_masks":"%s","logrotate_ceded_masks":"%s","logrotate_ceded_nocap":%s}\n' \
"$lro" "$lrc" "$lrcn"
}
if [[ "${1:-}" == "--json" ]]; then emit_json; exit 0; fi

Expand Down Expand Up @@ -1062,7 +1070,24 @@ DKBIG="$(find /var/lib/docker/containers -xdev -type f -name '*-json.log' -size
if ! command -v logrotate >/dev/null 2>&1; then
wrn "logrotate не установлен — стансы в /etc/logrotate.d не выполняются вообще"
elif systemctl is-active --quiet na-logrotate.timer 2>/dev/null; then
pass "ротация логов: часовой таймер активен"
# «Таймер активен» и «станса существует» — разные факты (issue #40): уступив все маски
# чужим стансам, optimize свою не создаёт, а таймер включает — и на трёх нодах флота
# диагностика светила ✔ при ротации, которую держала ручная станса weekly без maxsize.
LR_EN="$(sed -nE 's/.*\{ENABLE_LOGROTATE:=([^}]*)\}.*/\1/p' "$CONF_DIR/optimize.conf" 2>/dev/null | tail -1)"
if [[ -s "$STATE_DIR/logrotate.ceded" ]]; then
LR_CEDED="$(awk -F'\t' '{printf "%s%s → %s%s", (NR>1?", ":""), $1, $2, ($3=="none"?" (БЕЗ maxsize/size!)":($3=="unknown"?" (кап неизвестен)":""))}' "$STATE_DIR/logrotate.ceded" 2>/dev/null)"
LR_NOCAP="$(awk -F'\t' '$3=="none"' "$STATE_DIR/logrotate.ceded" 2>/dev/null | grep -c .)"
if [[ "${LR_NOCAP:-0}" -gt 0 ]]; then
wrn "ротация: таймер активен, но маски отданы чужим стансам, и у $LR_NOCAP из них НЕТ капа по размеру: $LR_CEDED — добавь maxsize в чужую стансу или сузь NA_LOG_PATHS (ре-ран optimize)"
else
wrn "ротация: таймер активен, но часть масок ротируют чужие стансы (у них кап есть): $LR_CEDED"
fi
[[ -s "$STATE_DIR/logrotate.owned" ]] && info "наша станса держит: $(paste -sd' ' "$STATE_DIR/logrotate.owned" 2>/dev/null)"
elif [[ "${LR_EN:-1}" == "1" && ! -s /etc/logrotate.d/na-node-logs ]]; then
wrn "ротация: таймер активен, а стансы /etc/logrotate.d/na-node-logs нет — тулкит ничего не ротирует (ре-ран optimize)"
else
pass "ротация логов: часовой таймер активен, станса на месте$( [[ -s "$STATE_DIR/logrotate.owned" ]] && echo " ($(paste -sd' ' "$STATE_DIR/logrotate.owned" 2>/dev/null))")"
fi
else
info "часовой таймер ротации не активен — работает только суточный logrotate.timer (maxsize проверяется раз в сутки)"
fi
Expand Down
8 changes: 4 additions & 4 deletions scripts/diagnose.sh.minisig
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
untrusted comment: node-accelerator v4.1
RUQrJghT9nkdBAhVc5hN2qvpWxRqKBW7EivyAkjnlvEChQZ9XIWepJk1Eq7I9rhUhLo9FN7CNY0aWk7tuyrlqRtg89jVpMO9cAY=
trusted comment: timestamp:1788348398 file:diagnose.sh hashed
hZZu8QDEvt0VgKsIkQ8PfxOIrdw9HxrN0WUwYeZKjOQ/Nf/mrkjENA/oXkBGS1A5CdS/2gjheaSU7fGhBlfDDw==
untrusted comment: node-accelerator v4.1.1
RUQrJghT9nkdBENoxhnU9rwsJzsLv3J7D9SkM1oe+0YmmPdKLZWtjkaSKCLIQJyFN6bIsc+I6+50gMTg6NHare9Bwo6SMiIB9AY=
trusted comment: timestamp:1788349350 file:diagnose.sh hashed
Kn3mUHqRXJ3TrbY2sTWuRegSUPxmAGUiC58AhBamEvfgFWFxmfcHS6pXKwfG5xCPdSznjgxXGwC5OFtLbCXlCw==
2 changes: 1 addition & 1 deletion scripts/lib/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# Версия тулкита — ЕДИНСТВЕННЫЙ источник. Пишется в installed-маркеры и отдаётся
# в na-diagnose/na-report --json, чтобы флот-мониторинг видел version-drift по нодам.
# shellcheck disable=SC2034
NA_VERSION="4.1"
NA_VERSION="4.1.1"

# shellcheck disable=SC2034
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
Expand Down
8 changes: 4 additions & 4 deletions scripts/lib/common.sh.minisig
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
untrusted comment: node-accelerator v4.1
RUQrJghT9nkdBDoV6iLC2aa+udx6asRQD+/4Jh2dHmhHFi2DwCsnKjDnugZ4sje0QhSLZCqfNewWNCx54rN3wX+dzt+3a62rgwU=
trusted comment: timestamp:1788348398 file:common.sh hashed
4C7yF1qh32Hfa8+0wiIibiEOJyYYSPJrWj1o+OxojOGDPXvcBwTNUKn5Nt5eBMuS1sPCV5aHm4eRzx7FT2sICQ==
untrusted comment: node-accelerator v4.1.1
RUQrJghT9nkdBPMjRYxTeyDRPHNs+d5y7lErzQAsoal8gZpryUTA0Ad09waeYLWIWZWthxvOsUaocoZ/qPl1PPhUPoRKACUfPwo=
trusted comment: timestamp:1788349350 file:common.sh hashed
l4F98FXGpy8Umba+MEjtxykaeq298rquHNSNvaJLxaZGqT33SYSK8WlY4IGmT997kPLSBZHWA1KFM74UU8gTBw==
Loading
Loading