Не люблю писать коментарии. Уверен, что во многих местах можно было переписать более корректно. Но работает и так.
Не бить меня за стилистику кавычек условных ветвлений. Ну не люблю я ставить кавычки в строке с if. Да и в vi так легче работать с кодом.
Зарание прошу извинения, если где ошибка. Скрипт робочий, но єто первая версия. Сейчас у меня версия три. На два канала и один сквид. Может где что-то и пропустил когда вырезал лишнее.
У меня из-за ограничений на одном из каналов скрипт работает только для 80 порта. Но это не приципиально.
Если используется прокси, то рекомендуется использовать прокси для каждого из каналов. Так как не исключено, что для другого ай-пи сервер вернет другой контент.
Можно добавить еще задержку перед переключением из альтернативного на основной канал.
Если использовать tcpdump невозможно, например роль внешних каналов играют сквиды, то можно анализировать результат роботы sockstat. Эфективность та-же.
фаервол на шлюзе. фря
после всех проверок командой skipto 50000 all from 10.0.0.0/8 to any 80 исходящие пакеты направляюся на обработку правилами фаерфола созданного скриптами, которые анализируют внешние каналы. Входящие пакеты фря разрулит сама, ибо keep-state.
50000-52000 - правила фаервола созданные скриптами вида
forward --gate-- tcp from --src-- to –-dst-- 80 keep-state
где
--gate-- - адрес шлюза
--src-- - адрес локальной машины
–-dst-- - адрес удаленного сервера
состояние при котором все пакеты для которых не прописаны правила выше будут идти на шлюз_1. При проходе через шлюз_1 скрипт пропишет правила фаерволы для каждой пары локальный клиент-удаленный сервер
59990 skipto 65000 tcp from any to any 80
60000 fwd шлюз_2 tcp from any to any 80 keep-state
65000 fwd шлюз_1 tcp from any to any 80 keep-state
состояние при котором все пакеты для которых не прописаны правила выше будут идти на шлюз_2. При проходе через шлюз_2 скрипт пропишет правила фаерволы для каждой пары локальный клиент-удаленный сервер
59990 правило удалено
60000 fwd шлюз_2 tcp from any to any 80 keep-state
65000 fwd шлюз_1 tcp from any to any 80 keep-state
правила 60000 и 65000 не удаляем, а обходим их через skipto т.к. при удалении удяляются и все динамические правила созданные по keep-state. А такие есть всегда, т.к. при первом соединении клиента с сервером скрипт не успевает прописать соответствующее правило и срабатывает одно из последних. Тоесть скрипты добавляет правила, которые работают ТОЛЬКО при повторном и всех последующих соединениях клиента с сервером. Например при загрузке следующей странички с сервера.
сообственно сами скрипты.
запускается все это дело так.
//убиваем старыые скрипты
kill -9 `ps -waux | grep '/usr/sbin/tcpdump -tlvni xl1' | grep -v grep | awk '{print $2};'`
kill -9 `ps -waux | grep '/usr/sbin/tcpdump -tlvni tun0' | grep -v grep | awk '{print $2};'`
//запускаем новые
(/usr/sbin/tcpdump -tlvni tun0 | /usr/local/bin/php traffic_link_selector_1.php >> log/traffic_link_selector_1.log ) &
(/usr/sbin/tcpdump -tlvni xl1 port 80| /usr/local/bin/php traffic_link_selector_2.php >> log/traffic_link_selector_2.log ) &
скрипты
traffic_link_selector_1.php – для первого канала. У меня это tun0. Для него tcpdump без фильтров т.к. надо точно определять текущую загрузку канала
<?
$global_low_slot=50001; //границі правил для єтого интерфейса
$global_hi_slot=51000;
$global_do_select=1; //может перебрасівать траффик на второй канал манипулируя правилом 59990
require_once('traffic_link_selector.php');
?>
traffic_link_selector_2.php – для второго канала. У меня это xl1
<?
$global_low_slot=51001;
$global_hi_slot=52000;
$global_do_select=0;
require_once('traffic_link_selector.php');
?>
traffic_link_selector.php
<?
//описываем границы загрузки основного канала при превышении которых переключаемся на второй канал
//днем
$global_hi_rate=14*1024;
$global_low_rate=11*1024;
//ночью
$global_hi_rate_n=19*1024;
$global_low_rate_n=15*1024;
//границы ночи
$global_n_start=9;
$global_n_stop=17;
$global_delta_time=30; //интервал подсчета загрузки канала
$global_time_expire=150; //время жизни правила фаервола. По умолчанию, во ФРЕ ИМХО 300 секунд, если явно не закрылось соодениние
$fp=fopen('php://stdin','r'); //подключамся к stdin (tcpdump)
if (!$fp) {report('error 2');} //абшибочка вышла
$time_pre=time();
$last=true;
$global_size=0;
$global_state=0; //0-normal, 1-switched //изначальное положение – используется основний канал
$global_routing_table=array(); //массив пар клиет-сервер
$global_slots=array(); //массив свободніх слотов для фаервола
$global_next_slot=$global_low_slot; следующий свободній слот
$ipfw='/sbin/ipfw';
//*******************************************************
if ($global_do_select==1) //проводим очистку если это скрипт для основного канала
{
exec($ipfw.' delete 59990');
exec($ipfw.' add 59990 skipto 65000 tcp from any to any 80');
}
for ($i=$global_low_slot; $i<=$global_hi_slot; $i++) //чистим все свои правила, если есть конечно
{
if ($i>20000) {exec($ipfw.' delete '.$i);} //заглушка >20000 что-бы не зацепить остальной фаервол при неправльной конфигурации скрипта
}
while ((!feof($fp)) or ($last)) читаем из stdin пока не получим последний пакет
{
if (feof($fp)) {$last=false;} последний пакет
$line= fgets($fp);
разбираем что нам выдал tcpdump. Осторожно с версиями. У меня tcpdump стал выдавать немного другую инфу при апгрейде с 4.7 на 4.11
Я люблю по старинке. Можно сделать то-же и с регулярными выражениями.
$size=trim(substr($line,strpos($line,', len ')+6));
$size=trim(substr($size,0,strpos($size,')')));
$line_1=trim(substr($line,0,strpos($line,' > ')));
$line_1=trim(substr($line_1,strrpos($line_1,' ')));
$src=explode('.',trim($line_1));
$src_ip=$src[0].'.'.$src[1].'.'.$src[2].'.'.$src[3];
$src_port=(int)$src[4];
$line_2=trim(substr($line,strpos($line,' > ')+3));
$line_2=trim(substr($line_2,0,strpos($line_2,':')));
$dst=explode('.',trim($line_2));
$dst_ip=$dst[0].'.'.$dst[1].'.'.$dst[2].'.'.$dst[3];
$dst_port=(int)$dst[4];
$proto='tcp'
if (strpos($line,'udp')>0) {$proto='udp';}
if (strpos($line,'icmp')>0) {$proto='icmp';}
if (strpos($line,'gre')>0) {$proto='gre';}
$hash1=$proto.'_'.$src_ip.':'.$src_port.'>'.$dst_ip.':'.$dst_port.' -- '.$size;
$global_size=$global_size+$size; //для определения текущей скрости
if (($proto=='tcp') and //выбираем нужные пакеты. Можно и не только 80 порт
(
(($dst_port==80) and (is_attend($src_ip))) //is_attend – истина если адрес локальный.
or
(($src_port==80) and (is_attend($dst_ip)))
)
)
{
if (is_attend($src_ip))
//сортируем источник-получатель источник – всегда локальный клиент, получатель – удаленнывй сервер. Получателя сразу преобразовываем в диапазон адресов. Можно скачать списки подсетей и и точно определять подсеть. Но как по мне – то это лишняя трата времени и ресурсов.
{
$src1=$src_ip;
$dst1=$dst[0].'.'.$dst[1].'.0.0/16';
}
else
{
$src1=$dst_ip;
$dst1=$src[0].'.'.$src[1].'.0.0/16';
}
$hash_mask=$src1.'_'.$dst1; //создали хеш
if (is_array($global_routing_table[$hash_mask])) //уже такие паеты ходили? обновляем время последнего пакета
{
$global_routing_table[$hash_mask]['time']=time();
}
else //новый пакет
{
$slot=get_next_slot(); //получаем следующее свободное правило
$chain=array();
$chain['src']=$src1;
$chain['dst']=$dst1;
$chain['time']=time();
$chain['slot']=$slot;
$global_slots[$slot]=$hash_mask; //добавили в список правил ссылку на запись в $global_routing_table. Чтобы можно было искать но номеру правила
$global_routing_table[$hash_mask]=$chain; //добавили в массив пар клиет-сервер (+время и номер правила)
echo date('d.m.y H:i:s ',time()).'Add '.$dst1.' '.$slot.' ('.count($global_routing_table).')'."\n";
if ($global_do_select==1) //выполняемся для основного канала
{
exec($ipfw.' add '.$slot.' forward шлюз_1 tcp from '.$src1.' to '.$dst1.' 80 keep-state ');
}
else //для альтернативного
{
exec($ipfw.' add '.$slot.' forward шлюз_2 tcp from '.$src1.' to '.$dst1.' 80 keep-state ');
}
// с этого момента все пакеты from '.$src1.' to '.$dst1.' будут бегать по указанному шлюзу. Таки только будут ибо они уже пошли на этот шлюз по правилу 60000 или 65000. Ибо иначе мы бы их tcpdump не увидели. Так что по только-что добавленому правилу пойдут все пакеты из новых соединений для этого сервера и клиента.
}
}
$delta_time=time()-$time_pre;
if (($delta_time>$global_delta_time) or (!$last)) //интервал уже минул или пакет последний? Тогда будем проверять наши правила на наличие устаревших и решать как дальше ходить пакетам
{
$time_pre=time();
//вычислеям текущую скорость
$rate=$global_size/$delta_time;
$global_size=0;
echo date('d.m.y H:i:s ',time()).'Rate: '.$rate."\n";
// второй канал не упал? Если упал, то флажка (файла) нет.
$is_ix=file_exists('ix_selected_flag');
if ($global_do_select==1) // мы в скрипте для основного канала?
{
определям пределы загзурки для основного канала
$hi_rate=$global_hi_rate_n;
$low_rate=$global_low_rate_n;
if ((date('G',time())>$global_n_start) and (date('G',time())<$global_n_stop))
{
$day_of_week=date('w',time());
if (($day_of_week>0) and ($day_of_week<6))
{
$hi_rate=$global_hi_rate;
$low_rate=$global_low_rate;
}
}
if ($global_state==0) //мы в нормальном состояниии – пакеты уходят на основной канал
{
if ($rate>$hi_rate) //превышение скорости?
{
if ($is_ix) // второй канал не упал? переключаемся на него
{
echo date('d.m.y H:i:s ',time()).'Switch to alternative'."\n";
$global_state=1; //меняем состояние
exec($ipfw.' delete 59990'); переключаемся на второй канал
}
else //Таки упал альтернативный канал. Жалко что не вовремя ибо нам он нужен.
{
echo date('d.m.y H:i:s ',time()).'Need switch to alternative but no IX found !!!'."\n";
}
}
else //превышения скорости нет. Все нормально
{
echo date('d.m.y H:i:s ',time()).'Normal'."\n";
}
}
else //мы в состояниии переключеного траффика на альтернативный канал
{
if (($rate<$low_rate) or (!$is_ix)) //если ситуация на основном нормализоваласи или альтернатывный упал
{
if (!$is_ix)
{
echo date('d.m.y H:i:s ',time()).'No IX - switch to normal immediatly!!!'."\n";
exec($ipfw.' delete 60000');
exec($ipfw.' add 60000 fwd шлюз_2 tcp from any to any 80 keep-state’);
//именно убиваем чтобы умерли все динамические правила созданные keep-state
}
//переключаемся на основной
echo date('d.m.y H:i:s ',time()).'Switch to normal'."\n";
$global_state=0;
exec($ipfw.' add 59990 skipto 65000 tcp from any to any 80');
}
else //на основном канале все-еще загрузка – продолжаем использовать резервный
{
echo date('d.m.y H:i:s ',time()).'Alternative'."\n";
}
}
}
else //мы в ветке для альтернативного канала?
{
if (!$is_ix) //упал альтернативный канал?
{
echo date('d.m.y H:i:s ',time()).'No IX - delete all routes!!!'."\n";
foreach($global_routing_table as $key=>$value) //обнуляем время последнего доступа для все правил фаервола альтернативного канала. Правила уб'ются следующим циклом
{
$global_routing_table[$key]['time']=0;
}
}
}
// для все каналов
foreach($global_routing_table as $key=>$value) // если пакеты по правилу не ходили слишком долго, то убиваем правило. Если убили правило в момент прохождения пакета, ничего страшного. Создастся новое правило для этого-же канала.
{
if ($value['time']+$global_time_expire<time())
{
$slot=(int)$value['slot'];
echo date('d.m.y H:i:s ',time()).'Deleting '.$value['dst'].' '.$slot.' ('.count($global_routing_table).')'."\n";
//print_r($value);
unset($global_slots[$value['slot']]);
unset($global_routing_table[$key]);
exec($ipfw.' delete '.$slot.' ');
}
}
}
}
print_r($global_routing_table); //на выходе из скрипта печатаем таблицу для диагностики
function get_next_slot() //выдает следующее свободное правило фаервола
{
global $global_slots;
global $global_next_slot;
global $global_low_slot;
global $global_hi_slot;
$round=2;
$next=$global_next_slot+1;
while($round>0)
{
if ($next>$global_hi_slot)
{
$next=$global_low_slot;
$round--;
}
if ($global_slots[$next]=='')
{
$global_next_slot=$next;
return $next;
}
$next++;
}
$global_next_slot=$global_hi_slot;
return $global_hi_slot;
}
function is_attend($ip) истина, если ай-пи локальный клиентский
{
if (strpos(' '.$ip,'10.')==1) {return true;}
return false;
}
?>