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


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

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

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

هناك أيضا جهاز Mikrotik الرائع. مخطط الشبكة العام هو شيء مثل هذا:



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

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

  • عندما يتم التصريح لـ PON OLT وفقًا لهذا المخطط ، يتم إرسال طلب إلى FreeRadius مع اسم مستخدم مساوٍ لعنوان MAC الخاص بالمحطة الرئيسية ، وسيط عامل Agent-ID مساوٍ لـ PON Onu MAC وكلمة مرور فارغة.
  • عند التفويض باستخدام رموز التبديل مع الخيار 82 ، يأتي الطلب إلى FreeRadius باسم مستخدم فارغ يساوي جهاز MAC الخاص بالمشترك والسمات الإضافية الإضافية لـ Agent-Circuit و Agent-Remote-Id التي تحتوي ، مرة أخرى ، على مفتاح الترحيل MAC والمنفذ المتصل بالمشترك.
  • يتم اعتماد بعض المشتركين الذين لديهم نقاط WiFI من خلال بروتوكولات PAP-CHAP
  • يتم اعتماد بعض المشتركين الذين لديهم نقاط WIFI باسم مستخدم مساوي لعنوان MAC لنقطة WIFI ، دون كلمة مرور.

الخلفية التاريخية: ما هو الخيار 82 ل DHCP

هذه هي خيارات إضافية لبروتوكول DHCP والتي تسمح لك بنقل معلومات إضافية ، على سبيل المثال ، في حقل معرف الدائرة-الوكيل و معرف الوكيل عن بُعد. وعادة ما يتم استخدامه لنقل عنوان MAC لمفتاح الترحيل والمنفذ الذي يتصل به المشترك. في حالة معدات PON أو محطات WIFI الأساسية ، لا يحمل حقل معرف الدائرة-الوكيل معلومات مفيدة (لا يوجد منفذ مشترك). في هذه الحالة ، يكون المخطط العام لـ DHCP في هذه الحالة كما يلي:



خطوة بخطوة ، هذا المخطط يعمل مثل هذا:

  1. تقدم معدات المشترك طلب بث DHCP لإعدادات الشبكة
  2. يقوم الجهاز (على سبيل المثال ، مفتاح التبديل أو محطة WiFi أو PON الأساسية) التي يتصل بها جهاز المشترك مباشرة "باعتراض" هذه الحزمة وتعديلها ، وإدخال خيارات عنوان IP لعامل الخيار 82 و Relay agent ، ونقلها بشكل إضافي عبر الشبكة.
  3. يقبل خادم DHCP الطلب ويشكل استجابة ويرسله إلى جهاز الترحيل
  4. يقوم جهاز الترحيل بإعادة توجيه حزمة الاستجابة إلى جهاز المشترك

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

تثبيت FreeRadius


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

لذلك ، بالنسبة للمبتدئين ، تثبيت FreeRadius:

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

في الإعدادات ، نحتفل بالتثبيت:



نقوم بعمل رابط إلى وحدة بيثون (أي ، قم بتشغيله):

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

إذا لم تقم بهذه الخطوة ، فلن تجد البرامج النصية المكتوبة بيثون ويديرها 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 مستخدم من شبكة "الضيف". يبقى لتكوين البرنامج النصي الافتراضي في المجلد تمكين المواقع بحيث الوظائف اللازمة من نشل البرنامج النصي بيثون في الأوقات المشار إليها. في الواقع ، يكفي إحضار الملف إلى النموذج:

الافتراضي
 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 أجنبي لنفسه وستكون هناك مشاكل
  • منطق إصدار شبكات الضيف هو أدنى من النقد. لا يوجد حتى الاختيار "هل يمكنك بالفعل عملاء مع عنوان IP هذا؟"

إنه مجرد "حل على الركبة" من أجل العمل بشكل خاص في ظروفي ، لا شيء أكثر من ذلك. لا تحكم بدقة ؛)

Source: https://habr.com/ru/post/ar446586/


All Articles