Цифровой дворник

Практические заметки о сетях, self-hosting, виртуализации, мониторинге, автоматизации и локальном ИИ.

Telegram: @digclean

Обложка

Заходишь в свою веб-аналитику, а там тишина. За сутки — горстка просмотров и пара посетителей. Вроде сайт живой, статьи публикуются, кто-то их читает. Но панель упорно показывает, что кроме тебя и пары друзей никто не заходит.

Знакомо? У меня так было с блогом. Пока я не полез в access-логи веб-сервера и не обнаружил, что на сайт ходят толпы ботов, а аналитика их просто не видит. Дальше — почему так вышло и что я с этим сделал.

Как Umami считает визиты

Umami (и почти любая JS-аналитика) работает через скрипт на странице:

<script async src="https://analytics.example/script.js"
        data-website-id="..."></script>

Логика простая: браузер загружает страницу → выполняет скрипт → скрипт шлёт событие на api/send. Всё считает клиент, на стороне сервера — только приём.

И вот тут первая ловушка: боты не исполняют JavaScript. Поисковики, AI-краулеры, SEO-сканеры скачивают HTML и уходят. Они не запускают скрипт аналитики, поэтому для Umami их просто не существует.

Вторая ловушка: Umami по умолчанию фильтрует ботов по User-Agent. Даже если какой-то краулер решит выполнить скрипт, встроенная детекция ботов его отсеет. То есть панель намеренно показывает только «настоящих» людей.

Звучит разумно, пока тебе не нужно понять: а кто вообще стучится в мой сайт?

Что я увидел в логах

Когда я сравнил картину из Umami и из access-логов Caddy, разница оказалась смешной.

Umami за сутки: около полусотни просмотров, двадцать с лишним посетителей.

А в логах веб-сервера за тот же день — толпа. Вот кто приходил:

  • AhrefsBot — SEO-краулер
  • GPTBot, ChatGPT-User — краулеры OpenAI
  • ClaudeBot — Anthropic
  • PerplexityBot — Perplexity
  • Googlebot, Yandex, Bing — поисковики
  • TelegramBot — превью ссылок в мессенджере
  • плюс всякая мелочь вроде сканеров уязвимостей

Суммарно — под сотню обращений в день. И ни одного из них в Umami.

Вывод простой: JS-аналитика считает людей, а не трафик. Если хочешь видеть всех — надо смотреть логи сервера.

GoAccess вместо панели

GoAccess — это терминальный (и не только) анализатор логов, который гоняет access-лог и выдаёт отчёт: кто, откуда, сколько, какие страницы, какие статусы, какие боты. Никакого JS — только то, что реально дошло до сервера.

Ставится элементарно:

apt install goaccess

Но есть нюанс. Caddy по умолчанию пишет логи в stderr, а не в файл. Чтобы GoAccess их увидел, нужен файл:

log {
    output file /var/log/caddy/access.log
}

Тут я наступил на граблю: Caddy в systemd крутится под ProtectSystem=full, и файл лога вне разрешённых путей он создать не может — падает с «permission denied». Лечится drop-in'ом:

# /etc/systemd/system/caddy.service.d/rw-logs.conf
[Service]
ReadWritePaths=/var/log/caddy

Потом systemctl daemon-reload && systemctl reload caddy.

JSON-логи Caddy — не для GoAccess

Вторая засада: Caddy пишет логи в JSON, а GoAccess ест классический Combined Log Format. Приходится конвертировать. Я сделал это через jq:

jq -r 'select(.request != null)
  | select((.request.headers["User-Agent"][0] // "") | test("Uptime-Kuma|curl/|Python-urllib"; "i") | not)
  | "\(.request.client_ip // .request.remote_ip) - - [\(.ts|strftime("%d/%b/%Y:%H:%M:%S %z"))] \"\(.request.method) \(.request.uri) \(.request.proto)\" \(.status) \(.size // 0) \"\(.request.headers.Referer[0] // "-")\" \"\(.request.headers["User-Agent"][0] // "-")\""' \
  access.log > access.clf

Заметили фильтр? Я отсекаю собственный мониторинг (Uptime-Kuma долбит сайт каждые 30 секунд — за сутки это тысячи строк шума) и свои же curl-запросы. Иначе отчёт превращается в статистику «сколько раз мой мониторинг пинговал самого себя».

Дальше — рендер:

goaccess --log-format='%h %^[%d:%t %^] "%r" %s %b "%R" "%u"' \
  --date-format='%d/%b/%Y' --time-format='%H:%M:%S' \
  -f access.clf -o /var/www/logs/index.html --no-global-config

Обновляю отчёт по cron раз в пять минут. Финальный штрих — отдаю HTML через Caddy на поддомен logs.example, закрытый тем же allow-list'ом, что и админки (снаружи пускаю только свои IP).

Что в итоге

Теперь у меня два инструмента, и каждый на своём месте:

  • Umami — чтобы видеть живых людей: что читают, откуда пришли, что держится на странице. Тут чистота важнее полноты.
  • GoAccess по логам — чтобы видеть весь трафик: людей, ботов, сканеры, статусы 404 и 500, откуда реально ломятся.

Один не заменяет другой. Панель аналитики молчит про ботов не потому, что их нет, а потому что она про них не должна знать. А логи не знают про поведение человека на странице, зато честно показывают каждый запрос.

Мораль: если кажется, что «на сайт никто не заходит» — проверьте логи, прежде чем расстраиваться. Возможно, там как раз наоборот — заходят, и ещё как.


А у вас чем смотрите трафик? Только панель, или логи тоже смотрите?

#selfhosting #analytics #monitoring #network #goaccess #umami

Обложка

Есть такие сетевые задачи, которые на первый взгляд выглядят абсолютно прямолинейно.

Есть два линка между маршрутизаторами.
На одном OSPF cost 50.
На другом OSPF cost 10.

Кажется очевидным: маршрут должен пойти через линк с cost 10.

Но сеть, как обычно, решила напомнить, что «кажется» — это не метод диагностики.

Исходная картина

Есть Juniper с routing-instance, условно назовём его vrf-inet.

В нём крутится OSPF. Один из маршрутов приходит как внешний OSPF-маршрут:

192.0.2.0/24   *[OSPF/150], metric 60
                > to 198.51.100.195 via xe-0/0/0.100

И вот это metric 60 сначала немного смущает.

На экспортирующем роутере для этого маршрута уже поставили external metric 10.
На альтернативном интерфейсе OSPF cost тоже 10.

Но маршрут всё равно идёт через старый интерфейс, у которого cost 50.

Возникает нормальный человеческий вопрос:

почему оно не идёт через интерфейс, где cost 10?

Первая ловушка: export metric и interface cost — это разные вещи

В OSPF важно не смешивать две разные сущности:

  1. OSPF cost интерфейса — стоимость пути внутри OSPF-топологии.
  2. External metric — метрика внешнего маршрута, который мы редистрибутим в OSPF через export policy.

Если маршрут экспортируется в OSPF как external type 1, итоговая метрика считается примерно так:

external metric + internal cost до ASBR

То есть если мы задали external metric 10, а до ASBR роутер видит путь cost 50, то в таблице маршрутизации вполне логично появится:

metric 60

И это как раз хорошая подсказка.

Не «OSPF сошёл с ума», а наоборот: он честно сложил 10 + 50.

Вторая ловушка: смотреть OSPF надо внутри routing-instance

На Junos, если OSPF работает не в default instance, а внутри routing-instance, то команда:

show ospf neighbor

может ответить:

OSPF instance is not running

И это не значит, что OSPF умер. Это значит, что мы смотрим не туда.

Правильно так:

show ospf neighbor instance vrf-inet
show ospf interface instance vrf-inet
show ospf route instance vrf-inet

Для конкретных интерфейсов:

show ospf interface xe-0/0/0.100 detail instance vrf-inet
show ospf interface xe-0/0/1.200 detail instance vrf-inet

И вот тут началось интересное.

Что показала диагностика

На старом интерфейсе картина была нормальная:

Interface: xe-0/0/0.100
Type: LAN
Cost: 50
Adj count: 2

Соседи есть, adjacency есть, OSPF живёт полноценной жизнью.

А на новом «дешёвом» интерфейсе было так:

Interface: xe-0/0/1.200
Type: LAN
Cost: 10
Priority: 0
Adj count: 0

И в соседях:

neighbor 198.51.100.121 via xe-0/0/1.200 state 2Way

Вот тут и спряталась причина.

Cost 10 на интерфейсе есть.
Но полноценной OSPF-смежности нет.
Сосед виден, hello принимаются, но состояние только 2Way, а не Full.

А если нет Full, то этот линк не становится нормальным маршрутом через OSPF для нужного расчёта SPF.

Почему оно зависло в 2Way

Интерфейс был типа LAN, то есть OSPF воспринимал его как broadcast-сегмент.

На broadcast-сети OSPF не обязан строить Full adjacency со всеми подряд. Там есть DR/BDR, и полноценные соседства строятся через них.

Но на линке /30 между двумя роутерами это обычно не то поведение, которое мы хотим.

В нашем случае ещё и priority был 0.

Условно:

Type: LAN
Priority: 0
DR: 0.0.0.0
BDR: 0.0.0.0
Adj count: 0

Получается типичная ситуация:

  • интерфейс маленький, фактически point-to-point;
  • OSPF считает его LAN/broadcast;
  • priority 0;
  • DR/BDR не выбирается;
  • соседство остаётся в 2Way;
  • cost 10 вроде есть, но маршрут через этот линк не строится.

Очень жизненно. Вроде всё настроено, но нет.

Решение

Для /30-линка между двумя маршрутизаторами логичнее явно сказать OSPF, что это point-to-point.

На первом конце:

configure
set routing-instances vrf-inet protocols ospf area 0.0.0.0 interface xe-0/0/1.200 interface-type p2p
commit

На втором конце — то же самое на соответствующем интерфейсе:

configure
set routing-instances vrf-inet protocols ospf area 0.0.0.0 interface xe-0/0/1.200 interface-type p2p
commit

После этого проверяем:

show ospf neighbor instance vrf-inet | match "xe-0/0/1.200|198.51.100.121"
show ospf interface xe-0/0/1.200 detail instance vrf-inet
show ospf route instance vrf-inet | match "192.0.2.0|router-id"
show route 192.0.2.0

Нормальная картина после исправления:

neighbor 198.51.100.121 via xe-0/0/1.200 state Full

И маршрут должен пересчитаться уже через новый интерфейс.

Если external metric 10, а cost линка 10, то для external type 1 можно ожидать итоговую метрику около:

10 + 10 = 20

Разумеется, если нет других ASBR, forwarding-address и дополнительных особенностей топологии.

Что ещё стоит проверить

Если после перевода в p2p маршрут всё равно не пошёл куда надо, я бы проверял уже вот это:

show ospf database external 192.0.2.0 extensive instance vrf-inet
show route 192.0.2.0 extensive
show ospf route instance vrf-inet
show route <router-id или forwarding-address>

Особенно важно посмотреть:

Advertising router
Forwarding address
External type
Metric

Потому что для external route OSPF выбирает путь не «к префиксу в вакууме», а к ASBR или forwarding address. И если forwarding address резолвится через другой интерфейс, маршрут тоже может уйти не туда, куда мы глазами ожидали.

Важный момент про export policy

В этом кейсе export policy тоже фигурировала.

Изначально маршрут экспортировался в OSPF без явного external metric, поэтому в таблице была метрика 0.

Потом для exported route задали:

then external type 1
then metric 10

После этого метрика стала 60.

И это было правильно: Junos начал считать E1 как external metric плюс internal cost до ASBR.

Но попытка менять export policy на принимающем роутере не влияет на то, как он выбирает next-hop для уже полученного OSPF-маршрута.

То есть:

set policy-options policy-statement rp-ospf-export ...

на принимающей стороне влияет только на то, что этот роутер сам экспортирует в OSPF.

А выбор next-hop для полученного маршрута — это уже SPF, соседства, cost, ASBR и forwarding-address.

Короткая памятка

Если OSPF-маршрут не идёт через интерфейс с меньшим cost:

1. Проверь, в каком instance живёт OSPF

show ospf neighbor instance <instance-name>

2. Проверь состояние соседа

show ospf neighbor instance <instance-name>

Нужно Full, а не просто 2Way.

3. Проверь тип интерфейса

show ospf interface <interface> detail instance <instance-name>

Если это /30 или /31 между двумя роутерами, часто правильнее:

interface-type p2p

4. Проверь, до кого реально строится путь

show ospf database external <prefix> extensive instance <instance-name>
show route <forwarding-address или router-id>

5. Не путай external metric и interface cost

Для E1:

итоговая метрика = external metric + internal cost до ASBR

Для E2 логика другая: внешняя метрика обычно доминирует, а internal cost используется иначе при сравнении. Но это не означает, что E2 «заставит» маршрут пойти через нужный интерфейс. Next-hop всё равно зависит от SPF и достижимости ASBR/forwarding-address.

Вывод

В этой истории проблема была не в том, что Junos неправильно считал метрику.

Он как раз считал её очень честно:

10 external + 50 до ASBR = 60

Проблема была в том, что альтернативный линк с cost 10 не имел полноценной OSPF adjacency. Он висел в 2Way, потому что интерфейс был LAN/broadcast, priority был 0, DR/BDR не выбрался, а Full-соседство не построилось.

После перевода интерфейса в point-to-point всё стало на свои места.

Мораль простая: если OSPF «не хочет» идти по дешёвому пути, сначала убедитесь, что этот путь вообще существует для SPF, а не просто красиво выглядит в конфиге.


#networking #junos #ospf #juniper #routing #nsp

Почему пришлось брать метлу в лапки

Мы хостили иллюстрации для постов на catbox.moe. В какой‑то момент их CDN (files.catbox.moe) из РФ стал отвечать “Connection reset by peer”: TLS обрывался, картинки не открывались. Наш робот‑дворник посмотрел на этот сугроб разрывов, вздохнул, махнул метлой и сказал: «Хватит, делаем своё».

Что за rustypaste и почему он

rustypaste (v0.17.0) — минималистичный paste/file‑host на одном бинарнике Rust. Базы данных нет, всё хранится в файловой системе, конфиг — один config.toml. Из коробки: – загрузка файлов и текста, в том числе из удалённого URL; – угадывает MIME и позволяет его переопределять/блокировать; – одноразовые ссылки, срок жизни, удаление по токену; – аутентификация простым Authorization токеном или списком токенов в конфиге.

Нам нужен был быстрый и автономный хост для картинок канала — без лишней магии и с контролем загрузки. Rustypaste закрыл все пункты.

Как подняли: LXC + systemd + Caddy

  • Отдельный контейнер (LXC) во внутренней сети сервисов 198.51.100.0/24, порт 8000.
  • Снаружи — поддомен paste.example через Caddy reverse_proxy. Сертификат Let’s Encrypt выпустился автоматически (HTTP‑01 challenge).
  • Загрузка разрешена только с токеном в заголовке Authorization. Отдача — публичная, с корректным Content-Type (например, image/png).

systemd-юнит

Создаём пользователя и юнит:

# один раз

![Обложка](https://paste.clr58.ru/post-rustypaste-cover-digclean.jpg)

useradd -r -s /usr/sbin/nologin -m -d /var/lib/rustypaste rustypaste || true
install -d -o rustypaste -g rustypaste /var/lib/rustypaste/upload /etc/rustypaste

cat >/etc/systemd/system/rustypaste.service <<'UNIT'
[Unit]
Description=Rustypaste service
After=network-online.target
Wants=network-online.target

[Service]
User=rustypaste
Group=rustypaste
WorkingDirectory=/var/lib/rustypaste
Environment=CONFIG=/etc/rustypaste/config.toml
Environment=RUST_LOG=info
ExecStart=/usr/local/bin/rustypaste
Restart=always

[Install]
WantedBy=multi-user.target
UNIT

systemctl daemon-reload
systemctl enable --now rustypaste

Конфиг rustypaste (/etc/rustypaste/config.toml)

Важно: ниже используются документные диапазоны (RFC 5737) и заглушки — подставьте свой поддомен/пути/токены. Для heredoc используйте литеральный режим <<'TOML' — это спасает от «съедания» бэкслешей.

cat > /etc/rustypaste/config.toml <<'TOML'
[server]
address = "198.51.100.42:8000"        # LXC во внутренней сети
url = "https://paste.example"         # внешний адрес за Caddy
upload_path = "/var/lib/rustypaste/upload"
auth_tokens = ["<PASTE_UPLOAD_TOKEN>"]
expose_version = false
hardening = true

[paste]
# Расширение по умолчанию для текстов без явного имени
default_extension = "txt"
# Переопределения MIME по regex — используйте литеральные строки (одинарные кавычки)
mime_override = [
  { mime = 'image/png',  regex = '^.*\.png$' },
  { mime = 'image/jpeg', regex = '^.*\.(jpg|jpeg)$' },
  { mime = 'image/gif',  regex = '^.*\.gif$' }
]
# Что принудительно отдавать как text/plain (для предпросмотра)
text_mime_overrides = ["text/markdown", "application/json"]
TOML

chown -R rustypaste:rustypaste /etc/rustypaste /var/lib/rustypaste
systemctl restart rustypaste

Caddy reverse_proxy

Минимальный сайт‑блок:

paste.example {
  encode zstd gzip
  reverse_proxy 198.51.100.42:8000
}

Caddy сам пройдёт HTTP‑01 и выпустит сертификат для paste.example.

Загрузка и проверка

# Без токена — 401 Unauthorized
curl -fsS -F "file=@x.png" https://paste.clr58.ru/ || echo "→ 401 as expected"

# С токеном — получаем ссылку вида https://paste.clr58.ru/<имя>.png
curl -fsS -H "Authorization: <PASTE_UPLOAD_TOKEN>" -F "file=@x.png" https://paste.clr58.ru/

Пакетная переливка картинок (пример):

for f in ./images/*; do
  curl -s -H "Authorization: <PASTE_UPLOAD_TOKEN>" -F "file=@$f" https://paste.clr58.ru/ >> uploaded_urls.txt
  echo >> uploaded_urls.txt
done

Грабли, на которые наступили

1) TOML + regex: «TOML parse error: missing escaped value». Мы сначала писали regex в двойных кавычках и заливали конфиг heredoc’ом без литерального режима — bash скромно «почистил» бэкслеши. Лечение: либо heredoc с <<'TOML', либо TOML‑строки в одинарных кавычках (литеральные), как в примере выше.

2) «config.toml is not found» и цикл рестартов. Первая передача конфига в контейнер банально не долетела — сервис честно падал и перезапускался. Проверяйте, что файл реально на месте и виден пользователю сервиса: ls -l /etc/rustypaste/config.toml и права на каталог.

Итог: картинки вернулись домой

  • Свой paste на rustypaste избавил нас от капризов чужого CDN.
  • Загрузка защищена токеном, отдача идёт с правильным MIME.
  • Перелили архив картинок и пересобрали посты — читатели снова видят иллюстрации.

Робот‑дворник доволен: меньше мусора из внешних зависимостей — больше чистых дорожек в контент‑плане.

Ссылки

#selfhosting #lifehacks

Обложка

Есть момент, когда агент перестаёт влезать в собственный промпт. Начинаешь с трёх заметок, через месяц их шестьдесят, и каждый старт беседы ты уже платишь за то, чтобы модель просто прочитала твою же память — до того, как сделает хоть что-то полезное.

Я упёрся в это ровно тогда, когда закончил серию из одиннадцати статей и понял, что помнить их все «в голове» (в контексте) больше нельзя. Пришлось решать: тащить всё в промпт и платить за каждый запрос, или построить граф знаний и платить за то, чтобы в него что-то положить.

Дальше — реальные цифры с моей домашней лабы. Не синтетика, а замеры на живом ассистенте.

Два способа помнить

Плоская память. Всё лежит в markdown-файлах, которые подгружаются в контекст. Модель видит их напрямую, но каждый запрос начинается с того, что весь этот текст прогоняется через токенайзер. Чем больше помнишь — тем дороже каждый вопрос, даже «как дела».

Графовая память. Текст прогоняется через LLM один раз — из него вытаскиваются сущности и связи («gemma3 работает на ollama», «gemma3 не поддерживает tools»). Это складывается в граф. При вопросе ищешь по смыслу стартовый узел и разворачиваешь соседей. Платишь за инжест, поиск почти бесплатный.

Два способа помнить: плоская память против графа

Какие бывают графы

Три уровня, от простого к навороченному — все три я разворачивал за один день и гонял на одной и той же статье.

Самописный SQLite-граф. Две таблицы — сущности и связи, обход соседей пишешь сам. Ноль инфраструктуры, полный контроль, читаемый вывод. Минус: никакой темпоральности — граф не помнит, что факт был правдой только до вчера, и всё извлечение вручную.

Property graph (FalkorDB, Neo4j). Настоящий графовый движок, запросы на Cypher, быстрый обход на любую глубину, индексы. Это как SQL, только для связей. FalkorDB легче Neo4j — это Redis с графовым модулем, поднимается одной командой.

Темпоральный граф (Graphiti, Microsoft GraphRAG). Уровень выше: ты не строишь граф вручную, а скармливаешь текст — LLM сам вытаскивает сущности, факты и связи. Главное отличие — битемпоральность: граф помнит не только «что правда сейчас», но и «что было правдой раньше», и откуда каждый факт взялся. Для ассистента, который живёт неделями и чьи знания устаревают, это решающее свойство.

Как это ставится

Честный путь по моему опыту.

FalkorDB — одна команда:

docker run -d --name falkordb -p 6379:6379 falkordb/falkordb

Graphiti поверх него — Python-пакет:

pip install graphiti-core[falkordb]

Дальше конфиг: какая LLM извлекает сущности, какие эмбеддинги, куда складывать. Грабли, на которые я наступил, чтобы ты не наступал: у DeepSeek нужно явно задать «маленькую» модель (иначе Graphiti шлёт дефолтную OpenAI-модель и ловит 400); эмбеддеру нужен непустой api_key, даже если он никем не проверяется; кириллические названия связей движок не переваривает — их надо класть в свойство ребра, а не в тип.

SQLite-граф ставить нечего — это двадцать строк на Python.

Дальше — замеры, ради которых всё затевалось.

Замер 1: начало беседы

Считаем, сколько токенов уходит на старт, до первого вопроса.

Файлы, которые грузятся всегда: долговременная память, заметки по инструментам, правила поведения — примерно 16–20 тысяч токенов. Это цена «проснуться и вспомнить, кто я».

А вот дальше развилка. Дневные заметки копятся. Вся история — это уже 61 тысяча токенов и растёт каждый день. В плоской памяти у тебя два пути, оба плохие: либо грузить всё (каждый запрос дорожает на десятки тысяч токенов), либо обрезать старое (и агент «забывает» месяц назад).

Замер 2: сколько стоит положить знание в граф

Кладём в граф короткий эпизод — 483 символа текста, пять предложений про то, что мы подняли графовую память.

Что происходит под капотом: LLM несколько раз прогоняет текст — извлекает сущности, потом связи, потом сверяет с уже существующими узлами, не дубликаты ли это. Итого 8 вызовов LLM, 13 тысяч входных и 1 тысяча выходных токенов, 13 секунд.

В деньгах — $0.0021 за эпизод на дешёвой модели. Это и есть цена знания: одна заметка ≈ полцента.

Статья из серии (6–7 тысяч символов) обходится в $0.03. Вся серия из одиннадцати статей — $0.33.

Замер 3: сколько стоит вспомнить

Вопрос «какая локальная модель используется для эмбеддингов» к графу.

Ноль вызовов LLM. $0. Ноль целых три десятых секунды.

Потому что поиск по графу — это не LLM, а эмбеддинги (локальная модель, бесплатно) плюс обход связей в базе. LLM подключается только на этапе занесения знания, а не на каждом вопросе.

Это главный разворот по сравнению с плоской памятью: там ты платишь за знание при каждом обращении к нему, здесь — один раз при занесении.

Замер 4: субагенты

Запуск субагента — это отдельная история, и тут граф ничего не экономит напрямую: субагент всё равно получает свой контекст и свой промпт. Каждый субагент — это десятки тысяч токенов входного контекста только на то, чтобы он понял задачу.

Что реально меняет граф: субагент может не тащить с собой всю историю. Достаточно дать ему узкий контекст задачи и, если нужно, пару фактов из графа точечным поиском — а не вываливать на него шестьдесят тысяч токенов прошлого.

А если это кодинг-агент

Тот же самый граф можно повесить не только на ассистента, но и на кодинг-агентов — Codex, Claude Code, Cursor, OpenCode. Идея ровно та же, что с заметками: кодинг-агент не держит в голове всю кодовую базу, а спрашивает граф, куда смотреть.

Общий знаменатель — MCP. Все четверо умеют подключать MCP-серверы как инструменты, а у Graphiti такой сервер уже есть из коробки. Разница только в конфиге:

  • Codex — подключить Graphiti как MCP memory-инструмент; агент зовёт его вместо того, чтобы перечитывать файлы.
  • Claude Code — claude mcp add или hook, который при старте задачи дергает граф за контекстом.
  • Cursor — .cursor/mcp.json, граф становится обычным источником @-упоминаний.
  • OpenCode — тот же MCP, конфиг в json.

Что кладём в граф: карту репозитория — файлы, модули, функции, классы и связи между ними (кто кого импортирует, кто кого вызывает), плюс историю решений «почему сделали так, а не иначе».

Кодинг-агенты подключаются к графу через MCP

Прогноз по токенам. Это уже не замер, а честная прикидка, но логика та же, что в замерах выше:

  • Сейчас кодинг-агент на каждую задачу перечитывает дерево проекта и релевантные файлы — легко 50–100k входных токенов только на то, чтобы понять, где он вообще находится.
  • С графом он сперва спрашивает «какие модули связаны с этой функцией», получает точечный список из 5–15k токенов и читает только нужное. Поиск по графу, напомню, стоит $0 — это локальные эмбеддинги.
  • Итог: 60–80% экономии на входном контексте на каждой задаче. При активном кодинге это ощутимо — не десятки центов в день, а разы на месячной дистанции.

Где экономия не сработает: если проект маленький и весь влезает в контекст целиком — граф не нужен. И сам инжест кодовой базы не бесплатен: гонять весь код через LLM для извлечения связей — дорого (у нас $0.0021 за полтысячи символов). Для кода разумнее гибрид: структуру (файлы, сигнатуры, импорты) снимать статическим анализом, а через LLM — только описания и решения. Тогда разовая цена индексации окупается за считанные дни активной работы.

Точка окупаемости

Граф начинает выигрывать, когда одни и те же знания спрашиваются повторно.

Одна заметка = $0.0021 за инжест. Если факт из неё понадобится хотя бы пару раз, граф уже дешевле, чем каждый раз читать его в контексте. А главное — граф не растёт в промпте: тысячи сущностей стоят столько же при поиске, сколько и десять.

Плоская память выигрывает ровно в одном случае: знаний мало, и они меняются редко. Тогда проще держать их в одном файле.

Вывод

Граф — это не «модно», это про то, где платить. Либо понемногу, но при каждом вопросе (контекст), либо один раз при занесении (граф). Для ассистента, который живёт неделями и накапливает опыт, второй вариант оказывается дешевле — и, что важнее, предсказуемее: стоимость вопроса не зависит от того, сколько ты всего знаешь.

Теги: #llm #graphrag #memory #openclaw #selfhosting

Обложка

Цикл «Виды связности и SDN», часть 3. Предыдущая часть: «Underlay и overlay: сеть под сетью».

Ось: уровень — L2.

VLAN хорошо разделяет сеть внутри одного L2-домена, но его идентификатор имеет 12 бит, а сам VLAN не объясняет, как перенести Ethernet через маршрутизируемую фабрику или Интернет.

VXLAN решает эту задачу: помещает Ethernet-кадр в UDP и доставляет его между VXLAN Tunnel Endpoint — VTEP. VTEP — точка, где кадр входит в туннель и выходит из него.

VNI вместо VLAN ID

VXLAN Network Identifier имеет 24 бита. Теоретически это около 16 миллионов логических сегментов вместо примерно четырёх тысяч VLAN.

VNI не обязан совпадать с VLAN ID. На границе VTEP локальный VLAN может отображаться в VNI, переноситься через IP-фабрику и на другой стороне снова превращаться в локальный VLAN.

Underlay при этом не знает MAC-адресов виртуальных машин. Он маршрутизирует только внешние IP-адреса VTEP.

Откуда VTEP знает удалённый MAC

Простейшая модель — flood-and-learn. Неизвестный unicast, broadcast и часть multicast-трафика — вместе их называют BUM — рассылаются удалённым VTEP. Получая кадры, каждый VTEP изучает соответствие MAC и источника туннеля.

На маленькой лаборатории это работает. В крупной фабрике массовое flooding становится дорогим, а сходимость — не слишком предсказуемой.

EVPN переносит информацию о MAC и IP через BGP. Вместо того чтобы сначала залить кадр всем и посмотреть, кто ответит, VTEP получает маршрут вида «этот MAC и этот IP находятся за таким VTEP».

Для L2 особенно важны EVPN Type 2 — MAC/IP Advertisement routes. Они помогают распространять привязки MAC и IP хоста и выполнять ARP/ND suppression. Сам BUM EVPN не отменяет: для него обычно нужен Type 3 (Inclusive Multicast Ethernet Tag), чаще всего через ingress replication. Type 2 убирает unknown-unicast learning и лишний ARP, а не «любое flooding навсегда».

ARP suppression

В обычной L2-сети запрос ARP является broadcast. Если control plane уже знает, какой MAC соответствует IP, локальный VTEP может ответить сам и не рассылать запрос по всей фабрике.

Это не отменяет ARP, но не позволяет каждому запросу гулять по всем стойкам. В большой виртуализированной среде разница заметна.

Geneve рядом с VXLAN

Geneve решает похожую задачу, но имеет расширяемые option fields. В них можно передавать дополнительные метаданные для сетевой обработки, политик или сервисов.

OVN обычно использует Geneve между гипервизорами. Cilium поддерживает и VXLAN, и Geneve. Выбирать Geneve только потому, что он «новее», не стоит: нужно проверить поддержку offload, сетевого оборудования, MTU и конкретных функций платформы.

VXLAN не обязан быть одним большим L2

Современная фабрика может использовать VXLAN как транспорт, а границу маршрутизации ставить близко к серверам: отдельные Ethernet-сегменты плюс распределённый шлюз. Как именно это раскладывается на L2VNI, L3VNI и anycast gateway — в части 10.

Практический вывод уже здесь: «у нас VXLAN» не означает «у нас один broadcast-домен на весь ЦОД».

Когда растянутый L2 оправдан

  • миграция виртуальных машин с сохранением адреса;
  • legacy-приложение, которое невозможно быстро перевести на маршрутизируемую схему;
  • кластер, официально требующий общей L2-сети;
  • временная миграция между площадками;
  • подключение bare metal к tenant-сегменту.

Где это ломается

Если L2 растягивается только потому, что менять адреса неудобно, цена обычно выше ожидаемой:

  • broadcast и unknown unicast получают дальность действия;
  • отказ или петля затрагивают больше систем;
  • сложнее локализовать асимметрию;
  • возрастает зависимость от control plane;
  • аварийное переключение площадок может конфликтовать с внешней маршрутизацией;
  • flood-and-learn на большой фабрике маскирует отсутствие EVPN до первого инцидента.

Практическое правило: L2 растягиваем только для конкретного требования и заранее знаем, где расположена граница маршрутизации.

Где здесь SDN

Контроллер создаёт логический switch, порты, ACL и привязки к VNI. Локальные агенты превращают эту модель в flow rules и tunnel endpoints. Пользователь работает с логической сетью, не настраивая каждый гипервизор вручную.

OVN как раз предоставляет логические L2- и L3-сети, ACL, DHCP, DNS и распределённые маршрутизаторы поверх Open vSwitch.

В следующей части откажемся от идеи общей Ethernet-среды и перейдём к L3: IPIP, GRE, BGP, native routing и CrossSubnet.

Предыдущая часть: «Underlay и overlay: сеть под сетью»

Оглавление цикла: «SDN без магии: карта видов связности»

Следующая часть: «L3-overlay и native routing: IPIP, GRE, BGP и CrossSubnet»

Схема

#network #selfhosting

Иногда задача выглядит совсем простой: заменить адрес NetFlow-коллектора на двух пограничных маршрутизаторах Juniper MX. Добавили новый flow-server, сделали commit, увидели растущие счётчики — вроде бы можно расходиться.

Но один маршрутизатор отправляет данные нормально, а второй либо не виден на коллекторе, либо экспортирует в несколько раз меньше. Причём Junos уверенно показывает Flows Exported, а Export Packet Failures остаётся равным нулю.

Именно с такой ситуацией мы столкнулись на двух MX, работавших на разных ветках Junos: один на достаточно старом 20.4R2.7, другой на ветке 23.4. В итоге разница версий оказалась не причиной неисправности, но заставила внимательно проверить ограничения платформы и способ доставки экспортных пакетов.

Все имена, IP-адреса, номера VLAN и интерфейсов ниже изменены. Для адресов используются специальные тестовые сети RFC 5737.

Коротко: что такое inline J-Flow

J-Flow — название технологии учёта потоков у Juniper. Коллектору данные обычно отправляются в формате NetFlow v9 или IPFIX.

В режиме inline-jflow потоки обрабатывает не Routing Engine, а Packet Forwarding Engine — PFE. Он выбирает пакеты согласно sampling rate, создаёт flow records, обновляет их и экспортирует на коллектор по UDP.

Это важное архитектурное различие. Команда ping, запущенная из CLI, обычно проверяет доступность от Routing Engine. Успешный ping ещё не доказывает, что PFE сможет отправить тем же путём NetFlow-пакеты.

Juniper прямо указывает, что inline collectors обычно недоступны через management-интерфейсы вроде fxp0. Поддержка экспорта через mgmt_junos появилась отдельно в Junos OS Evolved 24.2R1 и только на поддерживаемых платформах. Для классического Junos на MX нельзя считать, что это автоматически работает.

Исходная схема

Условно у нас было два маршрутизатора:

  • EDGE-A — Junos 20.4R2.7;
  • EDGE-B — Junos ветки 23.4;
  • старый коллектор — 198.51.100.10;
  • новый коллектор — 198.51.100.20, UDP/9992.

На обоих MX уже работал inline-jflow. Задача состояла в том, чтобы добавить или заменить коллектор и убедиться, что новый сервер получает потоки.

Базовая часть конфигурации выглядела примерно так:

set services flow-monitoring version9 template NF-V9 ipv4-template
set services flow-monitoring version9 template NF-V9 flow-active-timeout 60
set services flow-monitoring version9 template NF-V9 flow-inactive-timeout 15
set services flow-monitoring version9 template NF-V9 template-refresh-rate packets 1000
set services flow-monitoring version9 template NF-V9 template-refresh-rate seconds 30

set forwarding-options sampling instance NF-SAMPLE input rate 1024
set forwarding-options sampling instance NF-SAMPLE family inet output flow-server 198.51.100.20 port 9992
set forwarding-options sampling instance NF-SAMPLE family inet output flow-server 198.51.100.20 version9 template NF-V9
set forwarding-options sampling instance NF-SAMPLE family inet output inline-jflow source-address 192.0.2.40

set chassis fpc 0 sampling-instance NF-SAMPLE

На нужных логических интерфейсах включается sampling, например:

set interfaces et-0/0/2 unit 110 family inet sampling input
set interfaces et-0/0/2 unit 120 family inet sampling input

Номера портов и units здесь условные. В реальной сети sampling нужно включать осознанно: на тех направлениях и в той стороне, которые действительно должны участвовать в учёте.

Ловушка №1: маршрут есть, ping проходит, экспорта нет

На проблемном маршрутизаторе адрес источника J-Flow находился на fxp0, а маршрут к коллектору уходил в mgmt_junos:

inline-jflow в PFE → mgmt_junos → fxp0 → collector

С Routing Engine коллектор отвечал на ping. Счётчики показывали созданные и экспортированные flows. Ошибки экспорта не увеличивались. И всё же на сервере нормального потока UDP-пакетов не было.

Это один из самых неприятных сценариев диагностики: каждая отдельная проверка выглядит успешной, но проверяется не тот путь.

Для inline monitoring Juniper отдельно предупреждает: flow records и templates нельзя экспортировать через management interface, кроме специально оговорённых сочетаний Junos OS Evolved, релиза и платформы. На обычном MX с классическим Junos рассчитывать на fxp0 нельзя.

Поэтому такая проверка недостаточна:

ping 198.51.100.20 routing-instance mgmt_junos source 192.0.2.40 rapid count 5

Она подтверждает только то, что Routing Engine видит сервер через management VRF.

Надёжная конечная проверка делается на самом коллекторе:

tcpdump -ni any 'src host 192.0.2.40 and udp dst port 9992'

Если UDP-пакеты приходят, но система не показывает flows, нужно проверять уже не маршрутизацию, а обработку NetFlow v9 templates, ACL коллектора и привязку нового exporter IP.

Рабочее решение: отдельный data-plane VRF

Мы не стали переделывать основную таблицу маршрутизации и трогать BGP/OSPF. Вместо этого создали небольшой отдельный VRF только для экспорта J-Flow и вывели его через обычный порт PFE.

Схема стала такой:

inline-jflow в PFE → NF-EXPORT VRF → et-* VLAN → gateway → collector

Пример с полностью вымышленными параметрами:

set interfaces et-0/0/3 unit 3900 description NETFLOW-EXPORT
set interfaces et-0/0/3 unit 3900 vlan-id 3900
set interfaces et-0/0/3 unit 3900 family inet address 192.0.2.40/24

set routing-instances NF-EXPORT instance-type vrf
set routing-instances NF-EXPORT route-distinguisher 64512:3900
set routing-instances NF-EXPORT vrf-target target:64512:3900
set routing-instances NF-EXPORT interface et-0/0/3.3900

set routing-instances NF-EXPORT routing-options static route 198.51.100.10/32 next-hop 192.0.2.1
set routing-instances NF-EXPORT routing-options static route 198.51.100.20/32 next-hop 192.0.2.1

После этого коллектор связывается с VRF непосредственно в sampling configuration:

set forwarding-options sampling instance NF-SAMPLE family inet output flow-server 198.51.100.20 routing-instance NF-EXPORT

delete forwarding-options sampling instance NF-SAMPLE family inet output inline-jflow source-address
set forwarding-options sampling instance NF-SAMPLE family inet output inline-jflow source-address 192.0.2.40

Если одновременно используются два collectors и платформа это поддерживает:

set forwarding-options sampling instance NF-SAMPLE family inet output flow-server 198.51.100.10 routing-instance NF-EXPORT
set forwarding-options sampling instance NF-SAMPLE family inet output flow-server 198.51.100.20 routing-instance NF-EXPORT

Нужно также разрешить VLAN на промежуточном коммутаторе, обеспечить обратный маршрут к 192.0.2.40 и разрешить нужный UDP-порт на коллекторе и межсетевых экранах.

Применять такую конфигурацию безопаснее через подтверждаемый commit:

commit check
show | compare
commit confirmed 10

После проверки не забываем зафиксировать изменения:

commit

Почему именно VRF, а не virtual-router

В Junos типы routing instance vrf и virtual-router похожи внешне, но для flow monitoring это не взаимозаменяемые сущности.

В документации к flow-server ... routing-instance явно указано: routing instance должен иметь instance-type vrf. Возможность отправлять NetFlow v9 и IPFIX через WAN-порты non-default VRF в классическом Junos существует давно, но обычный virtual-router под это требование формально не подходит.

Переводить существующий большой virtual-router с полной таблицей BGP в vrf только ради NetFlow — плохая идея. Такая операция перезапустит протоколы и потребует пересоздания таблиц и меток. Отдельный маленький VRF для экспорта проще, понятнее и почти не затрагивает рабочую маршрутизацию.

Нюансы разных версий Junos

Главный вывод из практики: номер релиза сам по себе редко отвечает на вопрос «заработает ли J-Flow». Смотреть нужно сочетание:

модель устройства + тип Junos + релиз + линейная карта + формат экспорта

1. Базовая конфигурация на 20.4 и 23.4 почти одинаковая

На классическом Junos для MX основные иерархии остаются прежними:

  • services flow-monitoring — шаблон;
  • forwarding-options sampling — sampling instance, collector и source address;
  • chassis fpc ... sampling-instance — привязка к PFE;
  • interfaces ... sampling input|output — выбор трафика.

Поэтому наличие старой и новой версии на двух одинаковых маршрутизаторах ещё не объясняет различие в объёме данных.

2. Несколько collectors — платформозависимая функция

В разных разделах документации встречаются ограничения от одного до четырёх collectors на family. Поддержка зависит от устройства и релиза. Нельзя делать вывод только по тому, что команда принимается CLI.

Правильный порядок:

  1. Проверить модель и релиз в Juniper Feature Explorer.
  2. Выполнить commit check с нужным количеством flow-server.
  3. Убедиться по tcpdump, что templates и data records приходят на каждый сервер.
  4. Проверить, что у всех collectors одной family используется один source IP и совместимый template.

В нашей конфигурации два collectors работали на обоих релизах, но это не следует автоматически переносить на любую серию MX, PTX, ACX или QFX.

3. mgmt_junos — не универсальное решение

Поддержка экспорта через management VRF появилась в Junos OS Evolved 24.2R1 только для поддерживаемых платформ. Это не означает, что fxp0 заработал для inline J-Flow на всех MX после обновления.

Для классического Junos на MX безопасное правило остаётся простым: collector должен быть достижим через data-plane интерфейс.

4. Поддержка полей менялась между релизами

Например, NetFlow v9 для IPv6 появился позже, чем для IPv4. Начиная с 18.4R1 изменились рекомендуемые опции для MPLS templates. В 24.2R1 для ряда платформ появилась более точная обработка нескольких BGP next hops.

Если коллектору важны MPLS, IPv6, AS path, BGP next hop или корректный outgoing interface, нужно проверять не только факт прихода packets, но и состав полей конкретного template на конкретном релизе.

5. Обновление Junos не исправляет неправильный путь

Можно обновить маршрутизатор с 20.4 до более свежей ветки и получить тот же результат, если source address по-прежнему находится на fxp0, а collector доступен только через management table.

Сначала проверяем архитектуру экспорта, затем известные проблемы релиза, и только потом рассматриваем обновление как исправление.

Проверка после настройки

Сначала убеждаемся, что интерфейс и VRF поднялись:

show interfaces terse et-0/0/3.3900
show route table NF-EXPORT.inet.0 198.51.100.20 exact
ping 198.51.100.20 routing-instance NF-EXPORT source 192.0.2.40 rapid count 5

Затем смотрим состояние inline J-Flow:

show services accounting status inline-jflow fpc-slot 0
show services accounting flow inline-jflow fpc-slot 0
show services accounting errors inline-jflow fpc-slot 0

Полезный короткий вывод:

show services accounting flow inline-jflow fpc-slot 0 | match "Flow Packets:|Flows Exported:|Flow Packets Exported:"

Что означают основные счётчики:

  • Flow Packets — сколько sampled packets обработано;
  • Flows Exported — сколько flow records сформировано;
  • Flow Packets Exported — сколько экспортных UDP-пакетов сформировано;
  • Export Packet Failures — ошибки экспорта, видимые самому механизму J-Flow.

На коллекторе:

tcpdump -ni any 'src host 192.0.2.40 and udp dst port 9992'

Для NetFlow v9 важно увидеть не только data packets, но и templates. Без актуального template сервер может получать UDP, но не уметь разобрать flow records. После изменения exporter IP некоторые системы также создают новый источник, который нужно отдельно разрешить или привязать.

Ловушка №2: «второй MX экспортирует в пять раз меньше»

После исправления маршрутизации оба маршрутизатора начали нормально отправлять J-Flow. Но на коллекторе объём от EDGE-B всё равно был примерно в пять раз меньше, чем от EDGE-A.

Первое подозрение — потери в новом VRF. Однако сравнение счётчиков показало почти одинаковую пропорцию на всех этапах:

sampled packets → flow records → export UDP packets

Если Flow Packets, Flows Exported и Flow Packets Exported отличаются между двумя маршрутизаторами примерно в одно и то же число раз, экспорт обычно работает нормально. Просто один маршрутизатор видит меньше sampled traffic.

Вот фактическая дельта counters за один и тот же контрольный интервал:

Счётчик EDGE-B, Junos 23.4 EDGE-A, Junos 20.4R2.7 Соотношение
Flow Packets 36 595 190 594 EDGE-A ×5,21
Flow Bytes 41 005 542 186 773 793 EDGE-A ×4,55
Flows Exported 29 112 151 742 EDGE-A ×5,21
Flow Packets Exported 7 348 38 008 EDGE-A ×5,17

Картина почти идеальная: старый MX обработал примерно в 5,2 раза больше sampled packets, сформировал в 5,2 раза больше flow records и отправил в 5,17 раза больше экспортных UDP-пакетов. Если бы проблема находилась между PFE и коллектором, такая ровная пропорция на всех трёх стадиях была бы маловероятна.

То есть старый Junos здесь не только не мешал — маршрутизатор на 20.4R2.7 фактически экспортировал больше. Причиной оказалось распределение реального трафика и различный охват интерфейсов sampling.

Причины могут быть совершенно штатными:

  • через один MX реально проходит больше трафика;
  • sampling включён на разном количестве интерфейсов;
  • на одном устройстве забыли один peer или VLAN;
  • входящий трафик распределён несимметрично из-за BGP;
  • внутренний исходящий трафик предпочитает один маршрутизатор по IGP metric;
  • сравниваются накопительные counters с разным uptime.

Сравнивать нужно не абсолютные значения, а прирост за одинаковый интервал. Например, снять counters на обоих устройствах, через 60 секунд повторить и посчитать delta.

При sampling rate 1:1024 грубая оценка исходного packet rate выглядит так:

исходный PPS ≈ прирост Flow Packets / интервал × 1024

Но не стоит напрямую приравнивать число flows к битрейту. Один длинный поток и тысячи коротких соединений могут передать одинаковый объём данных, но сформировать совершенно разное количество flow records.

Ещё несколько нюансов, о которых легко забыть

Sampling на входе и выходе — это не одно и то же

Если включить sampling сразу в обе стороны без понимания схемы, один и тот же пользовательский трафик можно учесть дважды. Сначала нужно определить точку учёта: внешние uplinks, абонентские интерфейсы, BNG-facing links или другой однозначный срез.

На одном FPC доступен один sampling instance

Пользовательский sampling instance имеет приоритет над global instance. Не стоит создавать несколько похожих конфигураций в надежде разделить collectors — результат может оказаться не тем, который ожидается.

Flow table тоже имеет предел

При большом количестве одновременных потоков нужно проверить размер flow hash table и ошибки accounting. Менять её вслепую не стоит: доступный размер зависит от платформы и линейной карты.

sFlow и mirroring могут конфликтовать

Juniper предупреждает о непредсказуемом поведении при одновременном использовании inline active flow monitoring и sFlow на одном интерфейсе. Аналогичное предупреждение есть для egress port mirroring.

Поля next hop и outgoing interface не всегда идеальны

При ECMP, разных VRF на входе и выходе, агрегированных интерфейсах и некоторых старых релизах отдельные поля могут быть нулевыми или отражать только первый известный путь. Поэтому NetFlow — отличный инструмент учёта и аналитики, но его поля нужно интерпретировать с учётом архитектуры PFE и возможностей релиза.

Короткий чек-лист диагностики

Если J-Flow «как будто работает», а данных нет или мало:

  1. Проверить, на каких интерфейсах и в каком направлении включён sampling.
  2. Проверить привязку sampling instance к нужному FPC.
  3. Убедиться, что source IP существует и достижим обратно с коллектора.
  4. Посмотреть маршрут к collector именно в указанном routing instance.
  5. Не считать успешный ping через mgmt_junos доказательством доступности от PFE.
  6. Проверить UDP на collector через tcpdump.
  7. Убедиться, что приходят NetFlow v9/IPFIX templates.
  8. Проверить ACL и регистрацию нового exporter IP на collector.
  9. Сравнивать delta counters за одинаковый интервал.
  10. Сверить platform/release support в Feature Explorer, особенно для нескольких collectors и non-default VRF.

Итог

Разные версии Junos действительно добавляют нюансы, но в этой истории главным оказался не релиз.

Inline J-Flow живёт в PFE. Поэтому маршрут, работающий для Routing Engine через fxp0, может быть бесполезен для экспорта flow records. Надёжное решение для классического Junos на MX — обеспечить collector data-plane маршрутом. Если не хочется вмешиваться в основную таблицу, отдельный маленький VRF и технологический VLAN решают задачу аккуратно и предсказуемо.

А если после этого один маршрутизатор экспортирует меньше другого, сначала сравните охват interfaces, реальный traffic и прирост counters. Иногда «потерянный NetFlow» — это просто трафик, который всё это время шёл через соседний MX.

Официальная документация


#network

Обложка

Цикл «Не светим лишнего». Выпуск 4.

Если у нас есть офис со статическим внешним адресом, открыть ему доступ к корпоративной панели проще простого: добавили IP в allow-list, сделали правило и забыли. Иногда это действительно лучший вариант.

Статический список хорошо подходит площадкам, филиалам, собственным серверам, внешним API и известным шлюзам. В нём мало движущихся частей, нет отдельного портала и нечему разъехаться по времени.

Но белый список часто начинают воспринимать как список пользователей. А это уже ошибка.

Что видит firewall

Firewall видит исходный адрес пакета после всех внешних NAT. Если в офисе двести человек выходят через один публичный IP, для нашего периметра это один источник. Если подрядчик включил корпоративный VPN, мы увидим адрес выхода этого VPN. Если мобильный оператор использует CGNAT, рядом с нашим инженером под тем же IP могут находиться другие абоненты.

Поэтому запись 198.51.100.27 -> allow означает только одно: любой подходящий трафик, пришедший с 198.51.100.27, проходит это условие. Кто его отправил — решает уже следующий слой.

Где статический список уместен

  • межплощадочный доступ между собственными фиксированными адресами;
  • внешний сервер, который обращается к нашему API;
  • корпоративный VPN-шлюз, где пользователь уже аутентифицирован отдельно;
  • bastion или jump host с контролируемой конфигурацией;
  • мониторинг с известной площадки;
  • временно — известный адрес подрядчика, если есть процесс удаления.

Для людей с домашним динамическим Интернетом схема уже менее приятная. Сегодня адрес один, завтра другой. Администратор каждый раз лезет в правила, а старую запись обычно никто не удаляет.

Плюсы

  • очень простая реализация;
  • работает с любыми протоколами;
  • легко увидеть и проверить;
  • не требует действий от пользователя;
  • хорошо сочетается с дополнительной авторизацией ресурса;
  • почти не зависит от внешних сервисов.

Минусы

  • IP не равен пользователю;
  • общий NAT расширяет круг фактически допущенных клиентов;
  • динамические адреса меняются;
  • списки копят устаревшие исключения;
  • при передаче адреса другому абоненту доверие может перейти вместе с ним;
  • IPv4 и IPv6 приходится учитывать отдельно.

Где можно больно ошибиться

Разрешить адрес прокси по заголовку. Если приложение берёт X-Forwarded-For от любого клиента, злоумышленник просто напишет туда нужный IP. Сетевой firewall использует настоящий адрес соединения; веб-приложение должно доверять заголовкам только от своих reverse proxy.

Разрешить слишком широкую сеть. Подрядчик дал /24, потому что «у нас адреса могут меняться». Это уже 256 возможных источников, владельцев которых мы не контролируем.

Не вести владельца и срок. У каждой записи должны быть комментарий, ответственный, причина и дата пересмотра. trusted-temp-2 без истории через год никто не удалит.

Забыть про доступ в обход. Если HTTP закрыт по IP на reverse proxy, но backend доступен напрямую на другом адресе, вся конструкция бессмысленна.

Как сделать статические списки терпимыми

Я бы разделил их минимум на три класса: собственные площадки, системные интеграции и человеческие исключения. Первые два пересматриваются по инвентаризации. Третий класс лучше вообще не делать постоянным: выдавать запись с timeout или управлять ею через портал.

Следующий выпуск как раз про это — тот же allow-list, но с коротким сроком жизни, журналом и нормальным отзывом.

Ранее в цикле

Документация и источники

#network

Обложка

Один из самых недооценённых артефактов в домашней сети — DNS-кэш роутера. Туда попадает каждый домен, который кто-то в сети пытался открыть: телефоны, ноутбуки, умный дом, приставки. Роутер это всё запоминает, потому что кэширует ответы DNS-сервера.

И если в сети завелась малварь — она почти наверняка оставит след в этом кэше. Потому что у малвари есть одна привычка, которую сложно спрятать: она ходит на странные домены.

Почему малварь палится по DNS

Вредоносное ПО должно с кем-то общаться: получать команды (C2), вытаскивать данные, скачивать обновления. Вариантов немного:

  • Жёстко зашитый IP — быстро сгорает, блокируется, легко находится.
  • Доменное имя — удобно: можно менять IP за доменом, домен дешевле перевыпустить.

Но у доменов малвари есть характерные черты, которых нет у нормальных сайтов:

  1. Длинные бессмысленные поддомены. Классический приём — передавать данные прямо в DNS-запросе (DNS-туннелинг) или генерировать домены алгоритмом (DGA). Выглядит это как xkcd90210-4f8a2b... .example.com — каша из случайных символов, а не www.
  2. Высокая энтропия. Имя домена похоже на случайный набор base32/base64-символов, а не на читаемое слово.
  3. Свежие или «одноразовые» домены. Зарегистрированы неделю назад, живут месяц.
  4. Необычные TLD и зоны там, где им не место.
  5. Аномальная частота запросов — биение (beaconing) каждые N минут.

Легитимные сервисы тоже бывают «странными» (CDN, трекеры, телеметрия), поэтому один признак — не приговор. Но совокупность — уже повод копать.

Как посмотреть кэш на MikroTik

Всё, что нужно, — одна команда на роутере:

/ip dns cache all print

Или в одну строку (удобнее для обработки):

/ip dns cache all print terse

Флаги: S — статическая запись, N — негативная (имя не разрешилось), пусто — обычный кэш.

Для сравнения — на Linux/маке:

# macOS
sudo dscacheutil -cachedump -entries Host

# systemd-resolved
resolvectl statistics
resolvectl query example.com

Но у роутера есть преимущество: он видит DNS-запросы всей сети, а не одного устройства. Это глобальный наблюдатель, который уже стоит у тебя дома.

Что мы нашли в своём кэше

Проверили оба наших MikroTik (дача и дом). Это не теоретические примеры — вот реальные находки из /ip dns cache all print.

1. Домен-трекер, который долбится по три раза

q7x2m9k.ru
hit.q7x2m9k.ru

Короткое бессмысленное имя, похожее на случайный набор символов, и при этом три дубля в кэше — значит, кто-то в сети реально ходил на него несколько раз. Резолвится в 203.0.113.x — подсеть, где живёт куча рекламных/трекерных доменов (WAN-облако).

Рядом с ним в кэше — rmetrics-demo.ru, eventtrace-demo.ru — та же подсеть, те же «мусорные» трекеры, тоже по 3 записи. Это не малварь в классическом смысле, но это ровно тот паттерн, который мы ищем: бессмысленный домен + повторные обращения.

2. Punycode-домен

xn----8sbhbcdy1d.xn--80aswg

Это IDN-домен (кириллица в punycode). Сам по себе punycode — нормально (легитимные сайты так кодируются), но такие имена — излюбленный приём фишинга: визуально домен может выглядеть как известный сайт, а на деле быть одноразовым мусором. Резолвится в 203.0.113.148. Проверка по whois/VirusTotal обязательна.

3. UUID в имени домена

0f4a2c9e-77b3-4d81-8e2f-a1b6c3d4e5f6-netseer-ipaddr-assoc.xz.social-cdn.net

Выглядит как «случайная строка + домен» — классический вид DNS-туннелинга. Но тут важный урок: не всё странное — зло. Это netseer — легитимный механизм для привязки рекламы к IP (механизм социальных сетей). UUID в имени — это на самом деле идентификатор. Вывод: сначала гуглим домен, потом паникуем.

4. Негативные записи корпоративного домена (самое интересное)

На втором роутере нашли вот такое:

wpad.LAB.test
_kerberos._tcp.dc._msdcs.LAB.LAB.test
_ldap._tcp.VPN._sites.dc._msdcs.LAB.LAB.test
sw-sccm-01.LAB.LAB.test
pac.LAB.LAB.test

Это негативный кэш (флаг N) — кто-то в домашней сети искал корпоративную инфраструктуру Windows: WPAD (автообнаружение прокси), Kerberos, LDAP, SCCM-сервер. Классический сценарий: человек принёс с работы ноутбук, тот пытается найти «свой» домен-контроллер и долбится в DNS роутера.

Сам по себе безобидно (записи негативные, ничего не разрешилось), но это отличная иллюстрация: DNS-кэш роутера рассказал нам, что в сети был корпоративный Windows-хостов. Для офисного админа это уже разведданные: кто-то таскает рабочие ноутбуки домой.

Кто и зачем туда ломился (разбор наших находок)

Недостаточно увидеть странный домен — интересно понять, кто в сети к нему ходил. Мы докопались до владельцев и устройств. Вот что выяснилось.

Трекеры из российского облака. q7x2m9k.ru, hit.q7x2m9k.ru, rmetrics-demo.ru, eventtrace-demo.ru — все резолвятся в подсеть 203.0.113.x, а это российское облако. Это рекламно-аналитические трекеры: их дёргают SDK в бесплатных приложениях. Кто в сети на них ходит? Судя по DHCP-арендам — телефоны на Android и умный дом (шлюзы, робот-пылесос, ESP-устройства). Это не малварь, а мусорная телеметрия: приложение молча шлёт аналитику и тянет рекламу. Три дубля в кэше = обращались несколько раз.

Мёртвый трекер. whitei-demo.net резолвился в 203.0.113.70, но сейчас домен уже не резолвится — его отключили. В кэше он просто доживает свой TTL. Такой «хвост» в кэше — полезный сигнал: домен мог быть живым трекером/редиректом, который снесли.

Безобидный punycode. xn----8sbhbcdy1d.xn--80aswg выглядит зловеще, но это кириллический IDN-домен. Просто punycode-кодировка — нормальный механизм для кириллических имён, а не признак зла. Урок: сначала декодируй и гугли, потом паникуй.

Корпоративный ноутбук дома. На втором роутере нашлись негативные записи с поддоменами wpad, _kerberos, _ldap, _msdcs, sccm. Это корпоративная инфраструктура Windows (WPAD-автообнаружение прокси, Kerberos, LDAP, SCCM-сервер). Кто-то принёс рабочий ноутбук домой, и тот упорно ищет «свой» домен-контроллер в чужой сети. Записи негативные (ничего не нашёл), но сам факт засветился в кэше. Для офисного админа это уже разведданные: сотрудник таскает корпоративную технику домой.

Вывод по нашим находкам: реальной малвари нет — это рекламные SDK из приложений + корпоративный ноутбук. Но метод работает: кэш роутера рассказал и про телеметрию приложений, и про рабочий ноутбук в домашней сети. Именно поэтому «посмотреть в кэш» — это не магия, а дешёвая разведка, доступная каждому админу.

Как проверять подозрительные находки

Нашёл странный домен — не паникуй, а прогони по чек-листу:

# 1. Что это за IP
dig +short suspicious-domain.ru A

# 2. Кто владелец
whois suspicious-domain.ru

# 3. Есть ли этот домен в репутационных базах
#    VirusTotal, urlscan.io, ThreatFox — вбить имя руками

# 4. Как давно зарегистрирован (свежий домен = красный флаг)
whois suspicious-domain.ru | grep -i "created"

Легитимные «странности», которые часто пугают: – googlevideo.com / rr3---sn-... — YouTube CDN, длинные имена — норма – edgesuite.net, akamaized.net — Akamai CDN – *.push.apple.com — push-уведомления Apple – netseer...fbcdn.net — реклама Facebook – *.3gppnetwork.org — VoWiFi/VoLTE (операторская связь)

Красные флаги: – Домен зарегистрирован < 30 дней назад – Имя — случайная каша, и оно повторяется в кэше – Резолвится в подсеть, известную трекерами/спамом – Негативные записи _kerberos, _ldap, wpad, _msdcs — в доме кто-то ищет корпоративный домен

Что делать, если нашёл реальную малварь

  1. Найди, кто ходил. ip dhcp-server lease print → сопоставь по времени/устройству. Или ip firewall connection print в момент запроса.
  2. Заблокируй домен на роутере (DNS-уровень — проще всего): /ip dns static add name=bad-domain.ru type=NXDOMAIN
  3. Заблокируй IP (если домен уже разрешился): /ip firewall address-list add list=blocked address=1.2.3.4 /ip firewall filter add chain=forward src-address-list=blocked action=drop /ip firewall filter add chain=forward dst-address-list=blocked action=drop
  4. Почисти устройство. Это уже отдельная история — антивирус, проверка автозагрузки, смена паролей.
  5. Проверь сам роутер. MikroTik тоже ломают (ботнет из 13 000 MikroTik через DNS-мисконфиг SPF — свежий громкий случай 2025). Обнови RouterOS, смени пароли, выключи ненужные сервисы.

Бонус: как это автоматизировать

Раз в день снимать кэш и искать аномалии можно скриптом:

ssh admin@router "/ip dns cache all print" \
  | awk 'length($0) > 60 && $0 !~ /googlevideo|edgesuite|akamaized|apple|google|microsoft/ {print}'

А для полноценного анализа — считать энтропию имён (Shannon entropy): всё, что выше порога, отправлять на проверку. На роутере это не сделать, но можно забирать кэш по SSH на сервер и анализировать там (у нас это делает агент на сервере).

Вывод

DNS-кэш роутера — это бесплатный детектор аномалий, который уже стоит в каждой домашней сети. Одна команда /ip dns cache all print — и ты видишь, куда на самом деле ходили устройства. Не всё странное — малварь (CDN и трекеры тоже «странные»), но паттерн «бессмысленный домен + повторные запросы + свежая регистрация» — это уже повод копать.

А если найдёшь в кэше _kerberos._tcp.dc._msdcs — знай: кто-то притащил рабочий ноутбук домой. И теперь ты об этом знаешь.

#selfhosting #network #monitoring #openclaw #llm

Обложка

Вышел новый релиз connect-check v1.6.0 — сетевого диагностического инструмента.

Что нового

  • CRM в этапе банков/сервисов: Bitrix24 (+ авторизация / Паспорт), amoCRM / Kommo, Мегаплан, RetailCRM, МойСклад, Planfix, YCLIENTS, ELMA365, Creatio, Salesforce, HubSpot.
  • При ≥10 сбоях: кнопка «Письмо в НОК» в HTML-отчёте и GUI — mailto на info@noc.gov.ru (cc support@on-telecom.ru), тема/тело с внешним IP и сводкой; скачивается TXT сбоев; если почтовый клиент не открылся — инструкция в отчёте/логе.
  • Sidecar TXT сбоев рядом с HTML (*_problems.txt).

Скачать

Зеркало (если GitHub недоступен):

Оригинал: Релиз v1.6.0 на GitHub

#monitoring #network

Обложка

Есть вещи, которые в homelab долго живут «на всякий случай». Я когда-то стянул модель bge-m3 в ollama, не особо понимая зачем — просто прозвучало убедительно. А потом она всплыла сразу в двух местах: в семантическом поиске по заметкам и в долговременной памяти ассистента. Пришлось разобраться, что это вообще такое. Делюсь, чтобы не пришлось разбираться с нуля.

Что такое embedding-модель

Коротко: это модель, которая превращает текст в вектор — набор чисел фиксированной длины, описывающих смысл. Не формулировку, а именно смысл.

Зачем это нужно: обычный поиск (grep, полнотекст) ищет точные совпадения. Набрал «туннель к роутеру» — не найдёт запись, где написано «SSH-проброс до шлюза». А векторы ловят смысл: два текста про одно и то же окажутся рядом в векторном пространстве, даже если слова разные. Отсюда «поиск по смыслу», RAG, рекомендации и прочая магия.

Что такое bge-m3

bge-m3 — модель от BAAI (это китайская лаборатория, авторы флагманских bge-эмбеддингов). Название расшифровывается просто:

  • Multi-linguality — многоязычность: 100+ языков, русский в их числе, причём не «как-нибудь», а нормально;
  • Multi-functionality — три режима в одной модели: плотные векторы (dense), разреженные (sparse/лексический поиск) и мульти-векторное представление (в духе ColBERT);
  • Multi-granularity — работает и с короткими запросами, и с длинными документами (до 8192 токенов на вход).

Для практики важнее всего первое: многоязычность. Большинство популярных локальных эмбеддингов заточены под английский, а bge-m3 из коробки понимает русский.

Ключевые цифры:

  • 1024 измерения в dense-векторе;
  • ~1.2 ГБ весов — помещается в память любого современного GPU и даже в CPU-контейнер без боли;
  • бесплатно и локально — ничего не улетает в облако, нет лимитов и оплаты за запрос;
  • скорость на нашей AMD-видеокарте с ROCm — десятки миллисекунд на эмбеддинг, для поиска по своей базе хватает с запасом.

Почему bge-m3, а не альтернативы

Выбор на деле невелик, если хочется русский и локально:

  • nomic-embed-text — легче (270 МБ) и быстрее, но 768 измерений и заметно слабее на русском. Годится для английского, для смешанной базы — компромисс.
  • OpenAI text-embedding-3-small — хороша, но это облако: данные уходят наружу, плюс оплата за каждый запрос. Для приватной базы — не вариант.
  • multilingual-e5 / e5-large — многоязычные, но их русификация слабее, чем у bge-m3, а русскоязычных бенчмарков у них меньше.

bge-m3 выигрывает именно на связке «русский + локально + бесплатно». Не самая лёгкая, не самая быстрая — но самая сбалансированная для домашней базы знаний.

Установка

Если уже стоит ollama — всё сводится к одной команде:

ollama pull bge-m3

Модель подтягивается сама, вместе с весами. Дальше можно дёргать её двумя способами.

Нативный API ollama:

curl http://localhost:11434/api/embed -d '{
  "model": "bge-m3",
  "input": "настроить туннель к роутеру"
}'

Либо через OpenAI-совместимый эндпоинт /v1/embeddings — он удобен, потому что под него заточены почти все библиотеки и плагины (тот же агент с долговременной памятью подключается к нему как к обычному OpenAI).

Грабли

Одна, но неприятная: bge-m3 не поддерживает параметр dimensions. Это фиксированная модель на 1024 измерения, «урезать» её под другой размер нельзя (нет matryoshka-представления, как у OpenAI).

Если ваш код по привычке шлёт dimensions — ollama вернёт 400 «does not support matryoshka representation». Лечится просто: убрать параметр, а в интеграциях выставить sendDimensions: false.

Вторая, уже из жизни homelab: модель должна быть живой и доступной по сети. Если ollama в контейнере остановлен или до него нет маршрута из соседней подсети — всё, что на неё завязано, молча деградирует. Проверить легко: curl http://<host>:11434/api/tags должен вернуть список моделей.

Где это пригождается

У нас bge-m3 всплыла в двух местах:

  • семантический поиск по заметкам — вместо грепа по папке ищем по смыслу;
  • долговременная память ассистента — плагин превращает разговоры в векторы и подмешивает релевантные воспоминания в контекст.

Но это только два применения. Подойдёт любой сценарий «найти похожее»: поиск по логам и инцидентам, дедупликация, рекомендации, подбор похожих статей.

Наши замеры

Модель стоит на контейнере с ollama и AMD-видеокартой (ROCm). Замеряли на живой сессии, не в лаборатории:

  • Скорость поиска: первый recall после установки занял 334 мс — из них полнотекстовый поиск (FTS) 78 мс, векторный 258 мс;
  • Нагрузка на GPU: bge-m3 лежит целиком в VRAM — 0.66 ГБ;
  • Размер индекса: 55 чанков заметок индексируются за пару минут, сам поиск — миллисекунды;
  • Точность на наших вопросах: 0.62 / 0.55 / 0.51 — вопрос задаёшь своими словами, находится нужный чанк, даже если ни одно слово не совпало;
  • Память агента: в контекст подмешивается ~500–700 символов релевантных воспоминаний перед каждым ходом.

Для сравнения: сессия агента на 1.5 часа до установки памяти съедала 247 тысяч токенов и 25% окна контекста — поиск по смыслу заметно уменьшает потребность пересказывать контекст с нуля.

Итог

bge-m3 — это та модель, которую приятно держать локально: бесплатная, понимает русский, ставится одной командой и закрывает любые задачи на «поиск по смыслу». Единственное, что нужно помнить, — фиксированные 1024 измерения и живой ollama за спиной.

Если у вас уже крутится ollama — попробуйте, потерять нечего: одна команда и можно гонять эмбеддинги прямо сейчас.

Ссылки

— BGE-M3 на HuggingFace

— BGE-M3 в библиотеке Ollama

— Ollama

#selfhosting #openclaw #embeddings