Tinklo nustatymai iš FreeRadius per DHCP

Tinklo nustatymai iš FreeRadius per DHCP
Atėjo užduotis pasirūpinti IP adresų išdavimu abonentams. Problemos sąlygos:

  • Atskiro serverio autorizacijai nesuteiksime – susitvarkysite 😉
  • Abonentai turi gauti tinklo nustatymus per DHCP
  • Tinklas yra nevienalytis. Tai apima PON įrangą ir įprastus jungiklius su sukonfigūruota 82 parinktimi ir WiFi bazes su viešosios interneto prieigos taškais
  • Jei duomenims nepatenka nė viena iš IP išdavimo sąlygų, turite išduoti IP iš „svečio“ tinklo

Geroji pusė: FreeBSD vis dar yra serveris, kuris gali „veikti“, bet jis yra „toli“ ;), o ne „tiesiame šiame tinkle“.

Taip pat yra nuostabus prietaisas „Mikrotik“. Bendra tinklo schema yra maždaug tokia:

Tinklo nustatymai iš FreeRadius per DHCP

Šiek tiek pagalvojus, buvo nuspręsta naudoti „FreeRadius“ tinklo nustatymams išduoti abonentams. Iš esmės schema yra įprasta: „Microtick“ įjungiame DHCP serverį, o jame – „Radius Client“. Konfigūruojame DHCP serverį -> Radius Client -> Radius serverio ryšį.

Tai neatrodo sunku. Bet! Velnias slypi detalėse. Būtent:

  • Įgaliojant PON OLT naudojant šią schemą, į „FreeRadius“ siunčiama užklausa, kurios vartotojo vardas yra lygus antraštinės dalies MAC adresui, agento grandinės ID lygus MAC PON Onu ir tuščias slaptažodis.
  • Kai autorizuojama iš jungiklių su 82 parinktimi, „FreeRadius“ gauna užklausą su tuščiu vartotojo vardu, lygiu abonento įrenginio MAC ir užpildytu papildomais atributais Agent-Circuit-Id ir Agent-Remote-Id, kuriuose atitinkamai vėl yra MAC relės jungiklis ir prievadas, prie kurio prijungtas abonentas.
  • Kai kurie abonentai, turintys „WiFi“ taškus, yra įgalioti naudojant PAP-CHAP protokolus
  • Kai kurie abonentai iš WIFI taškų yra įgalioti su vartotojo vardu, lygiu WIFI taško MAC adresu, be slaptažodžio.

Istorinis pagrindas: kas yra DHCP „82 parinktis“.

Tai yra papildomos DHCP protokolo parinktys, leidžiančios perkelti papildomą informaciją, pavyzdžiui, laukuose Agent-Circuit-Id ir Agent-Remote-Id. Paprastai naudojamas perduoti relės jungiklio MAC adresą ir prievadą, prie kurio prijungtas abonentas. PON įrangos arba WIFI bazinių stočių atveju lauke Agent-Circuit-Id nėra naudingos informacijos (nėra abonento prievado). Bendra DHCP veikimo schema šiuo atveju yra tokia:

Tinklo nustatymai iš FreeRadius per DHCP

Žingsnis po žingsnio ši schema veikia taip:

  1. Vartotojo įranga pateikia DHCP transliacijos užklausą, kad gautų tinklo nustatymus
  2. Įrenginys (pavyzdžiui, jungiklis, WiFi ar PON bazinė stotis), prie kurio tiesiogiai prijungta abonento įranga, „perima“ šį paketą ir jį pakeičia, įvesdamas į jį papildomas parinktis Option 82 ir Perdavimo agento IP adresą ir perduoda jį toliau. tinklas.
  3. DHCP serveris priima užklausą, sugeneruoja atsakymą ir siunčia jį į perdavimo įrenginį
  4. Relės įrenginys perduoda atsakymo paketą abonento įrenginiui

Žinoma, viskas neveikia taip lengvai; turite atitinkamai sukonfigūruoti tinklo įrangą.

FreeRadius diegimas

Žinoma, tai galima pasiekti su „FreeRadius“ konfigūracijos nustatymais, bet tai sunku ir neaišku... ypač kai ten nuvažiuoji po N mėnesių ir „viskas veikia“. Todėl nusprendėme parašyti savo „FreeRadius“ autorizacijos modulį „Python“. Autorizacijos duomenis paimsime iš MySQL duomenų bazės. Nėra prasmės apibūdinti jo struktūrą, bet kokiu atveju kiekvienas pasidarys ją „sau“. Visų pirma, aš pasirinkau struktūrą, kuri yra siūloma su sql moduliu, skirtu „FreeRadius“, ir šiek tiek ją pakeičiau pridėdamas „mac“ ir prievado lauką kiekvienam abonentui, be prisijungimo slaptažodžio.

Taigi pirmiausia įdiekite „FreeRadius“:

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

Nustatymuose pasirinkite, kad įdiegtumėte:

Tinklo nustatymai iš FreeRadius per DHCP

Sukuriame simbolinę nuorodą į python modulį (t. y. „įjungiame“):

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

Įdiegkime papildomą python modulį:

pip install mysql-connector

Python modulio nustatymuose, skirtuose FreeRadius, kintamajame python_path turite nurodyti modulio paieškos kelius. Pavyzdžiui, aš turiu tai:

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"

Kelius galite sužinoti paleidę python interpreterį ir įvedę 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']
>

Jei neatliksite šio veiksmo, scenarijai, parašyti python ir paleisti FreeRadius, neras modulių, kurie yra išvardyti importuojant. Be to, modulio nustatymuose reikia atšaukti skambučių autorizacijos ir apskaitos funkcijas. Pavyzdžiui, šis modulis atrodo taip:

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 scenarijus (ir visi kiti) turi būti įdėtas į /usr/local/etc/raddb/mods-config/python. Iš viso turiu tris scenarijus.

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

Kaip matote iš kodo, bandome identifikuoti abonentą visais prieinamais būdais pagal jo žinomus abonento MAC adresus arba 82 variantą, o jei tai nepadeda, iš „svečio“ išduodame seniausią kada nors naudotą IP adresą. “ tinklą. Belieka sukonfigūruoti numatytąjį scenarijų aplanke, kuriame įjungtos svetainės, kad reikiamos python scenarijaus funkcijos sutrūktų tam tikru momentu. Tiesą sakant, pakanka perkelti failą į formą:

numatytasis

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

Pabandykime jį paleisti ir pažiūrėti, kas patenka į derinimo žurnalą:

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

Kas dar. Nustatant „FreeRadius“, patogu išbandyti jo veikimą naudojant „radclient“ įrankį. Pavyzdžiui, įgaliojimas:

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

Arba paskyra:

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

Noriu perspėti, kad visiškai neįmanoma naudoti tokios schemos ir scenarijų „be pakeitimų“ „pramoniniu“ mastu. Bent jau pastebimas:

  • galima „padirbti“ MAC adresą. Abonentui pakanka užregistruoti kažkieno MAC ir kils problemų
  • svečių tinklų išdavimo logika nekelia kritikos. Nėra net varnelės "gal jau yra klientų su tuo pačiu IP adresu?"

Tai tik „slapukų pjaustymo sprendimas“, sukurtas veikti būtent mano sąlygomis, nieko daugiau. Neteiskite griežtai 😉

Šaltinis: www.habr.com

Добавить комментарий