Netzwerkeinstellungen von FreeRadius über DHCP

Netzwerkeinstellungen von FreeRadius über DHCP
Es kam die Aufgabe an, die Vergabe von IP-Adressen an Abonnenten zu veranlassen. Bedingungen des Problems:

  • Wir stellen Ihnen keinen separaten Server für die Autorisierung zur Verfügung – Sie kommen damit zurecht 😉
  • Abonnenten müssen Netzwerkeinstellungen über DHCP erhalten
  • Das Netzwerk ist heterogen. Dazu gehören PON-Geräte und normale Switches mit konfigurierter Option 82 sowie WLAN-Basisstationen mit Hotspots
  • Wenn die Daten keine der Bedingungen für die Vergabe einer IP erfüllen, müssen Sie eine IP aus dem „Gast“-Netzwerk vergeben

Das Gute daran: Es gibt immer noch einen Server unter FreeBSD, der „funktionieren“ kann, aber er ist „weit weg“ ;), nicht „direkt in diesem Netzwerk“.

Es gibt auch ein wunderbares Gerät namens Mikrotik. Das allgemeine Netzwerkdiagramm sieht etwa so aus:

Netzwerkeinstellungen von FreeRadius über DHCP

Nach einigem Überlegen wurde beschlossen, FreeRadius zu verwenden, um den Abonnenten Netzwerkeinstellungen zuzuweisen. Im Prinzip ist das Schema üblich: Wir aktivieren den DHCP-Server auf Microtick und Radius Client darauf. Wir konfigurieren den DHCP-Server -> Radius Client -> Radius-Server-Verbindung.

Es scheint nicht schwierig zu sein. Aber! Der Teufel steckt im Detail. Nämlich:

  • Bei der Autorisierung eines PON OLT mit diesem Schema wird eine Anfrage mit einem Benutzernamen, der der MAC-Adresse des Headends entspricht, einer Agent-Circuit-ID, die dem MAC PON Onu entspricht, und einem leeren Passwort an FreeRadius gesendet.
  • Bei der Autorisierung von Switches mit Option 82 erhält FreeRadius eine Anfrage mit einem leeren Benutzernamen, der der MAC des Geräts des Teilnehmers entspricht und mit den zusätzlichen Attributen Agent-Circuit-Id und Agent-Remote-Id gefüllt ist, die jeweils wiederum die MAC von enthalten der Relaisschalter und der Port, an dem der Teilnehmer angeschlossen ist.
  • Einige Abonnenten mit WiFI-Punkten werden über PAP-CHAP-Protokolle autorisiert
  • Einige Abonnenten von WLAN-Punkten werden mit einem Benutzernamen autorisiert, der der MAC-Adresse des WLAN-Punkts entspricht, ohne Passwort.

Historischer Hintergrund: Was ist „Option 82“ in DHCP?

Hierbei handelt es sich um zusätzliche Optionen für das DHCP-Protokoll, mit denen Sie zusätzliche Informationen übertragen können, beispielsweise in den Feldern Agent-Circuit-Id und Agent-Remote-Id. Wird normalerweise zur Übertragung der MAC-Adresse des Relaisschalters und des Ports verwendet, an dem der Teilnehmer angeschlossen ist. Bei PON-Geräten oder WIFI-Basisstationen enthält das Feld „Agent-Circuit-Id“ keine nützlichen Informationen (es gibt keinen Teilnehmer-Port). Das allgemeine Schema des DHCP-Betriebs ist in diesem Fall wie folgt:

Netzwerkeinstellungen von FreeRadius über DHCP

Schritt für Schritt funktioniert dieses Schema folgendermaßen:

  1. Das Benutzergerät stellt eine DHCP-Broadcast-Anfrage, um Netzwerkeinstellungen zu erhalten
  2. Das Gerät (z. B. ein Switch, WLAN oder eine PON-Basisstation), mit dem das Teilnehmergerät direkt verbunden ist, „fängt“ dieses Paket ab und ändert es, indem es die zusätzlichen Optionen Option 82 und Relay-Agent-IP-Adresse einfügt und es weiterleitet das Netzwerk.
  3. Der DHCP-Server nimmt die Anfrage an, generiert eine Antwort und sendet sie an das Relay-Gerät
  4. Das Relay-Gerät leitet das Antwortpaket an das Teilnehmergerät weiter

Natürlich funktioniert das alles nicht so einfach; Sie müssen Ihre Netzwerkausrüstung entsprechend konfigurieren.

FreeRadius installieren

Natürlich kann dies mit den FreeRadius-Konfigurationseinstellungen erreicht werden, aber es ist schwierig und unklar ... vor allem, wenn man nach N Monaten dorthin geht und „alles funktioniert“. Deshalb haben wir uns entschieden, unser eigenes Autorisierungsmodul für FreeRadius in Python zu schreiben. Wir übernehmen die Autorisierungsdaten aus der MySQL-Datenbank. Es hat keinen Sinn, seine Struktur zu beschreiben; jeder wird es „für sich“ machen. Insbesondere habe ich die Struktur übernommen, die mit dem SQL-Modul für FreeRadius angeboten wird, und sie leicht geändert, indem ich zusätzlich zum Login-Passwort ein Mac- und Port-Feld für jeden Abonnenten hinzugefügt habe.

Installieren Sie also zunächst FreeRadius:

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

Wählen Sie in den Einstellungen zur Installation Folgendes aus:

Netzwerkeinstellungen von FreeRadius über DHCP

Wir erstellen einen Symlink zum Python-Modul (d. h. „aktivieren“ es):

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

Lassen Sie uns ein zusätzliches Modul für Python installieren:

pip install mysql-connector

In den Python-Moduleinstellungen für FreeRadius müssen Sie die Modulsuchpfade in der Variablen python_path angeben. Ich habe zum Beispiel das hier:

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"

Sie können die Pfade herausfinden, indem Sie den Python-Interpreter starten und die folgenden Befehle eingeben:

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

Wenn Sie diesen Schritt nicht ausführen, finden in Python geschriebene und von FreeRadius gestartete Skripte die im Import aufgeführten Module nicht. Darüber hinaus müssen Sie in den Moduleinstellungen die Funktionen zum Aufruf der Autorisierung und Abrechnung auskommentieren. Dieses Modul sieht beispielsweise so aus:

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}

}

Das work.py-Skript (und alle anderen) müssen in /usr/local/etc/raddb/mods-config/python abgelegt werden. Ich habe insgesamt drei Skripte.

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

Wie Sie dem Code entnehmen können, versuchen wir, den Teilnehmer mit allen verfügbaren Methoden anhand seiner bekannten Teilnehmer-MAC-Adressen oder Option 82-Kombination zu identifizieren. Wenn dies nicht funktioniert, vergeben wir die älteste jemals verwendete IP-Adresse des „Gasts“. „Netzwerk. Es bleibt nur noch, das Standardskript im Ordner „sites-enabled“ zu konfigurieren, damit die erforderlichen Funktionen des Python-Skripts zu den vorgesehenen Zeitpunkten ausgeführt werden. Tatsächlich reicht es aus, die Datei in das Formular zu bringen:

Standard

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

Versuchen wir es auszuführen und sehen, was im Debug-Protokoll erscheint:

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

Was sonst. Beim Einrichten von FreeRadius ist es praktisch, den Betrieb mit dem Dienstprogramm radclient zu testen. Beispiel Autorisierung:

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

Oder Konto:

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

Ich möchte Sie warnen, dass es absolut unmöglich ist, ein solches Schema und solche Skripte „ohne Änderungen“ im „industriellen“ Maßstab zu verwenden. Zumindest auffällig:

  • Es ist möglich, die MAC-Adresse zu „fälschen“. Es reicht aus, wenn der Abonnent den MAC einer anderen Person registriert, und es treten Probleme auf
  • Die Logik der Herausgabe von Gastnetzwerken ist nicht zu kritisieren. Es erfolgt nicht einmal eine Prüfung „Vielleicht gibt es bereits Clients mit der gleichen IP-Adresse?“

Dabei handelt es sich lediglich um eine „Plätzchen-Lösung“, die speziell für meine Bedingungen entwickelt wurde, mehr nicht. Urteile nicht streng 😉

Source: habr.com

Kommentar hinzufügen