Files
nettopo-go/README.md
T
Луценко Андрей Анатольевич 526c67b5b1 docs(readme): обновить документацию по последним доработкам
Добавил в README новые API и UI-возможности: IF-MIB интерфейсы, гибкий ввод CIDR,
обновления по LLDP/SNMP, форматирование MAC и админ-очистку данных сканов.

Made-with: Cursor
2026-04-10 16:18:11 +10:00

315 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# nettopo-go
Минимальный старт проекта сетового инвентаризатора и топологии на Go без Docker.
## Что уже есть
- `GET /health` - проверка состояния API.
- `POST /api/scans` - создание задания скана.
- `GET /api/scans` - список сканов.
- `GET /api/scans/{id}` - просмотр созданного задания.
- `GET /api/scans/diff?from=&to=` - сравнение двух сканов (hosts/links).
- `GET /api/scans/{id}/hosts` - результаты проверки хостов по скану.
- `GET /api/scans/{id}/ports` - результаты проверки TCP-портов по скану.
- `GET /api/scans/{id}/snmp` - результаты SNMP v2c (`sysName`, `sysDescr`, `sysObjectID`).
- `GET /api/scans/{id}/lldp` - LLDP соседи (если устройство отдает LLDP-MIB по SNMP).
- `GET /api/scans/{id}/interfaces?ip=` - интерфейсы устройства из IF-MIB (`ifIndex/ifName/ifDescr/ifOperStatus`).
- `GET /api/scans/{id}/links` - базовые связи из LLDP с попыткой сопоставления по `remote_sys_name`.
- `GET /api/scans/{id}/topology` - граф `nodes/edges` для визуализации.
- `POST /api/admin/purge-scans` - очистка всех scan-данных (защищено `X-Nettopo-Reset-Key`).
- `GET /` — UI для просмотра топологии (страница встроена в бинарник через `embed`, каталог `internal/webui/`; **не зависит** от `WorkingDirectory` и папки `web/` рядом с процессом).
- Валидация целей скана: одиночный CIDR, перечисление через запятую и диапазон `CIDR-CIDR` (IPv4).
- Два backend-хранилища: in-memory и PostgreSQL.
- После создания scan запускается фоновый discovery с обновлением статуса и прогресса.
- Отдельный чек-лист ручной приемки: `MANUAL_TEST_CHECKLIST.md`.
- План продолжения на завтра: `NEXT_STEPS.md`.
## Требования
- Go 1.22+ (достаточно пакета `golang-go` из Ubuntu 24.04). Зависимость `pgx` зафиксирована на **v5.7.4**, потому что **v5.7.5+** объявляет `go 1.23` и не соберётся на Go 1.22 из apt. При желании можно поднять toolchain до **Go 1.23+** и снова обновить `pgx`.
Проверка версии:
```bash
go version
```
## Зависимости Go (`go.mod`, `go.sum`)
В репозитории закоммичен **`go.sum`**: в нём зафиксированы контрольные суммы модулей из `go.mod`. Это нужно, чтобы на новом клоне (например на сервере после `git pull`) команда `go build` не завершалась ошибкой вида `missing go.sum entry for module ...`. Исходники зависимостей в git не копируются — при сборке модули по-прежнему загружаются через прокси модулей (`proxy.golang.org` или ваш `GOPROXY`), если не используете `vendor`.
После изменения зависимостей выполните **`go mod tidy`** и закоммитьте обновлённые `go.mod` и `go.sum`. Проверка целостности: **`go mod verify`**.
Если на машине **нет доступа в интернет** во время сборки, можно закоммитить каталог **`vendor/`** (команда `go mod vendor`) и собирать так: **`go build -mod=vendor -o nettopo-server ./cmd/server`**.
## Запуск локально
1. Перейдите в проект:
```bash
cd nettopo-go
```
2. Скопируйте переменные окружения:
```bash
cp .env.example .env
```
3. Запустите API:
```bash
HTTP_ADDR=:8080 go run ./cmd/server
```
UI открывается по адресу:
```bash
http://localhost:8080/
```
Что умеет UI сейчас:
- загрузить topology по `scan_id`;
- автоматически подставить последний scan (`Последний scan`);
- сравнить два scan прямо на странице (`from`/`to`) через `/api/scans/diff`.
- выбрать scan из выпадающего списка последних 20;
- подсветить на графе новые (`[+]`, зеленый) и пропавшие (`[-]`, красный) хосты по diff.
- использовать легенду цветов и фильтр узлов: все / только новые / только пропавшие.
- экспортировать topology и diff в JSON-файлы кнопками из UI.
- создать scan по целям из UI и дождаться завершения кнопкой "Ждать завершения";
- вводить цели как: один CIDR (`192.168.1.0/24`), список через запятую (`172.16.0.0/24,192.168.160.0/24`) и диапазон (`192.168.160.0/24-192.168.190.0/24`);
- в таблице "Связи выбранного устройства" видеть не только LLDP-порты, но и все интерфейсы из IF-MIB с пометкой, где LLDP отсутствует;
- сортировать "Таблица 1. Связи" по столбцу "Локальный порт";
- выполнять "Начать всё заново" (очистка scan-таблиц) из UI в админ-блоке при наличии секрета.
По умолчанию используется `STORE_BACKEND=memory`.
SNMP-проверка по умолчанию включена (`SNMP_ENABLED=true`, community `public`).
## Логика одного scan (что делает программа)
Один запуск scan — это **одна фоновая задача** (`runner`), без отдельных фаз «только ping» / «только LLDP» в API.
1. По полю `cidrs` строится список IP (с учётом `exclude_ips`). Поддерживается:
- один CIDR;
- список CIDR через запятую;
- диапазон `CIDR-CIDR` (только IPv4, одинаковая маска, границы — сетевые адреса).
2. Параллельно (ограничение `max_parallel_hosts`) для каждого IP:
- **ping** (ICMP через системную утилиту `ping`);
- если хост «вверх» и включён port-scan — проверка **TCP-портов** из `options.ports`;
- если хост «вверх» и `SNMP_ENABLED=true`**SNMP v2c** (sysName, sysDescr, sysObjectID), затем **LLDP-MIB** и **IF-MIB** (`ifIndex/ifName/ifDescr/ifOperStatus`).
3. Статус задачи: `queued``running` (поле `progress` 0…100, `stats.hosts_total` / `stats.hosts_up`) → `done` или `failed`.
4. Результаты читаются через API: `/hosts`, `/ports`, `/snmp`, `/lldp`, `/interfaces`, агрегированный граф — `/topology`.
В веб-UI после **«Создать scan и ждать»** опрашивается статус, показывается расшифровка в строке состояния, по завершении обновляется таблица **найденных устройств** (ping + SNMP) и строится граф по кнопке **«Загрузить»** (или автоматически после ожидания).
### LLDP пустой на коммутаторе (например Ubiquiti EdgeSwitch)
Опрос LLDP идёт по стандартному **LLDP-MIB** (`1.0.8802.1.1.2.1.4.1.1.*`) после успешного SNMP **Get** системной группы. Раньше при **GET-BULK** часть прошивок отвечала слишком долго или с ошибкой — обход обрывался, в БД не попадало ни строки («No LLDP data» в UI). Сейчас для фазы LLDP увеличен таймаут (не меньше **3 c** на запрос), снижен размер «пачки» **MaxRepetitions**, добавлен **повтор при ошибке через SNMP Walk (GetNext)**. В логах сервера ищите строки `lldp snmp BulkWalkAll` и `fallback Walk`. Проверьте с хоста nettopo: community (**`SNMP_COMMUNITY` / `public`**), доступ UDP **161** с этого сервера и что на свиче включён **SNMP v2c** (не только LLDP в L2, но и ответ агента SNMP).
### Частые замечания и пояснения
- **CLI LLDP есть, а в UI "No LLDP data"**: это значит, что LLDP на устройстве работает локально, но в конкретном скане данные не были успешно прочитаны из `lldpRemTable` по SNMP. Быстрая проверка: `snmpwalk -v2c -c <community> <ip> 1.0.8802.1.1.2.1.4.1`.
- **Поле "Порт соседа" вида `0x...`**: если это OctetString длиной 6 байт, теперь значение форматируется как MAC `aa:bb:cc:dd:ee:ff`; для остальных бинарных значений остаётся `0x...`.
- **SNMP "не опрашивался" при больших сканах**: `/hosts` и `/snmp` возвращают по одной актуальной записи на IP (dedupe latest), чтобы не терять устройства из-за ограничений выборки.
- **Почему раньше в "Таблица 1. Связи" были не все порты**: теперь таблица строится как IF-MIB + LLDP. Показываются все интерфейсы, а LLDP соседы подмешиваются по `ifIndex`.
### Начать всё заново (очистка БД)
Для полного сброса scan-данных добавлен endpoint:
```bash
POST /api/admin/purge-scans
X-Nettopo-Reset-Key: <секрет>
```
- Очистка доступна только при заданном `NETTOPO_RESET_DB_SECRET`.
- В UI есть админ-блок "Начать всё заново" с ручным вводом секрета.
- Для PostgreSQL выполняется `TRUNCATE scan_jobs RESTART IDENTITY CASCADE` (очищаются все связанные таблицы: hosts/ports/snmp/lldp/interfaces).
## PostgreSQL без Docker
### Вариант A: macOS (Homebrew)
```bash
brew install postgresql@16
brew services start postgresql@16
createdb nettopo
createuser nettopo
```
```bash
psql -d postgres -c "alter user nettopo with password 'nettopo';"
psql -d postgres -c "grant all privileges on database nettopo to nettopo;"
```
### Вариант B: Ubuntu/Debian
```bash
sudo apt update
sudo apt install -y postgresql postgresql-contrib
sudo -u postgres psql -c "create user nettopo with password 'nettopo';"
sudo -u postgres psql -c "create database nettopo owner nettopo;"
```
### Вариант C: Windows
- Установить PostgreSQL через официальный installer.
- Создать БД `nettopo` и пользователя `nettopo` (через pgAdmin или `psql`).
### Запуск API с PostgreSQL
```bash
export STORE_BACKEND=postgres
export DB_DSN='postgres://nettopo:nettopo@localhost:5432/nettopo?sslmode=disable'
export RUN_MIGRATIONS=true
go run ./cmd/server
```
При первом запуске автоматически применяются SQL-миграции из `internal/store/sql/` (включая таблицу `scan_interfaces`).
## Быстрая проверка API
Проверка health:
```bash
curl -s http://localhost:8080/health
```
Создать scan:
```bash
curl -s -X POST http://localhost:8080/api/scans \
-H "Content-Type: application/json" \
-d '{
"name":"Office scan",
"cidrs":["192.168.1.0/24"],
"exclude_ips":["192.168.1.1"],
"options":{
"ping_timeout_ms":700,
"ping_retries":1,
"max_parallel_hosts":128,
"port_scan_enabled":true,
"ports":[22,80,443,161]
}
}'
```
Получить scan по id:
```bash
curl -s http://localhost:8080/api/scans/<scan_id>
```
Получить список scan jobs:
```bash
curl -s http://localhost:8080/api/scans?limit=20
```
Сравнить два scan:
```bash
curl -s "http://localhost:8080/api/scans/diff?from=<scan_id_1>&to=<scan_id_2>"
```
В ответе у scan есть:
- `status`: `queued`, `running`, `done`, `failed`
- `progress`: 0..100
- `stats.hosts_total`, `stats.hosts_up`
Получить результаты по хостам:
```bash
curl -s http://localhost:8080/api/scans/<scan_id>/hosts?limit=200000
```
Получить результаты по портам:
```bash
curl -s http://localhost:8080/api/scans/<scan_id>/ports?limit=500
```
Получить SNMP результаты:
```bash
curl -s http://localhost:8080/api/scans/<scan_id>/snmp?limit=200000
```
Получить LLDP результаты:
```bash
curl -s http://localhost:8080/api/scans/<scan_id>/lldp?limit=20000
```
Получить интерфейсы устройства (IF-MIB):
```bash
curl -s "http://localhost:8080/api/scans/<scan_id>/interfaces?ip=192.168.1.10&limit=4096"
```
Получить связи:
```bash
curl -s http://localhost:8080/api/scans/<scan_id>/links?limit=20000
```
Получить topology (nodes/edges):
```bash
curl -s http://localhost:8080/api/scans/<scan_id>/topology?limit=5000
```
Очистить все scan-данные (защищено секретом):
```bash
curl -s -X POST http://localhost:8080/api/admin/purge-scans \
-H "X-Nettopo-Reset-Key: <NETTOPO_RESET_DB_SECRET>"
```
## Запуск как сервис на Linux (systemd)
1. Собрать бинарник:
```bash
go build -o nettopo-server ./cmd/server
```
2. Скопировать unit-файл:
```bash
sudo cp deploy/nettopo-go.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now nettopo-go
```
3. Проверить статус:
```bash
systemctl status nettopo-go
```
### Логи (systemd)
Сервер пишет в **стандартный лог** пакета `log` (stderr): старт, выбранное хранилище, фатальные ошибки БД. Просмотр:
```bash
journalctl -u nettopo-go -f
journalctl -u nettopo-go -n 100 --no-pager
```
Отдельного файла логов приложение **не создаёт**; при необходимости перенаправьте вывод unit-а в файл через `StandardOutput=` / `StandardError=` в override.
### Если в браузере `404 page not found` на `/`
Раньше UI отдавался из каталога `web/` относительно **текущей рабочей директории** процесса; при `WorkingDirectory=/opt/nettopo-go` без этой папки корень сайта давал 404. Сейчас UI **вшит в бинарник** — после обновления до актуального коммита пересоберите сервер (`go build ...`) и перезапустите сервис. Проверка API: `curl -s http://127.0.0.1:8080/health` должен вернуть JSON `{"status":"ok",...}`.
## Размещение за IIS на Windows
- Запускать `nettopo-server.exe` как Windows Service.
- На IIS настроить Reverse Proxy к `http://localhost:8080`.
- Требуются URL Rewrite + ARR.
Шаблон `web.config` лежит в `deploy/web.config.example`.