Retaj agordoj de FreeRadius per DHCP

Retaj agordoj de FreeRadius per DHCP
La tasko alvenis por aranĝi la emision de IP-adresoj al abonantoj. Kondiĉoj de la problemo:

  • Ni ne donos al vi apartan servilon por rajtigo - vi trankviliĝos 😉
  • Abonantoj devas ricevi retajn agordojn per DHCP
  • La reto estas heterogena. Ĉi tio inkluzivas PON-ekipaĵon kaj regulajn ŝaltilojn kun agordita Opcio 82 kaj WiFi-bazojn kun retpunktoj
  • Se la datumoj ne estas sub iuj el la kondiĉoj por eldonado de IP, vi devas eldoni IP de la "gasto" reto.

Bone: ekzistas ankoraŭ servilo sur FreeBSD, kiu povas "funkcii", sed ĝi estas "malproksime" ;), ne "ĝuste en ĉi tiu reto".

Ekzistas ankaŭ mirinda aparato nomata Mikrotik. La ĝenerala retdiagramo estas io kiel ĉi tio:

Retaj agordoj de FreeRadius per DHCP

Post iom da pripensado, estis decidite uzi FreeRadius por doni retajn agordojn al abonantoj. Principe, la skemo estas kutima: ni ebligas la DHCP-servilon sur Microtick, kaj Radius Client sur ĝi. Ni agordas la DHCP-servilon -> Radius-kliento -> Radius-servilo-konekto.

Ĝi ne ŝajnas malfacila. Sed! La diablo estas en la detaloj. Nome:

  • Kiam oni rajtigas PON OLT uzante ĉi tiun skemon, peto estas sendita al FreeRadius kun Uzantnomo egala al la MAC-adreso de la ĉefkapo, Agent-Circuit-Id egala al la MAC PON Onu kaj malplena pasvorto.
  • Kiam vi rajtigas de ŝaltiloj kun opcio 82, FreeRadius ricevas peton kun malplena Uzantnomo egala al la MAC de la aparato de la abonanto kaj plenigita kun aldonaj atributoj Agent-Circuit-Id kaj Agent-Remote-Id enhavantaj, respektive, denove la MAC de la relajsoŝaltilo kaj la haveno al kiu la abonanto estas konektita.
  • Iuj abonantoj kun WiFI-punktoj estas rajtigitaj per PAP-CHAP-protokoloj
  • Iuj abonantoj de WIFI-punktoj estas rajtigitaj kun Uzantnomo egala al la MAC-adreso de la WIFI-punkto, sen pasvorto.

Historia fono: kio estas "Opcio 82" en DHCP

Ĉi tiuj estas aldonaj opcioj por la DHCP-protokolo, kiuj permesas vin transdoni pliajn informojn, ekzemple en la kampoj Agent-Circuit-Id kaj Agent-Remote-Id. Kutime uzata por elsendi la MAC-adreson de la relajsoŝaltilo kaj la havenon al kiu la abonanto estas konektita. En la kazo de PON-ekipaĵo aŭ WIFI bazstacioj, la kampo Agent-Circuit-Id ne enhavas utilajn informojn (ne ekzistas abonanto-haveno). La ĝenerala skemo de DHCP-operacio en ĉi tiu kazo estas kiel sekvas:

Retaj agordoj de FreeRadius per DHCP

Paŝo post paŝo ĉi tiu skemo funkcias jene:

  1. La uzantekipaĵo faras DHCP-elsendan peton por akiri retajn agordojn
  2. La aparato (ekzemple ŝaltilo, WiFi aŭ PON bazstacio) al kiu la abonanto-ekipaĵo estas rekte konektita "kaptas" ĉi tiun pakaĵeton kaj ŝanĝas ĝin, enkondukante aldonajn opciojn Opcion 82 kaj Relay-agentan IP-adreson en ĝin, kaj transdonas ĝin plu. la reto.
  3. La DHCP-servilo akceptas la peton, generas respondon kaj sendas ĝin al la relajsa aparato
  4. La relajsa aparato plusendas la respondpakaĵon al la abonanta aparato

Kompreneble, ĉio ne funkcias tiel facile; vi devas agordi vian retan ekipaĵon laŭe.

Instalante FreeRadius

Kompreneble, ĉi tio povas esti atingita per la agordaj agordoj de FreeRadius, sed ĝi estas malfacila kaj neklara... precipe kiam vi iras tien post N monatoj kaj "ĉio funkcias." Tial ni decidis skribi nian propran rajtigan modulon por FreeRadius en Python. Ni prenos rajtigajn datumojn el la datumbazo MySQL. Ne utilas priskribi ĝian strukturon; ĉiuokaze, ĉiuj faros ĝin "por si mem". Aparte, mi prenis la strukturon, kiu estas ofertita kun la sql-modulo por FreeRadius, kaj iomete ŝanĝis ĝin aldonante mac kaj havenkampon por ĉiu abonanto, krom la ensaluto-pasvorto.

Do unue, instalu FreeRadius:

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

En la agordoj, elektu por instali:

Retaj agordoj de FreeRadius per DHCP

Ni faras simbolligon al la python-modulo (t.e. "ŝaltu" ĝin):

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

Ni instalu plian modulon por python:

pip install mysql-connector

En la agordoj de python-modulo por FreeRadius, vi devas specifi la modulajn serĉvojojn en la variablo python_path. Ekzemple mi havas ĉi tion:

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"

Vi povas ekscii la vojojn lanĉante la python-interpretilon kaj enigante la komandojn:

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

Se vi ne faras ĉi tiun paŝon, tiam skriptoj skribitaj en python kaj lanĉitaj de FreeRadius ne trovos la modulojn listigitajn en importo. Krome, vi devas malkomenti la funkciojn por voki rajtigon kaj kontadon en la modulaj agordoj. Ekzemple, ĉi tiu modulo aspektas jene:

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}

}

La work.py skripto (kaj ĉiuj aliaj) devas esti metita en /usr/local/etc/raddb/mods-config/python Mi havas tri skriptojn entute.

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

Kiel vi povas vidi el la kodo, ni provas identigi la abonanton uzante ĉiujn disponeblajn metodojn per liaj konataj MAC-adresoj aŭ Opcio 82-kombinaĵo, kaj se ĉi tio ne funkcias, tiam ni eldonas la plej malnovan IP-adreson iam uzatan de la "gasto". ” reto. Restas nur agordi la defaŭltan skripton en la dosierujo ebligita retejo, por ke la necesaj funkcioj de la python-skripto skuiĝos en la difinitaj momentoj. Fakte, sufiĉas alporti la dosieron al la formo:

defaŭlte

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

Ni provu ruli ĝin kaj vidu kio venas en la sencimigan protokolo:

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

Kion alian. Kiam oni agordas FreeRadius, estas oportune testi ĝian funkciadon per la radclient-ilaĵo. Ekzemple rajtigo:

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

Aŭ 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

Mi volas averti vin, ke estas absolute neeble uzi tian skemon kaj skriptojn "sen ŝanĝoj" je "industria" skalo. Almenaŭ rimarkinda:

  • eblas "falsi" la MAC-adreson. Sufiĉas, ke la abonanto registri la MAC de iu alia kaj estos problemoj
  • la logiko de eldonado de gastretoj estas preter kritiko. Eĉ ne estas ĉeko "eble jam ekzistas klientoj kun la sama IP-adreso?"

Ĉi tio estas nur "kuketo-tranĉilo solvo" dizajnita por funkcii specife en miaj kondiĉoj, nenio pli. Ne juĝu strikte 😉

fonto: www.habr.com

Aldoni komenton