FreeRadius'tan DHCP aracılığıyla ağ ayarları

FreeRadius'tan DHCP aracılığıyla ağ ayarları
Abonelere IP adreslerinin verilmesini düzenleme görevi geldi. Sorunun koşulları:

  • Yetkilendirme için size ayrı bir sunucu vermeyeceğiz; siz halledeceksiniz 😉
  • Abonelerin ağ ayarlarını DHCP aracılığıyla alması gerekir
  • Ağ heterojendir. Buna PON ekipmanı ve yapılandırılmış Seçenek 82'ye sahip normal anahtarlar ve erişim noktalarına sahip WiFi tabanları dahildir
  • Veriler IP verme koşullarından herhangi birine uymuyorsa, "misafir" ağdan bir IP vermelisiniz

İşin iyi tarafı: FreeBSD'de hala "çalışabilen" bir sunucu var, ancak "çok uzakta" ;), "tam bu ağda" değil.

Mikrotik adında harika bir cihaz da var. Genel ağ şeması şuna benzer:

FreeRadius'tan DHCP aracılığıyla ağ ayarları

Biraz düşündükten sonra abonelere ağ ayarlarını vermek için FreeRadius'un kullanılmasına karar verildi. Prensip olarak şema olağandır: Microtick'te DHCP sunucusunu ve üzerinde Radius Client'ı etkinleştiririz. DHCP sunucusu -> Radius Client -> Radius sunucu bağlantısını yapılandırıyoruz.

Zor görünmüyor. Ancak! Şeytan Ayrıntıda. Yani:

  • Bu şemayı kullanarak bir PON OLT'yi yetkilendirirken, FreeRadius'a, başlığın MAC adresine eşit bir Kullanıcı Adı, MAC PON Onu'na eşit bir Ajan Devre Kimliği ve boş bir parola içeren bir istek gönderilir.
  • Seçenek 82 ile anahtarlardan yetkilendirme yapılırken FreeRadius, abonenin cihazının MAC'ına eşit boş bir Kullanıcı Adına sahip olan ve yine sırasıyla MAC'ini içeren Ajan-Devre-Kimliği ve Ajan-Uzak-Kimlik ek nitelikleriyle doldurulmuş bir istek alır. röle anahtarı ve abonenin bağlı olduğu bağlantı noktası.
  • WiFI noktasına sahip bazı aboneler PAP-CHAP protokolleri aracılığıyla yetkilendirilir
  • WIFI noktalarından bazı aboneler, WIFI noktasının MAC adresine eşit bir Kullanıcı Adı ile şifresiz olarak yetkilendirilir.

Tarihsel arka plan: DHCP'de “Seçenek 82” nedir

Bunlar, DHCP protokolü için, örneğin Agent-Circuit-Id ve Agent-Remote-Id alanlarında ek bilgiler aktarmanıza olanak tanıyan ek seçeneklerdir. Genellikle röle anahtarının ve abonenin bağlı olduğu bağlantı noktasının MAC adresini iletmek için kullanılır. PON ekipmanı veya WIFI baz istasyonları durumunda, Agent-Circuit-Id alanı yararlı bilgiler içermez (abone bağlantı noktası yoktur). Bu durumda DHCP işleminin genel şeması aşağıdaki gibidir:

FreeRadius'tan DHCP aracılığıyla ağ ayarları

Adım adım bu şema şu şekilde çalışır:

  1. Kullanıcı ekipmanı ağ ayarlarını almak için DHCP yayın talebinde bulunur
  2. Abone ekipmanının doğrudan bağlı olduğu cihaz (örneğin bir anahtar, Wi-Fi veya PON baz istasyonu) bu paketi "yakalar" ve değiştirir, buna ek seçenekler Seçenek 82 ve Röle aracısı IP adresi ekler ve bunu daha da iletir. ağ.
  3. DHCP sunucusu isteği kabul eder, bir yanıt oluşturur ve bunu aktarma cihazına gönderir.
  4. Aktarma cihazı yanıt paketini abone cihazına iletir

Elbette her şey o kadar kolay olmuyor; ağ ekipmanınızı buna göre yapılandırmanız gerekiyor.

FreeRadius'u Yükleme

Elbette bu, FreeRadius yapılandırma ayarlarıyla başarılabilir, ancak bu zor ve belirsizdir... özellikle N ay sonra oraya gittiğinizde ve "her şey işe yarıyorsa." Bu nedenle FreeRadius için Python'da kendi yetkilendirme modülümüzü yazmaya karar verdik. Yetkilendirme verilerini MySQL veritabanından alacağız. Yapısını anlatmaya gerek yok, zaten herkes “kendisi için” yapacaktır. Özellikle FreeRadius için sql modülü ile sunulan yapıyı alıp, giriş-şifresine ek olarak her abone için bir mac ve port alanı ekleyerek biraz değiştirdim.

Öncelikle FreeRadius'u yükleyin:

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

Ayarlarda yüklemeyi seçin:

FreeRadius'tan DHCP aracılığıyla ağ ayarları

Python modülüne bir sembolik bağlantı yapıyoruz (yani onu “açıyoruz”):

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

Python için ek bir modül yükleyelim:

pip install mysql-connector

FreeRadius için python modül ayarlarında, python_path değişkeninde modül arama yollarını belirtmeniz gerekir. Mesela bende şu var:

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 yorumlayıcısını başlatıp komutları girerek yolları öğrenebilirsiniz:

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

Bu adımı atmazsanız python ile yazılan ve FreeRadius tarafından başlatılan scriptler, içe aktarmada listelenen modülleri bulamayacaktır. Ayrıca modül ayarlarında yetkilendirme ve muhasebe çağırma işlevlerinin açıklamasını kaldırmanız gerekir. Örneğin bu modül şuna benzer:

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 betiği (ve diğerleri) /usr/local/etc/raddb/mods-config/python dizinine yerleştirilmelidir. Toplamda üç betiğim var.

iş.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         

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

Koddan da görebileceğiniz gibi, bilinen abone MAC adresleri veya Seçenek 82 kombinasyonu ile mevcut tüm yöntemleri kullanarak aboneyi tanımlamaya çalışıyoruz ve bu işe yaramazsa, o zaman şimdiye kadar kullanılan en eski IP adresini "misafir"den veriyoruz. " ağ. Geriye kalan tek şey, sitelerin etkin olduğu klasördeki varsayılan betiği yapılandırmak, böylece python betiğindeki gerekli işlevler belirlenen anlarda seğirecektir. Aslında dosyayı şu forma getirmeniz yeterli:

varsayılan

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

Çalıştırmayı deneyelim ve hata ayıklama günlüğüne neler geldiğini görelim:

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

Başka ne. FreeRadius'u kurarken, radclient yardımcı programını kullanarak çalışmasını test etmek uygundur. Örneğin yetkilendirme:

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

Veya hesap:

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

Böyle bir şema ve senaryoları “endüstriyel” ölçekte “değişiklik yapmadan” kullanmanın kesinlikle imkansız olduğu konusunda sizi uyarmak istiyorum. En azından dikkat çekici:

  • MAC adresini "sahtelemek" mümkündür. Abonenin başka birinin MAC'ini kaydetmesi yeterlidir ve sorunlar yaşanacaktır
  • Konuk ağları yayınlamanın mantığı eleştirinin ötesindedir. “Belki de aynı IP adresine sahip istemciler zaten vardır?” diye bir onay bile yok.

Bu sadece benim koşullarımda özel olarak çalışmak üzere tasarlanmış "kural dışı bir çözüm", başka bir şey değil. Kesinlikle yargılamayın 😉

Kaynak: habr.com

Yorum ekle