Cài đặt mạng từ FreeRadius qua DHCP

Cài đặt mạng từ FreeRadius qua DHCP
Nhiệm vụ đến để sắp xếp việc cấp địa chỉ IP cho các thuê bao. Điều kiện của vấn đề:

  • Chúng tôi sẽ không cung cấp cho bạn một máy chủ riêng để ủy quyền - bạn sẽ làm được 😉
  • Thuê bao phải nhận cài đặt mạng qua DHCP
  • Mạng lưới không đồng nhất. Điều này bao gồm thiết bị PON và các thiết bị chuyển mạch thông thường với Tùy chọn 82 được định cấu hình và các đế WiFi có điểm phát sóng
  • Nếu dữ liệu không thuộc bất kỳ điều kiện nào để cấp IP, bạn phải cấp IP từ mạng “khách”

Về mặt tích cực: vẫn còn một máy chủ trên FreeBSD có thể “hoạt động”, nhưng nó ở “xa” ;) chứ không phải “ngay trên mạng này”.

Ngoài ra còn có một thiết bị tuyệt vời tên là Mikrotik. Sơ đồ mạng chung là như thế này:

Cài đặt mạng từ FreeRadius qua DHCP

Sau một hồi suy nghĩ, người ta quyết định sử dụng FreeRadius để cấp cài đặt mạng cho người đăng ký. Về nguyên tắc, sơ đồ này là thông thường: chúng tôi kích hoạt máy chủ DHCP trên Microtick và Radius Client trên đó. Chúng ta cấu hình máy chủ DHCP -> Radius Client -> kết nối máy chủ Radius.

Nó không có vẻ khó khăn. Nhưng! Ma quỷ là trong các chi tiết. Cụ thể là:

  • Khi ủy quyền PON OLT bằng cách sử dụng sơ đồ này, một yêu cầu sẽ được gửi đến FreeRadius với Tên người dùng bằng địa chỉ MAC của phần đầu, Id mạch tác nhân bằng MAC PON Onu và mật khẩu trống.
  • Khi ủy quyền từ các thiết bị chuyển mạch có tùy chọn 82, FreeRadius nhận được yêu cầu có Tên người dùng trống bằng MAC của thiết bị của người đăng ký và chứa đầy các thuộc tính bổ sung Agent-Circuit-Id và Agent-Remote-Id lần lượt chứa MAC của công tắc chuyển tiếp và cổng mà thuê bao được kết nối.
  • Một số thuê bao có điểm WiFI được cấp phép thông qua giao thức PAP-CHAP
  • Một số thuê bao từ các điểm WIFI được cấp quyền bằng Tên người dùng bằng địa chỉ MAC của điểm WIFI mà không cần mật khẩu.

Bối cảnh lịch sử: “Tùy chọn 82” trong DHCP là gì

Đây là các tùy chọn bổ sung cho giao thức DHCP cho phép bạn truyền thông tin bổ sung, ví dụ như trong các trường Agent-Circuit-Id và Agent-Remote-Id. Thường được sử dụng để truyền địa chỉ MAC của công tắc chuyển tiếp và cổng mà thuê bao được kết nối. Trong trường hợp thiết bị PON hoặc trạm gốc WIFI, trường Agent-Circuit-Id không chứa thông tin hữu ích (không có cổng thuê bao). Sơ đồ chung của hoạt động DHCP trong trường hợp này như sau:

Cài đặt mạng từ FreeRadius qua DHCP

Từng bước kế hoạch này hoạt động như thế này:

  1. Thiết bị người dùng thực hiện yêu cầu phát DHCP để lấy cài đặt mạng
  2. Thiết bị (ví dụ: bộ chuyển mạch, trạm gốc WiFi hoặc PON) mà thiết bị thuê bao được kết nối trực tiếp sẽ "chặn" gói này và thay đổi nó, đưa ra các tùy chọn bổ sung Tùy chọn 82 và địa chỉ IP của tác nhân chuyển tiếp vào đó và truyền nó đi xa hơn. mạng lưới.
  3. Máy chủ DHCP chấp nhận yêu cầu, tạo phản hồi và gửi đến thiết bị chuyển tiếp
  4. Thiết bị chuyển tiếp chuyển tiếp gói phản hồi đến thiết bị thuê bao

Tất nhiên, mọi việc không diễn ra dễ dàng như vậy; bạn cần phải cấu hình thiết bị mạng của mình cho phù hợp.

Cài đặt FreeRadius

Tất nhiên, điều này có thể đạt được bằng cài đặt cấu hình FreeRadius, nhưng nó khó và không rõ ràng... đặc biệt là khi bạn đến đó sau N tháng và “mọi thứ đều hoạt động”. Do đó, chúng tôi quyết định viết mô-đun ủy quyền của riêng mình cho FreeRadius bằng Python. Chúng tôi sẽ lấy dữ liệu ủy quyền từ cơ sở dữ liệu MySQL. Chẳng ích gì khi mô tả cấu trúc của nó; dù sao đi nữa, mọi người sẽ tạo ra nó “cho riêng mình”. Cụ thể, tôi đã lấy cấu trúc được cung cấp cùng với mô-đun sql cho FreeRadius và thay đổi nó một chút bằng cách thêm trường mac và cổng cho mỗi người đăng ký, ngoài mật khẩu đăng nhập.

Vì vậy, trước tiên hãy cài đặt FreeRadius:

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

Trong cài đặt chọn cài đặt:

Cài đặt mạng từ FreeRadius qua DHCP

Chúng tôi tạo một liên kết tượng trưng đến mô-đun python (tức là “bật” nó):

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

Hãy cài đặt một mô-đun bổ sung cho python:

pip install mysql-connector

Trong cài đặt mô-đun python cho FreeRadius, bạn cần chỉ định đường dẫn tìm kiếm mô-đun trong biến python_path. Ví dụ tôi có cái này:

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"

Bạn có thể tìm ra đường dẫn bằng cách khởi chạy trình thông dịch python và nhập lệnh:

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

Nếu bạn không thực hiện bước này thì các tập lệnh được viết bằng python và được FreeRadius khởi chạy sẽ không tìm thấy các mô-đun được liệt kê trong quá trình nhập. Ngoài ra, bạn cần bỏ ghi chú các chức năng ủy quyền cuộc gọi và tính toán trong cài đặt mô-đun. Ví dụ: mô-đun này trông như thế này:

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}

}

Tập lệnh work.py (và tất cả các tập lệnh khác) phải được đặt trong /usr/local/etc/raddb/mods-config/python. Tôi có tổng cộng ba tập lệnh.

công việc.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         

bán kính.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

Như bạn có thể thấy từ mã, chúng tôi đang cố gắng xác định người đăng ký bằng tất cả các phương pháp có sẵn bằng địa chỉ MAC của người đăng ký đã biết hoặc kết hợp Tùy chọn 82 và nếu điều này không hiệu quả thì chúng tôi sẽ cấp địa chỉ IP cũ nhất từng được sử dụng từ “khách " mạng. Tất cả những gì còn lại là định cấu hình tập lệnh mặc định trong thư mục hỗ trợ trang web để các chức năng cần thiết từ tập lệnh python sẽ hoạt động vào những thời điểm được chỉ định. Trên thực tế, chỉ cần đưa tệp về dạng:

mặc định

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

Hãy thử chạy nó và xem những gì có trong nhật ký gỡ lỗi:

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

Còn gì nữa. Khi thiết lập FreeRadius, thật thuận tiện để kiểm tra hoạt động của nó bằng tiện ích radclient. Ví dụ ủy quyền:

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

Hoặc tài khoản:

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

Tôi muốn cảnh báo bạn rằng hoàn toàn không thể sử dụng sơ đồ và tập lệnh như vậy mà “không thay đổi” ở quy mô “công nghiệp”. Ít nhất đáng chú ý:

  • có thể “làm giả” địa chỉ MAC. Người đăng ký đăng ký MAC của người khác là đủ và sẽ có vấn đề
  • logic của việc phát hành mạng khách là điều không thể chỉ trích. Thậm chí còn không có dấu kiểm tra "có thể đã có khách hàng có cùng địa chỉ IP?"

Đây chỉ là một “giải pháp cắt cookie” được thiết kế để hoạt động cụ thể trong điều kiện của tôi, không có gì hơn. Đừng phán xét khắt khe 😉

Nguồn: www.habr.com

Thêm một lời nhận xét