Давно хотел написать, да руки не доходили. 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 не критичен. Мы не жадные :) |