DHCP orqali FreeRadius-dan tarmoq sozlamalari

DHCP orqali FreeRadius-dan tarmoq sozlamalari
Abonentlarga IP-manzillar berishni tashkil etish vazifasi keldi. Muammoning shartlari:

  • Biz sizga avtorizatsiya uchun alohida server bermaymiz - buni qilasiz 😉
  • Abonentlar DHCP orqali tarmoq sozlamalarini olishlari kerak
  • Tarmoq heterojendir. Bunga PON uskunalari va konfiguratsiya qilingan 82-variantli oddiy kalitlar va ulanish nuqtalari bo'lgan WiFi bazalari kiradi
  • Agar ma'lumotlar IP-ni berish shartlaridan biriga to'g'ri kelmasa, siz "mehmon" tarmog'idan IP-ni berishingiz kerak.

Yaxshi tomoni: FreeBSD-da hali ham “ishlay oladigan” server mavjud, lekin u “uzoqda” ;), “to'g'ri shu tarmoqda” emas.

Mikrotik degan ajoyib qurilma ham bor. Umumiy tarmoq diagrammasi quyidagicha:

DHCP orqali FreeRadius-dan tarmoq sozlamalari

Biroz o'ylangandan so'ng, abonentlarga tarmoq sozlamalarini berish uchun FreeRadius-dan foydalanishga qaror qilindi. Asosan, sxema odatiy: biz Microtick-da DHCP serverini va unga Radius Client-ni yoqamiz. Biz DHCP server -> Radius Client -> Radius server ulanishini sozlaymiz.

Bu qiyin emasdek. Lekin! Shayton tafsilotlarda. Aynan:

  • Ushbu sxemadan foydalangan holda PON OLT ga avtorizatsiya qilinganda FreeRadius-ga so'rov sarlavhaning MAC manziliga teng foydalanuvchi nomi, MAC PON Onu ga teng Agent-Circuit-Id va bo'sh parol bilan yuboriladi.
  • 82-variantli kalitlardan avtorizatsiya qilishda FreeRadius abonent qurilmasining MAC-ga teng bo'sh foydalanuvchi nomi bilan so'rovni oladi va mos ravishda yana MAC-ni o'z ichiga olgan Agent-Circuit-Id va Agent-Remote-Id qo'shimcha atributlari bilan to'ldiriladi. o'rni kaliti va abonent ulangan port.
  • Wi-Fi nuqtalari bo'lgan ba'zi abonentlarga PAP-CHAP protokollari orqali ruxsat berilgan
  • WIFI nuqtalarining ba'zi abonentlari parolsiz, WIFI nuqtasining MAC manziliga teng bo'lgan foydalanuvchi nomi bilan avtorizatsiya qilinadi.

Tarixiy ma'lumot: DHCP-da "Variant 82" nima

Bular DHCP protokoli uchun qo'shimcha ma'lumotlarni uzatish imkonini beruvchi qo'shimcha imkoniyatlardir, masalan, Agent-Circuit-Id va Agent-Remote-Id maydonlarida. Odatda o'rni kalitining MAC manzilini va abonent ulangan portni uzatish uchun ishlatiladi. PON uskunasi yoki WIFI tayanch stantsiyalari bo'lsa, Agent-Circuit-Id maydonida foydali ma'lumotlar mavjud emas (abonent porti yo'q). Bu holda DHCP ishlashining umumiy sxemasi quyidagicha:

DHCP orqali FreeRadius-dan tarmoq sozlamalari

Bosqichma-bosqich ushbu sxema quyidagicha ishlaydi:

  1. Foydalanuvchi uskunasi tarmoq sozlamalarini olish uchun DHCP eshittirish so'rovini yuboradi
  2. Abonent uskunasi to'g'ridan-to'g'ri ulangan qurilma (masalan, kommutator, WiFi yoki PON tayanch stantsiyasi) ushbu paketni "tushib oladi" va uni o'zgartiradi, unga qo'shimcha 82-variant va Relay agent IP-manzilini kiritadi va uni yana uzatadi. tarmoq.
  3. DHCP serveri so'rovni qabul qiladi, javob hosil qiladi va uni o'tkazuvchi qurilmaga yuboradi
  4. Rele qurilmasi javob paketini abonent qurilmasiga uzatadi

Albatta, hammasi oson ishlamaydi, tarmoq uskunangizni shunga mos ravishda sozlashingiz kerak.

FreeRadius o'rnatilmoqda

Albatta, bunga FreeRadius konfiguratsiya sozlamalari bilan erishish mumkin, lekin bu qiyin va tushunarsiz ... ayniqsa, N oydan keyin u erga borganingizda va "hamma narsa ishlaydi". Shuning uchun biz Python-da FreeRadius uchun avtorizatsiya modulimizni yozishga qaror qildik. Biz MySQL ma'lumotlar bazasidan avtorizatsiya ma'lumotlarini olamiz. Uning tuzilishini tavsiflashning ma'nosi yo'q, baribir, har kim uni "o'zi uchun" qiladi. Xususan, men FreeRadius uchun sql moduli bilan taklif qilingan tuzilmani oldim va login-paroldan tashqari har bir abonent uchun mac va port maydonini qo'shish orqali uni biroz o'zgartirdim.

Shunday qilib, birinchi navbatda, FreeRadius-ni o'rnating:

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

Sozlamalarda o'rnatishni tanlang:

DHCP orqali FreeRadius-dan tarmoq sozlamalari

Biz python moduliga simli havola qilamiz (ya'ni uni "yoqish"):

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

Python uchun qo'shimcha modul o'rnatamiz:

pip install mysql-connector

FreeRadius uchun python moduli sozlamalarida siz python_path o'zgaruvchisida modul qidirish yo'llarini belgilashingiz kerak. Masalan, menda bu bor:

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 tarjimonini ishga tushirish va buyruqlarni kiritish orqali yo'llarni bilib olishingiz mumkin:

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

Agar siz bu qadamni qilmasangiz, u holda python-da yozilgan va FreeRadius tomonidan ishga tushirilgan skriptlar import ro'yxatidagi modullarni topa olmaydi. Bundan tashqari, siz modul sozlamalarida qo'ng'iroq qilish uchun avtorizatsiya va buxgalteriya funktsiyalarini izohlashingiz kerak. Masalan, ushbu modul quyidagicha ko'rinadi:

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 (va boshqa barcha) /usr/local/etc/raddb/mods-config/python ichiga joylashtirilishi kerak. Menda jami uchta skript bor.

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 ko'rinib turibdiki, biz abonentning ma'lum bo'lgan MAC manzillari yoki Variant 82 kombinatsiyasi bo'yicha barcha mavjud usullardan foydalangan holda abonentni aniqlashga harakat qilmoqdamiz va agar bu ishlamasa, biz "mehmon" dan foydalanilgan eng qadimgi IP manzilni beramiz. ” tarmoq. Python skriptining kerakli funksiyalari belgilangan daqiqalarda o'chib ketishi uchun saytlar yoqilgan papkada standart skriptni sozlash qoladi. Aslida, faylni shaklga keltirish kifoya:

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

Keling, uni ishga tushirishga harakat qilaylik va disk raskadrovka jurnaliga nima kirganini ko'ramiz:

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

Nima yana. FreeRadius-ni o'rnatishda radclient yordam dasturi yordamida uning ishlashini sinab ko'rish qulay. Masalan, avtorizatsiya:

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

Yoki hisob:

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

Men sizni ogohlantirmoqchimanki, bunday sxema va skriptlarni "sanoat" miqyosida "o'zgarishsiz" ishlatish mutlaqo mumkin emas. Hech bo'lmaganda sezilarli:

  • MAC manzilini "soxta" qilish mumkin. Abonentga boshqa birovning MAC-ni ro'yxatdan o'tkazish kifoya va muammolar paydo bo'ladi
  • mehmon tarmoqlarini chiqarish mantig'i tanqiddan tashqarida. Hatto "ehtimol bir xil IP-manzilga ega bo'lgan mijozlar bormi?" Degan tekshiruv ham yo'q.

Bu shunchaki mening sharoitimda ishlash uchun mo'ljallangan "cookie-to'sar yechimi", boshqa hech narsa emas. Qattiq hukm qilmang 😉

Manba: www.habr.com

a Izoh qo'shish