DHCP를 통한 FreeRadius의 네트워크 설정

DHCP를 통한 FreeRadius의 네트워크 설정
가입자에게 IP 주소 발급을 준비하는 작업이 도착했습니다. 문제의 조건:

  • 인증을 위해 별도의 서버를 제공하지 않습니다. 직접 하게 됩니다 😉
  • 가입자는 DHCP를 통해 네트워크 설정을 받아야 합니다.
  • 네트워크는 이기종입니다. 여기에는 PON 장비, 옵션 82가 구성된 일반 스위치, 핫스팟이 있는 WiFi 베이스가 포함됩니다.
  • IP 발급 조건에 해당하지 않는 데이터인 경우, “guest” 네트워크에서 IP를 발급받아야 합니다.

좋은 점은 FreeBSD에는 여전히 "작동"할 수 있는 서버가 있지만 "이 네트워크에 바로" 있는 것이 아니라 "멀리 떨어져" 있다는 것입니다.

Mikrotik이라는 멋진 장치도 있습니다. 일반적인 네트워크 다이어그램은 다음과 같습니다.

DHCP를 통한 FreeRadius의 네트워크 설정

고민 끝에 FreeRadius를 사용하여 가입자에게 네트워크 설정을 제공하기로 결정했습니다. 원칙적으로 구성표는 일반적입니다. Microtick에서 DHCP 서버를 활성화하고 이에 Radius 클라이언트를 활성화합니다. DHCP 서버 -> Radius 클라이언트 -> Radius 서버 연결을 구성합니다.

어렵지는 않은 것 같습니다. 하지만! 악마는 디테일에 있다. 즉:

  • 이 체계를 사용하여 PON OLT를 인증할 때 헤드엔드의 MAC 주소와 동일한 User-Name, MAC PON Onu와 동일한 Agent-Circuit-Id 및 빈 비밀번호를 사용하여 요청이 FreeRadius로 전송됩니다.
  • 옵션 82를 사용하여 스위치에서 인증할 때 FreeRadius는 가입자 장치의 MAC과 동일한 빈 User-Name이 포함된 요청을 수신하며 각각 다음의 MAC을 포함하는 Agent-Circuit-Id 및 Agent-Remote-Id 추가 속성으로 채워집니다. 릴레이 스위치와 가입자가 연결된 포트.
  • WiFI 포인트가 있는 일부 가입자는 PAP-CHAP 프로토콜을 통해 승인됩니다.
  • WIFI 포인트의 일부 가입자는 비밀번호 없이 WIFI 포인트의 MAC 주소와 동일한 사용자 이름으로 인증됩니다.

역사적 배경: DHCP의 "옵션 82"란 무엇입니까?

이는 Agent-Circuit-Id 및 Agent-Remote-Id 필드와 같은 추가 정보를 전송할 수 있는 DHCP 프로토콜에 대한 추가 옵션입니다. 일반적으로 중계 스위치의 MAC 주소와 가입자가 연결된 포트를 전송하는 데 사용됩니다. PON 장비나 WIFI 기지국의 경우 Agent-Circuit-Id 필드에 유용한 정보가 포함되어 있지 않습니다(가입자 포트가 없음). 이 경우 일반적인 DHCP 작동 방식은 다음과 같습니다.

DHCP를 통한 FreeRadius의 네트워크 설정

단계별로 이 체계는 다음과 같이 작동합니다.

  1. 사용자 장비는 네트워크 설정을 얻기 위해 DHCP 브로드캐스트 요청을 합니다.
  2. 가입자 장비가 직접 연결된 장치(예: 스위치, WiFi 또는 PON 기지국)는 이 패킷을 "가로채서" 변경하고 추가 옵션 옵션 82 및 릴레이 에이전트 IP 주소를 도입하여 더 멀리 전송합니다. 네트워크.
  3. DHCP 서버는 요청을 수락하고 응답을 생성하여 이를 릴레이 장치로 보냅니다.
  4. 중계 장치는 응답 패킷을 가입자 장치로 전달합니다.

물론 모든 것이 그렇게 쉽게 작동하는 것은 아니며 네트워크 장비를 그에 맞게 구성해야 합니다.

FreeRadius 설치

물론 이는 FreeRadius 구성 설정을 통해 달성할 수 있지만 어렵고 불분명합니다. 특히 N개월 후에 거기에 가서 "모든 것이 작동"하는 경우에는 더욱 그렇습니다. 따라서 우리는 Python으로 FreeRadius에 대한 자체 인증 모듈을 작성하기로 결정했습니다. MySQL 데이터베이스에서 인증 데이터를 가져옵니다. 그 구조를 설명하는 것은 의미가 없으며 어쨌든 모든 사람이 "스스로" 만들 것입니다. 특히 FreeRadius용 SQL 모듈과 함께 제공되는 구조를 취하여 로그인 비밀번호 외에 각 가입자에 대한 mac 및 포트 필드를 추가하여 약간 변경했습니다.

먼저 FreeRadius를 설치하십시오.

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

설정에서 다음을 설치하도록 선택합니다.

DHCP를 통한 FreeRadius의 네트워크 설정

Python 모듈에 대한 심볼릭 링크를 만듭니다(즉, "켜기").

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

Python용 추가 모듈을 설치해 보겠습니다.

pip install mysql-connector

FreeRadius의 Python 모듈 설정에서 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"

Python 인터프리터를 실행하고 다음 명령을 입력하여 경로를 찾을 수 있습니다.

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에 있어야 합니다. 총 XNUMX개의 스크립트가 있습니다.

일.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 주소를 발급합니다. ” 네트워크. 남은 것은 사이트 활성화 폴더에 기본 스크립트를 구성하여 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 주소를 가진 클라이언트가 이미 있습니까?"라는 확인조차 없습니다.

이것은 내 조건에 맞게 특별히 작동하도록 설계된 "쿠키 커터 솔루션"일 뿐이며 그 이상은 아닙니다. 엄격하게 판단하지 마세요😉

출처 : habr.com

코멘트를 추가