Netwerkinstellingen van FreeRadius via DHCP

Netwerkinstellingen van FreeRadius via DHCP
De taak kwam om de uitgifte van IP-adressen aan abonnees te regelen. Voorwaarden van het probleem:

  • We geven u geen aparte server voor autorisatie; u zult het doen 😉
  • Abonnees moeten netwerkinstellingen via DHCP ontvangen
  • Het netwerk is heterogeen. Denk hierbij aan PON-apparatuur en reguliere switches met geconfigureerde Optie 82 en WiFi-bases met hotspots
  • Als de gegevens niet onder een van de voorwaarden voor het verstrekken van een IP vallen, moet u een IP uit het “gastnetwerk” verstrekken

Aan de goede kant: er is nog steeds een server op FreeBSD die kan “werken”, maar die is “ver weg” ;), niet “recht op dit netwerk”.

Er is ook een prachtig apparaat genaamd Mikrotik. Het algemene netwerkdiagram ziet er ongeveer zo uit:

Netwerkinstellingen van FreeRadius via DHCP

Na enig nadenken werd besloten FreeRadius te gebruiken om netwerkinstellingen aan abonnees door te geven. In principe is het schema gebruikelijk: we schakelen de DHCP-server in op Microtick en Radius Client daarop. We configureren de DHCP-server -> Radius Client -> Radius-serververbinding.

Het lijkt niet moeilijk. Maar! De duivel is in de details. Namelijk:

  • Bij het autoriseren van een PON OLT met behulp van dit schema wordt een verzoek naar FreeRadius gestuurd met een gebruikersnaam gelijk aan het MAC-adres van het headend, een Agent-Circuit-Id gelijk aan de MAC PON Onu en een leeg wachtwoord.
  • Bij autorisatie vanaf switches met optie 82 ontvangt FreeRadius een verzoek met een lege gebruikersnaam die gelijk is aan de MAC van het apparaat van de abonnee en gevuld met extra attributen Agent-Circuit-Id en Agent-Remote-Id die respectievelijk wederom de MAC bevatten van de relaisschakelaar en de poort waarop de abonnee is aangesloten.
  • Sommige abonnees met WiFI-punten zijn geautoriseerd via PAP-CHAP-protocollen
  • Sommige abonnees van WIFI-punten zijn geautoriseerd met een gebruikersnaam gelijk aan het MAC-adres van het WIFI-punt, zonder wachtwoord.

Historische achtergrond: wat is “Optie 82” in DHCP

Dit zijn extra opties voor het DHCP-protocol waarmee u extra informatie kunt overbrengen, bijvoorbeeld in de velden Agent-Circuit-Id en Agent-Remote-Id. Meestal gebruikt om het MAC-adres van de relaisschakelaar en de poort waarmee de abonnee is verbonden, te verzenden. Bij PON-apparatuur of WIFI-basisstations bevat het veld Agent-Circuit-Id geen nuttige informatie (er is geen abonneepoort). Het algemene schema van de DHCP-werking is in dit geval als volgt:

Netwerkinstellingen van FreeRadius via DHCP

Stap voor stap werkt dit schema als volgt:

  1. De gebruikersapparatuur doet een DHCP-broadcastverzoek om netwerkinstellingen te verkrijgen
  2. Het apparaat (bijvoorbeeld een switch, WiFi of PON-basisstation) waarmee de abonneeapparatuur rechtstreeks is verbonden, ‘onderschept’ dit pakket en wijzigt het, introduceert er extra opties Optie 82 en Relay-agent IP-adres in, en verzendt het verder via het netwerk.
  3. De DHCP-server accepteert het verzoek, genereert een antwoord en stuurt dit naar het relaisapparaat
  4. Het relaisapparaat stuurt het antwoordpakket door naar het abonneeapparaat

Natuurlijk werkt het allemaal niet zo eenvoudig; je moet je netwerkapparatuur dienovereenkomstig configureren.

FreeRadius installeren

Natuurlijk kan dit worden bereikt met de FreeRadius-configuratie-instellingen, maar het is moeilijk en onduidelijk... vooral als je er na N maanden naartoe gaat en "alles werkt." Daarom hebben we besloten om onze eigen autorisatiemodule voor FreeRadius in Python te schrijven. We halen autorisatiegegevens uit de MySQL-database. Het heeft geen zin de structuur ervan te beschrijven; hoe dan ook, iedereen zal het ‘voor zichzelf’ maken. In het bijzonder heb ik de structuur genomen die wordt aangeboden met de sql-module voor FreeRadius, en deze enigszins gewijzigd door voor elke abonnee een mac- en poortveld toe te voegen, naast het login-wachtwoord.

Installeer dus eerst FreeRadius:

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

Selecteer in de instellingen om te installeren:

Netwerkinstellingen van FreeRadius via DHCP

We maken een symlink naar de Python-module (d.w.z. "aanzetten"):

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

Laten we een extra module voor Python installeren:

pip install mysql-connector

In de Python-module-instellingen voor FreeRadius moet u de modulezoekpaden opgeven in de python_path-variabele. Ik heb bijvoorbeeld dit:

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"

U kunt de paden achterhalen door de Python-interpreter te starten en de opdrachten in te voeren:

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

Als u deze stap niet uitvoert, zullen scripts die in Python zijn geschreven en door FreeRadius zijn gestart, de modules niet vinden die bij het importeren worden vermeld. Bovendien moet u de autorisatie- en accountingfuncties in de module-instellingen uitschakelen. Deze module ziet er bijvoorbeeld zo uit:

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}

}

Het work.py-script (en alle andere) moet in /usr/local/etc/raddb/mods-config/python worden geplaatst. Ik heb in totaal drie scripts.

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

Zoals u aan de code kunt zien, proberen we de abonnee te identificeren met behulp van alle beschikbare methoden aan de hand van zijn bekende MAC-adressen van de abonnee of een optie 82-combinatie. Als dit niet werkt, geven we het oudste IP-adres dat ooit is gebruikt door de 'gast'-adres. netwerk. Het enige dat overblijft is het configureren van het standaardscript in de map met sites ingeschakeld, zodat de noodzakelijke functies van het Python-script op de aangegeven momenten zullen trillen. In feite is het voldoende om het bestand naar het formulier te brengen:

verzuim

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

Laten we proberen het uit te voeren en kijken wat er in het foutopsporingslogboek komt:

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

Wat nog meer. Bij het instellen van FreeRadius is het handig om de werking ervan te testen met behulp van het hulpprogramma radclient. Bijvoorbeeld autorisatie:

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

Of rekening:

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

Ik wil u waarschuwen dat het absoluut onmogelijk is om een ​​dergelijk schema en dergelijke scripts “zonder veranderingen” op “industriële” schaal te gebruiken. Op zijn minst opvallend:

  • het is mogelijk om het MAC-adres te “faken”. Het volstaat dat de abonnee de MAC van iemand anders registreert en er zullen problemen optreden
  • de logica van het uitgeven van gastnetwerken staat buiten elke kritiek. Er is niet eens een controle “zijn er misschien al clients met hetzelfde IP-adres?”

Dit is slechts een “cookie-cutter-oplossing” die specifiek is ontworpen om onder mijn omstandigheden te werken, meer niet. Oordeel niet strikt 😉

Bron: www.habr.com

Voeg een reactie