вторник, 21 декабря 2010 г.

Squid: Transparent proxy на Solaris 10

Однажды мне потребовалось построить прозрачный прокси (transparent proxy) на Solaris 10. Тщательное исследование Великой Сети показало, что подобного решения либо не существует, либо оно держится в большом секрете.

Поскольку готового решения (и даже направления поисков) не нашлось, пришлось проводить исследования самостоятельно.

Итак, задача.

Построить высоконадежный и эффективный полностью автономный (и необслуживаемый) прозрачный прокси-сервер на Solaris 10 с использованием ZFS и всех необходимых системных фишек.

В качестве прокси-сервера выберем Squid, как надежный и зарекомендовавший себя.

Установленная система предварительно тщательно минимизируется и харденится.

Для прозрачного режима прежде всего необходимо обеспечить перехват трафика HTTP (по 80 порту) непосредственно на самом прокси-сервере. Для этого необходима поддержка файрвола прокси-сервером в прозрачном режиме.

Небольшое исследование показало, что последние версии Squid не собираются с включением поддержки IPFilter.

Что ж, возьмем предсобранный Squid c Sunfereeware.

Прежде всего, однако, настроим IPFilter и TCP-стек.

Для корректного функционирования Сквида в прозрачном режиме необходимо включить IPv4-Forwarding:

root @ ktulhu /# routeadm -e ipv4-forwarding
root @ ktulhu /# routeadm -u

и настроить IPFilter:

root @ ktulhu /# cat /etc/ipnat.conf
rdr bge0 0.0.0.0/0 port 80 -> 0/32 port 3128


root @ ktulhu /# cat /etc/ipf.conf

# Block rules for RPC (open firewall)
block return-rst in quick on bge0 from any to any port=111
block return-rst in quick on bge0 from any to any port=6112

В /etc/defaultrouter самого прокси следует указать адрес маршрутизатора в Интернет.

Обратите внимание на правило редиректа IPFilter (Правила NAT/PAT IpFilter работают до выполнения набора основных правил) - оно должно быть написано именно таким образом, и на набор основных правил ipf.conf. Последние требуют некоторого пояснения.

Мы вынуждены использовать файрвол в открытом режиме (то есть блокировать только ненужные открытые порты), потому что в закрытом режиме через прокси не проходит аутентификация instant messengers - ICQ, MAgent, Jabber. Для установления обратной сессии при аутентификации им требуется открытый диапазон портов > 1024, который в нормальной конфигурации файрвола закрыт. Соответственно, мы закрываем только порты, которые обнаружили посредством NMap (их два) и которые мы хотим закрыть так, чтобы файрвола не было видно вообще. Для этого мы ставим опцию return-rst.

Замечание. Если вы выполнили минимизацию системных сервисов, больше никаких открытых портов, кроме порта Сквида, SSH и Apache не будет.

Следующим шагом создаем ZFS-пул data для кэша и логов и устанавливаем на нем recordsize равным 2048 байт (нам не нужны крупные экстенты, так как мы собираемся хранить много мелких файлов).

Хотя существует рекомендация использовать aufs для кэша Сквида на ZFS, с нашей версией (2.7) данная опция не работает. Впрочем, это не смертельно и Сквид прекрасно функционирует на ZFS (причем работает весьма быстро - ну-ка, кто там говорил, что ZFS - тормоз и фрагментируется? Extent-based файловые системы не фрагментируются по определению!).

Пишем базовую конфигурацию прокси.

#Recommended minimum configuration:
acl all src all
acl manager proto cache_object
acl localhost src 127.0.0.1/32
acl to_localhost dst 127.0.0.0/8
#
# Example rule allowing access from your local networks.
# Adapt to list your (internal) IP networks from where browsing
# should be allowed
acl localnet src 10.0.0.0/8 # RFC1918 possible internal network
acl localnet src 172.16.0.0/12 # RFC1918 possible internal network
acl localnet src 192.168.0.0/16 # RFC1918 possible internal network
#
acl SSL_ports port 443
acl Safe_ports port 80 # http
acl Safe_ports port 21 # ftp
acl Safe_ports port 443 # https
acl Safe_ports port 70 # gopher
acl Safe_ports port 210 # wais
acl Safe_ports port 1025-65535 # unregistered ports
acl Safe_ports port 280 # http-mgmt
acl Safe_ports port 488 # gss-http
acl Safe_ports port 591 # filemaker
acl Safe_ports port 777 # multiling http

acl Safe_ports port 22 # SSH port
acl Safe_ports port 2222 # SSH alternate port

acl CONNECT method CONNECT

# TAG: http_access
# Allowing or Denying access based on defined access lists
#
# Access to the HTTP port:
# http_access allow|deny [!]aclname ...
#
# NOTE on default values:
#
# If there are no "access" lines present, the default is to deny
# the request.
#
# If none of the "access" lines cause a match, the default is the
# opposite of the last line in the list. If the last line was
# deny, the default is allow. Conversely, if the last line
# is allow, the default will be deny. For these reasons, it is a
# good idea to have an "deny all" or "allow all" entry at the end
# of your access lists to avoid potential confusion.
#
#Default:
# http_access deny all
#
#Recommended minimum configuration:
#
# Only allow cachemgr access from localhost
http_access allow manager localhost
http_access deny manager
# Deny requests to unknown ports
http_access deny !Safe_ports
# Deny CONNECT to other than SSL ports
http_access deny CONNECT !SSL_ports
#
# We strongly recommend the following be uncommented to protect innocent
# web applications running on the proxy server who think the only
# one who can access services on "localhost" is a local user
http_access deny to_localhost
#
# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS

# Example rule allowing access from your local networks.
# Adapt localnet in the ACL section to list your (internal) IP networks
# from where browsing should be allowed
http_access allow localnet

# And finally deny all other access to this proxy
http_access deny all

# Squid normally listens to port 3128
http_port 3128 transparent

# TCP outgoing address
#tcp_outgoing_address 192.168.201.22

# Cache manager
cache_mgr admin@server.com

# We recommend you to use at least the following line.
hierarchy_stoplist cgi-bin ?

# Uncomment and adjust the following to add a disk cache directory.
#cache_dir ufs /usr/loal/squid/var/cache 100 16 256
cache_dir ufs /data/cache 32767 16 256

# Memory parameters
cache_mem 512 Mb
memory_pools off
maximum_object_size 8092 Kb

# Access log
access_log /data/cache/log/access.log squid

# Cache log
cache_log /data/cache/log/cache.log

# Store log
cache_store_log none

# Leave coredumps in the first cache dir
#coredump_dir /usr/local/squid/var/cache
coredump_dir /var/core

# Pid file
pid_filename /usr/local/squid/var/logs/squid.pid

# Add any of your own refresh_pattern entries above these.
refresh_pattern ^ftp: 1440 20% 10080
refresh_pattern ^gopher: 1440 0% 1440
refresh_pattern -i (/cgi-bin/|\?) 0 0% 0
refresh_pattern . 0 20% 4320

cache_effective_user squid
cache_effective_group squid

# SquidGuard rewriter
url_rewrite_program /usr/local/bin/squidGuard -c /usr/local/squidGuard/squidGuard.conf
url_rewrite_children 96
redirect_rewrites_host_header on
#redirector_bypass on

Из конфигурации следует, что мы используем под кэш датасет на созданном ZFS-пуле, выделяем достаточно большое количество памяти, и пропускаем весь трафик, так как фильтрацию контента мы будем выполнять посредством SquidGuard. Так же из конфигурации следует, что мы намереваемся кэшировать 32 Гб на диске (то есть технически наш прокси сможет обслуживать достаточно большую и активную сеть).

Так как мы намереваемся использовать достаточно большой кэш Сквида в памяти, имеет смысл ограничить ARC ZFS неким разумным значением, скажем, в 1/4 или 1/3 всей оперативной памяти:

root @ ktulhu /# cat /etc/system

set maxphys=1048576
set noexec_user_stack=1
set noexec_user_stack_log=1
set zfs:zfs_arc_max=1073741824
set rlim_fd_max=65536

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

Дополнительно необходимо увеличить данный ограничитель пользователию squid вызовом ulimit в профиле:

root @ ktulhu / # cat /export/home/squid/.profile

MAIL=/usr/mail/${LOGNAME:?}

PATH=/usr/local/squid/sbin:/usr/local/ssl/bin:/usr/sfw/bin:/bin:/usr/bin:/usr/local/bin:/usr/ccs/bin:/usr/ucb
export PATH

ulimit -n 65536

Разумеется, после установки Сквида нужно обеспечить доступ ко всем необходимым директориям и файлам, выполнив, когда это необходимо, команды chown, предварительно создав непривилегированного пользователя squid и группу squid, от которых будет выполняться сам сквид, редиректор и Apache.

Следует также настроить resolver и запустить сервис dns/client (мы не будем поднимать автономный кэширующий DNS). В качестве серверов имен следует выбрать достаточно крупные и быстродействующие DNS-сервера. Я выбрал сервера OpenDNS.

На данном этапе следует закомментировать параметр url_rewrite_program.

Следующий шаг - это установка Apache (Apache 2) и SquidGuard. Веб-сервер нам потребуется для обслуживания редиректов SquidGuard, опциональной установки системы статистики и управления прокси-сервером (в данной статье не рассматривается).

Мы не будем мудрствовать и установим собранный Apache 2 с Sunfreeware.

После установки и предварительного конфигурирования Squid и Apache можно приступить к созданию структуры директорий кэша, проверке конфигураций и запуску. Убедившись, что все работает, можно создать SMF-сервисы для обеих служб (в данной статье не рассматривается; у автора написано готовое решение для указанных служб по созданию сервисов SMF в одно действие).

Следующим шагом установим и настроим SquidGuard.

Есть несколько неочевидных и неописанных тонкостей установки SquidGuard, не описанных по-сути нигде (я не нашел).

Во-первых, для установки SquidGuard потребуется Berkeley DB. После тщательного изучения руководств я выбрал версию 4.4.20 c Sunfreeware.

Во-вторых, перед сборкой необходимо установить в профиле пользователя squid (либо в глобальном профиле /etc/profile) переменную LD_LIBRARY_PATH:

root @ ktulhu /# export LD_LIBRARY_PATH=/usr/local/BerkeleyDB.4.4/lib:$LD_LIBRARY_PATH

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

./configure --with-db=/usr/local/BerkeleyDB.4.4 --with-db-inc=/usr/local/BerkeleyDB.4.4/include --with-db-lib=/usr/local/BerkeleyDB.4.4/lib

В-третьих, SquidGuard не собирается Sun Studio, и необходимо использовать штатный GCC.

И в-четвертых, перед сборкой SquidGuard в Solaris требуется установить GNU M4 и библиотеку libsigsegv.

ВАЖНО. Последнее условие (зависимость именно от GNU M4) нигде не документировано, пришлось эту зависимость находить опытным путем.

Далее скачиваем нужные блэклисты, распаковываем их в /usr/local/squidGuard/db.

У себя я использую один из наиболее полных блэклистов.

Теперь (до компиляции баз) нужно написать конфигурацию SquidGuard. Для прозрачного прокси напишем относительно простую конфигурацию, которая будет резать наиболее ресурсоемкие или опасные сайты и рекламу:

#
# CONFIG FILE FOR SQUIDGUARD
#

dbhome /usr/local/squidGuard/db
logdir /usr/local/squidGuard/log

dest banners {
domainlist banners/domains
urllist banners/urls
expressionlist banners/expressions
redirect http://localhost/nobanner.gif
}

dest adv {
domainlist BL/adv/domains
urllist BL/adv/urls
redirect http://localhost/nobanner.gif
}

dest porn {
domainlist BL/porn/domains
urllist BL/porn/urls
}

dest sex {
domainlist BL/sex/education/domains
urllist BL/sex/education/urls
}

dest sex2 {
domainlist BL/sex/lingerie/domains
urllist BL/sex/lingerie/urls
}

dest spyware {
domainlist BL/spyware/domains
urllist BL/spyware/urls
}

dest redirector {
domainlist BL/redirector/domains
urllist BL/redirector/urls
}

dest dating {
domainlist BL/dating/domains
urllist BL/dating/urls
}

dest socialnet {
domainlist BL/socialnet/domains
urllist BL/socialnet/urls
}

acl {
default {
pass !banners !adv !porn !sex !sex2 !spyware !dating !socialnet !redirector all
redirect http://localhost/block.html
}
}

После этого можно скомпилировать базы. Убедитесь, что компиляция запускается от пользователя squid (ошибки permission denied являются наиболее распространенными при конфигурировании).

squid @ ktulhu ~$ squidGuard -C all

Компиляция баз является очень ресурсоемкой, на несколько десятков секунд машина просто перестает отвечать на запросы, оперативная память во время данной операции заполняется почти до конца.

Будут скомпилированы базы, перечисленные в конфигурационном файле.

Убедимся, что все в порядке, права переданы - можно включить параметр url_rewrite_program в squid.conf и реконфигурировать сквид. После проверки лога cache.log, если все в порядке, все готово.

Замечание. Изменения конфигураций или баз данных SquidGuard требуют реконфигурации самого сквида.

Все?

Не совсем. Осталось немного. Мы хотим написать скрипт автоматического обновления баз SquidGuard раз в неделю (мы же создаем необслуживаемый сервер), и вызывать его из cron.

Что ж, напишем скрипт:

#!/sbin/sh

#
# SquidGuard blocklist update
#
# Written by Y.Voinov (C) 2010
#
# ident "@(#)update_blocklist.sh 1.1 10/12/21 YV"
#

#############
# Variables #
#############

# Modify PATH for SFW directory use
PATH=/usr/sfw/bin:$PATH

# List name
LIST_NAME="shallalist.tar.gz"

# Servers for downloading blacklist
SERVER1="http://www.shallalist.de/Downloads/$LIST_NAME"
SERVER2=""

SERVER_LIST="$SERVER1 $SERVER2"
TEMP_DIR="/tmp"

# Connection timeout for downloading
TIMEOUT=30

# Installation base dir
BASE="/usr/local"

# Base Squid installation directory
BASE_DIR="$BASE/squid"

# Base guard directory
BASE_DIR_GUARD="$BASE/squidGuard"

# Squid files paths
SQUID_PATH="$BASE_DIR""/sbin"
SQUID_CONF_PATH="$BASE_DIR""/etc"

# Squid files
SQUID_BIN_FILE="squid"
SQUID_CONF_FILE="squid.conf"

# Squid/SquidGuard user name
SQUID_USER="squid"

# SquidGuard location
SQUIDGUARD="$BASE/bin/squidGuard"

# OS utilities
CAT=`which cat`
CD=`which cd`
CUT=`which cut`
ECHO=`which echo`
GETENT=`which getent`
GZCAT=`which gzcat`
ID=`which id`
RM=`which rm`
SU=`which su`
GTAR=`which gtar`
UNAME=`which uname`
WGET=`which wget`

OS_VER=`$UNAME -r|$CUT -f2 -d"."`
OS_NAME=`$UNAME -s|$CUT -f1 -d" "`

###############
# Subroutines #
###############

os_check ()
{
if [ "$OS_NAME" != "SunOS" ]; then
$ECHO "ERROR: Unsupported OS $OS_NAME. Exiting..."
exit 1
elif [ "$OS_VER" -lt "10" ]; then
$ECHO "ERROR: Unsupported $OS_NAME version $OS_VER. Exiting..."
exit 1
fi
}

root_check ()
{
if [ ! `$ID | $CUT -f1 -d" "` = "uid=0(root)" ]; then
$ECHO "ERROR: You must be super-user to run this script."
exit 1
fi
}

checkuser ()
{
# Check squid user
username=$1
if [ ! -z "`$GETENT passwd $username`" ]; then
$ECHO "1"
else
$ECHO "0"
fi
}

checkconf ()
{
# Check Squid config file
config=$1
if [ -f "$SQUID_CONF_PATH"/"$config" ]; then
$ECHO "1"
else
$ECHO "0"
fi
}

download_list ()
{
# Get list from one server using server list
$ECHO "List downloading..."
for S in $SERVER_LIST; do
$SU - $SQUID_USER -c "$WGET -T $TIMEOUT -q -O $TEMP_DIR/$LIST_NAME $S"
retcode=`$ECHO $?`
case "$retcode" in
0)
$ECHO "List downloaded successfully."
break
;;
4)
$ECHO "Unable to resolve host address. Exiting..."
exit 4
;;
*)
$ECHO "Error downloading list from `$ECHO $S|$CUT -f1 -d '/'`. Try another server..."
continue
;;
esac
done

if [ "$retcode" != "0" ]; then
$ECHO "Error downloading list from all servers. Exiting..."
exit 1
fi
}

unpack_list ()
{
# Unpack list
$ECHO "List unpacking..."
$SU - $SQUID_USER -c "$GZCAT $TEMP_DIR/$LIST_NAME | $GTAR -x -C $BASE_DIR_GUARD/db"
}

list_compilation ()
{
# Call squidGuard DB recompilation
$ECHO "List compiling..."
$SU - $SQUID_USER -c "$SQUIDGUARD -C all"
}

reconfiguration ()
{
$ECHO "Reconfiguration..."
# Squid reconfiguration
program=$1
if [ "`checkconf $SQUID_CONF_FILE`" = "1" ]; then
if [ "`checkuser $SQUID_USER`" = "1" ]; then
$SU - $SQUID_USER -c "$SQUID_PATH/$program -k reconfigure"
else
$ECHO "ERROR: User $SQUID_USER not found."
$ECHO "Exiting..."
exit 2
fi
else
$ECHO "ERROR: Config file $SQUID_CONF_PATH/$SQUID_CONF_FILE not found."
$ECHO "Exiting..."
exit 2
fi
}

##############
# Main block #
##############

# OS check
os_check

# Root check
root_check

# Download list
download_list

# Unpack list
unpack_list

# Black list compilation
list_compilation

# Squid reconfiguration
reconfiguration $SQUID_BIN_FILE

exit 0

Помещаем скрипт в /usr/local/bin и создаем запись в /var/spool/cron/crontabs/root:

0 0 * * 6 [ -x /usr/local/bin/update_blocklist.sh ] && /usr/local/bin/update_blocklist.sh

Осталось рестартовать cron и готово:

root @ ktulhu /# svcadm restart cron

Все, наш сервер готов. Можно зайти на Зайцев.нет переполненный рекламой сайт и протестировать баннерорезку. :)

Финальный штрих - единственная настройка, которую надо сделать на клиентах - указать шлюзом по умолчанию адрес прокси.

PS. Ротация всех логов настраивается при помощи штатного для Solaris logadm. Если необходимо использовать SARG или другой счетчик статистики, следует убедиться, что график выполнения logadm не перекрывает график обновления статистики прокси. Разумеется, следует иметь в виду, что прозрачный прокси не позволяет использовать аутентификацию и статистика будет только по посещаемым сайтам и сетям, использующим проксирование.