
Une tâche a été confiée pour gérer l'attribution des adresses IP aux abonnés. Conditions de la tâche :
- Nous ne fournirons pas de serveur dédié pour l'authentification — vous vous en sortirez 😉
- Les abonnés doivent recevoir les paramètres réseau via DHCP
- Le réseau est hétérogène. Il comprend à la fois des équipements PON, des switchs standard avec l'Option 82 configurée et des bases Wi-Fi avec des points d'accès.
- Si aucune des conditions d'attribution des adresses IP n'est remplie, il est nécessaire d'attribuer une IP du réseau « invité ».
En bonne nouvelle : il y a effectivement serveur sous FreeBSD, qui peut « fonctionner », mais il est « à des kilomètres » ;), pas « directement dans ce réseau ».
Il existe également un excellent appareil Mikrotik. Le schéma général du réseau est à peu près le suivant :

Après réflexion, il a été décidé d'utiliser FreeRadius pour l'attribution des paramètres réseau aux abonnés. En gros, c'est un schéma classique : sur Mikrotik, nous activons le serveur DHCP, et le client Radius sur celui-ci. Nous configurons le lien serveur DHCP -> client Radius -> serveur Radius.
Cela ne semble pas compliqué. Mais ! Le diable est dans les détails. À savoir :
- Lors de l'authentification PON OLT selon ce schéma, une requête arrive sur FreeRadius avec un User-Name égal à l'adresse MAC de la station principale, un Agent-Circuit-Id égal à l'adresse MAC PON Onu et un mot de passe vide.
- Lors de l'authentification depuis des switchs avec l'option 82, une requête arrive sur FreeRadius avec un User-Name vide égal à l'adresse MAC de l'appareil abonnés, et avec les attributs supplémentaires Agent-Circuit-Id et Agent-Remote-Id remplis, contenant respectivement également l'adresse MAC du switch relais et le port auquel l'abonné est connecté.
- Une partie des abonnés avec des points Wi-Fi s'authentifient via les protocoles PAP-CHAP.
- Une partie des abonnés avec des points Wi-Fi s'authentifient avec un User-Name égal à l'adresse MAC du point Wi-Fi, sans mot de passe.
Note historique : qu'est-ce que l'« Option 82 » chez DHCP ?
Ce sont des options supplémentaires dans le protocole DHCP qui permettent de transmettre des informations supplémentaires, par exemple dans les champs Agent-Circuit-Id et Agent-Remote-Id. Généralement utilisées pour transmettre l'adresse MAC du switch relais et le port auquel l'abonné est connecté. Dans le cas d'équipements PON ou de stations de base Wi-Fi, le champ Agent-Circuit-Id ne contient pas d'informations utiles (il n'y a pas de port pour l'abonné). Le schéma général de fonctionnement du DHCP dans ce cas est le suivant :

Pas à pas, ce schéma fonctionne comme suit :
- L'équipement abonnés effectue une requête DHCP de broadcast pour obtenir les paramètres réseau.
- L'appareil (par exemple un switch, une station de base WiFi ou PON) auquel l'équipement client est directement connecté « intercepte » ce paquet et le modifie en y insérant des options supplémentaires Option 82 et Relay agent IP address, puis le transmet à travers le réseau.
- Le serveur DHCP reçoit la demande, forme une réponse et l'envoie à l'appareil relais.
- L'appareil relais transmet le paquet de réponse à l'appareil client.
Tout cela ne fonctionne bien sûr pas si simplement, un paramétrage adéquat de l'équipement réseau est nécessaire.
Installation de FreeRadius
Avec les paramètres de configuration de FreeRadius, il est bien sûr possible d'atteindre tout cela, mais c'est complexe et peu clair... surtout quand on s'y remet après plusieurs mois « tout fonctionne ». Par conséquent, il a été décidé d'écrire notre propre module d'autorisation pour FreeRadius en Python. Nous allons prendre les données d'authentification dans la base de données MySQL. Il n'est pas utile de décrire sa structure, car chacun la fera de « son côté ». En particulier, j'ai pris la structure proposée avec le module SQL pour FreeRadius et l'ai légèrement modifiée en ajoutant un champ mac et port pour chaque abonné, en plus du nom d'utilisateur et du mot de passe.
Donc, pour commencer, installons FreeRadius :
cd /usr/ports/net/freeradius3
make config
make
install clean
Dans les paramètres, cochez pour l'installation :

Créons un symlink vers le module python (c'est-à-dire que nous « l'activons ») :
ln -s /usr/local/etc/raddb/mods-available/python /usr/local/etc/raddb/mods-enabledInstallons un module supplémentaire pour python :
pip install mysql-connectorDans les paramètres du module python pour FreeRadius, il faut spécifier les chemins de recherche des modules dans la variable python_path. Par exemple, pour moi, cela donne :
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"
Les chemins peuvent être découverts 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)] sur freebsd12
Tapez "help", "copyright", "credits" ou "license" pour plus d'informations.
>>> 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 cette étape n'est pas effectuée, les scripts écrits en Python et exécutés par FreeRadius ne trouveront pas les modules mentionnés dans l'import. De plus, il est nécessaire de décommenter dans les paramètres du module les fonctions de rappel d'autorisation et de comptabilité. Par exemple, le module chez moi 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}
}Il est nécessaire de placer le script work.py (et tous les autres) dans /usr/local/etc/raddb/mods-config/python. En tout, j'ai trois scripts.
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
Comme on peut le voir dans le code, nous essayons par tous les moyens d'identifier l'abonné par ses adresses MAC connues ou par la combinaison de l'Option 82, et si cela ne fonctionne pas, nous fournissons l'une des plus anciennes adresses IP utilisées dans le réseau « invité ». Il reste à configurer le script default dans le dossier sites-enabled, afin que les fonctions nécessaires du script Python soient appelées aux moments appropriés. En fait, il suffit de modifier le fichier pour qu'il ressemble à :
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
}
}
Essayons de démarrer et de voir ce qui apparaît dans le journal de débogage :
/usr/local/etc/rc.d/radiusd debugUne autre chose. Lors de la configuration de FreeRadius, il est pratique de tester son fonctionnement à l'aide de l'outil radclient. Par exemple, pour l'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 testing123Ou comptabilité :
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 testing123Je tiens à avertir que l'application de ce type de schéma et de scripts « sans modifications » à une échelle « industrielle » n'est absolument pas recommandée. Au minimum, cela appelle à l'attention :
- il est possible de « falsifier » une adresse MAC. Il suffit que l'abonné se définir une adresse MAC d'un autre et il y aura des problèmes.
- la logique de délivrance des réseaux invités est sous toutes critiques. Il n'y a même pas de vérification : « Y a-t-il déjà des clients avec une telle adresse IP attribuée ? »
C'est simplement une « solution improvisée », qui fonctionne uniquement dans mes conditions spécifiques, pas plus que ça. Ne soyez pas trop sévère 😉
Source : habr.com
