
È arrivata la richiesta di configurare l'emissione degli indirizzi IP agli abbonati. Condizioni della richiesta:
- Non forniremo un server separato per l'autenticazione — dovrete adattarvi 😉
- Gli abbonati devono ricevere le impostazioni di rete tramite DHCP
- La rete è eterogenea. Ci sono sia attrezzature PON che normali switch con l'Opzione 82 configurata e basi WiFi con punti di accesso.
- Se nessuna delle condizioni per l'emissione degli IP è soddisfatta, è necessario emettere un IP dalla rete 'ospite'.
Dal lato positivo: c'è. server su FreeBSD, che può 'funzionare', ma si trova 'a tre isolette di distanza' ;), non 'proprio in questa rete'.
C'è anche un meraviglioso dispositivo Mikrotik. Lo schema generale della rete è circa questo:

Dopo aver riflettuto, è stata presa la decisione di utilizzare FreeRadius per emettere le impostazioni di rete agli abbonati. In linea di massima, lo schema è normale: accendiamo il server DHCP su Microtik, e su di esso il Client Radius. Configuriamo una connessione server DHCP -> Client Radius -> server Radius.
Sembra non difficile. Ma! Il diavolo sta nei dettagli. E precisamente:
- Durante l'autenticazione PON OLT secondo questo schema, a FreeRadius arriva una richiesta con User-Name pari all'indirizzo MAC della stazione principale, Agent-Circuit-Id pari all'indirizzo MAC PON Onu e password vuota.
- Durante l'autenticazione da switch con opzione 82, a FreeRadius arriva una richiesta con User-Name vuoto pari all'indirizzo MAC dell'abbonato e attributi aggiuntivi Agent-Circuit-Id e Agent-Remote-Id riempiti, contenenti rispettivamente anche l'indirizzo MAC dello switch di relay e la porta a cui è collegato l'abbonato.
- Una parte degli abbonati con i punti WiFi si autentica tramite i protocolli PAP-CHAP.
- Una parte degli abbonati con i punti WiFi si autentica con User-Name pari all'indirizzo MAC del punto WiFi, senza password.
Note storiche: che cos'è l'«Opzione 82» nel DHCP
Queste sono opzioni aggiuntive del protocollo DHCP che permettono di trasmettere informazioni supplementari, ad esempio nei campi Agent-Circuit-Id e Agent-Remote-Id. Di solito sono usate per il passaggio dell'indirizzo MAC dello switch di relay e della porta a cui è collegato l'abbonato. Nel caso di attrezzature PON o stazioni base WIFI, il campo Agent-Circuit-Id non porta informazioni utili (non c'è porta per l'abbonato). In questo caso, lo schema generale di funzionamento del DHCP è il seguente:

Passo dopo passo, questo schema funziona così:
- Le attrezzature degli abbonati fanno una richiesta DHCP broadcast per ricevere le impostazioni di rete.
- Il dispositivo (ad esempio uno switch, una stazione base WiFi o un PON) al quale si collega direttamente l'equipaggiamento dell'abbonato "intercetta" questo pacchetto e lo modifica, integrando opzioni aggiuntive Option 82 e indirizzo IP del Relay agent, e lo trasmette ulteriormente nella rete.
- Il server DHCP riceve la richiesta, forma la risposta e la invia al dispositivo di relay.
- Il dispositivo di relay inoltra il pacchetto di risposta all'apparecchiatura dell'abbonato.
Certo, non funziona tutto così semplicemente; è necessaria un'impostazione appropriata dell'equipaggiamento di rete.
Installazione di FreeRadius
Con le impostazioni di configurazione di FreeRadius è possibile raggiungere tutto ciò, ma è complicato e poco chiaro… soprattutto quando ci si immerge lì dopo N mesi con l'idea che "tutto funziona". Pertanto, è stata presa la decisione di scrivere il proprio modulo di autorizzazione per FreeRadius in Python. I dati per l'autorizzazione verranno presi da un database MySQL. Non ha senso descrivere la sua struttura, ognuno di certo la farà "su misura". In particolare, ho preso la struttura proposta con il modulo sql per FreeRadius e l'ho leggermente modificata, aggiungendo i campi mac e port per ciascun abbonato, oltre a login e password.
Quindi, per cominciare, installiamo FreeRadius:
cd /usr/ports/net/freeradius3
make config
make
install clean
Nelle impostazioni selezioniamo per l'installazione:

Creiamo un symlink per il modulo python (cioè "attiviamo"):
ln -s /usr/local/etc/raddb/mods-available/python /usr/local/etc/raddb/mods-enabledInstalleremo un modulo aggiuntivo per python:
pip install mysql-connectorNelle impostazioni del modulo python per FreeRadius, è necessario specificare i percorsi di ricerca dei moduli nella variabile python_path. Ad esempio, per me è questo:
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"
I percorsi possono essere scoperti avviando l'interprete python e digitando i comandi:
root@phaeton:/usr/local/etc/raddb/mods-enabled# python
Python 2.7.15 (default, 8 dic 2018, 01:22:25)
[GCC 4.2.1 Compatibile FreeBSD Clang 6.0.1 (tags/RELEASE_601/final 335540)] su freebsd12
Digita "help", "copyright", "credits" o "license" per ulteriori informazioni.
>>> 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']
>Se non si compie questo passo, gli script scritti in python e avviati con FreeRadius non troveranno quei moduli elencati in import. Inoltre, è necessario decommentare nelle impostazioni del modulo le funzioni di chiamata per l'autenticazione e la contabilità. Ad esempio, il mio modulo appare così:
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}
}Lo script work.py (e tutti gli altri) deve essere posizionato in /usr/local/etc/raddb/mods-config/python. In totale ho creato tre script.
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
Come si può vedere dal codice, stiamo cercando di identificare l'abbonato in tutti i modi possibili, utilizzando i suoi indirizzi MAC già noti o una combinazione di Option 82, e se questo non funziona, forniamo il più vecchio degli indirizzi IP utilizzati in precedenza dalla rete "ospite". Rimane da configurare lo script default nella cartella sites-enabled, in modo che le funzioni necessarie dello script python vengano chiamate nei momenti opportuni. In pratica, è sufficiente portare il file alla seguente forma:
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
}
}
Proviamo a eseguire e vedere cosa arriva nel log di debug:
/usr/local/etc/rc.d/radiusd debugCos'altro. Nella configurazione di FreeRadius è utile testarne il funzionamento tramite l'utilità radclient. Ad esempio, per l'autenticazione:
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 testing123Oppure contabilità:
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 testing123Voglio avvertire che utilizzare uno schema simile e script "senza modifiche" su scala "industriale" non è affatto consigliabile. Perlomeno si notano subito:
- è possibile la "falsificazione" dell'indirizzo MAC. È sufficiente che l'utente si imposti un indirizzo MAC estraneo e ci saranno problemi
- la logica di assegnazione delle reti ospiti è sotto ogni critica. Non c'è nemmeno un controllo "ci sono già clienti a cui è stato assegnato tale indirizzo IP?"
È semplicemente una "soluzione improvvisata", realizzata per funzionare specificamente nelle mie condizioni, nulla di più. Non giudicate troppo severamente 😉
Fonte: habr.com
