FreeRadius-аас DHCP-ээр дамжуулан сүлжээний тохиргоо

FreeRadius-аас DHCP-ээр дамжуулан сүлжээний тохиргоо
Захиалагчдад IP хаяг олгох ажлыг зохион байгуулах даалгавар ирлээ. Асуудлын нөхцөл:

  • Бид танд тусгай сервер өгөхгүй - та үүнийг хийх болно 😉
  • Захиалагч DHCP-ээр сүлжээний тохиргоог хүлээн авах ёстой
  • Сүлжээ нь нэг төрлийн бус байна. Үүнд PON төхөөрөмж, тохируулсан 82-р сонголттой ердийн унтраалга, халуун цэг бүхий WiFi суурь орно
  • Хэрэв өгөгдөл нь IP олгох нөхцөлийн аль нэгэнд хамаарахгүй бол та "зочин" сүлжээнээс IP өгөх ёстой.

Сайн тал нь: FreeBSD дээр "ажиллах" боломжтой сервер байсаар байгаа ч энэ нь "энэ сүлжээнд" биш "хол" ;).

Микротик гэдэг гайхалтай төхөөрөмж бас бий. Сүлжээний ерөнхий диаграм нь дараах байдалтай байна.

FreeRadius-аас DHCP-ээр дамжуулан сүлжээний тохиргоо

Хэсэг хугацааны дараа бодлын дараа FreeRadius-ийг ашиглан сүлжээний тохиргоог захиалагчдад олгохоор шийдсэн. Зарчмын хувьд схем нь ердийн зүйл юм: бид Microtick дээр DHCP сервер, түүн дээр Radius Client-ийг идэвхжүүлдэг. Бид DHCP сервер -> Radius Client -> Radius серверийн холболтыг тохируулдаг.

Энэ нь хэцүү биш юм шиг байна. Гэхдээ! Чөтгөр нь нарийн ширийн зүйлд байдаг. Тухайлбал:

  • Энэ схемийг ашиглан PON OLT-д зөвшөөрөл олгохдоо толгойн хэсгийн MAC хаягтай тэнцэх хэрэглэгчийн нэр, MAC PON Onu-той тэнцүү Agent-Circuit-Id, хоосон нууц үг бүхий хүсэлтийг FreeRadius руу илгээдэг.
  • 82-р сонголттой шилжүүлэгчээс зөвшөөрөл олгох үед FreeRadius нь захиалагчийн төхөөрөмжийн MAC-тай тэнцэх хоосон хэрэглэгчийн нэр бүхий хүсэлтийг хүлээн авч, Agent-Circuit-Id болон Agent-Remote-Id нэмэлт шинж чанаруудаар дүүргэсэн бөгөөд дахин MAC-ийг агуулсан. реле шилжүүлэгч болон захиалагч холбогдсон порт.
  • WiFI цэг бүхий зарим захиалагчдад PAP-CHAP протоколоор дамжуулан зөвшөөрөл олгодог
  • WIFI цэгийн зарим захиалагчид WIFI цэгийн MAC хаягтай тэнцэх хэрэглэгчийн нэрээр нууц үггүй зөвшөөрөл авдаг.

Түүхэн мэдээлэл: DHCP дахь "Хувилбар 82" гэж юу вэ

Эдгээр нь Agent-Circuit-Id болон Agent-Remote-Id талбарт нэмэлт мэдээллийг дамжуулах боломжийг олгодог DHCP протоколын нэмэлт сонголтууд юм. Ихэвчлэн реле шилжүүлэгчийн MAC хаяг болон захиалагч холбогдсон портыг дамжуулахад ашигладаг. PON төхөөрөмж эсвэл WIFI суурь станцын хувьд Agent-Circuit-Id талбарт хэрэгтэй мэдээлэл байхгүй (захиалагч порт байхгүй). Энэ тохиолдолд DHCP үйлдлийн ерөнхий схем дараах байдалтай байна.

FreeRadius-аас DHCP-ээр дамжуулан сүлжээний тохиргоо

Алхам алхмаар энэ схем дараах байдлаар ажиллана.

  1. Хэрэглэгчийн төхөөрөмж нь сүлжээний тохиргоог авахын тулд DHCP өргөн нэвтрүүлгийн хүсэлтийг гаргадаг
  2. Захиалагчийн төхөөрөмжийг шууд холбосон төхөөрөмж (жишээлбэл, унтраалга, WiFi эсвэл PON суурь станц) энэ пакетыг "тасалж" өөрчилснөөр 82-р сонголт болон Реле агент IP хаягийн нэмэлт сонголтуудыг оруулан цааш дамжуулдаг. сүлжээ.
  3. DHCP сервер нь хүсэлтийг хүлээн авч, хариу үүсгэж, реле төхөөрөмж рүү илгээдэг
  4. Реле төхөөрөмж нь хариу өгөх пакетыг захиалагч төхөөрөмж рүү дамжуулдаг

Мэдээжийн хэрэг, энэ бүхэн тийм ч амархан ажиллахгүй тул та сүлжээний төхөөрөмжөө зохих ёсоор тохируулах хэрэгтэй.

FreeRadius суулгаж байна

Мэдээжийн хэрэг, үүнийг FreeRadius тохиргооны тохиргоогоор хийж болно, гэхдээ энэ нь хэцүү бөгөөд тодорхойгүй байна ... ялангуяа N сарын дараа тийшээ очиход "бүх зүйл ажилладаг". Тиймээс бид Python дээр FreeRadius-д зориулсан зөвшөөрлийн модулийг бичихээр шийдсэн. Бид MySQL мэдээллийн сангаас зөвшөөрлийн өгөгдлийг авах болно. Түүний бүтцийг тайлбарлах нь утгагүй бөгөөд хүн бүр үүнийг "өөртөө зориулж" хийх болно. Ялангуяа би FreeRadius-д зориулсан sql модулийн хамт санал болгож буй бүтцийг авч, нэвтрэх-нууц үгээс гадна захиалагч бүрийн хувьд mac болон порт талбарыг нэмж бага зэрэг өөрчилсөн.

Тиймээс эхлээд 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

FreeRadius-ийн python модулийн тохиргоонд та 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']
>

Хэрэв та энэ алхамыг хийхгүй бол FreeRadius-аас эхлүүлсэн python дээр бичигдсэн скриптүүд импортын жагсаалтад орсон модулиудыг олохгүй. Нэмж дурдахад та модулийн тохиргоонд дуудлага хийх зөвшөөрөл, нягтлан бодох бүртгэлийн функцуудыг арилгах хэрэгтэй. Жишээлбэл, энэ модуль дараах байдалтай байна.

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 хаягууд эсвэл Сонголт 82-ын хослолоор боломжтой бүх аргыг ашиглан захиалагчийг тодорхойлохыг оролдож байгаа бөгөөд хэрэв энэ нь ажиллахгүй бол бид "зочин"-оос урьд өмнө хэрэглэж байсан хамгийн эртний IP хаягийг гаргадаг. ” сүлжээ. Сайтуудыг идэвхжүүлсэн хавтсанд өгөгдмөл скриптийг тохируулахад л үлддэг бөгөөд ингэснээр 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 хаягтай үйлчлүүлэгчид байгаа болов уу?" гэсэн чек ч байхгүй.

Энэ бол зүгээр л миний нөхцөлд ажиллахад зориулагдсан "жигнэмэг таслагчийн шийдэл" бөгөөд өөр юу ч биш. Битгий хатуу шүүж бай 😉

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх