تنظیمات شبکه از FreeRadius از طریق DHCP

تنظیمات شبکه از FreeRadius از طریق DHCP
وظیفه ترتیب صدور آدرس IP برای مشترکین رسید. شرایط مشکل:

  • ما به شما سرور جداگانه ای برای مجوز نمی دهیم - شما این کار را خواهید کرد
  • مشترکین باید تنظیمات شبکه را از طریق DHCP دریافت کنند
  • شبکه ناهمگن است. این شامل تجهیزات PON و سوئیچ های معمولی با گزینه 82 پیکربندی شده و پایه های WiFi با هات اسپات است.
  • اگر داده ها تحت هیچ یک از شرایط صدور IP قرار نگیرند، باید یک IP از شبکه "مهمان" صادر کنید.

از طرف خوب: هنوز یک سرور در FreeBSD وجود دارد که می تواند "کار کند"، اما "دور است"؛ نه "در این شبکه".

همچنین یک دستگاه فوق العاده به نام میکروتیک وجود دارد. نمودار کلی شبکه چیزی شبیه به این است:

تنظیمات شبکه از FreeRadius از طریق DHCP

پس از مدتی تفکر، تصمیم گرفته شد که از FreeRadius برای صدور تنظیمات شبکه برای مشترکین استفاده شود. در اصل، این طرح معمول است: ما سرور DHCP را در Microtick و Radius Client را در آن فعال می کنیم. ما سرور DHCP -> Radius Client -> اتصال سرور Radius را پیکربندی می کنیم.

سخت به نظر نمی رسد. ولی! شیطان در جزئیات است. برای مثال:

  • هنگام تأیید یک PON OLT با استفاده از این طرح، یک درخواست با یک نام کاربری برابر با آدرس MAC headend، یک Agent-Circuit-Id برابر با MAC PON Onu و یک رمز عبور خالی به FreeRadius ارسال می شود.
  • هنگام تأیید مجوز از سوییچ‌ها با گزینه 82، FreeRadius درخواستی با یک نام کاربری خالی برابر با MAC دستگاه مشترک دریافت می‌کند که با ویژگی‌های اضافی Agent-Circuit-Id و Agent-Remote-Id به ترتیب حاوی MAC مربوط به دستگاه مشترک است. سوئیچ رله و پورتی که مشترک به آن متصل است.
  • برخی از مشترکین دارای امتیاز WiFI از طریق پروتکل های PAP-CHAP مجاز هستند
  • برخی از مشترکین نقاط WIFI دارای یک نام کاربری برابر با آدرس MAC نقطه WIFI بدون رمز عبور مجاز هستند.

پیشینه تاریخی: گزینه 82 در DHCP چیست

اینها گزینه های اضافی برای پروتکل DHCP هستند که به شما امکان انتقال اطلاعات اضافی را می دهند، به عنوان مثال در قسمت های Agent-Circuit-Id و Agent-Remote-Id. معمولاً برای انتقال آدرس MAC سوئیچ رله و پورتی که مشترک به آن متصل است استفاده می شود. در مورد تجهیزات PON یا ایستگاه های پایه WIFI، قسمت Agent-Circuit-Id حاوی اطلاعات مفیدی نیست (درگاه مشترکی وجود ندارد). طرح کلی عملکرد DHCP در این مورد به شرح زیر است:

تنظیمات شبکه از FreeRadius از طریق DHCP

گام به گام این طرح به صورت زیر عمل می کند:

  1. تجهیزات کاربر برای دریافت تنظیمات شبکه یک درخواست پخش DHCP می دهد
  2. دستگاه (به عنوان مثال، یک سوئیچ، وای فای یا ایستگاه پایه PON) که تجهیزات مشترک به طور مستقیم به آن متصل است، این بسته را "رهگیری" می کند و آن را تغییر می دهد، گزینه های اضافی گزینه 82 و آدرس IP عامل رله را وارد آن می کند، و آن را بیشتر منتقل می کند. شبکه.
  3. سرور DHCP درخواست را می پذیرد، یک پاسخ تولید می کند و آن را به دستگاه رله می فرستد
  4. دستگاه رله بسته پاسخ را به دستگاه مشترک ارسال می کند

البته، همه اینها به این راحتی کار نمی کند؛ شما باید تجهیزات شبکه خود را بر این اساس پیکربندی کنید.

نصب FreeRadius

البته، این را می توان با تنظیمات پیکربندی FreeRadius به دست آورد، اما دشوار و نامشخص است... به خصوص وقتی بعد از N ماه به آنجا می روید و «همه چیز کار می کند». بنابراین، تصمیم گرفتیم ماژول مجوز خود را برای FreeRadius در پایتون بنویسیم. ما داده های مجوز را از پایگاه داده MySQL خواهیم گرفت. هیچ فایده ای در توصیف ساختار آن وجود ندارد، به هر حال، هرکس آن را "برای خود" خواهد ساخت. به طور خاص، ساختاری را که با ماژول sql برای FreeRadius ارائه می‌شود، گرفتم و با اضافه کردن یک فیلد مک و پورت برای هر مشترک، علاوه بر رمز عبور ورود، کمی آن را تغییر دادم.

بنابراین، ابتدا FreeRadius را نصب کنید:

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

در تنظیمات، نصب را انتخاب کنید:

تنظیمات شبکه از FreeRadius از طریق DHCP

ما یک پیوند نمادین به ماژول پایتون ایجاد می کنیم (یعنی آن را روشن کنید):

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

بیایید یک ماژول اضافی برای پایتون نصب کنیم:

pip install mysql-connector

در تنظیمات ماژول پایتون برای FreeRadius، باید مسیرهای جستجوی ماژول را در متغیر 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"

با راه اندازی مفسر پایتون و وارد کردن دستورات می توانید مسیرها را پیدا کنید:

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

اگر این مرحله را انجام ندهید، اسکریپت‌های نوشته شده در پایتون و راه‌اندازی شده توسط FreeRadius، ماژول‌هایی را که در import فهرست شده‌اند، پیدا نمی‌کنند. علاوه بر این، باید عملکردهای مربوط به مجوز تماس و حسابداری را در تنظیمات ماژول از حالت نظر خارج کنید. به عنوان مثال، این ماژول به شکل زیر است:

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 استفاده شده را از مهمان صادر می کنیم. " شبکه. تنها چیزی که باقی می‌ماند این است که اسکریپت پیش‌فرض را در پوشه فعال‌شده سایت‌ها پیکربندی کنید، به طوری که عملکردهای لازم از اسکریپت پایتون در لحظه‌های تعیین‌شده تکان بخورند. در واقع کافی است فایل را به فرم زیر بیاورید:

به طور پیش فرض

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، آزمایش عملکرد آن با استفاده از ابزار رادکلینت راحت است. به عنوان مثال مجوز:

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

اضافه کردن نظر