Мрежна подешавања са ФрееРадиус-а преко ДХЦП-а

Мрежна подешавања са ФрееРадиус-а преко ДХЦП-а
Стигао је задатак да договоримо издавање ИП адреса претплатницима. Услови проблема:

  • Нећемо вам дати посебан сервер за ауторизацију - снаћи ћете се 😉
  • Претплатници морају да приме мрежна подешавања преко ДХЦП-а
  • Мрежа је хетерогена. Ово укључује ПОН опрему и обичне прекидаче са конфигурисаном опцијом 82 и ВиФи базе са приступним тачкама
  • Ако подаци не потпадају под било који од услова за издавање ИП адресе, морате издати ИП са мреже „гост“.

Добра страна: још увек постоји сервер на ФрееБСД-у који може да „ради“, али је „далеко“ ;), а не „право на овој мрежи“.

Постоји и диван уређај који се зове Микротик. Општи дијаграм мреже је отприлике овако:

Мрежна подешавања са ФрееРадиус-а преко ДХЦП-а

Након неког размишљања, одлучено је да се користи ФрееРадиус за издавање мрежних подешавања претплатницима. У принципу, шема је уобичајена: омогућавамо ДХЦП сервер на Мицротицк-у и Радиус Цлиент на њему. Конфигуришемо ДХЦП сервер -> Радиус клијент -> Радиус сервер везу.

Не изгледа тешко. Али! Ђаво је у детаљима. Наиме:

  • Када се ауторизује ПОН ОЛТ коришћењем ове шеме, ФрееРадиус-у се шаље захтев са корисничким именом једнаким МАЦ адреси главног уређаја, Агент-Цирцуит-Ид једнаким МАЦ ПОН Ону и празном лозинком.
  • Приликом ауторизације са прекидача са опцијом 82, ФрееРадиус прима захтев са празним корисничким именом једнаким МАЦ-у претплатничког уређаја и испуњеним додатним атрибутима Агент-Цирцуит-Ид и Агент-Ремоте-Ид који садрже, респективно, поново МАЦ од релејни прекидач и порт на који је претплатник прикључен.
  • Неки претплатници са ВиФИ тачкама су овлашћени преко ПАП-ЦХАП протокола
  • Неки претплатници са ВИФИ тачака су ауторизовани са корисничким именом једнаким МАЦ адреси ВИФИ тачке, без лозинке.

Историјска позадина: шта је „Опција 82“ у ДХЦП-у

Ово су додатне опције за ДХЦП протокол које вам омогућавају да пренесете додатне информације, на пример у пољима Агент-Цирцуит-Ид и Агент-Ремоте-Ид. Обично се користи за пренос МАЦ адресе релејног прекидача и порта на који је претплатник повезан. У случају ПОН опреме или ВИФИ базних станица, поље Агент-Цирцуит-Ид не садржи корисне информације (нема претплатничког порта). Општа шема рада ДХЦП-а у овом случају је следећа:

Мрежна подешавања са ФрееРадиус-а преко ДХЦП-а

Корак по корак ова шема функционише овако:

  1. Корисничка опрема поставља ДХЦП захтев за емитовање да би добила мрежна подешавања
  2. Уређај (на пример, комутатор, ВиФи или ПОН базна станица) на који је претплатничка опрема директно повезана „пресреће“ овај пакет и мења га, уводећи додатне опције Опција 82 и ИП адресу Релеј агента, и даље га преноси преко мрежа.
  3. ДХЦП сервер прихвата захтев, генерише одговор и шаље га релејном уређају
  4. Релејни уређај прослеђује пакет одговора до претплатничког уређаја

Наравно, све то не функционише тако лако; потребно је да у складу са тим конфигуришете своју мрежну опрему.

Инсталирање ФрееРадиус-а

Наравно, ово се може постићи са поставкама конфигурације ФрееРадиус-а, али је тешко и нејасно... посебно када одете тамо након Н месеци и „све ради“. Стога смо одлучили да напишемо сопствени модул за ауторизацију за ФрееРадиус у Питхон-у. Узећемо податке о ауторизацији из МиСКЛ базе података. Нема смисла описивати његову структуру, ионако ће је свако направити „за себе“. Конкретно, узео сам структуру која се нуди са скл модулом за ФрееРадиус и мало је променио додавањем поља за мац и порт за сваког претплатника, поред лозинке за пријаву.

Дакле, прво, инсталирајте ФрееРадиус:

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

У подешавањима изаберите да инсталирате:

Мрежна подешавања са ФрееРадиус-а преко ДХЦП-а

Правимо симболичку везу до питхон модула (тј. „укључимо“ га):

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

Хајде да инсталирамо додатни модул за Питхон:

pip install mysql-connector

У подешавањима питхон модула за ФрееРадиус, потребно је да наведете путање за претрагу модула у променљивој питхон_патх. На пример, имам ово:

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"

Путања можете сазнати покретањем питхон интерпретера и уносом команди:

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 {
    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}

}

Ворк.пи скрипта (и све остале) морају бити смештене у /уср/лоцал/етц/раддб/модс-цонфиг/питхон. Имам три скрипте укупно.

ворк.пи:

#!/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

фунц.пи:

#!/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         

радиусд.пи:

#!/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

Као што видите из кода, покушавамо да идентификујемо претплатника користећи све доступне методе према његовим познатим МАЦ адресама претплатника или комбинацији Опција 82, а ако то не успије, онда издајемо најстарију ИП адресу икада коришћену од „гост ” мрежа. Остаје само да конфигуришете подразумевану скрипту у фасцикли са омогућеним сајтовима, тако да ће се неопходне функције из питхон скрипте трзати у одређеним тренуцима. У ствари, довољно је довести датотеку у форму:

Уобичајено

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

Шта још. Када подешавате ФрееРадиус, згодно је тестирати његов рад помоћу услужног програма радцлиент. На пример овлашћење:

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

Желим да вас упозорим да је апсолутно немогуће користити такву шему и скрипте „без промена“ у „индустријској“ скали. Барем приметно:

  • могуће је „лажирати“ МАЦ адресу. Довољно је да претплатник региструје туђи МАЦ и биће проблема
  • логика издавања мрежа гостију је ван сваке критике. Не постоји чак ни провера „можда већ постоје клијенти са истом ИП адресом?“

Ово је само „решење за резање колачића“ дизајнирано да ради посебно у мојим условима, ништа више. Не судите строго 😉

Извор: ввв.хабр.цом

Додај коментар