Configuració de xarxa des de FreeRadius mitjançant DHCP

Configuració de xarxa des de FreeRadius mitjançant DHCP
Va arribar la tasca d'organitzar l'emissió d'adreces IP als subscriptors. Condicions del problema:

  • No us donarem cap servidor separat per a l'autorització; us ho fareu 😉
  • Els subscriptors han de rebre la configuració de xarxa mitjançant DHCP
  • La xarxa és heterogènia. Això inclou equips PON i interruptors regulars amb l'opció 82 configurada i bases WiFi amb punts d'accés
  • Si les dades no entren en cap de les condicions per a l'emissió d'una IP, heu d'emetre una IP des de la xarxa "convidat".

En el costat bo: encara hi ha un servidor a FreeBSD que pot "funcionar", però està "lluny" ;), no "just en aquesta xarxa".

També hi ha un dispositiu meravellós anomenat Mikrotik. El diagrama general de la xarxa és una cosa així:

Configuració de xarxa des de FreeRadius mitjançant DHCP

Després d'una reflexió, es va decidir utilitzar FreeRadius per emetre la configuració de la xarxa als subscriptors. En principi, l'esquema és habitual: activem el servidor DHCP a Microtick i Radius Client en ell. Configurem el servidor DHCP -> Client Radius -> Connexió al servidor Radius.

No sembla difícil. Però! El diable està en els detalls. És a dir:

  • Quan s'autoritza un PON OLT utilitzant aquest esquema, s'envia una sol·licitud a FreeRadius amb un nom d'usuari igual a l'adreça MAC de la capçalera, un identificador de circuit d'agent igual al MAC PON Onu i una contrasenya buida.
  • Quan s'autoritza des de commutadors amb l'opció 82, FreeRadius rep una sol·licitud amb un nom d'usuari buit igual al MAC del dispositiu de l'abonat i ple d'atributs addicionals Agent-Circuit-Id i Agent-Remote-Id que contenen, respectivament, novament el MAC de l'interruptor de relé i el port al qual està connectat l'abonat.
  • Alguns subscriptors amb punts WiFI estan autoritzats mitjançant protocols PAP-CHAP
  • Alguns subscriptors dels punts WIFI estan autoritzats amb un nom d'usuari igual a l'adreça MAC del punt WIFI, sense contrasenya.

Antecedents històrics: què és l'"Opció 82" a DHCP

Aquestes són opcions addicionals per al protocol DHCP que us permeten transferir informació addicional, per exemple als camps Agent-Circuit-Id i Agent-Remote-Id. Normalment s'utilitza per transmetre l'adreça MAC del commutador de relé i el port al qual està connectat l'abonat. En el cas d'equips PON o estacions base WIFI, el camp Agent-Circuit-Id no conté informació útil (no hi ha cap port d'abonat). L'esquema general de funcionament de DHCP en aquest cas és el següent:

Configuració de xarxa des de FreeRadius mitjançant DHCP

Pas a pas, aquest esquema funciona així:

  1. L'equip de l'usuari fa una sol·licitud de difusió DHCP per obtenir la configuració de la xarxa
  2. El dispositiu (per exemple, un commutador, una estació base WiFi o PON) al qual està connectat directament l'equip d'abonat "intercepta" aquest paquet i el canvia, introduint-hi opcions addicionals l'Opció 82 i l'adreça IP de l'agent de retransmissió, i el transmet més endavant. la xarxa.
  3. El servidor DHCP accepta la sol·licitud, genera una resposta i l'envia al dispositiu de retransmissió
  4. El dispositiu de retransmissió reenvia el paquet de resposta al dispositiu subscriptor

Per descomptat, tot no funciona tan fàcilment; heu de configurar el vostre equip de xarxa en conseqüència.

Instal·lació de FreeRadius

Per descomptat, això es pot aconseguir amb la configuració de FreeRadius, però és difícil i poc clar... sobretot quan hi aneu després de N mesos i "tot funciona". Per tant, vam decidir escriure el nostre propi mòdul d'autorització per a FreeRadius a Python. Prendrem les dades d'autorització de la base de dades MySQL. No té sentit descriure la seva estructura; de totes maneres, tothom la farà "per si mateix". En particular, vaig agafar l'estructura que s'ofereix amb el mòdul sql per a FreeRadius i la vaig canviar lleugerament afegint un camp mac i port per a cada subscriptor, a més de la contrasenya d'inici de sessió.

Per tant, primer, instal·leu FreeRadius:

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

A la configuració, seleccioneu per instal·lar:

Configuració de xarxa des de FreeRadius mitjançant DHCP

Fem un enllaç simbòlic al mòdul Python (és a dir, "activa'l"):

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

Instal·lem un mòdul addicional per a Python:

pip install mysql-connector

A la configuració del mòdul Python per a FreeRadius, heu d'especificar els camins de cerca del mòdul a la variable python_path. Per exemple tinc això:

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"

Podeu esbrinar els camins llançant l'intèrpret de Python i introduint les ordres:

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

Si no feu aquest pas, aleshores els scripts escrits en Python i llançats per FreeRadius no trobaran els mòduls que es mostren a la importació. A més, heu de descomentar les funcions d'autorització de trucades i comptabilitat a la configuració del mòdul. Per exemple, aquest mòdul té aquest aspecte:

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}

}

L'script work.py (i tots els altres) s'ha de col·locar a /usr/local/etc/raddb/mods-config/python Tinc tres scripts en total.

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

Com podeu veure al codi, estem intentant identificar l'abonat utilitzant tots els mètodes disponibles mitjançant les seves adreces MAC conegudes d'abonat o la combinació de l'opció 82, i si això no funciona, emetem l'adreça IP més antiga que s'ha utilitzat mai des del "convidat". " xarxa. Només queda configurar l'script predeterminat a la carpeta habilitada per a llocs, de manera que les funcions necessàries de l'script de Python es mouran en els moments designats. De fet, n'hi ha prou amb portar el fitxer al formulari:

defecte

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

Intentem executar-lo i veure què entra al registre de depuració:

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

Què més. Quan configureu FreeRadius, és convenient provar el seu funcionament mitjançant la utilitat radclient. Per exemple autorització:

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

O compte:

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

Vull advertir-vos que és absolutament impossible utilitzar aquest esquema i scripts "sense canvis" a escala "industrial". Almenys notable:

  • és possible "falsificar" l'adreça MAC. N'hi ha prou amb que l'abonat registri el MAC d'una altra persona i hi haurà problemes
  • la lògica d'emetre xarxes de convidats està més enllà de les crítiques. Ni tan sols hi ha una comprovació "potser ja hi ha clients amb la mateixa adreça IP?"

Aquesta és només una "solució de tall de galetes" dissenyada per funcionar específicament a les meves condicions, res més. No jutgis estrictament 😉

Font: www.habr.com

Afegeix comentari