إعدادات الشبكة من FreeRadius عبر DHCP

إعدادات الشبكة من FreeRadius عبر DHCP
وصلت المهمة إلى إنشاء إصدار عناوين IP للمشتركين. شروط المشكلة:

  • لن نعطي خادمًا منفصلاً للترخيص - أنت الذي ستديره 😉
  • يجب أن يتلقى المشتركون إعدادات الشبكة عبر DHCP
  • الشبكة غير متجانسة. هذه عبارة عن معدات PON ومفاتيح عادية مع تكوين الخيار 82 وقواعد WiFi ذات النقاط
  • إذا كانت البيانات لا تندرج تحت أي من شروط إصدار IP، فمن الضروري إصدار IP من شبكة “الضيف”

من الجيد: يوجد خادم على FreeBSD يمكنه "العمل"، ولكنه "بعيد"؛)، وليس "موجود في هذه الشبكة مباشرة".

ويوجد أيضًا جهاز ميكروتيك الرائع. المخطط العام للشبكة يشبه هذا:

إعدادات الشبكة من FreeRadius عبر DHCP

بعد قليل من التفكير، تقرر استخدام FreeRadius لإصدار إعدادات الشبكة للمشتركين. من حيث المبدأ، المخطط معتاد: في Microtick، نقوم بتشغيل خادم DHCP، ولدينا أيضًا عميل Radius. قمنا بإعداد مجموعة من خادم DHCP -> عميل Radius -> خادم Radius.

يبدو أن الأمر ليس صعبا. لكن! الشر في التفاصيل. يسمى:

  • عندما يتم ترخيص PON OLT وفقًا لهذا المخطط، "يصل" طلب إلى FreeRadius مع اسم مستخدم يساوي عنوان MAC للمحطة الرئيسية، ومعرف دائرة الوكيل يساوي MAC PON Onu وكلمة مرور فارغة.
  • عند التفويض من المحولات باستخدام الخيار 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. الجهاز (على سبيل المثال، محول، محطة WiFi أساسية أو PON) الذي يتصل به جهاز المشترك مباشرة "يعترض" هذه الحزمة ويغيرها، ويقدم خيارات إضافية الخيار 82 وعنوان IP لوكيل الترحيل إليها، ويرسلها عبرها الشبكة.
  3. يتلقى خادم DHCP الطلب، وينشئ استجابة ويرسله إلى جهاز الترحيل
  4. يقوم جهاز الترحيل بإعادة توجيه حزمة الاستجابة إلى جهاز المشترك

كل هذا بالطبع لا يعمل، فأنت بحاجة إلى التكوين المناسب لمعدات الشبكة.

تثبيت FreeRadius

بالطبع، يمكن تحقيق كل شيء باستخدام إعدادات تكوين FreeRadius، لكن الأمر صعب وغير واضح ... خاصة عندما تضع رأسك هناك بعد N أشهر "كل شيء يعمل". لذلك، تقرر كتابة وحدة الترخيص الخاصة بنا لـ FreeRadius في Python. سنأخذ بيانات الترخيص من قاعدة بيانات MySQL. ليس من المنطقي وصف هيكلها، على أي حال، الجميع سوف يفعلون ذلك "لأنفسهم". على وجه الخصوص، أخذت البنية المقدمة مع وحدة SQL لـ FreeRadius، وقمت بتغييرها قليلاً عن طريق إضافة حقول mac والمنفذ لكل مشترك، بالإضافة إلى تسجيل الدخول وكلمة المرور.

لذلك، نقوم أولاً بتثبيت 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

في إعدادات وحدة python لـ 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']
>

إذا لم يتم اتخاذ هذه الخطوة، فلن تجد البرامج النصية المكتوبة بلغة 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، لقد حصلت على ثلاثة نصوص برمجية إجمالاً.

العمل.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         

نصف القطر.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 الخاصة بالمشترك المعروف أو حزمة Option 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

إضافة تعليق