Setări de rețea de la FreeRadius prin DHCP

Setări de rețea de la FreeRadius prin DHCP
A venit sarcina de a aranja emiterea de adrese IP către abonați. Condițiile problemei:

  • Nu vă vom oferi un server separat pentru autorizare - vă veți descurca 😉
  • Abonații trebuie să primească setări de rețea prin DHCP
  • Rețeaua este eterogenă. Aceasta include echipamente PON și comutatoare obișnuite cu opțiunea 82 configurată și baze WiFi cu hotspot-uri
  • Dacă datele nu se încadrează în niciuna dintre condițiile de emitere a unui IP, trebuie să emiteți un IP din rețeaua „oaspete”

Pe partea bună: există încă un server pe FreeBSD care poate „funcționa”, dar este „departe” ;), nu „chiar în această rețea”.

Există și un dispozitiv minunat numit Mikrotik. Diagrama generală a rețelei este cam așa:

Setări de rețea de la FreeRadius prin DHCP

După câteva gânduri, s-a decis să se utilizeze FreeRadius pentru a emite setări de rețea abonaților. În principiu, schema este obișnuită: activăm serverul DHCP pe Microtick și Radius Client pe acesta. Configuram serverul DHCP -> Client Radius -> Conexiunea la serverul Radius.

Nu pare dificil. Dar! Diavolul sta in detalii. Și anume:

  • Când se autorizează un PON OLT folosind această schemă, o solicitare este trimisă către FreeRadius cu un nume de utilizator egal cu adresa MAC a headend-ului, un agent-circuit-id egal cu MAC PON Onu și o parolă goală.
  • La autorizarea de la comutatoarele cu opțiunea 82, FreeRadius primește o solicitare cu un User-Name gol egal cu MAC-ul dispozitivului abonatului și completat cu atribute suplimentare Agent-Circuit-Id și Agent-Remote-Id care conțin, respectiv, din nou MAC-ul de comutatorul releului și portul la care este conectat abonatul.
  • Unii abonați cu puncte WiFI sunt autorizați prin protocoale PAP-CHAP
  • Unii abonați de la punctele WIFI sunt autorizați cu un nume de utilizator egal cu adresa MAC a punctului WIFI, fără o parolă.

Context istoric: ce este „Opțiunea 82” în DHCP

Acestea sunt opțiuni suplimentare pentru protocolul DHCP care vă permit să transferați informații suplimentare, de exemplu în câmpurile Agent-Circuit-Id și Agent-Remote-Id. Utilizat de obicei pentru a transmite adresa MAC a comutatorului releu și portul la care este conectat abonatul. În cazul echipamentelor PON sau al stațiilor de bază WIFI, câmpul Agent-Circuit-Id nu conține informații utile (nu există un port de abonat). Schema generală a funcționării DHCP în acest caz este următoarea:

Setări de rețea de la FreeRadius prin DHCP

Pas cu pas această schemă funcționează astfel:

  1. Echipamentul utilizatorului face o cerere de difuzare DHCP pentru a obține setări de rețea
  2. Dispozitivul (de exemplu, un comutator, WiFi sau stație de bază PON) la care echipamentul abonatului este conectat direct „interceptează” acest pachet și îl schimbă, introducând opțiuni suplimentare Opțiunea 82 și adresa IP a agentului de retransmisie în el și îl transmite mai departe. rețeaua.
  3. Serverul DHCP acceptă cererea, generează un răspuns și îl trimite către dispozitivul de releu
  4. Dispozitivul releu transmite pachetul de răspuns către dispozitivul abonat

Desigur, totul nu funcționează atât de ușor; trebuie să vă configurați echipamentul de rețea în consecință.

Instalarea FreeRadius

Desigur, acest lucru poate fi realizat cu setările de configurare FreeRadius, dar este dificil și neclar... mai ales când mergi acolo după N luni și „totul funcționează”. Prin urmare, am decis să scriem propriul nostru modul de autorizare pentru FreeRadius în Python. Vom prelua datele de autorizare din baza de date MySQL. Nu are rost să-i descriem structura; oricum, fiecare o va face „pentru ei înșiși”. În special, am luat structura care este oferită cu modulul sql pentru FreeRadius și am schimbat-o ușor prin adăugarea unui câmp mac și port pentru fiecare abonat, pe lângă parola de conectare.

Deci, mai întâi, instalați FreeRadius:

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

În setări, selectați pentru a instala:

Setări de rețea de la FreeRadius prin DHCP

Facem o legătură simbolică către modulul python (adică „porniți-l”):

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

Să instalăm un modul suplimentar pentru python:

pip install mysql-connector

În setările modulului python pentru FreeRadius, trebuie să specificați căile de căutare ale modulelor în variabila python_path. De exemplu am asta:

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"

Puteți afla căile lansând interpretul Python și introducând comenzile:

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

Dacă nu faci acest pas, atunci scripturile scrise în python și lansate de FreeRadius nu vor găsi modulele care sunt listate în import. În plus, trebuie să decomentați funcțiile pentru autorizarea apelurilor și contabilitate din setările modulului. De exemplu, acest modul arată astfel:

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}

}

Scriptul work.py (și toate celelalte) trebuie plasat în /usr/local/etc/raddb/mods-config/python Am trei scripturi în total.

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

După cum puteți vedea din cod, încercăm să identificăm abonatul folosind toate metodele disponibile prin adresele MAC cunoscute ale abonatului sau combinația de opțiune 82, iar dacă aceasta nu funcționează, atunci emitem cea mai veche adresă IP folosită vreodată de la „oaspete”. ” rețea. Tot ce rămâne este să configurați scriptul implicit în folderul activat pentru site-uri, astfel încât funcțiile necesare din scriptul python să se zvâcnească în momentele desemnate. De fapt, este suficient să aduceți fișierul la forma:

lipsă

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

Să încercăm să-l rulăm și să vedem ce apare în jurnalul de depanare:

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

Ce altceva. Când configurați FreeRadius, este convenabil să testați funcționarea acestuia folosind utilitarul radclient. De exemplu autorizare:

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

Sau cont:

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

Vreau să vă avertizez că este absolut imposibil să folosiți o astfel de schemă și scripturi „fără modificări” la scară „industrială”. Cel puțin vizibil:

  • este posibil să „falsăm” adresa MAC. Este suficient ca abonatul să înregistreze MAC-ul altcuiva și vor apărea probleme
  • logica emiterii rețelelor de invitați este dincolo de critică. Nici măcar nu există o verificare „poate că există deja clienți cu aceeași adresă IP?”

Aceasta este doar o „soluție de tăiat cookie-uri” concepută să funcționeze special în condițiile mele, nimic mai mult. Nu judeca strict 😉

Sursa: www.habr.com

Adauga un comentariu