воскресенье, 1 декабря 2019 г.

Скрипты управления списком IP-адресов в iptables/ipset и ipfw/table

Года 4 назад на работе перевёл всех Zabbix-агентов в активный режим, т.к. этот режим должен быть эффективнее чем опрос обычных пассивных Zabbix-агентов. Для снятия данных с обычных Zabbix-агентов сервер Zabbix сам устанавливает подключение к Zabbix-агенту, запрашивает у него необходимые метрики, после чего отключается. Для этого сервер Zabbix используют процессы poller, каждый из которых бывает занят не только во время активных действий, но и во время ожидания данных от Zabbix-агента. Если же Zabbix-агент работает в активном режиме, то сервер Zabbix не предпринимает никаких активных действий, а ждёт действий со стороны агента. Активный Zabbix-агент подключается к серверу Zabbix, запрашивает у него список метрик, за которыми нужно наблюдать, и периодичность их контроля. После этого Zabbix-агент самостоятельно собирает данные с необходимой периодичностью и отправляет их на сервер Zabbix. В этом случае сервер Zabbix использует процессы trapper, которые работают только во время приёма уже готовых данных. На самом деле на фоне общей нагрузки снижение использования ресурсов оказалось совсем незаметным, но речь сейчас не об этом.

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

Чтобы автоматизировать процесс добавления IP-адресов в сетевой фильтр на сервере Zabbix, а также максимально снизить возможность отправки поддельных данных с любого свободного IP-адреса, решил написать скрипт, который будет извлекать из базы данных Zabbix список IP-адресов интерфейсов из тех сетевых узлов, на которых есть элементы данных, имеющие тип "Zabbix-агент (активный)".

Для Linux с его iptables и ipset получился такой скрипт под названием ipset_auto.sh, который можно поместить в планировщик задач cron:
#!/bin/sh

AWK="/usr/bin/awk"
SORT="/usr/bin/sort"
UNIQ="/usr/bin/uniq"
IPSET="/sbin/ipset"
XARGS="/usr/bin/xargs"

update()
{
  SET="$1"
  NEED_IPS="$2"

  CURRENT_IPS=`$IPSET list $SET | $AWK '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/ { print $0; }'`

  DIFF_IPS=`(echo "$NEED_IPS" ; echo -n "$CURRENT_IPS") | $SORT | $UNIQ -u`
  ADD_IPS=`(echo "$NEED_IPS" ; echo -n "$DIFF_IPS") | $SORT | $UNIQ -d`
  DEL_IPS=`(echo "$CURRENT_IPS" ; echo -n "$DIFF_IPS") | $SORT | $UNIQ -d`

  if [ -n "$ADD_IPS" ]
  then
    echo "--- $SET add ---"
    echo "$ADD_IPS"
    echo "$ADD_IPS" | $XARGS -n1 $IPSET add $SET
  fi

  if [ -n "$DEL_IPS" ]
  then
    echo "--- $SET del ---"
    echo "$DEL_IPS"
    echo "$DEL_IPS" | $XARGS -n1 $IPSET del $SET
  fi
}

# ZABBIX

MYSQL=`$AWK '/^DBUser=/ { split($0, a, "=");
                          user = a[2]; }

             /^DBPassword=/ { split($0, a, "=");
                              password = a[2]; }

             /^DBName=/ { split($0, a, "=");
                          db = a[2]; }

             /^DBHost=/ { split($0, a, "=");
                          host = a[2]; }

             END { if (user && password && host && db) 
                     print "/usr/bin/mysql --connect-timeout=5 -u" user " -p" password " -h" host " " db;
                   else if (user && password && db)
                     print "/usr/bin/mysql --connect-timeout=5 -u" user " -p" password " " db; }' /etc/zabbix/zabbix_server.conf`

if [ -z "$MYSQL" ]
then
  echo "MYSQL not defined"
  exit
fi

NEED_IPS=`$MYSQL -N <<END 2>/dev/null
SELECT DISTINCT interface.ip
FROM items
JOIN hosts ON hosts.hostid = items.hostid
  AND hosts.status = 0
  AND hosts.proxy_hostid IS NULL
JOIN interface ON interface.hostid = items.hostid
  AND interface.type = 1
  AND interface.ip <> '127.0.0.1'
WHERE items.type = 7
  AND items.status = 0;
END
`
ERROR=$?
if [ $ERROR -ne 0 ]
then
  echo "Failed to execute SQL-query"
  exit
fi

update "zabbix_auto" "$NEED_IPS"
Для подключения к базе данных (в данном случае это MySQL, но переделка под другие СУБД тривиальна) скрипт использует настройки из файла конфигурации /etc/zabbix/zabbix_server.conf. Список требуемых IP-адресов в переменной NEED_IPS формируется SQL-запросом, который можно переработать под свои нужды. Например, у меня в скрипте есть ещё пара SQL-запросов, управляющих списками IP-адресов в множествах tftp_auto и ciu_auto. В последней строке скрипта функция update обновляет множество zabbix_auto так, чтобы в нём были только IP-адреса из переменной NEED_IPS.

Для создания множества IP-адресов zabbix_auto в ipset можно воспользоваться командой:
# ipset create zabbix_auto hash:ip
Для создания правила в iptables, которое разрешит всем IP-адресам из множества zabbix_auto взаимодействовать с сервером Zabbix, можно воспользоваться командой:
# iptables -A INPUT -p tcp -m set --match-set zabbix_auto src -m tcp --dport 10051 -j ACCEPT
Аналогичный скрипт для ipfw/table называется ipfw_auto.sh и выглядит следующим образом:
#!/bin/sh

AWK="/usr/bin/awk"
SED="/usr/bin/sed"
SORT="/usr/bin/sort"
UNIQ="/usr/bin/uniq"
XARGS="/usr/bin/xargs"

update()
{
  TABLE="$1"
  NEED_IPS="$2"

  IPFW=`$AWK -v TABLE="$TABLE" '{ split($0, a, "=");
                                  if (a[1] == TABLE)
                                  {
                                    table = a[2];
                                    print "/sbin/ipfw table " a[2];
                                  }
                                }' /etc/firewall.conf`

  if [ -z "$IPFW" ]
  then
    echo "IPFW not defined"
    exit
  fi

  CURRENT_IPS=`$IPFW list | $SED -e 's/\/32 0$//'`

  DIFF_IPS=`(echo "$NEED_IPS" ; echo -n "$CURRENT_IPS") | $SORT | $UNIQ -u`
  ADD_IPS=`(echo "$NEED_IPS" ; echo -n "$DIFF_IPS") | $SORT | $UNIQ -d`
  DEL_IPS=`(echo "$CURRENT_IPS" ; echo -n "$DIFF_IPS") | $SORT | $UNIQ -d`

  if [ -n "$ADD_IPS" ]
  then
    echo "--- $TABLE add ---"
    echo "$ADD_IPS"
    echo "$ADD_IPS" | $XARGS -n1 $IPFW add
  fi

  if [ -n "$DEL_IPS" ]
  then
    echo "--- $TABLE del ---"
    echo "$DEL_IPS"
    echo "$DEL_IPS" | $XARGS -n1 $IPFW delete
  fi
}

MYSQL=`$AWK '/^DBUser=/ { split($0, a, "=");
                          user = a[2]; }

             /^DBPassword=/ { split($0, a, "=");
                              password = a[2]; }

             /^DBName=/ { split($0, a, "=");
                          db = a[2]; }

             /^DBHost=/ { split($0, a, "=");
                          host = a[2]; }

             END { if (user && password && host && db) 
                     print "/usr/local/bin/mysql --connect-timeout=5 -u" user " -p" password " -h" host " " db;
                   else if (user && password && db)
                     print "/usr/local/bin/mysql --connect-timeout=5 -u" user " -p" password " " db; }' /usr/local/etc/zabbix34/zabbix_server.conf`

if [ -z "$MYSQL" ]
then
  echo "MYSQL not defined"
  exit
fi

# ZABBIX

NEED_IPS=`$MYSQL -N <<END 2>/dev/null
SELECT DISTINCT interface.ip
FROM items
JOIN hosts ON hosts.hostid = items.hostid
  AND hosts.status = 0
  AND hosts.proxy_hostid IS NULL
JOIN interface ON interface.hostid = items.hostid
  AND interface.type = 1
  AND interface.ip <> '127.0.0.1'
WHERE items.type = 7
  AND items.status = 0;
END
`
ERROR=$?
if [ $ERROR -ne 0 ]
then
  echo "Failed to execute SQL-query"
  exit
fi

update "table_zabbix_auto" "$NEED_IPS"
Особенность этого скрипта заключается в том, что в ipfw таблицы не имеют имён, а нумеруются. Номер таблицы выясняется через файл /etc/firewall.conf, в котором переменной с именем таблицы присваивается соответствующий номер. Например, для таблицы table_ssh номер задаётся следующим образом:
table_ssh=100

Подробнее о настройке ipfw/table можно прочитать в одной из моих прошлых заметок: Настройка ipfw во FreeBSD.

Активные Zabbix-агенты и база данных Zabbix приведены для примера, а вообще эти скрипты можно приспособить для любых других целей. Можно скачивать список IP-адресов с веб-страницы (главное, чтобы её не подменили и чтобы она не оказалась внезапно пустой), можно воспользоваться в каком-нибудь самодельном биллинге для открытия доступа пользователям, прошедшим авторизацию и закрытия доступа пользователям, превысившим лимит. Можно сочетать одно с другим.

FreeBSD на работе постепенно заменяем на Debian, поэтому скрипт ipfw_auto.sh скоро станет мне не нужным. Что касается Debian, то netfilter/iptables в Debian Buster уже заменён на nftables/nft. Пока что утилита iptables никуда не делась и умеет работать с nftables, но в будущем скрипт ipset_auto.sh тоже утратит актуальность и потребует переработки. Оба скрипта, однако, пока что могут пригодиться кому-нибудь ещё, поэтому решил поделиться ими.

Комментариев нет: