Netstillingar frá FreeRadius í gegnum DHCP

Netstillingar frá FreeRadius í gegnum DHCP
Það verkefni kom að sjá um útgáfu IP-tölu til áskrifenda. Skilyrði vandamálsins:

  • Við munum ekki gefa þér sérstakan netþjón til að fá heimild - þú munt láta þér nægja 😉
  • Áskrifendur verða að fá netstillingar í gegnum DHCP
  • Netið er misleitt. Þetta felur í sér PON búnað og venjulega rofa með stilltum valkosti 82 og þráðlausa stöð með heitum reitum
  • Ef gögnin falla ekki undir eitthvað af skilyrðunum fyrir útgáfu IP, verður þú að gefa út IP frá „gesta“ netinu

Það góða: það er enn þjónn á FreeBSD sem getur “virkað”, en hann er “langt í burtu” ;), ekki “beint á þessu neti”.

Það er líka til dásamlegt tæki sem heitir Mikrotik. Almennt netskýringarmynd er eitthvað á þessa leið:

Netstillingar frá FreeRadius í gegnum DHCP

Eftir nokkra umhugsun var ákveðið að nota FreeRadius til að gefa út netstillingar fyrir áskrifendur. Í grundvallaratriðum er kerfið venjulega: við virkum DHCP netþjóninn á Microtick og Radius Client á honum. Við stillum DHCP þjóninn -> Radius Client -> Radius miðlara tengingu.

Það virðist ekki erfitt. En! Djöfullinn er í smáatriðunum. Nefnilega:

  • Þegar PON OLT er heimilað með því að nota þetta kerfi er beiðni send til FreeRadius með notandanafni sem er jafnt og MAC vistfangi höfuðenda, Agent-Circuit-Id sem er jafnt og MAC PON Onu og tómu lykilorði.
  • Þegar heimild er veitt frá rofum með valmöguleika 82, fær FreeRadius beiðni með tómu notandanafni sem er jafnt og MAC tækisins áskrifanda og fyllt með viðbótareigindum Agent-Circuit-Id og Agent-Remote-Id sem inniheldur, í sömu röð, aftur MAC af gengisrofann og tengið sem áskrifandinn er tengdur við.
  • Sumir áskrifendur með WiFI punkta hafa heimild í gegnum PAP-CHAP samskiptareglur
  • Sumir áskrifendur frá WIFI punktum hafa leyfi með notandanafni sem er jafnt og MAC vistfangi WIFI punktsins, án lykilorðs.

Sögulegur bakgrunnur: hvað er „valkostur 82“ í DHCP

Þetta eru viðbótarvalkostir fyrir DHCP-samskiptareglur sem gera þér kleift að flytja viðbótarupplýsingar, til dæmis í reitunum Agent-Circuit-Id og Agent-Remote-Id. Venjulega notað til að senda MAC vistfang gengisrofans og tengið sem áskrifandinn er tengdur við. Þegar um er að ræða PON búnað eða WIFI grunnstöðvar, inniheldur Agent-Circuit-Id reiturinn ekki gagnlegar upplýsingar (það er engin áskrifendahöfn). Almennt kerfi DHCP aðgerða í þessu tilfelli er sem hér segir:

Netstillingar frá FreeRadius í gegnum DHCP

Skref fyrir skref þetta kerfi virkar svona:

  1. Notendabúnaður gerir DHCP útsendingarbeiðni til að fá netstillingar
  2. Tækið (td rofi, WiFi eða PON grunnstöð) sem áskrifendabúnaðurinn er beintengdur við „hlerar“ þennan pakka og breytir honum, kynnir viðbótarvalkosti 82 og Relay agent IP tölu inn í hann og sendir hann áfram yfir netið.
  3. DHCP þjónninn samþykkir beiðnina, býr til svar og sendir það til gengisbúnaðarins
  4. Sendibúnaðurinn sendir svarpakkann áfram til áskrifendabúnaðarins

Auðvitað virkar þetta allt ekki svo auðveldlega; þú þarft að stilla netbúnaðinn þinn í samræmi við það.

Að setja upp FreeRadius

Auðvitað er hægt að ná þessu með FreeRadius stillingum, en það er erfitt og óljóst ... sérstaklega þegar þú ferð þangað eftir N mánuði og "allt virkar." Þess vegna ákváðum við að skrifa okkar eigin heimildareiningu fyrir FreeRadius í Python. Við munum taka heimildargögn úr MySQL gagnagrunninum. Það þýðir ekkert að lýsa uppbyggingu þess; alla vega munu allir gera það „fyrir sig“. Sérstaklega tók ég uppbygginguna sem er í boði með sql einingunni fyrir FreeRadius, og breytti því örlítið með því að bæta við Mac og port reit fyrir hvern áskrifanda, auk innskráningarlykilorðsins.

Svo, fyrst skaltu setja upp FreeRadius:

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

Í stillingunum skaltu velja til að setja upp:

Netstillingar frá FreeRadius í gegnum DHCP

Við gerum samkennslu í python-eininguna (þ.e. „kveikjum“ á henni):

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

Við skulum setja upp viðbótareiningu fyrir Python:

pip install mysql-connector

Í stillingum python einingarinnar fyrir FreeRadius þarftu að tilgreina einingarleitarleiðirnar í python_path breytunni. Ég er til dæmis með þetta:

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"

Þú getur fundið út slóðirnar með því að ræsa python túlkinn og slá inn skipanirnar:

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

Ef þú tekur ekki þetta skref, þá munu forskriftir skrifaðar í python og hleypt af stokkunum af FreeRadius ekki finna einingarnar sem eru skráðar í innflutningi. Að auki þarftu að afmerkja aðgerðirnar til að kalla heimildir og bókhald í einingastillingunum. Til dæmis lítur þessi eining svona út:

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 forskriftin (og öll hin) verður að vera sett í /usr/local/etc/raddb/mods-config/python Ég er með þrjú forskrift alls.

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

Eins og þú sérð af kóðanum erum við að reyna að bera kennsl á áskrifandann með því að nota allar tiltækar aðferðir með þekktum MAC vistföngum hans eða valkosti 82 samsetningu, og ef þetta virkar ekki, þá gefum við út elstu IP tölu sem hefur verið notuð frá „gestinum“ “ neti. Allt sem er eftir er að stilla sjálfgefna skriftuna í möppunni sem er virkt fyrir vefsvæði, þannig að nauðsynlegar aðgerðir úr python skriftunni kippist við á tilgreindum augnablikum. Reyndar er nóg að koma skránni á formið:

sjálfgefið

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

Við skulum reyna að keyra það og sjá hvað kemur inn í kembiforritið:

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

Hvað annað. Þegar FreeRadius er sett upp er þægilegt að prófa virkni þess með radclient tólinu. Til dæmis heimild:

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

Eða reikningur:

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

Ég vil vara þig við því að það er algjörlega ómögulegt að nota slíkt kerfi og forskriftir „án breytinga“ á „iðnaðar“ mælikvarða. Að minnsta kosti áberandi:

  • það er hægt að "falsa" MAC vistfangið. Það er nóg fyrir áskrifandann að skrá MAC einhvers annars og það verða vandamál
  • rökfræðin í því að gefa út gestanet er hafin yfir gagnrýni. Það er ekki einu sinni hakið "kannski eru nú þegar viðskiptavinir með sömu IP tölu?"

Þetta er bara „kökuskera lausn“ sem er hönnuð til að virka sérstaklega við aðstæður mínar, ekkert annað. Ekki dæma strangt 😉

Heimild: www.habr.com

Bæta við athugasemd