Сеткавыя налады з FreeRadius праз DHCP

Сеткавыя налады з FreeRadius праз DHCP
Прыляцела задача наладзіць выдачу IP адрасоў абанентам. Умовы задачы:

  • Асобнага сервера пад аўтарызацыі не дамо - абыйдзецеся 😉
  • Абаненты павінны атрымліваць сеткавыя наладкі па DHCP
  • Сетка разнастайная. Гэта і PON абсталяванне, і звычайныя свічкі з настроенай Опцыяй 82 і WiFi базы з кропкамі
  • Калі ні пад адну з умоў выдачы IP дадзеныя не трапляюць – неабходна выдаць IP з «гасцявой» сеткі

З добрага: ёсць сервер на FreeBSD, які можа «папрацаваць», але ён «за трыдзевяць земляў» ;), не «прамы ў гэтай сетцы».

Яшчэ ёсць выдатная прылада Mikrotik. Агульная схема сеткі прыкладна такая:

Сеткавыя налады з FreeRadius праз DHCP

Ледзь паразважаўшы, было прынята рашэнне выкарыстоўваць для выдачы сеткавых настроек абанентам FreeRadius. У прынцыпе схема звычайная: на Microtick уключаем DHCP сервер, на ім-жа Radius Client. Наладжваем звязку DHCP server -> Radius Client -> Radius server.

Накшталт бы не складана. Але! Д'ябал крыецца ў дэталях. А менавіта:

  • Пры аўтарызацыі PON OLT па гэтай схеме на FreeRadius "прылятае" запыт з User-Name роўнаму МАС адрасу галаўной станцыі, Agent-Circuit-Id роўнаму МАС PON Onu і пустым паролем.
  • Пры аўтарызацыі са свечак з опцыяй 82, на FreeRadius прыходзіць запыт з пустым User-Name роўнаму МАС прылады абанента і запоўненымі дадатковымі атрыбутамі Agent-Circuit-Id і Agent-Remote-Id утрымоўвальнымі адпаведна ізноў жа МАС рэлейнай свічкі і порт да якога падлучаны абанент.
  • Частка абанентаў з WiFI кропак аўтарызуюцца праз PAP-CHAP пратаколы
  • Частка абанентаў з WIFI кропак аўтарызуюцца з User-Name роўнаму МАС адрасу WIFI кропкі, без пароля.

Гістарычная даведка: што такое Option 82 у DHCP

Гэта дадатковыя опцыі ў пратаколу DHCP якія дазваляюць перадаць дадатковую інфармацыю, напрыклад у палях Agent-Circuit-Id і Agent-Remote-Id. Звычайна выкарыстоўваецца для перадачы МАС адрасы рэлейнай свічкі і порта да якога падключаны абанент. У выпадку абсталявання PON або базавых станцый WIFI поле Agent-Circuit-Id карыснай інфармацыі не нясе (няма порта абанента). Пры гэтым агульная схема працы DHCP у гэтым выпадку наступная:

Сеткавыя налады з FreeRadius праз DHCP

Пакрокава гэтая схема працуе так:

  1. Абаненцкае абсталяванне робіць шырокавяшчальны DHCP запыт на атрыманне сеткавых настроек.
  2. Прылада (напрыклад свіч, базавая станцыя WiFi або PON) да якога непасрэдна падключаецца абаненцкае абсталяванне "перахапляе" гэты пакет і змяняе яго, укараняючы ў яго дадатковыя опцыі Option 82 і Relay agent IP address, і перадае яго далей па сетцы.
  3. DHCP сервер прымае запыт, фармуе адказ і адпраўляе яго рэлейнай прыладзе
  4. Рэлейная прылада перапраўляе пакет адказу на абаненцкую прыладу

Так проста ўсё гэта вядома не працуе, патрэбна якая адпавядае налада сеткавага абсталявання.

Ўстаноўка FreeRadius

Наладамі канфігурацыі FreeRadius гэтага вядома дасягнуць усяго можна, але складана і не зразумела… асабліва калі сунешся туды праз N месяцаў усё працуе . Таму было прынятае рашэнне напісаць свой модуль аўтарызацыі для FreeRadius на Python. Дадзеныя для аўтарызацыі будзем браць з базы MySQL. Структуру яе апісваць сэнсу няма, усё роўна кожны будзе яе рабіць "пад сябе". У прыватнасці я ўзяў структуру якая прапануецца з модулем sql для FreeRadius, і ледзь змяніў, дадаўшы поле mac і port для кожнага абанента, апроч лагіна-пароля.

Такім чынам, для пачатку ўсталёўваны FreeRadius:

cd /usr/ports/net/freeradius3
make config
make
install clean

У наладах адзначаем для ўстаноўкі:

Сеткавыя налады з FreeRadius праз DHCP

Які робіцца симлинк на модуль python (т.е. «уключаны» яго):

ln -s /usr/local/etc/raddb/mods-available/python /usr/local/etc/raddb/mods-enabled

Усталюем для python дадатковы модуль:

pip install mysql-connector

У наладах модуля python для FreeRadius, трэба прапісаць шляхі пошуку модуляў у зменную python_path. Напрыклад у мяне гэта:

python_path="/usr/local/etc/raddb/mods-config/python:/usr/local/lib/python2.7:/usr/local/lib/python27.zip:/usr/local/lib/python2.7:/usr/local/lib/python2.7/plat-freebsd12:/usr/local/lib/python2.7/lib-tk:/usr/local/lib/python2.7/lib-old:/usr/local/lib/python2.7/lib-dynload:/usr/local/lib/python2.7/site-packages"

Шляхі можна даведацца запусціўшы інтэрпрэтатар python і увёўшы каманды:

root@phaeton:/usr/local/etc/raddb/mods-enabled# python
Python 2.7.15 (default, Dec  8 2018, 01:22:25) 
[GCC 4.2.1 Compatible FreeBSD Clang 6.0.1 (tags/RELEASE_601/final 335540)] on freebsd12
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path
['', '/usr/local/lib/python27.zip', '/usr/local/lib/python2.7', '/usr/local/lib/python2.7/plat-freebsd12', '/usr/local/lib/python2.7/lib-tk', '/usr/local/lib/python2.7/lib-old', '/usr/local/lib/python2.7/lib-dynload', '/usr/local/lib/python2.7/site-packages']
>

Калі не зрабіць гэты крок, то скрыпты напісаныя на python і запушчаныя FreeRadius не знойдуць тыя модулі, якія пералічаны ў import. Акрамя таго, неабходна раскаментаваць у наладах модуля функцыі выкліку аўтарызацыі і акаўнтынгу. Напрыклад у мяне выглядае дадзены модуль так:

python {
    python_path="/usr/local/etc/raddb/mods-config/python:/usr/local/lib/python2.7:/usr/local/lib/python2.7/site-packages:/usr/local/lib/python27.zip:/usr/local/lib/python2.7:/usr/local/lib/python2.7/plat-freebsd12:/usr/local/lib/python2.7/lib-tk:/usr/local/lib/python2.7/lib-old:/usr/local/lib/python2.7/lib-dynload:/usr/local/lib/python2.7/site-packages"
    module = work
    mod_instantiate = ${.module}
    mod_detach = ${.module}

    mod_authorize = ${.module}
    func_authorize = authorize

    mod_authenticate = ${.module}
    func_authenticate = authenticate

    mod_preacct = ${.module}
    func_preacct = preacct

    mod_accounting = ${.module}
    func_accounting = accounting

    mod_checksimul = ${.module}
    mod_pre_proxy = ${.module}
    mod_post_proxy = ${.module}
    mod_post_auth = ${.module}
    mod_recv_coa = ${.module}
    mod_send_coa = ${.module}

}

Скрыпт work.py (і ўсе астатнія) неабходна пакласці ў /usr/local/etc/raddb/mods-config/python Усяго скрыптоў у мяне выйшла тры.

work.py:

#!/usr/local/bin/python
# coding=utf-8
import radiusd
import func
import sys
from pprint import pprint
mysql_host="localhost"
mysql_username="укацук"
mysql_password="ыукаыукаыук"
mysql_base="ыукаыкуаыу"
def instantiate(p):
print ("*** instantiate ***")
print (p)
# return 0 for success or -1 for failure
def authenticate(p):
print ("*** Аутенфикация!!***")
print (p)
def authorize(p):
radiusd.radlog(radiusd.L_INFO, '*** radlog call in authorize ***')    
conn=func.GetConnectionMysql(mysql_host, mysql_username, mysql_password, mysql_base);
param=func.ConvertArrayToNames(p);
pprint(param)
print ("*** Авторизация ***")
reply = ()
conf = ()
cnt=0
username="";mac="";
# сначала проверяем "как положено", по связке логин/пароль
if ("User-Name" in param) and ("User-Password" in param) :
print ("Вариант авторизации (1): есть логин-пароль")
pprint(param["User-Name"])
pprint(param["User-Password"])
pprint(conn)
print(sys.version_info)
print (radiusd.config)
sql="select radreply.attribute,radreply.value from radcheck inner join radreply on radreply.username=radcheck.username where radcheck.username=%s and radcheck.value=%s"
print(sql)
cursor = conn.cursor(dictionary=True,buffered=True)
cursor.execute(sql,[param["User-Name"], param["User-Password"]]);
row = cursor.fetchone()	
while row is not None:    
cnt=cnt+1
username=row["username"]
reply = reply+((str(row["attribute"]),str(row["value"])), )
row = cursor.fetchone()	          
# вариант, что User-Name - это МАС адрес БС,пароля и порта нет                
if ("User-Name" in param)  and ("User-Password" in param) and (cnt==0):
if param["User-Password"] =='':
if ":" in param["User-Name"]:
pprint(param["User-Name"])            
print ("Вариант авторизации (2): User-Name - это MAC адрес базовой станции, порта и пароля нет")
sql="select radreply.username,radreply.attribute,radreply.value from radcheck inner join radreply on radreply.username=radcheck.username where REPLACE(radcheck.mac,':','') = REPLACE(REPLACE('"+str(param["User-Name"])+"','0x',''),':','') and radcheck.sw_port=''"
print (sql)
cursor = conn.cursor(dictionary=True,buffered=True)
cursor.execute(sql);
row = cursor.fetchone()	
while row is not None:                  
cnt=cnt+1
username=row["username"]
mac=param["User-Name"]
reply = reply+((str(row["attribute"]),str(row["value"])), )
row = cursor.fetchone()	          
if ("Agent-Remote-Id" in param)  and ("User-Password" in param) and (cnt==0):
if param["User-Password"] =='':
pprint(param["Agent-Remote-Id"])            
print ("Вариант авторизации (2.5): Agent-Remote-Id - это MAC адрес PON оборудования")
sql="select radreply.username,radreply.attribute,radreply.value from radcheck inner join radreply on radreply.username=radcheck.username where REPLACE(radcheck.mac,':','') = REPLACE(REPLACE('"+str(param["Agent-Remote-Id"])+"','0x',''),':','') and radcheck.sw_port=''"
print (sql)
cursor = conn.cursor(dictionary=True,buffered=True)
cursor.execute(sql);
row = cursor.fetchone()	
while row is not None:                  
cnt=cnt+1
username=row["username"]
mac=param["User-Name"]
reply = reply+((str(row["attribute"]),str(row["value"])), )
row = cursor.fetchone()	          
#Вариант, что Agent-Remote-Id - это МАС адрес БС,пароля и порта нет и предыдущие варианты поиска IP результата не дали                
if ("Agent-Remote-Id" in param)  and ("User-Password" not in param) and (cnt==0):
pprint(param["Agent-Remote-Id"])            
print ("Вариант авторизации (3): Agent-Remote-Id - МАС базовой станции/пон. Порта в биллинге нет")
sql="select radreply.username,radreply.attribute,radreply.value from radcheck inner join radreply on radreply.username=radcheck.username where REPLACE(radcheck.mac,':','') = REPLACE(REPLACE('"+str(param["Agent-Remote-Id"])+"','0x',''),':','') and radcheck.sw_port=''"
print(sql)
cursor = conn.cursor(dictionary=True,buffered=True)
cursor.execute(sql);
row = cursor.fetchone()	
while row is not None:    
cnt=cnt+1
mac=param["Agent-Remote-Id"]
username=row["username"]
reply = reply+((str(row["attribute"]),str(row["value"])), )
row = cursor.fetchone()	          
#Вариант, что предыдущие попытки результата не дали, но есть Agent-Remote-Id и Agent-Circuit-Id
if ("Agent-Remote-Id" in param)  and ("Agent-Circuit-Id" in param) and (cnt==0):
pprint(param["Agent-Remote-Id"])            
pprint(param["Agent-Circuit-Id"])            
print ("Вариант авторизации (4): авторизация по Agent-Remote-Id и Agent-Circuit-Id, в биллинге есть порт/мак")
sql="select radreply.username,radreply.attribute,radreply.value from radcheck inner join radreply on radreply.username=radcheck.username where upper(radcheck.sw_mac)=upper(REPLACE('"+str(param["Agent-Remote-Id"])+"','0x','')) and upper(radcheck.sw_port)=upper(RIGHT('"+str(param["Agent-Circuit-Id"])+"',2)) and radcheck.sw_port<>''"
print(sql)
cursor = conn.cursor(dictionary=True,buffered=True)
cursor.execute(sql);
row = cursor.fetchone()	
while row is not None:    
cnt=cnt+1
mac=param["Agent-Remote-Id"]
username=row["username"]
reply = reply+((str(row["attribute"]),str(row["value"])), )
row = cursor.fetchone()	          
# если так до сих пор IP не получен, то выдаю иего из гостевой сети..
if cnt==0:      
print ("Ни один из вариантов авторизации не сработал, получаю IP из гостевой сети..")
ip=func.GetGuestNet(conn)      
if ip!="": 
cnt=cnt+1;
reply = reply+(("Framed-IP-Address",str(ip)), )
# если совсем всё плохо, то Reject
if cnt==0:
conf = ( ("Auth-Type", "Reject"), ) 
else:
#если авторизация успешная (есть такой абонент), то запишем историю авторизации
if username!="":
func.InsertToHistory(conn,username,mac, reply);
conf = ( ("Auth-Type", "Accept"), )             
pprint (reply)
conn=None;
return radiusd.RLM_MODULE_OK, reply, conf
def preacct(p):
print ("*** preacct ***")
print (p)
return radiusd.RLM_MODULE_OK
def accounting(p):
print ("*** Аккаунтинг ***")
radiusd.radlog(radiusd.L_INFO, '*** radlog call in accounting (0) ***')  
print (p)
conn=func.GetConnectionMysql(mysql_host, mysql_username, mysql_password, mysql_base);
param=func.ConvertArrayToNames(p);
pprint(param)  
print("Удалим старые сессии (более 20 минут нет аккаунтинга)");
sql="delete from radacct where TIMESTAMPDIFF(minute,acctupdatetime,now())>20"
cursor = conn.cursor(dictionary=True,buffered=True)
cursor.execute(sql);
conn.commit()
print("Обновим/добавим информацию о сессии")
if (("Acct-Unique-Session-Id" in param) and ("User-Name" in param) and ("Framed-IP-Address" in param)):
sql='insert into radacct (radacctid,acctuniqueid,username,framedipaddress,acctstarttime) values (null,"'+str(param['Acct-Unique-Session-Id'])+'","'+str(param['User-Name'])+'","'+str(param['Framed-IP-Address'])+'",now()) ON DUPLICATE KEY update acctupdatetime=now()'
print(sql)
cursor = conn.cursor(dictionary=True,buffered=True)
cursor.execute(sql)
conn.commit()
conn=None;
return radiusd.RLM_MODULE_OK
def pre_proxy(p):
print ("*** pre_proxy ***")
print (p)
return radiusd.RLM_MODULE_OK
def post_proxy(p):
print ("*** post_proxy ***")
print (p)
return radiusd.RLM_MODULE_OK
def post_auth(p):
print ("*** post_auth ***")
print (p)
return radiusd.RLM_MODULE_OK
def recv_coa(p):
print ("*** recv_coa ***")
print (p)
return radiusd.RLM_MODULE_OK
def send_coa(p):
print ("*** send_coa ***")
print (p)
return radiusd.RLM_MODULE_OK
def detach():
print ("*** На этом всё детишечки ***")
return radiusd.RLM_MODULE_OK

func.py:

#!/usr/bin/python2.7
# coding=utf-8
import mysql.connector
from mysql.connector import Error
# Функция возвращает соединение с MySQL
def GetConnectionMysql(mysql_host, mysql_username, mysql_password, mysql_base):    
try:
conn = mysql.connector.connect(host=mysql_host,database=mysql_base,user=mysql_username,password=mysql_password)
if conn.is_connected(): print('---cоединение с БД '+mysql_base+' установлено')
except Error as e:
print("Ошибка: ",e);
exit(1);       
return conn
def ConvertArrayToNames(p):
mass={};
for z in p:
mass[z[0]]=z[1]
return mass
# Функция записывает историю соединения по известным данным
def InsertToHistory(conn,username,mac, reply):
print("--записываю для истории")
repl=ConvertArrayToNames(reply)
if "Framed-IP-Address" in repl:
sql='insert into radpostauth (username,reply,authdate,ip,mac,session_id,comment) values ("'+username+'","Access-Accept",now(),"'+str(repl["Framed-IP-Address"])+'","'+str(mac)+'","","")'
print(sql)
cursor = conn.cursor(dictionary=True,buffered=True)          
cursor.execute(sql);
conn.commit()
# Функция выдает последний по дате выдачи IP адрес из гостевой сети        
def GetGuestNet(conn):
ip="";id=0
sql="select * from guestnet order by dt limit 1"
print (sql)
cursor = conn.cursor(dictionary=True,buffered=True)          
cursor.execute(sql);
row = cursor.fetchone()	
while row is not None:    
ip=row["ip"]
id=row["id"]
row = cursor.fetchone()	          
if id>0:
sql="update guestnet set dt=now() where id="+str(id)
print (sql)
cursor = conn.cursor(dictionary=True,buffered=True)          
cursor.execute(sql);
conn.commit()
return ip         

radiusd.py:

#!/usr/bin/python2.7
# coding=utf-8
# from modules.h
RLM_MODULE_REJECT = 0
RLM_MODULE_FAIL = 1
RLM_MODULE_OK = 2
RLM_MODULE_HANDLED = 3
RLM_MODULE_INVALID = 4
RLM_MODULE_USERLOCK = 5
RLM_MODULE_NOTFOUND = 6
RLM_MODULE_NOOP = 7
RLM_MODULE_UPDATED = 8
RLM_MODULE_NUMCODES = 9
# from log.h
L_AUTH = 2
L_INFO = 3
L_ERR = 4
L_WARN = 5
L_PROXY = 6
L_ACCT = 7
L_DBG = 16
L_DBG_WARN = 17
L_DBG_ERR = 18
L_DBG_WARN_REQ = 19
L_DBG_ERR_REQ = 20
# log function
def radlog(level, msg):
import sys
sys.stdout.write(msg + 'n')
level = level

Як відаць па кодзе, мы ўсімі даступнымі спосабамі спрабуем ідэнтыфікаваць абанента па яго заведама вядомым абаненцкім MAC адрасам або звязку Option 82, і калі гэта не атрымліваецца, то выдаём самы стары з выкарыстаных калі-небудзь IP адрасоў з "гасцявой" сеткі. Засталося наладзіць скрыпт default у тэчцы sites-enabled, для таго каб патрэбныя фукцыі са скрыпту на python тузаліся ў пазначаныя моманты. Фактычна дастаткова файл прывесці да ўвазе:

дэфолт

server default {
listen {
type = auth
ipaddr = *
port = 0
limit {
max_connections = 16
lifetime = 0
idle_timeout = 30
}
}
listen {
ipaddr = *
port = 0
type = acct
limit {
}
}
listen {
type = auth
port = 0
limit {
max_connections = 1600
lifetime = 0
idle_timeout = 30
}
}
listen {
ipv6addr = ::
port = 0
type = acct
limit {
}
}
authorize {
python
filter_username
preprocess
expiration
logintime
}
authenticate {
Auth-Type PAP {
pap
python
}
Auth-Type CHAP {
chap
python
}
Auth-Type MS-CHAP {
mschap
python
}
eap
}
preacct {
preprocess
acct_unique
suffix
files
}
accounting {
python
exec
attr_filter.accounting_response
}
session {
}
post-auth {
update {
&reply: += &session-state:
}
exec
remove_reply_message_if_eap
Post-Auth-Type REJECT {
attr_filter.access_reject
eap
remove_reply_message_if_eap
}
Post-Auth-Type Challenge {
}
}
pre-proxy {
}
post-proxy {
eap
}
}

Спрабуем запусціць і паглядзець што прылятае ў адладкавы лог:

/usr/local/etc/rc.d/radiusd debug

Што яшчэ. Пры наладзе FreeRadius зручна тэставаць яго працу пры дапамозе ўтыліты radclient. Напрыклад аўтарызацыя:

echo "User-Name=4C:5E:0C:2E:7F:15,Agent-Remote-Id=0x9845623a8c98,Agent-Circuit-Id=0x00010006" | radclient -x  127.0.0.1:1812 auth testing123

Або акаўнтынг:

echo "User-Name=4C:5E:0C:2E:7F:15,Agent-Remote-Id=0x00030f26054a,Agent-Circuit-Id=0x00010002" | radclient -x  127.0.0.1:1813 acct testing123

Жадаю папярэдзіць, што ўжываць падобную схему і скрыпты "без змен" у "прамысловых" маштабах ну ніяк нельга. Як мінімум кідаюцца ў вочы:

  • магчымая "падробка" MAC адрасы. Дастаткова абаненту прапісаць сабе чужы MAC і будуць праблемы
  • логіка выдачы гасцявых сетак ніжэй за ўсякую крытыку. Няма нават праверкі "у можа ўжо ёсць кліенты з выдадзеным такім IP адрасам?"

Гэта проста «рашэнне на каленцы», для таго каб працавала канкрэтна ў маіх умовах, не больш за тое. Не судзіце строга 😉

Крыніца: habr.com

Дадаць каментар