Mga setting ng network mula sa FreeRadius sa pamamagitan ng DHCP

Mga setting ng network mula sa FreeRadius sa pamamagitan ng DHCP
Dumating ang gawain upang ayusin ang pagpapalabas ng mga IP address sa mga subscriber. Mga kondisyon ng problema:

  • Hindi ka namin bibigyan ng hiwalay na server para sa pahintulot - gagawin mo 😉
  • Ang mga subscriber ay dapat makatanggap ng mga setting ng network sa pamamagitan ng DHCP
  • Ang network ay magkakaiba. Kabilang dito ang kagamitan ng PON at mga regular na switch na may naka-configure na Opsyon 82 at mga base ng WiFi na may mga hotspot
  • Kung ang data ay hindi nasa ilalim ng alinman sa mga kundisyon para sa pag-isyu ng IP, dapat kang mag-isyu ng IP mula sa network ng "panauhin"

Sa magandang bahagi: mayroon pa ring server sa FreeBSD na maaaring "gumana", ngunit ito ay "malayo" ;), hindi "tama sa network na ito".

Mayroon ding isang kahanga-hangang aparato na tinatawag na Mikrotik. Ang pangkalahatang network diagram ay katulad nito:

Mga setting ng network mula sa FreeRadius sa pamamagitan ng DHCP

Pagkatapos ng ilang pag-iisip, napagpasyahan na gamitin ang FreeRadius upang mag-isyu ng mga setting ng network sa mga subscriber. Sa prinsipyo, ang scheme ay karaniwan: pinapagana namin ang DHCP server sa Microtick, at ang Radius Client dito. Kino-configure namin ang DHCP server -> Radius Client -> Radius server connection.

Parang hindi mahirap. Ngunit! Ang diyablo ay nasa mga detalye. Namely:

  • Kapag pinahihintulutan ang isang PON OLT gamit ang scheme na ito, ipinapadala ang isang kahilingan sa FreeRadius na may User-Name na katumbas ng MAC address ng headend, isang Agent-Circuit-Id na katumbas ng MAC PON Onu at isang walang laman na password.
  • Kapag pinahihintulutan mula sa mga switch na may opsyon 82, ang FreeRadius ay tumatanggap ng kahilingan na may walang laman na User-Name na katumbas ng MAC ng device ng subscriber at napuno ng karagdagang mga katangian Agent-Circuit-Id at Agent-Remote-Id na naglalaman, ayon sa pagkakabanggit, muli ng MAC ng ang relay switch at ang port kung saan nakakonekta ang subscriber.
  • Ang ilang subscriber na may mga WiFI point ay pinahihintulutan sa pamamagitan ng mga protocol ng PAP-CHAP
  • Ang ilang subscriber mula sa mga WIFI point ay pinahintulutan na may User-Name na katumbas ng MAC address ng WIFI point, nang walang password.

Makasaysayang background: ano ang "Pagpipilian 82" sa DHCP

Ito ay mga karagdagang opsyon para sa DHCP protocol na nagbibigay-daan sa iyong maglipat ng karagdagang impormasyon, halimbawa sa mga field ng Agent-Circuit-Id at Agent-Remote-Id. Karaniwang ginagamit upang ipadala ang MAC address ng relay switch at ang port kung saan nakakonekta ang subscriber. Sa kaso ng PON equipment o WIFI base station, ang Agent-Circuit-Id field ay hindi naglalaman ng kapaki-pakinabang na impormasyon (walang subscriber port). Ang pangkalahatang pamamaraan ng pagpapatakbo ng DHCP sa kasong ito ay ang mga sumusunod:

Mga setting ng network mula sa FreeRadius sa pamamagitan ng DHCP

Hakbang-hakbang ang scheme na ito ay gumagana tulad nito:

  1. Gumagawa ang kagamitan ng user ng DHCP broadcast request para makakuha ng network settings
  2. Ang aparato (halimbawa, isang switch, WiFi o PON base station) kung saan direktang konektado ang kagamitan ng subscriber ay "nakaharang" sa packet na ito at binabago ito, na nagpapakilala ng mga karagdagang opsyon na Opsyon 82 at Relay agent IP address dito, at ipinapadala ito nang higit pa. ang network.
  3. Tinatanggap ng DHCP server ang kahilingan, bumubuo ng tugon at ipinapadala ito sa relay device
  4. Ipinapasa ng relay device ang response packet sa subscriber device

Siyempre, hindi ganoon kadaling gumana ang lahat; kailangan mong i-configure ang iyong kagamitan sa network nang naaayon.

Pag-install ng FreeRadius

Siyempre, maaari itong makamit gamit ang mga setting ng pagsasaayos ng FreeRadius, ngunit mahirap at hindi malinaw... lalo na kapag pumunta ka doon pagkatapos ng N buwan at "ang lahat ay gumagana." Samakatuwid, nagpasya kaming magsulat ng sarili naming module ng awtorisasyon para sa FreeRadius sa Python. Kukunin namin ang data ng pahintulot mula sa database ng MySQL. Walang saysay na ilarawan ang istraktura nito; gayunpaman, gagawin ito ng lahat "para sa kanilang sarili." Sa partikular, kinuha ko ang istraktura na inaalok kasama ang sql module para sa FreeRadius, at bahagyang binago ito sa pamamagitan ng pagdaragdag ng mac at port field para sa bawat subscriber, bilang karagdagan sa login-password.

Kaya, una, i-install ang FreeRadius:

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

Sa mga setting, piliin upang i-install:

Mga setting ng network mula sa FreeRadius sa pamamagitan ng DHCP

Gumagawa kami ng symlink sa python module (i.e. "i-on" ito):

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

Mag-install tayo ng karagdagang module para sa python:

pip install mysql-connector

Sa mga setting ng python module para sa FreeRadius, kailangan mong tukuyin ang mga path ng paghahanap ng module sa variable na python_path. Halimbawa, mayroon akong ganito:

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"

Maaari mong malaman ang mga landas sa pamamagitan ng paglulunsad ng python interpreter at pagpasok ng mga utos:

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

Kung hindi mo gagawin ang hakbang na ito, hindi mahahanap ng mga script na nakasulat sa python at inilunsad ng FreeRadius ang mga module na nakalista sa import. Bilang karagdagan, kailangan mong i-uncomment ang mga function para sa pagtawag sa awtorisasyon at accounting sa mga setting ng module. Halimbawa, ganito ang hitsura ng modyul na ito:

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}

}

Ang work.py script (at lahat ng iba pa) ay dapat ilagay sa /usr/local/etc/raddb/mods-config/python Mayroon akong tatlong script sa kabuuan.

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

Tulad ng nakikita mo mula sa code, sinusubukan naming kilalanin ang subscriber gamit ang lahat ng magagamit na pamamaraan sa pamamagitan ng kanyang kilalang mga MAC address ng subscriber o kumbinasyon ng Option 82, at kung hindi ito gumana, pagkatapos ay ilalabas namin ang pinakalumang IP address na ginamit mula sa "panauhin ” network. Ang natitira na lang ay upang i-configure ang default na script sa folder na pinagana ng mga site, upang ang mga kinakailangang function mula sa script ng python ay kumikibot sa mga itinalagang sandali. Sa katunayan, ito ay sapat na upang dalhin ang file sa form:

default

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

Subukan nating patakbuhin ito at tingnan kung ano ang pumapasok sa log ng debug:

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

Ano pa. Kapag nagse-set up ng FreeRadius, maginhawang subukan ang operasyon nito gamit ang radclient utility. Halimbawa ng awtorisasyon:

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

O account:

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

Nais kong balaan ka na talagang imposibleng gumamit ng gayong pamamaraan at mga script na "nang walang mga pagbabago" sa isang "pang-industriya" na sukat. Hindi bababa sa kapansin-pansin:

  • posibleng "pekeng" ang MAC address. Sapat na para sa subscriber na magrehistro ng MAC ng ibang tao at magkakaroon ng mga problema
  • ang lohika ng pag-isyu ng mga guest network ay lampas sa kritisismo. Walang kahit isang tseke "baka mayroon nang mga kliyente na may parehong IP address?"

Ito ay isa lamang "cookie-cutter solution" na idinisenyo para partikular na gumana sa aking mga kundisyon, wala nang iba pa. Huwag husgahan ng mahigpit 😉

Pinagmulan: www.habr.com

Magdagdag ng komento