Производительный роутер на FreeBSD

Опубликовано admin в Пт, 22/10/2010 - 10:24

Осенью 2009 года наша компания запустила новое техническое решение для предоставления доступа в сеть в виде услуги с названием "Прямой интернет". Суть заключается в более эффективной реализации доступа к интернету конечному пользователю.

Сервера сетевого доступа (NASы, или на жаргоне - аггрегаторы) выполняют функции терминирования клиента, шейпинга, сбор статистики по трафику, по необходимости натирования (трансляция сетевых адресов) и форвардинга наружу. Натирование, сбор статистики (netflow) и сам форвардинг работают достаточно эффективно.

Слабым звеном являются туннелирование и шейпинг большого количества клиентов. Решить проблему туннелирования просто - избавиться от неё. При использовании vpn-аггрегатора есть 2 приемущества: мы указываем куда надо подключаться и как надо подключаться, то есть даём адрес впн-сервера и требуем авторизацию в виде логина и пароля, без которых доступа никто не получит. Без впн-аггрегатора появляется необходимость решить эти 2 задачи. Первая задача решается колдовством на центральном маршрутизаторе сети, вторая - привязкой клиентского оборудования по ip,mac и порту. Вобщем эти 2 задачи здесь не описываются, так как выходят за рамки статьи.

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

На стадии тестирования, решения этих задач пробовались несколькими способами. Шейпинг с помощью altq, dummynet, ng_car. Натирование - pf nat, ipfw nat. Сбор статистики - ng_netflow. Избыточность роутеров с помощью carp, синхронизация nat-сессий с помощью pfsync. Всё это тестировалось на стабильность и производительность, в сети было найдено много подобных тестов, которые мы пробовали и сравнивали, эксперементировали с настройками сетевой подсистемы FreeBSD, разные версии 7-ой и 8-ой ветки, разные аппаратные конфигурации. К сожалению, результатов всех тестов не осталось, зато есть конечный работоспособный вариант. А подобные тесты и примеры конфигураций можно найти в сети.

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

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

Шейпинг с помощью ng_car сначала обрадовал - он показывал самые интересные результаты, точную скорость и давал возможность управления каналами налету. Но большое количество клиентов порождало для каждого отдельный свой узел в подсистеме netgraph. Она получалась сильно ветвлённая и общая производительность системы резко падала.

Тестировались сетевые карты от Broadcom и Intel. Наилучший результат показали карты на чипах Intel 82576, которые обладают буфером входящих и исходящих пакетов в 4к и работают отложенными прерываниями (аппаратный polling).

В итоге мы остановились на использовании ipfw+dummynet для шейпинга, ng_netflow для сбора статистики, pf nat для трансляции адресов. Роутерами служат сервера Hewlett-Packard на указанных выше сетевых платах Intel. В каждом таком сервере стоит по одному четырёх-ядерному процессору Intel Xeon и 2-4 гб ОЗУ. В качестве операционной системы используется FreeBSD 8.0 Release p2 amd64.

Тестирование конечного варианта проводилось на канале 800мбит/с. Через роутер проходило до 180 тысяч пакетов в секунду. Не наблюдалось ни единого отброшенного пакета или пакета с ошибкой. После чего сервер был поставлен на реальную нагрузку в 300-400 мбит/с входящего и исходящего трафика по отдельности. Проработав стабильно больше недели, нагруженность каждого сервера "на глаз" оценивалась как 40-60%. После количество серверов увеличилось, то есть каждый стал работать в режиме 20-30% своих возможностей. Ниже приведены данные с одного из работающих сейчас роутеров.

# netstat -w1 -h -d
 input        (Total)           output
 packets  errs      bytes    packets  errs      bytes colls drops
 54K     0        33M        53K     0        33M     0     0 
 54K     0        32M        53K     0        32M     0     0 
 54K     0        32M        52K     0        32M     0     0 
 54K     0        33M        53K     0        32M     0     0 
 57K     0        35M        56K     0        35M     0     0 
 54K     0        32M        52K     0        32M     0     0 
 
# top -SPH
last pid: 71507;  load averages:  0.43,  0.31,  0.26      up 41+06:17:06  23:16:07
101 processes: 6 running, 71 sleeping, 24 waiting
CPU 0:  0.0% user,  0.0% nice, 12.8% system,  4.9% interrupt, 82.3% idle
CPU 1:  0.0% user,  0.0% nice,  8.3% system, 27.8% interrupt, 63.9% idle
CPU 2:  0.0% user,  0.0% nice, 13.2% system, 13.2% interrupt, 73.7% idle
CPU 3:  0.0% user,  0.0% nice, 18.8% system,  1.1% interrupt, 80.1% idle
Mem: 12M Active, 160M Inact, 528M Wired, 148K Cache, 418M Buf, 3229M Free
Swap: 8192M Total, 8192M Free
 
 PID USERNAME PRI NICE   SIZE    RES STATE   C   TIME   WCPU COMMAND
 11 root     171 ki31     0K    64K CPU3    3 914.8H 97.07% {idle: cpu3}
 11 root     171 ki31     0K    64K RUN     0 830.3H 84.38% {idle: cpu0}
 11 root     171 ki31     0K    64K RUN     1 774.6H 78.47% {idle: cpu1}
 11 root     171 ki31     0K    64K RUN     2 827.6H 64.26% {idle: cpu2}
 13 root      50    -     0K    64K sleep   2  63.6H  4.05% {ng_queue2}
 13 root      49    -     0K    64K sleep   2  62.0H  3.66% {ng_queue1}
 13 root      50    -     0K    64K sleep   0  63.7H  2.88% {ng_queue0}
 12 root     -68    -     0K   400K WAIT    0 824:22  1.17% {irq259: igb1}
 0 root     -68    0     0K   144K -       3  17.9H  0.39% {igb0 taskq}
 # netstat -i
Name    Mtu Network       Address              Ipkts Ierrs    Opkts Oerrs  Coll
igb0   1500 <Link#1>      d8:d3:85:63:6d:1a 64828120893    15 60565734561     0     0
igb0   1500 172.16.0.0    dir5               3218463     - 914517644     -     -
igb1   1500 <Link#2>      d8:d3:85:63:6d:1b 65338495263  607 67475218921   244     0
igb1   1500 77.232.141.0/ dir5               8597342     - 3267277473     -     -
lo0   16384 <Link#3>                            8487     0     8487     0     0
lo0   16384 your-net      localhost             8487     -  2132159     -     -
# vmstat -i
interrupt                          total       rate
irq1: atkbd0                           4          0
irq14: ata0                      1562542          0
irq16: uhci0                          22          0
irq18: ehci0 uhci3                     2          0
cpu0: timer                   7076740658       1982
irq256: igb0                 18122121501       5077
irq257: igb0                 24209686691       6783
irq258: igb0                           2          0
irq259: igb1                 21958424484       6152
irq260: igb1                 25173771060       7053
irq261: igb1                         270          0
cpu2: timer                   7076707102       1982
cpu3: timer                   7076707117       1982
cpu1: timer                   7076715498       1982
Total                       117772436953      33000

А теперь посмотрим, что в конфигах.

Собираем своё ядро:

добавляем
options         IPFIREWALL
options         IPFIREWALL_DEFAULT_TO_ACCEPT
options         IPFIREWALL_FORWARD
options         DUMMYNET
options         NETGRAPH
options         NETGRAPH_BPF
options         NETGRAPH_IFACE
options         NETGRAPH_KSOCKET
options         NETGRAPH_IPFW
options         NETGRAPH_SOCKET
options         NETGRAPH_NETFLOW
options         NETGRAPH_ETHER
device          pf
и удаляем драйвера ненужных устройств

Тюнингуем систему:

# cat /boot/loader.conf 
hw.igb.rxd=4096
hw.igb.txd=4096
укажем максимальный размер буфера пакетов
# cat /etc/sysctl.conf 
net.inet.ip.forwarding=1 #включаем форвардинг пакетов
net.inet.ip.fastforwarding=1 #эта опция действительно ускоряет форвардинг
net.inet.tcp.blackhole=2 #ядро убивает tcp пакеты, приходящие в систему на непрослушиваемые порты
net.inet.udp.blackhole=0 #как и выше, только не убивает ибо traceroute пакеты не покажут этот хоп
net.inet.icmp.drop_redirect=1 #не обращаем внимания на icmp redirect
net.inet.icmp.log_redirect=0 #и не логируем их
net.inet.ip.redirect=0 #не реагируем на icmp redirect
net.inet.ip.sourceroute=0 #отключение маршрутизации от источника
net.inet.ip.accept_sourceroute=0 #старый и бесполезный механизм
net.inet.icmp.bmcastecho=0 #защита от SMURF атак
net.inet.icmp.maskrepl=0 #не отдавать по icmp паску своей подсети
net.link.ether.inet.max_age=30 #переспрашиваем каждые 30 секунд mac адреса в своём arp пространстве
net.inet.ip.ttl=226 #почему бы не поставить ttl побольше ;)
net.inet.tcp.drop_synfin=1 #небольшая защита
net.inet.tcp.syncookies=1 #от доса
kern.ipc.somaxconn=32768 #увеличиваем размер очереди для сокетов
kern.maxfiles=204800 #увеличиваем число открытых файловых дескрипторов
kern.maxfilesperproc=200000 #кол-во ф.д. на каждоый процесс
kern.ipc.nmbclusters=524288 #увеличиваем число сетевых буферов
kern.ipc.maxsockbuf=2097152 #
kern.random.sys.harvest.ethernet=0 #не использовать трафик и прерывания
kern.random.sys.harvest.interrupt=0 #как источник энтропии для random'a
net.inet.ip.dummynet.io_fast=1 #заставляет dummynet работать побыстрее
net.inet.ip.dummynet.max_chain_len=2048 #
net.inet.ip.dummynet.hash_size=65535 #
net.inet.ip.dummynet.pipe_slot_limit=2048 #
net.inet.carp.preempt=1 #включаем carp
net.inet.carp.log=2 #пишем логи карпа
kern.ipc.shmmax=67108864 #макс. размер сегмента памяти
net.inet.ip.intr_queue_maxlen=8192 #размер очереди ip-пакетов
net.inet.ip.fw.one_pass=0 #пакеты, прошедшие пайпы не вылетают из фаервола, а дальше идут по нему
dev.igb.0.enable_lro=0 #отключение large receive offloading 
dev.igb.1.enable_lro=0
dev.igb.0.enable_aim=0 #так нет аномалий с работой сетевушек
dev.igb.1.enable_aim=0
dev.igb.0.rx_processing_limit=2048 #адаптивный polling, разрешаем прерывания с сетевухи при достижении значения
dev.igb.0.flow_control=0 #отключение контроля потока
dev.igb.1.rx_processing_limit=2048
dev.igb.1.flow_control=0

Далее идёт скрипт создания графа в netgraph для сбора netflow статистики.

# cat /etc/rc.d/ngnetflow 
#!/bin/sh
. /etc/rc.subr
 name="ngnetflow"
rcvar=`set_rcvar`
load_rc_config $name
: ${ngnetflow_enable="NO"}
: ${ngnetflow_src="0.0.0.0:5525"}
: ${ngnetflow_dst="x.x.x.x:5525"}
start_cmd="ngnetflow_start"
stop_cmd="ngnetflow_stop"
ngnetflow_start() {
/usr/sbin/ngctl -f- <<-SEQ
mkpeer ipfw: netflow 65534 iface0
name ipfw:65534 netflow
connect ipfw: netflow: 65533 out0
msg netflow: setdlt { iface=0 dlt=12 }
msg netflow: settimeouts { inactive=30 active=600 }
mkpeer netflow: ksocket export inet/dgram/udp
name netflow:export flow-sensor
msg flow-sensor: bind inet/${ngnetflow_src}
msg flow-sensor: connect inet/${ngnetflow_dst}
SEQ
}
ngnetflow_stop() {
/usr/sbin/ngctl -f- <<-SEQ
shutdown netflow:
SEQ
}
run_rc_command "$1"

Для автозапуска этого скрипта помещаем в /etc/rc.conf строку ngnetflow_enable="YES"

Наш граф выглядет вот так:

ng_netflow

Если сделать ipfw add netgraph 65534 ip from any to me, то трафик, попадающий под это правило будет заворачиваться в netgraph на хук 65534, попадает в ноду netflow, где происходит сбор статистики с заголовков ip-пакетов, далее трафик возвращается по хуку 65533 обратно в фаервол. К ноде netflow подключен экспорт netflow-статистике ввиде udp дейтаграмм.

Если вместо ipfw add netgraph использовать ipfw add ngtee, то трафик, проходящий это правило, будет дублироваться(!), копия которого направляться на ng_netflow.

Теперь настроим NAT.

# cat /etc/pf.conf 
set limit states 16000000
set optimization aggressive
set limit src-nodes 160000
set limit table-entries 160000
nat-anchor "ftp-proxy/*"
nat on igb1 from 10.0.0.0/8 to any -> 77.232.142.0/24 source-hash #натирование пулом адресов

Конфиг фаервола, сделанный с любовью и без лишних замарочек

# cat /etc/ipfw.conf 
ipfw='/sbin/ipfw -q'
if_in='igb0'
if_out='igb1'
 
#--- reseting ---
${ipfw} flush
${ipfw} pipe flush
${ipfw} table all flush
#----------------
 
#--- red light---
${ipfw} add deny ip from any to any 135,137,138,139,445 #fucking ports
#-----------------
 
#--- green light ---
${ipfw} add allow ip from any to me #me
${ipfw} add allow ip from me to any #me
 
${ipfw} add allow ip from 77.232.142.0/24,77.232.143.0/24 to any #адресное пространство натирующих подсетей
${ipfw} add allow ip from any to 77.232.142.0/24,77.232.143.0/24 #в них превращаются уже отфильтрованные пакеты
 
${ipfw} add allow ip from 77.232.128.0/20 to 10.0.0.0/8,172.25.0.0/16,77.232.128.0/20 #это на не надо, но сделано на всякий случай
${ipfw} add allow ip from 10.0.0.0/8,172.25.0.0/16,77.232.128.0/20 to 77.232.128.0/20 #чтобы вдруг не обсчитать заблудший не туда лок. трафик
 
${ipfw} add allow ip from any to table\(1\) #таблица 1 содержит адреса и подсети
${ipfw} add allow ip from table\(1\) to any #которые пускаем в инет в обход учёта в биллинге
 
#---- TABLES -----
${ipfw} table 1 add 77.232.128.0/28 #servers network
${ipfw} table 1 add 77.232.135.0/29 #servers network
${ipfw} table 1 add 77.232.128.64/27 #billing network
${ipfw} table 1 add 77.232.128.192/28 #admins network
${ipfw} table 1 add 62.231.13.168 #cyberplat
#--------------------------
 
#--- valid abons ---
${ipfw} add pipe tablearg ip from any to table\(126\) out xmit ${if_in} #abons shape downstream
${ipfw} add netgraph 65534 ip from any to table\(126\) out xmit ${if_in} #abons netflow downstream
${ipfw} add allow ip from any to table\(126\) #out xmit ${if_in} #allow abons downstream
 
${ipfw} add pipe tablearg ip from table\(127\) to any in recv ${if_in} #abons shape upstream
${ipfw} add netgraph 65534 ip from table\(127\) to any in recv ${if_in} #abons netflow upstream
${ipfw} add allow ip from table\(127\) to any #in recv ${if_in} #allow abons upstream
#-------------------
 
#--- DENY ALL and ip.fw.one_pass=0 ---
${ipfw} add 65534 deny ip from any to any #по умолчанию фаерволл открытый
делаем его закрытым, то есть всё что не описано в правилах выше - не пускаем
#-------------------------------------
 

Эффективность работы dummynet достигается такой конструкцией:

${ipfw} add pipe tablearg

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

Далее по крону отрабатывает скрипт, который формирует динамические пайпы с указанной пропускной способностью:

ipfw pipe $номер_пайпы config mask dst-ip 0xffffffff bw $скоростьkbits

Добавляет в таблицы №126 и 127 пары (ip-адрес клиента, номер пайпы):

ipfw table 126 add $ip $номер

Этот скрипт писал мой коллега на perl'e. Скрипт обеспечивает добавление новых и удаление старых пайп для тарифных планов. Добавление и удаление текущих клиентов из таблиц 126, 127 с изменением у клиента $номер в случае смены тарифа. IP-адреса и скорости клиентов выгружаются с биллинга и обрабатываются каждые 5-10 минут.

Теперь мы имеет оттюнингованную для роутинга систему, которая способна не просто прокачать через себя около гигабита в секунду, но и успев зашейпить каждого клиента, собрать с него netflow и занатировать. Один такой аггрегат способен успешно работать с 3000-4000 клиентами одновременно, 800/800 мбит/с трафика и пропускать через себя 180 килопакетов в секунду на каждом интерфейса. На впн-сервере (pptp под linux'ом) у нас бывает по 400-600 пользователей, сервер такой же аппаратной конфигурации более 100мбит/с выдаёт с трудом.



Статья взята с http://bonzo.me (_http://bonzo.me/freebsd-router)