DHCP vasitəsilə FreeRadius-dan şəbəkə parametrləri

DHCP vasitəsilə FreeRadius-dan şəbəkə parametrləri
Abunəçilərə IP ünvanlarının verilməsini təşkil etmək tapşırığı gəldi. Problemin şərtləri:

  • Sizə avtorizasiya üçün ayrıca server verməyəcəyik - siz edəcəksiniz 😉
  • Abunəçilər DHCP vasitəsilə şəbəkə parametrlərini almalıdırlar
  • Şəbəkə heterojendir. Bura PON avadanlığı və konfiqurasiya edilmiş Variant 82 ilə müntəzəm açarlar və qaynar nöqtələri olan WiFi əsasları daxildir
  • Məlumatlar IP-nin verilməsi üçün şərtlərin heç birinə uyğun gəlmirsə, "qonaq" şəbəkəsindən IP verməlisiniz.

Yaxşı tərəfi: FreeBSD-də hələ də “işləyə” bilən bir server var, lakin o, “bu şəbəkədə” deyil, “uzaqda” ;).

Mikrotik adlı gözəl bir cihaz da var. Ümumi şəbəkə diaqramı belədir:

DHCP vasitəsilə FreeRadius-dan şəbəkə parametrləri

Bir az fikirləşdikdən sonra abunəçilərə şəbəkə parametrlərini vermək üçün FreeRadius-dan istifadə etmək qərara alındı. Prinsipcə, sxem adidir: biz Microtick-də DHCP serverini və onun üzərində Radius Client-i işə salırıq. Biz DHCP server -> Radius Client -> Radius server bağlantısını konfiqurasiya edirik.

Çətin görünmür. Amma! Şeytan təfərrüatlardadır. Məhz:

  • Bu sxemdən istifadə edərək PON OLT-ə icazə verərkən FreeRadius-a sorğu başlığın MAC ünvanına bərabər İstifadəçi Adı, MAC PON Onu ilə bərabər Agent-Circuit-Id və boş parol ilə göndərilir.
  • Seçim 82 olan açarlardan avtorizasiya edərkən, FreeRadius abunəçinin cihazının MAC-inə bərabər boş İstifadəçi Adı ilə sorğu alır və müvafiq olaraq Agent-Circuit-Id və Agent-Remote-Id əlavə atributları ilə doldurulur. rele keçidi və abunəçinin qoşulduğu port.
  • Wi-Fi nöqtələri olan bəzi abunəçilərə PAP-CHAP protokolları vasitəsilə icazə verilir
  • WIFI nöqtələrindən olan bəzi abunəçilərə parol olmadan WIFI nöqtəsinin MAC ünvanına bərabər İstifadəçi Adı ilə icazə verilir.

Tarixi məlumat: DHCP-də "Seçim 82" nədir

Bunlar, məsələn, Agent-Circuit-Id və Agent-Remote-Id sahələrində əlavə məlumat ötürməyə imkan verən DHCP protokolu üçün əlavə seçimlərdir. Adətən rele keçidinin MAC ünvanını və abunəçinin qoşulduğu portu ötürmək üçün istifadə olunur. PON avadanlığı və ya WIFI baza stansiyaları vəziyyətində, Agent-Circuit-Id sahəsində faydalı məlumat yoxdur (abunəçi portu yoxdur). Bu vəziyyətdə DHCP əməliyyatının ümumi sxemi aşağıdakı kimidir:

DHCP vasitəsilə FreeRadius-dan şəbəkə parametrləri

Addım-addım bu sxem belə işləyir:

  1. İstifadəçi avadanlığı şəbəkə parametrlərini əldə etmək üçün DHCP yayım sorğusu göndərir
  2. Abunəçi avadanlığının birbaşa qoşulduğu cihaz (məsələn, kommutator, WiFi və ya PON baza stansiyası) bu paketi “kesdirir” və onu dəyişdirir, əlavə variant 82 və Relay agent IP ünvanını daxil edir və onu daha da ötürür. şəbəkə.
  3. DHCP server sorğunu qəbul edir, cavab yaradır və onu relay cihazına göndərir
  4. Rele cihazı cavab paketini abunəçi cihazına ötürür

Əlbəttə ki, hər şey o qədər də asan işləmir; şəbəkə avadanlıqlarınızı buna uyğun konfiqurasiya etməlisiniz.

FreeRadius quraşdırılması

Əlbəttə ki, buna FreeRadius konfiqurasiya parametrləri ilə nail olmaq olar, lakin bu çətin və aydın deyil... xüsusən də N aydan sonra ora getdiyiniz zaman və “hər şey işləyir”. Buna görə də Python-da FreeRadius üçün öz avtorizasiya modulumuzu yazmağa qərar verdik. MySQL verilənlər bazasından avtorizasiya məlumatlarını alacağıq. Onun strukturunu təsvir etməyin mənası yoxdur, hər halda, hər kəs bunu "özləri üçün" edəcək. Xüsusilə, FreeRadius üçün sql modulu ilə təklif olunan strukturu götürdüm və login-paroldan əlavə hər bir abunəçi üçün mac və port sahəsi əlavə edərək onu bir az dəyişdirdim.

Beləliklə, əvvəlcə FreeRadius-u quraşdırın:

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

Parametrlərdə quraşdırmaq üçün seçin:

DHCP vasitəsilə FreeRadius-dan şəbəkə parametrləri

Python moduluna simvolik əlaqə yaradırıq (məsələn, onu "yandırın"):

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

Python üçün əlavə modul quraşdıraq:

pip install mysql-connector

FreeRadius üçün python modulu parametrlərində siz python_path dəyişənində modul axtarış yollarını təyin etməlisiniz. Məsələn, məndə bu var:

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 tərcüməçisini işə salaraq və əmrləri daxil etməklə yolları öyrənə bilərsiniz:

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']
>

Bu addımı atmasanız, o zaman python-da yazılmış və FreeRadius tərəfindən işə salınan skriptlər idxalda qeyd olunan modulları tapa bilməyəcək. Bundan əlavə, modul parametrlərində zəng icazəsi və uçot funksiyalarını şərh etməlisiniz. Məsələn, bu modul belə görünür:

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 skripti (və bütün digərləri) /usr/local/etc/raddb/mods-config/python-da yerləşdirilməlidir. Məndə cəmi üç skript var.

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

Koddan göründüyü kimi, biz abunəçinin məlum MAC ünvanları və ya Variant 82 kombinasiyası ilə bütün mövcud üsullardan istifadə edərək onu müəyyən etməyə çalışırıq və əgər bu işləmirsə, o zaman “qonaq”dan indiyə qədər istifadə edilmiş ən köhnə İP ünvanını veririk. ” şəbəkəsi. Qalan şey saytların aktivləşdirdiyi qovluqda standart skripti konfiqurasiya etməkdir ki, python skriptindən lazımi funksiyalar təyin olunmuş anlarda sönəcək. Əslində, faylı forma gətirmək kifayətdir:

default

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

Gəlin onu işə salmağa çalışaq və sazlama jurnalına nə daxil olduğunu görək:

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

Başqa. FreeRadius-u qurarkən, onun işini radclient yardım proqramından istifadə edərək yoxlamaq rahatdır. Məsələn, icazə:

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

Və ya hesab:

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

Sizi xəbərdar etmək istəyirəm ki, belə bir sxem və skriptləri "sənaye" miqyasında "dəyişikliklər olmadan" istifadə etmək tamamilə mümkün deyil. Ən azı nəzərə çarpan:

  • MAC ünvanını “saxta” etmək mümkündür. Abunəçinin başqasının MAC-ını qeydiyyatdan keçirməsi kifayətdir və problemlər yaranacaq
  • qonaq şəbəkələrinin verilməsinin məntiqi tənqiddən kənardır. Hətta "bəlkə eyni IP ünvanlı müştərilər var?" Yoxlaması belə yoxdur.

Bu, sadəcə mənim şərtlərimdə işləmək üçün nəzərdə tutulmuş “peçenye kəsici həll”dir, başqa heç nə yoxdur. Ciddi mühakimə etməyin 😉

Mənbə: www.habr.com

Добавить комментарий