javascript

Анализируем UDP логи Squid-proxy

  • воскресенье, 7 декабря 2025 г. в 00:00:07
https://habr.com/ru/articles/973934/

Вступление

Эта статья о том, как настроить Squid proxy с SSL bump в Docker и организовать realtime-мониторинг логов. В качестве практического примера я покажу open-source решение на современном стеке (Bun + Redis + Vue), которое решает проблему устаревших инструментов мониторинга.

Исходный код проекта: GitHub

Скриншоты приложения:
Access log screenshot
Access log screenshot
User data screenshot
User data screenshot
Metrics screenshot
Metrics screenshot

Что будет в статье:

  • Настройка Squid с SSL bump в Docker

  • UDP-приём логов (без ограничений syslog)

  • Realtime-анализ с использованием Redis RediSearch

  • Frontend на Vue для визуализации

Расскажите в комментариях, как вы организуете блок-листы и мониторинг прокси. Используете ли Squid или перешли на альтернативы? Какие инструменты применяете для анализа логов? Считаете ли squid архаичным?

Порядок:

├── Вступление
├── История
├── Легкая часть (для сис. админов)/
│---├── Установка squid docker + ssl_bump
│---└── Установка анализатора
└── Техническая часть (для nodejs/vue dev)/
-----├── Bun/Redis
-----├── Elysia
-----└── Frontend

История

Давным-давно, когда дети находили реликты прошлого в виде буквы Ш, а сайты верстали в таблицах, самыми тяжелыми элементами сайта были фотографии вашего питомца и красивые анимированные кнопочки во flash… Интернета было всем мало. Так мало, что его раздавали по талонам карточкам.

Священный роутер был отключен
Священный роутер был отключен

Тяжелые времена, требовали тяжелых решений. Все мечтали о безлимитном интернете, о настоящем безлимитище. Но я как засланец (я не перепутал букву) из будущего могу сказать, пока эти времена не наступили, хоть маркетологи и называют его безлимитным.

Людям требовалось ограничение, контроль, кэширование. Им нужен был squid cache. Времена изменились. Теперь squid поддерживается энтузиастами за шаурму, а большая часть ПО для него написана в мохнатые годы.

sqstat
sqstat

SqStat — это простое приложение для realtime-анализа логов cache manager. Squid отправлял данные по сокетам, и php сохранял данные в memcached, показывая простую таблицу хостов с метриками. Начиная со Squid v6.X.X cache manager перестал быть нужным, а логи, поступающие из него, были крайне скудны. Интернета стало больше, отпала необходимость в кэшировании на уровне прокси, большая часть трафика стала идти по https.

Большая часть приложений стала следить за log-файлами. И я думал, что это единственный способ, в интернете я часто натыкался на https://wiki.squid-cache.org/. Поэтому в первых версиях читал логи из папки. При помощи chokidar я следил за обновлениями файлов, и это было неэффективно.

Задача была простая: сделать ремейк старого анализатора для возможности мониторинга в новых версиях squid. Мне нужно было быстро сохранять и быстро доставать логи, для этого как-никак подходит redis.

Установка squid docker + ssl_bump

Для начала мы поднимем docker-версию squid с ssl_bump. Первое время я брал готовые access-логи и на базе них сделал скрипт-генератор. Это была ошибка.

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

  • b4tman/docker-squid — здесь на базе alpine, он простой и легкий. Не 7.X версия, но свежее и надёжнее нет.

  • Ajeris/squid-docker-source — здесь на базе debian-slim. Более комплексное решение с Kerberos, ACL-листами и т.д. Ближе к реальному проду.

Я воспользуюсь первым вариантом, т.к. его проще тестировать.

docker-compose.yml
  squid:
    user: "3128:3128"
    image: ghcr.io/b4tman/squid-ssl-bump:latest
    container_name: squid-proxy
    restart: unless-stopped
    ports:
	  - "3129:3129"
    volumes:
      - ./docker/ssl:/etc/squid/ssl/
      - ./docker/squid.conf:/etc/squid/conf.d/squid.conf:ro
      - ./docker/passwd:/etc/squid/passwords:ro # необязательно. Для тестов
      - ./logs:/var/log/squid
    environment:
      - TZ=Asia/Almaty
      - SQUID_CONFIG_FILE=/etc/squid/conf.d/squid.conf
	extra_hosts:  
	  - 'host.docker.internal:host-gateway' # это строчка для локальных экспериментов. Если у вас настроен macvlan, вам это не нужно
    networks:
      - squid-net

Нам требуется конфиг squid.conf (во втором репозитории есть примеры конфигов). Для получения детальных логов, а не просто одного лога с доменом, нам необходим ssl_bump. Для его работы необходимо сгенерировать корневые сертификаты.

Источник: https://github.com/Ajeris/squid-docker-source/blob/main/setup_project.sh (я только поменял пути и права на key)

ssl_bump_certs.sh
set -e  
  
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"  
SSL_DIR="${SSL_DIR:-${SCRIPT_DIR}/docker/ssl}"  
CA_KEY="${CA_KEY:-$SSL_DIR/squid.key}"  
CA_CRT="${CA_CRT:-$SSL_DIR/squid.crt}"  
  
CA_SUBJECT="${CA_SUBJECT:-/CN=proxy.tp.oil/O=Squid/C=KZ}"  
KEY_SIZE="${KEY_SIZE:-4096}"
CERT_DAYS="${CERT_DAYS:-3650}"  
  
RED='\033[0;31m'  
GREEN='\033[0;32m'  
YELLOW='\033[1;33m'  
BLUE='\033[0;34m'  
NC='\033[0m' # No Color  
  
print_info() {  
    echo -e "${BLUE}[INFO]${NC} $1"  
}  
  
print_success() {  
    echo -e "${GREEN}[SUCCESS]${NC} $1"  
}  
  
print_warning() {  
    echo -e "${YELLOW}[WARNING]${NC} $1"  
}  
  
print_error() {  
    echo -e "${RED}[ERROR]${NC} $1"  
}  
  
generate_ssl_cert() {  
    print_info "Checking SSL certificate setup..."  
  
    if [ ! -d "$SSL_DIR" ]; then  
      mkdir -p "$SSL_DIR"  
      print_info "Created: $SSL_DIR"  
    fi  
  
    # Set permissions for SSL certificate directory  
    if [ -d "$SSL_DIR" ]; then  
        chmod 755 "$SSL_DIR"  
        chmod 644 "$SSL_DIR"/* 2>/dev/null || true  
        print_success "SSL certificate directory permissions set"  
    fi  
  
    # Check if OpenSSL is available  
    if ! command -v openssl &> /dev/null; then  
        print_error "OpenSSL is not installed or not in PATH"  
        exit 1  
    fi  
  
    if [ ! -f "$CA_KEY" ] || [ ! -f "$CA_CRT" ]; then  
        print_info "Generating new SSL certificate..."  
        print_info "  Key size: $KEY_SIZE bits"
        print_info "  Validity: $CERT_DAYS days"
        print_info "  Subject: $CA_SUBJECT"
  
        # Generate private key  
        openssl genrsa -out "$CA_KEY" "$KEY_SIZE"  
  
        # Generate self-signed certificate  
        openssl req -new -x509 -days "$CERT_DAYS" \  
            -extensions v3_ca \  
            -key "$CA_KEY" \  
            -out "$CA_CRT" \  
            -subj "$CA_SUBJECT"  
  
        # Set correct permissions  
        chmod 644 "$CA_KEY"  
        chmod 644 "$CA_CRT"  
  
        print_success "SSL certificate generated:"  
        print_info "    Private key: $CA_KEY"  
        print_info "    Certificate: $CA_CRT"  
  
        # Show certificate info  
        echo ""  
        print_info "Certificate details:"  
        openssl x509 -in "$CA_CRT" -text -noout | grep -E "(Subject|Issuer|Not Before|Not After)" | sed 's/^/  /'  
  
        echo ""  
        print_warning "IMPORTANT: Install $CA_CRT as trusted CA in browsers to avoid SSL warnings"  
    else  
        print_success "SSL certificate already exists:"  
        print_info "  Private key: $CA_KEY"  
        print_info "  Certificate: $CA_CRT"  
  
        # Check expiration  
        if openssl x509 -checkend 86400 -noout -in "$CA_CRT" >/dev/null; then  
            print_success "Certificate is valid"  
        else  
            print_warning "Certificate is expiring soon or expired!"  
        fi  
    fi  
    # Show certificate fingerprint  
    echo ""  
    print_info "Certificate fingerprint (SHA256):"  
    openssl x509 -noout -fingerprint -sha256 -in "$CA_CRT" | sed 's/.*=/  /'  
}  
  
main() {  
  echo "Creating ssl certs..."  
  generate_ssl_cert  
  print_success "Certs created successfully!"  
}  
  
main "$@"

Обязательно читаем скрипты перед их выполнением. Если вы не понимаете содержимого скрипта, попросите чат агента. В данном скрипте необходимо задать стартовые переменные. В случае ошибки, проверьте права файлов и размер шифрования. Для старых версий squid используется комбинированный pem формат.

Включаем бампинг в squid.conf. В моём варианте всё упрощено, готовый конфиг с bump

http_port 3129 ssl-bump generate-host-certificates=on dynamic_cert_mem_cache_size=4MB cert=/etc/squid/ssl/squid.crt key=/etc/squid/ssl/squid.key  
  
sslcrtd_program /usr/lib/squid/security_file_certgen -s /var/cache/squid/ssl_db -M 4MB  
sslcrtd_children 8 startup=1 idle=1  
  
ssl_bump server-first all  
ssl_bump bump all

Установка анализатора логов

Для мониторинга логов в реальном времени разработал open-source решение на Elysia + Vue.

Технический стек:

  • Backend: Bun + Elysia + Redis (с модулем RediSearch)

  • Frontend: Vue 3 + NaiveUI + UnoCSS

  • Deploy: Docker Compose

Возможности:

  • UDP-приём логов

  • Realtime-анализ последнего часа работы

  • Поддержка нескольких Squid-инстансов одновременно

  • Алиасы пользователей

Настройка в squid.conf:

access_log udp://analyzer_host:5140 squid

Исходники и документация: GitHub

Альтернативы: Если у вас уже развёрнут ELK/GrayLog stack, можете использовать их. Но потребуется написать grok-паттерны для парсинга, настроить aggregations для Squid-специфичных метрик (hit/miss ratio, hierarchy codes) и создать дашборды. Готовое решение экономит 2-3 дня работы.

Совет: Если используете Squid версии ниже 6.x, рекомендую обновиться. Разница между версиями 5 и 6 существенна, особенно в части логирования и производительности.

Техническая часть (для nodejs/vue dev)

Если до этого был мануал, здесь больше мысли/записки разработчика. Я буду часто уходить в сторону

Bun/Redis

В squid.conf указываем адрес UDP-сервера для отправки логов:

access_log udp://analyzer_host:5140 squid

Сервер выступает в роли слушателя и может принимать логи с нескольких Squid-инстансов одновременно. В продакшене у нас работают два прокси-сервера, отправляющих логи на один анализатор.

Для приёма UDP-пакетов использую bun.udpSocket. Кстати, эта функциональность появилась в Bun относительно недавно — видимо, после того как адвокат девелопер из redis указал на её отсутствие.

Поначалу создал лишь базовый вариант логов, но потом мне показалось, что этого мало. На сайте squid нашёл форматы логов. Поэтому я попросил crush сгенерировать большой json.

{
    token: "%ts",
    field: "timestamp",
    redisType: "NUMERIC",
    postgresType: "BIGINT",
    transform: "timestampToMs",
 }
...

87 различных типов полей для логов. Конечно нет смысла прописывать в custom_logs все токены. Но я решил задать возможность на будущее использовать кастомные логи.

Стандартный формат логов:

%ts.%03tu %6tr %>a %Ss/%03>Hs %<st %rm %ru %[un %Sh/%<a %mt

К примеру, в дефолтных нет отображения User-Agent. Читать каждый раз огромный массив нет смысла, берём самое важное и сохраняем в кэш.

redis access log screenshot
redis access log screenshot

Токены squid могут содержать количество символов, мы это удаляем. Забавно, но redis не любит точки. Redis делит строку на токены по точкам, и поиск по ip перестаёт работать. Пришлось заменить точки на _

Несмотря на включенный SSL_Bump, нельзя быть на 100% уверенным в чистоте логов. Squid иногда подкидывает подлянку. Какие-то поля логов отдаются как -, где-то в serverIP попадает ::. Это приводило к ошибкам при попытке сложных агрегаций. Некоторые поля считаются числом, но всё равно могут быть -

Squid не всегда определяет MIME-типы, даже когда url оканчивается на .js. Конечно, я не могу со 100% уверенностью сказать, что файл с расширением .js — это действительно javascript. Однако решил подправить.

Планировалось собирать пачку логов за один час и отправлять потом в postgres. Поначалу не работало TLS-соединение (rediss), но там внесли строчки CIPHERS, заработало. Конечно, в Bun мы можем воспользоваться ORM, но оно было сделано для nodejs, поэтому решил оставить стандартный клиент для повышения скорости.

Несмотря на скорость разработчиков по интеграции nodejs в bun-среду, многое им приходится пропускать. В моём случае я использовал redis не просто для банального кэширования, а как полноценную БД. Drizzle ORM предназначен для реляционных БД, у него есть интеграция с Upstash redis, а с обычным нет.

Мне требовался поиск, агрегация. Собственно говоря, модуль RediSearch. Я бы мог написать провайдер для какой-нибудь популярной ORM. Но тогда бы я застрял на четверть века со своим приложением.

И не иди путём искушения. Дабы посмеешь написать сей труд, как официальная поддержка сразу появится

Для работы в Bun с модулями redis пришлось использовать

function send(command: string, args: string[]) {}

В redis есть возможность работы с JSON, однако процесс сериализации/десериализации может быть трудозатратным. А красивая работа с объектами добавлена недавно в hset. Ранее мне приходилось вручную переводить переменные в camelCase. Хочу напомнить некоторым: JavaScript и JSON используют camelCase. Исключения только для констант и классов. По возможности выполняйте преобразования имён переменных при работе с БД.

Мои попытки контролировать процесс транзакций приводили к ERROR MULTI calls not be nested. Я оставил попытки оптимизировать процессы в redis.

const [firstResult, lastResult] = await Promise.all([
    redisClient.send("FT.SEARCH", firstArgs),
    redisClient.send("FT.SEARCH", lastArgs),
]);

Оно само работает. Оно само оптимизирует pipeline. Не лезь оно тебя сожрёт. Возможно когда-нибудь я разберусь как правильно работать с транзакциями в redis, но не сегодня.

Elysia

Сперва я создаю возможность валидации и парсинга env-переменных. По возможности желательно избегать в проектах использования env напрямую через process.env или Bun.env. Так как у меня Elysia, проще воспользоваться typebox. К сожалению, встроенный валидатор в typebox не очень удобен, поэтому добавляем ajv. Почему не Elysia Env? Я использую env часто за пределами elysia, автор не догадался вынести функцию парсинга и поделить плагин на инициализацию, которая возвращает распарсенные переменные, и обёртку плагина для elysia.

Возможно, конфигуратор в стиле Bati решил бы проблему. Выбирать пакеты через CLI крайне неудобно из-за кросс-пакетной совместимости.

Отдельно стоит упомянуть авто-документацию валидатора ответов сервера — это удобно. Тебе не нужно писать отдельные аннотации для swagger. Swagger здесь, кстати, багованный, но современный. Scalar UI выглядит красиво и может публиковать документацию у вас в профиле, делая её общедоступной.

Я не буду писать про удобный синтаксис в стиле fastify и как писать роуты, этого полно в интернете. А вот что реально важно — это выбор плагинов. Я советую использовать только официальные, т.к. они лучше всего дружат со всей экосистемой. Пакеты сообщества, как правило, работают не супер.

Также советую сразу компилировать ваше приложение в бинарный формат. Пример:

bun build src/index.ts --compile --target=bun-linux-x64-baseline --outfile=bunsqstat-backend-binary

В своём варианте использую baseline для совместимости с kvm (классический работает только при наличии AVX2). В вашем варианте хватит обычного bun-linux-x64
Работает только на x64, но при таком методе компиляции вы можете использовать кластерный режим для продакшена. Bun умеет создавать tcp с одним и тем же портом.
Компиляция в бинарник упрощает деплой приложения, вам не нужен будет bun на продакшене.

Также в своём репозитории я использую turborepo с серверным кэшем, rolldown-vite для ускорения компиляции фронта. Rolldown уже корректно работает с github CI.

Моё приложение представляет собой стек из

  • Caddy для публикации Vue SPA

  • Elysia server

  • redis-stack Redis (RediSearch module)

В теории я думал убрать из этой схемы Caddy и добавить на Elysia, но в начале с Caddy было проще, поэтому я оставил. Хотя в таком случае устанавливать было бы заметно проще, можно было бы избавиться от supervisor. Я даже думал попробовать Tauri, но боялся застрять надолго без знаний Rust.

К сожалению, eden является самой странной частью Elysia.

Eden - это плагин для elysia, позволяет использовать на фронтенде клиент с типизированными запросами. (по сути sdk без танцов с бубном)

Я по прежнему не понимаю почему из раза в раз некорректно импортировались типы. Иногда типы появлялись в процессе разработки, а порой исчезали после добавления/удаления плагинов. Поэтому я часто пользуюсь более простым вариантом, сгенирировать типы для клиента из swagger документации. Есть конечно более схожий с Eden вариант, ts-rest он создаёт общий контракт между бэком и фронтом. Кстати дело не в webstorm, проверял на vscode/helix.

Elysia no types
Elysia no types

Frontend

Здесь на самом деле всё банально. NaiveUI в качестве кита, VueUse, UnoCSS. Кстати, по поводу UnoCSS меня позабавил факт, что разработчик ушёл делать NuxtUI на tailwind, а свой атомарный движок забросил, и теперь фанаты пилят UnoUI. Будет очередной новый кит с отсутствием кастомизации, и людям всё равно придётся делать свой kit на RekaUI или shadcdn. Зато добавили крутой тренд, теперь киты стали бесплатно публиковать дизайны в фигме.

Сам по себе фронт получился так себе. Хотя в целом я писал приложение весьма в расслабленном состоянии, получая порцию декаданса. Как вам такое: написать shared worker и принимать в нём веб-сокеты. Звучит весьма круто, ведь теперь при открытии в нескольких вкладках мы создаём одного клиента на сервере, а потом мы слушаем порт shared worker... и отправляем запрос на сервер для получения данных. (звук стрельбы дробовиком в ногу)

Самое первое — это таблица access data. Самое странное — нужно объяснить юзерам формат работы с redis. Если бы у нас был elastic или формат данных в json, мы бы могли позволить поиск по всем значениям таблицы. Но мы работаем с redis, дабы увеличить скорость, мы специально даём пользователю выбрать поле для поиска. Косяк моей архитектуры — я решил экранировать данные на фронте, и это плохой подход. Нет, меня не волнует безопасность, т.к. приложение для ограниченного круга и не будет публиковаться наружу. А то, что у пользователя будут возникать ошибки от redis.

Ещё я хорошо продумал такую вещь, как алиасы. В некоторых анализаторах есть возможность привязать алиас к IP клиента. Дело в том, что в squid есть авторизация, но пользоваться внутри сети ею не будут. Чтобы красиво помечать сотрудников, например, vitalik@osu.oil (после собаки — это название отдела) ну или просто по-дружески Витя_с_ОСУ.

Так вот, мы получаем из редиски имена пользователей через пробел user 192.168.1.1 user2 192.168.1.2, теперь создаём radix-дерево по ip. Зачем? Потому что Блиц — это скорость без границ. И по барабану, что в нашем кейсе пользователи внутри сети, а значит, два октета будут одинаковые, и в лучшем случае пользователи поделятся на две группы в 3 октете.

И это не всё, я решил не нагружать бэк, и вместо поиска алиаса на бэке я делаю его на фронте. Во вкладке пользователи у меня нет пагинации, и используется fuzzy search. В целом там всё норм. Но в остальных местах у меня пагинация реализована на бэке, соответственно, поиск по пользователю работать в первой таблице уже не будет. В теории я должен на бэке выполнить поиск по алиасу, если нахожу — сделать запрос по полю clientIP, хотя на фронте делаю запрос по имени пользователя.

Хотел сделать себе проще, а получилось как всегда. За лень приходится платить дважды.

ЧаВо:
  • Можно ли использовать GrayLog?

  • Да. Однако GrayLog будет лишь собирать данные. Вам все равно потребуется обрабатывать и выводить их в Grafana. Это не готовое решение.

  • Где и когда применим BunSqStat?

  • При первичной отладке Squid или для выявления аномалий в ближайшем временном промежутке. К примеру, зафиксировать блокировку важного ресурса.

  • Это замена SquidAnalyzer?

  • Нет. SquidAnalyzer читает логи по cron (раз в указанный промежуток), а не в реальном времени, однако способен обрабатывать большее количество логов. BunSqStat рассчитан на час работы, в дальнейшем планируется интегрировать postgres для сбора больших отчетов.

  • Почему 48 мб по умолчанию для Redis?

  • Это приблизительное значение, зависит от количества запросов. Количество логов можно регулировать изменением максимальной памяти Redis в настройках.

  • Логи бегут слишком быстро.

  • Можно регулировать интервал выпадения пачек логов в настройках. (только на фронтенде)

  • Что означает «продолжительность»?

  • Длительность запроса. Карточка в метриках отображает суммарную длительность всех запросов.

  • Я могу установить без Docker?

  • Да. На гитхабе можно скачать актуальный релиз в zip формате. В документации описан способ установки. В данный момент нет bash скрипта для установки всего стека. Требуются навыки деплоя веб-приложений. Вариант с Docker проще.

Вывод:

У проекта есть недостатки, большинство обнаруживаешь в конце (классика), но главное — получать удовольствие в процессе разработки. Даже несмотря на простоту задачи, я умудрился вставлять палки в колёса. Это был первый проект, где серьёзно использовал чат-агентов через warp и crush. И это добавило больше сложностей и рефакторинга. Схема -> Ядро -> Модули. В итоге получилось MVP, а не полноценное приложение.