Pengaturan jaringan dari FreeRadius melalui DHCP

Pengaturan jaringan dari FreeRadius melalui DHCP
Tugasnya tiba untuk mengatur penerbitan alamat IP kepada pelanggan. Kondisi masalah:

  • Kami tidak akan memberi Anda server terpisah untuk otorisasi - Anda akan melakukannya 😉
  • Pelanggan harus menerima pengaturan jaringan melalui DHCP
  • Jaringannya heterogen. Ini termasuk peralatan PON dan saklar reguler dengan Opsi 82 ​​yang dikonfigurasi dan basis WiFi dengan hotspot
  • Jika data tidak termasuk dalam salah satu ketentuan untuk mengeluarkan IP, Anda harus mengeluarkan IP dari jaringan "tamu"

Sisi baiknya: masih ada server di FreeBSD yang bisa “berfungsi”, tetapi “jauh” ;), bukan “tepat di jaringan ini”.

Ada juga perangkat luar biasa bernama Mikrotik. Diagram jaringan umumnya kira-kira seperti ini:

Pengaturan jaringan dari FreeRadius melalui DHCP

Setelah beberapa pemikiran, diputuskan untuk menggunakan FreeRadius untuk mengeluarkan pengaturan jaringan kepada pelanggan. Pada prinsipnya, skemanya biasa saja: kami mengaktifkan server DHCP di Microtick, dan Radius Client di dalamnya. Kami mengkonfigurasi server DHCP -> Radius Client -> koneksi server Radius.

Tampaknya tidak sulit. Tetapi! Iblis ada dalam detailnya. Yaitu:

  • Saat mengotorisasi PON OLT menggunakan skema ini, permintaan dikirim ke FreeRadius dengan Nama Pengguna sama dengan alamat MAC headend, Agent-Circuit-Id sama dengan MAC PON Onu dan kata sandi kosong.
  • Saat mengotorisasi dari sakelar dengan opsi 82, FreeRadius menerima permintaan dengan Nama Pengguna kosong yang sama dengan MAC perangkat pelanggan dan diisi dengan atribut tambahan Agent-Circuit-Id dan Agent-Remote-Id yang masing-masing berisi MAC dari sakelar relai dan port tempat pelanggan terhubung.
  • Beberapa pelanggan dengan titik WiFI diotorisasi melalui protokol PAP-CHAP
  • Beberapa pelanggan dari titik WIFI diberi otorisasi dengan Nama Pengguna yang sama dengan alamat MAC titik WIFI, tanpa kata sandi.

Latar belakang sejarah: apa itu “Opsi 82” di DHCP

Ini adalah opsi tambahan untuk protokol DHCP yang memungkinkan Anda mentransfer informasi tambahan, misalnya di bidang Agent-Circuit-Id dan Agent-Remote-Id. Biasanya digunakan untuk mengirimkan alamat MAC dari saklar relai dan port yang terhubung dengan pelanggan. Dalam hal peralatan PON atau stasiun pangkalan WIFI, bidang Agent-Circuit-Id tidak berisi informasi yang berguna (tidak ada port pelanggan). Skema umum operasi DHCP dalam hal ini adalah sebagai berikut:

Pengaturan jaringan dari FreeRadius melalui DHCP

Langkah demi langkah skema ini berfungsi seperti ini:

  1. Peralatan pengguna membuat permintaan siaran DHCP untuk mendapatkan pengaturan jaringan
  2. Perangkat (misalnya, sakelar, WiFi, atau stasiun pangkalan PON) yang terhubung langsung dengan peralatan pelanggan “mencegat” paket ini dan mengubahnya, memasukkan opsi tambahan Opsi 82 ​​dan alamat IP agen relai ke dalamnya, dan mengirimkannya lebih jauh melalui jaringan.
  3. Server DHCP menerima permintaan tersebut, menghasilkan respons dan mengirimkannya ke perangkat relay
  4. Perangkat relai meneruskan paket respons ke perangkat pelanggan

Tentu saja, semuanya tidak berjalan semudah itu; Anda perlu mengkonfigurasi peralatan jaringan Anda sesuai dengan itu.

Menginstal FreeRadius

Tentu saja, hal ini dapat dicapai dengan pengaturan konfigurasi FreeRadius, tetapi sulit dan tidak jelas... terutama ketika Anda pergi ke sana setelah N bulan dan “semuanya berfungsi.” Oleh karena itu, kami memutuskan untuk menulis modul otorisasi kami sendiri untuk FreeRadius dengan Python. Kami akan mengambil data otorisasi dari database MySQL. Tidak ada gunanya menjelaskan strukturnya; lagipula, setiap orang akan membuatnya “untuk dirinya sendiri”. Secara khusus, saya mengambil struktur yang ditawarkan dengan modul sql untuk FreeRadius, dan sedikit mengubahnya dengan menambahkan bidang mac dan port untuk setiap pelanggan, selain kata sandi login.

Jadi, pertama-tama instal FreeRadius:

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

Di pengaturan, pilih untuk menginstal:

Pengaturan jaringan dari FreeRadius melalui DHCP

Kami membuat symlink ke modul python (yaitu "nyalakan"):

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

Mari instal modul tambahan untuk python:

pip install mysql-connector

Dalam pengaturan modul python untuk FreeRadius, Anda perlu menentukan jalur pencarian modul dalam variabel python_path. Misalnya saya punya ini:

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"

Anda dapat mengetahui jalurnya dengan meluncurkan juru bahasa python dan memasukkan perintah:

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

Jika Anda tidak mengambil langkah ini, maka skrip yang ditulis dengan python dan diluncurkan oleh FreeRadius tidak akan menemukan modul yang tercantum dalam impor. Selain itu, Anda perlu menghapus komentar pada fungsi otorisasi panggilan dan akuntansi dalam pengaturan modul. Misalnya, modul ini terlihat seperti ini:

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}

}

Skrip work.py (dan yang lainnya) harus ditempatkan di /usr/local/etc/raddb/mods-config/python Saya memiliki total tiga skrip.

kerja.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

fungsi.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

Seperti yang dapat Anda lihat dari kode, kami mencoba mengidentifikasi pelanggan menggunakan semua metode yang tersedia berdasarkan alamat MAC pelanggan yang diketahui atau kombinasi Opsi 82, dan jika ini tidak berhasil, maka kami mengeluarkan alamat IP tertua yang pernah digunakan dari "tamu ” jaringan. Yang tersisa hanyalah mengonfigurasi skrip default di folder yang mendukung situs, sehingga fungsi yang diperlukan dari skrip python akan bergerak pada saat yang ditentukan. Sebenarnya cukup dengan membawa file tersebut ke dalam bentuk:

kegagalan

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

Mari kita coba jalankan dan lihat apa yang masuk ke log debug:

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

Apa lagi. Saat mengatur FreeRadius, akan lebih mudah untuk menguji operasinya menggunakan utilitas radclient. Misalnya otorisasi:

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

Atau akun:

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

Saya ingin memperingatkan Anda bahwa sangat tidak mungkin menggunakan skema dan skrip seperti itu “tanpa perubahan” pada skala “industri”. Setidaknya terlihat:

  • ada kemungkinan untuk "memalsukan" alamat MAC. Pelanggan cukup mendaftarkan MAC orang lain dan akan ada masalah
  • logika mengeluarkan jaringan tamu tidak dapat dikritik. Bahkan tidak ada centang “mungkin sudah ada klien dengan alamat IP yang sama?”

Ini hanyalah “solusi cookie-cutter” yang dirancang untuk bekerja secara khusus dalam kondisi saya, tidak lebih. Jangan menilai dengan ketat 😉

Sumber: www.habr.com

Tambah komentar