Tetapan rangkaian dari FreeRadius melalui DHCP

Tetapan rangkaian dari FreeRadius melalui DHCP
Tugas tiba untuk mengatur pengeluaran alamat IP kepada pelanggan. Keadaan masalah:

  • Kami tidak akan memberikan anda pelayan yang berasingan untuk kebenaran - anda akan lakukan 😉
  • Pelanggan mesti menerima tetapan rangkaian melalui DHCP
  • Rangkaian adalah heterogen. Ini termasuk peralatan PON dan suis biasa dengan Pilihan 82 yang dikonfigurasikan dan pangkalan WiFi dengan hotspot
  • Jika data tidak termasuk dalam mana-mana syarat untuk mengeluarkan IP, anda mesti mengeluarkan IP daripada rangkaian "tetamu"

Di sisi yang baik: masih terdapat pelayan di FreeBSD yang boleh "berfungsi", tetapi ia "jauh" ;), bukan "betul pada rangkaian ini".

Terdapat juga peranti hebat yang dipanggil Mikrotik. Gambar rajah rangkaian umum adalah seperti ini:

Tetapan rangkaian dari FreeRadius melalui DHCP

Selepas beberapa pemikiran, diputuskan untuk menggunakan FreeRadius untuk mengeluarkan tetapan rangkaian kepada pelanggan. Pada dasarnya, skema ini adalah biasa: kami mendayakan pelayan DHCP pada Microtick, dan Radius Client padanya. Kami mengkonfigurasi pelayan DHCP -> Radius Client -> Sambungan pelayan Radius.

Nampak tak susah. Tetapi! Syaitan ada dalam butirannya. Iaitu:

  • Apabila membenarkan PON OLT menggunakan skim ini, permintaan dihantar ke FreeRadius dengan Nama Pengguna bersamaan dengan alamat MAC hujung kepala, Ejen-Litar-Id bersamaan dengan MAC PON Onu dan kata laluan kosong.
  • Apabila membenarkan daripada suis dengan pilihan 82, FreeRadius menerima permintaan dengan Nama Pengguna kosong yang sama dengan MAC peranti pelanggan dan diisi dengan atribut tambahan Id-Litar Ejen dan Id-Jauh-Ejen yang mengandungi, sekali lagi MAC suis geganti dan port yang disambungkan kepada pelanggan.
  • Sesetengah pelanggan dengan mata WiFI diberi kuasa melalui protokol PAP-CHAP
  • Sesetengah pelanggan dari titik WIFI diberi kuasa dengan Nama Pengguna yang sama dengan alamat MAC titik WIFI, tanpa kata laluan.

Latar belakang sejarah: apakah itu "Pilihan 82" dalam DHCP

Ini adalah pilihan tambahan untuk protokol DHCP yang membolehkan anda memindahkan maklumat tambahan, contohnya dalam medan Agent-Circuit-Id dan Agent-Remote-Id. Biasanya digunakan untuk menghantar alamat MAC suis geganti dan port yang disambungkan kepada pelanggan. Dalam kes peralatan PON atau stesen pangkalan WIFI, medan Agent-Circuit-Id tidak mengandungi maklumat berguna (tiada port pelanggan). Skim umum operasi DHCP dalam kes ini adalah seperti berikut:

Tetapan rangkaian dari FreeRadius melalui DHCP

Langkah demi langkah skim ini berfungsi seperti ini:

  1. Peralatan pengguna membuat permintaan siaran DHCP untuk mendapatkan tetapan rangkaian
  2. Peranti (contohnya, suis, WiFi atau stesen pangkalan PON) yang peralatan pelanggan disambungkan terus "memintas" paket ini dan mengubahnya, memperkenalkan pilihan tambahan Pilihan 82 dan alamat IP ejen Relay ke dalamnya, dan menghantarnya lebih jauh rangkaian.
  3. Pelayan DHCP menerima permintaan, menjana respons dan menghantarnya ke peranti geganti
  4. Peranti geganti memajukan paket tindak balas kepada peranti pelanggan

Sudah tentu, semuanya tidak berfungsi dengan mudah; anda perlu mengkonfigurasi peralatan rangkaian anda dengan sewajarnya.

Memasang FreeRadius

Sudah tentu, ini boleh dicapai dengan tetapan konfigurasi FreeRadius, tetapi ia sukar dan tidak jelas... terutamanya apabila anda pergi ke sana selepas N bulan dan "semuanya berfungsi." Oleh itu, kami memutuskan untuk menulis modul kebenaran kami sendiri untuk FreeRadius dalam Python. Kami akan mengambil data kebenaran daripada pangkalan data MySQL. Tidak ada gunanya menerangkan strukturnya; bagaimanapun, semua orang akan menjadikannya "untuk diri mereka sendiri." Khususnya, saya mengambil struktur yang ditawarkan dengan modul sql untuk FreeRadius, dan mengubahnya sedikit dengan menambahkan medan mac dan port untuk setiap pelanggan, sebagai tambahan kepada kata laluan log masuk.

Jadi, pertama, pasang FreeRadius:

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

Dalam tetapan, pilih untuk memasang:

Tetapan rangkaian dari FreeRadius melalui DHCP

Kami membuat symlink kepada modul python (iaitu "hidupkan" ia):

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

Mari pasang modul tambahan untuk python:

pip install mysql-connector

Dalam tetapan modul python untuk FreeRadius, anda perlu menentukan laluan carian modul dalam pembolehubah python_path. Sebagai contoh saya mempunyai 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 boleh mengetahui laluan dengan melancarkan penterjemah python dan memasukkan arahan:

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 dalam python dan dilancarkan oleh FreeRadius tidak akan menemui modul yang disenaraikan dalam import. Di samping itu, anda perlu membatalkan ulasan fungsi untuk memanggil kebenaran dan perakaunan dalam tetapan modul. Sebagai contoh, modul ini kelihatan 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 semua yang lain) mesti diletakkan dalam /usr/local/etc/raddb/mods-config/python Saya mempunyai tiga skrip secara keseluruhan.

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

Seperti yang anda boleh lihat daripada kod tersebut, kami cuba mengenal pasti pelanggan menggunakan semua kaedah yang tersedia dengan alamat MAC pelanggannya yang diketahui atau gabungan Pilihan 82, dan jika ini tidak berfungsi, maka kami mengeluarkan alamat IP tertua yang pernah digunakan daripada "tetamu ” rangkaian. Yang tinggal hanyalah untuk mengkonfigurasi skrip lalai dalam folder yang didayakan tapak, supaya fungsi yang diperlukan daripada skrip python akan berkedut pada saat yang ditetapkan. Malah, sudah cukup untuk membawa fail ke borang:

lalai

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 cuba jalankan dan lihat perkara yang masuk ke dalam log nyahpepijat:

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

Apa lagi. Apabila menyediakan FreeRadius, adalah mudah untuk menguji operasinya menggunakan utiliti radclient. Contohnya kebenaran:

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 akaun:

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 memberi amaran kepada anda bahawa adalah mustahil untuk menggunakan skema dan skrip sedemikian "tanpa perubahan" pada skala "perindustrian". Sekurang-kurangnya ketara:

  • adalah mungkin untuk "memalsukan" alamat MAC. Cukuplah untuk pelanggan mendaftar MAC orang lain dan akan ada masalah
  • logik mengeluarkan rangkaian tetamu adalah di luar kritikan. Tidak ada semakan "mungkin sudah ada pelanggan dengan alamat IP yang sama?"

Ini hanyalah "penyelesaian pemotong kuki" yang direka untuk berfungsi secara khusus dalam keadaan saya, tidak lebih. Jangan menilai secara tegas 😉

Sumber: www.habr.com

Tambah komen