FreeBSD ipnat: автоматическая поддержка правил NAT в актуальном состоянии


Написал microsin

Такая необходимость возникает, если провайдер не выдает постоянный IP, либо Вам не хочется переплачивать за эту услугу (как например, было у меня с Корбиной).

В случае NAT, настроенного через ipnat, в файле /etc/ipnat.rules могут быть правила, привязанные к текущему внешнему IP, например (адрес 89.178.134.209 автоматически назначен провайдером на виртуальном внешнем интерфейсе ng0 при соединении по pptp VPN с Корбиной):
bimap ng0 192.168.7.1 -> 89.178.134.209
Это очень неудобно, поскольку при переподключении IP в этой строке должен меняться. Мне удалось избавиться от этой проблемы с помощью скрипта, который запускается каждую минуту, проверяет IP адрес на интерфейсе ng0, и делает все необходимые действия (если IP нет, то пытается переподключиться, если IP поменялся, то корректирует файл /etc/ipnat.rules и перезапускает правила NAT, если все нормально, то никаких действий не производится). Кроме того, если Вы поменяли правила в /etc/ipnat.rules, то новые правила применятся автоматически. Скрипт запускается заданием, добавленным вручную в файл /etc/crontab (системный crontab. Я пробовал его назначить иначе, через crontab -e, но почему-то скрипт работал неправильно - не находилась команда ifconfig, применяемая в скрипте):
* * * * * root /путь_до_скрипта/corbina.sh

Вот текст скрипта corbina.sh:


#!/bin/sh

logname=/var/log/corbina.log
byteIP="([0-9]|[0-9][0-9]|[01][0-9][0-9]|2[0-4][0-9]|25[0-5])"
regexIP="$byteIP(.$byteIP){3}(|/)"
natcfg='/etc/ipnat.rules'
outside=ng0
timeout=60 # timeout in seconds, while $outside interface bring up
tmpIPNATL='/scripts/ipnatl'

log ()
# процедура просто записывает строку в лог .var/log/corbina.log
{
prefix=`eval date`
echo $prefix': '$1 >> $logname
}

reloadNAT ()
# процедура вписывает в файл правил NAT новый IP
# и применяет новые правила
{
log "correct $natcfg with new $outside IP $WANIP"
# для корректировки правил применяем скрипт на Perl
# replace.pl (см. ниже)
regex="bimap $outside 192.168.7.1/32 -> $byteIP(.$byteIP){3}/"
replaceval="bimap $outside 192.168.7.1/32 -> $WANIP/"
perl /scripts/replace.pl $natcfg "$regex" "$replaceval"
ipnatmsg=`eval ipnat -CF -f $natcfg`
log "$ipnatmsg"
}

getWANIP ()
# если WAN интерфейс имеет IP, то он записывается в переменную $WANIP
# иначе $WANIP будет пустая
{
WANIP=`eval ifconfig $outside | grep -E -o --regexp="$byteIP(.$byteIP){3} -->" | grep -E -o --regexp="$byteIP(.$byteIP){3}"`
}

# тут начинается работа скрипта - получим IP на внешнем интерфейсе
getWANIP

# ветка 1: работает, если нет IP адреса на интерфейсе WAN
# В этом случае делается попытка переподключения (с помощью
# посылки сигналов демону mpd). В случае успеха
# корректируются правила NAT, а затем они вводятся в работу
if test -z "$WANIP"
then
log "no IP address at $outside adapter, try reconnect..."
log "killall -USR2 mpd"
killall -USR2 mpd
sleep 1
log "killall -USR1 mpd"
killall -USR1 mpd
# цикл: в течение 60 секунд ожидаем появления IP на интерфейсе $outside.
# Это должно означать, что подключение произошло успешно.
timecnt=0
while test $timecnt -ne $timeout
do
getWANIP
if test -n "$WANIP"
then
# IP назначен успешно (переменная $WANIP не пустая), прерываем цикл
break
fi
# задержка на 1 сек, ждем появления IP
sleep 1
# отсчет таймаута
timecnt=`expr $timecnt + 1`
done
if test -n "$WANIP"
then
# да, IP действительно назначен, перезагружаем ipnat новыми правилами
log "in $timecnt sec WAN $outside RST ok."
reloadNAT
exit 0
else
# подключиться на этот раз не удалось
log "timeout $timeout sec expired, WAN $outside RST error... Quit."
exit 1
fi
fi

# ветка 2: IP назначен, проверяем его на соответсвие правилам NAT
# в файле /etc/ipnat.rules
ipnatrulesIP=`eval more $natcfg | grep -E --regexp="^bimap" | grep -E -o --regexp="-> $byteIP(.$byteIP){3}(|/)" | grep -E -o --regexp="$byteIP(.$byteIP){3}"`
if test -n "$ipnatrulesIP"
then
# в файле правил NAT есть правило с IP, который возможно надо поменять
if test "$ipnatrulesIP" != "$WANIP"
then
# кроме того, его действительно надо поменять
log "$outside IP $WANIP != IP NAT, reload NAT rules"
reloadNAT
exit 0
fi
fi

# ветка 3: проверяем на соответствие текущих правил
# (которые работают) и правил из /etc/ipnat.rules
# Запишем во временный файл текущие правила NAT:
ipnat -l > $tmpIPNATL
# Отфильруем все ненужное (оставим в файле только правила NAT.
# для этого применяем скрипт на Perl cut_block.pl - см. ниже):
perl /scripts/cut_block.pl $tmpIPNATL "List of active MAP/Redirect filters:" "List of active sessions:"
# впишем в переменную $ipnatrules_curr текущие правила NAT
ipnatrules_curr=`more $tmpIPNATL`
# впишем в переменную $ipnatrules_file системные правила NAT
# (они в файле /etc/ipnat.rules)
ipnatrules_file=`more $natcfg | grep -E --regexp="^[^#]"`
# удаляем временный файл
rm $tmpIPNATL
if test "$ipnatrules_curr" != "$ipnatrules_file"
then
# правила NAT поменялись, применяем новые правила
log "ipnat -l != $natcfg, apply $natcfg"
ipnatmsg=`eval ipnat -CF -f $natcfg`
log "$ipnatmsg"
fi
exit 0
Немного замечаний по тексту. Переменная regexIP представляет регулярное выражение для поиска IP в тексте. Переменная outside задает имя внешнего интерфейса, на котором постоянно меняется IP (IP вычисляется командой ifconfig $outside). Процедура log() помогает вести отдельный лог для работы скрипта. Процедура getWANIP() получает IP на интерфейсе $outside и записывает его в переменную $WANIP. Процедура reloadNAT() меняет нужные правила в /etc/ipnat.rules и корректирует их новым IP, а потом перезапускает ipnat с новыми правилами. Остальные комментарии достаточно подробно вставлены по месту у текст.

Для корректировки файла /etc/ipnat.rules процедура reloadNAT() запускает скрипт replace.pl, написанный на Perl. Вот его текст:
#----------------------------------------------------------------------------------------------
# Script replaces in file <pattval> with <replval>. Value <pattval>
# is interpreted as regular expression.
#
# Usage:
# perl replace.pl file pattval repval
#----------------------------------------------------------------------------------------------

sub Usage
{
die "Usage: perl replace.pl file regex_pattern replace_value ";
}

# просто проверка наличия входных параментров, без затей:
if (@ARGV[0] eq "")
{
Usage();
}
if (@ARGV[1] eq "")
{
Usage();
}
if (@ARGV[2] eq "")
{
Usage();
}

$file=@ARGV[0]; # имя файла, в котором ищем
$tempfile=$file.".tmp"; # имя временного файла, который потом заменит исходный
$regex=@ARGV[1]; # регулярное выражение для поиска
$replval=@ARGV[2]; # текст, который заменит значение, попавшее в регулярное выражение

#Открываем рабочие файлы в двоичном режиме.
# Открыть входной файл только на чтение, если не получится, то выход:
open SRCFILE, '<', $file or die "Cannot open file ".$file.": $! ";
# Открыть выходной файл только на запись, если не получится, то выход:
open DSTFILE, '>', $tempfile or die "Cannot open file ".$tempfile.": $! "; #open only write

# Цикл: читаем построчно входной файл, корректируем, если надо, строку,
# и результат пишем в выходной файл.
while (!eof(SRCFILE))
{
$line = readline SRCFILE;
# /g задает менять все "срабатывания" $regex в строке.
# Если /g убрать, то произойдет только первая замена.
$line =~ s/$regex/$replval/g;
print DSTFILE $line;
}
# закрываем файлы
close SRCFILE;
close DSTFILE;
# старый файл удаляем и заменяем его на новый, скорректированный
unlink $file;
rename $tempfile, $file;

Еще один скрипт cut_block.pl, тоже на Perl, оставляет во временном файле только правила NAT, остальное все удаляется. Правила NAT выделяются по текстовым маркерам начала и конца блока текста. Скрипт, конечно, простейший, и вся система будет работать только в том случае, если у Вас в правилах NAT /etc/ipnat.rules записано все именно так, как должна вывести команда ipnat -l. Вот содержимое этого скрипта:
#----------------------------------------------------------------------------------------------
# Script get all strings block from file beetween beg_mark and end_mark
# and placed these strings to file (another strings are removed).
# Marks and empty strings excluded from block.
#
# Usage:
# perl cut_block.pl file beg_mark end_mark
#----------------------------------------------------------------------------------------------

sub Usage
{
die "Usage: perl cut_block.pl FILE BEGmark ENDmark ";
}

if (@ARGV[0] eq "")
{
Usage();
}
if (@ARGV[1] eq "")
{
Usage();
}
if (@ARGV[2] eq "")
{
Usage();
}

$file=@ARGV[0];
$tempfile=$file.".tmp";
$markBEG=@ARGV[1];
$markEND=@ARGV[2];
$BeginDetected=0;

open SRCFILE, '<', $file or die "Cannot open file ".$file.": $! "; #open only read
open DSTFILE, '>', $tempfile or die "Cannot open file ".$tempfile.": $! "; #open only write

sub trim
{
my($string)=@_;
for ($string)
{
s/^s+//; #trim begin
s/s+$//; #trim eng
}
return $string;
}

while (!eof(SRCFILE))
{
$line = readline SRCFILE;
$line = &trim($line);
if ($BeginDetected)
{
if (length($line))
{
$idx = index ($line, $markEND);
if (-1!=$idx)
{
last;
}
print DSTFILE $line." ";
}
}
else
{
if (length($line))
{

$idx = index ($line, $markBEG);
if (-1!=$idx)
{
$BeginDetected=1;
}
}
}
}
close SRCFILE;
close DSTFILE;

unlink $file;
rename $tempfile, $file;

http://microsin.ru/content/view/455/43/

Обновлено: 12.03.2015