Tīkla iestatījumi no FreeRadius, izmantojot DHCP

Tīkla iestatījumi no FreeRadius, izmantojot DHCP
Sanāca uzdevums noorganizēt IP adrešu izsniegšanu abonentiem. Problēmas apstākļi:

  • Mēs nedosim jums atsevišķu serveri autorizācijai - jūs iztiksiet 😉
  • Abonentiem ir jāsaņem tīkla iestatījumi, izmantojot DHCP
  • Tīkls ir neviendabīgs. Tas ietver PON aprīkojumu un parastos slēdžus ar konfigurētu opciju 82 un WiFi bāzes ar karstajiem punktiem
  • Ja uz datiem neattiecas neviens no IP izsniegšanas nosacījumiem, jums ir jāizsniedz IP no “viesa” tīkla

No labās puses: FreeBSD joprojām ir serveris, kas var “strādāt”, bet tas ir “tālu” ;), nevis “tieši šajā tīklā”.

Ir arī brīnišķīga ierīce ar nosaukumu Mikrotik. Vispārējā tīkla diagramma ir aptuveni šāda:

Tīkla iestatījumi no FreeRadius, izmantojot DHCP

Pēc pārdomām tika nolemts izmantot FreeRadius, lai abonentiem izsniegtu tīkla iestatījumus. Principā shēma ir parasta: Microtick iespējojam DHCP serveri un tajā Radius Client. Mēs konfigurējam DHCP serveri -> Radius Client -> Radius servera savienojumu.

Šķiet, ka tas nav grūti. Bet! Velns slēpjas detaļās. Proti:

  • Autorizējot PON OLT, izmantojot šo shēmu, FreeRadius tiek nosūtīts pieprasījums ar lietotāja vārdu, kas ir vienāds ar galvas galamērķa MAC adresi, aģenta ķēdes ID, kas vienāds ar MAC PON Onu, un tukšu paroli.
  • Autorizējot no slēdžiem ar opciju 82, FreeRadius saņem pieprasījumu ar tukšu lietotājvārdu, kas vienāds ar abonenta ierīces MAC un aizpildīts ar papildu atribūtiem Agent-Circuit-Id un Agent-Remote-Id, kas satur attiecīgi atkal MAC releja slēdzis un ports, kuram ir pievienots abonents.
  • Daži abonenti ar WiFI punktiem tiek autorizēti, izmantojot PAP-CHAP protokolus
  • Daži abonenti no WIFI punktiem tiek autorizēti ar lietotājvārdu, kas ir vienāds ar WIFI punkta MAC adresi, bez paroles.

Vēsturiskais fons: kas ir “82. iespēja” DHCP

Šīs ir DHCP protokola papildu opcijas, kas ļauj pārsūtīt papildu informāciju, piemēram, laukos Agent-Circuit-Id un Agent-Remote-Id. Parasti izmanto, lai pārsūtītu releja slēdža MAC adresi un portu, kuram ir pievienots abonents. PON aprīkojuma vai WIFI bāzes staciju gadījumā laukā Agent-Circuit-Id nav noderīgas informācijas (nav abonenta porta). Vispārējā DHCP darbības shēma šajā gadījumā ir šāda:

Tīkla iestatījumi no FreeRadius, izmantojot DHCP

Soli pa solim šī shēma darbojas šādi:

  1. Lietotāja iekārta veic DHCP apraides pieprasījumu, lai iegūtu tīkla iestatījumus
  2. Ierīce (piemēram, slēdzis, WiFi vai PON bāzes stacija), kurai ir tieši pieslēgta abonenta iekārta, “pārtver” šo paketi un maina to, ieviešot tajā papildu opcijas Option 82 un Relay agent IP address, un pārraida to tālāk. tīklu.
  3. DHCP serveris pieņem pieprasījumu, ģenerē atbildi un nosūta to releja ierīcei
  4. Releja ierīce pārsūta atbildes paketi uz abonenta ierīci

Protams, tas viss nedarbojas tik vienkārši; jums ir attiecīgi jākonfigurē tīkla aprīkojums.

FreeRadius instalēšana

Protams, to var panākt ar FreeRadius konfigurācijas iestatījumiem, taču tas ir sarežģīti un neskaidri... it īpaši, ja dodaties tur pēc N mēnešiem un "viss darbojas". Tāpēc mēs nolēmām uzrakstīt savu autorizācijas moduli FreeRadius programmā Python. Mēs ņemsim autorizācijas datus no MySQL datu bāzes. Nav jēgas aprakstīt tās struktūru; vienalga, katrs to izveidos "sev". Jo īpaši es izmantoju struktūru, kas tiek piedāvāta ar sql moduli FreeRadius, un nedaudz mainīju to, papildus pieteikšanās parolei pievienojot katram abonentam Mac un porta lauku.

Tātad, vispirms instalējiet FreeRadius:

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

Iestatījumos atlasiet, lai instalētu:

Tīkla iestatījumi no FreeRadius, izmantojot DHCP

Mēs izveidojam simbolisku saiti uz python moduli (t.i., “ieslēdzam” to):

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

Instalēsim papildu moduli python:

pip install mysql-connector

Python moduļa iestatījumos FreeRadius mainīgajā python_path ir jānorāda moduļa meklēšanas ceļi. Piemēram, man ir šāds:

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"

Ceļus var uzzināt, palaižot python tulku un ievadot komandas:

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

Ja neveicat šo darbību, skripti, kas rakstīti programmā python un kurus palaiž FreeRadius, neatradīs importēšanas sarakstā norādītos moduļus. Turklāt moduļa iestatījumos ir jāatmet izsaukuma autorizācijas un uzskaites funkcijas. Piemēram, šis modulis izskatās šādi:

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}

}

Work.py skripts (un visi pārējie) ir jāievieto mapē /usr/local/etc/raddb/mods-config/python Man kopā ir trīs 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

Kā redzams no koda, mēs cenšamies identificēt abonentu, izmantojot visas pieejamās metodes pēc viņa zināmajām abonenta MAC adresēm vai opcijas 82 kombinācijas, un, ja tas nedarbojas, mēs izsniedzam vecāko IP adresi, kas jebkad izmantota no “viesa”. ” tīkls. Atliek tikai konfigurēt noklusējuma skriptu vietņu iespējotajā mapē, lai norādītajos brīžos vajadzīgās funkcijas no python skripta raustītos. Faktiski pietiek ar failu nogādāšanu formā:

noklusējuma

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

Mēģināsim to palaist un redzēt, kas tiek iekļauts atkļūdošanas žurnālā:

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

Kas vēl. Iestatot FreeRadius, ir ērti pārbaudīt tā darbību, izmantojot utilītu radclient. Piemēram, autorizācija:

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

Vai konts:

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

Es gribu jūs brīdināt, ka ir absolūti neiespējami izmantot šādu shēmu un skriptus "bez izmaiņām" "rūpnieciskā" mērogā. Vismaz pamanāms:

  • ir iespējams “viltot” MAC adresi. Pietiek, ja abonents reģistrē kāda cita MAC, un radīsies problēmas
  • viesu tīklu izdošanas loģika ir ārpus kritikas. Nav pat atzīmes “varbūt jau ir klienti ar tādu pašu IP adresi?”

Šis ir tikai "sīkfailu griezēja risinājums", kas paredzēts darbam tieši manos apstākļos, nekas vairāk. Nespried stingri 😉

Avots: www.habr.com

Pievieno komentāru