Verkkoasetukset FreeRadiuksesta DHCP:n kautta

Verkkoasetukset FreeRadiuksesta DHCP:n kautta
Tehtävänä saapui IP-osoitteiden jakamisen järjestäminen tilaajille. Ongelman olosuhteet:

  • Emme anna sinulle erillistä palvelinta valtuutusta varten - sinä pärjäät 😉
  • Tilaajien on saatava verkkoasetukset DHCP:n kautta
  • Verkosto on heterogeeninen. Tämä sisältää PON-laitteet ja tavalliset kytkimet, joissa on konfiguroitu Optio 82, ja WiFi-tukiasemat hotspoteilla
  • Jos tiedot eivät täytä minkään IP-osoitteen myöntämisen ehtoa, sinun on annettava IP "vierasverkosta"

Hyvä puoli: FreeBSD:ssä on edelleen palvelin, joka voi "toimia", mutta se on "kaukana" ;), ei "oikein tässä verkossa".

On myös ihana laite nimeltä Mikrotik. Yleinen verkkokaavio on suunnilleen tällainen:

Verkkoasetukset FreeRadiuksesta DHCP:n kautta

Hetken harkinnan jälkeen päätettiin käyttää FreeRadiusta verkkoasetusten antamiseen tilaajille. Periaatteessa kaava on tavallinen: otamme käyttöön DHCP-palvelimen Microtickissä ja Radius Clientin siinä. Määritämme DHCP-palvelimen -> Radius Client -> Radius-palvelinyhteyden.

Ei vaikuta vaikealta. Mutta! Paholainen on yksityiskohdissa. Nimittäin:

  • Kun valtuutetaan PON OLT tällä menetelmällä, FreeRadiukselle lähetetään pyyntö, jonka käyttäjätunnus on yhtä suuri kuin päälaitteen MAC-osoite, Agent-Circuit-Id on yhtä suuri kuin MAC PON Onu ja tyhjä salasana.
  • Kun valtuutetaan kytkimiltä vaihtoehdolla 82, FreeRadius vastaanottaa pyynnön, jossa on tyhjä käyttäjätunnus, joka on yhtä suuri kuin tilaajan laitteen MAC ja joka on täytetty lisämääritteillä Agent-Circuit-Id ja Agent-Remote-Id, jotka sisältävät vastaavasti jälleen MAC:n relekytkin ja portti, johon tilaaja on kytketty.
  • Jotkut tilaajat, joilla on Wi-Fi-pisteitä, on valtuutettu PAP-CHAP-protokollien kautta
  • Jotkut WIFI-pisteiden tilaajat on valtuutettu käyttäjätunnuksella, joka on sama kuin WIFI-pisteen MAC-osoite, ilman salasanaa.

Historiallinen tausta: mikä on "Option 82" DHCP:ssä

Nämä ovat DHCP-protokollan lisäasetuksia, joiden avulla voit siirtää lisätietoja esimerkiksi Agent-Circuit-Id- ja Agent-Remote-Id-kentissä. Yleensä käytetään välittämään relekytkimen MAC-osoite ja portti, johon tilaaja on kytketty. PON-laitteiden tai WIFI-tukiasemien tapauksessa Agent-Circuit-Id-kenttä ei sisällä hyödyllistä tietoa (ei ole tilaajaporttia). DHCP-toiminnan yleinen kaavio tässä tapauksessa on seuraava:

Verkkoasetukset FreeRadiuksesta DHCP:n kautta

Askel askeleelta tämä kaavio toimii näin:

  1. Käyttäjälaite tekee DHCP-lähetyspyynnön verkkoasetusten saamiseksi
  2. Laite (esim. kytkin, WiFi tai PON-tukiasema), johon tilaajalaite on suoraan kytkettynä, "sieppaa" tämän paketin ja muuttaa sitä tuoden siihen lisävaihtoehdot Optio 82 ja Välitysagentin IP-osoite ja lähettää sen edelleen. verkko.
  3. DHCP-palvelin hyväksyy pyynnön, luo vastauksen ja lähettää sen välityslaitteeseen
  4. Välityslaite välittää vastauspaketin tilaajalaitteelle

Tietenkään kaikki ei toimi niin helposti; sinun on määritettävä verkkolaitteesi vastaavasti.

FreeRadiuksen asennus

Tietysti tämä voidaan saavuttaa FreeRadiuksen asetusten avulla, mutta se on vaikeaa ja epäselvää... varsinkin kun menee sinne N kuukauden kuluttua ja "kaikki toimii". Siksi päätimme kirjoittaa oman valtuutusmoduulimme FreeRadiukselle Pythonissa. Otamme valtuutustiedot MySQL-tietokannasta. Sen rakennetta on turha kuvailla, joka tapauksessa jokainen tekee sen "itsekseen". Erityisesti otin FreeRadiuksen sql-moduulin kanssa tarjotun rakenteen ja muutin sitä hieman lisäämällä jokaiselle tilaajalle mac- ja porttikentän sisäänkirjautumissalasanan lisäksi.

Joten asenna ensin FreeRadius:

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

Valitse asetuksista asennettava:

Verkkoasetukset FreeRadiuksesta DHCP:n kautta

Teemme symlinkin python-moduuliin (eli "ota käyttöön"):

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

Asennataan lisämoduuli pythonille:

pip install mysql-connector

FreeRadiuksen python-moduuliasetuksissa sinun on määritettävä moduulin hakupolut python_path-muuttujassa. Minulla on esimerkiksi tämä:

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"

Voit selvittää polut käynnistämällä python-tulkin ja kirjoittamalla komennot:

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

Jos et tee tätä vaihetta, pythonilla kirjoitetut ja FreeRadiuksen käynnistämät skriptit eivät löydä tuonnissa lueteltuja moduuleja. Lisäksi sinun on poistettava moduulin asetuksista kutsuvan valtuutuksen ja kirjanpidon toiminnot. Tämä moduuli näyttää esimerkiksi tältä:

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-skripti (ja kaikki muut) on sijoitettava kansioon /usr/local/etc/raddb/mods-config/python. Minulla on yhteensä kolme komentosarjaa.

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

Kuten koodista näkyy, yritämme tunnistaa tilaajan kaikilla käytettävissä olevilla menetelmillä hänen tunnettujen tilaajien MAC-osoitteiden tai vaihtoehdon 82 yhdistelmän perusteella, ja jos tämä ei auta, annamme vanhimman koskaan käytetyn IP-osoitteen "vieraalta". ”verkko. Jäljelle jää vain oletuskomentosarjan määrittäminen sites-enabled-kansioon, jotta python-skriptin tarvittavat toiminnot nykivät määrättyinä hetkinä. Itse asiassa riittää, että tiedosto tuodaan muotoon:

oletusarvo

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

Yritetään suorittaa se ja katsotaan, mitä virheenkorjauslokissa on:

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

Mitä muuta. FreeRadiusta määritettäessä on kätevää testata sen toimintaa radclient-apuohjelmalla. Esimerkiksi valtuutus:

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

Tai tili:

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

Haluan varoittaa, että on täysin mahdotonta käyttää tällaista järjestelmää ja komentosarjoja "ilman muutoksia" "teollisessa" mittakaavassa. Ainakin havaittavissa:

  • on mahdollista "väärentää" MAC-osoite. Riittää, kun tilaaja rekisteröi jonkun toisen MAC:n ja ongelmia tulee
  • Vierasverkkojen luomisen logiikka on kritisoimaton. Ei edes tarkistusta "ehkä on jo asiakkaita, joilla on sama IP-osoite?"

Tämä on vain "evästeen leikkaava ratkaisu", joka on suunniteltu toimimaan erityisesti minun olosuhteissani, ei sen enempää. Älä tuomitse tiukasti 😉

Lähde: will.com

Lisää kommentti