DHCP арқылы FreeRadius желі параметрлері

DHCP арқылы FreeRadius желі параметрлері
Абоненттерге IP мекенжайларын беруді ұйымдастыру міндеті келді. Мәселенің шарттары:

  • Біз сізге авторизация үшін бөлек сервер бермейміз - сіз жасайсыз 😉
  • Жазылушылар желі параметрлерін DHCP арқылы алуы керек
  • Желі гетерогенді. Бұған PON жабдығы мен конфигурацияланған 82 опциясы бар кәдімгі қосқыштар және хотспоттары бар WiFi базалары кіреді
  • Егер деректер IP беру шарттарына сәйкес келмесе, сіз «қонақ» желісінен IP беруіңіз керек.

Жақсы жағы: FreeBSD-де әлі де «жұмыс істей алатын» сервер бар, бірақ ол «тікелей осы желіде» емес, «алыс»;

Микротик деген керемет құрылғы да бар. Жалпы желі диаграммасы келесідей:

DHCP арқылы FreeRadius желі параметрлері

Біраз ойланып, жазылушыларға желі параметрлерін беру үшін FreeRadius пайдалану туралы шешім қабылданды. Негізінде, схема әдеттегідей: біз Microtick жүйесінде DHCP серверін және оған Radius Client-ті қосамыз. DHCP сервері -> Radius Client -> Radius сервер қосылымын конфигурациялаймыз.

Бұл қиын емес сияқты. Бірақ! Шайтан егжей-тегжейлі. Атап айтқанда:

  • Осы схеманы пайдаланып PON OLT авторизациясы кезінде FreeRadius-қа тақырыптың MAC мекенжайына тең Пайдаланушы атымен, MAC PON Onu-ға тең Агент-схема идентификаторымен және бос құпия сөзбен сұрау жіберіледі.
  • 82 опциясы бар коммутаторлардан авторизациялау кезінде FreeRadius абоненттік құрылғының MAC мәніне тең бос Пайдаланушы аты бар сұрауды алады және сәйкесінше Agent-Circuit-Id және Agent-Remote-Id қосымша атрибуттарымен толтырылған, қайтадан MAC-ты қамтиды. реле қосқышы және абонент қосылған порт.
  • WiFI нүктелері бар кейбір абоненттер PAP-CHAP протоколдары арқылы рұқсат етілген
  • WIFI нүктелерінің кейбір абоненттері WIFI нүктесінің MAC мекенжайына тең пайдаланушы атымен, құпия сөзсіз рұқсат етілген.

Тарихи дерек: DHCP жүйесіндегі «82 опциясы» дегеніміз не

Бұл DHCP протоколының қосымша опциялары, мысалы, Agent-Circuit-Id және Agent-Remote-Id өрістерінде қосымша ақпаратты тасымалдауға мүмкіндік береді. Әдетте релелік қосқыштың MAC мекенжайын және абонент қосылған портты жіберу үшін қолданылады. PON жабдығы немесе WIFI базалық станциялары жағдайында Agent-Circuit-Id өрісінде пайдалы ақпарат жоқ (абонент порты жоқ). Бұл жағдайда DHCP жұмысының жалпы схемасы келесідей:

DHCP арқылы FreeRadius желі параметрлері

Бұл схема қадам бойынша келесідей жұмыс істейді:

  1. Пайдаланушы жабдығы желі параметрлерін алу үшін DHCP тарату сұрауын жасайды
  2. Абоненттік жабдық тікелей қосылған құрылғы (мысалы, коммутатор, WiFi немесе PON базалық станциясы) бұл пакетті «ұстап» алады және оған қосымша опциялар 82 және Реле агентінің IP мекенжайын енгізе отырып, оны одан әрі жібереді. желі.
  3. DHCP сервері сұрауды қабылдайды, жауапты жасайды және оны релелік құрылғыға жібереді
  4. Релелік құрылғы жауап пакетін абоненттік құрылғыға жібереді

Әрине, бәрі оңай жұмыс істемейді; желілік жабдықты сәйкесінше конфигурациялау керек.

FreeRadius орнату

Әрине, бұған FreeRadius конфигурациясының параметрлері арқылы қол жеткізуге болады, бірақ бұл қиын және түсініксіз... әсіресе N айдан кейін барып, «бәрі жұмыс істейді». Сондықтан біз Python тілінде FreeRadius үшін авторизациялау модулін жазуды шештік. Біз MySQL дерекқорынан авторизация деректерін аламыз. Оның құрылымын сипаттаудың қажеті жоқ, бәрібір оны «өзі үшін» жасайды. Атап айтқанда, мен FreeRadius үшін sql модулімен ұсынылған құрылымды алдым және логин-парольден басқа әрбір абонент үшін Mac және порт өрісін қосу арқылы оны сәл өзгерттім.

Сонымен, алдымен FreeRadius орнатыңыз:

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

Параметрлерде орнату үшін таңдаңыз:

DHCP арқылы FreeRadius желі параметрлері

Біз python модуліне символдық сілтеме жасаймыз (яғни оны «қосу»):

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

Python үшін қосымша модуль орнатайық:

pip install mysql-connector

FreeRadius үшін python модулінің параметрлерінде python_path айнымалысында модуль іздеу жолдарын көрсету қажет. Мысалы, менде бұл:

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"

Python аудармашысын іске қосу және пәрмендерді енгізу арқылы жолдарды білуге ​​болады:

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

Бұл қадамды жасамасаңыз, python тілінде жазылған және FreeRadius іске қосқан сценарийлер импортта тізімделген модульдерді таба алмайды. Сонымен қатар, модуль параметрлерінде қоңырауды авторизациялау және есепке алу функцияларына түсініктеме беру қажет. Мысалы, бұл модуль келесідей көрінеді:

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 сценарийі (және басқалары) /usr/local/etc/raddb/mods-config/python ішінде орналастырылуы керек Менде барлығы үш сценарий бар.

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

Кодтан көріп отырғаныңыздай, біз абоненттің белгілі MAC мекенжайлары немесе 82 опция комбинациясы арқылы барлық қолжетімді әдістерді пайдалана отырып, абонентті анықтауға тырысамыз, егер бұл жұмыс істемесе, біз «қонақ» сайтынан бұрын-соңды пайдаланылған ең ескі IP мекенжайын береміз. ” желісі. Сайттар қосылған қалтадағы әдепкі сценарийді конфигурациялау ғана қалады, осылайша python сценарийіндегі қажетті функциялар белгіленген сәттерде бұралып қалады. Іс жүзінде файлды пішінге келтіру жеткілікті:

Әдепкі

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

Оны іске қосып, жөндеу журналына не кіретінін көрейік:

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

Тағы да не. FreeRadius орнату кезінде оның жұмысын radclient утилитасының көмегімен тексеру ыңғайлы. Мысалы, авторизация:

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

Немесе тіркелгі:

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

Мен мұндай схема мен сценарийлерді «өнеркәсіптік» масштабта «өзгеріссіз» пайдалану мүлдем мүмкін емес екенін ескерткім келеді. Кем дегенде байқалады:

  • MAC мекенжайын «жалған» жасауға болады. Абонентке басқа біреудің MAC тіркеуі жеткілікті және проблемалар туындайды
  • қонақ желілерін шығару логикасы сын көтермейді. Тіпті «бір IP мекенжайы бар клиенттер бар шығар?» деген тексеру де жоқ.

Бұл менің жағдайларымда арнайы жұмыс істеуге арналған «печенье кескіш шешім» ғана, басқа ештеңе емес. Қатаң үкім шығармаңыз😉

Ақпарат көзі: www.habr.com

пікір қалдыру