Добавил в README новые API и UI-возможности: IF-MIB интерфейсы, гибкий ввод CIDR, обновления по LLDP/SNMP, форматирование MAC и админ-очистку данных сканов. Made-with: Cursor
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.
Проверка версии:
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.
Запуск локально
- Перейдите в проект:
cd nettopo-go
- Скопируйте переменные окружения:
cp .env.example .env
- Запустите API:
HTTP_ADDR=:8080 go run ./cmd/server
UI открывается по адресу:
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.
- По полю
cidrsстроится список IP (с учётомexclude_ips). Поддерживается:- один CIDR;
- список CIDR через запятую;
- диапазон
CIDR-CIDR(только IPv4, одинаковая маска, границы — сетевые адреса).
- Параллельно (ограничение
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).
- ping (ICMP через системную утилиту
- Статус задачи:
queued→running(полеprogress0…100,stats.hosts_total/stats.hosts_up) →doneилиfailed. - Результаты читаются через 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 байт, теперь значение форматируется как MACaa:bb:cc:dd:ee:ff; для остальных бинарных значений остаётся0x.... - SNMP "не опрашивался" при больших сканах:
/hostsи/snmpвозвращают по одной актуальной записи на IP (dedupe latest), чтобы не терять устройства из-за ограничений выборки. - Почему раньше в "Таблица 1. Связи" были не все порты: теперь таблица строится как IF-MIB + LLDP. Показываются все интерфейсы, а LLDP соседы подмешиваются по
ifIndex.
Начать всё заново (очистка БД)
Для полного сброса scan-данных добавлен endpoint:
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)
brew install postgresql@16
brew services start postgresql@16
createdb nettopo
createuser nettopo
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
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
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:
curl -s http://localhost:8080/health
Создать scan:
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:
curl -s http://localhost:8080/api/scans/<scan_id>
Получить список scan jobs:
curl -s http://localhost:8080/api/scans?limit=20
Сравнить два scan:
curl -s "http://localhost:8080/api/scans/diff?from=<scan_id_1>&to=<scan_id_2>"
В ответе у scan есть:
status:queued,running,done,failedprogress: 0..100stats.hosts_total,stats.hosts_up
Получить результаты по хостам:
curl -s http://localhost:8080/api/scans/<scan_id>/hosts?limit=200000
Получить результаты по портам:
curl -s http://localhost:8080/api/scans/<scan_id>/ports?limit=500
Получить SNMP результаты:
curl -s http://localhost:8080/api/scans/<scan_id>/snmp?limit=200000
Получить LLDP результаты:
curl -s http://localhost:8080/api/scans/<scan_id>/lldp?limit=20000
Получить интерфейсы устройства (IF-MIB):
curl -s "http://localhost:8080/api/scans/<scan_id>/interfaces?ip=192.168.1.10&limit=4096"
Получить связи:
curl -s http://localhost:8080/api/scans/<scan_id>/links?limit=20000
Получить topology (nodes/edges):
curl -s http://localhost:8080/api/scans/<scan_id>/topology?limit=5000
Очистить все scan-данные (защищено секретом):
curl -s -X POST http://localhost:8080/api/admin/purge-scans \
-H "X-Nettopo-Reset-Key: <NETTOPO_RESET_DB_SECRET>"
Запуск как сервис на Linux (systemd)
- Собрать бинарник:
go build -o nettopo-server ./cmd/server
- Скопировать unit-файл:
sudo cp deploy/nettopo-go.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now nettopo-go
- Проверить статус:
systemctl status nettopo-go
Логи (systemd)
Сервер пишет в стандартный лог пакета log (stderr): старт, выбранное хранилище, фатальные ошибки БД. Просмотр:
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.