eth0.pro

Linux is working. The future is open.



В FreeRADIUS'е есть возможность хранить ippool'ы в SQL, что является необходимым для распределенных систем провайдеров. Есть дефолтный файлик с готовыми запросами, которые дают возможность из коробки начать работать с пулами. Но конфигурация по-умолчанию для работы с СУБД MySQL имеет недочеты. В файле sql.conf есть намек на них:

 ################################################################
 #
 #  WARNING: MySQL has certain limitations that means it can
 #           hand out the same IP address to 2 different users.
 #
 #           We suggest using an SQL DB with proper transaction
 #           support, such as PostgreSQL, or using MySQL
 #	     with InnoDB.
 #
 ################################################################

Здесь говорится о том, что MySQL имеет ограничения, из-за которых при определенных обстоятельствах нескольким пользователям может выдастся один и тот же IP-адрес. В результате чего ни у одного из них не будет Интернета. Начнутся звонки в техподдержку, недовольства пользователей и прочие расстройства.

Из-за чего это происходит? Представим ситуацию, что два пользователя одновременно поднимают VPN/PPPoE сессию. С NAS'а идет запрос на RADIUS, тот лезет в базу данных с SELECT-запросом на поиск свободного IP-адреса. Как только он его выдаст, выполнится запрос UPDATE и пометит IP'шник как занятый, но это происходит не моментально, и даже задержка в несколько миллисекунд сделает свое черное дело. Таким образом одновременно выполнится два SELECT-запроса, которые вернут один и тот же IP-арес. Не дело. Я не говоря уж о путаннице, которая наступит из-за некорректного UPDATE'а. Я видел костыль с использованием ORDER BY RAND() LIMIT 1, но он ужасно! Отсортировать тысяч пятнадцать запсией в случайном порядке, чтобы затем выбрать из них одну - очень затратно для сервера. К тому же опять никакой гарантии, что великий random не выберет одну и туже запись.

Что же делать? Во-первых, необходимо таблицу radippool (или какая у вас прописана в ippool_table в файле sqlippool.conf) перевеси в формат InnoDB (если это еще не так). Во-вторых, для SELECT'а allocate-find нужно использовать параметр FOR UPDATE, но он не отрабатывает как надо, если не использовать транзакции. Не буду расписывать что к чему, просто приведу пример конфига.

Но прежде еще об одной проблеме. Серверов доступа (NAS) скорей всего несколько и между ними осуществляется какая то баллансировка, например при помощи DNS. Клиентские сессии в Интернет выходят при помощи ProxyARP. Таким образом бордер (пограничный маршрутизатор) видит допустим 800 IP-адресов на MAC-адресе одного NAS'а, информацию о чем хранит в своей arp-таблице. У клиента что то оборвалось, он переподключается, его "закидывает" на другой NAS, выдает тот же IP-адрес (следуя дефолтной логике ippool'а FreeRADIUS'а), он пытается пингануть Яндекс, бодер видит пакет, идущщий от клиента с IP-адресом xx.xx.xx.xx, получает от Яндекса ответ, хочет его доставить до отправителя, видит оправителя в своей подсети, смотрит в ARP-таблицу, находит соответствие с MAC-адресом друго NAS'а (arp-таблица имеет свойство "кешироваться"), переправляет ему этот пакет, тот на своих интерфейсах не находит нужного IP и отбрасывает его (пакет). В итоге у клиента Интернета нет. Для этого нужно было придумать логику, которая бы выдавала клиенту один и тот же IP-адрес в течении lease-duration (sqlippool.conf) при условии, что он подключен на тот же NAS.

И еще, по-умолчанию признаком незанятого IP-адреса в radippool'е является условие pool_key = 0. Это тоже неправильно, т.к. этот столбец указывает номер интерфейса на сервере доступа, а в случае с Linux первый ppp-интерфейс как раз и называется ppp0 ;-)

Вашему вниманию предоставляю конфиг ippool.conf для MySQL, в котором эти все моменты предупреждены.

Структура таблицы:

CREATE TABLE IF NOT EXISTS radippool(
  id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  pool_name VARCHAR(30) NOT NULL,
  framedipaddress VARCHAR(15) NOT NULL DEFAULT '',
  nasipaddress VARCHAR(15) NOT NULL DEFAULT '',
  calledstationid VARCHAR(30) NOT NULL,
  callingstationid VARCHAR(30) NOT NULL,
  expiry_time DATETIME DEFAULT NULL,
  username VARCHAR(64) NOT NULL DEFAULT '',
  pool_key VARCHAR(30) NOT NULL,
  PRIMARY KEY (id),
  INDEX IX_radippool (pool_name, expiry_time),
  INDEX IX_radippool_pool_name (pool_name),
  UNIQUE INDEX NewIndex1 (framedipaddress)
)
ENGINE = INNODB
AUTO_INCREMENT = 40914
AVG_ROW_LENGTH = 115
CHARACTER SET cp1251
COLLATE cp1251_general_ci;

Конфиг FreeRADIUS, ippool.conf для MySQL:

# -*- text -*-
##
## ippool.conf -- MySQL queries for rlm_sqlippool
##
##	$Id$

allocate-begin = "START TRANSACTION"

# ## This series of queries allocates an IP address
 allocate-clear = "UPDATE ${ippool_table} \
  SET nasipaddress = '', pool_key = '', \
  callingstationid = '', calledstationid = '', username = '', \
  expiry_time = NULL \
  WHERE expiry_time <= NOW() - INTERVAL 1 SECOND \
  AND nasipaddress = '%{Nas-IP-Address}'"


## The ORDER BY clause of this query tries to allocate the same IP-address
## which user had last session...
allocate-find = "SELECT framedipaddress FROM ${ippool_table} \
 WHERE pool_name = '%{control:Pool-Name}' AND (expiry_time > NOW() OR expiry_time IS NULL) AND pool_key = '' \
 AND (nasipaddress='%{NAS-IP-Address}' OR nasipaddress='') \
 ORDER BY (username <> (TRIM(TRAILING '@time' FROM '%{User-Name}'))), \
 (callingstationid <> '%{Calling-Station-Id}'), \
 expiry_time \
 LIMIT 1 \
 FOR UPDATE"

## If an IP could not be allocated, check to see if the pool exists or not
## This allows the module to differentiate between a full pool and no pool
## Note: If you are not running redundant pool modules this query may be
## commented out to save running this query every time an ip is not allocated.
## В этой секции я еще не разбирался =)
#~ pool-check = "SELECT id FROM ${ippool_table} \
 #~ WHERE pool_name='%{control:Pool-Name}' LIMIT 1" 
pool-check = "SELECT id FROM ${ippool_table} \
 WHERE pool_name='%{control:Pool-Name}' LIMIT 1"


## This is the final IP Allocation query, which saves the allocated ip details
allocate-update = "UPDATE ${ippool_table} \
 SET nasipaddress = '%{NAS-IP-Address}', pool_key = '${pool-key}', \
 callingstationid = '%{Calling-Station-Id}', calledstationid = '%{Called-Station-Id}',  username = (TRIM(TRAILING '@time' FROM '%{User-Name}')), \
 expiry_time = NOW() + INTERVAL ${lease-duration} SECOND \
 WHERE framedipaddress = '%I' AND (expiry_time > NOW() OR expiry_time IS NULL) AND pool_key = ''"

allocate-commit = "COMMIT"
allocate-rollback = "ROLLBACK"

## This series of queries frees an IP number when an accounting
## START record arrives
start-update = "UPDATE ${ippool_table} \
 SET expiry_time = NOW() + INTERVAL ${lease-duration} SECOND \
 WHERE nasipaddress = '%{NAS-IP-Address}' AND  pool_key = '${pool-key}'"


## This series of queries frees an IP number when an accounting
## STOP record arrives
stop-clear = "UPDATE ${ippool_table} \
 SET pool_key = '' \
 WHERE nasipaddress = '%{Nas-IP-Address}' AND pool_key = '${pool-key}' \
 AND username = (TRIM(TRAILING '@time' FROM '%{User-Name}')) \
 AND callingstationid = '%{Calling-Station-Id}' \
 AND framedipaddress = '%{Framed-IP-Address}'"


## This series of queries frees an IP number when an accounting
## ALIVE record arrives
alive-update = "UPDATE ${ippool_table} \
 SET expiry_time = NOW() + INTERVAL ${lease-duration} SECOND \
 WHERE nasipaddress = '%{Nas-IP-Address}' AND pool_key = '${pool-key}' \
 AND username = (TRIM(TRAILING '@time' FROM '%{User-Name}')) \
 AND callingstationid = '%{Calling-Station-Id}' \
 AND framedipaddress = '%{Framed-IP-Address}'"



## This series of queries frees the IP numbers allocate to a
## NAS when an accounting ON record arrives
on-clear = "UPDATE ${ippool_table} \
 SET nasipaddress = '', pool_key = '', callingstationid = '', username = '', \
 expiry_time = NULL \
 WHERE nasipaddress = '%{Nas-IP-Address}'"


## This series of queries frees the IP numbers allocate to a
## NAS when an accounting OFF record arrives
off-clear = "UPDATE ${ippool_table} \
 SET nasipaddress = '', pool_key = '', callingstationid = '', username = '', \
 expiry_time = NULL \
 WHERE nasipaddress = '%{Nas-IP-Address}'"

30 янв 2013 г. | Теги: RADIUS MySQL