Paramètres réseau de FreeRadius via DHCP

Paramètres réseau de FreeRadius via DHCP
La tâche est venue d'organiser la délivrance des adresses IP aux abonnés. Conditions du problème :

  • Nous ne vous donnerons pas de serveur séparé pour l'autorisation - vous vous débrouillerez 😉
  • Les abonnés doivent recevoir les paramètres réseau via DHCP
  • Le réseau est hétérogène. Cela comprend l'équipement PON et les commutateurs réguliers avec l'option 82 configurée et les bases WiFi avec points d'accès
  • Si les données ne remplissent aucune des conditions d'émission d'une IP, vous devez émettre une IP depuis le réseau « invité »

Le bon côté : il existe encore un serveur sur FreeBSD qui peut « fonctionner », mais il est « loin » ;), pas « juste sur ce réseau ».

Il existe également un merveilleux appareil appelé Mikrotik. Le schéma général du réseau ressemble à ceci :

Paramètres réseau de FreeRadius via DHCP

Après réflexion, il a été décidé d'utiliser FreeRadius pour transmettre les paramètres réseau aux abonnés. En principe, le schéma est habituel : on active le serveur DHCP sur Microtick, et Radius Client dessus. Nous configurons la connexion du serveur DHCP -> Radius Client -> Radius server.

Cela ne semble pas difficile. Mais! Le diable est dans les détails. À savoir:

  • Lors de l'autorisation d'un PON OLT à l'aide de ce schéma, une demande est envoyée à FreeRadius avec un nom d'utilisateur égal à l'adresse MAC de la tête de réseau, un Agent-Circuit-Id égal au MAC PON Onu et un mot de passe vide.
  • Lors de l'autorisation à partir de commutateurs avec l'option 82, FreeRadius reçoit une requête avec un nom d'utilisateur vide égal au MAC de l'appareil de l'abonné et rempli d'attributs supplémentaires Agent-Circuit-Id et Agent-Remote-Id contenant, respectivement, à nouveau le MAC de le commutateur relais et le port auquel l'abonné est connecté.
  • Certains abonnés disposant de points WiFI sont autorisés via les protocoles PAP-CHAP
  • Certains abonnés des points WIFI sont autorisés avec un Nom d'Utilisateur égal à l'adresse MAC du point WIFI, sans mot de passe.

Contexte historique : qu'est-ce que « l'option 82 » dans DHCP

Il s'agit d'options supplémentaires pour le protocole DHCP qui permettent de transférer des informations supplémentaires, par exemple dans les champs Agent-Circuit-Id et Agent-Remote-Id. Généralement utilisé pour transmettre l'adresse MAC du commutateur relais et le port auquel l'abonné est connecté. Dans le cas des équipements PON ou des stations de base WIFI, le champ Agent-Circuit-Id ne contient pas d'informations utiles (il n'y a pas de port abonné). Le schéma général du fonctionnement DHCP dans ce cas est le suivant :

Paramètres réseau de FreeRadius via DHCP

Étape par étape, ce schéma fonctionne comme ceci :

  1. L'équipement utilisateur effectue une requête de diffusion DHCP pour obtenir les paramètres réseau
  2. L'appareil (par exemple, un commutateur, une station de base WiFi ou PON) auquel l'équipement de l'abonné est directement connecté « intercepte » ce paquet et le modifie, en y introduisant des options supplémentaires Option 82 et l'adresse IP de l'agent relais, et le transmet ensuite via le réseau.
  3. Le serveur DHCP accepte la demande, génère une réponse et l'envoie au périphérique relais
  4. Le dispositif relais transmet le paquet de réponse au dispositif abonné

Bien sûr, tout ne fonctionne pas si facilement ; vous devez configurer votre équipement réseau en conséquence.

Installation de FreeRadius

Bien sûr, cela peut être réalisé avec les paramètres de configuration de FreeRadius, mais c'est difficile et peu clair... surtout quand on y va après N mois et que « tout fonctionne ». Par conséquent, nous avons décidé d'écrire notre propre module d'autorisation pour FreeRadius en Python. Nous prendrons les données d'autorisation de la base de données MySQL. Cela ne sert à rien d’en décrire la structure, de toute façon chacun le fera « pour lui-même ». En particulier, j'ai repris la structure proposée avec le module sql pour FreeRadius, et je l'ai légèrement modifiée en ajoutant un champ mac et port pour chaque abonné, en plus du mot de passe de connexion.

Alors, commencez par installer FreeRadius :

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

Dans les paramètres, sélectionnez pour installer :

Paramètres réseau de FreeRadius via DHCP

Nous créons un lien symbolique vers le module python (c'est-à-dire « allumons-le » ):

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

Installons un module supplémentaire pour python :

pip install mysql-connector

Dans les paramètres du module python pour FreeRadius, vous devez spécifier les chemins de recherche du module dans la variable python_path. Par exemple j'ai ceci :

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"

Vous pouvez connaître les chemins en lançant l'interpréteur python et en entrant les commandes :

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 vous ne suivez pas cette étape, alors les scripts écrits en python et lancés par FreeRadius ne trouveront pas les modules répertoriés en import. De plus, vous devez décommenter les fonctions d'autorisation d'appel et de comptabilité dans les paramètres du module. Par exemple, ce module ressemble à ceci :

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}

}

Le script work.py (et tous les autres) doit être placé dans /usr/local/etc/raddb/mods-config/python. J'ai trois scripts au total.

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

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

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

Comme vous pouvez le voir sur le code, nous essayons d'identifier l'abonné en utilisant toutes les méthodes disponibles par ses adresses MAC d'abonné connues ou la combinaison de l'option 82, et si cela ne fonctionne pas, nous émettons l'adresse IP la plus ancienne jamais utilisée de la part du « invité ». " réseau. Il ne reste plus qu'à configurer le script par défaut dans le dossier sites-enabled, afin que les fonctions nécessaires du script python se contractent aux moments désignés. En fait, il suffit de ramener le fichier sous la forme :

défaut

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

Essayons de l'exécuter et voyons ce qui apparaît dans le journal de débogage :

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

Quoi d'autre. Lors de la configuration de FreeRadius, il est pratique de tester son fonctionnement à l'aide de l'utilitaire radclient. Par exemple autorisation :

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

Ou 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

Je tiens à vous avertir qu'il est absolument impossible d'utiliser un tel schéma et des scripts « sans modifications » à l'échelle « industrielle ». Au moins perceptible :

  • il est possible de « truquer » l'adresse MAC. Il suffit à l'abonné d'enregistrer le MAC de quelqu'un d'autre et il y aura des problèmes
  • la logique de l'émission de réseaux invités est irréprochable. Il n'y a même pas de vérification « peut-être qu'il y a déjà des clients avec la même adresse IP ?

Il s’agit simplement d’une « solution à l’emporte-pièce » conçue pour fonctionner spécifiquement dans mes conditions, rien de plus. Ne jugez pas strictement 😉

Source: habr.com

Ajouter un commentaire