Network Astellunge vum FreeRadius iwwer DHCP

Network Astellunge vum FreeRadius iwwer DHCP
D'Aufgab ass ukomm fir d'Emissioun vun IP Adressen un d'Abonnenten ze arrangéieren. Konditioune vum Problem:

  • Mir ginn Iech keen separaten Server fir d'Autorisatioun - Dir maacht Iech 😉
  • Abonnente mussen Reseau Astellungen iwwer DHCP kréien
  • D'Netz ass heterogen. Dëst beinhalt PON Ausrüstung a regelméisseg Schalter mat konfiguréierter Optioun 82 a WiFi Basen mat Hotspots
  • Wann d'Donnéeën net ënner enger vun de Konditioune falen fir eng IP auszeginn, musst Dir eng IP aus dem "Gaascht" Netzwierk erausginn

Op der gudder Säit: et gëtt nach ëmmer e Server op FreeBSD dee kann "schaffen", awer et ass "wäit ewech" ;), net "direkt op dësem Netzwierk".

Et gëtt och eng wonnerbar Apparat genannt Mikrotik. Den allgemenge Netzwierkdiagramm ass sou eppes:

Network Astellunge vum FreeRadius iwwer DHCP

No e puer Gedanken, gouf decidéiert FreeRadius ze benotzen fir Netzwierkastellungen un d'Abonnenten auszeginn. Am Prinzip ass de Schema üblech: mir aktivéieren den DHCP Server op Microtick, an de Radius Client op et. Mir konfiguréieren den DHCP Server -> Radius Client -> Radius Serververbindung.

Et schéngt net schwéier. Awer! Den Däiwel ass an den Detailer. Nämlech:

  • Wann Dir e PON OLT mat dësem Schema autoriséiert, gëtt eng Ufro un FreeRadius geschéckt mat engem Benotzernumm gläich wéi d'MAC Adress vum Headend, en Agent-Circuit-Id gläich mam MAC PON Onu an engem eidele Passwuert.
  • Beim Autorisatioun vu Schalter mat der Optioun 82 kritt FreeRadius eng Ufro mat engem eidele Benotzernumm gläich dem MAC vum Apparat vum Abonnent a gefëllt mat zousätzlech Attributer Agent-Circuit-Id an Agent-Remote-Id, respektiv erëm MAC vun de Relaisschalter an den Hafen mat deem den Abonnent verbonnen ass.
  • E puer Abonnente mat WiFI Punkte sinn iwwer PAP-CHAP Protokoller autoriséiert
  • E puer Abonnente vu WIFI Punkte sinn autoriséiert mat engem Benotzernumm gläich wéi d'MAC Adress vum WIFI Punkt, ouni Passwuert.

Historeschen Hannergrond: wat ass "Optioun 82" an DHCP

Dëst sinn zousätzlech Optiounen fir den DHCP-Protokoll, déi Iech erlaben zousätzlech Informatioun ze transferéieren, zum Beispill an de Felder Agent-Circuit-Id an Agent-Remote-Id. Typesch benotzt fir d'MAC Adress vum Relaisschalter an den Hafen ze iwwerdroen, mat deem den Abonnent verbonnen ass. Am Fall vun PON Ausrüstung oder WIFI Basisstatiounen enthält den Agent-Circuit-Id Feld keng nëtzlech Informatioun (et gëtt keen Abonnenthafen). D'allgemeng Schema vun DHCP Operatioun an dësem Fall ass wéi follegt:

Network Astellunge vum FreeRadius iwwer DHCP

Schrëtt fir Schrëtt funktionnéiert dëse Schema esou:

  1. D'Benotzerausrüstung mécht eng DHCP Broadcast Ufro fir Netzwierkastellungen ze kréien
  2. Den Apparat (zum Beispill, e Schalter, WiFi oder PON Basisstatioun), mat deem d'Abonnentausrüstung direkt verbonnen ass, "interceptéiert" dëse Paket an ännert et, a féiert zousätzlech Optiounen Optioun 82 a Relais Agent IP Adress an, a schéckt se weider iwwer d'Netzwierk.
  3. Den DHCP Server akzeptéiert d'Ufro, generéiert eng Äntwert a schéckt se un de Relaisapparat
  4. De Relaisapparat schéckt den Äntwertspaket un den Abonnentapparat weider

Natierlech funktionnéiert et alles net sou einfach; Dir musst Är Netzwierkausrüstung deementspriechend konfiguréieren.

Installéiert FreeRadius

Natierlech kann dëst mat de FreeRadius Konfiguratiounsastellungen erreecht ginn, awer et ass schwéier an onkloer ... besonnesch wann Dir no N Méint dohinner gitt an "alles funktionnéiert." Dofir hu mir decidéiert eisen eegene Autorisatiounsmodul fir FreeRadius am Python ze schreiwen. Mir huelen Autorisatiounsdaten aus der MySQL Datebank. Et ass kee Sënn seng Struktur ze beschreiwen; egal, jidderee wäert et "fir selwer" maachen. Besonnesch hunn ech d'Struktur geholl, déi mam sql Modul fir FreeRadius ugebuede gëtt, a liicht geännert andeems en e Mac- a Portfeld fir all Abonnent bäigefüügt gëtt, zousätzlech zum Login-Passwuert.

Also, als éischt, installéiert FreeRadius:

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

An den Astellungen, wielt fir ze installéieren:

Network Astellunge vum FreeRadius iwwer DHCP

Mir maachen e Symlink zum Python Modul (dh "schalten un"):

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

Loosst eis en zousätzleche Modul fir Python installéieren:

pip install mysql-connector

An de Python Modul Astellunge fir FreeRadius musst Dir d'Modul Sichweeër an der Python_path Variabel spezifizéieren. Zum Beispill hunn ech dëst:

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"

Dir kënnt d'Weeër erausfannen andeems Dir de Python Dolmetscher lancéiert an d'Befehle gitt:

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

Wann Dir dëse Schrëtt net maacht, da wäerte Skripte, déi am Python geschriwwe sinn a vum FreeRadius lancéiert ginn, d'Moduler net fannen, déi am Import opgelëscht sinn. Zousätzlech musst Dir d'Funktioune fir Autorisatioun a Comptabilitéit an de Modulastellungen unkommentéieren. Zum Beispill gesäit dëse Modul esou 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}

}

D'work.py Skript (an all déi aner) muss an /usr/local/etc/raddb/mods-config/python gesat ginn Ech hunn dräi Skripte am Ganzen.

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

Wéi Dir aus dem Code kënnt gesinn, probéieren mir den Abonnent mat all verfügbare Methoden duerch seng bekannte Abonnent MAC Adressen oder Optioun 82 Kombinatioun ze identifizéieren, a wann dëst net funktionnéiert, da gi mir déi eelst IP Adress déi jeemools vum "Gaast benotzt gouf" erausginn. "Netzwierk. Alles wat bleift ass de Standardskript am Site-aktivéierten Dossier ze konfiguréieren, sou datt déi néideg Funktiounen aus dem Python Skript op den designéierte Momenter zéien. Tatsächlech ass et genuch fir de Fichier op d'Form ze bréngen:

Standarddrécker

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

Loosst eis probéieren et auszeféieren a kucken wat an de Debug Log kënnt:

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

Wat soss. Wann Dir FreeRadius opstellt, ass et bequem seng Operatioun mam Radclient Utility ze testen. Zum Beispill Autorisatioun:

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 Kont:

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

Ech wëll Iech warnen datt et absolut onméiglech ass sou e Schema a Skripte "ouni Ännerungen" op enger "industrieller" Skala ze benotzen. Op d'mannst bemierkenswäert:

  • et ass méiglech d'MAC Adress ze "fälschen". Et ass genuch fir den Abonnent engem aneren MAC anzeschreiwen an et wäert Problemer ginn
  • d'Logik vun der erausginn Gaascht Netzwierker ass iwwer Kritik. Et gëtt net emol e Scheck "vläicht ginn et scho Cliente mat der selwechter IP Adress?"

Dëst ass just eng "Cookie-Cutter Léisung" entwéckelt fir speziell a menge Bedéngungen ze schaffen, näischt méi. Riichter net streng 😉

Source: will.com

Setzt e Commentaire