|
Давно хотел написать, да руки не доходили. FreeRADIUS, начиная с версии 2.0.4 поддерживает работу в качестве DHCP-сервера. Идея, в общем, логичная. Протоколы имеют много общего (пары атрибут-значение) и предназначены для схожих задач (выдать параметры конфигурации). Меня эта тема достаточно серьёзно интересует, т.к. решение с OMAPI хоть и "красивое", но, всё-таки, факт отсутствия жёсткой связи с базой биллинга, невозможность массовых операций и вообще достаточно жёсткие рамки ISC DHCP сервера заставляли искать альтернативу. А тут как раз подвернулся проектик: выдача IP-адресов STB'шкам. STB - Set Top Box - это такой embedded linux, подключается к ethernet, получает свой IP по DHCP и показывает мультикастовое IPTV на телевизор. Управляется с пульта. Идея была такая: мы не имеем пока доступа к middleware, где крутится IPTV, которое мы продаём, поэтому отключать за неуплату абонента можно только воздействуя либо на его порт (что возможно, но не совсем просто, т.к. у нас нет жёсткой привязки к портам) либо на сам STB. Придумалась схема: к аккаунту привязывается MAC-адрес устройства (который написан прямо у него на брюхе) и на каждый DHCPREQUEST мы анализируем состояние аккаунта и либо выдаём адрес, если всё хорошо, либо отвечаем NACK'ом и STB пишет "обратитесь к оператору", а телевидение, наоборот, не показывает. Под такое дело я решил попробовать мой любимый FreeRADIUS. Во-первых, в секции listen {} нужно прописать новые порты типа dhcp (что логично). Нужно подключить новый словарь dictionary.dhcp, который описывает DHCP-атрибуты. Обработка каждого типа запроса выполняется в отдельных секциях. Можно сделать ещё одну catch-all секцию без спецификации типа. В секциях всё описавается так же, как и, например, в authorize {} - т.е. модули и unlang. У модулей дёргается только post_auth хук, поэтому, к сожалению, не все rlm можно использовать. Например, я очень хотел применить rlm_sql, но в post_auth секции он не умеет отдавать атрибутов, и для меня оказался бесполезным. Для логики оставались два модуля rlm_perl и rlm_python. (rlm_exec отпал сразу, т.к. "уже сейчас понятно, что всё будет глючить и тормозить" ©) К сожалению, у rlm_python отсутсвовал post_auth хук (что решилось несложным патчем), но он ещё расстроил меня тем, что freeradius не захотел выходить по Ctrl+C в режиме отладки при используемом питоне. Сразу представились глюки при многопоточном режиме работы, непослушность сигналам и т.п. Отказать. Остаётся перл, хоть я и не очень его люблю. Был написан достаточно простой скрипт, который коннектится к базе, дёргает там функцию с двумя аргументами (chaddr, giaddr) и получает в ответ всё, что должен знать STB: ip, mask, gw и длительность лизы. Или ничего, если MAC базе не понравился. Ну и отладочное сообщение для инженеров, разумеется, чтоб не скучали. Кстати, огромный респект Perl DBI за connect_cached()! Это просто гениальная вещь, на мой взгляд. За одно это можно простить перлу весь его геморрой :) Выглядит это (за вычетом информации, которую я не могу разглашать) так: use DBI;
use vars qw(%RAD_REQUEST %RAD_REPLY %RAD_CHECK);
use constant RLM_MODULE_REJECT=> 0;# /* immediately reject the request */
use constant RLM_MODULE_FAIL=> 1;# /* module failed, don't reply */
use constant RLM_MODULE_OK=> 2;# /* the module is OK, continue */
use constant RLM_MODULE_HANDLED=> 3;# /* the module handled the request, so stop. */
use constant RLM_MODULE_INVALID=> 4;# /* the module considers the request invalid. */
use constant RLM_MODULE_USERLOCK=> 5;# /* reject the request (user is locked out) */
use constant RLM_MODULE_NOTFOUND=> 6;# /* user not found */
use constant RLM_MODULE_NOOP=> 7;# /* module succeeded without doing anything */
use constant RLM_MODULE_UPDATED=> 8;# /* OK (pairs modified) */
use constant RLM_MODULE_NUMCODES=> 9;# /* How many return codes there are */
use constant L_DBG=> 1;
use constant L_AUTH=> 2;
use constant L_INFO=> 3;
use constant L_ERR=> 4;
use constant L_PROXY=> 5;
use constant L_CONS=> 128;
my $dbh = "";
sub post_auth {
my $ciaddr = $RAD_REQUEST{'DHCP-Client-IP-Address'};
my $giaddr = $RAD_REQUEST{'DHCP-Gateway-IP-Address'};
my $chaddr = $RAD_REQUEST{'DHCP-Client-Hardware-Address'};
my $xid = $RAD_REQUEST{'DHCP-Transaction-Id'};
my $msgtype = $RAD_REQUEST{'DHCP-Message-Type'};
&dbConnect();
unless ( $dbh ) {
&radiusd::radlog(L_ERR, "$xid: No DB connection, failing...");
$RAD_REQUEST{'Tmp-String-0'} = 'No DB connection.';
return RLM_MODULE_NOOP;
};
unless ( $sth = $dbh->prepare("select host(ip), host(mask), host(gw), lease, message from dhcp_stb(?,?);") ) {
&radiusd::radlog(L_ERR, "$xid: DB statement preparation failed: " . $dbh->errstr);
$RAD_REQUEST{'Tmp-String-0'} = 'SQL statement preparation has been failed.';
return RLM_MODULE_NOOP;
};
unless ( $sth->execute($chaddr,$giaddr) ) {
&radiusd::radlog(L_ERR, "$xid: DB statement execution failed: " . $dbh->errstr);
$RAD_REQUEST{'Tmp-String-0'} = 'SQL statement execution has been failed.';
return RLM_MODULE_NOOP;
};
$sth->bind_columns(\$yiaddr, \$mask, \$gw, \$lease, \$message);
$ary_ref = $sth->fetchall_arrayref;
if ( $yiaddr and $mask and $gw ) {
$RAD_REPLY{'DHCP-Your-IP-Address'} = $yiaddr;
$RAD_REPLY{'DHCP-Subnet-Mask'} = $mask;
$RAD_REPLY{'DHCP-Router-Address'} = $gw;
$RAD_REPLY{'DHCP-IP-Address-Lease-Time'} = $lease;
if ( $message ) {
$RAD_REQUEST{'Tmp-String-0'} = $message;
} else
{
$RAD_REQUEST{'Tmp-String-0'} = 'OK';
};
return RLM_MODULE_OK;
};
if ( $message ) {
$RAD_REQUEST{'Tmp-String-0'} = $message;
} else
{
$RAD_REQUEST{'Tmp-String-0'} = 'no IP from DB.';
}
return RLM_MODULE_NOTFOUND;
};
sub dbConnect {
$dbh = DBI->connect_cached("DBI:Pg:dbname=$DBName;host=$DBHost", $DBUsername, $DBPassword) or
&radiusd::radlog(L_ERR, "DB connection failed: " . DBI->errstr);
};
Внутри конфига FreeRADIUS это обрабатывается следующим образом (опять же, некоторые критичные вещи вырезаны или изменены): dhcp DHCP-Discover {
# Set server ID field (it MUST be in the reply packet)
update reply {
DHCP-DHCP-Server-Identifier = "%{Packet-Dst-IP-Address}"
}
#Log the request
linelog
#Process the request
perl
#Update reponse if request succeeded
if (ok) {
# Address was found
update reply {
DHCP-Message-Type = DHCP-Offer
DHCP-NTP-Servers = 1.1.1.1
}
}
else {
#In any other case - don't send anything:
update reply {
DHCP-Message-Type = 0
}
}
# Log the response
linelog
ok
}
dhcp DHCP-Request {
# Set server ID field (it MUST be in the reply packet)
update reply {
DHCP-DHCP-Server-Identifier = "%{Packet-Dst-IP-Address}"
}
#Log the request
linelog
#Process the request
perl
#Update reponse with all required information on success
if (ok) {
# Address was found
update reply {
DHCP-Message-Type = DHCP-ACK
DHCP-NTP-Servers = 1.1.1.1
}
}
elsif (notfound) {
# Address was not found, send NAK response
update reply {
DHCP-Message-Type = DHCP-NAK
}
}
else {
#In any other case - don't send anything:
update reply {
DHCP-Message-Type = 0
}
}
#Log the response
linelog
ok
}
dhcp DHCP-Release {
handled
}
dhcp DHCP-Inform {
handled
}
dhcp {
handled
}
Вот и почти всё. Здесь, к сожалению, не полное соответствие RFC2131 (в частности, не анализируест список атрибутов, которые хочет видеть клиент), но нам это и не нужно. Логи для дежурных пишутся очень информативные, благодаря огромному количеству функций rlm_linelog: linelog {
filename = syslog
format = ""
reference = "%{%{reply:DHCP-Message-Type}:-%{request:DHCP-Message-Type}}"
DHCP-Discover = "%{DHCP-Transaction-Id} DISCOVER: [%{DHCP-Client-Hardware-Address}] via (%{DHCP-Gateway-IP-Address}) %{DHCP-Hostname}"
DHCP-Offer = "%{DHCP-Transaction-Id} OFFER: %{reply:DHCP-Your-IP-Address} to [%{DHCP-Client-Hardware-Address}] ..."
DHCP-Request = "%{DHCP-Transaction-Id} REQUEST: [%{DHCP-Client-Hardware-Address}] via (%{DHCP-Gateway-IP-Address}) ..."
DHCP-Ack = "%{DHCP-Transaction-Id} ACK: %{reply:DHCP-Your-IP-Address} to [%{DHCP-Client-Hardware-Address}] ..."
DHCP-NAK = "%{DHCP-Transaction-Id} NAK: [%{DHCP-Client-Hardware-Address}] for %{request:DHCP-Client-IP-Address}; ..."
0 = "%{DHCP-Transaction-Id} %{request:DHCP-Message-Type} DROPPED: ..."
}
Очень удобный модуль. Каждый трид freeradius запускает свою копию rlm_perl и устанавливает свой коннекшн с базой. Чтобы количество соединений не вырастало за разумные рамки, я ограничил максимальное количество тридов и, в спокойном режиме, когда у нас не более одного пакета за один раз (а обычно так и есть), у меня работает только один трид. Ну и перезапускаю я триды раз в 64 запроса, на всякий случай. thread pool {
start_servers = 1
max_servers = 8
min_spare_servers = 0
max_spare_servers = 2
max_requests_per_server = 64
}
Со стороны базы у нас уже была "схема сети" с VLAN'ами, сетями и диапазонами адресов под динамические пулы (калька конфига ISC DHCPd), поэтому особо мудрствовать не пришлось. Была написана пара функций на PlPgSQL и PlPython, которые и занимаются менеджементом адресов и лиз. Вот так. Работает, память (на первый взгляд) не жрёт, адреса отдаёт. Как только у абонента баланс проваливается ниже 0, на следующем DHCPREQUEST он получает NACK и перестаёт показывать картинку. В принципе, разбег в несколько часов (предполагаемая длительность lease в боевом режиме) между изменением баланса и отключением TV не критичен. Мы не жадные :) |