Võrguseaded FreeRadiusest DHCP kaudu

Võrguseaded FreeRadiusest DHCP kaudu
Saabus ülesanne korraldada tellijatele IP-aadresside väljastamine. Probleemi tingimused:

  • Me ei anna teile autoriseerimiseks eraldi serverit – saate hakkama 😉
  • Abonendid peavad saama võrgusätted DHCP kaudu
  • Võrk on heterogeenne. See hõlmab PON-seadmeid ja tavalisi lüliteid konfigureeritud valikuga 82 ja WiFi-baase koos levialadega
  • Kui andmed ei vasta ühelegi IP väljastamise tingimusele, peate väljastama IP külalisvõrgust

Hea külg on see, et FreeBSD-l on endiselt server, mis võib "töötada", kuid see on "kaugel" ;), mitte "otse selles võrgus".

Samuti on olemas imeline seade nimega Mikrotik. Üldine võrguskeem on umbes selline:

Võrguseaded FreeRadiusest DHCP kaudu

Pärast mõningast mõtlemist otsustati tellijatele võrguseadete väljastamiseks kasutada FreeRadiust. Põhimõtteliselt on skeem tavaline: Microtickis lubame DHCP-serveri ja sellel Radius Clienti. Konfigureerime DHCP serveri -> Radius Client -> Radius serveri ühenduse.

See ei tundu keeruline. Aga! Kurat peitub detailides. Nimelt:

  • PON OLT autoriseerimisel selle skeemi abil saadetakse FreeRadiusele päring, mille kasutajanimi võrdub peapea MAC-aadressiga, agent-Circuit-Id võrdub MAC PON Onu-ga ja parool on tühi.
  • Valikuga 82 lülititelt autoriseerimisel saab FreeRadius päringu tühja kasutajanimega, mis on võrdne abonendi seadme MAC-iga ja mis on täidetud täiendavate atribuutidega Agent-Circuit-Id ja Agent-Remote-Id, mis sisaldavad vastavalt uuesti MAC-i. relee lüliti ja port, millega abonent on ühendatud.
  • Mõned WiFi-punktidega abonendid on volitatud PAP-CHAP protokollide kaudu
  • Mõned WIFI-punktide abonendid on volitatud kasutajanimega, mis on võrdne WIFI-punkti MAC-aadressiga, ilma paroolita.

Ajalooline taust: mis on DHCP-s valik 82?

Need on DHCP-protokolli lisavalikud, mis võimaldavad teil edastada lisateavet, näiteks väljadel Agent-Circuit-Id ja Agent-Remote-Id. Tavaliselt kasutatakse releelüliti MAC-aadressi ja pordi, millega abonent on ühendatud, edastamiseks. PON-seadmete või WIFI-tugijaamade puhul ei sisalda väli Agent-Circuit-Id kasulikku teavet (puudub abonendiport). DHCP töö üldine skeem on sel juhul järgmine:

Võrguseaded FreeRadiusest DHCP kaudu

Samm-sammult töötab see skeem järgmiselt:

  1. Kasutajaseade teeb võrgusätete hankimiseks DHCP-levipäringu
  2. Seade (näiteks lüliti, WiFi või PON-tugijaam), millega abonendiseade on otse ühendatud, “peatab” selle paketi ja muudab seda, lisades sellesse lisavõimalused Option 82 ja Relay agendi IP-aadressi ning edastab selle edasi üle võrku.
  3. DHCP-server võtab päringu vastu, genereerib vastuse ja saadab selle releeseadmesse
  4. Releeseade edastab vastuse paketi abonendiseadmele

Loomulikult ei tööta see kõik nii lihtsalt; peate oma võrguseadmed vastavalt konfigureerima.

FreeRadiuse installimine

Muidugi on seda võimalik saavutada FreeRadiuse konfiguratsiooniseadetega, kuid see on keeruline ja ebaselge... eriti kui lähete sinna pärast N kuud ja "kõik töötab". Seetõttu otsustasime kirjutada Pythonis FreeRadiuse jaoks oma autoriseerimismooduli. Võtame autoriseerimisandmed MySQL-i andmebaasist. Selle ülesehitust pole mõtet kirjeldada, niikuinii teeb igaüks selle "ise jaoks". Eelkõige võtsin kasutusele FreeRadiuse sql-mooduliga pakutava struktuuri ja muutsin seda veidi, lisades iga abonendi jaoks lisaks sisselogimisparoolile ka maci ja pordi välja.

Nii et kõigepealt installige FreeRadius:

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

Seadetes valige installimiseks:

Võrguseaded FreeRadiusest DHCP kaudu

Teeme sümlingi pythoni moodulile (st lülitame selle sisse):

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

Installime pythoni jaoks täiendava mooduli:

pip install mysql-connector

FreeRadiuse pythoni mooduli sätetes peate määrama muutujas python_path mooduli otsinguteed. Näiteks mul on selline:

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"

Teed saate teada, käivitades pythoni tõlgi ja sisestades käsud:

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

Kui te seda sammu ei tee, ei leia pythonis kirjutatud ja FreeRadiuse käivitatud skriptid impordis loetletud mooduleid. Lisaks tuleb mooduli seadetes tühistada kõnede autoriseerimise ja arvestuse funktsioonid. Näiteks näeb see moodul välja selline:

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}

}

Skript work.py (ja kõik teised) tuleb paigutada kausta /usr/local/etc/raddb/mods-config/python Mul on kokku kolm skripti.

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

Nagu koodist näha, püüame abonendi tuvastada kõigil saadaolevatel meetoditel tema teadaolevate abonendi MAC-aadresside või valiku 82 kombinatsiooni järgi ja kui see ei õnnestu, siis väljastame külaliselt vanima IP-aadressi, mida kunagi kasutatud. ” võrku. Jääb üle vaid vaikeskript konfigureerida saitide lubatud kaustas, et pythoni skriptist vajalikud funktsioonid määratud hetkedel tõmbleksid. Tegelikult piisab faili viimisest vormile:

vaikimisi

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

Proovime seda käivitada ja vaadata, mis silumislogi tuleb:

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

Mida veel. FreeRadiuse seadistamisel on mugav testida selle toimimist utiliidi radclient abil. Näiteks autoriseerimine:

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

Või 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

Tahan teid hoiatada, et sellist skeemi ja skripte on täiesti võimatu kasutada "ilma muudatusteta" "tööstuslikus" skaalal. Vähemalt märgatav:

  • MAC-aadressi on võimalik “võltsida”. Piisab, kui tellija registreerib kellegi teise MAC-i ja tekivad probleemid
  • külalisvõrkude väljastamise loogika on väljaspool kriitikat. Pole isegi kontrolli "äkki on juba sama IP-aadressiga kliente?"

See on lihtsalt minu tingimustes töötama loodud küpsiselahendus, ei midagi enamat. Ärge otsustage rangelt 😉

Allikas: www.habr.com

Lisa kommentaar