Mrežne postavke iz FreeRadiusa preko DHCP-a

Mrežne postavke iz FreeRadiusa preko DHCP-a
Stigao je zadatak da dogovorimo izdavanje IP adresa pretplatnicima. Uslovi problema:

  • Nećemo vam dati poseban server za autorizaciju - snaći ćete se 😉
  • Pretplatnici moraju primiti mrežna podešavanja putem DHCP-a
  • Mreža je heterogena. Ovo uključuje PON opremu i obične prekidače s konfiguriranom opcijom 82 i WiFi baze sa hotspotovima
  • Ako podaci ne potpadaju pod bilo koji od uslova za izdavanje IP-a, morate izdati IP sa „gostjuće“ mreže

Dobra strana: još uvijek postoji server na FreeBSD-u koji može “raditi”, ali je “daleko” ;), a ne “pravo na ovoj mreži”.

Tu je i divan uređaj koji se zove Mikrotik. Opšti dijagram mreže je otprilike ovako:

Mrežne postavke iz FreeRadiusa preko DHCP-a

Nakon nekog razmišljanja, odlučeno je koristiti FreeRadius za izdavanje mrežnih postavki pretplatnicima. U principu, shema je uobičajena: omogućavamo DHCP server na Microtick-u, a Radius Client na njemu. Konfigurišemo DHCP server -> Radius klijent -> Radius server vezu.

Ne izgleda teško. Ali! Đavo je u detaljima. naime:

  • Prilikom autorizacije PON OLT-a koristeći ovu šemu, FreeRadius-u se šalje zahtjev sa korisničkim imenom jednakim MAC adresi glavnog uređaja, Agent-Circuit-Id jednakim MAC PON Onu i praznom lozinkom.
  • Prilikom autorizacije sa prekidača sa opcijom 82, FreeRadius prima zahtjev sa praznim korisničkim imenom jednakim MAC-u pretplatničkog uređaja i ispunjenim dodatnim atributima Agent-Circuit-Id i Agent-Remote-Id koji sadrže, respektivno, ponovo MAC od relejni prekidač i port na koji je pretplatnik povezan.
  • Neki pretplatnici sa WiFI tačkama su autorizovani putem PAP-CHAP protokola
  • Neki pretplatnici sa WIFI tačaka su autorizovani sa korisničkim imenom jednakim MAC adresi WIFI tačke, bez lozinke.

Istorijska pozadina: šta je “Opcija 82” u DHCP-u

Ovo su dodatne opcije za DHCP protokol koje vam omogućavaju prijenos dodatnih informacija, na primjer u poljima Agent-Circuit-Id i Agent-Remote-Id. Obično se koristi za prijenos MAC adrese relejnog prekidača i porta na koji je pretplatnik povezan. U slučaju PON opreme ili WIFI baznih stanica, polje Agent-Circuit-Id ne sadrži korisne informacije (nema pretplatničkog porta). Opća shema rada DHCP-a u ovom slučaju je sljedeća:

Mrežne postavke iz FreeRadiusa preko DHCP-a

Korak po korak ova šema funkcionira ovako:

  1. Korisnička oprema postavlja DHCP zahtjev za emitiranje za dobivanje mrežnih postavki
  2. Uređaj (na primjer, komutator, WiFi ili PON bazna stanica) na koji je pretplatnička oprema direktno povezana "presreće" ovaj paket i mijenja ga, uvodeći dodatne opcije Opcija 82 i IP adresu Relay agenta, te ga dalje prenosi preko mreža.
  3. DHCP server prihvaća zahtjev, generiše odgovor i šalje ga relejnom uređaju
  4. Relejni uređaj prosljeđuje paket odgovora do pretplatničkog uređaja

Naravno, sve to ne funkcionira tako lako; morate u skladu s tim konfigurirati svoju mrežnu opremu.

Instalacija FreeRadiusa

Naravno, to se može postići sa postavkama konfiguracije FreeRadius, ali je teško i nejasno... pogotovo kada odete tamo nakon N mjeseci i "sve radi". Stoga smo odlučili da napišemo vlastiti autorizacijski modul za FreeRadius u Pythonu. Autorizacijske podatke ćemo preuzeti iz MySQL baze podataka. Nema smisla opisivati ​​njegovu strukturu, ionako će je svako napraviti „za sebe“. Konkretno, uzeo sam strukturu koja se nudi sa sql modulom za FreeRadius i malo je promijenio dodavanjem mac i port polja za svakog pretplatnika, pored lozinke za prijavu.

Dakle, prvo instalirajte FreeRadius:

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

U postavkama odaberite da instalirate:

Mrežne postavke iz FreeRadiusa preko DHCP-a

Napravimo simboličku vezu do python modula (tj. "uključimo" ga):

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

Hajde da instaliramo dodatni modul za python:

pip install mysql-connector

U postavkama python modula za FreeRadius, trebate navesti staze za pretraživanje modula u varijabli python_path. Na primjer imam ovo:

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"

Puteve možete saznati pokretanjem python interpretera i unosom naredbi:

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

Ako ne preduzmete ovaj korak, skripte napisane na python-u i pokrenute od strane FreeRadius-a neće pronaći module koji su navedeni u uvozu. Osim toga, potrebno je dekomentirati funkcije za autorizaciju poziva i računovodstvo u postavkama modula. Na primjer, ovaj modul izgleda ovako:

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 skripta (i sve ostale) mora biti smještena u /usr/local/etc/raddb/mods-config/python. Imam tri skripte ukupno.

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

Kao što možete vidjeti iz koda, pokušavamo identificirati pretplatnika korištenjem svih dostupnih metoda prema njegovim poznatim pretplatničkim MAC adresama ili kombinacijom opcije 82, a ako to ne uspije, izdajemo najstariju IP adresu ikada korištenu od „gost ” mreže. Ostaje samo da konfigurišete podrazumevanu skriptu u fascikli sa omogućenim sajtovima, tako da će potrebne funkcije iz python skripte trzati u određenim trenucima. Zapravo, dovoljno je dovesti datoteku u formu:

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

Pokušajmo ga pokrenuti i vidjeti šta dolazi u dnevnik otklanjanja grešaka:

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

Šta još. Prilikom postavljanja FreeRadiusa, zgodno je testirati njegov rad pomoću uslužnog programa radclient. Na primjer autorizacija:

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

Ili račun:

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

Želim da vas upozorim da je apsolutno nemoguće koristiti takvu šemu i skripte "bez promjena" u "industrijskoj" skali. Barem primjetno:

  • moguće je “lažirati” MAC adresu. Dovoljno je da pretplatnik registruje tuđi MAC i biće problema
  • logika izdavanja mreža gostiju je van svake kritike. Ne postoji čak ni provjera "možda već postoje klijenti sa istom IP adresom?"

Ovo je samo “rješenje za rezanje kolačića” dizajnirano da radi posebno u mojim uvjetima, ništa više. Ne sudite striktno 😉

izvor: www.habr.com

Dodajte komentar