Haproxy: ограничения для подсети — Fastenv

Fastenv - системное администрирование от профессионалов индустрии. Работаем с linux, уважаем opensource.

Haproxy: ограничения для подсети
Сложность: Высокая | Автор: fastenv | Ноябрь 13, 2016

Защищая сервер иной раз возникает желание — сделать ограничение для ip-сети заданного размера. Покажем, как это делается на haproxy.

Задача №1. Разрешить не более 5 подключений с /22 сети к почтовому серверу в минуту

Мы полагаемся на то, что haproxy находится на шлюзе, а почтовый сервер имеет внутренний адрес 10.0.0.2 и слушает 25, 143 порты.

Решение:

frontend  mail
    bind ipv4@:25,ipv4@:143
    mode tcp
 
    stick-table type ip size 1k expire 1h store gpc0_rate(1m)
 
    tcp-request inspect-delay 20
    tcp-request content set-var(req.subnet) src,ipmask(22)
    tcp-request content track-sc0 var(req.subnet)
    tcp-request content sc-inc-gpc0(0)
 
    acl abuse1 sc0_gpc0_rate gt 5
    tcp-request content reject if abuse1
 
    default_backend mail
 
backend mail
    mode tcp
 
    server mail1 10.0.0.2 check port 25

Ключевая идея: в stick-table храним не адрес клиента, а IPv4 обработанный ipmask конвертером.
Разберем нетривиальные моменты:

  1. tcp-request inspect-delay 20

    Просим haproxy изучать входящее соединение не более 20 ms. Управление перейдет дальше или по истечению этого времени, или раньше, если будет получен весь запрос.

  2. tcp-request content set-var(req.subnet) src,ipmask(22)

    Создаем служебную переменную subnet видимую только на этапе запроса(префикс req.). Её значением будет адрес клиента(src) приведенный к 22 маске.
    Подробнее о префиксах.

  3. tcp-request content track-sc0 var(req.subnet)

    Haproxy имеет три(0, 1, 2) слота для отслеживания пользовательских параметров. Данной командой мы инструктируем следить за переменной req.subnet. Ньюанс в том, что обычно все счетчики обновляются после выполнения запроса, а команда track-sc* переводит заданные из них в режим реального времени.

  4. tcp-request content sc-inc-gpc0(0)

    Инкрементируем значение счетчика(gpc0) ассоциированого с переменной нулевого трекинг-слота.

Обзор решения

  1. Оно только для IPv4, можно ли универсально сделать и для IPv6? Можно, но лучше создать отдельный frontend с stick-table type ipv6. Если же очень хочется, то можно поменять на type string len 40. Тогда заработает для обоих протоколов.
  2. В сравнении с обычным DNAT имеется существенный недостаток — почтовый сервер видит адрес прокси, а не клиента. Можно ли это исправить? Да. Об этом transparent режим и наш следующий пример.

Задача №2. Пробросить ip-клиента в tcp-режиме

Продолжаем с нашим почтовым сервером. Брутфорс нас уже не беспокоит, но понимать что это за люди — хочется. А в логах сплошные адреса haproxy — надо исправлять.

Решение

Обзор

Данная техника чуть менее, чем полностью опирается на tproxy режим работы ядра. И концептуально сводится к следующему. Обращаясь к mail-серверу мы в src пакета помещем ip-клиента(как бы делая вид, что нет тут никакого прокси). Затем наша цель перехватить идущий обратно пакет(помним, что мы — роутер) и объяснить ядру, что его следует завернуть в haproxy. Теперь уже отправляя ответ клиенту мы подставим свой внешний айпишник, вместо внутреннего.

Подготовка системы

  1. Создаем вспомогательную цепочку в iptables:
    iptables -t mangle -N DIVERT

    Скрытого смысла особо нет — цепочка для удобства, в ней мы будем помечать нужные нам пакеты.

  2. Создаем хитрые iptables-правила:
    iptables -t mangle -A PREROUTING -p tcp -m socket --dport 25 -j DIVERT
    iptables -t mangle -A PREROUTING -p tcp -m socket --dport 143 -j DIVERT
    iptables -t mangle -A PREROUTING -p tcp -m socket -s 10.0.0.2 -j DIVERT

    Вся тонкость в том, как работает этот match-модуль. Он срабатывает в двух ситуациях:

    1. Есть установленное соединение связанное с текущим пакетом.
    2. Есть какой-то локальный процесс слушающий порт назначения.

    Тем самым из третьего правила управление переходит в цепочку DIVERT, только для почтовго трафика(по п.1).

  3. Помечаем пакет:
    iptables -t mangle -A DIVERT -j MARK --set-mark 1

    Напомним, что в самом пакете никаких меток нет. Они видны только в пределах ядра.

  4. Создаем правило и табличку маршрутизации для пакетов с меткой:
    ip rule add fwmark 1 lookup 100

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

  5. Осталось пояснить ядру, что пакеты с этой меткой будет обрабатывать какой-то локальный процесс:
    ip route add local 0.0.0.0/0 dev lo table 100

    Прошлым шагом у нас автоматически создалась табличка с номером 100. Теперь мы в неё добавили правило с ключевым словом local — что цель находится на этом хосте.

Перевод haproxy в transparent режим

frontend  mail
    bind ipv4@:25,ipv4@:143 transparent
    mode tcp
 
    stick-table type ip size 1k expire 1h store gpc0_rate(1m)
 
    tcp-request inspect-delay 20
    tcp-request content set-var(req.subnet) src,ipmask(22)
    tcp-request content track-sc0 var(req.subnet)
    tcp-request content sc-inc-gpc0(0)
 
    acl abuse1 sc0_gpc0_rate gt 5
    tcp-request content reject if abuse1
 
    default_backend mail
 
backend mail
    mode tcp
    source 0.0.0.0 usesrc clientip
 
    server mail1 10.0.0.2 check port 25

Основные моменты:

  1. Ключевое слово transparent в bind.
  2. Инструктируем haproxy использовать ip-клиента в качестве src подключаясь к mail-серверу:
    source 0.0.0.0 usesrc clientip

    Первые 4 нуля не несут какого-либо смысла в transparent режиме. Тут влияет только clientip, который напротив работает только в нём.