Omrežne nastavitve iz FreeRadiusa prek DHCP

Omrežne nastavitve iz FreeRadiusa prek DHCP
Prišla je naloga urediti izdajo IP naslovov naročnikom. Pogoji problema:

  • Ne bomo vam dali ločenega strežnika za avtorizacijo - zadovoljili se boste 😉
  • Naročniki morajo prejemati omrežne nastavitve prek DHCP
  • Omrežje je heterogeno. To vključuje opremo PON in običajna stikala s konfigurirano opcijo 82 ter WiFi baz z dostopnimi točkami
  • Če podatki ne sodijo pod nobenega od pogojev za izdajo IP-ja, morate izdati IP iz “guest” omrežja.

Dobra stran: še vedno obstaja strežnik na FreeBSD, ki lahko "deluje", vendar je "daleč stran" ;), ne "prav v tem omrežju".

Obstaja tudi čudovita naprava Mikrotik. Splošni diagram omrežja je približno takšen:

Omrežne nastavitve iz FreeRadiusa prek DHCP

Po premisleku je bilo odločeno, da uporabimo FreeRadius za izdajo omrežnih nastavitev naročnikom. Načeloma je shema običajna: na Microticku omogočimo strežnik DHCP, na njem pa Radius Client. Konfiguriramo strežnik DHCP -> Radius Client -> Povezava s strežnikom Radius.

Ne zdi se težko. ampak! Hudič je v podrobnostih. namreč:

  • Pri avtorizaciji PON OLT z uporabo te sheme je FreeRadiusu poslana zahteva z uporabniškim imenom, ki je enako MAC naslovu glavne postaje, Agent-Circuit-Id, ki je enak MAC PON Onu, in praznim geslom.
  • Pri avtorizaciji iz stikal z možnostjo 82 FreeRadius prejme zahtevo s praznim uporabniškim imenom, enakim MAC-ju naročnikove naprave in napolnjenim z dodatnima atributoma Agent-Circuit-Id in Agent-Remote-Id, ki ponovno vsebujeta MAC od relejno stikalo in vrata, na katera je priključen naročnik.
  • Nekateri naročniki z WiFI točkami so avtorizirani preko protokolov PAP-CHAP
  • Nekateri naročniki iz točk WIFI so avtorizirani z uporabniškim imenom, ki je enako naslovu MAC točke WIFI, brez gesla.

Zgodovinsko ozadje: kaj je »Možnost 82« v DHCP

To so dodatne možnosti za protokol DHCP, ki vam omogočajo prenos dodatnih informacij, na primer v poljih Agent-Circuit-Id in Agent-Remote-Id. Običajno se uporablja za prenos naslova MAC relejnega stikala in vrat, na katera je priključen naročnik. V primeru opreme PON ali baznih postaj WIFI polje Agent-Circuit-Id ne vsebuje uporabnih informacij (ni naročniških vrat). Splošna shema delovanja DHCP v tem primeru je naslednja:

Omrežne nastavitve iz FreeRadiusa prek DHCP

Korak za korakom ta shema deluje takole:

  1. Uporabniška oprema naredi zahtevo za oddajanje DHCP za pridobitev omrežnih nastavitev
  2. Naprava (na primer stikalo, WiFi ali bazna postaja PON), na katero je neposredno povezana naročniška oprema, ta paket »prestreže« in ga spremeni ter vanj vnese dodatne možnosti Opcija 82 in IP naslov posrednika ter ga posreduje naprej omrežje.
  3. Strežnik DHCP sprejme zahtevo, ustvari odgovor in ga pošlje relejni napravi
  4. Relejna naprava posreduje odzivni paket naročniški napravi

Seveda pa vse skupaj ne gre tako preprosto, zato morate ustrezno konfigurirati svojo omrežno opremo.

Namestitev FreeRadiusa

Seveda je to mogoče doseči s konfiguracijskimi nastavitvami FreeRadius, vendar je težko in nejasno ... še posebej, ko greš tja po N mesecih in "vse deluje." Zato smo se odločili napisati lasten avtorizacijski modul za FreeRadius v Pythonu. Podatke o avtorizaciji bomo vzeli iz baze MySQL. Nima smisla opisovati njegove strukture, tako ali tako si ga bo vsak naredil "zase". Še posebej sem vzel strukturo, ki je na voljo z modulom sql za FreeRadius, in jo nekoliko spremenil tako, da sem poleg gesla za prijavo dodal polje mac in vrata za vsakega naročnika.

Torej, najprej namestite FreeRadius:

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

V nastavitvah izberite namestitev:

Omrežne nastavitve iz FreeRadiusa prek DHCP

Naredimo simbolno povezavo do modula python (tj. "vklopimo" ga):

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

Namestimo dodatni modul za python:

pip install mysql-connector

V nastavitvah modula python za FreeRadius morate v spremenljivki python_path podati iskalne poti modula. Jaz imam na primer tole:

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"

Poti lahko najdete tako, da zaženete tolmač python in vnesete ukaze:

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

Če tega koraka ne storite, potem skripti, napisani v pythonu in zagnani s FreeRadiusom, ne bodo našli modulov, ki so navedeni pri uvozu. Poleg tega morate v nastavitvah modula odkomentirati funkcije za avtorizacijo klicev in obračunavanje. Na primer, ta modul izgleda takole:

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}

}

Skript work.py (in vsi ostali) je treba postaviti v /usr/local/etc/raddb/mods-config/python. Skupaj imam tri skripte.

delo.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

Kot je razvidno iz kode, skušamo naročnika identificirati z vsemi razpoložljivimi metodami po njegovih znanih naročnikovih MAC naslovih ali kombinaciji možnosti 82, in če to ne deluje, izdamo najstarejši IP naslov, ki je bil kdaj uporabljen od »guest ” omrežje. Vse, kar ostane, je, da konfigurirate privzeti skript v mapi sites-enabled, tako da bodo potrebne funkcije iz skripta python trzale ob določenih trenutkih. Pravzaprav je dovolj, da datoteko pripeljete v obrazec:

privzeto

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

Poskusimo ga zagnati in poglejmo, kaj pride v dnevnik odpravljanja napak:

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

Kaj drugega. Ko nastavljate FreeRadius, je priročno preizkusiti njegovo delovanje s pripomočkom radclient. Na primer avtorizacija:

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

Ali 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 vas opozoriti, da je absolutno nemogoče uporabljati takšno shemo in skripte "brez sprememb" v "industrijskem" obsegu. Vsaj opazno:

  • naslov MAC je možno »ponarediti«. Dovolj je, da naročnik registrira tuj MAC in težave bodo
  • logika izdajanja gostujočih omrežij je brez kritike. Niti preverjanja "morda že obstajajo odjemalci z istim naslovom IP?"

To je le "rešitev za izrezovanje piškotkov", zasnovana tako, da deluje posebej v mojih pogojih, nič več. Ne obsojajte strogo 😉

Vir: www.habr.com

Dodaj komentar