Ustawienia sieciowe z FreeRadius poprzez DHCP

Ustawienia sieciowe z FreeRadius poprzez DHCP
Przyszło zadanie zorganizowania przydziału adresów IP abonentom. Warunki problemu:

  • Nie udostępnimy Ci osobnego serwera do autoryzacji – dasz sobie radę 😉
  • Abonenci muszą otrzymać ustawienia sieciowe poprzez DHCP
  • Sieć jest heterogeniczna. Obejmuje to sprzęt PON i zwykłe przełączniki ze skonfigurowaną opcją 82 oraz bazy Wi-Fi z hotspotami
  • Jeżeli dane nie spełniają żadnego z warunków nadania adresu IP, należy wydać adres IP z sieci „gościnnej”

Dobra strona: na FreeBSD wciąż istnieje serwer, który może „działać”, ale jest „daleko” ;), a nie „tuż w tej sieci”.

Istnieje również wspaniałe urządzenie o nazwie Mikrotik. Ogólny schemat sieci wygląda mniej więcej tak:

Ustawienia sieciowe z FreeRadius poprzez DHCP

Po namyśle zdecydowano się użyć FreeRadius do wydawania abonentom ustawień sieciowych. Zasadniczo schemat jest typowy: włączamy serwer DHCP na Microtick i klienta Radius na nim. Konfigurujemy serwer DHCP -> Klient Radius -> Połączenie z serwerem Radius.

Nie wydaje się to trudne. Ale! Diabeł tkwi w szczegółach. Mianowicie:

  • Podczas autoryzacji PON OLT przy użyciu tego schematu do FreeRadius wysyłane jest żądanie z nazwą użytkownika równą adresowi MAC stacji czołowej, identyfikatorem obwodu agenta równym MAC PON Onu i pustym hasłem.
  • Autoryzując się ze switchy z opcją 82, FreeRadius otrzymuje żądanie z pustą nazwą użytkownika równą MAC urządzenia abonenta i wypełnioną dodatkowymi atrybutami Agent-Circuit-Id i Agent-Remote-Id zawierającymi odpowiednio ponownie MAC przełącznik przekaźnikowy i port, do którego podłączony jest abonent.
  • Niektórzy abonenci posiadający punkty WiFI autoryzują się za pomocą protokołów PAP-CHAP
  • Niektórzy abonenci punktów WIFI są autoryzowani przy użyciu Nazwy Użytkownika równej adresowi MAC punktu WIFI, bez hasła.

Tło historyczne: czym jest „Opcja 82” w DHCP

Są to dodatkowe opcje dla protokołu DHCP, które umożliwiają przesyłanie dodatkowych informacji, na przykład w polach Agent-Circuit-Id i Agent-Remote-Id. Zwykle używany do przesyłania adresu MAC przełącznika przekaźnikowego i portu, do którego podłączony jest abonent. W przypadku urządzeń PON lub stacji bazowych WIFI pole Agent-Circuit-Id nie zawiera przydatnych informacji (nie ma portu abonenckiego). Ogólny schemat działania DHCP w tym przypadku wygląda następująco:

Ustawienia sieciowe z FreeRadius poprzez DHCP

Krok po kroku ten schemat działa w następujący sposób:

  1. Urządzenie użytkownika wysyła żądanie rozgłoszeniowe DHCP w celu uzyskania ustawień sieciowych
  2. Urządzenie (na przykład przełącznik, stacja bazowa WiFi lub PON), do którego jest bezpośrednio podłączone urządzenie abonenckie, „przechwytuje” ten pakiet i zmienia go, wprowadzając do niego dodatkowe opcje Opcja 82 i Adres IP agenta przekazującego, a następnie przesyła go dalej sieć.
  3. Serwer DHCP akceptuje żądanie, generuje odpowiedź i wysyła ją do urządzenia przekazującego
  4. Urządzenie przekaźnikowe przekazuje pakiet odpowiedzi do urządzenia abonenckiego

Oczywiście to wszystko nie działa tak łatwo, należy odpowiednio skonfigurować sprzęt sieciowy.

Instalowanie FreeRadiusa

Można to oczywiście osiągnąć za pomocą ustawień konfiguracyjnych FreeRadius, ale jest to trudne i niejasne… szczególnie gdy idziesz tam po N miesiącach i „wszystko działa”. Dlatego postanowiliśmy napisać własny moduł autoryzacyjny dla FreeRadius w Pythonie. Dane autoryzacyjne pobierzemy z bazy MySQL. Nie ma sensu opisywać jego budowy, zresztą każdy zrobi to „dla siebie”. W szczególności wziąłem strukturę oferowaną z modułem sql dla FreeRadius i nieznacznie ją zmieniłem, dodając pole mac i port dla każdego abonenta, oprócz hasła logowania.

Więc najpierw zainstaluj FreeRadius:

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

W ustawieniach wybierz instalację:

Ustawienia sieciowe z FreeRadius poprzez DHCP

Tworzymy dowiązanie symboliczne do modułu Pythona (czyli „włączamy”):

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

Zainstalujmy dodatkowy moduł dla Pythona:

pip install mysql-connector

W ustawieniach modułu Pythona dla FreeRadius musisz określić ścieżki wyszukiwania modułu w zmiennej python_path. Na przykład mam to:

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"

Możesz znaleźć ścieżki, uruchamiając interpreter Pythona i wprowadzając polecenia:

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

Jeśli nie wykonasz tego kroku, skrypty napisane w Pythonie i uruchomione przez FreeRadius nie znajdą modułów wymienionych w imporcie. Dodatkowo należy w ustawieniach modułu odkomentować funkcje wywołania autoryzacji i rozliczeń. Przykładowo moduł ten wygląda następująco:

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}

}

Skrypt work.py (i wszystkie inne) musi zostać umieszczony w /usr/local/etc/raddb/mods-config/python. W sumie mam trzy skrypty.

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

promień.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

Jak widać z kodu staramy się zidentyfikować abonenta wszelkimi dostępnymi metodami po jego znanych adresach MAC abonenta lub kombinacji Opcji 82, a jeśli to nie zadziała, to nadajemy najstarszy kiedykolwiek używany adres IP z „gościa” " sieć. Pozostaje tylko skonfigurować domyślny skrypt w folderze sites-enabled, tak aby niezbędne funkcje ze skryptu Pythona drgnęły w wyznaczonych momentach. Tak naprawdę wystarczy doprowadzić plik do postaci:

domyślnym

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

Spróbujmy go uruchomić i zobaczmy, co pojawi się w dzienniku debugowania:

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

Co jeszcze. Podczas konfigurowania FreeRadius wygodnie jest przetestować jego działanie za pomocą narzędzia radclient. Na przykład autoryzacja:

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

Lub konto:

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

Chcę Was przestrzec, że stosowanie takiego schematu i skryptów „bez zmian” na skalę „przemysłową” jest absolutnie niemożliwe. Przynajmniej zauważalne:

  • możliwe jest „sfałszowanie” adresu MAC. Wystarczy, że abonent zarejestruje cudzy MAC i będą problemy
  • logika tworzenia sieci gościnnych nie podlega krytyce. Nie ma nawet zaznaczenia „może są już klienci o tym samym adresie IP?”

To po prostu „rozwiązanie do wycinania ciasteczek”, zaprojektowane tak, aby działało specjalnie w moich warunkach, nic więcej. Nie oceniaj rygorystycznie 😉

Źródło: www.habr.com

Dodaj komentarz